1 # vim: set encoding=utf-8 et sw=4 sts=4 :
4 from mako.template import Template
5 from mako.runtime import Context
8 import cPickle as pickle
12 from pymin.dispatcher import Handler, handler, HandlerError, \
18 __ALL__ = ('ServiceHandler', 'InitdHandler', 'SubHandler', 'DictSubHandler',
19 'ListSubHandler', 'Persistent', 'ConfigWriter', 'Error',
20 'ReturnNot0Error', 'ExecutionError', 'ItemError',
21 'ItemAlreadyExistsError', 'ItemNotFoundError', 'call')
23 class Error(HandlerError):
25 Error(message) -> Error instance :: Base ServiceHandler exception class.
27 All exceptions raised by the ServiceHandler inherits from this one, so
28 you can easily catch any ServiceHandler exception.
30 message - A descriptive error message.
34 class ReturnNot0Error(Error):
36 ReturnNot0Error(return_value) -> ReturnNot0Error instance.
38 A command didn't returned the expected 0 return value.
40 return_value - Return value returned by the command.
43 def __init__(self, return_value):
44 r"Initialize the object. See class documentation for more info."
45 self.return_value = return_value
47 def __unicode__(self):
48 return 'The command returned %d' % self.return_value
50 class ExecutionError(Error):
52 ExecutionError(command, error) -> ExecutionError instance.
54 Error executing a command.
56 command - Command that was tried to execute.
58 error - Error received when trying to execute the command.
61 def __init__(self, command, error):
62 r"Initialize the object. See class documentation for more info."
63 self.command = command
66 def __unicode__(self):
67 command = self.command
68 if not isinstance(self.command, basestring):
69 command = ' '.join(command)
70 return "Can't execute command %s: %s" % (command, self.error)
72 class ParameterError(Error, KeyError):
74 ParameterError(paramname) -> ParameterError instance
76 This is the base exception for all DhcpHandler parameters related errors.
79 def __init__(self, paramname):
80 r"Initialize the object. See class documentation for more info."
81 self.message = 'Parameter error: "%s"' % paramname
83 class ParameterNotFoundError(ParameterError):
85 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
87 This exception is raised when trying to operate on a parameter that doesn't
91 def __init__(self, paramname):
92 r"Initialize the object. See class documentation for more info."
93 self.message = 'Parameter not found: "%s"' % paramname
95 class ItemError(Error, KeyError):
97 ItemError(key) -> ItemError instance.
99 This is the base exception for all item related errors.
102 def __init__(self, key):
103 r"Initialize the object. See class documentation for more info."
104 self.message = u'Item error: "%s"' % key
106 class ItemAlreadyExistsError(ItemError):
108 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
110 This exception is raised when trying to add an item that already exists.
113 def __init__(self, key):
114 r"Initialize the object. See class documentation for more info."
115 self.message = u'Item already exists: "%s"' % key
117 class ItemNotFoundError(ItemError):
119 ItemNotFoundError(key) -> ItemNotFoundError instance
121 This exception is raised when trying to operate on an item that doesn't
125 def __init__(self, key):
126 r"Initialize the object. See class documentation for more info."
127 self.message = u'Item not found: "%s"' % key
130 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
131 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
134 if not isinstance(command, basestring):
135 command = ' '.join(command)
136 print 'Executing command:', command
139 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
140 universal_newlines=universal_newlines,
141 close_fds=close_fds, **kw)
143 raise ExecutionError(command, e)
145 raise ExecutionError(command, ReturnNot0Error(r))
148 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
150 This is a helper class to inherit from to automatically handle data
151 persistence using pickle.
153 The variables attributes to persist (attrs), and the pickle directory (dir)
154 and file extension (ext) can be defined by calling the constructor or in a
155 more declarative way as class attributes, like:
157 class TestHandler(Persistent):
158 _persistent_attrs = ('some_attr', 'other_attr')
159 _persistent_dir = 'persistent-data'
160 _persistent_ext = '.pickle'
162 The default dir is '.' and the default extension is '.pkl'. There are no
163 default variables, and they should be specified as string if a single
164 attribute should be persistent or as a tuple of strings if they are more.
165 The strings should be the attribute names to be persisted. For each
166 attribute a separated pickle file is generated in the pickle directory.
168 You can call _dump() and _load() to write and read the data respectively.
170 # TODO implement it using metaclasses to add the handlers method by demand
171 # (only for specifieds commands).
173 _persistent_attrs = ()
174 _persistent_dir = '.'
175 _persistent_ext = '.pkl'
177 def __init__(self, attrs=None, dir=None, ext=None):
178 r"Initialize the object, see the class documentation for details."
179 if attrs is not None:
180 self._persistent_attrs = attrs
182 self._persistent_dir = dir
184 self._persistent_ext = ext
187 r"_dump() -> None :: Dump all persistent data to pickle files."
188 if isinstance(self._persistent_attrs, basestring):
189 self._persistent_attrs = (self._persistent_attrs,)
190 for attrname in self._persistent_attrs:
191 self._dump_attr(attrname)
194 r"_load() -> None :: Load all persistent data from pickle files."
195 if isinstance(self._persistent_attrs, basestring):
196 self._persistent_attrs = (self._persistent_attrs,)
197 for attrname in self._persistent_attrs:
198 self._load_attr(attrname)
200 def _dump_attr(self, attrname):
201 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
202 f = file(self._pickle_filename(attrname), 'wb')
203 pickle.dump(getattr(self, attrname), f, 2)
206 def _load_attr(self, attrname):
207 r"_load_attr() -> object :: Load a specific pickle file."
208 f = file(self._pickle_filename(attrname))
209 setattr(self, attrname, pickle.load(f))
212 def _pickle_filename(self, name):
213 r"_pickle_filename() -> string :: Construct a pickle filename."
214 return path.join(self._persistent_dir, name) + self._persistent_ext
216 class Restorable(Persistent):
217 r"""Restorable([defaults]) -> Restorable.
219 This is a helper class to inherit from that provides a nice _restore()
220 method to restore the persistent data if any, or load some nice defaults
223 The defaults can be defined by calling the constructor or in a more
224 declarative way as class attributes, like:
226 class TestHandler(Restorable):
227 _persistent_attrs = ('some_attr', 'other_attr')
228 _restorable_defaults = dict(
229 some_attr = 'some_default',
230 other_attr = 'other_default')
232 The defaults is a dictionary, very coupled with the _persistent_attrs
233 attribute inherited from Persistent. The defaults keys should be the
234 values from _persistent_attrs, and the values the default values.
236 The _restore() method returns True if the data was restored successfully
237 or False if the defaults were loaded (in case you want to take further
238 actions). If a _write_config method if found, it's executed when a restore
241 # TODO implement it using metaclasses to add the handlers method by demand
242 # (only for specifieds commands).
244 _restorable_defaults = dict()
246 def __init__(self, defaults=None):
247 r"Initialize the object, see the class documentation for details."
248 if defaults is not None:
249 self._restorable_defaults = defaults
252 r"_restore() -> bool :: Restore persistent data or create a default."
255 # TODO tener en cuenta servicios que hay que levantar y los que no
256 if hasattr(self, 'commit'): # TODO deberia ser reload y/o algo para comandos
260 for (k, v) in self._restorable_defaults.items():
262 # TODO tener en cuenta servicios que hay que levantar y los que no
263 if hasattr(self, 'commit'):
267 if hasattr(self, '_write_config'):
269 if hasattr(self, 'reload'):
274 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
276 This is a helper class to inherit from to automatically handle
277 configuration generation. Mako template system is used for configuration
280 The configuration filenames, the generated configuration files directory
281 and the templates directory can be defined by calling the constructor or
282 in a more declarative way as class attributes, like:
284 class TestHandler(ConfigWriter):
285 _config_writer_files = ('base.conf', 'custom.conf')
286 _config_writer_cfg_dir = {
287 'base.conf': '/etc/service',
288 'custom.conf': '/etc/service/conf.d',
290 _config_writer_tpl_dir = 'templates'
292 The generated configuration files directory defaults to '.' and the
293 templates directory to 'templates'. _config_writer_files has no default and
294 must be specified in either way. It can be string or a tuple if more than
295 one configuration file must be generated. _config_writer_cfg_dir could be a
296 dict mapping which file should be stored in which directory, or a single
297 string if all the config files should go to the same directory.
299 The template filename and the generated configuration filename are both the
300 same (so if you want to generate some /etc/config, you should have some
301 templates/config template). That's why _config_writer_cfg_dir and
302 _config_writer_tpl_dir can't be the same. This is not true for very
303 specific cases where _write_single_config() is used.
305 When you write your Handler, you should call _config_build_templates() in
306 you Handler constructor to build the templates.
308 To write the configuration files, you must use the _write_config() method.
309 To know what variables to replace in the template, you have to provide a
310 method called _get_config_vars(tamplate_name), which should return a
311 dictionary of variables to pass to the template system to be replaced in
312 the template for the configuration file 'config_file'.
314 # TODO implement it using metaclasses to add the handlers method by demand
315 # (only for specifieds commands).
317 _config_writer_files = ()
318 _config_writer_cfg_dir = '.'
319 _config_writer_tpl_dir = 'templates'
321 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
322 r"Initialize the object, see the class documentation for details."
323 if files is not None:
324 self._config_writer_files = files
325 if cfg_dir is not None:
326 self._config_writer_cfg_dir = cfg_dir
327 if tpl_dir is not None:
328 self._config_writer_tpl_dir = tpl_dir
329 self._config_build_templates()
331 def _config_build_templates(self):
332 r"_config_writer_templates() -> None :: Build the template objects."
333 if isinstance(self._config_writer_files, basestring):
334 self._config_writer_files = (self._config_writer_files,)
335 if not hasattr(self, '_config_writer_templates') \
336 or not self._config_writer_templates:
337 self._config_writer_templates = dict()
338 for t in self._config_writer_files:
339 f = path.join(self._config_writer_tpl_dir, t)
340 self._config_writer_templates[t] = Template(filename=f)
342 def _render_config(self, template_name, vars=None):
343 r"""_render_config(template_name[, config_filename[, vars]]).
345 Render a single config file using the template 'template_name'. If
346 vars is specified, it's used as the dictionary with the variables
347 to replace in the templates, if not, it looks for a
348 _get_config_vars() method to get it.
351 if hasattr(self, '_get_config_vars'):
352 vars = self._get_config_vars(template_name)
356 vars = vars(template_name)
357 return self._config_writer_templates[template_name].render(**vars)
359 def _get_config_path(self, template_name, config_filename=None):
360 r"Get a complete configuration path."
361 if not config_filename:
362 config_filename = template_name
363 if isinstance(self._config_writer_cfg_dir, basestring):
364 return path.join(self._config_writer_cfg_dir, config_filename)
365 return path.join(self._config_writer_cfg_dir[template_name],
368 def _write_single_config(self, template_name, config_filename=None, vars=None):
369 r"""_write_single_config(template_name[, config_filename[, vars]]).
371 Write a single config file using the template 'template_name'. If no
372 config_filename is specified, the config filename will be the same as
373 the 'template_name' (but stored in the generated config files
374 directory). If it's specified, the generated config file is stored in
375 the file called 'config_filename' (also in the generated files
376 directory). If vars is specified, it's used as the dictionary with the
377 variables to replace in the templates, if not, it looks for a
378 _get_config_vars() method to get it.
381 if hasattr(self, '_get_config_vars'):
382 vars = self._get_config_vars(template_name)
386 vars = vars(template_name)
387 f = file(self._get_config_path(template_name, config_filename), 'w')
388 ctx = Context(f, **vars)
389 self._config_writer_templates[template_name].render_context(ctx)
392 def _write_config(self):
393 r"_write_config() -> None :: Generate all the configuration files."
394 for t in self._config_writer_files:
395 self._write_single_config(t)
398 class ServiceHandler(Handler):
399 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
401 This is a helper class to inherit from to automatically handle services
402 with start, stop, restart, reload actions.
404 The actions can be defined by calling the constructor with all the
405 parameters or in a more declarative way as class attributes, like:
407 class TestHandler(ServiceHandler):
408 _service_start = ('command', 'start')
409 _service_stop = ('command', 'stop')
410 _service_restart = ('command', 'restart')
411 _service_reload = 'reload-command'
413 Commands are executed without using the shell, that's why they are specified
414 as tuples (where the first element is the command and the others are the
415 command arguments). If only a command is needed (without arguments) a single
416 string can be specified.
418 All commands must be specified.
420 # TODO implement it using metaclasses to add the handlers method by demand
421 # (only for specifieds commands).
423 def __init__(self, start=None, stop=None, restart=None, reload=None):
424 r"Initialize the object, see the class documentation for details."
425 for (name, action) in dict(start=start, stop=stop, restart=restart,
426 reload=reload).items():
427 if action is not None:
428 setattr(self, '_service_%s' % name, action)
430 @handler(u'Start the service.')
432 r"start() -> None :: Start the service."
433 call(self._service_start)
435 @handler(u'Stop the service.')
437 r"stop() -> None :: Stop the service."
438 call(self._service_stop)
440 @handler(u'Restart the service.')
442 r"restart() -> None :: Restart the service."
443 call(self._service_restart)
445 @handler(u'Reload the service config (without restarting, if possible).')
447 r"reload() -> None :: Reload the configuration of the service."
448 call(self._service_reload)
450 class InitdHandler(Handler):
451 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
453 This is a helper class to inherit from to automatically handle services
454 with start, stop, restart, reload actions using a /etc/init.d like script.
456 The name and directory of the script can be defined by calling the
457 constructor or in a more declarative way as class attributes, like:
459 class TestHandler(ServiceHandler):
460 _initd_name = 'some-service'
461 _initd_dir = '/usr/local/etc/init.d'
463 The default _initd_dir is '/etc/init.d', _initd_name has no default and
464 must be specified in either way.
466 Commands are executed without using the shell.
468 # TODO implement it using metaclasses to add the handlers method by demand
469 # (only for specifieds commands).
471 _initd_dir = '/etc/init.d'
473 def __init__(self, initd_name=None, initd_dir=None):
474 r"Initialize the object, see the class documentation for details."
475 if initd_name is not None:
476 self._initd_name = initd_name
477 if initd_dir is not None:
478 self._initd_dir = initd_dir
480 @handler(u'Start the service.')
482 r"start() -> None :: Start the service."
483 call((path.join(self._initd_dir, self._initd_name), 'start'))
485 @handler(u'Stop the service.')
487 r"stop() -> None :: Stop the service."
488 call((path.join(self._initd_dir, self._initd_name), 'stop'))
490 @handler(u'Restart the service.')
492 r"restart() -> None :: Restart the service."
493 call((path.join(self._initd_dir, self._initd_name), 'restart'))
495 @handler(u'Reload the service config (without restarting, if possible).')
497 r"reload() -> None :: Reload the configuration of the service."
498 call((path.join(self._initd_dir, self._initd_name), 'reload'))
500 class TransactionalHandler(Handler):
501 r"""Handle command transactions providing a commit and rollback commands.
503 This is a helper class to inherit from to automatically handle
504 transactional handlers, which have commit and rollback commands.
506 The handler should provide a reload() method (see ServiceHandler and
507 InitdHandler for helper classes to provide this) which will be called
508 when a commit command is issued (if a reload() command is present).
509 The persistent data will be written too (if a _dump() method is provided,
510 see Persistent and Restorable for that), and the configuration files
511 will be generated (if a _write_config method is present, see ConfigWriter).
513 # TODO implement it using metaclasses to add the handlers method by demand
514 # (only for specifieds commands).
516 @handler(u'Commit the changes (reloading the service, if necessary).')
518 r"commit() -> None :: Commit the changes and reload the service."
519 if hasattr(self, '_dump'):
521 if hasattr(self, '_write_config'):
523 if hasattr(self, 'reload'):
526 @handler(u'Discard all the uncommited changes.')
528 r"rollback() -> None :: Discard the changes not yet commited."
529 if hasattr(self, '_load'):
532 class ParametersHandler(Handler):
533 r"""ParametersHandler([attr]) -> ParametersHandler.
535 This is a helper class to inherit from to automatically handle
536 service parameters, providing set, get, list and show commands.
538 The attribute that holds the parameters can be defined by calling the
539 constructor or in a more declarative way as class attributes, like:
541 class TestHandler(ServiceHandler):
542 _parameters_attr = 'some_attr'
544 The default is 'params' and it should be a dictionary.
546 # TODO implement it using metaclasses to add the handlers method by demand
547 # (only for specifieds commands).
549 _parameters_attr = 'params'
551 def __init__(self, attr=None):
552 r"Initialize the object, see the class documentation for details."
554 self._parameters_attr = attr
556 @handler(u'Set a service parameter.')
557 def set(self, param, value):
558 r"set(param, value) -> None :: Set a service parameter."
559 if not param in self.params:
560 raise ParameterNotFoundError(param)
561 self.params[param] = value
563 @handler(u'Get a service parameter.')
564 def get(self, param):
565 r"get(param) -> None :: Get a service parameter."
566 if not param in self.params:
567 raise ParameterNotFoundError(param)
568 return self.params[param]
570 @handler(u'List all available service parameters.')
572 r"list() -> tuple :: List all the parameter names."
573 return self.params.keys()
575 @handler(u'Get all service parameters, with their values.')
577 r"show() -> (key, value) tuples :: List all the parameters."
578 return self.params.items()
580 class SubHandler(Handler):
581 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
583 This is a helper class to build sub handlers that needs to reference the
586 parent - Parent Handler object.
589 def __init__(self, parent):
590 r"Initialize the object, see the class documentation for details."
593 class ListSubHandler(SubHandler):
594 r"""ListSubHandler(parent) -> ListSubHandler instance.
596 This is a helper class to inherit from to automatically handle subcommands
597 that operates over a list parent attribute.
599 The list attribute to handle and the class of objects that it contains can
600 be defined by calling the constructor or in a more declarative way as
601 class attributes, like:
603 class TestHandler(ListSubHandler):
604 _list_subhandler_attr = 'some_list'
605 _list_subhandler_class = SomeClass
607 This way, the parent's some_list attribute (self.parent.some_list) will be
608 managed automatically, providing the commands: add, update, delete, get,
609 list and show. New items will be instances of SomeClass, which should
610 provide a cmp operator to see if the item is on the list and an update()
611 method, if it should be possible to modify it.
614 def __init__(self, parent, attr=None, cls=None):
615 r"Initialize the object, see the class documentation for details."
618 self._list_subhandler_attr = attr
620 self._list_subhandler_class = cls
623 return getattr(self.parent, self._list_subhandler_attr)
625 @handler(u'Add a new item')
626 def add(self, *args, **kwargs):
627 r"add(...) -> None :: Add an item to the list."
628 item = self._list_subhandler_class(*args, **kwargs)
629 if item in self._list():
630 raise ItemAlreadyExistsError(item)
631 self._list().append(item)
633 @handler(u'Update an item')
634 def update(self, index, *args, **kwargs):
635 r"update(index, ...) -> None :: Update an item of the list."
636 # TODO make it right with metaclasses, so the method is not created
637 # unless the update() method really exists.
638 # TODO check if the modified item is the same of an existing one
639 index = int(index) # TODO validation
640 if not hasattr(self._list_subhandler_class, 'update'):
641 raise CommandNotFoundError(('update',))
643 self._list()[index].update(*args, **kwargs)
645 raise ItemNotFoundError(index)
647 @handler(u'Delete an item')
648 def delete(self, index):
649 r"delete(index) -> None :: Delete an item of the list."
650 index = int(index) # TODO validation
652 return self._list().pop(index)
654 raise ItemNotFoundError(index)
656 @handler(u'Get information about an item')
657 def get(self, index):
658 r"get(index) -> Host :: List all the information of an item."
659 index = int(index) # TODO validation
661 return self._list()[index]
663 raise ItemNotFoundError(index)
665 @handler(u'Get how many items are in the list')
667 r"len() -> int :: Get how many items are in the list."
668 return len(self._list())
670 @handler(u'Get information about all items')
672 r"show() -> list of Hosts :: List all the complete items information."
675 class DictSubHandler(SubHandler):
676 r"""DictSubHandler(parent) -> DictSubHandler instance.
678 This is a helper class to inherit from to automatically handle subcommands
679 that operates over a dict parent attribute.
681 The dict attribute to handle and the class of objects that it contains can
682 be defined by calling the constructor or in a more declarative way as
683 class attributes, like:
685 class TestHandler(DictSubHandler):
686 _dict_subhandler_attr = 'some_dict'
687 _dict_subhandler_class = SomeClass
689 This way, the parent's some_dict attribute (self.parent.some_dict) will be
690 managed automatically, providing the commands: add, update, delete, get,
691 list and show. New items will be instances of SomeClass, which should
692 provide a constructor with at least the key value and an update() method,
693 if it should be possible to modify it.
696 def __init__(self, parent, attr=None, cls=None):
697 r"Initialize the object, see the class documentation for details."
700 self._dict_subhandler_attr = attr
702 self._dict_subhandler_class = cls
705 return getattr(self.parent, self._dict_subhandler_attr)
707 @handler(u'Add a new item')
708 def add(self, key, *args, **kwargs):
709 r"add(key, ...) -> None :: Add an item to the dict."
710 item = self._dict_subhandler_class(key, *args, **kwargs)
711 if key in self._dict():
712 raise ItemAlreadyExistsError(key)
713 self._dict()[key] = item
715 @handler(u'Update an item')
716 def update(self, key, *args, **kwargs):
717 r"update(key, ...) -> None :: Update an item of the dict."
718 # TODO make it right with metaclasses, so the method is not created
719 # unless the update() method really exists.
720 if not hasattr(self._dict_subhandler_class, 'update'):
721 raise CommandNotFoundError(('update',))
722 if not key in self._dict():
723 raise ItemNotFoundError(key)
724 self._dict()[key].update(*args, **kwargs)
726 @handler(u'Delete an item')
727 def delete(self, key):
728 r"delete(key) -> None :: Delete an item of the dict."
729 if not key in self._dict():
730 raise ItemNotFoundError(key)
731 del self._dict()[key]
733 @handler(u'Get information about an item')
735 r"get(key) -> Host :: List all the information of an item."
736 if not key in self._dict():
737 raise ItemNotFoundError(key)
738 return self._dict()[key]
740 @handler(u'List all the items by key')
742 r"list() -> tuple :: List all the item keys."
743 return self._dict().keys()
745 @handler(u'Get information about all items')
747 r"show() -> list of Hosts :: List all the complete items information."
748 return self._dict().values()
751 if __name__ == '__main__':
754 class STestHandler1(ServiceHandler):
755 _service_start = ('service', 'start')
756 _service_stop = ('service', 'stop')
757 _service_restart = ('ls', '/')
758 _service_reload = ('cp', '/la')
759 class STestHandler2(ServiceHandler):
761 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
762 'cmd-restart', 'cmd-reload')
763 class ITestHandler1(InitdHandler):
764 _initd_name = 'test1'
765 class ITestHandler2(InitdHandler):
767 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
775 print h.__class__.__name__
778 except ExecutionError, e:
782 except ExecutionError, e:
786 except ExecutionError, e:
790 except ExecutionError, e:
796 class PTestHandler(Persistent):
797 _persistent_attrs = 'vars'
799 self.vars = dict(a=1, b=2)
818 class RTestHandler(Restorable):
819 _persistent_attrs = 'vars'
820 _restorable_defaults = dict(vars=dict(a=1, b=2))
834 os.mkdir('templates')
835 f = file('templates/config', 'w')
836 f.write('Hello, ${name}! You are ${what}.')
839 print file('templates/config').read()
840 class CTestHandler(ConfigWriter):
841 _config_writer_files = 'config'
843 self._config_build_templates()
844 def _get_config_vars(self, config_file):
845 return dict(name='you', what='a parrot')
849 print file('config').read()
851 os.unlink('templates/config')
852 os.rmdir('templates')