diff options
author | Michael DeHaan <mdehaan@mdehaan.rdu.redhat.com> | 2007-06-13 15:59:47 -0400 |
---|---|---|
committer | Michael DeHaan <mdehaan@mdehaan.rdu.redhat.com> | 2007-06-13 15:59:47 -0400 |
commit | 32908858c032f91726b02520d11a16d6cff2e93a (patch) | |
tree | d914d43b6231db3dc3755690fe1fca08223caee7 | |
parent | 72e9e1de6c3bc096180c62734476f775cdebfbee (diff) | |
download | cobbler-32908858c032f91726b02520d11a16d6cff2e93a.tar.gz cobbler-32908858c032f91726b02520d11a16d6cff2e93a.tar.xz cobbler-32908858c032f91726b02520d11a16d6cff2e93a.zip |
Lots of work towards profile inheritance. This works in the UI now, with
some rough edges (like listing the tree).
cobbler profile add --name=profile2 --inherit=profile1 --otherparameters=...
cobbler profile edit --name=profile2 --stillmoreparamters=...
Data is interleaved for hashes, combined for arrays, and overriden for scalar
values. This was heavily inspired by Will-It-Blend, and in this implementation
it all blends.
Implementation notes -- Updating a parent profile doesn't apply changes
to the child objects until a sync, so this seems like a good upgrade
for a future commit. Also, the children mapping that makes this
possible needs some tweaks because they may load out of order,
in which case "cobbler list" can't render a full
tree. There are various approaches to deal with this and it should be
a (relatively) easy change.
-rw-r--r-- | TODO | 2 | ||||
-rw-r--r-- | cobbler/api.py | 16 | ||||
-rwxr-xr-x | cobbler/cobbler.py | 54 | ||||
-rw-r--r-- | cobbler/collection.py | 4 | ||||
-rw-r--r-- | cobbler/config.py | 16 | ||||
-rw-r--r-- | cobbler/item.py | 37 | ||||
-rw-r--r-- | cobbler/item_distro.py | 34 | ||||
-rw-r--r-- | cobbler/item_profile.py | 72 | ||||
-rw-r--r-- | cobbler/item_repo.py | 25 | ||||
-rw-r--r-- | cobbler/item_system.py | 32 | ||||
-rw-r--r-- | cobbler/serializer.py | 1 | ||||
-rw-r--r-- | cobbler/utils.py | 34 |
12 files changed, 231 insertions, 96 deletions
@@ -14,4 +14,4 @@ cobbler TODO list. - build net-install CD images - build non-net-install CD from cobbler profile - have pre and post triggers, check return codes and validate - +- make is_valid throw exceptions that explains exactly what is wrong. diff --git a/cobbler/api.py b/cobbler/api.py index fe35751e..58f1046b 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -80,30 +80,30 @@ class BootAPI: """ return self._config.settings() - def new_system(self): + def new_system(self,is_subobject=False): """ Return a blank, unconfigured system, unattached to a collection """ - return self._config.new_system() + return self._config.new_system(is_subobject=is_subobject) - def new_distro(self): + def new_distro(self,is_subobject=False): """ Create a blank, unconfigured distro, unattached to a collection. """ - return self._config.new_distro() + return self._config.new_distro(is_subobject=is_subobject) - def new_profile(self): + def new_profile(self,is_subobject=False): """ Create a blank, unconfigured profile, unattached to a collection """ - return self._config.new_profile() + return self._config.new_profile(is_subobject=is_subobject) - def new_repo(self): + def new_repo(self,is_subobject=False): """ Create a blank, unconfigured repo, unattached to a collection """ - return self._config.new_repo() + return self._config.new_repo(is_subobject=is_subobject) def check(self): """ diff --git a/cobbler/cobbler.py b/cobbler/cobbler.py index 7d88ccbd..fb34871c 100755 --- a/cobbler/cobbler.py +++ b/cobbler/cobbler.py @@ -112,7 +112,7 @@ class BootCLI: Run the command line and return system exit code """ self.api.deserialize() - self.curry_args(self.args[1:], self.commands['toplevel']) + self.relay_args(self.args[1:], self.commands['toplevel']) def usage(self,args): """ @@ -173,8 +173,7 @@ class BootCLI: # LISTING FUNCTIONS def list(self,args): - collection = self.api.distros() - self.__tree(collection,0) + self.__tree(self.api.distros(),0) self.__tree(self.api.repos(),0) def __tree(self,collection,level): @@ -262,8 +261,8 @@ class BootCLI: ###################################################################### # BASIC FRAMEWORK - def __generic_add(self,args,new_fn,control_fn): - obj = new_fn() + def __generic_add(self,args,new_fn,control_fn,does_inherit): + obj = new_fn(is_subobject=does_inherit) control_fn(args,obj) def __generic_edit(self,args,collection_fn,control_fn,exc_msg): @@ -379,18 +378,42 @@ class BootCLI: ##################################################################### # ADD FUNCTIONS - + + def __prescan_for_inheritance_args(self,args): + """ + Normally we just feed all the arguments through to the functions + in question, but here, we need to send a special flag to the foo_add + functions if we are creating a subobject, because that needs to affect + what function calls we make. So, this checks to see if the user + is creating a subobject by looking for --inherit in the arguments list, + before we actually parse the --inherit arg. Complicated :) + """ + for x in args: + try: + key, value = x.split("=",1) + value = value.replace('"','').replace("'",'') + if key == "--inherit": + return True + except: + traceback.print_exc() # FIXME: remove + pass + return False + def distro_add(self,args): - self.__generic_add(args,self.api.new_distro,self.__distro_control) + does_inherit = self.__prescan_for_inheritance_args(args) + self.__generic_add(args,self.api.new_distro,self.__distro_control,does_inherit) def profile_add(self,args): - self.__generic_add(args,self.api.new_profile,self.__profile_control) + does_inherit = self.__prescan_for_inheritance_args(args) + self.__generic_add(args,self.api.new_profile,self.__profile_control,does_inherit) def system_add(self,args): - self.__generic_add(args,self.api.new_system,self.__system_control) + does_inherit = self.__prescan_for_inheritance_args(args) + self.__generic_add(args,self.api.new_system,self.__system_control,does_inherit) def repo_add(self,args): - self.__generic_add(args,self.api.new_repo,self.__repo_control) + does_inherit = self.__prescan_for_inheritance_args(args) + self.__generic_add(args,self.api.new_repo,self.__repo_control,does_inherit) ############################################################### @@ -402,6 +425,7 @@ class BootCLI: """ commands = { '--name' : lambda(a) : profile.set_name(a), + '--inherit' : lambda(a) : profile.set_parent(a), '--newname' : lambda(a) : True, '--profile' : lambda(a) : profile.set_name(a), '--distro' : lambda(a) : profile.set_distro(a), @@ -507,7 +531,7 @@ class BootCLI: on_ok() self.api.serialize() - def curry_args(self, args, commands): + def relay_args(self, args, commands): """ Lookup command args[0] in the dispatch table and feed it the remaining args[1:-1] as arguments. @@ -652,25 +676,25 @@ class BootCLI: """ Handles any of the 'cobbler distro' subcommands """ - return self.curry_args(args, self.commands['distro']) + return self.relay_args(args, self.commands['distro']) def profile(self,args): """ Handles any of the 'cobbler profile' subcommands """ - return self.curry_args(args, self.commands['profile']) + return self.relay_args(args, self.commands['profile']) def system(self,args): """ Handles any of the 'cobbler system' subcommands """ - return self.curry_args(args, self.commands['system']) + return self.relay_args(args, self.commands['system']) def repo(self,args): """ Handles any of the 'cobbler repo' subcommands """ - return self.curry_args(args, self.commands['repo']) + return self.relay_args(args, self.commands['repo']) #################################################### diff --git a/cobbler/collection.py b/cobbler/collection.py index a6a39915..43ad068e 100644 --- a/cobbler/collection.py +++ b/cobbler/collection.py @@ -97,7 +97,8 @@ class Collection(serializable.Serializable): """ if ref is None or not ref.is_valid(): - raise CX(_("invalid parameter")) + raise CX(_("insufficient or invalid arguments supplied")) + print "DEBUG: adding object %s" % ref.name if not with_copy: # don't need to run triggers, so add it already ... self.listing[ref.name.lower()] = ref @@ -126,7 +127,6 @@ class Collection(serializable.Serializable): parent = ref.get_parent() if parent != None: parent.children[ref.name] = ref - return True def _run_triggers(self,ref,globber): diff --git a/cobbler/config.py b/cobbler/config.py index 92bd9b5d..cd19d008 100644 --- a/cobbler/config.py +++ b/cobbler/config.py @@ -100,29 +100,29 @@ class Config: """ return self._repos - def new_distro(self): + def new_distro(self,is_subobject=False): """ Create a new distro object with a backreference to this object """ - return distro.Distro(weakref.proxy(self)) + return distro.Distro(weakref.proxy(self),is_subobject=is_subobject) - def new_system(self): + def new_system(self,is_subobject=False): """ Create a new system with a backreference to this object """ - return system.System(weakref.proxy(self)) + return system.System(weakref.proxy(self),is_subobject=is_subobject) - def new_profile(self): + def new_profile(self,is_subobject=False): """ Create a new profile with a backreference to this object """ - return profile.Profile(weakref.proxy(self)) + return profile.Profile(weakref.proxy(self),is_subobject=is_subobject) - def new_repo(self): + def new_repo(self,is_subobject=False): """ Create a new mirror to keep track of... """ - return repo.Repo(weakref.proxy(self)) + return repo.Repo(weakref.proxy(self),is_subobject=is_subobject) def clear(self): """ diff --git a/cobbler/item.py b/cobbler/item.py index af46defa..db2bef4e 100644 --- a/cobbler/item.py +++ b/cobbler/item.py @@ -22,15 +22,35 @@ class Item(serializable.Serializable): TYPE_NAME = "generic" - def __init__(self,config): + def __init__(self,config,is_subobject=False): """ Constructor. Requires a back reference to the Config management object. + + NOTE: is_subobject is used for objects that allow inheritance in their trees. This + inheritance refers to conceptual inheritance, not Python inheritance. Objects created + with is_subobject need to call their set_parent() method immediately after creation + and pass in a value of an object of the same type. Currently this is only supported + for profiles. Subobjects blend their data with their parent objects and only require + a valid parent name and a name for themselves, so other required options can be + gathered from items further up the cobbler tree. + + Old cobbler: New cobbler: + distro distro + profile profile + system profile <-- created with is_subobject=True + system <-- created as normal + + For consistancy, there is some code supporting this in all object types, though it is only usable + (and only should be used) for profiles at this time. Objects that are children of + objects of the same type (i.e. subprofiles) need to pass this in as True. Otherwise, just + use False for is_subobject and the parent object will (therefore) have a different type. + """ self.config = config self.settings = self.config._settings - self.clear() - self.children = {} # caching for performance reasons, not serialized - self.conceptual_parent = None # " " + self.clear(is_subobject) # reset behavior differs for inheritance cases + self.parent = None # all objects by default are not subobjects + self.children = {} # caching for performance reasons, not serialized def clear(self): raise exceptions.NotImplementedError @@ -72,9 +92,6 @@ class Item(serializable.Serializable): subprofile. Get the first parent of a different type. """ - if self.conceptual_parent is not None: - return self.conceptual_parent - # FIXME: this is a workaround to get the type of an instance var # what's a more clean way to do this that's python 2.3 friendly? # this returns something like: cobbler.item_system.System @@ -85,7 +102,7 @@ class Item(serializable.Serializable): if mtype != ptype: self.conceptual_parent = parent return parent - + parent = parent.get_parent() return None def set_name(self,name): @@ -93,6 +110,8 @@ class Item(serializable.Serializable): All objects have names, and with the exception of System they aren't picky about it. """ + if self.name not in ["",None] and self.parent not in ["",None] and self.name == self.parent: + raise CX(_("self parentage is weird")) self.name = name return True @@ -125,7 +144,7 @@ class Item(serializable.Serializable): """ Used in subclass from_datastruct functions to load items from a hash. Intented to ease backwards compatibility of config - files during upgrades. + files during upgrades. """ if datastruct.has_key(key): return datastruct[key] diff --git a/cobbler/item_distro.py b/cobbler/item_distro.py index 02f2eab4..b6a5bea8 100644 --- a/cobbler/item_distro.py +++ b/cobbler/item_distro.py @@ -26,18 +26,18 @@ class Distro(item.Item): TYPE_NAME = _("distro") - def clear(self): + def clear(self,is_subobject=False): """ Reset this object. """ - self.name = None - self.kernel = None - self.initrd = None - self.kernel_options = {} - self.ks_meta = {} - self.arch = "x86" - self.breed = "redhat" - self.source_repos = [] + self.name = None + self.kernel = (None, '<<inherit>>')[is_subobject] + self.initrd = (None, '<<inherit>>')[is_subobject] + self.kernel_options = ({}, '<<inherit>>')[is_subobject] + self.ks_meta = ({}, '<<inherit>>')[is_subobject] + self.arch = ('x86', '<<inherit>>')[is_subobject] + self.breed = ('redhat', '<<inherit>>')[is_subobject] + self.source_repos = ([], '<<inherit>>')[is_subobject] def make_clone(self): ds = self.to_datastruct() @@ -48,13 +48,19 @@ class Distro(item.Item): def get_parent(self): """ Return object next highest up the tree. + NOTE: conceptually there is no need for subdistros, but it's implemented + anyway for testing purposes """ - return None + if self.parent is None or self.parent == '': + return None + else: + return self.config.distros().find(self.parent) def from_datastruct(self,seed_data): """ Modify this object to take on values in seed_data """ + self.parent = self.load_item(seed_data,'parent') self.name = self.load_item(seed_data,'name') self.kernel = self.load_item(seed_data,'kernel') self.initrd = self.load_item(seed_data,'initrd') @@ -135,8 +141,11 @@ class Distro(item.Item): A distro requires that the kernel and initrd be set. All other variables are optional. """ + # NOTE: this code does not support inheritable distros at this time. + # this is by design because inheritable distros do not make sense. for x in (self.name,self.kernel,self.initrd): - if x is None: return False + if x is None: + return False return True def to_datastruct(self): @@ -151,7 +160,8 @@ class Distro(item.Item): 'ks_meta' : self.ks_meta, 'arch' : self.arch, 'breed' : self.breed, - 'source_repos' : self.source_repos + 'source_repos' : self.source_repos, + 'parent' : self.parent } def printable(self): diff --git a/cobbler/item_profile.py b/cobbler/item_profile.py index 8be30e95..79a7dcb3 100644 --- a/cobbler/item_profile.py +++ b/cobbler/item_profile.py @@ -28,24 +28,25 @@ class Profile(item.Item): cloned.from_datastruct(ds) return cloned - def clear(self): + def clear(self,is_subobject=False): """ Reset this object. """ - self.name = None - self.distro = None # a name, not a reference - self.kickstart = self.settings.default_kickstart - self.kernel_options = {} - self.ks_meta = {} - self.virt_file_size = 5 # GB. 5 = Decent _minimum_ default for FC5. - self.virt_ram = 512 # MB. Install with 256 not likely to pass - self.repos = "" # names of cobbler repo definitions + self.name = None + self.distro = (None, '<<inherit>>')[is_subobject] + self.kickstart = (self.settings.default_kickstart, '<<inherit>>')[is_subobject] + self.kernel_options = ({}, '<<inherit>>')[is_subobject] + self.ks_meta = ({}, '<<inherit>>')[is_subobject] + self.virt_file_size = (5, '<<inherit>>')[is_subobject] + self.virt_ram = (512, '<<inherit>>')[is_subobject] + self.repos = ("", '<<inherit>>')[is_subobject] def from_datastruct(self,seed_data): """ Load this object's properties based on seed_data """ + self.parent = self.load_item(seed_data,'parent') self.name = self.load_item(seed_data,'name') self.distro = self.load_item(seed_data,'distro') self.kickstart = self.load_item(seed_data,'kickstart') @@ -62,13 +63,33 @@ class Profile(item.Item): self.virt_file_size = self.load_item(seed_data,'virt_file_size') # backwards compatibility -- convert string entries to dicts for storage - if type(self.kernel_options) != dict: + if self.kernel_options != "<<inherit>>" and type(self.kernel_options) != dict: self.set_kernel_options(self.kernel_options) - if type(self.ks_meta) != dict: + if self.ks_meta != "<<inherit>>" and type(self.ks_meta) != dict: self.set_ksmeta(self.ks_meta) return self + def set_parent(self,parent_name): + """ + Instead of a --distro, set the parent of this object to another profile + and use the values from the parent instead of this one where the values + for this profile aren't filled in, and blend them together where they + are hashes. Basically this enables profile inheritance. To use this, + the object MUST have been constructed with is_subobject=True or the + default values for everything will be screwed up and this will likely NOT + work. So, API users -- make sure you pass is_subobject=True into the + constructor when using this. + """ + if parent_name == self.name: + # check must be done in two places as set_parent could be called before/after + # set_name... + raise CX(_("self parentage is weird")) + found = self.config.profiles().find(parent_name) + if found is None: + raise CX(_("profile %s not found, inheritance not possible") % parent_name) + self.parent = parent_name + def set_distro(self,distro_name): """ Sets the distro. This must be the name of an existing @@ -80,6 +101,10 @@ class Profile(item.Item): raise CX(_("distribution not found")) def set_repos(self,repos): + if repos == "<<inherit>>": + self.repos = "<<inherit>>" + return + if type(repos) != list: # allow backwards compatibility support of string input repolist = repos.split(None) @@ -152,7 +177,11 @@ class Profile(item.Item): """ Return object next highest up the tree. """ - return self.config.distros().find(self.distro) + if self.parent is None or self.parent == '': + result = self.config.distros().find(self.distro) + else: + result = self.config.profiles().find(self.parent) + return result def is_valid(self): """ @@ -160,9 +189,19 @@ class Profile(item.Item): as well as Virt info, are optional. (Though I would say provisioning without a kickstart is *usually* not a good idea). """ - for x in (self.name, self.distro): - if x is None: - return False + if self.parent is None or self.parent == '': + # all values must be filled in if not inheriting from another profile + if self.name is None: + raise CX(_("no name specified")) + if self.distro is None: + raise CX(_("no distro specified")) + else: + # if inheriting, specifying distro is not allowed, and + # name is required, but there are no other rules. + if self.name is None: + raise CX(_("no name specified")) + if self.distro != "<<inherit>>": + raise CX(_("cannot override distro when inheriting a profile")) return True def to_datastruct(self): @@ -177,7 +216,8 @@ class Profile(item.Item): 'virt_file_size' : self.virt_file_size, 'virt_ram' : self.virt_ram, 'ks_meta' : self.ks_meta, - 'repos' : self.repos + 'repos' : self.repos, + 'parent' : self.parent } def printable(self): diff --git a/cobbler/item_repo.py b/cobbler/item_repo.py index b7832644..c9846b1d 100644 --- a/cobbler/item_repo.py +++ b/cobbler/item_repo.py @@ -27,15 +27,17 @@ class Repo(item.Item): cloned.from_datastruct(ds) return cloned - def clear(self): - self.name = None # is required - self.mirror = None # is required - self.keep_updated = 1 # has reasonable defaults - self.local_filename = "" # off by default - self.rpm_list = "" # just get selected RPMs + deps - self.createrepo_flags = "-c cache" # none by default - + def clear(self,is_subobject=False): + self.parent = None + self.name = None + self.mirror = (None, '<<inherit>>')[is_subobject] + self.keep_updated = (None, '<<inherit>>')[is_subobject] + self.local_filename = ("", '<<inherit>>')[is_subobject] + self.rpm_list = ("", '<<inherit>>')[is_subobject] + self.createrepo_flags = ("-c cache", '<<inherit>>')[is_subobject] + def from_datastruct(self,seed_data): + self.parent = self.load_item(seed_data, 'parent') self.name = self.load_item(seed_data, 'name') self.mirror = self.load_item(seed_data, 'mirror') self.keep_updated = self.load_item(seed_data, 'keep_updated') @@ -127,7 +129,8 @@ class Repo(item.Item): 'keep_updated' : self.keep_updated, 'local_filename' : self.local_filename, 'rpm_list' : self.rpm_list, - 'createrepo_flags' : self.createrepo_flags + 'createrepo_flags' : self.createrepo_flags, + 'parent' : self.parent } def printable(self): @@ -140,6 +143,10 @@ class Repo(item.Item): return buf def get_parent(self): + """ + currently the Cobbler object space does not support subobjects of this object + as it is conceptually not useful. + """ return None def is_rsync_mirror(self): diff --git a/cobbler/item_system.py b/cobbler/item_system.py index 9de575e4..c2e82438 100644 --- a/cobbler/item_system.py +++ b/cobbler/item_system.py @@ -28,17 +28,21 @@ class System(item.Item): cloned.from_datastruct(ds) return cloned - def clear(self): - self.name = None - self.profile = None # a name, not a reference - self.kernel_options = {} - self.ks_meta = {} - self.ip_address = "" # bad naming here, to the UI, this is usually 'ip-address' - self.mac_address = "" - self.netboot_enabled = 1 - self.hostname = "" + def clear(self,is_subobject=False): + # names of cobbler repo definitions + + self.name = None + self.profile = (None, '<<inherit>>')[is_subobject] + self.kernel_options = ({}, '<<inherit>>')[is_subobject] + self.ks_meta = ({}, '<<inherit>>')[is_subobject] + self.ip_address = ("", '<<inherit>>')[is_subobject] + self.mac_address = ("", '<<inherit>>')[is_subobject] + self.netboot_enabled = (1, '<<inherit>>')[is_subobject] + self.hostname = ("", '<<inheirt>>')[is_subobject] def from_datastruct(self,seed_data): + + self.parent = self.load_item(seed_data, 'parent') self.name = self.load_item(seed_data, 'name') self.profile = self.load_item(seed_data, 'profile') self.kernel_options = self.load_item(seed_data, 'kernel_options') @@ -82,7 +86,10 @@ class System(item.Item): """ Return object next highest up the tree. """ - return self.config.profiles().find(self.profile) + if self.parent is None or self.parent == '': + return self.config.profiles().find(self.profile) + else: + return self.config.systems().find(self.parent) def set_name(self,name): """ @@ -198,6 +205,8 @@ class System(item.Item): """ A system is valid when it contains a valid name and a profile. """ + # NOTE: this validation code does not support inheritable distros at this time. + # this is by design as inheritable systems don't make sense. if self.name is None: return False if self.profile is None: @@ -213,7 +222,8 @@ class System(item.Item): 'ip_address' : self.ip_address, 'netboot_enabled' : self.netboot_enabled, 'hostname' : self.hostname, - 'mac_address' : self.mac_address + 'mac_address' : self.mac_address, + 'parent' : self.parent } def printable(self): diff --git a/cobbler/serializer.py b/cobbler/serializer.py index a8a3e739..38a182d6 100644 --- a/cobbler/serializer.py +++ b/cobbler/serializer.py @@ -27,7 +27,6 @@ def serialize(obj): Will create intermediate paths if it can. Returns True on Success, False on permission errors. """ - # FIXME: DEBUG filename = obj.filename() try: fd = open(filename,"w+") diff --git a/cobbler/utils.py b/cobbler/utils.py index e30c10ce..5a83047c 100644 --- a/cobbler/utils.py +++ b/cobbler/utils.py @@ -255,8 +255,8 @@ def blender(remove_hashes, root_obj): consolidated data. """ settings = api.BootAPI().settings() - tree = grab_tree(root_obj) + tree.reverse() # start with top of tree, override going down results = {} for node in tree: __consolidate(node,results) @@ -281,15 +281,41 @@ def __consolidate(node,results): specially. """ node_data = node.to_datastruct() - for field in node_data: - data_item = node_data[field] + + # if the node has any data items labelled <<inherit>> we need to expunge them. + # so that they do not override the supernodes. + node_data_copy = {} + for key in node_data: + value = node_data[key] + if value != "<<inherit>>": + node_data_copy[key] = value + + for field in node_data_copy: + + data_item = node_data_copy[field] if results.has_key(field): + + # FIXME: remove, we're doing this higher up + # for subobjects (child objects), a value of <<inherit>> + # means defer up the stack, and by definition usage of the API + # must ensure the value is valid somewhere up the stack. So, remove + # any magic values of <<inherit>> prior to blending. + #if data_item == '<<inherit>>': + # # don't load into results hash, the parent will have the + # # data we need. + # continue + + # now merge data types seperately depending on whether they are hash, list, + # or scalar. if type(data_item) == dict: + # interweave hash results results[field].update(data_item) elif type(data_item) == list or type(data_item) == tuple: + # add to lists (cobbler doesn't have many lists) + # FIXME: should probably uniqueify list after doing this results[field].extend(data_item) else: - # override + # just override scalars results[field] = data_item else: results[field] = data_item |