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__ = ('Error', 'ReturnNot0Error', 'ExecutionError', 'ItemError',
19 'ItemAlreadyExistsError', 'ItemNotFoundError', 'ContainerError',
20 'ContainerNotFoundError', 'call', 'get_network_devices',
21 'Persistent', 'Restorable', 'ConfigWriter', 'ServiceHandler',
22 'RestartHandler', 'ReloadHandler', 'InitdHandler', 'SubHandler',
23 'DictSubHandler', 'ListSubHandler', 'ComposedSubHandler',
24 'ListComposedSubHandler', 'DictComposedSubHandler')
26 class Error(HandlerError):
28 Error(message) -> Error instance :: Base ServiceHandler exception class.
30 All exceptions raised by the ServiceHandler inherits from this one, so
31 you can easily catch any ServiceHandler exception.
33 message - A descriptive error message.
37 class ReturnNot0Error(Error):
39 ReturnNot0Error(return_value) -> ReturnNot0Error instance.
41 A command didn't returned the expected 0 return value.
43 return_value - Return value returned by the command.
46 def __init__(self, return_value):
47 r"Initialize the object. See class documentation for more info."
48 self.return_value = return_value
50 def __unicode__(self):
51 return 'The command returned %d' % self.return_value
53 class ExecutionError(Error):
55 ExecutionError(command, error) -> ExecutionError instance.
57 Error executing a command.
59 command - Command that was tried to execute.
61 error - Error received when trying to execute the command.
64 def __init__(self, command, error):
65 r"Initialize the object. See class documentation for more info."
66 self.command = command
69 def __unicode__(self):
70 command = self.command
71 if not isinstance(self.command, basestring):
72 command = ' '.join(command)
73 return "Can't execute command %s: %s" % (command, self.error)
75 class ParameterError(Error, KeyError):
77 ParameterError(paramname) -> ParameterError instance
79 This is the base exception for all DhcpHandler parameters related errors.
82 def __init__(self, paramname):
83 r"Initialize the object. See class documentation for more info."
84 self.message = 'Parameter error: "%s"' % paramname
86 class ParameterNotFoundError(ParameterError):
88 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
90 This exception is raised when trying to operate on a parameter that doesn't
94 def __init__(self, paramname):
95 r"Initialize the object. See class documentation for more info."
96 self.message = 'Parameter not found: "%s"' % paramname
98 class ItemError(Error, KeyError):
100 ItemError(key) -> ItemError instance.
102 This is the base exception for all item related errors.
105 def __init__(self, key):
106 r"Initialize the object. See class documentation for more info."
107 self.message = u'Item error: "%s"' % key
109 class ItemAlreadyExistsError(ItemError):
111 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
113 This exception is raised when trying to add an item that already exists.
116 def __init__(self, key):
117 r"Initialize the object. See class documentation for more info."
118 self.message = u'Item already exists: "%s"' % key
120 class ItemNotFoundError(ItemError):
122 ItemNotFoundError(key) -> ItemNotFoundError instance.
124 This exception is raised when trying to operate on an item that doesn't
128 def __init__(self, key):
129 r"Initialize the object. See class documentation for more info."
130 self.message = u'Item not found: "%s"' % key
132 class ContainerError(Error, KeyError):
134 ContainerError(key) -> ContainerError instance.
136 This is the base exception for all container related errors.
139 def __init__(self, key):
140 r"Initialize the object. See class documentation for more info."
141 self.message = u'Container error: "%s"' % key
143 class ContainerNotFoundError(ContainerError):
145 ContainerNotFoundError(key) -> ContainerNotFoundError instance.
147 This exception is raised when trying to operate on an container that
151 def __init__(self, key):
152 r"Initialize the object. See class documentation for more info."
153 self.message = u'Container not found: "%s"' % key
156 def get_network_devices():
157 p = subprocess.Popen(('ip', 'link', 'list'), stdout=subprocess.PIPE,
159 string = p.stdout.read()
162 i = string.find('eth')
165 m = string.find('link/ether', i+4)
166 mac = string[ m+11 : m+11+17]
168 i = string.find('eth', m+11+17)
171 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
172 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
175 if not isinstance(command, basestring):
176 command = ' '.join(command)
177 print 'Executing command:', command
180 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
181 universal_newlines=universal_newlines,
182 close_fds=close_fds, **kw)
184 raise ExecutionError(command, e)
186 raise ExecutionError(command, ReturnNot0Error(r))
189 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
191 This is a helper class to inherit from to automatically handle data
192 persistence using pickle.
194 The variables attributes to persist (attrs), and the pickle directory (dir)
195 and file extension (ext) can be defined by calling the constructor or in a
196 more declarative way as class attributes, like:
198 class TestHandler(Persistent):
199 _persistent_attrs = ('some_attr', 'other_attr')
200 _persistent_dir = 'persistent-data'
201 _persistent_ext = '.pickle'
203 The default dir is '.' and the default extension is '.pkl'. There are no
204 default variables, and they should be specified as string if a single
205 attribute should be persistent or as a tuple of strings if they are more.
206 The strings should be the attribute names to be persisted. For each
207 attribute a separated pickle file is generated in the pickle directory.
209 You can call _dump() and _load() to write and read the data respectively.
211 # TODO implement it using metaclasses to add the handlers method by demand
212 # (only for specifieds commands).
214 _persistent_attrs = ()
215 _persistent_dir = '.'
216 _persistent_ext = '.pkl'
218 def __init__(self, attrs=None, dir=None, ext=None):
219 r"Initialize the object, see the class documentation for details."
220 if attrs is not None:
221 self._persistent_attrs = attrs
223 self._persistent_dir = dir
225 self._persistent_ext = ext
228 r"_dump() -> None :: Dump all persistent data to pickle files."
229 if isinstance(self._persistent_attrs, basestring):
230 self._persistent_attrs = (self._persistent_attrs,)
231 for attrname in self._persistent_attrs:
232 self._dump_attr(attrname)
235 r"_load() -> None :: Load all persistent data from pickle files."
236 if isinstance(self._persistent_attrs, basestring):
237 self._persistent_attrs = (self._persistent_attrs,)
238 for attrname in self._persistent_attrs:
239 self._load_attr(attrname)
241 def _dump_attr(self, attrname):
242 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
243 f = file(self._pickle_filename(attrname), 'wb')
244 pickle.dump(getattr(self, attrname), f, 2)
247 def _load_attr(self, attrname):
248 r"_load_attr() -> object :: Load a specific pickle file."
249 f = file(self._pickle_filename(attrname))
250 setattr(self, attrname, pickle.load(f))
253 def _pickle_filename(self, name):
254 r"_pickle_filename() -> string :: Construct a pickle filename."
255 return path.join(self._persistent_dir, name) + self._persistent_ext
257 class Restorable(Persistent):
258 r"""Restorable([defaults]) -> Restorable.
260 This is a helper class to inherit from that provides a nice _restore()
261 method to restore the persistent data if any, or load some nice defaults
264 The defaults can be defined by calling the constructor or in a more
265 declarative way as class attributes, like:
267 class TestHandler(Restorable):
268 _persistent_attrs = ('some_attr', 'other_attr')
269 _restorable_defaults = dict(
270 some_attr = 'some_default',
271 other_attr = 'other_default')
273 The defaults is a dictionary, very coupled with the _persistent_attrs
274 attribute inherited from Persistent. The defaults keys should be the
275 values from _persistent_attrs, and the values the default values.
277 The _restore() method returns True if the data was restored successfully
278 or False if the defaults were loaded (in case you want to take further
279 actions). If a _write_config method if found, it's executed when a restore
282 # TODO implement it using metaclasses to add the handlers method by demand
283 # (only for specifieds commands).
285 _restorable_defaults = dict()
287 def __init__(self, defaults=None):
288 r"Initialize the object, see the class documentation for details."
289 if defaults is not None:
290 self._restorable_defaults = defaults
293 r"_restore() -> bool :: Restore persistent data or create a default."
298 for (k, v) in self._restorable_defaults.items():
300 # TODO tener en cuenta servicios que hay que levantar y los que no
301 if hasattr(self, 'commit'):
305 if hasattr(self, '_write_config'):
310 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
312 This is a helper class to inherit from to automatically handle
313 configuration generation. Mako template system is used for configuration
316 The configuration filenames, the generated configuration files directory
317 and the templates directory can be defined by calling the constructor or
318 in a more declarative way as class attributes, like:
320 class TestHandler(ConfigWriter):
321 _config_writer_files = ('base.conf', 'custom.conf')
322 _config_writer_cfg_dir = {
323 'base.conf': '/etc/service',
324 'custom.conf': '/etc/service/conf.d',
326 _config_writer_tpl_dir = 'templates'
328 The generated configuration files directory defaults to '.' and the
329 templates directory to 'templates'. _config_writer_files has no default and
330 must be specified in either way. It can be string or a tuple if more than
331 one configuration file must be generated. _config_writer_cfg_dir could be a
332 dict mapping which file should be stored in which directory, or a single
333 string if all the config files should go to the same directory.
335 The template filename and the generated configuration filename are both the
336 same (so if you want to generate some /etc/config, you should have some
337 templates/config template). That's why _config_writer_cfg_dir and
338 _config_writer_tpl_dir can't be the same. This is not true for very
339 specific cases where _write_single_config() is used.
341 When you write your Handler, you should call _config_build_templates() in
342 you Handler constructor to build the templates.
344 To write the configuration files, you must use the _write_config() method.
345 To know what variables to replace in the template, you have to provide a
346 method called _get_config_vars(tamplate_name), which should return a
347 dictionary of variables to pass to the template system to be replaced in
348 the template for the configuration file 'config_file'.
350 # TODO implement it using metaclasses to add the handlers method by demand
351 # (only for specifieds commands).
353 _config_writer_files = ()
354 _config_writer_cfg_dir = '.'
355 _config_writer_tpl_dir = 'templates'
357 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
358 r"Initialize the object, see the class documentation for details."
359 if files is not None:
360 self._config_writer_files = files
361 if cfg_dir is not None:
362 self._config_writer_cfg_dir = cfg_dir
363 if tpl_dir is not None:
364 self._config_writer_tpl_dir = tpl_dir
365 self._config_build_templates()
367 def _config_build_templates(self):
368 r"_config_writer_templates() -> None :: Build the template objects."
369 if isinstance(self._config_writer_files, basestring):
370 self._config_writer_files = (self._config_writer_files,)
371 if not hasattr(self, '_config_writer_templates') \
372 or not self._config_writer_templates:
373 self._config_writer_templates = dict()
374 for t in self._config_writer_files:
375 f = path.join(self._config_writer_tpl_dir, t)
376 self._config_writer_templates[t] = Template(filename=f)
378 def _render_config(self, template_name, vars=None):
379 r"""_render_config(template_name[, config_filename[, vars]]).
381 Render a single config file using the template 'template_name'. If
382 vars is specified, it's used as the dictionary with the variables
383 to replace in the templates, if not, it looks for a
384 _get_config_vars() method to get it.
387 if hasattr(self, '_get_config_vars'):
388 vars = self._get_config_vars(template_name)
392 vars = vars(template_name)
393 return self._config_writer_templates[template_name].render(**vars)
395 def _get_config_path(self, template_name, config_filename=None):
396 r"Get a complete configuration path."
397 if not config_filename:
398 config_filename = template_name
399 if isinstance(self._config_writer_cfg_dir, basestring):
400 return path.join(self._config_writer_cfg_dir, config_filename)
401 return path.join(self._config_writer_cfg_dir[template_name],
404 def _write_single_config(self, template_name, config_filename=None, vars=None):
405 r"""_write_single_config(template_name[, config_filename[, vars]]).
407 Write a single config file using the template 'template_name'. If no
408 config_filename is specified, the config filename will be the same as
409 the 'template_name' (but stored in the generated config files
410 directory). If it's specified, the generated config file is stored in
411 the file called 'config_filename' (also in the generated files
412 directory). If vars is specified, it's used as the dictionary with the
413 variables to replace in the templates, if not, it looks for a
414 _get_config_vars() method to get it.
417 if hasattr(self, '_get_config_vars'):
418 vars = self._get_config_vars(template_name)
422 vars = vars(template_name)
423 f = file(self._get_config_path(template_name, config_filename), 'w')
424 ctx = Context(f, **vars)
425 self._config_writer_templates[template_name].render_context(ctx)
428 def _write_config(self):
429 r"_write_config() -> None :: Generate all the configuration files."
430 for t in self._config_writer_files:
431 self._write_single_config(t)
434 class ServiceHandler(Handler, Restorable):
435 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
437 This is a helper class to inherit from to automatically handle services
438 with start, stop, restart, reload actions.
440 The actions can be defined by calling the constructor with all the
441 parameters or in a more declarative way as class attributes, like:
443 class TestHandler(ServiceHandler):
444 _service_start = ('command', 'start')
445 _service_stop = ('command', 'stop')
446 _service_restart = ('command', 'restart')
447 _service_reload = 'reload-command'
449 Commands are executed without using the shell, that's why they are specified
450 as tuples (where the first element is the command and the others are the
451 command arguments). If only a command is needed (without arguments) a single
452 string can be specified.
454 All commands must be specified.
456 # TODO implement it using metaclasses to add the handlers method by demand
457 # (only for specifieds commands).
459 def __init__(self, start=None, stop=None, restart=None, reload=None):
460 r"Initialize the object, see the class documentation for details."
461 for (name, action) in dict(start=start, stop=stop, restart=restart,
462 reload=reload).items():
463 if action is not None:
464 setattr(self, '_service_%s' % name, action)
465 self._persistent_attrs = list(self._persistent_attrs)
466 self._persistent_attrs.append('_service_running')
467 if '_service_running' not in self._restorable_defaults:
468 self._restorable_defaults['_service_running'] = False
470 if self._service_running:
471 self._service_running = False
474 @handler(u'Start the service.')
476 r"start() -> None :: Start the service."
477 if not self._service_running:
478 if callable(self._service_start):
479 self._service_start()
481 call(self._service_start)
482 self._service_running = True
483 self._dump_attr('_service_running')
485 @handler(u'Stop the service.')
487 r"stop() -> None :: Stop the service."
488 if self._service_running:
489 if callable(self._service_stop):
492 call(self._service_stop)
493 self._service_running = False
494 self._dump_attr('_service_running')
496 @handler(u'Restart the service.')
498 r"restart() -> None :: Restart the service."
499 if callable(self._service_restart):
500 self._service_restart()
502 call(self._service_restart)
503 self._service_running = True
504 self._dump_attr('_service_running')
506 @handler(u'Reload the service config (without restarting, if possible).')
508 r"reload() -> None :: Reload the configuration of the service."
509 if self._service_running:
510 if callable(self._service_reload):
511 self._service_reload()
513 call(self._service_reload)
515 @handler(u'Tell if the service is running.')
517 r"reload() -> None :: Reload the configuration of the service."
518 if self._service_running:
523 class RestartHandler(Handler):
524 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
526 This is a helper class to inherit from to automatically add a restart
527 command that first stop the service and then starts it again (using start
528 and stop commands respectively).
531 @handler(u'Restart the service (alias to stop + start).')
533 r"restart() -> None :: Restart the service calling stop() and start()."
537 class ReloadHandler(Handler):
538 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
540 This is a helper class to inherit from to automatically add a reload
541 command that calls restart.
544 @handler(u'Reload the service config (alias to restart).')
546 r"reload() -> None :: Reload the configuration of the service."
547 if hasattr(self, '_service_running') and self._service_running:
550 class InitdHandler(ServiceHandler):
551 # TODO update docs, declarative style is depracated
552 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
554 This is a helper class to inherit from to automatically handle services
555 with start, stop, restart, reload actions using a /etc/init.d like script.
557 The name and directory of the script can be defined by calling the
558 constructor or in a more declarative way as class attributes, like:
560 class TestHandler(ServiceHandler):
561 _initd_name = 'some-service'
562 _initd_dir = '/usr/local/etc/init.d'
564 The default _initd_dir is '/etc/init.d', _initd_name has no default and
565 must be specified in either way.
567 Commands are executed without using the shell.
569 # TODO implement it using metaclasses to add the handlers method by demand
570 # (only for specifieds commands).
572 _initd_dir = '/etc/init.d'
574 def __init__(self, initd_name=None, initd_dir=None):
575 r"Initialize the object, see the class documentation for details."
576 if initd_name is not None:
577 self._initd_name = initd_name
578 if initd_dir is not None:
579 self._initd_dir = initd_dir
581 for action in ('start', 'stop', 'restart', 'reload'):
582 actions[action] = (path.join(self._initd_dir, self._initd_name),
584 ServiceHandler.__init__(self, **actions)
586 class TransactionalHandler(Handler):
587 r"""Handle command transactions providing a commit and rollback commands.
589 This is a helper class to inherit from to automatically handle
590 transactional handlers, which have commit and rollback commands.
592 The handler should provide a reload() method (see ServiceHandler and
593 InitdHandler for helper classes to provide this) which will be called
594 when a commit command is issued (if a reload() command is present).
595 The persistent data will be written too (if a _dump() method is provided,
596 see Persistent and Restorable for that), and the configuration files
597 will be generated (if a _write_config method is present, see ConfigWriter).
599 # TODO implement it using metaclasses to add the handlers method by demand
600 # (only for specifieds commands).
602 @handler(u'Commit the changes (reloading the service, if necessary).')
604 r"commit() -> None :: Commit the changes and reload the service."
605 if hasattr(self, '_dump'):
608 if hasattr(self, '_write_config'):
609 unchanged = self._write_config()
610 if not unchanged and hasattr(self, 'reload'):
613 @handler(u'Discard all the uncommited changes.')
615 r"rollback() -> None :: Discard the changes not yet commited."
616 if hasattr(self, '_load'):
619 class ParametersHandler(Handler):
620 r"""ParametersHandler([attr]) -> ParametersHandler.
622 This is a helper class to inherit from to automatically handle
623 service parameters, providing set, get, list and show commands.
625 The attribute that holds the parameters can be defined by calling the
626 constructor or in a more declarative way as class attributes, like:
628 class TestHandler(ServiceHandler):
629 _parameters_attr = 'some_attr'
631 The default is 'params' and it should be a dictionary.
633 # TODO implement it using metaclasses to add the handlers method by demand
634 # (only for specifieds commands).
636 _parameters_attr = 'params'
638 def __init__(self, attr=None):
639 r"Initialize the object, see the class documentation for details."
641 self._parameters_attr = attr
643 @handler(u'Set a service parameter.')
644 def set(self, param, value):
645 r"set(param, value) -> None :: Set a service parameter."
646 if not param in self.params:
647 raise ParameterNotFoundError(param)
648 self.params[param] = value
649 if hasattr(self, '_update'):
652 @handler(u'Get a service parameter.')
653 def get(self, param):
654 r"get(param) -> None :: Get a service parameter."
655 if not param in self.params:
656 raise ParameterNotFoundError(param)
657 return self.params[param]
659 @handler(u'List all available service parameters.')
661 r"list() -> tuple :: List all the parameter names."
662 return self.params.keys()
664 @handler(u'Get all service parameters, with their values.')
666 r"show() -> (key, value) tuples :: List all the parameters."
667 return self.params.items()
669 class SubHandler(Handler):
670 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
672 This is a helper class to build sub handlers that needs to reference the
675 parent - Parent Handler object.
678 def __init__(self, parent):
679 r"Initialize the object, see the class documentation for details."
682 class ContainerSubHandler(SubHandler):
683 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
685 This is a helper class to implement ListSubHandler and DictSubHandler. You
686 should not use it directly.
688 The container attribute to handle and the class of objects that it
689 contains can be defined by calling the constructor or in a more declarative
690 way as class attributes, like:
692 class TestHandler(ContainerSubHandler):
693 _cont_subhandler_attr = 'some_cont'
694 _cont_subhandler_class = SomeClass
696 This way, the parent's some_cont attribute (self.parent.some_cont)
697 will be managed automatically, providing the commands: add, update,
698 delete, get and show. New items will be instances of SomeClass,
699 which should provide a cmp operator to see if the item is on the
700 container and an update() method, if it should be possible to modify
701 it. If SomeClass has an _add, _update or _delete attribute, it set
702 them to true when the item is added, updated or deleted respectively
703 (in case that it's deleted, it's not removed from the container,
704 but it's not listed either).
707 def __init__(self, parent, attr=None, cls=None):
708 r"Initialize the object, see the class documentation for details."
711 self._cont_subhandler_attr = attr
713 self._cont_subhandler_class = cls
715 def _attr(self, attr=None):
717 return getattr(self.parent, self._cont_subhandler_attr)
718 setattr(self.parent, self._cont_subhandler_attr, attr)
721 if isinstance(self._attr(), dict):
722 return dict([(k, i) for (k, i) in self._attr().items()
723 if not hasattr(i, '_delete') or not i._delete])
724 return [i for i in self._attr()
725 if not hasattr(i, '_delete') or not i._delete]
727 @handler(u'Add a new item')
728 def add(self, *args, **kwargs):
729 r"add(...) -> None :: Add an item to the list."
730 item = self._cont_subhandler_class(*args, **kwargs)
731 if hasattr(item, '_add'):
734 if isinstance(self._attr(), dict):
735 key = item.as_tuple()[0]
736 # do we have the same item? then raise an error
737 if key in self._vattr():
738 raise ItemAlreadyExistsError(item)
739 # do we have the same item, but logically deleted? then update flags
740 if key in self._attr():
742 if not isinstance(self._attr(), dict):
743 index = self._attr().index(item)
744 if hasattr(item, '_add'):
745 self._attr()[index]._add = False
746 if hasattr(item, '_delete'):
747 self._attr()[index]._delete = False
748 else: # it's *really* new
749 if isinstance(self._attr(), dict):
750 self._attr()[key] = item
752 self._attr().append(item)
754 @handler(u'Update an item')
755 def update(self, index, *args, **kwargs):
756 r"update(index, ...) -> None :: Update an item of the container."
757 # TODO make it right with metaclasses, so the method is not created
758 # unless the update() method really exists.
759 # TODO check if the modified item is the same of an existing one
760 if not isinstance(self._attr(), dict):
761 index = int(index) # TODO validation
762 if not hasattr(self._cont_subhandler_class, 'update'):
763 raise CommandNotFoundError(('update',))
765 item = self._vattr()[index]
766 item.update(*args, **kwargs)
767 if hasattr(item, '_update'):
770 raise ItemNotFoundError(index)
772 @handler(u'Delete an item')
773 def delete(self, index):
774 r"delete(index) -> None :: Delete an item of the container."
775 if not isinstance(self._attr(), dict):
776 index = int(index) # TODO validation
778 item = self._vattr()[index]
779 if hasattr(item, '_delete'):
782 del self._attr()[index]
785 raise ItemNotFoundError(index)
787 @handler(u'Remove all items (use with care).')
789 r"clear() -> None :: Delete all items of the container."
790 if isinstance(self._attr(), dict):
795 @handler(u'Get information about an item')
796 def get(self, index):
797 r"get(index) -> item :: List all the information of an item."
798 if not isinstance(self._attr(), dict):
799 index = int(index) # TODO validation
801 return self._vattr()[index]
803 raise ItemNotFoundError(index)
805 @handler(u'Get information about all items')
807 r"show() -> list of items :: List all the complete items information."
808 if isinstance(self._attr(), dict):
809 return self._attr().values()
812 class ListSubHandler(ContainerSubHandler):
813 r"""ListSubHandler(parent) -> ListSubHandler instance.
815 ContainerSubHandler holding lists. See ComposedSubHandler documentation
819 @handler(u'Get how many items are in the list')
821 r"len() -> int :: Get how many items are in the list."
822 return len(self._vattr())
824 class DictSubHandler(ContainerSubHandler):
825 r"""DictSubHandler(parent) -> DictSubHandler instance.
827 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
831 @handler(u'List all the items by key')
833 r"list() -> tuple :: List all the item keys."
834 return self._attr().keys()
836 class ComposedSubHandler(SubHandler):
837 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
839 This is a helper class to implement ListComposedSubHandler and
840 DictComposedSubHandler. You should not use it directly.
842 This class is usefull when you have a parent that has a dict (cont)
843 that stores some object that has an attribute (attr) with a list or
844 a dict of objects of some class. In that case, this class provides
845 automated commands to add, update, delete, get and show that objects.
846 This commands takes the cont (key of the dict for the object holding
847 the attr), and an index for access the object itself (in the attr
850 The container object (cont) that holds a containers, the attribute of
851 that object that is the container itself, and the class of the objects
852 that it contains can be defined by calling the constructor or in a
853 more declarative way as class attributes, like:
855 class TestHandler(ComposedSubHandler):
856 _comp_subhandler_cont = 'some_cont'
857 _comp_subhandler_attr = 'some_attr'
858 _comp_subhandler_class = SomeClass
860 This way, the parent's some_cont attribute (self.parent.some_cont)
861 will be managed automatically, providing the commands: add, update,
862 delete, get and show for manipulating a particular instance that holds
863 of SomeClass. For example, updating an item at the index 5 is the same
864 (simplified) as doing parent.some_cont[cont][5].update().
865 SomeClass should provide a cmp operator to see if the item is on the
866 container and an update() method, if it should be possible to modify
867 it. If SomeClass has an _add, _update or _delete attribute, it set
868 them to true when the item is added, updated or deleted respectively
869 (in case that it's deleted, it's not removed from the container,
870 but it's not listed either). If the container objects
871 (parent.some_cont[cont]) has an _update attribute, it's set to True
872 when any add, update or delete command is executed.
875 def __init__(self, parent, cont=None, attr=None, cls=None):
876 r"Initialize the object, see the class documentation for details."
879 self._comp_subhandler_cont = cont
881 self._comp_subhandler_attr = attr
883 self._comp_subhandler_class = cls
886 return getattr(self.parent, self._comp_subhandler_cont)
888 def _attr(self, cont, attr=None):
890 return getattr(self._cont()[cont], self._comp_subhandler_attr)
891 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
893 def _vattr(self, cont):
894 if isinstance(self._attr(cont), dict):
895 return dict([(k, i) for (k, i) in self._attr(cont).items()
896 if not hasattr(i, '_delete') or not i._delete])
897 return [i for i in self._attr(cont)
898 if not hasattr(i, '_delete') or not i._delete]
900 @handler(u'Add a new item')
901 def add(self, cont, *args, **kwargs):
902 r"add(cont, ...) -> None :: Add an item to the list."
903 if not cont in self._cont():
904 raise ContainerNotFoundError(cont)
905 item = self._comp_subhandler_class(*args, **kwargs)
906 if hasattr(item, '_add'):
909 if isinstance(self._attr(cont), dict):
910 key = item.as_tuple()[0]
911 # do we have the same item? then raise an error
912 if key in self._vattr(cont):
913 raise ItemAlreadyExistsError(item)
914 # do we have the same item, but logically deleted? then update flags
915 if key in self._attr(cont):
917 if not isinstance(self._attr(cont), dict):
918 index = self._attr(cont).index(item)
919 if hasattr(item, '_add'):
920 self._attr(cont)[index]._add = False
921 if hasattr(item, '_delete'):
922 self._attr(cont)[index]._delete = False
923 else: # it's *really* new
924 if isinstance(self._attr(cont), dict):
925 self._attr(cont)[key] = item
927 self._attr(cont).append(item)
928 if hasattr(self._cont()[cont], '_update'):
929 self._cont()[cont]._update = True
931 @handler(u'Update an item')
932 def update(self, cont, index, *args, **kwargs):
933 r"update(cont, index, ...) -> None :: Update an item of the container."
934 # TODO make it right with metaclasses, so the method is not created
935 # unless the update() method really exists.
936 # TODO check if the modified item is the same of an existing one
937 if not cont in self._cont():
938 raise ContainerNotFoundError(cont)
939 if not isinstance(self._attr(cont), dict):
940 index = int(index) # TODO validation
941 if not hasattr(self._comp_subhandler_class, 'update'):
942 raise CommandNotFoundError(('update',))
944 item = self._vattr(cont)[index]
945 item.update(*args, **kwargs)
946 if hasattr(item, '_update'):
948 if hasattr(self._cont()[cont], '_update'):
949 self._cont()[cont]._update = True
951 raise ItemNotFoundError(index)
953 @handler(u'Delete an item')
954 def delete(self, cont, index):
955 r"delete(cont, index) -> None :: Delete an item of the container."
956 if not cont in self._cont():
957 raise ContainerNotFoundError(cont)
958 if not isinstance(self._attr(cont), dict):
959 index = int(index) # TODO validation
961 item = self._vattr(cont)[index]
962 if hasattr(item, '_delete'):
965 del self._attr(cont)[index]
966 if hasattr(self._cont()[cont], '_update'):
967 self._cont()[cont]._update = True
970 raise ItemNotFoundError(index)
972 @handler(u'Remove all items (use with care).')
973 def clear(self, cont):
974 r"clear(cont) -> None :: Delete all items of the container."
975 if not cont in self._cont():
976 raise ContainerNotFoundError(cont)
977 if isinstance(self._attr(cont), dict):
978 self._attr(cont).clear()
980 self._attr(cont, list())
982 @handler(u'Get information about an item')
983 def get(self, cont, index):
984 r"get(cont, index) -> item :: List all the information of an item."
985 if not cont in self._cont():
986 raise ContainerNotFoundError(cont)
987 if not isinstance(self._attr(cont), dict):
988 index = int(index) # TODO validation
990 return self._vattr(cont)[index]
992 raise ItemNotFoundError(index)
994 @handler(u'Get information about all items')
995 def show(self, cont):
996 r"show(cont) -> list of items :: List all the complete items information."
997 if not cont in self._cont():
998 raise ContainerNotFoundError(cont)
999 if isinstance(self._attr(cont), dict):
1000 return self._attr(cont).values()
1001 return self._vattr(cont)
1003 class ListComposedSubHandler(ComposedSubHandler):
1004 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1006 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1010 @handler(u'Get how many items are in the list')
1011 def len(self, cont):
1012 r"len(cont) -> int :: Get how many items are in the list."
1013 if not cont in self._cont():
1014 raise ContainerNotFoundError(cont)
1015 return len(self._vattr(cont))
1017 class DictComposedSubHandler(ComposedSubHandler):
1018 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1020 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1024 @handler(u'List all the items by key')
1025 def list(self, cont):
1026 r"list(cont) -> tuple :: List all the item keys."
1027 if not cont in self._cont():
1028 raise ContainerNotFoundError(cont)
1029 return self._attr(cont).keys()
1032 if __name__ == '__main__':
1035 class STestHandler1(ServiceHandler):
1036 _service_start = ('service', 'start')
1037 _service_stop = ('service', 'stop')
1038 _service_restart = ('ls', '/')
1039 _service_reload = ('cp', '/la')
1040 class STestHandler2(ServiceHandler):
1042 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1043 'cmd-restart', 'cmd-reload')
1044 class ITestHandler1(InitdHandler):
1045 _initd_name = 'test1'
1046 class ITestHandler2(InitdHandler):
1048 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1056 print h.__class__.__name__
1059 except ExecutionError, e:
1063 except ExecutionError, e:
1067 except ExecutionError, e:
1071 except ExecutionError, e:
1076 print 'PTestHandler'
1077 class PTestHandler(Persistent):
1078 _persistent_attrs = 'vars'
1080 self.vars = dict(a=1, b=2)
1098 print 'RTestHandler'
1099 class RTestHandler(Restorable):
1100 _persistent_attrs = 'vars'
1101 _restorable_defaults = dict(vars=dict(a=1, b=2))
1113 print 'CTestHandler'
1115 os.mkdir('templates')
1116 f = file('templates/config', 'w')
1117 f.write('Hello, ${name}! You are ${what}.')
1120 print file('templates/config').read()
1121 class CTestHandler(ConfigWriter):
1122 _config_writer_files = 'config'
1124 self._config_build_templates()
1125 def _get_config_vars(self, config_file):
1126 return dict(name='you', what='a parrot')
1130 print file('config').read()
1132 os.unlink('templates/config')
1133 os.rmdir('templates')