diff options
Diffstat (limited to 'cobbler')
67 files changed, 2694 insertions, 2870 deletions
diff --git a/cobbler/acls.py b/cobbler/acls.py index 8e7ecea9..e797f2f7 100644 --- a/cobbler/acls.py +++ b/cobbler/acls.py @@ -40,7 +40,7 @@ class AclEngine: yfh = open("/etc/cobbler/acls.conf") data = yfh.read() yfh.close() - self.data = yaml.load(data).next() + self.data = yaml.load(data) self.verbose = verbose def __match(self, needle, haystack): diff --git a/cobbler/action_buildiso.py b/cobbler/action_buildiso.py index de95afb9..2eebb668 100644 --- a/cobbler/action_buildiso.py +++ b/cobbler/action_buildiso.py @@ -29,6 +29,7 @@ import sys import traceback import shutil import sub_process +import re import utils from cexceptions import * @@ -68,8 +69,10 @@ class BuildIso: self.distros = config.distros() self.profiles = config.profiles() self.systems = config.systems() + self.distros = config.distros() self.distmap = {} self.distctr = 0 + self.source = "" def make_shorter(self,distname): if self.distmap.has_key(distname): @@ -79,44 +82,8 @@ class BuildIso: self.distmap[distname] = str(self.distctr) return str(self.distctr) - def run(self,iso=None,tempdir=None,profiles=None,systems=None): - - # if iso is none, create it in . as "kickstart.iso" - if iso is None: - iso = "kickstart.iso" - - if tempdir is None: - tempdir = os.path.join(os.getcwd(), "buildiso") - print _("- using/creating tempdir: %s") % tempdir - if not os.path.exists(tempdir): - os.makedirs(tempdir) - else: - shutil.rmtree(tempdir) - os.makedirs(tempdir) - - # if base of tempdir does not exist, fail - # create all profiles unless filtered by "profiles" - - imagesdir = os.path.join(tempdir, "images") - isolinuxdir = os.path.join(tempdir, "isolinux") - - print _("- building tree for isolinux") - if not os.path.exists(imagesdir): - os.makedirs(imagesdir) - if not os.path.exists(isolinuxdir): - os.makedirs(isolinuxdir) - - print _("- copying miscellaneous files") - isolinuxbin = "/usr/lib/syslinux/isolinux.bin" - menu = "/var/lib/cobbler/menu.c32" - chain = "/usr/lib/syslinux/chain.c32" - files = [ isolinuxbin, menu, chain ] - for f in files: - if not os.path.exists(f): - raise CX(_("Required file not found: %s") % f) - else: - utils.copyfile(f, os.path.join(isolinuxdir, os.path.basename(f))) - + + def generate_netboot_iso(self,imagesdir,isolinuxdir,profiles=None,systems=None): print _("- copying kernels and initrds - for profiles") # copy all images in included profiles to images dir for profile in self.api.profiles(): @@ -134,8 +101,14 @@ class BuildIso: distname = self.make_shorter(dist.name) # tempdir/isolinux/$distro/vmlinuz, initrd.img # FIXME: this will likely crash on non-Linux breeds - shutil.copyfile(dist.kernel, os.path.join(isolinuxdir, "%s.krn" % distname)) - shutil.copyfile(dist.initrd, os.path.join(isolinuxdir, "%s.img" % distname)) + f1 = os.path.join(isolinuxdir, "%s.krn" % distname) + f2 = os.path.join(isolinuxdir, "%s.img" % distname) + if not os.path.exists(dist.kernel): + raise CX("path does not exist: %s" % dist.kernel) + if not os.path.exists(dist.initrd): + raise CX("path does not exist: %s" % dist.initrd) + shutil.copyfile(dist.kernel, f1) + shutil.copyfile(dist.initrd, f2) if systems is not None: print _("- copying kernels and initrds - for systems") @@ -156,9 +129,15 @@ class BuildIso: isolinuxcfg = os.path.join(isolinuxdir, "isolinux.cfg") cfg = open(isolinuxcfg, "w+") cfg.write(HEADER) # fixme, use template - + print _("- generating profile list...") - for profile in self.api.profiles(): + #sort the profiles + profile_list = [profile for profile in self.profiles] + def sort_name(a,b): + return cmp(a.name,b.name) + profile_list.sort(sort_name) + + for profile in profile_list: use_this = True if profiles is not None: which_profiles = profiles.split(",") @@ -189,16 +168,22 @@ class BuildIso: length=len(append_line) if length>254: - print _("WARNING - append line length is greater than 254 chars: (%s chars)") % length - + print _("WARNING - append line length is greater than 254 chars: (%s chars)") % length + cfg.write(append_line) - + if systems is not None: print _("- generating system list...") cfg.write("\nMENU SEPARATOR\n") - for system in self.api.systems(): + #sort the systems + system_list = [system for system in self.systems] + def sort_name(a,b): + return cmp(a.name,b.name) + system_list.sort(sort_name) + + for system in system_list: use_this = False if systems is not None: which_systems = systems.split(",") @@ -230,35 +215,195 @@ class BuildIso: # add network info to avoid DHCP only if it is available - if data.has_key("ip_address_eth0") and data["ip_address_eth0"] != "": - append_line = append_line + " ip=%s" % data["ip_address_eth0"] - if data.has_key("subnet_eth0") and data["subnet_eth0"] != "": - append_line = append_line + " netmask=%s" % data["subnet_eth0"] - if data.has_key("gateway_eth0") and data["gateway_eth0"] != "": - append_line = append_line + " gateway=%s\n" % data["gateway_eth0"] + if data.has_key("bonding_master_eth0") and data["bonding_master_eth0"] != "": + primary_interface = data["bonding_master_eth0"] + else: + primary_interface = "eth0" + + if data.has_key("ip_address_" + primary_interface) and data["ip_address_" + primary_interface] != "": + append_line = append_line + " ip=%s" % data["ip_address_" + primary_interface] + + if data.has_key("subnet_" + primary_interface) and data["subnet_" + primary_interface] != "": + append_line = append_line + " netmask=%s" % data["subnet_" + primary_interface] + + if data.has_key("gateway") and data["gateway"] != "": + append_line = append_line + " gateway=%s" % data["gateway"] + + if data.has_key("name_servers") and data["name_servers"]: + append_line = append_line + " dns=%s\n" % ",".join(data["name_servers"]) length=len(append_line) if length > 254: - print _("WARNING - append line length is greater than 254 chars: (%s chars)") % length - + print _("WARNING - append line length is greater than 254 chars: (%s chars)") % length + cfg.write(append_line) - print _("- done writing config") + print _("- done writing config") cfg.write("\n") cfg.write("MENU END\n") cfg.close() - - cmd = "mkisofs -o %s -r -b isolinux/isolinux.bin -c isolinux/boot.cat" % iso - cmd = cmd + " -no-emul-boot -boot-load-size 4 " - cmd = cmd + " -boot-info-table -V Cobbler\ Install -R -J -T %s" % tempdir + + + def generate_standalone_iso(self,imagesdir,isolinuxdir,distname,filesource): + + # Get the distro object for the requested distro + # and then get all of its descendants (profiles/sub-profiles/systems) + distro = self.api.find_distro(distname) + if distro is None: + raise CX("distro %s was not found, aborting" % distname) + descendants = distro.get_descendants() + + if filesource is None: + # Try to determine the source from the distro kernel path + print _("- trying to locate source for distro") + found_source = False + (source_head, source_tail) = os.path.split(distro.kernel) + while source_tail != '': + if source_head == os.path.join(self.api.settings().webdir, "ks_mirror"): + filesource = os.path.join(source_head, source_tail) + found_source = True + print _(" found source in %s" % filesource) + break + (source_head, source_tail) = os.path.split(source_head) + # Can't find the source, raise an error + if not found_source: + raise CX(_(" Error, no installation source found. When building a standalone ISO, you must specify a --source if the distro install tree is not hosted locally")) + + print _("- copying kernels and initrds - for standalone distro") + # tempdir/isolinux/$distro/vmlinuz, initrd.img + # FIXME: this will likely crash on non-Linux breeds + f1 = os.path.join(isolinuxdir, "vmlinuz") + f2 = os.path.join(isolinuxdir, "initrd.img") + if not os.path.exists(distro.kernel): + raise CX("path does not exist: %s" % distro.kernel) + if not os.path.exists(distro.initrd): + raise CX("path does not exist: %s" % distro.initrd) + shutil.copyfile(distro.kernel, f1) + shutil.copyfile(distro.initrd, f2) + + cmd = "rsync -rlptgu --exclude=boot.cat --exclude=TRANS.TBL --exclude=isolinux/ %s/ %s/../" % (filesource, isolinuxdir) + print _("- copying distro %s files (%s)" % (distname,cmd)) + rc = sub_process.call(cmd, shell=True, close_fds=True) + if rc: + raise CX(_("rsync of files failed")) + + print _("- generating a isolinux.cfg") + isolinuxcfg = os.path.join(isolinuxdir, "isolinux.cfg") + cfg = open(isolinuxcfg, "w+") + cfg.write(HEADER) # fixme, use template + + for descendant in descendants: + data = utils.blender(self.api, True, descendant) + + cfg.write("\n") + cfg.write("LABEL %s\n" % descendant.name) + cfg.write(" MENU LABEL %s\n" % descendant.name) + cfg.write(" kernel vmlinuz\n") + + data["kickstart"] = "cdrom:/isolinux/ks-%s.cfg" % descendant.name + + append_line = " append initrd=initrd.img" + append_line = append_line + " ks=%s " % data["kickstart"] + append_line = append_line + " %s\n" % data["kernel_options"] + + cfg.write(append_line) + + if descendant.COLLECTION_TYPE == 'profile': + kickstart_data = self.api.kickgen.generate_kickstart_for_profile(descendant.name) + elif descendant.COLLECTION_TYPE == 'system': + kickstart_data = self.api.kickgen.generate_kickstart_for_system(descendant.name) + + cdregex = re.compile("url .*\n", re.IGNORECASE) + kickstart_data = cdregex.sub("cdrom\n", kickstart_data) + + ks_name = os.path.join(isolinuxdir, "ks-%s.cfg" % descendant.name) + ks_file = open(ks_name, "w+") + ks_file.write(kickstart_data) + ks_file.close() + + print _("- done writing config") + cfg.write("\n") + cfg.write("MENU END\n") + cfg.close() + + return + + + def run(self,iso=None,tempdir=None,profiles=None,systems=None,distro=None,standalone=None,source=None): + + # the distro option is for stand-alone builds only + if not standalone and distro is not None: + raise CX(_("The --distro option should only be used when creating a standalone ISO")) + # if building standalone, we only want --distro, + # profiles/systems are disallowed + if standalone: + if profiles is not None or systems is not None: + raise CX(_("When building a standalone ISO, use --distro only instead of --profiles/--systems")) + elif distro is None: + raise CX(_("When building a standalone ISO, you must specify a --distro")) + if source != None and not os.path.exists(source): + raise CX(_("The source specified (%s) does not exist" % source)) + + # if iso is none, create it in . as "kickstart.iso" + if iso is None: + iso = "kickstart.iso" + + if tempdir is None: + tempdir = os.path.join(os.getcwd(), "buildiso") + else: + if not os.path.isdir(tempdir): + raise CX(_("The --tempdir specified is not a directory")) + + (tempdir_head,tempdir_tail) = os.path.split(os.path.normpath(tempdir)) + if tempdir_tail != "buildiso": + tempdir = os.path.join(tempdir, "buildiso") + + print _("- using/creating tempdir: %s") % tempdir + if not os.path.exists(tempdir): + os.makedirs(tempdir) + else: + shutil.rmtree(tempdir) + os.makedirs(tempdir) + + # if base of tempdir does not exist, fail + # create all profiles unless filtered by "profiles" + + imagesdir = os.path.join(tempdir, "images") + isolinuxdir = os.path.join(tempdir, "isolinux") + + print _("- building tree for isolinux") + if not os.path.exists(imagesdir): + os.makedirs(imagesdir) + if not os.path.exists(isolinuxdir): + os.makedirs(isolinuxdir) + + print _("- copying miscellaneous files") + isolinuxbin = "/usr/lib/syslinux/isolinux.bin" + menu = "/var/lib/cobbler/menu.c32" + chain = "/usr/lib/syslinux/chain.c32" + files = [ isolinuxbin, menu, chain ] + for f in files: + if not os.path.exists(f): + raise CX(_("Required file not found: %s") % f) + else: + utils.copyfile(f, os.path.join(isolinuxdir, os.path.basename(f)), self.api) + + if standalone: + self.generate_standalone_iso(imagesdir,isolinuxdir,distro,source) + else: + self.generate_netboot_iso(imagesdir,isolinuxdir,profiles,systems) + + cmd = "mkisofs -quiet -o %s -r -b isolinux/isolinux.bin -c isolinux/boot.cat" % iso + cmd = cmd + " -no-emul-boot -boot-load-size 4" + cmd = cmd + " -boot-info-table -V Cobbler\ Install -R -J -T %s" % tempdir print _("- running: %s") % cmd rc = sub_process.call(cmd, shell=True, close_fds=True) if rc: raise CX(_("mkisofs failed")) - + print _("ISO build complete") print _("You may wish to delete: %s") % tempdir - print _("The output file is: %s") % iso - - + print _("The output file is: %s") % iso + + diff --git a/cobbler/action_check.py b/cobbler/action_check.py index 99fffbb0..a2885a28 100644 --- a/cobbler/action_check.py +++ b/cobbler/action_check.py @@ -44,6 +44,7 @@ class BootCheck: (The CLI usage is "cobbler check" before "cobbler sync") """ status = [] + self.checked_dist = utils.check_dist() self.check_name(status) self.check_selinux(status) if self.settings.manage_dhcp: @@ -95,13 +96,13 @@ class BootCheck: if notes != "": notes = " (NOTE: %s)" % notes rc = 0 - if utils.check_dist() == "redhat": + if self.checked_dist == "redhat" or self.checked_dist == "suse": if os.path.exists("/etc/rc.d/init.d/%s" % which): - rc = sub_process.call("/sbin/service %s status >/dev/null 2>/dev/null" % which, shell=True, close_fds=True) + rc = sub_process.call("/sbin/service %s status > /dev/null 2>/dev/null" % which, shell=True, close_fds=True) if rc != 0: status.append(_("service %s is not running%s") % (which,notes)) return False - elif utils.check_dist() == "debian": + elif self.checked_dist == "debian": if os.path.exists("/etc/init.d/%s" % which): rc = sub_process.call("/etc/init.d/%s status /dev/null 2>/dev/null" % which, shell=True, close_fds=True) if rc != 0: @@ -116,7 +117,7 @@ class BootCheck: if os.path.exists("/etc/rc.d/init.d/iptables"): rc = sub_process.call("/sbin/service iptables status >/dev/null 2>/dev/null", shell=True, close_fds=True) if rc == 0: - status.append(_("since iptables may be running, ensure 69, 80, %(syslog)s, and %(xmlrpc)s are unblocked") % { "syslog" : self.settings.syslog_port, "xmlrpc" : self.settings.xmlrpc_port }) + status.append(_("since iptables may be running, ensure 69, 80, and %(xmlrpc)s are unblocked") % { "xmlrpc" : self.settings.xmlrpc_port }) def check_yum(self,status): if not os.path.exists("/usr/bin/createrepo"): @@ -125,6 +126,11 @@ class BootCheck: status.append(_("reposync is not installed, need for cobbler reposync, install/upgrade yum-utils?")) if not os.path.exists("/usr/bin/yumdownloader"): status.append(_("yumdownloader is not installed, needed for cobbler repo add with --rpm-list parameter, install/upgrade yum-utils?")) + if self.settings.yumreposync_flags.find("\-l"): + if self.checked_dist == "redhat" or self.checked_dist == "suse": + yum_utils_ver = sub_process.call("/usr/bin/rpmquery --queryformat=%{VERSION} yum-utils", shell=True, close_fds=True) + if yum_utils_ver < "1.1.17": + status.append(_("yum-utils need to be at least version 1.1.17 for reposync -l")) def check_name(self,status): """ @@ -146,7 +152,30 @@ class BootCheck: if line.find("httpd_can_network_connect ") != -1: if line.find("off") != -1: status.append(_("Must enable selinux boolean to enable Apache and web services components, run: setsebool -P httpd_can_network_connect true")) - + data3 = sub_process.Popen("/usr/sbin/semanage fcontext -l | grep public_content_t",shell=True,stdout=sub_process.PIPE).communicate()[0] + + rule1 = False + rule2 = False + rule3 = False + selinux_msg = "/usr/sbin/semanage fcontext -a -t public_content_t \"%s\"" + for line in data3.split("\n"): + if line.startswith("/tftpboot/.*") and line.find("public_content_t") != -1: + rule1 = True + if line.startswith("/var/lib/tftpboot/.*") and line.find("public_content_t") != -1: + rule2 = True + if line.startswith("/var/www/cobbler/images/.*") and line.find("public_content_t") != -1: + rule3 = True + + rules = [] + if not os.path.exists("/tftpboot") and not rule1: + rules.append(selinux_msg % "/tftpboot/.*") + else: + if not rule2: + rules.append(selinux_msg % "/var/lib/tftpboot/.*") + if not rule3: + rules.append(selinux_msg % "/var/www/cobbler/images/.*") + if len(rules) > 0: + status.append("you need to set some SELinux content rules to ensure cobbler works correctly in your SELinux environment, run the following: %s" % " && ".join(rules)) def check_for_default_password(self,status): default_pass = self.settings.default_password_crypted @@ -185,8 +214,10 @@ class BootCheck: """ Check if Apache is installed. """ - self.check_service(status,"httpd") - + if self.checked_dist == "suse": + self.check_service(status,"apache2") + else: + self.check_service(status,"httpd") def check_dhcpd_bin(self,status): """ @@ -277,7 +308,7 @@ class BootCheck: f = open(self.settings.tftpd_conf) re_disable = re.compile(r'disable.*=.*yes') for line in f.readlines(): - if re_disable.search(line): + if re_disable.search(line) and not line.strip().startswith("#"): status.append(_("change 'disable' to 'no' in %(file)s") % { "file" : self.settings.tftpd_conf }) else: status.append(_("file %(file)s does not exist") % { "file" : self.settings.tftpd_conf }) diff --git a/cobbler/action_import.py b/cobbler/action_import.py index 5b94b89a..2008c53c 100644 --- a/cobbler/action_import.py +++ b/cobbler/action_import.py @@ -88,8 +88,8 @@ class Importer: if self.arch == "x86": # be consistent self.arch = "i386" - if self.arch not in [ "i386", "ia64", "ppc", "ppc64", "s390x", "x86_64", ]: - raise CX(_("arch must be i386, ia64, ppc, ppc64, s390x or x86_64")) + if self.arch not in [ "i386", "ia64", "ppc", "ppc64", "s390", "s390x", "x86_64", ]: + raise CX(_("arch must be i386, ia64, ppc, ppc64, s390, s390x or x86_64")) # if we're going to do any copying, set where to put things # and then make sure nothing is already there. @@ -113,7 +113,7 @@ class Importer: if self.arch: # append the arch path to the name if the arch is not already # found in the name. - for x in [ "i386", "ia64", "ppc", "ppc64", "s390x", "x86_64", "x86", ]: + for x in [ "i386", "ia64", "ppc", "ppc64", "s390", "s390x", "x86_64", "x86", ]: if self.mirror_name.lower().find(x) != -1: if self.arch != x : raise CX(_("Architecture found on pathname (%s) does not fit the one given in command line (%s)")%(x,self.arch)) @@ -209,11 +209,8 @@ class Importer: print _("---------------- (associating kickstarts)") self.kickstart_finder(distros_added) - # ensure everything is nicely written out to the filesystem - # (which is not so neccessary in newer Cobbler but we're paranoid) - - print _("---------------- (syncing)") - self.api.sync() + # ensure bootloaders are present + self.api.pxegen.copy_bootloaders() return True @@ -265,9 +262,9 @@ class Importer: # print _("- skipping distro %s since it wasn't imported this time") % profile.distro continue + kdir = os.path.dirname(distro.kernel) + importer = import_factory(kdir,self.path) if self.kickstart_file == None: - kdir = os.path.dirname(distro.kernel) - importer = import_factory(kdir,self.path) for rpm in importer.get_release_files(): # FIXME : This redhat specific check should go into the importer.find_release_files method if rpm.find("notes") != -1: @@ -519,9 +516,9 @@ class Importer: print "- following symlink: %s" % fullname os.path.walk(fullname, self.distro_adder, foo) - if x.startswith("initrd") or x.startswith("ramdisk.image.gz"): + if ( x.startswith("initrd") or x.startswith("ramdisk.image.gz") ) and x != "initrd.size": initrd = os.path.join(dirname,x) - if ( x.startswith("vmlinuz") or x.startswith("kernel.img") ) and x.find("initrd") == -1: + if ( x.startswith("vmlinu") or x.startswith("kernel.img") ) and x.find("initrd") == -1: kernel = os.path.join(dirname,x) if x.lower().startswith("startrom.n1_"): startrom = os.path.join(dirname,x) @@ -774,7 +771,7 @@ class Importer: name = name.replace("chrp","ppc64") for separator in [ '-' , '_' , '.' ] : - for arch in [ "i386" , "x86_64" , "ia64" , "ppc64", "ppc32", "ppc", "x86" , "s390x" , "386" , "amd" ]: + for arch in [ "i386" , "x86_64" , "ia64" , "ppc64", "ppc32", "ppc", "x86" , "s390x", "s390" , "386" , "amd" ]: name = name.replace("%s%s" % ( separator , arch ),"") return name @@ -792,8 +789,10 @@ class Importer: return "ia64" if dirname.find("i386") != -1 or dirname.find("386") != -1 or dirname.find("x86") != -1: return "i386" - if dirname.find("s390") != -1: + if dirname.find("s390x") != -1: return "s390x" + if dirname.find("s390") != -1: + return "s390" if dirname.find("ppc64") != -1 or dirname.find("chrp") != -1: return "ppc64" if dirname.find("ppc32") != -1: @@ -828,6 +827,7 @@ def guess_breed(kerneldir,path): [ 'Packages' , "redhat" ], [ 'Fedora' , "redhat" ], [ 'Server' , "redhat" ], + [ 'Client' , "redhat" ], [ 'setup.exe' , "windows" ], ] guess = None @@ -914,7 +914,7 @@ class BaseImporter: for x in fnames: if self.match_kernelarch_file(x): # print _("- kernel header found: %s") % x - for arch in [ "i386" , "x86_64" , "ia64" , "ppc64", "ppc", "s390x" ]: + for arch in [ "i386" , "x86_64" , "ia64" , "ppc64", "ppc", "s390", "s390x" ]: if x.find(arch) != -1: foo[arch] = 1 for arch in [ "i686" , "amd64" ]: @@ -986,8 +986,11 @@ class RedHatImporter ( BaseImporter ) : data = glob.glob(os.path.join(self.get_pkgdir(), "*release-*")) data2 = [] for x in data: - if x.find("generic") == -1: - data2.append(x) + b = os.path.basename(x) + if b.find("fedora") != -1 or \ + b.find("redhat") != -1 or \ + b.find("centos") != -1: + data2.append(x) return data2 # ================================================================ @@ -1099,13 +1102,14 @@ class RedHatImporter ( BaseImporter ) : # OS_VERSION next # OS_VERSION.MINOR next # ARCH/default.ks next - # default.ks finally. + # FLAVOR.ks next kickstarts = [ "%s/%s/%s.%i.ks" % (kickbase,arch,os_version,int(minor)), "%s/%s/%s.ks" % (kickbase,arch,os_version), "%s/%s.%i.ks" % (kickbase,os_version,int(minor)), "%s/%s.ks" % (kickbase,os_version), "%s/%s/default.ks" % (kickbase,arch), + "%s/%s.ks" % (kickbase,flavor), ] for kickstart in kickstarts: if os.path.exists(kickstart): diff --git a/cobbler/action_litesync.py b/cobbler/action_litesync.py index b040db0a..7773455e 100644 --- a/cobbler/action_litesync.py +++ b/cobbler/action_litesync.py @@ -46,11 +46,11 @@ class BootLiteSync: Handles conversion of internal state to the tftpboot tree layout """ - def __init__(self,config): + def __init__(self,config,verbose=False): """ Constructor """ - self.verbose = True + self.verbose = verbose self.config = config self.distros = config.distros() self.profiles = config.profiles() @@ -58,7 +58,7 @@ class BootLiteSync: self.images = config.images() self.settings = config.settings() self.repos = config.repos() - self.sync = config.api.get_sync() + self.sync = config.api.get_sync(verbose) def add_single_distro(self, name): # get the distro record @@ -101,7 +101,7 @@ class BootLiteSync: # get the profile object: profile = self.profiles.find(name=name) if profile is None: - raise CX(_("error in profile lookup")) + raise CX(_("error in profile lookup for %s" % name)) # rebuild the yum configuration files for any attached repos # generate any templates listed in the distro self.sync.pxegen.write_templates(profile) @@ -160,10 +160,6 @@ class BootLiteSync: system_record = self.systems.find(name=name) # delete contents of kickstarts_sys/$name in webdir system_record = self.systems.find(name=name) - # delete any kickstart files related to this system - for (name,interface) in system_record.interfaces.iteritems(): - filename = utils.get_config_filename(system_record,interface=name) - utils.rmtree(os.path.join(self.settings.webdir, "kickstarts_sys", filename)) if self.settings.manage_dhcp: if self.settings.omapi_enabled: @@ -179,8 +175,12 @@ class BootLiteSync: distro = self.distros.find(name=profile.distro) if distro is not None and distro in [ "ia64", "IA64"]: itanic = True - if not itanic: - utils.rmfile(os.path.join(bootloc, "pxelinux.cfg", filename)) - else: - utils.rmfile(os.path.join(bootloc, filename)) + + for (name,interface) in system_record.interfaces.iteritems(): + filename = utils.get_config_filename(system_record,interface=name) + + if not itanic: + utils.rmfile(os.path.join(bootloc, "pxelinux.cfg", filename)) + else: + utils.rmfile(os.path.join(bootloc, filename)) diff --git a/cobbler/action_power.py b/cobbler/action_power.py index 9db4535c..789750a1 100644 --- a/cobbler/action_power.py +++ b/cobbler/action_power.py @@ -31,6 +31,7 @@ import os.path import sub_process import sys import traceback +import time import utils from cexceptions import * @@ -106,7 +107,15 @@ class PowerTool: #if not os.path.exists(tool_needed): # print "warning: %s does not seem to be installed" % tool_needed - rc = sub_process.call(cmd, shell=False, close_fds=True) + # Try the power command 5 times before giving up. + # Some power switches are flakey + for x in range(0,5): + rc = sub_process.call(cmd, shell=False, close_fds=True) + if rc == 0: + break + else: + time.sleep(2) + if not rc == 0: raise CX("command failed (rc=%s), please validate the physical setup and cobbler config" % rc) @@ -137,6 +146,8 @@ class PowerTool: "lpar" : os.path.join(powerdir,"power_lpar.template"), "bladecenter": os.path.join(powerdir,"power_bladecenter.template"), "virsh" : os.path.join(powerdir,"power_virsh.template"), + "integrity" : os.path.join(powerdir,"power_integrity.template"), + "wti" : os.path.join(powerdir,"power_wti.template"), } result = map.get(self.system.power_type, "") diff --git a/cobbler/action_reposync.py b/cobbler/action_reposync.py index 4bb484c5..14ef1112 100644 --- a/cobbler/action_reposync.py +++ b/cobbler/action_reposync.py @@ -345,6 +345,24 @@ class RepoSync: if rc !=0: raise CX(_("cobbler reposync failed")) + repodata_path = os.path.join(dest_path, "repodata") + + if not os.path.exists("/usr/bin/wget"): + raise CX(_("no /usr/bin/wget found, please install wget")) + + cmd2 = "/usr/bin/wget -q %s/repodata/comps.xml -O /dev/null" % (repo_mirror) + rc = sub_process.call(cmd2, shell=True, close_fds=True) + if rc == 0: + if not os.path.isdir(repodata_path): + os.makedirs(repodata_path) + + cmd2 = "/usr/bin/wget -q %s/repodata/comps.xml -O %s/comps.xml" % (repo_mirror, repodata_path) + print _("- %s") % cmd2 + + rc = sub_process.call(cmd2, shell=True, close_fds=True) + if rc !=0: + raise CX(_("wget failed")) + # now run createrepo to rebuild the index if repo.mirror_locally: diff --git a/cobbler/action_sync.py b/cobbler/action_sync.py index f6e5bcd8..78f3c3d5 100644 --- a/cobbler/action_sync.py +++ b/cobbler/action_sync.py @@ -56,19 +56,22 @@ class BootSync: """ Constructor """ - self.verbose = verbose - self.config = config - self.api = config.api - self.distros = config.distros() - self.profiles = config.profiles() - self.systems = config.systems() - self.settings = config.settings() - self.repos = config.repos() - self.templar = templar.Templar(config) - self.pxegen = pxegen.PXEGen(config) - self.dns = dns - self.dhcp = dhcp - self.bootloc = utils.tftpboot_location() + self.verbose = verbose + self.config = config + self.api = config.api + self.distros = config.distros() + self.profiles = config.profiles() + self.systems = config.systems() + self.settings = config.settings() + self.repos = config.repos() + self.templar = templar.Templar(config) + self.pxegen = pxegen.PXEGen(config) + self.dns = dns + self.dhcp = dhcp + self.bootloc = utils.tftpboot_location() + self.pxegen.verbose = verbose + self.dns.verbose = verbose + self.dhcp.verbose = verbose def run(self): """ @@ -78,37 +81,67 @@ class BootSync: if not os.path.exists(self.bootloc): raise CX(_("cannot find directory: %s") % self.bootloc) + if self.verbose: + print "- running pre-sync triggers" + # run pre-triggers... - utils.run_triggers(None, "/var/lib/cobbler/triggers/sync/pre/*") + utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/sync/pre/*") # (paranoid) in case the pre-trigger modified any objects... + + if self.verbose: + print "- loading configuration" self.api.deserialize() + self.distros = self.config.distros() self.profiles = self.config.profiles() self.systems = self.config.systems() self.settings = self.config.settings() self.repos = self.config.repos() - self.pxegen = pxegen.PXEGen(self.config) # execute the core of the sync operation + + if self.verbose: + print "- cleaning trees" self.clean_trees() + + if self.verbose: + print "- copying bootloaders" self.pxegen.copy_bootloaders() + + if self.verbose: + print "- copying distros" self.pxegen.copy_distros() + + if self.verbose: + print "- copying images" self.pxegen.copy_images() self.pxegen.generate_windows_files() for x in self.systems: + if self.verbose: + print "- copying files for system: %s" % x.name self.pxegen.write_all_system_files(x) + if self.settings.manage_dhcp: + if self.verbose: + print "- rendering DHCP files" self.dhcp.write_dhcp_file() self.dhcp.regen_ethers() if self.settings.manage_dns: + if self.verbose: + print "- rendering DNS files" self.dns.regen_hosts() self.dns.write_dns_files() + + if self.verbose: + print "- generating PXE menu structure" self.pxegen.make_pxe_menu() self.pxegen.write_tftpd_rules(True) # run post-triggers - utils.run_triggers(None, "/var/lib/cobbler/triggers/sync/post/*") + if self.verbose: + print "- running post-sync triggers" + utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/sync/post/*") return True def clean_trees(self): @@ -128,14 +161,14 @@ class BootSync: path = os.path.join(self.settings.webdir,x) if os.path.isfile(path): if not x.endswith(".py"): - utils.rmfile(path) + utils.rmfile(path,verbose=self.verbose) if os.path.isdir(path): - if not x in ["web", "webui", "localmirror","repo_mirror","ks_mirror","images","links","repo_profile","repo_system","svc","rendered"] : + if not x in ["aux", "web", "webui", "localmirror","repo_mirror","ks_mirror","images","links","repo_profile","repo_system","svc","rendered"] : # delete directories that shouldn't exist - utils.rmtree(path) + utils.rmtree(path,verbose=self.verbose) if x in ["kickstarts","kickstarts_sys","images","systems","distros","profiles","repo_profile","repo_system","rendered"]: # clean out directory contents - utils.rmtree_contents(path) + utils.rmtree_contents(path,verbose=self.verbose) pxelinux_dir = os.path.join(self.bootloc, "pxelinux.cfg") images_dir = os.path.join(self.bootloc, "images") yaboot_bin_dir = os.path.join(self.bootloc, "ppc") @@ -143,21 +176,21 @@ class BootSync: s390_dir = os.path.join(self.bootloc, "s390x") rendered_dir = os.path.join(self.settings.webdir, "rendered") if not os.path.exists(pxelinux_dir): - utils.mkdir(pxelinux_dir) + utils.mkdir(pxelinux_dir,verbose=self.verbose) if not os.path.exists(images_dir): - utils.mkdir(images_dir) + utils.mkdir(images_dir,verbose=self.verbose) if not os.path.exists(rendered_dir): - utils.mkdir(rendered_dir) + utils.mkdir(rendered_dir,verbose=self.verbose) if not os.path.exists(yaboot_bin_dir): - utils.mkdir(yaboot_bin_dir) + utils.mkdir(yaboot_bin_dir,verbose=self.verbose) if not os.path.exists(yaboot_cfg_dir): - utils.mkdir(yaboot_cfg_dir) - utils.rmtree_contents(os.path.join(self.bootloc, "pxelinux.cfg")) - utils.rmtree_contents(os.path.join(self.bootloc, "images")) - utils.rmtree_contents(os.path.join(self.bootloc, "s390x")) - utils.rmtree_contents(os.path.join(self.bootloc, "ppc")) - utils.rmtree_contents(os.path.join(self.bootloc, "etc")) - utils.rmtree_contents(rendered_dir) + utils.mkdir(yaboot_cfg_dir,verbose=self.verbose) + utils.rmtree_contents(os.path.join(self.bootloc, "pxelinux.cfg"),verbose=self.verbose) + utils.rmtree_contents(os.path.join(self.bootloc, "images"),verbose=self.verbose) + utils.rmtree_contents(os.path.join(self.bootloc, "s390x"),verbose=self.verbose) + utils.rmtree_contents(os.path.join(self.bootloc, "ppc"),verbose=self.verbose) + utils.rmtree_contents(os.path.join(self.bootloc, "etc"),verbose=self.verbose) + utils.rmtree_contents(rendered_dir,verbose=self.verbose) diff --git a/cobbler/api.py b/cobbler/api.py index 84c2c638..ffc62644 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """ +import yaml import config import utils import action_sync @@ -48,7 +49,8 @@ import logging import time import random import os -import yaml +import xmlrpclib +import traceback ERROR = 100 INFO = 10 @@ -65,7 +67,7 @@ class BootAPI: __shared_state = {} __has_loaded = False - def __init__(self, log_settings={}): + def __init__(self, log_settings={}, is_cobblerd=False): """ Constructor """ @@ -82,6 +84,7 @@ class BootAPI: # level (and remote.py web service level) instead. random.seed() + self.is_cobblerd = is_cobblerd try: self.logger = self.__setup_logger("api") @@ -90,14 +93,16 @@ class BootAPI: # perms_ok is False return - self.logger_remote = self.__setup_logger("remote") + # FIMXE: conslidate into 1 server instance + self.selinux_enabled = utils.is_selinux_enabled() self.dist = utils.check_dist() self.os_version = utils.os_release() self.acl_engine = acls.AclEngine() - + BootAPI.__has_loaded = True + module_loader.load_modules() self._config = config.Config(self) @@ -118,9 +123,10 @@ class BootAPI: self.pxegen = pxegen.PXEGen(self._config) self.logger.debug("API handle initialized") self.perms_ok = True - + + def __setup_logger(self,name): - return utils.setup_logger(name, **self.log_settings) + return utils.setup_logger(name, is_cobblerd=self.is_cobblerd, **self.log_settings) def is_selinux_enabled(self): """ @@ -142,6 +148,28 @@ class BootAPI: return False return True + def _internal_cache_update(self, collection_type, name, remove=False): + """ + Update cobblerd so it won't have to ever reload the config, once started. + """ + # FIXME: take value from settings, use raw port + if self.is_cobblerd: + # don't signal yourself, that's asking for trouble. + return True + self.server = xmlrpclib.Server("http://127.0.0.1:%s" % self.settings().xmlrpc_port) + try: + if not remove: + self.server.internal_cache_update(collection_type, name) + else: + self.server.internal_cache_remove(collection_type, name) + except Exception, e: + if len(e.args) == 2 and e[0] == 111: + # if cobblerd is not running, no harm done, nothing to signal + pass + else: + raise CX("error contacting cobblerd") + return False + def last_modified_time(self): """ Returns the time of the last modification to cobbler, made by any @@ -181,14 +209,16 @@ class BootAPI: version_tuple -- something like [ 1, 3, 2 ] """ fd = open("/var/lib/cobbler/version") - data = yaml.load(fd.read()).next() + ydata = fd.read() fd.close() + data = yaml.load(ydata) if not extended: # for backwards compatibility and use with koan's comparisons elems = data["version_tuple"] + print elems return int(elems[0]) + 0.1*int(elems[1]) + 0.001*int(elems[2]) else: - return data + return data def clear(self): """ @@ -237,14 +267,10 @@ class BootAPI: def update(self): """ - This can be called when you expect a cobbler object - to have changed outside of your API call. It does not - have to be called before read operations but should be - called before write operations depending on the last - modification time. For the local API it is not needed. + This can be called is no longer used by cobbler. + And is here to just avoid breaking older scripts. """ - self.clear() - self.deserialize() + return True def copy_distro(self, ref, newname): self.log("copy_distro",[ref.name, newname]) @@ -266,46 +292,45 @@ class BootAPI: self.log("copy_image",[ref.name, newname]) return self._config.images().copy(ref,newname) - def remove_distro(self, ref, recursive=False): + def remove_distro(self, ref, recursive=False, delete=True, with_triggers=True, ): if type(ref) != str: self.log("remove_distro",[ref.name]) - return self._config.distros().remove(ref.name, recursive=recursive) + return self._config.distros().remove(ref.name, recursive=recursive, with_delete=delete, with_triggers=with_triggers) else: self.log("remove_distro",ref) - return self._config.distros().remove(ref, recursive=recursive) - + return self._config.distros().remove(ref, recursive=recursive, with_delete=delete, with_triggers=with_triggers) - def remove_profile(self,ref, recursive=False): + def remove_profile(self,ref, recursive=False, delete=True, with_triggers=True): if type(ref) != str: self.log("remove_profile",[ref.name]) - return self._config.profiles().remove(ref.name, recursive=recursive) + return self._config.profiles().remove(ref.name, recursive=recursive, with_delete=delete, with_triggers=with_triggers) else: self.log("remove_profile",ref) - return self._config.profiles().remove(ref, recursive=recursive) + return self._config.profiles().remove(ref, recursive=recursive, with_delete=delete, with_triggers=with_triggers) - def remove_system(self, ref, recursive=False): + def remove_system(self, ref, recursive=False, delete=True, with_triggers=True): if type(ref) != str: self.log("remove_system",[ref.name]) - return self._config.systems().remove(ref.name) + return self._config.systems().remove(ref.name, with_delete=delete, with_triggers=with_triggers) else: self.log("remove_system",ref) - return self._config.systems().remove(ref) + return self._config.systems().remove(ref, with_delete=delete, with_triggers=with_triggers) - def remove_repo(self, ref, recursive=False): + def remove_repo(self, ref, recursive=False, delete=True, with_triggers=True): if type(ref) != str: self.log("remove_repo",[ref.name]) - return self._config.repos().remove(ref.name) + return self._config.repos().remove(ref.name, with_delete=delete, with_triggers=with_triggers) else: self.log("remove_repo",ref) - return self._config.repos().remove(ref) + return self._config.repos().remove(ref, with_delete=delete, with_triggers=with_triggers) - def remove_image(self, ref, recursive=False): + def remove_image(self, ref, recursive=False, delete=True, with_triggers=True): if type(ref) != str: self.log("remove_image",[ref.name]) - return self._config.images().remove(ref.name, recursive=recursive) + return self._config.images().remove(ref.name, recursive=recursive, with_delete=delete, with_triggers=with_triggers) else: self.log("remove_image",ref) - return self._config.images().remove(ref, recursive=recursive) + return self._config.images().remove(ref, recursive=recursive, with_delete=delete, with_triggers=with_triggers) def rename_distro(self, ref, newname): self.log("rename_distro",[ref.name,newname]) @@ -347,29 +372,34 @@ class BootAPI: self.log("new_image",[is_subobject]) return self._config.new_image(is_subobject=is_subobject) - def add_distro(self, ref, check_for_duplicate_names=False): + def add_distro(self, ref, check_for_duplicate_names=False, save=True): self.log("add_distro",[ref.name]) - return self._config.distros().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) + rc = self._config.distros().add(ref,check_for_duplicate_names=check_for_duplicate_names,save=save) + return rc - def add_profile(self, ref, check_for_duplicate_names=False): + def add_profile(self, ref, check_for_duplicate_names=False,save=True): self.log("add_profile",[ref.name]) - return self._config.profiles().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) + rc = self._config.profiles().add(ref,check_for_duplicate_names=check_for_duplicate_names,save=save) + return rc - def add_system(self, ref, check_for_duplicate_names=False, check_for_duplicate_netinfo=False): + def add_system(self, ref, check_for_duplicate_names=False, check_for_duplicate_netinfo=False, save=True): self.log("add_system",[ref.name]) - return self._config.systems().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names,check_for_duplicate_netinfo=check_for_duplicate_netinfo) + rc = self._config.systems().add(ref,check_for_duplicate_names=check_for_duplicate_names,check_for_duplicate_netinfo=check_for_duplicate_netinfo,save=save) + return rc - def add_repo(self, ref, check_for_duplicate_names=False): + def add_repo(self, ref, check_for_duplicate_names=False,save=True): self.log("add_repo",[ref.name]) - return self._config.repos().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) - - def add_image(self, ref, check_for_duplicate_names=False): + rc = self._config.repos().add(ref,check_for_duplicate_names=check_for_duplicate_names,save=save) + return rc + + def add_image(self, ref, check_for_duplicate_names=False,save=True): self.log("add_image",[ref.name]) - return self._config.images().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) + rc = self._config.images().add(ref,check_for_duplicate_names=check_for_duplicate_names,save=save) + return rc def find_distro(self, name=None, return_list=False, no_errors=False, **kargs): return self._config.distros().find(name=name, return_list=return_list, no_errors=no_errors, **kargs) - + def find_profile(self, name=None, return_list=False, no_errors=False, **kargs): return self._config.profiles().find(name=name, return_list=return_list, no_errors=no_errors, **kargs) @@ -389,12 +419,11 @@ class BootAPI: results1 = collector() results2 = [] for x in results1: - print "INPUT: %s ACTUAL: %s" % (mtime, x.mtime) if x.mtime == 0 or x.mtime >= mtime: if not collapse: - results2.append(results1) + results2.append(x) else: - results2.append(results1.to_datastruct()) + results2.append(x.to_datastruct()) return results2 def get_distros_since(self,mtime,collapse=False): @@ -511,7 +540,7 @@ class BootAPI: validator = action_validate.Validate(self._config) return validator.run() - def sync(self): + def sync(self,verbose=False): """ Take the values currently written to the configuration files in /etc, and /var, and build out the information tree found in @@ -519,10 +548,10 @@ class BootAPI: saved with serialize() will NOT be synchronized with this command. """ self.log("sync") - sync = self.get_sync() + sync = self.get_sync(verbose=verbose) return sync.run() - def get_sync(self): + def get_sync(self,verbose=False): self.dhcp = self.get_module_from_file( "dhcp", "module", @@ -533,7 +562,7 @@ class BootAPI: "module", "manage_bind" ).get_manager(self._config) - return action_sync.BootSync(self._config,dhcp=self.dhcp,dns=self.dns) + return action_sync.BootSync(self._config,dhcp=self.dhcp,dns=self.dns,verbose=verbose) def reposync(self, name=None, tries=1, nofail=False): """ @@ -646,10 +675,10 @@ class BootAPI: self.log("authorize",[user,resource,arg1,arg2,rc],debug=True) return rc - def build_iso(self,iso=None,profiles=None,systems=None,tempdir=None): + def build_iso(self,iso=None,profiles=None,systems=None,tempdir=None,distro=None,standalone=None,source=None): builder = action_buildiso.BuildIso(self._config) return builder.run( - iso=iso, profiles=profiles, systems=systems, tempdir=tempdir + iso=iso, profiles=profiles, systems=systems, tempdir=tempdir, distro=distro, standalone=standalone, source=source ) def replicate(self, cobbler_master = None, sync_all=False, sync_kickstarts=False, sync_trees=False, sync_repos=False, sync_triggers=False, systems=False): @@ -697,7 +726,7 @@ class BootAPI: Cycles power on a system that has power management configured. """ self.power_off(system, user, password) - time.sleep(1) + time.sleep(5) return self.power_on(system, user, password) def get_os_details(self): diff --git a/cobbler/cobbler.py b/cobbler/cobbler.py index c896d266..5bbbcef5 100755 --- a/cobbler/cobbler.py +++ b/cobbler/cobbler.py @@ -42,7 +42,7 @@ I18N_DOMAIN = "cobbler" class BootCLI: def __init__(self): - self.api = api.BootAPI() + self.api = api.BootAPI(is_cobblerd=False) self.loader = commands.FunctionLoader(self.api) climods = self.api.get_modules_in_category("cli") for mod in climods: diff --git a/cobbler/cobblerd.py b/cobbler/cobblerd.py index 36645750..8abe8329 100644 --- a/cobbler/cobblerd.py +++ b/cobbler/cobblerd.py @@ -41,25 +41,14 @@ import remote def main(): core(logger=None) -def core(logger=None): +def core(api): - bootapi = cobbler_api.BootAPI() + bootapi = api settings = bootapi.settings() - syslog_port = settings.syslog_port xmlrpc_port = settings.xmlrpc_port - xmlrpc_port2 = settings.xmlrpc_rw_port - - pid = os.fork() regen_ss_file() - - if pid == 0: - # part one: XMLRPC -- which may be just read-only or both read-only and read-write - do_xmlrpc_tasks(bootapi, settings, xmlrpc_port, xmlrpc_port2, logger) - else: - # part two: syslog, or syslog+avahi if avahi is installed - do_other_tasks(bootapi, settings, syslog_port, logger) - os.waitpid(pid, 0) + do_xmlrpc_tasks(bootapi, settings, xmlrpc_port) def regen_ss_file(): # this is only used for Kerberos auth at the moment. @@ -76,44 +65,23 @@ def regen_ss_file(): os.system("chown apache /var/lib/cobbler/web.ss") return 1 -def do_xmlrpc_tasks(bootapi, settings, xmlrpc_port, xmlrpc_port2, logger): - if str(settings.xmlrpc_rw_enabled) != "0": - pid2 = os.fork() - if pid2 == 0: - do_mandatory_xmlrpc_tasks(bootapi, settings, xmlrpc_port, logger) - else: - do_xmlrpc_rw(bootapi, settings, xmlrpc_port2, logger) - os.waitpid(pid2, 0) - else: - logger.debug("xmlrpc_rw is disabled in the settings file") - do_mandatory_xmlrpc_tasks(bootapi, settings, xmlrpc_port, logger) - -def do_mandatory_xmlrpc_tasks(bootapi,settings,xmlrpc_port,logger): - #pid3 = os.fork() - #if pid3 == 0: - # do_xmlrpc(bootapi, settings, xmlrpc_port, logger) - #else: - # # NOTE: this shouldn't be enabled unless we decide - # # to use it for something. - # # do_xmlrpc_unix(bootapi, settings, logger) - # pass - do_xmlrpc(bootapi, settings, xmlrpc_port, logger) - - -def do_other_tasks(bootapi, settings, syslog_port, logger): - - # FUTURE: this should also start the Web UI, if the dependencies - # are available. - - if os.path.exists("/usr/bin/avahi-publish-service"): - pid2 = os.fork() - if pid2 == 0: - do_syslog(bootapi, settings, syslog_port, logger) - else: - do_avahi(bootapi, settings, logger) - os.waitpid(pid2, 0) - else: - do_syslog(bootapi, settings, syslog_port, logger) +def do_xmlrpc_tasks(bootapi, settings, xmlrpc_port): + do_xmlrpc_rw(bootapi, settings, xmlrpc_port) + +#def do_other_tasks(bootapi, settings, syslog_port, logger): +# +# # FUTURE: this should also start the Web UI, if the dependencies +# # are available. +# +# if os.path.exists("/usr/bin/avahi-publish-service"): +# pid2 = os.fork() +# if pid2 == 0: +# do_syslog(bootapi, settings, syslog_port, logger) +# else: +# do_avahi(bootapi, settings, logger) +# os.waitpid(pid2, 0) +# else: +# do_syslog(bootapi, settings, syslog_port, logger) def log(logger,msg): @@ -122,106 +90,34 @@ def log(logger,msg): else: print >>sys.stderr, msg -def do_avahi(bootapi, settings, logger): - # publish via zeroconf. This command will not terminate - log(logger, "publishing avahi service") - cmd = [ "/usr/bin/avahi-publish-service", - "cobblerd", - "_http._tcp", - "%s" % settings.xmlrpc_port ] - proc = sub_process.Popen(cmd, shell=False, stderr=sub_process.PIPE, stdout=sub_process.PIPE, close_fds=True) - proc.communicate()[0] - log(logger, "avahi service terminated") - - -def do_xmlrpc(bootapi, settings, port, logger): - - # This is the simple XMLRPC API we provide to koan and other - # apps that do not need to manage Cobbler's config - - xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerXMLRPCInterface,False) - - server = remote.CobblerXMLRPCServer(('', port)) - server.logRequests = 0 # don't print stuff - log(logger, "XMLRPC running on %s" % port) - server.register_instance(xinterface) - - while True: - try: - server.serve_forever() - except IOError: - # interrupted? try to serve again - time.sleep(0.5) +#def do_avahi(bootapi, settings, logger): +# # publish via zeroconf. This command will not terminate +# log(logger, "publishing avahi service") +# cmd = [ "/usr/bin/avahi-publish-service", +# "cobblerd", +# "_http._tcp", +# "%s" % settings.xmlrpc_port ] +# proc = sub_process.Popen(cmd, shell=False, stderr=sub_process.PIPE, stdout=sub_process.PIPE, close_fds=True) +# proc.communicate()[0] +# log(logger, "avahi service terminated") -def do_xmlrpc_rw(bootapi,settings,port,logger): - xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerReadWriteXMLRPCInterface,True) - server = remote.CobblerReadWriteXMLRPCServer(('127.0.0.1', port)) - server.logRequests = 0 # don't print stuff - logger.debug("XMLRPC (read-write variant) running on %s" % port) - server.register_instance(xinterface) +def do_xmlrpc_rw(bootapi,settings,port): - while True: - try: - server.serve_forever() - except IOError: - # interrupted? try to serve again - time.sleep(0.5) - -def do_xmlrpc_unix(bootapi,settings,logger): - - xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerReadWriteXMLRPCInterface,True) - SOCKT = "/var/lib/cobbler/sock" - server = xmlrpclib2.UnixXMLRPCServer(SOCKT) + xinterface = remote.ProxiedXMLRPCInterface(bootapi,remote.CobblerXMLRPCInterface,True) + server = remote.CobblerXMLRPCServer(('127.0.0.1', port)) server.logRequests = 0 # don't print stuff - logger.debug("XMLRPC (socket variant) available on %s" % SOCKT) + xinterface.logger.debug("XMLRPC running on %s" % port) server.register_instance(xinterface) while True: try: + print "SERVING!" server.serve_forever() except IOError: # interrupted? try to serve again time.sleep(0.5) -def do_syslog(bootapi, settings, port, logger): - - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.bind(("0.0.0.0", port)) - log(logger, "syslog running on %s" % port) - - buf = 1024 - - while 1: - data, addr = s.recvfrom(buf) - (ip, port) = addr - system = bootapi.systems().find(ip_address = ip) - if not system: - usename = ip - else: - usename = system.name - - if not data: - break - else: - logfile = open("/var/log/cobbler/syslog/%s" % usename, "a+") - t = time.localtime() - # write numeric time - seconds = str(time.mktime(t)) - logfile.write(seconds) - logfile.write("\t") - # write string time - timestr = str(time.asctime(t)) - logfile.write(timestr) - logfile.write("\t") - # write the IP address of the client - logfile.write(ip) - logfile.write("\t") - # write the data - logfile.write(data) - logfile.write("\n") - logfile.close() - if __name__ == "__main__": #main() diff --git a/cobbler/collection.py b/cobbler/collection.py index 564a9dec..3bc15cc7 100644 --- a/cobbler/collection.py +++ b/cobbler/collection.py @@ -148,6 +148,13 @@ class Collection(serializable.Serializable): def copy(self,ref,newname): ref.name = newname ref.uid = self.config.generate_uid() + if ref.COLLECTION_TYPE == "system": + # this should only happen for systems + for iname in ref.interfaces.keys(): + # clear all these out to avoid DHCP/DNS conflicts + ref.set_dns_name("",iname) + ref.set_mac_address("",iname) + ref.set_ip_address("",iname) return self.add(ref,save=True,with_copy=True,with_triggers=True,with_sync=True,check_for_duplicate_names=True,check_for_duplicate_netinfo=False) def rename(self,ref,newname,with_sync=True,with_triggers=True): @@ -251,7 +258,7 @@ class Collection(serializable.Serializable): self.log_func("saving %s %s" % (self.collection_type(), ref.name)) # failure of a pre trigger will prevent the object from being added if with_triggers: - self._run_triggers(ref,"/var/lib/cobbler/triggers/add/%s/pre/*" % self.collection_type()) + self._run_triggers(self.api, ref,"/var/lib/cobbler/triggers/add/%s/pre/*" % self.collection_type()) self.listing[ref.name.lower()] = ref # save just this item if possible, if not, save @@ -277,17 +284,22 @@ class Collection(serializable.Serializable): # save the tree, so if neccessary, scripts can examine it. if with_triggers: - self._run_triggers(ref,"/var/lib/cobbler/triggers/add/%s/post/*" % self.collection_type()) - + self._run_triggers(self.api, ref,"/var/lib/cobbler/triggers/add/%s/post/*" % self.collection_type()) + + # update children cache in parent object parent = ref.get_parent() if parent != None: parent.children[ref.name] = ref + # signal remote cobblerd to update it's cache of this item. + if save and not self.api.is_cobblerd: + self.api._internal_cache_update(ref.COLLECTION_TYPE,ref.name) + return True - def _run_triggers(self,ref,globber): - return utils.run_triggers(ref,globber) + def _run_triggers(self,api_handle,ref,globber): + return utils.run_triggers(api_handle,ref,globber) def __duplication_checks(self,ref,check_for_duplicate_names,check_for_duplicate_netinfo): """ @@ -307,6 +319,10 @@ class Collection(serializable.Serializable): match = self.api.find_distro(ref.name) elif isinstance(ref, item_repo.Repo): match = self.api.find_repo(ref.name) + elif isinstance(ref, item_image.Image): + match = self.api.find_image(ref.name) + else: + raise CX("internal error, unknown object type") if match: raise CX(_("An object already exists with that name. Try 'edit'?")) diff --git a/cobbler/collection_distros.py b/cobbler/collection_distros.py index 1ca3ca92..ae75f407 100644 --- a/cobbler/collection_distros.py +++ b/cobbler/collection_distros.py @@ -60,11 +60,11 @@ class Distros(collection.Collection): if recursive: kids = obj.get_children() for k in kids: - self.config.api.remove_profile(k, recursive=True) + self.config.api.remove_profile(k, recursive=recursive, delete=with_delete, with_triggers=with_triggers) if with_delete: if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/distro/pre/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/distro/pre/*") if with_sync: lite_sync = action_litesync.BootLiteSync(self.config) lite_sync.remove_single_profile(name) @@ -75,7 +75,7 @@ class Distros(collection.Collection): if with_delete: self.log_func("deleted distro %s" % name) if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/distro/post/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/distro/post/*") # look through all mirrored directories and find if any directory is holding # this particular distribution's kernel and initrd @@ -99,7 +99,11 @@ class Distros(collection.Collection): if not found: utils.rmtree(path) + if with_delete and not self.api.is_cobblerd: + self.api._internal_cache_update("distro", name, remove=True) + return True - raise CX(_("cannot delete object that does not exist: %s") % name) + #if not recursive: + # raise CX(_("cannot delete object that does not exist: %s") % name) diff --git a/cobbler/collection_images.py b/cobbler/collection_images.py index 12cd5c60..2dfeeac1 100644 --- a/cobbler/collection_images.py +++ b/cobbler/collection_images.py @@ -39,11 +39,25 @@ class Images(collection.Collection): # but is left in for consistancy in the API. Unused. name = name.lower() + + # first see if any Groups use this distro + if not recursive: + for v in self.config.systems(): + if v.image is not None and v.image.lower() == name: + raise CX(_("removal would orphan system: %s") % v.name) + obj = self.find(name=name) + if obj is not None: + + if recursive: + kids = obj.get_children() + for k in kids: + self.config.api.remove_system(k, recursive=True) + if with_delete: if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/image/pre/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/image/pre/*") if with_sync: lite_sync = action_litesync.BootLiteSync(self.config) lite_sync.remove_single_image(name) @@ -54,7 +68,11 @@ class Images(collection.Collection): if with_delete: self.log_func("deleted repo %s" % name) if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/image/post/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/image/post/*") return True - raise CX(_("cannot delete an object that does not exist: %s") % name) + if with_delete and not self.api.is_cobblerd: + self.api._internal_cache_update("image", name, remove=True) + + #else: + # raise CX(_("cannot delete an object that does not exist: %s") % name) diff --git a/cobbler/collection_profiles.py b/cobbler/collection_profiles.py index 5554eeab..bb27dea4 100644 --- a/cobbler/collection_profiles.py +++ b/cobbler/collection_profiles.py @@ -58,13 +58,13 @@ class Profiles(collection.Collection): kids = obj.get_children() for k in kids: if k.COLLECTION_TYPE == "profile": - self.config.api.remove_profile(k, recursive=True) + self.config.api.remove_profile(k, recursive=recursive, delete=with_delete, with_triggers=with_triggers) else: - self.config.api.remove_system(k) + self.config.api.remove_system(k, recursive=recursive, delete=with_delete, with_triggers=with_triggers) if with_delete: if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/profile/pre/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/profile/pre/*") if with_sync: lite_sync = action_litesync.BootLiteSync(self.config) lite_sync.remove_single_profile(name) @@ -73,7 +73,12 @@ class Profiles(collection.Collection): if with_delete: self.log_func("deleted profile %s" % name) if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/profile/post/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/profile/post/*") + + if with_delete and not self.api.is_cobblerd: + self.api._internal_cache_update("profile", name, remove=True) + return True - raise CX(_("cannot delete an object that does not exist: %s") % name) + #if not recursive: + # raise CX(_("cannot delete an object that does not exist: %s") % name) diff --git a/cobbler/collection_repos.py b/cobbler/collection_repos.py index 8ce150d4..3b2c1c20 100644 --- a/cobbler/collection_repos.py +++ b/cobbler/collection_repos.py @@ -56,7 +56,7 @@ class Repos(collection.Collection): if obj is not None: if with_delete: if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/repo/pre/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/repo/pre/*") del self.listing[name] self.config.serialize_delete(self, obj) @@ -64,12 +64,16 @@ class Repos(collection.Collection): if with_delete: self.log_func("deleted repo %s" % name) if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/repo/post/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/repo/post/*") path = "/var/www/cobbler/repo_mirror/%s" % obj.name if os.path.exists(path): utils.rmtree(path) + if with_delete and not self.api.is_cobblerd: + self.api._internal_cache_update("repo", name, remove=True) + return True - raise CX(_("cannot delete an object that does not exist: %s") % name) + #if not recursive: + # raise CX(_("cannot delete an object that does not exist: %s") % name) diff --git a/cobbler/collection_systems.py b/cobbler/collection_systems.py index 211c5da9..5a628248 100644 --- a/cobbler/collection_systems.py +++ b/cobbler/collection_systems.py @@ -52,7 +52,7 @@ class Systems(collection.Collection): if with_delete: if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/system/pre/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/system/pre/*") if with_sync: lite_sync = action_litesync.BootLiteSync(self.config) lite_sync.remove_single_system(name) @@ -61,9 +61,13 @@ class Systems(collection.Collection): if with_delete: self.log_func("deleted system %s" % name) if with_triggers: - self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/system/post/*") + self._run_triggers(self.config.api, obj, "/var/lib/cobbler/triggers/delete/system/post/*") + + if with_delete and not self.api.is_cobblerd: + self.api._internal_cache_update("system", name, remove=True) return True - - raise CX(_("cannot delete an object that does not exist: %s") % name) + + #if not recursive: + # raise CX(_("cannot delete an object that does not exist: %s") % name) diff --git a/cobbler/commands.py b/cobbler/commands.py index 98a3a3a7..4bb6ec33 100644 --- a/cobbler/commands.py +++ b/cobbler/commands.py @@ -307,7 +307,7 @@ class CobblerFunction: if not self.options.name: raise CX(_("name is required")) if not recursive: - collect_fn().remove(self.options.name,with_delete=True) + collect_fn().remove(self.options.name,with_delete=True,recursive=False) else: collect_fn().remove(self.options.name,with_delete=True,recursive=True) return None # signal that we want no further processing on the object @@ -332,6 +332,12 @@ class CobblerFunction: raise CX(_("object not found")) return obj + try: + # catch some invalid executions of the CLI + getattr(self, "options") + except: + sys.exit(1) + if not self.options.name: raise CX(_("name is required")) @@ -377,8 +383,18 @@ class CobblerFunction: if "copy" in self.args: if self.options.newname: - obj = obj.make_clone() - obj.set_name(self.options.newname) + # FIXME: this should just use the copy function! + if obj.COLLECTION_TYPE == "distro": + return self.api.copy_distro(obj, self.options.newname) + if obj.COLLECTION_TYPE == "profile": + return self.api.copy_profile(obj, self.options.newname) + if obj.COLLECTION_TYPE == "system": + return self.api.copy_system(obj, self.options.newname) + if obj.COLLECTION_TYPE == "repo": + return self.api.copy_repo(obj, self.options.newname) + if obj.COLLECTION_TYPE == "image": + return self.api.copy_image(obj, self.options.newname) + raise CX(_("internal error, don't know how to copy")) else: raise CX(_("--newname is required")) diff --git a/cobbler/config.py b/cobbler/config.py index 78a4d7ca..2608b84d 100644 --- a/cobbler/config.py +++ b/cobbler/config.py @@ -44,7 +44,7 @@ import settings import serializer from utils import _ - +from cexceptions import * class Config: @@ -204,7 +204,10 @@ class Config: """ Load the object hierachy from disk, using the filenames referenced in each object. """ - serializer.deserialize(self._settings) + try: + serializer.deserialize(self._settings) + except: + raise CX("/etc/cobbler/settings is not a valid YAML file") serializer.deserialize(self._distros) serializer.deserialize(self._repos) serializer.deserialize(self._profiles) diff --git a/cobbler/demo_connect.py b/cobbler/demo_connect.py index b9d5192e..12627b1e 100644 --- a/cobbler/demo_connect.py +++ b/cobbler/demo_connect.py @@ -31,7 +31,7 @@ if __name__ == "__main__": # NOTE: if you've changed your xmlrpc_rw port or # disabled xmlrpc_rw this test probably won't work - sp = ServerProxy("http://127.0.0.1:25152") + sp = ServerProxy("http://127.0.0.1:25151") (options, args) = p.parse_args() print "- trying to login with user=%s" % options.user token = sp.login(options.user,options.password) diff --git a/cobbler/item.py b/cobbler/item.py index a4ecea5b..31697a2f 100644 --- a/cobbler/item.py +++ b/cobbler/item.py @@ -138,7 +138,7 @@ class Item(serializable.Serializable): like authz_ownership, which ships with Cobbler but is off by default. Consult the Wiki docs for more info on CustomizableAuthorization. """ - owners = utils.input_string_or_list(data) + owners = utils.input_string_or_list(data, delim=" ") self.owners = owners return True @@ -196,7 +196,8 @@ class Item(serializable.Serializable): Assigns a list of configuration management classes that can be assigned to any object, such as those used by Puppet's external_nodes feature. """ - self.mgmt_classes = utils.input_string_or_list(mgmt_classes) + mgmt_classes_split = utils.input_string_or_list(mgmt_classes, delim=" ") + self.mgmt_classes = utils.input_string_or_list(mgmt_classes_split) return True def set_template_files(self,template_files,inplace=False): diff --git a/cobbler/item_distro.py b/cobbler/item_distro.py index 9b34d2e7..9be4f7d3 100644 --- a/cobbler/item_distro.py +++ b/cobbler/item_distro.py @@ -41,24 +41,25 @@ class Distro(item.Item): """ Reset this object. """ - self.name = None - self.uid = "" - self.owners = self.settings.default_ownership - self.kernel = None - self.initrd = None - self.kernel_options = {} - self.kernel_options_post = {} - self.ks_meta = {} - self.arch = 'i386' - self.breed = 'redhat' - self.os_version = '' - self.source_repos = [] - self.mgmt_classes = [] - self.depth = 0 - self.template_files = {} - self.comment = "" - self.tree_build_time = 0 - self.redhat_management_key = "<<inherit>>" + self.name = None + self.uid = "" + self.owners = self.settings.default_ownership + self.kernel = None + self.initrd = None + self.kernel_options = {} + self.kernel_options_post = {} + self.ks_meta = {} + self.arch = 'i386' + self.breed = 'redhat' + self.os_version = '' + self.source_repos = [] + self.mgmt_classes = [] + self.depth = 0 + self.template_files = {} + self.comment = "" + self.tree_build_time = 0 + self.redhat_management_key = "<<inherit>>" + self.redhat_management_server = "<<inherit>>" def make_clone(self): ds = self.to_datastruct() @@ -77,23 +78,24 @@ class Distro(item.Item): """ Modify this object to take on values in seed_data """ - self.parent = self.load_item(seed_data,'parent') - self.name = self.load_item(seed_data,'name') - self.owners = self.load_item(seed_data,'owners',self.settings.default_ownership) - self.kernel = self.load_item(seed_data,'kernel') - self.initrd = self.load_item(seed_data,'initrd') - self.kernel_options = self.load_item(seed_data,'kernel_options') - self.kernel_options_post = self.load_item(seed_data,'kernel_options_post') - self.ks_meta = self.load_item(seed_data,'ks_meta') - self.arch = self.load_item(seed_data,'arch','i386') - self.breed = self.load_item(seed_data,'breed','redhat') - self.os_version = self.load_item(seed_data,'os_version','') - self.source_repos = self.load_item(seed_data,'source_repos',[]) - self.depth = self.load_item(seed_data,'depth',0) - self.mgmt_classes = self.load_item(seed_data,'mgmt_classes',[]) - self.template_files = self.load_item(seed_data,'template_files',{}) - self.comment = self.load_item(seed_data,'comment') - self.redhat_management_key = self.load_item(seed_data,'redhat_management_key',"<<inherit>>") + self.parent = self.load_item(seed_data,'parent') + self.name = self.load_item(seed_data,'name') + self.owners = self.load_item(seed_data,'owners',self.settings.default_ownership) + self.kernel = self.load_item(seed_data,'kernel') + self.initrd = self.load_item(seed_data,'initrd') + self.kernel_options = self.load_item(seed_data,'kernel_options') + self.kernel_options_post = self.load_item(seed_data,'kernel_options_post') + self.ks_meta = self.load_item(seed_data,'ks_meta') + self.arch = self.load_item(seed_data,'arch','i386') + self.breed = self.load_item(seed_data,'breed','redhat') + self.os_version = self.load_item(seed_data,'os_version','') + self.source_repos = self.load_item(seed_data,'source_repos',[]) + self.depth = self.load_item(seed_data,'depth',0) + self.mgmt_classes = self.load_item(seed_data,'mgmt_classes',[]) + self.template_files = self.load_item(seed_data,'template_files',{}) + self.comment = self.load_item(seed_data,'comment') + self.redhat_management_key = self.load_item(seed_data,'redhat_management_key',"<<inherit>>") + self.redhat_management_server = self.load_item(seed_data,'redhat_management_server',"<<inherit>>") # backwards compatibility enforcement self.set_arch(self.arch) @@ -159,6 +161,9 @@ class Distro(item.Item): def set_redhat_management_key(self,key): return utils.set_redhat_management_key(self,key) + + def set_redhat_management_server(self,server): + return utils.set_redhat_management_server(self,server) def set_source_repos(self, repos): """ @@ -209,27 +214,28 @@ class Distro(item.Item): Return a serializable datastructure representation of this object. """ return { - 'name' : self.name, - 'kernel' : self.kernel, - 'initrd' : self.initrd, - 'kernel_options' : self.kernel_options, - 'kernel_options_post' : self.kernel_options_post, - 'ks_meta' : self.ks_meta, - 'mgmt_classes' : self.mgmt_classes, - 'template_files' : self.template_files, - 'arch' : self.arch, - 'breed' : self.breed, - 'os_version' : self.os_version, - 'source_repos' : self.source_repos, - 'parent' : self.parent, - 'depth' : self.depth, - 'owners' : self.owners, - 'comment' : self.comment, - 'tree_build_time' : self.tree_build_time, - 'ctime' : self.ctime, - 'mtime' : self.mtime, - 'uid' : self.uid, - 'redhat_management_key' : self.redhat_management_key + 'name' : self.name, + 'kernel' : self.kernel, + 'initrd' : self.initrd, + 'kernel_options' : self.kernel_options, + 'kernel_options_post' : self.kernel_options_post, + 'ks_meta' : self.ks_meta, + 'mgmt_classes' : self.mgmt_classes, + 'template_files' : self.template_files, + 'arch' : self.arch, + 'breed' : self.breed, + 'os_version' : self.os_version, + 'source_repos' : self.source_repos, + 'parent' : self.parent, + 'depth' : self.depth, + 'owners' : self.owners, + 'comment' : self.comment, + 'tree_build_time' : self.tree_build_time, + 'ctime' : self.ctime, + 'mtime' : self.mtime, + 'uid' : self.uid, + 'redhat_management_key' : self.redhat_management_key, + 'redhat_management_server' : self.redhat_management_server } def printable(self): @@ -257,28 +263,30 @@ class Distro(item.Item): buf = buf + _("owners : %s\n") % self.owners buf = buf + _("post kernel options : %s\n") % self.kernel_options_post buf = buf + _("redhat mgmt key : %s\n") % self.redhat_management_key + buf = buf + _("redhat mgmt server : %s\n") % self.redhat_management_server buf = buf + _("template files : %s\n") % self.template_files return buf def remote_methods(self): return { - 'name' : self.set_name, - 'kernel' : self.set_kernel, - 'initrd' : self.set_initrd, - 'kopts' : self.set_kernel_options, - 'kopts-post' : self.set_kernel_options_post, - 'kopts_post' : self.set_kernel_options_post, - 'arch' : self.set_arch, - 'ksmeta' : self.set_ksmeta, - 'breed' : self.set_breed, - 'os-version' : self.set_os_version, - 'os_version' : self.set_os_version, - 'owners' : self.set_owners, - 'mgmt-classes' : self.set_mgmt_classes, - 'mgmt_classes' : self.set_mgmt_classes, - 'template-files': self.set_template_files, - 'template_files': self.set_template_files, - 'comment' : self.set_comment, - 'redhat_management_key' : self.set_redhat_management_key + 'name' : self.set_name, + 'kernel' : self.set_kernel, + 'initrd' : self.set_initrd, + 'kopts' : self.set_kernel_options, + 'kopts-post' : self.set_kernel_options_post, + 'kopts_post' : self.set_kernel_options_post, + 'arch' : self.set_arch, + 'ksmeta' : self.set_ksmeta, + 'breed' : self.set_breed, + 'os-version' : self.set_os_version, + 'os_version' : self.set_os_version, + 'owners' : self.set_owners, + 'mgmt-classes' : self.set_mgmt_classes, + 'mgmt_classes' : self.set_mgmt_classes, + 'template-files' : self.set_template_files, + 'template_files' : self.set_template_files, + 'comment' : self.set_comment, + 'redhat_management_key' : self.set_redhat_management_key, + 'redhat_management_server' : self.set_redhat_management_server } diff --git a/cobbler/item_image.py b/cobbler/item_image.py index 98e02558..5a4123b2 100644 --- a/cobbler/item_image.py +++ b/cobbler/item_image.py @@ -113,11 +113,49 @@ class Image(item.Item): def set_file(self,filename): """ Stores the image location. This should be accessible on all nodes - that need to access it. Format: either /mnt/commonpath/foo.iso or - nfs://host/path/foo.iso + that need to access it. Format: can be one of the following: + * username:password@hostname:/path/to/the/filename.ext + * username@hostname:/path/to/the/filename.ext + * hostname:/path/to/the/filename.ext + * /path/to/the/filename.ext """ - # FIXME: this should accept NFS paths or filesystem paths - self.file = filename + uri = "" + scheme = auth = hostname = path = "" + # we'll discard the protocol if it's supplied, for legacy support + if filename.find("://") != -1: + scheme, uri = filename.split("://") + filename = uri + else: + uri = filename + + if filename.find("@") != -1: + auth, filename = filename.split("@") + # extract the hostname + # 1. if we have a colon, then everything before it is a hostname + # 2. if we don't have a colon, then check if we had a scheme; if + # we did, then grab all before the first forward slash as the + # hostname; otherwise, we've got a bad file + if filename.find(":") != -1: + hostname, filename = filename.split(":") + elif filename[0] != '/': + if len(scheme) > 0: + index = filename.find("/") + hostname = filename[:index] + filename = filename[index:] + else: + raise CX(_("invalid file: %s" % filename)) + # raise an exception if we don't have a valid path + if len(filename) > 0 and filename[0] != '/': + raise CX(_("file contains an invalid path: %s" % filename)) + if filename.find("/") != -1: + path, filename = filename.rsplit("/", 1) + + if len(filename) == 0: + raise CX(_("missing filename")) + if len(auth) > 0 and len(hostname) == 0: + raise CX(_("a hostname must be specified with authentication details")) + + self.file = uri return True def set_os_version(self,os_version): diff --git a/cobbler/item_profile.py b/cobbler/item_profile.py index d30eb52a..3f9e9460 100644 --- a/cobbler/item_profile.py +++ b/cobbler/item_profile.py @@ -42,60 +42,64 @@ class Profile(item.Item): """ Reset this object. """ - self.name = None - self.uid = "" - self.random_id = "" - self.owners = self.settings.default_ownership - self.distro = (None, '<<inherit>>')[is_subobject] - self.enable_menu = (self.settings.enable_menu, '<<inherit>>')[is_subobject] - self.kickstart = (self.settings.default_kickstart , '<<inherit>>')[is_subobject] - self.kernel_options = ({}, '<<inherit>>')[is_subobject] - self.kernel_options_post = ({}, '<<inherit>>')[is_subobject] - self.ks_meta = ({}, '<<inherit>>')[is_subobject] - self.template_files = ({}, '<<inherit>>')[is_subobject] - self.virt_cpus = (1, '<<inherit>>')[is_subobject] - self.virt_file_size = (self.settings.default_virt_file_size, '<<inherit>>')[is_subobject] - self.virt_ram = (self.settings.default_virt_ram, '<<inherit>>')[is_subobject] - self.repos = ([], '<<inherit>>')[is_subobject] - self.depth = 1 - self.virt_type = (self.settings.default_virt_type, '<<inherit>>')[is_subobject] - self.virt_path = ("", '<<inherit>>')[is_subobject] - self.virt_bridge = (self.settings.default_virt_bridge, '<<inherit>>')[is_subobject] - self.dhcp_tag = ("default", '<<inherit>>')[is_subobject] - self.mgmt_classes = ([], '<<inherit>>')[is_subobject] - self.parent = '' - self.server = "<<inherit>>" - self.comment = "" - self.ctime = 0 - self.mtime = 0 - self.name_servers = (self.settings.default_name_servers, '<<inherit>>')[is_subobject] - self.redhat_management_key = "<<inherit>>" + self.name = None + self.uid = "" + self.random_id = "" + self.owners = self.settings.default_ownership + self.distro = (None, '<<inherit>>')[is_subobject] + self.enable_menu = (self.settings.enable_menu, '<<inherit>>')[is_subobject] + self.kickstart = (self.settings.default_kickstart , '<<inherit>>')[is_subobject] + self.kernel_options = ({}, '<<inherit>>')[is_subobject] + self.kernel_options_post = ({}, '<<inherit>>')[is_subobject] + self.ks_meta = ({}, '<<inherit>>')[is_subobject] + self.template_files = ({}, '<<inherit>>')[is_subobject] + self.virt_cpus = (1, '<<inherit>>')[is_subobject] + self.virt_file_size = (self.settings.default_virt_file_size, '<<inherit>>')[is_subobject] + self.virt_ram = (self.settings.default_virt_ram, '<<inherit>>')[is_subobject] + self.repos = ([], '<<inherit>>')[is_subobject] + self.depth = 1 + self.virt_type = (self.settings.default_virt_type, '<<inherit>>')[is_subobject] + self.virt_path = ("", '<<inherit>>')[is_subobject] + self.virt_bridge = (self.settings.default_virt_bridge, '<<inherit>>')[is_subobject] + self.dhcp_tag = ("default", '<<inherit>>')[is_subobject] + self.mgmt_classes = ([], '<<inherit>>')[is_subobject] + self.parent = '' + self.server = "<<inherit>>" + self.comment = "" + self.ctime = 0 + self.mtime = 0 + self.name_servers = (self.settings.default_name_servers,[])[is_subobject] + self.name_servers_search = (self.settings.default_name_servers_search,[])[is_subobject] + self.redhat_management_key = "<<inherit>>" + self.redhat_management_server = "<<inherit>>" def from_datastruct(self,seed_data): """ Load this object's properties based on seed_data """ - self.parent = self.load_item(seed_data,'parent','') - self.name = self.load_item(seed_data,'name') - self.owners = self.load_item(seed_data,'owners',self.settings.default_ownership) - self.distro = self.load_item(seed_data,'distro') - self.enable_menu = self.load_item(seed_data,'enable_menu', self.settings.enable_menu) - self.kickstart = self.load_item(seed_data,'kickstart') - self.kernel_options = self.load_item(seed_data,'kernel_options') - self.kernel_options_post = self.load_item(seed_data,'kernel_options_post') - self.ks_meta = self.load_item(seed_data,'ks_meta') - self.template_files = self.load_item(seed_data,'template_files', {}) - self.repos = self.load_item(seed_data,'repos', []) - self.depth = self.load_item(seed_data,'depth', 1) - self.dhcp_tag = self.load_item(seed_data,'dhcp_tag', 'default') - self.server = self.load_item(seed_data,'server', '<<inherit>>') - self.mgmt_classes = self.load_item(seed_data,'mgmt_classes', []) - self.comment = self.load_item(seed_data,'comment','') - self.ctime = self.load_item(seed_data,'ctime',0) - self.mtime = self.load_item(seed_data,'mtime',0) - self.name_servers = self.load_item(seed_data,'name_servers',[]) - self.redhat_management_key = self.load_item(seed_data,'redhat_management_key', '<<inherit>>') + self.parent = self.load_item(seed_data,'parent','') + self.name = self.load_item(seed_data,'name') + self.owners = self.load_item(seed_data,'owners',self.settings.default_ownership) + self.distro = self.load_item(seed_data,'distro') + self.enable_menu = self.load_item(seed_data,'enable_menu', self.settings.enable_menu) + self.kickstart = self.load_item(seed_data,'kickstart') + self.kernel_options = self.load_item(seed_data,'kernel_options') + self.kernel_options_post = self.load_item(seed_data,'kernel_options_post') + self.ks_meta = self.load_item(seed_data,'ks_meta') + self.template_files = self.load_item(seed_data,'template_files', {}) + self.repos = self.load_item(seed_data,'repos', []) + self.depth = self.load_item(seed_data,'depth', 1) + self.dhcp_tag = self.load_item(seed_data,'dhcp_tag', 'default') + self.server = self.load_item(seed_data,'server', '<<inherit>>') + self.mgmt_classes = self.load_item(seed_data,'mgmt_classes', []) + self.comment = self.load_item(seed_data,'comment','') + self.ctime = self.load_item(seed_data,'ctime',0) + self.mtime = self.load_item(seed_data,'mtime',0) + self.name_servers = self.load_item(seed_data,'name_servers',[]) + self.name_servers_search = self.load_item(seed_data,'name_servers_search',[]) + self.redhat_management_key = self.load_item(seed_data,'redhat_management_key', '<<inherit>>') + self.redhat_management_server = self.load_item(seed_data,'redhat_management_server', '<<inherit>>') # backwards compatibility if type(self.repos) != list: @@ -179,11 +183,20 @@ class Profile(item.Item): def set_redhat_management_key(self,key): return utils.set_redhat_management_key(self,key) + def set_redhat_management_server(self,server): + return utils.set_redhat_management_server(self,server) + def set_name_servers(self,data): - data = utils.input_string_or_list(data) + # FIXME: move to utils since shared with system + data = utils.input_string_or_list(data, delim=" ") self.name_servers = data return True + def set_name_servers_search(self,data): + data = utils.input_string_or_list(data, delim=" ") + self.name_servers_search = data + return True + def set_enable_menu(self,enable_menu): """ Sets whether or not the profile will be listed in the default @@ -200,7 +213,7 @@ class Profile(item.Item): def set_server(self,server): if server is None or server == "": - server = "<inherit>" + server = "<<inherit>>" self.server = server return True @@ -279,34 +292,36 @@ class Profile(item.Item): Return hash representation for the serializer """ return { - 'name' : self.name, - 'owners' : self.owners, - 'distro' : self.distro, - 'enable_menu' : self.enable_menu, - 'kickstart' : self.kickstart, - 'kernel_options' : self.kernel_options, - 'kernel_options_post' : self.kernel_options_post, - 'virt_file_size' : self.virt_file_size, - 'virt_ram' : self.virt_ram, - 'virt_bridge' : self.virt_bridge, - 'virt_cpus' : self.virt_cpus, - 'ks_meta' : self.ks_meta, - 'template_files' : self.template_files, - 'repos' : self.repos, - 'parent' : self.parent, - 'depth' : self.depth, - 'virt_type' : self.virt_type, - 'virt_path' : self.virt_path, - 'dhcp_tag' : self.dhcp_tag, - 'server' : self.server, - 'mgmt_classes' : self.mgmt_classes, - 'comment' : self.comment, - 'ctime' : self.ctime, - 'mtime' : self.mtime, - 'name_servers' : self.name_servers, - 'uid' : self.uid, - 'random_id' : self.random_id, - 'redhat_management_key' : self.redhat_management_key + 'name' : self.name, + 'owners' : self.owners, + 'distro' : self.distro, + 'enable_menu' : self.enable_menu, + 'kickstart' : self.kickstart, + 'kernel_options' : self.kernel_options, + 'kernel_options_post' : self.kernel_options_post, + 'virt_file_size' : self.virt_file_size, + 'virt_ram' : self.virt_ram, + 'virt_bridge' : self.virt_bridge, + 'virt_cpus' : self.virt_cpus, + 'ks_meta' : self.ks_meta, + 'template_files' : self.template_files, + 'repos' : self.repos, + 'parent' : self.parent, + 'depth' : self.depth, + 'virt_type' : self.virt_type, + 'virt_path' : self.virt_path, + 'dhcp_tag' : self.dhcp_tag, + 'server' : self.server, + 'mgmt_classes' : self.mgmt_classes, + 'comment' : self.comment, + 'ctime' : self.ctime, + 'mtime' : self.mtime, + 'name_servers' : self.name_servers, + 'name_servers_search' : self.name_servers_search, + 'uid' : self.uid, + 'random_id' : self.random_id, + 'redhat_management_key' : self.redhat_management_key, + 'redhat_management_server' : self.redhat_management_server } def printable(self): @@ -328,9 +343,11 @@ class Profile(item.Item): buf = buf + _("mgmt classes : %s\n") % self.mgmt_classes buf = buf + _("modified : %s\n") % time.ctime(self.mtime) buf = buf + _("name servers : %s\n") % self.name_servers + buf = buf + _("name servers search : %s\n") % self.name_servers_search buf = buf + _("owners : %s\n") % self.owners buf = buf + _("post kernel options : %s\n") % self.kernel_options_post buf = buf + _("redhat mgmt key : %s\n") % self.redhat_management_key + buf = buf + _("redhat mgmt server : %s\n") % self.redhat_management_server buf = buf + _("repos : %s\n") % self.repos buf = buf + _("server : %s\n") % self.server buf = buf + _("template_files : %s\n") % self.template_files @@ -345,40 +362,42 @@ class Profile(item.Item): def remote_methods(self): return { - 'name' : self.set_name, - 'parent' : self.set_parent, - 'profile' : self.set_name, - 'distro' : self.set_distro, - 'enable-menu' : self.set_enable_menu, - 'enable_menu' : self.set_enable_menu, - 'kickstart' : self.set_kickstart, - 'kopts' : self.set_kernel_options, - 'kopts-post' : self.set_kernel_options_post, - 'kopts_post' : self.set_kernel_options_post, - 'virt-file-size' : self.set_virt_file_size, - 'virt_file_size' : self.set_virt_file_size, - 'virt-ram' : self.set_virt_ram, - 'virt_ram' : self.set_virt_ram, - 'ksmeta' : self.set_ksmeta, - 'template-files' : self.set_template_files, - 'template_files' : self.set_template_files, - 'repos' : self.set_repos, - 'virt-path' : self.set_virt_path, - 'virt_path' : self.set_virt_path, - 'virt-type' : self.set_virt_type, - 'virt_type' : self.set_virt_type, - 'virt-bridge' : self.set_virt_bridge, - 'virt_bridge' : self.set_virt_bridge, - 'virt-cpus' : self.set_virt_cpus, - 'virt_cpus' : self.set_virt_cpus, - 'dhcp-tag' : self.set_dhcp_tag, - 'dhcp_tag' : self.set_dhcp_tag, - 'server' : self.set_server, - 'owners' : self.set_owners, - 'mgmt-classes' : self.set_mgmt_classes, - 'mgmt_classes' : self.set_mgmt_classes, - 'comment' : self.set_comment, - 'name_servers' : self.set_name_servers, - 'redhat_management_key' : self.set_redhat_management_key + 'name' : self.set_name, + 'parent' : self.set_parent, + 'profile' : self.set_name, + 'distro' : self.set_distro, + 'enable-menu' : self.set_enable_menu, + 'enable_menu' : self.set_enable_menu, + 'kickstart' : self.set_kickstart, + 'kopts' : self.set_kernel_options, + 'kopts-post' : self.set_kernel_options_post, + 'kopts_post' : self.set_kernel_options_post, + 'virt-file-size' : self.set_virt_file_size, + 'virt_file_size' : self.set_virt_file_size, + 'virt-ram' : self.set_virt_ram, + 'virt_ram' : self.set_virt_ram, + 'ksmeta' : self.set_ksmeta, + 'template-files' : self.set_template_files, + 'template_files' : self.set_template_files, + 'repos' : self.set_repos, + 'virt-path' : self.set_virt_path, + 'virt_path' : self.set_virt_path, + 'virt-type' : self.set_virt_type, + 'virt_type' : self.set_virt_type, + 'virt-bridge' : self.set_virt_bridge, + 'virt_bridge' : self.set_virt_bridge, + 'virt-cpus' : self.set_virt_cpus, + 'virt_cpus' : self.set_virt_cpus, + 'dhcp-tag' : self.set_dhcp_tag, + 'dhcp_tag' : self.set_dhcp_tag, + 'server' : self.set_server, + 'owners' : self.set_owners, + 'mgmt-classes' : self.set_mgmt_classes, + 'mgmt_classes' : self.set_mgmt_classes, + 'comment' : self.set_comment, + 'name_servers' : self.set_name_servers, + 'name_servers_search' : self.set_name_servers_search, + 'redhat_management_key' : self.set_redhat_management_key, + 'redhat_management_server' : self.set_redhat_management_server } diff --git a/cobbler/item_repo.py b/cobbler/item_repo.py index 3a62a21f..02622004 100644 --- a/cobbler/item_repo.py +++ b/cobbler/item_repo.py @@ -83,6 +83,7 @@ class Repo(item.Item): self.set_mirror_locally(self.mirror_locally) self.set_owners(self.owners) self.set_environment(self.environment) + self.set_arch(self.arch) self._guess_breed() self.uid = self.load_item(seed_data,'uid','') @@ -177,17 +178,7 @@ class Repo(item.Item): contains games, and we probably don't want those), make it possible to list the packages one wants out of those repos, so only those packages + deps can be mirrored. """ - if rpms is None: - rpms = "" - if type(rpms) != list: - rpmlist = rpms.split(None) - else: - rpmlist = rpms - try: - rpmlist.remove('') - except: - pass - self.rpm_list = rpmlist + self.rpm_list = utils.input_string_or_list(rpms,delim=" ") return True def set_createrepo_flags(self,createrepo_flags): @@ -208,7 +199,7 @@ class Repo(item.Item): """ Override the arch used for reposync """ - return utils.set_arch(self,arch) + return utils.set_arch(self,arch,repo=True) def is_valid(self): """ diff --git a/cobbler/item_system.py b/cobbler/item_system.py index d1411092..b7e77448 100644 --- a/cobbler/item_system.py +++ b/cobbler/item_system.py @@ -38,65 +38,65 @@ class System(item.Item): return cloned def clear(self,is_subobject=False): - self.name = None - self.uid = "" - self.owners = self.settings.default_ownership - self.profile = None - self.image = None - self.kernel_options = {} - self.kernel_options_post = {} - self.ks_meta = {} - self.interfaces = {} - self.netboot_enabled = True - self.depth = 2 - self.mgmt_classes = [] - self.template_files = {} - self.kickstart = "<<inherit>>" # use value in profile - self.server = "<<inherit>>" # "" (or settings) - self.virt_path = "<<inherit>>" # "" - self.virt_type = "<<inherit>>" # "" - self.virt_cpus = "<<inherit>>" # "" - self.virt_file_size = "<<inherit>>" # "" - self.virt_ram = "<<inherit>>" # "" - self.virt_type = "<<inherit>>" # "" - self.virt_path = "<<inherit>>" # "" - self.virt_bridge = "<<inherit>>" # "" - self.comment = "" - self.ctime = 0 - self.mtime = 0 - self.uid = "" - self.random_id = "" - self.power_type = self.settings.power_management_default_type - self.power_address = "" - self.power_user = "" - self.power_pass = "" - self.power_id = "" - self.hostname = "" - self.gateway = "" - self.name_servers = "" - self.bonding = "" - self.bonding_master = "" - self.bonding_opts = "" - self.redhat_management_key = "<<inherit>>" + self.name = None + self.uid = "" + self.owners = self.settings.default_ownership + self.profile = None + self.image = None + self.kernel_options = {} + self.kernel_options_post = {} + self.ks_meta = {} + self.interfaces = {} + self.netboot_enabled = True + self.depth = 2 + self.mgmt_classes = [] + self.template_files = {} + self.kickstart = "<<inherit>>" # use value in profile + self.server = "<<inherit>>" # "" (or settings) + self.virt_path = "<<inherit>>" # "" + self.virt_type = "<<inherit>>" # "" + self.virt_cpus = "<<inherit>>" # "" + self.virt_file_size = "<<inherit>>" # "" + self.virt_ram = "<<inherit>>" # "" + self.virt_type = "<<inherit>>" # "" + self.virt_path = "<<inherit>>" # "" + self.virt_bridge = "<<inherit>>" # "" + self.comment = "" + self.ctime = 0 + self.mtime = 0 + self.uid = "" + self.random_id = "" + self.power_type = self.settings.power_management_default_type + self.power_address = "" + self.power_user = "" + self.power_pass = "" + self.power_id = "" + self.hostname = "" + self.gateway = "" + self.name_servers = [] + self.name_servers_search = [] + self.bonding = "" + self.bonding_master = "" + self.bonding_opts = "" + self.redhat_management_key = "<<inherit>>" + self.redhat_management_server = "<<inherit>>" def delete_interface(self,name): """ - Used to remove an interface. Not valid for the default -interface. + Used to remove an interface. """ - if self.interfaces.has_key(name) and name != "eth0": + if self.interfaces.has_key(name) and len(self.interfaces) > 1: del self.interfaces[name] else: - if name == "eth0": - raise CX(_("Interface %s can never be deleted") % name) - else: + if not self.interfaces.has_key(name): raise CX(_("Cannot delete interface that is not present: %s") % name) + else: + raise CX(_("At least one interface needs to be defined.")) + return True def __get_interface(self,name): - if name is None: - return self.__get_default_interface() if not self.interfaces.has_key(name): self.interfaces[name] = { @@ -115,8 +115,6 @@ interface. return self.interfaces[name] - def __get_default_interface(self): - return self.__get_interface("eth0") def from_datastruct(self,seed_data): @@ -179,7 +177,9 @@ interface. self.hostname = self.load_item(seed_data, 'hostname', __hostname) self.name_servers = self.load_item(seed_data, 'name_servers', '<<inherit>>') + self.name_servers_search = self.load_item(seed_data, 'name_servers_search', '<<inherit>>') self.redhat_management_key = self.load_item(seed_data, 'redhat_management_key', '<<inherit>>') + self.redhat_management_server = self.load_item(seed_data, 'redhat_management_server', '<<inherit>>') # virt specific @@ -290,6 +290,12 @@ interface. self.set_image(self.image) self.set_profile(self.profile) + + # enforce that the system extends from a profile or system but not both + # profile wins as it's the more common usage + self.set_image(self.image) + self.set_profile(self.profile) + return self def get_parent(self): @@ -308,8 +314,6 @@ interface. Set the name. If the name is a MAC or IP, and the first MAC and/or IP is not defined, go ahead and fill that value in. """ - intf = self.__get_default_interface() - if self.name not in ["",None] and self.parent not in ["",None] and self.name == self.parent: raise CX(_("self parentage is weird")) @@ -319,10 +323,15 @@ interface. if not x.isalnum() and not x in [ "_", "-", ".", ":", "+" ] : raise CX(_("invalid characters in name: %s") % x) + # Stuff here defaults to eth0. Yes, it's ugly and hardcoded, but so was + # the default interface behaviour that's now removed. ;) + # --Jasper Capel if utils.is_mac(name): + intf = self.__get_interface("eth0") if intf["mac_address"] == "": intf["mac_address"] = name elif utils.is_ip(name): + intf = self.__get_interface("eth0") if intf["ip_address"] == "": intf["ip_address"] = name self.name = name @@ -332,6 +341,9 @@ interface. def set_redhat_management_key(self,key): return utils.set_redhat_management_key(self,key) + def set_redhat_management_server(self,server): + return utils.set_redhat_management_server(self,server) + def set_server(self,server): """ If a system can't reach the boot server at the value configured in settings @@ -351,7 +363,7 @@ interface. intf = self.__get_interface(interface) if intf["mac_address"] != "": - return intf["mac_address"] + return intf["mac_address"].strip() else: return None @@ -364,7 +376,7 @@ interface. intf = self.__get_interface(interface) if intf["ip_address"] != "": - return intf["ip_address"] + return intf["ip_address"].strip() else: return None @@ -387,12 +399,6 @@ interface. return True return False - def set_default_interface(self,interface): - if self.interfaces.has_key(interface): - self.default_interface = interface - else: - raise CX(_("invalid interface (%s)") % interface) - def set_dhcp_tag(self,dhcp_tag,interface): intf = self.__get_interface(interface) intf["dhcp_tag"] = dhcp_tag @@ -427,7 +433,7 @@ interface. """ intf = self.__get_interface(interface) if address == "" or utils.is_ip(address): - intf["ip_address"] = address + intf["ip_address"] = address.strip() return True raise CX(_("invalid format for IP address (%s)") % address) @@ -445,16 +451,23 @@ interface. return True def set_name_servers(self,data): - data = utils.input_string_or_list(data) + data = utils.input_string_or_list(data, delim=" ") self.name_servers = data return True + def set_name_servers_search(self,data): + data = utils.input_string_or_list(data, delim=" ") + self.name_servers_search = data + return True + def set_subnet(self,subnet,interface): intf = self.__get_interface(interface) intf["subnet"] = subnet return True def set_virt_bridge(self,bridge,interface): + if bridge == "": + bridge = self.settings.default_virt_bridge intf = self.__get_interface(interface) intf["virt_bridge"] = bridge return True @@ -591,7 +604,7 @@ interface. if power_type is None: power_type = "" power_type = power_type.lower() - valid = "bullpap wti apc_snmp ether-wake ipmilan drac ipmitool ilo rsai lpar bladecenter virsh none" + valid = "bullpap wti apc_snmp ether-wake ipmilan drac ipmitool ilo rsa lpar bladecenter virsh integrity none" choices = valid.split(" ") choices.sort() if power_type not in choices: @@ -629,41 +642,43 @@ interface. def to_datastruct(self): return { - 'name' : self.name, - 'uid' : self.uid, - 'random_id' : self.random_id, - 'kernel_options' : self.kernel_options, - 'kernel_options_post' : self.kernel_options_post, - 'depth' : self.depth, - 'interfaces' : self.interfaces, - 'ks_meta' : self.ks_meta, - 'kickstart' : self.kickstart, - 'netboot_enabled' : self.netboot_enabled, - 'owners' : self.owners, - 'parent' : self.parent, - 'profile' : self.profile, - 'image' : self.image, - 'server' : self.server, - 'virt_cpus' : self.virt_cpus, - 'virt_bridge' : self.virt_bridge, - 'virt_file_size' : self.virt_file_size, - 'virt_path' : self.virt_path, - 'virt_ram' : self.virt_ram, - 'virt_type' : self.virt_type, - 'mgmt_classes' : self.mgmt_classes, - 'template_files' : self.template_files, - 'comment' : self.comment, - 'ctime' : self.ctime, - 'mtime' : self.mtime, - 'power_type' : self.power_type, - 'power_address' : self.power_address, - 'power_user' : self.power_user, - 'power_pass' : self.power_pass, - 'power_id' : self.power_id, - 'hostname' : self.hostname, - 'gateway' : self.gateway, - 'name_servers' : self.name_servers, - 'redhat_management_key' : self.redhat_management_key + 'name' : self.name, + 'uid' : self.uid, + 'random_id' : self.random_id, + 'kernel_options' : self.kernel_options, + 'kernel_options_post' : self.kernel_options_post, + 'depth' : self.depth, + 'interfaces' : self.interfaces, + 'ks_meta' : self.ks_meta, + 'kickstart' : self.kickstart, + 'netboot_enabled' : self.netboot_enabled, + 'owners' : self.owners, + 'parent' : self.parent, + 'profile' : self.profile, + 'image' : self.image, + 'server' : self.server, + 'virt_cpus' : self.virt_cpus, + 'virt_bridge' : self.virt_bridge, + 'virt_file_size' : self.virt_file_size, + 'virt_path' : self.virt_path, + 'virt_ram' : self.virt_ram, + 'virt_type' : self.virt_type, + 'mgmt_classes' : self.mgmt_classes, + 'template_files' : self.template_files, + 'comment' : self.comment, + 'ctime' : self.ctime, + 'mtime' : self.mtime, + 'power_type' : self.power_type, + 'power_address' : self.power_address, + 'power_user' : self.power_user, + 'power_pass' : self.power_pass, + 'power_id' : self.power_id, + 'hostname' : self.hostname, + 'gateway' : self.gateway, + 'name_servers' : self.name_servers, + 'name_servers_search' : self.name_servers_search, + 'redhat_management_key' : self.redhat_management_key, + 'redhat_management_server' : self.redhat_management_server } def printable(self): @@ -682,6 +697,7 @@ interface. buf = buf + _("modified : %s\n") % time.ctime(self.mtime) buf = buf + _("name servers : %s\n") % self.name_servers + buf = buf + _("name servers search : %s\n") % self.name_servers_search buf = buf + _("netboot enabled? : %s\n") % self.netboot_enabled buf = buf + _("owners : %s\n") % self.owners buf = buf + _("server : %s\n") % self.server @@ -745,50 +761,52 @@ interface. # compatibility. At some point they may be removed. return { - 'name' : self.set_name, - 'profile' : self.set_profile, - 'image' : self.set_image, - 'kopts' : self.set_kernel_options, - 'kopts-post' : self.set_kernel_options_post, - 'kopts_post' : self.set_kernel_options_post, - 'ksmeta' : self.set_ksmeta, - 'kickstart' : self.set_kickstart, - 'netboot-enabled' : self.set_netboot_enabled, - 'netboot_enabled' : self.set_netboot_enabled, - 'virt-path' : self.set_virt_path, - 'virt_path' : self.set_virt_path, - 'virt-type' : self.set_virt_type, - 'virt_type' : self.set_virt_type, - 'modify-interface' : self.modify_interface, - 'modify_interface' : self.modify_interface, - 'delete-interface' : self.delete_interface, - 'delete_interface' : self.delete_interface, - 'virt-path' : self.set_virt_path, - 'virt_path' : self.set_virt_path, - 'virt-ram' : self.set_virt_ram, - 'virt_ram' : self.set_virt_ram, - 'virt-type' : self.set_virt_type, - 'virt_type' : self.set_virt_type, - 'virt-cpus' : self.set_virt_cpus, - 'virt_cpus' : self.set_virt_cpus, - 'virt-file-size' : self.set_virt_file_size, - 'virt_file_size' : self.set_virt_file_size, - 'server' : self.set_server, - 'owners' : self.set_owners, - 'mgmt-classes' : self.set_mgmt_classes, - 'mgmt_classes' : self.set_mgmt_classes, - 'template-files' : self.set_template_files, - 'template_files' : self.set_template_files, - 'comment' : self.set_comment, - 'power_type' : self.set_power_type, - 'power_address' : self.set_power_address, - 'power_user' : self.set_power_user, - 'power_pass' : self.set_power_pass, - 'power_id' : self.set_power_id, - 'hostname' : self.set_hostname, - 'gateway' : self.set_gateway, - 'name_servers' : self.set_name_servers, - 'redhat_management_key' : self.set_redhat_management_key + 'name' : self.set_name, + 'profile' : self.set_profile, + 'image' : self.set_image, + 'kopts' : self.set_kernel_options, + 'kopts-post' : self.set_kernel_options_post, + 'kopts_post' : self.set_kernel_options_post, + 'ksmeta' : self.set_ksmeta, + 'kickstart' : self.set_kickstart, + 'netboot-enabled' : self.set_netboot_enabled, + 'netboot_enabled' : self.set_netboot_enabled, + 'virt-path' : self.set_virt_path, + 'virt_path' : self.set_virt_path, + 'virt-type' : self.set_virt_type, + 'virt_type' : self.set_virt_type, + 'modify-interface' : self.modify_interface, + 'modify_interface' : self.modify_interface, + 'delete-interface' : self.delete_interface, + 'delete_interface' : self.delete_interface, + 'virt-path' : self.set_virt_path, + 'virt_path' : self.set_virt_path, + 'virt-ram' : self.set_virt_ram, + 'virt_ram' : self.set_virt_ram, + 'virt-type' : self.set_virt_type, + 'virt_type' : self.set_virt_type, + 'virt-cpus' : self.set_virt_cpus, + 'virt_cpus' : self.set_virt_cpus, + 'virt-file-size' : self.set_virt_file_size, + 'virt_file_size' : self.set_virt_file_size, + 'server' : self.set_server, + 'owners' : self.set_owners, + 'mgmt-classes' : self.set_mgmt_classes, + 'mgmt_classes' : self.set_mgmt_classes, + 'template-files' : self.set_template_files, + 'template_files' : self.set_template_files, + 'comment' : self.set_comment, + 'power_type' : self.set_power_type, + 'power_address' : self.set_power_address, + 'power_user' : self.set_power_user, + 'power_pass' : self.set_power_pass, + 'power_id' : self.set_power_id, + 'hostname' : self.set_hostname, + 'gateway' : self.set_gateway, + 'name_servers' : self.set_name_servers, + 'name_servers_search' : self.set_name_servers_search, + 'redhat_management_key' : self.set_redhat_management_key, + 'redhat_management_server' : self.set_redhat_management_server } diff --git a/cobbler/manage_ctrl.py b/cobbler/manage_ctrl.py index 10d44e8b..95ad5b8f 100644 --- a/cobbler/manage_ctrl.py +++ b/cobbler/manage_ctrl.py @@ -30,7 +30,6 @@ import sys import glob import traceback import errno -import popen2 from shlex import shlex diff --git a/cobbler/module_loader.py b/cobbler/module_loader.py index a7be1f68..f50e4bf4 100644 --- a/cobbler/module_loader.py +++ b/cobbler/module_loader.py @@ -82,7 +82,7 @@ def load_modules(module_path=mod_path, blacklist=None): def get_module_by_name(name): return MODULE_CACHE.get(name, None) -def get_module_from_file(category,field,fallback_module_name=None): +def get_module_from_file(category,field,fallback_module_name=None,just_name=False): try: value = cp.get(category,field) @@ -91,6 +91,8 @@ def get_module_from_file(category,field,fallback_module_name=None): value = fallback_module_name else: raise CX(_("Cannot find config file setting for: %s") % field) + if just_name: + return value rc = MODULE_CACHE.get(value, None) if rc is None: raise CX(_("Failed to load module for %s/%s") % (category,field)) diff --git a/cobbler/modules/authn_configfile.py b/cobbler/modules/authn_configfile.py index 1f4cdb26..b9d2996e 100644 --- a/cobbler/modules/authn_configfile.py +++ b/cobbler/modules/authn_configfile.py @@ -26,7 +26,7 @@ import ConfigParser import sys import os from utils import _ -import md5 +from utils import md5 import traceback plib = distutils.sysconfig.get_python_lib() @@ -75,7 +75,7 @@ def authenticate(api_handle,username,password): for (user,realm,actual_blob) in userlist: if user == username and realm == "Cobbler": input = ":".join([user,realm,password]) - input_blob = md5.md5(input).hexdigest() + input_blob = md5(input).hexdigest() if input_blob.lower() == actual_blob.lower(): return True diff --git a/cobbler/modules/authn_ldap.py b/cobbler/modules/authn_ldap.py index e4313e07..a8718c5b 100644 --- a/cobbler/modules/authn_ldap.py +++ b/cobbler/modules/authn_ldap.py @@ -23,7 +23,6 @@ import distutils.sysconfig import sys import os from utils import _ -import md5 import traceback # we'll import this just a bit later diff --git a/cobbler/modules/authn_spacewalk.py b/cobbler/modules/authn_spacewalk.py index 45a26a01..eec3bd7c 100644 --- a/cobbler/modules/authn_spacewalk.py +++ b/cobbler/modules/authn_spacewalk.py @@ -29,35 +29,126 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) - def register(): """ The mandatory cobbler module registration hook. """ return "authn" +def __looks_like_a_token(password): + + # what spacewalk sends us could be an internal token or it could be a password + # if it's long and lowercase hex, it's /likely/ a token, and we should try to treat + # it as a token first, if not, we should treat it as a password. All of this + # code is there to avoid extra XMLRPC calls, which are slow. + + # we can't use binascii.unhexlify here as it's an "odd length string" + + if password.lower() != password: + # tokens are always lowercase, this isn't a token + return False + + #try: + # #data = binascii.unhexlify(password) + # return True # looks like a token, but we can't be sure + #except: + # return False # definitely not a token + + return (len(password) > 45) + def authenticate(api_handle,username,password): """ Validate a username/password combo, returning True/False This will pass the username and password back to Spacewalk to see if this authentication request is valid. + + See also: http://www.redhat.com/spacewalk/documentation/api/0.4/ + """ - #spacewalk_url = api_handle.settings().spacewalk_url - server = api_handle.settings().redhat_management_server + if api_handle is not None: + server = api_handle.settings().redhat_management_server + user_enabled = api_handle.settings().redhat_management_permissive + else: + server = "columbia.devel.redhat.com" + user_enabled = True + if server == "xmlrpc.rhn.redhat.com": - return False # don't bother RHN! + return False # emergency fail, don't bother RHN! + spacewalk_url = "https://%s/rpc/api" % server client = xmlrpclib.Server(spacewalk_url, verbose=0) - valid = client.auth.checkAuthToken(username,password) - - if valid is None: - return False + if __looks_like_a_token(password) or username == 'taskomatic_user': + + # The tokens + # are lowercase hex, but a password can also be lowercase hex, + # so we have to try it as both a token and then a password if + # we are unsure. We do it this way to be faster but also to avoid + # any login failed stuff in the logs that we don't need to send. + + try: + valid = client.auth.checkAuthToken(username,password) + except: + # if the token is not a token this will raise an exception + # rather than return an integer. + valid = 0 + + # problem at this point, 0xdeadbeef is valid as a token but if that + # fails, it's also a valid password, so we must try auth system #2 + + if valid != 1: + # first API code returns 1 on success + # the second uses exceptions for login failed. + # + # so... token check failed, but maybe the username/password + # is just a simple username/pass! + + if user_enabled == 0: + # this feature must be explicitly enabled. + return False + + + session = "" + try: + session = client.auth.login(username,password) + except: + # FIXME: should log exceptions that are not excepted + # as we could detect spacewalk java errors here that + # are not login related. + return False + + # login success by username, role must also match + roles = client.user.listRoles(session, username) + if not ("config_admin" in roles or "org_admin" in roles): + return False + + return True - return (valid == 1) - + else: + + # it's an older version of spacewalk, so just try the username/pass + # OR: we know for sure it's not a token because it's not lowercase hex. + + if user_enabled == 0: + # this feature must be explicitly enabled. + return False + + + session = "" + try: + session = client.auth.login(username,password) + except: + return False + + # login success by username, role must also match + roles = client.user.listRoles(session, username) + if not ("config_admin" in roles or "org_admin" in roles): + return False + return True +if __name__ == "__main__": + print authenticate(None,"admin","redhat") diff --git a/cobbler/modules/cli_distro.py b/cobbler/modules/cli_distro.py index 0c1425b1..7105b403 100644 --- a/cobbler/modules/cli_distro.py +++ b/cobbler/modules/cli_distro.py @@ -28,7 +28,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _ -import commands +import cobbler.commands as commands import cexceptions @@ -61,6 +61,7 @@ class DistroFunction(commands.CobblerFunction): p.add_option("--ksmeta", dest="ksmeta", help="ex: 'blippy=7'") p.add_option("--mgmt-classes", dest="mgmt_classes", help="list of config management classes (for Puppet, etc)") p.add_option("--redhat-management-key", dest="redhat_management_key", help="authentication token for RHN/Spacewalk/Satellite") + p.add_option("--redhat-management-server", dest="redhat_management_server", help="RHN/Spacewalk/Satellite server") p.add_option("--template-files", dest="template_files", help="specify files to be generated from templates during a sync") p.add_option("--name", dest="name", help="ex: 'RHEL-5-i386' (REQUIRED)") @@ -118,6 +119,8 @@ class DistroFunction(commands.CobblerFunction): obj.set_template_files(self.options.template_files,self.options.inplace) if self.options.redhat_management_key is not None: obj.set_redhat_management_key(self.options.redhat_management_key) + if self.options.redhat_management_server is not None: + obj.set_redhat_management_server(self.options.redhat_management_server) return self.object_manipulator_finish(obj, self.api.distros, self.options) diff --git a/cobbler/modules/cli_image.py b/cobbler/modules/cli_image.py index b6b96ff4..0265bd78 100644 --- a/cobbler/modules/cli_image.py +++ b/cobbler/modules/cli_image.py @@ -20,7 +20,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _ -import commands +import cobbler.commands as commands import cexceptions @@ -54,6 +54,8 @@ class ImageFunction(commands.CobblerFunction): if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--os-version", dest="os_version", help="ex: rhel4, fedora 9") + if self.matches_args(args,["remove"]): + p.add_option("--recursive", action="store_true", dest="recursive", help="also delete child objects") if self.matches_args(args,["copy","rename"]): diff --git a/cobbler/modules/cli_misc.py b/cobbler/modules/cli_misc.py index 94ccc0d1..f83b5c8c 100644 --- a/cobbler/modules/cli_misc.py +++ b/cobbler/modules/cli_misc.py @@ -21,7 +21,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _ -import commands +import cobbler.commands as commands from cexceptions import * HELP_FORMAT = commands.HELP_FORMAT @@ -63,7 +63,7 @@ class CheckFunction(commands.CobblerFunction): if len(status) == 0: self.logprint(fd,"No setup problems found") - self.logprint(fd,"Manual review and editing of /var/lib/cobbler/settings is recommended to tailor cobbler to your particular configuration.") + self.logprint(fd,"Manual review and editing of /etc/cobbler/settings is recommended to tailor cobbler to your particular configuration.") self.logprint(fd,"Good luck.") return True else: @@ -172,7 +172,10 @@ class StatusFunction(commands.CobblerFunction): ######################################################## class SyncFunction(commands.CobblerFunction): - + + def add_options(self, p, args): + p.add_option("--verbose", dest="verbose", action="store_true", help="run sync with more output") + def help_me(self): return HELP_FORMAT % ("cobbler sync","") @@ -180,7 +183,7 @@ class SyncFunction(commands.CobblerFunction): return "sync" def run(self): - return self.api.sync() + return self.api.sync(verbose=self.options.verbose) ######################################################## @@ -222,6 +225,9 @@ class BuildIsoFunction(commands.CobblerFunction): p.add_option("--profiles", dest="profiles", help="(OPTIONAL) use these profiles only") p.add_option("--systems", dest="systems", help="(OPTIONAL) use these systems only") p.add_option("--tempdir", dest="tempdir", help="(OPTIONAL) working directory") + p.add_option("--distro", dest="distro", help="(OPTIONAL) used with --standalone to create a distro-based ISO including all associated profiles/systems") + p.add_option("--standalone", dest="standalone", action="store_true", help="(OPTIONAL) creates a standalone ISO with all required distro files on it") + p.add_option("--source", dest="source", help="(OPTIONAL) used with --standalone to specify a source for the distribution files") def help_me(self): return HELP_FORMAT % ("cobbler buildiso","[ARGS]") @@ -234,7 +240,10 @@ class BuildIsoFunction(commands.CobblerFunction): iso=self.options.isoname, profiles=self.options.profiles, systems=self.options.systems, - tempdir=self.options.tempdir + tempdir=self.options.tempdir, + distro=self.options.distro, + standalone=self.options.standalone, + source=self.options.source ) ######################################################## diff --git a/cobbler/modules/cli_profile.py b/cobbler/modules/cli_profile.py index 1f8a5655..a3ca5d4c 100644 --- a/cobbler/modules/cli_profile.py +++ b/cobbler/modules/cli_profile.py @@ -28,7 +28,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _ -import commands +import cobbler.commands as commands import cexceptions @@ -72,6 +72,7 @@ class ProfileFunction(commands.CobblerFunction): if not self.matches_args(args,["dumpvars","remove","report","getks","list"]): p.add_option("--name-servers", dest="name_servers", help="name servers for static setups") + p.add_option("--name-servers-search", dest="name_servers_search", help="name servers search path for static setups") if "copy" in args or "rename" in args: p.add_option("--newname", dest="newname") @@ -88,6 +89,7 @@ class ProfileFunction(commands.CobblerFunction): if not self.matches_args(args,["dumpvars","remove","report","getks","list"]): p.add_option("--redhat-management-key", dest="redhat_management_key", help="authentication token for RHN/Spacewalk/Satellite") + p.add_option("--redhat-management-server", dest="redhat_management_server", help="RHN/Spacewalk/Satellite server") p.add_option("--repos", dest="repos", help="names of cobbler repos") p.add_option("--server", dest="server_override", help="overrides value in settings file") p.add_option("--template-files", dest="template_files", help="specify files to be generated from templates during a sync") @@ -148,7 +150,7 @@ class ProfileFunction(commands.CobblerFunction): if self.options.dhcp_tag is not None: obj.set_dhcp_tag(self.options.dhcp_tag) if self.options.server_override is not None: - obj.set_server(self.options.server) + obj.set_server(self.options.server_overide) if self.options.owners is not None: obj.set_owners(self.options.owners) @@ -158,8 +160,12 @@ class ProfileFunction(commands.CobblerFunction): obj.set_template_files(self.options.template_files,self.options.inplace) if self.options.name_servers is not None: obj.set_name_servers(self.options.name_servers) + if self.options.name_servers_search is not None: + obj.set_name_servers_search(self.options.name_servers_search) if self.options.redhat_management_key is not None: obj.set_redhat_management_key(self.options.redhat_management_key) + if self.options.redhat_management_server is not None: + obj.set_redhat_management_server(self.options.redhat_management_server) return self.object_manipulator_finish(obj, self.api.profiles, self.options) diff --git a/cobbler/modules/cli_repo.py b/cobbler/modules/cli_repo.py index a8483dcb..53c0232c 100644 --- a/cobbler/modules/cli_repo.py +++ b/cobbler/modules/cli_repo.py @@ -28,7 +28,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _ -import commands +import cobbler.commands as commands import cexceptions @@ -83,7 +83,7 @@ class RepoFunction(commands.CobblerFunction): def run(self): if self.args and "find" in self.args: - items = self.api.find_system(return_list=True, no_errors=True, **self.options.__dict__) + items = self.api.find_repo(return_list=True, no_errors=True, **self.options.__dict__) for x in items: print x.name return True diff --git a/cobbler/modules/cli_report.py b/cobbler/modules/cli_report.py index 82060dda..4bec4b5d 100644 --- a/cobbler/modules/cli_report.py +++ b/cobbler/modules/cli_report.py @@ -20,7 +20,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _, get_random_mac -import commands +import cobbler.commands as commands from cexceptions import * HELP_FORMAT = commands.HELP_FORMAT diff --git a/cobbler/modules/cli_system.py b/cobbler/modules/cli_system.py index 4805f9c9..7c0924ed 100644 --- a/cobbler/modules/cli_system.py +++ b/cobbler/modules/cli_system.py @@ -28,7 +28,7 @@ mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) from utils import _, get_random_mac -import commands +import cobbler.commands as commands from cexceptions import * @@ -73,7 +73,9 @@ class SystemFunction(commands.CobblerFunction): p.add_option("--mac", dest="mac", help="ex: 'AA:BB:CC:DD:EE:FF', (RECOMMENDED)") p.add_option("--mgmt-classes", dest="mgmt_classes", help="list of config management classes (for Puppet, etc)") p.add_option("--name-servers", dest="name_servers", help="name servers for static setups") + p.add_option("--name-servers-search", dest="name_servers_search", help="name servers search path for static setups") p.add_option("--redhat-management-key", dest="redhat_management_key", help="authentication token for RHN/Spacewalk/Satellite") + p.add_option("--redhat-management-server", dest="redhat_management_server", help="RHN/Spacewalk/Satellite server") p.add_option("--static-routes", dest="static_routes", help="sets static routes (see manpage)") p.add_option("--template-files", dest="template_files",help="specify files to be generated from templates during a sync") @@ -101,7 +103,7 @@ class SystemFunction(commands.CobblerFunction): if not self.matches_args(args,["dumpvars","remove","report","getks","list"]): p.add_option("--power-pass", dest="power_pass", help="password for power management interface") if not self.matches_args(args,["dumpvars","poweron","poweroff","reboot","remove","report","getks","list"]): - p.add_option("--power-type", dest="power_type", help="one of: none, apc_snmp, bullpap, drac, ether-wake, ilo, ipmilan, ipmitool, wti, lpar, bladecenter, virsh") + p.add_option("--power-type", dest="power_type", help="one of: none, apc_snmp, bullpap, drac, ether-wake, ilo, ipmilan, ipmitool, wti, lpar, bladecenter, virsh, integrity") if not self.matches_args(args,["dumpvars","remove","report","getks","list"]): p.add_option("--power-user", dest="power_user", help="username for power management interface, if required") @@ -245,8 +247,12 @@ class SystemFunction(commands.CobblerFunction): obj.set_template_files(self.options.template_files,self.options.inplace) if self.options.name_servers is not None: obj.set_name_servers(self.options.name_servers) + if self.options.name_servers_search is not None: + obj.set_name_servers_search(self.options.name_servers_search) if self.options.redhat_management_key is not None: obj.set_redhat_management_key(self.options.redhat_management_key) + if self.options.redhat_management_server is not None: + obj.set_redhat_management_server(self.options.redhat_management_server) rc = self.object_manipulator_finish(obj, self.api.systems, self.options) diff --git a/cobbler/modules/install_post_log.py b/cobbler/modules/install_post_log.py new file mode 100644 index 00000000..02c0b55e --- /dev/null +++ b/cobbler/modules/install_post_log.py @@ -0,0 +1,29 @@ +import distutils.sysconfig +import sys +import os +from utils import _ +import traceback +import cexceptions +import os +import sys +import time + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +def register(): + # this pure python trigger acts as if it were a legacy shell-trigger, but is much faster. + # the return of this method indicates the trigger type + return "/var/lib/cobbler/triggers/install/post/*" + +def run(api, args): + objtype = args[0] # "system" or "profile" + name = args[1] # name of system or profile + ip = args[2] # ip or "?" + + fd = open("/var/log/cobbler/install.log","a+") + fd.write("%s\t%s\t%s\tstop\t%s\n" % (objtype,name,ip,time.time())) + fd.close() + + return 0 diff --git a/cobbler/modules/install_post_report.py b/cobbler/modules/install_post_report.py new file mode 100755 index 00000000..b42753b4 --- /dev/null +++ b/cobbler/modules/install_post_report.py @@ -0,0 +1,101 @@ +# (c) 2008-2009 +# Jeff Schroeder <jeffschroeder@computer.org> +# Michael DeHaan <mdehaan@redhat.com> +# +# License: GPLv2+ + +# Post install trigger for cobbler to +# send out a pretty email report that +# contains target information. + +import distutils.sysconfig +import sys +import os +import traceback + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +from utils import _ +import smtplib +import sys +import cobbler.templar as templar +from cobbler.cexceptions import CX +import utils + +def register(): + # this pure python trigger acts as if it were a legacy shell-trigger, but is much faster. + # the return of this method indicates the trigger type + return "/var/lib/cobbler/triggers/install/post/*" + +def run(api, args): + + settings = api.settings() + + # go no further if this feature is turned off + if not str(settings.build_reporting_enabled).lower() in [ "1", "yes", "y", "true"]: + print "not enabled" + return 0 + + objtype = args[0] # "target" or "profile" + name = args[1] # name of target or profile + boot_ip = args[2] # ip or "?" + + if objtype == "system": + target = api.find_system(name) + else: + target = api.find_profile(name) + + # collapse the object down to a rendered datastructure + target = utils.blender(api, False, target) + + if target == {}: + raise CX("failure looking up target") + + to_addr = settings.build_reporting_email + if to_addr == "": + return 0 + + # add the ability to specify an MTA for servers that don't run their own + smtp_server = settings.build_reporting_smtp_server + if smtp_server == "": + smtp_server = "localhost" + + # use a custom from address or fall back to a reasonable default + from_addr = settings.build_reporting_sender + if from_addr == "": + from_addr = "cobbler@%s" % settings.server + + subject = settings.build_reporting_subject + if subject == "": + subject = '[Cobbler] install complete ' + + to_addr = ", ".join(to_addr) + metadata = { + "from_addr" : from_addr, + "to_addr" : to_addr, + "subject" : subject, + "boot_ip" : boot_ip + } + metadata.update(target) + + input_template = open("/etc/cobbler/reporting/build_report_email.template") + input_data = input_template.read() + input_template.close() + + message = templar.Templar().render(input_data, metadata, None) + # for debug, call + # print message + + # Send the mail + # FIXME: on error, return non-zero + server_handle = smtplib.SMTP(smtp_server) + server_handle.sendmail(from_addr, to_addr, message) + server_handle.quit() + + return 0 + + + + diff --git a/cobbler/modules/install_pre_clear_anamon_logs.py b/cobbler/modules/install_pre_clear_anamon_logs.py new file mode 100755 index 00000000..381759aa --- /dev/null +++ b/cobbler/modules/install_pre_clear_anamon_logs.py @@ -0,0 +1,51 @@ +import distutils.sysconfig +import sys +import os +from utils import _ +import traceback +import cexceptions + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +import os +import glob +import sys + + +def register(): + # this pure python trigger acts as if it were a legacy shell-trigger, but is much faster. + # the return of this method indicates the trigger type + return "/var/lib/cobbler/triggers/install/pre/*" + +def run(api, args): + + if len(args) < 3: + raise CX("invalid invocation") + + objtype = args[0] # "system" or "profile" + name = args[1] # name of system or profile + ip = args[2] # ip or "?" + + settings = api.settings() + anamon_enabled = str(settings.anamon_enabled) + + # Remove any files matched with the given glob pattern + def unlink_files(globex): + for f in glob.glob(globex): + if os.path.isfile(f): + try: + os.unlink(f) + except OSError, e: + pass + + if str(anamon_enabled) in [ "true", "1", "y", "yes"]: + dirname = "/var/log/cobbler/anamon/%s" % name + if os.path.isdir(dirname): + unlink_files(os.path.join(dirname, "*")) + + # TODO - log somewhere that we cleared a systems anamon logs + return 0 + + diff --git a/cobbler/modules/install_pre_log.py b/cobbler/modules/install_pre_log.py new file mode 100644 index 00000000..4469a514 --- /dev/null +++ b/cobbler/modules/install_pre_log.py @@ -0,0 +1,29 @@ +import distutils.sysconfig +import sys +import os +from utils import _ +import traceback +import cexceptions +import os +import sys +import time + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +def register(): + # this pure python trigger acts as if it were a legacy shell-trigger, but is much faster. + # the return of this method indicates the trigger type + return "/var/lib/cobbler/triggers/install/pre/*" + +def run(api, args): + objtype = args[0] # "system" or "profile" + name = args[1] # name of system or profile + ip = args[2] # ip or "?" + + fd = open("/var/log/cobbler/install.log","a+") + fd.write("%s\t%s\t%s\tstart\t%s\n" % (objtype,name,ip,time.time())) + fd.close() + + return 0 diff --git a/cobbler/modules/manage_bind.py b/cobbler/modules/manage_bind.py index e7cdc3a7..657a9a77 100644 --- a/cobbler/modules/manage_bind.py +++ b/cobbler/modules/manage_bind.py @@ -30,7 +30,6 @@ import sys import glob import traceback import errno -import popen2 import re from shlex import shlex @@ -82,7 +81,13 @@ class BindManager: in them """ zones = {} - for zone in self.settings.manage_forward_zones: + forward_zones = self.settings.manage_forward_zones + if type(forward_zones) != type([]): + # gracefully handle when user inputs only a single zone + # as a string instead of a list with only a single item + forward_zones = [forward_zones] + + for zone in forward_zones: zones[zone] = {} for system in self.systems: @@ -126,7 +131,13 @@ class BindManager: in them """ zones = {} - for zone in self.settings.manage_reverse_zones: + reverse_zones = self.settings.manage_reverse_zones + if type(reverse_zones) != type([]): + # gracefully handle when user inputs only a single zone + # as a string instead of a list with only a single item + reverse_zones = [reverse_zones] + + for zone in reverse_zones: zones[zone] = {} for sys in self.systems: @@ -223,14 +234,14 @@ zone "%(arpa)s." { octets = map(lambda x: [str(i) for i in x], octets) return ['.'.join(i) for i in octets] - def __pretty_print_host_records(self, hosts, type='A', rclass='IN'): + def __pretty_print_host_records(self, hosts, rectype='A', rclass='IN'): """ Format host records by order and with consistent indentation """ names = [k for k,v in hosts.iteritems()] if not names: return '' # zones with no hosts - if type == 'PTR': + if rectype == 'PTR': names = self.__ip_sort(names) else: names.sort() @@ -239,10 +250,10 @@ zone "%(arpa)s." { s = "" for name in names: - s += "%s %s %s %s\n" % (name + (" " * (max_name - len(name))), - rclass, - type, - hosts[name]) + spacing = " " * (max_name - len(name)) + my_name = "%s%s" % (name, spacing) + my_host = hosts[name] + s += "%s %s %s %s\n" % (my_name, rclass, rectype, my_host) return s def __write_zone_files(self): @@ -297,7 +308,7 @@ zone "%(arpa)s." { except: template_data = default_template_data - metadata['host_record'] = self.__pretty_print_host_records(hosts, type='PTR') + metadata['host_record'] = self.__pretty_print_host_records(hosts, rectype='PTR') self.templar.render(template_data, metadata, '/var/named/' + zone, None) diff --git a/cobbler/modules/manage_dnsmasq.py b/cobbler/modules/manage_dnsmasq.py index 344020f8..4a9b47d5 100644 --- a/cobbler/modules/manage_dnsmasq.py +++ b/cobbler/modules/manage_dnsmasq.py @@ -31,7 +31,6 @@ import sys import glob import traceback import errno -import popen2 from shlex import shlex import utils from cexceptions import * diff --git a/cobbler/modules/manage_isc.py b/cobbler/modules/manage_isc.py index fb22a380..9846314b 100644 --- a/cobbler/modules/manage_isc.py +++ b/cobbler/modules/manage_isc.py @@ -30,7 +30,7 @@ import sys import glob import traceback import errno -import popen2 +from utils import popen2 from shlex import shlex @@ -83,7 +83,7 @@ class IscManager: if ip.find("/") != -1: return try: - fromchild, tochild = popen2.popen2(self.settings.omshell_bin) + fromchild, tochild = popen2([self.settings.omshell_bin]) tochild.write("port %s\n" % port) tochild.flush() tochild.write("connect\n") @@ -111,7 +111,7 @@ class IscManager: Use DHCP's API to delete a DHCP entry in the /var/lib/dhcpd/dhcpd.leases file """ - fromchild, tochild = popen2.popen2(self.settings.omshell_bin) + fromchild, tochild = popen2([self.settings.omshell_bin]) try: tochild.write("port %s\n" % port) tochild.flush() diff --git a/cobbler/modules/serializer_catalog.py b/cobbler/modules/serializer_catalog.py index 35d1517a..72ab2cc3 100644 --- a/cobbler/modules/serializer_catalog.py +++ b/cobbler/modules/serializer_catalog.py @@ -25,6 +25,7 @@ import os import sys import glob import traceback +import yaml # PyYAML plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib @@ -32,8 +33,7 @@ sys.path.insert(0, mod_path) from utils import _ import utils -import yaml # Howell-Clark version -import cexceptions +from cexceptions import * import os def register(): @@ -46,7 +46,10 @@ def serialize_item(obj, item): filename = "/var/lib/cobbler/config/%ss.d/%s" % (obj.collection_type(),item.name) datastruct = item.to_datastruct() fd = open(filename,"w+") - fd.write(yaml.dump(datastruct)) + ydata = yaml.dump(datastruct) + if ydata is None or ydata == "": + raise CX("internal yaml error, tried to write empty file to %s, data was %s" % (filename, datastruct)) + fd.write(ydata) fd.close() return True @@ -63,7 +66,7 @@ def deserialize_item_raw(collection_type, item_name): if not os.path.exists(filename): return None fd = open(filename) - datastruct = yaml.load(fd.read()).next() + datastruct = yaml.load(fd.read()) fd.close() return datastruct @@ -84,14 +87,14 @@ def deserialize_raw(collection_type): old_filename = "/var/lib/cobbler/%ss" % collection_type if collection_type == "settings": fd = open("/etc/cobbler/settings") - datastruct = yaml.load(fd.read()).next() + datastruct = yaml.load(fd.read()) fd.close() return datastruct elif os.path.exists(old_filename): # for use in migration sys.stderr.write("reading from old config format: %s\n" % old_filename) fd = open(old_filename) - datastruct = yaml.load(fd.read()).next() + datastruct = yaml.load(fd.read()) fd.close() return datastruct else: @@ -99,7 +102,14 @@ def deserialize_raw(collection_type): files = glob.glob("/var/lib/cobbler/config/%ss.d/*" % collection_type) for f in files: fd = open(f) - results.append(yaml.load(fd.read()).next()) + ydata = fd.read() + if ydata is None or ydata == "": + raise CX("error, empty file %s" % f) + try: + datastruct = yaml.load(ydata) + except: + raise CX("error parsing yaml file: %s" % f) + results.append(datastruct) fd.close() return results diff --git a/cobbler/modules/serializer_yaml.py b/cobbler/modules/serializer_yaml.py index 75f89cf9..f583c7cc 100644 --- a/cobbler/modules/serializer_yaml.py +++ b/cobbler/modules/serializer_yaml.py @@ -25,6 +25,7 @@ import os import sys import glob import traceback +import yaml # PyYAML plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib @@ -32,7 +33,6 @@ sys.path.insert(0, mod_path) from utils import _ import utils -import yaml # Howell-Clark version import cexceptions import os @@ -87,7 +87,7 @@ def deserialize_raw(collection_type): except IOError, ioe: return [{}] data = fd.read() - datastruct = yaml.load(data).next() # first record + datastruct = yaml.load(data) # first record fd.close() return datastruct @@ -120,7 +120,7 @@ def deserialize(obj,topological=False): raise cexceptions.CX(_("Need permissions to read %s") % obj.filename()) data = fd.read() try: - datastruct = yaml.load(data).next() # first record + datastruct = yaml.load(data) # first record except: # load failure, make empty list datastruct = [] diff --git a/cobbler/modules/sync_post_restart_services.py b/cobbler/modules/sync_post_restart_services.py new file mode 100644 index 00000000..fa9699b9 --- /dev/null +++ b/cobbler/modules/sync_post_restart_services.py @@ -0,0 +1,70 @@ +import distutils.sysconfig +import sys +import os +from utils import _ +import traceback +import cexceptions +import os +import sys +import xmlrpclib +import cobbler.module_loader as module_loader + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +def register(): + # this pure python trigger acts as if it were a legacy shell-trigger, but is much faster. + # the return of this method indicates the trigger type + return "/var/lib/cobbler/triggers/sync/post/*" + +def run(api,args): + + settings = api.settings() + + manage_dhcp = str(settings.manage_dhcp).lower() + manage_dns = str(settings.manage_dns).lower() + manage_xinetd = str(settings.manage_xinetd).lower() + restart_dhcp = str(settings.restart_dhcp).lower() + restart_dns = str(settings.restart_dns).lower() + restart_xinetd = str(settings.restart_xinetd).lower() + omapi_enabled = str(settings.omapi_enabled).lower() + omapi_port = str(settings.omapi_port).lower() + + which_dhcp_module = module_loader.get_module_from_file("dhcp","module",just_name=True).strip() + which_dns_module = module_loader.get_module_from_file("dns","module",just_name=True).strip() + + # special handling as we don't want to restart it twice + has_restarted_dnsmasq = False + + rc = 0 + if manage_dhcp != "0": + if which_dhcp_module == "manage_isc": + if not omapi_enabled in [ "1", "true", "yes", "y" ] and restart_dhcp: + rc = os.system("/usr/sbin/dhcpd -t") + if rc != 0: + print "/usr/sbin/dhcpd -t failed" + return 1 + rc = os.system("/sbin/service dhcpd restart") + elif which_dhcp_module == "manage_dnsmasq": + if restart_dhcp: + rc = os.system("/sbin/service dnsmasq restart") + has_restarted_dnsmasq = True + else: + print "- error: unknown DHCP engine: %s" % which_dhcp_module + rc = 411 + + if manage_dns != "0" and restart_dns != "0": + if which_dns_module == "manage_bind": + rc = os.system("/sbin/service named restart") + elif which_dns_module == "manage_dnsmasq" and not has_restarted_dnsmasq: + rc = os.ssytem("/sbin/service dnsmasq restart") + else: + print "- error: unknown DNS engine: %s" % which_dns_module + rc = 412 + + if manage_xinetd != "0" and restart_xinetd != "0": + rc = os.system("/sbin/service xinetd restart") + + return rc + diff --git a/cobbler/pxegen.py b/cobbler/pxegen.py index 3fcc4030..7ff07808 100644 --- a/cobbler/pxegen.py +++ b/cobbler/pxegen.py @@ -24,6 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA import os import os.path import shutil +import shlex import time import sys import glob @@ -64,6 +65,7 @@ class PXEGen: self.images = config.images() self.templar = templar.Templar(config) self.bootloc = utils.tftpboot_location() + self.verbose = False def copy_bootloaders(self): """ @@ -75,27 +77,27 @@ class PXEGen: # copy syslinux from one of two locations try: - utils.copyfile_pattern('/usr/lib/syslinux/pxelinux.0', dst, api=self.api) + utils.copyfile_pattern('/usr/lib/syslinux/pxelinux.0', dst, api=self.api, verbose=self.verbose) except: - utils.copyfile_pattern('/usr/share/syslinux/pxelinux.0', dst, api=self.api) + utils.copyfile_pattern('/usr/share/syslinux/pxelinux.0', dst, api=self.api, verbose=self.verbose) # copy memtest only if we find it - utils.copyfile_pattern('/boot/memtest*', dst, require_match=False, api=self.api) + utils.copyfile_pattern('/boot/memtest*', dst, require_match=False, api=self.api, verbose=self.verbose) # copy elilo which we include for IA64 targets - utils.copyfile_pattern('/var/lib/cobbler/elilo-3.8-ia64.efi', dst, api=self.api) + utils.copyfile_pattern('/var/lib/cobbler/elilo-3.8-ia64.efi', dst, api=self.api, verbose=self.verbose) # copy menu.c32 as the older one has some bugs on certain RHEL - utils.copyfile_pattern('/var/lib/cobbler/menu.c32', dst, api=self.api) + utils.copyfile_pattern('/var/lib/cobbler/menu.c32', dst, api=self.api, verbose=self.verbose) # copy yaboot which we include for PowerPC targets - utils.copyfile_pattern('/var/lib/cobbler/yaboot-1.3.14', dst, api=self.api) + utils.copyfile_pattern('/var/lib/cobbler/yaboot-1.3.14', dst, api=self.api, verbose=self.verbose) # copy memdisk as we need it to boot ISOs try: - utils.copyfile_pattern('/usr/lib/syslinux/memdisk', dst, api=self.api) + utils.copyfile_pattern('/usr/lib/syslinux/memdisk', dst, api=self.api, verbose=self.verbose) except: - utils.copyfile_pattern('/usr/share/syslinux/memdisk', dst, api=self.api) + utils.copyfile_pattern('/usr/share/syslinux/memdisk', dst, api=self.api, verbose=self.verbose) def copy_distros(self): @@ -114,14 +116,14 @@ class PXEGen: if d.breed == "windows": continue try: + if self.verbose: + print "- copying files for distro: %s" % d.name self.copy_single_distro_files(d) except CX, e: errors.append(e) print e.value # FIXME: using logging module so this ends up in cobbler.log? - if len(errors) > 0: - raise CX(_("Error(s) encountered while copying distro files")) def copy_images(self): """ @@ -130,14 +132,14 @@ class PXEGen: errors = list() for i in self.images: try: + if self.verbose: + print "- copying files for image: %s" % i.name self.copy_single_image_files(i) except CX, e: errors.append(e) print e.value # FIXME: using logging module so this ends up in cobbler.log? - if len(errors) > 0: - raise CX(_("Error(s) encountered while copying image files")) def copy_single_distro_files(self, d): for dirtree in [self.bootloc, self.settings.webdir]: @@ -157,8 +159,9 @@ class PXEGen: allow_symlink=True dst1 = os.path.join(distro_dir, b_kernel) dst2 = os.path.join(distro_dir, b_initrd) - utils.linkfile(kernel, dst1, symlink_ok=allow_symlink, api=self.api) - utils.linkfile(initrd, dst2, symlink_ok=allow_symlink, api=self.api) + utils.linkfile(kernel, dst1, symlink_ok=allow_symlink, api=self.api, verbose=self.verbose) + + utils.linkfile(initrd, dst2, symlink_ok=allow_symlink, api=self.api, verbose=self.verbose) def copy_single_image_files(self, img): images_dir = os.path.join(self.bootloc, "images2") @@ -170,7 +173,7 @@ class PXEGen: os.makedirs(images_dir) basename = os.path.basename(img.file) newfile = os.path.join(images_dir, img.name) - utils.linkfile(filename, newfile, api=self.api) + utils.linkfile(filename, newfile, api=self.api, verbose=self.verbose) return True def generate_windows_files(self): @@ -316,6 +319,150 @@ class PXEGen: return True + def generate_windows_files(self): + # FIXME: hard coding of /tftpboot here is wrong + utils.mkdir("/tftpboot/profiles") + for p in self.profiles: + distro = p.get_conceptual_parent() + if distro and distro.breed != "windows": + continue + else: + self.generate_windows_profile_pxe(p) + utils.mkdir("/tftpboot/systems") + for s in self.systems: + profile = s.get_conceptual_parent() + if profile: + distro = profile.get_conceptual_parent() + if distro and distro.breed != "windows": + continue + else: + self.generate_windows_system_pxe(s) + + def generate_windows_profile_pxe(self, profile): + distro = profile.get_conceptual_parent() + + dest_dir = os.path.join("/tftpboot/profiles", profile.name) + utils.mkdir(dest_dir) + + utils.cabextract(distro.kernel, dest_dir, self.api) + + src_file = os.path.join(dest_dir, "startrom.n12") + dest_file = os.path.join(dest_dir, "winpxe.0") + cmd = [ "/bin/sed", "-i", "-e", "s/ntldr/L%s/gi" % profile.random_id, src_file ] + sub_process.call(cmd, shell=False, close_fds=False) + os.rename(src_file, dest_file) + + utils.cabextract(distro.initrd, dest_dir, self.api) + + src_file = os.path.join(dest_dir, "setupldr.exe") + dest_file = os.path.join(dest_dir, "NTLDR") + cmd = [ "/bin/sed", "-i", "-e", "s/winnt\\.sif/w%s.sif/gi" % profile.random_id, src_file ] + sub_process.call(cmd, shell=False, close_fds=False) + cmd = [ "/bin/sed", "-i", "-e", "s/ntdetect\\.com/ntd_%s.com/gi" % profile.random_id, src_file ] + sub_process.call(cmd, shell=False, close_fds=False) + os.rename(src_file, dest_file) + + src_dir = os.path.dirname(distro.kernel) + src_file = os.path.join(src_dir, "ntdetect.com") + file_name = os.path.join(dest_dir, "ntdetect.com") + utils.copyfile(src_file, file_name, self.api) + + template = profile.kickstart + fd = open(template, "r") + template_data = fd.read() + fd.close() + + blended = utils.blender(self.api, False, profile) + blended['next_server'] = self.settings.next_server + + ksmeta = blended.get("ks_meta",{}) + del blended["ks_meta"] + blended.update(ksmeta) # make available at top level + + # this is a workaround for a dumb bug in cheetah... + # a backslash before a variable is always treated as + # escaping the $, so things are not rendered correctly. + # The only option is to use another variable for + # backslashes when leading another variable. i.e.: + # \\$variable + blended['bsp'] = '\\' + + data = self.templar.render(template_data, blended, None) + + # write .sif to /tftpboot/profiles/$name/winnt.sif + file_name = os.path.join(dest_dir, "winnt.sif") + fd = open(file_name, "w") + fd.write(data) + fd.close() + + return True + + def generate_windows_system_pxe(self, system): + profile = system.get_conceptual_parent() + if not profile: + return False + + distro = profile.get_conceptual_parent() + + dest_dir = os.path.join("/tftpboot/systems", system.name) + utils.mkdir(dest_dir) + + utils.cabextract(distro.kernel, dest_dir, self.api) + + src_file = os.path.join(dest_dir, "startrom.n12") + dest_file = os.path.join(dest_dir, "winpxe.0") + cmd = [ "/bin/sed", "-i", "-e", "s/ntldr/L%s/gi" % system.random_id, src_file ] + sub_process.call(cmd, shell=False, close_fds=False) + os.rename(src_file, dest_file) + + utils.cabextract(distro.initrd, dest_dir, self.api) + + src_file = os.path.join(dest_dir, "setupldr.exe") + dest_file = os.path.join(dest_dir, "NTLDR") + cmd = [ "/bin/sed", "-i", "-e", "s/winnt\\.sif/w%s.sif/gi" % system.random_id, src_file ] + sub_process.call(cmd, shell=False, close_fds=False) + cmd = [ "/bin/sed", "-i", "-e", "s/ntdetect\\.com/ntd_%s.com/gi" % system.random_id, src_file ] + sub_process.call(cmd, shell=False, close_fds=False) + os.rename(src_file, dest_file) + + src_dir = os.path.dirname(distro.kernel) + src_file = os.path.join(src_dir, "ntdetect.com") + file_name = os.path.join(dest_dir, "ntdetect.com") + utils.copyfile(src_file, file_name, self.api) + + template = system.kickstart + if template == "<<inherit>>": + template = profile.kickstart + + fd = open(template, "r") + template_data = fd.read() + fd.close() + + blended = utils.blender(self.api, False, system) + blended['next_server'] = self.settings.next_server + + ksmeta = blended.get("ks_meta",{}) + del blended["ks_meta"] + blended.update(ksmeta) # make available at top level + + # this is a workaround for a dumb bug in cheetah... + # a backslash before a variable is always treated as + # escaping the $, so things are not rendered correctly. + # The only option is to use another variable for + # backslashes when leading another variable. i.e.: + # \\$variable + blended['bsp'] = '\\' + + data = self.templar.render(template_data, blended, None) + + # write .sif to /tftpboot/systems/$name/winnt.sif + file_name = os.path.join(dest_dir, "winnt.sif") + fd = open(file_name, "w") + fd.write(data) + fd.close() + + return True + def write_all_system_files(self,system): profile = system.get_conceptual_parent() @@ -331,8 +478,35 @@ class PXEGen: image_based = True image = profile - # this used to just generate a single PXE config file, but now must - # generate one record for each described NIC ... + # hack: s390 generates files per system not per interface + if not image_based and distro.arch.startswith("s390"): + # Always write a system specific _conf and _parm file + f2 = os.path.join(self.bootloc, "s390x", "s_%s" % system.name) + cf = "%s_conf" % f2 + pf = "%s_parm" % f2 + template_cf = open("/etc/cobbler/pxe/s390x_conf.template") + template_pf = open("/etc/cobbler/pxe/s390x_parm.template") + blended = utils.blender(self.api, True, system) + self.templar.render(template_cf, blended, cf) + # FIXME: profiles also need this data! + # FIXME: the _conf and _parm files are limited to 80 characters in length + kickstart_path = "http://%s/cblr/svc/op/ks/system/%s" % (blended["http_server"], system.name) + # gather default kernel_options and default kernel_options_s390x + kopts = blended.get("kernel_options","") + hkopts = shlex.split(utils.hash_to_string(kopts)) + blended["kickstart_expanded"] = "ks=%s" % kickstart_path + blended["kernel_options"] = hkopts + self.templar.render(template_pf, blended, pf) + + # Write system specific zPXE file + if system.is_management_supported(): + self.write_pxe_file(f2,system,profile,distro,distro.arch) + else: + # ensure the file doesn't exist + utils.rmfile(f2) + return + + # generate one record for each described NIC .. for (name,interface) in system.interfaces.iteritems(): @@ -345,8 +519,8 @@ class PXEGen: else: working_arch = distro.arch - if working_arch is None or working_arch == "": - working_arch = "x86" + if working_arch is None: + raise "internal error, invalid arch supplied" # for tftp only ... if working_arch in [ "i386", "x86", "x86_64", "standard"]: @@ -370,10 +544,6 @@ class PXEGen: if os.path.lexists(f3): utils.rmfile(f3) os.symlink("../yaboot-1.3.14", f3) - - elif working_arch == "s390x": - filename = "%s" % utils.get_config_filename(system,interface=name) - f2 = os.path.join(self.bootloc, "s390x", filename) else: continue @@ -405,18 +575,27 @@ class PXEGen: distro = profile.get_conceptual_parent() if distro is None: raise CX(_("profile is missing distribution: %s, %s") % (profile.name, profile.distro)) - if distro.arch == "s390x": + if distro.arch.startswith("s390"): listfile.write("%s\n" % profile.name) - f2 = os.path.join(self.bootloc, "s390x", profile.name) - self.write_pxe_file(f2,None,profile,distro,distro.arch) - listfile2 = open(os.path.join(s390path, "image_list"),"w+") - for image in image_list: - if os.path.exists(image.file): - listfile2.write("%s\n" % image.name) - f2 = os.path.join(self.bootloc, "s390x", image.name) - self.write_pxe_file(f2,None,None,None,image.arch,image=image) + f2 = os.path.join(self.bootloc, "s390x", "p_%s" % profile.name) + self.write_pxe_file(f2,None,profile,distro,distro.arch) + cf = "%s_conf" % f2 + pf = "%s_parm" % f2 + template_cf = open("/etc/cobbler/pxe/s390x_conf.template") + template_pf = open("/etc/cobbler/pxe/s390x_parm.template") + blended = utils.blender(self.api, True, profile) + self.templar.render(template_cf, blended, cf) + # FIXME: profiles also need this data! + # FIXME: the _conf and _parm files are limited to 80 characters in length + kickstart_path = "http://%s/cblr/svc/op/ks/profile/%s" % (blended["http_server"], profile.name) + # gather default kernel_options and default kernel_options_s390x + kopts = blended.get("kernel_options","") + hkopts = shlex.split(utils.hash_to_string(kopts)) + blended["kickstart_expanded"] = "ks=%s" % kickstart_path + blended["kernel_options"] = hkopts + self.templar.render(template_pf, blended, pf) + listfile.close() - listfile2.close() def make_actual_pxe_menu(self): # only do this if there is NOT a system named default. @@ -448,7 +627,7 @@ class PXEGen: continue distro = profile.get_conceptual_parent() # xen distros can be ruled out as they won't boot - if distro.name.find("-xen") != -1: + if distro.name.find("-xen") != -1 or distro.arch not in ["i386", "x86_64"]: # can't PXE Xen continue contents = self.write_pxe_file(None,None,profile,distro,distro.arch,include_header=False) @@ -524,6 +703,9 @@ class PXEGen: more details """ + if arch is None: + raise "missing arch" + if image and not os.path.exists(image.file): return None # nfs:// URLs or something, can't use for TFTP @@ -539,9 +721,10 @@ class PXEGen: initrd_path = None if image is None: - # profile or system+profile based, not image based, or system+image based + # not image based, it's something normalish if distro is not None and distro.breed == "windows": + # this is to support linux-ris if system: kernel_path = "systems/%s/winpxe.0" % system.name elif profile: @@ -551,8 +734,14 @@ class PXEGen: initrd_path = os.path.join("/images",distro.name,os.path.basename(distro.initrd)) # Find the kickstart if we inherit from another profile - kickstart_path = utils.blender(self.api, True, profile)["kickstart"] + if system: + blended = utils.blender(self.api, True, system) + else: + blended = utils.blender(self.api, True, profile) + kickstart_path = blended["kickstart"] + else: + # this is an image we are making available, not kernel+initrd if image.image_type == "direct": kernel_path = os.path.join("/images2",image.name) elif image.image_type == "memdisk": @@ -571,19 +760,18 @@ class PXEGen: else: template = os.path.join(self.settings.pxe_template_dir,"pxesystem.template") - if arch == "s390x": - template = os.path.join(self.settings.pxe_template_dir,"pxesystem_s390x.template") - elif arch == "ia64": - template = os.path.join(self.settings.pxe_template_dir,"pxesystem_ia64.template") - elif arch.startswith("ppc"): - template = os.path.join(self.settings.pxe_template_dir,"pxesystem_ppc.template") + if arch.startswith("s390"): + template = os.path.join(self.settings.pxe_template_dir,"pxesystem_s390x.template") + elif arch == "ia64": + template = os.path.join(self.settings.pxe_template_dir,"pxesystem_ia64.template") + elif arch.startswith("ppc"): + template = os.path.join(self.settings.pxe_template_dir,"pxesystem_ppc.template") else: # local booting on ppc requires removing the system-specific dhcpd.conf filename if arch is not None and arch.startswith("ppc"): # Disable yaboot network booting for all interfaces on the system for (name,interface) in system.interfaces.iteritems(): - # Determine filename for system-specific yaboot.conf filename = "%s" % utils.get_config_filename(system, interface=name).lower() # Remove symlink to the yaboot binary @@ -599,15 +787,20 @@ class PXEGen: # Yaboot/OF doesn't support booting locally once you've # booted off the network, so nothing left to do return None + elif arch is not None and arch.startswith("s390"): + template = os.path.join(self.settings.pxe_template_dir,"pxelocal_s390x.template") else: template = os.path.join(self.settings.pxe_template_dir,"pxelocal.template") else: + # not a system record, so this is a profile record + if distro is not None and distro.breed == "windows": template = os.path.join(self.settings.pxe_template_dir,"pxeprofile_win.template") + elif arch.startswith("s390"): + template = os.path.join(self.settings.pxe_template_dir,"pxeprofile_s390x.template") else: template = os.path.join(self.settings.pxe_template_dir,"pxeprofile.template") - # now build the kernel command line if system is not None: blended = utils.blender(self.api, True, system) @@ -624,15 +817,18 @@ class PXEGen: else: append_line = "append %s" % hkopts + # FIXME - the append_line length limit is architecture specific if len(append_line) >= 255 + len("append "): print _("warning: kernel option length exceeds 255") # kickstart path rewriting (get URLs for local files) if kickstart_path is not None and kickstart_path != "": + # FIXME: need to make shorter rewrite rules for these URLs + if system is not None and kickstart_path.startswith("/"): kickstart_path = "http://%s/cblr/svc/op/ks/system/%s" % (blended["http_server"], system.name) - elif kickstart_path.startswith("/") or kickstart_path.find("/cobbler/kickstarts/") != -1: + elif kickstart_path.startswith("/"): kickstart_path = "http://%s/cblr/svc/op/ks/profile/%s" % (blended["http_server"], profile.name) if distro.breed is None or distro.breed == "redhat": @@ -644,14 +840,14 @@ class PXEGen: # interface=bootif causes a failure # append_line = append_line.replace("ksdevice","interface") - if arch in ["s390x", "ppc", "ppc64"]: + if arch.startswith("ppc") or arch.startswith("s390"): # remove the prefix "append" append_line = append_line[7:] # store variables for templating metadata["menu_label"] = "" if profile: - if not arch in [ "ia64", "ppc", "ppc64", "s390x" ]: + if not arch in [ "ia64", "ppc", "ppc64", "s390", "s390x" ]: metadata["menu_label"] = "MENU LABEL %s" % profile.name metadata["profile_name"] = profile.name elif image: @@ -706,10 +902,15 @@ class PXEGen: return results blended = utils.blender(self.api, False, obj) + ksmeta = blended.get("ks_meta",{}) del blended["ks_meta"] blended.update(ksmeta) # make available at top level + templates = blended.get("template_files",{}) + del blended["template_files"] + blended.update(templates) # make available at top level + (success, templates) = utils.input_string_or_hash(templates) if not success: @@ -718,7 +919,9 @@ class PXEGen: for template in templates.keys(): dest = templates[template] - + if dest is None: + continue + # Run the source and destination files through # templar first to allow for variables in the path template = self.templar.render(template, blended, None).strip() diff --git a/cobbler/remote.py b/cobbler/remote.py index 8154dc58..10e9cc20 100644 --- a/cobbler/remote.py +++ b/cobbler/remote.py @@ -27,10 +27,13 @@ import sys import socket import time import os +import base64 import SimpleXMLRPCServer import xmlrpclib import random +import stat import base64 +import fcntl import string import traceback import glob @@ -45,7 +48,7 @@ import item_system import item_repo import item_image from utils import * -from utils import _ # * does not import _ +from utils import _ # FIXME: make configurable? TOKEN_TIMEOUT = 60*60 # 60 minutes @@ -53,25 +56,86 @@ OBJECT_TIMEOUT = 60*60 # 60 minutes TOKEN_CACHE = {} OBJECT_CACHE = {} +class DataCache: + + def __init__(self, api): + """ + Constructor + """ + self.api = api + + def update(self,collection_type, name): + data = self.api.deserialize_item_raw(collection_type, name) + + if data is None: + return False + + if collection_type == "distro": + obj = item_distro.Distro(self.api._config) + obj.from_datastruct(data) + self.api.add_distro(obj, False, False) + + if collection_type == "profile": + subprofile = False + if data.has_key("parent") and data["parent"] != "": + subprofile = True + obj = item_profile.Profile(self.api._config, is_subobject = subprofile) + obj.from_datastruct(data) + self.api.add_profile(obj, False, False) + + if collection_type == "system": + obj = item_system.System(self.api._config) + obj.from_datastruct(data) + self.api.add_system(obj, False, False, False) + + if collection_type == "repo": + obj = item_repo.Repo(self.api._config) + obj.from_datastruct(data) + self.api.add_repo(obj, False, False) + + if collection_type == "image": + obj = item_image.Image(self.api._config) + obj.from_datastruct(data) + self.api.add_image(obj, False, False) + + + def remove(self,collection_type, name): + # for security reasons, only remove if actually gone + data = self.api.deserialize_item_raw(collection_type, name) + if data is None: + if collection_type == "distro": + self.api.remove_distro(name, delete=False, recursive=True, with_triggers=False) + if collection_type == "profile": + self.api.remove_profile(name, delete=False, recursive=True, with_triggers=False) + if collection_type == "system": + self.api.remove_system(name, delete=False, recursive=True, with_triggers=False) + if collection_type == "repo": + self.api.remove_repo(name, delete=False, recursive=True, with_triggers=False) + if collection_type == "image": + self.api.remove_image(name, delete=False, recursive=True, with_triggers=False) + # ********************************************************************* # ********************************************************************* class CobblerXMLRPCInterface: """ - This is the interface used for all public XMLRPC methods, for instance, - as used by koan. The read-write interface which inherits from this adds - more methods, though that interface can be disabled. + This is the interface used for all XMLRPC methods, for instance, + as used by koan or CobblerWeb note: public methods take an optional parameter token that is just - here for consistancy with the ReadWrite API. The tokens for the read only - interface are intentionally /not/ validated. It's a public API. + here for consistancy with the ReadWrite API. Read write operations do + require the token. """ - def __init__(self,api,logger,enable_auth_if_relevant): + def __init__(self,api,enable_auth_if_relevant): self.api = api - self.logger = logger self.auth_enabled = enable_auth_if_relevant + self.cache = DataCache(self.api) + self.logger = self.api.logger + self.token_cache = TOKEN_CACHE + self.object_cache = OBJECT_CACHE self.timestamp = self.api.last_modified_time() + random.seed(time.time()) def __sorter(self,a,b): return cmp(a["name"],b["name"]) @@ -85,10 +149,15 @@ class CobblerXMLRPCInterface: return self.api.last_modified_time() def update(self, token=None): - now = self.api.last_modified_time() - if (now > self.timestamp): - self.timestamp = now - self.api.update() + # no longer neccessary + return True + + def internal_cache_update(self, collection_type, data): + self.cache.update(collection_type, data) + return True + + def internal_cache_remove(self, collection_type, data): + self.cache.remove(collection_type, data) return True def ping(self): @@ -163,11 +232,26 @@ class CobblerXMLRPCInterface: # FIXME: a global lock or module around data access loading # would be useful for non-db backed storage - data = self.api.deserialize_raw(collection_name) - total_items = len(data) - if collection_name == "settings": - return self._fix_none(data) + data = self.api.deserialize_raw("settings") + return self.xmlrpc_hacks(data) + else: + contents = [] + if collection_name.startswith("distro"): + contents = self.api.distros() + elif collection_name.startswith("profile"): + contents = self.api.profiles() + elif collection_name.startswith("system"): + contents = self.api.systems() + elif collection_name.startswith("repo"): + contents = self.api.repos() + elif collection_name.startswith("image"): + contents = self.api.images() + else: + raise CX("internal error, collection name is %s" % collection_name) + # FIXME: speed this up + data = contents.to_datastruct() + total_items = len(data) data.sort(self.__sorter) @@ -184,9 +268,9 @@ class CobblerXMLRPCInterface: start_point = total_items - 1 # correct ??? if end_point > total_items: end_point = total_items - data = self._fix_none(data[start_point:end_point]) + data = self.xmlrpc_hacks(data[start_point:end_point]) - return self._fix_none(data) + return self.xmlrpc_hacks(data) def get_kickstart_templates(self,token=None,**rest): """ @@ -208,10 +292,6 @@ class CobblerXMLRPCInterface: def generate_kickstart(self,profile=None,system=None,REMOTE_ADDR=None,REMOTE_MAC=None,**rest): self._log("generate_kickstart") - - if profile and not system: - regrc = self.register_mac(REMOTE_MAC,profile) - return self.api.generate_kickstart(profile,system) def get_settings(self,token=None,**rest): @@ -219,7 +299,9 @@ class CobblerXMLRPCInterface: Return the contents of /etc/cobbler/settings, which is a hash. """ self._log("get_settings",token=token) - return self.__get_all("settings") + results = self.api.settings().to_datastruct() + self._log("my settings are: %s" % results) + return self.xmlrpc_hacks(results) def get_repo_config_for_profile(self,profile_name,**rest): """ @@ -259,43 +341,78 @@ class CobblerXMLRPCInterface: return "# object not found: %s" % system_name return self.api.get_template_file_for_system(obj,path) - def register_mac(self,mac,profile,token=None,**rest): + def register_new_system(self,info,token=None,**rest): """ If register_new_installs is enabled in settings, this allows - kickstarts to add new system records for per-profile-provisioned - systems automatically via a wget in %post. This has security - implications. - READ: https://fedorahosted.org/cobbler/wiki/AutoRegistration + /usr/bin/cobbler-register (part of the koan package) to add + new system records remotely if they don't already exist. + There is a cobbler_register snippet that helps with doing + this automatically for new installs but it can also be used + for existing installs. See "AutoRegistration" on the Wiki. """ - - if mac is None: - # don't go further if not being called by anaconda - return 1 - - if not self.api.settings().register_new_installs: - # must be enabled in settings - return 2 - - system = self.api.find_system(mac_address=mac) - if system is not None: - # do not allow overwrites - return 3 - - # the MAC probably looks like "eth0 AA:BB:CC:DD:EE:FF" now, fix it - if mac.find(" ") != -1: - mac = mac.split()[-1] - - dup = self.api.find_system(mac_address=mac) - if dup is not None: - return 4 - - self._log("register mac for profile %s" % profile,token=token,name=mac) + + enabled = self.api.settings().register_new_installs + if not str(enabled) in [ "1", "y", "yes", "true" ]: + raise CX("registration is disabled in cobbler settings") + + # validate input + name = info.get("name","") + profile = info.get("profile","") + hostname = info.get("hostname","") + interfaces = info.get("interfaces",{}) + ilen = len(interfaces.keys()) + + if name == "": + raise CX("no system name submitted") + if profile == "": + raise CX("profile not submitted") + if ilen == 0: + raise CX("no interfaces submitted") + if ilen >= 64: + raise CX("too many interfaces submitted") + + # validate things first + name = info.get("name","") + inames = interfaces.keys() + if self.api.find_system(name=name): + raise CX("system name conflicts") + if hostname != "" and self.api.find_system(hostname=hostname): + raise CX("hostname conflicts") + + for iname in inames: + mac = info["interfaces"][iname].get("mac_address","") + ip = info["interfaces"][iname].get("ip_address","") + if ip.find("/") != -1: + raise CX("no CIDR ips are allowed") + if mac == "": + raise CX("missing MAC address for interface %s" % iname) + if mac != "": + system = self.api.find_system(mac_address=mac) + if system is not None: + raise CX("mac conflict: %s" % mac) + if ip != "": + system = self.api.find_system(ip_address=ip) + if system is not None: + raise CX("ip conflict: %s"% ip) + + # looks like we can go ahead and create a system now obj = self.api.new_system() obj.set_profile(profile) - name = mac.replace(":","_") obj.set_name(name) - obj.set_mac_address(mac, "eth0") + if hostname != "": + obj.set_hostname(hostname) obj.set_netboot_enabled(False) + for iname in inames: + mac = info["interfaces"][iname].get("mac_address","") + ip = info["interfaces"][iname].get("ip_address","") + netmask = info["interfaces"][iname].get("netmask","") + obj.set_mac_address(mac, iname) + if hostname != "": + obj.set_dns_name(hostname, iname) + if ip != "": + obj.set_ip_address(ip, iname) + if netmask != "": + obj.set_subnet(netmask, iname) self.api.add_system(obj) return 0 @@ -320,6 +437,109 @@ class CobblerXMLRPCInterface: systems.add(obj,save=True,with_triggers=False,with_sync=False,quick_pxe_update=True) return True + def upload_log_data(self, sys_name, file, size, offset, data, token=None,**rest): + + """ + This is a logger function used by the "anamon" logging system to + upload all sorts of auxilliary data from Anaconda. + As it's a bit of a potential log-flooder, it's off by default + and needs to be enabled in /etc/cobbler/settings. + """ + + self._log("upload_log_data (file: '%s', size: %s, offset: %s)" % (file, size, offset), token=token, name=sys_name) + + # Check if enabled in self.api.settings() + if not self.api.settings().anamon_enabled: + # feature disabled! + return False + + # Find matching system record + systems = self.api.systems() + obj = systems.find(name=sys_name) + if obj == None: + # system not found! + self._log("upload_log_data - system '%s' not found" % sys_name, token=token, name=sys_name) + return False + + return self.__upload_file(sys_name, file, size, offset, data) + + def __upload_file(self, sys_name, file, size, offset, data): + ''' + system: the name of the system + name: the name of the file + size: size of contents (bytes) + data: base64 encoded file contents + offset: the offset of the chunk + files can be uploaded in chunks, if so the size describes + the chunk rather than the whole file. the offset indicates where + the chunk belongs + the special offset -1 is used to indicate the final chunk''' + contents = base64.decodestring(data) + del data + if offset != -1: + if size is not None: + if size != len(contents): + return False + + #XXX - have an incoming dir and move after upload complete + # SECURITY - ensure path remains under uploadpath + tt = string.maketrans("/","+") + fn = string.translate(file, tt) + if fn.startswith('..'): + raise CX(_("invalid filename used: %s") % fn) + + # FIXME ... get the base dir from cobbler settings() + udir = "/var/log/cobbler/anamon/%s" % sys_name + if not os.path.isdir(udir): + os.mkdir(udir, 0755) + + fn = "%s/%s" % (udir, fn) + try: + st = os.lstat(fn) + except OSError, e: + if e.errno == errno.ENOENT: + pass + else: + raise + else: + if not stat.S_ISREG(st.st_mode): + raise CX(_("destination not a file: %s") % fn) + + fd = os.open(fn, os.O_RDWR | os.O_CREAT, 0644) + # log_error("fd=%r" %fd) + try: + if offset == 0 or (offset == -1 and size == len(contents)): + #truncate file + fcntl.lockf(fd, fcntl.LOCK_EX|fcntl.LOCK_NB) + try: + os.ftruncate(fd, 0) + # log_error("truncating fd %r to 0" %fd) + finally: + fcntl.lockf(fd, fcntl.LOCK_UN) + if offset == -1: + os.lseek(fd,0,2) + else: + os.lseek(fd,offset,0) + #write contents + fcntl.lockf(fd, fcntl.LOCK_EX|fcntl.LOCK_NB, len(contents), 0, 2) + try: + os.write(fd, contents) + # log_error("wrote contents") + finally: + fcntl.lockf(fd, fcntl.LOCK_UN, len(contents), 0, 2) + if offset == -1: + if size is not None: + #truncate file + fcntl.lockf(fd, fcntl.LOCK_EX|fcntl.LOCK_NB) + try: + os.ftruncate(fd, size) + # log_error("truncating fd %r to size %r" % (fd,size)) + finally: + fcntl.lockf(fd, fcntl.LOCK_UN) + finally: + os.close(fd) + return True + def run_install_triggers(self,mode,objtype,name,ip,token=None,**rest): """ @@ -339,7 +559,7 @@ class CobblerXMLRPCInterface: # time if reinstalling all of a cluster all at once. # we can do that at "cobbler check" time. - utils.run_triggers(None, "/var/lib/cobbler/triggers/install/%s/*" % mode, additional=[objtype,name,ip]) + utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/install/%s/*" % mode, additional=[objtype,name,ip]) return True @@ -366,41 +586,81 @@ class CobblerXMLRPCInterface: self._log("get_distros",token=token) return self.__get_all("distro",page,results_per_page) + + def __find(self,find_function,criteria={},expand=False,token=None): + name = criteria.get("name",None) + if name is not None: + del criteria["name"] + if not expand: + data = [x.name for x in find_function(name, True, True, **criteria)] + else: + data = [x.to_datastruct() for x in find_function(name, True, True, **criteria)] + return self.xmlrpc_hacks(data) + + def find_distro(self,criteria={},expand=False,token=None,**rest): + self._log("find_distro", token=token) + # FIXME DEBUG + self._log(criteria) + data = self.__find(self.api.find_distro,criteria,expand=expand,token=token) + # FIXME DEBUG + self._log(data) + return data + + def find_profile(self,criteria={},expand=False,token=None,**rest): + self._log("find_profile", token=token) + data = self.__find(self.api.find_profile,criteria,expand=expand,token=token) + return data + + def find_system(self,criteria={},expand=False,token=None,**rest): + self._log("find_system", token=token) + data = self.__find(self.api.find_system,criteria,expand=expand,token=token) + return data + + def find_repo(self,criteria={},expand=False,token=None,**rest): + self._log("find_repo", token=token) + data = self.__find(self.api.find_repo,criteria,expand=expand,token=token) + return data + + def find_image(self,criteria={},expand=False,token=None,**rest): + self._log("find_image", token=token) + data = self.__find(self.api.find_image,criteria,expand=expand,token=token) + return data + def get_distros_since(self,mtime): """ Return all of the distro objects that have been modified after mtime. """ data = self.api.get_distros_since(mtime, collapse=True) - return self._fix_none(data) + return self.xmlrpc_hacks(data) def get_profiles_since(self,mtime): """ See documentation for get_distros_since """ data = self.api.get_profiles_since(mtime, collapse=True) - return self._fix_none(data) + return self.xmlrpc_hacks(data) def get_systems_since(self,mtime): """ See documentation for get_distros_since """ data = self.api.get_systems_since(mtime, collapse=True) - return self._fix_none(data) + return self.xmlrpc_hacks(data) def get_repos_since(self,mtime): """ See documentation for get_distros_since """ data = self.api.get_repos_since(mtime, collapse=True) - return self._fix_none(data) + return self.xmlrpc_hacks(data) def get_images_since(self,mtime): """ See documentation for get_distros_since """ data = self.api.get_images_since(mtime, collapse=True) - return self._fix_none(data) + return self.xmlrpc_hacks(data) def get_profiles(self,page=None,results_per_page=None,token=None,**rest): """ @@ -476,7 +736,7 @@ class CobblerXMLRPCInterface: return {} if flatten: result = utils.flatten(result) - return self._fix_none(result) + return self.xmlrpc_hacks(result) def get_distro(self,name,flatten=False,token=None,**rest): """ @@ -541,8 +801,8 @@ class CobblerXMLRPCInterface: self._log("get_distro_as_rendered",name=name,token=token) obj = self.api.find_distro(name=name) if obj is not None: - return self._fix_none(utils.blender(self.api, True, obj)) - return self._fix_none({}) + return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) + return self.xmlrpc_hacks({}) def get_profile_as_rendered(self,name,token=None,**rest): """ @@ -559,8 +819,8 @@ class CobblerXMLRPCInterface: self._log("get_profile_as_rendered", name=name, token=token) obj = self.api.find_profile(name=name) if obj is not None: - return self._fix_none(utils.blender(self.api, True, obj)) - return self._fix_none({}) + return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) + return self.xmlrpc_hacks({}) def get_system_as_rendered(self,name,token=None,**rest): """ @@ -577,8 +837,8 @@ class CobblerXMLRPCInterface: self._log("get_system_as_rendered",name=name,token=token) obj = self.api.find_system(name=name) if obj is not None: - return self._fix_none(utils.blender(self.api, True, obj)) - return self._fix_none({}) + return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) + return self.xmlrpc_hacks({}) def get_repo_as_rendered(self,name,token=None,**rest): """ @@ -595,8 +855,8 @@ class CobblerXMLRPCInterface: self._log("get_repo_as_rendered",name=name,token=token) obj = self.api.find_repo(name=name) if obj is not None: - return self._fix_none(utils.blender(self.api, True, obj)) - return self._fix_none({}) + return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) + return self.xmlrpc_hacks({}) def get_image_as_rendered(self,name,token=None,**rest): """ @@ -613,8 +873,8 @@ class CobblerXMLRPCInterface: self._log("get_image_as_rendered",name=name,token=token) obj = self.api.find_image(name=name) if obj is not None: - return self._fix_none(utils.blender(self.api, True, obj)) - return self._fix_none({}) + return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) + return self.xmlrpc_hacks({}) def get_random_mac(self,token=None,**rest): """ @@ -625,21 +885,29 @@ class CobblerXMLRPCInterface: self._log("get_random_mac",token=None) return utils.get_random_mac(self.api) - def _fix_none(self,data): + def xmlrpc_hacks(self,data): """ - Convert None in XMLRPC to just '~'. The above - XMLRPC module hack should do this, but let's make extra sure. + Convert None in XMLRPC to just '~' to make extra sure a client + that can't allow_none can deal with this. ALSO: a weird hack ensuring + that when dicts with integer keys (or other types) are transmitted + with string keys. """ if data is None: data = '~' elif type(data) == list: - data = [ self._fix_none(x) for x in data ] + data = [ self.xmlrpc_hacks(x) for x in data ] elif type(data) == dict: + data2 = {} for key in data.keys(): - data[key] = self._fix_none(data[key]) + keydata = data[key] + data2[str(key)] = self.xmlrpc_hacks(data[key]) + return data2 + + else: + data = '~' return data @@ -649,50 +917,11 @@ class CobblerXMLRPCInterface: """ return self.api.status() -# ********************************************************************************* -# ********************************************************************************* - -class CobblerXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): - def __init__(self, args): - self.allow_reuse_address = True - SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self,args) - -# ********************************************************************************* -# ********************************************************************************* - + ###### + # READ WRITE METHODS BELOW REQUIRE A TOKEN, use login() + # TO OBTAIN ONE + ###### -class ProxiedXMLRPCInterface: - - def __init__(self,api,logger,proxy_class,enable_auth_if_relevant=True): - self.logger = logger - self.proxied = proxy_class(api,logger,enable_auth_if_relevant) - - def _dispatch(self, method, params, **rest): - - if not hasattr(self.proxied, method): - self.logger.error("remote:unknown method %s" % method) - raise CX(_("Unknown remote method")) - - method_handle = getattr(self.proxied, method) - - try: - return method_handle(*params) - except Exception, e: - utils.log_exc(self.logger) - raise e - -# ********************************************************************** - -class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): - - def __init__(self,api,logger,enable_auth_if_relevant): - self.api = api - self.auth_enabled = enable_auth_if_relevant - self.logger = logger - self.token_cache = TOKEN_CACHE - self.object_cache = OBJECT_CACHE - self.timestamp = self.api.last_modified_time() - random.seed(time.time()) def __next_id(self,retry=0): """ @@ -701,7 +930,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): """ if retry > 10: # I have no idea why this would happen but I want to be through :) - raise CX(_("internal error")) + raise CX(_("internal error, retry exceeded")) next_id = self.__get_random(25) if self.object_cache.has_key(next_id): return self.__next_id(retry=retry+1) @@ -907,6 +1136,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): do reposync this way. Would be nice to send output over AJAX/other later. """ + # FIXME: performance self._log("sync",token=token) self.check_access(token,"sync") return self.api.sync() @@ -1136,7 +1366,6 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): object. """ self._log("rename_distro",object_id=object_id,token=token) - self.api.deserialize() # FIXME: make this unneeded obj = self.__get_object(object_id) return self.api.rename_distro(obj,newname) @@ -1161,7 +1390,6 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): def rename_image(self,object_id,newname,token=None): self._log("rename_image",object_id=object_id,token=token) self.check_access(token,"rename_image") - self.api.deserialize() # FIXME: make this unneeded obj = self.__get_object(object_id) return self.api.rename_image(obj,newname) @@ -1335,18 +1563,41 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): return rc -# ********************************************************************* -# ********************************************************************* -class CobblerReadWriteXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): - """ - This is just a wrapper used for launching the Read/Write XMLRPC Server. - """ + +# ********************************************************************************* +# ********************************************************************************* + +class CobblerXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): def __init__(self, args): self.allow_reuse_address = True SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self,args) +# ********************************************************************************* +# ********************************************************************************* + + +class ProxiedXMLRPCInterface: + + def __init__(self,api,proxy_class,enable_auth_if_relevant=True): + self.proxied = proxy_class(api,enable_auth_if_relevant) + self.logger = self.proxied.api.logger + + def _dispatch(self, method, params, **rest): + + if not hasattr(self.proxied, method): + self.logger.error("remote:unknown method %s" % method) + raise CX(_("Unknown remote method")) + + method_handle = getattr(self.proxied, method) + + try: + return method_handle(*params) + except Exception, e: + utils.log_exc(self.logger) + raise e + # ********************************************************************* # ********************************************************************* @@ -1360,7 +1611,9 @@ def _test_setup_modules(authn="authn_testing",authz="authz_allowall",pxe_once=1) MODULES_TEMPLATE = "installer_templates/modules.conf.template" DEFAULTS = "installer_templates/defaults" - data = yaml.loadFile(DEFAULTS).next() + fh = open(DEFAULTS) + data = yaml.load(fh.read()) + fh.close() data["authn_module"] = authn data["authz_module"] = authz data["pxe_once"] = pxe_once @@ -1379,7 +1632,9 @@ def _test_setup_settings(pxe_once=1): MODULES_TEMPLATE = "installer_templates/settings.template" DEFAULTS = "installer_templates/defaults" - data = yaml.loadFile(DEFAULTS).next() + fh = open(DEFAULTS) + data = yaml.load(fh.read()) + fh.close() data["pxe_once"] = pxe_once t = Template.Template(file=MODULES_TEMPLATE, searchList=[data]) @@ -1393,13 +1648,13 @@ def _test_bootstrap_restart(): assert rc1 == 0 rc2 = subprocess.call(["/sbin/service","httpd","restart"],shell=False,close_fds=True) assert rc2 == 0 - time.sleep(2) + time.sleep(5) _test_remove_objects() def _test_remove_objects(): - api = cobbler_api.BootAPI() + api = cobbler_api.BootAPI() # local handle # from ro tests d0 = api.find_distro("distro0") @@ -1444,8 +1699,7 @@ def test_xmlrpc_ro(): # now populate with something more useful # using the non-remote API - api = cobbler_api.BootAPI() - api.deserialize() # FIXME: redundant + api = cobbler_api.BootAPI() # local handle before_distros = len(api.distros()) before_profiles = len(api.profiles()) @@ -1453,10 +1707,14 @@ def test_xmlrpc_ro(): before_repos = len(api.repos()) before_images = len(api.images()) + fake = open("/tmp/cobbler.fake","w+") + fake.write("") + fake.close() + distro = api.new_distro() distro.set_name("distro0") - distro.set_kernel("/etc/hosts") - distro.set_initrd("/etc/hosts") + distro.set_kernel("/tmp/cobbler.fake") + distro.set_initrd("/tmp/cobbler.fake") api.add_distro(distro) repo = api.new_repo() @@ -1488,7 +1746,7 @@ def test_xmlrpc_ro(): image = api.new_image() image.set_name("image0") - image.set_file("/etc/hosts") + image.set_file("/tmp/cobbler.fake") api.add_image(image) # reposync is required in order to create the repo config files @@ -1676,8 +1934,8 @@ def test_xmlrpc_rw(): _test_setup_modules(authn="authn_testing",authz="authz_allowall") _test_bootstrap_restart() - server = xmlrpclib.Server("http://127.0.0.1/cobbler_api_rw") # remote - api = cobbler_api.BootAPI() # local + server = xmlrpclib.Server("http://127.0.0.1/cobbler_api") # remote + api = cobbler_api.BootAPI() # local instance, /DO/ ping cobblerd # note if authn_testing is not engaged this will not work # test getting token, will raise remote exception on fail @@ -1687,17 +1945,18 @@ def test_xmlrpc_rw(): # create distro did = server.new_distro(token) server.modify_distro(did, "name", "distro1", token) - server.modify_distro(did, "kernel", "/etc/hosts", token) - server.modify_distro(did, "initrd", "/etc/hosts", token) + server.modify_distro(did, "kernel", "/tmp/cobbler.fake", token) + server.modify_distro(did, "initrd", "/tmp/cobbler.fake", token) server.modify_distro(did, "kopts", { "dog" : "fido", "cat" : "fluffy" }, token) # hash or string server.modify_distro(did, "ksmeta", "good=sg1 evil=gould", token) # hash or string server.modify_distro(did, "breed", "redhat", token) server.modify_distro(did, "os-version", "rhel5", token) server.modify_distro(did, "owners", "sam dave", token) # array or string server.modify_distro(did, "mgmt-classes", "blip", token) # list or string - server.modify_distro(did, "template-files", "/etc/hosts=/tmp/a /etc/fstab=/tmp/b",token) # hash or string + server.modify_distro(did, "template-files", "/tmp/cobbler.fake=/tmp/a /etc/fstab=/tmp/b",token) # hash or string server.modify_distro(did, "comment", "...", token) server.modify_distro(did, "redhat_management_key", "ALPHA", token) + server.modify_distro(did, "redhat_management_server", "rhn.example.com", token) server.save_distro(did, token) # use the non-XMLRPC API to check that it's added seeing we tested XMLRPC RW APIs above @@ -1723,7 +1982,9 @@ def test_xmlrpc_rw(): server.modify_profile(pid, "mgmt-classes", "one two three", token) server.modify_profile(pid, "comment", "...", token) server.modify_profile(pid, "name_servers", ["one","two"], token) + server.modify_profile(pid, "name_servers_search", ["one","two"], token) server.modify_profile(pid, "redhat_management_key", "BETA", token) + server.modify_distro(did, "redhat_management_server", "sat.example.com", token) server.save_profile(pid, token) api.deserialize() @@ -1741,6 +2002,7 @@ def test_xmlrpc_rw(): server.modify_system(sid, 'virt-path', "/opt/images", token) server.modify_system(sid, 'virt-type', 'qemu', token) server.modify_system(sid, 'name_servers', 'one two three four', token) + server.modify_system(sid, 'name_servers_search', 'one two three four', token) server.modify_system(sid, 'modify-interface', { "macaddress-eth0" : "AA:BB:CC:EE:EE:EE", "ipaddress-eth0" : "192.168.10.50", @@ -1764,6 +2026,7 @@ def test_xmlrpc_rw(): server.modify_system(sid, "power_pass", "magic", token) server.modify_system(sid, "power_id", "7", token) server.modify_system(sid, "redhat_management_key", "GAMMA", token) + server.modify_distro(did, "redhat_management_server", "spacewalk.example.com", token) server.save_system(sid,token) diff --git a/cobbler/services.py b/cobbler/services.py index 98a7a8dd..9b9f10e5 100644 --- a/cobbler/services.py +++ b/cobbler/services.py @@ -29,7 +29,7 @@ import string import sys import time import urlgrabber -import yaml # cobbler packaged version +import yaml # PyYAML # the following imports are largely for the test code import urlgrabber @@ -68,7 +68,6 @@ class CobblerSvc(object): """ if self.remote is None: self.remote = xmlrpclib.Server(self.server, allow_none=True) - self.remote.update() def index(self,**args): return "no mode specified" @@ -241,12 +240,15 @@ def __test_setup(): # module later. api = cobbler_api.BootAPI() - api.deserialize() # FIXME: redundant + + fake = open("/tmp/cobbler.fake","w+") + fake.write("") + fake.close() distro = api.new_distro() distro.set_name("distro0") - distro.set_kernel("/etc/hosts") - distro.set_initrd("/etc/hosts") + distro.set_kernel("/tmp/cobbler.fake") + distro.set_initrd("/tmp/cobbler.fake") api.add_distro(distro) repo = api.new_repo() @@ -291,7 +293,7 @@ def __test_setup(): image = api.new_image() image.set_name("image0") - image.set_file("/etc/hosts") + image.set_file("/tmp/cobbler.fake") api.add_image(image) # perhaps an artifact of the test process? @@ -374,12 +376,9 @@ def test_services_access(): url = "http://127.0.0.1/cblr/svc/op/nopxe/system/system0" data = urlgrabber.urlread(url) - print "NOPXE DATA: %s" % data - time.sleep(10) + time.sleep(2) - api.deserialize() # ensure we have the latest data in the API handle sys = api.find_system("system0") - print "NE STATUS: %s" % sys.netboot_enabled assert str(sys.netboot_enabled).lower() not in [ "1", "true", "yes" ] # now let's test the listing URLs since we document @@ -415,13 +414,12 @@ def test_services_access(): url = "http://127.0.0.1/cblr/svc/op/puppet/hostname/hostname0" data = urlgrabber.urlread(url) - print "puppet DATA: %s" % data assert data.find("alpha") != -1 assert data.find("beta") != -1 assert data.find("gamma") != -1 assert data.find("3") != -1 - data = yaml.load(data).next() + data = yaml.load(data) assert data.has_key("classes") assert data.has_key("parameters") @@ -431,13 +429,11 @@ def test_services_access(): url = "http://127.0.0.1/cblr/svc/op/template/profile/profile0/path/_tmp_t1-rendered" data = urlgrabber.urlread(url) - print "T1: %s" % data assert data.find("profile0") != -1 assert data.find("$profile_name") == -1 url = "http://127.0.0.1/cblr/svc/op/template/system/system0/path/_tmp_t2-rendered" data = urlgrabber.urlread(url) - print "T2: %s" % data assert data.find("system0") != -1 assert data.find("$system_name") == -1 diff --git a/cobbler/settings.py b/cobbler/settings.py index c415150b..2f2fddb3 100644 --- a/cobbler/settings.py +++ b/cobbler/settings.py @@ -30,14 +30,21 @@ TESTMODE = False # we need. DEFAULTS = { + "anamon_enabled" : 0, "allow_duplicate_hostnames" : 0, "allow_duplicate_macs" : 0, "allow_duplicate_ips" : 0, "bind_bin" : "/usr/sbin/named", + "build_reporting_enabled" : 0, + "build_reporting_to_address" : "", + "build_reporting_sender" : "", + "build_reporting_subject" : "", + "build_reporting_smtp_server" : "localhost", "cheetah_import_whitelist" : [ "re", "random", "time" ], "cobbler_master" : '', "default_kickstart" : "/var/lib/cobbler/kickstarts/default.ks", "default_name_servers" : '', + "default_name_servers_search" : '', "default_password_crypted" : "\$1\$mF86/UHC\$WvcIcX2t6crBz2onWxyac.", "default_virt_bridge" : "xenbr0", "default_virt_type" : "auto", @@ -68,6 +75,7 @@ DEFAULTS = { "text" : None, "ksdevice" : "eth0" }, + "kernel_options_s390x" : {}, "manage_dhcp" : 0, "manage_dns" : 0, "manage_xinetd" : 0, @@ -84,6 +92,7 @@ DEFAULTS = { "power_template_dir" : "/etc/cobbler/power", "pxe_just_once" : 0, "pxe_template_dir" : "/etc/cobbler/pxe", + "redhat_management_permissive" : 0, "redhat_management_type" : "off", "redhat_management_key" : "", "redhat_management_server" : "xmlrpc.rhn.redhat.com", @@ -94,15 +103,12 @@ DEFAULTS = { "run_install_triggers" : 1, "server" : "127.0.0.1", "snippetsdir" : "/var/lib/cobbler/snippets", - "syslog_port" : 25150, "tftpd_bin" : "/usr/sbin/in.tftpd", "tftpd_conf" : "/etc/xinetd.d/tftp", "tftpd_rules" : "/etc/tftpd.rules", "vsftpd_bin" : "/usr/sbin/vsftpd", "webdir" : "/var/www/cobbler", "xmlrpc_port" : 25151, - "xmlrpc_rw_enabled" : 1, - "xmlrpc_rw_port" : 25152, "yum_post_install_mirror" : 1, "yumdownloader_flags" : "--resolve", "yumreposync_flags" : "-l" @@ -145,8 +151,9 @@ class Settings(serializable.Serializable): if datastruct is None: print _("warning: not loading empty structure for %s") % self.filename() return - - self._attributes = datastruct + + self._attributes = DEFAULTS + self._attributes.update(datastruct) return self diff --git a/cobbler/templar.py b/cobbler/templar.py index 8a261084..f2e4b7f1 100644 --- a/cobbler/templar.py +++ b/cobbler/templar.py @@ -32,13 +32,14 @@ import utils class Templar: - def __init__(self,config): + def __init__(self,config=None): """ Constructor """ - self.config = config - self.api = config.api - self.settings = config.settings() + if config is not None: + self.config = config + self.api = config.api + self.settings = config.settings() def check_for_invalid_imports(self,data): """ @@ -130,11 +131,12 @@ class Templar: search_table["http_server"] = repstr for x in search_table.keys(): - data_out = data_out.replace("@@%s@@" % str(x), str(search_table[str(x)])) + if type(x) == str: + data_out = data_out.replace("@@%s@@" % str(x), str(search_table[str(x)])) # remove leading newlines which apparently breaks AutoYAST ? if data_out.startswith("\n"): - data_out = data_out.strip() + data_out = data_out.lstrip() if out_path is not None: utils.mkdir(os.path.dirname(out_path)) diff --git a/cobbler/test_basic.py b/cobbler/test_basic.py index 2a839c5a..12bdc26a 100644 --- a/cobbler/test_basic.py +++ b/cobbler/test_basic.py @@ -109,7 +109,7 @@ class BootTest(unittest.TestCase): image = self.api.new_image() self.assertTrue(image.set_name("testimage0")) - self.assertTrue(image.set_file("/etc/hosts")) # meaningless path + self.assertTrue(image.set_file(self.fk_initrd)) # meaningless path self.assertTrue(self.api.add_image(image)) @@ -134,13 +134,9 @@ class RenameTest(BootTest): def test_renames(self): self.__tester(self.api.find_distro, self.api.rename_distro, "testdistro0", "testdistro1") - self.api.update() # unneccessary? self.__tester(self.api.find_profile, self.api.rename_profile, "testprofile0", "testprofile1") - self.api.update() # unneccessary? self.__tester(self.api.find_system, self.api.rename_system, "testsystem0", "testsystem1") - self.api.update() # unneccessary? self.__tester(self.api.find_repo, self.api.rename_repo, "testrepo0", "testrepo1") - self.api.update() # unneccessary? self.__tester(self.api.find_image, self.api.rename_image, "testimage0", "testimage1") @@ -763,20 +759,17 @@ class SyncContents(BootTest): def test_blender_cache_works(self): - # this is just a file that exists that we don't have to create - fake_file = "/etc/hosts" - distro = self.api.new_distro() self.assertTrue(distro.set_name("D1")) - self.assertTrue(distro.set_kernel(fake_file)) - self.assertTrue(distro.set_initrd(fake_file)) + self.assertTrue(distro.set_kernel(self.fk_kernel)) + self.assertTrue(distro.set_initrd(self.fk_initrd)) self.assertTrue(self.api.add_distro(distro)) self.assertTrue(self.api.find_distro(name="D1")) profile = self.api.new_profile() self.assertTrue(profile.set_name("P1")) self.assertTrue(profile.set_distro("D1")) - self.assertTrue(profile.set_kickstart(fake_file)) + self.assertTrue(profile.set_kickstart("/var/lib/cobbler/kickstarts/sample.ks")) self.assertTrue(self.api.add_profile(profile)) assert self.api.find_profile(name="P1") != None @@ -818,17 +811,17 @@ class SyncContents(BootTest): class Deletions(BootTest): - def test_invalid_delete_profile_doesnt_exist(self): - self.failUnlessRaises(CobblerException, self.api.profiles().remove, "doesnotexist") + #def test_invalid_delete_profile_doesnt_exist(self): + # self.failUnlessRaises(CobblerException, self.api.profiles().remove, "doesnotexist") def test_invalid_delete_profile_would_orphan_systems(self): self.failUnlessRaises(CobblerException, self.api.profiles().remove, "testprofile0") - def test_invalid_delete_system_doesnt_exist(self): - self.failUnlessRaises(CobblerException, self.api.systems().remove, "doesnotexist") + #def test_invalid_delete_system_doesnt_exist(self): + # self.failUnlessRaises(CobblerException, self.api.systems().remove, "doesnotexist") - def test_invalid_delete_distro_doesnt_exist(self): - self.failUnlessRaises(CobblerException, self.api.distros().remove, "doesnotexist") + #def test_invalid_delete_distro_doesnt_exist(self): + # self.failUnlessRaises(CobblerException, self.api.distros().remove, "doesnotexist") def test_invalid_delete_distro_would_orphan_profile(self): self.failUnlessRaises(CobblerException, self.api.distros().remove, "testdistro0") @@ -870,6 +863,22 @@ class TestListings(BootTest): self.assertTrue(len(self.api.profiles().printable()) > 0) self.assertTrue(len(self.api.distros().printable()) > 0) +class TestImage(BootTest): + + def test_image_file(self): + # ensure that only valid names are accepted and invalid ones are rejected + image = self.api.new_image() + self.assertTrue(image.set_file("nfs://hostname/path/to/filename.iso")) + self.assertTrue(image.set_file("nfs://mcpierce@hostname:/path/to/filename.iso")) + self.assertTrue(image.set_file("nfs://hostname:/path/to/filename.iso")) + self.assertTrue(image.set_file("nfs://hostname/filename.iso")) + self.assertTrue(image.set_file("hostname:/path/to/the/filename.iso")) + self.failUnlessRaises(CX, image.set_file, "hostname:filename.iso") + self.failUnlessRaises(CX, image.set_file, "path/to/filename.iso") + self.failUnlessRaises(CX, image.set_file, "hostname:") + # port is not allowed + self.failUnlessRaises(CX, image.set_file, "nfs://hostname:1234/path/to/the/filename.iso") + #class TestCLIBasic(BootTest): # # def test_cli(self): diff --git a/cobbler/utils.py b/cobbler/utils.py index 23794a83..18535638 100644 --- a/cobbler/utils.py +++ b/cobbler/utils.py @@ -37,6 +37,20 @@ import tempfile import signal from cexceptions import * import codes +import time +import netaddr +import shlex + +try: + import hashlib as fiver + def md5(key): + return fiver.md5(key) +except ImportError: + # for Python < 2.5 + import md5 as fiver + def md5(key): + return fiver.md5(key) + CHEETAH_ERROR_DISCLAIMER=""" # *** ERROR *** @@ -62,12 +76,12 @@ def _(foo): MODULE_CACHE = {} -# import api # factor out - _re_kernel = re.compile(r'vmlinuz(.*)') _re_initrd = re.compile(r'initrd(.*).img') -def setup_logger(name, log_level=logging.INFO, log_file="/var/log/cobbler/cobbler.log"): +def setup_logger(name, is_cobblerd=False, log_level=logging.INFO, log_file="/var/log/cobbler/cobbler.log"): + if is_cobblerd: + log_file = "/var/log/cobbler/cobblerd.log" logger = logging.getLogger(name) logger.setLevel(log_level) try: @@ -90,18 +104,6 @@ def log_exc(logger): logger.info("Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tb)))) -def print_exc(exc,full=False): - (t, v, tb) = sys.exc_info() - try: - getattr(exc, "from_cobbler") - print >> sys.stderr, str(exc)[1:-1] - except: - print >> sys.stderr, t - print >> sys.stderr, v - if full: - print >> sys.stderr, string.join(traceback.format_list(traceback.extract_tb(tb))) - return 1 - def get_exc(exc,full=True): (t, v, tb) = sys.exc_info() buf = "" @@ -133,39 +135,33 @@ def trace_me(): bar = string.join(traceback.format_list(x)) return bar +def pretty_hex(ip, length=8): + """ + Pads an IP object with leading zeroes so that the result is + _length_ hex digits. Also do an upper(). + """ + hexval = "%x" % ip.value + if len(hexval) < length: + hexval = '0' * (length - len(hexval)) + hexval + return hexval.upper() def get_host_ip(ip, shorten=True): """ Return the IP encoding needed for the TFTP boot tree. """ + ip = netaddr.IP(ip) + cidr = ip.cidr() - slash = None - if ip.find("/") != -1: - # CIDR notation - (ip, slash) = ip.split("/") - - handle = sub_process.Popen("/usr/bin/gethostip %s" % ip, shell=True, stdout=sub_process.PIPE, close_fds=True) - out = handle.stdout - results = out.read() - converted = results.split(" ")[-1][0:8] - - if slash is None: - return converted + if len(cidr) == 1: # Just an IP, e.g. a /32 + return pretty_hex(ip) else: - slash = int(slash) - num = int(converted, 16) - delta = 32 - slash - mask = (0xFFFFFFFF << delta) - num = num & mask - num = "%0x" % num - if len(num) != 8: - num = '0' * (8 - len(num)) + num - num = num.upper() - if shorten: - nibbles = delta / 4 - for x in range(0,nibbles): - num = num[0:-1] - return num + pretty = pretty_hex(cidr[0]) + if not shorten or len(cidr) <= 8: + # not enough to make the last nibble insignificant + return pretty + else: + cutoff = (32 - cidr.prefixlen) / 4 + return pretty[0:-cutoff] def get_config_filename(sys,interface): """ @@ -393,28 +389,32 @@ def input_string_or_hash(options,delim=",",allow_multiples=True): raise CX(_("No idea what to do with list: %s") % options) elif type(options) == str: new_dict = {} - tokens = options.split(delim) + tokens = shlex.split(options) for t in tokens: - tokens2 = t.split("=") - if len(tokens2) == 1 and tokens2[0] != '': + tokens2 = t.split("=",1) + if len(tokens2) == 1: # this is a singleton option, no value - tokens2.append(None) - elif tokens2[0] == '': - return (False, {}) + key = tokens2[0] + value = None + else: + key = tokens2[0] + value = tokens2[1] # if we're allowing multiple values for the same key, # check to see if this token has already been # inserted into the dictionary of values already - if tokens2[0] in new_dict.keys() and allow_multiples: + + if key in new_dict.keys() and allow_multiples: # if so, check to see if there is already a list of values # otherwise convert the dictionary value to an array, and add # the new value to the end of the list - if type(new_dict[tokens2[0]]) == list: - new_dict[tokens2[0]].append(tokens2[1]) + if type(new_dict[key]) == list: + new_dict[key].append(value) else: - new_dict[tokens2[0]] = [new_dict[tokens2[0]], tokens2[1]] + new_dict[key] = [new_dict[key], value] else: - new_dict[tokens2[0]] = tokens2[1] + new_dict[key] = value + # make sure we have no empty entries new_dict.pop('', None) return (True, new_dict) elif type(options) == dict: @@ -457,12 +457,13 @@ def blender(api_handle,remove_hashes, root_obj): for node in tree: __consolidate(node,results) - # add in syslog to results (magic) - if settings.syslog_port != 0: - if not results.has_key("kernel_options"): - results["kernel_options"] = {} - syslog = "%s:%s" % (results["server"], settings.syslog_port) - results["kernel_options"]["syslog"] = syslog + # hack -- s390 nodes get additional default kernel options + arch = results.get("arch","?") + if arch.startswith("s390"): + keyz = settings.kernel_options_s390x.keys() + for k in keyz: + if not results.has_key(k): + results["kernel_options"][k] = settings.kernel_options_s390x[k] # determine if we have room to add kssendmac to the kernel options line kernel_txt = hash_to_string(results["kernel_options"]) @@ -633,7 +634,7 @@ def hash_removals(results,subkey): return scan = results[subkey].keys() for k in scan: - if k.startswith("!") and k != "!": + if str(k).startswith("!") and k != "!": remove_me = k[1:] if results[subkey].has_key(remove_me): del results[subkey][remove_me] @@ -662,15 +663,33 @@ def hash_to_string(hash): buffer = buffer + str(key) + "=" + str(value) + " " return buffer -def run_triggers(ref,globber,additional=[]): +def run_triggers(api,ref,globber,additional=[]): """ Runs all the trigger scripts in a given directory. ref can be a cobbler object, if not None, the name will be passed to the script. If ref is None, the script will be called with no argumenets. Globber is a wildcard expression indicating which triggers to run. Example: "/var/lib/cobbler/triggers/blah/*" + + As of Cobbler 1.5.X, this also runs cobbler modules that match the globbing paths. """ + # Python triggers first, before shell + + modules = api.get_modules_in_category(globber) + for m in modules: + arglist = [] + if ref: + arglist.append(ref.name) + for x in additional: + arglist.append(x) + rc = m.run(api, arglist) + if rc != 0: + raise CX("cobbler trigger failed: %s" % m.__name__) + + # now do the old shell triggers, which are usually going to be slower, but are easier to write + # and support any language + triggers = glob.glob(globber) triggers.sort() for file in triggers: @@ -730,7 +749,7 @@ def os_release(): if not os.path.exists("/bin/rpm"): return ("unknown", 0) - args = ["/bin/rpm", "-q", "--whatprovides", "redhat-release"] + args = ["/bin/rpm", "-q", "--whatprovides", "redhat-release", "--queryformat", "%"+"{name}"+"-%" + "{version}" + "-%" + "{release}"] cmd = sub_process.Popen(args,shell=False,stdout=sub_process.PIPE,close_fds=True) data = cmd.communicate()[0] data = data.rstrip().lower() @@ -821,7 +840,7 @@ def is_safe_to_hardlink(src,dst,api): # we're dealing with SELinux and files that are not safe to chcon return False -def linkfile(src, dst, symlink_ok=False, api=None): +def linkfile(src, dst, symlink_ok=False, api=None, verbose=False): """ Attempt to create a link dst that points to src. Because file systems suck we attempt several different methods or bail to @@ -842,25 +861,30 @@ def linkfile(src, dst, symlink_ok=False, api=None): if not is_safe_to_hardlink(src,dst,api): # may have to remove old hardlinks for SELinux reasons # as previous implementations were not complete - os.remove(dst) + if verbose: + print "- removing: %s" % dst + os.remove(dst) else: - restorecon(dst,api=api) + # restorecon(dst,api=api,verbose=verbose) return True elif os.path.islink(dst): # existing path exists and is a symlink, update the symlink + if verbose: + print "- removing: %s" % dst os.remove(dst) if is_safe_to_hardlink(src,dst,api): # we can try a hardlink if the destination isn't to NFS or Samba # this will help save space and sync time. try: + if verbose: + print "- trying hardlink %s -> %s" % (src,dst) rc = os.link(src, dst) - restorecon(dst,api=api) + # restorecon(dst,api=api,verbose=verbose) return rc except (IOError, OSError): # hardlink across devices, or link already exists - # can result in extra call to restorecon but no - # major harm, we'll just symlink it if we can + # we'll just symlink it if we can # or otherwise copy it pass @@ -868,20 +892,24 @@ def linkfile(src, dst, symlink_ok=False, api=None): # we can symlink anywhere except for /tftpboot because # that is run chroot, so if we can symlink now, try it. try: + if verbose: + print "- trying symlink %s -> %s" % (src,dst) rc = os.symlink(src, dst) - restorecon(dst,api=api) + # restorecon(dst,api=api,verbose=verbose) return rc except (IOError, OSError): pass # we couldn't hardlink and we couldn't symlink so we must copy - return copyfile(src, dst, api=api) + return copyfile(src, dst, api=api, verbose=verbose) -def copyfile(src,dst,api=None): +def copyfile(src,dst,api=None,verbose=False): try: + if verbose: + print "- copying: %s -> %s" % (src,dst) rc = shutil.copyfile(src,dst) - restorecon(dst,api) + # restorecon(dst,api,verbose=verbose) return rc except: if not os.access(src,os.R_OK): @@ -982,58 +1010,44 @@ def umount(src): # raise CX(_("Error bind-mounting %(src)s to %(dst)s") % { "src" : src, "dst" : dst}) -def copyfile_pattern(pattern,dst,require_match=True,symlink_ok=False,api=None): +def copyfile_pattern(pattern,dst,require_match=True,symlink_ok=False,api=None, verbose=False): files = glob.glob(pattern) if require_match and not len(files) > 0: raise CX(_("Could not find files matching %s") % pattern) for file in files: base = os.path.basename(file) dst1 = os.path.join(dst,os.path.basename(file)) - linkfile(file,dst1,symlink_ok=symlink_ok,api=api) - restorecon(dst1,api=api) - -def restorecon(dest, api): + linkfile(file,dst1,symlink_ok=symlink_ok,api=api,verbose=verbose) + # restorecon(dst1,api=api,verbose=verbose) - """ - Wrapper around functions to manage SELinux contexts. - Use chcon public_content_t where we can to allow - hardlinking between /var/www and tftpboot but use - restorecon everywhere else. - """ - - if not api.is_selinux_enabled(): - return True - - tdest = os.path.realpath(dest) - - matched_path = False - if dest.startswith("/var/www"): - matched_path = True - elif dest.find("/tftpboot/"): - matched_path = True - remoted = is_remote_file(tdest) - - - if matched_path and not is_remote_file(tdest): - # ensure the file is flagged as public_content_t - # because it's something we've likely hardlinked - # three ways between tftpboot, /var/www and the source - cmd = ["/usr/bin/chcon","-t","public_content_t", tdest] - rc = sub_process.call(cmd,shell=False,close_fds=True) - if rc != 0: - raise CX("chcon operation failed: %s" % cmd) - - if (not matched_path) or (matched_path and remoted): - # the basic restorecon stuff... - cmd = [ "/sbin/restorecon",dest ] - rc = sub_process.call(cmd,shell=False,close_fds=True) - if rc != 0: - raise CX("restorecon operation failed: %s" % cmd) - - return 0 +#def restorecon(dest, api, verbose=False): +# +# """ +# Wrapper around functions to manage SELinux contexts. +# Use chcon public_content_t where we can to allow +# hardlinking between /var/www and tftpboot but use +# restorecon everywhere else. +# """ +# +# if not api.is_selinux_enabled(): +# return True +# +# tdest = os.path.realpath(dest) +# # remoted = is_remote_file(tdest) +# +# cmd = [ "/sbin/restorecon",dest ] +# if verbose: +# print "- %s" % " ".join(cmd) +# rc = sub_process.call(cmd,shell=False,close_fds=True) +# if rc != 0: +# raise CX("restorecon operation failed: %s" % cmd) +# +# return 0 -def rmfile(path): +def rmfile(path,verbose=False): try: + if verbose: + print "- removing: %s" % path os.unlink(path) return True except OSError, ioe: @@ -1042,16 +1056,18 @@ def rmfile(path): raise CX(_("Error deleting %s") % path) return True -def rmtree_contents(path): +def rmtree_contents(path,verbose=False): what_to_delete = glob.glob("%s/*" % path) for x in what_to_delete: - rmtree(x) + rmtree(x,verbose=verbose) -def rmtree(path): +def rmtree(path,verbose=False): try: if os.path.isfile(path): - return rmfile(path) + return rmfile(path,verbose=verbose) else: + if verbose: + print "- removing: %s" % path return shutil.rmtree(path,ignore_errors=True) except OSError, ioe: traceback.print_exc() @@ -1059,8 +1075,10 @@ def rmtree(path): raise CX(_("Error deleting %s") % path) return True -def mkdir(path,mode=0777): +def mkdir(path,mode=0777,verbose=False): try: + if verbose: + "- mkdir: %s" % path return os.makedirs(path,mode) except OSError, oe: if not oe.errno == 17: # already exists (no constant for 17?) @@ -1072,16 +1090,24 @@ def set_redhat_management_key(self,key): self.redhat_management_key = key return True -def set_arch(self,arch): - if arch is None or arch == "": - arch = "x86" - if arch in [ "standard", "ia64", "x86", "i386", "ppc", "ppc64", "x86_64", "s390x" ]: - if arch == "x86" or arch == "standard": - # be consistent - arch = "i386" +def set_redhat_management_server(self,server): + self.redhat_management_server = server + return True + +def set_arch(self,arch,repo=False): + if arch is None or arch == "" or arch == "standard" or arch == "x86": + arch = "i386" + + if repo: + valids = [ "i386", "x86_64", "ia64", "ppc", "ppc64", "s390", "s390x", "noarch", "src" ] + else: + valids = [ "i386", "x86_64", "ia64", "ppc", "ppc64", "s390", "s390x" ] + + if arch in valids: self.arch = arch return True - raise CX(_("arch choices include: x86, x86_64, ppc, ppc64, s390x and ia64")) + + raise CX("arch choices include: %s" % ", ".join(valids)) def set_os_version(self,os_version): if os_version == "" or os_version is None: @@ -1238,8 +1264,8 @@ def set_virt_bridge(self,vbridge): """ The default bridge for all virtual interfaces under this profile. """ - if vbridge is None: - vbridge = "" + if vbridge is None or vbridge == "": + vbridge = self.settings.default_virt_bridge self.virt_bridge = vbridge return True @@ -1304,6 +1330,8 @@ def safe_filter(var): raise CX("Invalid characters found in input") def is_selinux_enabled(): + if not os.path.exists("/usr/sbin/selinuxenabled"): + return False args = "/usr/sbin/selinuxenabled" selinuxenabled = sub_process.call(args,close_fds=True) if selinuxenabled == 0: @@ -1411,7 +1439,7 @@ def popen2(args, **kwargs): Leftovers from borrowing some bits from Snake, replace this function with just the subprocess call. """ - p = sub_process.Popen(args, stdout=subprocess.PIPE, stdin=subprocess.PIPE, **kwargs) + p = sub_process.Popen(args, stdout=sub_process.PIPE, stdin=sub_process.PIPE, **kwargs) return (p.stdout, p.stdin) if __name__ == "__main__": diff --git a/cobbler/webui/CobblerWeb.py b/cobbler/webui/CobblerWeb.py index ccdfe9c0..6175ef50 100644 --- a/cobbler/webui/CobblerWeb.py +++ b/cobbler/webui/CobblerWeb.py @@ -128,7 +128,7 @@ class CobblerWeb(object): }) def menu(self,**args): - return self.__render( 'blank.tmpl', { } ) + return self.__render( 'blank.tmpl', {} ) # ------------------------------------------------------------------------ # # Settings @@ -151,6 +151,85 @@ class CobblerWeb(object): # Distributions # ------------------------------------------------------------------------ # + def distro_menu(self,**spam): + return self.__render('blank.tmpl',{ 'more_blank' : 1}) + + def __search_execute(self,what,key1=None,value1=None,key2=None,value2=None,key3=None,value3=None): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + criteria={} + if key1 is not None and key1 != "": + criteria[key1] = value1.replace('"','') + if key2 is not None and key2 != "": + criteria[key2] = value2.replace('"','') + if key3 is not None and key3 != "": + criteria[key3] = value3.replace('"','') + + params = {} + params['page'] = -1 + + results = [] + if what == "distro": + results = params['distros'] = self.remote.find_distro(criteria,True) + elif what == "profile": + results = params['profiles'] = self.remote.find_profile(criteria,True) + elif what == "system": + results = params['systems'] = self.remote.find_system(criteria,True) + elif what == "image": + results = params['images'] = self.remote.find_image(criteria,True) + elif what == "repo": + results = params['repos'] = self.remote.find_repo(criteria,True) + else: + raise "internal error, unknown search type" + + + if len(results) > 0: + return self.__render( "%s_list.tmpl" % what, params) + else: + return self.__render('empty.tmpl', { 'search' : 1 }) + + def distro_search_execute(self,key1=None,value1=None,key2=None,value2=None,key3=None,value3=None,**rest): + return self.__search_execute("distro",key1,value1,key2,value2,key3,value3) + def profile_search_execute(self,key1=None,value1=None,key2=None,value2=None,key3=None,value3=None,**rest): + return self.__search_execute("profile",key1,value1,key2,value2,key3,value3) + def system_search_execute(self,key1=None,value1=None,key2=None,value2=None,key3=None,value3=None,**rest): + return self.__search_execute("system",key1,value1,key2,value2,key3,value3) + def image_search_execute(self,key1=None,value1=None,key2=None,value2=None,key3=None,value3=None,**rest): + return self.__search_execute("image",key1,value1,key2,value2,key3,value3) + def repo_search_execute(self,key1=None,value1=None,key2=None,value2=None,key3=None,value3=None,**rest): + return self.__search_execute("repo",key1,value1,key2,value2,key3,value3) + + def __search(self, what): + caption = "" + dest = "" + if what == "distro": + caption = "Search distros" + dest = "distro_search_execute" + elif what == "profile": + caption = "Search profiles" + dest = "profile_search_execute" + elif what == "system": + caption = "Search systems" + dest = "system_search_execute" + elif what == "repo": + caption = "Search repos" + dest = "repo_search_execute" + elif what == "image": + caption = "Search image" + dest = "image_search_execute" + else: + raise "internal error, unknown object type in search" + + return self.__render('search.tmpl', { + 'what' : what, + 'caption' : caption, + 'submit_dest' : dest + }) + + def distro_search(self,**spam): + return self.__search('distro') + def distro_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -197,7 +276,7 @@ class CobblerWeb(object): def distro_save(self,name=None,comment=None,oldname=None,new_or_edit=None,editmode='edit',kernel=None, initrd=None,kopts=None,koptspost=None,ksmeta=None,owners=None,arch=None,breed=None,redhatmanagementkey=None, - osversion=None,delete1=False,delete2=False,recursive=False,**args): + mgmt_classes=None,osversion=None,delete1=False,delete2=False,recursive=False,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -258,6 +337,8 @@ class CobblerWeb(object): self.remote.modify_distro(distro, 'os-version', osversion, self.token) self.remote.modify_distro(distro, 'comment', comment, self.token) self.remote.modify_distro(distro, 'redhat_management_key', redhatmanagementkey, self.token) + self.remote.modify_distro(distro, 'redhat_management_server', redhatmanagementserver, self.token) + self.remote.modify_distro(distro, 'mgmt_classes', mgmt_classes, self.token) # now time to save, do we want to run duplication checks? self.remote.save_distro(distro, self.token, editmode) @@ -302,6 +383,9 @@ class CobblerWeb(object): pages = total_size / results_per_page return (page, results_per_page, pages) + + def system_menu(self,**spam): + return self.__render('blank.tmpl',{ 'more_blank' : 1}) def system_list(self,page=None,limit=None,**spam): @@ -323,50 +407,225 @@ class CobblerWeb(object): else: return self.__render('empty.tmpl',{}) - def system_save(self,name=None,oldname=None,comment=None,editmode="edit",profile=None, - new_or_edit=None, + + def system_list_action(self,actionname=None,targetlist=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + if actionname is None: + return self.error_page("Actionname parameter is REQUIRED.") + + if actionname == 'add': + return self.system_edit_new() + + if targetlist is None: + return self.error_page("Targetlist parameter is REQUIRED.") + + # Single item actions + if actionname == 'copy': + return self.system_edit_copy(targetlist) + if actionname == 'edit': + return self.system_edit(targetlist) + + # Multiple items actions + systems=[] + for targetname in targetlist.split(): + systems.append(self.remote.get_system(targetname,self.token)) + + return self.__render( 'system_'+actionname+'.tmpl', { + 'systems' : systems, + 'profiles' : self.remote.get_profiles(), + 'targetlist' : targetlist, + } ) + + + def system_netboot(self,targetlist=None,netboot=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + if targetlist is None: + return self.error_page("Targetlist parameter is REQUIRED.") + if netboot is None: + return self.error_page("Netboot parameter is REQUIRED.") + try: + systems=[] + for targetname in targetlist.split(): + systems.append(self.remote.get_system_handle(targetname,self.token)) + for system in systems: + self.remote.modify_system(system, 'netboot-enabled', netboot, self.token) + self.remote.save_system(system, self.token) + return self.system_list() + except Exception, e: + log_exc(self.apache) + return self.error_page("Error while saving system: %s" % str(e)) + + + def system_profile(self,targetlist=None,profile=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + if targetlist is None: + return self.error_page("Targetlist parameter is REQUIRED.") + if profile is None: + return self.error_page("Profile parameter is REQUIRED.") + try: + systems=[] + for targetname in targetlist.split(): + systems.append(self.remote.get_system_handle(targetname,self.token)) + for system in systems: + self.remote.modify_system(system, 'profile', profile, self.token) + self.remote.save_system(system, self.token) + return self.system_list() + except Exception, e: + log_exc(self.apache) + return self.error_page("Error while saving system: %s" % str(e)) + + + def system_power(self,targetlist=None,power=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + if targetlist is None: + return self.error_page("Targetlist parameter is REQUIRED.") + if power is None: + return self.error_page("Power parameter is REQUIRED.") + try: + systems=[] + for targetname in targetlist.split(): + systems.append(self.remote.get_system_handle(targetname,self.token)) + for system in systems: + self.remote.power_system(system, power, self.token) + return self.system_list() + except Exception, e: + log_exc(self.apache) + return self.error_page("Error while controlling power of system: %s" % str(e)) + + + def system_rename(self,targetlist=None,name=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + if targetlist is None: + return self.error_page("Targetlist parameter is REQUIRED.") + try: + systems=[] + for targetname in targetlist.split(): + systems.append(self.remote.get_system_handle(targetname,self.token)) + for system in systems: + self.remote.rename_system(system, name, self.token) + return self.system_list() + except Exception, e: + log_exc(self.apache) + return self.error_page("Error while renaming system: %s" % str(e)) + + + def system_delete(self,targetlist=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + if targetlist is None: + return self.error_page("Targetlist parameter is REQUIRED.") + try: + for targetname in targetlist.split(): + self.remote.remove_system(targetname, self.token) + return self.system_list() + except Exception, e: + log_exc(self.apache) + return self.error_page("Error while deleting system: %s" % str(e)) + + + + def system_edit(self, name=None,**spam): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + if name is None: + return self.error_page("Name parameter is REQUIRED") + input_system = self.remote.get_system(name,True) + can_edit = self.remote.check_access_no_fail(self.token,"modify_system",name) + + return self.__render( 'system_edit.tmpl', { + 'user' : self.username, + 'editmode' : 'edit', + 'editable' : can_edit, + 'system': input_system, + 'profiles': self.remote.get_profiles() + } ) + + + def system_edit_new(self,**spam): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + can_edit = self.remote.check_access_no_fail(self.token,"new_system",None) + if not can_edit: + return self.__render('message.tmpl', { + 'message1' : "Access denied.", + 'message2' : "You do not have permission to create new objects." + }) + + return self.__render( 'system_edit.tmpl', { + 'user' : self.username, + 'editmode' : 'new', + 'editable' : True, + 'system': None, + 'profiles': self.remote.get_profiles() + } ) + + + def system_edit_copy(self, name=None,**spam): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + if name is None: + return self.error_page("Name parameter is REQUIRED") + + can_edit = self.remote.check_access_no_fail(self.token,"new_system",None) + if not can_edit: + return self.__render('message.tmpl', { + 'message1' : "Access denied.", + 'message2' : "You do not have permission to create new objects." + }) + + input_system = self.remote.get_system(name,True) + + return self.__render( 'system_edit.tmpl', { + 'user' : self.username, + 'editmode' : 'copy', + 'editable' : True, + 'system': input_system, + 'profiles': self.remote.get_profiles() + } ) + + + def system_save(self,name=None,comment=None,editmode="edit",profile=None, kopts=None, koptspost=None, ksmeta=None, owners=None, server_override=None, netboot='n', virtpath=None,virtram=None,virttype=None,virtcpus=None,virtfilesize=None, - name_servers=None, + name_servers=None,name_servers_search=None, power_type=None, power_user=None, power_pass=None, power_id=None, power_address=None, - gateway=None,hostname=None,redhatmanagementkey=None,delete1=None, delete2=None, **args): + gateway=None,hostname=None,redhatmanagementkey=None,mgmt_classes=None,delete1=None, delete2=None, **args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() # parameter checking - if name is None and oldname is not None: - name = oldname if name is None: return self.error_page("System name parameter is REQUIRED.") - if (editmode == 'rename' or editmode == 'copy') and name == oldname: - return self.error_page("The name has not been changed.") - - # handle deletes as a special case - if new_or_edit == 'edit' and delete1 and delete2: - try: - self.remote.remove_system(name,self.token) - except Exception, e: - return self.error_page("could not delete %s, %s" % (name,str(e))) - return self.system_list() # grab a reference to the object - if new_or_edit == "edit" and editmode in [ "edit", "rename" ] : + if editmode == "edit": try: - if editmode == "edit": - system = self.remote.get_system_handle( name, self.token ) - else: - system = self.remote.get_system_handle( oldname, self.token ) - + system = self.remote.get_system_handle( name, self.token ) except: return self.error_page("Failed to lookup system: %s" % name) else: + try: + system = self.remote.get_system_handle( name, self.token ) + except: + system = None + if system is not None: + return self.error_page("Failed to create new system: %s already exists." % name) system = self.remote.new_system( self.token ) # go! try: - if editmode != "rename" and name: + if editmode != "edit": self.remote.modify_system(system, 'name', name, self.token ) self.remote.modify_system(system, 'profile', profile, self.token) self.remote.modify_system(system, 'kopts', kopts, self.token) @@ -390,9 +649,12 @@ class CobblerWeb(object): self.remote.modify_system(system, 'power_id', power_id, self.token) self.remote.modify_system(system, 'power_address', power_address, self.token) self.remote.modify_system(system, 'name_servers', name_servers, self.token) + self.remote.modify_system(system, 'name_servers_search', name_servers_search, self.token) self.remote.modify_system(system, 'gateway', gateway, self.token) self.remote.modify_system(system, 'hostname', hostname, self.token) self.remote.modify_system(system, 'redhat_management_key', redhatmanagementkey, self.token) + self.remote.modify_system(system, 'redhat_management_server', redhatmanagementserver, self.token) + self.remote.modify_system(system, 'mgmt_classes', mgmt_classes, self.token) interfaces = args.get("interface_list","") interfaces = interfaces.split(",") @@ -439,46 +701,21 @@ class CobblerWeb(object): log_exc(self.apache) return self.error_page("Error while saving system: %s" % str(e)) - - - if editmode == "rename" and name != oldname: - try: - self.remote.rename_system(system, name, self.token) - except Exception, e: - return self.error_page("Rename unsuccessful") - return self.system_list() - - def system_edit(self, name=None,**spam): - - if not self.__xmlrpc_setup(): - return self.xmlrpc_auth_failure() - - input_system = None - if name is not None: - input_system = self.remote.get_system(name,True) - can_edit = self.remote.check_access_no_fail(self.token,"modify_system",name) - else: - can_edit = self.remote.check_access_no_fail(self.token,"new_system",None) - if not can_edit: - return self.__render('message.tmpl', { - 'message1' : "Access denied.", - 'message2' : "You do not have permission to create new objects." - }) - - - return self.__render( 'system_edit.tmpl', { - 'user' : self.username, - 'edit' : True, - 'editable' : can_edit, - 'system': input_system, - 'profiles': self.remote.get_profiles() - } ) + def system_search(self,**spam): + return self.__search('system') # ------------------------------------------------------------------------ # # Profiles # ------------------------------------------------------------------------ # + + def profile_search(self,**spam): + return self.__search('profile') + + def profile_menu(self,**spam): + return self.__render('blank.tmpl', { 'more_blank' : 1}) + def profile_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -537,7 +774,7 @@ class CobblerWeb(object): ksmeta=None,owners=None,enablemenu=None,virtfilesize=None,virtram=None,virttype=None, virtpath=None,repos=None,dhcptag=None,delete1=False,delete2=False, parent=None,virtcpus=None,virtbridge=None,subprofile=None,server_override=None, - name_servers=None,redhatmanagementkey=None,recursive=False,**args): + name_servers=None,name_servers_search=None,redhatmanagementkey=None,mgmt_classes=None,recursive=False,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -604,7 +841,10 @@ class CobblerWeb(object): self.remote.modify_profile(profile, 'server', server_override, self.token) self.remote.modify_profile(profile, 'comment', comment, self.token) self.remote.modify_profile(profile, 'name_servers', name_servers, self.token) + self.remote.modify_profile(profile, 'name_servers_search', name_servers_search, self.token) self.remote.modify_profile(profile, 'redhat_management_key', redhatmanagementkey, self.token) + self.remote.modify_profile(profile, 'redhat_management_server', redhatmanagementserver, self.token) + self.remote.modify_profile(profile, 'mgmt_classes', mgmt_classes, self.token) if repos is None: repos = [] @@ -634,6 +874,12 @@ class CobblerWeb(object): # Repos # ------------------------------------------------------------------------ # + def repo_search(self,**spam): + return self.__search('repo') + + def repo_menu(self,**spam): + return self.__render('blank.tmpl', { 'more_blank' : 1}) + def repo_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -748,6 +994,12 @@ class CobblerWeb(object): # Images # ------------------------------------------------------------------------ # + def image_search(self,**spam): + return self.__search('image') + + def image_menu(self,**spam): + return self.__render('blank.tmpl', { 'more_blank' : 1}) + def image_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -871,6 +1123,9 @@ class CobblerWeb(object): # Kickstart files # ------------------------------------------------------------------------ # + def ksfile_menu(self,**spam): + return self.__render('blank.tmpl', { 'more_blank' : 1}) + def ksfile_list(self,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -982,26 +1237,49 @@ class CobblerWeb(object): index.exposed = True menu.exposed = True + distro_menu.exposed = True distro_edit.exposed = True distro_list.exposed = True distro_save.exposed = True - + distro_search.exposed = True + distro_search_execute.exposed = True + + profile_menu.exposed = True subprofile_edit.exposed = True profile_edit.exposed = True profile_list.exposed = True + profile_search.exposed = True profile_save.exposed = True + profile_search_execute.exposed = True + system_menu.exposed = True system_edit.exposed = True + system_edit_new.exposed = True + system_edit_copy.exposed = True system_list.exposed = True + system_list_action.exposed = True + system_netboot.exposed = True + system_profile.exposed = True + system_power.exposed = True + system_rename.exposed = True + system_delete.exposed = True system_save.exposed = True + system_search.exposed = True + system_search_execute.exposed = True + repo_menu.exposed = True repo_edit.exposed = True repo_list.exposed = True repo_save.exposed = True + repo_search.exposed = True + repo_search_execute.exposed = True + image_menu.exposed = True image_edit.exposed = True image_list.exposed = True image_save.exposed = True + image_search.exposed = True + image_search_execute.exposed = True settings_view.exposed = True ksfile_edit.exposed = True diff --git a/cobbler/yaml/__init__.py b/cobbler/yaml/__init__.py deleted file mode 100644 index bd21b40e..00000000 --- a/cobbler/yaml/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-__version__ = "0.32"
-from load import loadFile, load, Parser, l
-from dump import dump, dumpToFile, Dumper, d
-from stream import YamlLoaderException, StringStream, FileStream
-from timestamp import timestamp
-import sys
-if sys.hexversion >= 0x02020000:
- from redump import loadOrdered
-
-try:
- from ypath import ypath
-except NameError:
- def ypath(expr,target='',cntx=''):
- raise NotImplementedError("ypath requires Python 2.2")
-
-if sys.hexversion < 0x02010000:
- raise 'YAML is not tested for pre-2.1 versions of Python'
diff --git a/cobbler/yaml/dump.py b/cobbler/yaml/dump.py deleted file mode 100644 index eb34955b..00000000 --- a/cobbler/yaml/dump.py +++ /dev/null @@ -1,305 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-
-import types
-import string
-from types import StringType, UnicodeType, IntType, FloatType
-from types import DictType, ListType, TupleType, InstanceType
-from klass import hasMethod, isDictionary
-import re
-
-"""
- The methods from this module that are exported to the top
- level yaml package should remain stable. If you call
- directly into other methods of this module, be aware that
- they may change or go away in future implementations.
- Contact the authors if there are methods in this file
- that you wish to remain stable.
-"""
-
-def dump(*data):
- return Dumper().dump(*data)
-
-def d(data): return dump(data)
-
-def dumpToFile(file, *data):
- return Dumper().dumpToFile(file, *data)
-
-class Dumper:
- def __init__(self):
- self.currIndent = "\n"
- self.indent = " "
- self.keysrt = None
- self.alphaSort = 1 # legacy -- on by default
-
- def setIndent(self, indent):
- self.indent = indent
- return self
-
- def setSort(self, sort_hint):
- self.keysrt = sortMethod(sort_hint)
- return self
-
- def dump(self, *data):
- self.result = []
- self.output = self.outputToString
- self.dumpDocuments(data)
- return string.join(self.result,"")
-
- def outputToString(self, data):
- self.result.append(data)
-
- def dumpToFile(self, file, *data):
- self.file = file
- self.output = self.outputToFile
- self.dumpDocuments(data)
-
- def outputToFile(self, data):
- self.file.write(data)
-
- def dumpDocuments(self, data):
- for obj in data:
- self.anchors = YamlAnchors(obj)
- self.output("---")
- self.dumpData(obj)
- self.output("\n")
-
- def indentDump(self, data):
- oldIndent = self.currIndent
- self.currIndent += self.indent
- self.dumpData(data)
- self.currIndent = oldIndent
-
- def dumpData(self, data):
- anchor = self.anchors.shouldAnchor(data)
- # Disabling anchors because they are lame for strings that the user might want to view/edit -- mdehaan
- #
- #if anchor:
- # self.output(" &%d" % anchor )
- #else:
- # anchor = self.anchors.isAlias(data)
- # if anchor:
- # self.output(" *%d" % anchor )
- # return
- if (data is None):
- self.output(' ~')
- elif hasMethod(data, 'to_yaml'):
- self.dumpTransformedObject(data)
- elif hasMethod(data, 'to_yaml_implicit'):
- self.output(" " + data.to_yaml_implicit())
- elif type(data) is InstanceType:
- self.dumpRawObject(data)
- elif isDictionary(data):
- self.dumpDict(data)
- elif type(data) in [ListType, TupleType]:
- self.dumpList(data)
- else:
- self.dumpScalar(data)
-
- def dumpTransformedObject(self, data):
- obj_yaml = data.to_yaml()
- if type(obj_yaml) is not TupleType:
- self.raiseToYamlSyntaxError()
- (data, typestring) = obj_yaml
- if typestring:
- self.output(" " + typestring)
- self.dumpData(data)
-
- def dumpRawObject(self, data):
- self.output(' !!%s.%s' % (data.__module__, data.__class__.__name__))
- self.dumpData(data.__dict__)
-
- def dumpDict(self, data):
- keys = data.keys()
- if len(keys) == 0:
- self.output(" {}")
- return
- if self.keysrt:
- keys = sort_keys(keys,self.keysrt)
- else:
- if self.alphaSort:
- keys.sort()
- for key in keys:
- self.output(self.currIndent)
- self.dumpKey(key)
- self.output(":")
- self.indentDump(data[key])
-
- def dumpKey(self, key):
- if type(key) is TupleType:
- self.output("?")
- self.indentDump(key)
- self.output("\n")
- else:
- self.output(quote(key))
-
- def dumpList(self, data):
- if len(data) == 0:
- self.output(" []")
- return
- for item in data:
- self.output(self.currIndent)
- self.output("-")
- self.indentDump(item)
-
- def dumpScalar(self, data):
- if isUnicode(data):
- self.output(' "%s"' % repr(data)[2:-1])
- elif isMulti(data):
- self.dumpMultiLineScalar(data.splitlines())
- else:
- self.output(" ")
- self.output(quote(data))
-
- def dumpMultiLineScalar(self, lines):
- self.output(" |")
- if lines[-1] == "":
- self.output("+")
- for line in lines:
- self.output(self.currIndent)
- self.output(line)
-
- def raiseToYamlSyntaxError(self):
- raise """
-to_yaml should return tuple w/object to dump
-and optional YAML type. Example:
-({'foo': 'bar'}, '!!foobar')
-"""
-
-#### ANCHOR-RELATED METHODS
-
-def accumulate(obj,occur):
- typ = type(obj)
- if obj is None or \
- typ is IntType or \
- typ is FloatType or \
- ((typ is StringType or typ is UnicodeType) \
- and len(obj) < 32): return
- obid = id(obj)
- if 0 == occur.get(obid,0):
- occur[obid] = 1
- if typ is ListType:
- for x in obj:
- accumulate(x,occur)
- if typ is DictType:
- for (x,y) in obj.items():
- accumulate(x,occur)
- accumulate(y,occur)
- else:
- occur[obid] = occur[obid] + 1
-
-class YamlAnchors:
- def __init__(self,data):
- occur = {}
- accumulate(data,occur)
- anchorVisits = {}
- for (obid, occur) in occur.items():
- if occur > 1:
- anchorVisits[obid] = 0
- self._anchorVisits = anchorVisits
- self._currentAliasIndex = 0
- def shouldAnchor(self,obj):
- ret = self._anchorVisits.get(id(obj),None)
- if 0 == ret:
- self._currentAliasIndex = self._currentAliasIndex + 1
- ret = self._currentAliasIndex
- self._anchorVisits[id(obj)] = ret
- return ret
- return 0
- def isAlias(self,obj):
- return self._anchorVisits.get(id(obj),0)
-
-### SORTING METHODS
-
-def sort_keys(keys,fn):
- tmp = []
- for key in keys:
- val = fn(key)
- if val is None: val = '~'
- tmp.append((val,key))
- tmp.sort()
- return [ y for (x,y) in tmp ]
-
-def sortMethod(sort_hint):
- typ = type(sort_hint)
- if DictType == typ:
- return sort_hint.get
- elif ListType == typ or TupleType == typ:
- indexes = {}; idx = 0
- for item in sort_hint:
- indexes[item] = idx
- idx += 1
- return indexes.get
- else:
- return sort_hint
-
-### STRING QUOTING AND SCALAR HANDLING
-def isStr(data):
- # XXX 2.1 madness
- if type(data) == type(''):
- return 1
- if type(data) == type(u''):
- return 1
- return 0
-
-def doubleUpQuotes(data):
- return data.replace("'", "''")
-
-def quote(data):
- if not isStr(data):
- return str(data)
- single = "'"
- double = '"'
- quote = ''
- if len(data) == 0:
- return "''"
- if hasSpecialChar(data) or data[0] == single:
- data = `data`[1:-1]
- data = string.replace(data, r"\x08", r"\b")
- quote = double
- elif needsSingleQuote(data):
- quote = single
- data = doubleUpQuotes(data)
- return "%s%s%s" % (quote, data, quote)
-
-def needsSingleQuote(data):
- if re.match(r"^-?\d", data):
- return 1
- if re.match(r"\*\S", data):
- return 1
- if data[0] in ['&', ' ']:
- return 1
- if data[0] == '"':
- return 1
- if data[-1] == ' ':
- return 1
- return (re.search(r'[:]', data) or re.search(r'(\d\.){2}', data))
-
-def hasSpecialChar(data):
- # need test to drive out '#' from this
- return re.search(r'[\t\b\r\f#]', data)
-
-def isMulti(data):
- if not isStr(data):
- return 0
- if hasSpecialChar(data):
- return 0
- return re.search("\n", data)
-
-def isUnicode(data):
- return type(data) == unicode
-
-def sloppyIsUnicode(data):
- # XXX - hack to make tests pass for 2.1
- return repr(data)[:2] == "u'" and repr(data) != data
-
-import sys
-if sys.hexversion < 0x20200000:
- isUnicode = sloppyIsUnicode
-
-
-
diff --git a/cobbler/yaml/implicit.py b/cobbler/yaml/implicit.py deleted file mode 100644 index 49d65e03..00000000 --- a/cobbler/yaml/implicit.py +++ /dev/null @@ -1,52 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-import re
-import string
-from timestamp import timestamp, matchTime
-
-DATETIME_REGEX = re.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
-FLOAT_REGEX = re.compile("^[-+]?[0-9][0-9,]*\.[0-9]*$")
-SCIENTIFIC_REGEX = re.compile("^[-+]?[0-9]+(\.[0-9]*)?[eE][-+][0-9]+$")
-OCTAL_REGEX = re.compile("^[-+]?([0][0-7,]*)$")
-HEX_REGEX = re.compile("^[-+]?0x[0-9a-fA-F,]+$")
-INT_REGEX = re.compile("^[-+]?(0|[1-9][0-9,]*)$")
-
-def convertImplicit(val):
- if val == '~':
- return None
- if val == '+':
- return 1
- if val == '-':
- return 0
- if val[0] == "'" and val[-1] == "'":
- val = val[1:-1]
- return string.replace(val, "''", "\'")
- if val[0] == '"' and val[-1] == '"':
- if re.search(r"\u", val):
- val = "u" + val
- unescapedStr = eval (val)
- return unescapedStr
- if matchTime.match(val):
- return timestamp(val)
- if INT_REGEX.match(val):
- return int(cleanseNumber(val))
- if OCTAL_REGEX.match(val):
- return int(val, 8)
- if HEX_REGEX.match(val):
- return int(val, 16)
- if FLOAT_REGEX.match(val):
- return float(cleanseNumber(val))
- if SCIENTIFIC_REGEX.match(val):
- return float(cleanseNumber(val))
- return val
-
-def cleanseNumber(str):
- if str[0] == '+':
- str = str[1:]
- str = string.replace(str,',','')
- return str
-
diff --git a/cobbler/yaml/inline.py b/cobbler/yaml/inline.py deleted file mode 100644 index d4f6439a..00000000 --- a/cobbler/yaml/inline.py +++ /dev/null @@ -1,44 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-import re
-import string
-
-class InlineTokenizer:
- def __init__(self, data):
- self.data = data
-
- def punctuation(self):
- puncts = [ '[', ']', '{', '}' ]
- for punct in puncts:
- if self.data[0] == punct:
- self.data = self.data[1:]
- return punct
-
- def up_to_comma(self):
- match = re.match('(.*?)\s*, (.*)', self.data)
- if match:
- self.data = match.groups()[1]
- return match.groups()[0]
-
- def up_to_end_brace(self):
- match = re.match('(.*?)(\s*[\]}].*)', self.data)
- if match:
- self.data = match.groups()[1]
- return match.groups()[0]
-
- def next(self):
- self.data = string.strip(self.data)
- productions = [
- self.punctuation,
- self.up_to_comma,
- self.up_to_end_brace
- ]
- for production in productions:
- token = production()
- if token:
- return token
-
diff --git a/cobbler/yaml/klass.py b/cobbler/yaml/klass.py deleted file mode 100644 index c182fcf2..00000000 --- a/cobbler/yaml/klass.py +++ /dev/null @@ -1,54 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-import new
-import re
-
-class DefaultResolver:
- def resolveType(self, data, typestring):
- match = re.match('!!(.*?)\.(.*)', typestring)
- if not match:
- raise "Invalid private type specifier"
- (modname, classname) = match.groups()
- return makeClass(modname, classname, data)
-
-def makeClass(module, classname, dict):
- exec('import %s' % (module))
- klass = eval('%s.%s' % (module, classname))
- obj = new.instance(klass)
- if hasMethod(obj, 'from_yaml'):
- return obj.from_yaml(dict)
- obj.__dict__ = dict
- return obj
-
-def hasMethod(object, method_name):
- try:
- klass = object.__class__
- except:
- return 0
- if not hasattr(klass, method_name):
- return 0
- method = getattr(klass, method_name)
- if not callable(method):
- return 0
- return 1
-
-def isDictionary(data):
- return isinstance(data, dict)
-
-try:
- isDictionary({})
-except:
- def isDictionary(data): return type(data) == type({}) # XXX python 2.1
-
-if __name__ == '__main__':
- print isDictionary({'foo': 'bar'})
- try:
- print isDictionary(dict())
- from ordered_dict import OrderedDict
- print isDictionary(OrderedDict())
- except:
- pass
diff --git a/cobbler/yaml/load.py b/cobbler/yaml/load.py deleted file mode 100644 index 54931d67..00000000 --- a/cobbler/yaml/load.py +++ /dev/null @@ -1,333 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-import re, string
-from implicit import convertImplicit
-from inline import InlineTokenizer
-from klass import DefaultResolver
-from stream import YamlLoaderException, FileStream, StringStream, NestedDocs
-
-try:
- iter(list()) # is iter supported by this version of Python?
-except:
- # XXX - Python 2.1 does not support iterators
- class StopIteration: pass
- class iter:
- def __init__(self,parser):
- self._docs = []
- try:
- while 1:
- self._docs.append(parser.next())
- except StopIteration: pass
- self._idx = 0
- def __len__(self): return len(self._docs)
- def __getitem__(self,idx): return self._docs[idx]
- def next(self):
- if self._idx < len(self._docs):
- ret = self._docs[self._idx]
- self._idx = self._idx + 1
- return ret
- raise StopIteration
-
-def loadFile(filename, typeResolver=None):
- return loadStream(FileStream(filename),typeResolver)
-
-def load(str, typeResolver=None):
- return loadStream(StringStream(str), typeResolver)
-
-def l(str): return load(str).next()
-
-def loadStream(stream, typeResolver):
- return iter(Parser(stream, typeResolver))
-
-def tryProductions(productions, value):
- for production in productions:
- results = production(value)
- if results:
- (ok, result) = results
- if ok:
- return (1, result)
-
-def dumpDictionary(): return {}
-
-class Parser:
- def __init__(self, stream, typeResolver=None):
- try:
- self.dictionary = dict
- except:
- self.dictionary = dumpDictionary
- self.nestedDocs = NestedDocs(stream)
- self.aliases = {}
- if typeResolver:
- self.typeResolver = typeResolver
- else:
- self.typeResolver = DefaultResolver()
-
- def error(self, msg):
- self.nestedDocs.error(msg, self.line)
-
- def nestPop(self):
- line = self.nestedDocs.pop()
- if line is not None:
- self.line = line
- return 1
-
- def value(self, indicator):
- return getToken(indicator+"\s*(.*)", self.line)
-
- def getNextDocument(self): raise "getNextDocument() deprecated--use next()"
-
- def next(self):
- line = self.nestedDocs.popDocSep()
- indicator = getIndicator(line)
- if indicator:
- return self.parse_value(indicator)
- if line:
- self.nestedDocs.nestToNextLine()
- return self.parseLines()
- raise StopIteration
-
- def __iter__(self): return self
-
- def parseLines(self):
- peekLine = self.nestedDocs.peek()
- if peekLine:
- if re.match("\s*-", peekLine):
- return self.parse_collection([], self.parse_seq_line)
- else:
- return self.parse_collection(self.dictionary(), self.parse_map_line)
- raise StopIteration
-
- def parse_collection(self, items, lineParser):
- while self.nestPop():
- if self.line:
- lineParser(items)
- return items
-
- def parse_seq_line(self, items):
- value = self.value("-")
- if value is not None:
- items.append(self.parse_seq_value(value))
- else:
- self.error("missing '-' for seq")
-
- def parse_map_line(self, items):
- if (self.line == '?'):
- self.parse_map_line_nested(items)
- else:
- self.parse_map_line_simple(items, self.line)
-
- def parse_map_line_nested(self, items):
- self.nestedDocs.nestToNextLine()
- key = self.parseLines()
- if self.nestPop():
- value = self.value(':')
- if value is not None:
- items[tuple(key)] = self.parse_value(value)
- return
- self.error("key has no value for nested map")
-
- def parse_map_line_simple(self, items, line):
- map_item = self.key_value(line)
- if map_item:
- (key, value) = map_item
- key = convertImplicit(key)
- if items.has_key(key):
- self.error("Duplicate key "+key)
- items[key] = self.parse_value(value)
- else:
- self.error("bad key for map")
-
- def is_map(self, value):
- # XXX - need real tokenizer
- if len(value) == 0:
- return 0
- if value[0] == "'":
- return 0
- if re.search(':(\s|$)', value):
- return 1
-
- def parse_seq_value(self, value):
- if self.is_map(value):
- return self.parse_compressed_map(value)
- else:
- return self.parse_value(value)
-
- def parse_compressed_map(self, value):
- items = self.dictionary()
- line = self.line
- token = getToken("(\s*-\s*)", line)
- self.nestedDocs.nestBySpecificAmount(len(token))
- self.parse_map_line_simple(items, value)
- return self.parse_collection(items, self.parse_map_line)
-
- def parse_value(self, value):
- (alias, value) = self.testForRepeatOfAlias(value)
- if alias:
- return value
- (alias, value) = self.testForAlias(value)
- value = self.parse_unaliased_value(value)
- if alias:
- self.aliases[alias] = value
- return value
-
- def parse_unaliased_value(self, value):
- match = re.match(r"(!\S*)(.*)", value)
- if match:
- (url, value) = match.groups()
- value = self.parse_untyped_value(value)
- if url[:2] == '!!':
- return self.typeResolver.resolveType(value, url)
- else:
- # XXX - allows syntax, but ignores it
- return value
- return self.parse_untyped_value(value)
-
- def parseInlineArray(self, value):
- if re.match("\s*\[", value):
- return self.parseInline([], value, ']',
- self.parseInlineArrayItem)
-
- def parseInlineHash(self, value):
- if re.match("\s*{", value):
- return self.parseInline(self.dictionary(), value, '}',
- self.parseInlineHashItem)
-
- def parseInlineArrayItem(self, result, token):
- return result.append(convertImplicit(token))
-
- def parseInlineHashItem(self, result, token):
- (key, value) = self.key_value(token)
- result[key] = value
-
- def parseInline(self, result, value, end_marker, itemMethod):
- tokenizer = InlineTokenizer(value)
- tokenizer.next()
- while 1:
- token = tokenizer.next()
- if token == end_marker:
- break
- itemMethod(result, token)
- return (1, result)
-
- def parseSpecial(self, value):
- productions = [
- self.parseMultiLineScalar,
- self.parseInlineHash,
- self.parseInlineArray,
- ]
- return tryProductions(productions, value)
-
- def parse_untyped_value(self, value):
- parse = self.parseSpecial(value)
- if parse:
- (ok, data) = parse
- return data
- token = getToken("(\S.*)", value)
- if token:
- lines = [token] + \
- pruneTrailingEmpties(self.nestedDocs.popNestedLines())
- return convertImplicit(joinLines(lines))
- else:
- self.nestedDocs.nestToNextLine()
- return self.parseLines()
-
- def parseNative(self, value):
- return (1, convertImplicit(value))
-
- def parseMultiLineScalar(self, value):
- if value == '>':
- return (1, self.parseFolded())
- elif value == '|':
- return (1, joinLiteral(self.parseBlock()))
- elif value == '|+':
- return (1, joinLiteral(self.unprunedBlock()))
-
- def parseFolded(self):
- data = self.parseBlock()
- i = 0
- resultString = ''
- while i < len(data)-1:
- resultString = resultString + data[i]
- resultString = resultString + foldChar(data[i], data[i+1])
- i = i + 1
- return resultString + data[-1] + "\n"
-
- def unprunedBlock(self):
- self.nestedDocs.nestToNextLine()
- data = []
- while self.nestPop():
- data.append(self.line)
- return data
-
- def parseBlock(self):
- return pruneTrailingEmpties(self.unprunedBlock())
-
- def testForAlias(self, value):
- match = re.match("&(\S*)\s*(.*)", value)
- if match:
- return match.groups()
- return (None, value)
-
- def testForRepeatOfAlias(self, value):
- match = re.match("\*(\S+)", value)
- if match:
- alias = match.groups()[0]
- if self.aliases.has_key(alias):
- return (alias, self.aliases[alias])
- else:
- self.error("Unknown alias")
- return (None, value)
-
- def key_value(self, str):
- if str[-1] == ' ':
- self.error("Trailing spaces not allowed without quotes.")
- # XXX This allows mis-balanced " vs. ' stuff
- match = re.match("[\"'](.+)[\"']\s*:\s*(.*)", str)
- if match:
- (key, value) = match.groups()
- return (key, value)
- match = re.match("(.+?)\s*:\s*(.*)", str)
- if match:
- (key, value) = match.groups()
- if len(value) and value[0] == '#':
- value = ''
- return (key, value)
-
-def getToken(regex, value):
- match = re.search(regex, value)
- if match:
- return match.groups()[0]
-
-def pruneTrailingEmpties(data):
- while len(data) > 0 and data[-1] == '':
- data = data[:-1]
- return data
-
-def foldChar(line1, line2):
- if re.match("^\S", line1) and re.match("^\S", line2):
- return " "
- return "\n"
-
-def getIndicator(line):
- if line:
- header = r"(#YAML:\d+\.\d+\s*){0,1}"
- match = re.match("--- "+header+"(\S*.*)", line)
- if match:
- return match.groups()[-1]
-
-def joinLines(lines):
- result = ''
- for line in lines[:-1]:
- if line[-1] == '\\':
- result = result + line[:-1]
- else:
- result = result + line + " "
- return result + lines[-1]
-
-def joinLiteral(data):
- return string.join(data,"\n") + "\n"
-
diff --git a/cobbler/yaml/ordered_dict.py b/cobbler/yaml/ordered_dict.py deleted file mode 100644 index 5bc2e3e0..00000000 --- a/cobbler/yaml/ordered_dict.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -pyyaml legacy -Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved -(see open source license information in docs/ directory) -""" - - -# This is extremely crude implementation of an OrderedDict. -# If you know of a better implementation, please send it to -# the author Steve Howell. You can find my email via -# the YAML mailing list or wiki. - -class OrderedDict(dict): - def __init__(self): - self._keys = [] - - def __setitem__(self, key, val): - self._keys.append(key) - dict.__setitem__(self, key, val) - - def keys(self): - return self._keys - - def items(self): - return [(key, self[key]) for key in self._keys] - -if __name__ == '__main__': - data = OrderedDict() - data['z'] = 26 - data['m'] = 13 - data['a'] = 1 - for key in data.keys(): - print "The value for %s is %s" % (key, data[key]) - print data - - - - diff --git a/cobbler/yaml/redump.py b/cobbler/yaml/redump.py deleted file mode 100644 index eefd68ec..00000000 --- a/cobbler/yaml/redump.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -pyyaml legacy -Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved -(see open source license information in docs/ directory) -""" - -from ordered_dict import OrderedDict -from load import Parser -from dump import Dumper -from stream import StringStream - -def loadOrdered(stream): - parser = Parser(StringStream(stream)) - parser.dictionary = OrderedDict - return iter(parser) - -def redump(stream): - docs = list(loadOrdered(stream)) - dumper = Dumper() - dumper.alphaSort = 0 - return dumper.dump(*docs) - diff --git a/cobbler/yaml/stream.py b/cobbler/yaml/stream.py deleted file mode 100644 index dcd65c34..00000000 --- a/cobbler/yaml/stream.py +++ /dev/null @@ -1,199 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-import re
-import string
-
-def indentLevel(line):
- n = 0
- while n < len(line) and line[n] == ' ':
- n = n + 1
- return n
-
-class LineNumberStream:
- def __init__(self, filename=None):
- self.curLine = 0
- self.filename = filename
-
- def get(self):
- line = self.getLine()
- self.curLine += 1 # used by subclass
- if line:
- line = noLineFeed(line)
- return line
-
- def lastLineRead(self):
- return self.curLine
-
-class FileStream(LineNumberStream):
- def __init__(self, filename):
- self.fp = open(filename)
- LineNumberStream.__init__(self, filename)
-
- def getLine(self):
- line = self.fp.readline()
- if line == '': line = None
- return line
-
-class StringStream(LineNumberStream):
- def __init__(self, text):
- self.lines = split(text)
- self.numLines = len(self.lines)
- LineNumberStream.__init__(self)
-
- def getLine(self):
- if self.curLine < self.numLines:
- return self.lines[self.curLine]
-
-def split(text):
- lines = string.split(text, '\n')
- if lines[-1] == '':
- lines.pop()
- return lines
-
-def eatNewLines(stream):
- while 1:
- line = stream.get()
- if line is None or len(string.strip(line)):
- return line
-
-COMMENT_LINE_REGEX = re.compile(R"\s*#")
-def isComment(line):
- return line is not None and COMMENT_LINE_REGEX.match(line)
-
-class CommentEater:
- def __init__(self, stream):
- self.stream = stream
- self.peeked = 1
- self.line = eatNewLines(stream)
- self.eatComments()
-
- def eatComments(self):
- while isComment(self.line):
- self.line = self.stream.get()
-
- def peek(self):
- if self.peeked:
- return self.line
- self.peeked = 1
- self.line = self.stream.get()
- self.eatComments()
- return self.line
-
- def lastLineRead(self):
- return self.stream.lastLineRead()
-
- def pop(self):
- data = self.peek()
- self.peeked = 0
- return data
-
-class NestedText:
- def __init__(self, stream):
- self.commentEater = CommentEater(stream)
- self.reset()
-
- def lastLineRead(self):
- return self.commentEater.lastLineRead()
-
- def reset(self):
- self.indentLevel = 0
- self.oldIndents = [0]
-
- def peek(self):
- nextLine = self.commentEater.peek()
- if nextLine is not None:
- if indentLevel(nextLine) >= self.indentLevel:
- return nextLine[self.indentLevel:]
- elif nextLine == '':
- return ''
-
- def pop(self):
- line = self.peek()
- if line is None:
- self.indentLevel = self.oldIndents.pop()
- return
- self.commentEater.pop()
- return line
-
- def popNestedLines(self):
- nextLine = self.peek()
- if nextLine is None or nextLine == '' or nextLine[0] != ' ':
- return []
- self.nestToNextLine()
- lines = []
- while 1:
- line = self.pop()
- if line is None:
- break
- lines.append(line)
- return lines
-
- def nestToNextLine(self):
- line = self.commentEater.peek()
- indentation = indentLevel(line)
- if len(self.oldIndents) > 1 and indentation <= self.indentLevel:
- self.error("Inadequate indentation", line)
- self.setNewIndent(indentation)
-
- def nestBySpecificAmount(self, adjust):
- self.setNewIndent(self.indentLevel + adjust)
-
- def setNewIndent(self, indentLevel):
- self.oldIndents.append(self.indentLevel)
- self.indentLevel = indentLevel
-
-class YamlLoaderException(Exception):
- def __init__(self, *args):
- (self.msg, self.lineNum, self.line, self.filename) = args
-
- def __str__(self):
- msg = """\
-%(msg)s:
-near line %(lineNum)d:
-%(line)s
-""" % self.__dict__
- if self.filename:
- msg += "file: " + self.filename
- return msg
-
-class NestedDocs(NestedText):
- def __init__(self, stream):
- self.filename = stream.filename
- NestedText.__init__(self,stream)
- line = NestedText.peek(self)
- self.sep = '---'
- if self.startsWithSep(line):
- self.eatenDocSep = NestedText.pop(self)
- else:
- self.eatenDocSep = self.sep
-
- def startsWithSep(self,line):
- if line and self.sep == line[:3]: return 1
- return 0
-
- def popDocSep(self):
- line = self.eatenDocSep
- self.eatenDocSep = None
- self.reset()
- return line
-
- def pop(self):
- if self.eatenDocSep is not None:
- raise "error"
- line = self.commentEater.peek()
- if line and self.startsWithSep(line):
- self.eatenDocSep = NestedText.pop(self)
- return None
- return NestedText.pop(self)
-
- def error(self, msg, line):
- raise YamlLoaderException(msg, self.lastLineRead(), line, self.filename)
-
-def noLineFeed(s):
- while s[-1:] in ('\n', '\r'):
- s = s[:-1]
- return s
diff --git a/cobbler/yaml/timestamp.py b/cobbler/yaml/timestamp.py deleted file mode 100644 index 5c522f6e..00000000 --- a/cobbler/yaml/timestamp.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -pyyaml legacy -Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved -(see open source license information in docs/ directory) -""" - - -import time, re, string -from types import ListType, TupleType - -PRIVATE_NOTICE = """ - This module is considered to be private implementation - details and is subject to change. Please only use the - objects and methods exported to the top level yaml package. -""" - -# -# Time specific operations -# - -_splitTime = re.compile('\-|\s|T|t|:|\.|Z') -matchTime = re.compile(\ - '\d+-\d+-\d+([\s|T|t]\d+:\d+:\d+.\d+(Z|(\s?[\-|\+]\d+:\d+)))?') - -def _parseTime(val): - if not matchTime.match(val): raise ValueError(val) - tpl = _splitTime.split(val) - if not(tpl): raise ValueError(val) - siz = len(tpl) - sec = 0 - if 3 == siz: - tpl += [0,0,0,0,0,-1] - elif 7 == siz: - tpl.append(0) - tpl.append(-1) - elif 8 == siz: - if len(tpl.pop()) > 0: raise ValueError(val) - tpl.append(0) - tpl.append(-1) - elif 9 == siz or 10 == siz: - mn = int(tpl.pop()) - hr = int(tpl.pop()) - sec = (hr*60+mn)*60 - if val.find("+") > -1: sec = -sec - if 10 == siz: tpl.pop() - tpl.append(0) - tpl.append(-1) - else: - raise ValueError(val) - idx = 0 - while idx < 9: - tpl[idx] = int(tpl[idx]) - idx += 1 - if tpl[1] < 1 or tpl[1] > 12: raise ValueError(val) - if tpl[2] < 1 or tpl[2] > 31: raise ValueError(val) - if tpl[3] > 24: raise ValueError(val) - if tpl[4] > 61: raise ValueError(val) - if tpl[5] > 61: raise ValueError(val) - if tpl[0] > 2038: - #TODO: Truncation warning - tpl = (2038,1,18,0,0,0,0,0,-1) - tpl = tuple(tpl) - ret = time.mktime(tpl) - ret = time.localtime(ret+sec) - ret = ret[:8] + (0,) - return ret - - -class _timestamp: - def __init__(self,val=None): - if not val: - self.__tval = time.gmtime() - else: - typ = type(val) - if ListType == typ: - self.__tval = tuple(val) - elif TupleType == typ: - self.__tval = val - else: - self.__tval = _parseTime(val) - if 9 != len(self.__tval): raise ValueError - def __getitem__(self,idx): return self.__tval[idx] - def __len__(self): return 9 - def strftime(self,format): return time.strftime(format,self.__tval) - def mktime(self): return time.mktime(self.__tval) - def asctime(self): return time.asctime(self.__tval) - def isotime(self): - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % self.__tval[:6] - def __repr__(self): return "yaml.timestamp('%s')" % self.isotime() - def __str__(self): return self.isotime() - def to_yaml_implicit(self): return self.isotime() - def __hash__(self): return hash(self.__tval[:6]) - def __cmp__(self,other): - try: - return cmp(self.__tval[:6],other.__tval[:6]) - except AttributeError: - return -1 - -try: # inherit from mx.DateTime functionality if available - from mx import DateTime - class timestamp(_timestamp): - def __init__(self,val=None): - _timestamp.__init__(self,val) - self.__mxdt = DateTime.mktime(self.__tval) - def __getattr__(self, name): - return getattr(self.__mxdt, name) -except: - class timestamp(_timestamp): pass - - - -def unquote(expr): - """ - summary: > - Simply returns the unquoted string, and the - length of the quoted string token at the - beginning of the expression. - """ - tok = expr[0] - if "'" == tok: - idx = 1 - odd = 0 - ret = "" - while idx < len(expr): - chr = expr[idx] - if "'" == chr: - if odd: ret += chr - odd = not odd - else: - if odd: - tok = expr[:idx] - break - ret += chr - idx += 1 - if "'" == tok: tok = expr - return (ret,len(tok)) - if '"' == tok: - idx = 1 - esc = 0 - while idx < len(expr): - chr = expr[idx] - if '"' == chr and not esc: - tok = expr[:idx] + '"' - break - if '\\' == chr and not esc: esc = 1 - else: esc = 0 - idx += 1 - if '"' == tok: - raise SyntaxError("unmatched quote: " + expr) - ret = eval(tok) #TODO: find better way to unquote - return (ret,len(tok)) - return (expr,len(expr)) diff --git a/cobbler/yaml/ypath.py b/cobbler/yaml/ypath.py deleted file mode 100644 index b183a231..00000000 --- a/cobbler/yaml/ypath.py +++ /dev/null @@ -1,469 +0,0 @@ -"""
-pyyaml legacy
-Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
-(see open source license information in docs/ directory)
-"""
-
-
-from types import ListType, StringType, IntType, DictType, InstanceType
-import re
-from urllib import quote
-from timestamp import unquote
-
-noTarget = object()
-
-def escape(node):
- """
- summary: >
- This function escapes a given key so that it
- may appear within a ypath. URI style escaping
- is used so that ypath expressions can be a
- valid URI expression.
- """
- typ = type(node)
- if typ is IntType: return str(node)
- if typ is StringType:
- return quote(node,'')
- raise ValueError("TODO: Support more than just string and integer keys.")
-
-class context:
- """
- summary: >
- A ypath visit context through a YAML rooted graph.
- This is implemented as a 3-tuple including the parent
- node, the current key/index and the value. This is
- an immutable object so it can be cached.
- properties:
- key: mapping key or index within the parent collection
- value: current value within the parent's range
- parent: the parent context
- root: the very top of the yaml graph
- path: a tuple of the domain keys
- notes: >
- The context class doesn't yet handle going down the
- domain side of the tree...
- """
- def __init__(self,parent,key,value):
- """
- args:
- parent: parent context (or None if this is the root)
- key: mapping key or index for this context
- value: value of current location...
- """
- self.parent = parent
- self.key = key
- self.value = value
- if parent:
- assert parent.__class__ is self.__class__
- self.path = parent.path + (escape(key),)
- self.root = parent.root
- else:
- assert not key
- self.path = tuple()
- self.root = self
- def __setattr__(self,attname,attval):
- if attname in ('parent','key','value'):
- if self.__dict__.get(attname):
- raise ValueError("context is read-only")
- self.__dict__[attname] = attval
- def __hash__(self): return hash(self.path)
- def __cmp__(self,other):
- try:
- return cmp(self.path,other.path)
- except AttributeError:
- return -1
- def __str__(self):
- if self.path:
- return "/".join(('',)+self.path)
- else:
- return '/'
-
-def to_context(target):
- if type(target) is InstanceType:
- if target.__class__ is context:
- return target
- return context(None,None,target)
-
-def context_test():
- lst = ['value']
- map = {'key':lst}
- x = context(None,None,map)
- y = context(x,'key',lst)
- z = context(y,0,'value')
- assert ('key',) == y.path
- assert 'key' == y.key
- assert lst == y.value
- assert x == y.parent
- assert x == y.root
- assert 0 == z.key
- assert 'value' == z.value
- assert y == z.parent
- assert x == z.root
- assert hash(x)
- assert hash(y)
- assert hash(z)
- assert '/' == str(x)
- assert '/key' == str(y)
- assert '/key/0' == str(z)
-
-class null_seg:
- """
- summary: >
- This is the simplest path segment, it
- doesn't return any results and doesn't
- depend upon its context. It also happens to
- be the base class which all segments derive.
- """
- def __iter__(self):
- return self
- def next_null(self):
- raise StopIteration
- def bind(self,cntx):
- """
- summary: >
- The bind function is called whenever
- the parent context has changed.
- """
- assert(cntx.__class__ is context)
- self.cntx = cntx
- def apply(self,target):
- self.bind(to_context(target))
- return iter(self)
- def exists(self,cntx):
- try:
- self.bind(cntx)
- self.next()
- return 1
- except StopIteration:
- return 0
- next = next_null
-
-class self_seg(null_seg):
- """
- summary: >
- This path segment returns the context
- node exactly once.
- """
- def __str__(self): return '.'
- def next_self(self):
- self.next = self.next_null
- return self.cntx
- def bind(self,cntx):
- null_seg.bind(self,cntx)
- self.next = self.next_self
-
-class root_seg(self_seg):
- def __str__(self): return '/'
- def bind(self,cntx):
- self_seg.bind(self,cntx.root)
-
-class parent_seg(self_seg):
- def __str__(self): return '..'
- def bind(self,cntx):
- if cntx.parent: cntx = cntx.parent
- self_seg.bind(self,cntx)
-
-class wild_seg(null_seg):
- """
- summary: >
- The wild segment simply loops through
- all of the sub-contexts for a given object.
- If there aren't any children, this isn't an
- error it just doesn't return anything.
- """
- def __str__(self): return '*'
- def next_wild(self):
- key = self.keys.next()
- return context(self.cntx,key,self.values[key])
- def bind(self,cntx):
- null_seg.bind(self,cntx)
- typ = type(cntx.value)
- if typ is ListType:
- self.keys = iter(xrange(0,len(cntx.value)))
- self.values = cntx.value
- self.next = self.next_wild
- return
- if typ is DictType:
- self.keys = iter(cntx.value)
- self.values = cntx.value
- self.next = self.next_wild
- return
- self.next = self.next_null
-
-class trav_seg(null_seg):
- """
- summary: >
- This is a recursive traversal of the range, preorder.
- It is a recursive combination of self and wild.
- """
- def __str__(self): return '/'
- def next(self):
- while 1:
- (cntx,seg) = self.stk[-1]
- if not seg:
- seg = wild_seg()
- seg.bind(cntx)
- self.stk[-1] = (cntx,seg)
- return cntx
- try:
- cntx = seg.next()
- self.stk.append((cntx,None))
- except StopIteration:
- self.stk.pop()
- if not(self.stk):
- self.next = self.next_null
- raise StopIteration
-
- def bind(self,cntx):
- null_seg.bind(self,cntx)
- self.stk = [(cntx,None)]
-
-class match_seg(self_seg):
- """
- summary: >
- Matches a particular key within the
- current context. Kinda boring.
- """
- def __str__(self): return str(self.key)
- def __init__(self,key):
- #TODO: Do better implicit typing
- try:
- key = int(key)
- except: pass
- self.key = key
- def bind(self,cntx):
- try:
- mtch = cntx.value[self.key]
- cntx = context(cntx,self.key,mtch)
- self_seg.bind(self,cntx)
- except:
- null_seg.bind(self,cntx)
-
-class conn_seg(null_seg):
- """
- summary: >
- When two segments are connected via a slash,
- this is a composite. For each context of the
- parent, it binds the child, and returns each
- context of the child.
- """
- def __str__(self):
- if self.parent.__class__ == root_seg:
- return "/%s" % self.child
- return "%s/%s" % (self.parent, self.child)
- def __init__(self,parent,child):
- self.parent = parent
- self.child = child
- def next(self):
- while 1:
- try:
- return self.child.next()
- except StopIteration:
- cntx = self.parent.next()
- self.child.bind(cntx)
-
- def bind(self,cntx):
- null_seg.bind(self,cntx)
- self.parent.bind(cntx)
- try:
- cntx = self.parent.next()
- except StopIteration:
- return
- self.child.bind(cntx)
-
-
-class pred_seg(null_seg):
- def __str__(self): return "%s[%s]" % (self.parent, self.filter)
- def __init__(self,parent,filter):
- self.parent = parent
- self.filter = filter
- def next(self):
- while 1:
- ret = self.parent.next()
- if self.filter.exists(ret):
- return ret
- def bind(self,cntx):
- null_seg.bind(self,cntx)
- self.parent.bind(cntx)
-
-class or_seg(null_seg):
- def __str__(self): return "%s|%s" % (self.lhs,self.rhs)
- def __init__(self,lhs,rhs):
- self.rhs = rhs
- self.lhs = lhs
- self.unq = {}
- def next(self):
- seg = self.lhs
- try:
- nxt = seg.next()
- self.unq[nxt] = nxt
- return nxt
- except StopIteration: pass
- seg = self.rhs
- while 1:
- nxt = seg.next()
- if self.unq.get(nxt,None):
- continue
- return nxt
- def bind(self,cntx):
- null_seg.bind(self,cntx)
- self.lhs.bind(cntx)
- self.rhs.bind(cntx)
-
-class scalar:
- def __init__(self,val):
- self.val = val
- def __str__(self):
- return str(self.val)
- def value(self):
- return self.val
-
-class equal_pred:
- def exists_true(self,cntx): return 1
- def exists_false(self,cntx): return 0
- def exists_scalar(self,cntx):
- self.rhs.bind(cntx)
- try:
- while 1:
- cntx = self.rhs.next()
- if str(cntx.value) == self.lhs: #TODO: Remove type hack
- return 1
- except StopIteration: pass
- return 0
- def exists_segment(self,cntx):
- raise NotImplementedError()
- def __init__(self,lhs,rhs):
- if lhs.__class__ == scalar:
- if rhs.__class__ == scalar:
- if rhs.value() == lhs.value():
- self.exists = self.exists_true
- else:
- self.exists = self.exists_false
- else:
- self.exists = self.exists_scalar
- else:
- if rhs.__class__ == scalar:
- (lhs,rhs) = (rhs,lhs)
- self.exists = self.exists_scalar
- else:
- self.exists = self.exists_segment
- self.lhs = str(lhs.value()) #TODO: Remove type hack
- self.rhs = rhs
-
-matchSegment = re.compile(r"""^(\w+|/|\.|\*|\"|\')""")
-
-def parse_segment(expr):
- """
- Segments occur between the slashes...
- """
- mtch = matchSegment.search(expr)
- if not(mtch): return (None,expr)
- tok = mtch.group(); siz = len(tok)
- if '/' == tok: return (trav_seg(),expr)
- elif '.' == tok:
- if len(expr) > 1 and '.' == expr[1]:
- seg = parent_seg()
- siz = 2
- else:
- seg = self_seg()
- elif '*' == tok: seg = wild_seg()
- elif '"' == tok or "'" == tok:
- (cur,siz) = unquote(expr)
- seg = match_seg(cur)
- else:
- seg = match_seg(tok)
- return (seg,expr[siz:])
-
-matchTerm = re.compile(r"""^(\w+|/|\.|\(|\"|\')""")
-
-def parse_term(expr):
- mtch = matchTerm.search(expr)
- if not(mtch): return (None,expr)
- tok = mtch.group(); siz = len(tok)
- if '/' == tok or '.' == tok:
- return parse(expr)
- if '(' == tok:
- (term,expr) = parse_predicate(expr)
- assert ')' == expr[0]
- return (term,expr[1:])
- elif '"' == tok or "'" == tok:
- (val,siz) = unquote(expr)
- else:
- val = tok; siz = len(tok)
- return (scalar(val),expr[siz:])
-
-def parse_predicate(expr):
- (term,expr) = parse_term(expr)
- if not term: raise SyntaxError("term expected: '%s'" % expr)
- tok = expr[0]
- if '=' == tok:
- (rhs,expr) = parse_term(expr[1:])
- return (equal_pred(term,rhs),expr)
- if '(' == tok:
- raise "No functions allowed... yet!"
- if ']' == tok or ')' == tok:
- if term.__class__ is scalar:
- term = match_seg(str(term))
- return (term,expr)
- raise SyntaxError("ypath: expecting operator '%s'" % expr)
-
-def parse_start(expr):
- """
- Initial checking on the expression, and
- determine if it is relative or absolute.
- """
- if type(expr) != StringType or len(expr) < 1:
- raise TypeError("string required: " + repr(expr))
- if '/' == expr[0]:
- ypth = root_seg()
- else:
- ypth = self_seg()
- expr = '/' + expr
- return (ypth,expr)
-
-def parse(expr):
- """
- This the parser entry point, the top level node
- is always a root or self segment. The self isn't
- strictly necessary, but it keeps things simple.
- """
- (ypth,expr) = parse_start(expr)
- while expr:
- tok = expr[0]
- if '/' == tok:
- (child, expr) = parse_segment(expr[1:])
- if child: ypth = conn_seg(ypth,child)
- continue
- if '[' == tok:
- (filter, expr) = parse_predicate(expr[1:])
- assert ']' == expr[0]
- expr = expr[1:]
- ypth = pred_seg(ypth,filter)
- continue
- if '|' == tok:
- (rhs, expr) = parse(expr[1:])
- ypth = or_seg(ypth,rhs)
- continue
- if '(' == tok:
- (child,expr) = parse(expr[1:])
- assert ')' == expr[0]
- expr = expr[1:]
- ypth = conn_seg(ypth,child)
- continue
- break
- return (ypth,expr)
-
-class convert_to_value(null_seg):
- def __init__(self,itr):
- self.itr = itr
- def next(self):
- return self.itr.next().value
- def bind(self,cntx):
- self.itr.bind(cntx)
-
-def ypath(expr,target=noTarget,cntx=0):
- (ret,expr) = parse(expr)
- if expr: raise SyntaxError("ypath parse error `%s`" % expr)
- if not cntx: ret = convert_to_value(ret)
- if target is noTarget: return ret
- return ret.apply(target)
|