From 57f449cc30b59970b97d7ee4d74cc02a98e07e30 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 12 Nov 2007 13:25:23 -0500 Subject: Abstract out the modules system to allow for other types of modules, and to also centralize configuration/loading/access some more. --- cobbler/api.py | 16 +- cobbler/cobbler2.py | 77 +++++++++ cobbler/command.py | 291 +++++++++++++++++++++++++++++++++++ cobbler/module_loader.py | 22 ++- cobbler/modules/serializer_shelve.py | 2 +- cobbler/modules/serializer_yaml.py | 2 +- cobbler/serializer.py | 33 ++-- cobbler/utils.py | 2 + 8 files changed, 422 insertions(+), 23 deletions(-) create mode 100644 cobbler/cobbler2.py create mode 100644 cobbler/command.py (limited to 'cobbler') diff --git a/cobbler/api.py b/cobbler/api.py index 2a51c1a..b846c81 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -38,7 +38,7 @@ class BootAPI: self.__dict__ = self.__shared_state if not BootAPI.has_loaded: BootAPI.has_loaded = True - self.modules = module_loader.load_modules() + module_loader.load_modules() self._config = config.Config(self) self.deserialize() @@ -232,6 +232,20 @@ class BootAPI: """ return self._config.deserialize_raw(collection_name) + def get_module_by_name(self,module_name): + """ + Returns a loaded cobbler module named 'name', if one exists, else None. + """ + return module_loader.get_module_by_name(module_name) + + def get_module_from_file(self,section,name): + """ + Looks in /etc/cobbler/modules.conf for a section called 'section' + and a key called 'name', and then returns the module that corresponds + to the value of that key. + """ + return module_loader.get_module_from_file(section,name) + if __name__ == "__main__": api = BootAPI() print api.version() diff --git a/cobbler/cobbler2.py b/cobbler/cobbler2.py new file mode 100644 index 0000000..29fe2b6 --- /dev/null +++ b/cobbler/cobbler2.py @@ -0,0 +1,77 @@ +#!/usr/bin/python + +""" +Command line interface for cobbler, a network provisioning configuration +library. Consult 'man cobbler' for general info. + +Copyright 2006-2007, Red Hat, Inc +Michael DeHaan + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + + + +import glob +import sys + + +import command + +#FIXME: need a plug-in runtime module loader here +from cmd_modules import call +from cmd_modules import show +from cmd_modules import copyfile +from cmd_modules import listminions +from cmd_modules import ping + +from func.overlord import client + +class FuncCommandLine(command.Command): + name = "func" + useage = "func is the commandline interface to a func minion" + + subCommandClasses = [call.Call, show.Show, + copyfile.CopyFile, listminions.ListMinions, ping.Ping] + + def __init__(self): + + command.Command.__init__(self) + + def do(self, args): + pass + + def addOptions(self): + self.parser.add_option('', '--version', action="store_true", + help="show version information") + + # just some ugly goo to try to guess if arg[1] is hostnamegoo or + # a command name + def _isGlob(self, str): + if str.find("*") or str.find("?") or str.find("[") or str.find("]"): + return True + return False + + def handleArguments(self, args): + if len(args) < 2: + print "see the func manpage for usage" + sys.exit(411) + server_string = args[0] + # try to be clever about this for now + if client.isServer(server_string) or self._isGlob(server_string): + self.server_spec = server_string + args.pop(0) + # if it doesn't look like server, assume it + # is a sub command? that seems wrong, what about + # typo's and such? How to catch that? -akl + # maybe a class variable self.data on Command? + + def handleOptions(self, options): + if options.version: + #FIXME + print "version is NOT IMPLEMENTED YET" diff --git a/cobbler/command.py b/cobbler/command.py new file mode 100644 index 0000000..c8de66b --- /dev/null +++ b/cobbler/command.py @@ -0,0 +1,291 @@ +# -*- Mode: Python; test-case-name: test_command -*- +# vi:si:et:sw=4:sts=4:ts=4 + +# This file is released under the standard PSF license. +# +# from MOAP - https://thomas.apestaart.org/moap/trac +# written by Thomas Vander Stichele (thomas at apestaart dot org) +# + +# Modified from usage in func +# https://hosted.fedoraproject.org/projects/func/ + +""" +Command class. +""" + +import optparse +import sys + +# from func.config import read_config, CONFIG_FILE +# from func.commonconfig import CMConfig + +class CommandHelpFormatter(optparse.IndentedHelpFormatter): + """ + I format the description as usual, but add an overview of commands + after it if there are any, formatted like the options. + """ + _commands = None + + def addCommand(self, name, description): + if self._commands is None: + self._commands = {} + self._commands[name] = description + + ### override parent method + def format_description(self, description): + # textwrap doesn't allow for a way to preserve double newlines + # to separate paragraphs, so we do it here. + blocks = description.split('\n\n') + rets = [] + + for block in blocks: + rets.append(optparse.IndentedHelpFormatter.format_description(self, + block)) + ret = "\n".join(rets) + if self._commands: + commandDesc = [] + commandDesc.append("commands:") + keys = self._commands.keys() + keys.sort() + length = 0 + for key in keys: + if len(key) > length: + length = len(key) + for name in keys: + format = " %-" + "%d" % length + "s %s" + commandDesc.append(format % (name, self._commands[name])) + ret += "\n" + "\n".join(commandDesc) + "\n" + return ret + +class CommandOptionParser(optparse.OptionParser): + """ + I parse options as usual, but I explicitly allow setting stdout + so that our print_help() method (invoked by default with -h/--help) + defaults to writing there. + """ + _stdout = sys.stdout + + def set_stdout(self, stdout): + self._stdout = stdout + + # we're overriding the built-in file, but we need to since this is + # the signature from the base class + __pychecker__ = 'no-shadowbuiltin' + def print_help(self, file=None): + # we are overriding a parent method so we can't do anything about file + __pychecker__ = 'no-shadowbuiltin' + if file is None: + file = self._stdout + file.write(self.format_help()) + +class Command: + """ + I am a class that handles a command for a program. + Commands can be nested underneath a command for further processing. + + @cvar name: name of the command, lowercase + @cvar aliases: list of alternative lowercase names recognized + @type aliases: list of str + @cvar usage: short one-line usage string; + %command gets expanded to a sub-command or [commands] + as appropriate + @cvar summary: short one-line summary of the command + @cvar description: longer paragraph explaining the command + @cvar subCommands: dict of name -> commands below this command + @type subCommands: dict of str -> L{Command} + """ + name = None + aliases = None + usage = None + summary = None + description = None + parentCommand = None + subCommands = None + subCommandClasses = None + aliasedSubCommands = None + + def __init__(self, parentCommand=None, stdout=sys.stdout, + stderr=sys.stderr): + """ + Create a new command instance, with the given parent. + Allows for redirecting stdout and stderr if needed. + This redirection will be passed on to child commands. + """ + if not self.name: + self.name = str(self.__class__).split('.')[-1].lower() + self.stdout = stdout + self.stderr = stderr + self.parentCommand = parentCommand + + # from Func, now removed: + # self.config = read_config(CONFIG_FILE, CMConfig) + + # create subcommands if we have them + self.subCommands = {} + self.aliasedSubCommands = {} + if self.subCommandClasses: + for C in self.subCommandClasses: + c = C(self, stdout=stdout, stderr=stderr) + self.subCommands[c.name] = c + if c.aliases: + for alias in c.aliases: + self.aliasedSubCommands[alias] = c + + # create our formatter and add subcommands if we have them + formatter = CommandHelpFormatter() + if self.subCommands: + for name, command in self.subCommands.items(): + formatter.addCommand(name, command.summary or + command.description) + + # expand %command for the bottom usage + usage = self.usage or self.name + if usage.find("%command") > -1: + usage = usage.split("%command")[0] + '[command]' + usages = [usage, ] + + # FIXME: abstract this into getUsage that takes an optional + # parentCommand on where to stop recursing up + # useful for implementing subshells + + # walk the tree up for our usage + c = self.parentCommand + while c: + usage = c.usage or c.name + if usage.find(" %command") > -1: + usage = usage.split(" %command")[0] + usages.append(usage) + c = c.parentCommand + usages.reverse() + usage = " ".join(usages) + + # create our parser + description = self.description or self.summary + self.parser = CommandOptionParser( + usage=usage, description=description, + formatter=formatter) + self.parser.set_stdout(self.stdout) + self.parser.disable_interspersed_args() + + # allow subclasses to add options + self.addOptions() + + def addOptions(self): + """ + Override me to add options to the parser. + """ + pass + + def do(self, args): + """ + Override me to implement the functionality of the command. + """ + pass + + def parse(self, argv): + """ + Parse the given arguments and act on them. + + @rtype: int + @returns: an exit code + """ + self.options, args = self.parser.parse_args(argv) + + # FIXME: make handleOptions not take options, since we store it + # in self.options now + ret = self.handleOptions(self.options) + if ret: + return ret + + # handle pleas for help + if args and args[0] == 'help': + self.debug('Asked for help, args %r' % args) + + # give help on current command if only 'help' is passed + if len(args) == 1: + self.outputHelp() + return 0 + + # complain if we were asked for help on a subcommand, but we don't + # have any + if not self.subCommands: + self.stderr.write('No subcommands defined.') + self.parser.print_usage(file=self.stderr) + self.stderr.write( + "Use --help to get more information about this command.\n") + return 1 + + # rewrite the args the other way around; + # help doap becomes doap help so it gets deferred to the doap + # command + args = [args[1], args[0]] + + + # if we have args that we need to deal with, do it now + # before we start looking for subcommands + self.handleArguments(args) + + # if we don't have subcommands, defer to our do() method + if not self.subCommands: + ret = self.do(args) + + # if everything's fine, we return 0 + if not ret: + ret = 0 + + return ret + + + # if we do have subcommands, defer to them + try: + command = args[0] + except IndexError: + self.parser.print_usage(file=self.stderr) + self.stderr.write( + "Use --help to get a list of commands.\n") + return 1 + + if command in self.subCommands.keys(): + return self.subCommands[command].parse(args[1:]) + + if self.aliasedSubCommands: + if command in self.aliasedSubCommands.keys(): + return self.aliasedSubCommands[command].parse(args[1:]) + + self.stderr.write("Unknown command '%s'.\n" % command) + return 1 + + def outputHelp(self): + """ + Output help information. + """ + self.parser.print_help(file=self.stderr) + + def outputUsage(self): + """ + Output usage information. + Used when the options or arguments were missing or wrong. + """ + self.parser.print_usage(file=self.stderr) + + def handleOptions(self, options): + """ + Handle the parsed options. + """ + pass + + def handleArguments(self, arguments): + """ + Handle the parsed arguments. + """ + pass + + def getRootCommand(self): + """ + Return the top-level command, which is typically the program. + """ + c = self + while c.parentCommand: + c = c.parentCommand + return c diff --git a/cobbler/module_loader.py b/cobbler/module_loader.py index 5b27a01..bb94fec 100644 --- a/cobbler/module_loader.py +++ b/cobbler/module_loader.py @@ -20,6 +20,13 @@ import os import sys import glob from rhpl.translate import _, N_, textdomain, utf8 +import ConfigParser + +MODULE_CACHE = {} +MODULES_BY_CATEGORY = {} + +cp = ConfigParser.ConfigParser() +cp.read("/etc/cobbler/modules.conf") plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler/modules" % plib @@ -51,16 +58,25 @@ def load_modules(module_path=mod_path, blacklist=None): errmsg = _("%(module_path)s/%(modname)s is not a proper module") print errmsg % {'module_path': module_path, 'modname':modname} continue - if blip.register(): - mods[modname] = blip + category = blip.register() + if category: + MODULE_CACHE[modname] = blip + if not MODULES_BY_CATEGORY.has_key(category): + MODULES_BY_CATEGORY[category] = {} + MODULES_BY_CATEGORY[category][modname] = blip except ImportError, e: print e raise - return mods + return (MODULE_CACHE, MODULES_BY_CATEGORY) +def get_module_by_name(name): + return MODULE_CACHE.get(name, None) +def get_module_from_file(category,field): + value = cp.get("serializers",field) + return MODULE_CACHE.get(value, None) if __name__ == "__main__": print load_modules(module_path) diff --git a/cobbler/modules/serializer_shelve.py b/cobbler/modules/serializer_shelve.py index fa5b1d0..246a92a 100644 --- a/cobbler/modules/serializer_shelve.py +++ b/cobbler/modules/serializer_shelve.py @@ -67,7 +67,7 @@ def register(): """ The mandatory cobbler module registration hook. """ - return True + return "serializer" def serialize(obj): """ diff --git a/cobbler/modules/serializer_yaml.py b/cobbler/modules/serializer_yaml.py index 4c3dfbb..6425bd1 100644 --- a/cobbler/modules/serializer_yaml.py +++ b/cobbler/modules/serializer_yaml.py @@ -31,7 +31,7 @@ def register(): """ The mandatory cobbler module registration hook. """ - return True + return "serializer" def serialize(obj): """ diff --git a/cobbler/serializer.py b/cobbler/serializer.py index f7904c1..8593aad 100644 --- a/cobbler/serializer.py +++ b/cobbler/serializer.py @@ -17,28 +17,22 @@ import errno import os from rhpl.translate import _, N_, textdomain, utf8 -import yaml # Howell-Clark version - from cexceptions import * import utils import api as cobbler_api -import modules.serializer_yaml as serializer_yaml -import ConfigParser - -MODULE_CACHE = {} -cp = ConfigParser.ConfigParser() -cp.read("/etc/cobbler/modules.conf") def serialize(obj): """ Save a collection to disk or other storage. """ - storage_module = __get_storage_module(obj.collection_type()) storage_module.serialize(obj) return True def serialize_item(collection, item): + """ + Save an item. + """ storage_module = __get_storage_module(collection.collection_type()) save_fn = getattr(storage_module, "serialize_item", None) if save_fn is None: @@ -49,6 +43,9 @@ def serialize_item(collection, item): return save_fn(collection,item) def serialize_delete(collection, item): + """ + Delete an object from a saved state. + """ storage_module = __get_storage_module(collection.collection_type()) delete_fn = getattr(storage_module, "serialize_delete", None) if delete_fn is None: @@ -67,17 +64,19 @@ def deserialize(obj,topological=False): return storage_module.deserialize(obj,topological) def deserialize_raw(collection_type): + """ + Return the datastructure corresponding to the serialized + disk state, without going through the Cobbler object system. + Much faster, when you don't need the objects. + """ storage_module = __get_storage_module(collection_type) return storage_module.deserialize_raw(collection_type) def __get_storage_module(collection_type): + """ + Look up serializer in /etc/cobbler/modules.conf + """ + capi = cobbler_api.BootAPI() + return capi.get_module_from_file("serializers",collection_type) - if not MODULE_CACHE.has_key(collection_type): - value = cp.get("serializers",collection_type) - module = cobbler_api.BootAPI().modules[value] - MODULE_CACHE[collection_type] = module - return module - else: - return MODULE_CACHE[collection_type] - diff --git a/cobbler/utils.py b/cobbler/utils.py index 46f0010..ad72feb 100644 --- a/cobbler/utils.py +++ b/cobbler/utils.py @@ -25,6 +25,8 @@ import logging from cexceptions import * from rhpl.translate import _, N_, textdomain, utf8 +MODULE_CACHE = {} + # import api # factor out _re_kernel = re.compile(r'vmlinuz(.*)') -- cgit