summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api.py262
-rwxr-xr-xbootconf154
-rw-r--r--check.py52
-rw-r--r--config.py4
-rw-r--r--msg.py14
-rw-r--r--sync.py262
-rw-r--r--util.py79
7 files changed, 531 insertions, 296 deletions
diff --git a/api.py b/api.py
index 0d0e422..eb64aab 100644
--- a/api.py
+++ b/api.py
@@ -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
diff --git a/bootconf b/bootconf
index 78b9a19..8d22e71 100755
--- a/bootconf
+++ b/bootconf
@@ -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__":
diff --git a/check.py b/check.py
index cdfdba0..efafb7c 100644
--- a/check.py
+++ b/check.py
@@ -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
diff --git a/config.py b/config.py
index 9aded94..0adf97e 100644
--- a/config.py
+++ b/config.py
@@ -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)):
diff --git a/msg.py b/msg.py
index 609597e..28fec38 100644
--- a/msg.py
+++ b/msg.py
@@ -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]
diff --git a/sync.py b/sync.py
index dfbfa4c..664bbba 100644
--- a/sync.py
+++ b/sync.py
@@ -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
diff --git a/util.py b/util.py
index 8752a35..9b23ee9 100644
--- a/util.py
+++ b/util.py
@@ -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):