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)
223 p = subprocess.Popen(('ip', '-o', 'addr'), stdout=subprocess.PIPE,
226 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
227 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
230 if not isinstance(command, basestring):
231 command = ' '.join(command)
232 print 'Executing command:', command
235 print 'Executing command:', command
236 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
237 universal_newlines=universal_newlines,
238 close_fds=close_fds, **kw)
240 raise ExecutionError(command, e)
242 raise ExecutionError(command, ReturnNot0Error(r))
245 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
247 This is a helper class to inherit from to automatically handle data
248 persistence using pickle.
250 The variables attributes to persist (attrs), and the pickle directory (dir)
251 and file extension (ext) can be defined by calling the constructor or in a
252 more declarative way as class attributes, like:
254 class TestHandler(Persistent):
255 _persistent_attrs = ('some_attr', 'other_attr')
256 _persistent_dir = 'persistent-data'
257 _persistent_ext = '.pickle'
259 The default dir is '.' and the default extension is '.pkl'. There are no
260 default variables, and they should be specified as string if a single
261 attribute should be persistent or as a tuple of strings if they are more.
262 The strings should be the attribute names to be persisted. For each
263 attribute a separated pickle file is generated in the pickle directory.
265 You can call _dump() and _load() to write and read the data respectively.
267 # TODO implement it using metaclasses to add the handlers method by demand
268 # (only for specifieds commands).
270 _persistent_attrs = ()
271 _persistent_dir = '.'
272 _persistent_ext = '.pkl'
274 def __init__(self, attrs=None, dir=None, ext=None):
275 r"Initialize the object, see the class documentation for details."
276 if attrs is not None:
277 self._persistent_attrs = attrs
279 self._persistent_dir = dir
281 self._persistent_ext = ext
284 r"_dump() -> None :: Dump all persistent data to pickle files."
285 if isinstance(self._persistent_attrs, basestring):
286 self._persistent_attrs = (self._persistent_attrs,)
287 for attrname in self._persistent_attrs:
288 self._dump_attr(attrname)
291 r"_load() -> None :: Load all persistent data from pickle files."
292 if isinstance(self._persistent_attrs, basestring):
293 self._persistent_attrs = (self._persistent_attrs,)
294 for attrname in self._persistent_attrs:
295 self._load_attr(attrname)
297 def _dump_attr(self, attrname):
298 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
299 f = file(self._pickle_filename(attrname), 'wb')
300 pickle.dump(getattr(self, attrname), f, 2)
303 def _load_attr(self, attrname):
304 r"_load_attr() -> object :: Load a specific pickle file."
305 f = file(self._pickle_filename(attrname))
306 setattr(self, attrname, pickle.load(f))
309 def _pickle_filename(self, name):
310 r"_pickle_filename() -> string :: Construct a pickle filename."
311 return path.join(self._persistent_dir, name) + self._persistent_ext
313 class Restorable(Persistent):
314 r"""Restorable([defaults]) -> Restorable.
316 This is a helper class to inherit from that provides a nice _restore()
317 method to restore the persistent data if any, or load some nice defaults
320 The defaults can be defined by calling the constructor or in a more
321 declarative way as class attributes, like:
323 class TestHandler(Restorable):
324 _persistent_attrs = ('some_attr', 'other_attr')
325 _restorable_defaults = dict(
326 some_attr = 'some_default',
327 other_attr = 'other_default')
329 The defaults is a dictionary, very coupled with the _persistent_attrs
330 attribute inherited from Persistent. The defaults keys should be the
331 values from _persistent_attrs, and the values the default values.
333 The _restore() method returns True if the data was restored successfully
334 or False if the defaults were loaded (in case you want to take further
335 actions). If a _write_config method if found, it's executed when a restore
338 # TODO implement it using metaclasses to add the handlers method by demand
339 # (only for specifieds commands).
341 _restorable_defaults = dict()
343 def __init__(self, defaults=None):
344 r"Initialize the object, see the class documentation for details."
345 if defaults is not None:
346 self._restorable_defaults = defaults
349 r"_restore() -> bool :: Restore persistent data or create a default."
354 for (k, v) in self._restorable_defaults.items():
356 # TODO tener en cuenta servicios que hay que levantar y los que no
357 if hasattr(self, 'commit'):
361 if hasattr(self, '_write_config'):
366 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
368 This is a helper class to inherit from to automatically handle
369 configuration generation. Mako template system is used for configuration
372 The configuration filenames, the generated configuration files directory
373 and the templates directory can be defined by calling the constructor or
374 in a more declarative way as class attributes, like:
376 class TestHandler(ConfigWriter):
377 _config_writer_files = ('base.conf', 'custom.conf')
378 _config_writer_cfg_dir = {
379 'base.conf': '/etc/service',
380 'custom.conf': '/etc/service/conf.d',
382 _config_writer_tpl_dir = 'templates'
384 The generated configuration files directory defaults to '.' and the
385 templates directory to 'templates'. _config_writer_files has no default and
386 must be specified in either way. It can be string or a tuple if more than
387 one configuration file must be generated. _config_writer_cfg_dir could be a
388 dict mapping which file should be stored in which directory, or a single
389 string if all the config files should go to the same directory.
391 The template filename and the generated configuration filename are both the
392 same (so if you want to generate some /etc/config, you should have some
393 templates/config template). That's why _config_writer_cfg_dir and
394 _config_writer_tpl_dir can't be the same. This is not true for very
395 specific cases where _write_single_config() is used.
397 When you write your Handler, you should call _config_build_templates() in
398 you Handler constructor to build the templates.
400 To write the configuration files, you must use the _write_config() method.
401 To know what variables to replace in the template, you have to provide a
402 method called _get_config_vars(tamplate_name), which should return a
403 dictionary of variables to pass to the template system to be replaced in
404 the template for the configuration file 'config_file'.
406 # TODO implement it using metaclasses to add the handlers method by demand
407 # (only for specifieds commands).
409 _config_writer_files = ()
410 _config_writer_cfg_dir = '.'
411 _config_writer_tpl_dir = 'templates'
413 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
414 r"Initialize the object, see the class documentation for details."
415 if files is not None:
416 self._config_writer_files = files
417 if cfg_dir is not None:
418 self._config_writer_cfg_dir = cfg_dir
419 if tpl_dir is not None:
420 self._config_writer_tpl_dir = tpl_dir
421 self._config_build_templates()
423 def _config_build_templates(self):
424 r"_config_writer_templates() -> None :: Build the template objects."
425 if isinstance(self._config_writer_files, basestring):
426 self._config_writer_files = (self._config_writer_files,)
427 if not hasattr(self, '_config_writer_templates') \
428 or not self._config_writer_templates:
429 self._config_writer_templates = dict()
430 for t in self._config_writer_files:
431 f = path.join(self._config_writer_tpl_dir, t)
432 self._config_writer_templates[t] = Template(filename=f)
434 def _render_config(self, template_name, vars=None):
435 r"""_render_config(template_name[, config_filename[, vars]]).
437 Render a single config file using the template 'template_name'. If
438 vars is specified, it's used as the dictionary with the variables
439 to replace in the templates, if not, it looks for a
440 _get_config_vars() method to get it.
443 if hasattr(self, '_get_config_vars'):
444 vars = self._get_config_vars(template_name)
448 vars = vars(template_name)
449 return self._config_writer_templates[template_name].render(**vars)
451 def _get_config_path(self, template_name, config_filename=None):
452 r"Get a complete configuration path."
453 if not config_filename:
454 config_filename = template_name
455 if isinstance(self._config_writer_cfg_dir, basestring):
456 return path.join(self._config_writer_cfg_dir, config_filename)
457 return path.join(self._config_writer_cfg_dir[template_name],
460 def _write_single_config(self, template_name, config_filename=None, vars=None):
461 r"""_write_single_config(template_name[, config_filename[, vars]]).
463 Write a single config file using the template 'template_name'. If no
464 config_filename is specified, the config filename will be the same as
465 the 'template_name' (but stored in the generated config files
466 directory). If it's specified, the generated config file is stored in
467 the file called 'config_filename' (also in the generated files
468 directory). If vars is specified, it's used as the dictionary with the
469 variables to replace in the templates, if not, it looks for a
470 _get_config_vars() method to get it.
473 if hasattr(self, '_get_config_vars'):
474 vars = self._get_config_vars(template_name)
478 vars = vars(template_name)
479 f = file(self._get_config_path(template_name, config_filename), 'w')
480 ctx = Context(f, **vars)
481 self._config_writer_templates[template_name].render_context(ctx)
484 def _write_config(self):
485 r"_write_config() -> None :: Generate all the configuration files."
486 for t in self._config_writer_files:
487 self._write_single_config(t)
490 class ServiceHandler(Handler, Restorable):
491 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
493 This is a helper class to inherit from to automatically handle services
494 with start, stop, restart, reload actions.
496 The actions can be defined by calling the constructor with all the
497 parameters or in a more declarative way as class attributes, like:
499 class TestHandler(ServiceHandler):
500 _service_start = ('command', 'start')
501 _service_stop = ('command', 'stop')
502 _service_restart = ('command', 'restart')
503 _service_reload = 'reload-command'
505 Commands are executed without using the shell, that's why they are specified
506 as tuples (where the first element is the command and the others are the
507 command arguments). If only a command is needed (without arguments) a single
508 string can be specified.
510 All commands must be specified.
512 # TODO implement it using metaclasses to add the handlers method by demand
513 # (only for specifieds commands).
515 def __init__(self, start=None, stop=None, restart=None, reload=None):
516 r"Initialize the object, see the class documentation for details."
517 for (name, action) in dict(start=start, stop=stop, restart=restart,
518 reload=reload).items():
519 if action is not None:
520 setattr(self, '_service_%s' % name, action)
521 self._persistent_attrs = list(self._persistent_attrs)
522 self._persistent_attrs.append('_service_running')
523 if '_service_running' not in self._restorable_defaults:
524 self._restorable_defaults['_service_running'] = False
526 if self._service_running:
527 self._service_running = False
530 @handler(u'Start the service.')
532 r"start() -> None :: Start the service."
533 if not self._service_running:
534 if callable(self._service_start):
535 self._service_start()
537 call(self._service_start)
538 self._service_running = True
539 self._dump_attr('_service_running')
541 @handler(u'Stop the service.')
543 r"stop() -> None :: Stop the service."
544 if self._service_running:
545 if callable(self._service_stop):
548 call(self._service_stop)
549 self._service_running = False
550 self._dump_attr('_service_running')
552 @handler(u'Restart the service.')
554 r"restart() -> None :: Restart the service."
555 if callable(self._service_restart):
556 self._service_restart()
558 call(self._service_restart)
559 self._service_running = True
560 self._dump_attr('_service_running')
562 @handler(u'Reload the service config (without restarting, if possible).')
564 r"reload() -> None :: Reload the configuration of the service."
565 if self._service_running:
566 if callable(self._service_reload):
567 self._service_reload()
569 call(self._service_reload)
571 @handler(u'Tell if the service is running.')
573 r"reload() -> None :: Reload the configuration of the service."
574 if self._service_running:
579 class RestartHandler(Handler):
580 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
582 This is a helper class to inherit from to automatically add a restart
583 command that first stop the service and then starts it again (using start
584 and stop commands respectively).
587 @handler(u'Restart the service (alias to stop + start).')
589 r"restart() -> None :: Restart the service calling stop() and start()."
593 class ReloadHandler(Handler):
594 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
596 This is a helper class to inherit from to automatically add a reload
597 command that calls restart.
600 @handler(u'Reload the service config (alias to restart).')
602 r"reload() -> None :: Reload the configuration of the service."
603 if hasattr(self, '_service_running') and self._service_running:
606 class InitdHandler(ServiceHandler):
607 # TODO update docs, declarative style is depracated
608 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
610 This is a helper class to inherit from to automatically handle services
611 with start, stop, restart, reload actions using a /etc/init.d like script.
613 The name and directory of the script can be defined by calling the
614 constructor or in a more declarative way as class attributes, like:
616 class TestHandler(ServiceHandler):
617 _initd_name = 'some-service'
618 _initd_dir = '/usr/local/etc/init.d'
620 The default _initd_dir is '/etc/init.d', _initd_name has no default and
621 must be specified in either way.
623 Commands are executed without using the shell.
625 # TODO implement it using metaclasses to add the handlers method by demand
626 # (only for specifieds commands).
628 _initd_dir = '/etc/init.d'
630 def __init__(self, initd_name=None, initd_dir=None):
631 r"Initialize the object, see the class documentation for details."
632 if initd_name is not None:
633 self._initd_name = initd_name
634 if initd_dir is not None:
635 self._initd_dir = initd_dir
637 for action in ('start', 'stop', 'restart', 'reload'):
638 actions[action] = (path.join(self._initd_dir, self._initd_name),
640 ServiceHandler.__init__(self, **actions)
642 def handle_timer(self):
643 p = subprocess.Popen(('pgrep', '-f', self._initd_name),
644 stdout=subprocess.PIPE)
645 pid = p.communicate()[0]
646 if p.returncode == 0 and len(pid) > 0:
647 self._service_running = True
649 self._service_running = False
651 class TransactionalHandler(Handler):
652 r"""Handle command transactions providing a commit and rollback commands.
654 This is a helper class to inherit from to automatically handle
655 transactional handlers, which have commit and rollback commands.
657 The handler should provide a reload() method (see ServiceHandler and
658 InitdHandler for helper classes to provide this) which will be called
659 when a commit command is issued (if a reload() command is present).
660 The persistent data will be written too (if a _dump() method is provided,
661 see Persistent and Restorable for that), and the configuration files
662 will be generated (if a _write_config method is present, see ConfigWriter).
664 # TODO implement it using metaclasses to add the handlers method by demand
665 # (only for specifieds commands).
667 @handler(u'Commit the changes (reloading the service, if necessary).')
669 r"commit() -> None :: Commit the changes and reload the service."
670 if hasattr(self, '_dump'):
673 if hasattr(self, '_write_config'):
674 unchanged = self._write_config()
675 if not unchanged and hasattr(self, 'reload'):
678 @handler(u'Discard all the uncommited changes.')
680 r"rollback() -> None :: Discard the changes not yet commited."
681 if hasattr(self, '_load'):
684 class ParametersHandler(Handler):
685 r"""ParametersHandler([attr]) -> ParametersHandler.
687 This is a helper class to inherit from to automatically handle
688 service parameters, providing set, get, list and show commands.
690 The attribute that holds the parameters can be defined by calling the
691 constructor or in a more declarative way as class attributes, like:
693 class TestHandler(ServiceHandler):
694 _parameters_attr = 'some_attr'
696 The default is 'params' and it should be a dictionary.
698 # TODO implement it using metaclasses to add the handlers method by demand
699 # (only for specifieds commands).
701 _parameters_attr = 'params'
703 def __init__(self, attr=None):
704 r"Initialize the object, see the class documentation for details."
706 self._parameters_attr = attr
708 @handler(u'Set a service parameter.')
709 def set(self, param, value):
710 r"set(param, value) -> None :: Set a service parameter."
711 if not param in self.params:
712 raise ParameterNotFoundError(param)
713 self.params[param] = value
714 if hasattr(self, '_update'):
717 @handler(u'Get a service parameter.')
718 def get(self, param):
719 r"get(param) -> None :: Get a service parameter."
720 if not param in self.params:
721 raise ParameterNotFoundError(param)
722 return self.params[param]
724 @handler(u'List all available service parameters.')
726 r"list() -> tuple :: List all the parameter names."
727 return self.params.keys()
729 @handler(u'Get all service parameters, with their values.')
731 r"show() -> (key, value) tuples :: List all the parameters."
732 return self.params.items()
734 class SubHandler(Handler):
735 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
737 This is a helper class to build sub handlers that needs to reference the
740 parent - Parent Handler object.
743 def __init__(self, parent):
744 r"Initialize the object, see the class documentation for details."
747 class ContainerSubHandler(SubHandler):
748 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
750 This is a helper class to implement ListSubHandler and DictSubHandler. You
751 should not use it directly.
753 The container attribute to handle and the class of objects that it
754 contains can be defined by calling the constructor or in a more declarative
755 way as class attributes, like:
757 class TestHandler(ContainerSubHandler):
758 _cont_subhandler_attr = 'some_cont'
759 _cont_subhandler_class = SomeClass
761 This way, the parent's some_cont attribute (self.parent.some_cont)
762 will be managed automatically, providing the commands: add, update,
763 delete, get and show. New items will be instances of SomeClass,
764 which should provide a cmp operator to see if the item is on the
765 container and an update() method, if it should be possible to modify
766 it. If SomeClass has an _add, _update or _delete attribute, it set
767 them to true when the item is added, updated or deleted respectively
768 (in case that it's deleted, it's not removed from the container,
769 but it's not listed either).
772 def __init__(self, parent, attr=None, cls=None):
773 r"Initialize the object, see the class documentation for details."
776 self._cont_subhandler_attr = attr
778 self._cont_subhandler_class = cls
780 def _attr(self, attr=None):
782 return getattr(self.parent, self._cont_subhandler_attr)
783 setattr(self.parent, self._cont_subhandler_attr, attr)
786 if isinstance(self._attr(), dict):
787 return dict([(k, i) for (k, i) in self._attr().items()
788 if not hasattr(i, '_delete') or not i._delete])
789 return [i for i in self._attr()
790 if not hasattr(i, '_delete') or not i._delete]
792 @handler(u'Add a new item')
793 def add(self, *args, **kwargs):
794 r"add(...) -> None :: Add an item to the list."
795 item = self._cont_subhandler_class(*args, **kwargs)
796 if hasattr(item, '_add'):
799 if isinstance(self._attr(), dict):
800 key = item.as_tuple()[0]
801 # do we have the same item? then raise an error
802 if key in self._vattr():
803 raise ItemAlreadyExistsError(item)
804 # do we have the same item, but logically deleted? then update flags
805 if key in self._attr():
807 if not isinstance(self._attr(), dict):
808 index = self._attr().index(item)
809 if hasattr(item, '_add'):
810 self._attr()[index]._add = False
811 if hasattr(item, '_delete'):
812 self._attr()[index]._delete = False
813 else: # it's *really* new
814 if isinstance(self._attr(), dict):
815 self._attr()[key] = item
817 self._attr().append(item)
819 @handler(u'Update an item')
820 def update(self, index, *args, **kwargs):
821 r"update(index, ...) -> None :: Update an item of the container."
822 # TODO make it right with metaclasses, so the method is not created
823 # unless the update() method really exists.
824 # TODO check if the modified item is the same of an existing one
825 if not isinstance(self._attr(), dict):
826 index = int(index) # TODO validation
827 if not hasattr(self._cont_subhandler_class, 'update'):
828 raise CommandNotFoundError(('update',))
830 item = self._vattr()[index]
831 item.update(*args, **kwargs)
832 if hasattr(item, '_update'):
835 raise ItemNotFoundError(index)
837 @handler(u'Delete an item')
838 def delete(self, index):
839 r"delete(index) -> None :: Delete an item of the container."
840 if not isinstance(self._attr(), dict):
841 index = int(index) # TODO validation
843 item = self._vattr()[index]
844 if hasattr(item, '_delete'):
847 del self._attr()[index]
850 raise ItemNotFoundError(index)
852 @handler(u'Remove all items (use with care).')
854 r"clear() -> None :: Delete all items of the container."
855 if isinstance(self._attr(), dict):
860 @handler(u'Get information about an item')
861 def get(self, index):
862 r"get(index) -> item :: List all the information of an item."
863 if not isinstance(self._attr(), dict):
864 index = int(index) # TODO validation
866 return self._vattr()[index]
868 raise ItemNotFoundError(index)
870 @handler(u'Get information about all items')
872 r"show() -> list of items :: List all the complete items information."
873 if isinstance(self._attr(), dict):
874 return self._attr().values()
877 class ListSubHandler(ContainerSubHandler):
878 r"""ListSubHandler(parent) -> ListSubHandler instance.
880 ContainerSubHandler holding lists. See ComposedSubHandler documentation
884 @handler(u'Get how many items are in the list')
886 r"len() -> int :: Get how many items are in the list."
887 return len(self._vattr())
889 class DictSubHandler(ContainerSubHandler):
890 r"""DictSubHandler(parent) -> DictSubHandler instance.
892 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
896 @handler(u'List all the items by key')
898 r"list() -> tuple :: List all the item keys."
899 return self._attr().keys()
901 class ComposedSubHandler(SubHandler):
902 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
904 This is a helper class to implement ListComposedSubHandler and
905 DictComposedSubHandler. You should not use it directly.
907 This class is usefull when you have a parent that has a dict (cont)
908 that stores some object that has an attribute (attr) with a list or
909 a dict of objects of some class. In that case, this class provides
910 automated commands to add, update, delete, get and show that objects.
911 This commands takes the cont (key of the dict for the object holding
912 the attr), and an index for access the object itself (in the attr
915 The container object (cont) that holds a containers, the attribute of
916 that object that is the container itself, and the class of the objects
917 that it contains can be defined by calling the constructor or in a
918 more declarative way as class attributes, like:
920 class TestHandler(ComposedSubHandler):
921 _comp_subhandler_cont = 'some_cont'
922 _comp_subhandler_attr = 'some_attr'
923 _comp_subhandler_class = SomeClass
925 This way, the parent's some_cont attribute (self.parent.some_cont)
926 will be managed automatically, providing the commands: add, update,
927 delete, get and show for manipulating a particular instance that holds
928 of SomeClass. For example, updating an item at the index 5 is the same
929 (simplified) as doing parent.some_cont[cont][5].update().
930 SomeClass should provide a cmp operator to see if the item is on the
931 container and an update() method, if it should be possible to modify
932 it. If SomeClass has an _add, _update or _delete attribute, it set
933 them to true when the item is added, updated or deleted respectively
934 (in case that it's deleted, it's not removed from the container,
935 but it's not listed either). If the container objects
936 (parent.some_cont[cont]) has an _update attribute, it's set to True
937 when any add, update or delete command is executed.
940 def __init__(self, parent, cont=None, attr=None, cls=None):
941 r"Initialize the object, see the class documentation for details."
944 self._comp_subhandler_cont = cont
946 self._comp_subhandler_attr = attr
948 self._comp_subhandler_class = cls
951 return getattr(self.parent, self._comp_subhandler_cont)
953 def _attr(self, cont, attr=None):
955 return getattr(self._cont()[cont], self._comp_subhandler_attr)
956 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
958 def _vattr(self, cont):
959 if isinstance(self._attr(cont), dict):
960 return dict([(k, i) for (k, i) in self._attr(cont).items()
961 if not hasattr(i, '_delete') or not i._delete])
962 return [i for i in self._attr(cont)
963 if not hasattr(i, '_delete') or not i._delete]
965 @handler(u'Add a new item')
966 def add(self, cont, *args, **kwargs):
967 r"add(cont, ...) -> None :: Add an item to the list."
968 if not cont in self._cont():
969 raise ContainerNotFoundError(cont)
970 item = self._comp_subhandler_class(*args, **kwargs)
971 if hasattr(item, '_add'):
974 if isinstance(self._attr(cont), dict):
975 key = item.as_tuple()[0]
976 # do we have the same item? then raise an error
977 if key in self._vattr(cont):
978 raise ItemAlreadyExistsError(item)
979 # do we have the same item, but logically deleted? then update flags
980 if key in self._attr(cont):
982 if not isinstance(self._attr(cont), dict):
983 index = self._attr(cont).index(item)
984 if hasattr(item, '_add'):
985 self._attr(cont)[index]._add = False
986 if hasattr(item, '_delete'):
987 self._attr(cont)[index]._delete = False
988 else: # it's *really* new
989 if isinstance(self._attr(cont), dict):
990 self._attr(cont)[key] = item
992 self._attr(cont).append(item)
993 if hasattr(self._cont()[cont], '_update'):
994 self._cont()[cont]._update = True
996 @handler(u'Update an item')
997 def update(self, cont, index, *args, **kwargs):
998 r"update(cont, index, ...) -> None :: Update an item of the container."
999 # TODO make it right with metaclasses, so the method is not created
1000 # unless the update() method really exists.
1001 # TODO check if the modified item is the same of an existing one
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
1006 if not hasattr(self._comp_subhandler_class, 'update'):
1007 raise CommandNotFoundError(('update',))
1009 item = self._vattr(cont)[index]
1010 item.update(*args, **kwargs)
1011 if hasattr(item, '_update'):
1013 if hasattr(self._cont()[cont], '_update'):
1014 self._cont()[cont]._update = True
1016 raise ItemNotFoundError(index)
1018 @handler(u'Delete an item')
1019 def delete(self, cont, index):
1020 r"delete(cont, index) -> None :: Delete an item of the container."
1021 if not cont in self._cont():
1022 raise ContainerNotFoundError(cont)
1023 if not isinstance(self._attr(cont), dict):
1024 index = int(index) # TODO validation
1026 item = self._vattr(cont)[index]
1027 if hasattr(item, '_delete'):
1030 del self._attr(cont)[index]
1031 if hasattr(self._cont()[cont], '_update'):
1032 self._cont()[cont]._update = True
1035 raise ItemNotFoundError(index)
1037 @handler(u'Remove all items (use with care).')
1038 def clear(self, cont):
1039 r"clear(cont) -> None :: Delete all items of the container."
1040 if not cont in self._cont():
1041 raise ContainerNotFoundError(cont)
1042 if isinstance(self._attr(cont), dict):
1043 self._attr(cont).clear()
1045 self._attr(cont, list())
1047 @handler(u'Get information about an item')
1048 def get(self, cont, index):
1049 r"get(cont, index) -> item :: List all the information of an item."
1050 if not cont in self._cont():
1051 raise ContainerNotFoundError(cont)
1052 if not isinstance(self._attr(cont), dict):
1053 index = int(index) # TODO validation
1055 return self._vattr(cont)[index]
1057 raise ItemNotFoundError(index)
1059 @handler(u'Get information about all items')
1060 def show(self, cont):
1061 r"show(cont) -> list of items :: List all the complete items information."
1062 if not cont in self._cont():
1063 raise ContainerNotFoundError(cont)
1064 if isinstance(self._attr(cont), dict):
1065 return self._attr(cont).values()
1066 return self._vattr(cont)
1068 class ListComposedSubHandler(ComposedSubHandler):
1069 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1071 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1075 @handler(u'Get how many items are in the list')
1076 def len(self, cont):
1077 r"len(cont) -> int :: Get how many items are in the list."
1078 if not cont in self._cont():
1079 raise ContainerNotFoundError(cont)
1080 return len(self._vattr(cont))
1082 class DictComposedSubHandler(ComposedSubHandler):
1083 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1085 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1089 @handler(u'List all the items by key')
1090 def list(self, cont):
1091 r"list(cont) -> tuple :: List all the item keys."
1092 if not cont in self._cont():
1093 raise ContainerNotFoundError(cont)
1094 return self._attr(cont).keys()
1097 if __name__ == '__main__':
1102 class STestHandler1(ServiceHandler):
1103 _service_start = ('service', 'start')
1104 _service_stop = ('service', 'stop')
1105 _service_restart = ('ls', '/')
1106 _service_reload = ('cp', '/la')
1107 class STestHandler2(ServiceHandler):
1109 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1110 'cmd-restart', 'cmd-reload')
1111 class ITestHandler1(InitdHandler):
1112 _initd_name = 'test1'
1113 class ITestHandler2(InitdHandler):
1115 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1123 print h.__class__.__name__
1126 except ExecutionError, e:
1130 except ExecutionError, e:
1134 except ExecutionError, e:
1138 except ExecutionError, e:
1143 print 'PTestHandler'
1144 class PTestHandler(Persistent):
1145 _persistent_attrs = 'vars'
1147 self.vars = dict(a=1, b=2)
1165 print 'RTestHandler'
1166 class RTestHandler(Restorable):
1167 _persistent_attrs = 'vars'
1168 _restorable_defaults = dict(vars=dict(a=1, b=2))
1180 print 'CTestHandler'
1182 os.mkdir('templates')
1183 f = file('templates/config', 'w')
1184 f.write('Hello, ${name}! You are ${what}.')
1187 print file('templates/config').read()
1188 class CTestHandler(ConfigWriter):
1189 _config_writer_files = 'config'
1191 self._config_build_templates()
1192 def _get_config_vars(self, config_file):
1193 return dict(name='you', what='a parrot')
1197 print file('config').read()
1199 os.unlink('templates/config')
1200 os.rmdir('templates')
1203 print get_network_devices()