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

decorare-oglive-methodsfix-urlmainoglogoglog2ping3ping4sched-tasktls 3.1.0
Last change on this file since 0e113ec was 1fdeb2a, checked in by Natalia Serrano <natalia.serrano@…>, 7 months ago

refs #986 use logger.debug

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