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

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

#750: Adapting route {{GET /command}}} parameters.

  • Property mode set to 100644
File size: 22.0 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 signal
37import string
38import subprocess
39import threading
40import time
41import urllib
42
43from opengnsys import REST
44from opengnsys import operations
45from opengnsys.log import logger
46from opengnsys.workers import ServerWorker
47from six.moves.urllib import parse
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
56    def wrapper(*args, **kwargs):
57        try:
58            this, path, get_params, post_params, server = args  # @UnusedVariable
59            if this.random == server.headers['Authorization']:
60                fnc(*args, **kwargs)
61            else:
62                raise Exception('Unauthorized operation')
63        except Exception as e:
64            logger.error(e)
65            raise Exception(e)
66
67    return wrapper
68
69
70# Error handler decorator.
71def catch_background_error(fnc):
72    def wrapper(*args, **kwargs):
73        this = args[0]
74        try:
75            fnc(*args, **kwargs)
76        except Exception as e:
77            this.REST.sendMessage('error?id={}'.format(kwargs.get('requestId', 'error')), {'error': '{}'.format(e)})
78
79    return wrapper
80
81
82def check_locked_partition(sync=False):
83    """
84    Decorator to check if a partition is locked
85    """
86
87    def outer(fnc):
88        def wrapper(*args, **kwargs):
89            part_id = 'None'
90            try:
91                this, path, get_params, post_params, server = args  # @UnusedVariable
92                part_id = post_params['disk'] + post_params['part']
93                if this.locked.get(part_id, False):
94                    this.locked[part_id] = True
95                    fnc(*args, **kwargs)
96                else:
97                    return 'partition locked'
98            except Exception as e:
99                this.locked[part_id] = False
100                return 'error {}'.format(e)
101            finally:
102                if sync is True:
103                    this.locked[part_id] = False
104            logger.debug('Lock status: {} {}'.format(fnc, this.locked))
105
106        return wrapper
107
108    return outer
109
110
111class OpenGnSysWorker(ServerWorker):
112    name = 'opengnsys'
113    interface = None  # Bound interface for OpenGnsys
114    REST = None  # REST object
115    logged_in = False  # User session flag
116    browser = {}  # Browser info
117    commands = []  # Running commands
118    random = None  # Random string for secure connections
119    length = 32  # Random string length
120
121    def _launch_browser(self, url):
122        """
123        Launches the Browser with specified URL
124        :param url: URL to show
125        """
126        logger.debug('Launching browser with URL: {}'.format(url))
127        # Trying to kill an old browser
128        try:
129            os.kill(self.browser['process'].pid, signal.SIGKILL)
130        except OSError:
131            logger.warn('Cannot kill the old browser process')
132        except KeyError:
133            # There is no previous browser
134            pass
135        self.browser['url'] = url
136        self.browser['process'] = subprocess.Popen(['browser', '-qws', url])
137
138    def _task_command(self, route, code, op_id, send_config=False):
139        """
140        Task to execute a command and return results to a server URI
141        :param route: server callback REST route to return results
142        :param code: code to execute
143        :param op_id: operation id.
144        """
145        menu_url = ''
146        # Show execution tacking log, if OGAgent runs on ogLive
147        os_type = operations.os_type.lower()
148        if os_type == 'oglive':
149            menu_url = self.browser['url']
150            self._launch_browser('http://localhost/cgi-bin/httpd-log.sh')
151        # Execute the code
152        (stat, out, err) = operations.exec_command(code)
153        # Remove command from the list
154        for c in self.commands:
155            if c.getName() == op_id:
156                self.commands.remove(c)
157        # Remove the REST API prefix, if needed
158        if route.startswith(self.REST.endpoint):
159            route = route[len(self.REST.endpoint):]
160        # Send back exit status and outputs (base64-encoded)
161        self.REST.sendMessage(route, {'mac': self.interface.mac, 'ip': self.interface.ip, 'trace': op_id,
162                                      'status': stat, 'output': out.encode('base64'), 'error': err.encode('base64')})
163        # Show latest menu, if OGAgent runs on ogLive
164        if os_type == 'oglive':
165            # Send configuration data, if needed
166            if send_config:
167                self.REST.sendMessage('client/configs', {'mac': self.interface.mac, 'ip': self.interface.ip,
168                                                         'config': operations.get_configuration()})
169            self._launch_browser(menu_url)
170
171    def onActivation(self):
172        """
173        Sends OGAgent activation notification to OpenGnsys server
174        """
175        t = 0
176        # Generate random secret to send on activation
177        self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
178        # Ensure cfg has required configuration variables or an exception will be thrown
179        url = self.service.config.get('opengnsys', 'remote')
180        if operations.os_type == 'ogLive' and 'oglive' in os.environ:
181            # Replacing server IP if its running on ogLive clinet
182            logger.debug('Activating on ogLive client, new server is {}'.format(os.environ['oglive']))
183            url = parse.urlsplit(url)._replace(netloc=os.environ['oglive']).geturl()
184        if not url.endswith(os.path.sep):
185            url += os.path.sep
186        self.REST = REST(url)
187        # Get network interfaces until they are active or timeout (5 minutes)
188        for t in range(0, 300):
189            try:
190                self.interface = list(operations.getNetworkInfo())[0]  # Get first network interface
191            except Exception as e:
192                # Wait 1 sec. and retry
193                time.sleep(1)
194            finally:
195                # Exit loop if interface is active
196                if self.interface:
197                    if t > 0:
198                        logger.debug("Fetch connection data after {} tries".format(t))
199                    break
200        # Raise error after timeout
201        if not self.interface:
202            raise e
203        # Loop to send initialization message
204        for t in range(0, 100):
205            try:
206                try:
207                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
208                                                              'secret': self.random, 'ostype': operations.os_type,
209                                                              'osversion': operations.os_version})
210                    break
211                except:
212                    # Trying to initialize on alternative server, if defined
213                    # (used in "exam mode" from the University of Seville)
214                    self.REST = REST(self.service.config.get('opengnsys', 'altremote'))
215                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
216                                                              'secret': self.random, 'ostype': operations.os_type,
217                                                              'osversion': operations.os_version, 'alt_url': True})
218                    break
219            except:
220                time.sleep(3)
221        # Raise error after timeout
222        if 0 < t < 100:
223            logger.debug('Successful connection after {} tries'.format(t))
224        elif t == 100:
225            raise Exception('Initialization error: Cannot connect to remote server')
226        # Completing OGAgent initialization process
227        os_type = operations.os_type.lower()
228        if os_type == 'oglive':
229            # # Following code may be separated into a different function to launch browser while getting the disk configuration
230            message = """
231<html>
232<head></head>
233<style>
234  #bar { width: 20px; height: 10px; position: relative; background: darkslategrey; }
235</style>
236<body>
237<h1 style="margin: 5em 0 0 5em; font-size: 250%; color: darkslategrey;">
238  <span id="opengnsys"><span style="font-weight: lighter;">Open</span>Gnsys 3</div>
239  <div id="bar"></span>
240</h1>
241<script>
242  var elem = document.getElementById("bar");
243  var max = document.getElementById("opengnsys").offsetWidth;
244  var pos = 0;
245  var inc = true;
246  var id = setInterval(frame, 5);
247  function frame() {
248    if (inc) {
249      if (pos == max - 20) { inc = false; } else { pos++; }
250    } else {
251      if (pos == 0) { inc = true; } else { pos--; }
252    }
253    elem.style.left = pos + 'px';
254  }
255</script>
256</body>
257</html>
258"""
259            f = open('/tmp/init.html', 'w')
260            f.write(message)
261            f.close()
262            # Launch the Browser
263            self._launch_browser('/tmp/init.html')
264            config = operations.get_configuration()
265            self.REST.sendMessage('ogagent/config', {'mac': self.interface.mac, 'ip': self.interface.ip,
266                                                     'config': config})
267        else:
268            # Delete marking files
269            for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
270                try:
271                    os.remove(os.sep + f)
272                except OSError:
273                    pass
274            # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
275            # (used in "exam mode" from the University of Seville)
276            hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
277            new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
278            if os.path.isfile(new_hosts_file):
279                shutil.copyfile(new_hosts_file, hosts_file)
280
281    def onDeactivation(self):
282        """
283        Sends OGAgent stopping notification to OpenGnsys server
284        """
285        logger.debug('onDeactivation')
286        self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
287                                                  'ostype': operations.os_type, 'osversion': operations.os_version})
288
289    def processClientMessage(self, message, data):
290        logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data))
291
292    def onLogin(self, data):
293        """
294        Sends session login notification to OpenGnsys server
295        """
296        user, sep, language = data.partition(',')
297        logger.debug('Received login for {} with language {}'.format(user, language))
298        self.logged_in = True
299        self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
300                                                   'ostype': operations.os_type, 'osversion': operations.os_version})
301
302    def onLogout(self, user):
303        """
304        Sends session logout notification to OpenGnsys server
305        """
306        logger.debug('Received logout for {}'.format(user))
307        self.logged_in = False
308        self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
309
310    def process_ogclient(self, path, get_params, post_params, server):
311        """
312        This method can be overridden to provide your own message processor, or better you can
313        implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
314        array) and this default processMessage will invoke it
315        * Example:
316            Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
317            The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
318            module.processMessage(["mazinger","Z"], get_params, post_params)
319
320            This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
321            and invoke it this way:
322               return self.process_mazinger(["Z"], get_params, post_params)
323
324            In the case path is empty (that is, the path is composed only by the module name, like in
325            "http://example.com/Sample", the "process" method will be invoked directly
326
327            The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
328            basic type are)
329        """
330        if not path:
331            return "ok"
332        try:
333            operation = getattr(self, 'ogclient_' + path[0])
334        except Exception:
335            raise Exception('Message processor for "{}" not found'.format(path[0]))
336        return operation(path[1:], get_params, post_params)
337
338    def process_status(self, path, get_params, post_params, server):
339        """
340        Returns client status (OS type or execution status) and login status
341        :param path:
342        :param get_params:
343        :param post_params:
344        :param server:
345        :return: JSON object {"status": "status_code", "loggedin": boolean}
346        """
347        res = {'loggedin': self.logged_in}
348        try:
349            res['status'] = operations.os_type.lower()
350        except KeyError:
351            res['status'] = ''
352        # Check if OpenGnsys Client is busy
353        if res['status'] == 'oglive' and len(self.commands) > 0:
354            res['status'] = 'busy'
355        return res
356
357    @check_secret
358    def process_reboot(self, path, get_params, post_params, server):
359        """
360        Launches a system reboot operation
361        :param path:
362        :param get_params:
363        :param post_params:
364        :param server: authorization header
365        :return: JSON object {"op": "launched"}
366        """
367        logger.debug('Received reboot operation')
368
369        # Rebooting thread
370        def rebt():
371            operations.reboot()
372
373        threading.Thread(target=rebt).start()
374        return {'op': 'launched'}
375
376    @check_secret
377    def process_poweroff(self, path, get_params, post_params, server):
378        """
379        Launches a system power off operation
380        :param path:
381        :param get_params:
382        :param post_params:
383        :param server: authorization header
384        :return: JSON object {"op": "launched"}
385        """
386        logger.debug('Received poweroff operation')
387
388        # Powering off thread
389        def pwoff():
390            time.sleep(2)
391            operations.poweroff()
392
393        threading.Thread(target=pwoff).start()
394        return {'op': 'launched'}
395
396    @check_secret
397    def process_script(self, path, get_params, post_params, server):
398        """
399        Processes an script execution (script should be encoded in base64)
400        :param path:
401        :param get_params:
402        :param post_params: object with format:
403            id: operation id.
404            script: command code
405            redirect_url: callback REST route
406        :param server: headers data
407        :rtype: JSON object with launching status
408        """
409        logger.debug('Processing script operation with params: {}'.format(post_params))
410        # Processing data
411        try:
412            script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8')
413            op_id = post_params.get('id')
414            route = post_params.get('redirect_uri')
415            # Checking if the thread id. exists
416            for c in self.commands:
417                if c.getName() == str(op_id):
418                    raise Exception('Task id. already exists: {}'.format(op_id))
419            if post_params.get('client', 'false') == 'false':
420                # Launching a new thread
421                thr = threading.Thread(name=op_id, target=self._task_command, args=(route, script, op_id))
422                thr.start()
423                self.commands.append(thr)
424            else:
425                # Executing as normal user
426                self.sendClientMessage('script', {'code': script})
427        except Exception as e:
428            logger.error('Got exception {}'.format(e))
429            return {'error': e}
430        return {'op': 'launched'}
431
432    @check_secret
433    def process_logoff(self, path, get_params, post_params, server):
434        """
435        Closes user session
436        """
437        logger.debug('Received logoff operation')
438        # Send log off message to OGAgent client
439        self.sendClientMessage('logoff', {})
440        return {'op': 'sent to client'}
441
442    @check_secret
443    def process_popup(self, path, get_params, post_params, server):
444        """
445        Shows a message popup on the user's session
446        """
447        logger.debug('Received message operation')
448        # Send popup message to OGAgent client
449        self.sendClientMessage('popup', post_params)
450        return {'op': 'launched'}
451
452    def process_client_popup(self, params):
453        self.REST.sendMessage('popup_done', params)
454
455    @check_secret
456    def process_config(self, path, get_params, post_params, server):
457        """
458        Returns client configuration
459        :param path:
460        :param get_params:
461        :param post_params:
462        :param server:
463        :return: object
464        """
465        serial_no = ''  # Serial number
466        storage = []  # Storage configuration
467        warnings = 0  # Number of warnings
468        logger.debug('Received getconfig operation')
469        # Processing data
470        for row in operations.get_configuration().split(';'):
471            cols = row.split(':')
472            if len(cols) == 1:
473                if cols[0] != '':
474                    # Serial number
475                    serial_no = cols[0]
476                else:
477                    # Skip blank rows
478                    pass
479            elif len(cols) == 7:
480                disk, part_no, part_type, fs, op_sys, size, usage = cols
481                try:
482                    if int(part_no) == 0:
483                        # Disk information
484                        storage.append({'disk': int(disk), 'parttable': int(part_type), 'size': int(size)})
485                    else:
486                        # Partition information
487                        storage.append({'disk': int(disk), 'partition': int(part_no), 'parttype': part_type,
488                                        'filesystem': fs, 'operatingsystem': op_sys, 'size': int(size),
489                                        'usage': int(usage)})
490                except ValueError:
491                    logger.warn('Configuration parameter error: {}'.format(cols))
492                    warnings += 1
493            else:
494                # Logging warnings
495                logger.warn('Configuration data error: {}'.format(cols))
496                warnings += 1
497        # Returning configuration data and count of warnings
498        return {'serial': serial_no, 'storage': storage, 'warnings': warnings}
499
500    @check_secret
501    def process_execinfo(self, path, get_params, post_params, server):
502        """
503        Returns running commands information
504        :param path:
505        :param get_params:
506        :param post_params:
507        :param server:
508        :return: object
509        """
510        data = []
511        logger.debug('Received execinfo operation')
512        # Returning the arguments of all running threads
513        for c in self.commands:
514            if c.is_alive():
515                data.append(c.__dict__['_Thread__args'])
516        return data
517
518    @check_secret
519    def process_stopcmd(self, path, get_params, post_params, server):
520        logger.debug('Received stopcmd operation with params {}:'.format(post_params))
521        op_id = post_params.get('trace')
522        for c in self.commands:
523            if c.is_alive() and c.getName() == str(op_id):
524                c._Thread__stop()
525                return {"stopped": op_id}
526        return {}
527
528    @check_secret
529    def process_hardware(self, path, get_params, post_params, server):
530        """
531        Returns client's hardware profile
532        :param path:
533        :param get_params:
534        :param post_params:
535        :param server:
536        :return: array of component data objects
537        """
538        data = []
539        logger.debug('Received hardware operation')
540        self.checkSecret(server)
541        # Processing data
542        try:
543            for comp in operations.get_hardware():
544                data.append({'component': comp.split('=')[0], 'value': comp.split('=')[1]})
545        except:
546            pass
547        # Return list of hardware components
548        return data
549
550    @check_secret
551    def process_software(self, path, get_params, post_params, server):
552        """
553        Returns software profile installed on an operating system
554        :param path:
555        :param get_params:
556        :param post_params:
557        :param server:
558        :return:
559        """
560        logger.debug('Received software operation with params: {}'.format(post_params))
561        return operations.get_software(post_params.get('disk'), post_params.get('part'))
Note: See TracBrowser for help on using the repository browser.