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

browser-nuevodecorare-oglive-methodserror-no-netexec-ogbrowserfix-urlfixes-winlgromero-filebeatlog-sess-lenmainmodulesnew-browserno-ptt-paramno-tlsogadmcliogadmclient-statusogagent-jobsogagent-macosogagentuser-sigtermogcore1oggitoggit-changelogoggit-debian-changelogoggit-notlsogliveoglogoglog2override-moduleping1ping2ping3ping4py3-winpython3qndtestreport-progresssched-tasktlstls-againunification2unification3versionswindows-fixes
Last change on this file since e3878fd was e3878fd, checked in by Ramón M. Gómez <ramongomez@…>, 5 years ago

#962: OGAgent runs each script line independently to solve a Python for Windows problem.

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