source: ogAgent-Git/src/opengnsys/modules/server/OpenGnSys/__init__.py

main 5.1.1
Last change on this file was 16d24f3, checked in by Natalia Serrano <natalia.serrano@…>, 13 days ago

refs #1943 have REST object support TLS

  • Property mode set to 100644
File size: 17.1 KB
RevLine 
[53e7d45]1#!/usr/bin/env python3
[11f7a07]2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2014 Virtual Cable S.L.
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without modification,
8# are permitted provided that the following conditions are met:
9#
10#    * Redistributions of source code must retain the above copyright notice,
11#      this list of conditions and the following disclaimer.
12#    * Redistributions in binary form must reproduce the above copyright notice,
13#      this list of conditions and the following disclaimer in the documentation
14#      and/or other materials provided with the distribution.
15#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
16#      may be used to endorse or promote products derived from this software
17#      without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[ed55bec]29"""
[08ecf23]30@author: Ramón M. Gómez, ramongomez at us dot es
[ed55bec]31"""
[53e7d45]32
[683d8d4]33import base64
[11f7a07]34import os
[5d68449]35import random
[bedce23]36import shutil
[5d68449]37import string
[a850bd1]38import threading
[0440c7c]39import time
[53e7d45]40import urllib.error
41import urllib.parse
42import urllib.request
[11f7a07]43
[68c4c91]44from configparser import NoOptionError
[53e7d45]45from opengnsys import REST, operations, VERSION
[90e5c2d]46from opengnsys.log import logger
[8c6a652]47from opengnsys.jobmgr import JobMgr
[53e7d45]48from opengnsys.workers import ServerWorker
[90e5c2d]49
[03a1cb2]50
[ed55bec]51# Check authorization header decorator
52def check_secret(fnc):
53    """
54    Decorator to check for received secret key and raise exception if it isn't valid.
55    """
56    def wrapper(*args, **kwargs):
57        try:
[de4289a]58            this, path, get_params, post_params, server = args
[3a3b642]59            # Accept "status" operation with no arguments or any function with Authorization header
60            if fnc.__name__ == 'process_status' and not get_params:
61                return fnc(*args, **kwargs)
62            elif this.random == server.headers['Authorization']:
63                return fnc(*args, **kwargs)
[ed55bec]64            else:
65                raise Exception('Unauthorized operation')
66        except Exception as e:
[1fdeb2a]67            logger.debug (str(e))
[ed55bec]68            raise Exception(e)
69
70    return wrapper
71
72
[0440c7c]73# Check if operation is permitted
74def execution_level(level):
75    def check_permitted(fnc):
76        def wrapper(*args, **kwargs):
77            levels = ['status', 'halt', 'full']
78            this = args[0]
79            try:
80                if levels.index(level) <= levels.index(this.exec_level):
81                    return fnc(*args, **kwargs)
82                else:
83                    raise Exception('Unauthorized operation')
84            except Exception as e:
[1fdeb2a]85                logger.debug (str(e))
[0440c7c]86                raise Exception(e)
87
88        return wrapper
89
90    return check_permitted
91
92
[11f7a07]93class OpenGnSysWorker(ServerWorker):
[4aa86de]94    name = 'opengnsys'  # Module name
[03a1cb2]95    interface = None  # Bound interface for OpenGnsys
[ed55bec]96    REST = None  # REST object
[e298c49]97    user = []  # User sessions
[be263c6]98    session_type = ''  # User session type
[dab4e35]99    random = None  # Random string for secure connections
100    length = 32  # Random string length
[0440c7c]101    exec_level = None  # Execution level (permitted operations)
[8c6a652]102    jobmgr = JobMgr()
[90e5c2d]103
[16554e5]104    ## pings ogcore
105    def mon (self):
106        n = 0
107        while True:
108            time.sleep (1)
109            n += 1
110            if not n % 10:
111                body = {
112                    "iph": self.interface.ip,
113                    "timestamp": int (time.time()),
114                }
115                logger.debug (f'about to send ping ({body})')
116                self.REST.sendMessage ('clients/status/webhook', body)
117
[11f7a07]118    def onActivation(self):
[bedce23]119        """
[44e1e4c]120        Sends OGAgent activation notification to OpenGnsys server
[bedce23]121        """
[a5d0da2]122        if os.path.exists ('/scripts/oginit'):
123            ## estamos en oglive, este modulo no debe cargarse
124            ## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
125            raise Exception ('Refusing to load within an ogLive image')
126
[68c4c91]127        e = None  # Error info
128        t = 0  # Count of time
[feb481a]129        # Generate random secret to send on activation
130        self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
[11f7a07]131        # Ensure cfg has required configuration variables or an exception will be thrown
[68c4c91]132        try:
[16d24f3]133            url      = self.service.config.get(self.name, 'remote')
134            ca_file  = self.service.config.get(self.name, 'ca')
135            crt_file = self.service.config.get(self.name, 'crt')
136            key_file = self.service.config.get(self.name, 'key')
[68c4c91]137        except NoOptionError as e:
138            logger.error("Configuration error: {}".format(e))
139            raise e
[16d24f3]140        self.REST = REST (url, ca_file=ca_file, crt_file=crt_file, key_file=key_file)
[0440c7c]141        # Execution level ('full' by default)
142        try:
[a5d0da2]143            self.exec_level = self.service.config.get(self.name, 'level')
[68c4c91]144        except NoOptionError:
[0440c7c]145            self.exec_level = 'full'
[2e072d2]146        # Get network interfaces until they are active or timeout (5 minutes)
147        for t in range(0, 300):
[804c389]148            try:
[68c4c91]149                # Get the first network interface
150                self.interface = list(operations.getNetworkInfo())[0]
[804c389]151            except Exception as e:
152                # Wait 1 sec. and retry
[b84ab33]153                logger.warn (e)
[02399e9]154                time.sleep(1)
[804c389]155            finally:
156                # Exit loop if interface is active
157                if self.interface:
158                    if t > 0:
159                        logger.debug("Fetch connection data after {} tries".format(t))
160                    break
161        # Raise error after timeout
162        if not self.interface:
[f69d3ab]163            ## UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
[804c389]164            raise e
[6a01818]165
[feb481a]166        # Loop to send initialization message
[6a01818]167        init_retries = 100
168        for t in range(0, init_retries):
[feb481a]169            try:
170                try:
171                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
[ed55bec]172                                                              'secret': self.random, 'ostype': operations.os_type,
[a850bd1]173                                                              'osversion': operations.os_version,
174                                                              'agent_version': VERSION})
[feb481a]175                    break
[6a01818]176                except Exception as e:
177                    logger.warn (str (e))
[feb481a]178                    # Trying to initialize on alternative server, if defined
179                    # (used in "exam mode" from the University of Seville)
[16d24f3]180                    self.REST = REST(self.service.config.get(self.name, 'altremote'), ca_file=ca_file, crt_file=crt_file, key_file=key_file)
[feb481a]181                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
[ed55bec]182                                                              'secret': self.random, 'ostype': operations.os_type,
[a850bd1]183                                                              'osversion': operations.os_version, 'alt_url': True,
184                                                              'agent_version': VERSION})
[feb481a]185                    break
[6a01818]186            except Exception as e:
187                logger.warn (str (e))
[feb481a]188                time.sleep(3)
[ed55bec]189        # Raise error after timeout
[6a01818]190        if t < init_retries-1:
[ed55bec]191            logger.debug('Successful connection after {} tries'.format(t))
[6a01818]192        elif t == init_retries-1:
[feb481a]193            raise Exception('Initialization error: Cannot connect to remote server')
[6a01818]194
[6c8f1c2]195        # Delete marking files
196        for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
197            try:
198                os.remove(os.sep + f)
199            except OSError:
200                pass
[bedce23]201        # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
[03a1cb2]202        # (used in "exam mode" from the University of Seville)
[ed55bec]203        hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
204        new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
205        if os.path.isfile(new_hosts_file):
206            shutil.copyfile(new_hosts_file, hosts_file)
[90e5c2d]207
[16554e5]208        threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
209
[bf09933]210        logger.debug ('onActivation ok')
[a67669b]211
[11f7a07]212    def onDeactivation(self):
[bedce23]213        """
[44e1e4c]214        Sends OGAgent stopping notification to OpenGnsys server
[bedce23]215        """
[11f7a07]216        logger.debug('onDeactivation')
[03a1cb2]217        self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
[ed55bec]218                                                  'ostype': operations.os_type, 'osversion': operations.os_version})
[90e5c2d]219
[bedce23]220    def onLogin(self, data):
221        """
[44e1e4c]222        Sends session login notification to OpenGnsys server
[bedce23]223        """
[be263c6]224        user, language, self.session_type = tuple(data.split(','))
225        logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type))
[e298c49]226        self.user.append(user)
[03a1cb2]227        self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
[be263c6]228                                                   'session': self.session_type,
[ed55bec]229                                                   'ostype': operations.os_type, 'osversion': operations.os_version})
[5d68449]230
[11f7a07]231    def onLogout(self, user):
[bedce23]232        """
[44e1e4c]233        Sends session logout notification to OpenGnsys server
[bedce23]234        """
[11f7a07]235        logger.debug('Received logout for {}'.format(user))
[e298c49]236        try:
237            self.user.pop()
238        except IndexError:
239            pass
[44e1e4c]240        self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
[11f7a07]241
[ed55bec]242    def process_ogclient(self, path, get_params, post_params, server):
[bedce23]243        """
244        This method can be overridden to provide your own message processor, or better you can
245        implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
246        array) and this default processMessage will invoke it
[11f7a07]247        * Example:
248            Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
249            The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
[ed55bec]250            module.processMessage(["mazinger","Z"], get_params, post_params)
[90e5c2d]251
[bedce23]252            This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
253            and invoke it this way:
[ed55bec]254               return self.process_mazinger(["Z"], get_params, post_params)
[90e5c2d]255
[bedce23]256            In the case path is empty (that is, the path is composed only by the module name, like in
257            "http://example.com/Sample", the "process" method will be invoked directly
[90e5c2d]258
[bedce23]259            The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
260            basic type are)
261        """
[90e5c2d]262        if not path:
[11f7a07]263            return "ok"
264        try:
265            operation = getattr(self, 'ogclient_' + path[0])
266        except Exception:
267            raise Exception('Message processor for "{}" not found'.format(path[0]))
[ed55bec]268        return operation(path[1:], get_params, post_params)
[90e5c2d]269
[de4289a]270    # Warning: the order of the decorators matters
[0440c7c]271    @execution_level('status')
[de4289a]272    @check_secret
[ed55bec]273    def process_status(self, path, get_params, post_params, server):
[bedce23]274        """
[ed55bec]275        Returns client status (OS type or execution status) and login status
276        :param path:
[3a3b642]277        :param get_params: optional parameter "detail" to show extended status
[ed55bec]278        :param post_params:
279        :param server:
[3a3b642]280        :return: JSON object {"status": "status_code", "loggedin": boolean, ...}
[bedce23]281        """
[4aa86de]282        st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
283        try:
[3a3b642]284            # Standard status
[be263c6]285            res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0,
286                   'session': self.session_type}
[3a3b642]287            # Detailed status
288            if get_params.get('detail', 'false') == 'true':
289                res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()})
290                if res['loggedin']:
291                    res.update({'sessions': len(self.user), 'current_user': self.user[-1]})
[4aa86de]292        except KeyError:
[e298c49]293            # Unknown operating system
294            res = {'status': 'UNK'}
[11f7a07]295        return res
[90e5c2d]296
[0440c7c]297    @execution_level('halt')
[de4289a]298    @check_secret
[ed55bec]299    def process_reboot(self, path, get_params, post_params, server):
[bedce23]300        """
[ed55bec]301        Launches a system reboot operation
302        :param path:
303        :param get_params:
304        :param post_params:
305        :param server: authorization header
306        :return: JSON object {"op": "launched"}
[bedce23]307        """
[11f7a07]308        logger.debug('Received reboot operation')
[03a1cb2]309
[ed55bec]310        # Rebooting thread
[11f7a07]311        def rebt():
312            operations.reboot()
313        threading.Thread(target=rebt).start()
314        return {'op': 'launched'}
315
[0440c7c]316    @execution_level('halt')
[de4289a]317    @check_secret
[ed55bec]318    def process_poweroff(self, path, get_params, post_params, server):
[bedce23]319        """
[ed55bec]320        Launches a system power off operation
321        :param path:
322        :param get_params:
323        :param post_params:
324        :param server: authorization header
325        :return: JSON object {"op": "launched"}
[bedce23]326        """
[11f7a07]327        logger.debug('Received poweroff operation')
[03a1cb2]328
[ed55bec]329        # Powering off thread
[11f7a07]330        def pwoff():
331            time.sleep(2)
332            operations.poweroff()
333        threading.Thread(target=pwoff).start()
334        return {'op': 'launched'}
335
[0440c7c]336    @execution_level('full')
[de4289a]337    @check_secret
[ed55bec]338    def process_script(self, path, get_params, post_params, server):
[bedce23]339        """
[937c21f]340        Processes an script execution (script should be encoded in base64)
[ed55bec]341        :param path:
342        :param get_params:
343        :param post_params: JSON object {"script": "commands"}
344        :param server: authorization header
345        :return: JSON object {"op": "launched"}
[bedce23]346        """
[937c21f]347        logger.debug('Processing script request')
[f69d3ab]348        # Decoding script
[683d8d4]349        script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8'))
[8c6a652]350        logger.debug('received script "{}"'.format(script))
351
[ed55bec]352        if post_params.get('client', 'false') == 'false':
[8c6a652]353            jobid = self.jobmgr.launch_job (script, False)
354            return {'op': 'launched', 'jobid': jobid}
355
356        else:   ## post_params.get('client') is not 'false'
357            ## send script as-is
[08ecf23]358            self.sendClientMessage('script', {'code': script})
[8c6a652]359            #return {'op': 'launched', 'jobid': jobid}       ## TODO obtain jobid generated at the client (can it be done?)
360            return {'op': 'launched'}
361
362    @execution_level('full')
363    @check_secret
[d4e21da]364    def process_terminatescript(self, path, get_params, post_params, server):
365        jobid = post_params.get('jobid', None)
366        logger.debug('Processing terminate_script request, jobid "{}"'.format (jobid))
367        if jobid is None:
368            return {}
369        self.sendClientMessage('terminatescript', {'jobid': jobid})
370        self.jobmgr.terminate_job (jobid)
371        return {}
372
373    @execution_level('full')
374    @check_secret
[8c6a652]375    def process_preparescripts(self, path, get_params, post_params, server):
376        logger.debug('Processing preparescripts request')
377        self.st = self.jobmgr.prepare_jobs()
378        logger.debug('Sending preparescripts to client')
379        self.sendClientMessage('preparescripts', None)
380        return {}
381
382    def process_client_preparescripts(self, params):
383        logger.debug('Processing preparescripts message from client')
384        for p in params:
385            #logger.debug ('p "{}"'.format(p))
[d4e21da]386            self.st.append (p)
[8c6a652]387
388    @execution_level('full')
389    @check_secret
390    def process_getscripts(self, path, get_params, post_params, server):
391        logger.debug('Processing getscripts request')
392        return self.st
[90e5c2d]393
[0440c7c]394    @execution_level('full')
[de4289a]395    @check_secret
[ed55bec]396    def process_logoff(self, path, get_params, post_params, server):
[bedce23]397        """
[ed55bec]398        Closes user session
[bedce23]399        """
[11f7a07]400        logger.debug('Received logoff operation')
[ed55bec]401        # Sending log off message to OGAgent client
[11f7a07]402        self.sendClientMessage('logoff', {})
[bedce23]403        return {'op': 'sent to client'}
[11f7a07]404
[0440c7c]405    @execution_level('full')
[de4289a]406    @check_secret
[ed55bec]407    def process_popup(self, path, get_params, post_params, server):
[bedce23]408        """
[ed55bec]409        Shows a message popup on the user's session
[bedce23]410        """
[1deb0d1]411        logger.debug('Received message operation')
[ed55bec]412        # Sending popup message to OGAgent client
413        self.sendClientMessage('popup', post_params)
[1deb0d1]414        return {'op': 'launched'}
415
[90e5c2d]416    def process_client_popup(self, params):
[1deb0d1]417        self.REST.sendMessage('popup_done', params)
Note: See TracBrowser for help on using the repository browser.