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

decorare-oglive-methodsexec-ogbrowserfix-urlfixes-winlgromero-filebeatlog-sess-lenmainmodulesnew-browserno-ptt-paramno-tlsogadmcliogadmclient-statusogagent-jobsogagent-macosogcore1oggitoglogoglog2override-moduleping1ping2ping3ping4py3-winpython3report-progresssched-tasktlsunification2unification3versionswindows-fixes
Last change on this file since 0440c7c was 0440c7c, checked in by Ramón M. Gómez <ramongomez@…>, 5 years ago

#968: Define execution levels in OGAgent.

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