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, \
14 from pymin.seqtools import Sequence
19 __ALL__ = ('Error', 'ExecutionError', 'ItemError', 'ItemAlreadyExistsError',
20 'ItemNotFoundError', 'ContainerError', 'ContainerNotFoundError',
21 'call', 'get_network_devices', 'Persistent', 'Restorable',
22 'ConfigWriter', 'ServiceHandler', 'RestartHandler',
23 'ReloadHandler', 'InitdHandler', 'SubHandler', 'DictSubHandler',
24 'ListSubHandler', 'ComposedSubHandler', 'ListComposedSubHandler',
25 'DictComposedSubHandler', 'Device','Address')
27 class Error(HandlerError):
29 Error(message) -> Error instance :: Base ServiceHandler exception class.
31 All exceptions raised by the ServiceHandler inherits from this one, so
32 you can easily catch any ServiceHandler exception.
34 message - A descriptive error message.
38 class ExecutionError(Error):
40 ExecutionError(command, error) -> ExecutionError instance.
42 Error executing a command.
44 command - Command that was tried to execute.
46 error - Error received when trying to execute the command.
49 def __init__(self, command, error):
50 r"Initialize the object. See class documentation for more info."
51 self.command = command
54 def __unicode__(self):
55 command = self.command
56 if not isinstance(self.command, basestring):
57 command = ' '.join(command)
58 return "Can't execute command %s: %s" % (command, self.error)
60 class ParameterError(Error, KeyError):
62 ParameterError(paramname) -> ParameterError instance
64 This is the base exception for all DhcpHandler parameters related errors.
67 def __init__(self, paramname):
68 r"Initialize the object. See class documentation for more info."
69 self.message = 'Parameter error: "%s"' % paramname
71 class ParameterNotFoundError(ParameterError):
73 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
75 This exception is raised when trying to operate on a parameter that doesn't
79 def __init__(self, paramname):
80 r"Initialize the object. See class documentation for more info."
81 self.message = 'Parameter not found: "%s"' % paramname
83 class ItemError(Error, KeyError):
85 ItemError(key) -> ItemError instance.
87 This is the base exception for all item related errors.
90 def __init__(self, key):
91 r"Initialize the object. See class documentation for more info."
92 self.message = u'Item error: "%s"' % key
94 class ItemAlreadyExistsError(ItemError):
96 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
98 This exception is raised when trying to add an item that already exists.
101 def __init__(self, key):
102 r"Initialize the object. See class documentation for more info."
103 self.message = u'Item already exists: "%s"' % key
105 class ItemNotFoundError(ItemError):
107 ItemNotFoundError(key) -> ItemNotFoundError instance.
109 This exception is raised when trying to operate on an item that doesn't
113 def __init__(self, key):
114 r"Initialize the object. See class documentation for more info."
115 self.message = u'Item not found: "%s"' % key
117 class ContainerError(Error, KeyError):
119 ContainerError(key) -> ContainerError instance.
121 This is the base exception for all container related errors.
124 def __init__(self, key):
125 r"Initialize the object. See class documentation for more info."
126 self.message = u'Container error: "%s"' % key
128 class ContainerNotFoundError(ContainerError):
130 ContainerNotFoundError(key) -> ContainerNotFoundError instance.
132 This exception is raised when trying to operate on an container that
136 def __init__(self, key):
137 r"Initialize the object. See class documentation for more info."
138 self.message = u'Container not found: "%s"' % key
140 class Address(Sequence):
141 def __init__(self, ip, netmask, broadcast=None, peer=None):
143 self.netmask = netmask
144 self.broadcast = broadcast
146 def update(self, netmask=None, broadcast=None):
147 if netmask is not None: self.netmask = netmask
148 if broadcast is not None: self.broadcast = broadcast
150 return (self.ip, self.netmask, self.broadcast, self.peer)
153 class Device(Sequence):
154 def __init__(self, name, mac, ppp):
162 return (self.name, self.mac, self.active, self.addrs)
166 def get_network_devices():
167 p = subprocess.Popen(('ip', '-o', 'link'), stdout=subprocess.PIPE,
169 string = p.stdout.read()
172 devices = string.splitlines()
175 if dev.find('link/ether') != -1:
176 i = dev.find('link/ether')
177 mac = dev[i+11 : i+11+17]
179 j = dev.find(':', i+1)
181 d[name] = Device(name,mac,False)
182 elif dev.find('link/ppp') != -1:
183 i = dev.find('link/ppp')
184 mac = '00:00:00:00:00:00'
186 j = dev.find(':', i+1)
188 d[name] = Device(name,mac,True)
189 #since the device is ppp, get the address and peer
191 p = subprocess.Popen(('ip', '-o', 'addr', 'show', name), stdout=subprocess.PIPE,
192 close_fds=True, stderr=subprocess.PIPE)
193 string = p.stdout.read()
195 addrs = string.splitlines()
196 inet = addrs[1].find('inet')
197 peer = addrs[1].find('peer')
198 bar = addrs[1].find('/')
199 from_addr = addrs[1][inet+5 : peer-1]
200 to_addr = addrs[1][peer+5 : bar]
201 d[name].addrs[from_addr] = Address(from_addr,24, peer=to_addr)
207 p = subprocess.Popen(('ip', '-o', 'addr'), stdout=subprocess.PIPE,
210 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
211 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
214 if not isinstance(command, basestring):
215 command = ' '.join(command)
216 print 'Executing command:', command
219 print 'Executing command:', command
220 r = subprocess.check_call(command, stdin=stdin, stdout=stdout,
221 stderr=stderr, close_fds=close_fds,
222 universal_newlines=universal_newlines, **kw)
224 raise ExecutionError(command, e)
227 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
229 This is a helper class to inherit from to automatically handle data
230 persistence using pickle.
232 The variables attributes to persist (attrs), and the pickle directory (dir)
233 and file extension (ext) can be defined by calling the constructor or in a
234 more declarative way as class attributes, like:
236 class TestHandler(Persistent):
237 _persistent_attrs = ('some_attr', 'other_attr')
238 _persistent_dir = 'persistent-data'
239 _persistent_ext = '.pickle'
241 The default dir is '.' and the default extension is '.pkl'. There are no
242 default variables, and they should be specified as string if a single
243 attribute should be persistent or as a tuple of strings if they are more.
244 The strings should be the attribute names to be persisted. For each
245 attribute a separated pickle file is generated in the pickle directory.
247 You can call _dump() and _load() to write and read the data respectively.
249 # TODO implement it using metaclasses to add the handlers method by demand
250 # (only for specifieds commands).
252 _persistent_attrs = ()
253 _persistent_dir = '.'
254 _persistent_ext = '.pkl'
256 def __init__(self, attrs=None, dir=None, ext=None):
257 r"Initialize the object, see the class documentation for details."
258 if attrs is not None:
259 self._persistent_attrs = attrs
261 self._persistent_dir = dir
263 self._persistent_ext = ext
266 r"_dump() -> None :: Dump all persistent data to pickle files."
267 if isinstance(self._persistent_attrs, basestring):
268 self._persistent_attrs = (self._persistent_attrs,)
269 for attrname in self._persistent_attrs:
270 self._dump_attr(attrname)
273 r"_load() -> None :: Load all persistent data from pickle files."
274 if isinstance(self._persistent_attrs, basestring):
275 self._persistent_attrs = (self._persistent_attrs,)
276 for attrname in self._persistent_attrs:
277 self._load_attr(attrname)
279 def _dump_attr(self, attrname):
280 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
281 f = file(self._pickle_filename(attrname), 'wb')
282 pickle.dump(getattr(self, attrname), f, 2)
285 def _load_attr(self, attrname):
286 r"_load_attr() -> object :: Load a specific pickle file."
287 f = file(self._pickle_filename(attrname))
288 setattr(self, attrname, pickle.load(f))
291 def _pickle_filename(self, name):
292 r"_pickle_filename() -> string :: Construct a pickle filename."
293 return path.join(self._persistent_dir, name) + self._persistent_ext
295 class Restorable(Persistent):
296 r"""Restorable([defaults]) -> Restorable.
298 This is a helper class to inherit from that provides a nice _restore()
299 method to restore the persistent data if any, or load some nice defaults
302 The defaults can be defined by calling the constructor or in a more
303 declarative way as class attributes, like:
305 class TestHandler(Restorable):
306 _persistent_attrs = ('some_attr', 'other_attr')
307 _restorable_defaults = dict(
308 some_attr = 'some_default',
309 other_attr = 'other_default')
311 The defaults is a dictionary, very coupled with the _persistent_attrs
312 attribute inherited from Persistent. The defaults keys should be the
313 values from _persistent_attrs, and the values the default values.
315 The _restore() method returns True if the data was restored successfully
316 or False if the defaults were loaded (in case you want to take further
317 actions). If a _write_config method if found, it's executed when a restore
320 # TODO implement it using metaclasses to add the handlers method by demand
321 # (only for specifieds commands).
323 _restorable_defaults = dict()
325 def __init__(self, defaults=None):
326 r"Initialize the object, see the class documentation for details."
327 if defaults is not None:
328 self._restorable_defaults = defaults
331 r"_restore() -> bool :: Restore persistent data or create a default."
336 for (k, v) in self._restorable_defaults.items():
338 # TODO tener en cuenta servicios que hay que levantar y los que no
339 if hasattr(self, 'commit'):
343 if hasattr(self, '_write_config'):
348 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
350 This is a helper class to inherit from to automatically handle
351 configuration generation. Mako template system is used for configuration
354 The configuration filenames, the generated configuration files directory
355 and the templates directory can be defined by calling the constructor or
356 in a more declarative way as class attributes, like:
358 class TestHandler(ConfigWriter):
359 _config_writer_files = ('base.conf', 'custom.conf')
360 _config_writer_cfg_dir = {
361 'base.conf': '/etc/service',
362 'custom.conf': '/etc/service/conf.d',
364 _config_writer_tpl_dir = 'templates'
366 The generated configuration files directory defaults to '.' and the
367 templates directory to 'templates'. _config_writer_files has no default and
368 must be specified in either way. It can be string or a tuple if more than
369 one configuration file must be generated. _config_writer_cfg_dir could be a
370 dict mapping which file should be stored in which directory, or a single
371 string if all the config files should go to the same directory.
373 The template filename and the generated configuration filename are both the
374 same (so if you want to generate some /etc/config, you should have some
375 templates/config template). That's why _config_writer_cfg_dir and
376 _config_writer_tpl_dir can't be the same. This is not true for very
377 specific cases where _write_single_config() is used.
379 When you write your Handler, you should call _config_build_templates() in
380 you Handler constructor to build the templates.
382 To write the configuration files, you must use the _write_config() method.
383 To know what variables to replace in the template, you have to provide a
384 method called _get_config_vars(tamplate_name), which should return a
385 dictionary of variables to pass to the template system to be replaced in
386 the template for the configuration file 'config_file'.
388 # TODO implement it using metaclasses to add the handlers method by demand
389 # (only for specifieds commands).
391 _config_writer_files = ()
392 _config_writer_cfg_dir = '.'
393 _config_writer_tpl_dir = 'templates'
395 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
396 r"Initialize the object, see the class documentation for details."
397 if files is not None:
398 self._config_writer_files = files
399 if cfg_dir is not None:
400 self._config_writer_cfg_dir = cfg_dir
401 if tpl_dir is not None:
402 self._config_writer_tpl_dir = tpl_dir
403 self._config_build_templates()
405 def _config_build_templates(self):
406 r"_config_writer_templates() -> None :: Build the template objects."
407 if isinstance(self._config_writer_files, basestring):
408 self._config_writer_files = (self._config_writer_files,)
409 if not hasattr(self, '_config_writer_templates') \
410 or not self._config_writer_templates:
411 self._config_writer_templates = dict()
412 for t in self._config_writer_files:
413 f = path.join(self._config_writer_tpl_dir, t)
414 self._config_writer_templates[t] = Template(filename=f)
416 def _render_config(self, template_name, vars=None):
417 r"""_render_config(template_name[, config_filename[, vars]]).
419 Render a single config file using the template 'template_name'. If
420 vars is specified, it's used as the dictionary with the variables
421 to replace in the templates, if not, it looks for a
422 _get_config_vars() method to get it.
425 if hasattr(self, '_get_config_vars'):
426 vars = self._get_config_vars(template_name)
430 vars = vars(template_name)
431 return self._config_writer_templates[template_name].render(**vars)
433 def _get_config_path(self, template_name, config_filename=None):
434 r"Get a complete configuration path."
435 if not config_filename:
436 config_filename = template_name
437 if isinstance(self._config_writer_cfg_dir, basestring):
438 return path.join(self._config_writer_cfg_dir, config_filename)
439 return path.join(self._config_writer_cfg_dir[template_name],
442 def _write_single_config(self, template_name, config_filename=None, vars=None):
443 r"""_write_single_config(template_name[, config_filename[, vars]]).
445 Write a single config file using the template 'template_name'. If no
446 config_filename is specified, the config filename will be the same as
447 the 'template_name' (but stored in the generated config files
448 directory). If it's specified, the generated config file is stored in
449 the file called 'config_filename' (also in the generated files
450 directory). If vars is specified, it's used as the dictionary with the
451 variables to replace in the templates, if not, it looks for a
452 _get_config_vars() method to get it.
455 if hasattr(self, '_get_config_vars'):
456 vars = self._get_config_vars(template_name)
460 vars = vars(template_name)
461 f = file(self._get_config_path(template_name, config_filename), 'w')
462 ctx = Context(f, **vars)
463 self._config_writer_templates[template_name].render_context(ctx)
466 def _write_config(self):
467 r"_write_config() -> None :: Generate all the configuration files."
468 for t in self._config_writer_files:
469 self._write_single_config(t)
472 class ServiceHandler(Handler, Restorable):
473 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
475 This is a helper class to inherit from to automatically handle services
476 with start, stop, restart, reload actions.
478 The actions can be defined by calling the constructor with all the
479 parameters or in a more declarative way as class attributes, like:
481 class TestHandler(ServiceHandler):
482 _service_start = ('command', 'start')
483 _service_stop = ('command', 'stop')
484 _service_restart = ('command', 'restart')
485 _service_reload = 'reload-command'
487 Commands are executed without using the shell, that's why they are specified
488 as tuples (where the first element is the command and the others are the
489 command arguments). If only a command is needed (without arguments) a single
490 string can be specified.
492 All commands must be specified.
494 # TODO implement it using metaclasses to add the handlers method by demand
495 # (only for specifieds commands).
497 def __init__(self, start=None, stop=None, restart=None, reload=None):
498 r"Initialize the object, see the class documentation for details."
499 for (name, action) in dict(start=start, stop=stop, restart=restart,
500 reload=reload).items():
501 if action is not None:
502 setattr(self, '_service_%s' % name, action)
503 self._persistent_attrs = list(self._persistent_attrs)
504 self._persistent_attrs.append('_service_running')
505 if '_service_running' not in self._restorable_defaults:
506 self._restorable_defaults['_service_running'] = False
508 if self._service_running:
509 self._service_running = False
512 @handler(u'Start the service.')
514 r"start() -> None :: Start the service."
515 if not self._service_running:
516 if callable(self._service_start):
517 self._service_start()
519 call(self._service_start)
520 self._service_running = True
521 self._dump_attr('_service_running')
523 @handler(u'Stop the service.')
525 r"stop() -> None :: Stop the service."
526 if self._service_running:
527 if callable(self._service_stop):
530 call(self._service_stop)
531 self._service_running = False
532 self._dump_attr('_service_running')
534 @handler(u'Restart the service.')
536 r"restart() -> None :: Restart the service."
537 if callable(self._service_restart):
538 self._service_restart()
540 call(self._service_restart)
541 self._service_running = True
542 self._dump_attr('_service_running')
544 @handler(u'Reload the service config (without restarting, if possible).')
546 r"reload() -> None :: Reload the configuration of the service."
547 if self._service_running:
548 if callable(self._service_reload):
549 self._service_reload()
551 call(self._service_reload)
553 @handler(u'Tell if the service is running.')
555 r"reload() -> None :: Reload the configuration of the service."
556 if self._service_running:
561 class RestartHandler(Handler):
562 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
564 This is a helper class to inherit from to automatically add a restart
565 command that first stop the service and then starts it again (using start
566 and stop commands respectively).
569 @handler(u'Restart the service (alias to stop + start).')
571 r"restart() -> None :: Restart the service calling stop() and start()."
575 class ReloadHandler(Handler):
576 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
578 This is a helper class to inherit from to automatically add a reload
579 command that calls restart.
582 @handler(u'Reload the service config (alias to restart).')
584 r"reload() -> None :: Reload the configuration of the service."
585 if hasattr(self, '_service_running') and self._service_running:
588 class InitdHandler(ServiceHandler):
589 # TODO update docs, declarative style is depracated
590 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
592 This is a helper class to inherit from to automatically handle services
593 with start, stop, restart, reload actions using a /etc/init.d like script.
595 The name and directory of the script can be defined by calling the
596 constructor or in a more declarative way as class attributes, like:
598 class TestHandler(ServiceHandler):
599 _initd_name = 'some-service'
600 _initd_dir = '/usr/local/etc/init.d'
602 The default _initd_dir is '/etc/init.d', _initd_name has no default and
603 must be specified in either way.
605 Commands are executed without using the shell.
607 # TODO implement it using metaclasses to add the handlers method by demand
608 # (only for specifieds commands).
610 _initd_dir = '/etc/init.d'
612 def __init__(self, initd_name=None, initd_dir=None):
613 r"Initialize the object, see the class documentation for details."
614 if initd_name is not None:
615 self._initd_name = initd_name
616 if initd_dir is not None:
617 self._initd_dir = initd_dir
619 for action in ('start', 'stop', 'restart', 'reload'):
620 actions[action] = (path.join(self._initd_dir, self._initd_name),
622 ServiceHandler.__init__(self, **actions)
624 def handle_timer(self):
625 p = subprocess.Popen(('pgrep', '-f', self._initd_name),
626 stdout=subprocess.PIPE)
627 pid = p.communicate()[0]
628 if p.returncode == 0 and len(pid) > 0:
629 self._service_running = True
631 self._service_running = False
633 class TransactionalHandler(Handler):
634 r"""Handle command transactions providing a commit and rollback commands.
636 This is a helper class to inherit from to automatically handle
637 transactional handlers, which have commit and rollback commands.
639 The handler should provide a reload() method (see ServiceHandler and
640 InitdHandler for helper classes to provide this) which will be called
641 when a commit command is issued (if a reload() command is present).
642 The persistent data will be written too (if a _dump() method is provided,
643 see Persistent and Restorable for that), and the configuration files
644 will be generated (if a _write_config method is present, see ConfigWriter).
646 # TODO implement it using metaclasses to add the handlers method by demand
647 # (only for specifieds commands).
649 @handler(u'Commit the changes (reloading the service, if necessary).')
651 r"commit() -> None :: Commit the changes and reload the service."
652 if hasattr(self, '_dump'):
655 if hasattr(self, '_write_config'):
656 unchanged = self._write_config()
657 if not unchanged and hasattr(self, 'reload'):
660 @handler(u'Discard all the uncommited changes.')
662 r"rollback() -> None :: Discard the changes not yet commited."
663 if hasattr(self, '_load'):
666 class ParametersHandler(Handler):
667 r"""ParametersHandler([attr]) -> ParametersHandler.
669 This is a helper class to inherit from to automatically handle
670 service parameters, providing set, get, list and show commands.
672 The attribute that holds the parameters can be defined by calling the
673 constructor or in a more declarative way as class attributes, like:
675 class TestHandler(ServiceHandler):
676 _parameters_attr = 'some_attr'
678 The default is 'params' and it should be a dictionary.
680 # TODO implement it using metaclasses to add the handlers method by demand
681 # (only for specifieds commands).
683 _parameters_attr = 'params'
685 def __init__(self, attr=None):
686 r"Initialize the object, see the class documentation for details."
688 self._parameters_attr = attr
690 @handler(u'Set a service parameter.')
691 def set(self, param, value):
692 r"set(param, value) -> None :: Set a service parameter."
693 if not param in self.params:
694 raise ParameterNotFoundError(param)
695 self.params[param] = value
696 if hasattr(self, '_update'):
699 @handler(u'Get a service parameter.')
700 def get(self, param):
701 r"get(param) -> None :: Get a service parameter."
702 if not param in self.params:
703 raise ParameterNotFoundError(param)
704 return self.params[param]
706 @handler(u'List all available service parameters.')
708 r"list() -> tuple :: List all the parameter names."
709 return self.params.keys()
711 @handler(u'Get all service parameters, with their values.')
713 r"show() -> (key, value) tuples :: List all the parameters."
714 return self.params.items()
716 class SubHandler(Handler):
717 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
719 This is a helper class to build sub handlers that needs to reference the
722 parent - Parent Handler object.
725 def __init__(self, parent):
726 r"Initialize the object, see the class documentation for details."
729 class ContainerSubHandler(SubHandler):
730 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
732 This is a helper class to implement ListSubHandler and DictSubHandler. You
733 should not use it directly.
735 The container attribute to handle and the class of objects that it
736 contains can be defined by calling the constructor or in a more declarative
737 way as class attributes, like:
739 class TestHandler(ContainerSubHandler):
740 _cont_subhandler_attr = 'some_cont'
741 _cont_subhandler_class = SomeClass
743 This way, the parent's some_cont attribute (self.parent.some_cont)
744 will be managed automatically, providing the commands: add, update,
745 delete, get and show. New items will be instances of SomeClass,
746 which should provide a cmp operator to see if the item is on the
747 container and an update() method, if it should be possible to modify
748 it. If SomeClass has an _add, _update or _delete attribute, it set
749 them to true when the item is added, updated or deleted respectively
750 (in case that it's deleted, it's not removed from the container,
751 but it's not listed either).
754 def __init__(self, parent, attr=None, cls=None):
755 r"Initialize the object, see the class documentation for details."
758 self._cont_subhandler_attr = attr
760 self._cont_subhandler_class = cls
762 def _attr(self, attr=None):
764 return getattr(self.parent, self._cont_subhandler_attr)
765 setattr(self.parent, self._cont_subhandler_attr, attr)
768 if isinstance(self._attr(), dict):
769 return dict([(k, i) for (k, i) in self._attr().items()
770 if not hasattr(i, '_delete') or not i._delete])
771 return [i for i in self._attr()
772 if not hasattr(i, '_delete') or not i._delete]
774 @handler(u'Add a new item')
775 def add(self, *args, **kwargs):
776 r"add(...) -> None :: Add an item to the list."
777 item = self._cont_subhandler_class(*args, **kwargs)
778 if hasattr(item, '_add'):
781 if isinstance(self._attr(), dict):
782 key = item.as_tuple()[0]
783 # do we have the same item? then raise an error
784 if key in self._vattr():
785 raise ItemAlreadyExistsError(item)
786 # do we have the same item, but logically deleted? then update flags
787 if key in self._attr():
789 if not isinstance(self._attr(), dict):
790 index = self._attr().index(item)
791 if hasattr(item, '_add'):
792 self._attr()[index]._add = False
793 if hasattr(item, '_delete'):
794 self._attr()[index]._delete = False
795 else: # it's *really* new
796 if isinstance(self._attr(), dict):
797 self._attr()[key] = item
799 self._attr().append(item)
801 @handler(u'Update an item')
802 def update(self, index, *args, **kwargs):
803 r"update(index, ...) -> None :: Update an item of the container."
804 # TODO make it right with metaclasses, so the method is not created
805 # unless the update() method really exists.
806 # TODO check if the modified item is the same of an existing one
807 if not isinstance(self._attr(), dict):
808 index = int(index) # TODO validation
809 if not hasattr(self._cont_subhandler_class, 'update'):
810 raise CommandNotFoundError(('update',))
812 item = self._vattr()[index]
813 item.update(*args, **kwargs)
814 if hasattr(item, '_update'):
817 raise ItemNotFoundError(index)
819 @handler(u'Delete an item')
820 def delete(self, index):
821 r"delete(index) -> None :: Delete an item of the container."
822 if not isinstance(self._attr(), dict):
823 index = int(index) # TODO validation
825 item = self._vattr()[index]
826 if hasattr(item, '_delete'):
829 del self._attr()[index]
832 raise ItemNotFoundError(index)
834 @handler(u'Remove all items (use with care).')
836 r"clear() -> None :: Delete all items of the container."
837 if isinstance(self._attr(), dict):
842 @handler(u'Get information about an item')
843 def get(self, index):
844 r"get(index) -> item :: List all the information of an item."
845 if not isinstance(self._attr(), dict):
846 index = int(index) # TODO validation
848 return self._vattr()[index]
850 raise ItemNotFoundError(index)
852 @handler(u'Get information about all items')
854 r"show() -> list of items :: List all the complete items information."
855 if isinstance(self._attr(), dict):
856 return self._attr().values()
859 class ListSubHandler(ContainerSubHandler):
860 r"""ListSubHandler(parent) -> ListSubHandler instance.
862 ContainerSubHandler holding lists. See ComposedSubHandler documentation
866 @handler(u'Get how many items are in the list')
868 r"len() -> int :: Get how many items are in the list."
869 return len(self._vattr())
871 class DictSubHandler(ContainerSubHandler):
872 r"""DictSubHandler(parent) -> DictSubHandler instance.
874 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
878 @handler(u'List all the items by key')
880 r"list() -> tuple :: List all the item keys."
881 return self._attr().keys()
883 class ComposedSubHandler(SubHandler):
884 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
886 This is a helper class to implement ListComposedSubHandler and
887 DictComposedSubHandler. You should not use it directly.
889 This class is usefull when you have a parent that has a dict (cont)
890 that stores some object that has an attribute (attr) with a list or
891 a dict of objects of some class. In that case, this class provides
892 automated commands to add, update, delete, get and show that objects.
893 This commands takes the cont (key of the dict for the object holding
894 the attr), and an index for access the object itself (in the attr
897 The container object (cont) that holds a containers, the attribute of
898 that object that is the container itself, and the class of the objects
899 that it contains can be defined by calling the constructor or in a
900 more declarative way as class attributes, like:
902 class TestHandler(ComposedSubHandler):
903 _comp_subhandler_cont = 'some_cont'
904 _comp_subhandler_attr = 'some_attr'
905 _comp_subhandler_class = SomeClass
907 This way, the parent's some_cont attribute (self.parent.some_cont)
908 will be managed automatically, providing the commands: add, update,
909 delete, get and show for manipulating a particular instance that holds
910 of SomeClass. For example, updating an item at the index 5 is the same
911 (simplified) as doing parent.some_cont[cont][5].update().
912 SomeClass should provide a cmp operator to see if the item is on the
913 container and an update() method, if it should be possible to modify
914 it. If SomeClass has an _add, _update or _delete attribute, it set
915 them to true when the item is added, updated or deleted respectively
916 (in case that it's deleted, it's not removed from the container,
917 but it's not listed either). If the container objects
918 (parent.some_cont[cont]) has an _update attribute, it's set to True
919 when any add, update or delete command is executed.
922 def __init__(self, parent, cont=None, attr=None, cls=None):
923 r"Initialize the object, see the class documentation for details."
926 self._comp_subhandler_cont = cont
928 self._comp_subhandler_attr = attr
930 self._comp_subhandler_class = cls
933 return getattr(self.parent, self._comp_subhandler_cont)
935 def _attr(self, cont, attr=None):
937 return getattr(self._cont()[cont], self._comp_subhandler_attr)
938 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
940 def _vattr(self, cont):
941 if isinstance(self._attr(cont), dict):
942 return dict([(k, i) for (k, i) in self._attr(cont).items()
943 if not hasattr(i, '_delete') or not i._delete])
944 return [i for i in self._attr(cont)
945 if not hasattr(i, '_delete') or not i._delete]
947 @handler(u'Add a new item')
948 def add(self, cont, *args, **kwargs):
949 r"add(cont, ...) -> None :: Add an item to the list."
950 if not cont in self._cont():
951 raise ContainerNotFoundError(cont)
952 item = self._comp_subhandler_class(*args, **kwargs)
953 if hasattr(item, '_add'):
956 if isinstance(self._attr(cont), dict):
957 key = item.as_tuple()[0]
958 # do we have the same item? then raise an error
959 if key in self._vattr(cont):
960 raise ItemAlreadyExistsError(item)
961 # do we have the same item, but logically deleted? then update flags
962 if key in self._attr(cont):
964 if not isinstance(self._attr(cont), dict):
965 index = self._attr(cont).index(item)
966 if hasattr(item, '_add'):
967 self._attr(cont)[index]._add = False
968 if hasattr(item, '_delete'):
969 self._attr(cont)[index]._delete = False
970 else: # it's *really* new
971 if isinstance(self._attr(cont), dict):
972 self._attr(cont)[key] = item
974 self._attr(cont).append(item)
975 if hasattr(self._cont()[cont], '_update'):
976 self._cont()[cont]._update = True
978 @handler(u'Update an item')
979 def update(self, cont, index, *args, **kwargs):
980 r"update(cont, index, ...) -> None :: Update an item of the container."
981 # TODO make it right with metaclasses, so the method is not created
982 # unless the update() method really exists.
983 # TODO check if the modified item is the same of an existing one
984 if not cont in self._cont():
985 raise ContainerNotFoundError(cont)
986 if not isinstance(self._attr(cont), dict):
987 index = int(index) # TODO validation
988 if not hasattr(self._comp_subhandler_class, 'update'):
989 raise CommandNotFoundError(('update',))
991 item = self._vattr(cont)[index]
992 item.update(*args, **kwargs)
993 if hasattr(item, '_update'):
995 if hasattr(self._cont()[cont], '_update'):
996 self._cont()[cont]._update = True
998 raise ItemNotFoundError(index)
1000 @handler(u'Delete an item')
1001 def delete(self, cont, index):
1002 r"delete(cont, index) -> None :: Delete an item of the container."
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 item = self._vattr(cont)[index]
1009 if hasattr(item, '_delete'):
1012 del self._attr(cont)[index]
1013 if hasattr(self._cont()[cont], '_update'):
1014 self._cont()[cont]._update = True
1017 raise ItemNotFoundError(index)
1019 @handler(u'Remove all items (use with care).')
1020 def clear(self, cont):
1021 r"clear(cont) -> None :: Delete all items of the container."
1022 if not cont in self._cont():
1023 raise ContainerNotFoundError(cont)
1024 if isinstance(self._attr(cont), dict):
1025 self._attr(cont).clear()
1027 self._attr(cont, list())
1029 @handler(u'Get information about an item')
1030 def get(self, cont, index):
1031 r"get(cont, index) -> item :: List all the information of an item."
1032 if not cont in self._cont():
1033 raise ContainerNotFoundError(cont)
1034 if not isinstance(self._attr(cont), dict):
1035 index = int(index) # TODO validation
1037 return self._vattr(cont)[index]
1039 raise ItemNotFoundError(index)
1041 @handler(u'Get information about all items')
1042 def show(self, cont):
1043 r"show(cont) -> list of items :: List all the complete items information."
1044 if not cont in self._cont():
1045 raise ContainerNotFoundError(cont)
1046 if isinstance(self._attr(cont), dict):
1047 return self._attr(cont).values()
1048 return self._vattr(cont)
1050 class ListComposedSubHandler(ComposedSubHandler):
1051 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1053 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1057 @handler(u'Get how many items are in the list')
1058 def len(self, cont):
1059 r"len(cont) -> int :: Get how many items are in the list."
1060 if not cont in self._cont():
1061 raise ContainerNotFoundError(cont)
1062 return len(self._vattr(cont))
1064 class DictComposedSubHandler(ComposedSubHandler):
1065 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1067 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1071 @handler(u'List all the items by key')
1072 def list(self, cont):
1073 r"list(cont) -> tuple :: List all the item keys."
1074 if not cont in self._cont():
1075 raise ContainerNotFoundError(cont)
1076 return self._attr(cont).keys()
1079 if __name__ == '__main__':
1084 class STestHandler1(ServiceHandler):
1085 _service_start = ('service', 'start')
1086 _service_stop = ('service', 'stop')
1087 _service_restart = ('ls', '/')
1088 _service_reload = ('cp', '/la')
1089 class STestHandler2(ServiceHandler):
1091 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1092 'cmd-restart', 'cmd-reload')
1093 class ITestHandler1(InitdHandler):
1094 _initd_name = 'test1'
1095 class ITestHandler2(InitdHandler):
1097 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1105 print h.__class__.__name__
1108 except ExecutionError, e:
1112 except ExecutionError, e:
1116 except ExecutionError, e:
1120 except ExecutionError, e:
1125 print 'PTestHandler'
1126 class PTestHandler(Persistent):
1127 _persistent_attrs = 'vars'
1129 self.vars = dict(a=1, b=2)
1147 print 'RTestHandler'
1148 class RTestHandler(Restorable):
1149 _persistent_attrs = 'vars'
1150 _restorable_defaults = dict(vars=dict(a=1, b=2))
1162 print 'CTestHandler'
1164 os.mkdir('templates')
1165 f = file('templates/config', 'w')
1166 f.write('Hello, ${name}! You are ${what}.')
1169 print file('templates/config').read()
1170 class CTestHandler(ConfigWriter):
1171 _config_writer_files = 'config'
1173 self._config_build_templates()
1174 def _get_config_vars(self, config_file):
1175 return dict(name='you', what='a parrot')
1179 print file('config').read()
1181 os.unlink('templates/config')
1182 os.rmdir('templates')
1185 print get_network_devices()