processhandler.py 18.4 KB
Newer Older
Ad Schellevis's avatar
Ad Schellevis committed
1 2 3
"""
    Copyright (c) 2014 Ad Schellevis

4
    part of OPNsense (https://www.opnsense.org/)
Ad Schellevis's avatar
Ad Schellevis committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

    --------------------------------------------------------------------------------------
30
    package : configd
Ad Schellevis's avatar
Ad Schellevis committed
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
    function: unix domain socket process worker process


"""
__author__ = 'Ad Schellevis'

import os
import subprocess
import socket
import traceback
import syslog
import threading
import ConfigParser
import glob
import time
46
import uuid
47
import shlex
48
import ph_inline_actions
Ad Schellevis's avatar
Ad Schellevis committed
49

Ad Schellevis's avatar
Ad Schellevis committed
50

Ad Schellevis's avatar
Ad Schellevis committed
51 52 53 54 55 56 57 58
class Handler(object):
    """ Main handler class, opens unix domain socket and starts listening
        - New connections are handed over to a HandlerClient type object in a new thread
        - All possible actions are stored in 1 ActionHandler type object and parsed to every client for script execution

        processflow:
            Handler ( waits for client )
                -> new client is send to HandlerClient
59
                    -> execute ActionHandler command using Action objects
Ad Schellevis's avatar
Ad Schellevis committed
60 61
                    <- send back result string
    """
Ad Schellevis's avatar
Ad Schellevis committed
62
    def __init__(self, socket_filename, config_path, simulation_mode=False):
Ad Schellevis's avatar
Ad Schellevis committed
63 64 65 66
        """ Constructor

        :param socket_filename: filename of unix domain socket to use
        :param config_path: location of configuration files
Ad Schellevis's avatar
Ad Schellevis committed
67
        :param simulation_mode: emulation mode, do not start actual (script) commands
Ad Schellevis's avatar
Ad Schellevis committed
68 69 70 71
        :return: object
        """
        self.socket_filename = socket_filename
        self.config_path = config_path
72 73
        self.simulation_mode = simulation_mode
        self.single_threaded = False
Ad Schellevis's avatar
Ad Schellevis committed
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

    def run(self):
        """ Run process handler

        :return:
        """
        while True:
            try:
                # open action handler
                actHandler = ActionHandler(config_path=self.config_path)

                # remove previous socket ( if exists )
                try:
                    os.unlink(self.socket_filename)
                except OSError:
                    if os.path.exists(self.socket_filename):
                        raise

                sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                sock.bind(self.socket_filename)
Ad Schellevis's avatar
Ad Schellevis committed
94
                os.chmod(self.socket_filename, 0o666)
Ad Schellevis's avatar
Ad Schellevis committed
95 96 97 98 99 100 101 102 103
                sock.listen(30)
                while True:
                    # wait for a connection to arrive
                    connection, client_address = sock.accept()
                    # spawn a client connection
                    cmd_thread = HandlerClient(connection=connection,
                                               client_address=client_address,
                                               action_handler=actHandler,
                                               simulation_mode=self.simulation_mode)
Ad Schellevis's avatar
Ad Schellevis committed
104
                    if self.single_threaded:
105 106 107
                        # run single threaded
                        cmd_thread.run()
                    else:
108
                        # run threaded
109 110
                        cmd_thread.start()

Ad Schellevis's avatar
Ad Schellevis committed
111 112 113
            except KeyboardInterrupt:
                # exit on <ctrl><c>
                raise
114 115 116
            except SystemExit:
                # stop process handler on system exit
                return
Ad Schellevis's avatar
Ad Schellevis committed
117 118 119
            except:
                # something went wrong... send traceback to syslog, restart listener (wait for a short time)
                print (traceback.format_exc())
Ad Schellevis's avatar
Ad Schellevis committed
120
                syslog.syslog(syslog.LOG_ERR, 'Handler died on %s' % traceback.format_exc())
Ad Schellevis's avatar
Ad Schellevis committed
121 122 123 124 125 126
                time.sleep(1)


class HandlerClient(threading.Thread):
    """ Handle commands via specified socket connection
    """
Ad Schellevis's avatar
Ad Schellevis committed
127
    def __init__(self, connection, client_address, action_handler, simulation_mode=False):
Ad Schellevis's avatar
Ad Schellevis committed
128 129 130 131
        """
        :param connection: socket connection object
        :param client_address: client address ( from socket accept )
        :param action_handler: action handler object
Ad Schellevis's avatar
Ad Schellevis committed
132
        :param simulation_mode: Emulation mode, do not start actual (script) commands
Ad Schellevis's avatar
Ad Schellevis committed
133 134 135 136 137 138 139
        :return: None
        """
        threading.Thread.__init__(self)
        self.connection = connection
        self.client_address = client_address
        self.action_handler = action_handler
        self.simulation_mode = simulation_mode
140
        self.message_uuid = uuid.uuid4()
Ad Schellevis's avatar
Ad Schellevis committed
141 142 143 144 145 146

    def run(self):
        """ handle single action ( read data, execute command, send response )

        :return: None
        """
147 148
        result = ''
        exec_command = ''
Ad Schellevis's avatar
Ad Schellevis committed
149 150
        exec_action = ''
        exec_params = ''
151
        exec_in_background = False
Ad Schellevis's avatar
Ad Schellevis committed
152 153 154 155
        try:
            # receive command, maximum data length is 4k... longer messages will be truncated
            data = self.connection.recv(4096)
            # map command to action
156
            data_parts = shlex.split(data)
Ad Schellevis's avatar
Ad Schellevis committed
157 158 159 160 161
            if len(data_parts) == 0 or len(data_parts[0]) == 0:
                # no data found
                self.connection.sendall('no data\n')
            else:
                exec_command = data_parts[0]
162 163 164
                if exec_command[0] == "&":
                    # set run in background
                    exec_in_background = True
165
                    exec_command = exec_command[1:]
Ad Schellevis's avatar
Ad Schellevis committed
166 167 168 169
                if len(data_parts) > 1:
                    exec_action = data_parts[1]
                else:
                    exec_action = None
170
                if len(data_parts) > 2:
Ad Schellevis's avatar
Ad Schellevis committed
171 172 173 174
                    exec_params = data_parts[2:]
                else:
                    exec_params = None

175 176 177 178 179 180
                # when running in background, return this message uuid and detach socket
                if exec_in_background:
                    result = self.message_uuid
                    self.connection.sendall('%s\n%c%c%c' % (result, chr(0), chr(0), chr(0)))
                    self.connection.close()

Ad Schellevis's avatar
Ad Schellevis committed
181
                # execute requested action
182
                if self.simulation_mode:
183
                    self.action_handler.showAction(exec_command, exec_action, exec_params, self.message_uuid)
184
                    result = 'OK'
Ad Schellevis's avatar
Ad Schellevis committed
185
                else:
186
                    result = self.action_handler.execute(exec_command, exec_action, exec_params, self.message_uuid)
Ad Schellevis's avatar
Ad Schellevis committed
187

188 189 190 191 192 193 194 195 196
                if not exec_in_background:
                    # send response back to client( including trailing enter )
                    self.connection.sendall('%s\n' % result)
                else:
                    # log response
                    syslog.syslog(syslog.LOG_INFO, "message %s [%s.%s] returned %s " % (self.message_uuid,
                                                                                        exec_command,
                                                                                        exec_action,
                                                                                        unicode(result)[:100]))
197 198

            # send end of stream characters
199
            if not exec_in_background:
Ad Schellevis's avatar
Ad Schellevis committed
200
                self.connection.sendall("%c%c%c" % (chr(0), chr(0), chr(0)))
201 202 203
        except SystemExit:
            # ignore system exit related errors
            pass
Ad Schellevis's avatar
Ad Schellevis committed
204 205
        except:
            print (traceback.format_exc())
Ad Schellevis's avatar
Ad Schellevis committed
206
            syslog.syslog(syslog.LOG_ERR,
207 208 209 210 211 212
                          'unable to sendback response [%s] for [%s][%s][%s] {%s}, message was %s' % (result,
                                                                                                      exec_command,
                                                                                                      exec_action,
                                                                                                      exec_params,
                                                                                                      self.message_uuid,
                                                                                                      traceback.format_exc()))
Ad Schellevis's avatar
Ad Schellevis committed
213
        finally:
214 215
            if not exec_in_background:
                self.connection.close()
Ad Schellevis's avatar
Ad Schellevis committed
216

Ad Schellevis's avatar
Ad Schellevis committed
217

Ad Schellevis's avatar
Ad Schellevis committed
218
class ActionHandler(object):
Ad Schellevis's avatar
Ad Schellevis committed
219
    """ Start/stop services and functions using configuration data defined in conf/actions_<topic>.conf
Ad Schellevis's avatar
Ad Schellevis committed
220
    """
Ad Schellevis's avatar
Ad Schellevis committed
221
    def __init__(self, config_path):
Ad Schellevis's avatar
Ad Schellevis committed
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
        """ Initialize action handler to start system functions

        :param config_path: full path of configuration data
        :return:
        """
        self.config_path = config_path
        self.action_map = {}
        self.load_config()

    def load_config(self):
        """ load action configuration from config files into local dictionary

        :return: None
        """

        self.action_map = {}
Ad Schellevis's avatar
Ad Schellevis committed
238 239
        for config_filename in glob.glob('%s/actions_*.conf' % self.config_path) \
                + glob.glob('%s/actions.d/actions_*.conf' % self.config_path):
240 241 242
            # this topic's name (service, filter, template, etc)
            # make sure there's an action map index for this topic
            topic_name = config_filename.split('actions_')[-1].split('.')[0]
Ad Schellevis's avatar
Ad Schellevis committed
243
            if topic_name not in self.action_map:
244
                self.action_map[topic_name] = {}
245

Ad Schellevis's avatar
Ad Schellevis committed
246
            # traverse config directory and open all filenames starting with actions_
247
            cnf = ConfigParser.RawConfigParser()
Ad Schellevis's avatar
Ad Schellevis committed
248 249 250 251 252
            cnf.read(config_filename)
            for section in cnf.sections():
                # map configuration data on object
                action_obj = Action()
                for act_prop in cnf.items(section):
Ad Schellevis's avatar
Ad Schellevis committed
253
                    setattr(action_obj, act_prop[0], act_prop[1])
Ad Schellevis's avatar
Ad Schellevis committed
254 255 256 257

                if section.find('.') > -1:
                    # at this moment we only support 2 levels of actions ( 3 if you count topic as well )
                    for alias in section.split('.')[0].split('|'):
Ad Schellevis's avatar
Ad Schellevis committed
258
                        if alias not in self.action_map[topic_name]:
259 260
                            self.action_map[topic_name][alias] = {}
                        self.action_map[topic_name][alias][section.split('.')[1]] = action_obj
Ad Schellevis's avatar
Ad Schellevis committed
261 262
                else:
                    for alias in section.split('|'):
263
                        self.action_map[topic_name][alias] = action_obj
Ad Schellevis's avatar
Ad Schellevis committed
264

265
    def findAction(self, command, action, parameters):
Ad Schellevis's avatar
Ad Schellevis committed
266 267 268 269 270 271 272 273
        """ find action object

        :param command: command/topic for example interface
        :param action: action to run ( for example linkup )
        :param parameters: the parameters to supply
        :return: action object or None if not found
        """
        action_obj = None
Ad Schellevis's avatar
Ad Schellevis committed
274 275
        if command in self.action_map:
            if action in self.action_map[command]:
Ad Schellevis's avatar
Ad Schellevis committed
276
                if type(self.action_map[command][action]) == dict:
Ad Schellevis's avatar
Ad Schellevis committed
277
                    if len(parameters) > 0 and parameters[0] in self.action_map[command][action]:
Ad Schellevis's avatar
Ad Schellevis committed
278
                        # 3 level action (  "interface linkup start" for example )
Ad Schellevis's avatar
Ad Schellevis committed
279
                        if isinstance(self.action_map[command][action][parameters[0]], Action):
Ad Schellevis's avatar
Ad Schellevis committed
280 281
                            action_obj = self.action_map[command][action][parameters[0]]
                            action_obj.setParameterStartPos(1)
Ad Schellevis's avatar
Ad Schellevis committed
282
                elif isinstance(self.action_map[command][action], Action):
Ad Schellevis's avatar
Ad Schellevis committed
283 284 285 286
                    action_obj = self.action_map[command][action]

        return action_obj

287
    def execute(self, command, action, parameters, message_uuid):
Ad Schellevis's avatar
Ad Schellevis committed
288 289 290 291 292
        """ execute configuration defined action

        :param command: command/topic for example interface
        :param action: action to run ( for example linkup )
        :param parameters: the parameters to supply
293
        :param message_uuid: message unique id
Ad Schellevis's avatar
Ad Schellevis committed
294 295 296
        :return: OK on success, else error code
        """
        action_params = []
Ad Schellevis's avatar
Ad Schellevis committed
297
        action_obj = self.findAction(command, action, parameters)
Ad Schellevis's avatar
Ad Schellevis committed
298

299 300
        if action_obj is not None:
            if parameters is not None and len(parameters) > action_obj.getParameterStartPos():
Ad Schellevis's avatar
Ad Schellevis committed
301 302
                action_params = parameters[action_obj.getParameterStartPos():]

303
            return '%s\n' % action_obj.execute(action_params, message_uuid)
Ad Schellevis's avatar
Ad Schellevis committed
304 305 306

        return 'Action not found\n'

307
    def showAction(self, command, action, parameters, message_uuid):
Ad Schellevis's avatar
Ad Schellevis committed
308
        """ debug/simulation mode: show action information
309 310 311 312 313
        :param command: command/topic for example interface
        :param action: action to run ( for example linkup )
        :param parameters: the parameters to supply
        :param message_uuid: message unique id
        :return: None
Ad Schellevis's avatar
Ad Schellevis committed
314
        """
Ad Schellevis's avatar
Ad Schellevis committed
315
        action_obj = self.findAction(command, action, parameters)
Ad Schellevis's avatar
Ad Schellevis committed
316
        print ('---------------------------------------------------------------------')
Ad Schellevis's avatar
Ad Schellevis committed
317 318
        print ('execute %s.%s with parameters : %s ' % (command, action, parameters))
        print ('action object %s (%s)' % (action_obj, action_obj.command))
Ad Schellevis's avatar
Ad Schellevis committed
319
        print ('---------------------------------------------------------------------')
320

Ad Schellevis's avatar
Ad Schellevis committed
321

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
class Action(object):
    """ Action class,  handles actual (system) calls.
    set command, parameters (template) type and log message
    """
    def __init__(self):
        """ setup default properties

        :return:
        """
        self.command = None
        self.parameters = None
        self.type = None
        self.message = None
        self._parameter_start_pos = 0

Ad Schellevis's avatar
Ad Schellevis committed
337
    def setParameterStartPos(self, pos):
338 339 340 341 342 343 344 345 346 347 348 349 350
        """

        :param pos: start position of parameter list
        :return: position
        """
        self._parameter_start_pos = pos

    def getParameterStartPos(self):
        """ getter for _parameter_start_pos
        :return: start position of parameter list ( first argument can be part of action to start )
        """
        return self._parameter_start_pos

351
    def execute(self, parameters, message_uuid):
352 353 354
        """ execute an action

        :param parameters: list of parameters
355
        :param message_uuid: unique message id
356 357
        :return:
        """
358
        # send-out syslog message
359
        if self.message is not None:
360
            log_message = '[%s] ' % message_uuid
361
            if self.message.count('%s') > 0 and parameters is not None and len(parameters) > 0:
362
                log_message = log_message + self.message % tuple(parameters[0:self.message.count('%s')])
363
            else:
364
                log_message = log_message + self.message.replace("%s", "")
365
            syslog.syslog(syslog.LOG_NOTICE, log_message)
366

367
        # validate input
368
        if self.type is None:
369
            # no action type, nothing to do here
370
            return 'No action type'
Ad Schellevis's avatar
Ad Schellevis committed
371
        elif self.type.lower() in ('script', 'script_output'):
372
            # script type commands, basic script type only uses exit statuses, script_output sends back stdout data.
373
            if self.command is None:
374
                # no command supplied, exit
375
                syslog.syslog(syslog.LOG_ERR, '[%s] returned "No command"' % message_uuid)
376 377
                return 'No command'

378 379
            # build script command to execute, shared for both types
            script_command = self.command
380
            if self.parameters is not None and type(self.parameters) == str and len(parameters) > 0:
Ad Schellevis's avatar
Ad Schellevis committed
381
                script_command = '%s %s' % (script_command, self.parameters)
382
                if script_command.find('%s') > -1:
383 384
                    # use command execution parameters in action parameter template
                    # use quotes on parameters to prevent code injection
Ad Schellevis's avatar
Ad Schellevis committed
385
                    script_command = script_command % tuple(map(lambda x: '"'+x.replace('"', '\\"')+'"',
386 387 388 389 390 391 392
                                                                parameters[0:script_command.count('%s')]))

            if self.type.lower() == 'script':
                # execute script type command
                try:
                    exit_status = subprocess.call(script_command, shell=True)
                    # send response
393
                    if exit_status == 0:
394
                        return 'OK'
Ad Schellevis's avatar
Ad Schellevis committed
395
                    else:
396
                        syslog.syslog(syslog.LOG_ERR, '[%s] returned exit status %d' % (message_uuid, exit_status))
Ad Schellevis's avatar
Ad Schellevis committed
397
                        return 'Error (%d)' % exit_status
398
                except:
399 400
                    syslog.syslog(syslog.LOG_ERR, '[%s] Script action failed at %s' % (message_uuid,
                                                                                       traceback.format_exc()))
401 402 403 404 405 406
                    return 'Execute error'
            elif self.type.lower() == 'script_output':
                try:
                    script_output = subprocess.check_output(script_command, shell=True)
                    return script_output
                except:
407 408
                    syslog.syslog(syslog.LOG_ERR, '[%s] Script action failed at %s' % (message_uuid,
                                                                                       traceback.format_exc()))
409 410 411 412
                    return 'Execute error'

            # fallback should never get here
            return "type error"
413 414 415 416 417 418 419 420 421
        elif self.type.lower() == 'inline':
            # Handle inline service actions
            try:
                # match parameters, serialize to parameter string defined by action template
                if len(parameters) > 0:
                    inline_act_parameters = self.parameters % tuple(parameters)
                else:
                    inline_act_parameters = ''

Ad Schellevis's avatar
Ad Schellevis committed
422
                return ph_inline_actions.execute(self, inline_act_parameters)
423 424

            except:
425 426
                syslog.syslog(syslog.LOG_ERR, '[%s] Inline action failed at %s' % (message_uuid,
                                                                                   traceback.format_exc()))
427 428 429
                return 'Execute error'

        return 'Unknown action type'