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 = '/etc/service'
287 _config_writer_tpl_dir = 'templates'
289 The generated configuration files directory defaults to '.' and the
290 templates directory to 'templates'. _config_writer_files has no default and
291 must be specified in either way. It can be string or a tuple if more than
292 one configuration file must be generated.
294 The template filename and the generated configuration filename are both the
295 same (so if you want to generate some /etc/config, you should have some
296 templates/config template). That's why _config_writer_cfg_dir and
297 _config_writer_tpl_dir can't be the same.
299 When you write your Handler, you should call _config_build_templates() in
300 you Handler constructor to build the templates.
302 To write the configuration files, you must use the _write_config() method.
303 To know what variables to replace in the template, you have to provide a
304 method called _get_config_vars(tamplate_name), which should return a
305 dictionary of variables to pass to the template system to be replaced in
306 the template for the configuration file 'config_file'.
308 # TODO implement it using metaclasses to add the handlers method by demand
309 # (only for specifieds commands).
311 _config_writer_files = ()
312 _config_writer_cfg_dir = '.'
313 _config_writer_tpl_dir = 'templates'
315 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
316 r"Initialize the object, see the class documentation for details."
317 if files is not None:
318 self._config_writer_files = files
319 if cfg_dir is not None:
320 self._config_writer_cfg_dir = cfg_dir
321 if tpl_dir is not None:
322 self._config_writer_tpl_dir = tpl_dir
323 self._config_build_templates()
325 def _config_build_templates(self):
326 r"_config_writer_templates() -> None :: Build the template objects."
327 if isinstance(self._config_writer_files, basestring):
328 self._config_writer_files = (self._config_writer_files,)
329 if not hasattr(self, '_config_writer_templates') \
330 or not self._config_writer_templates:
331 self._config_writer_templates = dict()
332 for t in self._config_writer_files:
333 f = path.join(self._config_writer_tpl_dir, t)
334 self._config_writer_templates[t] = Template(filename=f)
336 def _render_config(self, template_name, vars=None):
337 r"""_render_config(template_name[, config_filename[, vars]]).
339 Render a single config file using the template 'template_name'. If
340 vars is specified, it's used as the dictionary with the variables
341 to replace in the templates, if not, it looks for a
342 _get_config_vars() method to get it.
345 if hasattr(self, '_get_config_vars'):
346 vars = self._get_config_vars(template_name)
350 vars = vars(template_name)
351 return self._config_writer_templates[template_name].render(**vars)
353 def _write_single_config(self, template_name, config_filename=None, vars=None):
354 r"""_write_single_config(template_name[, config_filename[, vars]]).
356 Write a single config file using the template 'template_name'. If no
357 config_filename is specified, the config filename will be the same as
358 the 'template_name' (but stored in the generated config files
359 directory). If it's specified, the generated config file is stored in
360 the file called 'config_filename' (also in the generated files
361 directory). If vars is specified, it's used as the dictionary with the
362 variables to replace in the templates, if not, it looks for a
363 _get_config_vars() method to get it.
365 if not config_filename:
366 config_filename = template_name
368 if hasattr(self, '_get_config_vars'):
369 vars = self._get_config_vars(template_name)
373 vars = vars(template_name)
374 f = file(path.join(self._config_writer_cfg_dir, config_filename), 'w')
375 ctx = Context(f, **vars)
376 self._config_writer_templates[template_name].render_context(ctx)
379 def _write_config(self):
380 r"_write_config() -> None :: Generate all the configuration files."
381 for t in self._config_writer_files:
382 self._write_single_config(t)
385 class ServiceHandler(Handler):
386 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
388 This is a helper class to inherit from to automatically handle services
389 with start, stop, restart, reload actions.
391 The actions can be defined by calling the constructor with all the
392 parameters or in a more declarative way as class attributes, like:
394 class TestHandler(ServiceHandler):
395 _service_start = ('command', 'start')
396 _service_stop = ('command', 'stop')
397 _service_restart = ('command', 'restart')
398 _service_reload = 'reload-command'
400 Commands are executed without using the shell, that's why they are specified
401 as tuples (where the first element is the command and the others are the
402 command arguments). If only a command is needed (without arguments) a single
403 string can be specified.
405 All commands must be specified.
407 # TODO implement it using metaclasses to add the handlers method by demand
408 # (only for specifieds commands).
410 def __init__(self, start=None, stop=None, restart=None, reload=None):
411 r"Initialize the object, see the class documentation for details."
412 for (name, action) in dict(start=start, stop=stop, restart=restart,
413 reload=reload).items():
414 if action is not None:
415 setattr(self, '_service_%s' % name, action)
417 @handler(u'Start the service.')
419 r"start() -> None :: Start the service."
420 call(self._service_start)
422 @handler(u'Stop the service.')
424 r"stop() -> None :: Stop the service."
425 call(self._service_stop)
427 @handler(u'Restart the service.')
429 r"restart() -> None :: Restart the service."
430 call(self._service_restart)
432 @handler(u'Reload the service config (without restarting, if possible).')
434 r"reload() -> None :: Reload the configuration of the service."
435 call(self._service_reload)
437 class InitdHandler(Handler):
438 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
440 This is a helper class to inherit from to automatically handle services
441 with start, stop, restart, reload actions using a /etc/init.d like script.
443 The name and directory of the script can be defined by calling the
444 constructor or in a more declarative way as class attributes, like:
446 class TestHandler(ServiceHandler):
447 _initd_name = 'some-service'
448 _initd_dir = '/usr/local/etc/init.d'
450 The default _initd_dir is '/etc/init.d', _initd_name has no default and
451 must be specified in either way.
453 Commands are executed without using the shell.
455 # TODO implement it using metaclasses to add the handlers method by demand
456 # (only for specifieds commands).
458 _initd_dir = '/etc/init.d'
460 def __init__(self, initd_name=None, initd_dir=None):
461 r"Initialize the object, see the class documentation for details."
462 if initd_name is not None:
463 self._initd_name = initd_name
464 if initd_dir is not None:
465 self._initd_dir = initd_dir
467 @handler(u'Start the service.')
469 r"start() -> None :: Start the service."
470 call((path.join(self._initd_dir, self._initd_name), 'start'))
472 @handler(u'Stop the service.')
474 r"stop() -> None :: Stop the service."
475 call((path.join(self._initd_dir, self._initd_name), 'stop'))
477 @handler(u'Restart the service.')
479 r"restart() -> None :: Restart the service."
480 call((path.join(self._initd_dir, self._initd_name), 'restart'))
482 @handler(u'Reload the service config (without restarting, if possible).')
484 r"reload() -> None :: Reload the configuration of the service."
485 call((path.join(self._initd_dir, self._initd_name), 'reload'))
487 class TransactionalHandler(Handler):
488 r"""Handle command transactions providing a commit and rollback commands.
490 This is a helper class to inherit from to automatically handle
491 transactional handlers, which have commit and rollback commands.
493 The handler should provide a reload() method (see ServiceHandler and
494 InitdHandler for helper classes to provide this) which will be called
495 when a commit command is issued (if a reload() command is present).
496 The persistent data will be written too (if a _dump() method is provided,
497 see Persistent and Restorable for that), and the configuration files
498 will be generated (if a _write_config method is present, see ConfigWriter).
500 # TODO implement it using metaclasses to add the handlers method by demand
501 # (only for specifieds commands).
503 @handler(u'Commit the changes (reloading the service, if necessary).')
505 r"commit() -> None :: Commit the changes and reload the service."
506 if hasattr(self, '_dump'):
508 if hasattr(self, '_write_config'):
510 if hasattr(self, 'reload'):
513 @handler(u'Discard all the uncommited changes.')
515 r"rollback() -> None :: Discard the changes not yet commited."
516 if hasattr(self, '_load'):
519 class ParametersHandler(Handler):
520 r"""ParametersHandler([attr]) -> ParametersHandler.
522 This is a helper class to inherit from to automatically handle
523 service parameters, providing set, get, list and show commands.
525 The attribute that holds the parameters can be defined by calling the
526 constructor or in a more declarative way as class attributes, like:
528 class TestHandler(ServiceHandler):
529 _parameters_attr = 'some_attr'
531 The default is 'params' and it should be a dictionary.
533 # TODO implement it using metaclasses to add the handlers method by demand
534 # (only for specifieds commands).
536 _parameters_attr = 'params'
538 def __init__(self, attr=None):
539 r"Initialize the object, see the class documentation for details."
541 self._parameters_attr = attr
543 @handler(u'Set a service parameter.')
544 def set(self, param, value):
545 r"set(param, value) -> None :: Set a service parameter."
546 if not param in self.params:
547 raise ParameterNotFoundError(param)
548 self.params[param] = value
550 @handler(u'Get a service parameter.')
551 def get(self, param):
552 r"get(param) -> None :: Get a service parameter."
553 if not param in self.params:
554 raise ParameterNotFoundError(param)
555 return self.params[param]
557 @handler(u'List all available service parameters.')
559 r"list() -> tuple :: List all the parameter names."
560 return self.params.keys()
562 @handler(u'Get all service parameters, with their values.')
564 r"show() -> (key, value) tuples :: List all the parameters."
565 return self.params.items()
567 class SubHandler(Handler):
568 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
570 This is a helper class to build sub handlers that needs to reference the
573 parent - Parent Handler object.
576 def __init__(self, parent):
577 r"Initialize the object, see the class documentation for details."
580 class ListSubHandler(SubHandler):
581 r"""ListSubHandler(parent) -> ListSubHandler instance.
583 This is a helper class to inherit from to automatically handle subcommands
584 that operates over a list parent attribute.
586 The list attribute to handle and the class of objects that it contains can
587 be defined by calling the constructor or in a more declarative way as
588 class attributes, like:
590 class TestHandler(ListSubHandler):
591 _list_subhandler_attr = 'some_list'
592 _list_subhandler_class = SomeClass
594 This way, the parent's some_list attribute (self.parent.some_list) will be
595 managed automatically, providing the commands: add, update, delete, get,
596 list and show. New items will be instances of SomeClass, which should
597 provide a cmp operator to see if the item is on the list and an update()
598 method, if it should be possible to modify it.
601 def __init__(self, parent, attr=None, cls=None):
602 r"Initialize the object, see the class documentation for details."
605 self._list_subhandler_attr = attr
607 self._list_subhandler_class = cls
610 return getattr(self.parent, self._list_subhandler_attr)
612 @handler(u'Add a new item')
613 def add(self, *args, **kwargs):
614 r"add(...) -> None :: Add an item to the list."
615 item = self._list_subhandler_class(*args, **kwargs)
616 if item in self._list():
617 raise ItemAlreadyExistsError(item)
618 self._list().append(item)
620 @handler(u'Update an item')
621 def update(self, index, *args, **kwargs):
622 r"update(index, ...) -> None :: Update an item of the list."
623 # TODO make it right with metaclasses, so the method is not created
624 # unless the update() method really exists.
625 # TODO check if the modified item is the same of an existing one
626 index = int(index) # TODO validation
627 if not hasattr(self._list_subhandler_class, 'update'):
628 raise CommandNotFoundError(('update',))
630 self._list()[index].update(*args, **kwargs)
632 raise ItemNotFoundError(index)
634 @handler(u'Delete an item')
635 def delete(self, index):
636 r"delete(index) -> None :: Delete an item of the list."
637 index = int(index) # TODO validation
639 return self._list().pop(index)
641 raise ItemNotFoundError(index)
643 @handler(u'Get information about an item')
644 def get(self, index):
645 r"get(index) -> Host :: List all the information of an item."
646 index = int(index) # TODO validation
648 return self._list()[index]
650 raise ItemNotFoundError(index)
652 @handler(u'Get how many items are in the list')
654 r"len() -> int :: Get how many items are in the list."
655 return len(self._list())
657 @handler(u'Get information about all items')
659 r"show() -> list of Hosts :: List all the complete items information."
662 class DictSubHandler(SubHandler):
663 r"""DictSubHandler(parent) -> DictSubHandler instance.
665 This is a helper class to inherit from to automatically handle subcommands
666 that operates over a dict parent attribute.
668 The dict attribute to handle and the class of objects that it contains can
669 be defined by calling the constructor or in a more declarative way as
670 class attributes, like:
672 class TestHandler(DictSubHandler):
673 _dict_subhandler_attr = 'some_dict'
674 _dict_subhandler_class = SomeClass
676 This way, the parent's some_dict attribute (self.parent.some_dict) will be
677 managed automatically, providing the commands: add, update, delete, get,
678 list and show. New items will be instances of SomeClass, which should
679 provide a constructor with at least the key value and an update() method,
680 if it should be possible to modify it.
683 def __init__(self, parent, attr=None, cls=None):
684 r"Initialize the object, see the class documentation for details."
687 self._dict_subhandler_attr = attr
689 self._dict_subhandler_class = cls
692 return getattr(self.parent, self._dict_subhandler_attr)
694 @handler(u'Add a new item')
695 def add(self, key, *args, **kwargs):
696 r"add(key, ...) -> None :: Add an item to the dict."
697 item = self._dict_subhandler_class(key, *args, **kwargs)
698 if key in self._dict():
699 raise ItemAlreadyExistsError(key)
700 self._dict()[key] = item
702 @handler(u'Update an item')
703 def update(self, key, *args, **kwargs):
704 r"update(key, ...) -> None :: Update an item of the dict."
705 # TODO make it right with metaclasses, so the method is not created
706 # unless the update() method really exists.
707 if not hasattr(self._dict_subhandler_class, 'update'):
708 raise CommandNotFoundError(('update',))
709 if not key in self._dict():
710 raise ItemNotFoundError(key)
711 self._dict()[key].update(*args, **kwargs)
713 @handler(u'Delete an item')
714 def delete(self, key):
715 r"delete(key) -> None :: Delete an item of the dict."
716 if not key in self._dict():
717 raise ItemNotFoundError(key)
718 del self._dict()[key]
720 @handler(u'Get information about an item')
722 r"get(key) -> Host :: List all the information of an item."
723 if not key in self._dict():
724 raise ItemNotFoundError(key)
725 return self._dict()[key]
727 @handler(u'List all the items by key')
729 r"list() -> tuple :: List all the item keys."
730 return self._dict().keys()
732 @handler(u'Get information about all items')
734 r"show() -> list of Hosts :: List all the complete items information."
735 return self._dict().values()
738 if __name__ == '__main__':
741 class STestHandler1(ServiceHandler):
742 _service_start = ('service', 'start')
743 _service_stop = ('service', 'stop')
744 _service_restart = ('ls', '/')
745 _service_reload = ('cp', '/la')
746 class STestHandler2(ServiceHandler):
748 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
749 'cmd-restart', 'cmd-reload')
750 class ITestHandler1(InitdHandler):
751 _initd_name = 'test1'
752 class ITestHandler2(InitdHandler):
754 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
762 print h.__class__.__name__
765 except ExecutionError, e:
769 except ExecutionError, e:
773 except ExecutionError, e:
777 except ExecutionError, e:
783 class PTestHandler(Persistent):
784 _persistent_attrs = 'vars'
786 self.vars = dict(a=1, b=2)
805 class RTestHandler(Restorable):
806 _persistent_attrs = 'vars'
807 _restorable_defaults = dict(vars=dict(a=1, b=2))
821 os.mkdir('templates')
822 f = file('templates/config', 'w')
823 f.write('Hello, ${name}! You are ${what}.')
826 print file('templates/config').read()
827 class CTestHandler(ConfigWriter):
828 _config_writer_files = 'config'
830 self._config_build_templates()
831 def _get_config_vars(self, config_file):
832 return dict(name='you', what='a parrot')
836 print file('config').read()
838 os.unlink('templates/config')
839 os.rmdir('templates')