diff options
-rw-r--r-- | api.py | 262 | ||||
-rwxr-xr-x | bootconf | 154 | ||||
-rw-r--r-- | check.py | 52 | ||||
-rw-r--r-- | config.py | 4 | ||||
-rw-r--r-- | msg.py | 14 | ||||
-rw-r--r-- | sync.py | 262 | ||||
-rw-r--r-- | util.py | 79 |
7 files changed, 531 insertions, 296 deletions
@@ -13,10 +13,11 @@ from msg import * class BootAPI: - """ - Constructor... - """ + def __init__(self): + """ + Constructor... + """ self.last_error = '' self.config = config.BootConfig(self) self.utils = util.BootUtil(self,self.config) @@ -35,72 +36,82 @@ class BootAPI: if not self.config.files_exist(): self.config.serialize() - """ - Forget about current list of profiles, distros, and systems - """ + def clear(self): + """ + Forget about current list of profiles, distros, and systems + """ self.config.clear() - """ - Return the current list of systems - """ + def get_systems(self): + """ + Return the current list of systems + """ return self.config.get_systems() - """ - Return the current list of profiles - """ + def get_profiles(self): + """ + Return the current list of profiles + """ return self.config.get_profiles() - """ - Return the current list of distributions - """ + def get_distros(self): + """ + Return the current list of distributions + """ return self.config.get_distros() - """ - Create a blank, unconfigured system - """ + def new_system(self): + """ + Create a blank, unconfigured system + """ return System(self,None) - """ - Create a blank, unconfigured distro - """ + def new_distro(self): + """ + Create a blank, unconfigured distro + """ return Distro(self,None) - """ - Create a blank, unconfigured profile - """ + def new_profile(self): + """ + Create a blank, unconfigured profile + """ return Profile(self,None) - """ - See if all preqs for network booting are operational - """ + def check(self): + """ + See if all preqs for network booting are operational + """ return check.BootCheck(self).run() - """ - Update the system with what is specified in the config file - """ + def sync(self,dry_run=True): + """ + Update the system with what is specified in the config file + """ self.config.deserialize(); configurator = sync.BootSync(self) configurator.sync(dry_run) - """ - Save the config file - """ + def serialize(self): + """ + Save the config file + """ self.config.serialize() - """ - Make the API's internal state reflect that of the config file - """ def deserialize(self): + """ + Make the API's internal state reflect that of the config file + """ self.config.deserialize() #-------------------------------------- @@ -110,25 +121,27 @@ Base class for any serializable lists of things... """ class Collection: - """ - Return anything named 'name' in the collection, else return None - """ + def find(self,name): + """ + Return anything named 'name' in the collection, else return None + """ if name in self.listing.keys(): return self.listing[name] return None - """ - Return datastructure representation (to feed to serializer) - """ + def to_datastruct(self): + """ + Return datastructure representation (to feed to serializer) + """ return [x.to_datastruct() for x in self.listing.values()] - """ - Add an object to the collection, if it's valid - """ def add(self,ref): + """ + Add an object to the collection, if it's valid + """ 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") @@ -136,10 +149,11 @@ class Collection: self.listing[ref.name] = ref return True - """ - Printable representation - """ + def __str__(self): + """ + Printable representation + """ buf = "" values = map(lambda(a): str(a), sorted(self.listing.values())) if len(values) > 0: @@ -148,8 +162,26 @@ class Collection: 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 + """ 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()) + + #-------------------------------------------- """ @@ -159,15 +191,21 @@ and initrd files class Distros(Collection): 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(Distro(self.api,x)) - """ - Remove element named 'name' from the collection - """ + 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: @@ -196,10 +234,11 @@ class Profiles(Collection): if seed_data is not None: for x in seed_data: self.add(Profile(self.api,x)) - """ - Remove element named 'name' from the collection - """ + 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") @@ -225,10 +264,11 @@ class Systems(Collection): if seed_data is not None: for x in seed_data: self.add(System(self.api,x)) - """ - Remove element named 'name' from the collection - """ + def remove(self,name): + """ + Remove element named 'name' from the collection + """ if self.find(name): del self.listing[name] return True @@ -243,22 +283,37 @@ An Item is a serializable thing that can appear in a Collection """ class Item: - """ - All objects have names, and with the exception of System - they aren't picky about it. - """ + 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 "not implemented" 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 #------------------------------------------ @@ -278,6 +333,13 @@ class Distro(Item): 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/bootconf.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 @@ -285,6 +347,10 @@ class Distro(Item): 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 @@ -292,6 +358,10 @@ class Distro(Item): 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 @@ -305,6 +375,9 @@ class Distro(Item): } def __str__(self): + """ + Human-readable representation. + """ kstr = self.api.utils.find_kernel(self.kernel) istr = self.api.utils.find_initrd(self.initrd) if kstr is None: @@ -350,6 +423,10 @@ class Profile(Item): 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 @@ -357,6 +434,10 @@ class Profile(Item): 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 @@ -364,6 +445,13 @@ class Profile(Item): return False def set_xen_name_prefix(self,str): + """ + For Xen only. + Specifies that Xen filenames created with xen-net-install should + start with 'str'. To keep the shell happy, the 'str' cannot + contain wildcards or slashes. xen-net-install is free to ignore + this suggestion. + """ # no slashes or wildcards for bad in [ '/', '*', '?' ]: if str.find(bad) != -1: @@ -372,6 +460,12 @@ class Profile(Item): return True def set_xen_file_path(self,str): + """ + For Xen only. + Specifies that Xen filenames be stored in path specified by 'str'. + Paths must be absolute. xen-net-install will ignore this suggestion + if it cannot write to the given location. + """ # path must look absolute if len(str) < 1 or str[0] != "/": return False @@ -379,6 +473,14 @@ class Profile(Item): return True def set_xen_file_size(self,num): + """ + For Xen only. + Specifies the size of the Xen image in megabytes. 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) @@ -392,6 +494,15 @@ class Profile(Item): 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): @@ -401,6 +512,12 @@ class Profile(Item): 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'): @@ -414,6 +531,11 @@ class Profile(Item): 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 @@ -461,12 +583,13 @@ class System(Item): self.profile = seed_data['profile'] self.kernel_options = seed_data['kernel_options'] - """ - 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 - """ + 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") @@ -475,12 +598,19 @@ class System(Item): 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 @@ -1,12 +1,11 @@ #!/usr/bin/env python - -# BootConf.py -# -# The command line interface for BootConf, a network boot configuration -# library ... -# # Michael DeHaan <mdehaan@redhat.com> +""" +Command line interface for BootConf, a network boot configuration +library +""" + import os import sys import getopt @@ -18,10 +17,11 @@ from msg import * class BootCLI: - """ - Build the command line parser and API instances, etc. - """ + def __init__(self,args): + """ + Build the command line parser and API instances, etc. + """ self.args = args self.api = api.BootAPI() self.commands = {} @@ -58,82 +58,91 @@ class BootCLI: 'help' : self.help } - """ - Run the command line - """ + def run(self): + """ + Run the command line + """ rc = self.curry_args(self.args[1:], self.commands['toplevel']) if not rc: print self.api.last_error return rc - """ - Print out abbreviated help if user gives bad syntax - """ def usage(self): + """ + Print out abbreviated help if user gives bad syntax + """ print m("usage") return False - """ - Print out tediously wrong help: 'bootconf help' - """ + def help(self,args): + """ + Print out tediously wrong help: 'bootconf help' + """ print m("help") return False - """ - Print out the list of systems: 'bootconf system list' - """ + def system_list(self,args): + """ + Print out the list of systems: 'bootconf system list' + """ print str(self.api.get_systems()) - """ - Print out the list of profiles: 'bootconf profile list' - """ + def profile_list(self,args): + """ + Print out the list of profiles: 'bootconf profile list' + """ print str(self.api.get_profiles()) - """ - Print out the list of distros: 'bootconf distro list' - """ + def distro_list(self,args): + """ + Print out the list of distros: 'bootconf distro list' + """ print str(self.api.get_distros()) - """ - Delete a system: 'bootconf system remove --name=foo' - """ + def system_remove(self,args): + """ + Delete a system: 'bootconf system remove --name=foo' + """ commands = { '--name' : lambda(a): self.api.get_systems().remove(a) } on_ok = lambda: True return self.apply_args(args,commands,on_ok,True) - """ - Delete a profile: 'bootconf profile remove --name=foo' - """ + def profile_remove(self,args): + """ + Delete a profile: 'bootconf profile remove --name=foo' + """ commands = { '--name' : lambda(a): self.api.get_profiles().remove(a) } on_ok = lambda: True return self.apply_args(args,commands,on_ok,True) - """ - Delete a distro: 'bootconf distro remove --name='foo' - """ + def distro_remove(self,args): + """ + Delete a distro: 'bootconf distro remove --name='foo' + """ commands = { '--name' : lambda(a): self.api.get_distros().remove(a) } on_ok = lambda: True return self.apply_args(args,commands,on_ok,True) - """ - Create/Edit a system: 'bootconf system edit --name='foo' ... - """ + def system_edit(self,args): + """ + Create/Edit a system: 'bootconf system edit --name='foo' ... + """ sys = self.api.new_system() commands = { '--name' : lambda(a) : sys.set_name(a), @@ -144,10 +153,11 @@ class BootCLI: on_ok = lambda: self.api.get_systems().add(sys) return self.apply_args(args,commands,on_ok,True) - """ - Create/Edit a profile: 'bootconf profile edit --name='foo' ... - """ + def profile_edit(self,args): + """ + Create/Edit a profile: 'bootconf profile edit --name='foo' ... + """ profile = self.api.new_profile() commands = { '--name' : lambda(a) : profile.set_name(a), @@ -165,10 +175,11 @@ class BootCLI: on_ok = lambda: self.api.get_profiles().add(profile) return self.apply_args(args,commands,on_ok,True) - """ - Create/Edit a distro: 'bootconf distro edit --name='foo' ... - """ + def distro_edit(self,args): + """ + Create/Edit a distro: 'bootconf distro edit --name='foo' ... + """ distro = self.api.new_distro() commands = { '--name' : lambda(a) : distro.set_name(a), @@ -179,11 +190,12 @@ class BootCLI: on_ok = lambda: self.api.get_distros().add(distro) return self.apply_args(args,commands,on_ok,True) - """ - Instead of getopt... - Parses arguments of the form --foo=bar, see profile_edit for example - """ + def apply_args(self,args,input_routines,on_ok,serialize): + """ + Instead of getopt... + Parses arguments of the form --foo=bar, see profile_edit for example + """ if len(args) == 0: print m("no_args") return False @@ -206,11 +218,12 @@ class BootCLI: self.api.serialize() return rc - """ - Helper function to make subcommands a bit more friendly. - See profiles(), system(), or distro() for examples - """ + def curry_args(self, args, commands): + """ + Helper function to make subcommands a bit more friendly. + See profiles(), system(), or distro() for examples + """ if args is None or len(args) == 0: print m("help") return False @@ -223,10 +236,11 @@ class BootCLI: return False return True - """ - Sync the config file with the system config: 'bootconf sync [--dryrun]' - """ + def sync(self, args): + """ + Sync the config file with the system config: 'bootconf sync [--dryrun]' + """ status = None if args is not None and "--dryrun" in args: status = self.api.sync(dry_run=True) @@ -234,10 +248,11 @@ class BootCLI: status = self.api.sync(dry_run=False) return status - """ - Check system for network boot decency/prereqs: 'bootconf check' - """ + def check(self,args): + """ + Check system for network boot decency/prereqs: 'bootconf check' + """ status = self.api.check() if status is None: return False @@ -250,22 +265,25 @@ class BootCLI: print "#%d: %s" % (i,x) return False - """ - Handles any of the 'bootconf distro' subcommands - """ + def distro(self,args): + """ + Handles any of the 'bootconf distro' subcommands + """ return self.curry_args(args, self.commands['distro']) - """ - Handles any of the 'bootconf profile' subcommands - """ + def profile(self,args): + """ + Handles any of the 'bootconf profile' subcommands + """ return self.curry_args(args, self.commands['profile']) - """ - Handles any of the 'bootconf system' subcommands - """ + def system(self,args): + """ + Handles any of the 'bootconf system' subcommands + """ return self.curry_args(args, self.commands['system']) if __name__ == "__main__": @@ -17,11 +17,12 @@ class BootCheck: self.api = api self.config = self.api.config - """ - Returns None if there are no errors, otherwise returns a list - of things to correct prior to running bootconf 'for real'. - """ + def run(self): + """ + Returns None if there are no errors, otherwise returns a list + of things to correct prior to running bootconf 'for real'. + """ status = [] self.check_dhcpd_bin(status) self.check_pxelinux_bin(status) @@ -31,39 +32,41 @@ class BootCheck: self.check_dhcpd_conf(status) return status - """ - Check if dhcpd is installed - """ + def check_dhcpd_bin(self,status): + """ + Check if dhcpd is installed + """ if not os.path.exists(self.config.dhcpd_bin): status.append(m("no_dhcpd")) - """ - Check if pxelinux (part of syslinux) is installed - """ def check_pxelinux_bin(self,status): + """ + Check if pxelinux (part of syslinux) is installed + """ if not os.path.exists(self.config.pxelinux): status.append(m("no_pxelinux")) - """ - Check if tftpd is installed - """ def check_tftpd_bin(self,status): + """ + Check if tftpd is installed + """ if not os.path.exists(self.config.tftpd_bin): status.append(m("no_tftpd")) - """ - Check if bootconf.conf's tftpboot directory exists - """ def check_tftpd_dir(self,status): + """ + Check if bootconf.conf's tftpboot directory exists + """ if not os.path.exists(self.config.tftpboot): status.append(m("no_dir") % self.config.tftpboot) - """ - Check that bootconf tftpd boot directory matches with tftpd directory - Check that tftpd is enabled to autostart - """ + def check_tftpd_conf(self,status): + """ + Check that bootconf tftpd boot directory matches with tftpd directory + Check that tftpd is enabled to autostart + """ if os.path.exists(self.config.tftpd_conf): f = open(self.config.tftpd_conf) re_1 = re.compile(r'default:.*off') @@ -81,11 +84,12 @@ class BootCheck: else: status.append(m("no_exist") % self.tftpd_conf) - """ - Check that dhcpd *appears* to be configured for pxe booting. - We can't assure file correctness - """ + def check_dhcpd_conf(self,status): + """ + Check that dhcpd *appears* to be configured for pxe booting. + We can't assure file correctness + """ if os.path.exists(self.config.dhcpd_conf): match_next = False match_file = False @@ -9,6 +9,8 @@ from msg import * import os import yaml +#import syck -- we *want* to use syck, but the FC syck currently does not +# -- contain the dump function, i.e. not gonna work import traceback class BootConfig: @@ -146,7 +148,7 @@ class BootConfig: return False data = self.to_hash(True) settings.write(yaml.dump(data)) - + # ------ # dump internal state (distros, profiles, systems...) if not os.path.isdir(os.path.dirname(self.state_file)): @@ -1,9 +1,11 @@ # Messages used by bootconf. -# Just consolidated here so they're not in the source. -# No plans on localization any time soon. -# # Michael DeHaan <mdehaan@redhat.com> +""" +This module encapsulates strings so they can +be reused and potentially translated. +""" + msg_table = { "parse_error" : "could not parse /etc/bootconf.conf", "no_create" : "cannot create: %s", @@ -50,10 +52,10 @@ Good luck. "help" : "see 'man bootconf'" } -""" -Return the lookup of a string key. -""" def m(key): + """ + Return the lookup of a string key. + """ if key in msg_table: # localization could use different tables or just gettext. return msg_table[key] @@ -12,19 +12,25 @@ import re import shutil import IPy +import yaml from msg import * +""" +Handles conversion of internal state to the tftpboot tree layout +""" + class BootSync: def __init__(self,api): self.api = api self.verbose = True - """ - Syncs the current bootconf configuration. - Using the Check().run_ functions previously is recommended - """ + def sync(self,dry_run=False,verbose=True): + """ + Syncs the current bootconf configuration. + Using the Check().run_ functions previously is recommended + """ self.dry_run = dry_run #results = self.api.check() #if results != []: @@ -32,46 +38,45 @@ class BootSync: # return False try: self.copy_pxelinux() - self.clean_pxelinux_tree() + self.clean_trees() self.copy_distros() self.validate_kickstarts() - self.build_pxelinux_tree() + self.build_trees() except: traceback.print_exc() return False return True - """ - Copy syslinux to the configured tftpboot directory - """ + def copy_pxelinux(self): + """ + Copy syslinux to the configured tftpboot directory + """ self.copy(self.api.config.pxelinux, os.path.join(self.api.config.tftpboot, "pxelinux.0")) - """ - Delete any previously built pxelinux.cfg tree for individual systems. - This is better than trying to just add additional entries - as both MAC and IP settings could have been added and the MACs will - take precedence. So we can't really trust human edits won't - conflict. - """ - def clean_pxelinux_tree(self): - self.rmtree(os.path.join(self.api.config.tftpboot, "pxelinux.cfg"), True) - - """ - A distro is a kernel and an initrd. Copy all of them and error - out if any files are missing. The conf file was correct if built - via the CLI or API, though it's possible files have been moved - since or perhaps they reference NFS directories that are no longer - mounted. - """ + + def clean_trees(self): + """ + Delete any previously built pxelinux.cfg tree and xen tree info. + """ + for x in ["pxelinux.cfg","images","systems","distros","profiles"]: + dir = os.path.join(self.api.config.tftpboot,x) + self.rmtree(dir, True) + self.mkdir(dir) + def copy_distros(self): + """ + A distro is a kernel and an initrd. Copy all of them and error + out if any files are missing. The conf file was correct if built + via the CLI or API, though it's possible files have been moved + since or perhaps they reference NFS directories that are no longer + mounted. + """ # copy is a 4-letter word but tftpboot runs chroot, thus it's required. - images = os.path.join(self.api.config.tftpboot, "images") - self.rmtree(os.path.join(self.api.config.tftpboot, "images"), True) - self.mkdir(images) + distros = os.path.join(self.api.config.tftpboot, "images") for d in self.api.get_distros().contents(): - distro_dir = os.path.join(images,d.name) + 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 @@ -86,15 +91,16 @@ class BootSync: self.copyfile(kernel, os.path.join(distro_dir, b_kernel)) self.copyfile(initrd, os.path.join(distro_dir, b_initrd)) - """ - Similar to what we do for distros, ensure all the kickstarts - in conf file are valid. Since kickstarts are referenced by URL - (http or ftp), we do not have to copy them. They are already - expected to be in the right place. We can't check to see that the - URLs are right (or we don't, we could...) but we do check to see - that the files are at least still there. - """ + def validate_kickstarts(self): + """ + Similar to what we do for distros, ensure all the kickstarts + in conf file are valid. Since kickstarts are referenced by URL + (http or ftp), we do not have to copy them. They are already + expected to be in the right place. We can't check to see that the + URLs are right (or we don't, we could...) but we do check to see + that the files are at least still there. + """ # ensure all referenced kickstarts exist # 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 @@ -104,18 +110,31 @@ class BootSync: self.api.last_error = m("err_kickstart") % (g.name, g.kickstart) raise "error" - """ - Now that kernels and initrds are copied and kickstarts are all valid, - build the pxelinux.cfg tree, which contains a directory for each - configured IP or MAC address. - """ - def build_pxelinux_tree(self): + + def build_trees(self): + """ + Now that kernels and initrds are copied and kickstarts are all valid, + build the pxelinux.cfg tree, which contains a directory for each + configured IP or MAC address. Also build a parallel 'xeninfo' tree + for xen-net-install info. + """ # 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() - self.mkdir(os.path.join(self.api.config.tftpboot,"pxelinux.cfg")) + + for d in self.api.get_distros().contents(): + # TODO: add check to ensure all distros have profiles (=warning) + filename = os.path.join(self.api.config.tftpboot,"distros",d.name) + self.write_distro_file(filename,d) + + for p in self.api.get_profiles().contents(): + # 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) + self.write_profile_file(filename,p) + for system in self.api.get_systems().contents(): profile = profiles.find(system.profile) if profile is None: @@ -125,18 +144,21 @@ class BootSync: if distro is None: self.api.last_error = m("orphan_system2") raise "error" - filename = self.get_pxelinux_filename(system.name) - filename = os.path.join(self.api.config.tftpboot, "pxelinux.cfg", filename) - self.write_pxelinux_file(filename,system,profile,distro) - - """ - The configuration file for each system pxelinux uses is either - a form of the MAC address of the hex version of the IP. Not sure - about ipv6 (or if that works). The system name in the config file - 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. - """ + 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) + self.write_pxelinux_file(f2,system,profile,distro) + self.write_system_file(f3,system) + + def get_pxelinux_filename(self,name_input): + """ + The configuration file for each system pxelinux uses is either + a form of the MAC address of the hex version of the IP. Not sure + about ipv6 (or if that works). The system name in the config file + 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): return IPy.IP(name).strHex()[2:] @@ -146,29 +168,24 @@ class BootSync: self.api.last_error = m("err_resolv") % name raise "error" - """ - Write a configuration file for the pxelinux boot loader. - More system-specific configuration may come in later, if so - that would appear inside the system object in api.py - """ + def write_pxelinux_file(self,filename,system,profile,distro): + """ + Write a configuration file for the pxelinux boot loader. + More system-specific configuration may come in later, if so + that would appear inside the system object in api.py + """ kernel_path = os.path.join("/images",distro.name,os.path.basename(distro.kernel)) initrd_path = os.path.join("/images",distro.name,os.path.basename(distro.initrd)) kickstart_path = profile.kickstart self.sync_log("writing: %s" % filename) self.sync_log("---------------------------------") - if self.dry_run: - file = None - else: - file = open(filename,"w+") + file = self.open_file(filename,"w+") self.tee(file,"default linux\n") self.tee(file,"prompt 0\n") self.tee(file,"timeout 1\n") self.tee(file,"label linux\n") self.tee(file," kernel %s\n" % kernel_path) - # FIXME: allow leaving off the kickstart if no kickstart... - # FIXME: if the users kernel_options string has zero chance of - # booting we *could* try to detect it and warn them. kopts = self.blend_kernel_options(( self.api.config.kernel_options, profile.kernel_options, @@ -179,64 +196,117 @@ class BootSync: if kickstart_path is not None and kickstart_path != "": nextline = nextline + " ks=%s" % kickstart_path self.tee(file, nextline) - if not self.dry_run: - file.close() + self.close_file(file) self.sync_log("--------------------------------") - """ - For dry_run support, and logging... - """ + + def write_distro_file(self,filename,distro): + """ + Create distro information for xen-net-install + """ + file = self.open_file(filename,"w+") + file.write(yaml.dump(distro.to_datastruct())) + self.close_file(file) + + + def write_profile_file(self,filename,profile): + """ + Create profile information for xen-net-install + """ + file = self.open_file(filename,"w+") + file.write(yaml.dump(profile.to_datastruct())) + self.close_file(file) + + + def write_system_file(self,filename,system): + """ + Create system information for xen-net-install + """ + file = self.open_file(filename,"w+") + file.write(yaml.dump(system.to_datastruct())) + self.close_file(file) + + def tee(self,file,text): + """ + For dry_run support: send data to screen and potentially to disk + """ self.sync_log(text) if not self.dry_run: file.write(text) + def open_file(self,filename,mode): + """ + For dry_run support: open a file if not in dry_run mode. + """ + if self.dry_run: + return None + return open(filename,mode) + + def close_file(self,file): + """ + For dry_run support: close a file if not in dry_run mode. + """ + if not self.dry_run: + file.close() + def copyfile(self,src,dst): + """ + For dry_run support: potentially copy a file. + """ self.sync_log("copy %s to %s" % (src,dst)) if self.dry_run: return True return shutil.copyfile(src,dst) def copy(self,src,dst): - self.sync_log("copy %s to %s" % (src,dst)) - if self.dry_run: - return True - return shutil.copy(src,dst) + """ + For dry_run support: potentially copy a file. + """ + self.sync_log("copy %s to %s" % (src,dst)) + if self.dry_run: + return True + return shutil.copy(src,dst) def rmtree(self,path,ignore): + """ + For dry_run support: potentially delete a tree. + """ self.sync_log("removing dir %s" % (path)) if self.dry_run: return True return shutil.rmtree(path,ignore) def mkdir(self,path,mode=0777): + """ + For dry_run support: potentially make a directory. + """ self.sync_log("creating dir %s" % (path)) if self.dry_run: return True return os.mkdir(path,mode) - """ - Used to differentiate dry_run output from the real thing - automagically - """ def sync_log(self,message): - if self.verbose: - if self.dry_run: - print "dry_run | %s" % message - else: - print message + """ + Used to differentiate dry_run output from the real thing + automagically + """ + if self.verbose: + if self.dry_run: + print "dry_run | %s" % message + else: + print message - - """ - Given a list of kernel options, take the values used by the - first argument in the list unless overridden by those in the - second (or further on), according to --key=value formats. - - This is used such that we can have default kernel options - in /etc and then distro, profile, and system options with various - levels of configurability. - """ def blend_kernel_options(self, list_of_opts): + """ + Given a list of kernel options, take the values used by the + first argument in the list unless overridden by those in the + second (or further on), according to --key=value formats. + + This is used such that we can have default kernel options + in /etc and then distro, profile, and system options with various + levels of configurability. + """ internal = {} results = [] # for all list of kernel options @@ -17,51 +17,56 @@ class BootUtil: self.re_kernel = re.compile(r'vmlinuz-(\d+)\.(\d+)\.(\d+)-(.*)') self.re_initrd = re.compile(r'initrd-(\d+)\.(\d+)\.(\d+)-(.*).img') - """ - 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 - """ + 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) - """ - Return whether the argument is an IP address. ipv6 needs - to be added... - """ + 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 - """ - Return whether the argument is a mac address. - """ + 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 - """ - Resolve the IP address and handle errors... - """ + def resolve_ip(self,strdata): + """ + Resolve the IP address and handle errors... + """ try: return socket.gethostbyname(strdata) except: return None - """ - Find all files in a given directory that match a given regex. - Can't use glob directly as glob doesn't take regexen. - """ + 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: @@ -69,12 +74,13 @@ class BootUtil: results.append(f) return results - """ - 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. - """ + 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 sort(a,b): @@ -97,11 +103,12 @@ class BootUtil: return last_chance return None - """ - 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. - """ + 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): @@ -112,11 +119,12 @@ class BootUtil: return self.find_highest_files(path,"vmlinuz",self.re_kernel) return None - """ - Given a directory or a filename, see if the path can be made - to resolve into an intird, return that full path if possible. - """ + 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) @@ -128,10 +136,11 @@ class BootUtil: return self.find_highest_files(path,"initrd.img",self.re_initrd) return None - """ - Check if a kickstart url looks like an http, ftp, or nfs url. - """ + def find_kickstart(self,url): + """ + Check if a kickstart url looks like an http, ftp, or nfs url. + """ x = url.lower() for y in ["http://","nfs://","ftp://"]: if x.startswith(y): |