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
11 import logging ; log = logging.getLogger('pymin.services.util')
13 from pymin.dispatcher import Handler, handler, HandlerError, \
15 from pymin.seqtools import Sequence
20 __all__ = ('Error', 'ExecutionError', 'ItemError', 'ItemAlreadyExistsError',
21 'ItemNotFoundError', 'ContainerError', 'ContainerNotFoundError',
22 'call', 'get_network_devices', 'Persistent', 'Restorable',
23 'ConfigWriter', 'ServiceHandler', 'RestartHandler',
24 'ReloadHandler', 'InitdHandler', 'SubHandler', 'DictSubHandler',
25 'ListSubHandler', 'ComposedSubHandler', 'ListComposedSubHandler',
26 'DictComposedSubHandler', 'Device','Address')
28 class Error(HandlerError):
30 Error(message) -> Error instance :: Base ServiceHandler exception class.
32 All exceptions raised by the ServiceHandler inherits from this one, so
33 you can easily catch any ServiceHandler exception.
35 message - A descriptive error message.
39 class ExecutionError(Error):
41 ExecutionError(command, error) -> ExecutionError instance.
43 Error executing a command.
45 command - Command that was tried to execute.
47 error - Error received when trying to execute the command.
50 def __init__(self, command, error):
51 r"Initialize the object. See class documentation for more info."
52 self.command = command
55 def __unicode__(self):
56 command = self.command
57 if not isinstance(self.command, basestring):
58 command = ' '.join(command)
59 return "Can't execute command %s: %s" % (command, self.error)
61 class ParameterError(Error, KeyError):
63 ParameterError(paramname) -> ParameterError instance
65 This is the base exception for all DhcpHandler parameters related errors.
68 def __init__(self, paramname):
69 r"Initialize the object. See class documentation for more info."
70 self.message = 'Parameter error: "%s"' % paramname
72 class ParameterNotFoundError(ParameterError):
74 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
76 This exception is raised when trying to operate on a parameter that doesn't
80 def __init__(self, paramname):
81 r"Initialize the object. See class documentation for more info."
82 self.message = 'Parameter not found: "%s"' % paramname
84 class ItemError(Error, KeyError):
86 ItemError(key) -> ItemError instance.
88 This is the base exception for all item related errors.
91 def __init__(self, key):
92 r"Initialize the object. See class documentation for more info."
93 self.message = u'Item error: "%s"' % key
95 class ItemAlreadyExistsError(ItemError):
97 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
99 This exception is raised when trying to add an item that already exists.
102 def __init__(self, key):
103 r"Initialize the object. See class documentation for more info."
104 self.message = u'Item already exists: %s' % key
106 class ItemNotFoundError(ItemError):
108 ItemNotFoundError(key) -> ItemNotFoundError instance.
110 This exception is raised when trying to operate on an item that doesn't
114 def __init__(self, key):
115 r"Initialize the object. See class documentation for more info."
116 self.message = u'Item not found: "%s"' % key
118 class ContainerError(Error, KeyError):
120 ContainerError(key) -> ContainerError instance.
122 This is the base exception for all container related errors.
125 def __init__(self, key):
126 r"Initialize the object. See class documentation for more info."
127 self.message = u'Container error: "%s"' % key
129 class ContainerNotFoundError(ContainerError):
131 ContainerNotFoundError(key) -> ContainerNotFoundError instance.
133 This exception is raised when trying to operate on an container that
137 def __init__(self, key):
138 r"Initialize the object. See class documentation for more info."
139 self.message = u'Container not found: "%s"' % key
141 class Address(Sequence):
142 def __init__(self, ip, netmask, broadcast=None, peer=None):
144 self.netmask = netmask
145 self.broadcast = broadcast
147 def update(self, netmask=None, broadcast=None):
148 if netmask is not None: self.netmask = netmask
149 if broadcast is not None: self.broadcast = broadcast
151 return (self.ip, self.netmask, self.broadcast, self.peer)
154 class Device(Sequence):
155 def __init__(self, name, mac, ppp):
163 return (self.name, self.mac, self.active, self.addrs)
167 def get_network_devices():
168 p = subprocess.Popen(('ip', '-o', 'link'), stdout=subprocess.PIPE,
170 string = p.stdout.read()
173 devices = string.splitlines()
176 if dev.find('link/ether') != -1:
177 i = dev.find('link/ether')
178 mac = dev[i+11 : i+11+17]
180 j = dev.find(':', i+1)
182 d[name] = Device(name,mac,False)
183 elif dev.find('link/ppp') != -1:
184 i = dev.find('link/ppp')
185 mac = '00:00:00:00:00:00'
187 j = dev.find(':', i+1)
189 d[name] = Device(name,mac,True)
190 #since the device is ppp, get the address and peer
192 p = subprocess.Popen(('ip', '-o', 'addr', 'show', name), stdout=subprocess.PIPE,
193 close_fds=True, stderr=subprocess.PIPE)
194 string = p.stdout.read()
196 addrs = string.splitlines()
197 inet = addrs[1].find('inet')
198 peer = addrs[1].find('peer')
199 bar = addrs[1].find('/')
200 from_addr = addrs[1][inet+5 : peer-1]
201 to_addr = addrs[1][peer+5 : bar]
202 d[name].addrs[from_addr] = Address(from_addr,24, peer=to_addr)
208 p = subprocess.Popen(('ip', '-o', 'addr'), stdout=subprocess.PIPE,
211 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
212 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
214 log.debug(u'call(%r)', command)
216 log.debug(u'call: not really executing, DEBUG mode')
219 subprocess.check_call(command, stdin=stdin, stdout=stdout,
220 stderr=stderr, close_fds=close_fds,
221 universal_newlines=universal_newlines, **kw)
223 log.debug(u'call: Execution error %r', e)
224 raise ExecutionError(command, e)
227 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
229 This is a helper class to inherit from to automatically handle data
230 persistence using pickle.
232 The variables attributes to persist (attrs), and the pickle directory (dir)
233 and file extension (ext) can be defined by calling the constructor or in a
234 more declarative way as class attributes, like:
236 class TestHandler(Persistent):
237 _persistent_attrs = ('some_attr', 'other_attr')
238 _persistent_dir = 'persistent-data'
239 _persistent_ext = '.pickle'
241 The default dir is '.' and the default extension is '.pkl'. There are no
242 default variables, and they should be specified as string if a single
243 attribute should be persistent or as a tuple of strings if they are more.
244 The strings should be the attribute names to be persisted. For each
245 attribute a separated pickle file is generated in the pickle directory.
247 You can call _dump() and _load() to write and read the data respectively.
249 # TODO implement it using metaclasses to add the handlers method by demand
250 # (only for specifieds commands).
252 _persistent_attrs = ()
253 _persistent_dir = '.'
254 _persistent_ext = '.pkl'
256 def __init__(self, attrs=None, dir=None, ext=None):
257 r"Initialize the object, see the class documentation for details."
258 if attrs is not None:
259 self._persistent_attrs = attrs
261 self._persistent_dir = dir
263 self._persistent_ext = ext
266 r"_dump() -> None :: Dump all persistent data to pickle files."
267 if isinstance(self._persistent_attrs, basestring):
268 self._persistent_attrs = (self._persistent_attrs,)
269 for attrname in self._persistent_attrs:
270 self._dump_attr(attrname)
273 r"_load() -> None :: Load all persistent data from pickle files."
274 if isinstance(self._persistent_attrs, basestring):
275 self._persistent_attrs = (self._persistent_attrs,)
276 for attrname in self._persistent_attrs:
277 self._load_attr(attrname)
279 def _dump_attr(self, attrname):
280 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
281 fname = self._pickle_filename(attrname)
282 attr = getattr(self, attrname)
283 log.debug(u'Persistent._dump_attr(%r) -> file=%r, value=%r',
284 attrname, fname, attr)
285 f = file(self._pickle_filename(attrname), 'wb')
286 pickle.dump(attr, f, 2)
289 def _load_attr(self, attrname):
290 r"_load_attr() -> object :: Load a specific pickle file."
291 fname = self._pickle_filename(attrname)
293 attr = pickle.load(f)
294 log.debug(u'Persistent._load_attr(%r) -> file=%r, value=%r',
295 attrname, fname, attr)
296 setattr(self, attrname, attr)
299 def _pickle_filename(self, name):
300 r"_pickle_filename() -> string :: Construct a pickle filename."
301 return path.join(self._persistent_dir, name) + self._persistent_ext
303 class Restorable(Persistent):
304 r"""Restorable([defaults]) -> Restorable.
306 This is a helper class to inherit from that provides a nice _restore()
307 method to restore the persistent data if any, or load some nice defaults
310 The defaults can be defined by calling the constructor or in a more
311 declarative way as class attributes, like:
313 class TestHandler(Restorable):
314 _persistent_attrs = ('some_attr', 'other_attr')
315 _restorable_defaults = dict(
316 some_attr = 'some_default',
317 other_attr = 'other_default')
319 The defaults is a dictionary, very coupled with the _persistent_attrs
320 attribute inherited from Persistent. The defaults keys should be the
321 values from _persistent_attrs, and the values the default values.
323 The _restore() method returns True if the data was restored successfully
324 or False if the defaults were loaded (in case you want to take further
325 actions). If a _write_config method if found, it's executed when a restore
328 # TODO implement it using metaclasses to add the handlers method by demand
329 # (only for specifieds commands).
331 _restorable_defaults = dict()
333 def __init__(self, defaults=None):
334 r"Initialize the object, see the class documentation for details."
335 if defaults is not None:
336 self._restorable_defaults = defaults
339 r"_restore() -> bool :: Restore persistent data or create a default."
340 log.debug(u'Restorable._restore()')
342 log.debug(u'Restorable._restore: trying to load...')
344 log.debug(u'Restorable._restore: load OK')
347 log.debug(u'Restorable._restore: load FAILED, making defaults: %r',
348 self._restorable_defaults)
349 for (k, v) in self._restorable_defaults.items():
351 # TODO tener en cuenta servicios que hay que levantar y los que no
352 if hasattr(self, 'commit'):
353 log.debug(u'Restorable._restore: commit() found, commiting...')
356 log.debug(u'Restorable._restore: dumping new defaults...')
358 if hasattr(self, '_write_config'):
359 log.debug(u'Restorable._restore: _write_config() found, '
360 u'writing new config...')
365 r"""ConfigWriter([files[, cfg_dir[, tpl_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 log.debug(u'ConfigWriter.build_templates()')
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 log.debug(u'ConfigWriter._render_config: rendering template %r with '
450 u'vars=%r', template_name, vars)
451 return self._config_writer_templates[template_name].render(**vars)
453 def _get_config_path(self, template_name, config_filename=None):
454 r"Get a complete configuration path."
455 if not config_filename:
456 config_filename = template_name
457 if isinstance(self._config_writer_cfg_dir, basestring):
458 return path.join(self._config_writer_cfg_dir, config_filename)
459 return path.join(self._config_writer_cfg_dir[template_name],
462 def _write_single_config(self, template_name, config_filename=None, vars=None):
463 r"""_write_single_config(template_name[, config_filename[, vars]]).
465 Write a single config file using the template 'template_name'. If no
466 config_filename is specified, the config filename will be the same as
467 the 'template_name' (but stored in the generated config files
468 directory). If it's specified, the generated config file is stored in
469 the file called 'config_filename' (also in the generated files
470 directory). If vars is specified, it's used as the dictionary with the
471 variables to replace in the templates, if not, it looks for a
472 _get_config_vars() method to get it.
475 if hasattr(self, '_get_config_vars'):
476 vars = self._get_config_vars(template_name)
480 vars = vars(template_name)
481 fname = self._get_config_path(template_name, config_filename)
483 ctx = Context(f, **vars)
484 log.debug(u'ConfigWriter._write_single_config: rendering template '
485 u'%r with vars=%r to file %r', template_name, vars, fname)
486 self._config_writer_templates[template_name].render_context(ctx)
489 def _write_config(self):
490 r"_write_config() -> None :: Generate all the configuration files."
491 for t in self._config_writer_files:
492 self._write_single_config(t)
495 class ServiceHandler(Handler, Restorable):
496 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
498 This is a helper class to inherit from to automatically handle services
499 with start, stop, restart, reload actions.
501 The actions can be defined by calling the constructor with all the
502 parameters or in a more declarative way as class attributes, like:
504 class TestHandler(ServiceHandler):
505 _service_start = ('command', 'start')
506 _service_stop = ('command', 'stop')
507 _service_restart = ('command', 'restart')
508 _service_reload = 'reload-command'
510 Commands are executed without using the shell, that's why they are specified
511 as tuples (where the first element is the command and the others are the
512 command arguments). If only a command is needed (without arguments) a single
513 string can be specified.
515 All commands must be specified.
517 # TODO implement it using metaclasses to add the handlers method by demand
518 # (only for specifieds commands).
520 def __init__(self, start=None, stop=None, restart=None, reload=None):
521 r"Initialize the object, see the class documentation for details."
522 log.debug(u'ServiceHandler(%r, %r, %r, %r)', start, stop, restart,
524 for (name, action) in dict(start=start, stop=stop, restart=restart,
525 reload=reload).items():
526 if action is not None:
527 attr_name = '_service_%s' % name
528 log.debug(u'ServiceHandler: using %r as %s', attr_name, action)
529 setattr(self, attr_name, action)
530 self._persistent_attrs = list(self._persistent_attrs)
531 self._persistent_attrs.append('_service_running')
532 if '_service_running' not in self._restorable_defaults:
533 self._restorable_defaults['_service_running'] = False
534 log.debug(u'ServiceHandler: restoring service configuration...')
536 if self._service_running:
537 log.debug(u'ServiceHandler: service was running, starting it...')
538 self._service_running = False
541 @handler(u'Start the service.')
543 r"start() -> None :: Start the service."
544 log.debug(u'ServiceHandler.start()')
545 if not self._service_running:
546 log.debug(u'ServiceHandler.start: not running, starting it...')
547 if callable(self._service_start):
548 self._service_start()
550 call(self._service_start)
551 self._service_running = True
552 self._dump_attr('_service_running')
554 @handler(u'Stop the service.')
556 log.debug(u'ServiceHandler.stop()')
557 r"stop() -> None :: Stop the service."
558 if self._service_running:
559 log.debug(u'ServiceHandler.stop: running, stoping it...')
560 if callable(self._service_stop):
563 call(self._service_stop)
564 self._service_running = False
565 self._dump_attr('_service_running')
567 @handler(u'Restart the service.')
569 r"restart() -> None :: Restart the service."
570 log.debug(u'ServiceHandler.restart()')
571 if callable(self._service_restart):
572 self._service_restart()
574 call(self._service_restart)
575 self._service_running = True
576 self._dump_attr('_service_running')
578 @handler(u'Reload the service config (without restarting, if possible).')
580 r"reload() -> None :: Reload the configuration of the service."
581 log.debug(u'ServiceHandler.reload()')
582 if self._service_running:
583 log.debug(u'ServiceHandler.reload: running, reloading...')
584 if callable(self._service_reload):
585 self._service_reload()
587 call(self._service_reload)
589 @handler(u'Tell if the service is running.')
591 r"reload() -> None :: Reload the configuration of the service."
592 log.debug(u'ServiceHandler.running() -> %r', self._service_running)
593 if self._service_running:
598 class RestartHandler(Handler):
599 r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
601 This is a helper class to inherit from to automatically add a restart
602 command that first stop the service and then starts it again (using start
603 and stop commands respectively).
606 @handler(u'Restart the service (alias to stop + start).')
608 r"restart() -> None :: Restart the service calling stop() and start()."
609 log.debug(u'RestartHandler.restart()')
613 class ReloadHandler(Handler):
614 r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
616 This is a helper class to inherit from to automatically add a reload
617 command that calls restart.
620 @handler(u'Reload the service config (alias to restart).')
622 r"reload() -> None :: Reload the configuration of the service."
623 log.debug(u'ReloadHandler.reload()')
624 if hasattr(self, '_service_running') and self._service_running:
625 log.debug(u'ReloadHandler.reload: running, reloading...')
628 class InitdHandler(ServiceHandler):
629 # TODO update docs, declarative style is depracated
630 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
632 This is a helper class to inherit from to automatically handle services
633 with start, stop, restart, reload actions using a /etc/init.d like script.
635 The name and directory of the script can be defined by calling the
636 constructor or in a more declarative way as class attributes, like:
638 class TestHandler(ServiceHandler):
639 _initd_name = 'some-service'
640 _initd_dir = '/usr/local/etc/init.d'
642 The default _initd_dir is '/etc/init.d', _initd_name has no default and
643 must be specified in either way.
645 Commands are executed without using the shell.
647 # TODO implement it using metaclasses to add the handlers method by demand
648 # (only for specifieds commands).
650 _initd_dir = '/etc/init.d'
652 def __init__(self, initd_name=None, initd_dir=None):
653 r"Initialize the object, see the class documentation for details."
654 log.debug(u'InitdHandler(%r, %r)', initd_name, initd_dir)
655 if initd_name is not None:
656 self._initd_name = initd_name
657 if initd_dir is not None:
658 self._initd_dir = initd_dir
660 for action in ('start', 'stop', 'restart', 'reload'):
661 actions[action] = (path.join(self._initd_dir, self._initd_name),
663 ServiceHandler.__init__(self, **actions)
665 def handle_timer(self):
667 log.debug(u'InitdHandler.handle_timer(): self=%r', self)
668 p = subprocess.Popen(('pgrep', '-f', self._initd_name),
669 stdout=subprocess.PIPE)
670 pid = p.communicate()[0]
671 if p.returncode == 0 and len(pid) > 0:
672 log.debug(u'InitdHandler.handle_timer: pid present, running')
673 self._service_running = True
675 log.debug(u'InitdHandler.handle_timer: pid absent, NOT running')
676 self._service_running = False
678 class TransactionalHandler(Handler):
679 r"""Handle command transactions providing a commit and rollback commands.
681 This is a helper class to inherit from to automatically handle
682 transactional handlers, which have commit and rollback commands.
684 The handler should provide a reload() method (see ServiceHandler and
685 InitdHandler for helper classes to provide this) which will be called
686 when a commit command is issued (if a reload() command is present).
687 The persistent data will be written too (if a _dump() method is provided,
688 see Persistent and Restorable for that), and the configuration files
689 will be generated (if a _write_config method is present, see ConfigWriter).
691 # TODO implement it using metaclasses to add the handlers method by demand
692 # (only for specifieds commands).
694 @handler(u'Commit the changes (reloading the service, if necessary).')
696 r"commit() -> None :: Commit the changes and reload the service."
697 log.debug(u'TransactionalHandler.commit()')
698 if hasattr(self, '_dump'):
699 log.debug(u'TransactionalHandler.commit: _dump() present, '
703 if hasattr(self, '_write_config'):
704 log.debug(u'TransactionalHandler.commit: _write_config() present, '
705 u'writing config...')
706 unchanged = self._write_config()
707 if not unchanged and hasattr(self, 'reload'):
708 log.debug(u'TransactionalHandler.commit: reload() present, and'
709 u'configuration changed, reloading...')
712 @handler(u'Discard all the uncommited changes.')
714 r"rollback() -> None :: Discard the changes not yet commited."
715 log.debug(u'TransactionalHandler.reload()')
716 if hasattr(self, '_load'):
717 log.debug(u'TransactionalHandler.reload: _load() present, loading'
718 u'pickled values...')
721 class ParametersHandler(Handler):
722 r"""ParametersHandler([attr]) -> ParametersHandler.
724 This is a helper class to inherit from to automatically handle
725 service parameters, providing set, get, list and show commands.
727 The attribute that holds the parameters can be defined by calling the
728 constructor or in a more declarative way as class attributes, like:
730 class TestHandler(ServiceHandler):
731 _parameters_attr = 'some_attr'
733 The default is 'params' and it should be a dictionary.
735 # TODO implement it using metaclasses to add the handlers method by demand
736 # (only for specifieds commands).
738 _parameters_attr = 'params'
740 def __init__(self, attr=None):
741 r"Initialize the object, see the class documentation for details."
742 log.debug(u'ParametersHandler(%r)', attr)
744 self._parameters_attr = attr
746 @handler(u'Set a service parameter.')
747 def set(self, param, value):
748 r"set(param, value) -> None :: Set a service parameter."
749 log.debug(u'ParametersHandler.set(%r, %r)', param, value)
750 if not param in self.params:
751 log.debug(u'ParametersHandler.set: parameter not found')
752 raise ParameterNotFoundError(param)
753 self.params[param] = value
754 if hasattr(self, '_update'):
755 log.debug(u'ParametersHandler.set: _update found, setting to True')
758 @handler(u'Get a service parameter.')
759 def get(self, param):
760 r"get(param) -> None :: Get a service parameter."
761 log.debug(u'ParametersHandler.get(%r)', param)
762 if not param in self.params:
763 log.debug(u'ParametersHandler.get: parameter not found')
764 raise ParameterNotFoundError(param)
765 return self.params[param]
767 @handler(u'List all available service parameters.')
769 r"list() -> tuple :: List all the parameter names."
770 log.debug(u'ParametersHandler.list()')
771 return self.params.keys()
773 @handler(u'Get all service parameters, with their values.')
775 r"show() -> (key, value) tuples :: List all the parameters."
776 log.debug(u'ParametersHandler.show()')
777 return self.params.items()
779 class SubHandler(Handler):
780 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
782 This is a helper class to build sub handlers that needs to reference the
785 parent - Parent Handler object.
788 def __init__(self, parent):
789 r"Initialize the object, see the class documentation for details."
790 log.debug(u'SubHandler(%r)', parent)
793 class ContainerSubHandler(SubHandler):
794 r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
796 This is a helper class to implement ListSubHandler and DictSubHandler. You
797 should not use it directly.
799 The container attribute to handle and the class of objects that it
800 contains can be defined by calling the constructor or in a more declarative
801 way as class attributes, like:
803 class TestHandler(ContainerSubHandler):
804 _cont_subhandler_attr = 'some_cont'
805 _cont_subhandler_class = SomeClass
807 This way, the parent's some_cont attribute (self.parent.some_cont)
808 will be managed automatically, providing the commands: add, update,
809 delete, get and show. New items will be instances of SomeClass,
810 which should provide a cmp operator to see if the item is on the
811 container and an update() method, if it should be possible to modify
812 it. If SomeClass has an _add, _update or _delete attribute, it set
813 them to true when the item is added, updated or deleted respectively
814 (in case that it's deleted, it's not removed from the container,
815 but it's not listed either).
818 def __init__(self, parent, attr=None, cls=None):
819 r"Initialize the object, see the class documentation for details."
820 log.debug(u'ContainerSubHandler(%r, %r, %r)', parent, attr, cls)
823 self._cont_subhandler_attr = attr
825 self._cont_subhandler_class = cls
827 def _attr(self, attr=None):
829 return getattr(self.parent, self._cont_subhandler_attr)
830 setattr(self.parent, self._cont_subhandler_attr, attr)
833 if isinstance(self._attr(), dict):
834 return dict([(k, i) for (k, i) in self._attr().items()
835 if not hasattr(i, '_delete') or not i._delete])
836 return [i for i in self._attr()
837 if not hasattr(i, '_delete') or not i._delete]
839 @handler(u'Add a new item')
840 def add(self, *args, **kwargs):
841 r"add(...) -> None :: Add an item to the list."
842 log.debug(u'ContainerSubHandler.add(%r, %r)', args, kwargs)
843 item = self._cont_subhandler_class(*args, **kwargs)
844 if hasattr(item, '_add'):
845 log.debug(u'ContainerSubHandler.add: _add found, setting to True')
848 if isinstance(self._attr(), dict):
849 key = item.as_tuple()[0]
850 # do we have the same item? then raise an error
851 if key in self._vattr():
852 log.debug(u'ContainerSubHandler.add: allready exists')
853 if not isinstance(self._attr(), dict):
854 key = self._attr().index(item)
855 raise ItemAlreadyExistsError(key)
856 # do we have the same item, but logically deleted? then update flags
857 if key in self._attr():
858 log.debug(u'ContainerSubHandler.add: was deleted, undeleting it')
860 if not isinstance(self._attr(), dict):
861 index = self._attr().index(item)
862 if hasattr(item, '_add'):
863 self._attr()[index]._add = False
864 if hasattr(item, '_delete'):
865 self._attr()[index]._delete = False
866 else: # it's *really* new
867 if isinstance(self._attr(), dict):
868 self._attr()[key] = item
870 self._attr().append(item)
872 @handler(u'Update an item')
873 def update(self, index, *args, **kwargs):
874 r"update(index, ...) -> None :: Update an item of the container."
875 log.debug(u'ContainerSubHandler.update(%r, %r, %r)',
877 # TODO make it right with metaclasses, so the method is not created
878 # unless the update() method really exists.
879 if not isinstance(self._attr(), dict):
880 index = int(index) # TODO validation
881 if not hasattr(self._cont_subhandler_class, 'update'):
882 log.debug(u'ContainerSubHandler.update: update() not found, '
883 u"can't really update, raising command not found")
884 raise CommandNotFoundError(('update',))
886 item = self._vattr()[index]
887 item.update(*args, **kwargs)
888 if hasattr(item, '_update'):
889 log.debug(u'ContainerSubHandler.update: _update found, '
893 log.debug(u'ContainerSubHandler.update: item not found')
894 raise ItemNotFoundError(index)
896 @handler(u'Delete an item')
897 def delete(self, index):
898 r"delete(index) -> None :: Delete an item of the container."
899 log.debug(u'ContainerSubHandler.delete(%r)', index)
900 if not isinstance(self._attr(), dict):
901 index = int(index) # TODO validation
903 item = self._vattr()[index]
904 if hasattr(item, '_delete'):
905 log.debug(u'ContainerSubHandler.delete: _delete found, '
909 del self._attr()[index]
912 log.debug(u'ContainerSubHandler.delete: item not found')
913 raise ItemNotFoundError(index)
915 @handler(u'Remove all items (use with care).')
917 r"clear() -> None :: Delete all items of the container."
918 log.debug(u'ContainerSubHandler.clear()')
919 # FIXME broken really, no _delete attribute is setted :S
920 if isinstance(self._attr(), dict):
925 @handler(u'Get information about an item')
926 def get(self, index):
927 r"get(index) -> item :: List all the information of an item."
928 log.debug(u'ContainerSubHandler.get(%r)', index)
929 if not isinstance(self._attr(), dict):
930 index = int(index) # TODO validation
932 return self._vattr()[index]
934 log.debug(u'ContainerSubHandler.get: item not found')
935 raise ItemNotFoundError(index)
937 @handler(u'Get information about all items')
939 r"show() -> list of items :: List all the complete items information."
940 log.debug(u'ContainerSubHandler.show()')
941 if isinstance(self._attr(), dict):
942 return self._attr().values()
945 class ListSubHandler(ContainerSubHandler):
946 r"""ListSubHandler(parent) -> ListSubHandler instance.
948 ContainerSubHandler holding lists. See ComposedSubHandler documentation
952 @handler(u'Get how many items are in the list')
954 r"len() -> int :: Get how many items are in the list."
955 log.debug(u'ListContainerSubHandler.len()')
956 return len(self._vattr())
958 class DictSubHandler(ContainerSubHandler):
959 r"""DictSubHandler(parent) -> DictSubHandler instance.
961 ContainerSubHandler holding dicts. See ComposedSubHandler documentation
965 @handler(u'List all the items by key')
967 r"list() -> tuple :: List all the item keys."
968 log.debug(u'DictContainerSubHandler.list()')
969 return self._attr().keys()
971 class ComposedSubHandler(SubHandler):
972 r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
974 This is a helper class to implement ListComposedSubHandler and
975 DictComposedSubHandler. You should not use it directly.
977 This class is usefull when you have a parent that has a dict (cont)
978 that stores some object that has an attribute (attr) with a list or
979 a dict of objects of some class. In that case, this class provides
980 automated commands to add, update, delete, get and show that objects.
981 This commands takes the cont (key of the dict for the object holding
982 the attr), and an index for access the object itself (in the attr
985 The container object (cont) that holds a containers, the attribute of
986 that object that is the container itself, and the class of the objects
987 that it contains can be defined by calling the constructor or in a
988 more declarative way as class attributes, like:
990 class TestHandler(ComposedSubHandler):
991 _comp_subhandler_cont = 'some_cont'
992 _comp_subhandler_attr = 'some_attr'
993 _comp_subhandler_class = SomeClass
995 This way, the parent's some_cont attribute (self.parent.some_cont)
996 will be managed automatically, providing the commands: add, update,
997 delete, get and show for manipulating a particular instance that holds
998 of SomeClass. For example, updating an item at the index 5 is the same
999 (simplified) as doing parent.some_cont[cont][5].update().
1000 SomeClass should provide a cmp operator to see if the item is on the
1001 container and an update() method, if it should be possible to modify
1002 it. If SomeClass has an _add, _update or _delete attribute, it set
1003 them to true when the item is added, updated or deleted respectively
1004 (in case that it's deleted, it's not removed from the container,
1005 but it's not listed either). If the container objects
1006 (parent.some_cont[cont]) has an _update attribute, it's set to True
1007 when any add, update or delete command is executed.
1010 def __init__(self, parent, cont=None, attr=None, cls=None):
1011 r"Initialize the object, see the class documentation for details."
1012 log.debug(u'ComposedSubHandler(%r, %r, %r, %r)',
1013 parent, cont, attr, cls)
1014 self.parent = parent
1015 if cont is not None:
1016 self._comp_subhandler_cont = cont
1017 if attr is not None:
1018 self._comp_subhandler_attr = attr
1020 self._comp_subhandler_class = cls
1023 return getattr(self.parent, self._comp_subhandler_cont)
1025 def _attr(self, cont, attr=None):
1027 return getattr(self._cont()[cont], self._comp_subhandler_attr)
1028 setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
1030 def _vattr(self, cont):
1031 if isinstance(self._attr(cont), dict):
1032 return dict([(k, i) for (k, i) in self._attr(cont).items()
1033 if not hasattr(i, '_delete') or not i._delete])
1034 return [i for i in self._attr(cont)
1035 if not hasattr(i, '_delete') or not i._delete]
1037 @handler(u'Add a new item')
1038 def add(self, cont, *args, **kwargs):
1039 r"add(cont, ...) -> None :: Add an item to the list."
1040 log.debug(u'ComposedSubHandler.add(%r, %r, %r)', cont, args, kwargs)
1041 if not cont in self._cont():
1042 log.debug(u'ComposedSubHandler.add: container not found')
1043 raise ContainerNotFoundError(cont)
1044 item = self._comp_subhandler_class(*args, **kwargs)
1045 if hasattr(item, '_add'):
1046 log.debug(u'ComposedSubHandler.add: _add found, setting to True')
1049 if isinstance(self._attr(cont), dict):
1050 key = item.as_tuple()[0]
1051 # do we have the same item? then raise an error
1052 if key in self._vattr(cont):
1053 log.debug(u'ComposedSubHandler.add: allready exists')
1054 if not isinstance(self._attr(), dict):
1055 key = self._attr().index(item)
1056 raise ItemAlreadyExistsError(key)
1057 # do we have the same item, but logically deleted? then update flags
1058 if key in self._attr(cont):
1059 log.debug(u'ComposedSubHandler.add: was deleted, undeleting it')
1061 if not isinstance(self._attr(cont), dict):
1062 index = self._attr(cont).index(item)
1063 if hasattr(item, '_add'):
1064 self._attr(cont)[index]._add = False
1065 if hasattr(item, '_delete'):
1066 self._attr(cont)[index]._delete = False
1067 else: # it's *really* new
1068 if isinstance(self._attr(cont), dict):
1069 self._attr(cont)[key] = item
1071 self._attr(cont).append(item)
1072 if hasattr(self._cont()[cont], '_update'):
1073 log.debug(u"ComposedSubHandler.add: container's _update found, "
1075 self._cont()[cont]._update = True
1077 @handler(u'Update an item')
1078 def update(self, cont, index, *args, **kwargs):
1079 r"update(cont, index, ...) -> None :: Update an item of the container."
1080 # TODO make it right with metaclasses, so the method is not created
1081 # unless the update() method really exists.
1082 log.debug(u'ComposedSubHandler.update(%r, %r, %r, %r)',
1083 cont, index, args, kwargs)
1084 if not cont in self._cont():
1085 log.debug(u'ComposedSubHandler.add: container not found')
1086 raise ContainerNotFoundError(cont)
1087 if not isinstance(self._attr(cont), dict):
1088 index = int(index) # TODO validation
1089 if not hasattr(self._comp_subhandler_class, 'update'):
1090 log.debug(u'ComposedSubHandler.update: update() not found, '
1091 u"can't really update, raising command not found")
1092 raise CommandNotFoundError(('update',))
1094 item = self._vattr(cont)[index]
1095 item.update(*args, **kwargs)
1096 if hasattr(item, '_update'):
1097 log.debug(u'ComposedSubHandler.update: _update found, '
1100 if hasattr(self._cont()[cont], '_update'):
1101 log.debug(u"ComposedSubHandler.add: container's _update found, "
1103 self._cont()[cont]._update = True
1105 log.debug(u'ComposedSubHandler.update: item not found')
1106 raise ItemNotFoundError(index)
1108 @handler(u'Delete an item')
1109 def delete(self, cont, index):
1110 r"delete(cont, index) -> None :: Delete an item of the container."
1111 log.debug(u'ComposedSubHandler.delete(%r, %r)', cont, index)
1112 if not cont in self._cont():
1113 log.debug(u'ComposedSubHandler.add: container not found')
1114 raise ContainerNotFoundError(cont)
1115 if not isinstance(self._attr(cont), dict):
1116 index = int(index) # TODO validation
1118 item = self._vattr(cont)[index]
1119 if hasattr(item, '_delete'):
1120 log.debug(u'ComposedSubHandler.delete: _delete found, '
1124 del self._attr(cont)[index]
1125 if hasattr(self._cont()[cont], '_update'):
1126 log.debug(u"ComposedSubHandler.add: container's _update found, "
1128 self._cont()[cont]._update = True
1131 log.debug(u'ComposedSubHandler.delete: item not found')
1132 raise ItemNotFoundError(index)
1134 @handler(u'Remove all items (use with care).')
1135 def clear(self, cont):
1136 r"clear(cont) -> None :: Delete all items of the container."
1137 # FIXME broken really, no item or container _delete attribute is
1139 log.debug(u'ComposedSubHandler.clear(%r)', cont)
1140 if not cont in self._cont():
1141 log.debug(u'ComposedSubHandler.add: container not found')
1142 raise ContainerNotFoundError(cont)
1143 if isinstance(self._attr(cont), dict):
1144 self._attr(cont).clear()
1146 self._attr(cont, list())
1148 @handler(u'Get information about an item')
1149 def get(self, cont, index):
1150 r"get(cont, index) -> item :: List all the information of an item."
1151 log.debug(u'ComposedSubHandler.get(%r, %r)', cont, index)
1152 if not cont in self._cont():
1153 log.debug(u'ComposedSubHandler.add: container not found')
1154 raise ContainerNotFoundError(cont)
1155 if not isinstance(self._attr(cont), dict):
1156 index = int(index) # TODO validation
1158 return self._vattr(cont)[index]
1160 log.debug(u'ComposedSubHandler.get: item not found')
1161 raise ItemNotFoundError(index)
1163 @handler(u'Get information about all items')
1164 def show(self, cont):
1165 r"show(cont) -> list of items :: List all the complete items information."
1166 log.debug(u'ComposedSubHandler.show(%r)', cont)
1167 if not cont in self._cont():
1168 log.debug(u'ComposedSubHandler.add: container not found')
1169 raise ContainerNotFoundError(cont)
1170 if isinstance(self._attr(cont), dict):
1171 return self._attr(cont).values()
1172 return self._vattr(cont)
1174 class ListComposedSubHandler(ComposedSubHandler):
1175 r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1177 ComposedSubHandler holding lists. See ComposedSubHandler documentation
1181 @handler(u'Get how many items are in the list')
1182 def len(self, cont):
1183 r"len(cont) -> int :: Get how many items are in the list."
1184 log.debug(u'ListComposedSubHandler.len(%r)', cont)
1185 if not cont in self._cont():
1186 raise ContainerNotFoundError(cont)
1187 return len(self._vattr(cont))
1189 class DictComposedSubHandler(ComposedSubHandler):
1190 r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1192 ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1196 @handler(u'List all the items by key')
1197 def list(self, cont):
1198 r"list(cont) -> tuple :: List all the item keys."
1199 log.debug(u'DictComposedSubHandler.list(%r)', cont)
1200 if not cont in self._cont():
1201 raise ContainerNotFoundError(cont)
1202 return self._attr(cont).keys()
1205 if __name__ == '__main__':
1207 logging.basicConfig(
1208 level = logging.DEBUG,
1209 format = '%(asctime)s %(levelname)-8s %(message)s',
1210 datefmt = '%H:%M:%S',
1216 class STestHandler1(ServiceHandler):
1217 _service_start = ('service', 'start')
1218 _service_stop = ('service', 'stop')
1219 _service_restart = ('ls', '/')
1220 _service_reload = ('cp', '/la')
1221 class STestHandler2(ServiceHandler):
1223 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1224 'cmd-restart', 'cmd-reload')
1225 class ITestHandler1(InitdHandler):
1226 _initd_name = 'test1'
1227 class ITestHandler2(InitdHandler):
1229 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1237 print h.__class__.__name__
1240 except ExecutionError, e:
1244 except ExecutionError, e:
1248 except ExecutionError, e:
1252 except ExecutionError, e:
1257 print 'PTestHandler'
1258 class PTestHandler(Persistent):
1259 _persistent_attrs = 'vars'
1261 self.vars = dict(a=1, b=2)
1279 print 'RTestHandler'
1280 class RTestHandler(Restorable):
1281 _persistent_attrs = 'vars'
1282 _restorable_defaults = dict(vars=dict(a=1, b=2))
1294 print 'CTestHandler'
1296 os.mkdir('templates')
1297 f = file('templates/config', 'w')
1298 f.write('Hello, ${name}! You are ${what}.')
1301 print file('templates/config').read()
1302 class CTestHandler(ConfigWriter):
1303 _config_writer_files = 'config'
1305 self._config_build_templates()
1306 def _get_config_vars(self, config_file):
1307 return dict(name='you', what='a parrot')
1311 print file('config').read()
1313 os.unlink('templates/config')
1314 os.rmdir('templates')
1317 print get_network_devices()