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 def handle_timer(self):
596 p = subprocess.Popen(('pgrep', '-f', self._initd_name),
597 stdout=subprocess.PIPE)
598 pid = p.communicate()[0]
599 if p.wait() == 0 and len(pid) > 0:
600 c._service_running = True
602 c._service_running = False
604 class TransactionalHandler(Handler):
605 r"""Handle command transactions providing a commit and rollback commands.
607 This is a helper class to inherit from to automatically handle
608 transactional handlers, which have commit and rollback commands.
610 The handler should provide a reload() method (see ServiceHandler and
611 InitdHandler for helper classes to provide this) which will be called
612 when a commit command is issued (if a reload() command is present).
613 The persistent data will be written too (if a _dump() method is provided,
614 see Persistent and Restorable for that), and the configuration files
615 will be generated (if a _write_config method is present, see ConfigWriter).
617 # TODO implement it using metaclasses to add the handlers method by demand
618 # (only for specifieds commands).
620 @handler(u'Commit the changes (reloading the service, if necessary).')
622 r"commit() -> None :: Commit the changes and reload the service."
623 if hasattr(self, '_dump'):
626 if hasattr(self, '_write_config'):
627 unchanged = self._write_config()
628 if not unchanged and hasattr(self, 'reload'):
631 @handler(u'Discard all the uncommited changes.')
633 r"rollback() -> None :: Discard the changes not yet commited."
634 if hasattr(self, '_load'):
637 class ParametersHandler(Handler):
638 r"""ParametersHandler([attr]) -> ParametersHandler.
640 This is a helper class to inherit from to automatically handle
641 service parameters, providing set, get, list and show commands.
643 The attribute that holds the parameters can be defined by calling the
644 constructor or in a more declarative way as class attributes, like:
646 class TestHandler(ServiceHandler):
647 _parameters_attr = 'some_attr'
649 The default is 'params' and it should be a dictionary.
651 # TODO implement it using metaclasses to add the handlers method by demand
652 # (only for specifieds commands).
654 _parameters_attr = 'params'
656 def __init__(self, attr=None):
657 r"Initialize the object, see the class documentation for details."
659 self._parameters_attr = attr
661 @handler(u'Set a service parameter.')
662 def set(self, param, value):
663 r"set(param, value) -> None :: Set a service parameter."
664 if not param in self.params:
665 raise ParameterNotFoundError(param)
666 self.params[param] = value
667 if hasattr(self, '_update'):
670 @handler(u'Get a service parameter.')
671 def get(self, param):
672 r"get(param) -> None :: Get a service parameter."
673 if not param in self.params:
674 raise ParameterNotFoundError(param)
675 return self.params[param]
677 @handler(u'List all available service parameters.')
679 r"list() -> tuple :: List all the parameter names."
680 return self.params.keys()
682 @handler(u'Get all service parameters, with their values.')
684 r"show() -> (key, value) tuples :: List all the parameters."
685 return self.params.items()
687 class SubHandler(Handler):
688 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
690 This is a helper class to build sub handlers that needs to reference the
693 parent - Parent Handler object.
696 def __init__(self, parent):
697 r"Initialize the object, see the class documentation for details."
700 class ContainerSubHandler(SubHandler):
701 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
703 This is a helper class to implement ListSubHandler and DictSubHandler. You
704 should not use it directly.
706 The container attribute to handle and the class of objects that it
707 contains can be defined by calling the constructor or in a more declarative
708 way as class attributes, like:
710 class TestHandler(ContainerSubHandler):
711 _cont_subhandler_attr = 'some_cont'
712 _cont_subhandler_class = SomeClass
714 This way, the parent's some_cont attribute (self.parent.some_cont)
715 will be managed automatically, providing the commands: add, update,
716 delete, get and show. New items will be instances of SomeClass,
717 which should provide a cmp operator to see if the item is on the
718 container and an update() method, if it should be possible to modify
719 it. If SomeClass has an _add, _update or _delete attribute, it set
720 them to true when the item is added, updated or deleted respectively
721 (in case that it's deleted, it's not removed from the container,
722 but it's not listed either).
725 def __init__(self, parent, attr=None, cls=None):
726 r"Initialize the object, see the class documentation for details."
729 self._cont_subhandler_attr = attr
731 self._cont_subhandler_class = cls
733 def _attr(self, attr=None):
735 return getattr(self.parent, self._cont_subhandler_attr)
736 setattr(self.parent, self._cont_subhandler_attr, attr)
739 if isinstance(self._attr(), dict):
740 return dict([(k, i) for (k, i) in self._attr().items()
741 if not hasattr(i, '_delete') or not i._delete])
742 return [i for i in self._attr()
743 if not hasattr(i, '_delete') or not i._delete]
745 @handler(u'Add a new item')
746 def add(self, *args, **kwargs):
747 r"add(...) -> None :: Add an item to the list."
748 item = self._cont_subhandler_class(*args, **kwargs)
749 if hasattr(item, '_add'):
752 if isinstance(self._attr(), dict):
753 key = item.as_tuple()[0]
754 # do we have the same item? then raise an error
755 if key in self._vattr():
756 raise ItemAlreadyExistsError(item)
757 # do we have the same item, but logically deleted? then update flags
758 if key in self._attr():
760 if not isinstance(self._attr(), dict):
761 index = self._attr().index(item)
762 if hasattr(item, '_add'):
763 self._attr()[index]._add = False
764 if hasattr(item, '_delete'):
765 self._attr()[index]._delete = False
766 else: # it's *really* new
767 if isinstance(self._attr(), dict):
768 self._attr()[key] = item
770 self._attr().append(item)
772 @handler(u'Update an item')
773 def update(self, index, *args, **kwargs):
774 r"update(index, ...) -> None :: Update an item of the container."
775 # TODO make it right with metaclasses, so the method is not created
776 # unless the update() method really exists.
777 # TODO check if the modified item is the same of an existing one
778 if not isinstance(self._attr(), dict):
779 index = int(index) # TODO validation
780 if not hasattr(self._cont_subhandler_class, 'update'):
781 raise CommandNotFoundError(('update',))
783 item = self._vattr()[index]
784 item.update(*args, **kwargs)
785 if hasattr(item, '_update'):
788 raise ItemNotFoundError(index)
790 @handler(u'Delete an item')
791 def delete(self, index):
792 r"delete(index) -> None :: Delete an item of the container."
793 if not isinstance(self._attr(), dict):
794 index = int(index) # TODO validation
796 item = self._vattr()[index]
797 if hasattr(item, '_delete'):
800 del self._attr()[index]
803 raise ItemNotFoundError(index)
805 @handler(u'Remove all items (use with care).')
807 r"clear() -> None :: Delete all items of the container."
808 if isinstance(self._attr(), dict):
813 @handler(u'Get information about an item')
814 def get(self, index):
815 r"get(index) -> item :: List all the information of an item."
816 if not isinstance(self._attr(), dict):
817 index = int(index) # TODO validation
819 return self._vattr()[index]
821 raise ItemNotFoundError(index)
823 @handler(u'Get information about all items')
825 r"show() -> list of items :: List all the complete items information."
826 if isinstance(self._attr(), dict):
827 return self._attr().values()
830 class ListSubHandler(ContainerSubHandler):
831 r"""ListSubHandler(parent) -> ListSubHandler instance.
833 ContainerSubHandler holding lists. See ComposedSubHandler documentation
837 @handler(u'Get how many items are in the list')
839 r"len() -> int :: Get how many items are in the list."
840 return len(self._vattr())
842 class DictSubHandler(ContainerSubHandler):
843 r"""DictSubHandler(parent) -> DictSubHandler instance.
845 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
849 @handler(u'List all the items by key')
851 r"list() -> tuple :: List all the item keys."
852 return self._attr().keys()
854 class ComposedSubHandler(SubHandler):
855 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
857 This is a helper class to implement ListComposedSubHandler and
858 DictComposedSubHandler. You should not use it directly.
860 This class is usefull when you have a parent that has a dict (cont)
861 that stores some object that has an attribute (attr) with a list or
862 a dict of objects of some class. In that case, this class provides
863 automated commands to add, update, delete, get and show that objects.
864 This commands takes the cont (key of the dict for the object holding
865 the attr), and an index for access the object itself (in the attr
868 The container object (cont) that holds a containers, the attribute of
869 that object that is the container itself, and the class of the objects
870 that it contains can be defined by calling the constructor or in a
871 more declarative way as class attributes, like:
873 class TestHandler(ComposedSubHandler):
874 _comp_subhandler_cont = 'some_cont'
875 _comp_subhandler_attr = 'some_attr'
876 _comp_subhandler_class = SomeClass
878 This way, the parent's some_cont attribute (self.parent.some_cont)
879 will be managed automatically, providing the commands: add, update,
880 delete, get and show for manipulating a particular instance that holds
881 of SomeClass. For example, updating an item at the index 5 is the same
882 (simplified) as doing parent.some_cont[cont][5].update().
883 SomeClass should provide a cmp operator to see if the item is on the
884 container and an update() method, if it should be possible to modify
885 it. If SomeClass has an _add, _update or _delete attribute, it set
886 them to true when the item is added, updated or deleted respectively
887 (in case that it's deleted, it's not removed from the container,
888 but it's not listed either). If the container objects
889 (parent.some_cont[cont]) has an _update attribute, it's set to True
890 when any add, update or delete command is executed.
893 def __init__(self, parent, cont=None, attr=None, cls=None):
894 r"Initialize the object, see the class documentation for details."
897 self._comp_subhandler_cont = cont
899 self._comp_subhandler_attr = attr
901 self._comp_subhandler_class = cls
904 return getattr(self.parent, self._comp_subhandler_cont)
906 def _attr(self, cont, attr=None):
908 return getattr(self._cont()[cont], self._comp_subhandler_attr)
909 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
911 def _vattr(self, cont):
912 if isinstance(self._attr(cont), dict):
913 return dict([(k, i) for (k, i) in self._attr(cont).items()
914 if not hasattr(i, '_delete') or not i._delete])
915 return [i for i in self._attr(cont)
916 if not hasattr(i, '_delete') or not i._delete]
918 @handler(u'Add a new item')
919 def add(self, cont, *args, **kwargs):
920 r"add(cont, ...) -> None :: Add an item to the list."
921 if not cont in self._cont():
922 raise ContainerNotFoundError(cont)
923 item = self._comp_subhandler_class(*args, **kwargs)
924 if hasattr(item, '_add'):
927 if isinstance(self._attr(cont), dict):
928 key = item.as_tuple()[0]
929 # do we have the same item? then raise an error
930 if key in self._vattr(cont):
931 raise ItemAlreadyExistsError(item)
932 # do we have the same item, but logically deleted? then update flags
933 if key in self._attr(cont):
935 if not isinstance(self._attr(cont), dict):
936 index = self._attr(cont).index(item)
937 if hasattr(item, '_add'):
938 self._attr(cont)[index]._add = False
939 if hasattr(item, '_delete'):
940 self._attr(cont)[index]._delete = False
941 else: # it's *really* new
942 if isinstance(self._attr(cont), dict):
943 self._attr(cont)[key] = item
945 self._attr(cont).append(item)
946 if hasattr(self._cont()[cont], '_update'):
947 self._cont()[cont]._update = True
949 @handler(u'Update an item')
950 def update(self, cont, index, *args, **kwargs):
951 r"update(cont, index, ...) -> None :: Update an item of the container."
952 # TODO make it right with metaclasses, so the method is not created
953 # unless the update() method really exists.
954 # TODO check if the modified item is the same of an existing one
955 if not cont in self._cont():
956 raise ContainerNotFoundError(cont)
957 if not isinstance(self._attr(cont), dict):
958 index = int(index) # TODO validation
959 if not hasattr(self._comp_subhandler_class, 'update'):
960 raise CommandNotFoundError(('update',))
962 item = self._vattr(cont)[index]
963 item.update(*args, **kwargs)
964 if hasattr(item, '_update'):
966 if hasattr(self._cont()[cont], '_update'):
967 self._cont()[cont]._update = True
969 raise ItemNotFoundError(index)
971 @handler(u'Delete an item')
972 def delete(self, cont, index):
973 r"delete(cont, index) -> None :: Delete an item of the container."
974 if not cont in self._cont():
975 raise ContainerNotFoundError(cont)
976 if not isinstance(self._attr(cont), dict):
977 index = int(index) # TODO validation
979 item = self._vattr(cont)[index]
980 if hasattr(item, '_delete'):
983 del self._attr(cont)[index]
984 if hasattr(self._cont()[cont], '_update'):
985 self._cont()[cont]._update = True
988 raise ItemNotFoundError(index)
990 @handler(u'Remove all items (use with care).')
991 def clear(self, cont):
992 r"clear(cont) -> None :: Delete all items of the container."
993 if not cont in self._cont():
994 raise ContainerNotFoundError(cont)
995 if isinstance(self._attr(cont), dict):
996 self._attr(cont).clear()
998 self._attr(cont, list())
1000 @handler(u'Get information about an item')
1001 def get(self, cont, index):
1002 r"get(cont, index) -> item :: List all the information of an item."
1003 if not cont in self._cont():
1004 raise ContainerNotFoundError(cont)
1005 if not isinstance(self._attr(cont), dict):
1006 index = int(index) # TODO validation
1008 return self._vattr(cont)[index]
1010 raise ItemNotFoundError(index)
1012 @handler(u'Get information about all items')
1013 def show(self, cont):
1014 r"show(cont) -> list of items :: List all the complete items information."
1015 if not cont in self._cont():
1016 raise ContainerNotFoundError(cont)
1017 if isinstance(self._attr(cont), dict):
1018 return self._attr(cont).values()
1019 return self._vattr(cont)
1021 class ListComposedSubHandler(ComposedSubHandler):
1022 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1024 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1028 @handler(u'Get how many items are in the list')
1029 def len(self, cont):
1030 r"len(cont) -> int :: Get how many items are in the list."
1031 if not cont in self._cont():
1032 raise ContainerNotFoundError(cont)
1033 return len(self._vattr(cont))
1035 class DictComposedSubHandler(ComposedSubHandler):
1036 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1038 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1042 @handler(u'List all the items by key')
1043 def list(self, cont):
1044 r"list(cont) -> tuple :: List all the item keys."
1045 if not cont in self._cont():
1046 raise ContainerNotFoundError(cont)
1047 return self._attr(cont).keys()
1050 if __name__ == '__main__':
1053 class STestHandler1(ServiceHandler):
1054 _service_start = ('service', 'start')
1055 _service_stop = ('service', 'stop')
1056 _service_restart = ('ls', '/')
1057 _service_reload = ('cp', '/la')
1058 class STestHandler2(ServiceHandler):
1060 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1061 'cmd-restart', 'cmd-reload')
1062 class ITestHandler1(InitdHandler):
1063 _initd_name = 'test1'
1064 class ITestHandler2(InitdHandler):
1066 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1074 print h.__class__.__name__
1077 except ExecutionError, e:
1081 except ExecutionError, e:
1085 except ExecutionError, e:
1089 except ExecutionError, e:
1094 print 'PTestHandler'
1095 class PTestHandler(Persistent):
1096 _persistent_attrs = 'vars'
1098 self.vars = dict(a=1, b=2)
1116 print 'RTestHandler'
1117 class RTestHandler(Restorable):
1118 _persistent_attrs = 'vars'
1119 _restorable_defaults = dict(vars=dict(a=1, b=2))
1131 print 'CTestHandler'
1133 os.mkdir('templates')
1134 f = file('templates/config', 'w')
1135 f.write('Hello, ${name}! You are ${what}.')
1138 print file('templates/config').read()
1139 class CTestHandler(ConfigWriter):
1140 _config_writer_files = 'config'
1142 self._config_build_templates()
1143 def _get_config_vars(self, config_file):
1144 return dict(name='you', what='a parrot')
1148 print file('config').read()
1150 os.unlink('templates/config')
1151 os.rmdir('templates')