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

oglive
Last change on this file since bb1ff4d was bb1ff4d, checked in by Ramón M. Gómez <ramongomez@…>, 5 years ago

#750: OGAgent for ogLive looks for oglive environ variable; route GET /getconfig returns data in JSON format.

  • Property mode set to 100644
File size: 15.1 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 2014 Virtual Cable S.L.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without modification,
7# are permitted provided that the following conditions are met:
8#
9#    * Redistributions of source code must retain the above copyright notice,
10#      this list of conditions and the following disclaimer.
11#    * Redistributions in binary form must reproduce the above copyright notice,
12#      this list of conditions and the following disclaimer in the documentation
13#      and/or other materials provided with the distribution.
14#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
15#      may be used to endorse or promote products derived from this software
16#      without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28"""
29@author: Ramón M. Gómez, ramongomez at us dot es
30"""
31from __future__ import unicode_literals
32
33import os
34import random
35import shutil
36import string
37import threading
38import time
39import urllib
40
41from opengnsys import REST
42from opengnsys import operations
43from opengnsys.log import logger
44from opengnsys.scriptThread import ScriptExecutorThread
45from opengnsys.workers import ServerWorker
46from six.moves.urllib import parse
47
48
49
50# Check authorization header decorator
51def check_secret(fnc):
52    """
53    Decorator to check for received secret key and raise exception if it isn't valid.
54    """
55    def wrapper(*args, **kwargs):
56        try:
57            this, path, get_params, post_params, server = args  # @UnusedVariable
58            if this.random == server.headers['Authorization']:
59                fnc(*args, **kwargs)
60            else:
61                raise Exception('Unauthorized operation')
62        except Exception as e:
63            logger.error(e)
64            raise Exception(e)
65
66    return wrapper
67
68
69# Error handler decorator.
70def catch_background_error(fnc):
71    def wrapper(*args, **kwargs):
72        this = args[0]
73        try:
74            fnc(*args, **kwargs)
75        except Exception as e:
76            this.REST.sendMessage('error?id={}'.format(kwargs.get('requestId', 'error')), {'error': '{}'.format(e)})
77    return wrapper
78
79
80class OpenGnSysWorker(ServerWorker):
81    name = 'opengnsys'
82    interface = None  # Bound interface for OpenGnsys
83    REST = None  # REST object
84    logged_in = False  # User session flag
85    locked = {}
86    random = None     # Random string for secure connections
87    length = 32       # Random string length
88
89    def onActivation(self):
90        """
91        Sends OGAgent activation notification to OpenGnsys server
92        """
93        t = 0
94        # Generate random secret to send on activation
95        self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
96        # Ensure cfg has required configuration variables or an exception will be thrown
97        url = self.service.config.get('opengnsys', 'remote')
98        if operations.os_type == 'ogLive' and 'oglive' in os.environ:
99            # Replacing server IP if its running on ogLive clinet
100            logger.debug('Activating on ogLive client, new server is {}'.format(os.environ['oglive']))
101            url = parse.urlsplit(url)._replace(netloc=os.environ['oglive']).geturl()
102        self.REST = REST(url)
103        # Get network interfaces until they are active or timeout (5 minutes)
104        for t in range(0, 300):
105            try:
106                self.interface = list(operations.getNetworkInfo())[0]  # Get first network interface
107            except Exception as e:
108                # Wait 1 sec. and retry
109                time.sleep(1)
110            finally:
111                # Exit loop if interface is active
112                if self.interface:
113                    if t > 0:
114                        logger.debug("Fetch connection data after {} tries".format(t))
115                    break
116        # Raise error after timeout
117        if not self.interface:
118            raise e
119        # Loop to send initialization message
120        for t in range(0, 100):
121            try:
122                try:
123                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
124                                                              'secret': self.random, 'ostype': operations.os_type,
125                                                              'osversion': operations.os_version})
126                    break
127                except:
128                    # Trying to initialize on alternative server, if defined
129                    # (used in "exam mode" from the University of Seville)
130                    self.REST = REST(self.service.config.get('opengnsys', 'altremote'))
131                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
132                                                              'secret': self.random, 'ostype': operations.os_type,
133                                                              'osversion': operations.os_version, 'alt_url': True})
134                    break
135            except:
136                time.sleep(3)
137        # Raise error after timeout
138        if 0 < t < 100:
139            logger.debug('Successful connection after {} tries'.format(t))
140        elif t == 100:
141            raise Exception('Initialization error: Cannot connect to remote server')
142        # Delete marking files
143        for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
144            try:
145                os.remove(os.sep + f)
146            except OSError:
147                pass
148        # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
149        # (used in "exam mode" from the University of Seville)
150        hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
151        new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
152        if os.path.isfile(new_hosts_file):
153            shutil.copyfile(new_hosts_file, hosts_file)
154
155    def onDeactivation(self):
156        """
157        Sends OGAgent stopping notification to OpenGnsys server
158        """
159        logger.debug('onDeactivation')
160        self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
161                                                  'ostype': operations.os_type, 'osversion': operations.os_version})
162
163    def processClientMessage(self, message, data):
164        logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data))
165
166    def onLogin(self, data):
167        """
168        Sends session login notification to OpenGnsys server
169        """
170        user, sep, language = data.partition(',')
171        logger.debug('Received login for {} with language {}'.format(user, language))
172        self.logged_in = True
173        self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
174                                                   'ostype': operations.os_type, 'osversion': operations.os_version})
175
176    def onLogout(self, user):
177        """
178        Sends session logout notification to OpenGnsys server
179        """
180        logger.debug('Received logout for {}'.format(user))
181        self.logged_in = False
182        self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
183
184    def process_ogclient(self, path, get_params, post_params, server):
185        """
186        This method can be overridden to provide your own message processor, or better you can
187        implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
188        array) and this default processMessage will invoke it
189        * Example:
190            Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
191            The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
192            module.processMessage(["mazinger","Z"], get_params, post_params)
193
194            This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
195            and invoke it this way:
196               return self.process_mazinger(["Z"], get_params, post_params)
197
198            In the case path is empty (that is, the path is composed only by the module name, like in
199            "http://example.com/Sample", the "process" method will be invoked directly
200
201            The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
202            basic type are)
203        """
204        if not path:
205            return "ok"
206        try:
207            operation = getattr(self, 'ogclient_' + path[0])
208        except Exception:
209            raise Exception('Message processor for "{}" not found'.format(path[0]))
210        return operation(path[1:], get_params, post_params)
211
212    def process_status(self, path, get_params, post_params, server):
213        """
214        Returns client status (OS type or execution status) and login status
215        :param path:
216        :param get_params:
217        :param post_params:
218        :param server:
219        :return: JSON object {"status": "status_code", "loggedin": boolean}
220        """
221        res = {'status': '', 'loggedin': self.logged_in}
222        if platform.system() == 'Linux':        # GNU/Linux
223            # Check if it's OpenGnsys Client.
224            if os.path.exists('/scripts/oginit'):
225                # Check if OpenGnsys Client is busy.
226                if self.locked:
227                    res['status'] = 'BSY'
228                else:
229                    res['status'] = 'OPG'
230            else:
231                # Check if there is an active session.
232                res['status'] = 'LNX'
233        elif platform.system() == 'Windows':    # Windows
234            # Check if there is an active session.
235            res['status'] = 'WIN'
236        elif platform.system() == 'Darwin':     # Mac OS X  ??
237            res['status'] = 'OSX'
238        return res
239
240    @check_secret
241    def process_reboot(self, path, get_params, post_params, server):
242        """
243        Launches a system reboot operation
244        :param path:
245        :param get_params:
246        :param post_params:
247        :param server: authorization header
248        :return: JSON object {"op": "launched"}
249        """
250        logger.debug('Received reboot operation')
251
252        # Rebooting thread
253        def rebt():
254            operations.reboot()
255        threading.Thread(target=rebt).start()
256        return {'op': 'launched'}
257
258    @check_secret
259    def process_poweroff(self, path, get_params, post_params, server):
260        """
261        Launches a system power off operation
262        :param path:
263        :param get_params:
264        :param post_params:
265        :param server: authorization header
266        :return: JSON object {"op": "launched"}
267        """
268        logger.debug('Received poweroff operation')
269
270        # Powering off thread
271        def pwoff():
272            time.sleep(2)
273            operations.poweroff()
274        threading.Thread(target=pwoff).start()
275        return {'op': 'launched'}
276
277    @check_secret
278    def process_script(self, path, get_params, post_params, server):
279        """
280        Processes an script execution (script should be encoded in base64)
281        :param path:
282        :param get_params:
283        :param post_params: JSON object {"script": "commands"}
284        :param server: authorization header
285        :return: JSON object {"op": "launched"}
286        """
287        logger.debug('Processing script request')
288        # Decoding script
289        script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8')
290        script = 'import subprocess; subprocess.check_output("""{}""",shell=True)'.format(script)
291        # Executing script.
292        if post_params.get('client', 'false') == 'false':
293            thr = ScriptExecutorThread(script)
294            thr.start()
295        else:
296            self.sendClientMessage('script', {'code': script})
297        return {'op': 'launched'}
298
299    @check_secret
300    def process_logoff(self, path, get_params, post_params, server):
301        """
302        Closes user session
303        """
304        logger.debug('Received logoff operation')
305        # Sending log off message to OGAgent client
306        self.sendClientMessage('logoff', {})
307        return {'op': 'sent to client'}
308
309    @check_secret
310    def process_popup(self, path, get_params, post_params, server):
311        """
312        Shows a message popup on the user's session
313        """
314        logger.debug('Received message operation')
315        # Sending popup message to OGAgent client
316        self.sendClientMessage('popup', post_params)
317        return {'op': 'launched'}
318
319    def process_client_popup(self, params):
320        self.REST.sendMessage('popup_done', params)
321
322    def process_getconfig(self, path, get_params, post_params, server):
323        """
324        Returns client configuration
325        :param path:
326        :param get_params:
327        :param post_params:
328        :param server:
329        :return: object
330        """
331        serialno = ''   # Serial number
332        storage = []    # Storage configuration
333        warnings = 0    # Number of warnings
334        logger.debug('Recieved getconfig operation')
335        self.checkSecret(server)
336        # Processing data
337        for row in operations.get_disk_config().strip().split(';'):
338            cols = row.split(':')
339            if len(cols) == 1:
340                if cols[0] != '':
341                    # Serial number
342                    serialno = cols[0]
343                else:
344                    # Skip blank rows
345                    pass
346            elif len(cols) == 7:
347                disk, npart, tpart, fs, os, size, usage = cols
348                try:
349                    if int(npart) == 0:
350                        # Disk information
351                        storage.append({'disk': int(disk), 'parttable': int(tpart), 'size': int(size)})
352                    else:
353                        # Partition information
354                        storage.append({'disk': int(disk), 'partition': int(npart), 'parttype': tpart,
355                                        'filesystem': fs, 'operatingsystem': os, 'size': int(size),
356                                        'usage': int(usage)})
357                except ValueError:
358                    logger.warn('Configuration parameter error: {}'.format(cols))
359                    warnings += 1
360            else:
361                # Logging warnings
362                logger.warn('Configuration data error: {}'.format(cols))
363                warnings += 1
364        # Returning configuration data and count of warnings
365        return {'serialno': serialno, 'storage': storage, 'warnings': warnings}
Note: See TracBrowser for help on using the repository browser.