1 # vim: set encoding=utf-8 et sw=4 sts=4 :
4 from mako.template import Template
5 from mako.runtime import Context
8 import cPickle as pickle
12 from pymin.dispatcher import Handler, handler, HandlerError, \
18 __ALL__ = ('Error', 'ReturnNot0Error', 'ExecutionError', 'ItemError',
19 'ItemAlreadyExistsError', 'ItemNotFoundError', 'ContainerError',
20 'ContainerNotFoundError', 'call', 'get_network_devices',
21 'Persistent', 'Restorable', 'ConfigWriter', 'ServiceHandler',
22 'RestartHandler', 'ReloadHandler', 'InitdHandler', 'SubHandler',
23 'DictSubHandler', 'ListSubHandler', 'ComposedSubHandler',
24 'ListComposedSubHandler', 'DictComposedSubHandler')
26 class Error(HandlerError):
28 Error(message) -> Error instance :: Base ServiceHandler exception class.
30 All exceptions raised by the ServiceHandler inherits from this one, so
31 you can easily catch any ServiceHandler exception.
33 message - A descriptive error message.
37 class ReturnNot0Error(Error):
39 ReturnNot0Error(return_value) -> ReturnNot0Error instance.
41 A command didn't returned the expected 0 return value.
43 return_value - Return value returned by the command.
46 def __init__(self, return_value):
47 r"Initialize the object. See class documentation for more info."
48 self.return_value = return_value
50 def __unicode__(self):
51 return 'The command returned %d' % self.return_value
53 class ExecutionError(Error):
55 ExecutionError(command, error) -> ExecutionError instance.
57 Error executing a command.
59 command - Command that was tried to execute.
61 error - Error received when trying to execute the command.
64 def __init__(self, command, error):
65 r"Initialize the object. See class documentation for more info."
66 self.command = command
69 def __unicode__(self):
70 command = self.command
71 if not isinstance(self.command, basestring):
72 command = ' '.join(command)
73 return "Can't execute command %s: %s" % (command, self.error)
75 class ParameterError(Error, KeyError):
77 ParameterError(paramname) -> ParameterError instance
79 This is the base exception for all DhcpHandler parameters related errors.
82 def __init__(self, paramname):
83 r"Initialize the object. See class documentation for more info."
84 self.message = 'Parameter error: "%s"' % paramname
86 class ParameterNotFoundError(ParameterError):
88 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
90 This exception is raised when trying to operate on a parameter that doesn't
94 def __init__(self, paramname):
95 r"Initialize the object. See class documentation for more info."
96 self.message = 'Parameter not found: "%s"' % paramname
98 class ItemError(Error, KeyError):
100 ItemError(key) -> ItemError instance.
102 This is the base exception for all item related errors.
105 def __init__(self, key):
106 r"Initialize the object. See class documentation for more info."
107 self.message = u'Item error: "%s"' % key
109 class ItemAlreadyExistsError(ItemError):
111 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
113 This exception is raised when trying to add an item that already exists.
116 def __init__(self, key):
117 r"Initialize the object. See class documentation for more info."
118 self.message = u'Item already exists: "%s"' % key
120 class ItemNotFoundError(ItemError):
122 ItemNotFoundError(key) -> ItemNotFoundError instance.
124 This exception is raised when trying to operate on an item that doesn't
128 def __init__(self, key):
129 r"Initialize the object. See class documentation for more info."
130 self.message = u'Item not found: "%s"' % key
132 class ContainerError(Error, KeyError):
134 ContainerError(key) -> ContainerError instance.
136 This is the base exception for all container related errors.
139 def __init__(self, key):
140 r"Initialize the object. See class documentation for more info."
141 self.message = u'Container error: "%s"' % key
143 class ContainerNotFoundError(ContainerError):
145 ContainerNotFoundError(key) -> ContainerNotFoundError instance.
147 This exception is raised when trying to operate on an container that
151 def __init__(self, key):
152 r"Initialize the object. See class documentation for more info."
153 self.message = u'Container not found: "%s"' % key
156 def get_network_devices():
157 p = subprocess.Popen(('ip', 'link', 'list'), stdout=subprocess.PIPE,
159 string = p.stdout.read()
162 i = string.find('eth')
165 m = string.find('link/ether', i+4)
166 mac = string[ m+11 : m+11+17]
168 i = string.find('eth', m+11+17)
171 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
172 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
175 if not isinstance(command, basestring):
176 command = ' '.join(command)
177 print 'Executing command:', command
180 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
181 universal_newlines=universal_newlines,
182 close_fds=close_fds, **kw)
184 raise ExecutionError(command, e)
186 raise ExecutionError(command, ReturnNot0Error(r))
189 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
191 This is a helper class to inherit from to automatically handle data
192 persistence using pickle.
194 The variables attributes to persist (attrs), and the pickle directory (dir)
195 and file extension (ext) can be defined by calling the constructor or in a
196 more declarative way as class attributes, like:
198 class TestHandler(Persistent):
199 _persistent_attrs = ('some_attr', 'other_attr')
200 _persistent_dir = 'persistent-data'
201 _persistent_ext = '.pickle'
203 The default dir is '.' and the default extension is '.pkl'. There are no
204 default variables, and they should be specified as string if a single
205 attribute should be persistent or as a tuple of strings if they are more.
206 The strings should be the attribute names to be persisted. For each
207 attribute a separated pickle file is generated in the pickle directory.
209 You can call _dump() and _load() to write and read the data respectively.
211 # TODO implement it using metaclasses to add the handlers method by demand
212 # (only for specifieds commands).
214 _persistent_attrs = ()
215 _persistent_dir = '.'
216 _persistent_ext = '.pkl'
218 def __init__(self, attrs=None, dir=None, ext=None):
219 r"Initialize the object, see the class documentation for details."
220 if attrs is not None:
221 self._persistent_attrs = attrs
223 self._persistent_dir = dir
225 self._persistent_ext = ext
228 r"_dump() -> None :: Dump all persistent data to pickle files."
229 if isinstance(self._persistent_attrs, basestring):
230 self._persistent_attrs = (self._persistent_attrs,)
231 for attrname in self._persistent_attrs:
232 self._dump_attr(attrname)
235 r"_load() -> None :: Load all persistent data from pickle files."
236 if isinstance(self._persistent_attrs, basestring):
237 self._persistent_attrs = (self._persistent_attrs,)
238 for attrname in self._persistent_attrs:
239 self._load_attr(attrname)
241 def _dump_attr(self, attrname):
242 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
243 f = file(self._pickle_filename(attrname), 'wb')
244 pickle.dump(getattr(self, attrname), f, 2)
247 def _load_attr(self, attrname):
248 r"_load_attr() -> object :: Load a specific pickle file."
249 f = file(self._pickle_filename(attrname))
250 setattr(self, attrname, pickle.load(f))
253 def _pickle_filename(self, name):
254 r"_pickle_filename() -> string :: Construct a pickle filename."
255 return path.join(self._persistent_dir, name) + self._persistent_ext
257 class Restorable(Persistent):
258 r"""Restorable([defaults]) -> Restorable.
260 This is a helper class to inherit from that provides a nice _restore()
261 method to restore the persistent data if any, or load some nice defaults
264 The defaults can be defined by calling the constructor or in a more
265 declarative way as class attributes, like:
267 class TestHandler(Restorable):
268 _persistent_attrs = ('some_attr', 'other_attr')
269 _restorable_defaults = dict(
270 some_attr = 'some_default',
271 other_attr = 'other_default')
273 The defaults is a dictionary, very coupled with the _persistent_attrs
274 attribute inherited from Persistent. The defaults keys should be the
275 values from _persistent_attrs, and the values the default values.
277 The _restore() method returns True if the data was restored successfully
278 or False if the defaults were loaded (in case you want to take further
279 actions). If a _write_config method if found, it's executed when a restore
282 # TODO implement it using metaclasses to add the handlers method by demand
283 # (only for specifieds commands).
285 _restorable_defaults = dict()
287 def __init__(self, defaults=None):
288 r"Initialize the object, see the class documentation for details."
289 if defaults is not None:
290 self._restorable_defaults = defaults
293 r"_restore() -> bool :: Restore persistent data or create a default."
296 # TODO tener en cuenta servicios que hay que levantar y los que no
297 if hasattr(self, 'commit'): # TODO deberia ser reload y/o algo para comandos
301 for (k, v) in self._restorable_defaults.items():
303 # TODO tener en cuenta servicios que hay que levantar y los que no
304 if hasattr(self, 'commit'):
308 if hasattr(self, '_write_config'):
310 if hasattr(self, 'reload'):
315 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
317 This is a helper class to inherit from to automatically handle
318 configuration generation. Mako template system is used for configuration
321 The configuration filenames, the generated configuration files directory
322 and the templates directory can be defined by calling the constructor or
323 in a more declarative way as class attributes, like:
325 class TestHandler(ConfigWriter):
326 _config_writer_files = ('base.conf', 'custom.conf')
327 _config_writer_cfg_dir = {
328 'base.conf': '/etc/service',
329 'custom.conf': '/etc/service/conf.d',
331 _config_writer_tpl_dir = 'templates'
333 The generated configuration files directory defaults to '.' and the
334 templates directory to 'templates'. _config_writer_files has no default and
335 must be specified in either way. It can be string or a tuple if more than
336 one configuration file must be generated. _config_writer_cfg_dir could be a
337 dict mapping which file should be stored in which directory, or a single
338 string if all the config files should go to the same directory.
340 The template filename and the generated configuration filename are both the
341 same (so if you want to generate some /etc/config, you should have some
342 templates/config template). That's why _config_writer_cfg_dir and
343 _config_writer_tpl_dir can't be the same. This is not true for very
344 specific cases where _write_single_config() is used.
346 When you write your Handler, you should call _config_build_templates() in
347 you Handler constructor to build the templates.
349 To write the configuration files, you must use the _write_config() method.
350 To know what variables to replace in the template, you have to provide a
351 method called _get_config_vars(tamplate_name), which should return a
352 dictionary of variables to pass to the template system to be replaced in
353 the template for the configuration file 'config_file'.
355 # TODO implement it using metaclasses to add the handlers method by demand
356 # (only for specifieds commands).
358 _config_writer_files = ()
359 _config_writer_cfg_dir = '.'
360 _config_writer_tpl_dir = 'templates'
362 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
363 r"Initialize the object, see the class documentation for details."
364 if files is not None:
365 self._config_writer_files = files
366 if cfg_dir is not None:
367 self._config_writer_cfg_dir = cfg_dir
368 if tpl_dir is not None:
369 self._config_writer_tpl_dir = tpl_dir
370 self._config_build_templates()
372 def _config_build_templates(self):
373 r"_config_writer_templates() -> None :: Build the template objects."
374 if isinstance(self._config_writer_files, basestring):
375 self._config_writer_files = (self._config_writer_files,)
376 if not hasattr(self, '_config_writer_templates') \
377 or not self._config_writer_templates:
378 self._config_writer_templates = dict()
379 for t in self._config_writer_files:
380 f = path.join(self._config_writer_tpl_dir, t)
381 self._config_writer_templates[t] = Template(filename=f)
383 def _render_config(self, template_name, vars=None):
384 r"""_render_config(template_name[, config_filename[, vars]]).
386 Render a single config file using the template 'template_name'. If
387 vars is specified, it's used as the dictionary with the variables
388 to replace in the templates, if not, it looks for a
389 _get_config_vars() method to get it.
392 if hasattr(self, '_get_config_vars'):
393 vars = self._get_config_vars(template_name)
397 vars = vars(template_name)
398 return self._config_writer_templates[template_name].render(**vars)
400 def _get_config_path(self, template_name, config_filename=None):
401 r"Get a complete configuration path."
402 if not config_filename:
403 config_filename = template_name
404 if isinstance(self._config_writer_cfg_dir, basestring):
405 return path.join(self._config_writer_cfg_dir, config_filename)
406 return path.join(self._config_writer_cfg_dir[template_name],
409 def _write_single_config(self, template_name, config_filename=None, vars=None):
410 r"""_write_single_config(template_name[, config_filename[, vars]]).
412 Write a single config file using the template 'template_name'. If no
413 config_filename is specified, the config filename will be the same as
414 the 'template_name' (but stored in the generated config files
415 directory). If it's specified, the generated config file is stored in
416 the file called 'config_filename' (also in the generated files
417 directory). If vars is specified, it's used as the dictionary with the
418 variables to replace in the templates, if not, it looks for a
419 _get_config_vars() method to get it.
422 if hasattr(self, '_get_config_vars'):
423 vars = self._get_config_vars(template_name)
427 vars = vars(template_name)
428 f = file(self._get_config_path(template_name, config_filename), 'w')
429 ctx = Context(f, **vars)
430 self._config_writer_templates[template_name].render_context(ctx)
433 def _write_config(self):
434 r"_write_config() -> None :: Generate all the configuration files."
435 for t in self._config_writer_files:
436 self._write_single_config(t)
439 class ServiceHandler(Handler):
440 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
442 This is a helper class to inherit from to automatically handle services
443 with start, stop, restart, reload actions.
445 The actions can be defined by calling the constructor with all the
446 parameters or in a more declarative way as class attributes, like:
448 class TestHandler(ServiceHandler):
449 _service_start = ('command', 'start')
450 _service_stop = ('command', 'stop')
451 _service_restart = ('command', 'restart')
452 _service_reload = 'reload-command'
454 Commands are executed without using the shell, that's why they are specified
455 as tuples (where the first element is the command and the others are the
456 command arguments). If only a command is needed (without arguments) a single
457 string can be specified.
459 All commands must be specified.
461 # TODO implement it using metaclasses to add the handlers method by demand
462 # (only for specifieds commands).
464 def __init__(self, start=None, stop=None, restart=None, reload=None):
465 r"Initialize the object, see the class documentation for details."
466 for (name, action) in dict(start=start, stop=stop, restart=restart,
467 reload=reload).items():
468 if action is not None:
469 setattr(self, '_service_%s' % name, action)
471 @handler(u'Start the service.')
473 r"start() -> None :: Start the service."
474 call(self._service_start)
476 @handler(u'Stop the service.')
478 r"stop() -> None :: Stop the service."
479 call(self._service_stop)
481 @handler(u'Restart the service.')
483 r"restart() -> None :: Restart the service."
484 call(self._service_restart)
486 @handler(u'Reload the service config (without restarting, if possible).')
488 r"reload() -> None :: Reload the configuration of the service."
489 call(self._service_reload)
491 class RestartHandler(Handler):
492 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
494 This is a helper class to inherit from to automatically add a restart
495 command that first stop the service and then starts it again (using start
496 and stop commands respectively).
499 @handler(u'Restart the service (alias to stop + start).')
501 r"restart() -> None :: Restart the service calling stop() and start()."
505 class ReloadHandler(Handler):
506 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
508 This is a helper class to inherit from to automatically add a reload
509 command that calls restart.
512 @handler(u'Reload the service config (alias to restart).')
514 r"reload() -> None :: Reload the configuration of the service."
517 class InitdHandler(Handler):
518 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
520 This is a helper class to inherit from to automatically handle services
521 with start, stop, restart, reload actions using a /etc/init.d like script.
523 The name and directory of the script can be defined by calling the
524 constructor or in a more declarative way as class attributes, like:
526 class TestHandler(ServiceHandler):
527 _initd_name = 'some-service'
528 _initd_dir = '/usr/local/etc/init.d'
530 The default _initd_dir is '/etc/init.d', _initd_name has no default and
531 must be specified in either way.
533 Commands are executed without using the shell.
535 # TODO implement it using metaclasses to add the handlers method by demand
536 # (only for specifieds commands).
538 _initd_dir = '/etc/init.d'
540 def __init__(self, initd_name=None, initd_dir=None):
541 r"Initialize the object, see the class documentation for details."
542 if initd_name is not None:
543 self._initd_name = initd_name
544 if initd_dir is not None:
545 self._initd_dir = initd_dir
547 @handler(u'Start the service.')
549 r"start() -> None :: Start the service."
550 call((path.join(self._initd_dir, self._initd_name), 'start'))
552 @handler(u'Stop the service.')
554 r"stop() -> None :: Stop the service."
555 call((path.join(self._initd_dir, self._initd_name), 'stop'))
557 @handler(u'Restart the service.')
559 r"restart() -> None :: Restart the service."
560 call((path.join(self._initd_dir, self._initd_name), 'restart'))
562 @handler(u'Reload the service config (without restarting, if possible).')
564 r"reload() -> None :: Reload the configuration of the service."
565 call((path.join(self._initd_dir, self._initd_name), 'reload'))
567 class TransactionalHandler(Handler):
568 r"""Handle command transactions providing a commit and rollback commands.
570 This is a helper class to inherit from to automatically handle
571 transactional handlers, which have commit and rollback commands.
573 The handler should provide a reload() method (see ServiceHandler and
574 InitdHandler for helper classes to provide this) which will be called
575 when a commit command is issued (if a reload() command is present).
576 The persistent data will be written too (if a _dump() method is provided,
577 see Persistent and Restorable for that), and the configuration files
578 will be generated (if a _write_config method is present, see ConfigWriter).
580 # TODO implement it using metaclasses to add the handlers method by demand
581 # (only for specifieds commands).
583 @handler(u'Commit the changes (reloading the service, if necessary).')
585 r"commit() -> None :: Commit the changes and reload the service."
586 if hasattr(self, '_dump'):
588 if hasattr(self, '_write_config'):
590 if hasattr(self, 'reload'):
593 @handler(u'Discard all the uncommited changes.')
595 r"rollback() -> None :: Discard the changes not yet commited."
596 if hasattr(self, '_load'):
599 class ParametersHandler(Handler):
600 r"""ParametersHandler([attr]) -> ParametersHandler.
602 This is a helper class to inherit from to automatically handle
603 service parameters, providing set, get, list and show commands.
605 The attribute that holds the parameters can be defined by calling the
606 constructor or in a more declarative way as class attributes, like:
608 class TestHandler(ServiceHandler):
609 _parameters_attr = 'some_attr'
611 The default is 'params' and it should be a dictionary.
613 # TODO implement it using metaclasses to add the handlers method by demand
614 # (only for specifieds commands).
616 _parameters_attr = 'params'
618 def __init__(self, attr=None):
619 r"Initialize the object, see the class documentation for details."
621 self._parameters_attr = attr
623 @handler(u'Set a service parameter.')
624 def set(self, param, value):
625 r"set(param, value) -> None :: Set a service parameter."
626 if not param in self.params:
627 raise ParameterNotFoundError(param)
628 self.params[param] = value
629 if hasattr(self, '_update'):
632 @handler(u'Get a service parameter.')
633 def get(self, param):
634 r"get(param) -> None :: Get a service parameter."
635 if not param in self.params:
636 raise ParameterNotFoundError(param)
637 return self.params[param]
639 @handler(u'List all available service parameters.')
641 r"list() -> tuple :: List all the parameter names."
642 return self.params.keys()
644 @handler(u'Get all service parameters, with their values.')
646 r"show() -> (key, value) tuples :: List all the parameters."
647 return self.params.items()
649 class SubHandler(Handler):
650 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
652 This is a helper class to build sub handlers that needs to reference the
655 parent - Parent Handler object.
658 def __init__(self, parent):
659 r"Initialize the object, see the class documentation for details."
662 class ContainerSubHandler(SubHandler):
663 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
665 This is a helper class to implement ListSubHandler and DictSubHandler. You
666 should not use it directly.
668 The container attribute to handle and the class of objects that it
669 contains can be defined by calling the constructor or in a more declarative
670 way as class attributes, like:
672 class TestHandler(ContainerSubHandler):
673 _cont_subhandler_attr = 'some_cont'
674 _cont_subhandler_class = SomeClass
676 This way, the parent's some_cont attribute (self.parent.some_cont)
677 will be managed automatically, providing the commands: add, update,
678 delete, get and show. New items will be instances of SomeClass,
679 which should provide a cmp operator to see if the item is on the
680 container and an update() method, if it should be possible to modify
681 it. If SomeClass has an _add, _update or _delete attribute, it set
682 them to true when the item is added, updated or deleted respectively
683 (in case that it's deleted, it's not removed from the container,
684 but it's not listed either).
687 def __init__(self, parent, attr=None, cls=None):
688 r"Initialize the object, see the class documentation for details."
691 self._cont_subhandler_attr = attr
693 self._cont_subhandler_class = cls
695 def _attr(self, attr=None):
697 return getattr(self.parent, self._cont_subhandler_attr)
698 setattr(self.parent, self._cont_subhandler_attr, attr)
701 if isinstance(self._attr(), dict):
702 return dict([(k, i) for (k, i) in self._attr().items()
703 if not hasattr(i, '_delete') or not i._delete])
704 return [i for i in self._attr()
705 if not hasattr(i, '_delete') or not i._delete]
707 @handler(u'Add a new item')
708 def add(self, *args, **kwargs):
709 r"add(...) -> None :: Add an item to the list."
710 item = self._cont_subhandler_class(*args, **kwargs)
711 if hasattr(item, '_add'):
714 if isinstance(self._attr(), dict):
715 key = item.as_tuple()[0]
716 # do we have the same item? then raise an error
717 if key in self._vattr():
718 raise ItemAlreadyExistsError(item)
719 # do we have the same item, but logically deleted? then update flags
720 if key in self._attr():
722 if not isinstance(self._attr(), dict):
723 index = self._attr().index(item)
724 if hasattr(item, '_add'):
725 self._attr()[index]._add = False
726 if hasattr(item, '_delete'):
727 self._attr()[index]._delete = False
728 else: # it's *really* new
729 if isinstance(self._attr(), dict):
730 self._attr()[key] = item
732 self._attr().append(item)
734 @handler(u'Update an item')
735 def update(self, index, *args, **kwargs):
736 r"update(index, ...) -> None :: Update an item of the container."
737 # TODO make it right with metaclasses, so the method is not created
738 # unless the update() method really exists.
739 # TODO check if the modified item is the same of an existing one
740 if not isinstance(self._attr(), dict):
741 index = int(index) # TODO validation
742 if not hasattr(self._cont_subhandler_class, 'update'):
743 raise CommandNotFoundError(('update',))
745 item = self._vattr()[index]
746 item.update(*args, **kwargs)
747 if hasattr(item, '_update'):
750 raise ItemNotFoundError(index)
752 @handler(u'Delete an item')
753 def delete(self, index):
754 r"delete(index) -> None :: Delete an item of the container."
755 if not isinstance(self._attr(), dict):
756 index = int(index) # TODO validation
758 item = self._vattr()[index]
759 if hasattr(item, '_delete'):
762 del self._attr()[index]
765 raise ItemNotFoundError(index)
767 @handler(u'Remove all items (use with care).')
769 r"clear() -> None :: Delete all items of the container."
770 if isinstance(self._attr(), dict):
775 @handler(u'Get information about an item')
776 def get(self, index):
777 r"get(index) -> item :: List all the information of an item."
778 if not isinstance(self._attr(), dict):
779 index = int(index) # TODO validation
781 return self._vattr()[index]
783 raise ItemNotFoundError(index)
785 @handler(u'Get information about all items')
787 r"show() -> list of items :: List all the complete items information."
788 if isinstance(self._attr(), dict):
789 return self._attr().values()
792 class ListSubHandler(ContainerSubHandler):
793 r"""ListSubHandler(parent) -> ListSubHandler instance.
795 ContainerSubHandler holding lists. See ComposedSubHandler documentation
799 @handler(u'Get how many items are in the list')
801 r"len() -> int :: Get how many items are in the list."
802 return len(self._vattr())
804 class DictSubHandler(ContainerSubHandler):
805 r"""DictSubHandler(parent) -> DictSubHandler instance.
807 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
811 @handler(u'List all the items by key')
813 r"list() -> tuple :: List all the item keys."
814 return self._attr().keys()
816 class ComposedSubHandler(SubHandler):
817 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
819 This is a helper class to implement ListComposedSubHandler and
820 DictComposedSubHandler. You should not use it directly.
822 This class is usefull when you have a parent that has a dict (cont)
823 that stores some object that has an attribute (attr) with a list or
824 a dict of objects of some class. In that case, this class provides
825 automated commands to add, update, delete, get and show that objects.
826 This commands takes the cont (key of the dict for the object holding
827 the attr), and an index for access the object itself (in the attr
830 The container object (cont) that holds a containers, the attribute of
831 that object that is the container itself, and the class of the objects
832 that it contains can be defined by calling the constructor or in a
833 more declarative way as class attributes, like:
835 class TestHandler(ComposedSubHandler):
836 _comp_subhandler_cont = 'some_cont'
837 _comp_subhandler_attr = 'some_attr'
838 _comp_subhandler_class = SomeClass
840 This way, the parent's some_cont attribute (self.parent.some_cont)
841 will be managed automatically, providing the commands: add, update,
842 delete, get and show for manipulating a particular instance that holds
843 of SomeClass. For example, updating an item at the index 5 is the same
844 (simplified) as doing parent.some_cont[cont][5].update().
845 SomeClass should provide a cmp operator to see if the item is on the
846 container and an update() method, if it should be possible to modify
847 it. If SomeClass has an _add, _update or _delete attribute, it set
848 them to true when the item is added, updated or deleted respectively
849 (in case that it's deleted, it's not removed from the container,
850 but it's not listed either). If the container objects
851 (parent.some_cont[cont]) has an _update attribute, it's set to True
852 when any add, update or delete command is executed.
855 def __init__(self, parent, cont=None, attr=None, cls=None):
856 r"Initialize the object, see the class documentation for details."
859 self._comp_subhandler_cont = cont
861 self._comp_subhandler_attr = attr
863 self._comp_subhandler_class = cls
866 return getattr(self.parent, self._comp_subhandler_cont)
868 def _attr(self, cont, attr=None):
870 return getattr(self._cont()[cont], self._comp_subhandler_attr)
871 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
873 def _vattr(self, cont):
874 if isinstance(self._attr(cont), dict):
875 return dict([(k, i) for (k, i) in self._attr(cont).items()
876 if not hasattr(i, '_delete') or not i._delete])
877 return [i for i in self._attr(cont)
878 if not hasattr(i, '_delete') or not i._delete]
880 @handler(u'Add a new item')
881 def add(self, cont, *args, **kwargs):
882 r"add(cont, ...) -> None :: Add an item to the list."
883 if not cont in self._cont():
884 raise ContainerNotFoundError(cont)
885 item = self._comp_subhandler_class(*args, **kwargs)
886 if hasattr(item, '_add'):
889 if isinstance(self._attr(cont), dict):
890 key = item.as_tuple()[0]
891 # do we have the same item? then raise an error
892 if key in self._vattr(cont):
893 raise ItemAlreadyExistsError(item)
894 # do we have the same item, but logically deleted? then update flags
895 if key in self._attr(cont):
897 if not isinstance(self._attr(cont), dict):
898 index = self._attr(cont).index(item)
899 if hasattr(item, '_add'):
900 self._attr(cont)[index]._add = False
901 if hasattr(item, '_delete'):
902 self._attr(cont)[index]._delete = False
903 else: # it's *really* new
904 if isinstance(self._attr(cont), dict):
905 self._attr(cont)[key] = item
907 self._attr(cont).append(item)
908 if hasattr(self._cont()[cont], '_update'):
909 self._cont()[cont]._update = True
911 @handler(u'Update an item')
912 def update(self, cont, index, *args, **kwargs):
913 r"update(cont, index, ...) -> None :: Update an item of the container."
914 # TODO make it right with metaclasses, so the method is not created
915 # unless the update() method really exists.
916 # TODO check if the modified item is the same of an existing one
917 if not cont in self._cont():
918 raise ContainerNotFoundError(cont)
919 if not isinstance(self._attr(cont), dict):
920 index = int(index) # TODO validation
921 if not hasattr(self._comp_subhandler_class, 'update'):
922 raise CommandNotFoundError(('update',))
924 item = self._vattr(cont)[index]
925 item.update(*args, **kwargs)
926 if hasattr(item, '_update'):
928 if hasattr(self._cont()[cont], '_update'):
929 self._cont()[cont]._update = True
931 raise ItemNotFoundError(index)
933 @handler(u'Delete an item')
934 def delete(self, cont, index):
935 r"delete(cont, index) -> None :: Delete an item of the container."
936 if not cont in self._cont():
937 raise ContainerNotFoundError(cont)
938 if not isinstance(self._attr(cont), dict):
939 index = int(index) # TODO validation
941 item = self._vattr(cont)[index]
942 if hasattr(item, '_delete'):
945 del self._attr(cont)[index]
946 if hasattr(self._cont()[cont], '_update'):
947 self._cont()[cont]._update = True
950 raise ItemNotFoundError(index)
952 @handler(u'Remove all items (use with care).')
953 def clear(self, cont):
954 r"clear(cont) -> None :: Delete all items of the container."
955 if not cont in self._cont():
956 raise ContainerNotFoundError(cont)
957 if isinstance(self._attr(cont), dict):
958 self._attr(cont).clear()
960 self._attr(cont, list())
962 @handler(u'Get information about an item')
963 def get(self, cont, index):
964 r"get(cont, index) -> item :: List all the information of an item."
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 return self._vattr(cont)[index]
972 raise ItemNotFoundError(index)
974 @handler(u'Get information about all items')
975 def show(self, cont):
976 r"show(cont) -> list of items :: List all the complete items information."
977 if not cont in self._cont():
978 raise ContainerNotFoundError(cont)
979 if isinstance(self._attr(cont), dict):
980 return self._attr(cont).values()
981 return self._vattr(cont)
983 class ListComposedSubHandler(ComposedSubHandler):
984 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
986 ComposedSubHandler holding lists. See ComposedSubHandler documentation
990 @handler(u'Get how many items are in the list')
992 r"len(cont) -> int :: Get how many items are in the list."
993 if not cont in self._cont():
994 raise ContainerNotFoundError(cont)
995 return len(self._vattr(cont))
997 class DictComposedSubHandler(ComposedSubHandler):
998 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1000 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1004 @handler(u'List all the items by key')
1005 def list(self, cont):
1006 r"list(cont) -> tuple :: List all the item keys."
1007 if not cont in self._cont():
1008 raise ContainerNotFoundError(cont)
1009 return self._attr(cont).keys()
1012 if __name__ == '__main__':
1015 class STestHandler1(ServiceHandler):
1016 _service_start = ('service', 'start')
1017 _service_stop = ('service', 'stop')
1018 _service_restart = ('ls', '/')
1019 _service_reload = ('cp', '/la')
1020 class STestHandler2(ServiceHandler):
1022 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1023 'cmd-restart', 'cmd-reload')
1024 class ITestHandler1(InitdHandler):
1025 _initd_name = 'test1'
1026 class ITestHandler2(InitdHandler):
1028 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1036 print h.__class__.__name__
1039 except ExecutionError, e:
1043 except ExecutionError, e:
1047 except ExecutionError, e:
1051 except ExecutionError, e:
1056 print 'PTestHandler'
1057 class PTestHandler(Persistent):
1058 _persistent_attrs = 'vars'
1060 self.vars = dict(a=1, b=2)
1078 print 'RTestHandler'
1079 class RTestHandler(Restorable):
1080 _persistent_attrs = 'vars'
1081 _restorable_defaults = dict(vars=dict(a=1, b=2))
1093 print 'CTestHandler'
1095 os.mkdir('templates')
1096 f = file('templates/config', 'w')
1097 f.write('Hello, ${name}! You are ${what}.')
1100 print file('templates/config').read()
1101 class CTestHandler(ConfigWriter):
1102 _config_writer_files = 'config'
1104 self._config_build_templates()
1105 def _get_config_vars(self, config_file):
1106 return dict(name='you', what='a parrot')
1110 print file('config').read()
1112 os.unlink('templates/config')
1113 os.rmdir('templates')