#908: Fast-forward branch.

remotes/github/oglive
Ramón M. Gómez 2020-05-20 14:23:02 +02:00
commit eb2c5f8768
25 changed files with 1014 additions and 75 deletions

View File

@ -68,6 +68,4 @@ ifeq ($(DISTRO),rh)
endif
uninstall:
rm -rf $(LIBDIR)
# rm -f $(BINDIR)/ogagent
rm -rf $(CFGDIR)
rm -rf $(LIBDIR) $(CFGDIR) $(BINDIR)/ogagent $(INITDIR)/ogagent

42
oglive/Makefile 100644
View File

@ -0,0 +1,42 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Directories
SOURCEDIR := ../src
LIBDIR := $(DESTDIR)/usr/share/OGAgent
BINDIR := $(DESTDIR)/usr/bin
SBINDIR = $(DESTDIR)/usr/sbin
APPSDIR := $(DESTDIR)/usr/share/applications
CFGDIR := $(DESTDIR)/etc/ogagent
INITDIR := $(DESTDIR)/etc/init.d
PYC := $(shell find $(SOURCEDIR) -name '*.py[co]')
CACHES := $(shell find $(SOURCEDIR) -name '__pycache__')
clean:
rm -rf $(PYC) $(CACHES) $(DESTDIR)
install-ogagent:
rm -rf $(DESTDIR)
mkdir -p $(LIBDIR)
mkdir -p $(BINDIR)
mkdir -p $(SBINDIR)
mkdir -p $(APPSDIR)
mkdir -p $(CFGDIR)
mkdir $(LIBDIR)/img
# Cleans up .pyc and cache folders
rm -f $(PYC) $(CACHES)
cp -r $(SOURCEDIR)/opengnsys $(LIBDIR)/opengnsys
cp -r $(SOURCEDIR)/cfg $(LIBDIR)/cfg
ln -fs $(LIBDIR)/cfg/ogagent.cfg $(CFGDIR)
ln -fs $(LIBDIR)/cfg/ogclient.cfg $(CFGDIR)
cp scripts/ogagent $(BINDIR)
chmod 755 $(BINDIR)/ogagent
uninstall:
rm -rf $(LIBDIR)
rm -f $(BINDIR)/ogagent
rm -rf $(CFGDIR)

View File

@ -0,0 +1,7 @@
#!/bin/bash
cd $(dirname "$0")
# Build package
dpkg-buildpackage -b -d

View File

@ -0,0 +1,6 @@
ogagent-oglive (1.1.1) unstable; urgency=medium
* Initial release.
-- Ramón M. Gómez <ramongomez@us.es> Mon, 18 Jun 2018 13:00:00 +0200

View File

@ -0,0 +1 @@
9

View File

@ -0,0 +1,15 @@
Source: ogagent-oglive
Section: admin
Priority: optional
Maintainer: Ramón M. Gómez <ramongomez@us.es>
Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2
Homepage: https://opengnsys.es
Package: ogagent-oglive
Section: admin
Priority: optional
Architecture: all
Depends: python-requests (>=0.8.2), python-six(>=1.1), python-prctl(>=1.1.1), python (>=2.7), libxss1, ${misc:Depends}
Description: OpenGnsys Agent for ogLive client
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.

View File

@ -0,0 +1,26 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: ogagent
Maintainer: Ramón M. Gómez
Source: https://opengnsys.es
Copyright: 2014 Virtual Cable S.L.U.
License: BSD-3-clause
License: GPL-2+
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
.
On Debian systems, the full text of the GNU General Public
License version 2 can be found in the file
`/usr/share/common-licenses/GPL-2'.

View File

@ -0,0 +1 @@
readme.txt

View File

@ -0,0 +1,2 @@
/usr/share/OGAgent/cfg/ogagent.cfg /etc/ogagent/ogagent.cfg
/usr/share/OGAgent/cfg/ogclient.cfg /etc/ogagent/ogclient.cfg

View File

@ -0,0 +1,21 @@
#!/bin/sh
. /usr/share/debconf/confmodule
set -e
case "$1" in
configure)
chmod 600 /usr/share/OGAgent/cfg/ogagent.cfg
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

View File

@ -0,0 +1,10 @@
#!/bin/sh -e
. /usr/share/debconf/confmodule
set -e
if [ "$1" = "purge" ] ; then
rm -rf /usr/share/OGAgent || true > /dev/null 2>&1
fi

View File

@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=

View File

@ -0,0 +1,23 @@
#!/bin/sh -e
### BEGIN INIT INFO
# Provides: ogagent
# Required-Start: $local_fs $remote_fs $network $syslog $named
# Required-Stop: $local_fs $remote_fs $network $syslog $named
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: OpenGnsys Agent Service
### END INIT INFO
#
# . /lib/lsb/init-functions
case "$1" in
start|stop|restart)
/usr/bin/ogagent $1
;;
force-reload)
/usr/bin/ogagent restart
;;
*) echo "Usage: $0 {start|stop|restart|force-reload}" >&2; exit 1 ;;
esac

View File

@ -0,0 +1,44 @@
#!/usr/bin/make -f
# -*- makefile -*-
configure: configure-stamp
configure-stamp:
dh_testdir
touch configure-stamp
build: build-arch build-indep
build-arch: build-stamp
build-indep: build-stamp
build-stamp: configure-stamp
dh_testdir
$(MAKE)
touch $@
clean:
dh_testdir
dh_testroot
rm -f build-stamp configure-stamp
dh_clean
install: build
dh_testdir
dh_testroot
dh_prep
dh_installdirs
$(MAKE) DESTDIR=$(CURDIR)/debian/ogagent-oglive install-ogagent
binary-arch: build install
# emptyness
binary-indep: build install
dh_testdir
dh_testroot
dh_installchangelogs
dh_installdocs
dh_installdebconf
dh_installinit --no-start
dh_python2=python
dh_compress
dh_link
dh_fixperms
dh_installdeb
dh_shlibdeps
dh_gencontrol
dh_md5sums
dh_builddeb
binary: binary-indep
.PHONY: build clean binary-indep binary install configure

View File

@ -0,0 +1 @@
3.0 (native)

View File

@ -0,0 +1,3 @@
OGAgent is the agent intended for OpengGnsys interaction.
Please, visit https://opengnsys.es for more information

View File

@ -0,0 +1,6 @@
#!/bin/sh
FOLDER=/usr/share/OGAgent
cd $FOLDER
python -m opengnsys.linux.OGAgentService $@

View File

@ -1,9 +1,10 @@
<<<<<<< HEAD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="UDSActorService"
version="1.6.0.0"
name="OGAgentService"
version="1.1.2.0"
processorArchitecture="x86"
/>
<description>Description</description>

View File

@ -155,11 +155,7 @@ def reboot(flags=0):
import threading
threading._DummyThread._Thread__stop = lambda x: 42
# Check for OpenGnsys Client or GNU/Linux distribution.
if os.path.exists('/scripts/oginit'):
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True)
else:
subprocess.call(['/sbin/reboot'])
subprocess.call(['/sbin/reboot'])
def poweroff(flags=0):
@ -171,11 +167,7 @@ def poweroff(flags=0):
import threading
threading._DummyThread._Thread__stop = lambda x: 42
# Check for OpenGnsys Client or GNU/Linux distribution.
if os.path.exists('/scripts/oginit'):
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True)
else:
subprocess.call(['/sbin/poweroff'])
subprocess.call(['/sbin/poweroff'])
def logoff():

View File

@ -30,20 +30,21 @@
"""
from __future__ import unicode_literals
import threading
import os
import platform
import time
import random
import shutil
import signal
import string
import subprocess
import threading
import time
import urllib
from opengnsys.workers import ServerWorker
from opengnsys import REST, RESTError
from opengnsys import REST
from opengnsys import operations
from opengnsys.log import logger
from opengnsys.scriptThread import ScriptExecutorThread
from opengnsys.workers import ServerWorker
from six.moves.urllib import parse
# Check authorization header decorator
@ -51,6 +52,7 @@ def check_secret(fnc):
"""
Decorator to check for received secret key and raise exception if it isn't valid.
"""
def wrapper(*args, **kwargs):
try:
this, path, get_params, post_params, server = args # @UnusedVariable
@ -73,18 +75,100 @@ def catch_background_error(fnc):
fnc(*args, **kwargs)
except Exception as e:
this.REST.sendMessage('error?id={}'.format(kwargs.get('requestId', 'error')), {'error': '{}'.format(e)})
return wrapper
def check_locked_partition(sync=False):
"""
Decorator to check if a partition is locked
"""
def outer(fnc):
def wrapper(*args, **kwargs):
part_id = 'None'
try:
this, path, get_params, post_params, server = args # @UnusedVariable
part_id = post_params['disk'] + post_params['part']
if this.locked.get(part_id, False):
this.locked[part_id] = True
fnc(*args, **kwargs)
else:
return 'partition locked'
except Exception as e:
this.locked[part_id] = False
return 'error {}'.format(e)
finally:
if sync is True:
this.locked[part_id] = False
logger.debug('Lock status: {} {}'.format(fnc, this.locked))
return wrapper
return outer
class OpenGnSysWorker(ServerWorker):
name = 'opengnsys'
interface = None # Bound interface for OpenGnsys
REST = None # REST object
logged_in = False # User session flag
locked = {}
browser = {} # Browser info
commands = [] # Running commands
random = None # Random string for secure connections
length = 32 # Random string length
def _launch_browser(self, url):
"""
Launches the Browser with specified URL
:param url: URL to show
"""
logger.debug('Launching browser with URL: {}'.format(url))
# Trying to kill an old browser
try:
os.kill(self.browser['process'].pid, signal.SIGKILL)
except OSError:
logger.warn('Cannot kill the old browser process')
except KeyError:
# There is no previous browser
pass
self.browser['url'] = url
self.browser['process'] = subprocess.Popen(['browser', '-qws', url])
def _task_command(self, route, code, op_id, send_config=False):
"""
Task to execute a command and return results to a server URI
:param route: server callback REST route to return results
:param code: code to execute
:param op_id: operation id.
:param send_config: indicate if client will send configuration data after command execution
"""
menu_url = ''
# Show execution tacking log, if OGAgent runs on ogLive
os_type = operations.os_type.lower()
if os_type == 'oglive':
menu_url = self.browser['url']
self._launch_browser('http://localhost/cgi-bin/httpd-log.sh')
# Execute the code
(stat, out, err) = operations.exec_command(code)
# Remove command from the list
for c in self.commands:
if c.getName() == op_id:
self.commands.remove(c)
# Remove the REST API prefix, if needed
if route.startswith(self.REST.endpoint):
route = route[len(self.REST.endpoint):]
# Send back exit status and outputs (base64-encoded)
self.REST.sendMessage(route, {'mac': self.interface.mac, 'ip': self.interface.ip, 'trace': op_id,
'status': stat, 'output': out.encode('base64'), 'error': err.encode('base64')})
# Show latest menu, if OGAgent runs on ogLive
if os_type == 'oglive':
# Send configuration data, if needed
if send_config:
self.REST.sendMessage('ogagent/config', {'mac': self.interface.mac, 'ip': self.interface.ip,
'config': operations.get_configuration()})
self._launch_browser(menu_url)
def onActivation(self):
"""
Sends OGAgent activation notification to OpenGnsys server
@ -94,6 +178,12 @@ class OpenGnSysWorker(ServerWorker):
self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
# Ensure cfg has required configuration variables or an exception will be thrown
url = self.service.config.get('opengnsys', 'remote')
if operations.os_type == 'ogLive' and 'oglive' in os.environ:
# Replacing server IP if it's running on ogLive client
logger.debug('Activating on ogLive client, new server is {}'.format(os.environ['oglive']))
url = parse.urlsplit(url)._replace(netloc=os.environ['oglive']).geturl()
if not url.endswith(os.path.sep):
url += os.path.sep
self.REST = REST(url)
# Get network interfaces until they are active or timeout (5 minutes)
for t in range(0, 300):
@ -134,18 +224,61 @@ class OpenGnSysWorker(ServerWorker):
logger.debug('Successful connection after {} tries'.format(t))
elif t == 100:
raise Exception('Initialization error: Cannot connect to remote server')
# Delete marking files
for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
try:
os.remove(os.sep + f)
except OSError:
pass
# Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
# (used in "exam mode" from the University of Seville)
hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
if os.path.isfile(new_hosts_file):
shutil.copyfile(new_hosts_file, hosts_file)
# Completing OGAgent initialization process
os_type = operations.os_type.lower()
if os_type == 'oglive':
# # Following code may be separated into a different function to launch the browser while getting the disk
# # configuration
message = """
<html>
<head></head>
<style>
#bar { width: 20px; height: 10px; position: relative; background: darkslategrey; }
</style>
<body>
<h1 style="margin: 5em 0 0 5em; font-size: 250%; color: darkslategrey;">
<span id="opengnsys"><span style="font-weight: lighter;">Open</span>Gnsys 3</div>
<div id="bar"></span>
</h1>
<script>
var elem = document.getElementById("bar");
var max = document.getElementById("opengnsys").offsetWidth;
var pos = 0;
var inc = true;
var id = setInterval(frame, 5);
function frame() {
if (inc) {
if (pos == max - 20) { inc = false; } else { pos++; }
} else {
if (pos == 0) { inc = true; } else { pos--; }
}
elem.style.left = pos + 'px';
}
</script>
</body>
</html>
"""
f = open('/tmp/init.html', 'w')
f.write(message)
f.close()
# Launching the Browser
self._launch_browser('/tmp/init.html')
config = operations.get_configuration()
self.REST.sendMessage('ogagent/config', {'mac': self.interface.mac, 'ip': self.interface.ip,
'config': config})
else:
# Delete marking files
for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
try:
os.remove(os.sep + f)
except OSError:
pass
# Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
# (used in "exam mode" from the University of Seville)
hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
if os.path.isfile(new_hosts_file):
shutil.copyfile(new_hosts_file, hosts_file)
def onDeactivation(self):
"""
@ -213,23 +346,14 @@ class OpenGnSysWorker(ServerWorker):
:param server:
:return: JSON object {"status": "status_code", "loggedin": boolean}
"""
res = {'status': '', 'loggedin': self.logged_in}
if platform.system() == 'Linux': # GNU/Linux
# Check if it's OpenGnsys Client.
if os.path.exists('/scripts/oginit'):
# Check if OpenGnsys Client is busy.
if self.locked:
res['status'] = 'BSY'
else:
res['status'] = 'OPG'
else:
# Check if there is an active session.
res['status'] = 'LNX'
elif platform.system() == 'Windows': # Windows
# Check if there is an active session.
res['status'] = 'WIN'
elif platform.system() == 'Darwin': # Mac OS X ??
res['status'] = 'OSX'
res = {'loggedin': self.logged_in}
try:
res['status'] = operations.os_type.lower()
except KeyError:
res['status'] = ''
# Check if OpenGnsys Client is busy
if res['status'] == 'oglive' and len(self.commands) > 0:
res['status'] = 'busy'
return res
@check_secret
@ -247,6 +371,7 @@ class OpenGnSysWorker(ServerWorker):
# Rebooting thread
def rebt():
operations.reboot()
threading.Thread(target=rebt).start()
return {'op': 'launched'}
@ -266,6 +391,7 @@ class OpenGnSysWorker(ServerWorker):
def pwoff():
time.sleep(2)
operations.poweroff()
threading.Thread(target=pwoff).start()
return {'op': 'launched'}
@ -275,24 +401,42 @@ class OpenGnSysWorker(ServerWorker):
Processes an script execution (script should be encoded in base64)
:param path:
:param get_params:
:param post_params: JSON object {"script": "commands"}
:param server: authorization header
:return: JSON object {"op": "launched"}
:param post_params: object with format:
id: operation id.
script: command code
redirect_url: callback REST route
send_config: flag to send client's configuration after command execution (optional)
:param server: headers data
:rtype: JSON object with launching status
"""
logger.debug('Processing script request')
# Decoding script (Windows scripts need a subprocess call per line)
script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8')
if operations.os_type == 'Windows':
script = 'import subprocess; {0}'.format(
';'.join(['subprocess.check_output({0},shell=True)'.format(repr(c)) for c in script.split('\n')]))
else:
script = 'import subprocess; subprocess.check_output("""{0}""",shell=True)'.format(script)
# Executing script.
if post_params.get('client', 'false') == 'false':
thr = ScriptExecutorThread(script)
thr.start()
else:
self.sendClientMessage('script', {'code': script})
logger.debug('Processing script operation with params: {}'.format(post_params))
# Processing data
try:
# Decoding script (Windows scripts need a subprocess call per line)
script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8')
if operations.os_type == 'Windows':
script = 'import subprocess; {0}'.format(
';'.join(['subprocess.check_output({0},shell=True)'.format(repr(c)) for c in script.split('\n')]))
else:
script = 'import subprocess; subprocess.check_output("""{0}""",shell=True)'.format(script)
op_id = post_params.get('id')
route = post_params.get('redirect_uri')
send_config = (post_params.get('send_config', 'false') == 'true')
# Checking if the thread id. exists
for c in self.commands:
if c.getName() == str(op_id):
raise Exception('Task id. already exists: {}'.format(op_id))
if post_params.get('client', 'false') == 'false':
# Launching a new thread
thr = threading.Thread(name=op_id, target=self._task_command, args=(route, script, op_id, send_config))
thr.start()
self.commands.append(thr)
else:
# Executing as normal user
self.sendClientMessage('script', {'code': script})
except Exception as e:
logger.error('Got exception {}'.format(e))
return {'error': e}
return {'op': 'launched'}
@check_secret
@ -301,7 +445,7 @@ class OpenGnSysWorker(ServerWorker):
Closes user session
"""
logger.debug('Received logoff operation')
# Sending log off message to OGAgent client
# Send log off message to OGAgent client
self.sendClientMessage('logoff', {})
return {'op': 'sent to client'}
@ -311,9 +455,125 @@ class OpenGnSysWorker(ServerWorker):
Shows a message popup on the user's session
"""
logger.debug('Received message operation')
# Sending popup message to OGAgent client
# Send popup message to OGAgent client
self.sendClientMessage('popup', post_params)
return {'op': 'launched'}
def process_client_popup(self, params):
self.REST.sendMessage('popup_done', params)
@check_secret
def process_config(self, path, get_params, post_params, server):
"""
Returns client configuration
:param path:
:param get_params:
:param post_params:
:param server:
:return: object
"""
serial_no = '' # Serial number
storage = [] # Storage configuration
warnings = 0 # Number of warnings
logger.debug('Received getconfig operation')
# Processing data
for row in operations.get_configuration().split(';'):
cols = row.split(':')
if len(cols) == 1:
if cols[0] != '':
# Serial number
serial_no = cols[0]
else:
# Skip blank rows
pass
elif len(cols) == 7:
disk, part_no, part_type, fs, op_sys, size, usage = cols
try:
if int(part_no) == 0:
# Disk information
storage.append({'disk': int(disk), 'parttable': int(part_type), 'size': int(size)})
else:
# Partition information
storage.append({'disk': int(disk), 'partition': int(part_no), 'parttype': part_type,
'filesystem': fs, 'operatingsystem': op_sys, 'size': int(size),
'usage': int(usage)})
except ValueError:
logger.warn('Configuration parameter error: {}'.format(cols))
warnings += 1
else:
# Logging warnings
logger.warn('Configuration data error: {}'.format(cols))
warnings += 1
# Returning configuration data and count of warnings
return {'serial': serial_no, 'storage': storage, 'warnings': warnings}
@check_secret
def process_execinfo(self, path, get_params, post_params, server):
"""
Returns running commands information
:param path:
:param get_params:
:param post_params:
:param server:
:return: object
"""
data = []
logger.debug('Received execinfo operation')
# Returning the arguments of all running threads
for c in self.commands:
if c.is_alive():
data.append(c.__dict__['_Thread__args'])
return data
@check_secret
def process_stopcmd(self, path, get_params, post_params, server):
"""
Stops a running process identified by its trace id.
:param path:
:param get_params:
:param post_params: JSON object {"trace": trace_id}
:param server: authorization header
:return: JSON object: {"stopped": trace_id}
"""
logger.debug('Received stopcmd operation with params {}:'.format(post_params))
# Find operation id. and stop the thread
op_id = post_params.get('trace')
for c in self.commands:
if c.is_alive() and c.getName() == str(op_id):
c._Thread__stop()
return {"stopped": op_id}
return {}
@check_secret
def process_hardware(self, path, get_params, post_params, server):
"""
Returns client's hardware profile
:param path:
:param get_params:
:param post_params:
:param server:
:return: array of component data objects
"""
data = []
logger.debug('Received hardware operation')
# Processing data
try:
for comp in operations.get_hardware():
data.append({'component': comp.split('=')[0], 'value': comp.split('=')[1]})
except:
pass
# Return list of hardware components
return data
@check_secret
def process_software(self, path, get_params, post_params, server):
"""
Returns software profile installed on an operating system
:param path:
:param get_params:
:param post_params:
:param server:
:return:
"""
logger.debug('Received software operation with params: {}'.format(post_params))
return operations.get_software(post_params.get('disk'), post_params.get('part'))

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals

View File

@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: : http://www.jejik.com/authors/sander_marechal/
@see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
'''
from __future__ import unicode_literals
import sys
import os
import time
import atexit
from opengnsys.log import logger
from signal import SIGTERM
class Daemon:
"""
A generic daemon class.
Usage: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
def daemonize(self):
"""
do the UNIX double-fork magic, see Stevens' "Advanced
Programming in the UNIX Environment" for details (ISBN 0201563177)
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as e:
logger.error("fork #1 error: {}".format(e))
sys.stderr.write("fork #1 failed: {}\n".format(e))
sys.exit(1)
# decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as e:
logger.error("fork #2 error: {}".format(e))
sys.stderr.write("fork #2 failed: {}\n".format(e))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(self.stdin, 'r')
so = open(self.stdout, 'a+')
se = open(self.stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile, 'w+') as f:
f.write("{}\n".format(pid))
def delpid(self):
try:
os.remove(self.pidfile)
except Exception:
# Not found/not permissions or whatever...
pass
def start(self):
"""
Start the daemon
"""
logger.debug('Starting daemon')
# Check for a pidfile to see if the daemon already runs
try:
pf = open(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid:
message = "pidfile {} already exist. Daemon already running?\n".format(pid)
logger.error(message)
sys.stderr.write(message)
sys.exit(1)
# Start the daemon
self.daemonize()
try:
self.run()
except Exception as e:
logger.error('Exception running process: {}'.format(e))
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
def stop(self):
"""
Stop the daemon
"""
# Get the pid from the pidfile
try:
pf = open(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid is None:
message = "pidfile {} does not exist. Daemon not running?\n".format(self.pidfile)
logger.info(message)
# sys.stderr.write(message)
return # not an error in a restart
# Try killing the daemon process
try:
for i in range(10):
os.kill(pid, SIGTERM)
time.sleep(1)
except OSError as err:
if err.errno == 3: # No such process
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
sys.stderr.write(err)
sys.exit(1)
def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()
# Overridables
def run(self):
"""
You should override this method when you subclass Daemon. It will be called after the process has been
daemonized by start() or restart().
"""

View File

@ -0,0 +1,258 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Ramón M. Gómez, ramongomez at us dot es
"""
from __future__ import unicode_literals
import socket
import platform
import os
import fcntl
import subprocess
import struct
import array
import six
import chardet
from opengnsys import utils
from opengnsys.log import logger
def _getMacAddr(ifname):
"""
Returns the mac address of an interface
Mac is returned as unicode utf-8 encoded
"""
if isinstance(ifname, list):
return dict([(name, _getMacAddr(name)) for name in ifname])
if isinstance(ifname, six.text_type):
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15])))
return six.text_type(''.join(['%02x:' % char for char in info[18:24]])[:-1])
except Exception:
return None
def _getIpAddr(ifname):
"""
Returns the ip address of an interface
Ip is returned as unicode utf-8 encoded
"""
if isinstance(ifname, list):
return dict([(name, _getIpAddr(name)) for name in ifname])
if isinstance(ifname, six.text_type):
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return six.text_type(socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915, # SIOCGIFADDR
struct.pack(str('256s'), ifname[:15])
)[20:24]))
except Exception:
return None
def _getInterfaces():
"""
Returns a list of interfaces names coded in utf-8
"""
max_possible = 128 # arbitrary. raise if needed.
space = max_possible * 16
if platform.architecture()[0] == '32bit':
offset, length = 32, 32
elif platform.architecture()[0] == '64bit':
offset, length = 16, 40
else:
raise OSError('Unknown arquitecture {0}'.format(platform.architecture()[0]))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
names = array.array(str('B'), b'\0' * space)
outbytes = struct.unpack(str('iL'), fcntl.ioctl(
s.fileno(),
0x8912, # SIOCGIFCONF
struct.pack(str('iL'), space, names.buffer_info()[0])
))[0]
namestr = names.tostring()
# return namestr, outbytes
return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
def _getIpAndMac(ifname):
ip, mac = _getIpAddr(ifname), _getMacAddr(ifname)
return ip, mac
def _exec_ogcommand(ogcmd):
"""
Loads OpenGnsys environment variables, executes the command and returns the result
"""
ret = subprocess.check_output(ogcmd, shell=True)
return ret
def getComputerName():
"""
Returns computer name, with no domain
"""
return socket.gethostname().split('.')[0]
def getNetworkInfo():
"""
Obtains a list of network interfaces
:return: A "generator" of elements, that are dict-as-object, with this elements:
name: Name of the interface
mac: mac of the interface
ip: ip of the interface
"""
for ifname in _getInterfaces():
ip, mac = _getIpAndMac(ifname)
if mac != '00:00:00:00:00:00': # Skips local interfaces
yield utils.Bunch(name=ifname, mac=mac, ip=ip)
def getDomainName():
return ''
def get_oglive_version():
"""
Returns ogLive Kernel version and architecture
:return: kernel version
"""
kv = platform.os.uname()
return kv[2] + ', ' + kv[4]
def reboot():
"""
Simple reboot using OpenGnsys script
"""
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
_exec_ogcommand('/opt/opengnsys/scripts/reboot')
def poweroff():
"""
Simple power off using OpenGnsys script
"""
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
_exec_ogcommand('/opt/opengnsys/scripts/poweroff')
def get_etc_path():
"""
Returns etc directory path.
"""
return os.sep + 'etc'
def get_configuration():
"""
Returns client's configuration
Warning: this operation may take some time
:return:
"""
try:
_exec_ogcommand('/opt/opengnsys/interfaceAdm/getConfiguration')
# Returns content of configuration file
cfgdata = open('/tmp/getconfig', 'r').read().strip()
except IOError:
cfgdata = ''
return cfgdata
def exec_command(cmd):
"""
Executing a shell command
:param cmd:
:return: object with components:
output: standard output
error: error output
exit: exit code
"""
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
try:
if out is not None:
encoding = chardet.detect(out)["encoding"]
if encoding is not None:
out = out.decode(encoding).encode("utf8")
if err is not None:
encoding = chardet.detect(err)["encoding"]
if encoding is not None:
err = err.decode(encoding).encode("utf8")
except Exception as e:
logger.debug("ERROR EXEC COMMAND: {}".format(str(e)))
stat = proc.returncode
return stat, out, err
def get_hardware():
"""
Returns client's hardware list
:return:
"""
try:
filepath = _exec_ogcommand('/opt/opengnsys/scripts/listHardwareInfo').strip()
# Returns content of configuration file, skipping the header line and newline characters
with open(filepath, 'r') as f:
harddata = map(str.strip, f.readlines()[1:])
except IOError:
harddata = ''
return harddata
def get_software(disk, part):
"""
Returns software list installed on an operating system
:param disk:
:param part:
:return:
"""
try:
filepath = _exec_ogcommand('/opt/opengnsys/scripts/listSoftwareInfo {} {}'.format(disk, part)).strip()
# Returns content of configuration file, skipping the header line and newline characters
with open(filepath, 'r') as f:
softdata = map(str.strip, f.readlines())
except IOError:
softdata = ''
return softdata

View File

@ -27,12 +27,13 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
@author: Ramón M. Gómez, ramongomez at us dot es
"""
# pylint: disable=unused-wildcard-import,wildcard-import
from __future__ import unicode_literals
import sys
import os
# Importing platform operations and getting operating system data.
if sys.platform == 'win32':
@ -45,6 +46,11 @@ else:
os_type = 'MacOS'
os_version = getMacosVersion().replace(',', '')
else:
from .linux.operations import * # @UnusedWildImport
os_type = 'Linux'
os_version = getLinuxVersion()
if os.path.exists('/scripts/oginit'):
from .oglive.operations import * # @UnusedWildImport
os_type = 'ogLive'
os_version = get_oglive_version().replace(',', '')
else:
from .linux.operations import * # @UnusedWildImport
os_type = 'Linux'
os_version = getLinuxVersion()

View File

@ -139,5 +139,5 @@ setup(
description='OpenGnsys Agent',
author='Adolfo Gomez',
author_email='agomez@virtualcable.es',
zipfile='OGAgent.zip',
zipfile='OGAgent.zip', requires=['six']
)