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):
161 return (self.name, self.mac, self.addrs)
165 def get_network_devices():
166 p = subprocess.Popen(('ip', '-o', 'link'), stdout=subprocess.PIPE,
168 string = p.stdout.read()
171 devices = string.splitlines()
174 if dev.find('link/ether') != -1:
175 i = dev.find('link/ether')
176 mac = dev[i+11 : i+11+17]
178 j = dev.find(':', i+1)
180 d[name] = Device(name,mac,False)
181 elif dev.find('link/ppp') != -1:
182 i = dev.find('link/ppp')
183 mac = '00:00:00:00:00:00'
185 j = dev.find(':', i+1)
187 d[name] = Device(name,mac,True)
188 #since the device is ppp, get the address and peer
190 p = subprocess.Popen(('ip', '-o', 'addr', 'show', name), stdout=subprocess.PIPE,
191 close_fds=True, stderr=subprocess.PIPE)
192 string = p.stdout.read()
194 addrs = string.splitlines()
195 inet = addrs[1].find('inet')
196 peer = addrs[1].find('peer')
197 bar = addrs[1].find('/')
198 from_addr = addrs[1][inet+5 : peer-1]
199 to_addr = addrs[1][peer+5 : bar]
200 d[name].addrs[from_addr] = Address(from_addr,24, peer=to_addr)
206 p = subprocess.Popen(('ip', '-o', 'addr'), stdout=subprocess.PIPE,
209 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
210 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
213 if not isinstance(command, basestring):
214 command = ' '.join(command)
215 print 'Executing command:', command
218 print 'Executing command:', command
219 r = subprocess.check_call(command, stdin=stdin, stdout=stdout,
220 stderr=stderr, close_fds=close_fds,
221 universal_newlines=universal_newlines, **kw)
223 raise ExecutionError(command, e)
226 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
228 This is a helper class to inherit from to automatically handle data
229 persistence using pickle.
231 The variables attributes to persist (attrs), and the pickle directory (dir)
232 and file extension (ext) can be defined by calling the constructor or in a
233 more declarative way as class attributes, like:
235 class TestHandler(Persistent):
236 _persistent_attrs = ('some_attr', 'other_attr')
237 _persistent_dir = 'persistent-data'
238 _persistent_ext = '.pickle'
240 The default dir is '.' and the default extension is '.pkl'. There are no
241 default variables, and they should be specified as string if a single
242 attribute should be persistent or as a tuple of strings if they are more.
243 The strings should be the attribute names to be persisted. For each
244 attribute a separated pickle file is generated in the pickle directory.
246 You can call _dump() and _load() to write and read the data respectively.
248 # TODO implement it using metaclasses to add the handlers method by demand
249 # (only for specifieds commands).
251 _persistent_attrs = ()
252 _persistent_dir = '.'
253 _persistent_ext = '.pkl'
255 def __init__(self, attrs=None, dir=None, ext=None):
256 r"Initialize the object, see the class documentation for details."
257 if attrs is not None:
258 self._persistent_attrs = attrs
260 self._persistent_dir = dir
262 self._persistent_ext = ext
265 r"_dump() -> None :: Dump all persistent data to pickle files."
266 if isinstance(self._persistent_attrs, basestring):
267 self._persistent_attrs = (self._persistent_attrs,)
268 for attrname in self._persistent_attrs:
269 self._dump_attr(attrname)
272 r"_load() -> None :: Load all persistent data from pickle files."
273 if isinstance(self._persistent_attrs, basestring):
274 self._persistent_attrs = (self._persistent_attrs,)
275 for attrname in self._persistent_attrs:
276 self._load_attr(attrname)
278 def _dump_attr(self, attrname):
279 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
280 f = file(self._pickle_filename(attrname), 'wb')
281 pickle.dump(getattr(self, attrname), f, 2)
284 def _load_attr(self, attrname):
285 r"_load_attr() -> object :: Load a specific pickle file."
286 f = file(self._pickle_filename(attrname))
287 setattr(self, attrname, pickle.load(f))
290 def _pickle_filename(self, name):
291 r"_pickle_filename() -> string :: Construct a pickle filename."
292 return path.join(self._persistent_dir, name) + self._persistent_ext
294 class Restorable(Persistent):
295 r"""Restorable([defaults]) -> Restorable.
297 This is a helper class to inherit from that provides a nice _restore()
298 method to restore the persistent data if any, or load some nice defaults
301 The defaults can be defined by calling the constructor or in a more
302 declarative way as class attributes, like:
304 class TestHandler(Restorable):
305 _persistent_attrs = ('some_attr', 'other_attr')
306 _restorable_defaults = dict(
307 some_attr = 'some_default',
308 other_attr = 'other_default')
310 The defaults is a dictionary, very coupled with the _persistent_attrs
311 attribute inherited from Persistent. The defaults keys should be the
312 values from _persistent_attrs, and the values the default values.
314 The _restore() method returns True if the data was restored successfully
315 or False if the defaults were loaded (in case you want to take further
316 actions). If a _write_config method if found, it's executed when a restore
319 # TODO implement it using metaclasses to add the handlers method by demand
320 # (only for specifieds commands).
322 _restorable_defaults = dict()
324 def __init__(self, defaults=None):
325 r"Initialize the object, see the class documentation for details."
326 if defaults is not None:
327 self._restorable_defaults = defaults
330 r"_restore() -> bool :: Restore persistent data or create a default."
335 for (k, v) in self._restorable_defaults.items():
337 # TODO tener en cuenta servicios que hay que levantar y los que no
338 if hasattr(self, 'commit'):
342 if hasattr(self, '_write_config'):
347 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
349 This is a helper class to inherit from to automatically handle
350 configuration generation. Mako template system is used for configuration
353 The configuration filenames, the generated configuration files directory
354 and the templates directory can be defined by calling the constructor or
355 in a more declarative way as class attributes, like:
357 class TestHandler(ConfigWriter):
358 _config_writer_files = ('base.conf', 'custom.conf')
359 _config_writer_cfg_dir = {
360 'base.conf': '/etc/service',
361 'custom.conf': '/etc/service/conf.d',
363 _config_writer_tpl_dir = 'templates'
365 The generated configuration files directory defaults to '.' and the
366 templates directory to 'templates'. _config_writer_files has no default and
367 must be specified in either way. It can be string or a tuple if more than
368 one configuration file must be generated. _config_writer_cfg_dir could be a
369 dict mapping which file should be stored in which directory, or a single
370 string if all the config files should go to the same directory.
372 The template filename and the generated configuration filename are both the
373 same (so if you want to generate some /etc/config, you should have some
374 templates/config template). That's why _config_writer_cfg_dir and
375 _config_writer_tpl_dir can't be the same. This is not true for very
376 specific cases where _write_single_config() is used.
378 When you write your Handler, you should call _config_build_templates() in
379 you Handler constructor to build the templates.
381 To write the configuration files, you must use the _write_config() method.
382 To know what variables to replace in the template, you have to provide a
383 method called _get_config_vars(tamplate_name), which should return a
384 dictionary of variables to pass to the template system to be replaced in
385 the template for the configuration file 'config_file'.
387 # TODO implement it using metaclasses to add the handlers method by demand
388 # (only for specifieds commands).
390 _config_writer_files = ()
391 _config_writer_cfg_dir = '.'
392 _config_writer_tpl_dir = 'templates'
394 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
395 r"Initialize the object, see the class documentation for details."
396 if files is not None:
397 self._config_writer_files = files
398 if cfg_dir is not None:
399 self._config_writer_cfg_dir = cfg_dir
400 if tpl_dir is not None:
401 self._config_writer_tpl_dir = tpl_dir
402 self._config_build_templates()
404 def _config_build_templates(self):
405 r"_config_writer_templates() -> None :: Build the template objects."
406 if isinstance(self._config_writer_files, basestring):
407 self._config_writer_files = (self._config_writer_files,)
408 if not hasattr(self, '_config_writer_templates') \
409 or not self._config_writer_templates:
410 self._config_writer_templates = dict()
411 for t in self._config_writer_files:
412 f = path.join(self._config_writer_tpl_dir, t)
413 self._config_writer_templates[t] = Template(filename=f)
415 def _render_config(self, template_name, vars=None):
416 r"""_render_config(template_name[, config_filename[, vars]]).
418 Render a single config file using the template 'template_name'. If
419 vars is specified, it's used as the dictionary with the variables
420 to replace in the templates, if not, it looks for a
421 _get_config_vars() method to get it.
424 if hasattr(self, '_get_config_vars'):
425 vars = self._get_config_vars(template_name)
429 vars = vars(template_name)
430 return self._config_writer_templates[template_name].render(**vars)
432 def _get_config_path(self, template_name, config_filename=None):
433 r"Get a complete configuration path."
434 if not config_filename:
435 config_filename = template_name
436 if isinstance(self._config_writer_cfg_dir, basestring):
437 return path.join(self._config_writer_cfg_dir, config_filename)
438 return path.join(self._config_writer_cfg_dir[template_name],
441 def _write_single_config(self, template_name, config_filename=None, vars=None):
442 r"""_write_single_config(template_name[, config_filename[, vars]]).
444 Write a single config file using the template 'template_name'. If no
445 config_filename is specified, the config filename will be the same as
446 the 'template_name' (but stored in the generated config files
447 directory). If it's specified, the generated config file is stored in
448 the file called 'config_filename' (also in the generated files
449 directory). If vars is specified, it's used as the dictionary with the
450 variables to replace in the templates, if not, it looks for a
451 _get_config_vars() method to get it.
454 if hasattr(self, '_get_config_vars'):
455 vars = self._get_config_vars(template_name)
459 vars = vars(template_name)
460 f = file(self._get_config_path(template_name, config_filename), 'w')
461 ctx = Context(f, **vars)
462 self._config_writer_templates[template_name].render_context(ctx)
465 def _write_config(self):
466 r"_write_config() -> None :: Generate all the configuration files."
467 for t in self._config_writer_files:
468 self._write_single_config(t)
471 class ServiceHandler(Handler, Restorable):
472 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
474 This is a helper class to inherit from to automatically handle services
475 with start, stop, restart, reload actions.
477 The actions can be defined by calling the constructor with all the
478 parameters or in a more declarative way as class attributes, like:
480 class TestHandler(ServiceHandler):
481 _service_start = ('command', 'start')
482 _service_stop = ('command', 'stop')
483 _service_restart = ('command', 'restart')
484 _service_reload = 'reload-command'
486 Commands are executed without using the shell, that's why they are specified
487 as tuples (where the first element is the command and the others are the
488 command arguments). If only a command is needed (without arguments) a single
489 string can be specified.
491 All commands must be specified.
493 # TODO implement it using metaclasses to add the handlers method by demand
494 # (only for specifieds commands).
496 def __init__(self, start=None, stop=None, restart=None, reload=None):
497 r"Initialize the object, see the class documentation for details."
498 for (name, action) in dict(start=start, stop=stop, restart=restart,
499 reload=reload).items():
500 if action is not None:
501 setattr(self, '_service_%s' % name, action)
502 self._persistent_attrs = list(self._persistent_attrs)
503 self._persistent_attrs.append('_service_running')
504 if '_service_running' not in self._restorable_defaults:
505 self._restorable_defaults['_service_running'] = False
507 if self._service_running:
508 self._service_running = False
511 @handler(u'Start the service.')
513 r"start() -> None :: Start the service."
514 if not self._service_running:
515 if callable(self._service_start):
516 self._service_start()
518 call(self._service_start)
519 self._service_running = True
520 self._dump_attr('_service_running')
522 @handler(u'Stop the service.')
524 r"stop() -> None :: Stop the service."
525 if self._service_running:
526 if callable(self._service_stop):
529 call(self._service_stop)
530 self._service_running = False
531 self._dump_attr('_service_running')
533 @handler(u'Restart the service.')
535 r"restart() -> None :: Restart the service."
536 if callable(self._service_restart):
537 self._service_restart()
539 call(self._service_restart)
540 self._service_running = True
541 self._dump_attr('_service_running')
543 @handler(u'Reload the service config (without restarting, if possible).')
545 r"reload() -> None :: Reload the configuration of the service."
546 if self._service_running:
547 if callable(self._service_reload):
548 self._service_reload()
550 call(self._service_reload)
552 @handler(u'Tell if the service is running.')
554 r"reload() -> None :: Reload the configuration of the service."
555 if self._service_running:
560 class RestartHandler(Handler):
561 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
563 This is a helper class to inherit from to automatically add a restart
564 command that first stop the service and then starts it again (using start
565 and stop commands respectively).
568 @handler(u'Restart the service (alias to stop + start).')
570 r"restart() -> None :: Restart the service calling stop() and start()."
574 class ReloadHandler(Handler):
575 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
577 This is a helper class to inherit from to automatically add a reload
578 command that calls restart.
581 @handler(u'Reload the service config (alias to restart).')
583 r"reload() -> None :: Reload the configuration of the service."
584 if hasattr(self, '_service_running') and self._service_running:
587 class InitdHandler(ServiceHandler):
588 # TODO update docs, declarative style is depracated
589 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
591 This is a helper class to inherit from to automatically handle services
592 with start, stop, restart, reload actions using a /etc/init.d like script.
594 The name and directory of the script can be defined by calling the
595 constructor or in a more declarative way as class attributes, like:
597 class TestHandler(ServiceHandler):
598 _initd_name = 'some-service'
599 _initd_dir = '/usr/local/etc/init.d'
601 The default _initd_dir is '/etc/init.d', _initd_name has no default and
602 must be specified in either way.
604 Commands are executed without using the shell.
606 # TODO implement it using metaclasses to add the handlers method by demand
607 # (only for specifieds commands).
609 _initd_dir = '/etc/init.d'
611 def __init__(self, initd_name=None, initd_dir=None):
612 r"Initialize the object, see the class documentation for details."
613 if initd_name is not None:
614 self._initd_name = initd_name
615 if initd_dir is not None:
616 self._initd_dir = initd_dir
618 for action in ('start', 'stop', 'restart', 'reload'):
619 actions[action] = (path.join(self._initd_dir, self._initd_name),
621 ServiceHandler.__init__(self, **actions)
623 def handle_timer(self):
624 p = subprocess.Popen(('pgrep', '-f', self._initd_name),
625 stdout=subprocess.PIPE)
626 pid = p.communicate()[0]
627 if p.returncode == 0 and len(pid) > 0:
628 self._service_running = True
630 self._service_running = False
632 class TransactionalHandler(Handler):
633 r"""Handle command transactions providing a commit and rollback commands.
635 This is a helper class to inherit from to automatically handle
636 transactional handlers, which have commit and rollback commands.
638 The handler should provide a reload() method (see ServiceHandler and
639 InitdHandler for helper classes to provide this) which will be called
640 when a commit command is issued (if a reload() command is present).
641 The persistent data will be written too (if a _dump() method is provided,
642 see Persistent and Restorable for that), and the configuration files
643 will be generated (if a _write_config method is present, see ConfigWriter).
645 # TODO implement it using metaclasses to add the handlers method by demand
646 # (only for specifieds commands).
648 @handler(u'Commit the changes (reloading the service, if necessary).')
650 r"commit() -> None :: Commit the changes and reload the service."
651 if hasattr(self, '_dump'):
654 if hasattr(self, '_write_config'):
655 unchanged = self._write_config()
656 if not unchanged and hasattr(self, 'reload'):
659 @handler(u'Discard all the uncommited changes.')
661 r"rollback() -> None :: Discard the changes not yet commited."
662 if hasattr(self, '_load'):
665 class ParametersHandler(Handler):
666 r"""ParametersHandler([attr]) -> ParametersHandler.
668 This is a helper class to inherit from to automatically handle
669 service parameters, providing set, get, list and show commands.
671 The attribute that holds the parameters can be defined by calling the
672 constructor or in a more declarative way as class attributes, like:
674 class TestHandler(ServiceHandler):
675 _parameters_attr = 'some_attr'
677 The default is 'params' and it should be a dictionary.
679 # TODO implement it using metaclasses to add the handlers method by demand
680 # (only for specifieds commands).
682 _parameters_attr = 'params'
684 def __init__(self, attr=None):
685 r"Initialize the object, see the class documentation for details."
687 self._parameters_attr = attr
689 @handler(u'Set a service parameter.')
690 def set(self, param, value):
691 r"set(param, value) -> None :: Set a service parameter."
692 if not param in self.params:
693 raise ParameterNotFoundError(param)
694 self.params[param] = value
695 if hasattr(self, '_update'):
698 @handler(u'Get a service parameter.')
699 def get(self, param):
700 r"get(param) -> None :: Get a service parameter."
701 if not param in self.params:
702 raise ParameterNotFoundError(param)
703 return self.params[param]
705 @handler(u'List all available service parameters.')
707 r"list() -> tuple :: List all the parameter names."
708 return self.params.keys()
710 @handler(u'Get all service parameters, with their values.')
712 r"show() -> (key, value) tuples :: List all the parameters."
713 return self.params.items()
715 class SubHandler(Handler):
716 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
718 This is a helper class to build sub handlers that needs to reference the
721 parent - Parent Handler object.
724 def __init__(self, parent):
725 r"Initialize the object, see the class documentation for details."
728 class ContainerSubHandler(SubHandler):
729 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
731 This is a helper class to implement ListSubHandler and DictSubHandler. You
732 should not use it directly.
734 The container attribute to handle and the class of objects that it
735 contains can be defined by calling the constructor or in a more declarative
736 way as class attributes, like:
738 class TestHandler(ContainerSubHandler):
739 _cont_subhandler_attr = 'some_cont'
740 _cont_subhandler_class = SomeClass
742 This way, the parent's some_cont attribute (self.parent.some_cont)
743 will be managed automatically, providing the commands: add, update,
744 delete, get and show. New items will be instances of SomeClass,
745 which should provide a cmp operator to see if the item is on the
746 container and an update() method, if it should be possible to modify
747 it. If SomeClass has an _add, _update or _delete attribute, it set
748 them to true when the item is added, updated or deleted respectively
749 (in case that it's deleted, it's not removed from the container,
750 but it's not listed either).
753 def __init__(self, parent, attr=None, cls=None):
754 r"Initialize the object, see the class documentation for details."
757 self._cont_subhandler_attr = attr
759 self._cont_subhandler_class = cls
761 def _attr(self, attr=None):
763 return getattr(self.parent, self._cont_subhandler_attr)
764 setattr(self.parent, self._cont_subhandler_attr, attr)
767 if isinstance(self._attr(), dict):
768 return dict([(k, i) for (k, i) in self._attr().items()
769 if not hasattr(i, '_delete') or not i._delete])
770 return [i for i in self._attr()
771 if not hasattr(i, '_delete') or not i._delete]
773 @handler(u'Add a new item')
774 def add(self, *args, **kwargs):
775 r"add(...) -> None :: Add an item to the list."
776 item = self._cont_subhandler_class(*args, **kwargs)
777 if hasattr(item, '_add'):
780 if isinstance(self._attr(), dict):
781 key = item.as_tuple()[0]
782 # do we have the same item? then raise an error
783 if key in self._vattr():
784 raise ItemAlreadyExistsError(item)
785 # do we have the same item, but logically deleted? then update flags
786 if key in self._attr():
788 if not isinstance(self._attr(), dict):
789 index = self._attr().index(item)
790 if hasattr(item, '_add'):
791 self._attr()[index]._add = False
792 if hasattr(item, '_delete'):
793 self._attr()[index]._delete = False
794 else: # it's *really* new
795 if isinstance(self._attr(), dict):
796 self._attr()[key] = item
798 self._attr().append(item)
800 @handler(u'Update an item')
801 def update(self, index, *args, **kwargs):
802 r"update(index, ...) -> None :: Update an item of the container."
803 # TODO make it right with metaclasses, so the method is not created
804 # unless the update() method really exists.
805 # TODO check if the modified item is the same of an existing one
806 if not isinstance(self._attr(), dict):
807 index = int(index) # TODO validation
808 if not hasattr(self._cont_subhandler_class, 'update'):
809 raise CommandNotFoundError(('update',))
811 item = self._vattr()[index]
812 item.update(*args, **kwargs)
813 if hasattr(item, '_update'):
816 raise ItemNotFoundError(index)
818 @handler(u'Delete an item')
819 def delete(self, index):
820 r"delete(index) -> None :: Delete an item of the container."
821 if not isinstance(self._attr(), dict):
822 index = int(index) # TODO validation
824 item = self._vattr()[index]
825 if hasattr(item, '_delete'):
828 del self._attr()[index]
831 raise ItemNotFoundError(index)
833 @handler(u'Remove all items (use with care).')
835 r"clear() -> None :: Delete all items of the container."
836 if isinstance(self._attr(), dict):
841 @handler(u'Get information about an item')
842 def get(self, index):
843 r"get(index) -> item :: List all the information of an item."
844 if not isinstance(self._attr(), dict):
845 index = int(index) # TODO validation
847 return self._vattr()[index]
849 raise ItemNotFoundError(index)
851 @handler(u'Get information about all items')
853 r"show() -> list of items :: List all the complete items information."
854 if isinstance(self._attr(), dict):
855 return self._attr().values()
858 class ListSubHandler(ContainerSubHandler):
859 r"""ListSubHandler(parent) -> ListSubHandler instance.
861 ContainerSubHandler holding lists. See ComposedSubHandler documentation
865 @handler(u'Get how many items are in the list')
867 r"len() -> int :: Get how many items are in the list."
868 return len(self._vattr())
870 class DictSubHandler(ContainerSubHandler):
871 r"""DictSubHandler(parent) -> DictSubHandler instance.
873 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
877 @handler(u'List all the items by key')
879 r"list() -> tuple :: List all the item keys."
880 return self._attr().keys()
882 class ComposedSubHandler(SubHandler):
883 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
885 This is a helper class to implement ListComposedSubHandler and
886 DictComposedSubHandler. You should not use it directly.
888 This class is usefull when you have a parent that has a dict (cont)
889 that stores some object that has an attribute (attr) with a list or
890 a dict of objects of some class. In that case, this class provides
891 automated commands to add, update, delete, get and show that objects.
892 This commands takes the cont (key of the dict for the object holding
893 the attr), and an index for access the object itself (in the attr
896 The container object (cont) that holds a containers, the attribute of
897 that object that is the container itself, and the class of the objects
898 that it contains can be defined by calling the constructor or in a
899 more declarative way as class attributes, like:
901 class TestHandler(ComposedSubHandler):
902 _comp_subhandler_cont = 'some_cont'
903 _comp_subhandler_attr = 'some_attr'
904 _comp_subhandler_class = SomeClass
906 This way, the parent's some_cont attribute (self.parent.some_cont)
907 will be managed automatically, providing the commands: add, update,
908 delete, get and show for manipulating a particular instance that holds
909 of SomeClass. For example, updating an item at the index 5 is the same
910 (simplified) as doing parent.some_cont[cont][5].update().
911 SomeClass should provide a cmp operator to see if the item is on the
912 container and an update() method, if it should be possible to modify
913 it. If SomeClass has an _add, _update or _delete attribute, it set
914 them to true when the item is added, updated or deleted respectively
915 (in case that it's deleted, it's not removed from the container,
916 but it's not listed either). If the container objects
917 (parent.some_cont[cont]) has an _update attribute, it's set to True
918 when any add, update or delete command is executed.
921 def __init__(self, parent, cont=None, attr=None, cls=None):
922 r"Initialize the object, see the class documentation for details."
925 self._comp_subhandler_cont = cont
927 self._comp_subhandler_attr = attr
929 self._comp_subhandler_class = cls
932 return getattr(self.parent, self._comp_subhandler_cont)
934 def _attr(self, cont, attr=None):
936 return getattr(self._cont()[cont], self._comp_subhandler_attr)
937 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
939 def _vattr(self, cont):
940 if isinstance(self._attr(cont), dict):
941 return dict([(k, i) for (k, i) in self._attr(cont).items()
942 if not hasattr(i, '_delete') or not i._delete])
943 return [i for i in self._attr(cont)
944 if not hasattr(i, '_delete') or not i._delete]
946 @handler(u'Add a new item')
947 def add(self, cont, *args, **kwargs):
948 r"add(cont, ...) -> None :: Add an item to the list."
949 if not cont in self._cont():
950 raise ContainerNotFoundError(cont)
951 item = self._comp_subhandler_class(*args, **kwargs)
952 if hasattr(item, '_add'):
955 if isinstance(self._attr(cont), dict):
956 key = item.as_tuple()[0]
957 # do we have the same item? then raise an error
958 if key in self._vattr(cont):
959 raise ItemAlreadyExistsError(item)
960 # do we have the same item, but logically deleted? then update flags
961 if key in self._attr(cont):
963 if not isinstance(self._attr(cont), dict):
964 index = self._attr(cont).index(item)
965 if hasattr(item, '_add'):
966 self._attr(cont)[index]._add = False
967 if hasattr(item, '_delete'):
968 self._attr(cont)[index]._delete = False
969 else: # it's *really* new
970 if isinstance(self._attr(cont), dict):
971 self._attr(cont)[key] = item
973 self._attr(cont).append(item)
974 if hasattr(self._cont()[cont], '_update'):
975 self._cont()[cont]._update = True
977 @handler(u'Update an item')
978 def update(self, cont, index, *args, **kwargs):
979 r"update(cont, index, ...) -> None :: Update an item of the container."
980 # TODO make it right with metaclasses, so the method is not created
981 # unless the update() method really exists.
982 # TODO check if the modified item is the same of an existing one
983 if not cont in self._cont():
984 raise ContainerNotFoundError(cont)
985 if not isinstance(self._attr(cont), dict):
986 index = int(index) # TODO validation
987 if not hasattr(self._comp_subhandler_class, 'update'):
988 raise CommandNotFoundError(('update',))
990 item = self._vattr(cont)[index]
991 item.update(*args, **kwargs)
992 if hasattr(item, '_update'):
994 if hasattr(self._cont()[cont], '_update'):
995 self._cont()[cont]._update = True
997 raise ItemNotFoundError(index)
999 @handler(u'Delete an item')
1000 def delete(self, cont, index):
1001 r"delete(cont, index) -> None :: Delete an item of the container."
1002 if not cont in self._cont():
1003 raise ContainerNotFoundError(cont)
1004 if not isinstance(self._attr(cont), dict):
1005 index = int(index) # TODO validation
1007 item = self._vattr(cont)[index]
1008 if hasattr(item, '_delete'):
1011 del self._attr(cont)[index]
1012 if hasattr(self._cont()[cont], '_update'):
1013 self._cont()[cont]._update = True
1016 raise ItemNotFoundError(index)
1018 @handler(u'Remove all items (use with care).')
1019 def clear(self, cont):
1020 r"clear(cont) -> None :: Delete all items of the container."
1021 if not cont in self._cont():
1022 raise ContainerNotFoundError(cont)
1023 if isinstance(self._attr(cont), dict):
1024 self._attr(cont).clear()
1026 self._attr(cont, list())
1028 @handler(u'Get information about an item')
1029 def get(self, cont, index):
1030 r"get(cont, index) -> item :: List all the information of an item."
1031 if not cont in self._cont():
1032 raise ContainerNotFoundError(cont)
1033 if not isinstance(self._attr(cont), dict):
1034 index = int(index) # TODO validation
1036 return self._vattr(cont)[index]
1038 raise ItemNotFoundError(index)
1040 @handler(u'Get information about all items')
1041 def show(self, cont):
1042 r"show(cont) -> list of items :: List all the complete items information."
1043 if not cont in self._cont():
1044 raise ContainerNotFoundError(cont)
1045 if isinstance(self._attr(cont), dict):
1046 return self._attr(cont).values()
1047 return self._vattr(cont)
1049 class ListComposedSubHandler(ComposedSubHandler):
1050 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1052 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1056 @handler(u'Get how many items are in the list')
1057 def len(self, cont):
1058 r"len(cont) -> int :: Get how many items are in the list."
1059 if not cont in self._cont():
1060 raise ContainerNotFoundError(cont)
1061 return len(self._vattr(cont))
1063 class DictComposedSubHandler(ComposedSubHandler):
1064 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1066 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1070 @handler(u'List all the items by key')
1071 def list(self, cont):
1072 r"list(cont) -> tuple :: List all the item keys."
1073 if not cont in self._cont():
1074 raise ContainerNotFoundError(cont)
1075 return self._attr(cont).keys()
1078 if __name__ == '__main__':
1083 class STestHandler1(ServiceHandler):
1084 _service_start = ('service', 'start')
1085 _service_stop = ('service', 'stop')
1086 _service_restart = ('ls', '/')
1087 _service_reload = ('cp', '/la')
1088 class STestHandler2(ServiceHandler):
1090 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1091 'cmd-restart', 'cmd-reload')
1092 class ITestHandler1(InitdHandler):
1093 _initd_name = 'test1'
1094 class ITestHandler2(InitdHandler):
1096 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1104 print h.__class__.__name__
1107 except ExecutionError, e:
1111 except ExecutionError, e:
1115 except ExecutionError, e:
1119 except ExecutionError, e:
1124 print 'PTestHandler'
1125 class PTestHandler(Persistent):
1126 _persistent_attrs = 'vars'
1128 self.vars = dict(a=1, b=2)
1146 print 'RTestHandler'
1147 class RTestHandler(Restorable):
1148 _persistent_attrs = 'vars'
1149 _restorable_defaults = dict(vars=dict(a=1, b=2))
1161 print 'CTestHandler'
1163 os.mkdir('templates')
1164 f = file('templates/config', 'w')
1165 f.write('Hello, ${name}! You are ${what}.')
1168 print file('templates/config').read()
1169 class CTestHandler(ConfigWriter):
1170 _config_writer_files = 'config'
1172 self._config_build_templates()
1173 def _get_config_vars(self, config_file):
1174 return dict(name='you', what='a parrot')
1178 print file('config').read()
1180 os.unlink('templates/config')
1181 os.rmdir('templates')
1184 print get_network_devices()