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
17 __ALL__ = ('ServiceHandler', 'InitdHandler', 'SubHandler', 'DictSubHandler',
18 'ListSubHandler', 'Persistent', 'ConfigWriter', 'Error',
19 'ReturnNot0Error', 'ExecutionError', 'ItemError',
20 'ItemAlreadyExistsError', 'ItemNotFoundError', 'call')
22 class Error(HandlerError):
24 Error(message) -> Error instance :: Base ServiceHandler exception class.
26 All exceptions raised by the ServiceHandler inherits from this one, so
27 you can easily catch any ServiceHandler exception.
29 message - A descriptive error message.
33 class ReturnNot0Error(Error):
35 ReturnNot0Error(return_value) -> ReturnNot0Error instance.
37 A command didn't returned the expected 0 return value.
39 return_value - Return value returned by the command.
42 def __init__(self, return_value):
43 r"Initialize the object. See class documentation for more info."
44 self.return_value = return_value
46 def __unicode__(self):
47 return 'The command returned %d' % self.return_value
49 class ExecutionError(Error):
51 ExecutionError(command, error) -> ExecutionError instance.
53 Error executing a command.
55 command - Command that was tried to execute.
57 error - Error received when trying to execute the command.
60 def __init__(self, command, error):
61 r"Initialize the object. See class documentation for more info."
62 self.command = command
65 def __unicode__(self):
66 command = self.command
67 if not isinstance(self.command, basestring):
68 command = ' '.join(command)
69 return "Can't execute command %s: %s" % (command, self.error)
71 class ParameterError(Error, KeyError):
73 ParameterError(paramname) -> ParameterError instance
75 This is the base exception for all DhcpHandler parameters related errors.
78 def __init__(self, paramname):
79 r"Initialize the object. See class documentation for more info."
80 self.message = 'Parameter error: "%s"' % paramname
82 class ParameterNotFoundError(ParameterError):
84 ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
86 This exception is raised when trying to operate on a parameter that doesn't
90 def __init__(self, paramname):
91 r"Initialize the object. See class documentation for more info."
92 self.message = 'Parameter not found: "%s"' % paramname
94 class ItemError(Error, KeyError):
96 ItemError(key) -> ItemError instance.
98 This is the base exception for all item related errors.
101 def __init__(self, key):
102 r"Initialize the object. See class documentation for more info."
103 self.message = u'Item error: "%s"' % key
105 class ItemAlreadyExistsError(ItemError):
107 ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
109 This exception is raised when trying to add an item that already exists.
112 def __init__(self, key):
113 r"Initialize the object. See class documentation for more info."
114 self.message = u'Item already exists: "%s"' % key
116 class ItemNotFoundError(ItemError):
118 ItemNotFoundError(key) -> ItemNotFoundError instance
120 This exception is raised when trying to operate on an item that doesn't
124 def __init__(self, key):
125 r"Initialize the object. See class documentation for more info."
126 self.message = u'Item not found: "%s"' % key
129 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
130 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
133 if not isinstance(command, basestring):
134 command = ' '.join(command)
135 print 'Executing command:', command
138 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
139 universal_newlines=universal_newlines,
140 close_fds=close_fds, **kw)
142 raise ExecutionError(command, e)
144 raise ExecutionError(command, ReturnNot0Error(r))
147 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
149 This is a helper class to inherit from to automatically handle data
150 persistence using pickle.
152 The variables attributes to persist (attrs), and the pickle directory (dir)
153 and file extension (ext) can be defined by calling the constructor or in a
154 more declarative way as class attributes, like:
156 class TestHandler(Persistent):
157 _persistent_attrs = ('some_attr', 'other_attr')
158 _persistent_dir = 'persistent-data'
159 _persistent_ext = '.pickle'
161 The default dir is '.' and the default extension is '.pkl'. There are no
162 default variables, and they should be specified as string if a single
163 attribute should be persistent or as a tuple of strings if they are more.
164 The strings should be the attribute names to be persisted. For each
165 attribute a separated pickle file is generated in the pickle directory.
167 You can call _dump() and _load() to write and read the data respectively.
169 # TODO implement it using metaclasses to add the handlers method by demand
170 # (only for specifieds commands).
172 _persistent_attrs = ()
173 _persistent_dir = '.'
174 _persistent_ext = '.pkl'
176 def __init__(self, attrs=None, dir=None, ext=None):
177 r"Initialize the object, see the class documentation for details."
178 if attrs is not None:
179 self._persistent_attrs = attrs
181 self._persistent_dir = dir
183 self._persistent_ext = ext
186 r"_dump() -> None :: Dump all persistent data to pickle files."
187 if isinstance(self._persistent_attrs, basestring):
188 self._persistent_attrs = (self._persistent_attrs,)
189 for attrname in self._persistent_attrs:
190 self._dump_attr(attrname)
193 r"_load() -> None :: Load all persistent data from pickle files."
194 if isinstance(self._persistent_attrs, basestring):
195 self._persistent_attrs = (self._persistent_attrs,)
196 for attrname in self._persistent_attrs:
197 self._load_attr(attrname)
199 def _dump_attr(self, attrname):
200 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
201 f = file(self._pickle_filename(attrname), 'wb')
202 pickle.dump(getattr(self, attrname), f, 2)
205 def _load_attr(self, attrname):
206 r"_load_attr() -> object :: Load a specific pickle file."
207 f = file(self._pickle_filename(attrname))
208 setattr(self, attrname, pickle.load(f))
211 def _pickle_filename(self, name):
212 r"_pickle_filename() -> string :: Construct a pickle filename."
213 return path.join(self._persistent_dir, name) + self._persistent_ext
215 class Restorable(Persistent):
216 r"""Restorable([defaults]) -> Restorable.
218 This is a helper class to inherit from that provides a nice _restore()
219 method to restore the persistent data if any, or load some nice defaults
222 The defaults can be defined by calling the constructor or in a more
223 declarative way as class attributes, like:
225 class TestHandler(Restorable):
226 _persistent_attrs = ('some_attr', 'other_attr')
227 _restorable_defaults = dict(
228 some_attr = 'some_default',
229 other_attr = 'other_default')
231 The defaults is a dictionary, very coupled with the _persistent_attrs
232 attribute inherited from Persistent. The defaults keys should be the
233 values from _persistent_attrs, and the values the default values.
235 The _restore() method returns True if the data was restored successfully
236 or False if the defaults were loaded (in case you want to take further
237 actions). If a _write_config method if found, it's executed when a restore
240 # TODO implement it using metaclasses to add the handlers method by demand
241 # (only for specifieds commands).
243 _restorable_defaults = dict()
245 def __init__(self, defaults=None):
246 r"Initialize the object, see the class documentation for details."
247 if defaults is not None:
248 self._restorable_defaults = defaults
251 r"_restore() -> bool :: Restore persistent data or create a default."
254 # TODO tener en cuenta servicios que hay que levantar y los que no
255 if hasattr(self, 'commit'): # TODO deberia ser reload y/o algo para comandos
259 for (k, v) in self._restorable_defaults.items():
261 # TODO tener en cuenta servicios que hay que levantar y los que no
262 if hasattr(self, 'commit'):
266 if hasattr(self, '_write_config'):
268 if hasattr(self, 'reload'):
273 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
275 This is a helper class to inherit from to automatically handle
276 configuration generation. Mako template system is used for configuration
279 The configuration filenames, the generated configuration files directory
280 and the templates directory can be defined by calling the constructor or
281 in a more declarative way as class attributes, like:
283 class TestHandler(ConfigWriter):
284 _config_writer_files = ('base.conf', 'custom.conf')
285 _config_writer_cfg_dir = '/etc/service'
286 _config_writer_tpl_dir = 'templates'
288 The generated configuration files directory defaults to '.' and the
289 templates directory to 'templates'. _config_writer_files has no default and
290 must be specified in either way. It can be string or a tuple if more than
291 one configuration file must be generated.
293 The template filename and the generated configuration filename are both the
294 same (so if you want to generate some /etc/config, you should have some
295 templates/config template). That's why _config_writer_cfg_dir and
296 _config_writer_tpl_dir can't be the same.
298 When you write your Handler, you should call _config_build_templates() in
299 you Handler constructor to build the templates.
301 To write the configuration files, you must use the _write_config() method.
302 To know what variables to replace in the template, you have to provide a
303 method called _get_config_vars(tamplate_name), which should return a
304 dictionary of variables to pass to the template system to be replaced in
305 the template for the configuration file 'config_file'.
307 # TODO implement it using metaclasses to add the handlers method by demand
308 # (only for specifieds commands).
310 _config_writer_files = ()
311 _config_writer_cfg_dir = '.'
312 _config_writer_tpl_dir = 'templates'
314 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
315 r"Initialize the object, see the class documentation for details."
316 if files is not None:
317 self._config_writer_files = files
318 if cfg_dir is not None:
319 self._config_writer_cfg_dir = cfg_dir
320 if tpl_dir is not None:
321 self._config_writer_tpl_dir = tpl_dir
322 self._config_build_templates()
324 def _config_build_templates(self):
325 r"_config_writer_templates() -> None :: Build the template objects."
326 if isinstance(self._config_writer_files, basestring):
327 self._config_writer_files = (self._config_writer_files,)
328 if not hasattr(self, '_config_writer_templates') \
329 or not self._config_writer_templates:
330 self._config_writer_templates = dict()
331 for t in self._config_writer_files:
332 f = path.join(self._config_writer_tpl_dir, t)
333 self._config_writer_templates[t] = Template(filename=f)
335 def _render_config(self, template_name, vars=None):
336 r"""_render_config(template_name[, config_filename[, vars]]).
338 Render a single config file using the template 'template_name'. If
339 vars is specified, it's used as the dictionary with the variables
340 to replace in the templates, if not, it looks for a
341 _get_config_vars() method to get it.
344 if hasattr(self, '_get_config_vars'):
345 vars = self._get_config_vars(template_name)
349 vars = vars(template_name)
350 return self._config_writer_templates[template_name].render(**vars)
352 def _write_single_config(self, template_name, config_filename=None, vars=None):
353 r"""_write_single_config(template_name[, config_filename[, vars]]).
355 Write a single config file using the template 'template_name'. If no
356 config_filename is specified, the config filename will be the same as
357 the 'template_name' (but stored in the generated config files
358 directory). If it's specified, the generated config file is stored in
359 the file called 'config_filename' (also in the generated files
360 directory). If vars is specified, it's used as the dictionary with the
361 variables to replace in the templates, if not, it looks for a
362 _get_config_vars() method to get it.
364 if not config_filename:
365 config_filename = template_name
367 if hasattr(self, '_get_config_vars'):
368 vars = self._get_config_vars(template_name)
372 vars = vars(template_name)
373 f = file(path.join(self._config_writer_cfg_dir, config_filename), 'w')
374 ctx = Context(f, **vars)
375 self._config_writer_templates[template_name].render_context(ctx)
378 def _write_config(self):
379 r"_write_config() -> None :: Generate all the configuration files."
380 for t in self._config_writer_files:
381 self._write_single_config(t)
384 class ServiceHandler(Handler):
385 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
387 This is a helper class to inherit from to automatically handle services
388 with start, stop, restart, reload actions.
390 The actions can be defined by calling the constructor with all the
391 parameters or in a more declarative way as class attributes, like:
393 class TestHandler(ServiceHandler):
394 _service_start = ('command', 'start')
395 _service_stop = ('command', 'stop')
396 _service_restart = ('command', 'restart')
397 _service_reload = 'reload-command'
399 Commands are executed without using the shell, that's why they are specified
400 as tuples (where the first element is the command and the others are the
401 command arguments). If only a command is needed (without arguments) a single
402 string can be specified.
404 All commands must be specified.
406 # TODO implement it using metaclasses to add the handlers method by demand
407 # (only for specifieds commands).
409 def __init__(self, start=None, stop=None, restart=None, reload=None):
410 r"Initialize the object, see the class documentation for details."
411 for (name, action) in dict(start=start, stop=stop, restart=restart,
412 reload=reload).items():
413 if action is not None:
414 setattr(self, '_service_%s' % name, action)
416 @handler(u'Start the service.')
418 r"start() -> None :: Start the service."
419 call(self._service_start)
421 @handler(u'Stop the service.')
423 r"stop() -> None :: Stop the service."
424 call(self._service_stop)
426 @handler(u'Restart the service.')
428 r"restart() -> None :: Restart the service."
429 call(self._service_restart)
431 @handler(u'Reload the service config (without restarting, if possible).')
433 r"reload() -> None :: Reload the configuration of the service."
434 call(self._service_reload)
436 class InitdHandler(Handler):
437 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
439 This is a helper class to inherit from to automatically handle services
440 with start, stop, restart, reload actions using a /etc/init.d like script.
442 The name and directory of the script can be defined by calling the
443 constructor or in a more declarative way as class attributes, like:
445 class TestHandler(ServiceHandler):
446 _initd_name = 'some-service'
447 _initd_dir = '/usr/local/etc/init.d'
449 The default _initd_dir is '/etc/init.d', _initd_name has no default and
450 must be specified in either way.
452 Commands are executed without using the shell.
454 # TODO implement it using metaclasses to add the handlers method by demand
455 # (only for specifieds commands).
457 _initd_dir = '/etc/init.d'
459 def __init__(self, initd_name=None, initd_dir=None):
460 r"Initialize the object, see the class documentation for details."
461 if initd_name is not None:
462 self._initd_name = initd_name
463 if initd_dir is not None:
464 self._initd_dir = initd_dir
466 @handler(u'Start the service.')
468 r"start() -> None :: Start the service."
469 call((path.join(self._initd_dir, self._initd_name), 'start'))
471 @handler(u'Stop the service.')
473 r"stop() -> None :: Stop the service."
474 call((path.join(self._initd_dir, self._initd_name), 'stop'))
476 @handler(u'Restart the service.')
478 r"restart() -> None :: Restart the service."
479 call((path.join(self._initd_dir, self._initd_name), 'restart'))
481 @handler(u'Reload the service config (without restarting, if possible).')
483 r"reload() -> None :: Reload the configuration of the service."
484 call((path.join(self._initd_dir, self._initd_name), 'reload'))
486 class TransactionalHandler(Handler):
487 r"""Handle command transactions providing a commit and rollback commands.
489 This is a helper class to inherit from to automatically handle
490 transactional handlers, which have commit and rollback commands.
492 The handler should provide a reload() method (see ServiceHandler and
493 InitdHandler for helper classes to provide this) which will be called
494 when a commit command is issued (if a reload() command is present).
495 The persistent data will be written too (if a _dump() method is provided,
496 see Persistent and Restorable for that), and the configuration files
497 will be generated (if a _write_config method is present, see ConfigWriter).
499 # TODO implement it using metaclasses to add the handlers method by demand
500 # (only for specifieds commands).
502 @handler(u'Commit the changes (reloading the service, if necessary).')
504 r"commit() -> None :: Commit the changes and reload the service."
505 if hasattr(self, '_dump'):
507 if hasattr(self, '_write_config'):
509 if hasattr(self, 'reload'):
512 @handler(u'Discard all the uncommited changes.')
514 r"rollback() -> None :: Discard the changes not yet commited."
515 if hasattr(self, '_load'):
518 class ParametersHandler(Handler):
519 r"""ParametersHandler([attr]) -> ParametersHandler.
521 This is a helper class to inherit from to automatically handle
522 service parameters, providing set, get, list and show commands.
524 The attribute that holds the parameters can be defined by calling the
525 constructor or in a more declarative way as class attributes, like:
527 class TestHandler(ServiceHandler):
528 _parameters_attr = 'some_attr'
530 The default is 'params' and it should be a dictionary.
532 # TODO implement it using metaclasses to add the handlers method by demand
533 # (only for specifieds commands).
535 _parameters_attr = 'params'
537 def __init__(self, attr=None):
538 r"Initialize the object, see the class documentation for details."
540 self._parameters_attr = attr
542 @handler(u'Set a service parameter.')
543 def set(self, param, value):
544 r"set(param, value) -> None :: Set a service parameter."
545 if not param in self.params:
546 raise ParameterNotFoundError(param)
547 self.params[param] = value
549 @handler(u'Get a service parameter.')
550 def get(self, param):
551 r"get(param) -> None :: Get a service parameter."
552 if not param in self.params:
553 raise ParameterNotFoundError(param)
554 return self.params[param]
556 @handler(u'List all available service parameters.')
558 r"list() -> tuple :: List all the parameter names."
559 return self.params.keys()
561 @handler(u'Get all service parameters, with their values.')
563 r"show() -> (key, value) tuples :: List all the parameters."
564 return self.params.items()
566 class SubHandler(Handler):
567 r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
569 This is a helper class to build sub handlers that needs to reference the
572 parent - Parent Handler object.
575 def __init__(self, parent):
576 r"Initialize the object, see the class documentation for details."
579 class DictSubHandler(SubHandler):
580 r"""DictSubHandler(parent) -> DictSubHandler instance.
582 This is a helper class to inherit from to automatically handle subcommands
583 that operates over a dict parent attribute.
585 The dict attribute to handle and the class of objects that it contains can
586 be defined by calling the constructor or in a more declarative way as
587 class attributes, like:
589 class TestHandler(DictSubHandler):
590 _dict_subhandler_attr = 'some_dict'
591 _dict_subhandler_class = SomeClass
593 This way, the parent's some_dict attribute (self.parent.some_dict) will be
594 managed automatically, providing the commands: add, update, delete, get,
598 def __init__(self, parent, attr=None, key=None, cls=None):
599 r"Initialize the object, see the class documentation for details."
602 self._dict_subhandler_attr = attr
604 self._dict_subhandler_key = key
606 self._dict_subhandler_class = cls
609 return getattr(self.parent, self._dict_subhandler_attr)
611 @handler(u'Add a new item')
612 def add(self, key, *args, **kwargs):
613 r"add(key, ...) -> None :: Add an item to the dict."
614 item = self._dict_subhandler_class(key, *args, **kwargs)
615 if key in self._dict():
616 raise ItemAlreadyExistsError(key)
617 self._dict()[key] = item
619 @handler(u'Update an item')
620 def update(self, key, *args, **kwargs):
621 r"update(key, ...) -> None :: Update an item of the dict."
622 if not key in self._dict():
623 raise ItemNotFoundError(key)
624 self._dict()[key].update(*args, **kwargs)
626 @handler(u'Delete an item')
627 def delete(self, key):
628 r"delete(key) -> None :: Delete an item of the dict."
629 if not key in self._dict():
630 raise ItemNotFoundError(key)
631 del self._dict()[key]
633 @handler(u'Get information about an item')
635 r"get(key) -> Host :: List all the information of an item."
636 if not key in self._dict():
637 raise ItemNotFoundError(key)
638 return self._dict()[key]
640 @handler(u'List all the items by key')
642 r"list() -> tuple :: List all the item keys."
643 return self._dict().keys()
645 @handler(u'Get information about all items')
647 r"show() -> list of Hosts :: List all the complete items information."
648 return self._dict().values()
652 if __name__ == '__main__':
655 class STestHandler1(ServiceHandler):
656 _service_start = ('service', 'start')
657 _service_stop = ('service', 'stop')
658 _service_restart = ('ls', '/')
659 _service_reload = ('cp', '/la')
660 class STestHandler2(ServiceHandler):
662 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
663 'cmd-restart', 'cmd-reload')
664 class ITestHandler1(InitdHandler):
665 _initd_name = 'test1'
666 class ITestHandler2(InitdHandler):
668 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
676 print h.__class__.__name__
679 except ExecutionError, e:
683 except ExecutionError, e:
687 except ExecutionError, e:
691 except ExecutionError, e:
697 class PTestHandler(Persistent):
698 _persistent_attrs = 'vars'
700 self.vars = dict(a=1, b=2)
719 class RTestHandler(Restorable):
720 _persistent_attrs = 'vars'
721 _restorable_defaults = dict(vars=dict(a=1, b=2))
735 os.mkdir('templates')
736 f = file('templates/config', 'w')
737 f.write('Hello, ${name}! You are ${what}.')
740 print file('templates/config').read()
741 class CTestHandler(ConfigWriter):
742 _config_writer_files = 'config'
744 self._config_build_templates()
745 def _get_config_vars(self, config_file):
746 return dict(name='you', what='a parrot')
750 print file('config').read()
752 os.unlink('templates/config')
753 os.rmdir('templates')