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 print 'Executing command:', command
189 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
190 universal_newlines=universal_newlines,
191 close_fds=close_fds, **kw)
193 raise ExecutionError(command, e)
195 raise ExecutionError(command, ReturnNot0Error(r))
198 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
200 This is a helper class to inherit from to automatically handle data
201 persistence using pickle.
203 The variables attributes to persist (attrs), and the pickle directory (dir)
204 and file extension (ext) can be defined by calling the constructor or in a
205 more declarative way as class attributes, like:
207 class TestHandler(Persistent):
208 _persistent_attrs = ('some_attr', 'other_attr')
209 _persistent_dir = 'persistent-data'
210 _persistent_ext = '.pickle'
212 The default dir is '.' and the default extension is '.pkl'. There are no
213 default variables, and they should be specified as string if a single
214 attribute should be persistent or as a tuple of strings if they are more.
215 The strings should be the attribute names to be persisted. For each
216 attribute a separated pickle file is generated in the pickle directory.
218 You can call _dump() and _load() to write and read the data respectively.
220 # TODO implement it using metaclasses to add the handlers method by demand
221 # (only for specifieds commands).
223 _persistent_attrs = ()
224 _persistent_dir = '.'
225 _persistent_ext = '.pkl'
227 def __init__(self, attrs=None, dir=None, ext=None):
228 r"Initialize the object, see the class documentation for details."
229 if attrs is not None:
230 self._persistent_attrs = attrs
232 self._persistent_dir = dir
234 self._persistent_ext = ext
237 r"_dump() -> None :: Dump all persistent data to pickle files."
238 if isinstance(self._persistent_attrs, basestring):
239 self._persistent_attrs = (self._persistent_attrs,)
240 for attrname in self._persistent_attrs:
241 self._dump_attr(attrname)
244 r"_load() -> None :: Load all persistent data from pickle files."
245 if isinstance(self._persistent_attrs, basestring):
246 self._persistent_attrs = (self._persistent_attrs,)
247 for attrname in self._persistent_attrs:
248 self._load_attr(attrname)
250 def _dump_attr(self, attrname):
251 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
252 f = file(self._pickle_filename(attrname), 'wb')
253 pickle.dump(getattr(self, attrname), f, 2)
256 def _load_attr(self, attrname):
257 r"_load_attr() -> object :: Load a specific pickle file."
258 f = file(self._pickle_filename(attrname))
259 setattr(self, attrname, pickle.load(f))
262 def _pickle_filename(self, name):
263 r"_pickle_filename() -> string :: Construct a pickle filename."
264 return path.join(self._persistent_dir, name) + self._persistent_ext
266 class Restorable(Persistent):
267 r"""Restorable([defaults]) -> Restorable.
269 This is a helper class to inherit from that provides a nice _restore()
270 method to restore the persistent data if any, or load some nice defaults
273 The defaults can be defined by calling the constructor or in a more
274 declarative way as class attributes, like:
276 class TestHandler(Restorable):
277 _persistent_attrs = ('some_attr', 'other_attr')
278 _restorable_defaults = dict(
279 some_attr = 'some_default',
280 other_attr = 'other_default')
282 The defaults is a dictionary, very coupled with the _persistent_attrs
283 attribute inherited from Persistent. The defaults keys should be the
284 values from _persistent_attrs, and the values the default values.
286 The _restore() method returns True if the data was restored successfully
287 or False if the defaults were loaded (in case you want to take further
288 actions). If a _write_config method if found, it's executed when a restore
291 # TODO implement it using metaclasses to add the handlers method by demand
292 # (only for specifieds commands).
294 _restorable_defaults = dict()
296 def __init__(self, defaults=None):
297 r"Initialize the object, see the class documentation for details."
298 if defaults is not None:
299 self._restorable_defaults = defaults
302 r"_restore() -> bool :: Restore persistent data or create a default."
307 for (k, v) in self._restorable_defaults.items():
309 # TODO tener en cuenta servicios que hay que levantar y los que no
310 if hasattr(self, 'commit'):
314 if hasattr(self, '_write_config'):
319 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
321 This is a helper class to inherit from to automatically handle
322 configuration generation. Mako template system is used for configuration
325 The configuration filenames, the generated configuration files directory
326 and the templates directory can be defined by calling the constructor or
327 in a more declarative way as class attributes, like:
329 class TestHandler(ConfigWriter):
330 _config_writer_files = ('base.conf', 'custom.conf')
331 _config_writer_cfg_dir = {
332 'base.conf': '/etc/service',
333 'custom.conf': '/etc/service/conf.d',
335 _config_writer_tpl_dir = 'templates'
337 The generated configuration files directory defaults to '.' and the
338 templates directory to 'templates'. _config_writer_files has no default and
339 must be specified in either way. It can be string or a tuple if more than
340 one configuration file must be generated. _config_writer_cfg_dir could be a
341 dict mapping which file should be stored in which directory, or a single
342 string if all the config files should go to the same directory.
344 The template filename and the generated configuration filename are both the
345 same (so if you want to generate some /etc/config, you should have some
346 templates/config template). That's why _config_writer_cfg_dir and
347 _config_writer_tpl_dir can't be the same. This is not true for very
348 specific cases where _write_single_config() is used.
350 When you write your Handler, you should call _config_build_templates() in
351 you Handler constructor to build the templates.
353 To write the configuration files, you must use the _write_config() method.
354 To know what variables to replace in the template, you have to provide a
355 method called _get_config_vars(tamplate_name), which should return a
356 dictionary of variables to pass to the template system to be replaced in
357 the template for the configuration file 'config_file'.
359 # TODO implement it using metaclasses to add the handlers method by demand
360 # (only for specifieds commands).
362 _config_writer_files = ()
363 _config_writer_cfg_dir = '.'
364 _config_writer_tpl_dir = 'templates'
366 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
367 r"Initialize the object, see the class documentation for details."
368 if files is not None:
369 self._config_writer_files = files
370 if cfg_dir is not None:
371 self._config_writer_cfg_dir = cfg_dir
372 if tpl_dir is not None:
373 self._config_writer_tpl_dir = tpl_dir
374 self._config_build_templates()
376 def _config_build_templates(self):
377 r"_config_writer_templates() -> None :: Build the template objects."
378 if isinstance(self._config_writer_files, basestring):
379 self._config_writer_files = (self._config_writer_files,)
380 if not hasattr(self, '_config_writer_templates') \
381 or not self._config_writer_templates:
382 self._config_writer_templates = dict()
383 for t in self._config_writer_files:
384 f = path.join(self._config_writer_tpl_dir, t)
385 self._config_writer_templates[t] = Template(filename=f)
387 def _render_config(self, template_name, vars=None):
388 r"""_render_config(template_name[, config_filename[, vars]]).
390 Render a single config file using the template 'template_name'. If
391 vars is specified, it's used as the dictionary with the variables
392 to replace in the templates, if not, it looks for a
393 _get_config_vars() method to get it.
396 if hasattr(self, '_get_config_vars'):
397 vars = self._get_config_vars(template_name)
401 vars = vars(template_name)
402 return self._config_writer_templates[template_name].render(**vars)
404 def _get_config_path(self, template_name, config_filename=None):
405 r"Get a complete configuration path."
406 if not config_filename:
407 config_filename = template_name
408 if isinstance(self._config_writer_cfg_dir, basestring):
409 return path.join(self._config_writer_cfg_dir, config_filename)
410 return path.join(self._config_writer_cfg_dir[template_name],
413 def _write_single_config(self, template_name, config_filename=None, vars=None):
414 r"""_write_single_config(template_name[, config_filename[, vars]]).
416 Write a single config file using the template 'template_name'. If no
417 config_filename is specified, the config filename will be the same as
418 the 'template_name' (but stored in the generated config files
419 directory). If it's specified, the generated config file is stored in
420 the file called 'config_filename' (also in the generated files
421 directory). If vars is specified, it's used as the dictionary with the
422 variables to replace in the templates, if not, it looks for a
423 _get_config_vars() method to get it.
426 if hasattr(self, '_get_config_vars'):
427 vars = self._get_config_vars(template_name)
431 vars = vars(template_name)
432 f = file(self._get_config_path(template_name, config_filename), 'w')
433 ctx = Context(f, **vars)
434 self._config_writer_templates[template_name].render_context(ctx)
437 def _write_config(self):
438 r"_write_config() -> None :: Generate all the configuration files."
439 for t in self._config_writer_files:
440 self._write_single_config(t)
443 class ServiceHandler(Handler, Restorable):
444 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
446 This is a helper class to inherit from to automatically handle services
447 with start, stop, restart, reload actions.
449 The actions can be defined by calling the constructor with all the
450 parameters or in a more declarative way as class attributes, like:
452 class TestHandler(ServiceHandler):
453 _service_start = ('command', 'start')
454 _service_stop = ('command', 'stop')
455 _service_restart = ('command', 'restart')
456 _service_reload = 'reload-command'
458 Commands are executed without using the shell, that's why they are specified
459 as tuples (where the first element is the command and the others are the
460 command arguments). If only a command is needed (without arguments) a single
461 string can be specified.
463 All commands must be specified.
465 # TODO implement it using metaclasses to add the handlers method by demand
466 # (only for specifieds commands).
468 def __init__(self, start=None, stop=None, restart=None, reload=None):
469 r"Initialize the object, see the class documentation for details."
470 for (name, action) in dict(start=start, stop=stop, restart=restart,
471 reload=reload).items():
472 if action is not None:
473 setattr(self, '_service_%s' % name, action)
474 self._persistent_attrs = list(self._persistent_attrs)
475 self._persistent_attrs.append('_service_running')
476 if '_service_running' not in self._restorable_defaults:
477 self._restorable_defaults['_service_running'] = False
479 if self._service_running:
480 self._service_running = False
483 @handler(u'Start the service.')
485 r"start() -> None :: Start the service."
486 if not self._service_running:
487 if callable(self._service_start):
488 self._service_start()
490 call(self._service_start)
491 self._service_running = True
492 self._dump_attr('_service_running')
494 @handler(u'Stop the service.')
496 r"stop() -> None :: Stop the service."
497 if self._service_running:
498 if callable(self._service_stop):
501 call(self._service_stop)
502 self._service_running = False
503 self._dump_attr('_service_running')
505 @handler(u'Restart the service.')
507 r"restart() -> None :: Restart the service."
508 if callable(self._service_restart):
509 self._service_restart()
511 call(self._service_restart)
512 self._service_running = True
513 self._dump_attr('_service_running')
515 @handler(u'Reload the service config (without restarting, if possible).')
517 r"reload() -> None :: Reload the configuration of the service."
518 if self._service_running:
519 if callable(self._service_reload):
520 self._service_reload()
522 call(self._service_reload)
524 @handler(u'Tell if the service is running.')
526 r"reload() -> None :: Reload the configuration of the service."
527 if self._service_running:
532 class RestartHandler(Handler):
533 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
535 This is a helper class to inherit from to automatically add a restart
536 command that first stop the service and then starts it again (using start
537 and stop commands respectively).
540 @handler(u'Restart the service (alias to stop + start).')
542 r"restart() -> None :: Restart the service calling stop() and start()."
546 class ReloadHandler(Handler):
547 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
549 This is a helper class to inherit from to automatically add a reload
550 command that calls restart.
553 @handler(u'Reload the service config (alias to restart).')
555 r"reload() -> None :: Reload the configuration of the service."
556 if hasattr(self, '_service_running') and self._service_running:
559 class InitdHandler(ServiceHandler):
560 # TODO update docs, declarative style is depracated
561 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
563 This is a helper class to inherit from to automatically handle services
564 with start, stop, restart, reload actions using a /etc/init.d like script.
566 The name and directory of the script can be defined by calling the
567 constructor or in a more declarative way as class attributes, like:
569 class TestHandler(ServiceHandler):
570 _initd_name = 'some-service'
571 _initd_dir = '/usr/local/etc/init.d'
573 The default _initd_dir is '/etc/init.d', _initd_name has no default and
574 must be specified in either way.
576 Commands are executed without using the shell.
578 # TODO implement it using metaclasses to add the handlers method by demand
579 # (only for specifieds commands).
581 _initd_dir = '/etc/init.d'
583 def __init__(self, initd_name=None, initd_dir=None):
584 r"Initialize the object, see the class documentation for details."
585 if initd_name is not None:
586 self._initd_name = initd_name
587 if initd_dir is not None:
588 self._initd_dir = initd_dir
590 for action in ('start', 'stop', 'restart', 'reload'):
591 actions[action] = (path.join(self._initd_dir, self._initd_name),
593 ServiceHandler.__init__(self, **actions)
595 class TransactionalHandler(Handler):
596 r"""Handle command transactions providing a commit and rollback commands.
598 This is a helper class to inherit from to automatically handle
599 transactional handlers, which have commit and rollback commands.
601 The handler should provide a reload() method (see ServiceHandler and
602 InitdHandler for helper classes to provide this) which will be called
603 when a commit command is issued (if a reload() command is present).
604 The persistent data will be written too (if a _dump() method is provided,
605 see Persistent and Restorable for that), and the configuration files
606 will be generated (if a _write_config method is present, see ConfigWriter).
608 # TODO implement it using metaclasses to add the handlers method by demand
609 # (only for specifieds commands).
611 @handler(u'Commit the changes (reloading the service, if necessary).')
613 r"commit() -> None :: Commit the changes and reload the service."
614 if hasattr(self, '_dump'):
617 if hasattr(self, '_write_config'):
618 unchanged = self._write_config()
619 if not unchanged and hasattr(self, 'reload'):
622 @handler(u'Discard all the uncommited changes.')
624 r"rollback() -> None :: Discard the changes not yet commited."
625 if hasattr(self, '_load'):
628 class ParametersHandler(Handler):
629 r"""ParametersHandler([attr]) -> ParametersHandler.
631 This is a helper class to inherit from to automatically handle
632 service parameters, providing set, get, list and show commands.
634 The attribute that holds the parameters can be defined by calling the
635 constructor or in a more declarative way as class attributes, like:
637 class TestHandler(ServiceHandler):
638 _parameters_attr = 'some_attr'
640 The default is 'params' and it should be a dictionary.
642 # TODO implement it using metaclasses to add the handlers method by demand
643 # (only for specifieds commands).
645 _parameters_attr = 'params'
647 def __init__(self, attr=None):
648 r"Initialize the object, see the class documentation for details."
650 self._parameters_attr = attr
652 @handler(u'Set a service parameter.')
653 def set(self, param, value):
654 r"set(param, value) -> None :: Set a service parameter."
655 if not param in self.params:
656 raise ParameterNotFoundError(param)
657 self.params[param] = value
658 if hasattr(self, '_update'):
661 @handler(u'Get a service parameter.')
662 def get(self, param):
663 r"get(param) -> None :: Get a service parameter."
664 if not param in self.params:
665 raise ParameterNotFoundError(param)
666 return self.params[param]
668 @handler(u'List all available service parameters.')
670 r"list() -> tuple :: List all the parameter names."
671 return self.params.keys()
673 @handler(u'Get all service parameters, with their values.')
675 r"show() -> (key, value) tuples :: List all the parameters."
676 return self.params.items()
678 class SubHandler(Handler):
679 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
681 This is a helper class to build sub handlers that needs to reference the
684 parent - Parent Handler object.
687 def __init__(self, parent):
688 r"Initialize the object, see the class documentation for details."
691 class ContainerSubHandler(SubHandler):
692 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
694 This is a helper class to implement ListSubHandler and DictSubHandler. You
695 should not use it directly.
697 The container attribute to handle and the class of objects that it
698 contains can be defined by calling the constructor or in a more declarative
699 way as class attributes, like:
701 class TestHandler(ContainerSubHandler):
702 _cont_subhandler_attr = 'some_cont'
703 _cont_subhandler_class = SomeClass
705 This way, the parent's some_cont attribute (self.parent.some_cont)
706 will be managed automatically, providing the commands: add, update,
707 delete, get and show. New items will be instances of SomeClass,
708 which should provide a cmp operator to see if the item is on the
709 container and an update() method, if it should be possible to modify
710 it. If SomeClass has an _add, _update or _delete attribute, it set
711 them to true when the item is added, updated or deleted respectively
712 (in case that it's deleted, it's not removed from the container,
713 but it's not listed either).
716 def __init__(self, parent, attr=None, cls=None):
717 r"Initialize the object, see the class documentation for details."
720 self._cont_subhandler_attr = attr
722 self._cont_subhandler_class = cls
724 def _attr(self, attr=None):
726 return getattr(self.parent, self._cont_subhandler_attr)
727 setattr(self.parent, self._cont_subhandler_attr, attr)
730 if isinstance(self._attr(), dict):
731 return dict([(k, i) for (k, i) in self._attr().items()
732 if not hasattr(i, '_delete') or not i._delete])
733 return [i for i in self._attr()
734 if not hasattr(i, '_delete') or not i._delete]
736 @handler(u'Add a new item')
737 def add(self, *args, **kwargs):
738 r"add(...) -> None :: Add an item to the list."
739 item = self._cont_subhandler_class(*args, **kwargs)
740 if hasattr(item, '_add'):
743 if isinstance(self._attr(), dict):
744 key = item.as_tuple()[0]
745 # do we have the same item? then raise an error
746 if key in self._vattr():
747 raise ItemAlreadyExistsError(item)
748 # do we have the same item, but logically deleted? then update flags
749 if key in self._attr():
751 if not isinstance(self._attr(), dict):
752 index = self._attr().index(item)
753 if hasattr(item, '_add'):
754 self._attr()[index]._add = False
755 if hasattr(item, '_delete'):
756 self._attr()[index]._delete = False
757 else: # it's *really* new
758 if isinstance(self._attr(), dict):
759 self._attr()[key] = item
761 self._attr().append(item)
763 @handler(u'Update an item')
764 def update(self, index, *args, **kwargs):
765 r"update(index, ...) -> None :: Update an item of the container."
766 # TODO make it right with metaclasses, so the method is not created
767 # unless the update() method really exists.
768 # TODO check if the modified item is the same of an existing one
769 if not isinstance(self._attr(), dict):
770 index = int(index) # TODO validation
771 if not hasattr(self._cont_subhandler_class, 'update'):
772 raise CommandNotFoundError(('update',))
774 item = self._vattr()[index]
775 item.update(*args, **kwargs)
776 if hasattr(item, '_update'):
779 raise ItemNotFoundError(index)
781 @handler(u'Delete an item')
782 def delete(self, index):
783 r"delete(index) -> None :: Delete an item of the container."
784 if not isinstance(self._attr(), dict):
785 index = int(index) # TODO validation
787 item = self._vattr()[index]
788 if hasattr(item, '_delete'):
791 del self._attr()[index]
794 raise ItemNotFoundError(index)
796 @handler(u'Remove all items (use with care).')
798 r"clear() -> None :: Delete all items of the container."
799 if isinstance(self._attr(), dict):
804 @handler(u'Get information about an item')
805 def get(self, index):
806 r"get(index) -> item :: List all the information of an item."
807 if not isinstance(self._attr(), dict):
808 index = int(index) # TODO validation
810 return self._vattr()[index]
812 raise ItemNotFoundError(index)
814 @handler(u'Get information about all items')
816 r"show() -> list of items :: List all the complete items information."
817 if isinstance(self._attr(), dict):
818 return self._attr().values()
821 class ListSubHandler(ContainerSubHandler):
822 r"""ListSubHandler(parent) -> ListSubHandler instance.
824 ContainerSubHandler holding lists. See ComposedSubHandler documentation
828 @handler(u'Get how many items are in the list')
830 r"len() -> int :: Get how many items are in the list."
831 return len(self._vattr())
833 class DictSubHandler(ContainerSubHandler):
834 r"""DictSubHandler(parent) -> DictSubHandler instance.
836 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
840 @handler(u'List all the items by key')
842 r"list() -> tuple :: List all the item keys."
843 return self._attr().keys()
845 class ComposedSubHandler(SubHandler):
846 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
848 This is a helper class to implement ListComposedSubHandler and
849 DictComposedSubHandler. You should not use it directly.
851 This class is usefull when you have a parent that has a dict (cont)
852 that stores some object that has an attribute (attr) with a list or
853 a dict of objects of some class. In that case, this class provides
854 automated commands to add, update, delete, get and show that objects.
855 This commands takes the cont (key of the dict for the object holding
856 the attr), and an index for access the object itself (in the attr
859 The container object (cont) that holds a containers, the attribute of
860 that object that is the container itself, and the class of the objects
861 that it contains can be defined by calling the constructor or in a
862 more declarative way as class attributes, like:
864 class TestHandler(ComposedSubHandler):
865 _comp_subhandler_cont = 'some_cont'
866 _comp_subhandler_attr = 'some_attr'
867 _comp_subhandler_class = SomeClass
869 This way, the parent's some_cont attribute (self.parent.some_cont)
870 will be managed automatically, providing the commands: add, update,
871 delete, get and show for manipulating a particular instance that holds
872 of SomeClass. For example, updating an item at the index 5 is the same
873 (simplified) as doing parent.some_cont[cont][5].update().
874 SomeClass should provide a cmp operator to see if the item is on the
875 container and an update() method, if it should be possible to modify
876 it. If SomeClass has an _add, _update or _delete attribute, it set
877 them to true when the item is added, updated or deleted respectively
878 (in case that it's deleted, it's not removed from the container,
879 but it's not listed either). If the container objects
880 (parent.some_cont[cont]) has an _update attribute, it's set to True
881 when any add, update or delete command is executed.
884 def __init__(self, parent, cont=None, attr=None, cls=None):
885 r"Initialize the object, see the class documentation for details."
888 self._comp_subhandler_cont = cont
890 self._comp_subhandler_attr = attr
892 self._comp_subhandler_class = cls
895 return getattr(self.parent, self._comp_subhandler_cont)
897 def _attr(self, cont, attr=None):
899 return getattr(self._cont()[cont], self._comp_subhandler_attr)
900 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
902 def _vattr(self, cont):
903 if isinstance(self._attr(cont), dict):
904 return dict([(k, i) for (k, i) in self._attr(cont).items()
905 if not hasattr(i, '_delete') or not i._delete])
906 return [i for i in self._attr(cont)
907 if not hasattr(i, '_delete') or not i._delete]
909 @handler(u'Add a new item')
910 def add(self, cont, *args, **kwargs):
911 r"add(cont, ...) -> None :: Add an item to the list."
912 if not cont in self._cont():
913 raise ContainerNotFoundError(cont)
914 item = self._comp_subhandler_class(*args, **kwargs)
915 if hasattr(item, '_add'):
918 if isinstance(self._attr(cont), dict):
919 key = item.as_tuple()[0]
920 # do we have the same item? then raise an error
921 if key in self._vattr(cont):
922 raise ItemAlreadyExistsError(item)
923 # do we have the same item, but logically deleted? then update flags
924 if key in self._attr(cont):
926 if not isinstance(self._attr(cont), dict):
927 index = self._attr(cont).index(item)
928 if hasattr(item, '_add'):
929 self._attr(cont)[index]._add = False
930 if hasattr(item, '_delete'):
931 self._attr(cont)[index]._delete = False
932 else: # it's *really* new
933 if isinstance(self._attr(cont), dict):
934 self._attr(cont)[key] = item
936 self._attr(cont).append(item)
937 if hasattr(self._cont()[cont], '_update'):
938 self._cont()[cont]._update = True
940 @handler(u'Update an item')
941 def update(self, cont, index, *args, **kwargs):
942 r"update(cont, index, ...) -> None :: Update an item of the container."
943 # TODO make it right with metaclasses, so the method is not created
944 # unless the update() method really exists.
945 # TODO check if the modified item is the same of an existing one
946 if not cont in self._cont():
947 raise ContainerNotFoundError(cont)
948 if not isinstance(self._attr(cont), dict):
949 index = int(index) # TODO validation
950 if not hasattr(self._comp_subhandler_class, 'update'):
951 raise CommandNotFoundError(('update',))
953 item = self._vattr(cont)[index]
954 item.update(*args, **kwargs)
955 if hasattr(item, '_update'):
957 if hasattr(self._cont()[cont], '_update'):
958 self._cont()[cont]._update = True
960 raise ItemNotFoundError(index)
962 @handler(u'Delete an item')
963 def delete(self, cont, index):
964 r"delete(cont, index) -> None :: Delete an item of the container."
965 if not cont in self._cont():
966 raise ContainerNotFoundError(cont)
967 if not isinstance(self._attr(cont), dict):
968 index = int(index) # TODO validation
970 item = self._vattr(cont)[index]
971 if hasattr(item, '_delete'):
974 del self._attr(cont)[index]
975 if hasattr(self._cont()[cont], '_update'):
976 self._cont()[cont]._update = True
979 raise ItemNotFoundError(index)
981 @handler(u'Remove all items (use with care).')
982 def clear(self, cont):
983 r"clear(cont) -> None :: Delete all items of the container."
984 if not cont in self._cont():
985 raise ContainerNotFoundError(cont)
986 if isinstance(self._attr(cont), dict):
987 self._attr(cont).clear()
989 self._attr(cont, list())
991 @handler(u'Get information about an item')
992 def get(self, cont, index):
993 r"get(cont, index) -> item :: List all the information of an item."
994 if not cont in self._cont():
995 raise ContainerNotFoundError(cont)
996 if not isinstance(self._attr(cont), dict):
997 index = int(index) # TODO validation
999 return self._vattr(cont)[index]
1001 raise ItemNotFoundError(index)
1003 @handler(u'Get information about all items')
1004 def show(self, cont):
1005 r"show(cont) -> list of items :: List all the complete items information."
1006 if not cont in self._cont():
1007 raise ContainerNotFoundError(cont)
1008 if isinstance(self._attr(cont), dict):
1009 return self._attr(cont).values()
1010 return self._vattr(cont)
1012 class ListComposedSubHandler(ComposedSubHandler):
1013 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1015 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1019 @handler(u'Get how many items are in the list')
1020 def len(self, cont):
1021 r"len(cont) -> int :: Get how many items are in the list."
1022 if not cont in self._cont():
1023 raise ContainerNotFoundError(cont)
1024 return len(self._vattr(cont))
1026 class DictComposedSubHandler(ComposedSubHandler):
1027 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1029 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1033 @handler(u'List all the items by key')
1034 def list(self, cont):
1035 r"list(cont) -> tuple :: List all the item keys."
1036 if not cont in self._cont():
1037 raise ContainerNotFoundError(cont)
1038 return self._attr(cont).keys()
1041 if __name__ == '__main__':
1044 class STestHandler1(ServiceHandler):
1045 _service_start = ('service', 'start')
1046 _service_stop = ('service', 'stop')
1047 _service_restart = ('ls', '/')
1048 _service_reload = ('cp', '/la')
1049 class STestHandler2(ServiceHandler):
1051 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1052 'cmd-restart', 'cmd-reload')
1053 class ITestHandler1(InitdHandler):
1054 _initd_name = 'test1'
1055 class ITestHandler2(InitdHandler):
1057 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1065 print h.__class__.__name__
1068 except ExecutionError, e:
1072 except ExecutionError, e:
1076 except ExecutionError, e:
1080 except ExecutionError, e:
1085 print 'PTestHandler'
1086 class PTestHandler(Persistent):
1087 _persistent_attrs = 'vars'
1089 self.vars = dict(a=1, b=2)
1107 print 'RTestHandler'
1108 class RTestHandler(Restorable):
1109 _persistent_attrs = 'vars'
1110 _restorable_defaults = dict(vars=dict(a=1, b=2))
1122 print 'CTestHandler'
1124 os.mkdir('templates')
1125 f = file('templates/config', 'w')
1126 f.write('Hello, ${name}! You are ${what}.')
1129 print file('templates/config').read()
1130 class CTestHandler(ConfigWriter):
1131 _config_writer_files = 'config'
1133 self._config_build_templates()
1134 def _get_config_vars(self, config_file):
1135 return dict(name='you', what='a parrot')
1139 print file('config').read()
1141 os.unlink('templates/config')
1142 os.rmdir('templates')