refs #522 #527 begin integrating ogAdmClient.c into the agent

versions
Natalia Serrano 2024-07-24 15:15:21 +02:00
parent 360d0f8fb8
commit bf061b13db
6 changed files with 516 additions and 10 deletions

View File

@ -20,7 +20,14 @@ log=DEBUG
# Module specific # Module specific
# The sections must match the module name # The sections must match the module name
# This section will be passes on activation to module # This section will be passes on activation to module
#[Sample1] [ogAdmClient]
#value1=Mariete #path=test_modules/server
#value2=Yo ## this URL will probably be left equal to the other one, but let's see
#remote=https://172.27.0.1:9999/rest remote=https://192.168.1.249/opengnsys/rest/__ogAdmClient
log=DEBUG
#servidorAdm=192.168.2.1
#puerto=2008
pathinterface=/opt/opengnsys/interfaceAdm
urlMenu=https://192.168.2.1/opengnsys/varios/menubrowser.php
urlMsg=http://localhost/cgi-bin/httpd-log.sh

View File

@ -52,7 +52,7 @@ class LocalLogger(object):
logging.basicConfig( logging.basicConfig(
filename=fname, filename=fname,
filemode='a', filemode='a',
format='%(levelname)s %(asctime)s %(message)s', format='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s',
level=logging.DEBUG level=logging.DEBUG
) )
self.logger = logging.getLogger('opengnsys') self.logger = logging.getLogger('opengnsys')
@ -69,7 +69,7 @@ class LocalLogger(object):
# our loglevels are 10000 (other), 20000 (debug), .... # our loglevels are 10000 (other), 20000 (debug), ....
# logging levels are 10 (debug), 20 (info) # logging levels are 10 (debug), 20 (info)
# OTHER = logging.NOTSET # OTHER = logging.NOTSET
self.logger.log(int(level / 1000) - 10, message) self.logger.log(int(level / 1000) - 10, message, stacklevel=4)
def isWindows(self): def isWindows(self):
return False return False

View File

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# Copyright (c) 2024 Qindel Formación y Servicios 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
@author: Natalia Serrano, nserrano at qindel dot com
"""
from opengnsys.service import CommonService
from opengnsys.log import logger
from opengnsys.linux.daemon import Daemon
import sys
import signal
import json
try:
from prctl import set_proctitle # @UnresolvedImport
except ImportError:
def set_proctitle(_):
pass
class OGAgentSvc(Daemon, CommonService):
def __init__(self, args=None):
Daemon.__init__(self, '/var/run/opengnsys-agent.pid')
CommonService.__init__(self)
def run(self):
logger.debug('** Running Daemon **')
set_proctitle('ogAdmClient')
self.initialize()
# Call modules initialization
# They are called in sequence, no threading is done at this point, so ensure modules onActivate always returns
# *********************
# * Main Service loop *
# *********************
# Counter used to check ip changes only once every 10 seconds, for
# example
try:
while self.isAlive:
self.doWait(1000)
except (KeyboardInterrupt, SystemExit) as e:
logger.error('Requested exit of main loop')
except Exception as e:
logger.exception()
logger.error('Caught exception on main loop: {}'.format(e))
self.terminate()
self.notifyStop()
def signal_handler(self, signal, frame):
self.isAlive = False
sys.stderr.write("signal handler: {}".format(signal))
def usage():
sys.stderr.write("usage: {} start|stop|restart|fg\n".format(sys.argv[0]))
sys.exit(2)
if __name__ == '__main__':
logger.setLevel('DEBUG')
logger.debug('Executing actor')
daemon = OGAgentSvc()
signal.signal(signal.SIGTERM, daemon.signal_handler)
signal.signal(signal.SIGINT, daemon.signal_handler)
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
elif 'stop' == sys.argv[1]:
daemon.stop()
elif 'restart' == sys.argv[1]:
daemon.restart()
elif 'fg' == sys.argv[1]:
daemon.run()
else:
usage()
sys.exit(0)
else:
usage()

View File

@ -100,12 +100,12 @@ def loadModules(controller, client=False):
# paths += (os.path.dirname(sys.modules[modPath].__file__),) # paths += (os.path.dirname(sys.modules[modPath].__file__),)
logger.debug('Loading modules from {}'.format(paths))
# Load modules # Load modules
logger.debug('Loading modules from {}'.format(paths))
doLoad(paths) doLoad(paths)
# Add to list of available modules # Add to list of available modules
logger.debug('Adding {} classes'.format('server' if modType == ServerWorker else 'client'))
recursiveAdd(modType) recursiveAdd(modType)
return ogModules return ogModules

View File

@ -47,7 +47,7 @@ class LocalLogger(object):
logging.basicConfig( logging.basicConfig(
filename=os.path.join(tempfile.gettempdir(), 'opengnsys.log'), filename=os.path.join(tempfile.gettempdir(), 'opengnsys.log'),
filemode='a', filemode='a',
format='%(levelname)s %(asctime)s (%(threadName)s) %(message)s', format='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s',
level=logging.DEBUG level=logging.DEBUG
) )
self.logger = logging.getLogger('opengnsys') self.logger = logging.getLogger('opengnsys')
@ -58,7 +58,7 @@ class LocalLogger(object):
# our loglevels are 10000 (other), 20000 (debug), .... # our loglevels are 10000 (other), 20000 (debug), ....
# logging levels are 10 (debug), 20 (info) # logging levels are 10 (debug), 20 (info)
# OTHER = logging.NOTSET # OTHER = logging.NOTSET
self.logger.log(int(level / 1000 - 10), message) self.logger.log(int(level / 1000 - 10), message, stacklevel=4)
if level < INFO or self.serviceLogger is False: # Only information and above will be on event log if level < INFO or self.serviceLogger is False: # Only information and above will be on event log
return return

View File

@ -0,0 +1,385 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# Copyright (c) 2024 Qindel Formación y Servicios 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
@author: Natalia Serrano, nserrano at qindel dot com
"""
import base64
import os
import random
import shutil
import string
import threading
import time
import subprocess
import urllib.error
import urllib.parse
import urllib.request
from configparser import NoOptionError
from opengnsys import REST, operations, VERSION
from opengnsys.log import logger
from opengnsys.scriptThread import ScriptExecutorThread
from opengnsys.workers import ServerWorker
pathinterface = None
# Check authorization header decorator
def check_secret(fnc):
"""
Decorator to check for received secret key and raise exception if it isn't valid.
"""
def wrapper(*args, **kwargs):
return fnc(*args, **kwargs)
#try:
# this, path, get_params, post_params, server = args
# # Accept "status" operation with no arguments or any function with Authorization header
# if fnc.__name__ == 'process_status' and not get_params:
# return fnc(*args, **kwargs)
# elif this.random == server.headers['Authorization']:
# return fnc(*args, **kwargs)
# else:
# raise Exception('Unauthorized operation')
#except Exception as e:
# logger.error(str(e))
# raise Exception(e)
return wrapper
def interfaceAdmin (exe, parametros=[]):
devel_bash_prefix = '''
PATH=/opt/opengnsys/scripts/:$PATH;
for I in /opt/opengnsys/lib/engine/bin/*.lib; do source $I; done;
for i in $(declare -F |cut -f3 -d" "); do export -f $i; done;
'''
if 'nati' == os.environ['USER'] or 'nati' == os.environ['SUDO_USER']: ## DO NOT COMMIT
devel_bash_prefix = '''
PATH=/home/nati/Downloads/work/opengnsys/opengnsys/client/shared/scripts:$PATH;
for I in /home/nati/Downloads/work/opengnsys/opengnsys/client/engine/*.lib; do source $I; done;
for i in $(declare -F |cut -f3 -d" "); do export -f $i; done;
'''
if parametros:
proc = ['bash', '-c', '{} {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))]
else:
proc = ['bash', '-c', '{} {}'.format (devel_bash_prefix, exe)]
logger.debug ('subprocess.run ("{}", capture_output=True)'.format (proc))
return subprocess.run (proc, capture_output=True).stdout.strip().decode ('utf-8')
def tomaIPlocal():
logger.debug (__name__)
interface = '{}/getIpAddress'.format (pathinterface)
try:
IPlocal = interfaceAdmin (interface);
logger.info (IPlocal)
except Exception as e:
logger.error (e)
logger.error ('No se ha podido recuperar la dirección IP del cliente')
return False
return True
class ogAdmClientWorker(ServerWorker):
name = 'ogAdmClient' # Module name
interface = None # Bound interface for OpenGnsys
REST = None # REST object
#user = [] # User sessions
#session_type = '' # User session type
random = None # Random string for secure connections
length = 32 # Random string length
def onActivation(self):
"""
Sends OGAgent activation notification to OpenGnsys server
"""
t = 0 # Count of time
# Generate random secret to send on activation
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
try:
global pathinterface
url = self.service.config.get('ogAdmClient', 'remote')
loglevel = self.service.config.get('ogAdmClient', 'log')
#servidorAdm = self.service.config.get('ogAdmClient', 'servidorAdm')
#puerto = self.service.config.get('ogAdmClient', 'puerto')
pathinterface = self.service.config.get('ogAdmClient', 'pathinterface')
urlMenu = self.service.config.get('ogAdmClient', 'urlMenu')
urlMsg = self.service.config.get('ogAdmClient', 'urlMsg')
logger.setLevel(loglevel)
except NoOptionError as e:
logger.error("Configuration error: {}".format(e))
raise e
self.REST = REST(url)
# Get network interfaces until they are active or timeout (5 minutes)
for t in range(0, 300):
try:
# Get the first network interface
self.interface = list(operations.getNetworkInfo())[0]
except Exception as e:
# Wait 1 sec. and retry
logger.warn (e)
time.sleep(1)
finally:
# Exit loop if interface is active
if self.interface:
if t > 0:
logger.debug("Fetch connection data after {} tries".format(t))
break
# Raise error after timeout
if not self.interface:
raise Exception ('not self.interface')
# Loop to send initialization message
init_retries = 100
for t in range(0, init_retries):
try:
self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
'secret': self.random, 'ostype': operations.os_type,
'osversion': operations.os_version,
'agent_version': VERSION})
break
except Exception as e:
logger.warn (str (e))
time.sleep(3)
# Raise error after timeout
if t < init_retries-1:
logger.debug('Successful connection after {} tries'.format(t))
elif t == init_retries-1:
raise Exception('Initialization error: Cannot connect to remote server')
tomaIPlocal()
def onDeactivation(self):
"""
Sends OGAgent stopping notification to OpenGnsys server
"""
logger.debug('onDeactivation')
self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
'ostype': operations.os_type, 'osversion': operations.os_version})
def processClientMessage(self, message, data):
logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data))
def onLogin(self, data):
logger.warn('in onLogin, should not happen')
def onLogout(self, user):
logger.warn('in onLogout, should not happen')
def process_ogclient(self, path, get_params, post_params, server):
"""
This method can be overridden to provide your own message processor, or better you can
implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
array) and this default processMessage will invoke it
* Example:
Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
module.processMessage(["mazinger","Z"], get_params, post_params)
This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
and invoke it this way:
return self.process_mazinger(["Z"], get_params, post_params)
In the case path is empty (that is, the path is composed only by the module name, like in
"http://example.com/Sample", the "process" method will be invoked directly
The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
basic type are)
"""
if not path:
return "ok"
try:
operation = getattr(self, 'ogclient_' + path[0])
except Exception:
raise Exception('Message processor for "{}" not found'.format(path[0]))
return operation(path[1:], get_params, post_params)
@check_secret
def process_status(self, path, get_params, post_params, server):
return {'ogAdmClient': 'in process_status'}
@check_secret
def process_reboot(self, path, get_params, post_params, server):
"""
Launches a system reboot operation
:param path:
:param get_params:
:param post_params:
:param server: authorization header
:return: JSON object {"op": "launched"}
"""
logger.debug('Received reboot operation')
# Rebooting thread
def rebt():
operations.reboot()
threading.Thread(target=rebt).start()
return {'op': 'launched'}
@check_secret
def process_poweroff(self, path, get_params, post_params, server):
"""
Launches a system power off operation
:param path:
:param get_params:
:param post_params:
:param server: authorization header
:return: JSON object {"op": "launched"}
"""
logger.debug('Received poweroff operation')
# Powering off thread
def pwoff():
time.sleep(2)
operations.poweroff()
threading.Thread(target=pwoff).start()
return {'op': 'launched'}
@check_secret
def process_script(self, path, get_params, post_params, server):
"""
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"}
"""
logger.debug('Processing script request')
# Decoding script
script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8'))
logger.debug('received script {}'.format(script))
if operations.os_type == 'Windows':
## for windows, we turn the script into utf16le, then to b64 again, and feed the blob to powershell
u16 = script.encode ('utf-16le') ## utf16
b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8))
script = """
import os
import tempfile
import subprocess
cp = subprocess.run ("powershell -WindowStyle Hidden -EncodedCommand {}", capture_output=True)
subprocs_log = os.path.join (tempfile.gettempdir(), 'opengnsys-subprocs.log')
with open (subprocs_log, 'ab') as fd: ## TODO improve this logging
fd.write (cp.stdout)
fd.write (cp.stderr)
""".format (b64)
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})
return {'op': 'launched'}
@check_secret
def process_logoff(self, path, get_params, post_params, server):
logger.warn('in process_logoff, should not happen')
@check_secret
def process_popup(self, path, get_params, post_params, server):
logger.warn('in process_popup, should not happen')
#def process_client_popup(self, params):
# logger.warn('in process_client_popup')
## process_* are invoked from opengnsys/httpserver.py:99 "data = module.processServerMessage(path, get_params, post_params, self)" (via opengnsys/workers/server_worker.py)
## process_client_* are invoked from opengnsys/service.py:123 "v.processClientMessage(message, json.loads(data))" (via opengnsys/workers/server_worker.py)
def process_RESPUESTA_AutoexecCliente(self, path, get_params, post_params, server):
logger.warn('in process_RESPUESTA_AutoexecCliente')
def process_RESPUESTA_InclusionCliente(self, path, get_params, post_params, server):
logger.warn('in process_RESPUESTA_InclusionCliente')
def process_NoComandosPtes(self, path, get_params, post_params, server):
logger.warn('in process_NoComandosPtes')
def process_Actualizar(self, path, get_params, post_params, server):
logger.warn('in process_Actualizar')
def process_Purgar(self, path, get_params, post_params, server):
logger.warn('in process_Purgar')
def process_ConsolaRemota(self, path, get_params, post_params, server):
logger.warn('in process_ConsolaRemota')
def process_Sondeo(self, path, get_params, post_params, server):
logger.warn('in process_Sondeo')
def process_Arrancar(self, path, get_params, post_params, server):
logger.warn('in process_Arrancar')
def process_Apagar(self, path, get_params, post_params, server):
logger.warn('in process_Apagar')
def process_Reiniciar(self, path, get_params, post_params, server):
logger.warn('in process_Reiniciar')
def process_IniciarSesion(self, path, get_params, post_params, server):
logger.warn('in process_IniciarSesion')
def process_CrearImagen(self, path, get_params, post_params, server):
logger.warn('in process_CrearImagen')
def process_CrearImagenBasica(self, path, get_params, post_params, server):
logger.warn('in process_CrearImagenBasica')
def process_CrearSoftIncremental(self, path, get_params, post_params, server):
logger.warn('in process_CrearSoftIncremental')
def process_RestaurarImagen(self, path, get_params, post_params, server):
logger.warn('in process_RestaurarImagen')
def process_RestaurarImagenBasica(self, path, get_params, post_params, server):
logger.warn('in process_RestaurarImagenBasica')
def process_RestaurarSoftIncremental(self, path, get_params, post_params, server):
logger.warn('in process_RestaurarSoftIncremental')
def process_Configurar(self, path, get_params, post_params, server):
logger.warn('in process_Configurar')
def process_EjecutarScript(self, path, get_params, post_params, server):
logger.warn('in process_EjecutarScript')
def process_InventarioHardware(self, path, get_params, post_params, server):
logger.warn('in process_InventarioHardware')
def process_InventarioSoftware(self, path, get_params, post_params, server):
logger.warn('in process_InventarioSoftware')
def process_EjecutaComandosPendientes(self, path, get_params, post_params, server):
logger.warn('in process_EjecutaComandosPendientes')