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

decorare-oglive-methodsmaintls 3.3.0
Last change on this file since ff042ca was 16554e5, checked in by Natalia Serrano <natalia.serrano@…>, 3 weeks ago

refs #1853 ping ogcore from the OS as well

  • Property mode set to 100644
File size: 16.8 KB
Line 
1#!/usr/bin/env python3
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.
29"""
30@author: Ramón M. Gómez, ramongomez at us dot es
31"""
32
33import base64
34import os
35import random
36import shutil
37import string
38import threading
39import time
40import urllib.error
41import urllib.parse
42import urllib.request
43
44from configparser import NoOptionError
45from opengnsys import REST, operations, VERSION
46from opengnsys.log import logger
47from opengnsys.jobmgr import JobMgr
48from opengnsys.workers import ServerWorker
49
50
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:
58            this, path, get_params, post_params, server = args
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)
64            else:
65                raise Exception('Unauthorized operation')
66        except Exception as e:
67            logger.debug (str(e))
68            raise Exception(e)
69
70    return wrapper
71
72
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:
85                logger.debug (str(e))
86                raise Exception(e)
87
88        return wrapper
89
90    return check_permitted
91
92
93class OpenGnSysWorker(ServerWorker):
94    name = 'opengnsys'  # Module name
95    interface = None  # Bound interface for OpenGnsys
96    REST = None  # REST object
97    user = []  # User sessions
98    session_type = ''  # User session type
99    random = None  # Random string for secure connections
100    length = 32  # Random string length
101    exec_level = None  # Execution level (permitted operations)
102    jobmgr = JobMgr()
103
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
118    def onActivation(self):
119        """
120        Sends OGAgent activation notification to OpenGnsys server
121        """
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
127        e = None  # Error info
128        t = 0  # Count of time
129        # Generate random secret to send on activation
130        self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
131        # Ensure cfg has required configuration variables or an exception will be thrown
132        try:
133            url = self.service.config.get(self.name, 'remote')
134        except NoOptionError as e:
135            logger.error("Configuration error: {}".format(e))
136            raise e
137        self.REST = REST(url)
138        # Execution level ('full' by default)
139        try:
140            self.exec_level = self.service.config.get(self.name, 'level')
141        except NoOptionError:
142            self.exec_level = 'full'
143        # Get network interfaces until they are active or timeout (5 minutes)
144        for t in range(0, 300):
145            try:
146                # Get the first network interface
147                self.interface = list(operations.getNetworkInfo())[0]
148            except Exception as e:
149                # Wait 1 sec. and retry
150                logger.warn (e)
151                time.sleep(1)
152            finally:
153                # Exit loop if interface is active
154                if self.interface:
155                    if t > 0:
156                        logger.debug("Fetch connection data after {} tries".format(t))
157                    break
158        # Raise error after timeout
159        if not self.interface:
160            ## UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
161            raise e
162
163        # Loop to send initialization message
164        init_retries = 100
165        for t in range(0, init_retries):
166            try:
167                try:
168                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
169                                                              'secret': self.random, 'ostype': operations.os_type,
170                                                              'osversion': operations.os_version,
171                                                              'agent_version': VERSION})
172                    break
173                except Exception as e:
174                    logger.warn (str (e))
175                    # Trying to initialize on alternative server, if defined
176                    # (used in "exam mode" from the University of Seville)
177                    self.REST = REST(self.service.config.get(self.name, 'altremote'))
178                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
179                                                              'secret': self.random, 'ostype': operations.os_type,
180                                                              'osversion': operations.os_version, 'alt_url': True,
181                                                              'agent_version': VERSION})
182                    break
183            except Exception as e:
184                logger.warn (str (e))
185                time.sleep(3)
186        # Raise error after timeout
187        if t < init_retries-1:
188            logger.debug('Successful connection after {} tries'.format(t))
189        elif t == init_retries-1:
190            raise Exception('Initialization error: Cannot connect to remote server')
191
192        # Delete marking files
193        for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
194            try:
195                os.remove(os.sep + f)
196            except OSError:
197                pass
198        # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
199        # (used in "exam mode" from the University of Seville)
200        hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
201        new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
202        if os.path.isfile(new_hosts_file):
203            shutil.copyfile(new_hosts_file, hosts_file)
204
205        threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
206
207        logger.debug ('onActivation ok')
208
209    def onDeactivation(self):
210        """
211        Sends OGAgent stopping notification to OpenGnsys server
212        """
213        logger.debug('onDeactivation')
214        self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
215                                                  'ostype': operations.os_type, 'osversion': operations.os_version})
216
217    def onLogin(self, data):
218        """
219        Sends session login notification to OpenGnsys server
220        """
221        user, language, self.session_type = tuple(data.split(','))
222        logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type))
223        self.user.append(user)
224        self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
225                                                   'session': self.session_type,
226                                                   'ostype': operations.os_type, 'osversion': operations.os_version})
227
228    def onLogout(self, user):
229        """
230        Sends session logout notification to OpenGnsys server
231        """
232        logger.debug('Received logout for {}'.format(user))
233        try:
234            self.user.pop()
235        except IndexError:
236            pass
237        self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
238
239    def process_ogclient(self, path, get_params, post_params, server):
240        """
241        This method can be overridden to provide your own message processor, or better you can
242        implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
243        array) and this default processMessage will invoke it
244        * Example:
245            Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
246            The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
247            module.processMessage(["mazinger","Z"], get_params, post_params)
248
249            This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
250            and invoke it this way:
251               return self.process_mazinger(["Z"], get_params, post_params)
252
253            In the case path is empty (that is, the path is composed only by the module name, like in
254            "http://example.com/Sample", the "process" method will be invoked directly
255
256            The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
257            basic type are)
258        """
259        if not path:
260            return "ok"
261        try:
262            operation = getattr(self, 'ogclient_' + path[0])
263        except Exception:
264            raise Exception('Message processor for "{}" not found'.format(path[0]))
265        return operation(path[1:], get_params, post_params)
266
267    # Warning: the order of the decorators matters
268    @execution_level('status')
269    @check_secret
270    def process_status(self, path, get_params, post_params, server):
271        """
272        Returns client status (OS type or execution status) and login status
273        :param path:
274        :param get_params: optional parameter "detail" to show extended status
275        :param post_params:
276        :param server:
277        :return: JSON object {"status": "status_code", "loggedin": boolean, ...}
278        """
279        st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
280        try:
281            # Standard status
282            res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0,
283                   'session': self.session_type}
284            # Detailed status
285            if get_params.get('detail', 'false') == 'true':
286                res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()})
287                if res['loggedin']:
288                    res.update({'sessions': len(self.user), 'current_user': self.user[-1]})
289        except KeyError:
290            # Unknown operating system
291            res = {'status': 'UNK'}
292        return res
293
294    @execution_level('halt')
295    @check_secret
296    def process_reboot(self, path, get_params, post_params, server):
297        """
298        Launches a system reboot operation
299        :param path:
300        :param get_params:
301        :param post_params:
302        :param server: authorization header
303        :return: JSON object {"op": "launched"}
304        """
305        logger.debug('Received reboot operation')
306
307        # Rebooting thread
308        def rebt():
309            operations.reboot()
310        threading.Thread(target=rebt).start()
311        return {'op': 'launched'}
312
313    @execution_level('halt')
314    @check_secret
315    def process_poweroff(self, path, get_params, post_params, server):
316        """
317        Launches a system power off operation
318        :param path:
319        :param get_params:
320        :param post_params:
321        :param server: authorization header
322        :return: JSON object {"op": "launched"}
323        """
324        logger.debug('Received poweroff operation')
325
326        # Powering off thread
327        def pwoff():
328            time.sleep(2)
329            operations.poweroff()
330        threading.Thread(target=pwoff).start()
331        return {'op': 'launched'}
332
333    @execution_level('full')
334    @check_secret
335    def process_script(self, path, get_params, post_params, server):
336        """
337        Processes an script execution (script should be encoded in base64)
338        :param path:
339        :param get_params:
340        :param post_params: JSON object {"script": "commands"}
341        :param server: authorization header
342        :return: JSON object {"op": "launched"}
343        """
344        logger.debug('Processing script request')
345        # Decoding script
346        script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8'))
347        logger.debug('received script "{}"'.format(script))
348
349        if post_params.get('client', 'false') == 'false':
350            jobid = self.jobmgr.launch_job (script, False)
351            return {'op': 'launched', 'jobid': jobid}
352
353        else:   ## post_params.get('client') is not 'false'
354            ## send script as-is
355            self.sendClientMessage('script', {'code': script})
356            #return {'op': 'launched', 'jobid': jobid}       ## TODO obtain jobid generated at the client (can it be done?)
357            return {'op': 'launched'}
358
359    @execution_level('full')
360    @check_secret
361    def process_terminatescript(self, path, get_params, post_params, server):
362        jobid = post_params.get('jobid', None)
363        logger.debug('Processing terminate_script request, jobid "{}"'.format (jobid))
364        if jobid is None:
365            return {}
366        self.sendClientMessage('terminatescript', {'jobid': jobid})
367        self.jobmgr.terminate_job (jobid)
368        return {}
369
370    @execution_level('full')
371    @check_secret
372    def process_preparescripts(self, path, get_params, post_params, server):
373        logger.debug('Processing preparescripts request')
374        self.st = self.jobmgr.prepare_jobs()
375        logger.debug('Sending preparescripts to client')
376        self.sendClientMessage('preparescripts', None)
377        return {}
378
379    def process_client_preparescripts(self, params):
380        logger.debug('Processing preparescripts message from client')
381        for p in params:
382            #logger.debug ('p "{}"'.format(p))
383            self.st.append (p)
384
385    @execution_level('full')
386    @check_secret
387    def process_getscripts(self, path, get_params, post_params, server):
388        logger.debug('Processing getscripts request')
389        return self.st
390
391    @execution_level('full')
392    @check_secret
393    def process_logoff(self, path, get_params, post_params, server):
394        """
395        Closes user session
396        """
397        logger.debug('Received logoff operation')
398        # Sending log off message to OGAgent client
399        self.sendClientMessage('logoff', {})
400        return {'op': 'sent to client'}
401
402    @execution_level('full')
403    @check_secret
404    def process_popup(self, path, get_params, post_params, server):
405        """
406        Shows a message popup on the user's session
407        """
408        logger.debug('Received message operation')
409        # Sending popup message to OGAgent client
410        self.sendClientMessage('popup', post_params)
411        return {'op': 'launched'}
412
413    def process_client_popup(self, params):
414        self.REST.sendMessage('popup_done', params)
Note: See TracBrowser for help on using the repository browser.