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 print 'Executing command:', command
181 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
182 universal_newlines=universal_newlines,
183 close_fds=close_fds, **kw)
185 raise ExecutionError(command, e)
187 raise ExecutionError(command, ReturnNot0Error(r))
190 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
192 This is a helper class to inherit from to automatically handle data
193 persistence using pickle.
195 The variables attributes to persist (attrs), and the pickle directory (dir)
196 and file extension (ext) can be defined by calling the constructor or in a
197 more declarative way as class attributes, like:
199 class TestHandler(Persistent):
200 _persistent_attrs = ('some_attr', 'other_attr')
201 _persistent_dir = 'persistent-data'
202 _persistent_ext = '.pickle'
204 The default dir is '.' and the default extension is '.pkl'. There are no
205 default variables, and they should be specified as string if a single
206 attribute should be persistent or as a tuple of strings if they are more.
207 The strings should be the attribute names to be persisted. For each
208 attribute a separated pickle file is generated in the pickle directory.
210 You can call _dump() and _load() to write and read the data respectively.
212 # TODO implement it using metaclasses to add the handlers method by demand
213 # (only for specifieds commands).
215 _persistent_attrs = ()
216 _persistent_dir = '.'
217 _persistent_ext = '.pkl'
219 def __init__(self, attrs=None, dir=None, ext=None):
220 r"Initialize the object, see the class documentation for details."
221 if attrs is not None:
222 self._persistent_attrs = attrs
224 self._persistent_dir = dir
226 self._persistent_ext = ext
229 r"_dump() -> None :: Dump all persistent data to pickle files."
230 if isinstance(self._persistent_attrs, basestring):
231 self._persistent_attrs = (self._persistent_attrs,)
232 for attrname in self._persistent_attrs:
233 self._dump_attr(attrname)
236 r"_load() -> None :: Load all persistent data from 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._load_attr(attrname)
242 def _dump_attr(self, attrname):
243 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
244 f = file(self._pickle_filename(attrname), 'wb')
245 pickle.dump(getattr(self, attrname), f, 2)
248 def _load_attr(self, attrname):
249 r"_load_attr() -> object :: Load a specific pickle file."
250 f = file(self._pickle_filename(attrname))
251 setattr(self, attrname, pickle.load(f))
254 def _pickle_filename(self, name):
255 r"_pickle_filename() -> string :: Construct a pickle filename."
256 return path.join(self._persistent_dir, name) + self._persistent_ext
258 class Restorable(Persistent):
259 r"""Restorable([defaults]) -> Restorable.
261 This is a helper class to inherit from that provides a nice _restore()
262 method to restore the persistent data if any, or load some nice defaults
265 The defaults can be defined by calling the constructor or in a more
266 declarative way as class attributes, like:
268 class TestHandler(Restorable):
269 _persistent_attrs = ('some_attr', 'other_attr')
270 _restorable_defaults = dict(
271 some_attr = 'some_default',
272 other_attr = 'other_default')
274 The defaults is a dictionary, very coupled with the _persistent_attrs
275 attribute inherited from Persistent. The defaults keys should be the
276 values from _persistent_attrs, and the values the default values.
278 The _restore() method returns True if the data was restored successfully
279 or False if the defaults were loaded (in case you want to take further
280 actions). If a _write_config method if found, it's executed when a restore
283 # TODO implement it using metaclasses to add the handlers method by demand
284 # (only for specifieds commands).
286 _restorable_defaults = dict()
288 def __init__(self, defaults=None):
289 r"Initialize the object, see the class documentation for details."
290 if defaults is not None:
291 self._restorable_defaults = defaults
294 r"_restore() -> bool :: Restore persistent data or create a default."
297 # TODO tener en cuenta servicios que hay que levantar y los que no
298 if hasattr(self, 'commit'): # TODO deberia ser reload y/o algo para comandos
302 for (k, v) in self._restorable_defaults.items():
304 # TODO tener en cuenta servicios que hay que levantar y los que no
305 if hasattr(self, 'commit'):
309 if hasattr(self, '_write_config'):
311 if hasattr(self, 'reload'):
316 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
318 This is a helper class to inherit from to automatically handle
319 configuration generation. Mako template system is used for configuration
322 The configuration filenames, the generated configuration files directory
323 and the templates directory can be defined by calling the constructor or
324 in a more declarative way as class attributes, like:
326 class TestHandler(ConfigWriter):
327 _config_writer_files = ('base.conf', 'custom.conf')
328 _config_writer_cfg_dir = {
329 'base.conf': '/etc/service',
330 'custom.conf': '/etc/service/conf.d',
332 _config_writer_tpl_dir = 'templates'
334 The generated configuration files directory defaults to '.' and the
335 templates directory to 'templates'. _config_writer_files has no default and
336 must be specified in either way. It can be string or a tuple if more than
337 one configuration file must be generated. _config_writer_cfg_dir could be a
338 dict mapping which file should be stored in which directory, or a single
339 string if all the config files should go to the same directory.
341 The template filename and the generated configuration filename are both the
342 same (so if you want to generate some /etc/config, you should have some
343 templates/config template). That's why _config_writer_cfg_dir and
344 _config_writer_tpl_dir can't be the same. This is not true for very
345 specific cases where _write_single_config() is used.
347 When you write your Handler, you should call _config_build_templates() in
348 you Handler constructor to build the templates.
350 To write the configuration files, you must use the _write_config() method.
351 To know what variables to replace in the template, you have to provide a
352 method called _get_config_vars(tamplate_name), which should return a
353 dictionary of variables to pass to the template system to be replaced in
354 the template for the configuration file 'config_file'.
356 # TODO implement it using metaclasses to add the handlers method by demand
357 # (only for specifieds commands).
359 _config_writer_files = ()
360 _config_writer_cfg_dir = '.'
361 _config_writer_tpl_dir = 'templates'
363 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
364 r"Initialize the object, see the class documentation for details."
365 if files is not None:
366 self._config_writer_files = files
367 if cfg_dir is not None:
368 self._config_writer_cfg_dir = cfg_dir
369 if tpl_dir is not None:
370 self._config_writer_tpl_dir = tpl_dir
371 self._config_build_templates()
373 def _config_build_templates(self):
374 r"_config_writer_templates() -> None :: Build the template objects."
375 if isinstance(self._config_writer_files, basestring):
376 self._config_writer_files = (self._config_writer_files,)
377 if not hasattr(self, '_config_writer_templates') \
378 or not self._config_writer_templates:
379 self._config_writer_templates = dict()
380 for t in self._config_writer_files:
381 f = path.join(self._config_writer_tpl_dir, t)
382 self._config_writer_templates[t] = Template(filename=f)
384 def _render_config(self, template_name, vars=None):
385 r"""_render_config(template_name[, config_filename[, vars]]).
387 Render a single config file using the template 'template_name'. If
388 vars is specified, it's used as the dictionary with the variables
389 to replace in the templates, if not, it looks for a
390 _get_config_vars() method to get it.
393 if hasattr(self, '_get_config_vars'):
394 vars = self._get_config_vars(template_name)
398 vars = vars(template_name)
399 return self._config_writer_templates[template_name].render(**vars)
401 def _get_config_path(self, template_name, config_filename=None):
402 r"Get a complete configuration path."
403 if not config_filename:
404 config_filename = template_name
405 if isinstance(self._config_writer_cfg_dir, basestring):
406 return path.join(self._config_writer_cfg_dir, config_filename)
407 return path.join(self._config_writer_cfg_dir[template_name],
410 def _write_single_config(self, template_name, config_filename=None, vars=None):
411 r"""_write_single_config(template_name[, config_filename[, vars]]).
413 Write a single config file using the template 'template_name'. If no
414 config_filename is specified, the config filename will be the same as
415 the 'template_name' (but stored in the generated config files
416 directory). If it's specified, the generated config file is stored in
417 the file called 'config_filename' (also in the generated files
418 directory). If vars is specified, it's used as the dictionary with the
419 variables to replace in the templates, if not, it looks for a
420 _get_config_vars() method to get it.
423 if hasattr(self, '_get_config_vars'):
424 vars = self._get_config_vars(template_name)
428 vars = vars(template_name)
429 f = file(self._get_config_path(template_name, config_filename), 'w')
430 ctx = Context(f, **vars)
431 self._config_writer_templates[template_name].render_context(ctx)
434 def _write_config(self):
435 r"_write_config() -> None :: Generate all the configuration files."
436 for t in self._config_writer_files:
437 self._write_single_config(t)
440 class ServiceHandler(Handler):
441 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
443 This is a helper class to inherit from to automatically handle services
444 with start, stop, restart, reload actions.
446 The actions can be defined by calling the constructor with all the
447 parameters or in a more declarative way as class attributes, like:
449 class TestHandler(ServiceHandler):
450 _service_start = ('command', 'start')
451 _service_stop = ('command', 'stop')
452 _service_restart = ('command', 'restart')
453 _service_reload = 'reload-command'
455 Commands are executed without using the shell, that's why they are specified
456 as tuples (where the first element is the command and the others are the
457 command arguments). If only a command is needed (without arguments) a single
458 string can be specified.
460 All commands must be specified.
462 # TODO implement it using metaclasses to add the handlers method by demand
463 # (only for specifieds commands).
465 def __init__(self, start=None, stop=None, restart=None, reload=None):
466 r"Initialize the object, see the class documentation for details."
467 for (name, action) in dict(start=start, stop=stop, restart=restart,
468 reload=reload).items():
469 if action is not None:
470 setattr(self, '_service_%s' % name, action)
472 @handler(u'Start the service.')
474 r"start() -> None :: Start the service."
475 call(self._service_start)
477 @handler(u'Stop the service.')
479 r"stop() -> None :: Stop the service."
480 call(self._service_stop)
482 @handler(u'Restart the service.')
484 r"restart() -> None :: Restart the service."
485 call(self._service_restart)
487 @handler(u'Reload the service config (without restarting, if possible).')
489 r"reload() -> None :: Reload the configuration of the service."
490 call(self._service_reload)
492 class RestartHandler(Handler):
493 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
495 This is a helper class to inherit from to automatically add a restart
496 command that first stop the service and then starts it again (using start
497 and stop commands respectively).
500 @handler(u'Restart the service (alias to stop + start).')
502 r"restart() -> None :: Restart the service calling stop() and start()."
506 class ReloadHandler(Handler):
507 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
509 This is a helper class to inherit from to automatically add a reload
510 command that calls restart.
513 @handler(u'Reload the service config (alias to restart).')
515 r"reload() -> None :: Reload the configuration of the service."
518 class InitdHandler(Handler):
519 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
521 This is a helper class to inherit from to automatically handle services
522 with start, stop, restart, reload actions using a /etc/init.d like script.
524 The name and directory of the script can be defined by calling the
525 constructor or in a more declarative way as class attributes, like:
527 class TestHandler(ServiceHandler):
528 _initd_name = 'some-service'
529 _initd_dir = '/usr/local/etc/init.d'
531 The default _initd_dir is '/etc/init.d', _initd_name has no default and
532 must be specified in either way.
534 Commands are executed without using the shell.
536 # TODO implement it using metaclasses to add the handlers method by demand
537 # (only for specifieds commands).
539 _initd_dir = '/etc/init.d'
541 def __init__(self, initd_name=None, initd_dir=None):
542 r"Initialize the object, see the class documentation for details."
543 if initd_name is not None:
544 self._initd_name = initd_name
545 if initd_dir is not None:
546 self._initd_dir = initd_dir
548 @handler(u'Start the service.')
550 r"start() -> None :: Start the service."
551 call((path.join(self._initd_dir, self._initd_name), 'start'))
553 @handler(u'Stop the service.')
555 r"stop() -> None :: Stop the service."
556 call((path.join(self._initd_dir, self._initd_name), 'stop'))
558 @handler(u'Restart the service.')
560 r"restart() -> None :: Restart the service."
561 call((path.join(self._initd_dir, self._initd_name), 'restart'))
563 @handler(u'Reload the service config (without restarting, if possible).')
565 r"reload() -> None :: Reload the configuration of the service."
566 call((path.join(self._initd_dir, self._initd_name), 'reload'))
568 class TransactionalHandler(Handler):
569 r"""Handle command transactions providing a commit and rollback commands.
571 This is a helper class to inherit from to automatically handle
572 transactional handlers, which have commit and rollback commands.
574 The handler should provide a reload() method (see ServiceHandler and
575 InitdHandler for helper classes to provide this) which will be called
576 when a commit command is issued (if a reload() command is present).
577 The persistent data will be written too (if a _dump() method is provided,
578 see Persistent and Restorable for that), and the configuration files
579 will be generated (if a _write_config method is present, see ConfigWriter).
581 # TODO implement it using metaclasses to add the handlers method by demand
582 # (only for specifieds commands).
584 @handler(u'Commit the changes (reloading the service, if necessary).')
586 r"commit() -> None :: Commit the changes and reload the service."
587 if hasattr(self, '_dump'):
589 if hasattr(self, '_write_config'):
591 if hasattr(self, 'reload'):
594 @handler(u'Discard all the uncommited changes.')
596 r"rollback() -> None :: Discard the changes not yet commited."
597 if hasattr(self, '_load'):
600 class ParametersHandler(Handler):
601 r"""ParametersHandler([attr]) -> ParametersHandler.
603 This is a helper class to inherit from to automatically handle
604 service parameters, providing set, get, list and show commands.
606 The attribute that holds the parameters can be defined by calling the
607 constructor or in a more declarative way as class attributes, like:
609 class TestHandler(ServiceHandler):
610 _parameters_attr = 'some_attr'
612 The default is 'params' and it should be a dictionary.
614 # TODO implement it using metaclasses to add the handlers method by demand
615 # (only for specifieds commands).
617 _parameters_attr = 'params'
619 def __init__(self, attr=None):
620 r"Initialize the object, see the class documentation for details."
622 self._parameters_attr = attr
624 @handler(u'Set a service parameter.')
625 def set(self, param, value):
626 r"set(param, value) -> None :: Set a service parameter."
627 if not param in self.params:
628 raise ParameterNotFoundError(param)
629 self.params[param] = value
631 @handler(u'Get a service parameter.')
632 def get(self, param):
633 r"get(param) -> None :: Get a service parameter."
634 if not param in self.params:
635 raise ParameterNotFoundError(param)
636 return self.params[param]
638 @handler(u'List all available service parameters.')
640 r"list() -> tuple :: List all the parameter names."
641 return self.params.keys()
643 @handler(u'Get all service parameters, with their values.')
645 r"show() -> (key, value) tuples :: List all the parameters."
646 return self.params.items()
648 class SubHandler(Handler):
649 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
651 This is a helper class to build sub handlers that needs to reference the
654 parent - Parent Handler object.
657 def __init__(self, parent):
658 r"Initialize the object, see the class documentation for details."
661 class ContainerSubHandler(SubHandler):
662 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
664 This is a helper class to implement ListSubHandler and DictSubHandler. You
665 should not use it directly.
667 The container attribute to handle and the class of objects that it
668 contains can be defined by calling the constructor or in a more declarative
669 way as class attributes, like:
671 class TestHandler(ContainerSubHandler):
672 _cont_subhandler_attr = 'some_cont'
673 _cont_subhandler_class = SomeClass
675 This way, the parent's some_cont attribute (self.parent.some_cont)
676 will be managed automatically, providing the commands: add, update,
677 delete, get and show. New items will be instances of SomeClass,
678 which should provide a cmp operator to see if the item is on the
679 container and an update() method, if it should be possible to modify
680 it. If SomeClass has an _add, _update or _delete attribute, it set
681 them to true when the item is added, updated or deleted respectively
682 (in case that it's deleted, it's not removed from the container,
683 but it's not listed either).
686 def __init__(self, parent, attr=None, cls=None):
687 r"Initialize the object, see the class documentation for details."
690 self._cont_subhandler_attr = attr
692 self._cont_subhandler_class = cls
694 def _attr(self, attr=None):
696 return getattr(self.parent, self._cont_subhandler_attr)
697 setattr(self.parent, self._cont_subhandler_attr, attr)
700 if isinstance(self._attr(), dict):
701 return dict([(k, i) for (k, i) in self._attr().items()
702 if not hasattr(i, '_delete') or not i._delete])
703 return [i for i in self._attr()
704 if not hasattr(i, '_delete') or not i._delete]
706 @handler(u'Add a new item')
707 def add(self, *args, **kwargs):
708 r"add(...) -> None :: Add an item to the list."
709 item = self._cont_subhandler_class(*args, **kwargs)
710 if hasattr(item, '_add'):
713 if isinstance(self._attr(), dict):
714 key = item.as_tuple()[0]
715 # do we have the same item? then raise an error
716 if key in self._vattr():
717 raise ItemAlreadyExistsError(item)
718 # do we have the same item, but logically deleted? then update flags
719 if key in self._attr():
721 if not isinstance(self._attr(), dict):
722 index = self._attr().index(item)
723 if hasattr(item, '_add'):
724 self._attr()[index]._add = False
725 if hasattr(item, '_delete'):
726 self._attr()[index]._delete = False
727 else: # it's *really* new
728 if isinstance(self._attr(), dict):
729 self._attr()[key] = item
731 self._attr().append(item)
733 @handler(u'Update an item')
734 def update(self, index, *args, **kwargs):
735 r"update(index, ...) -> None :: Update an item of the container."
736 # TODO make it right with metaclasses, so the method is not created
737 # unless the update() method really exists.
738 # TODO check if the modified item is the same of an existing one
739 if not isinstance(self._attr(), dict):
740 index = int(index) # TODO validation
741 if not hasattr(self._cont_subhandler_class, 'update'):
742 raise CommandNotFoundError(('update',))
744 item = self._vattr()[index]
745 item.update(*args, **kwargs)
746 if hasattr(item, '_update'):
749 raise ItemNotFoundError(index)
751 @handler(u'Delete an item')
752 def delete(self, index):
753 r"delete(index) -> None :: Delete an item of the container."
754 if not isinstance(self._attr(), dict):
755 index = int(index) # TODO validation
757 item = self._vattr()[index]
758 if hasattr(item, '_delete'):
761 del self._attr()[index]
764 raise ItemNotFoundError(index)
766 @handler(u'Remove all items (use with care).')
768 r"clear() -> None :: Delete all items of the container."
769 if isinstance(self._attr(), dict):
774 @handler(u'Get information about an item')
775 def get(self, index):
776 r"get(index) -> item :: List all the information of an item."
777 if not isinstance(self._attr(), dict):
778 index = int(index) # TODO validation
780 return self._vattr()[index]
782 raise ItemNotFoundError(index)
784 @handler(u'Get information about all items')
786 r"show() -> list of items :: List all the complete items information."
787 if isinstance(self._attr(), dict):
788 return self._attr().values()
791 class ListSubHandler(ContainerSubHandler):
792 r"""ListSubHandler(parent) -> ListSubHandler instance.
794 ContainerSubHandler holding lists. See ComposedSubHandler documentation
798 @handler(u'Get how many items are in the list')
800 r"len() -> int :: Get how many items are in the list."
801 return len(self._vattr())
803 class DictSubHandler(ContainerSubHandler):
804 r"""DictSubHandler(parent) -> DictSubHandler instance.
806 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
810 @handler(u'List all the items by key')
812 r"list() -> tuple :: List all the item keys."
813 return self._attr().keys()
815 class ComposedSubHandler(SubHandler):
816 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
818 This is a helper class to implement ListComposedSubHandler and
819 DictComposedSubHandler. You should not use it directly.
821 This class is usefull when you have a parent that has a dict (cont)
822 that stores some object that has an attribute (attr) with a list or
823 a dict of objects of some class. In that case, this class provides
824 automated commands to add, update, delete, get and show that objects.
825 This commands takes the cont (key of the dict for the object holding
826 the attr), and an index for access the object itself (in the attr
829 The container object (cont) that holds a containers, the attribute of
830 that object that is the container itself, and the class of the objects
831 that it contains can be defined by calling the constructor or in a
832 more declarative way as class attributes, like:
834 class TestHandler(ComposedSubHandler):
835 _comp_subhandler_cont = 'some_cont'
836 _comp_subhandler_attr = 'some_attr'
837 _comp_subhandler_class = SomeClass
839 This way, the parent's some_cont attribute (self.parent.some_cont)
840 will be managed automatically, providing the commands: add, update,
841 delete, get and show for manipulating a particular instance that holds
842 of SomeClass. For example, updating an item at the index 5 is the same
843 (simplified) as doing parent.some_cont[cont][5].update().
844 SomeClass should provide a cmp operator to see if the item is on the
845 container and an update() method, if it should be possible to modify
846 it. If SomeClass has an _add, _update or _delete attribute, it set
847 them to true when the item is added, updated or deleted respectively
848 (in case that it's deleted, it's not removed from the container,
849 but it's not listed either). If the container objects
850 (parent.some_cont[cont]) has an _update attribute, it's set to True
851 when any add, update or delete command is executed.
854 def __init__(self, parent, cont=None, attr=None, cls=None):
855 r"Initialize the object, see the class documentation for details."
858 self._comp_subhandler_cont = cont
860 self._comp_subhandler_attr = attr
862 self._comp_subhandler_class = cls
865 return getattr(self.parent, self._comp_subhandler_cont)
867 def _attr(self, cont, attr=None):
869 return getattr(self._cont()[cont], self._comp_subhandler_attr)
870 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
872 def _vattr(self, cont):
873 if isinstance(self._attr(cont), dict):
874 return dict([(k, i) for (k, i) in self._attr(cont).items()
875 if not hasattr(i, '_delete') or not i._delete])
876 return [i for i in self._attr(cont)
877 if not hasattr(i, '_delete') or not i._delete]
879 @handler(u'Add a new item')
880 def add(self, cont, *args, **kwargs):
881 r"add(cont, ...) -> None :: Add an item to the list."
882 if not cont in self._cont():
883 raise ContainerNotFoundError(cont)
884 item = self._comp_subhandler_class(*args, **kwargs)
885 if hasattr(item, '_add'):
888 if isinstance(self._attr(cont), dict):
889 key = item.as_tuple()[0]
890 # do we have the same item? then raise an error
891 if key in self._vattr(cont):
892 raise ItemAlreadyExistsError(item)
893 # do we have the same item, but logically deleted? then update flags
894 if key in self._attr(cont):
896 if not isinstance(self._attr(cont), dict):
897 index = self._attr(cont).index(item)
898 if hasattr(item, '_add'):
899 self._attr(cont)[index]._add = False
900 if hasattr(item, '_delete'):
901 self._attr(cont)[index]._delete = False
902 else: # it's *really* new
903 if isinstance(self._attr(cont), dict):
904 self._attr(cont)[key] = item
906 self._attr(cont).append(item)
907 if hasattr(self._cont()[cont], '_update'):
908 self._cont()[cont]._update = True
910 @handler(u'Update an item')
911 def update(self, cont, index, *args, **kwargs):
912 r"update(cont, index, ...) -> None :: Update an item of the container."
913 # TODO make it right with metaclasses, so the method is not created
914 # unless the update() method really exists.
915 # TODO check if the modified item is the same of an existing one
916 if not cont in self._cont():
917 raise ContainerNotFoundError(cont)
918 if not isinstance(self._attr(cont), dict):
919 index = int(index) # TODO validation
920 if not hasattr(self._comp_subhandler_class, 'update'):
921 raise CommandNotFoundError(('update',))
923 item = self._vattr(cont)[index]
924 item.update(*args, **kwargs)
925 if hasattr(item, '_update'):
927 if hasattr(self._cont()[cont], '_update'):
928 self._cont()[cont]._update = True
930 raise ItemNotFoundError(index)
932 @handler(u'Delete an item')
933 def delete(self, cont, index):
934 r"delete(cont, index) -> None :: Delete an item of the container."
935 if not cont in self._cont():
936 raise ContainerNotFoundError(cont)
937 if not isinstance(self._attr(cont), dict):
938 index = int(index) # TODO validation
940 item = self._vattr(cont)[index]
941 if hasattr(item, '_delete'):
944 del self._attr(cont)[index]
945 if hasattr(self._cont()[cont], '_update'):
946 self._cont()[cont]._update = True
949 raise ItemNotFoundError(index)
951 @handler(u'Remove all items (use with care).')
952 def clear(self, cont):
953 r"clear(cont) -> None :: Delete all items of the container."
954 if not cont in self._cont():
955 raise ContainerNotFoundError(cont)
956 if isinstance(self._attr(cont), dict):
957 self._attr(cont).clear()
959 self._attr(cont, list())
961 @handler(u'Get information about an item')
962 def get(self, cont, index):
963 r"get(cont, index) -> item :: List all the information of an item."
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 return self._vattr(cont)[index]
971 raise ItemNotFoundError(index)
973 @handler(u'Get information about all items')
974 def show(self, cont):
975 r"show(cont) -> list of items :: List all the complete items information."
976 if not cont in self._cont():
977 raise ContainerNotFoundError(cont)
978 if isinstance(self._attr(cont), dict):
979 return self._attr(cont).values()
980 return self._vattr(cont)
982 class ListComposedSubHandler(ComposedSubHandler):
983 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
985 ComposedSubHandler holding lists. See ComposedSubHandler documentation
989 @handler(u'Get how many items are in the list')
991 r"len(cont) -> int :: Get how many items are in the list."
992 if not cont in self._cont():
993 raise ContainerNotFoundError(cont)
994 return len(self._vattr(cont))
996 class DictComposedSubHandler(ComposedSubHandler):
997 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
999 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1003 @handler(u'List all the items by key')
1004 def list(self, cont):
1005 r"list(cont) -> tuple :: List all the item keys."
1006 if not cont in self._cont():
1007 raise ContainerNotFoundError(cont)
1008 return self._attr(cont).keys()
1011 if __name__ == '__main__':
1014 class STestHandler1(ServiceHandler):
1015 _service_start = ('service', 'start')
1016 _service_stop = ('service', 'stop')
1017 _service_restart = ('ls', '/')
1018 _service_reload = ('cp', '/la')
1019 class STestHandler2(ServiceHandler):
1021 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1022 'cmd-restart', 'cmd-reload')
1023 class ITestHandler1(InitdHandler):
1024 _initd_name = 'test1'
1025 class ITestHandler2(InitdHandler):
1027 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1035 print h.__class__.__name__
1038 except ExecutionError, e:
1042 except ExecutionError, e:
1046 except ExecutionError, e:
1050 except ExecutionError, e:
1055 print 'PTestHandler'
1056 class PTestHandler(Persistent):
1057 _persistent_attrs = 'vars'
1059 self.vars = dict(a=1, b=2)
1077 print 'RTestHandler'
1078 class RTestHandler(Restorable):
1079 _persistent_attrs = 'vars'
1080 _restorable_defaults = dict(vars=dict(a=1, b=2))
1092 print 'CTestHandler'
1094 os.mkdir('templates')
1095 f = file('templates/config', 'w')
1096 f.write('Hello, ${name}! You are ${what}.')
1099 print file('templates/config').read()
1100 class CTestHandler(ConfigWriter):
1101 _config_writer_files = 'config'
1103 self._config_build_templates()
1104 def _get_config_vars(self, config_file):
1105 return dict(name='you', what='a parrot')
1109 print file('config').read()
1111 os.unlink('templates/config')
1112 os.rmdir('templates')