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

main 6.1.1
Last change on this file was 22f7ce0, checked in by Natalia Serrano <natalia.serrano@…>, 8 days ago

refs #2257 rename some endpoints

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