1
2
3
4
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
"""
Copyright (c) 2014 Ad Schellevis
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.
--------------------------------------------------------------------------------------
package : configd
function: unix domain socket process worker process
"""
import os
import subprocess
import socket
import traceback
import syslog
import threading
import ConfigParser
import glob
import time
import uuid
import shlex
import tempfile
import ph_inline_actions
from modules import singleton
__author__ = 'Ad Schellevis'
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
-> execute ActionHandler command using Action objects
<- send back result string
"""
def __init__(self, socket_filename, config_path, config_environment=None, simulation_mode=False):
""" Constructor
:param socket_filename: filename of unix domain socket to use
:param config_path: location of configuration files
:param simulation_mode: emulation mode, do not start actual (script) commands
:return: object
"""
if config_environment is None:
config_environment = {}
self.socket_filename = socket_filename
self.config_path = config_path
self.simulation_mode = simulation_mode
self.config_environment = config_environment
self.single_threaded = False
def run(self):
""" Run process handler
:return:
"""
while True:
# noinspection PyBroadException
try:
# open action handler
act_handler = ActionHandler(config_path=self.config_path,
config_environment=self.config_environment)
# 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)
os.chmod(self.socket_filename, 0o666)
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=act_handler,
simulation_mode=self.simulation_mode)
if self.single_threaded:
# run single threaded
cmd_thread.run()
else:
# run threaded
cmd_thread.start()
except KeyboardInterrupt:
# exit on <ctrl><c>
if os.path.exists(self.socket_filename):
# cleanup, remove socket
os.remove(self.socket_filename)
raise
except SystemExit:
# stop process handler on system exit
if os.path.exists(self.socket_filename):
# cleanup on exit, remove socket
os.remove(self.socket_filename)
return
except Exception:
# something went wrong... send traceback to syslog, restart listener (wait for a short time)
print (traceback.format_exc())
syslog.syslog(syslog.LOG_ERR, 'Handler died on %s' % traceback.format_exc())
time.sleep(1)
class HandlerClient(threading.Thread):
""" Handle commands via specified socket connection
"""
def __init__(self, connection, client_address, action_handler, simulation_mode=False):
"""
:param connection: socket connection object
:param client_address: client address ( from socket accept )
:param action_handler: action handler object
:param simulation_mode: Emulation mode, do not start actual (script) commands
:return: None
"""
threading.Thread.__init__(self)
self.connection = connection
self.client_address = client_address
self.action_handler = action_handler
self.simulation_mode = simulation_mode
self.message_uuid = uuid.uuid4()
def run(self):
""" handle single action ( read data, execute command, send response )
:return: None
"""
result = ''
exec_command = ''
exec_action = ''
exec_params = ''
exec_in_background = False
# noinspection PyBroadException
try:
# receive command, maximum data length is 4k... longer messages will be truncated
data = self.connection.recv(4096)
# map command to action
data_parts = shlex.split(data)
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]
if exec_command[0] == "&":
# set run in background
exec_in_background = True
exec_command = exec_command[1:]
if len(data_parts) > 1:
exec_action = data_parts[1]
else:
exec_action = None
if len(data_parts) > 2:
exec_params = data_parts[2:]
else:
exec_params = None
# 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()
# execute requested action
if self.simulation_mode:
self.action_handler.show_action(exec_command, exec_action, exec_params, self.message_uuid)
result = 'OK'
else:
result = self.action_handler.execute(exec_command, exec_action, exec_params, self.message_uuid)
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]))
# send end of stream characters
if not exec_in_background:
self.connection.sendall("%c%c%c" % (chr(0), chr(0), chr(0)))
except SystemExit:
# ignore system exit related errors
pass
except Exception:
print (traceback.format_exc())
syslog.syslog(
syslog.LOG_ERR,
'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()
)
)
finally:
if not exec_in_background:
self.connection.close()
@singleton
class ActionHandler(object):
""" Start/stop services and functions using configuration data defined in conf/actions_<topic>.conf
"""
def __init__(self, config_path=None, config_environment=None):
""" Initialize action handler to start system functions
:param config_path: full path of configuration data
:param config_environment: environment to use (if possible)
:return:
"""
if config_path is not None:
self.config_path = config_path
if config_environment is not None:
self.config_environment = config_environment
# try to load data on initial start
if not hasattr(self, 'action_map'):
self.action_map = {}
self.load_config()
def load_config(self):
""" load action configuration from config files into local dictionary
:return: None
"""
for config_filename in glob.glob('%s/actions_*.conf' % self.config_path) \
+ glob.glob('%s/actions.d/actions_*.conf' % self.config_path):
# 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]
if topic_name not in self.action_map:
self.action_map[topic_name] = {}
# traverse config directory and open all filenames starting with actions_
cnf = ConfigParser.RawConfigParser()
cnf.read(config_filename)
for section in cnf.sections():
# map configuration data on object
action_obj = Action(config_environment=self.config_environment)
for act_prop in cnf.items(section):
setattr(action_obj, act_prop[0], act_prop[1])
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('|'):
if alias not in self.action_map[topic_name]:
self.action_map[topic_name][alias] = {}
self.action_map[topic_name][alias][section.split('.')[1]] = action_obj
else:
for alias in section.split('|'):
self.action_map[topic_name][alias] = action_obj
def list_actions(self, attributes=None):
""" list all available actions
:param attributes:
:return: dict
"""
if attributes is None:
attributes = []
result = {}
for command in self.action_map:
for action in self.action_map[command]:
if type(self.action_map[command][action]) == dict:
# parse second level actions
# TODO: nesting actions may be better to solve recursive in here and in load_config part
for subAction in self.action_map[command][action]:
cmd = '%s %s %s' % (command, action, subAction)
result[cmd] = {}
for actAttr in attributes:
if hasattr(self.action_map[command][action][subAction], actAttr):
result[cmd][actAttr] = getattr(self.action_map[command][action][subAction], actAttr)
else:
result[cmd][actAttr] = ''
else:
cmd = '%s %s' % (command, action)
result[cmd] = {}
for actAttr in attributes:
if hasattr(self.action_map[command][action], actAttr):
result[cmd][actAttr] = getattr(self.action_map[command][action], actAttr)
else:
result[cmd][actAttr] = ''
return result
def find_action(self, command, action, parameters):
""" 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
if command in self.action_map:
if action in self.action_map[command]:
if type(self.action_map[command][action]) == dict:
if parameters is not None and len(parameters) > 0 \
and parameters[0] in self.action_map[command][action]:
# 3 level action ( "interface linkup start" for example )
if isinstance(self.action_map[command][action][parameters[0]], Action):
action_obj = self.action_map[command][action][parameters[0]]
action_obj.set_parameter_start_pos(1)
elif isinstance(self.action_map[command][action], Action):
action_obj = self.action_map[command][action]
return action_obj
def execute(self, command, action, parameters, message_uuid):
""" 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
:param message_uuid: message unique id
:return: OK on success, else error code
"""
action_params = []
action_obj = self.find_action(command, action, parameters)
if action_obj is not None:
if parameters is not None and len(parameters) > action_obj.get_parameter_start_pos():
action_params = parameters[action_obj.get_parameter_start_pos():]
return '%s\n' % action_obj.execute(action_params, message_uuid)
return 'Action not found\n'
def show_action(self, command, action, parameters, message_uuid):
""" debug/simulation mode: show action information
: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
"""
action_obj = self.find_action(command, action, parameters)
print ('---------------------------------------------------------------------')
print ('execute %s.%s with parameters : %s ' % (command, action, parameters))
print ('action object %s (%s) %s' % (action_obj, action_obj.command, message_uuid))
print ('---------------------------------------------------------------------')
class Action(object):
""" Action class, handles actual (system) calls.
set command, parameters (template) type and log message
"""
def __init__(self, config_environment):
""" setup default properties
:param config_environment: environment to use
:return:
"""
self.config_environment = config_environment
self.command = None
self.parameters = None
self.type = None
self.message = None
self._parameter_start_pos = 0
def set_parameter_start_pos(self, pos):
"""
:param pos: start position of parameter list
:return: position
"""
self._parameter_start_pos = pos
def get_parameter_start_pos(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
def execute(self, parameters, message_uuid):
""" execute an action
:param parameters: list of parameters
:param message_uuid: unique message id
:return:
"""
# send-out syslog message
if self.message is not None:
log_message = '[%s] ' % message_uuid
if self.message.count('%s') > 0 and parameters is not None and len(parameters) > 0:
log_message = log_message + self.message % tuple(parameters[0:self.message.count('%s')])
else:
log_message = log_message + self.message.replace("%s", "")
syslog.syslog(syslog.LOG_NOTICE, log_message)
# validate input
if self.type is None:
# no action type, nothing to do here
return 'No action type'
elif self.type.lower() in ('script', 'script_output'):
# script type commands, basic script type only uses exit statuses, script_output sends back stdout data.
if self.command is None:
# no command supplied, exit
syslog.syslog(syslog.LOG_ERR, '[%s] returned "No command"' % message_uuid)
return 'No command'
# build script command to execute, shared for both types
script_command = self.command
if self.parameters is not None and type(self.parameters) == str and len(parameters) > 0:
script_command = '%s %s' % (script_command, self.parameters)
if script_command.find('%s') > -1:
# use command execution parameters in action parameter template
# use quotes on parameters to prevent code injection
if script_command.count('%s') > len(parameters):
# script command accepts more parameters then given, fill with empty parameters
for i in range(script_command.count('%s') - len(parameters)):
parameters.append("")
elif len(parameters) > script_command.count('%s'):
# parameters then expected, fail execution
return 'Parameter mismatch'
# force escape of shell exploitable characters for all user parameters
for escape_char in ['`', '$', '!', '(', ')', '|']:
for i in range(len(parameters[0:script_command.count('%s')])):
parameters[i] = parameters[i].replace(escape_char, '\\%s' % escape_char)
script_command = script_command % tuple(map(lambda x: '"' + x.replace('"', '\\"') + '"',
parameters[0:script_command.count('%s')]))
if self.type.lower() == 'script':
# execute script type command
try:
exit_status = subprocess.call(script_command, env=self.config_environment, shell=True)
# send response
if exit_status == 0:
return 'OK'
else:
syslog.syslog(syslog.LOG_ERR, '[%s] returned exit status %d' % (message_uuid, exit_status))
return 'Error (%d)' % exit_status
except Exception as script_exception:
syslog.syslog(syslog.LOG_ERR, '[%s] Script action failed with %s at %s' % (message_uuid,
script_exception,
traceback.format_exc()))
return 'Execute error'
elif self.type.lower() == 'script_output':
try:
with tempfile.NamedTemporaryFile() as error_stream:
with tempfile.NamedTemporaryFile() as output_stream:
subprocess.check_call(script_command, env=self.config_environment, shell=True,
stdout=output_stream, stderr=error_stream)
output_stream.seek(0)
error_stream.seek(0)
script_output = output_stream.read()
script_error_output = error_stream.read()
if len(script_error_output) > 0:
syslog.syslog(syslog.LOG_ERR,
'[%s] Script action stderr returned "%s"' %
(message_uuid, script_error_output.strip()[:255])
)
return script_output
except Exception as script_exception:
syslog.syslog(syslog.LOG_ERR, '[%s] Script action failed with %s at %s' % (message_uuid,
script_exception,
traceback.format_exc()))
return 'Execute error'
# fallback should never get here
return "type error"
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 = ''
return ph_inline_actions.execute(self, inline_act_parameters)
except Exception as inline_exception:
syslog.syslog(syslog.LOG_ERR, '[%s] Inline action failed with %s at %s' % (message_uuid,
inline_exception,
traceback.format_exc()))
return 'Execute error'
return 'Unknown action type'