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
630 @handler(u'Get a service parameter.')
631 def get(self, param):
632 r"get(param) -> None :: Get a service parameter."
633 if not param in self.params:
634 raise ParameterNotFoundError(param)
635 return self.params[param]
637 @handler(u'List all available service parameters.')
639 r"list() -> tuple :: List all the parameter names."
640 return self.params.keys()
642 @handler(u'Get all service parameters, with their values.')
644 r"show() -> (key, value) tuples :: List all the parameters."
645 return self.params.items()
647 class SubHandler(Handler):
648 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
650 This is a helper class to build sub handlers that needs to reference the
653 parent - Parent Handler object.
656 def __init__(self, parent):
657 r"Initialize the object, see the class documentation for details."
660 class ContainerSubHandler(SubHandler):
661 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
663 This is a helper class to implement ListSubHandler and DictSubHandler. You
664 should not use it directly.
666 The container attribute to handle and the class of objects that it
667 contains can be defined by calling the constructor or in a more declarative
668 way as class attributes, like:
670 class TestHandler(ContainerSubHandler):
671 _cont_subhandler_attr = 'some_cont'
672 _cont_subhandler_class = SomeClass
674 This way, the parent's some_cont attribute (self.parent.some_cont)
675 will be managed automatically, providing the commands: add, update,
676 delete, get and show. New items will be instances of SomeClass,
677 which should provide a cmp operator to see if the item is on the
678 container and an update() method, if it should be possible to modify
679 it. If SomeClass has an _add, _update or _delete attribute, it set
680 them to true when the item is added, updated or deleted respectively
681 (in case that it's deleted, it's not removed from the container,
682 but it's not listed either).
685 def __init__(self, parent, attr=None, cls=None):
686 r"Initialize the object, see the class documentation for details."
689 self._cont_subhandler_attr = attr
691 self._cont_subhandler_class = cls
693 def _attr(self, attr=None):
695 return getattr(self.parent, self._cont_subhandler_attr)
696 setattr(self.parent, self._cont_subhandler_attr, attr)
699 if isinstance(self._attr(), dict):
700 return dict([(k, i) for (k, i) in self._attr().items()
701 if not hasattr(i, '_delete') or not i._delete])
702 return [i for i in self._attr()
703 if not hasattr(i, '_delete') or not i._delete]
705 @handler(u'Add a new item')
706 def add(self, *args, **kwargs):
707 r"add(...) -> None :: Add an item to the list."
708 item = self._cont_subhandler_class(*args, **kwargs)
709 if hasattr(item, '_add'):
712 if isinstance(self._attr(), dict):
713 key = item.as_tuple()[0]
714 # do we have the same item? then raise an error
715 if key in self._vattr():
716 raise ItemAlreadyExistsError(item)
717 # do we have the same item, but logically deleted? then update flags
718 if key in self._attr():
720 if not isinstance(self._attr(), dict):
721 index = self._attr().index(item)
722 if hasattr(item, '_add'):
723 self._attr()[index]._add = False
724 if hasattr(item, '_delete'):
725 self._attr()[index]._delete = False
726 else: # it's *really* new
727 if isinstance(self._attr(), dict):
728 self._attr()[key] = item
730 self._attr().append(item)
732 @handler(u'Update an item')
733 def update(self, index, *args, **kwargs):
734 r"update(index, ...) -> None :: Update an item of the container."
735 # TODO make it right with metaclasses, so the method is not created
736 # unless the update() method really exists.
737 # TODO check if the modified item is the same of an existing one
738 if not isinstance(self._attr(), dict):
739 index = int(index) # TODO validation
740 if not hasattr(self._cont_subhandler_class, 'update'):
741 raise CommandNotFoundError(('update',))
743 item = self._vattr()[index]
744 item.update(*args, **kwargs)
745 if hasattr(item, '_update'):
748 raise ItemNotFoundError(index)
750 @handler(u'Delete an item')
751 def delete(self, index):
752 r"delete(index) -> None :: Delete an item of the container."
753 if not isinstance(self._attr(), dict):
754 index = int(index) # TODO validation
756 item = self._vattr()[index]
757 if hasattr(item, '_delete'):
760 del self._attr()[index]
763 raise ItemNotFoundError(index)
765 @handler(u'Remove all items (use with care).')
767 r"clear() -> None :: Delete all items of the container."
768 if isinstance(self._attr(), dict):
773 @handler(u'Get information about an item')
774 def get(self, index):
775 r"get(index) -> item :: List all the information of an item."
776 if not isinstance(self._attr(), dict):
777 index = int(index) # TODO validation
779 return self._vattr()[index]
781 raise ItemNotFoundError(index)
783 @handler(u'Get information about all items')
785 r"show() -> list of items :: List all the complete items information."
786 if isinstance(self._attr(), dict):
787 return self._attr().values()
790 class ListSubHandler(ContainerSubHandler):
791 r"""ListSubHandler(parent) -> ListSubHandler instance.
793 ContainerSubHandler holding lists. See ComposedSubHandler documentation
797 @handler(u'Get how many items are in the list')
799 r"len() -> int :: Get how many items are in the list."
800 return len(self._vattr())
802 class DictSubHandler(ContainerSubHandler):
803 r"""DictSubHandler(parent) -> DictSubHandler instance.
805 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
809 @handler(u'List all the items by key')
811 r"list() -> tuple :: List all the item keys."
812 return self._attr().keys()
814 class ComposedSubHandler(SubHandler):
815 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
817 This is a helper class to implement ListComposedSubHandler and
818 DictComposedSubHandler. You should not use it directly.
820 This class is usefull when you have a parent that has a dict (cont)
821 that stores some object that has an attribute (attr) with a list or
822 a dict of objects of some class. In that case, this class provides
823 automated commands to add, update, delete, get and show that objects.
824 This commands takes the cont (key of the dict for the object holding
825 the attr), and an index for access the object itself (in the attr
828 The container object (cont) that holds a containers, the attribute of
829 that object that is the container itself, and the class of the objects
830 that it contains can be defined by calling the constructor or in a
831 more declarative way as class attributes, like:
833 class TestHandler(ComposedSubHandler):
834 _comp_subhandler_cont = 'some_cont'
835 _comp_subhandler_attr = 'some_attr'
836 _comp_subhandler_class = SomeClass
838 This way, the parent's some_cont attribute (self.parent.some_cont)
839 will be managed automatically, providing the commands: add, update,
840 delete, get and show for manipulating a particular instance that holds
841 of SomeClass. For example, updating an item at the index 5 is the same
842 (simplified) as doing parent.some_cont[cont][5].update().
843 SomeClass should provide a cmp operator to see if the item is on the
844 container and an update() method, if it should be possible to modify
845 it. If SomeClass has an _add, _update or _delete attribute, it set
846 them to true when the item is added, updated or deleted respectively
847 (in case that it's deleted, it's not removed from the container,
848 but it's not listed either). If the container objects
849 (parent.some_cont[cont]) has an _update attribute, it's set to True
850 when any add, update or delete command is executed.
853 def __init__(self, parent, cont=None, attr=None, cls=None):
854 r"Initialize the object, see the class documentation for details."
857 self._comp_subhandler_cont = cont
859 self._comp_subhandler_attr = attr
861 self._comp_subhandler_class = cls
864 return getattr(self.parent, self._comp_subhandler_cont)
866 def _attr(self, cont, attr=None):
868 return getattr(self._cont()[cont], self._comp_subhandler_attr)
869 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
871 def _vattr(self, cont):
872 if isinstance(self._attr(cont), dict):
873 return dict([(k, i) for (k, i) in self._attr(cont).items()
874 if not hasattr(i, '_delete') or not i._delete])
875 return [i for i in self._attr(cont)
876 if not hasattr(i, '_delete') or not i._delete]
878 @handler(u'Add a new item')
879 def add(self, cont, *args, **kwargs):
880 r"add(cont, ...) -> None :: Add an item to the list."
881 if not cont in self._cont():
882 raise ContainerNotFoundError(cont)
883 item = self._comp_subhandler_class(*args, **kwargs)
884 if hasattr(item, '_add'):
887 if isinstance(self._attr(cont), dict):
888 key = item.as_tuple()[0]
889 # do we have the same item? then raise an error
890 if key in self._vattr(cont):
891 raise ItemAlreadyExistsError(item)
892 # do we have the same item, but logically deleted? then update flags
893 if key in self._attr(cont):
895 if not isinstance(self._attr(cont), dict):
896 index = self._attr(cont).index(item)
897 if hasattr(item, '_add'):
898 self._attr(cont)[index]._add = False
899 if hasattr(item, '_delete'):
900 self._attr(cont)[index]._delete = False
901 else: # it's *really* new
902 if isinstance(self._attr(cont), dict):
903 self._attr(cont)[key] = item
905 self._attr(cont).append(item)
906 if hasattr(self._cont()[cont], '_update'):
907 self._cont()[cont]._update = True
909 @handler(u'Update an item')
910 def update(self, cont, index, *args, **kwargs):
911 r"update(cont, index, ...) -> None :: Update an item of the container."
912 # TODO make it right with metaclasses, so the method is not created
913 # unless the update() method really exists.
914 # TODO check if the modified item is the same of an existing one
915 if not cont in self._cont():
916 raise ContainerNotFoundError(cont)
917 if not isinstance(self._attr(cont), dict):
918 index = int(index) # TODO validation
919 if not hasattr(self._comp_subhandler_class, 'update'):
920 raise CommandNotFoundError(('update',))
922 item = self._vattr(cont)[index]
923 item.update(*args, **kwargs)
924 if hasattr(item, '_update'):
926 if hasattr(self._cont()[cont], '_update'):
927 self._cont()[cont]._update = True
929 raise ItemNotFoundError(index)
931 @handler(u'Delete an item')
932 def delete(self, cont, index):
933 r"delete(cont, index) -> None :: Delete an item of the container."
934 if not cont in self._cont():
935 raise ContainerNotFoundError(cont)
936 if not isinstance(self._attr(cont), dict):
937 index = int(index) # TODO validation
939 item = self._vattr(cont)[index]
940 if hasattr(item, '_delete'):
943 del self._attr(cont)[index]
944 if hasattr(self._cont()[cont], '_update'):
945 self._cont()[cont]._update = True
948 raise ItemNotFoundError(index)
950 @handler(u'Remove all items (use with care).')
951 def clear(self, cont):
952 r"clear(cont) -> None :: Delete all items of the container."
953 if not cont in self._cont():
954 raise ContainerNotFoundError(cont)
955 if isinstance(self._attr(cont), dict):
956 self._attr(cont).clear()
958 self._attr(cont, list())
960 @handler(u'Get information about an item')
961 def get(self, cont, index):
962 r"get(cont, index) -> item :: List all the information of an item."
963 if not cont in self._cont():
964 raise ContainerNotFoundError(cont)
965 if not isinstance(self._attr(cont), dict):
966 index = int(index) # TODO validation
968 return self._vattr(cont)[index]
970 raise ItemNotFoundError(index)
972 @handler(u'Get information about all items')
973 def show(self, cont):
974 r"show(cont) -> list of items :: List all the complete items information."
975 if not cont in self._cont():
976 raise ContainerNotFoundError(cont)
977 if isinstance(self._attr(cont), dict):
978 return self._attr(cont).values()
979 return self._vattr(cont)
981 class ListComposedSubHandler(ComposedSubHandler):
982 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
984 ComposedSubHandler holding lists. See ComposedSubHandler documentation
988 @handler(u'Get how many items are in the list')
990 r"len(cont) -> int :: Get how many items are in the list."
991 if not cont in self._cont():
992 raise ContainerNotFoundError(cont)
993 return len(self._vattr(cont))
995 class DictComposedSubHandler(ComposedSubHandler):
996 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
998 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1002 @handler(u'List all the items by key')
1003 def list(self, cont):
1004 r"list(cont) -> tuple :: List all the item keys."
1005 if not cont in self._cont():
1006 raise ContainerNotFoundError(cont)
1007 return self._attr(cont).keys()
1010 if __name__ == '__main__':
1013 class STestHandler1(ServiceHandler):
1014 _service_start = ('service', 'start')
1015 _service_stop = ('service', 'stop')
1016 _service_restart = ('ls', '/')
1017 _service_reload = ('cp', '/la')
1018 class STestHandler2(ServiceHandler):
1020 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1021 'cmd-restart', 'cmd-reload')
1022 class ITestHandler1(InitdHandler):
1023 _initd_name = 'test1'
1024 class ITestHandler2(InitdHandler):
1026 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1034 print h.__class__.__name__
1037 except ExecutionError, e:
1041 except ExecutionError, e:
1045 except ExecutionError, e:
1049 except ExecutionError, e:
1054 print 'PTestHandler'
1055 class PTestHandler(Persistent):
1056 _persistent_attrs = 'vars'
1058 self.vars = dict(a=1, b=2)
1076 print 'RTestHandler'
1077 class RTestHandler(Restorable):
1078 _persistent_attrs = 'vars'
1079 _restorable_defaults = dict(vars=dict(a=1, b=2))
1091 print 'CTestHandler'
1093 os.mkdir('templates')
1094 f = file('templates/config', 'w')
1095 f.write('Hello, ${name}! You are ${what}.')
1098 print file('templates/config').read()
1099 class CTestHandler(ConfigWriter):
1100 _config_writer_files = 'config'
1102 self._config_build_templates()
1103 def _get_config_vars(self, config_file):
1104 return dict(name='you', what='a parrot')
1108 print file('config').read()
1110 os.unlink('templates/config')
1111 os.rmdir('templates')