From 036eb874ffbaf70236427ebe7c10fb883706e291 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Fri, 5 May 2006 15:41:27 -0400 Subject: Interim commit while refactoring. This breaks lots of stuff. --- cobbler/api.py | 548 ++------------------------------------------ cobbler/collection.py | 89 +++++++ cobbler/config.py | 209 ----------------- cobbler/distro.py | 76 ++++++ cobbler/distros.py | 26 +++ cobbler/item.py | 37 +++ cobbler/msg.py | 2 +- cobbler/profile.py | 158 +++++++++++++ cobbler/profiles.py | 28 +++ cobbler/serializer_tools.py | 218 ++++++++++++++++++ cobbler/sync.py | 81 +++---- cobbler/system.py | 62 +++++ cobbler/systems.py | 22 ++ cobbler/util.py | 275 +++++++++++----------- cobbler/~api.py | 0 15 files changed, 915 insertions(+), 916 deletions(-) create mode 100644 cobbler/collection.py delete mode 100644 cobbler/config.py create mode 100644 cobbler/distro.py create mode 100644 cobbler/distros.py create mode 100644 cobbler/item.py create mode 100644 cobbler/profile.py create mode 100644 cobbler/profiles.py create mode 100644 cobbler/serializer_tools.py create mode 100644 cobbler/system.py create mode 100644 cobbler/systems.py create mode 100644 cobbler/~api.py (limited to 'cobbler') diff --git a/cobbler/api.py b/cobbler/api.py index e48fe7a..0c0141f 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -22,77 +22,73 @@ class BootAPI: """ Constructor... """ - self.last_error = '' - self.config = config.BootConfig(self) - self.utils = util.BootUtil(self,self.config) # if the file already exists, load real data now try: - if self.config.files_exist(): - self.config.deserialize() + if config.files_exist(): + config.deserialize() except Exception, e: # parse errors, attempt to recover - print self.last_error - if self.last_error == m("parse_error"): - # the error came from /etc/cobbler.conf, and must be fixed - # manually, CLI can't do it for policy reasons on /etc + print runtime.last_error() + if runtime.last_error() == m("parse_error"): + # it's bad, raise it so we can croak raise Exception, "parse_error" try: - self.config.serialize() + config.serialize() except: - # shouldn't get here. File permissions issue, perhaps? + # it's bad, raise it so we can croak traceback.print_exc() raise Exception, "parse_error2" - if not self.config.files_exist(): - self.config.serialize() + if not config.files_exist(): + config.serialize() def clear(self): """ Forget about current list of profiles, distros, and systems """ - self.config.clear() + config.clear() def get_systems(self): """ Return the current list of systems """ - return self.config.get_systems() + return config.get_systems() def get_profiles(self): """ Return the current list of profiles """ - return self.config.get_profiles() + return config.get_profiles() def get_distros(self): """ Return the current list of distributions """ - return self.config.get_distros() + return config.get_distros() def new_system(self): """ Return a blank, unconfigured system, unattached to a collection """ - return System(self,None) + return system.System(self,None) def new_distro(self): """ Create a blank, unconfigured distro, unattached to a collection. """ - return Distro(self,None) + return distro.Distro(self,None) def new_profile(self): """ Create a blank, unconfigured profile, unattached to a collection """ - return Profile(self,None) + return profile.Profile(self,None) def check(self): @@ -104,7 +100,7 @@ class BootAPI: for human admins, who may, for instance, forget to properly set up their TFTP servers for PXE, etc. """ - return check.BootCheck(self).run() + return check.bootcheck().run() def sync(self,dry_run=True): @@ -114,521 +110,19 @@ class BootAPI: /tftpboot. Any operations done in the API that have not been saved with serialize() will NOT be synchronized with this command. """ - self.config.deserialize(); - configurator = sync.BootSync(self) - return configurator.sync(dry_run) + config.deserialize(); + return sync.bootsync(self).sync(dry_run) def serialize(self): """ Save the config file(s) to disk. """ - self.config.serialize() + config.serialize() def deserialize(self): """ Load the current configuration from config file(s) """ - self.config.deserialize() - -#----------------------------------------- - -""" -An Item is a serializable thing that can appear in a Collection -""" -class Item: - - - def set_name(self,name): - """ - All objects have names, and with the exception of System - they aren't picky about it. - """ - self.name = name - return True - - def set_kernel_options(self,options_string): - """ - Kernel options are a comma delimited list of key value pairs, - like 'a=b,c=d,e=f' - """ - self.kernel_options = options_string - return True - - def to_datastruct(self): - """ - Returns an easily-marshalable representation of the collection. - i.e. dictionaries/arrays/scalars. - """ - raise exceptions.NotImplementedError - - def is_valid(self): - """ - The individual set_ methods will return failure if any set is - rejected, but the is_valid method is intended to indicate whether - the object is well formed ... i.e. have all of the important - items been set, are they free of conflicts, etc. - """ - return False - -#------------------------------------------ - -class Distro(Item): - - def __init__(self,api,seed_data): - self.api = api - self.name = None - self.kernel = None - self.initrd = None - self.kernel_options = "" - if seed_data is not None: - self.name = seed_data['name'] - self.kernel = seed_data['kernel'] - self.initrd = seed_data['initrd'] - self.kernel_options = seed_data['kernel_options'] - - def set_kernel(self,kernel): - """ - Specifies a kernel. The kernel parameter is a full path, a filename - in the configured kernel directory (set in /etc/cobbler.conf) or a - directory path that would contain a selectable kernel. Kernel - naming conventions are checked, see docs in the utils module - for find_kernel. - """ - if self.api.utils.find_kernel(kernel): - self.kernel = kernel - return True - self.api.last_error = m("no_kernel") - return False - - def set_initrd(self,initrd): - """ - Specifies an initrd image. Path search works as in set_kernel. - File must be named appropriately. - """ - if self.api.utils.find_initrd(initrd): - self.initrd = initrd - return True - self.api.last_error = m("no_initrd") - return False - - def is_valid(self): - """ - A distro requires that the kernel and initrd be set. All - other variables are optional. - """ - for x in (self.name,self.kernel,self.initrd): - if x is None: return False - return True - - def to_datastruct(self): - return { - 'name': self.name, - 'kernel': self.kernel, - 'initrd' : self.initrd, - 'kernel_options' : self.kernel_options - } - - def printable(self): - """ - Human-readable representation. - """ - kstr = self.api.utils.find_kernel(self.kernel) - istr = self.api.utils.find_initrd(self.initrd) - if kstr is None: - kstr = "%s (NOT FOUND!)" % self.kernel - elif os.path.isdir(self.kernel): - kstr = "%s (FOUND BY SEARCH)" % kstr - if istr is None: - istr = "%s (NOT FOUND)" % self.initrd - elif os.path.isdir(self.initrd): - istr = "%s (FOUND BY SEARCH)" % istr - buf = "" - buf = buf + "distro : %s\n" % self.name - buf = buf + "kernel : %s\n" % kstr - buf = buf + "initrd : %s\n" % istr - buf = buf + "kernel opts : %s" % self.kernel_options - return buf - -#--------------------------------------------- - -class Profile(Item): - - def __init__(self,api,seed_data): - self.api = api - self.name = None - self.distro = None # a name, not a reference - self.kickstart = None - self.kernel_options = '' - self.xen_name = 'xenguest' - self.xen_file_size = 5 # GB - self.xen_ram = 2048 # MB - self.xen_mac = '' - self.xen_paravirt = True - if seed_data is not None: - self.name = seed_data['name'] - self.distro = seed_data['distro'] - self.kickstart = seed_data['kickstart'] - self.kernel_options = seed_data['kernel_options'] - self.xen_name = seed_data['xen_name'] - if not self.xen_name or self.xen_name == '': - self.xen_name = self.name - self.xen_ram = seed_data['xen_ram'] - self.xen_file_size = seed_data['xen_file_size'] - self.xen_mac = seed_data['xen_mac'] - self.xen_paravirt = seed_data['xen_paravirt'] - - def set_distro(self,distro_name): - """ - Sets the distro. This must be the name of an existing - Distro object in the Distros collection. - """ - if self.api.get_distros().find(distro_name): - self.distro = distro_name - return True - self.last_error = m("no_distro") - return False - - def set_kickstart(self,kickstart): - """ - Sets the kickstart. This must be a NFS, HTTP, or FTP URL. - Minor checking of the URL is performed here. - """ - if self.api.utils.find_kickstart(kickstart): - self.kickstart = kickstart - return True - self.last_error = m("no_kickstart") - return False - - def set_xen_name(self,str): - """ - For Xen only. - Specifies what xenguest install should use for --name. - xen-net-install may do conflict resolution, so this is mostly - a hint... To keep the shell happy, the 'str' cannot - contain wildcards or slashes and may be subject to some other - untainting later. - """ - # no slashes or wildcards - for bad in [ '/', '*', '?' ]: - if str.find(bad) != -1: - return False - self.xen_name = str - return True - - def set_xen_file_size(self,num): - """ - For Xen only. - Specifies the size of the Xen image in gigabytes. xen-net-install - may contain some logic to ignore 'illogical' values of this size, - though there are no guarantees. 0 tells xen-net-install to just - let it pick a semi-reasonable size. When in doubt, specify the - size you want. - """ - # num is a non-negative integer (0 means default) - try: - inum = int(num) - if inum != float(num): - return False - self.xen_file_size = inum - if inum >= 0: - return True - return False - except: - return False - - def set_xen_mac(self,mac): - """ - For Xen only. - Specifies the mac address (or possibly later, a range) that - xen-net-install should try to set on the domU. Seeing these - have a good chance of conflicting with other domU's, especially - on a network, this setting is fairly experimental at this time. - It's recommended that it *not* be used until we can get - decent use cases for how this might work. - """ - # mac needs to be in mac format AA:BB:CC:DD:EE:FF or a range - # ranges currently *not* supported, so we'll fail them - if self.api.utils.is_mac(mac): - self.xen_mac = mac - return True - else: - return False - - def set_xen_paravirt(self,truthiness): - """ - For Xen only. - Specifies whether the system is a paravirtualized system or not. - For ordinary computers, you want to pick 'true'. Method accepts string - 'true'/'false' in all cases, or Python True/False. - """ - # truthiness needs to be True or False, or (lcased) string equivalents - try: - if (truthiness == False or truthiness.lower() == 'false'): - self.xen_paravirt = False - elif (truthiness == True or truthiness.lower() == 'true'): - self.xen_paravirt = True - else: - return False - except: - return False - return True - - def is_valid(self): - """ - A profile only needs a name and a distro. Kickstart info, - as well as Xen 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 - return True - - def to_datastruct(self): - return { - 'name' : self.name, - 'distro' : self.distro, - 'kickstart' : self.kickstart, - 'kernel_options' : self.kernel_options, - 'xen_name' : self.xen_name, - 'xen_file_size' : self.xen_file_size, - 'xen_ram' : self.xen_ram, - 'xen_mac' : self.xen_mac, - 'xen_paravirt' : self.xen_paravirt - } - - def printable(self): - buf = "" - buf = buf + "profile : %s\n" % self.name - buf = buf + "distro : %s\n" % self.distro - buf = buf + "kickstart : %s\n" % self.kickstart - buf = buf + "kernel opts : %s" % self.kernel_options - buf = buf + "xen name : %s" % self.xen_name - buf = buf + "xen file size : %s" % self.xen_file_size - buf = buf + "xen ram : %s" % self.xen_ram - buf = buf + "xen mac : %s" % self.xen_mac - buf = buf + "xen paravirt : %s" % self.xen_paravirt - return buf - -#--------------------------------------------- - -class System(Item): - - def __init__(self,api,seed_data): - self.api = api - self.name = None - self.profile = None # a name, not a reference - self.kernel_options = "" - if seed_data is not None: - self.name = seed_data['name'] - self.profile = seed_data['profile'] - self.kernel_options = seed_data['kernel_options'] - - - def set_name(self,name): - """ - A name can be a resolvable hostname (it instantly resolved and replaced with the IP), - any legal ipv4 address, or any legal mac address. ipv6 is not supported yet but _should_ be. - See utils.py - """ - new_name = self.api.utils.find_system_identifier(name) - if new_name is None or new_name == False: - self.api.last_error = m("bad_sys_name") - return False - self.name = name # we check it add time, but store the original value. - return True - - def set_profile(self,profile_name): - """ - Set the system to use a certain named profile. The profile - must have already been loaded into the Profiles collection. - """ - if self.api.get_profiles().find(profile_name): - self.profile = profile_name - return True - return False - - def is_valid(self): - """ - A system is valid when it contains a valid name and a profile. - """ - if self.name is None: - self.api.last_error = m("bad_sys_name") - return False - if self.profile is None: - return False - return True - - def to_datastruct(self): - return { - 'name' : self.name, - 'profile' : self.profile, - 'kernel_options' : self.kernel_options - } - - def printable(self): - buf = "" - buf = buf + "system : %s\n" % self.name - buf = buf + "profile : %s\n" % self.profile - buf = buf + "kernel opts : %s" % self.kernel_options - return buf - -#-------------------------------------- - -""" -Base class for any serializable lists of things... -""" -class Collection: - _item_factory = None - - def __init__(self, api, seed_data): - """ - Constructor. Requires an API reference. seed_data - is a hash of data to feed into the collection, that would - come from the config file in /var. - """ - self.api = api - self.listing = {} - if seed_data is not None: - for x in seed_data: - self.add(self._item_factory(self.api, x)) - - def find(self,name): - """ - Return anything named 'name' in the collection, else return None if - no objects can be found. - """ - if name in self.listing.keys(): - return self.listing[name] - return None - - - def to_datastruct(self): - """ - Return datastructure representation of this collection suitable - for feeding to a serializer (such as YAML) - """ - return [x.to_datastruct() for x in self.listing.values()] - - - def add(self,ref): - """ - Add an object to the collection, if it's valid. Returns True - if the object was added to the collection. Returns False if the - object specified by ref deems itself invalid (and therefore - won't be added to the collection). - """ - if ref is None or not ref.is_valid(): - if self.api.last_error is None or self.api.last_error == "": - self.api.last_error = m("bad_param") - return False - self.listing[ref.name] = ref - return True - - - def printable(self): - """ - Creates a printable representation of the collection suitable - for reading by humans or parsing from scripts. Actually scripts - would be better off reading the YAML in the config files directly. - """ - values = map(lambda(a): a.printable(), sorted(self.listing.values())) - if len(values) > 0: - return "\n\n".join(values) - else: - return m("empty_list") - - #def contents(self): - # """ - # Access the raw contents of the collection. Classes shouldn't - # be doing this (preferably) and should use the __iter__ interface. - # Deprecrated. - # """ - # return self.listing.values() - - def __iter__(self): - """ - Iterator for the collection. Allows list comprehensions, etc - """ - for a in self.listing.values(): - yield a - - def __len__(self): - """ - Returns size of the collection - """ - return len(self.listing.values()) - - -#-------------------------------------------- - -""" -A distro represents a network bootable matched set of kernels -and initrd files -""" -class Distros(Collection): - _item_factory = Distro - - def remove(self,name): - """ - Remove element named 'name' from the collection - """ - # first see if any Groups use this distro - for k,v in self.api.get_profiles().listing.items(): - if v.distro == name: - self.api.last_error = m("orphan_profiles") - return False - if self.find(name): - del self.listing[name] - return True - self.api.last_error = m("delete_nothing") - return False - - -#-------------------------------------------- - -""" -A profile represents a distro paired with a kickstart file. -For instance, FC5 with a kickstart file specifying OpenOffice -might represent a 'desktop' profile. For Xen, there are many -additional options, with client-side defaults (not kept here). -""" -class Profiles(Collection): - _item_factory = Profile - - def remove(self,name): - """ - Remove element named 'name' from the collection - """ - for k,v in self.api.get_systems().listing.items(): - if v.profile == name: - self.api.last_error = m("orphan_system") - return False - if self.find(name): - del self.listing[name] - return True - self.api.last_error = m("delete_nothing") - return False - - -#-------------------------------------------- - -""" -Systems are hostnames/MACs/IP names and the associated profile -they belong to. -""" -class Systems(Collection): - _item_factory = System - - def remove(self,name): - """ - Remove element named 'name' from the collection - """ - if self.find(name): - del self.listing[name] - return True - self.api.last_error = m("delete_nothing") - return False - + config.deserialize() diff --git a/cobbler/collection.py b/cobbler/collection.py new file mode 100644 index 0000000..196bf7d --- /dev/null +++ b/cobbler/collection.py @@ -0,0 +1,89 @@ + +""" +Base class for any serializable lists of things... +""" +class Collection: + _item_factory = None + + def __init__(self): + """ + Constructor. Requires an API reference. seed_data + is a hash of data to feed into the collection, that would + come from the config file in /var. + """ + self.listing = {} + # no longer done at construct time, use from_datastruct + #if seed_data is not None: + # for x in seed_data: + # self.add(self._item_factory(self.api, x)) + + def find(self,name): + """ + Return anything named 'name' in the collection, else return None if + no objects can be found. + """ + if name in self.listing.keys(): + return self.listing[name] + return None + + + def to_datastruct(self): + """ + Return datastructure representation of this collection suitable + for feeding to a serializer (such as YAML) + """ + return [x.to_datastruct() for x in self.listing.values()] + + + def from_datastruct(self,datastruct): + for x in datastruct: + self._item_factory(x) + + def add(self,ref): + """ + Add an object to the collection, if it's valid. Returns True + if the object was added to the collection. Returns False if the + object specified by ref deems itself invalid (and therefore + won't be added to the collection). + """ + if ref is None or not ref.is_valid(): + if runtime.last_error() is None or runtime.last_error() == "": + runtime.set_error("bad_param") + return False + self.listing[ref.name] = ref + return True + + + def printable(self): + """ + Creates a printable representation of the collection suitable + for reading by humans or parsing from scripts. Actually scripts + would be better off reading the YAML in the config files directly. + """ + values = map(lambda(a): a.printable(), sorted(self.listing.values())) + if len(values) > 0: + return "\n\n".join(values) + else: + return m("empty_list") + + #def contents(self): + # """ + # Access the raw contents of the collection. Classes shouldn't + # be doing this (preferably) and should use the __iter__ interface. + # Deprecrated. + # """ + # return self.listing.values() + + def __iter__(self): + """ + Iterator for the collection. Allows list comprehensions, etc + """ + for a in self.listing.values(): + yield a + + def __len__(self): + """ + Returns size of the collection + """ + return len(self.listing.values()) + diff --git a/cobbler/config.py b/cobbler/config.py deleted file mode 100644 index ac08ca5..0000000 --- a/cobbler/config.py +++ /dev/null @@ -1,209 +0,0 @@ -# Abstracts out the config file format/access and holds -# reasonable default settings. -# -# Michael DeHaan - -import api -import util -from msg import * -import weakref -import syck # pysyck > 0.61, so it has dump() - -import os -import traceback - -global_settings_file = "/etc/cobbler.conf" -global_state_file = "/var/lib/cobbler/cobbler.conf" - - -class BootConfig: - - def __init__(self,api): - """ - Constructor. This class maintains both the logical - configuration for Cobbler and the file representation thereof. - Creating the config object only loads the default values, - users of this class need to call deserialize() to load config - file values. See cobbler.py for how the CLI does it. - """ - self.api = weakref.proxy(api) - self.settings_file = global_settings_file - self.state_file = global_state_file - self.set_defaults() - self.clear() - - def files_exist(self): - """ - Returns whether the config files exist. - """ - return os.path.exists(self.settings_file) and os.path.exists(self.state_file) - - def clear(self): - """ - Establish an empty list of profiles distros, and systems. - """ - self.profiles = api.Profiles(self.api,None) - self.distros = api.Distros(self.api,None) - self.systems = api.Systems(self.api,None) - - def set_defaults(self): - """ - Set some reasonable defaults in case no values are available - """ - self.server = "localhost" - self.tftpboot = "/tftpboot" - self.dhcpd_conf = "/etc/dhcpd.conf" - self.tftpd_conf = "/etc/xinetd.d/tftp" - self.pxelinux = "/usr/lib/syslinux/pxelinux.0" - self.tftpd_bin = "/usr/sbin/in.tftpd" - self.dhcpd_bin = "/usr/sbin/dhcpd" - self.httpd_bin = "/usr/sbin/httpd" - self.kernel_options = "append devfs=nomount ramdisk_size=16438 lang= vga=788 ksdevice=eth0" #initrd and ks added programmatically - - def get_profiles(self): - """ - Access the current profiles list - """ - return self.profiles - - def get_distros(self): - """ - Access the current distros list - """ - return self.distros - - def get_systems(self): - """ - Access the current systems list - """ - return self.systems - - def config_to_hash(self): - """ - Save all global config options in hash form (for serialization) - """ - data = {} - data["server"] = self.server - data['tftpboot'] = self.tftpboot - data['dhcpd_conf'] = self.dhcpd_conf - data['tftpd_conf'] = self.tftpd_conf - data['pxelinux'] = self.pxelinux - data['tftpd_bin'] = self.tftpd_bin - data['dhcpd_bin'] = self.dhcpd_bin - data['httpd_bin'] = self.httpd_bin - data['kernel_options'] = self.kernel_options - return data - - def config_from_hash(self,hash): - """ - Load all global config options from hash form (for deserialization) - """ - try: - self.server = hash['server'] - self.tftpboot = hash['tftpboot'] - self.dhcpd_conf = hash['dhcpd_conf'] - self.tftpd_conf = hash['tftpd_conf'] - self.pxelinux = hash['pxelinux'] - self.tftpd_bin = hash['tftpd_bin'] - self.dhcpd_bin = hash['dhcpd_bin'] - self.httpd_bin = hash['httpd_bin'] - self.kernel_options = hash['kernel_options'] - except: - print "WARNING: config file error: %s" % (self.settings_file) - self.set_defaults() - - def to_hash(self,is_etc): - """ - Convert all items cobbler knows about to a nested hash. - There are seperate hashes for the /etc and /var portions. - """ - world = {} - if is_etc: - world['config'] = self.config_to_hash() - else: - world['distros'] = self.get_distros().to_datastruct() - world['profiles'] = self.get_profiles().to_datastruct() - world['systems'] = self.get_systems().to_datastruct() - return world - - - def from_hash(self,hash,is_etc): - """ - Convert a hash representation of a cobbler to 'reality' - There are seperate hashes for the /etc and /var portions. - """ - if is_etc: - self.config_from_hash(hash['config']) - else: - self.distros = api.Distros(self.api, hash['distros']) - self.profiles = api.Profiles(self.api, hash['profiles']) - self.systems = api.Systems(self.api, hash['systems']) - - # ------------------------------------------------------ - # we don't care about file formats until below this line - - def serialize(self): - """ - Save everything to the config file. - This goes through an intermediate data format so we - could use YAML later if we wanted. - """ - - settings = None - state = None - - # ------ - # dump internal state (distros, profiles, systems...) into /var/lib/... - # /etc is not serialized, it's packaged. - - if not os.path.isdir(os.path.dirname(self.state_file)): - dirname = os.path.dirname(self.state_file) - if dirname != "": - os.makedirs(os.path.dirname(self.state_file)) - try: - state = open(self.state_file,"w+") - except: - self.api.last_error = m("cant_create: %s" % self.state_file) - return False - data = self.to_hash(False) - state.write(syck.dump(data)) - - # all good - return True - - def deserialize(self): - """ - Load everything from the config file. - This goes through an intermediate data structure format so we - could use YAML later if we wanted. - """ - - # ----- - # load global config (pathing, urls, etc)... - try: - settings = syck.load(open(self.settings_file,"r").read()) - if settings is not None: - self.from_hash(settings,True) - else: - self.last_error = m("parse_error") - raise Exception("parse_error") - except: - self.api.last_error = m("parse_error") - raise Exception("parse_error") - - # ----- - # load internal state(distros, systems, profiles...) - try: - state = syck.load(open(self.state_file,"r").read()) - if state is not None: - self.from_hash(state,False) - else: - self.last_error = m("parse_error2") - raise Exception("parse_error2") - except: - self.api.last_error = m("parse_error2") - raise Exception("parse_error2") - - # all good - return True - diff --git a/cobbler/distro.py b/cobbler/distro.py new file mode 100644 index 0000000..ddfed2b --- /dev/null +++ b/cobbler/distro.py @@ -0,0 +1,76 @@ +class Distro(Item): + + def __init__(self,seed_data): + self.name = None + self.kernel = None + self.initrd = None + self.kernel_options = "" + if seed_data is not None: + self.name = seed_data['name'] + self.kernel = seed_data['kernel'] + self.initrd = seed_data['initrd'] + self.kernel_options = seed_data['kernel_options'] + + def set_kernel(self,kernel): + """ + Specifies a kernel. The kernel parameter is a full path, a filename + in the configured kernel directory (set in /etc/cobbler.conf) or a + directory path that would contain a selectable kernel. Kernel + naming conventions are checked, see docs in the utils module + for find_kernel. + """ + if utils.find_kernel(kernel): + self.kernel = kernel + return True + runtime.set_error("no_kernel") + return False + + def set_initrd(self,initrd): + """ + Specifies an initrd image. Path search works as in set_kernel. + File must be named appropriately. + """ + if utils.find_initrd(initrd): + self.initrd = initrd + return True + runtime.set_error("no_initrd") + return False + + def is_valid(self): + """ + A distro requires that the kernel and initrd be set. All + other variables are optional. + """ + for x in (self.name,self.kernel,self.initrd): + if x is None: return False + return True + + def to_datastruct(self): + return { + 'name': self.name, + 'kernel': self.kernel, + 'initrd' : self.initrd, + 'kernel_options' : self.kernel_options + } + + def printable(self): + """ + Human-readable representation. + """ + kstr = utils.find_kernel(self.kernel) + istr = utils.find_initrd(self.initrd) + if kstr is None: + kstr = "%s (NOT FOUND!)" % self.kernel + elif os.path.isdir(self.kernel): + kstr = "%s (FOUND BY SEARCH)" % kstr + if istr is None: + istr = "%s (NOT FOUND)" % self.initrd + elif os.path.isdir(self.initrd): + istr = "%s (FOUND BY SEARCH)" % istr + buf = "" + buf = buf + "distro : %s\n" % self.name + buf = buf + "kernel : %s\n" % kstr + buf = buf + "initrd : %s\n" % istr + buf = buf + "kernel opts : %s" % self.kernel_options + return buf + diff --git a/cobbler/distros.py b/cobbler/distros.py new file mode 100644 index 0000000..a6b4b14 --- /dev/null +++ b/cobbler/distros.py @@ -0,0 +1,26 @@ +import distro +import runtime +import profiles + +""" +A distro represents a network bootable matched set of kernels +and initrd files +""" +class Distros(Collection): + _item_factory = distro.Distro + + def remove(self,name): + """ + Remove element named 'name' from the collection + """ + # first see if any Groups use this distro + for k,v in profile.profiles().listing.items(): + if v.distro == name: + runtime.set_error("orphan_files") + return False + if self.find(name): + del self.listing[name] + return True + runtime.set_error("delete_nothing") + return False + diff --git a/cobbler/item.py b/cobbler/item.py new file mode 100644 index 0000000..e30ada5 --- /dev/null +++ b/cobbler/item.py @@ -0,0 +1,37 @@ +""" +An Item is a serializable thing that can appear in a Collection +""" +class Item: + + + def set_name(self,name): + """ + All objects have names, and with the exception of System + they aren't picky about it. + """ + self.name = name + return True + + def set_kernel_options(self,options_string): + """ + Kernel options are a comma delimited list of key value pairs, + like 'a=b,c=d,e=f' + """ + self.kernel_options = options_string + return True + + def to_datastruct(self): + """ + Returns an easily-marshalable representation of the collection. + i.e. dictionaries/arrays/scalars. + """ + raise exceptions.NotImplementedError + + def is_valid(self): + """ + The individual set_ methods will return failure if any set is + rejected, but the is_valid method is intended to indicate whether + the object is well formed ... i.e. have all of the important + items been set, are they free of conflicts, etc. + """ + return False diff --git a/cobbler/msg.py b/cobbler/msg.py index 1eeb8e6..a9e1676 100644 --- a/cobbler/msg.py +++ b/cobbler/msg.py @@ -63,5 +63,5 @@ def m(key): # localization could use different tables or just gettext. return msg_table[key] else: - return "?%s?" % key + return key diff --git a/cobbler/profile.py b/cobbler/profile.py new file mode 100644 index 0000000..19db0e8 --- /dev/null +++ b/cobbler/profile.py @@ -0,0 +1,158 @@ +class Profile(Item): + + def __init__(self,api,seed_data): + self.api = api + self.name = None + self.distro = None # a name, not a reference + self.kickstart = None + self.kernel_options = '' + self.xen_name = 'xenguest' + self.xen_file_size = 5 # GB + self.xen_ram = 2048 # MB + self.xen_mac = '' + self.xen_paravirt = True + if seed_data is not None: + self.name = seed_data['name'] + self.distro = seed_data['distro'] + self.kickstart = seed_data['kickstart'] + self.kernel_options = seed_data['kernel_options'] + self.xen_name = seed_data['xen_name'] + if not self.xen_name or self.xen_name == '': + self.xen_name = self.name + self.xen_ram = seed_data['xen_ram'] + self.xen_file_size = seed_data['xen_file_size'] + self.xen_mac = seed_data['xen_mac'] + self.xen_paravirt = seed_data['xen_paravirt'] + + def set_distro(self,distro_name): + """ + Sets the distro. This must be the name of an existing + Distro object in the Distros collection. + """ + if self.api.get_distros().find(distro_name): + self.distro = distro_name + return True + self.last_error = m("no_distro") + return False + + def set_kickstart(self,kickstart): + """ + Sets the kickstart. This must be a NFS, HTTP, or FTP URL. + Minor checking of the URL is performed here. + """ + if self.api.utils.find_kickstart(kickstart): + self.kickstart = kickstart + return True + self.last_error = m("no_kickstart") + return False + + def set_xen_name(self,str): + """ + For Xen only. + Specifies what xenguest install should use for --name. + xen-net-install may do conflict resolution, so this is mostly + a hint... To keep the shell happy, the 'str' cannot + contain wildcards or slashes and may be subject to some other + untainting later. + """ + # no slashes or wildcards + for bad in [ '/', '*', '?' ]: + if str.find(bad) != -1: + return False + self.xen_name = str + return True + + def set_xen_file_size(self,num): + """ + For Xen only. + Specifies the size of the Xen image in gigabytes. xen-net-install + may contain some logic to ignore 'illogical' values of this size, + though there are no guarantees. 0 tells xen-net-install to just + let it pick a semi-reasonable size. When in doubt, specify the + size you want. + """ + # num is a non-negative integer (0 means default) + try: + inum = int(num) + if inum != float(num): + return False + self.xen_file_size = inum + if inum >= 0: + return True + return False + except: + return False + + def set_xen_mac(self,mac): + """ + For Xen only. + Specifies the mac address (or possibly later, a range) that + xen-net-install should try to set on the domU. Seeing these + have a good chance of conflicting with other domU's, especially + on a network, this setting is fairly experimental at this time. + It's recommended that it *not* be used until we can get + decent use cases for how this might work. + """ + # mac needs to be in mac format AA:BB:CC:DD:EE:FF or a range + # ranges currently *not* supported, so we'll fail them + if self.api.utils.is_mac(mac): + self.xen_mac = mac + return True + else: + return False + + def set_xen_paravirt(self,truthiness): + """ + For Xen only. + Specifies whether the system is a paravirtualized system or not. + For ordinary computers, you want to pick 'true'. Method accepts string + 'true'/'false' in all cases, or Python True/False. + """ + # truthiness needs to be True or False, or (lcased) string equivalents + try: + if (truthiness == False or truthiness.lower() == 'false'): + self.xen_paravirt = False + elif (truthiness == True or truthiness.lower() == 'true'): + self.xen_paravirt = True + else: + return False + except: + return False + return True + + def is_valid(self): + """ + A profile only needs a name and a distro. Kickstart info, + as well as Xen 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 + return True + + def to_datastruct(self): + return { + 'name' : self.name, + 'distro' : self.distro, + 'kickstart' : self.kickstart, + 'kernel_options' : self.kernel_options, + 'xen_name' : self.xen_name, + 'xen_file_size' : self.xen_file_size, + 'xen_ram' : self.xen_ram, + 'xen_mac' : self.xen_mac, + 'xen_paravirt' : self.xen_paravirt + } + + def printable(self): + buf = "" + buf = buf + "profile : %s\n" % self.name + buf = buf + "distro : %s\n" % self.distro + buf = buf + "kickstart : %s\n" % self.kickstart + buf = buf + "kernel opts : %s" % self.kernel_options + buf = buf + "xen name : %s" % self.xen_name + buf = buf + "xen file size : %s" % self.xen_file_size + buf = buf + "xen ram : %s" % self.xen_ram + buf = buf + "xen mac : %s" % self.xen_mac + buf = buf + "xen paravirt : %s" % self.xen_paravirt + return buf diff --git a/cobbler/profiles.py b/cobbler/profiles.py new file mode 100644 index 0000000..c73e8c3 --- /dev/null +++ b/cobbler/profiles.py @@ -0,0 +1,28 @@ +import profile +import runtime + +#-------------------------------------------- + +""" +A profile represents a distro paired with a kickstart file. +For instance, FC5 with a kickstart file specifying OpenOffice +might represent a 'desktop' profile. For Xen, there are many +additional options, with client-side defaults (not kept here). +""" +class Profiles(Collection): + _item_factory = profile.Profile + + def remove(self,name): + """ + Remove element named 'name' from the collection + """ + for k,v in self.api.get_systems().listing.items(): + if v.profile == name: + runtime.set_error("orphan_system") + return False + if self.find(name): + del self.listing[name] + return True + runtime.set_error("delete_nothing") + return False + diff --git a/cobbler/serializer_tools.py b/cobbler/serializer_tools.py new file mode 100644 index 0000000..7c32ce5 --- /dev/null +++ b/cobbler/serializer_tools.py @@ -0,0 +1,218 @@ +# Abstracts out the config file format/access and holds +# reasonable default settings. +# +# Michael DeHaan + +import api +import util +from msg import * +import distros +import profiles +import systems + +import weakref +import syck # pysyck > 0.61, so it has dump() +import os +import traceback + +global_settings_file = "/etc/cobbler.conf" +global_state_file = "/var/lib/cobbler/cobbler.conf" + + +class BootConfig: + + __instance + + def __new__(typ, *args, **kwargs): + if __instance is None: + __instance = object.__new__(type,*args,**kwargs) + return __instance + + def __init__(self): + """ + Constructor. This class maintains both the logical + configuration for Cobbler and the file representation thereof. + Creating the config object only loads the default values, + users of this class need to call deserialize() to load config + file values. See cobbler.py for how the CLI does it. + """ + self.settings_file = global_settings_file + self.state_file = global_state_file + self.set_defaults() + self.clear() + + def files_exist(self): + """ + Returns whether the config files exist. + """ + return os.path.exists(self.settings_file) and os.path.exists(self.state_file) + + def clear(self): + """ + Establish an empty list of profiles distros, and systems. + """ + profiles.clear() + distros.clear() + systems.clear() + + def set_defaults(self): + """ + Set some reasonable defaults in case no values are available + """ + self.server = "localhost" + self.tftpboot = "/tftpboot" + self.dhcpd_conf = "/etc/dhcpd.conf" + self.tftpd_conf = "/etc/xinetd.d/tftp" + self.pxelinux = "/usr/lib/syslinux/pxelinux.0" + self.tftpd_bin = "/usr/sbin/in.tftpd" + self.dhcpd_bin = "/usr/sbin/dhcpd" + self.httpd_bin = "/usr/sbin/httpd" + self.kernel_options = "append devfs=nomount ramdisk_size=16438 lang= vga=788 ksdevice=eth0" #initrd and ks added programmatically + + def get_profiles(self): + """ + Access the current profiles list + """ + return self.profiles + + def get_distros(self): + """ + Access the current distros list + """ + return self.distros + + def get_systems(self): + """ + Access the current systems list + """ + return self.systems + + def config_to_hash(self): + """ + Save all global config options in hash form (for serialization) + """ + data = {} + data["server"] = self.server + data['tftpboot'] = self.tftpboot + data['dhcpd_conf'] = self.dhcpd_conf + data['tftpd_conf'] = self.tftpd_conf + data['pxelinux'] = self.pxelinux + data['tftpd_bin'] = self.tftpd_bin + data['dhcpd_bin'] = self.dhcpd_bin + data['httpd_bin'] = self.httpd_bin + data['kernel_options'] = self.kernel_options + return data + + def config_from_hash(self,hash): + """ + Load all global config options from hash form (for deserialization) + """ + try: + self.server = hash['server'] + self.tftpboot = hash['tftpboot'] + self.dhcpd_conf = hash['dhcpd_conf'] + self.tftpd_conf = hash['tftpd_conf'] + self.pxelinux = hash['pxelinux'] + self.tftpd_bin = hash['tftpd_bin'] + self.dhcpd_bin = hash['dhcpd_bin'] + self.httpd_bin = hash['httpd_bin'] + self.kernel_options = hash['kernel_options'] + except: + print "WARNING: config file error: %s" % (self.settings_file) + self.set_defaults() + + def to_hash(self,is_etc): + """ + Convert all items cobbler knows about to a nested hash. + There are seperate hashes for the /etc and /var portions. + """ + world = {} + if is_etc: + world['config'] = self.config_to_hash() + else: + world['distros'] = self.get_distros().to_datastruct() + world['profiles'] = self.get_profiles().to_datastruct() + world['systems'] = self.get_systems().to_datastruct() + return world + + + def from_hash(self,hash,is_etc): + """ + Convert a hash representation of a cobbler to 'reality' + There are seperate hashes for the /etc and /var portions. + """ + if is_etc: + self.config_from_hash(hash['config']) + else: + distros.from_datastruct(hash['distros']) + profiles.from_datastruct(hash['profiles']) + systems.from_datastruct(hash['systems']) + + # ------------------------------------------------------ + # we don't care about file formats until below this line + + def serialize(self): + """ + Save everything to the config file. + This goes through an intermediate data format so we + could use YAML later if we wanted. + """ + + settings = None + state = None + + # ------ + # dump internal state (distros, profiles, systems...) into /var/lib/... + # /etc is not serialized, it's packaged. + + if not os.path.isdir(os.path.dirname(self.state_file)): + dirname = os.path.dirname(self.state_file) + if dirname != "": + os.makedirs(os.path.dirname(self.state_file)) + try: + state = open(self.state_file,"w+") + except: + runtime.set_error(m("cant_create: %s" % self.state_file)) + return False + data = self.to_hash(False) + state.write(syck.dump(data)) + + # all good + return True + + def deserialize(self): + """ + Load everything from the config file. + This goes through an intermediate data structure format so we + could use YAML later if we wanted. + """ + + # ----- + # load global config (pathing, urls, etc)... + try: + settings = syck.load(open(self.settings_file,"r").read()) + if settings is not None: + self.from_hash(settings,True) + else: + self.last_error = m("parse_error") + raise Exception("parse_error") + except: + runtime.set_error("parse_error") + raise Exception("parse_error") + + # ----- + # load internal state(distros, systems, profiles...) + try: + state = syck.load(open(self.state_file,"r").read()) + if state is not None: + self.from_hash(state,False) + else: + self.last_error = m("parse_error2") + raise Exception("parse_error2") + except: + runtime.set_error("parse_error2") + raise Exception("parse_error2") + + # all good + return True + diff --git a/cobbler/sync.py b/cobbler/sync.py index f555b4f..31ab481 100644 --- a/cobbler/sync.py +++ b/cobbler/sync.py @@ -21,8 +21,7 @@ Handles conversion of internal state to the tftpboot tree layout class BootSync: - def __init__(self,api): - self.api = weakref.proxy(api) + def __init__(self): self.verbose = True @@ -49,7 +48,7 @@ class BootSync: """ Copy syslinux to the configured tftpboot directory """ - self.copy(self.api.config.pxelinux, os.path.join(self.api.config.tftpboot, "pxelinux.0")) + self.copy(config.config().pxelinux, os.path.join(config.config().tftpboot, "pxelinux.0")) def configure_httpd(self): """ @@ -72,7 +71,7 @@ class BootSync: Allow from all """ - config.replace("/tftpboot",self.api.config.tftpboot) + config.replace("/tftpboot",config.config().tftpboot) self.tee(f, config) self.close_file(f) @@ -81,7 +80,7 @@ class BootSync: Delete any previously built pxelinux.cfg tree and xen tree info. """ for x in ["pxelinux.cfg","images","systems","distros","profiles","kickstarts"]: - path = os.path.join(self.api.config.tftpboot,x) + path = os.path.join(config.config().tftpboot,x) self.rmtree(path, True) self.mkdir(path) @@ -94,19 +93,19 @@ class BootSync: mounted. """ # copy is a 4-letter word but tftpboot runs chroot, thus it's required. - distros = os.path.join(self.api.config.tftpboot, "images") - for d in self.api.get_distros(): + distros = os.path.join(config.config().tftpboot, "images") + for d in distros.get_distros(): distro_dir = os.path.join(distros,d.name) self.mkdir(distro_dir) - kernel = self.api.utils.find_kernel(d.kernel) # full path - initrd = self.api.utils.find_initrd(d.initrd) # full path + kernel = utils.find_kernel(d.kernel) # full path + initrd = utils.find_initrd(d.initrd) # full path if kernel is None or not os.path.isfile(kernel): - self.api.last_error = "Kernel for distro (%s) cannot be found and needs to be fixed: %s" % (d.name, d.kernel) - print self.api.last_error - raise "error" + runtime.set_error("Kernel for distro (%s) cannot be found and needs to be fixed: %s" % (d.name, d.kernel)) + print runtime.last_error() + raise Exception("error") if initrd is None or not os.path.isfile(initrd): - self.api.last_error = "Initrd for distro (%s) cannot be found and needs to be fixed: %s" % (d.name, d.initrd) - raise "error" + runtime.last_error("Initrd for distro (%s) cannot be found and needs to be fixed: %s" % (d.name, d.initrd)) + raise Exception("error") b_kernel = os.path.basename(kernel) b_initrd = os.path.basename(initrd) self.copyfile(kernel, os.path.join(distro_dir, b_kernel)) @@ -124,18 +123,18 @@ class BootSync: # these are served by either NFS, Apache, or some ftpd, so we don't need to copy them # it's up to the user to make sure they are nicely served by their URLs - for g in self.api.get_profiles(): + for g in profiles.get_profiles(): self.sync_log("mirroring any local kickstarts: %s" % g.name) - kickstart_path = self.api.utils.find_kickstart(g.kickstart) + kickstart_path = utils.find_kickstart(g.kickstart) if kickstart_path and os.path.exists(kickstart_path): # the input is an *actual* file, hence we have to copy it - copy_path = os.path.join(self.api.config.tftpboot, "kickstarts", g.name) + copy_path = os.path.join(config.config().tftpboot, "kickstarts", g.name) self.mkdir(copy_path) dest = os.path.join(copy_path, "ks.cfg") try: self.copyfile(g.kickstart, dest) except: - self.api.last_error = m("err_kickstart2") % (g.kickstart,dest) + runtime.set_error("err_kickstart2") raise "error" def build_trees(self): @@ -148,16 +147,16 @@ class BootSync: print "building trees..." # create pxelinux.cfg under tftpboot # and file for each MAC or IP (hex encoded 01-XX-XX-XX-XX-XX-XX) - systems = self.api.get_systems() - profiles = self.api.get_profiles() - distros = self.api.get_distros() + systems = systems.get_systems() + profiles = profiles.get_profiles() + distros = distros.get_distros() for d in distros: self.sync_log("processing distro: %s" % d.name) # TODO: add check to ensure all distros have profiles (=warning) - filename = os.path.join(self.api.config.tftpboot,"distros",d.name) + filename = os.path.join(config.config().tftpboot,"distros",d.name) d.kernel_options = self.blend_kernel_options(( - self.api.config.kernel_options, + config.config().kernel_options, d.kernel_options )) self.write_distro_file(filename,d) @@ -166,11 +165,11 @@ class BootSync: self.sync_log("processing profile: %s" % p.name) # TODO: add check to ensure all profiles have distros (=error) # TODO: add check to ensure all profiles have systems (=warning) - filename = os.path.join(self.api.config.tftpboot,"profiles",p.name) - distro = self.api.get_distros().find(p.distro) + filename = os.path.join(config.config().tftpboot,"profiles",p.name) + distro = distros.get_distros().find(p.distro) if distro is not None: p.kernel_options = self.blend_kernel_options(( - self.api.config.kernel_options, + config.config().kernel_options, distro.kernel_options, p.kernel_options )) @@ -180,15 +179,15 @@ class BootSync: self.sync_log("processing system: %s" % system.name) profile = profiles.find(system.profile) if profile is None: - self.api.last_error = m("orphan_profile2") + runtime.set_error("orphan_profile2") raise "error" distro = distros.find(profile.distro) if distro is None: - self.api.last_error = m("orphan_system2") + runtime.set_error("orphan_system2") raise "error" f1 = self.get_pxelinux_filename(system.name) - f2 = os.path.join(self.api.config.tftpboot, "pxelinux.cfg", f1) - f3 = os.path.join(self.api.config.tftpboot, "systems", f1) + f2 = os.path.join(config.config().tftpboot, "pxelinux.cfg", f1) + f3 = os.path.join(config.config().tftpboot, "systems", f1) self.write_pxelinux_file(f2,system,profile,distro) self.write_system_file(f3,system) @@ -201,13 +200,13 @@ class BootSync: is either a system name, an IP, or the MAC, so figure it out, resolve the host if needed, and return the pxelinux directory name. """ - name = self.api.utils.find_system_identifier(name_input) - if self.api.utils.is_ip(name): + name = utils.find_system_identifier(name_input) + if utils.is_ip(name): return IPy.IP(name).strHex()[2:] - elif self.api.utils.is_mac(name): + elif utils.is_mac(name): return "01-" + "-".join(name.split(":")).lower() else: - self.api.last_error = m("err_resolv") % name + runtime.set_error(m("err_resolv" % name) raise "error" @@ -229,7 +228,7 @@ class BootSync: self.tee(fd,"label linux\n") self.tee(fd," kernel %s\n" % kernel_path) kopts = self.blend_kernel_options(( - self.api.config.kernel_options, + config.config().kernel_options, profile.kernel_options, distro.kernel_options, system.kernel_options @@ -239,7 +238,7 @@ class BootSync: # if kickstart path is local, we've already copied it into # the HTTP mirror, so make it something anaconda can get at if kickstart_path.startswith("/"): - kickstart_path = "http://%s/cobbler/kickstarts/%s/ks.cfg" % (self.api.config.server, profile.name) + kickstart_path = "http://%s/cobbler/kickstarts/%s/ks.cfg" % (config.config().server, profile.name) nextline = nextline + " ks=%s" % kickstart_path self.tee(fd, nextline) self.close_file(fd) @@ -252,8 +251,8 @@ class BootSync: """ fd = self.open_file(filename,"w+") # resolve to current values - distro.kernel = self.api.utils.find_kernel(distro.kernel) - distro.initrd = self.api.utils.find_initrd(distro.initrd) + distro.kernel = utils.find_kernel(distro.kernel) + distro.initrd = utils.find_initrd(distro.initrd) self.tee(fd,syck.dump(distro.to_datastruct())) self.close_file(fd) @@ -266,7 +265,7 @@ class BootSync: # if kickstart path is local, we've already copied it into # the HTTP mirror, so make it something anaconda can get at if profile.kickstart and profile.kickstart.startswith("/"): - profile.kickstart = "http://%s/cobbler/kickstarts/%s/ks.cfg" % (self.api.config.server, profile.name) + profile.kickstart = "http://%s/cobbler/kickstarts/%s/ks.cfg" % (config.config().server, profile.name) self.tee(fd,syck.dump(profile.to_datastruct())) self.close_file(fd) @@ -386,4 +385,8 @@ class BootSync: # end result is a new fragment of a kernel options string return " ".join(results) +__instance = BootSync() + +def bootsync() + return __instance diff --git a/cobbler/system.py b/cobbler/system.py new file mode 100644 index 0000000..dc1a2f7 --- /dev/null +++ b/cobbler/system.py @@ -0,0 +1,62 @@ + +class System(Item): + + def __init__(self,api,seed_data): + self.api = api + self.name = None + self.profile = None # a name, not a reference + self.kernel_options = "" + if seed_data is not None: + self.name = seed_data['name'] + self.profile = seed_data['profile'] + self.kernel_options = seed_data['kernel_options'] + + + def set_name(self,name): + """ + A name can be a resolvable hostname (it instantly resolved and replaced with the IP), + any legal ipv4 address, or any legal mac address. ipv6 is not supported yet but _should_ be. + See utils.py + """ + new_name = self.api.utils.find_system_identifier(name) + if new_name is None or new_name == False: + self.api.last_error = m("bad_sys_name") + return False + self.name = name # we check it add time, but store the original value. + return True + + def set_profile(self,profile_name): + """ + Set the system to use a certain named profile. The profile + must have already been loaded into the Profiles collection. + """ + if self.api.get_profiles().find(profile_name): + self.profile = profile_name + return True + return False + + def is_valid(self): + """ + A system is valid when it contains a valid name and a profile. + """ + if self.name is None: + self.api.last_error = m("bad_sys_name") + return False + if self.profile is None: + return False + return True + + def to_datastruct(self): + return { + 'name' : self.name, + 'profile' : self.profile, + 'kernel_options' : self.kernel_options + } + + def printable(self): + buf = "" + buf = buf + "system : %s\n" % self.name + buf = buf + "profile : %s\n" % self.profile + buf = buf + "kernel opts : %s" % self.kernel_options + return buf + diff --git a/cobbler/systems.py b/cobbler/systems.py new file mode 100644 index 0000000..47dc042 --- /dev/null +++ b/cobbler/systems.py @@ -0,0 +1,22 @@ +import system +import runtime + +#-------------------------------------------- + +""" +Systems are hostnames/MACs/IP names and the associated profile +they belong to. +""" +class Systems(Collection): + _item_factory = system.System + + def remove(self,name): + """ + Remove element named 'name' from the collection + """ + if self.find(name): + del self.listing[name] + return True + runtime.set_error("delete_nothing") + return False + diff --git a/cobbler/util.py b/cobbler/util.py index 3a4d852..ed18869 100644 --- a/cobbler/util.py +++ b/cobbler/util.py @@ -10,148 +10,143 @@ import socket import glob import weakref -class BootUtil: - - def __init__(self,api,config): - # BootAPI is the preferred holder of all references - self.api = weakref.proxy(api) - self.config = weakref.proxy(config) - self.re_kernel = re.compile(r'vmlinuz-(\d+)\.(\d+)\.(\d+)-(.*)') - self.re_initrd = re.compile(r'initrd-(\d+)\.(\d+)\.(\d+)-(.*).img') - - - def find_system_identifier(self,strdata): - """ - If the input is a MAC or an IP, return that. - If it's not, resolve the hostname and return the IP. - pxelinux doesn't work in hostnames - """ - if self.is_mac(strdata): - return strdata - if self.is_ip(strdata): - return strdata - return self.resolve_ip(strdata) - - - def is_ip(self,strdata): - """ - Return whether the argument is an IP address. ipv6 needs - to be added... - """ - # needs testcase - if re.search(r'\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}',strdata): - return True - return False - - - def is_mac(self,strdata): - """ - Return whether the argument is a mac address. - """ - # needs testcase - if re.search(r'[A-F0-9]{2}:[A-F0-9]{2}:[A-F0-9]{2}:[A-F0-9]{2}:[A-F:0-9]{2}:[A-F:0-9]{2}',strdata): - return True - return False - - - def resolve_ip(self,strdata): - """ - Resolve the IP address and handle errors... - """ - try: - return socket.gethostbyname(strdata) - except: - return None - - - def find_matching_files(self,directory,regex): - """ - Find all files in a given directory that match a given regex. - Can't use glob directly as glob doesn't take regexen. - """ - files = glob.glob(os.path.join(directory,"*")) - results = [] - for f in files: - if regex.match(os.path.basename(f)): - results.append(f) - return results - - - def find_highest_files(self,directory,unversioned,regex): - """ - Find the highest numbered file (kernel or initrd numbering scheme) - in a given directory that matches a given pattern. Used for - auto-booting the latest kernel in a directory. - """ - files = self.find_matching_files(directory, regex) - get_numbers = re.compile(r'(\d+).(\d+).(\d+)') - def max2(a, b): - """Returns the larger of the two values""" - av = get_numbers.search(os.path.basename(a)).groups() - bv = get_numbers.search(os.path.basename(b)).groups() - - ret = cmp(av[0], bv[0]) or cmp(av[1], bv[1]) or cmp(av[2], bv[2]) - if ret < 0: - return b - return a - - if len(files) > 0: - return reduce(max2, files) - - # couldn't find a highest numbered file, but maybe there - # is just a 'vmlinuz' or an 'initrd.img' in this directory? - last_chance = os.path.join(directory,unversioned) - if os.path.exists(last_chance): - return last_chance +re_kernel = re.compile(r'vmlinuz-(\d+)\.(\d+)\.(\d+)-(.*)') +re_initrd = re.compile(r'initrd-(\d+)\.(\d+)\.(\d+)-(.*).img') + + +def find_system_identifier(self,strdata): + """ + If the input is a MAC or an IP, return that. + If it's not, resolve the hostname and return the IP. + pxelinux doesn't work in hostnames + """ + if self.is_mac(strdata): + return strdata + if self.is_ip(strdata): + return strdata + return self.resolve_ip(strdata) + + +def is_ip(self,strdata): + """ + Return whether the argument is an IP address. ipv6 needs + to be added... + """ + # needs testcase + if re.search(r'\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}',strdata): + return True + return False + + +def is_mac(self,strdata): + """ + Return whether the argument is a mac address. + """ + # needs testcase + if re.search(r'[A-F0-9]{2}:[A-F0-9]{2}:[A-F0-9]{2}:[A-F0-9]{2}:[A-F:0-9]{2}:[A-F:0-9]{2}',strdata): + return True + return False + + +def resolve_ip(self,strdata): + """ + Resolve the IP address and handle errors... + """ + try: + return socket.gethostbyname(strdata) + except: + return None + + +def find_matching_files(self,directory,regex): + """ + Find all files in a given directory that match a given regex. + Can't use glob directly as glob doesn't take regexen. + """ + files = glob.glob(os.path.join(directory,"*")) + results = [] + for f in files: + if regex.match(os.path.basename(f)): + results.append(f) + return results + + +def find_highest_files(self,directory,unversioned,regex): + """ + Find the highest numbered file (kernel or initrd numbering scheme) + in a given directory that matches a given pattern. Used for + auto-booting the latest kernel in a directory. + """ + files = self.find_matching_files(directory, regex) + get_numbers = re.compile(r'(\d+).(\d+).(\d+)') + def max2(a, b): + """Returns the larger of the two values""" + av = get_numbers.search(os.path.basename(a)).groups() + bv = get_numbers.search(os.path.basename(b)).groups() + + ret = cmp(av[0], bv[0]) or cmp(av[1], bv[1]) or cmp(av[2], bv[2]) + if ret < 0: + return b + return a + + if len(files) > 0: + return reduce(max2, files) + + # couldn't find a highest numbered file, but maybe there + # is just a 'vmlinuz' or an 'initrd.img' in this directory? + last_chance = os.path.join(directory,unversioned) + if os.path.exists(last_chance): + return last_chance + return None + + +def find_kernel(self,path): + """ + Given a directory or a filename, find if the path can be made + to resolve into a kernel, and return that full path if possible. + """ + if os.path.isfile(path): + filename = os.path.basename(path) + if self.re_kernel.match(filename): + return path + elif filename == "vmlinuz": + return path + elif os.path.isdir(path): + return self.find_highest_files(path,"vmlinuz",self.re_kernel) return None - def find_kernel(self,path): - """ - Given a directory or a filename, find if the path can be made - to resolve into a kernel, and return that full path if possible. - """ - if os.path.isfile(path): - filename = os.path.basename(path) - if self.re_kernel.match(filename): - return path - if filename == "vmlinuz": - return path - elif os.path.isdir(path): - return self.find_highest_files(path,"vmlinuz",self.re_kernel) - return None - - - def find_initrd(self,path): - """ - Given a directory or a filename, see if the path can be made - to resolve into an intird, return that full path if possible. - """ - # FUTURE: add another function to see if kernel and initrd have matched numbers (and throw a warning?) - if os.path.isfile(path): - filename = os.path.basename(path) - if self.re_initrd.match(filename): - return path - if filename == "initrd.img" or filename == "initrd": - return path - elif os.path.isdir(path): - return self.find_highest_files(path,"initrd.img",self.re_initrd) - return None - +def find_initrd(self,path): + """ + Given a directory or a filename, see if the path can be made + to resolve into an intird, return that full path if possible. + """ + # FUTURE: try to match kernel/initrd pairs? + if os.path.isfile(path): + filename = os.path.basename(path) + if self.re_initrd.match(filename): + return path + if filename == "initrd.img" or filename == "initrd": + return path + elif os.path.isdir(path): + return self.find_highest_files(path,"initrd.img",self.re_initrd) + return None + + +def find_kickstart(self,url): + """ + Check if a kickstart url looks like an http, ftp, nfs or local path. + If a local path is used, cobbler will copy the kickstart and serve + it over http. + """ + if url is None: + return None + x = url.lower() + for y in ["http://","nfs://","ftp://","/"]: + if x.startswith(y): + if x.startswith("/") and not os.path.isfile(x): + return None + return url + return None - def find_kickstart(self,url): - """ - Check if a kickstart url looks like an http, ftp, nfs or local path. - If a local path is used, cobbler will copy the kickstart and serve - it over http. - """ - if url is None: - return None - x = url.lower() - for y in ["http://","nfs://","ftp://","/"]: - if x.startswith(y): - if x.startswith("/") and not os.path.isfile(x): - return None - return url - return None diff --git a/cobbler/~api.py b/cobbler/~api.py new file mode 100644 index 0000000..e69de29 -- cgit