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', 'ReturnNot0Error', 'ExecutionError', 'ItemError',
20 'ItemAlreadyExistsError', 'ItemNotFoundError', 'ContainerError',
21 'ContainerNotFoundError', 'call', 'get_network_devices',
22 'Persistent', 'Restorable', 'ConfigWriter', 'ServiceHandler',
23 'RestartHandler', 'ReloadHandler', 'InitdHandler', 'SubHandler',
24 'DictSubHandler', 'ListSubHandler', 'ComposedSubHandler',
25 'ListComposedSubHandler', '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 ReturnNot0Error(Error):
40 ReturnNot0Error(return_value) -> ReturnNot0Error instance.
42 A command didn't returned the expected 0 return value.
44 return_value - Return value returned by the command.
47 def __init__(self, return_value):
48 r"Initialize the object. See class documentation for more info."
49 self.return_value = return_value
51 def __unicode__(self):
52 return 'The command returned %d' % self.return_value
54 class ExecutionError(Error):
56 ExecutionError(command, error) -> ExecutionError instance.
58 Error executing a command.
60 command - Command that was tried to execute.
62 error - Error received when trying to execute the command.
65 def __init__(self, command, error):
66 r"Initialize the object. See class documentation for more info."
67 self.command = command
70 def __unicode__(self):
71 command = self.command
72 if not isinstance(self.command, basestring):
73 command = ' '.join(command)
74 return "Can't execute command %s: %s" % (command, self.error)
76 class ParameterError(Error, KeyError):
78 ParameterError(paramname) -> ParameterError instance
80 This is the base exception for all DhcpHandler parameters related errors.
83 def __init__(self, paramname):
84 r"Initialize the object. See class documentation for more info."
85 self.message = 'Parameter error: "%s"' % paramname
87 class ParameterNotFoundError(ParameterError):
89 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
91 This exception is raised when trying to operate on a parameter that doesn't
95 def __init__(self, paramname):
96 r"Initialize the object. See class documentation for more info."
97 self.message = 'Parameter not found: "%s"' % paramname
99 class ItemError(Error, KeyError):
101 ItemError(key) -> ItemError instance.
103 This is the base exception for all item related errors.
106 def __init__(self, key):
107 r"Initialize the object. See class documentation for more info."
108 self.message = u'Item error: "%s"' % key
110 class ItemAlreadyExistsError(ItemError):
112 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
114 This exception is raised when trying to add an item that already exists.
117 def __init__(self, key):
118 r"Initialize the object. See class documentation for more info."
119 self.message = u'Item already exists: "%s"' % key
121 class ItemNotFoundError(ItemError):
123 ItemNotFoundError(key) -> ItemNotFoundError instance.
125 This exception is raised when trying to operate on an item that doesn't
129 def __init__(self, key):
130 r"Initialize the object. See class documentation for more info."
131 self.message = u'Item not found: "%s"' % key
133 class ContainerError(Error, KeyError):
135 ContainerError(key) -> ContainerError instance.
137 This is the base exception for all container related errors.
140 def __init__(self, key):
141 r"Initialize the object. See class documentation for more info."
142 self.message = u'Container error: "%s"' % key
144 class ContainerNotFoundError(ContainerError):
146 ContainerNotFoundError(key) -> ContainerNotFoundError instance.
148 This exception is raised when trying to operate on an container that
152 def __init__(self, key):
153 r"Initialize the object. See class documentation for more info."
154 self.message = u'Container not found: "%s"' % key
156 class Address(Sequence):
157 def __init__(self, ip, netmask, broadcast=None, peer=None):
159 self.netmask = netmask
160 self.broadcast = broadcast
162 def update(self, netmask=None, broadcast=None):
163 if netmask is not None: self.netmask = netmask
164 if broadcast is not None: self.broadcast = broadcast
166 return (self.ip, self.netmask, self.broadcast, self.peer)
169 class Device(Sequence):
170 def __init__(self, name, mac, ppp):
177 return (self.name, self.mac, self.addrs)
181 def get_network_devices():
182 p = subprocess.Popen(('ip', '-o', 'link'), stdout=subprocess.PIPE,
184 string = p.stdout.read()
187 devices = string.splitlines()
190 if dev.find('link/ether') != -1:
191 i = dev.find('link/ether')
192 mac = dev[i+11 : i+11+17]
194 j = dev.find(':', i+1)
196 d[name] = Device(name,mac,False)
197 elif dev.find('link/ppp') != -1:
198 i = dev.find('link/ppp')
199 mac = '00:00:00:00:00:00'
201 j = dev.find(':', i+1)
203 d[name] = Device(name,mac,True)
204 #since the device is ppp, get the address and peer
206 p = subprocess.Popen(('ip', '-o', 'addr', 'show', name), stdout=subprocess.PIPE,
207 close_fds=True, stderr=subprocess.PIPE)
208 string = p.stdout.read()
210 addrs = string.splitlines()
211 inet = addrs[1].find('inet')
212 peer = addrs[1].find('peer')
213 bar = addrs[1].find('/')
214 from_addr = addrs[1][inet+5 : peer-1]
215 to_addr = addrs[1][peer+5 : bar]
216 d[name].addrs[from_addr] = Address(from_addr,24, peer=to_addr)
222 p = subprocess.Popen(('ip', '-o', 'addr'), stdout=subprocess.PIPE,
225 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
226 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
229 if not isinstance(command, basestring):
230 command = ' '.join(command)
231 print 'Executing command:', command
234 print 'Executing command:', command
235 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
236 universal_newlines=universal_newlines,
237 close_fds=close_fds, **kw)
239 raise ExecutionError(command, e)
241 raise ExecutionError(command, ReturnNot0Error(r))
244 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
246 This is a helper class to inherit from to automatically handle data
247 persistence using pickle.
249 The variables attributes to persist (attrs), and the pickle directory (dir)
250 and file extension (ext) can be defined by calling the constructor or in a
251 more declarative way as class attributes, like:
253 class TestHandler(Persistent):
254 _persistent_attrs = ('some_attr', 'other_attr')
255 _persistent_dir = 'persistent-data'
256 _persistent_ext = '.pickle'
258 The default dir is '.' and the default extension is '.pkl'. There are no
259 default variables, and they should be specified as string if a single
260 attribute should be persistent or as a tuple of strings if they are more.
261 The strings should be the attribute names to be persisted. For each
262 attribute a separated pickle file is generated in the pickle directory.
264 You can call _dump() and _load() to write and read the data respectively.
266 # TODO implement it using metaclasses to add the handlers method by demand
267 # (only for specifieds commands).
269 _persistent_attrs = ()
270 _persistent_dir = '.'
271 _persistent_ext = '.pkl'
273 def __init__(self, attrs=None, dir=None, ext=None):
274 r"Initialize the object, see the class documentation for details."
275 if attrs is not None:
276 self._persistent_attrs = attrs
278 self._persistent_dir = dir
280 self._persistent_ext = ext
283 r"_dump() -> None :: Dump all persistent data to pickle files."
284 if isinstance(self._persistent_attrs, basestring):
285 self._persistent_attrs = (self._persistent_attrs,)
286 for attrname in self._persistent_attrs:
287 self._dump_attr(attrname)
290 r"_load() -> None :: Load all persistent data from pickle files."
291 if isinstance(self._persistent_attrs, basestring):
292 self._persistent_attrs = (self._persistent_attrs,)
293 for attrname in self._persistent_attrs:
294 self._load_attr(attrname)
296 def _dump_attr(self, attrname):
297 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
298 f = file(self._pickle_filename(attrname), 'wb')
299 pickle.dump(getattr(self, attrname), f, 2)
302 def _load_attr(self, attrname):
303 r"_load_attr() -> object :: Load a specific pickle file."
304 f = file(self._pickle_filename(attrname))
305 setattr(self, attrname, pickle.load(f))
308 def _pickle_filename(self, name):
309 r"_pickle_filename() -> string :: Construct a pickle filename."
310 return path.join(self._persistent_dir, name) + self._persistent_ext
312 class Restorable(Persistent):
313 r"""Restorable([defaults]) -> Restorable.
315 This is a helper class to inherit from that provides a nice _restore()
316 method to restore the persistent data if any, or load some nice defaults
319 The defaults can be defined by calling the constructor or in a more
320 declarative way as class attributes, like:
322 class TestHandler(Restorable):
323 _persistent_attrs = ('some_attr', 'other_attr')
324 _restorable_defaults = dict(
325 some_attr = 'some_default',
326 other_attr = 'other_default')
328 The defaults is a dictionary, very coupled with the _persistent_attrs
329 attribute inherited from Persistent. The defaults keys should be the
330 values from _persistent_attrs, and the values the default values.
332 The _restore() method returns True if the data was restored successfully
333 or False if the defaults were loaded (in case you want to take further
334 actions). If a _write_config method if found, it's executed when a restore
337 # TODO implement it using metaclasses to add the handlers method by demand
338 # (only for specifieds commands).
340 _restorable_defaults = dict()
342 def __init__(self, defaults=None):
343 r"Initialize the object, see the class documentation for details."
344 if defaults is not None:
345 self._restorable_defaults = defaults
348 r"_restore() -> bool :: Restore persistent data or create a default."
353 for (k, v) in self._restorable_defaults.items():
355 # TODO tener en cuenta servicios que hay que levantar y los que no
356 if hasattr(self, 'commit'):
360 if hasattr(self, '_write_config'):
365 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
367 This is a helper class to inherit from to automatically handle
368 configuration generation. Mako template system is used for configuration
371 The configuration filenames, the generated configuration files directory
372 and the templates directory can be defined by calling the constructor or
373 in a more declarative way as class attributes, like:
375 class TestHandler(ConfigWriter):
376 _config_writer_files = ('base.conf', 'custom.conf')
377 _config_writer_cfg_dir = {
378 'base.conf': '/etc/service',
379 'custom.conf': '/etc/service/conf.d',
381 _config_writer_tpl_dir = 'templates'
383 The generated configuration files directory defaults to '.' and the
384 templates directory to 'templates'. _config_writer_files has no default and
385 must be specified in either way. It can be string or a tuple if more than
386 one configuration file must be generated. _config_writer_cfg_dir could be a
387 dict mapping which file should be stored in which directory, or a single
388 string if all the config files should go to the same directory.
390 The template filename and the generated configuration filename are both the
391 same (so if you want to generate some /etc/config, you should have some
392 templates/config template). That's why _config_writer_cfg_dir and
393 _config_writer_tpl_dir can't be the same. This is not true for very
394 specific cases where _write_single_config() is used.
396 When you write your Handler, you should call _config_build_templates() in
397 you Handler constructor to build the templates.
399 To write the configuration files, you must use the _write_config() method.
400 To know what variables to replace in the template, you have to provide a
401 method called _get_config_vars(tamplate_name), which should return a
402 dictionary of variables to pass to the template system to be replaced in
403 the template for the configuration file 'config_file'.
405 # TODO implement it using metaclasses to add the handlers method by demand
406 # (only for specifieds commands).
408 _config_writer_files = ()
409 _config_writer_cfg_dir = '.'
410 _config_writer_tpl_dir = 'templates'
412 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
413 r"Initialize the object, see the class documentation for details."
414 if files is not None:
415 self._config_writer_files = files
416 if cfg_dir is not None:
417 self._config_writer_cfg_dir = cfg_dir
418 if tpl_dir is not None:
419 self._config_writer_tpl_dir = tpl_dir
420 self._config_build_templates()
422 def _config_build_templates(self):
423 r"_config_writer_templates() -> None :: Build the template objects."
424 if isinstance(self._config_writer_files, basestring):
425 self._config_writer_files = (self._config_writer_files,)
426 if not hasattr(self, '_config_writer_templates') \
427 or not self._config_writer_templates:
428 self._config_writer_templates = dict()
429 for t in self._config_writer_files:
430 f = path.join(self._config_writer_tpl_dir, t)
431 self._config_writer_templates[t] = Template(filename=f)
433 def _render_config(self, template_name, vars=None):
434 r"""_render_config(template_name[, config_filename[, vars]]).
436 Render a single config file using the template 'template_name'. If
437 vars is specified, it's used as the dictionary with the variables
438 to replace in the templates, if not, it looks for a
439 _get_config_vars() method to get it.
442 if hasattr(self, '_get_config_vars'):
443 vars = self._get_config_vars(template_name)
447 vars = vars(template_name)
448 return self._config_writer_templates[template_name].render(**vars)
450 def _get_config_path(self, template_name, config_filename=None):
451 r"Get a complete configuration path."
452 if not config_filename:
453 config_filename = template_name
454 if isinstance(self._config_writer_cfg_dir, basestring):
455 return path.join(self._config_writer_cfg_dir, config_filename)
456 return path.join(self._config_writer_cfg_dir[template_name],
459 def _write_single_config(self, template_name, config_filename=None, vars=None):
460 r"""_write_single_config(template_name[, config_filename[, vars]]).
462 Write a single config file using the template 'template_name'. If no
463 config_filename is specified, the config filename will be the same as
464 the 'template_name' (but stored in the generated config files
465 directory). If it's specified, the generated config file is stored in
466 the file called 'config_filename' (also in the generated files
467 directory). If vars is specified, it's used as the dictionary with the
468 variables to replace in the templates, if not, it looks for a
469 _get_config_vars() method to get it.
472 if hasattr(self, '_get_config_vars'):
473 vars = self._get_config_vars(template_name)
477 vars = vars(template_name)
478 f = file(self._get_config_path(template_name, config_filename), 'w')
479 ctx = Context(f, **vars)
480 self._config_writer_templates[template_name].render_context(ctx)
483 def _write_config(self):
484 r"_write_config() -> None :: Generate all the configuration files."
485 for t in self._config_writer_files:
486 self._write_single_config(t)
489 class ServiceHandler(Handler, Restorable):
490 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
492 This is a helper class to inherit from to automatically handle services
493 with start, stop, restart, reload actions.
495 The actions can be defined by calling the constructor with all the
496 parameters or in a more declarative way as class attributes, like:
498 class TestHandler(ServiceHandler):
499 _service_start = ('command', 'start')
500 _service_stop = ('command', 'stop')
501 _service_restart = ('command', 'restart')
502 _service_reload = 'reload-command'
504 Commands are executed without using the shell, that's why they are specified
505 as tuples (where the first element is the command and the others are the
506 command arguments). If only a command is needed (without arguments) a single
507 string can be specified.
509 All commands must be specified.
511 # TODO implement it using metaclasses to add the handlers method by demand
512 # (only for specifieds commands).
514 def __init__(self, start=None, stop=None, restart=None, reload=None):
515 r"Initialize the object, see the class documentation for details."
516 for (name, action) in dict(start=start, stop=stop, restart=restart,
517 reload=reload).items():
518 if action is not None:
519 setattr(self, '_service_%s' % name, action)
520 self._persistent_attrs = list(self._persistent_attrs)
521 self._persistent_attrs.append('_service_running')
522 if '_service_running' not in self._restorable_defaults:
523 self._restorable_defaults['_service_running'] = False
525 if self._service_running:
526 self._service_running = False
529 @handler(u'Start the service.')
531 r"start() -> None :: Start the service."
532 if not self._service_running:
533 if callable(self._service_start):
534 self._service_start()
536 call(self._service_start)
537 self._service_running = True
538 self._dump_attr('_service_running')
540 @handler(u'Stop the service.')
542 r"stop() -> None :: Stop the service."
543 if self._service_running:
544 if callable(self._service_stop):
547 call(self._service_stop)
548 self._service_running = False
549 self._dump_attr('_service_running')
551 @handler(u'Restart the service.')
553 r"restart() -> None :: Restart the service."
554 if callable(self._service_restart):
555 self._service_restart()
557 call(self._service_restart)
558 self._service_running = True
559 self._dump_attr('_service_running')
561 @handler(u'Reload the service config (without restarting, if possible).')
563 r"reload() -> None :: Reload the configuration of the service."
564 if self._service_running:
565 if callable(self._service_reload):
566 self._service_reload()
568 call(self._service_reload)
570 @handler(u'Tell if the service is running.')
572 r"reload() -> None :: Reload the configuration of the service."
573 if self._service_running:
578 class RestartHandler(Handler):
579 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
581 This is a helper class to inherit from to automatically add a restart
582 command that first stop the service and then starts it again (using start
583 and stop commands respectively).
586 @handler(u'Restart the service (alias to stop + start).')
588 r"restart() -> None :: Restart the service calling stop() and start()."
592 class ReloadHandler(Handler):
593 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
595 This is a helper class to inherit from to automatically add a reload
596 command that calls restart.
599 @handler(u'Reload the service config (alias to restart).')
601 r"reload() -> None :: Reload the configuration of the service."
602 if hasattr(self, '_service_running') and self._service_running:
605 class InitdHandler(ServiceHandler):
606 # TODO update docs, declarative style is depracated
607 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
609 This is a helper class to inherit from to automatically handle services
610 with start, stop, restart, reload actions using a /etc/init.d like script.
612 The name and directory of the script can be defined by calling the
613 constructor or in a more declarative way as class attributes, like:
615 class TestHandler(ServiceHandler):
616 _initd_name = 'some-service'
617 _initd_dir = '/usr/local/etc/init.d'
619 The default _initd_dir is '/etc/init.d', _initd_name has no default and
620 must be specified in either way.
622 Commands are executed without using the shell.
624 # TODO implement it using metaclasses to add the handlers method by demand
625 # (only for specifieds commands).
627 _initd_dir = '/etc/init.d'
629 def __init__(self, initd_name=None, initd_dir=None):
630 r"Initialize the object, see the class documentation for details."
631 if initd_name is not None:
632 self._initd_name = initd_name
633 if initd_dir is not None:
634 self._initd_dir = initd_dir
636 for action in ('start', 'stop', 'restart', 'reload'):
637 actions[action] = (path.join(self._initd_dir, self._initd_name),
639 ServiceHandler.__init__(self, **actions)
641 def handle_timer(self):
642 p = subprocess.Popen(('pgrep', '-f', self._initd_name),
643 stdout=subprocess.PIPE)
644 pid = p.communicate()[0]
645 if p.returncode == 0 and len(pid) > 0:
646 self._service_running = True
648 self._service_running = False
650 class TransactionalHandler(Handler):
651 r"""Handle command transactions providing a commit and rollback commands.
653 This is a helper class to inherit from to automatically handle
654 transactional handlers, which have commit and rollback commands.
656 The handler should provide a reload() method (see ServiceHandler and
657 InitdHandler for helper classes to provide this) which will be called
658 when a commit command is issued (if a reload() command is present).
659 The persistent data will be written too (if a _dump() method is provided,
660 see Persistent and Restorable for that), and the configuration files
661 will be generated (if a _write_config method is present, see ConfigWriter).
663 # TODO implement it using metaclasses to add the handlers method by demand
664 # (only for specifieds commands).
666 @handler(u'Commit the changes (reloading the service, if necessary).')
668 r"commit() -> None :: Commit the changes and reload the service."
669 if hasattr(self, '_dump'):
672 if hasattr(self, '_write_config'):
673 unchanged = self._write_config()
674 if not unchanged and hasattr(self, 'reload'):
677 @handler(u'Discard all the uncommited changes.')
679 r"rollback() -> None :: Discard the changes not yet commited."
680 if hasattr(self, '_load'):
683 class ParametersHandler(Handler):
684 r"""ParametersHandler([attr]) -> ParametersHandler.
686 This is a helper class to inherit from to automatically handle
687 service parameters, providing set, get, list and show commands.
689 The attribute that holds the parameters can be defined by calling the
690 constructor or in a more declarative way as class attributes, like:
692 class TestHandler(ServiceHandler):
693 _parameters_attr = 'some_attr'
695 The default is 'params' and it should be a dictionary.
697 # TODO implement it using metaclasses to add the handlers method by demand
698 # (only for specifieds commands).
700 _parameters_attr = 'params'
702 def __init__(self, attr=None):
703 r"Initialize the object, see the class documentation for details."
705 self._parameters_attr = attr
707 @handler(u'Set a service parameter.')
708 def set(self, param, value):
709 r"set(param, value) -> None :: Set a service parameter."
710 if not param in self.params:
711 raise ParameterNotFoundError(param)
712 self.params[param] = value
713 if hasattr(self, '_update'):
716 @handler(u'Get a service parameter.')
717 def get(self, param):
718 r"get(param) -> None :: Get a service parameter."
719 if not param in self.params:
720 raise ParameterNotFoundError(param)
721 return self.params[param]
723 @handler(u'List all available service parameters.')
725 r"list() -> tuple :: List all the parameter names."
726 return self.params.keys()
728 @handler(u'Get all service parameters, with their values.')
730 r"show() -> (key, value) tuples :: List all the parameters."
731 return self.params.items()
733 class SubHandler(Handler):
734 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
736 This is a helper class to build sub handlers that needs to reference the
739 parent - Parent Handler object.
742 def __init__(self, parent):
743 r"Initialize the object, see the class documentation for details."
746 class ContainerSubHandler(SubHandler):
747 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
749 This is a helper class to implement ListSubHandler and DictSubHandler. You
750 should not use it directly.
752 The container attribute to handle and the class of objects that it
753 contains can be defined by calling the constructor or in a more declarative
754 way as class attributes, like:
756 class TestHandler(ContainerSubHandler):
757 _cont_subhandler_attr = 'some_cont'
758 _cont_subhandler_class = SomeClass
760 This way, the parent's some_cont attribute (self.parent.some_cont)
761 will be managed automatically, providing the commands: add, update,
762 delete, get and show. New items will be instances of SomeClass,
763 which should provide a cmp operator to see if the item is on the
764 container and an update() method, if it should be possible to modify
765 it. If SomeClass has an _add, _update or _delete attribute, it set
766 them to true when the item is added, updated or deleted respectively
767 (in case that it's deleted, it's not removed from the container,
768 but it's not listed either).
771 def __init__(self, parent, attr=None, cls=None):
772 r"Initialize the object, see the class documentation for details."
775 self._cont_subhandler_attr = attr
777 self._cont_subhandler_class = cls
779 def _attr(self, attr=None):
781 return getattr(self.parent, self._cont_subhandler_attr)
782 setattr(self.parent, self._cont_subhandler_attr, attr)
785 if isinstance(self._attr(), dict):
786 return dict([(k, i) for (k, i) in self._attr().items()
787 if not hasattr(i, '_delete') or not i._delete])
788 return [i for i in self._attr()
789 if not hasattr(i, '_delete') or not i._delete]
791 @handler(u'Add a new item')
792 def add(self, *args, **kwargs):
793 r"add(...) -> None :: Add an item to the list."
794 item = self._cont_subhandler_class(*args, **kwargs)
795 if hasattr(item, '_add'):
798 if isinstance(self._attr(), dict):
799 key = item.as_tuple()[0]
800 # do we have the same item? then raise an error
801 if key in self._vattr():
802 raise ItemAlreadyExistsError(item)
803 # do we have the same item, but logically deleted? then update flags
804 if key in self._attr():
806 if not isinstance(self._attr(), dict):
807 index = self._attr().index(item)
808 if hasattr(item, '_add'):
809 self._attr()[index]._add = False
810 if hasattr(item, '_delete'):
811 self._attr()[index]._delete = False
812 else: # it's *really* new
813 if isinstance(self._attr(), dict):
814 self._attr()[key] = item
816 self._attr().append(item)
818 @handler(u'Update an item')
819 def update(self, index, *args, **kwargs):
820 r"update(index, ...) -> None :: Update an item of the container."
821 # TODO make it right with metaclasses, so the method is not created
822 # unless the update() method really exists.
823 # TODO check if the modified item is the same of an existing one
824 if not isinstance(self._attr(), dict):
825 index = int(index) # TODO validation
826 if not hasattr(self._cont_subhandler_class, 'update'):
827 raise CommandNotFoundError(('update',))
829 item = self._vattr()[index]
830 item.update(*args, **kwargs)
831 if hasattr(item, '_update'):
834 raise ItemNotFoundError(index)
836 @handler(u'Delete an item')
837 def delete(self, index):
838 r"delete(index) -> None :: Delete an item of the container."
839 if not isinstance(self._attr(), dict):
840 index = int(index) # TODO validation
842 item = self._vattr()[index]
843 if hasattr(item, '_delete'):
846 del self._attr()[index]
849 raise ItemNotFoundError(index)
851 @handler(u'Remove all items (use with care).')
853 r"clear() -> None :: Delete all items of the container."
854 if isinstance(self._attr(), dict):
859 @handler(u'Get information about an item')
860 def get(self, index):
861 r"get(index) -> item :: List all the information of an item."
862 if not isinstance(self._attr(), dict):
863 index = int(index) # TODO validation
865 return self._vattr()[index]
867 raise ItemNotFoundError(index)
869 @handler(u'Get information about all items')
871 r"show() -> list of items :: List all the complete items information."
872 if isinstance(self._attr(), dict):
873 return self._attr().values()
876 class ListSubHandler(ContainerSubHandler):
877 r"""ListSubHandler(parent) -> ListSubHandler instance.
879 ContainerSubHandler holding lists. See ComposedSubHandler documentation
883 @handler(u'Get how many items are in the list')
885 r"len() -> int :: Get how many items are in the list."
886 return len(self._vattr())
888 class DictSubHandler(ContainerSubHandler):
889 r"""DictSubHandler(parent) -> DictSubHandler instance.
891 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
895 @handler(u'List all the items by key')
897 r"list() -> tuple :: List all the item keys."
898 return self._attr().keys()
900 class ComposedSubHandler(SubHandler):
901 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
903 This is a helper class to implement ListComposedSubHandler and
904 DictComposedSubHandler. You should not use it directly.
906 This class is usefull when you have a parent that has a dict (cont)
907 that stores some object that has an attribute (attr) with a list or
908 a dict of objects of some class. In that case, this class provides
909 automated commands to add, update, delete, get and show that objects.
910 This commands takes the cont (key of the dict for the object holding
911 the attr), and an index for access the object itself (in the attr
914 The container object (cont) that holds a containers, the attribute of
915 that object that is the container itself, and the class of the objects
916 that it contains can be defined by calling the constructor or in a
917 more declarative way as class attributes, like:
919 class TestHandler(ComposedSubHandler):
920 _comp_subhandler_cont = 'some_cont'
921 _comp_subhandler_attr = 'some_attr'
922 _comp_subhandler_class = SomeClass
924 This way, the parent's some_cont attribute (self.parent.some_cont)
925 will be managed automatically, providing the commands: add, update,
926 delete, get and show for manipulating a particular instance that holds
927 of SomeClass. For example, updating an item at the index 5 is the same
928 (simplified) as doing parent.some_cont[cont][5].update().
929 SomeClass should provide a cmp operator to see if the item is on the
930 container and an update() method, if it should be possible to modify
931 it. If SomeClass has an _add, _update or _delete attribute, it set
932 them to true when the item is added, updated or deleted respectively
933 (in case that it's deleted, it's not removed from the container,
934 but it's not listed either). If the container objects
935 (parent.some_cont[cont]) has an _update attribute, it's set to True
936 when any add, update or delete command is executed.
939 def __init__(self, parent, cont=None, attr=None, cls=None):
940 r"Initialize the object, see the class documentation for details."
943 self._comp_subhandler_cont = cont
945 self._comp_subhandler_attr = attr
947 self._comp_subhandler_class = cls
950 return getattr(self.parent, self._comp_subhandler_cont)
952 def _attr(self, cont, attr=None):
954 return getattr(self._cont()[cont], self._comp_subhandler_attr)
955 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
957 def _vattr(self, cont):
958 if isinstance(self._attr(cont), dict):
959 return dict([(k, i) for (k, i) in self._attr(cont).items()
960 if not hasattr(i, '_delete') or not i._delete])
961 return [i for i in self._attr(cont)
962 if not hasattr(i, '_delete') or not i._delete]
964 @handler(u'Add a new item')
965 def add(self, cont, *args, **kwargs):
966 r"add(cont, ...) -> None :: Add an item to the list."
967 if not cont in self._cont():
968 raise ContainerNotFoundError(cont)
969 item = self._comp_subhandler_class(*args, **kwargs)
970 if hasattr(item, '_add'):
973 if isinstance(self._attr(cont), dict):
974 key = item.as_tuple()[0]
975 # do we have the same item? then raise an error
976 if key in self._vattr(cont):
977 raise ItemAlreadyExistsError(item)
978 # do we have the same item, but logically deleted? then update flags
979 if key in self._attr(cont):
981 if not isinstance(self._attr(cont), dict):
982 index = self._attr(cont).index(item)
983 if hasattr(item, '_add'):
984 self._attr(cont)[index]._add = False
985 if hasattr(item, '_delete'):
986 self._attr(cont)[index]._delete = False
987 else: # it's *really* new
988 if isinstance(self._attr(cont), dict):
989 self._attr(cont)[key] = item
991 self._attr(cont).append(item)
992 if hasattr(self._cont()[cont], '_update'):
993 self._cont()[cont]._update = True
995 @handler(u'Update an item')
996 def update(self, cont, index, *args, **kwargs):
997 r"update(cont, index, ...) -> None :: Update an item of the container."
998 # TODO make it right with metaclasses, so the method is not created
999 # unless the update() method really exists.
1000 # TODO check if the modified item is the same of an existing one
1001 if not cont in self._cont():
1002 raise ContainerNotFoundError(cont)
1003 if not isinstance(self._attr(cont), dict):
1004 index = int(index) # TODO validation
1005 if not hasattr(self._comp_subhandler_class, 'update'):
1006 raise CommandNotFoundError(('update',))
1008 item = self._vattr(cont)[index]
1009 item.update(*args, **kwargs)
1010 if hasattr(item, '_update'):
1012 if hasattr(self._cont()[cont], '_update'):
1013 self._cont()[cont]._update = True
1015 raise ItemNotFoundError(index)
1017 @handler(u'Delete an item')
1018 def delete(self, cont, index):
1019 r"delete(cont, index) -> None :: Delete an item of the container."
1020 if not cont in self._cont():
1021 raise ContainerNotFoundError(cont)
1022 if not isinstance(self._attr(cont), dict):
1023 index = int(index) # TODO validation
1025 item = self._vattr(cont)[index]
1026 if hasattr(item, '_delete'):
1029 del self._attr(cont)[index]
1030 if hasattr(self._cont()[cont], '_update'):
1031 self._cont()[cont]._update = True
1034 raise ItemNotFoundError(index)
1036 @handler(u'Remove all items (use with care).')
1037 def clear(self, cont):
1038 r"clear(cont) -> None :: Delete all items of the container."
1039 if not cont in self._cont():
1040 raise ContainerNotFoundError(cont)
1041 if isinstance(self._attr(cont), dict):
1042 self._attr(cont).clear()
1044 self._attr(cont, list())
1046 @handler(u'Get information about an item')
1047 def get(self, cont, index):
1048 r"get(cont, index) -> item :: List all the information of an item."
1049 if not cont in self._cont():
1050 raise ContainerNotFoundError(cont)
1051 if not isinstance(self._attr(cont), dict):
1052 index = int(index) # TODO validation
1054 return self._vattr(cont)[index]
1056 raise ItemNotFoundError(index)
1058 @handler(u'Get information about all items')
1059 def show(self, cont):
1060 r"show(cont) -> list of items :: List all the complete items information."
1061 if not cont in self._cont():
1062 raise ContainerNotFoundError(cont)
1063 if isinstance(self._attr(cont), dict):
1064 return self._attr(cont).values()
1065 return self._vattr(cont)
1067 class ListComposedSubHandler(ComposedSubHandler):
1068 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1070 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1074 @handler(u'Get how many items are in the list')
1075 def len(self, cont):
1076 r"len(cont) -> int :: Get how many items are in the list."
1077 if not cont in self._cont():
1078 raise ContainerNotFoundError(cont)
1079 return len(self._vattr(cont))
1081 class DictComposedSubHandler(ComposedSubHandler):
1082 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1084 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1088 @handler(u'List all the items by key')
1089 def list(self, cont):
1090 r"list(cont) -> tuple :: List all the item keys."
1091 if not cont in self._cont():
1092 raise ContainerNotFoundError(cont)
1093 return self._attr(cont).keys()
1096 if __name__ == '__main__':
1101 class STestHandler1(ServiceHandler):
1102 _service_start = ('service', 'start')
1103 _service_stop = ('service', 'stop')
1104 _service_restart = ('ls', '/')
1105 _service_reload = ('cp', '/la')
1106 class STestHandler2(ServiceHandler):
1108 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1109 'cmd-restart', 'cmd-reload')
1110 class ITestHandler1(InitdHandler):
1111 _initd_name = 'test1'
1112 class ITestHandler2(InitdHandler):
1114 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1122 print h.__class__.__name__
1125 except ExecutionError, e:
1129 except ExecutionError, e:
1133 except ExecutionError, e:
1137 except ExecutionError, e:
1142 print 'PTestHandler'
1143 class PTestHandler(Persistent):
1144 _persistent_attrs = 'vars'
1146 self.vars = dict(a=1, b=2)
1164 print 'RTestHandler'
1165 class RTestHandler(Restorable):
1166 _persistent_attrs = 'vars'
1167 _restorable_defaults = dict(vars=dict(a=1, b=2))
1179 print 'CTestHandler'
1181 os.mkdir('templates')
1182 f = file('templates/config', 'w')
1183 f.write('Hello, ${name}! You are ${what}.')
1186 print file('templates/config').read()
1187 class CTestHandler(ConfigWriter):
1188 _config_writer_files = 'config'
1190 self._config_build_templates()
1191 def _get_config_vars(self, config_file):
1192 return dict(name='you', what='a parrot')
1196 print file('config').read()
1198 os.unlink('templates/config')
1199 os.rmdir('templates')
1202 print get_network_devices()