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', '-o', 'link'), stdout=subprocess.PIPE,
159 string = p.stdout.read()
162 devices = string.splitlines()
165 if dev.find('link/ether') != -1:
166 i = dev.find('link/ether')
167 mac = dev[i+11 : i+11+17]
171 elif dev.find('link/ppp') != -1:
172 i = dev.find('link/ppp')
173 mac = '00:00:00:00:00:00'
179 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
180 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
183 if not isinstance(command, basestring):
184 command = ' '.join(command)
185 print 'Executing command:', command
188 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
189 universal_newlines=universal_newlines,
190 close_fds=close_fds, **kw)
192 raise ExecutionError(command, e)
194 raise ExecutionError(command, ReturnNot0Error(r))
197 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
199 This is a helper class to inherit from to automatically handle data
200 persistence using pickle.
202 The variables attributes to persist (attrs), and the pickle directory (dir)
203 and file extension (ext) can be defined by calling the constructor or in a
204 more declarative way as class attributes, like:
206 class TestHandler(Persistent):
207 _persistent_attrs = ('some_attr', 'other_attr')
208 _persistent_dir = 'persistent-data'
209 _persistent_ext = '.pickle'
211 The default dir is '.' and the default extension is '.pkl'. There are no
212 default variables, and they should be specified as string if a single
213 attribute should be persistent or as a tuple of strings if they are more.
214 The strings should be the attribute names to be persisted. For each
215 attribute a separated pickle file is generated in the pickle directory.
217 You can call _dump() and _load() to write and read the data respectively.
219 # TODO implement it using metaclasses to add the handlers method by demand
220 # (only for specifieds commands).
222 _persistent_attrs = ()
223 _persistent_dir = '.'
224 _persistent_ext = '.pkl'
226 def __init__(self, attrs=None, dir=None, ext=None):
227 r"Initialize the object, see the class documentation for details."
228 if attrs is not None:
229 self._persistent_attrs = attrs
231 self._persistent_dir = dir
233 self._persistent_ext = ext
236 r"_dump() -> None :: Dump all persistent data to pickle files."
237 if isinstance(self._persistent_attrs, basestring):
238 self._persistent_attrs = (self._persistent_attrs,)
239 for attrname in self._persistent_attrs:
240 self._dump_attr(attrname)
243 r"_load() -> None :: Load all persistent data from pickle files."
244 if isinstance(self._persistent_attrs, basestring):
245 self._persistent_attrs = (self._persistent_attrs,)
246 for attrname in self._persistent_attrs:
247 self._load_attr(attrname)
249 def _dump_attr(self, attrname):
250 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
251 f = file(self._pickle_filename(attrname), 'wb')
252 pickle.dump(getattr(self, attrname), f, 2)
255 def _load_attr(self, attrname):
256 r"_load_attr() -> object :: Load a specific pickle file."
257 f = file(self._pickle_filename(attrname))
258 setattr(self, attrname, pickle.load(f))
261 def _pickle_filename(self, name):
262 r"_pickle_filename() -> string :: Construct a pickle filename."
263 return path.join(self._persistent_dir, name) + self._persistent_ext
265 class Restorable(Persistent):
266 r"""Restorable([defaults]) -> Restorable.
268 This is a helper class to inherit from that provides a nice _restore()
269 method to restore the persistent data if any, or load some nice defaults
272 The defaults can be defined by calling the constructor or in a more
273 declarative way as class attributes, like:
275 class TestHandler(Restorable):
276 _persistent_attrs = ('some_attr', 'other_attr')
277 _restorable_defaults = dict(
278 some_attr = 'some_default',
279 other_attr = 'other_default')
281 The defaults is a dictionary, very coupled with the _persistent_attrs
282 attribute inherited from Persistent. The defaults keys should be the
283 values from _persistent_attrs, and the values the default values.
285 The _restore() method returns True if the data was restored successfully
286 or False if the defaults were loaded (in case you want to take further
287 actions). If a _write_config method if found, it's executed when a restore
290 # TODO implement it using metaclasses to add the handlers method by demand
291 # (only for specifieds commands).
293 _restorable_defaults = dict()
295 def __init__(self, defaults=None):
296 r"Initialize the object, see the class documentation for details."
297 if defaults is not None:
298 self._restorable_defaults = defaults
301 r"_restore() -> bool :: Restore persistent data or create a default."
306 for (k, v) in self._restorable_defaults.items():
308 # TODO tener en cuenta servicios que hay que levantar y los que no
309 if hasattr(self, 'commit'):
313 if hasattr(self, '_write_config'):
318 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
320 This is a helper class to inherit from to automatically handle
321 configuration generation. Mako template system is used for configuration
324 The configuration filenames, the generated configuration files directory
325 and the templates directory can be defined by calling the constructor or
326 in a more declarative way as class attributes, like:
328 class TestHandler(ConfigWriter):
329 _config_writer_files = ('base.conf', 'custom.conf')
330 _config_writer_cfg_dir = {
331 'base.conf': '/etc/service',
332 'custom.conf': '/etc/service/conf.d',
334 _config_writer_tpl_dir = 'templates'
336 The generated configuration files directory defaults to '.' and the
337 templates directory to 'templates'. _config_writer_files has no default and
338 must be specified in either way. It can be string or a tuple if more than
339 one configuration file must be generated. _config_writer_cfg_dir could be a
340 dict mapping which file should be stored in which directory, or a single
341 string if all the config files should go to the same directory.
343 The template filename and the generated configuration filename are both the
344 same (so if you want to generate some /etc/config, you should have some
345 templates/config template). That's why _config_writer_cfg_dir and
346 _config_writer_tpl_dir can't be the same. This is not true for very
347 specific cases where _write_single_config() is used.
349 When you write your Handler, you should call _config_build_templates() in
350 you Handler constructor to build the templates.
352 To write the configuration files, you must use the _write_config() method.
353 To know what variables to replace in the template, you have to provide a
354 method called _get_config_vars(tamplate_name), which should return a
355 dictionary of variables to pass to the template system to be replaced in
356 the template for the configuration file 'config_file'.
358 # TODO implement it using metaclasses to add the handlers method by demand
359 # (only for specifieds commands).
361 _config_writer_files = ()
362 _config_writer_cfg_dir = '.'
363 _config_writer_tpl_dir = 'templates'
365 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
366 r"Initialize the object, see the class documentation for details."
367 if files is not None:
368 self._config_writer_files = files
369 if cfg_dir is not None:
370 self._config_writer_cfg_dir = cfg_dir
371 if tpl_dir is not None:
372 self._config_writer_tpl_dir = tpl_dir
373 self._config_build_templates()
375 def _config_build_templates(self):
376 r"_config_writer_templates() -> None :: Build the template objects."
377 if isinstance(self._config_writer_files, basestring):
378 self._config_writer_files = (self._config_writer_files,)
379 if not hasattr(self, '_config_writer_templates') \
380 or not self._config_writer_templates:
381 self._config_writer_templates = dict()
382 for t in self._config_writer_files:
383 f = path.join(self._config_writer_tpl_dir, t)
384 self._config_writer_templates[t] = Template(filename=f)
386 def _render_config(self, template_name, vars=None):
387 r"""_render_config(template_name[, config_filename[, vars]]).
389 Render a single config file using the template 'template_name'. If
390 vars is specified, it's used as the dictionary with the variables
391 to replace in the templates, if not, it looks for a
392 _get_config_vars() method to get it.
395 if hasattr(self, '_get_config_vars'):
396 vars = self._get_config_vars(template_name)
400 vars = vars(template_name)
401 return self._config_writer_templates[template_name].render(**vars)
403 def _get_config_path(self, template_name, config_filename=None):
404 r"Get a complete configuration path."
405 if not config_filename:
406 config_filename = template_name
407 if isinstance(self._config_writer_cfg_dir, basestring):
408 return path.join(self._config_writer_cfg_dir, config_filename)
409 return path.join(self._config_writer_cfg_dir[template_name],
412 def _write_single_config(self, template_name, config_filename=None, vars=None):
413 r"""_write_single_config(template_name[, config_filename[, vars]]).
415 Write a single config file using the template 'template_name'. If no
416 config_filename is specified, the config filename will be the same as
417 the 'template_name' (but stored in the generated config files
418 directory). If it's specified, the generated config file is stored in
419 the file called 'config_filename' (also in the generated files
420 directory). If vars is specified, it's used as the dictionary with the
421 variables to replace in the templates, if not, it looks for a
422 _get_config_vars() method to get it.
425 if hasattr(self, '_get_config_vars'):
426 vars = self._get_config_vars(template_name)
430 vars = vars(template_name)
431 f = file(self._get_config_path(template_name, config_filename), 'w')
432 ctx = Context(f, **vars)
433 self._config_writer_templates[template_name].render_context(ctx)
436 def _write_config(self):
437 r"_write_config() -> None :: Generate all the configuration files."
438 for t in self._config_writer_files:
439 self._write_single_config(t)
442 class ServiceHandler(Handler, Restorable):
443 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
445 This is a helper class to inherit from to automatically handle services
446 with start, stop, restart, reload actions.
448 The actions can be defined by calling the constructor with all the
449 parameters or in a more declarative way as class attributes, like:
451 class TestHandler(ServiceHandler):
452 _service_start = ('command', 'start')
453 _service_stop = ('command', 'stop')
454 _service_restart = ('command', 'restart')
455 _service_reload = 'reload-command'
457 Commands are executed without using the shell, that's why they are specified
458 as tuples (where the first element is the command and the others are the
459 command arguments). If only a command is needed (without arguments) a single
460 string can be specified.
462 All commands must be specified.
464 # TODO implement it using metaclasses to add the handlers method by demand
465 # (only for specifieds commands).
467 def __init__(self, start=None, stop=None, restart=None, reload=None):
468 r"Initialize the object, see the class documentation for details."
469 for (name, action) in dict(start=start, stop=stop, restart=restart,
470 reload=reload).items():
471 if action is not None:
472 setattr(self, '_service_%s' % name, action)
473 self._persistent_attrs = list(self._persistent_attrs)
474 self._persistent_attrs.append('_service_running')
475 if '_service_running' not in self._restorable_defaults:
476 self._restorable_defaults['_service_running'] = False
478 if self._service_running:
479 self._service_running = False
482 @handler(u'Start the service.')
484 r"start() -> None :: Start the service."
485 if not self._service_running:
486 if callable(self._service_start):
487 self._service_start()
489 call(self._service_start)
490 self._service_running = True
491 self._dump_attr('_service_running')
493 @handler(u'Stop the service.')
495 r"stop() -> None :: Stop the service."
496 if self._service_running:
497 if callable(self._service_stop):
500 call(self._service_stop)
501 self._service_running = False
502 self._dump_attr('_service_running')
504 @handler(u'Restart the service.')
506 r"restart() -> None :: Restart the service."
507 if callable(self._service_restart):
508 self._service_restart()
510 call(self._service_restart)
511 self._service_running = True
512 self._dump_attr('_service_running')
514 @handler(u'Reload the service config (without restarting, if possible).')
516 r"reload() -> None :: Reload the configuration of the service."
517 if self._service_running:
518 if callable(self._service_reload):
519 self._service_reload()
521 call(self._service_reload)
523 @handler(u'Tell if the service is running.')
525 r"reload() -> None :: Reload the configuration of the service."
526 if self._service_running:
531 class RestartHandler(Handler):
532 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
534 This is a helper class to inherit from to automatically add a restart
535 command that first stop the service and then starts it again (using start
536 and stop commands respectively).
539 @handler(u'Restart the service (alias to stop + start).')
541 r"restart() -> None :: Restart the service calling stop() and start()."
545 class ReloadHandler(Handler):
546 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
548 This is a helper class to inherit from to automatically add a reload
549 command that calls restart.
552 @handler(u'Reload the service config (alias to restart).')
554 r"reload() -> None :: Reload the configuration of the service."
555 if hasattr(self, '_service_running') and self._service_running:
558 class InitdHandler(ServiceHandler):
559 # TODO update docs, declarative style is depracated
560 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
562 This is a helper class to inherit from to automatically handle services
563 with start, stop, restart, reload actions using a /etc/init.d like script.
565 The name and directory of the script can be defined by calling the
566 constructor or in a more declarative way as class attributes, like:
568 class TestHandler(ServiceHandler):
569 _initd_name = 'some-service'
570 _initd_dir = '/usr/local/etc/init.d'
572 The default _initd_dir is '/etc/init.d', _initd_name has no default and
573 must be specified in either way.
575 Commands are executed without using the shell.
577 # TODO implement it using metaclasses to add the handlers method by demand
578 # (only for specifieds commands).
580 _initd_dir = '/etc/init.d'
582 def __init__(self, initd_name=None, initd_dir=None):
583 r"Initialize the object, see the class documentation for details."
584 if initd_name is not None:
585 self._initd_name = initd_name
586 if initd_dir is not None:
587 self._initd_dir = initd_dir
589 for action in ('start', 'stop', 'restart', 'reload'):
590 actions[action] = (path.join(self._initd_dir, self._initd_name),
592 ServiceHandler.__init__(self, **actions)
594 class TransactionalHandler(Handler):
595 r"""Handle command transactions providing a commit and rollback commands.
597 This is a helper class to inherit from to automatically handle
598 transactional handlers, which have commit and rollback commands.
600 The handler should provide a reload() method (see ServiceHandler and
601 InitdHandler for helper classes to provide this) which will be called
602 when a commit command is issued (if a reload() command is present).
603 The persistent data will be written too (if a _dump() method is provided,
604 see Persistent and Restorable for that), and the configuration files
605 will be generated (if a _write_config method is present, see ConfigWriter).
607 # TODO implement it using metaclasses to add the handlers method by demand
608 # (only for specifieds commands).
610 @handler(u'Commit the changes (reloading the service, if necessary).')
612 r"commit() -> None :: Commit the changes and reload the service."
613 if hasattr(self, '_dump'):
616 if hasattr(self, '_write_config'):
617 unchanged = self._write_config()
618 if not unchanged and hasattr(self, 'reload'):
621 @handler(u'Discard all the uncommited changes.')
623 r"rollback() -> None :: Discard the changes not yet commited."
624 if hasattr(self, '_load'):
627 class ParametersHandler(Handler):
628 r"""ParametersHandler([attr]) -> ParametersHandler.
630 This is a helper class to inherit from to automatically handle
631 service parameters, providing set, get, list and show commands.
633 The attribute that holds the parameters can be defined by calling the
634 constructor or in a more declarative way as class attributes, like:
636 class TestHandler(ServiceHandler):
637 _parameters_attr = 'some_attr'
639 The default is 'params' and it should be a dictionary.
641 # TODO implement it using metaclasses to add the handlers method by demand
642 # (only for specifieds commands).
644 _parameters_attr = 'params'
646 def __init__(self, attr=None):
647 r"Initialize the object, see the class documentation for details."
649 self._parameters_attr = attr
651 @handler(u'Set a service parameter.')
652 def set(self, param, value):
653 r"set(param, value) -> None :: Set a service parameter."
654 if not param in self.params:
655 raise ParameterNotFoundError(param)
656 self.params[param] = value
657 if hasattr(self, '_update'):
660 @handler(u'Get a service parameter.')
661 def get(self, param):
662 r"get(param) -> None :: Get a service parameter."
663 if not param in self.params:
664 raise ParameterNotFoundError(param)
665 return self.params[param]
667 @handler(u'List all available service parameters.')
669 r"list() -> tuple :: List all the parameter names."
670 return self.params.keys()
672 @handler(u'Get all service parameters, with their values.')
674 r"show() -> (key, value) tuples :: List all the parameters."
675 return self.params.items()
677 class SubHandler(Handler):
678 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
680 This is a helper class to build sub handlers that needs to reference the
683 parent - Parent Handler object.
686 def __init__(self, parent):
687 r"Initialize the object, see the class documentation for details."
690 class ContainerSubHandler(SubHandler):
691 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
693 This is a helper class to implement ListSubHandler and DictSubHandler. You
694 should not use it directly.
696 The container attribute to handle and the class of objects that it
697 contains can be defined by calling the constructor or in a more declarative
698 way as class attributes, like:
700 class TestHandler(ContainerSubHandler):
701 _cont_subhandler_attr = 'some_cont'
702 _cont_subhandler_class = SomeClass
704 This way, the parent's some_cont attribute (self.parent.some_cont)
705 will be managed automatically, providing the commands: add, update,
706 delete, get and show. New items will be instances of SomeClass,
707 which should provide a cmp operator to see if the item is on the
708 container and an update() method, if it should be possible to modify
709 it. If SomeClass has an _add, _update or _delete attribute, it set
710 them to true when the item is added, updated or deleted respectively
711 (in case that it's deleted, it's not removed from the container,
712 but it's not listed either).
715 def __init__(self, parent, attr=None, cls=None):
716 r"Initialize the object, see the class documentation for details."
719 self._cont_subhandler_attr = attr
721 self._cont_subhandler_class = cls
723 def _attr(self, attr=None):
725 return getattr(self.parent, self._cont_subhandler_attr)
726 setattr(self.parent, self._cont_subhandler_attr, attr)
729 if isinstance(self._attr(), dict):
730 return dict([(k, i) for (k, i) in self._attr().items()
731 if not hasattr(i, '_delete') or not i._delete])
732 return [i for i in self._attr()
733 if not hasattr(i, '_delete') or not i._delete]
735 @handler(u'Add a new item')
736 def add(self, *args, **kwargs):
737 r"add(...) -> None :: Add an item to the list."
738 item = self._cont_subhandler_class(*args, **kwargs)
739 if hasattr(item, '_add'):
742 if isinstance(self._attr(), dict):
743 key = item.as_tuple()[0]
744 # do we have the same item? then raise an error
745 if key in self._vattr():
746 raise ItemAlreadyExistsError(item)
747 # do we have the same item, but logically deleted? then update flags
748 if key in self._attr():
750 if not isinstance(self._attr(), dict):
751 index = self._attr().index(item)
752 if hasattr(item, '_add'):
753 self._attr()[index]._add = False
754 if hasattr(item, '_delete'):
755 self._attr()[index]._delete = False
756 else: # it's *really* new
757 if isinstance(self._attr(), dict):
758 self._attr()[key] = item
760 self._attr().append(item)
762 @handler(u'Update an item')
763 def update(self, index, *args, **kwargs):
764 r"update(index, ...) -> None :: Update an item of the container."
765 # TODO make it right with metaclasses, so the method is not created
766 # unless the update() method really exists.
767 # TODO check if the modified item is the same of an existing one
768 if not isinstance(self._attr(), dict):
769 index = int(index) # TODO validation
770 if not hasattr(self._cont_subhandler_class, 'update'):
771 raise CommandNotFoundError(('update',))
773 item = self._vattr()[index]
774 item.update(*args, **kwargs)
775 if hasattr(item, '_update'):
778 raise ItemNotFoundError(index)
780 @handler(u'Delete an item')
781 def delete(self, index):
782 r"delete(index) -> None :: Delete an item of the container."
783 if not isinstance(self._attr(), dict):
784 index = int(index) # TODO validation
786 item = self._vattr()[index]
787 if hasattr(item, '_delete'):
790 del self._attr()[index]
793 raise ItemNotFoundError(index)
795 @handler(u'Remove all items (use with care).')
797 r"clear() -> None :: Delete all items of the container."
798 if isinstance(self._attr(), dict):
803 @handler(u'Get information about an item')
804 def get(self, index):
805 r"get(index) -> item :: List all the information of an item."
806 if not isinstance(self._attr(), dict):
807 index = int(index) # TODO validation
809 return self._vattr()[index]
811 raise ItemNotFoundError(index)
813 @handler(u'Get information about all items')
815 r"show() -> list of items :: List all the complete items information."
816 if isinstance(self._attr(), dict):
817 return self._attr().values()
820 class ListSubHandler(ContainerSubHandler):
821 r"""ListSubHandler(parent) -> ListSubHandler instance.
823 ContainerSubHandler holding lists. See ComposedSubHandler documentation
827 @handler(u'Get how many items are in the list')
829 r"len() -> int :: Get how many items are in the list."
830 return len(self._vattr())
832 class DictSubHandler(ContainerSubHandler):
833 r"""DictSubHandler(parent) -> DictSubHandler instance.
835 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
839 @handler(u'List all the items by key')
841 r"list() -> tuple :: List all the item keys."
842 return self._attr().keys()
844 class ComposedSubHandler(SubHandler):
845 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
847 This is a helper class to implement ListComposedSubHandler and
848 DictComposedSubHandler. You should not use it directly.
850 This class is usefull when you have a parent that has a dict (cont)
851 that stores some object that has an attribute (attr) with a list or
852 a dict of objects of some class. In that case, this class provides
853 automated commands to add, update, delete, get and show that objects.
854 This commands takes the cont (key of the dict for the object holding
855 the attr), and an index for access the object itself (in the attr
858 The container object (cont) that holds a containers, the attribute of
859 that object that is the container itself, and the class of the objects
860 that it contains can be defined by calling the constructor or in a
861 more declarative way as class attributes, like:
863 class TestHandler(ComposedSubHandler):
864 _comp_subhandler_cont = 'some_cont'
865 _comp_subhandler_attr = 'some_attr'
866 _comp_subhandler_class = SomeClass
868 This way, the parent's some_cont attribute (self.parent.some_cont)
869 will be managed automatically, providing the commands: add, update,
870 delete, get and show for manipulating a particular instance that holds
871 of SomeClass. For example, updating an item at the index 5 is the same
872 (simplified) as doing parent.some_cont[cont][5].update().
873 SomeClass should provide a cmp operator to see if the item is on the
874 container and an update() method, if it should be possible to modify
875 it. If SomeClass has an _add, _update or _delete attribute, it set
876 them to true when the item is added, updated or deleted respectively
877 (in case that it's deleted, it's not removed from the container,
878 but it's not listed either). If the container objects
879 (parent.some_cont[cont]) has an _update attribute, it's set to True
880 when any add, update or delete command is executed.
883 def __init__(self, parent, cont=None, attr=None, cls=None):
884 r"Initialize the object, see the class documentation for details."
887 self._comp_subhandler_cont = cont
889 self._comp_subhandler_attr = attr
891 self._comp_subhandler_class = cls
894 return getattr(self.parent, self._comp_subhandler_cont)
896 def _attr(self, cont, attr=None):
898 return getattr(self._cont()[cont], self._comp_subhandler_attr)
899 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
901 def _vattr(self, cont):
902 if isinstance(self._attr(cont), dict):
903 return dict([(k, i) for (k, i) in self._attr(cont).items()
904 if not hasattr(i, '_delete') or not i._delete])
905 return [i for i in self._attr(cont)
906 if not hasattr(i, '_delete') or not i._delete]
908 @handler(u'Add a new item')
909 def add(self, cont, *args, **kwargs):
910 r"add(cont, ...) -> None :: Add an item to the list."
911 if not cont in self._cont():
912 raise ContainerNotFoundError(cont)
913 item = self._comp_subhandler_class(*args, **kwargs)
914 if hasattr(item, '_add'):
917 if isinstance(self._attr(cont), dict):
918 key = item.as_tuple()[0]
919 # do we have the same item? then raise an error
920 if key in self._vattr(cont):
921 raise ItemAlreadyExistsError(item)
922 # do we have the same item, but logically deleted? then update flags
923 if key in self._attr(cont):
925 if not isinstance(self._attr(cont), dict):
926 index = self._attr(cont).index(item)
927 if hasattr(item, '_add'):
928 self._attr(cont)[index]._add = False
929 if hasattr(item, '_delete'):
930 self._attr(cont)[index]._delete = False
931 else: # it's *really* new
932 if isinstance(self._attr(cont), dict):
933 self._attr(cont)[key] = item
935 self._attr(cont).append(item)
936 if hasattr(self._cont()[cont], '_update'):
937 self._cont()[cont]._update = True
939 @handler(u'Update an item')
940 def update(self, cont, index, *args, **kwargs):
941 r"update(cont, index, ...) -> None :: Update an item of the container."
942 # TODO make it right with metaclasses, so the method is not created
943 # unless the update() method really exists.
944 # TODO check if the modified item is the same of an existing one
945 if not cont in self._cont():
946 raise ContainerNotFoundError(cont)
947 if not isinstance(self._attr(cont), dict):
948 index = int(index) # TODO validation
949 if not hasattr(self._comp_subhandler_class, 'update'):
950 raise CommandNotFoundError(('update',))
952 item = self._vattr(cont)[index]
953 item.update(*args, **kwargs)
954 if hasattr(item, '_update'):
956 if hasattr(self._cont()[cont], '_update'):
957 self._cont()[cont]._update = True
959 raise ItemNotFoundError(index)
961 @handler(u'Delete an item')
962 def delete(self, cont, index):
963 r"delete(cont, index) -> None :: Delete an item of the container."
964 if not cont in self._cont():
965 raise ContainerNotFoundError(cont)
966 if not isinstance(self._attr(cont), dict):
967 index = int(index) # TODO validation
969 item = self._vattr(cont)[index]
970 if hasattr(item, '_delete'):
973 del self._attr(cont)[index]
974 if hasattr(self._cont()[cont], '_update'):
975 self._cont()[cont]._update = True
978 raise ItemNotFoundError(index)
980 @handler(u'Remove all items (use with care).')
981 def clear(self, cont):
982 r"clear(cont) -> None :: Delete all items of the container."
983 if not cont in self._cont():
984 raise ContainerNotFoundError(cont)
985 if isinstance(self._attr(cont), dict):
986 self._attr(cont).clear()
988 self._attr(cont, list())
990 @handler(u'Get information about an item')
991 def get(self, cont, index):
992 r"get(cont, index) -> item :: List all the information of an item."
993 if not cont in self._cont():
994 raise ContainerNotFoundError(cont)
995 if not isinstance(self._attr(cont), dict):
996 index = int(index) # TODO validation
998 return self._vattr(cont)[index]
1000 raise ItemNotFoundError(index)
1002 @handler(u'Get information about all items')
1003 def show(self, cont):
1004 r"show(cont) -> list of items :: List all the complete items information."
1005 if not cont in self._cont():
1006 raise ContainerNotFoundError(cont)
1007 if isinstance(self._attr(cont), dict):
1008 return self._attr(cont).values()
1009 return self._vattr(cont)
1011 class ListComposedSubHandler(ComposedSubHandler):
1012 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1014 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1018 @handler(u'Get how many items are in the list')
1019 def len(self, cont):
1020 r"len(cont) -> int :: Get how many items are in the list."
1021 if not cont in self._cont():
1022 raise ContainerNotFoundError(cont)
1023 return len(self._vattr(cont))
1025 class DictComposedSubHandler(ComposedSubHandler):
1026 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1028 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1032 @handler(u'List all the items by key')
1033 def list(self, cont):
1034 r"list(cont) -> tuple :: List all the item keys."
1035 if not cont in self._cont():
1036 raise ContainerNotFoundError(cont)
1037 return self._attr(cont).keys()
1040 if __name__ == '__main__':
1043 class STestHandler1(ServiceHandler):
1044 _service_start = ('service', 'start')
1045 _service_stop = ('service', 'stop')
1046 _service_restart = ('ls', '/')
1047 _service_reload = ('cp', '/la')
1048 class STestHandler2(ServiceHandler):
1050 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1051 'cmd-restart', 'cmd-reload')
1052 class ITestHandler1(InitdHandler):
1053 _initd_name = 'test1'
1054 class ITestHandler2(InitdHandler):
1056 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1064 print h.__class__.__name__
1067 except ExecutionError, e:
1071 except ExecutionError, e:
1075 except ExecutionError, e:
1079 except ExecutionError, e:
1084 print 'PTestHandler'
1085 class PTestHandler(Persistent):
1086 _persistent_attrs = 'vars'
1088 self.vars = dict(a=1, b=2)
1106 print 'RTestHandler'
1107 class RTestHandler(Restorable):
1108 _persistent_attrs = 'vars'
1109 _restorable_defaults = dict(vars=dict(a=1, b=2))
1121 print 'CTestHandler'
1123 os.mkdir('templates')
1124 f = file('templates/config', 'w')
1125 f.write('Hello, ${name}! You are ${what}.')
1128 print file('templates/config').read()
1129 class CTestHandler(ConfigWriter):
1130 _config_writer_files = 'config'
1132 self._config_build_templates()
1133 def _get_config_vars(self, config_file):
1134 return dict(name='you', what='a parrot')
1138 print file('config').read()
1140 os.unlink('templates/config')
1141 os.rmdir('templates')