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', 'Persistent', 'ConfigWriter',
18 'Error', 'ReturnNot0Error', 'ExecutionError', 'call')
20 class Error(HandlerError):
22 Error(message) -> Error instance :: Base ServiceHandler exception class.
24 All exceptions raised by the ServiceHandler inherits from this one, so
25 you can easily catch any ServiceHandler exception.
27 message - A descriptive error message.
31 class ReturnNot0Error(Error):
33 ReturnNot0Error(return_value) -> ReturnNot0Error instance.
35 A command didn't returned the expected 0 return value.
37 return_value - Return value returned by the command.
40 def __init__(self, return_value):
41 r"Initialize the object. See class documentation for more info."
42 self.return_value = return_value
44 def __unicode__(self):
45 return 'The command returned %d' % self.return_value
47 class ExecutionError(Error):
49 ExecutionError(command, error) -> ExecutionError instance.
51 Error executing a command.
53 command - Command that was tried to execute.
55 error - Error received when trying to execute the command.
58 def __init__(self, command, error):
59 r"Initialize the object. See class documentation for more info."
60 self.command = command
63 def __unicode__(self):
64 command = self.command
65 if not isinstance(self.command, basestring):
66 command = ' '.join(command)
67 return "Can't execute command %s: %s" % (command, self.error)
69 class ParameterError(Error, KeyError):
71 ParameterError(paramname) -> ParameterError instance
73 This is the base exception for all DhcpHandler parameters related errors.
76 def __init__(self, paramname):
77 r"Initialize the object. See class documentation for more info."
78 self.message = 'Parameter error: "%s"' % paramname
80 class ParameterNotFoundError(ParameterError):
82 ParameterNotFoundError(hostname) -> ParameterNotFoundError instance
84 This exception is raised when trying to operate on a parameter that doesn't
88 def __init__(self, paramname):
89 r"Initialize the object. See class documentation for more info."
90 self.message = 'Parameter not found: "%s"' % paramname
93 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
94 stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
97 if not isinstance(command, basestring):
98 command = ' '.join(command)
99 print 'Executing command:', command
102 r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
103 universal_newlines=universal_newlines,
104 close_fds=close_fds, **kw)
106 raise ExecutionError(command, e)
108 raise ExecutionError(command, ReturnNot0Error(r))
111 r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
113 This is a helper class to inherit from to automatically handle data
114 persistence using pickle.
116 The variables attributes to persist (attrs), and the pickle directory (dir)
117 and file extension (ext) can be defined by calling the constructor or in a
118 more declarative way as class attributes, like:
120 class TestHandler(Persistent):
121 _persistent_attrs = ('some_attr', 'other_attr')
122 _persistent_dir = 'persistent-data'
123 _persistent_ext = '.pickle'
125 The default dir is '.' and the default extension is '.pkl'. There are no
126 default variables, and they should be specified as string if a single
127 attribute should be persistent or as a tuple of strings if they are more.
128 The strings should be the attribute names to be persisted. For each
129 attribute a separated pickle file is generated in the pickle directory.
131 You can call _dump() and _load() to write and read the data respectively.
133 # TODO implement it using metaclasses to add the handlers method by demand
134 # (only for specifieds commands).
136 _persistent_attrs = ()
137 _persistent_dir = '.'
138 _persistent_ext = '.pkl'
140 def __init__(self, attrs=None, dir=None, ext=None):
141 r"Initialize the object, see the class documentation for details."
142 if attrs is not None:
143 self._persistent_attrs = attrs
145 self._persistent_dir = dir
147 self._persistent_ext = ext
150 r"_dump() -> None :: Dump all persistent data to pickle files."
151 if isinstance(self._persistent_attrs, basestring):
152 self._persistent_attrs = (self._persistent_attrs,)
153 for attrname in self._persistent_attrs:
154 self._dump_attr(attrname)
157 r"_load() -> None :: Load all persistent data from pickle files."
158 if isinstance(self._persistent_attrs, basestring):
159 self._persistent_attrs = (self._persistent_attrs,)
160 for attrname in self._persistent_attrs:
161 self._load_attr(attrname)
163 def _dump_attr(self, attrname):
164 r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
165 f = file(self._pickle_filename(attrname), 'wb')
166 pickle.dump(getattr(self, attrname), f, 2)
169 def _load_attr(self, attrname):
170 r"_load_attr() -> object :: Load a specific pickle file."
171 f = file(self._pickle_filename(attrname))
172 setattr(self, attrname, pickle.load(f))
175 def _pickle_filename(self, name):
176 r"_pickle_filename() -> string :: Construct a pickle filename."
177 return path.join(self._persistent_dir, name) + self._persistent_ext
179 class Restorable(Persistent):
180 r"""Restorable([defaults]) -> Restorable.
182 This is a helper class to inherit from that provides a nice _restore()
183 method to restore the persistent data if any, or load some nice defaults
186 The defaults can be defined by calling the constructor or in a more
187 declarative way as class attributes, like:
189 class TestHandler(Restorable):
190 _persistent_attrs = ('some_attr', 'other_attr')
191 _restorable_defaults = dict(
192 some_attr = 'some_default',
193 other_attr = 'other_default')
195 The defaults is a dictionary, very coupled with the _persistent_attrs
196 attribute inherited from Persistent. The defaults keys should be the
197 values from _persistent_attrs, and the values the default values.
199 The _restore() method returns True if the data was restored successfully
200 or False if the defaults were loaded (in case you want to take further
201 actions). If a _write_config method if found, it's executed when a restore
204 # TODO implement it using metaclasses to add the handlers method by demand
205 # (only for specifieds commands).
207 _restorable_defaults = dict()
209 def __init__(self, defaults=None):
210 r"Initialize the object, see the class documentation for details."
211 if defaults is not None:
212 self._restorable_defaults = defaults
215 r"_restore() -> bool :: Restore persistent data or create a default."
218 # TODO tener en cuenta servicios que hay que levantar y los que no
219 if hasattr(self, 'commit'): # TODO deberia ser reload y/o algo para comandos
223 for (k, v) in self._restorable_defaults.items():
225 # TODO tener en cuenta servicios que hay que levantar y los que no
226 if hasattr(self, 'commit'):
230 if hasattr(self, '_write_config'):
232 if hasattr(self, 'reload'):
237 r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
239 This is a helper class to inherit from to automatically handle
240 configuration generation. Mako template system is used for configuration
243 The configuration filenames, the generated configuration files directory
244 and the templates directory can be defined by calling the constructor or
245 in a more declarative way as class attributes, like:
247 class TestHandler(ConfigWriter):
248 _config_writer_files = ('base.conf', 'custom.conf')
249 _config_writer_cfg_dir = '/etc/service'
250 _config_writer_tpl_dir = 'templates'
252 The generated configuration files directory defaults to '.' and the
253 templates directory to 'templates'. _config_writer_files has no default and
254 must be specified in either way. It can be string or a tuple if more than
255 one configuration file must be generated.
257 The template filename and the generated configuration filename are both the
258 same (so if you want to generate some /etc/config, you should have some
259 templates/config template). That's why _config_writer_cfg_dir and
260 _config_writer_tpl_dir can't be the same.
262 When you write your Handler, you should call _config_build_templates() in
263 you Handler constructor to build the templates.
265 To write the configuration files, you must use the _write_config() method.
266 To know what variables to replace in the template, you have to provide a
267 method called _get_config_vars(tamplate_name), which should return a
268 dictionary of variables to pass to the template system to be replaced in
269 the template for the configuration file 'config_file'.
271 # TODO implement it using metaclasses to add the handlers method by demand
272 # (only for specifieds commands).
274 _config_writer_files = ()
275 _config_writer_cfg_dir = '.'
276 _config_writer_tpl_dir = 'templates'
278 def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
279 r"Initialize the object, see the class documentation for details."
280 if files is not None:
281 self._config_writer_files = files
282 if cfg_dir is not None:
283 self._config_writer_cfg_dir = cfg_dir
284 if tpl_dir is not None:
285 self._config_writer_tpl_dir = tpl_dir
286 self._config_build_templates()
288 def _config_build_templates(self):
289 r"_config_writer_templates() -> None :: Build the template objects."
290 if isinstance(self._config_writer_files, basestring):
291 self._config_writer_files = (self._config_writer_files,)
292 if not hasattr(self, '_config_writer_templates') \
293 or not self._config_writer_templates:
294 self._config_writer_templates = dict()
295 for t in self._config_writer_files:
296 f = path.join(self._config_writer_tpl_dir, t)
297 self._config_writer_templates[t] = Template(filename=f)
299 def _render_config(self, template_name, vars=None):
300 r"""_render_config(template_name[, config_filename[, vars]]).
302 Render a single config file using the template 'template_name'. If
303 vars is specified, it's used as the dictionary with the variables
304 to replace in the templates, if not, it looks for a
305 _get_config_vars() method to get it.
308 if hasattr(self, '_get_config_vars'):
309 vars = self._get_config_vars(template_name)
313 vars = vars(template_name)
314 return self._config_writer_templates[template_name].render(**vars)
316 def _write_single_config(self, template_name, config_filename=None, vars=None):
317 r"""_write_single_config(template_name[, config_filename[, vars]]).
319 Write a single config file using the template 'template_name'. If no
320 config_filename is specified, the config filename will be the same as
321 the 'template_name' (but stored in the generated config files
322 directory). If it's specified, the generated config file is stored in
323 the file called 'config_filename' (also in the generated files
324 directory). If vars is specified, it's used as the dictionary with the
325 variables to replace in the templates, if not, it looks for a
326 _get_config_vars() method to get it.
328 if not config_filename:
329 config_filename = template_name
331 if hasattr(self, '_get_config_vars'):
332 vars = self._get_config_vars(template_name)
336 vars = vars(template_name)
337 f = file(path.join(self._config_writer_cfg_dir, config_filename), 'w')
338 ctx = Context(f, **vars)
339 self._config_writer_templates[template_name].render_context(ctx)
342 def _write_config(self):
343 r"_write_config() -> None :: Generate all the configuration files."
344 for t in self._config_writer_files:
345 self._write_single_config(t)
348 class ServiceHandler(Handler):
349 r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
351 This is a helper class to inherit from to automatically handle services
352 with start, stop, restart, reload actions.
354 The actions can be defined by calling the constructor with all the
355 parameters or in a more declarative way as class attributes, like:
357 class TestHandler(ServiceHandler):
358 _service_start = ('command', 'start')
359 _service_stop = ('command', 'stop')
360 _service_restart = ('command', 'restart')
361 _service_reload = 'reload-command'
363 Commands are executed without using the shell, that's why they are specified
364 as tuples (where the first element is the command and the others are the
365 command arguments). If only a command is needed (without arguments) a single
366 string can be specified.
368 All commands must be specified.
370 # TODO implement it using metaclasses to add the handlers method by demand
371 # (only for specifieds commands).
373 def __init__(self, start=None, stop=None, restart=None, reload=None):
374 r"Initialize the object, see the class documentation for details."
375 for (name, action) in dict(start=start, stop=stop, restart=restart,
376 reload=reload).items():
377 if action is not None:
378 setattr(self, '_service_%s' % name, action)
380 @handler(u'Start the service.')
382 r"start() -> None :: Start the service."
383 call(self._service_start)
385 @handler(u'Stop the service.')
387 r"stop() -> None :: Stop the service."
388 call(self._service_stop)
390 @handler(u'Restart the service.')
392 r"restart() -> None :: Restart the service."
393 call(self._service_restart)
395 @handler(u'Reload the service config (without restarting, if possible).')
397 r"reload() -> None :: Reload the configuration of the service."
398 call(self._service_reload)
400 class InitdHandler(Handler):
401 r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
403 This is a helper class to inherit from to automatically handle services
404 with start, stop, restart, reload actions using a /etc/init.d like script.
406 The name and directory of the script can be defined by calling the
407 constructor or in a more declarative way as class attributes, like:
409 class TestHandler(ServiceHandler):
410 _initd_name = 'some-service'
411 _initd_dir = '/usr/local/etc/init.d'
413 The default _initd_dir is '/etc/init.d', _initd_name has no default and
414 must be specified in either way.
416 Commands are executed without using the shell.
418 # TODO implement it using metaclasses to add the handlers method by demand
419 # (only for specifieds commands).
421 _initd_dir = '/etc/init.d'
423 def __init__(self, initd_name=None, initd_dir=None):
424 r"Initialize the object, see the class documentation for details."
425 if initd_name is not None:
426 self._initd_name = initd_name
427 if initd_dir is not None:
428 self._initd_dir = initd_dir
430 @handler(u'Start the service.')
432 r"start() -> None :: Start the service."
433 call((path.join(self._initd_dir, self._initd_name), 'start'))
435 @handler(u'Stop the service.')
437 r"stop() -> None :: Stop the service."
438 call((path.join(self._initd_dir, self._initd_name), 'stop'))
440 @handler(u'Restart the service.')
442 r"restart() -> None :: Restart the service."
443 call((path.join(self._initd_dir, self._initd_name), 'restart'))
445 @handler(u'Reload the service config (without restarting, if possible).')
447 r"reload() -> None :: Reload the configuration of the service."
448 call((path.join(self._initd_dir, self._initd_name), 'reload'))
450 class TransactionalHandler(Handler):
451 r"""Handle command transactions providing a commit and rollback commands.
453 This is a helper class to inherit from to automatically handle
454 transactional handlers, which have commit and rollback commands.
456 The handler should provide a reload() method (see ServiceHandler and
457 InitdHandler for helper classes to provide this) which will be called
458 when a commit command is issued (if a reload() command is present).
459 The persistent data will be written too (if a _dump() method is provided,
460 see Persistent and Restorable for that), and the configuration files
461 will be generated (if a _write_config method is present, see ConfigWriter).
463 # TODO implement it using metaclasses to add the handlers method by demand
464 # (only for specifieds commands).
466 @handler(u'Commit the changes (reloading the service, if necessary).')
468 r"commit() -> None :: Commit the changes and reload the service."
469 if hasattr(self, '_dump'):
471 if hasattr(self, '_write_config'):
473 if hasattr(self, 'reload'):
476 @handler(u'Discard all the uncommited changes.')
478 r"rollback() -> None :: Discard the changes not yet commited."
479 if hasattr(self, '_load'):
482 class ParametersHandler(Handler):
483 r"""ParametersHandler([attr]) -> ParametersHandler.
485 This is a helper class to inherit from to automatically handle
486 service parameters, providing set, get, list and show commands.
488 The attribute that holds the parameters can be defined by calling the
489 constructor or in a more declarative way as class attributes, like:
491 class TestHandler(ServiceHandler):
492 _parameters_attr = 'some_attr'
494 The default is 'params' and it should be a dictionary.
496 # TODO implement it using metaclasses to add the handlers method by demand
497 # (only for specifieds commands).
499 _parameters_attr = 'params'
501 def __init__(self, attr=None):
502 r"Initialize the object, see the class documentation for details."
504 self._parameters_attr = attr
506 @handler(u'Set a service parameter.')
507 def set(self, param, value):
508 r"set(param, value) -> None :: Set a service parameter."
509 if not param in self.params:
510 raise ParameterNotFoundError(param)
511 self.params[param] = value
513 @handler(u'Get a service parameter.')
514 def get(self, param):
515 r"get(param) -> None :: Get a service parameter."
516 if not param in self.params:
517 raise ParameterNotFoundError(param)
518 return self.params[param]
520 @handler(u'List all available service parameters.')
522 r"list() -> tuple :: List all the parameter names."
523 return self.params.keys()
525 @handler(u'Get all service parameters, with their values.')
527 r"show() -> (key, value) tuples :: List all the parameters."
528 return self.params.items()
531 if __name__ == '__main__':
534 class STestHandler1(ServiceHandler):
535 _service_start = ('service', 'start')
536 _service_stop = ('service', 'stop')
537 _service_restart = ('ls', '/')
538 _service_reload = ('cp', '/la')
539 class STestHandler2(ServiceHandler):
541 ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
542 'cmd-restart', 'cmd-reload')
543 class ITestHandler1(InitdHandler):
544 _initd_name = 'test1'
545 class ITestHandler2(InitdHandler):
547 InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
555 print h.__class__.__name__
558 except ExecutionError, e:
562 except ExecutionError, e:
566 except ExecutionError, e:
570 except ExecutionError, e:
576 class PTestHandler(Persistent):
577 _persistent_attrs = 'vars'
579 self.vars = dict(a=1, b=2)
598 class RTestHandler(Restorable):
599 _persistent_attrs = 'vars'
600 _restorable_defaults = dict(vars=dict(a=1, b=2))
614 os.mkdir('templates')
615 f = file('templates/config', 'w')
616 f.write('Hello, ${name}! You are ${what}.')
619 print file('templates/config').read()
620 class CTestHandler(ConfigWriter):
621 _config_writer_files = 'config'
623 self._config_build_templates()
624 def _get_config_vars(self, config_file):
625 return dict(name='you', what='a parrot')
629 print file('config').read()
631 os.unlink('templates/config')
632 os.rmdir('templates')