diff options
author | Michael DeHaan <mdehaan@redhat.com> | 2008-05-28 11:07:39 -0400 |
---|---|---|
committer | Michael DeHaan <mdehaan@redhat.com> | 2008-05-28 11:07:39 -0400 |
commit | 36fd29894dc5859fa295aac08d2726cf77667088 (patch) | |
tree | 75ec187193a029af50a898878c9fe9f56735075b /cobbler | |
parent | 60e7b2048cb810082d9f5da189d827d0dec3747c (diff) | |
parent | 2963016f5c143a6f3c3bc711a2b6d1ed07d65abe (diff) | |
download | third_party-cobbler-36fd29894dc5859fa295aac08d2726cf77667088.tar.gz third_party-cobbler-36fd29894dc5859fa295aac08d2726cf77667088.tar.xz third_party-cobbler-36fd29894dc5859fa295aac08d2726cf77667088.zip |
Merge branch 'devel'
Conflicts:
cobbler.spec
cobbler/item_system.py
cobbler/webui/master.py
Diffstat (limited to 'cobbler')
70 files changed, 4443 insertions, 2110 deletions
diff --git a/cobbler/action_buildiso.py b/cobbler/action_buildiso.py new file mode 100644 index 0000000..639bd6b --- /dev/null +++ b/cobbler/action_buildiso.py @@ -0,0 +1,181 @@ +""" +Builds non-live bootable CD's that have PXE-equivalent behavior +for all cobbler profiles currently in memory. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import sub_process +import sys +import traceback +import shutil +import sub_process + +import utils +from cexceptions import * +from utils import _ + +# FIXME: lots of overlap with pxegen.py, should consolidate +# FIXME: disable timeouts and remove local boot for this? +HEADER = """ + +DEFAULT menu +PROMPT 0 +MENU TITLE Cobbler | http://cobbler.et.redhat.com +TIMEOUT 200 +TOTALTIMEOUT 6000 +ONTIMEOUT local + +LABEL local + MENU LABEL (local) + MENU DEFAULT + LOCALBOOT -1 + +""" + +class BuildIso: + """ + Handles conversion of internal state to the tftpboot tree layout + """ + + def __init__(self,config,verbose=False): + """ + Constructor + """ + self.verbose = verbose + self.config = config + self.api = config.api + self.distros = config.distros() + self.profiles = config.profiles() + self.distmap = {} + self.distctr = 0 + + def make_shorter(self,distname): + if self.distmap.has_key(distname): + return self.distmap[distname] + else: + self.distctr = self.distctr + 1 + self.distmap[distname] = str(self.distctr) + return str(self.distctr) + + def run(self,iso=None,tempdir=None,profiles=None): + + # verify we can find isolinux.bin + + if iso is None: + iso = "kickstart.iso" + + isolinuxbin = "/usr/lib/syslinux/isolinux.bin" + if not os.path.exists(isolinuxbin): + raise CX(_("Required file not found: %s") % isolinuxbin) + + # if iso is none, create it in . as "cobbler.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) + + # 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") + utils.copyfile(isolinuxbin, os.path.join(isolinuxdir, "isolinux.bin")) + menu = "/var/lib/cobbler/menu.c32" + files = [ isolinuxbin, menu ] + for f in files: + if not os.path.exists(f): + raise CX(_("Required file not found: %s") % f) + utils.copyfile(f, os.path.join(isolinuxdir, os.path.basename(f))) + + print _("- copying kernels and initrds") + # copy all images in included profiles to images dir + for x in self.api.profiles(): + use_this = True + if profiles is not None: + which_profiles = profiles.split(",") + if not use_this in which_profiles: + use_this = False + dist = x.get_conceptual_parent() + if dist.name.find("-xen") != -1: + continue + 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)) + + # generate isolinux.cfg + print _("- generating a isolinux.cfg") + isolinuxcfg = os.path.join(isolinuxdir, "isolinux.cfg") + cfg = open(isolinuxcfg, "w+") + cfg.write(HEADER) # fixme, use template + + for x in self.api.profiles(): + # FIXME + use_this = True + if profiles is not None: + which_profiles = profiles.split(",") + if not use_this in which_profiles: + use_this = False + if use_this: + dist = x.get_conceptual_parent() + if dist.name.find("-xen") != -1: + continue + data = utils.blender(self.api, True, x) + distname = self.make_shorter(dist.name) + + cfg.write("\n") + cfg.write("LABEL %s\n" % x.name) + cfg.write(" MENU LABEL %s\n" % x.name) + cfg.write(" kernel %s.krn\n" % distname) + + if data["kickstart"].startswith("/"): + data["kickstart"] = "http://%s/cblr/svc/op/ks/profile/%s" % ( + data["server"], + x.name + ) + + append_line = " append initrd=%s.img" % distname + append_line = append_line + " ks=%s " % data["kickstart"] + append_line = append_line + " %s\n" % data["kernel_options"] + cfg.write(append_line) + + 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 + + print _("- running: %s") % cmd + rc = sub_process.call(cmd, shell=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 + + diff --git a/cobbler/action_check.py b/cobbler/action_check.py index f7bc9d9..74af0c6 100644 --- a/cobbler/action_check.py +++ b/cobbler/action_check.py @@ -17,8 +17,8 @@ import os import re import sub_process import action_sync -from rhpl.translate import _, N_, textdomain, utf8 - +import utils +from utils import _ class BootCheck: def __init__(self,config): @@ -36,8 +36,9 @@ class BootCheck: """ status = [] self.check_name(status) + self.check_selinux(status) if self.settings.manage_dhcp: - mode = self.settings.manage_dhcp_mode.lower() + mode = self.config.api.get_sync().dhcp.what() if mode == "isc": self.check_dhcpd_bin(status) self.check_dhcpd_conf(status) @@ -45,8 +46,16 @@ class BootCheck: elif mode == "dnsmasq": self.check_dnsmasq_bin(status) self.check_service(status,"dnsmasq") - else: - status.append(_("manage_dhcp_mode in /var/lib/cobbler/settings should be 'isc' or 'dnsmasq'")) + + if self.settings.manage_dns: + mode = self.config.api.get_sync().dns.what() + if mode == "bind": + self.check_bind_bin(status) + self.check_service(status,"named") + elif mode == "dnsmasq" and not self.settings.manage_dhcp: + self.check_dnsmasq_bin(status) + self.check_service(status,"dnsmasq") + self.check_service(status, "cobblerd") self.check_bootloaders(status) @@ -56,14 +65,25 @@ class BootCheck: self.check_httpd(status) self.check_iptables(status) self.check_yum(status) + self.check_for_default_password(status) + self.check_for_unreferenced_repos(status) + self.check_for_unsynced_repos(status) return status def check_service(self, status, which): - 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) - if rc != 0: - status.append(_("service %s is not running") % which) + if utils.check_dist() == "redhat": + 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) + if rc != 0: + status.append(_("service %s is not running") % which) + elif utils.check_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) + if rc != 0: + status.append(_("service %s is not running") % which) + else: + status.append(_("Unknown distribution type, cannot check for running service %s" % which)) def check_iptables(self, status): if os.path.exists("/etc/rc.d/init.d/iptables"): @@ -86,18 +106,67 @@ class BootCheck: parameters. """ if self.settings.server == "127.0.0.1": - status.append(_("The 'server' field in /var/lib/cobbler/settings must be set to something other than localhost, or kickstarting features will not work. This should be a resolvable hostname or IP for the boot server as reachable by all machines that will use it.")) + status.append(_("The 'server' field in /etc/cobbler/settings must be set to something other than localhost, or kickstarting features will not work. This should be a resolvable hostname or IP for the boot server as reachable by all machines that will use it.")) if self.settings.next_server == "127.0.0.1": - status.append(_("For PXE to be functional, the 'next_server' field in /var/lib/cobbler/settings must be set to something other than 127.0.0.1, and should match the IP of the boot server on the PXE network.")) + status.append(_("For PXE to be functional, the 'next_server' field in /etc/cobbler/settings must be set to something other than 127.0.0.1, and should match the IP of the boot server on the PXE network.")) + + def check_selinux(self,status): + prc = sub_process.Popen("/usr/sbin/getenforce",shell=True,stdout=sub_process.PIPE) + data = prc.communicate()[0] + if data.lower().find("disabled") == -1: + # permissive or enforcing or something else + prc2 = sub_process.Popen("/usr/sbin/getsebool -a",shell=True,stdout=sub_process.PIPE) + data2 = prc2.communicate()[0] + for line in data2.split("\n"): + 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")) + + + def check_for_default_password(self,status): + templates = utils.get_kickstart_templates(self.config.api) + files = [] + for t in templates: + fd = open(t) + data = fd.read() + fd.close() + if data.find("\$1\$mF86/UHC\$WvcIcX2t6crBz2onWxyac.") != -1: + files.append(t) + if len(files) > 0: + status.append(_("One or more kickstart templates references default password 'cobbler' and should be changed for security reasons: %s") % ", ".join(files)) + + + def check_for_unreferenced_repos(self,status): + repos = [] + referenced = [] + not_found = [] + for r in self.config.api.repos(): + repos.append(r.name) + for p in self.config.api.profiles(): + my_repos = p.repos + referenced.extend(my_repos) + for r in referenced: + if r not in repos: + not_found.append(r) + if len(not_found) > 0: + status.append(_("One or more repos referenced by profile objects is no longer defined in cobbler: %s") % ", ".join(not_found)) + + def check_for_unsynced_repos(self,status): + need_sync = [] + for r in self.config.repos(): + if r.mirror_locally == 1: + lookfor = os.path.join(self.settings.webdir, "repo_mirror", r.name) + if not os.path.exists(lookfor): + need_sync.append(r.name) + if len(need_sync) > 0: + status.append(_("One or more repos need to be processed by cobbler reposync for the first time before kickstarting against them: %s") % ", ".join(need_sync)) + def check_httpd(self,status): """ Check if Apache is installed. """ - if not os.path.exists(self.settings.httpd_bin): - status.append(_("Apache doesn't appear to be installed")) - else: - self.check_service(status,"httpd") + self.check_service(status,"httpd") def check_dhcpd_bin(self,status): @@ -105,14 +174,22 @@ class BootCheck: Check if dhcpd is installed """ if not os.path.exists(self.settings.dhcpd_bin): - status.append(_("dhcpd isn't installed, but is enabled in /var/lib/cobbler/settings")) + status.append(_("dhcpd isn't installed, but management is enabled in /etc/cobbler/settings")) def check_dnsmasq_bin(self,status): """ Check if dnsmasq is installed """ if not os.path.exists(self.settings.dnsmasq_bin): - status.append(_("dnsmasq isn't installed, but is enabled in /var/lib/cobbler/settings")) + status.append(_("dnsmasq isn't installed, but management is enabled in /etc/cobbler/settings")) + + def check_bind_bin(self,status): + """ + Check if bind is installed. + """ + if not os.path.exists(self.settings.bind_bin): + status.append(_("bind isn't installed, but management is enabled in /etc/cobbler/settings")) + def check_bootloaders(self,status): """ @@ -140,8 +217,9 @@ class BootCheck: """ Check if cobbler.conf's tftpboot directory exists """ - if not os.path.exists(self.settings.tftpboot): - status.append(_("please create directory: %(dirname)s") % { "dirname" : self.settings.tftpboot }) + bootloc = utils.tftpboot_location() + if not os.path.exists(bootloc): + status.append(_("please create directory: %(dirname)s") % { "dirname" : bootloc }) def check_tftpd_conf(self,status): @@ -152,17 +230,15 @@ class BootCheck: if os.path.exists(self.settings.tftpd_conf): f = open(self.settings.tftpd_conf) re_disable = re.compile(r'disable.*=.*yes') - found_bootdir = False for line in f.readlines(): if re_disable.search(line): status.append(_("change 'disable' to 'no' in %(file)s") % { "file" : self.settings.tftpd_conf }) - if line.find("-s %s" % self.settings.tftpboot) != -1: - found_bootdir = True - if not found_bootdir: - status.append(_("change 'server_args' to '-s %(args)s' in %(file)s") % { "file" : "/etc/xinetd.d/tftp", "args" : self.settings.tftpboot }) - else: status.append(_("file %(file)s does not exist") % { "file" : self.settings.tftpd_conf }) + + bootloc = utils.tftpboot_location() + if not os.path.exists(bootloc): + status.append(_("directory needs to be created: %s" % bootloc)) def check_dhcpd_conf(self,status): diff --git a/cobbler/action_import.py b/cobbler/action_import.py index db09818..3e726a6 100644 --- a/cobbler/action_import.py +++ b/cobbler/action_import.py @@ -23,7 +23,7 @@ import glob import api import utils import shutil -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ WGET_CMD = "wget --mirror --no-parent --no-host-directories --directory-prefix %s/%s %s" RSYNC_CMD = "rsync -a %s '%s' %s/ks_mirror/%s --exclude-from=/etc/cobbler/rsync.exclude --progress" @@ -36,7 +36,7 @@ TRY_LIST = [ class Importer: - def __init__(self,api,config,mirror,mirror_name,network_root=None,kickstart_file=None,rsync_flags=None): + def __init__(self,api,config,mirror,mirror_name,network_root=None,kickstart_file=None,rsync_flags=None,arch=None): """ Performs an import of a install tree (or trees) from the given mirror address. The prefix of the distro is to be specified @@ -59,6 +59,7 @@ class Importer: self.distros_added = [] self.kickstart_file = kickstart_file self.rsync_flags = rsync_flags + self.arch = arch # ---------------------------------------------------------------------- @@ -67,6 +68,26 @@ class Importer: raise CX(_("import failed. no --mirror specified")) if self.mirror_name is None: raise CX(_("import failed. no --name specified")) + if self.arch is not None: + self.arch = self.arch.lower() + if self.arch not in [ "x86", "ia64", "x86_64" ]: + raise CX(_("arch must be x86, x86_64, or ia64")) + + mpath = os.path.join(self.settings.webdir, "ks_mirror", self.mirror_name) + + if os.path.exists(mpath) and self.arch is None: + raise CX(_("Something already exists at this import location (%s). You must specify --arch to avoid potentially overwriting existing files.") % mpath) + + if self.arch: + # append the arch path to the name if the arch is not already + # found in the name. + found = False + for x in [ "ia64", "i386", "x86_64", "x86" ]: + if self.mirror_name.lower().find(x) != -1: + found = True + break + if not found: + self.mirror_name = self.mirror_name + "-" + self.arch if self.mirror_name is None: raise CX(_("import failed. no --name specified")) @@ -171,13 +192,6 @@ class Importer: print _("- skipping distro %s since it wasn't imported this time") % profile.distro continue - # THIS IS OBSOLETE: - # - #if not distro.kernel.startswith("%s/ks_mirror/" % self.settings.webdir): - # # this isn't a mirrored profile, so we won't touch it - # print _("- skipping %s since profile isn't mirrored") % profile.name - # continue - if (self.kickstart_file == None): kdir = os.path.dirname(distro.kernel) base_dir = "/".join(kdir.split("/")[0:-2]) @@ -266,6 +280,8 @@ class Importer: def set_kickstart(self, profile, flavor, major, minor): if flavor == "fedora": + if major >= 8: + return profile.set_kickstart("/etc/cobbler/sample_end.ks") if major >= 6: return profile.set_kickstart("/etc/cobbler/sample.ks") if flavor == "redhat" or flavor == "centos": @@ -442,8 +458,7 @@ class Importer: config_file.write("baseurl=http://@@http_server@@/cobbler/ks_mirror/%s\n" % (urlseg)) config_file.write("enabled=1\n") config_file.write("gpgcheck=0\n") - # NOTE: yum priority defaults to 99 if that plugin is enabled - # so don't need to add priority=99 here + config_file.write("priority=1\n") config_file.close() # don't run creatrepo twice -- this can happen easily for Xen and PXE, when diff --git a/cobbler/action_litesync.py b/cobbler/action_litesync.py index 457f3af..ad669be 100644 --- a/cobbler/action_litesync.py +++ b/cobbler/action_litesync.py @@ -30,7 +30,7 @@ from cexceptions import * import traceback import errno -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class BootLiteSync: @@ -49,45 +49,36 @@ class BootLiteSync: self.systems = config.systems() self.settings = config.settings() self.repos = config.repos() - self.sync = action_sync.BootSync(self.config) + self.sync = config.api.get_sync() def add_single_distro(self, name): # get the distro record distro = self.distros.find(name=name) if distro is None: raise CX(_("error in distro lookup: %s") % name) - # generate YAML file in distros/$name in webdir - self.sync.write_distro_file(distro) # copy image files to images/$name in webdir & tftpboot: - self.sync.copy_single_distro_files(distro) + self.sync.pxegen.copy_single_distro_files(distro) # cascade sync kids = distro.get_children() for k in kids: self.add_single_profile(k.name) def remove_single_distro(self, name): - # delete distro YAML file in distros/$name in webdir - self.sync.rmfile(os.path.join(self.settings.webdir, "distros", name)) + bootloc = utils.tftpboot_location() # delete contents of images/$name directory in webdir - self.sync.rmtree(os.path.join(self.settings.webdir, "images", name)) + utils.rmtree(os.path.join(self.settings.webdir, "images", name)) # delete contents of images/$name in tftpboot - self.sync.rmtree(os.path.join(self.settings.tftpboot, "images", name)) + utils.rmtree(os.path.join(bootloc, "images", name)) # delete potential symlink to tree in webdir/links - self.sync.rmfile(os.path.join(self.settings.webdir, "links", name)) + utils.rmfile(os.path.join(self.settings.webdir, "links", name)) def add_single_profile(self, name): # get the profile object: profile = self.profiles.find(name=name) if profile is None: raise CX(_("error in profile lookup")) - # rebuild profile_list YAML file in webdir - self.sync.write_listings() - # add profiles/$name YAML file in webdir - self.sync.write_profile_file(profile) - # generate kickstart for kickstarts/$name/ks.cfg in webdir - self.sync.validate_kickstart_for_specific_profile(profile) # rebuild the yum configuration files for any attached repos - self.sync.retemplate_yum_repos(profile,True) + self.sync.yumgen.retemplate_yum_repos(profile,True) # cascade sync kids = profile.get_children() for k in kids: @@ -97,18 +88,16 @@ class BootLiteSync: self.add_single_system(k.name) def remove_single_profile(self, name): - # rebuild profile_list YAML file in webdir - self.sync.write_listings() # delete profiles/$name file in webdir - self.sync.rmfile(os.path.join(self.settings.webdir, "profiles", name)) + utils.rmfile(os.path.join(self.settings.webdir, "profiles", name)) # delete contents on kickstarts/$name directory in webdir - self.sync.rmtree(os.path.join(self.settings.webdir, "kickstarts", name)) + utils.rmtree(os.path.join(self.settings.webdir, "kickstarts", name)) def update_system_netboot_status(self,name): system = self.systems.find(name=name) if system is None: raise CX(_("error in system lookup for %s") % name) - self.sync.write_all_system_files(system,True) + self.sync.pxegen.write_all_system_files(system) def add_single_system(self, name): # get the system object: @@ -116,28 +105,42 @@ class BootLiteSync: if system is None: raise CX(_("error in system lookup for %s") % name) # rebuild system_list file in webdir - self.sync.regen_ethers() # /etc/ethers, for dnsmasq & rarpd - self.sync.regen_hosts() # /var/lib/cobbler/cobbler_hosts, pretty much for dnsmasq - self.sync.write_listings() - # write the PXE and YAML files for the system - self.sync.write_all_system_files(system) + if self.settings.manage_dhcp: + self.sync.dhcp.regen_ethers() + if self.settings.manage_dns: + self.sync.dns.regen_hosts() + # write the PXE files for the system + self.sync.pxegen.write_all_system_files(system) # per system kickstarts - self.sync.validate_kickstart_for_specific_system(system) - # rebuild the yum configuration files for any attached repos - self.sync.retemplate_yum_repos(system,False) + self.sync.yumgen.retemplate_yum_repos(system,False) + if self.settings.manage_dhcp: + if self.settings.omapi_enabled: + for (name,interface) in system.interfaces.iteritems(): + self.sync.dhcp.write_dhcp_lease( + self.settings.omapi_port, + interface["hostname"], + interface["mac_address"], + interface["ip_address"] + ) + def remove_single_system(self, name): + bootloc = utils.tftpboot_location() system_record = self.systems.find(name=name) - # rebuild system_list file in webdir - self.sync.write_listings() - # delete system YAML file in systems/$name in webdir - self.sync.rmfile(os.path.join(self.settings.webdir, "systems", 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) - self.sync.rmtree(os.path.join(self.settings.webdir, "kickstarts_sys", filename)) + utils.rmtree(os.path.join(self.settings.webdir, "kickstarts_sys", filename)) + + if self.settings.manage_dhcp: + if self.settings.omapi_enabled: + for (name,interface) in system_record.interfaces.iteritems(): + self.sync.dhcp.remove_dhcp_lease( + self.settings.omapi_port, + interface["hostname"] + ) # unneeded #if not system_record.is_pxe_supported(): @@ -152,7 +155,7 @@ class BootLiteSync: if distro is not None and distro in [ "ia64", "IA64"]: itanic = True if not itanic: - self.sync.rmfile(os.path.join(self.settings.tftpboot, "pxelinux.cfg", filename)) + utils.rmfile(os.path.join(bootloc, "pxelinux.cfg", filename)) else: - self.sync.rmfile(os.path.join(self.settings.tftpboot, filename)) + utils.rmfile(os.path.join(bootloc, filename)) diff --git a/cobbler/action_replicate.py b/cobbler/action_replicate.py new file mode 100644 index 0000000..ae3adc8 --- /dev/null +++ b/cobbler/action_replicate.py @@ -0,0 +1,160 @@ +""" +Replicate from a cobbler master. + +Copyright 2007-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> +Scott Henson <shenson@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import xmlrpclib +import api as cobbler_api +from utils import _ + +class Replicate: + def __init__(self,config): + """ + Constructor + """ + self.config = config + self.settings = config.settings() + self.api = config.api + self.remote = None + self.uri = None + + # ------------------------------------------------------- + + def link_distro(self, distro): + """ + Create the distro links + """ + # find the tree location + dirname = os.path.dirname(distro.kernel) + tokens = dirname.split("/") + tokens = tokens[:-2] + base = "/".join(tokens) + dest_link = os.path.join(self.settings.webdir, "links", distro.name) + + # create the links directory only if we are mirroring because with + # SELinux Apache can't symlink to NFS (without some doing) + + if not os.path.exists(dest_link): + try: + os.symlink(base, dest_link) + except: + # this shouldn't happen but I've seen it ... debug ... + print _("- symlink creation failed: %(base)s, %(dest)s") % { "base" : base, "dest" : dest_link } + + # ------------------------------------------------------- + + def add_distro(self, distro): + """ + Add a distro that has been found + """ + #Register the distro + if os.path.exists(distro['kernel']): + new_distro = self.api.new_distro() + new_distro.from_datastruct(distro) + #create the symlinks + self.link_distro(new_distro) + #Add the distro permanently + self.api.distros().add(new_distro, save=True) + print 'Added distro %s. Creating Links.' % distro['name'] + else: + print 'Distro %s not here yet.' % distro['name'] + + # ------------------------------------------------------- + + def add_profile(self, profile): + """ + Add a profile that has been found + """ + #Register the new profile + new_profile = self.api.new_profile() + new_profile.from_datastruct(profile) + self.api.profiles().add(new_profile, save=True) + print 'Added profile %s.' % profile['name'] + + + # ------------------------------------------------------- + + def check_profile(self, profile): + """ + Check that a profile belongs to a distro + """ + profiles = self.api.profiles().to_datastruct() + if profile not in profiles: + for distro in self.api.distros().to_datastruct(): + if distro['name'] == profile['name']: + return True + return False + + + # ------------------------------------------------------- + + def sync_distros(self): + """ + Sync distros from master + """ + local_distros = self.api.distros() + remote_distros = self.remote.get_distros() + + needsync = False + for distro in remote_distros: + if distro not in local_distros.to_datastruct(): + print 'Found distro %s.' % distro['name'] + self.add_distro(distro) + needsync = True + + self.call_sync(needsync) + + + # ------------------------------------------------------- + + def sync_profiles(self): + """ + Sync profiles from master + """ + local_profiles = self.api.profiles() + remote_profiles = self.remote.get_profiles() + + needsync = False + for profile in remote_profiles: + if self.check_profile(profile): + print 'Found profile %s.' % profilew['name'] + self.add_profile(profile) + needsync = True + self.call_sync(needsync) + + + # ------------------------------------------------------- + + def call_sync(self, needsync): + if needsync: + self.api.sync() + + # ------------------------------------------------------- + + def run(self, cobbler_master=None): + """ + Get remote profiles and distros and sync them locally + """ + if cobbler_master is not None: + self.uri = 'http://%s/cobbler_api' % cobbler_master + elif len(self.settings.cobbler_master) > 0: + self.uri = 'http://%s/cobbler_api' % self.settings.cobbler_master + else: + print _('No cobbler master found to replicate from, try --master.') + if self.uri is not None: + self.remote = xmlrpclib.Server(self.uri) + self.sync_distros() + self.sync_profiles() + diff --git a/cobbler/action_reposync.py b/cobbler/action_reposync.py index 32d38bd..57c2d08 100644 --- a/cobbler/action_reposync.py +++ b/cobbler/action_reposync.py @@ -25,7 +25,7 @@ from cexceptions import * import traceback import errno -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class RepoSync: """ @@ -57,8 +57,10 @@ class RepoSync: self.verbose = verbose for repo in self.repos: if name is not None and repo.name != name: + # invoked to sync only a specific repo, this is not the one continue elif name is None and not repo.keep_updated: + # invoked to run against all repos, but this one is off print _("- %s is set to not be updated") % repo.name continue @@ -71,6 +73,8 @@ class RepoSync: if repo.is_rsync_mirror(): self.do_rsync(repo) else: + # which may actually NOT reposync if the repo is set to not mirror locally + # but that's a technicality self.do_reposync(repo) self.update_permissions(repo_path) @@ -105,7 +109,7 @@ class RepoSync: store_path = os.path.join(self.settings.webdir, "repo_mirror") dest_path = os.path.join(store_path, repo.name) temp_path = os.path.join(store_path, ".origin") - if not os.path.isdir(temp_path): + if not os.path.isdir(temp_path) and repo.mirror_locally: # FIXME: there's a chance this might break the RHN D/L case os.makedirs(temp_path) @@ -118,18 +122,21 @@ class RepoSync: # this is the simple non-RHN case. # create the config file that yum will use for the copying - temp_file = self.create_local_file(repo, temp_path, output=False) + if repo.mirror_locally: + temp_file = self.create_local_file(repo, temp_path, output=False) - if not has_rpm_list: + if not has_rpm_list and repo.mirror_locally: # if we have not requested only certain RPMs, use reposync cmd = "/usr/bin/reposync --config=%s --repoid=%s --download_path=%s" % (temp_file, repo.name, store_path) if repo.arch != "": + if repo.arch == "x86": + repo.arch = "i386" # FIX potential arch errors cmd = "%s -a %s" % (cmd, repo.arch) print _("- %s") % cmd cmds.append(cmd) - else: + elif repo.mirror_locally: # create the output directory if it doesn't exist if not os.path.exists(dest_path): @@ -149,6 +156,8 @@ class RepoSync: # this is the somewhat more-complex RHN case. # NOTE: this requires that you have entitlements for the server and you give the mirror as rhn://$channelname + if not repo.mirror_locally: + raise CX(_("rhn:// repos do not work with --mirror-locally=1")) if has_rpm_list: print _("- warning: --rpm-list is not supported for RHN content") @@ -161,7 +170,6 @@ class RepoSync: if repo.arch != "": cmd = "%s -a %s" % (cmd, repo.arch) - print _("- %s") % cmd cmds.append(cmd) # now regardless of whether we're doing yumdownloader or reposync @@ -169,9 +177,10 @@ class RepoSync: # commands here. Any failure at any point stops the operation. for cmd in cmds: - rc = sub_process.call(cmd, shell=True) - if rc !=0: - raise CX(_("cobbler reposync failed")) + if repo.mirror_locally: + rc = sub_process.call(cmd, shell=True) + if rc !=0: + raise CX(_("cobbler reposync failed")) # some more special case handling for RHN. # create the config file now, because the directory didn't exist earlier @@ -181,7 +190,8 @@ class RepoSync: # now run createrepo to rebuild the index - os.path.walk(dest_path, self.createrepo_walker, repo) + if repo.mirror_locally: + os.path.walk(dest_path, self.createrepo_walker, repo) # create the config file the hosts will use to access the repository. @@ -196,6 +206,9 @@ class RepoSync: Handle copying of rsync:// and rsync-over-ssh repos. """ + if not repo.mirror_locally: + raise CX(_("rsync:// urls must be mirrored locally, yum cannot access them directly")) + if repo.rpm_list != "": print _("- warning: --rpm-list is not supported for rsync'd repositories") dest_path = os.path.join(self.settings.webdir, "repo_mirror", repo.name) @@ -235,22 +248,34 @@ class RepoSync: config_file = open(fname, "w+") config_file.write("[%s]\n" % repo.name) config_file.write("name=%s\n" % repo.name) + optenabled = False + optgpgcheck = False if output: - line = "baseurl=http://${server}/cobbler/repo_mirror/%s\n" % (repo.name) + if repo.mirror_locally: + line = "baseurl=http://${server}/cobbler/repo_mirror/%s\n" % (repo.name) + else: + line = "baseurl=%s\n" % (repo.mirror) + config_file.write(line) # user may have options specific to certain yum plugins # add them to the file for x in repo.yumopts: config_file.write("%s=%s\n" % (x, repo.yumopts[x])) + if x == "enabled": + optenabled = True + if x == "gpgcheck": + optgpgcheck = True else: line = "baseurl=%s\n" % repo.mirror http_server = "%s:%s" % (self.settings.server, self.settings.http_port) line = line.replace("@@server@@",http_server) config_file.write(line) - config_file.write("enabled=1\n") + if not optenabled: + config_file.write("enabled=1\n") config_file.write("priority=%s\n" % repo.priority) # FIXME: potentially might want a way to turn this on/off on a per-repo basis - config_file.write("gpgcheck=0\n") + if not optgpgcheck: + config_file.write("gpgcheck=0\n") config_file.close() return fname @@ -260,7 +285,7 @@ class RepoSync: """ Used to run createrepo on a copied mirror. """ - if os.path.exists(os.path.join(dirname,"RPMS")) or repo.is_rsync_mirror(): + if os.path.exists(dirname) or repo.is_rsync_mirror(): utils.remove_yum_olddata(dirname) try: cmd = "createrepo %s %s" % (repo.createrepo_flags, dirname) @@ -289,7 +314,7 @@ class RepoSync: if os.path.exists(getenforce): data = sub_process.Popen(getenforce, shell=True, stdout=sub_process.PIPE).communicate()[0] if data.lower().find("disabled") == -1: - cmd3 = "chcon --reference /var/www %s" % repo_path + cmd3 = "chcon --reference /var/www %s >/dev/null 2>/dev/null" % repo_path sub_process.call(cmd3, shell=True) diff --git a/cobbler/action_status.py b/cobbler/action_status.py index cc6ebd4..79f9083 100644 --- a/cobbler/action_status.py +++ b/cobbler/action_status.py @@ -2,7 +2,7 @@ Reports on kickstart activity by examining the logs in /var/log/cobbler. -Copyright 2007, Red Hat, Inc +Copyright 2007-2008, Red Hat, Inc Michael DeHaan <mdehaan@redhat.com> This software may be freely redistributed under the terms of the GNU @@ -19,213 +19,138 @@ import glob import time import api as cobbler_api -from rhpl.translate import _, N_, textdomain, utf8 +#from utils import _ +# ARRAY INDEXES +MOST_RECENT_START = 0 +MOST_RECENT_STOP = 1 +MOST_RECENT_TARGET = 2 +SEEN_START = 3 +SEEN_STOP = 4 +STATE = 5 class BootStatusReport: + def __init__(self,config,mode): """ Constructor """ self.config = config self.settings = config.settings() + self.ip_data = {} self.mode = mode # ------------------------------------------------------- - def scan_apache_logfiles(self): - results = {} - files = [ "/var/log/httpd/access_log" ] - for x in range(1,4): - consider = "/var/log/httpd/access_log.%s" % x - if os.path.exists(consider): - files.append(consider) + def scan_logfiles(self): + + #profile foosball ? 127.0.0.1 start 1208294043.58 + #system neo ? 127.0.0.1 start 1208295122.86 + + + files = glob.glob("/var/log/cobbler/install.log*") for fname in files: - fh = open(fname) - data = fh.readline() - while (data is not None and data != ""): - data = fh.readline() - tokens = data.split(None) - if len(tokens) < 6: - continue - ip = tokens[0] - stime = tokens[3].replace("[","") - req = tokens[6] - if req.find("/cblr") == -1: - continue - ttime = time.strptime(stime,"%d/%b/%Y:%H:%M:%S") - itime = time.mktime(ttime) - if not results.has_key(ip): - results[ip] = {} - results[ip][itime] = req - - return results + fd = open(fname) + data = fd.read() + for line in data.split("\n"): + tokens = line.split() + if len(tokens) == 0: + continue + (profile_or_system, name, ip, start_or_stop, ts) = tokens + self.catalog(profile_or_system,name,ip,start_or_stop,ts) + fd.close() + + # ------------------------------------------------------ + + def catalog(self,profile_or_system,name,ip,start_or_stop,ts): + ip_data = self.ip_data + + if not ip_data.has_key(ip): + ip_data[ip] = [ -1, -1, "?", 0, 0, "?" ] + elem = ip_data[ip] + + ts = float(ts) + + mrstart = elem[MOST_RECENT_START] + mrstop = elem[MOST_RECENT_STOP] + mrtarg = elem[MOST_RECENT_TARGET] + snstart = elem[SEEN_START] + snstop = elem[SEEN_STOP] + + + if start_or_stop == "start": + if mrstart < ts: + mrstart = ts + mrtarg = "%s:%s" % (profile_or_system, name) + elem[SEEN_START] = elem[SEEN_START] + 1 + + if start_or_stop == "stop": + if mrstop < ts: + mrstop = ts + mrtarg = "%s:%s" % (profile_or_system, name) + elem[SEEN_STOP] = elem[SEEN_STOP] + 1 + + elem[MOST_RECENT_START] = mrstart + elem[MOST_RECENT_STOP] = mrstop + elem[MOST_RECENT_TARGET] = mrtarg # ------------------------------------------------------- - def scan_syslog_logfiles(self): - - # find all of the logged IP addrs - filelist = glob.glob("/var/log/cobbler/syslog/*") - filelist.sort() - results = {} - - for fullname in filelist: - #fname = os.path.basename(fullname) - logfile = open(fullname, "r") - # for each line in the file... - data = logfile.readline() - while(data is not None and data != ""): - data = logfile.readline() - - try: - (epoch, strdate, ip, request) = data.split("\t", 3) - epoch = float(epoch) - except: - continue - - if not results.has_key(ip): - results[ip] = {} - results[ip][epoch] = request - - return results + def process_results(self): + # FIXME: this should update the times here + + tnow = int(time.time()) + for ip in self.ip_data.keys(): + elem = self.ip_data[ip] + + start = int(elem[MOST_RECENT_START]) + stop = int(elem[MOST_RECENT_STOP]) + if (stop > start): + elem[STATE] = "finished" + else: + delta = tnow - start + min = delta / 60 + sec = delta % 60 + if min > 100: + elem[STATE] = "unknown/stalled" + else: + elem[STATE] = "installing (%sm %ss)" % (min,sec) + + return self.ip_data + + def get_printable_results(self): + format = "%-15s|%-20s|%-17s|%-17s" + ip_data = self.ip_data + ips = ip_data.keys() + ips.sort() + line = ( + "ip", + "target", + "start", + "state", + ) + buf = format % line + for ip in ips: + elem = ip_data[ip] + line = ( + ip, + elem[MOST_RECENT_TARGET], + time.ctime(elem[MOST_RECENT_START]), + elem[STATE] + ) + buf = buf + "\n" + format % line + return buf # ------------------------------------------------------- def run(self): """ Calculate and print a kickstart-status report. - For kickstart trees not in /var/lib/cobbler (or a symlink off of there) - tracking will be incomplete. This should be noted in the docs. """ - api = cobbler_api.BootAPI() - - apache_results = self.scan_apache_logfiles() - syslog_results = self.scan_syslog_logfiles() - ips = apache_results.keys() - ips.sort() - ips2 = syslog_results.keys() - ips2.sort() - - ips.extend(ips2) - ip_printed = {} - - last_recorded_time = 0 - time_collisions = 0 - - #header = ("Name", "State", "Started", "Last Request", "Seconds", "Log Entries") - print "%-20s | %-15s | %-25s | %-25s | %-10s | %-6s" % ( - _("Name"), - _("State"), - _("Last Request"), - _("Started"), - _("Seconds"), - _("Log Entries") - ) - - - for ip in ips: - if ip_printed.has_key(ip): - continue - ip_printed[ip] = 1 - entries = {} # hash of access times and messages - if apache_results.has_key(ip): - times = apache_results[ip].keys() - for logtime in times: - request = apache_results[ip][logtime] - if request.find("?system_done") != -1: - entries[logtime] = "DONE" - elif request.find("?profile_done") != -1: - entries[logtime] = "DONE" - else: - entries[logtime] = "1" # don't really care what the filename was - - if syslog_results.has_key(ip): - times = syslog_results[ip].keys() - for logtime in times: - request = syslog_results[ip][logtime] - if request.find("methodcomplete") != -1: - entries[logtime] = "DONE" - elif request.find("Method =") != -1: - entries[logtime] = "START" - else: - entries[logtime] = "1" - - obj = api.systems().find(ip_address=ip) - - if obj is not None: - self.generate_report(entries,obj.name) - else: - self.generate_report(entries,ip) - + self.scan_logfiles() + self.process_results() + print self.get_printable_results() return True - #----------------------------------------- - - def generate_report(self,entries,name): - """ - Given the information about transferred files and kickstart finish times, attempt - to produce a report that most describes the state of the system. - """ - # sort the access times - rtimes = entries.keys() - rtimes.sort() - - # variables for calculating kickstart state - last_request_time = 0 - last_start_time = 0 - last_done_time = 0 - fcount = 0 - - if len(rtimes) == 0: - print _("%s: ?") % name - return - - # for each request time the machine has made - for rtime in rtimes: - - rtime = rtime - fname = entries[rtime] - - if fname == "START": - install_state = "installing" - last_start_time = rtime - last_request_time = rtime - fcount = 0 - elif fname == "DONE": - # kickstart finished - last_done_time = rtime - install_state = "done" - else: - install_state = "?" - last_request_time = rtime - fcount = fcount + 1 - - # calculate elapsed time for kickstart - elapsed_time = 0 - if install_state == "done": - elapsed_time = int(last_done_time - last_start_time) - else: - elapsed_time = int(last_request_time - last_start_time) - - # FIXME: IP to MAC mapping where cobbler knows about it would be nice. - display_start = time.asctime(time.localtime(last_start_time)) - display_last = time.asctime(time.localtime(last_request_time)) - - if display_start.find(" 1969") != -1: - display_start = "?" - elapsed_time = "?" - - # print the status line for this IP address - print "%-20s | %-15s | %-25s | %-25s | %-10s | %-6s" % ( - name, - install_state, - display_start, - display_last, - elapsed_time, - fcount - ) - - diff --git a/cobbler/action_sync.py b/cobbler/action_sync.py index 139066e..55cdad5 100644 --- a/cobbler/action_sync.py +++ b/cobbler/action_sync.py @@ -1,10 +1,9 @@ """ -Builds out a TFTP/cobbler boot tree based on the object tree. +Builds out filesystem trees/data based on the object tree. This is the code behind 'cobbler sync'. -Copyright 2006,2007, Red Hat, Inc +Copyright 2006-2008, Red Hat, Inc Michael DeHaan <mdehaan@redhat.com> -Tim Verhoeven <tim.verhoeven.be@gmail.com> This software may be freely redistributed under the terms of the GNU general public license. @@ -22,19 +21,23 @@ import yaml # Howell-Clark version import sub_process import sys import glob +import traceback +import errno import utils from cexceptions import * -import traceback -import errno +import templar +import pxegen +import yumgen import item_distro import item_profile +import item_repo import item_system from Cheetah.Template import Template -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class BootSync: @@ -42,7 +45,7 @@ class BootSync: Handles conversion of internal state to the tftpboot tree layout """ - def __init__(self,config,verbose=False): + def __init__(self,config,verbose=False,dhcp=None,dns=None): """ Constructor """ @@ -54,202 +57,53 @@ class BootSync: self.systems = config.systems() self.settings = config.settings() self.repos = config.repos() - self.blend_cache = {} - self.load_snippet_cache() + self.templar = templar.Templar(config) + self.pxegen = pxegen.PXEGen(config) + self.dns = dns + self.dhcp = dhcp + self.yumgen = yumgen.YumGen(config) + self.bootloc = utils.tftpboot_location() def run(self): """ Syncs the current configuration file with the config tree. Using the Check().run_ functions previously is recommended """ - if not os.path.exists(self.settings.tftpboot): - raise CX(_("cannot find directory: %s") % self.settings.tftpboot) + if not os.path.exists(self.bootloc): + raise CX(_("cannot find directory: %s") % self.bootloc) # run pre-triggers... utils.run_triggers(None, "/var/lib/cobbler/triggers/sync/pre/*") - # in case the pre-trigger modified any objects... + # (paranoid) in case the pre-trigger modified any objects... 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) + self.yumgen = yumgen.YumGen(self.config) # execute the core of the sync operation self.clean_trees() - self.copy_bootloaders() - self.copy_distros() - self.retemplate_all_yum_repos() - self.validate_kickstarts() - self.build_trees() + self.pxegen.copy_bootloaders() + self.pxegen.copy_distros() + for x in self.systems: + self.pxegen.write_all_system_files(x) + self.yumgen.retemplate_all_yum_repos() if self.settings.manage_dhcp: - # these functions DRT for ISC or dnsmasq - self.write_dhcp_file() - self.regen_ethers() - self.regen_hosts() - self.make_pxe_menu() + self.dhcp.write_dhcp_file() + self.dhcp.regen_ethers() + if self.settings.manage_dns: + self.dns.regen_hosts() + self.dns.write_dns_files() + self.pxegen.make_pxe_menu() # run post-triggers utils.run_triggers(None, "/var/lib/cobbler/triggers/sync/post/*") return True - def copy_bootloaders(self): - """ - Copy bootloaders to the configured tftpboot directory - NOTE: we support different arch's if defined in - /var/lib/cobbler/settings. - """ - for loader in self.settings.bootloaders.keys(): - path = self.settings.bootloaders[loader] - newname = os.path.basename(path) - destpath = os.path.join(self.settings.tftpboot, newname) - self.copyfile(path, destpath) - self.copyfile("/var/lib/cobbler/menu.c32", os.path.join(self.settings.tftpboot, "menu.c32")) - - def write_dhcp_file(self): - """ - DHCP files are written when manage_dhcp is set in - /var/lib/cobbler/settings. - """ - - settings_file = self.settings.dhcpd_conf - template_file = "/etc/cobbler/dhcp.template" - mode = self.settings.manage_dhcp_mode.lower() - if mode == "dnsmasq": - settings_file = self.settings.dnsmasq_conf - template_file = "/etc/cobbler/dnsmasq.template" - - try: - f2 = open(template_file,"r") - except: - raise CX(_("error writing template to file: %s") % template_file) - template_data = "" - template_data = f2.read() - f2.close() - - # build each per-system definition - # as configured, this only works for ISC, patches accepted - # from those that care about Itanium. elilo seems to be unmaintained - # so additional maintaince in other areas may be required to keep - # this working. - - elilo = os.path.basename(self.settings.bootloaders["ia64"]) - - system_definitions = {} - counter = 0 - - # we used to just loop through each system, but now we must loop - # through each network interface of each system. - - for system in self.systems: - profile = system.get_conceptual_parent() - distro = profile.get_conceptual_parent() - for (name, interface) in system.interfaces.iteritems(): - - mac = interface["mac_address"] - ip = interface["ip_address"] - host = interface["hostname"] - - if mac is None or mac == "": - # can't write a DHCP entry for this system - continue - - counter = counter + 1 - systxt = "" - - if mode == "isc": - - # the label the entry after the hostname if possible - if host is not None and host != "": - systxt = "\nhost %s {\n" % host - else: - systxt = "\nhost generic%d {\n" % counter - - if distro.arch == "ia64": - # can't use pxelinux.0 anymore - systxt = systxt + " filename \"/%s\";\n" % elilo - systxt = systxt + " hardware ethernet %s;\n" % mac - if ip is not None and ip != "": - systxt = systxt + " fixed-address %s;\n" % ip - systxt = systxt + "}\n" - - else: - # dnsmasq. don't have to write IP and other info here, but we do tag - # each MAC based on the arch of it's distro, if it needs something other - # than pxelinux.0 -- for these arches, and these arches only, a dnsmasq - # reload (full "cobbler sync") would be required after adding the system - # to cobbler, just to tag this relationship. - - if ip is not None and ip != "": - if distro.arch.lower() == "ia64": - systxt = "dhcp-host=net:ia64," + ip + "\n" - # support for other arches needs modifications here - else: - systxt = "" - - dhcp_tag = interface["dhcp_tag"] - if dhcp_tag == "": - dhcp_tag = "default" - - if not system_definitions.has_key(dhcp_tag): - system_definitions[dhcp_tag] = "" - system_definitions[dhcp_tag] = system_definitions[dhcp_tag] + systxt - - # we are now done with the looping through each interface of each system - - metadata = { - "insert_cobbler_system_definitions" : system_definitions.get("default",""), - "date" : time.asctime(time.gmtime()), - "cobbler_server" : self.settings.server, - "next_server" : self.settings.next_server, - "elilo" : elilo - } - - # now add in other DHCP expansions that are not tagged with "default" - for x in system_definitions.keys(): - if x == "default": - continue - metadata["insert_cobbler_system_definitions_%s" % x] = system_definitions[x] - - self.apply_template(template_data, metadata, settings_file) - - def regen_ethers(self): - # dnsmasq knows how to read this database of MACs -> IPs, so we'll keep it up to date - # every time we add a system. - # read 'man ethers' for format info - fh = open("/etc/ethers","w+") - for sys in self.systems: - for (name, interface) in sys.interfaces.iteritems(): - mac = interface["mac_address"] - ip = interface["ip_address"] - if mac is None or mac == "": - # can't write this w/o a MAC address - continue - if ip is not None and ip != "": - fh.write(mac.upper() + "\t" + ip + "\n") - fh.close() - - def regen_hosts(self): - # dnsmasq knows how to read this database for host info - # (other things may also make use of this later) - fh = open("/var/lib/cobbler/cobbler_hosts","w+") - for sys in self.systems: - for (name, interface) in sys.interfaces.iteritems(): - mac = interface["mac_address"] - host = interface["hostname"] - ip = interface["ip_address"] - if mac is None or mac == "": - continue - if host is not None and host != "" and ip is not None and ip != "": - fh.write(ip + "\t" + host + "\n") - fh.close() - - - #def templatify(self, data, metadata, outfile): - # for x in metadata.keys(): - # template_data = template_data.replace("$%s" % x, metadata[x]) - def clean_trees(self): """ Delete any previously built pxelinux.cfg tree and virt tree info and then create @@ -267,770 +121,15 @@ class BootSync: path = os.path.join(self.settings.webdir,x) if os.path.isfile(path): if not x.endswith(".py"): - self.rmfile(path) + utils.rmfile(path) if os.path.isdir(path): - if not x in ["web", "webui", "localmirror","repo_mirror","ks_mirror","kickstarts","kickstarts_sys","distros","images","systems","profiles","links","repo_profile","repo_system"] : + if not x in ["web", "webui", "localmirror","repo_mirror","ks_mirror","images","links","repo_profile","repo_system","svc"] : # delete directories that shouldn't exist - self.rmtree(path) + utils.rmtree(path) if x in ["kickstarts","kickstarts_sys","images","systems","distros","profiles","repo_profile","repo_system"]: # clean out directory contents - self.rmtree_contents(path) - self.rmtree_contents(os.path.join(self.settings.tftpboot, "pxelinux.cfg")) - self.rmtree_contents(os.path.join(self.settings.tftpboot, "images")) - - def copy_distros(self): - """ - A distro is a kernel and an initrd. Copy all of them and error - out if any files are missing. The conf file was correct if built - via the CLI or API, though it's possible files have been moved - since or perhaps they reference NFS directories that are no longer - mounted. - - NOTE: this has to be done for both tftp and http methods - """ - # copy is a 4-letter word but tftpboot runs chroot, thus it's required. - for d in self.distros: - print _("sync distro: %s") % d.name - self.copy_single_distro_files(d) - - def copy_single_distro_files(self, d): - for dirtree in [self.settings.tftpboot, self.settings.webdir]: - distros = os.path.join(dirtree, "images") - distro_dir = os.path.join(distros,d.name) - self.mkdir(distro_dir) - kernel = utils.find_kernel(d.kernel) # full path - initrd = utils.find_initrd(d.initrd) # full path - if kernel is None or not os.path.isfile(kernel): - raise CX(_("kernel not found: %(file)s, distro: %(distro)s") % { "file" : d.kernel, "distro" : d.name }) - if initrd is None or not os.path.isfile(initrd): - raise CX(_("initrd not found: %(file)s, distro: %(distro)s") % { "file" : d.initrd, "distro" : d.name }) - b_kernel = os.path.basename(kernel) - b_initrd = os.path.basename(initrd) - if kernel.startswith(dirtree): - self.linkfile(kernel, os.path.join(distro_dir, b_kernel)) - else: - self.copyfile(kernel, os.path.join(distro_dir, b_kernel)) - if initrd.startswith(dirtree): - self.linkfile(initrd, os.path.join(distro_dir, b_initrd)) - else: - self.copyfile(initrd, os.path.join(distro_dir, b_initrd)) - - def validate_kickstarts(self): - """ - Similar to what we do for distros, ensure all the kickstarts - in conf file are valid. kickstarts are referenced by URL - (http or ftp), can stay as is. kickstarts referenced by absolute - path (i.e. are files path) will be mirrored over http. - """ - - self.validate_kickstarts_per_profile() - self.validate_kickstarts_per_system() - return True - - def validate_kickstarts_per_profile(self): - """ - Koan provisioning (Virt + auto-ks) needs kickstarts - per profile. Validate them as needed. Local kickstarts - get template substitution. Since http:// kickstarts might - get generated via magic URLs, those are *not* substituted. - NFS kickstarts are also not substituted when referenced - by NFS URL's as we don't copy those files over to the cobbler - directories. They are supposed to be live such that an - admin can update those without needing to run 'sync' again. - - NOTE: kickstart only uses the web directory (if it uses them at all) - """ - - for g in self.profiles: - print _("sync profile: %s") % g.name - self.validate_kickstart_for_specific_profile(g) - - def validate_kickstart_for_specific_profile(self,g): - distro = g.get_conceptual_parent() - meta = utils.blender(self.api, False, g, self.blend_cache) - if distro is None: - raise CX(_("profile %(profile)s references missing distro %(distro)s") % { "profile" : g.name, "distro" : g.distro }) - kickstart_path = utils.find_kickstart(meta["kickstart"]) - if kickstart_path is not None and os.path.exists(kickstart_path): - # the input is an *actual* file, hence we have to copy it - copy_path = os.path.join( - self.settings.webdir, - "kickstarts", # profile kickstarts go here - g.name - ) - self.mkdir(copy_path) - dest = os.path.join(copy_path, "ks.cfg") - try: - meta = utils.blender(self.api, False, g, self.blend_cache) - ksmeta = meta["ks_meta"] - del meta["ks_meta"] - meta.update(ksmeta) # make available at top level - meta["yum_repo_stanza"] = self.generate_repo_stanza(g,True) - meta["yum_config_stanza"] = self.generate_config_stanza(g,True) - meta["kickstart_done"] = self.generate_kickstart_signal(g, None) - meta["kernel_options"] = utils.hash_to_string(meta["kernel_options"]) - kfile = open(kickstart_path) - self.apply_template(kfile, meta, dest) - kfile.close() - except: - traceback.print_exc() # leave this in, for now... - msg = "err_kickstart2" - raise CX(_("Error while rendering kickstart file %(src)s to %(dest)s") % { "src" : kickstart_path, "dest" : dest }) - - def generate_kickstart_signal(self, profile, system=None): - """ - Do things that we do at the end of kickstarts... - * signal the status watcher we're done - * disable PXE if needed - * save the original kickstart file for debug - """ - - # FIXME: watcher is more of a request than a packaged file - # we should eventually package something and let it do something important" - pattern1 = "wget \"http://%s/cgi-bin/cobbler/nopxe.cgi?system=%s\"" - pattern2 = "wget \"http://%s/cobbler/%s/%s/ks.cfg\" -O /root/cobbler.ks" - pattern3 = "wget \"http://%s/cgi-bin/cobbler/post_install_trigger.cgi?system=%s\"" - - blend_this = profile - if system: - blend_this = system - - blended = utils.blender(self.api, False, blend_this, self.blend_cache) - kickstart = blended.get("kickstart",None) - - buf = "" - if system is not None: - if str(self.settings.pxe_just_once).upper() in [ "1", "Y", "YES", "TRUE" ]: - buf = buf + "\n" + pattern1 % (blended["http_server"], system.name) - if kickstart and os.path.exists(kickstart): - buf = buf + "\n" + pattern2 % (blended["http_server"], "kickstarts_sys", system.name) - if self.settings.run_post_install_trigger: - buf = buf + "\n" + pattern3 % (blended["http_server"], system.name) - - else: - if kickstart and os.path.exists(kickstart): - buf = buf + "\n" + pattern2 % (blended["http_server"], "kickstarts", profile.name) - - return buf - - def get_repo_segname(self, is_profile): - if is_profile: - return "repos_profile" - else: - return "repos_system" - - def generate_repo_stanza(self, obj, is_profile=True): - - """ - Automatically attaches yum repos to profiles/systems in kickstart files - that contain the magic $yum_repo_stanza variable. - """ - - buf = "" - blended = utils.blender(self.api, False, obj, self.blend_cache) - - configs = self.get_repo_filenames(obj,is_profile) - for c in configs: - name = c.split("/")[-1].replace(".repo","") - (is_core, baseurl) = self.analyze_repo_config(c) - buf = buf + "repo --name=%s --baseurl=%s\n" % (name, baseurl) - - return buf - - def analyze_repo_config(self, filename): - fd = open(filename) - data = fd.read() - lines = data.split("\n") - ret = False - baseurl = None - for line in lines: - if line.find("ks_mirror") != -1: - ret = True - if line.find("baseurl") != -1: - first, baseurl = line.split("=") - fd.close() - return (ret, baseurl) - - def get_repo_baseurl(self, server, repo_name, is_repo_mirror=True): - """ - Construct the URL to a repo definition. - """ - if is_repo_mirror: - return "http://%s/cobbler/repo_mirror/%s" % (server, repo_name) - else: - return "http://%s/cobbler/ks_mirror/config/%s" % (server, repo_name) - - def get_repo_filenames(self, obj, is_profile=True): - """ - For a given object, return the paths to repo configuration templates - that will be used to generate per-object repo configuration files and - baseurls - """ - - blended = utils.blender(self.api, False, obj, self.blend_cache) - urlseg = self.get_repo_segname(is_profile) - - topdir = "%s/%s/%s/*.repo" % (self.settings.webdir, urlseg, blended["name"]) - files = glob.glob(topdir) - return files - - - def generate_config_stanza(self, obj, is_profile=True): - - """ - Add in automatic to configure /etc/yum.repos.d on the remote system - if the kickstart file contains the magic $yum_config_stanza. - """ - - if not self.settings.yum_post_install_mirror: - return "" - - urlseg = self.get_repo_segname(is_profile) - - distro = obj.get_conceptual_parent() - if not is_profile: - distro = distro.get_conceptual_parent() - - blended = utils.blender(self.api, False, obj, self.blend_cache) - configs = self.get_repo_filenames(obj, is_profile) - buf = "" - - # for each kickstart template we have rendered ... - for c in configs: - - name = c.split("/")[-1].replace(".repo","") - # add the line to create the yum config file on the target box - conf = self.get_repo_config_file(blended["http_server"],urlseg,blended["name"],name) - buf = buf + "wget \"%s\" --output-document=/etc/yum.repos.d/%s.repo\n" % (conf, name) - - return buf - - def get_repo_config_file(self,server,urlseg,obj_name,repo_name): - """ - Construct the URL to a repo config file that is usable in kickstart - for use with yum. This is different than the templates cobbler reposync - creates, as this file will allow the server to migrate and have different - variables for different subnets/profiles/etc. - """ - return "http://%s/cblr/%s/%s/%s.repo" % (server,urlseg,obj_name,repo_name) - - def validate_kickstarts_per_system(self): - """ - PXE provisioning needs kickstarts evaluated per system. - Profiles would normally be sufficient, but not in cases - such as static IP, where we want to be able to do templating - on a system basis. - - NOTE: kickstart only uses the web directory (if it uses them at all) - """ - - for s in self.systems: - print _("sync system: %s") % s.name - self.validate_kickstart_for_specific_system(s) - - def validate_kickstart_for_specific_system(self,s): - profile = s.get_conceptual_parent() - if profile is None: - raise CX(_("system %(system)s references missing profile %(profile)s") % { "system" : s.name, "profile" : s.profile }) - distro = profile.get_conceptual_parent() - meta = utils.blender(self.api, False, s, self.blend_cache) - kickstart_path = utils.find_kickstart(meta["kickstart"]) - if kickstart_path and os.path.exists(kickstart_path): - copy_path = os.path.join(self.settings.webdir, - "kickstarts_sys", # system kickstarts go here - s.name - ) - self.mkdir(copy_path) - dest = os.path.join(copy_path, "ks.cfg") - try: - ksmeta = meta["ks_meta"] - del meta["ks_meta"] - meta.update(ksmeta) # make available at top level - meta["yum_repo_stanza"] = self.generate_repo_stanza(s, False) - meta["yum_config_stanza"] = self.generate_config_stanza(s, False) - meta["kickstart_done"] = self.generate_kickstart_signal(profile, s) - meta["kernel_options"] = utils.hash_to_string(meta["kernel_options"]) - kfile = open(kickstart_path) - self.apply_template(kfile, meta, dest) - kfile.close() - except: - traceback.print_exc() - raise CX(_("Error templating file %(src)s to %(dest)s") % { "src" : meta["kickstart"], "dest" : dest }) - - def load_snippet_cache(self): - - # first load all of the files in /var/lib/cobbler/snippets and load them, for use - # in adding long bits to kickstart templates without having to have them hard coded - # inside the sync code. - - snippet_cache = {} - snippets = glob.glob("%s/*" % self.settings.snippetsdir) - for snip in snippets: - if os.path.isdir(snip): - continue - snip_file = open(snip) - data = snip_file.read() - snip_file.close() - snippet_cache[os.path.basename(snip)] = data - self.snippet_cache = snippet_cache - - - def apply_template(self, data_input, metadata, out_path): - """ - Take filesystem file kickstart_input, apply metadata using - Cheetah and save as out_path. - """ - - if type(data_input) != str: - data = data_input.read() - else: - data = data_input - - # backward support for Cobbler's legacy (and slightly more readable) - # template syntax. - data = data.replace("TEMPLATE::","$") - - # replace contents of the data stream with items from the snippet cache - # do not use Cheetah yet, Cheetah can't really be run twice on the same - # stream and be expected to do the right thing - newdata = "" - for line in data.split("\n"): - for x in self.snippet_cache: - if not line.startswith("#"): - line = line.replace("SNIPPET::%s" % x, self.snippet_cache[x]) - newdata = "\n".join((newdata, line)) - data = newdata - - # HACK: the ksmeta field may contain nfs://server:/mount in which - # case this is likely WRONG for kickstart, which needs the NFS - # directive instead. Do this to make the templates work. - newdata = "" - if metadata.has_key("tree") and metadata["tree"].startswith("nfs://"): - for line in data.split("\n"): - if line.find("--url") != -1 and line.find("url ") != -1: - rest = metadata["tree"][6:] # strip off "nfs://" part - try: - (server, dir) = rest.split(":",2) - except: - raise CX(_("Invalid syntax for NFS path given during import: %s" % metadata["tree"])) - line = "nfs --server %s --dir %s" % (server,dir) - # but put the URL part back in so koan can still see - # what the original value was - line = line + "\n" + "#url --url=%s" % metadata["tree"] - newdata = newdata + line + "\n" - data = newdata - - # tell Cheetah not to blow up if it can't find a symbol for something - data = "#errorCatcher Echo\n" + data - - # now do full templating scan, where we will also templatify the snippet insertions - t = Template(source=data, searchList=[metadata]) - try: - data_out = str(t) - except: - print _("There appears to be an formatting error in the template file.") - print _("For completeness, the traceback from Cheetah has been included below.") - raise - - # now apply some magic post-filtering that is used by cobbler import and some - # other places, but doesn't use Cheetah. Forcing folks to double escape - # things would be very unwelcome. - - for x in metadata: - if type(metadata[x]) == str: - data_out = data_out.replace("@@%s@@" % x, metadata[x]) - - # remove leading newlines which apparently breaks AutoYAST ? - if data_out.startswith("\n"): - data_out = data_out.strip() - - if out_path is not None: - self.mkdir(os.path.dirname(out_path)) - fd = open(out_path, "w+") - fd.write(data_out) - fd.close() - - return data_out - - def build_trees(self): - """ - Now that kernels and initrds are copied and kickstarts are all valid, - build the pxelinux.cfg tree, which contains a directory for each - configured IP or MAC address. Also build a tree for Virt info. - - NOTE: some info needs to go in TFTP and HTTP directories, but not all. - Usually it's just one or the other. - - """ - - self.write_listings() - - # create pxelinux.cfg under tftpboot - # and file for each MAC or IP (hex encoded 01-XX-XX-XX-XX-XX-XX) - - for d in self.distros: - self.write_distro_file(d) - - for p in self.profiles: - self.write_profile_file(p) - - for system in self.systems: - self.write_all_system_files(system) - - def retemplate_all_yum_repos(self): - for p in self.profiles: - self.retemplate_yum_repos(p,True) - for system in self.systems: - self.retemplate_yum_repos(system,False) - - def retemplate_yum_repos(self,obj,is_profile): - # FIXME: blender could use caching for performance - # FIXME: make stanza generation code load stuff from the right place - """ - Yum repository management files are in self.settings.webdir/repo_mirror/$name/config.repo - and also potentially in listed in the source_repos structure of the distro object, however - these files have server URLs in them that must be templated out. This function does this. - """ - blended = utils.blender(self.api, False, obj, self.blend_cache) - - if is_profile: - outseg = "repos_profile" - else: - outseg = "repos_system" - - input_files = [] - - # chance old versions from upgrade do not have a source_repos - # workaround for user bug - if not blended.has_key("source_repos"): - blended["source_repos"] = [] - - # tack on all the install source repos IF there is more than one. - # this is basically to support things like RHEL5 split trees - # if there is only one, then there is no need to do this. - - for r in blended["source_repos"]: - filename = self.settings.webdir + "/" + "/".join(r[0].split("/")[4:]) - input_files.append(filename) - - for repo in blended["repos"]: - input_files.append(os.path.join(self.settings.webdir, "repo_mirror", repo, "config.repo")) - - for infile in input_files: - if infile.find("ks_mirror") == -1: - dispname = infile.split("/")[-2] - else: - dispname = infile.split("/")[-1].replace(".repo","") - confdir = os.path.join(self.settings.webdir, outseg) - outdir = os.path.join(confdir, blended["name"]) - self.mkdir(outdir) - try: - infile_h = open(infile) - except: - print _("WARNING: cobbler reposync needs to be run on repo (%s), then re-run cobbler sync") % dispname - continue - infile_data = infile_h.read() - infile_h.close() - outfile = os.path.join(outdir, "%s.repo" % (dispname)) - self.apply_template(infile_data, blended, outfile) - - - def write_all_system_files(self,system,just_edit_pxe=False): - - profile = system.get_conceptual_parent() - if profile is None: - raise CX(_("system %(system)s references a missing profile %(profile)s") % { "system" : system.name, "profile" : system.profile}) - distro = profile.get_conceptual_parent() - if distro is None: - raise CX(_("profile %(profile)s references a missing distro %(distro)s") % { "profile" : system.profile, "distro" : profile.distro}) - - # this used to just generate a single PXE config file, but now must - # generate one record for each described NIC ... - - counter = 0 - for (name,interface) in system.interfaces.iteritems(): - - ip = interface["ip_address"] - - f1 = utils.get_config_filename(system,interface=name) - - # for tftp only ... - if distro.arch in [ "x86", "x86_64", "standard"]: - # pxelinux wants a file named $name under pxelinux.cfg - f2 = os.path.join(self.settings.tftpboot, "pxelinux.cfg", f1) - if distro.arch == "ia64": - # elilo expects files to be named "$name.conf" in the root - # and can not do files based on the MAC address - if ip is not None and ip != "": - print _("Warning: Itanium system object (%s) needs an IP address to PXE") % system.name - - filename = "%s.conf" % utils.get_config_filename(system,interface=name) - f2 = os.path.join(self.settings.tftpboot, filename) - - f3 = os.path.join(self.settings.webdir, "systems", f1) - - if system.netboot_enabled and system.is_pxe_supported(): - if distro.arch in [ "x86", "x86_64", "standard"]: - self.write_pxe_file(f2,system,profile,distro,False) - if distro.arch == "ia64": - self.write_pxe_file(f2,system,profile,distro,True) - else: - # ensure the file doesn't exist - self.rmfile(f2) - - if not just_edit_pxe: - # allows netboot-disable to be highly performant - # by not invoking the Cheetah engine - self.write_system_file(f3,system) - - counter = counter + 1 - - - def make_pxe_menu(self): - # only do this if there is NOT a system named default. - default = self.systems.find(name="default") - if default is not None: - return - - fname = os.path.join(self.settings.tftpboot, "pxelinux.cfg", "default") - - # read the default template file - template_src = open("/etc/cobbler/pxedefault.template") - template_data = template_src.read() - - # 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) - - # build out the menu entries - pxe_menu_items = "" - for profile in profile_list: - distro = profile.get_conceptual_parent() - contents = self.write_pxe_file(None,None,profile,distro,False,include_header=False) - if contents is not None: - pxe_menu_items = pxe_menu_items + contents + "\n" - - # save the template. - metadata = { "pxe_menu_items" : pxe_menu_items } - outfile = os.path.join(self.settings.tftpboot, "pxelinux.cfg", "default") - self.apply_template(template_data, metadata, outfile) - template_src.close() - - - def write_pxe_file(self,filename,system,profile,distro,is_ia64, include_header=True): - """ - Write a configuration file for the boot loader(s). - More system-specific configuration may come in later, if so - that would appear inside the system object in api.py - - NOTE: relevant to tftp only - """ - - # --- - # system might have netboot_enabled set to False (see item_system.py), if so, - # don't do anything else and flag the error condition. - if system is not None and not system.netboot_enabled: - return None - - # --- - # just some random variables - template = None - metadata = {} - buffer = "" - - # --- - # find kernel and initrd - kernel_path = os.path.join("/images",distro.name,os.path.basename(distro.kernel)) - initrd_path = os.path.join("/images",distro.name,os.path.basename(distro.initrd)) - - # Find the kickstart if we inherit from another profile - kickstart_path = utils.blender(self.api, True, profile, self.blend_cache)["kickstart"] - - # --- - # choose a template - if system is None: - template = "/etc/cobbler/pxeprofile.template" - elif not is_ia64: - template = "/etc/cobbler/pxesystem.template" - else: - template = "/etc/cobbler/pxesystem_ia64.template" - - # now build the kernel command line - if system is not None: - blended = utils.blender(self.api, True,system,self.blend_cache) - else: - blended = utils.blender(self.api, True,profile,self.blend_cache) - kopts = blended["kernel_options"] - - # --- - # generate the append line - append_line = "append %s" % utils.hash_to_string(kopts) - if not is_ia64: - append_line = "%s initrd=%s" % (append_line, initrd_path) - 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 != "": - - if system is not None and kickstart_path.startswith("/"): - kickstart_path = "http://%s/cblr/kickstarts_sys/%s/ks.cfg" % (blended["http_server"], system.name) - elif kickstart_path.startswith("/") or kickstart_path.find("/cobbler/kickstarts/") != -1: - kickstart_path = "http://%s/cblr/kickstarts/%s/ks.cfg" % (blended["http_server"], profile.name) - - if distro.breed is None or distro.breed == "redhat": - append_line = "%s ks=%s" % (append_line, kickstart_path) - elif distro.breed == "suse": - append_line = "%s autoyast=%s" % (append_line, kickstart_path) - elif distro.breed == "debian": - append_line = "%s auto=true url=%s" % (append_line, kickstart_path) - append_line = append_line.replace("ksdevice","interface") - - # --- - # store variables for templating - metadata["menu_label"] = "" - if not is_ia64 and system is None: - metadata["menu_label"] = "MENU LABEL %s" % profile.name - metadata["profile_name"] = profile.name - metadata["kernel_path"] = kernel_path - metadata["initrd_path"] = initrd_path - metadata["append_line"] = append_line - - # --- - # get the template - template_fh = open(template) - template_data = template_fh.read() - template_fh.close() - - # --- - # save file and/or return results, depending on how called. - buffer = self.apply_template(template_data, metadata, None) - if filename is not None: - fd = open(filename, "w") - fd.write(buffer) - fd.close() - return buffer - - - def write_listings(self): - """ - Creates a very simple index of available systems and profiles - that cobbler knows about. Just the names, no details. - """ - names1 = [x.name for x in self.profiles] - names2 = [x.name for x in self.systems] - data1 = yaml.dump(names1) - data2 = yaml.dump(names2) - fd1 = open(os.path.join(self.settings.webdir, "profile_list"), "w+") - fd2 = open(os.path.join(self.settings.webdir, "system_list"), "w+") - fd1.write(data1) - fd2.write(data2) - fd1.close() - fd2.close() - - def write_distro_file(self,distro): - """ - Create distro information for koan install - """ - blended = utils.blender(self.api, True, distro, self.blend_cache) - filename = os.path.join(self.settings.webdir,"distros",distro.name) - fd = open(filename, "w+") - fd.write(yaml.dump(blended)) - fd.close() - - def write_profile_file(self,profile): - """ - Create profile information for virt install - - NOTE: relevant to http only - """ - - blended = utils.blender(self.api, True, profile, self.blend_cache) - filename = os.path.join(self.settings.webdir,"profiles",profile.name) - fd = open(filename, "w+") - if blended.has_key("kickstart") and blended["kickstart"].startswith("/"): - # write the file location as needed by koan - blended["kickstart"] = "http://%s/cblr/kickstarts/%s/ks.cfg" % (blended["http_server"], profile.name) - fd.write(yaml.dump(blended)) - fd.close() - - def write_system_file(self,filename,system): - """ - Create system information for virt install - - NOTE: relevant to http only - """ - - blended = utils.blender(self.api, True, system, self.blend_cache) - filename = os.path.join(self.settings.webdir,"systems",system.name) - fd = open(filename, "w+") - fd.write(yaml.dump(blended)) - fd.close() - - def linkfile(self, src, dst): - """ - Attempt to create a link dst that points to src. Because file - systems suck we attempt several different methods or bail to - self.copyfile() - """ - - try: - return os.link(src, dst) - except (IOError, OSError): - pass - - try: - return os.symlink(src, dst) - except (IOError, OSError): - pass - - return self.copyfile(src, dst) - - def copyfile(self,src,dst): - try: - return shutil.copyfile(src,dst) - except: - if not os.path.samefile(src,dst): - # accomodate for the possibility that we already copied - # the file as a symlink/hardlink - raise CX(_("Error copying %(src)s to %(dst)s") % { "src" : src, "dst" : dst}) - - def rmfile(self,path): - try: - os.unlink(path) - return True - except OSError, ioe: - if not ioe.errno == errno.ENOENT: # doesn't exist - traceback.print_exc() - raise CX(_("Error deleting %s") % path) - return True - - def rmtree_contents(self,path): - what_to_delete = glob.glob("%s/*" % path) - for x in what_to_delete: - self.rmtree(x) - - def rmtree(self,path): - try: - if os.path.isfile(path): - return self.rmfile(path) - else: - return shutil.rmtree(path,ignore_errors=True) - except OSError, ioe: - traceback.print_exc() - if not ioe.errno == errno.ENOENT: # doesn't exist - raise CX(_("Error deleting %s") % path) - return True + utils.rmtree_contents(path) + utils.rmtree_contents(os.path.join(self.bootloc, "pxelinux.cfg")) + utils.rmtree_contents(os.path.join(self.bootloc, "images")) - def mkdir(self,path,mode=0777): - try: - return os.makedirs(path,mode) - except OSError, oe: - if not oe.errno == 17: # already exists (no constant for 17?) - traceback.print_exc() - print oe.errno - raise CX(_("Error creating") % path) diff --git a/cobbler/action_validate.py b/cobbler/action_validate.py index f898478..44f3a9a 100644 --- a/cobbler/action_validate.py +++ b/cobbler/action_validate.py @@ -15,7 +15,8 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import os import re import sub_process -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ +import utils class Validate: @@ -39,16 +40,10 @@ class Validate: failed = False for x in self.config.profiles(): - distro = x.get_conceptual_parent() - if distro.breed != "redhat": - continue - if not self.checkfile(x.name, "%s/kickstarts/%s/ks.cfg" % (self.settings.webdir, x.name)): + if not self.checkfile(x, True): failed = True for x in self.config.systems(): - distro = x.get_conceptual_parent().get_conceptual_parent() - if distro.breed != "redhat": - continue - if not self.checkfile(x.name, "%s/kickstarts_sys/%s/ks.cfg" % (self.settings.webdir, x.name)): + if not self.checkfile(x, False): failed = True if failed: @@ -58,15 +53,32 @@ class Validate: return failed - def checkfile(self,name,file): - # print _("scanning rendered kickstart template: %s" % file) - if not os.path.exists(file): - print _("kickstart file does not exist for: %s") % name - return False - rc = os.system("/usr/bin/ksvalidator %s" % file) - if not rc == 0: - print _("ksvalidator detected a possible problem for: %s") % name - print _(" rendered kickstart template at: %s" % file) + def checkfile(self,obj,is_profile): + blended = utils.blender(self.config.api, False, obj) + ks = blended["kickstart"] + breed = blended["breed"] + if breed != "redhat": + print "%s has a breed of %s, skipping" % (obj.name, breed) + return True + if ks is None or ks == "": + print "%s has no kickstart, skipping" % obj.name + return True + + server = blended["server"] + if not ks.startswith("/"): + url = self.kickstart + elif is_profile: + url = "http://%s/cblr/svc/?op=ks;profile=%s" % (server,obj.name) + else: + url = "http://%s/cblr/svc/?op=ks;system=%s" % (server,obj.name) + + print "----------------------------" + print "checking url: %s" % url + + + rc = os.system("/usr/bin/ksvalidator \"%s\"" % url) + if rc != 0: return False + return True diff --git a/cobbler/api.py b/cobbler/api.py index 13fc5f3..9f4a636 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -22,14 +22,17 @@ import action_import import action_reposync import action_status import action_validate +import action_buildiso +import action_replicate from cexceptions import * import sub_process import module_loader +import kickgen import logging import os import fcntl -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ ERROR = 100 INFO = 10 @@ -38,7 +41,7 @@ DEBUG = 5 # notes on locking: # BootAPI is a singleton object # the XMLRPC variants allow 1 simultaneous request -# therefore we flock on /var/lib/cobbler/settings for now +# therefore we flock on /etc/cobbler/settings for now # on a request by request basis. class BootAPI: @@ -79,20 +82,11 @@ class BootAPI: "module", "authz_allowall" ) + self.kickgen = kickgen.KickGen(self._config) self.logger.debug("API handle initialized") def __setup_logger(self,name): - logger = logging.getLogger(name) - logger.setLevel(logging.INFO) - try: - ch = logging.FileHandler("/var/log/cobbler/cobbler.log") - except: - raise CX(_("No write permissions on log file. Are you root?")) - ch.setLevel(logging.INFO) - formatter = logging.Formatter("%(asctime)s - %(name)s - %(message)s") - ch.setFormatter(formatter) - logger.addHandler(ch) - return logger + return utils.setup_logger(name) def log(self,msg,args=None,debug=False): if debug: @@ -221,21 +215,21 @@ class BootAPI: self.log("new_repo",[is_subobject]) return self._config.new_repo(is_subobject=is_subobject) - def add_distro(self, ref): + def add_distro(self, ref, check_for_duplicate_names=False): self.log("add_distro",[ref.name]) - return self._config.distros().add(ref,save=True) + return self._config.distros().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) - def add_profile(self, ref): + def add_profile(self, ref, check_for_duplicate_names=False): self.log("add_profile",[ref.name]) - return self._config.profiles().add(ref,save=True) + return self._config.profiles().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) - def add_system(self,ref): + def add_system(self, ref, check_for_duplicate_names=False, check_for_duplicate_netinfo=False): self.log("add_system",[ref.name]) - return self._config.systems().add(ref,save=True) + return self._config.systems().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names,check_for_duplicate_netinfo=check_for_duplicate_netinfo) - def add_repo(self,ref): + def add_repo(self, ref, check_for_duplicate_names=False): self.log("add_repo",[ref.name]) - return self._config.repos().add(ref,save=True) + return self._config.repos().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names) def find_distro(self, name=None, return_list=False, **kargs): return self._config.distros().find(name=name, return_list=return_list, **kargs) @@ -249,6 +243,9 @@ class BootAPI: def find_repo(self, name=None, return_list=False, **kargs): return self._config.repos().find(name=name, return_list=return_list, **kargs) + def dump_vars(self, obj, format=False): + return obj.dump_vars(format) + def auto_add_repos(self): """ Import any repos this server knows about and mirror them. @@ -284,7 +281,14 @@ class BootAPI: # run cobbler reposync to apply changes return True - + + def generate_kickstart(self,profile,system): + self.log("generate_kickstart") + if system: + return self.kickgen.generate_kickstart_for_system(system) + else: + return self.kickgen.generate_kickstart_for_profile(profile) + def check(self): """ See if all preqs for network booting are valid. This returns @@ -319,9 +323,22 @@ class BootAPI: saved with serialize() will NOT be synchronized with this command. """ self.log("sync") - sync = action_sync.BootSync(self._config) + sync = self.get_sync() return sync.run() + def get_sync(self): + self.dhcp = self.get_module_from_file( + "dhcp", + "module", + "manage_isc" + ).get_manager(self._config) + self.dns = self.get_module_from_file( + "dns", + "module", + "manage_bind" + ).get_manager(self._config) + return action_sync.BootSync(self._config,dhcp=self.dhcp,dns=self.dns) + def reposync(self, name=None): """ Take the contents of /var/lib/cobbler/repos and update them -- @@ -332,11 +349,11 @@ class BootAPI: return reposync.run(name) def status(self,mode): - self.log("status",[mode]) - statusifier = action_status.BootStatusReport(self._config, mode) + self.log("status") + statusifier = action_status.BootStatusReport(self._config,mode) return statusifier.run() - def import_tree(self,mirror_url,mirror_name,network_root=None,kickstart_file=None,rsync_flags=None): + def import_tree(self,mirror_url,mirror_name,network_root=None,kickstart_file=None,rsync_flags=None,arch=None): """ Automatically import a directory tree full of distribution files. mirror_url can be a string that represents a path, a user@host @@ -346,7 +363,7 @@ class BootAPI: """ self.log("import_tree",[mirror_url, mirror_name, network_root, kickstart_file, rsync_flags]) importer = action_import.Importer( - self, self._config, mirror_url, mirror_name, network_root, kickstart_file, rsync_flags + self, self._config, mirror_url, mirror_name, network_root, kickstart_file, rsync_flags, arch ) return importer.run() @@ -405,3 +422,16 @@ class BootAPI: self.log("authorize",[user,resource,arg1,arg2,rc],debug=True) return rc + def build_iso(self,iso=None,profiles=None,tempdir=None): + builder = action_buildiso.BuildIso(self._config) + return builder.run( + iso=iso, profiles=profiles, tempdir=tempdir + ) + + def replicate(self, cobbler_master = None): + replicator = action_replicate.Replicate(self._config) + return replicator.run(cobbler_master = cobbler_master) + + def get_kickstart_templates(self): + return utils.get_kickstar_templates(self) + diff --git a/cobbler/cexceptions.py b/cobbler/cexceptions.py index a78f7f2..dba2857 100644 --- a/cobbler/cexceptions.py +++ b/cobbler/cexceptions.py @@ -20,6 +20,9 @@ class CobblerException(exceptions.Exception): def __init__(self, value, *args): self.value = value % args + # this is a hack to work around some odd exception handling + # in older pythons + self.from_cobbler = 1 def __str__(self): return repr(self.value) diff --git a/cobbler/cobbler.py b/cobbler/cobbler.py index c02302e..2aed672 100755 --- a/cobbler/cobbler.py +++ b/cobbler/cobbler.py @@ -20,10 +20,12 @@ import os import os.path import traceback import optparse +import string import commands +import cexceptions from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ I18N_DOMAIN = "cobbler" #################################################### @@ -31,7 +33,6 @@ I18N_DOMAIN = "cobbler" class BootCLI: def __init__(self): - textdomain(I18N_DOMAIN) self.api = api.BootAPI() self.loader = commands.FunctionLoader() climods = self.api.get_modules_in_category("cli") @@ -44,26 +45,36 @@ class BootCLI: #################################################### +def run_upgrade_checks(): + """ + Cobbler tries to make manual upgrade steps unneeded, though + this function serves to inform users of manual steps when they /are/ + needed. + """ + # for users running pre-1.0 upgrading to 1.0 + if os.path.exists("/var/lib/cobbler/settings"): + raise CX(_("/var/lib/cobbler/settings is no longer in use, remove this file to acknowledge you have migrated your configuration to /etc/cobbler/settings. Do not simply copy the file over or you will lose new configuration entries. Run 'cobbler check' and then 'cobbler sync' after making changes.")) + def main(): """ CLI entry point """ exitcode = 0 try: - # FIXME: redo locking code? + run_upgrade_checks() return BootCLI().run(sys.argv) - except CX, exc: - print str(exc)[1:-1] # remove framing air quotes - except SystemExit: - pass # probably exited from optparse, nothing extra to print - except Exception, exc2: - if str(type(exc2)).find("CX") == -1: - traceback.print_exc() - else: - print str(exc2)[1:-1] # remove framing air quotes + except SystemExit, ex: + return 1 + except Exception, exc: + (t, v, tb) = sys.exc_info() + try: + getattr(exc, "from_cobbler") + print str(exc)[1:-1] + except: + print t + print v + print string.join(traceback.format_list(traceback.extract_tb(tb))) return 1 - return 1 - if __name__ == "__main__": sys.exit(main()) diff --git a/cobbler/cobblerd.py b/cobbler/cobblerd.py index 3c06723..065e99e 100644 --- a/cobbler/cobblerd.py +++ b/cobbler/cobblerd.py @@ -16,8 +16,9 @@ import time import os import SimpleXMLRPCServer import glob -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import xmlrpclib +import binascii from server import xmlrpclib2 import api as cobbler_api @@ -40,6 +41,8 @@ def core(logger=None): 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) @@ -47,6 +50,21 @@ def core(logger=None): # part two: syslog, or syslog+avahi if avahi is installed do_other_tasks(bootapi, settings, syslog_port, logger) +def regen_ss_file(): + # this is only used for Kerberos auth at the moment. + # it identifies XMLRPC requests from Apache that have already + # been cleared by Kerberos. + + fd = open("/dev/urandom") + data = fd.read(512) + fd.close() + fd = open("/var/lib/cobbler/web.ss","w+") + fd.write(binascii.hexlify(data)) + fd.close() + os.system("chmod 700 /var/lib/cobbler/web.ss") + 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() @@ -108,7 +126,7 @@ 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,True) + xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerXMLRPCInterface,False) server = remote.CobblerXMLRPCServer(('', port)) server.logRequests = 0 # don't print stuff @@ -124,7 +142,7 @@ def do_xmlrpc(bootapi, settings, port, logger): def do_xmlrpc_rw(bootapi,settings,port,logger): - xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerReadWriteXMLRPCInterface,False) + 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) @@ -195,11 +213,14 @@ if __name__ == "__main__": #main() - bootapi = cobbler_api.BootAPI() - settings = bootapi.settings() - syslog_port = settings.syslog_port - xmlrpc_port = settings.xmlrpc_port - xmlrpc_port2 = settings.xmlrpc_rw_port - logger = bootapi.logger_remote - do_xmlrpc_unix(bootapi, settings, logger) + #bootapi = cobbler_api.BootAPI() + #settings = bootapi.settings() + #syslog_port = settings.syslog_port + #xmlrpc_port = settings.xmlrpc_port + #xmlrpc_port2 = settings.xmlrpc_rw_port + #logger = bootapi.logger_remote + #do_xmlrpc_unix(bootapi, settings, logger) + + regen_ss_file() + diff --git a/cobbler/collection.py b/cobbler/collection.py index 339a4b2..1b509f2 100644 --- a/cobbler/collection.py +++ b/cobbler/collection.py @@ -25,7 +25,7 @@ import item_profile import item_distro import item_repo -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class Collection(serializable.Serializable): @@ -35,7 +35,8 @@ class Collection(serializable.Serializable): """ self.config = config self.clear() - self.log_func = self.config.api.log + self.api = self.config.api + self.log_func = self.api.log self.lite_sync = None def factory_produce(self,config,seed_data): @@ -125,10 +126,10 @@ class Collection(serializable.Serializable): k.set_parent(newname) else: k.set_distro(newname) - self.config.api.profiles().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers) + self.api.profiles().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers) elif k.COLLECTION_TYPE == "system": k.set_profile(newname) - self.config.api.systems().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers) + self.api.systems().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers) elif k.COLLECTION_TYPE == "repo": raise CX(_("internal error, not expected to have repo child objects")) else: @@ -139,7 +140,7 @@ class Collection(serializable.Serializable): return True - def add(self,ref,save=False,with_copy=False,with_triggers=True,with_sync=True,quick_pxe_update=False): + def add(self,ref,save=False,with_copy=False,with_triggers=True,with_sync=True,quick_pxe_update=False,check_for_duplicate_names=False,check_for_duplicate_netinfo=False): """ Add an object to the collection, if it's valid. Returns True if the object was added to the collection. Returns False if the @@ -167,6 +168,11 @@ class Collection(serializable.Serializable): # if not saving the object, you can't run these features with_triggers = False with_sync = False + + # Avoid adding objects to the collection + # if an object of the same/ip/mac already exists. + self.__duplication_checks(ref,check_for_duplicate_names,check_for_duplicate_netinfo) + if ref is None or not ref.is_valid(): raise CX(_("insufficient or invalid arguments supplied")) @@ -220,6 +226,51 @@ class Collection(serializable.Serializable): def _run_triggers(self,ref,globber): return utils.run_triggers(ref,globber) + def __duplication_checks(self,ref,check_for_duplicate_names,check_for_duplicate_netinfo): + """ + Prevents adding objects with the same name. + Prevents adding or editing to provide the same IP, or MAC. + Enforcement is based on whether the API caller requests it. + """ + + # always protect against duplicate names + if check_for_duplicate_names: + match = None + if isinstance(ref, item_system.System): + match = self.api.find_system(ref.name) + elif isinstance(ref, item_profile.Profile): + match = self.api.find_profile(ref.name) + elif isinstance(ref, item_distro.Distro): + match = self.api.find_distro(ref.name) + elif isinstance(ref, item_repo.Repo): + match = self.api.find_repo(ref.name) + + if match: + raise CX(_("An object already exists with that name. Try 'edit'?")) + + # the duplicate mac/ip checks can be disabled. + if not check_for_duplicate_netinfo: + return + + if isinstance(ref, item_system.System): + for (name, intf) in ref.interfaces.iteritems(): + match_ip = [] + match_mac = [] + input_mac = intf["mac_address"] + input_ip = intf["ip_address"] + if not self.api.settings().allow_duplicate_macs and input_mac is not None and input_mac != "": + match_mac = self.api.find_system(mac_address=input_mac,return_list=True) + if not self.api.settings().allow_duplicate_ips and input_ip is not None and input_ip != "": + match_ip = self.api.find_system(ip_address=input_ip,return_list=True) + # it's ok to conflict with your own net info. + + for x in match_mac: + if x.name != ref.name: + raise CX(_("Can't save system %s. The MAC address (%s) is already used by system %s (%s)") % (ref.name, intf["mac_address"], x.name, name)) + for x in match_ip: + if x.name != ref.name: + raise CX(_("Can't save system %s. The IP address (%s) is already used by system %s (%s)") % (ref.name, intf["ip_address"], x.name, name)) + def printable(self): """ Creates a printable representation of the collection suitable diff --git a/cobbler/collection_distros.py b/cobbler/collection_distros.py index f78dd64..5857b9a 100644 --- a/cobbler/collection_distros.py +++ b/cobbler/collection_distros.py @@ -18,7 +18,7 @@ import collection import item_distro as distro from cexceptions import * import action_litesync -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class Distros(collection.Collection): diff --git a/cobbler/collection_profiles.py b/cobbler/collection_profiles.py index f00a8dc..139c94e 100644 --- a/cobbler/collection_profiles.py +++ b/cobbler/collection_profiles.py @@ -20,7 +20,7 @@ import utils import collection from cexceptions import * import action_litesync -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ #-------------------------------------------- diff --git a/cobbler/collection_repos.py b/cobbler/collection_repos.py index 44bef2a..91e0667 100644 --- a/cobbler/collection_repos.py +++ b/cobbler/collection_repos.py @@ -18,7 +18,7 @@ import item_repo as repo import utils import collection from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ TESTMODE = False diff --git a/cobbler/collection_systems.py b/cobbler/collection_systems.py index 140a981..8a967be 100644 --- a/cobbler/collection_systems.py +++ b/cobbler/collection_systems.py @@ -18,7 +18,7 @@ import utils import collection from cexceptions import * import action_litesync -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ #-------------------------------------------- diff --git a/cobbler/commands.py b/cobbler/commands.py index d97a6e1..705249d 100644 --- a/cobbler/commands.py +++ b/cobbler/commands.py @@ -14,7 +14,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import optparse from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import sys HELP_FORMAT = "%-20s%s" @@ -195,6 +195,14 @@ class CobblerFunction: Boilerplate for objects that offer add/edit/delete/remove/copy functionality. """ + if "dumpvars" in self.args: + if not self.options.name: + raise CX(_("name is required")) + obj = collect_fn().find(self.options.name) + if obj is None: + raise CX(_("object not found")) + return obj + if "remove" in self.args: recursive = False # only applies to distros/profiles and is not supported elsewhere @@ -219,11 +227,12 @@ class CobblerFunction: self.reporting_list_names2(collect_fn(),self.options.name) return None + if not self.options.name: + raise CX(_("name is required")) + if "add" in self.args: obj = new_fn(is_subobject=subobject) else: - if not self.options.name: - raise CX(_("name is required")) if "delete" in self.args: collect_fn().remove(self.options.name, with_delete=True) return None @@ -241,6 +250,14 @@ class CobblerFunction: Boilerplate for objects that offer add/edit/delete/remove/copy functionality. """ + if "dumpvars" in self.args: + print obj.dump_vars(True) + return True + + clobber = False + if "add" in self.args: + clobber = options.clobber + if "copy" in self.args: # or "rename" in self.args: if self.options.newname: obj = obj.make_clone() @@ -251,10 +268,45 @@ class CobblerFunction: opt_sync = not options.nosync opt_triggers = not options.notriggers + # ** WARNING: COMPLICATED ** + # what operation we call depends on what type of object we are editing + # and what the operation is. The details behind this is that the + # add operation has special semantics around adding objects that might + # clobber other objects, and we don't want that to happen. Edit + # does not have to check for named clobbering but still needs + # to check for IP/MAC clobbering in some scenarios (FIXME). + # this is all enforced by collections.py though we need to make + # the apppropriate call to add to invoke the safety code in the right + # places -- and not in places where the safety code will generate + # errors under legit circumstances. + if not ("rename" in self.args): - rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers) + if "add" in self.args: + if obj.COLLECTION_TYPE == "system": + # duplicate names and netinfo are both bad. + if not clobber: + rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=True, check_for_duplicate_netinfo=True) + else: + rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=False, check_for_duplicate_netinfo=True) + else: + # duplicate names are bad + if not clobber: + rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=True, check_for_duplicate_netinfo=False) + else: + rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=False, check_for_duplicate_netinfo=False) + else: + check_dup = False + if not "copy" in self.args: + check_dup = True + # FIXME: this ensures duplicate prevention on copy, but not + # rename? + rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_netinfo=check_dup) + else: + # we are renaming here, so duplicate netinfo checks also + # need to be made.(FIXME) rc = collect_fn().rename(obj, self.options.newname) + return rc def reporting_sorter(self, a, b): diff --git a/cobbler/config.py b/cobbler/config.py index 258b241..4a1b9a4 100644 --- a/cobbler/config.py +++ b/cobbler/config.py @@ -29,7 +29,7 @@ import modules.serializer_yaml as serializer_yaml import settings import serializer -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class Config: diff --git a/cobbler/demo_connect.py b/cobbler/demo_connect.py index 0fa058b..94fa390 100644 --- a/cobbler/demo_connect.py +++ b/cobbler/demo_connect.py @@ -11,12 +11,25 @@ along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. """ -from server.xmlrpcclient import ServerProxy +from xmlrpclib import ServerProxy +import optparse if __name__ == "__main__": - sp = ServerProxy("httpu:///var/lib/cobbler/sock") - print sp.login("<system>","") - + p = optparse.OptionParser() + p.add_option("-u","--user",dest="user",default="test") + p.add_option("-p","--pass",dest="password",default="test") + + # 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") + (options, args) = p.parse_args() + print "- trying to login with user=%s" % options.user + token = sp.login(options.user,options.password) + print "- token: %s" % token + print "- authenticated ok, now seeing if user is authorized" + check = sp.check_access(token,"imaginary_method_name") + print "- access ok? %s" % check diff --git a/cobbler/item.py b/cobbler/item.py index fd79012..992a18d 100644 --- a/cobbler/item.py +++ b/cobbler/item.py @@ -16,7 +16,8 @@ import exceptions import serializable import utils from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ +import pprint class Item(serializable.Serializable): @@ -51,7 +52,6 @@ class Item(serializable.Serializable): self.clear(is_subobject) # reset behavior differs for inheritance cases self.parent = '' # all objects by default are not subobjects self.children = {} # caching for performance reasons, not serialized - self.log_func = self.config.api.log def clear(self): @@ -122,6 +122,16 @@ class Item(serializable.Serializable): self.name = name return True + def set_owners(self,data): + """ + The owners field is a comment unless using an authz module that pays attention to it, + 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) + self.owners = owners + return True + def set_kernel_options(self,options): """ Kernel options are a space delimited list, @@ -189,8 +199,9 @@ class Item(serializable.Serializable): if key in [ "mac_address", "ip_address", "subnet", "gateway", "virt_bridge", "dhcp_tag", "hostname" ]: key_found_already = True for (name, interface) in data["interfaces"].iteritems(): - if interface[key].lower() == value.lower(): - return True + if value is not None: + if interface[key].lower() == value.lower(): + return True if not data.has_key(key): if not key_found_already: @@ -202,3 +213,10 @@ class Item(serializable.Serializable): else: return False + def dump_vars(self,data,format=True): + raw = utils.blender(self.config.api, False, self) + if format: + return pprint.pformat(raw) + else: + return raw + diff --git a/cobbler/item_distro.py b/cobbler/item_distro.py index f52ad0b..1a4c296 100644 --- a/cobbler/item_distro.py +++ b/cobbler/item_distro.py @@ -20,7 +20,7 @@ import weakref import os from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class Distro(item.Item): @@ -32,6 +32,7 @@ class Distro(item.Item): Reset this object. """ self.name = None + self.owners = self.settings.default_ownership self.kernel = (None, '<<inherit>>')[is_subobject] self.initrd = (None, '<<inherit>>')[is_subobject] self.kernel_options = ({}, '<<inherit>>')[is_subobject] @@ -60,6 +61,7 @@ class Distro(item.Item): """ 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') @@ -75,6 +77,8 @@ class Distro(item.Item): if self.ks_meta != "<<inherit>>" and type(self.ks_meta) != dict: self.set_ksmeta(self.ks_meta) + self.set_owners(self.owners) + return self def set_kernel(self,kernel): @@ -165,7 +169,8 @@ class Distro(item.Item): 'breed' : self.breed, 'source_repos' : self.source_repos, 'parent' : self.parent, - 'depth' : self.depth + 'depth' : self.depth, + 'owners' : self.owners } def printable(self): @@ -175,12 +180,13 @@ class Distro(item.Item): kstr = utils.find_kernel(self.kernel) istr = utils.find_initrd(self.initrd) buf = _("distro : %s\n") % self.name - buf = buf + _("kernel : %s\n") % kstr + buf = buf + _("breed : %s\n") % self.breed + buf = buf + _("architecture : %s\n") % self.arch buf = buf + _("initrd : %s\n") % istr + buf = buf + _("kernel : %s\n") % kstr buf = buf + _("kernel options : %s\n") % self.kernel_options - buf = buf + _("architecture : %s\n") % self.arch buf = buf + _("ks metadata : %s\n") % self.ks_meta - buf = buf + _("breed : %s\n") % self.breed + buf = buf + _("owners : %s\n") % self.owners return buf def remote_methods(self): @@ -191,7 +197,8 @@ class Distro(item.Item): 'kopts' : self.set_kernel_options, 'arch' : self.set_arch, 'ksmeta' : self.set_ksmeta, - 'breed' : self.set_breed + 'breed' : self.set_breed, + 'owners' : self.set_owners } diff --git a/cobbler/item_profile.py b/cobbler/item_profile.py index f229d4c..0e16f46 100644 --- a/cobbler/item_profile.py +++ b/cobbler/item_profile.py @@ -16,7 +16,7 @@ import utils import item from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class Profile(item.Item): @@ -34,6 +34,7 @@ class Profile(item.Item): Reset this object. """ self.name = None + self.owners = self.settings.default_ownership self.distro = (None, '<<inherit>>')[is_subobject] self.kickstart = (self.settings.default_kickstart , '<<inherit>>')[is_subobject] self.kernel_options = ({}, '<<inherit>>')[is_subobject] @@ -57,6 +58,7 @@ class Profile(item.Item): 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.kickstart = self.load_item(seed_data,'kickstart') self.kernel_options = self.load_item(seed_data,'kernel_options') @@ -68,7 +70,10 @@ class Profile(item.Item): # backwards compatibility if type(self.repos) != list: - self.set_repos(self.repos) + # ensure we are formatted correctly though if some repo + # defs don't exist on this side, don't fail as we need + # to convert everything -- cobbler check can report it + self.set_repos(self.repos,bypass_check=True) self.set_parent(self.parent) # virt specific @@ -85,7 +90,9 @@ class Profile(item.Item): if self.ks_meta != "<<inherit>>" and type(self.ks_meta) != dict: self.set_ksmeta(self.ks_meta) if self.repos != "<<inherit>>" and type(self.ks_meta) != list: - self.set_repos(self.repos) + self.set_repos(self.repos,bypass_check=True) + + self.set_owners(self.owners) return self @@ -134,46 +141,7 @@ class Profile(item.Item): self.server = server return True - def set_repos(self,repos): - - # WARNING: hack - repos = utils.fix_mod_python_select_submission(repos) - - - # allow the magic inherit string to persist - if repos == "<<inherit>>": - # FIXME: this is not inheritable in the WebUI presently ? - self.repos = "<<inherit>>" - return - - # store as an array regardless of input type - if repos is None: - repolist = [] - elif type(repos) != list: - # allow backwards compatibility support of string input - repolist = repos.split(None) - else: - repolist = repos - - - # make sure there are no empty strings - try: - repolist.remove('') - except: - pass - - self.repos = [] - - # if any repos don't exist, fail the operation - ok = True - for r in repolist: - if self.config.repos().find(name=r) is not None: - self.repos.append(r) - else: - print _("warning: repository not found: %s" % r) - - return True - + def set_kickstart(self,kickstart): """ Sets the kickstart. This must be a NFS, HTTP, or FTP URL. @@ -188,108 +156,27 @@ class Profile(item.Item): raise CX(_("kickstart not found")) def set_virt_cpus(self,num): - """ - For Virt only. Set the number of virtual CPUs to give to the - virtual machine. This is fed to virtinst RAW, so cobbler - will not yelp if you try to feed it 9999 CPUs. No formatting - like 9,999 please :) - """ - if num == "<<inherit>>": - self.virt_cpus = "<<inherit>>" - return True - - try: - num = int(str(num)) - except: - raise CX(_("invalid number of virtual CPUs")) - - self.virt_cpus = num - return True + return utils.set_virt_cpus(self,num) def set_virt_file_size(self,num): - """ - For Virt only. - Specifies the size of the virt image in gigabytes. - Older versions of koan (x<0.6.3) interpret 0 as "don't care" - Newer versions (x>=0.6.4) interpret 0 as "no disks" - """ - # num is a non-negative integer (0 means default) - # can also be a comma seperated list -- for usage with multiple disks - - if num == "<<inherit>>": - self.virt_file_size = "<<inherit>>" - return True - - if type(num) == str and num.find(",") != -1: - tokens = num.split(",") - for t in tokens: - # hack to run validation on each - self.set_virt_file_size(t) - # if no exceptions raised, good enough - self.virt_file_size = num - return True - - try: - inum = int(num) - if inum != float(num): - return CX(_("invalid virt file size")) - if inum >= 0: - self.virt_file_size = inum - return True - raise CX(_("invalid virt file size")) - except: - raise CX(_("invalid virt file size")) - + return utils.set_virt_file_size(self,num) + def set_virt_ram(self,num): - """ - For Virt only. - Specifies the size of the Virt RAM in MB. - 0 tells Koan to just choose a reasonable default. - """ - - if num == "<<inherit>>": - self.virt_ram = "<<inherit>>" - return True - - # num is a non-negative integer (0 means default) - try: - inum = int(num) - if inum != float(num): - return CX(_("invalid virt ram size")) - if inum >= 0: - self.virt_ram = inum - return True - return CX(_("invalid virt ram size")) - except: - return CX(_("invalid virt ram size")) + return utils.set_virt_ram(self,num) def set_virt_type(self,vtype): - """ - Virtualization preference, can be overridden by koan. - """ - - if vtype == "<<inherit>>": - self.virt_type == "<<inherit>>" - return True - - if vtype.lower() not in [ "qemu", "xenpv", "xenfv", "vmware", "auto" ]: - raise CX(_("invalid virt type")) - self.virt_type = vtype - return True + return utils.set_virt_type(self,vtype) def set_virt_bridge(self,vbridge): - """ - The default bridge for all virtual interfaces under this profile. - """ self.virt_bridge = vbridge return True def set_virt_path(self,path): - """ - Virtual storage location suggestion, can be overriden by koan. - """ - self.virt_path = path - return True + return utils.set_virt_path(self,path) + + def set_repos(self,repos,bypass_check=False): + return utils.set_repos(self,repos,bypass_check) + def get_parent(self): """ @@ -328,6 +215,7 @@ class Profile(item.Item): """ return { 'name' : self.name, + 'owners' : self.owners, 'distro' : self.distro, 'kickstart' : self.kickstart, 'kernel_options' : self.kernel_options, @@ -342,7 +230,8 @@ class Profile(item.Item): 'virt_type' : self.virt_type, 'virt_path' : self.virt_path, 'dhcp_tag' : self.dhcp_tag, - 'server' : self.server + 'server' : self.server, + } def printable(self): @@ -354,20 +243,22 @@ class Profile(item.Item): buf = buf + _("parent : %s\n") % self.parent else: buf = buf + _("distro : %s\n") % self.distro - buf = buf + _("kickstart : %s\n") % self.kickstart + buf = buf + _("dhcp tag : %s\n") % self.dhcp_tag buf = buf + _("kernel options : %s\n") % self.kernel_options + buf = buf + _("kickstart : %s\n") % self.kickstart buf = buf + _("ks metadata : %s\n") % self.ks_meta + buf = buf + _("owners : %s\n") % self.owners + buf = buf + _("repos : %s\n") % self.repos + buf = buf + _("server : %s\n") % self.server + buf = buf + _("virt bridge : %s\n") % self.virt_bridge + buf = buf + _("virt cpus : %s\n") % self.virt_cpus buf = buf + _("virt file size : %s\n") % self.virt_file_size + buf = buf + _("virt path : %s\n") % self.virt_path buf = buf + _("virt ram : %s\n") % self.virt_ram buf = buf + _("virt type : %s\n") % self.virt_type - buf = buf + _("virt path : %s\n") % self.virt_path - buf = buf + _("virt bridge : %s\n") % self.virt_bridge - buf = buf + _("virt cpus : %s\n") % self.virt_cpus - buf = buf + _("repos : %s\n") % self.repos - buf = buf + _("dhcp tag : %s\n") % self.dhcp_tag - buf = buf + _("server : %s\n") % self.server return buf + def remote_methods(self): return { 'name' : self.set_name, @@ -385,6 +276,7 @@ class Profile(item.Item): 'virt-bridge' : self.set_virt_bridge, 'virt-cpus' : self.set_virt_cpus, 'dhcp-tag' : self.set_dhcp_tag, - 'server' : self.set_server + 'server' : self.set_server, + 'owners' : self.set_owners } diff --git a/cobbler/item_repo.py b/cobbler/item_repo.py index ca9e94f..3b9b839 100644 --- a/cobbler/item_repo.py +++ b/cobbler/item_repo.py @@ -15,7 +15,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import utils import item from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class Repo(item.Item): @@ -31,6 +31,7 @@ class Repo(item.Item): def clear(self,is_subobject=False): self.parent = None self.name = None + # FIXME: subobject code does not really make sense for repos self.mirror = (None, '<<inherit>>')[is_subobject] self.keep_updated = ('y', '<<inherit>>')[is_subobject] self.priority = (99, '<<inherit>>')[is_subobject] @@ -39,6 +40,8 @@ class Repo(item.Item): self.depth = 2 # arbitrary, as not really apart of the graph self.arch = "" # use default arch self.yumopts = {} + self.owners = self.settings.default_ownership + self.mirror_locally = 1 def from_datastruct(self,seed_data): self.parent = self.load_item(seed_data, 'parent') @@ -51,9 +54,12 @@ class Repo(item.Item): self.arch = self.load_item(seed_data, 'arch') self.depth = self.load_item(seed_data, 'depth', 2) self.yumopts = self.load_item(seed_data, 'yumopts', {}) + self.owners = self.load_item(seed_data, 'owners', self.settings.default_ownership) + self.mirror_locally = self.load_item(seed_data, 'mirror_locally', '1') - # force this to be saved as a boolean + # coerce types from input file self.set_keep_updated(self.keep_updated) + self.set_owners(self.owners) return self @@ -63,6 +69,13 @@ class Repo(item.Item): reposync/repotrack integration over HTTP might come later. """ self.mirror = mirror + if self.arch is None or self.arch == "": + if mirror.find("x86_64") != -1: + self.set_arch("x86_64") + elif mirror.find("x86") != -1 or mirror.find("i386") != -1: + self.set_arch("i386") + elif mirror.find("ia64") != -1: + self.set_arch("ia64") return True def set_keep_updated(self,keep_updated): @@ -153,7 +166,9 @@ class Repo(item.Item): def to_datastruct(self): return { 'name' : self.name, + 'owners' : self.owners, 'mirror' : self.mirror, + 'mirror_locally' : self.mirror_locally, 'keep_updated' : self.keep_updated, 'priority' : self.priority, 'rpm_list' : self.rpm_list, @@ -164,14 +179,24 @@ class Repo(item.Item): 'yumopts' : self.yumopts } + def set_mirror_locally(self,value): + value = str(value).lower() + if value in [ "yes", "y", "1", "on", "true" ]: + self.mirror_locally = 1 + else: + self.mirror_locally = 0 + return True + def printable(self): buf = _("repo : %s\n") % self.name - buf = buf + _("mirror : %s\n") % self.mirror + buf = buf + _("arch : %s\n") % self.arch + buf = buf + _("createrepo_flags : %s\n") % self.createrepo_flags buf = buf + _("keep updated : %s\n") % self.keep_updated + buf = buf + _("mirror : %s\n") % self.mirror + buf = buf + _("mirror locally : %s\n") % self.mirror_locally + buf = buf + _("owners : %s\n") % self.owners buf = buf + _("priority : %s\n") % self.priority buf = buf + _("rpm list : %s\n") % self.rpm_list - buf = buf + _("createrepo_flags : %s\n") % self.createrepo_flags - buf = buf + _("arch : %s\n") % self.arch buf = buf + _("yum options : %s\n") % self.yumopts return buf @@ -202,6 +227,8 @@ class Repo(item.Item): 'priority' : self.set_priority, 'rpm-list' : self.set_rpm_list, 'createrepo-flags' : self.set_createrepo_flags, - 'yumopts' : self.set_yumopts + 'yumopts' : self.set_yumopts, + 'owners' : self.set_owners, + 'mirror-locally' : self.set_mirror_locally } diff --git a/cobbler/item_system.py b/cobbler/item_system.py index dadd8d1..09f169d 100644 --- a/cobbler/item_system.py +++ b/cobbler/item_system.py @@ -15,7 +15,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import utils import item from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ class System(item.Item): @@ -30,6 +30,7 @@ class System(item.Item): def clear(self,is_subobject=False): self.name = None + self.owners = self.settings.default_ownership self.profile = None self.kernel_options = {} self.ks_meta = {} @@ -37,9 +38,15 @@ class System(item.Item): self.netboot_enabled = True self.depth = 2 self.kickstart = "<<inherit>>" # use value in profile + self.server = "<<inherit>>" # "" (or settings) self.virt_path = "<<inherit>>" # "" self.virt_type = "<<inherit>>" # "" - self.server = "<<inherit>>" # "" (or settings) + 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>>" # "" def delete_interface(self,name): """ @@ -80,16 +87,25 @@ class System(item.Item): 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.profile = self.load_item(seed_data, 'profile') self.kernel_options = self.load_item(seed_data, 'kernel_options', {}) self.ks_meta = self.load_item(seed_data, 'ks_meta', {}) self.depth = self.load_item(seed_data, 'depth', 2) self.kickstart = self.load_item(seed_data, 'kickstart', '<<inherit>>') - self.virt_path = self.load_item(seed_data, 'virt_path', '<<inherit>>') - self.virt_type = self.load_item(seed_data, 'virt_type', '<<inherit>>') self.netboot_enabled = self.load_item(seed_data, 'netboot_enabled', True) self.server = self.load_item(seed_data, 'server', '<<inherit>>') + # virt specific + self.virt_path = self.load_item(seed_data, 'virt_path', '<<inherit>>') + self.virt_type = self.load_item(seed_data, 'virt_type', '<<inherit>>') + self.virt_ram = self.load_item(seed_data,'virt_ram','<<inherit>>') + self.virt_file_size = self.load_item(seed_data,'virt_file_size','<<inherit>>') + self.virt_path = self.load_item(seed_data,'virt_path','<<inherit>>') + self.virt_type = self.load_item(seed_data,'virt_type','<<inherit>>') + self.virt_bridge = self.load_item(seed_data,'virt_bridge','<<inherit>>') + self.virt_cpus = self.load_item(seed_data,'virt_cpus','<<inherit>>') + # backwards compat, these settings are now part of the interfaces data structure # and will contain data only in upgrade scenarios. @@ -126,8 +142,9 @@ class System(item.Item): # explicitly re-call the set_name function to possibily populate MAC/IP. self.set_name(self.name) - # coerce this into a boolean + # coerce types from input file self.set_netboot_enabled(self.netboot_enabled) + self.set_owners(self.owners) return self @@ -154,7 +171,7 @@ class System(item.Item): raise CX(_("name must be a string")) for x in name: if not x.isalnum() and not x in [ "_", "-", ".", ":", "+" ] : - raise CX(_("invalid characters in name")) + raise CX(_("invalid characters in name: %s") % x) if utils.is_mac(name): if intf["mac_address"] == "": @@ -212,11 +229,12 @@ class System(item.Item): """ if self.name == "default": return True - mac = self.get_mac_address(interface) - ip = self.get_ip_address(interface) - if mac is None and ip is None: - return False - return True + for (name,x) in self.interfaces.iteritems(): + mac = x.get("mac_address",None) + ip = x.get("ip_address",None) + if mac is not None or ip is not None: + return True + return False def set_dhcp_tag(self,dhcp_tag,interface="intf0"): intf = self.__get_interface(interface) @@ -231,7 +249,7 @@ class System(item.Item): def set_ip_address(self,address,interface="intf0"): """ Assign a IP or hostname in DHCP when this MAC boots. - Only works if manage_dhcp is set in /var/lib/cobbler/settings + Only works if manage_dhcp is set in /etc/cobbler/settings """ intf = self.__get_interface(interface) if address == "" or utils.is_ip(address): @@ -273,21 +291,20 @@ class System(item.Item): return True raise CX(_("invalid profile name")) - def set_virt_path(self,path): - """ - Virtual storage location suggestion, can be overriden by koan. - """ - self.virt_path = path - return True + def set_virt_cpus(self,num): + return utils.set_virt_cpus(self,num) + + def set_virt_file_size(self,num): + return utils.set_virt_file_size(self,num) + + def set_virt_ram(self,num): + return utils.set_virt_ram(self,num) def set_virt_type(self,vtype): - """ - Virtualization preference, can be overridden by koan. - """ - if vtype.lower() not in [ "qemu", "xenpv", "xenfv", "vmware", "auto" ]: - raise CX(_("invalid virt type")) - self.virt_type = vtype - return True + return utils.set_virt_type(self,vtype) + + def set_virt_path(self,path): + return utils.set_virt_path(self,path) def set_netboot_enabled(self,netboot_enabled): """ @@ -336,6 +353,9 @@ class System(item.Item): abstraction layer -- assigning systems to defined and repeatable roles. """ + if kickstart is None or kickstart == "" or kickstart == "delete": + self.kickstart = "<<inherit>>" + return True if utils.find_kickstart(kickstart): self.kickstart = kickstart return True @@ -344,32 +364,44 @@ class System(item.Item): def to_datastruct(self): return { - 'name' : self.name, - 'profile' : self.profile, - 'kernel_options' : self.kernel_options, - 'ks_meta' : self.ks_meta, - 'netboot_enabled' : self.netboot_enabled, - 'parent' : self.parent, - 'depth' : self.depth, - 'kickstart' : self.kickstart, - 'virt_type' : self.virt_type, - 'virt_path' : self.virt_path, - 'interfaces' : self.interfaces, - 'server' : self.server + 'name' : self.name, + 'kernel_options' : self.kernel_options, + '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, + '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 + } def printable(self): buf = _("system : %s\n") % self.name buf = buf + _("profile : %s\n") % self.profile buf = buf + _("kernel options : %s\n") % self.kernel_options + buf = buf + _("kickstart : %s\n") % self.kickstart buf = buf + _("ks metadata : %s\n") % self.ks_meta buf = buf + _("netboot enabled? : %s\n") % self.netboot_enabled - buf = buf + _("kickstart : %s\n") % self.kickstart - buf = buf + _("virt type : %s\n") % self.virt_type - buf = buf + _("virt path : %s\n") % self.virt_path + buf = buf + _("owners : %s\n") % self.owners buf = buf + _("server : %s\n") % self.server + buf = buf + _("virt cpus : %s\n") % self.virt_cpus + buf = buf + _("virt file size : %s\n") % self.virt_file_size + buf = buf + _("virt path : %s\n") % self.virt_path + buf = buf + _("virt ram : %s\n") % self.virt_ram + buf = buf + _("virt type : %s\n") % self.virt_type + + counter = 0 for (name,x) in self.interfaces.iteritems(): buf = buf + _("interface : %s\n") % (name) @@ -414,6 +446,12 @@ class System(item.Item): 'virt-type' : self.set_virt_type, 'modify-interface' : self.modify_interface, 'delete-interface' : self.delete_interface, - 'server' : self.set_server + 'virt-path' : self.set_virt_path, + 'virt-ram' : self.set_virt_ram, + 'virt-type' : self.set_virt_type, + 'virt-cpus' : self.set_virt_cpus, + 'virt-file-size' : self.set_virt_file_size, + 'server' : self.set_server, + 'owners' : self.set_owners } diff --git a/cobbler/kickgen.py b/cobbler/kickgen.py new file mode 100644 index 0000000..f302d00 --- /dev/null +++ b/cobbler/kickgen.py @@ -0,0 +1,279 @@ +""" +Builds out filesystem trees/data based on the object tree. +This is the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno + +import utils +from cexceptions import * +import templar + +import item_distro +import item_profile +import item_repo +import item_system + +from utils import _ + + +class KickGen: + """ + Handles conversion of internal state to the tftpboot tree layout + """ + + def __init__(self,config): + """ + Constructor + """ + 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) + + def generate_kickstart_for_profile(self,g): + + g = self.api.find_profile(name=g) + if g is None: + return "# profile not found" + + distro = g.get_conceptual_parent() + meta = utils.blender(self.api, False, g) + if distro is None: + raise CX(_("profile %(profile)s references missing distro %(distro)s") % { "profile" : g.name, "distro" : g.distro }) + kickstart_path = utils.find_kickstart(meta["kickstart"]) + if kickstart_path is not None and os.path.exists(kickstart_path): + # the input is an *actual* file, hence we have to copy it + try: + meta = utils.blender(self.api, False, g) + ksmeta = meta["ks_meta"] + del meta["ks_meta"] + meta.update(ksmeta) # make available at top level + meta["yum_repo_stanza"] = self.generate_repo_stanza(g,True) + meta["yum_config_stanza"] = self.generate_config_stanza(g,True) + meta["kickstart_done"] = self.generate_kickstart_signal(0, g, None) + meta["kickstart_start"] = self.generate_kickstart_signal(1, g, None) + meta["kernel_options"] = utils.hash_to_string(meta["kernel_options"]) + kfile = open(kickstart_path) + data = self.templar.render(kfile, meta, None, g) + kfile.close() + return data + except: + utils.log_exc(self.api.logger) + return _("# Error while rendering kickstart file, see /var/log/cobbler/cobbler.log for details") + + def generate_kickstart_signal(self, is_pre=0, profile=None, system=None): + """ + Do things that we do at the start/end of kickstarts... + * start: signal the status watcher we're starting + * end: signal the status watcher we're done + * end: disable PXE if needed + * end: save the original kickstart file for debug + """ + + nopxe = "\nwget \"http://%s/cblr/svc/op/nopxe/system/%s\" -O /dev/null" + saveks = "\nwget \"http://%s/cblr/svc/op/ks/%s/%s\" -O /root/cobbler.ks" + runpost = "\nwget \"http://%s/cblr/svc/op/trig/mode/post/%s/%s\" -O /dev/null" + runpre = "\nwget \"http://%s/cblr/svc/op/trig/mode/pre/%s/%s\" -O /dev/null" + + what = "profile" + blend_this = profile + if system: + what = "system" + blend_this = system + + blended = utils.blender(self.api, False, blend_this) + kickstart = blended.get("kickstart",None) + + buf = "" + srv = blended["http_server"] + if system is not None: + if not is_pre: + if str(self.settings.pxe_just_once).upper() in [ "1", "Y", "YES", "TRUE" ]: + buf = buf + nopxe % (srv, system.name) + if kickstart and os.path.exists(kickstart): + buf = buf + saveks % (srv, "system", system.name) + if self.settings.run_install_triggers: + buf = buf + runpost % (srv, what, system.name) + else: + if self.settings.run_install_triggers: + buf = buf + runpre % (srv, what, system.name) + + else: + if not is_pre: + if kickstart and os.path.exists(kickstart): + buf = buf + saveks % (srv, "profile", profile.name) + if self.settings.run_install_triggers: + buf = buf + runpost % (srv, what, profile.name) + else: + if self.settings.run_install_triggers: + buf = buf + runpre % (srv, what, profile.name) + + return buf + + def generate_repo_stanza(self, obj, is_profile=True): + + """ + Automatically attaches yum repos to profiles/systems in kickstart files + that contain the magic $yum_repo_stanza variable. + """ + + buf = "" + blended = utils.blender(self.api, False, obj) + configs = self.get_repo_filenames(obj,is_profile) + repos = self.repos + + # FIXME: this really should be dynamically generated as with the kickstarts + for c in configs: + name = c.split("/")[-1].replace(".repo","") + (is_core, baseurl) = self.analyze_repo_config(c) + for repo in repos: + if repo.name == name: + if not repo.yumopts.has_key('enabled') or repo.yumopts['enabled'] == '1': + if repo.mirror_locally: + buf = buf + "repo --name=%s --baseurl=%s\n" % (name, baseurl) + else: + buf = buf + "repo --name=%s --baseurl=%s\n" % (name, repo.mirror) + + return buf + + def analyze_repo_config(self, filename): + fd = open(filename) + data = fd.read() + lines = data.split("\n") + ret = False + baseurl = None + for line in lines: + if line.find("ks_mirror") != -1: + ret = True + if line.find("baseurl") != -1: + try: + first, baseurl = line.split("=",1) + except: + raise CX(_("error scanning repo: %s" % filename)) + fd.close() + return (ret, baseurl) + + def get_repo_baseurl(self, server, repo_name, is_repo_mirror=True): + """ + Construct the URL to a repo definition. + """ + if is_repo_mirror: + return "http://%s/cobbler/repo_mirror/%s" % (server, repo_name) + else: + return "http://%s/cobbler/ks_mirror/config/%s" % (server, repo_name) + + def get_repo_filenames(self, obj, is_profile=True): + """ + For a given object, return the paths to repo configuration templates + that will be used to generate per-object repo configuration files and + baseurls + """ + + blended = utils.blender(self.api, False, obj) + urlseg = self.get_repo_segname(is_profile) + + topdir = "%s/%s/%s/*.repo" % (self.settings.webdir, urlseg, blended["name"]) + files = glob.glob(topdir) + return files + + + def get_repo_segname(self, is_profile): + if is_profile: + return "repos_profile" + else: + return "repos_system" + + + def generate_config_stanza(self, obj, is_profile=True): + + """ + Add in automatic to configure /etc/yum.repos.d on the remote system + if the kickstart file contains the magic $yum_config_stanza. + """ + + if not self.settings.yum_post_install_mirror: + return "" + + urlseg = self.get_repo_segname(is_profile) + + distro = obj.get_conceptual_parent() + if not is_profile: + distro = distro.get_conceptual_parent() + + blended = utils.blender(self.api, False, obj) + configs = self.get_repo_filenames(obj, is_profile) + buf = "" + + # for each kickstart template we have rendered ... + for c in configs: + + name = c.split("/")[-1].replace(".repo","") + # add the line to create the yum config file on the target box + conf = self.get_repo_config_file(blended["http_server"],urlseg,blended["name"],name) + buf = buf + "wget \"%s\" --output-document=/etc/yum.repos.d/%s.repo\n" % (conf, name) + + return buf + + def get_repo_config_file(self,server,urlseg,obj_name,repo_name): + """ + Construct the URL to a repo config file that is usable in kickstart + for use with yum. This is different than the templates cobbler reposync + creates, as this file will allow the server to migrate and have different + variables for different subnets/profiles/etc. + """ + return "http://%s/cblr/%s/%s/%s.repo" % (server,urlseg,obj_name,repo_name) + + def generate_kickstart_for_system(self,s): + + + s = self.api.find_system(name=s) + if s is None: + return "# system not found" + + profile = s.get_conceptual_parent() + if profile is None: + raise CX(_("system %(system)s references missing profile %(profile)s") % { "system" : s.name, "profile" : s.profile }) + distro = profile.get_conceptual_parent() + meta = utils.blender(self.api, False, s) + kickstart_path = utils.find_kickstart(meta["kickstart"]) + if kickstart_path and os.path.exists(kickstart_path): + try: + ksmeta = meta["ks_meta"] + del meta["ks_meta"] + meta.update(ksmeta) # make available at top level + meta["yum_repo_stanza"] = self.generate_repo_stanza(s, False) + meta["yum_config_stanza"] = self.generate_config_stanza(s, False) + meta["kickstart_done"] = self.generate_kickstart_signal(0, profile, s) + meta["kickstart_start"] = self.generate_kickstart_signal(1, profile, s) + meta["kernel_options"] = utils.hash_to_string(meta["kernel_options"]) + kfile = open(kickstart_path) + data = self.templar.render(kfile, meta, None, s) + kfile.close() + return data + except: + traceback.print_exc() + raise CX(_("Error templating file")) + diff --git a/cobbler/manage_ctrl.py b/cobbler/manage_ctrl.py new file mode 100644 index 0000000..a01910a --- /dev/null +++ b/cobbler/manage_ctrl.py @@ -0,0 +1,76 @@ +""" +Builds out DHCP info +This is the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno +import popen2 +from shlex import shlex + + +import utils +from cexceptions import * +import templar + +import item_distro +import item_profile +import item_repo +import item_system + +from utils import _ + +class ManageCtrl: + """ + Handles conversion of internal state to the tftpboot tree layout + """ + + def __init__(self,config,verbose=False,dns=None,dhcp=None): + """ + 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.dns = dns + + def write_dhcp_lease(self,port,host,ip,mac): + return dhcp.write_dhcp_lease(port,host,ip,mac) + + def remove_dhcp_lease(self,port,host): + return dhcp.remove_dhcp_lease(port,host) + + def write_dhcp_file(self): + return dhcp.write_dhcp_file() + + def regen_ethers(self): + return dhcp.regen_ethers() + + def regen_hosts(self): + return dns.regen_hosts() + + def write_dns_files(self): + return dns.write_bind_files() diff --git a/cobbler/module_loader.py b/cobbler/module_loader.py index b695a04..d584ce3 100644 --- a/cobbler/module_loader.py +++ b/cobbler/module_loader.py @@ -19,7 +19,7 @@ import distutils.sysconfig import os import sys import glob -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import ConfigParser MODULE_CACHE = {} diff --git a/cobbler/modules/authn_configfile.py b/cobbler/modules/authn_configfile.py index ffe87a7..be453be 100644 --- a/cobbler/modules/authn_configfile.py +++ b/cobbler/modules/authn_configfile.py @@ -17,7 +17,7 @@ import distutils.sysconfig import ConfigParser import sys import os -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import md5 import traceback diff --git a/cobbler/modules/authn_denyall.py b/cobbler/modules/authn_denyall.py new file mode 100644 index 0000000..91e27d4 --- /dev/null +++ b/cobbler/modules/authn_denyall.py @@ -0,0 +1,43 @@ +""" +Authentication module that denies everything. +Used to disable the WebUI by default. + +Copyright 2007-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import distutils.sysconfig +import sys + +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 authenticate(api_handle,username,password): + """ + Validate a username/password combo, returning True/False + + Thanks to http://trac.edgewall.org/ticket/845 for supplying + the algorithm info. + """ + + # debugging only (not safe to enable) + # api_handle.logger.debug("backend authenticate (%s,%s)" % (username,password)) + + return False + + diff --git a/cobbler/modules/authn_kerberos.py b/cobbler/modules/authn_kerberos.py deleted file mode 100644 index 7f85db6..0000000 --- a/cobbler/modules/authn_kerberos.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Authentication module that uses kerberos. - -Copyright 2007, Red Hat, Inc -Michael DeHaan <mdehaan@redhat.com> - -This software may be freely redistributed under the terms of the GNU -general public license. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. -""" - -# NOTE: this is not using 'straight up' kerberos in that we -# relay passwords through cobblerd for authentication, that may -# be done later. It does of course check against kerberos, -# however. - -# ALSO NOTE: we're calling out to a Perl program to make -# this work. You must install Authen::Simple::Kerberos -# from CPAN and the Kerberos libraries for this to work. -# See the Cobbler Wiki for more info. - -# ALSO ALSO NOTE: set kerberos_realm in /var/lib/cobbler/settings -# to something appropriate or this will never work. CASING -# MATTERS. example.com != EXAMPLE.COM. - -import distutils.sysconfig -import ConfigParser -import sys -import os -from rhpl.translate import _, N_, textdomain, utf8 -import md5 -import traceback -# since sub_process isn't available on older OS's -try: - import sub_process as subprocess -except: - import subprocess - -plib = distutils.sysconfig.get_python_lib() -mod_path="%s/cobbler" % plib -sys.path.insert(0, mod_path) - -import cexceptions -import utils - -def register(): - """ - The mandatory cobbler module registration hook. - """ - return "authn" - -def authenticate(api_handle,username,password): - """ - Validate a username/password combo, returning True/False - Uses cobbler_auth_helper - """ - - realm = self.api.settings().kerberos_realm - api_handle.logger.debug("authenticating %s against %s" % (username,realm)) - - rc = subprocess.call([ - "/usr/bin/cobbler_auth_help", - "--method=kerberos", - "--username=%s" % username, - "--password=%s" % password, - "--realm=%s" % realm - ]) - print rc - if rc == 42: - api_handle.logger.debug("authenticated ok") - # authentication ok (FIXME: log) - return True - else: - api_handle.logger.debug("authentication failed") - # authentication failed - return False - - diff --git a/cobbler/modules/authn_ldap.py b/cobbler/modules/authn_ldap.py new file mode 100644 index 0000000..ff31750 --- /dev/null +++ b/cobbler/modules/authn_ldap.py @@ -0,0 +1,118 @@ +""" +Authentication module that uses ldap +Settings in /etc/cobbler/authn_ldap.conf +Choice of authentication module is in /etc/cobbler/modules.conf + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import distutils.sysconfig +import sys +import os +from utils import _ +import md5 +import traceback + +# we'll import this just a bit later +# to keep it from being a requirement +# import ldap + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +import cexceptions +import utils +import api as cobbler_api + +def register(): + """ + The mandatory cobbler module registration hook. + """ + + return "authn" + +def authenticate(api_handle,username,password): + """ + Validate an ldap bind, returning True/False + """ + + import ldap + + server = api_handle.settings().ldap_server + basedn = api_handle.settings().ldap_base_dn + port = api_handle.settings().ldap_port + tls = api_handle.settings().ldap_tls + anon_bind = api_handle.settings().ldap_anonymous_bind + prefix = api_handle.settings().ldap_search_prefix + + # form our ldap uri based on connection port + if port == '389': + uri = 'ldap://' + server + elif port == '636': + uri = 'ldaps://' + server + else: + uri = 'ldap://' + "%s:%s" % (server,port) + + # connect to LDAP host + dir = ldap.initialize(uri) + + # start_tls if tls is 'on', 'true' or 'yes' + # and we're not already using old-SSL + tls = str(tls).lower() + if port != '636': + if tls in [ "on", "true", "yes", "1" ]: + try: + dir.start_tls_s() + except: + traceback.print_exc() + return False + + # if we're not allowed to search anonymously, + # grok the search bind settings and attempt to bind + anon_bind = str(anon_bind).lower() + if anon_bind not in [ "on", "true", "yes", "1" ]: + searchdn = api_handle.settings().ldap_search_bind_dn + searchpw = api_handle.settings().ldap_search_passwd + + if searchdn == '' or searchpw == '': + raise "Missing search bind settings" + + try: + dir.simple_bind_s(searchdn, searchpw) + except: + traceback.print_exc() + return False + + # perform a subtree search in basedn to find the full dn of the user + # TODO: what if username is a CN? maybe it goes into the config file as well? + filter = prefix + username + result = dir.search_s(basedn, ldap.SCOPE_SUBTREE, filter, []) + if result: + for dn,entry in result: + # username _should_ be unique so we should only have one result + # ignore entry; we don't need it + pass + else: + return False + + try: + # attempt to bind as the user + dir.simple_bind_s(dn,password) + dir.unbind() + return True + except: + # traceback.print_exc() + return False + # catch-all + return False + +if __name__ == "__main__": + api_handle = cobbler_api.BootAPI() + print authenticate(api_handle, "guest", "guest") + diff --git a/cobbler/modules/authn_passthru.py b/cobbler/modules/authn_passthru.py new file mode 100644 index 0000000..ebbe79a --- /dev/null +++ b/cobbler/modules/authn_passthru.py @@ -0,0 +1,49 @@ +""" +Authentication module that defers to Apache and trusts +what Apache trusts. + +Copyright 2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import distutils.sysconfig +import sys +import os +from utils import _ +import traceback + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +import cexceptions +import utils + +def register(): + """ + The mandatory cobbler module registration hook. + """ + return "authn" + +def authenticate(api_handle,username,password): + """ + Validate a username/password combo, returning True/False + Uses cobbler_auth_helper + """ + + fd = open("/var/lib/cobbler/web.ss") + data = fd.read() + if password == data: + rc = 1 + else: + rc = 0 + fd.close() + return data + diff --git a/cobbler/modules/authn_testing.py b/cobbler/modules/authn_testing.py new file mode 100644 index 0000000..cd74cdf --- /dev/null +++ b/cobbler/modules/authn_testing.py @@ -0,0 +1,42 @@ +""" +Authentication module that denies everything. +Unsafe demo. Allows anyone in with testing/testing. + +Copyright 2007-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import distutils.sysconfig +import sys + +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 authenticate(api_handle,username,password): + """ + Validate a username/password combo, returning True/False + + Thanks to http://trac.edgewall.org/ticket/845 for supplying + the algorithm info. + """ + + if username == "testing" and password == "testing": + return True + return False + + diff --git a/cobbler/modules/authz_allowall.py b/cobbler/modules/authz_allowall.py index 1b05630..10d4b17 100644 --- a/cobbler/modules/authz_allowall.py +++ b/cobbler/modules/authz_allowall.py @@ -16,7 +16,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import distutils.sysconfig import ConfigParser import sys -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib diff --git a/cobbler/modules/authz_configfile.py b/cobbler/modules/authz_configfile.py new file mode 100644 index 0000000..84343e2 --- /dev/null +++ b/cobbler/modules/authz_configfile.py @@ -0,0 +1,64 @@ +""" +Authorization module that allow users listed in +/etc/cobbler/users.conf to be permitted to access resources. +For instance, when using authz_ldap, you want to use authn_configfile, +not authz_allowall, which will most likely NOT do what you want. + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import distutils.sysconfig +import ConfigParser +import sys +import os +from utils import _ + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +import cexceptions +import utils + +CONFIG_FILE='/etc/cobbler/users.conf' + +def register(): + """ + The mandatory cobbler module registration hook. + """ + return "authz" + +def __parse_config(): + if not os.path.exists(CONFIG_FILE): + return [] + config = ConfigParser.SafeConfigParser() + config.read(CONFIG_FILE) + alldata = {} + groups = config.sections() + for g in groups: + alldata[str(g)] = {} + opts = config.options(g) + for o in opts: + alldata[g][o] = 1 + return alldata + + +def authorize(api_handle,user,resource,arg1=None,arg2=None): + """ + Validate a user against a resource. + All users in the file are permitted by this module. + """ + + data = __parse_config() + for g in data: + if user in data[g]: + return 1 + return 0 + +if __name__ == "__main__": + print __parse_config() diff --git a/cobbler/modules/authz_ownership.py b/cobbler/modules/authz_ownership.py new file mode 100644 index 0000000..fbd00f9 --- /dev/null +++ b/cobbler/modules/authz_ownership.py @@ -0,0 +1,178 @@ +""" +Authorization module that allow users listed in +/etc/cobbler/users.conf to be permitted to access resources, with +the further restriction that cobbler objects can be edited to only +allow certain users/groups to access those specific objects. + +Copyright 2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import distutils.sysconfig +import ConfigParser +import sys +import os +from cobbler.utils import _ + +plib = distutils.sysconfig.get_python_lib() +mod_path="%s/cobbler" % plib +sys.path.insert(0, mod_path) + +import cexceptions +import utils + + +def register(): + """ + The mandatory cobbler module registration hook. + """ + return "authz" + +def __parse_config(): + etcfile='/etc/cobbler/users.conf' + if not os.path.exists(etcfile): + raise CX(_("/etc/cobbler/users.conf does not exist")) + config = ConfigParser.ConfigParser() + config.read(etcfile) + alldata = {} + sections = config.sections() + for g in sections: + alldata[str(g)] = {} + opts = config.options(g) + for o in opts: + alldata[g][o] = 1 + return alldata + +def __authorize_kickstart(api_handle, user, user_groups, kickstart): + # the authorization rules for kickstart editing are a bit + # of a special case. Non-admin users can edit a kickstart + # only if all objects that depend on that kickstart are + # editable by the user in question. + # + # Example: + # if Pinky owns ProfileA + # and the Brain owns ProfileB + # and both profiles use the same kickstart template + # and neither Pinky nor the Brain is an admin + # neither is allowed to edit the kickstart template + # because they would make unwanted changes to each other + # + # In the above scenario the UI will explain the problem + # and ask that the user asks the admin to resolve it if required. + # NOTE: this function is only called by authorize so admin users are + # cleared before this function is called. + + lst = api_handle.find_profile(kickstart=kickstart, return_list=True) + lst.extend(api_handle.find_system(kickstart=kickstart, return_list=True)) + for obj in lst: + if not __is_user_allowed(obj, user, user_groups): + return 0 + return 1 + +def __is_user_allowed(obj, user, user_groups): + if obj.owners == []: + # no ownership restrictions, cleared + return 1 + for allowed in obj.owners: + if user == allowed: + # user match + return 1 + # else look for a group match + for group in user_groups: + if group == allowed and user in user_groups[group]: + return 1 + return 0 + + + +def authorize(api_handle,user,resource,arg1=None,arg2=None): + """ + Validate a user against a resource. + All users in the file are permitted by this module. + """ + + # everybody can get read-only access to everything + # if they pass authorization, they don't have to be in users.conf + if resource is not None: + for x in [ "get", "read", "/cobbler/web" ]: + if resource.startswith(x): + return 1 + + user_groups = __parse_config() + + # classify the type of operation + modify_operation = False + for criteria in ["save","remove","modify","write","edit"]: + if resource.find(criteria) != -1: + modify_operation = True + + # FIXME: is everyone allowed to copy? I think so. + # FIXME: deal with the problem of deleted parents and promotion + + found_user = False + for g in user_groups: + for x in user_groups[g]: + if x == user: + found_user = True + # if user is in the admin group, always authorize + # regardless of the ownership of the object. + if g == "admins" or g == "admin": + return 1 + break + + if not found_user: + # if the user isn't anywhere in the file, reject regardless + # they can still use read-only XMLRPC + return 0 + if not modify_operation: + # sufficient to allow access for non save/remove ops to all + # users for now, may want to refine later. + return 1 + + # now we have a modify_operation op, so we must check ownership + # of the object. remove ops pass in arg1 as a string name, + # saves pass in actual objects, so we must treat them differently. + # kickstarts are even more special so we call those out to another + # function, rather than going through the rest of the code here. + + if resource.find("kickstart") != -1: + return __authorize_kickstart(api_handle,user,user_groups,arg1) + + obj = None + if resource.find("remove") != -1: + if resource == "remove_distro": + obj = api_handle.find_distro(arg1) + elif resource == "remove_profile": + obj = api_handle.find_profile(arg1) + elif resource == "remove_system": + obj = api_handle.find_system(arg1) + elif resource == "remove_repo": + obj = api_handle.find_system(arg1) + elif resource.find("save") != -1 or resource.find("modify") != -1: + obj = arg1 + + # if the object has no ownership data, allow access regardless + if obj.owners is None or obj.owners == []: + return 1 + + return __is_user_allowed(obj,user,user_groups) + + +if __name__ == "__main__": + # real tests are contained in tests/tests.py + import api as cobbler_api + api = cobbler_api.BootAPI() + print __parse_config() + print authorize(api, "admin1", "sync") + d = api.find_distro("F9B-i386") + d.set_owners(["allowed"]) + api.add_distro(d) + print authorize(api, "admin1", "save_distro", d) + print authorize(api, "basement2", "save_distro", d) diff --git a/cobbler/modules/cli_distro.py b/cobbler/modules/cli_distro.py index 35f5a4b..b7a8fa7 100644 --- a/cobbler/modules/cli_distro.py +++ b/cobbler/modules/cli_distro.py @@ -19,7 +19,7 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import commands import cexceptions @@ -33,13 +33,16 @@ class DistroFunction(commands.CobblerFunction): return "distro" def subcommands(self): - return [ "add", "edit", "copy", "rename", "remove", "list", "report" ] + return [ "add", "edit", "copy", "rename", "remove", "list", "report", "dumpvars" ] def add_options(self, p, args): - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--arch", dest="arch", help="ex: x86, x86_64, ia64") p.add_option("--breed", dest="breed", help="ex: redhat, debian, suse") + if self.matches_args(args,["add"]): + p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true") + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--initrd", dest="initrd", help="absolute path to initrd.img (REQUIRED)") p.add_option("--kernel", dest="kernel", help="absolute path to vmlinuz (REQUIRED)") p.add_option("--kopts", dest="kopts", help="ex: 'noipv6'") @@ -47,12 +50,16 @@ class DistroFunction(commands.CobblerFunction): p.add_option("--name", dest="name", help="ex: 'RHEL-5-i386' (REQUIRED)") + + if self.matches_args(args,["copy","rename"]): p.add_option("--newname", dest="newname", help="for copy/rename commands") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") - if not self.matches_args(args,["report","list"]): + if not self.matches_args(args,["dumpvars","report","list"]): p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") + if not self.matches_args(args,["dumpvars","remove","report","list"]): + p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module") if self.matches_args(args,["remove"]): p.add_option("--recursive", action="store_true", dest="recursive", help="also delete child objects") @@ -63,16 +70,19 @@ class DistroFunction(commands.CobblerFunction): if obj is None: return True - if self.options.kernel: - obj.set_kernel(self.options.kernel) - if self.options.initrd: - obj.set_initrd(self.options.initrd) - if self.options.kopts: - obj.set_kernel_options(self.options.kopts) - if self.options.ksmeta: - obj.set_ksmeta(self.options.ksmeta) - if self.options.breed: - obj.set_breed(self.options.breed) + if not "dumpvars" in self.args: + if self.options.kernel: + obj.set_kernel(self.options.kernel) + if self.options.initrd: + obj.set_initrd(self.options.initrd) + if self.options.kopts: + obj.set_kernel_options(self.options.kopts) + if self.options.ksmeta: + obj.set_ksmeta(self.options.ksmeta) + if self.options.breed: + obj.set_breed(self.options.breed) + if self.options.owners: + obj.set_owners(self.options.owners) return self.object_manipulator_finish(obj, self.api.distros, self.options) diff --git a/cobbler/modules/cli_misc.py b/cobbler/modules/cli_misc.py index ebbba47..c72b11d 100644 --- a/cobbler/modules/cli_misc.py +++ b/cobbler/modules/cli_misc.py @@ -19,7 +19,7 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import commands from cexceptions import * HELP_FORMAT = commands.HELP_FORMAT @@ -73,11 +73,12 @@ class ImportFunction(commands.CobblerFunction): return "import" def add_options(self, p, args): + p.add_option("--arch", dest="arch", help="explicitly specify the architecture being imported (RECOMENDED)") p.add_option("--path", dest="mirror", help="local path or rsync location (REQUIRED)") p.add_option("--mirror", dest="mirror_alt", help="alias for --path") p.add_option("--name", dest="name", help="name, ex 'RHEL-5', (REQUIRED)") - p.add_option("--available-as", dest="available_as", help="do not mirror, use this as install tree") - p.add_option("--kickstart", dest="kickstart_file", help="use the kickstart file specified as the profile's kickstart file") + p.add_option("--available-as", dest="available_as", help="do not mirror, use this as install tree base") + p.add_option("--kickstart", dest="kickstart_file", help="use the kickstart file specified as the profile's kickstart file, do not auto-assign") p.add_option("--rsync-flags", dest="rsync_flags", help="pass additional flags to rsync") def run(self): @@ -92,7 +93,8 @@ class ImportFunction(commands.CobblerFunction): self.options.name, network_root=self.options.available_as, kickstart_file=self.options.kickstart_file, - rsync_flags=self.options.rsync_flags + rsync_flags=self.options.rsync_flags, + arch=self.options.arch ) @@ -182,11 +184,6 @@ class ReportFunction(commands.CobblerFunction): self.reporting_print_sorted(self.api.repos()) return True -## FIXME: add legacy command translator to keep things simple -## cobbler system report foo --> cobbler report --what=systems --name=foo -## cobbler system report --> cobbler report --what=systems -## ditto for "cobbler list" - ######################################################## class StatusFunction(commands.CobblerFunction): @@ -198,7 +195,7 @@ class StatusFunction(commands.CobblerFunction): return "status" def run(self): - return self.api.status("text") # no other output modes supported yet + return self.api.status("text") # no other output modes supported yet ######################################################## @@ -243,6 +240,46 @@ class ValidateKsFunction(commands.CobblerFunction): return self.api.validateks() ######################################################## + +class BuildIsoFunction(commands.CobblerFunction): + + def add_options(self,p,args): + p.add_option("--iso", dest="isoname", help="(OPTIONAL) output ISO to this path") + p.add_option("--profiles", dest="profiles", help="(OPTIONAL) use these profiles only") + p.add_option("--tempdir", dest="tempdir", help="(OPTIONAL) working directory") + + def help_me(self): + return HELP_FORMAT % ("cobbler buildiso","") + + def command_name(self): + return "buildiso" + + def run(self): + return self.api.build_iso( + iso=self.options.isoname, + profiles=self.options.profiles, + tempdir=self.options.tempdir + ) + +######################################################## + +class ReplicateFunction(commands.CobblerFunction): + + def help_me(self): + return HELP_FORMAT % ("cobbler replicate","[ARGS|--help]") + + def command_name(self): + return "replicate" + + def add_options(self, p, args): + p.add_option("--master", dest="master", help="Cobbler server to replicate from.") + + def run(self): + return self.api.replicate(cobbler_master = self.options.master) + + + +######################################################## # MODULE HOOKS def register(): @@ -253,9 +290,11 @@ def register(): def cli_functions(api): return [ + BuildIsoFunction(api), CheckFunction(api), ImportFunction(api), ReserializeFunction(api), ListFunction(api), ReportFunction(api), StatusFunction(api), - SyncFunction(api), RepoSyncFunction(api), ValidateKsFunction(api) + SyncFunction(api), RepoSyncFunction(api), ValidateKsFunction(api), + ReplicateFunction(api) ] return [] diff --git a/cobbler/modules/cli_profile.py b/cobbler/modules/cli_profile.py index e9d1d23..45b7b2e 100644 --- a/cobbler/modules/cli_profile.py +++ b/cobbler/modules/cli_profile.py @@ -19,7 +19,7 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import commands import cexceptions @@ -33,10 +33,15 @@ class ProfileFunction(commands.CobblerFunction): return "profile" def subcommands(self): - return [ "add", "edit", "copy", "rename", "remove", "list", "report" ] + return [ "add", "edit", "copy", "rename", "remove", "list", "report", "dumpvars" ] def add_options(self, p, args): - if not self.matches_args(args,["remove","report","list"]): + + if self.matches_args(args,["add"]): + p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true") + + + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--distro", dest="distro", help="ex: 'RHEL-5-i386' (REQUIRED)") p.add_option("--dhcp-tag", dest="dhcp_tag", help="for use in advanced DHCP configuration") @@ -46,18 +51,20 @@ class ProfileFunction(commands.CobblerFunction): p.add_option("--kopts", dest="kopts", help="ex: 'noipv6'") p.add_option("--name", dest="name", help="a name for the profile (REQUIRED)") + if "copy" in args or "rename" in args: p.add_option("--newname", dest="newname") - if not self.matches_args(args,["remove","report", "list"]): + if not self.matches_args(args,["dumpvars","remove","report", "list"]): p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") - if not self.matches_args(args,["report", "list"]): + if not self.matches_args(args,["dumpvars","report", "list"]): p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") + p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module") if self.matches_args(args,["remove"]): p.add_option("--recursive", action="store_true", dest="recursive", help="also delete child objects") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--repos", dest="repos", help="names of cobbler repos") p.add_option("--server-override", dest="server_override", help="overrides value in settings file") p.add_option("--virt-bridge", dest="virt_bridge", help="ex: 'virbr0'") @@ -69,8 +76,7 @@ class ProfileFunction(commands.CobblerFunction): def run(self): - - if self.matches_args(self.args,["report","list","remove"]) or not self.options.inherit: + if self.matches_args(self.args,["report","list","remove","dumpvars"]) or not self.options.inherit: obj = self.object_manipulator_start(self.api.new_profile,self.api.profiles,subobject=False) else: obj = self.object_manipulator_start(self.api.new_profile,self.api.profiles,subobject=True) @@ -78,20 +84,23 @@ class ProfileFunction(commands.CobblerFunction): if obj is None: return True - if self.options.inherit: obj.set_parent(self.options.inherit) - if self.options.distro: obj.set_distro(self.options.distro) - if self.options.kickstart: obj.set_kickstart(self.options.kickstart) - if self.options.kopts: obj.set_kernel_options(self.options.kopts) - if self.options.ksmeta: obj.set_ksmeta(self.options.ksmeta) - if self.options.virt_file_size: obj.set_virt_file_size(self.options.virt_file_size) - if self.options.virt_ram: obj.set_virt_ram(self.options.virt_ram) - if self.options.virt_bridge: obj.set_virt_bridge(self.options.virt_bridge) - if self.options.virt_type: obj.set_virt_type(self.options.virt_type) - if self.options.virt_cpus: obj.set_virt_cpus(self.options.virt_cpus) - if self.options.repos: obj.set_repos(self.options.repos) - if self.options.virt_path: obj.set_virt_path(self.options.virt_path) - if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag) - if self.options.server_override: obj.set_server(self.options.server) + if not self.matches_args(self.args,["dumpvars"]): + if self.options.inherit: obj.set_parent(self.options.inherit) + if self.options.distro: obj.set_distro(self.options.distro) + if self.options.kickstart: obj.set_kickstart(self.options.kickstart) + if self.options.kopts: obj.set_kernel_options(self.options.kopts) + if self.options.ksmeta: obj.set_ksmeta(self.options.ksmeta) + if self.options.virt_file_size: obj.set_virt_file_size(self.options.virt_file_size) + if self.options.virt_ram: obj.set_virt_ram(self.options.virt_ram) + if self.options.virt_bridge: obj.set_virt_bridge(self.options.virt_bridge) + if self.options.virt_type: obj.set_virt_type(self.options.virt_type) + if self.options.virt_cpus: obj.set_virt_cpus(self.options.virt_cpus) + if self.options.repos: obj.set_repos(self.options.repos) + if self.options.virt_path: obj.set_virt_path(self.options.virt_path) + if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag) + if self.options.server_override: obj.set_server(self.options.server) + + if self.options.owners: obj.set_owners(self.options.owners) 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 96afa6f..af6a9d1 100644 --- a/cobbler/modules/cli_repo.py +++ b/cobbler/modules/cli_repo.py @@ -19,7 +19,7 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import commands import cexceptions @@ -33,20 +33,25 @@ class RepoFunction(commands.CobblerFunction): return "repo" def subcommands(self): - return [ "add", "edit", "copy", "rename", "remove", "list", "report" ] + return [ "add", "edit", "copy", "rename", "remove", "list", "report", "dumpvars" ] def add_options(self, p, args): - if not self.matches_args(args,["remove","report","list"]): + + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--arch", dest="arch", help="overrides repo arch if required") + if self.matches_args(args,["add"]): + p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true") + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--createrepo-flags", dest="createrepo_flags", help="additional flags for createrepo") p.add_option("--keep-updated", dest="keep_updated", help="update on each reposync, yes/no") p.add_option("--name", dest="name", help="ex: 'Fedora-8-updates-i386' (REQUIRED)") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--mirror", dest="mirror", help="source to mirror (REQUIRED)") + p.add_option("--mirror-locally", dest="mirror_locally", help="mirror or use external directly? (default 1)") p.add_option("--priority", dest="priority", help="set priority") p.add_option("--rpm-list", dest="rpm_list", help="just mirror these rpms") p.add_option("--yumopts", dest="yumopts", help="ex: pluginvar=abcd") @@ -55,10 +60,12 @@ class RepoFunction(commands.CobblerFunction): p.add_option("--newname", dest="newname", help="used for copy/edit") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") - if not self.matches_args(args,["report","list"]): + if not self.matches_args(args,["dumpvars","report","list"]): p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") + if not self.matches_args(args,["dumpvars","remove","report","list"]): + p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module") def run(self): @@ -66,6 +73,8 @@ class RepoFunction(commands.CobblerFunction): obj = self.object_manipulator_start(self.api.new_repo,self.api.repos) if obj is None: return True + if self.matches_args(self.args,["dumpvars"]): + return self.object_manipulator_finish(obj, self.api.profiles, self.options) if self.options.arch: obj.set_arch(self.options.arch) if self.options.createrepo_flags: obj.set_createrepo_flags(self.options.createrepo_flags) @@ -73,8 +82,12 @@ class RepoFunction(commands.CobblerFunction): if self.options.keep_updated: obj.set_keep_updated(self.options.keep_updated) if self.options.priority: obj.set_priority(self.options.priority) if self.options.mirror: obj.set_mirror(self.options.mirror) + if self.options.mirror_locally: obj.set_mirror_locally(self.options.mirror_locally) if self.options.yumopts: obj.set_yumopts(self.options.yumopts) + if self.options.owners: + obj.set_owners(self.options.owners) + return self.object_manipulator_finish(obj, self.api.repos, self.options) diff --git a/cobbler/modules/cli_system.py b/cobbler/modules/cli_system.py index c463b8c..d8f8edf 100644 --- a/cobbler/modules/cli_system.py +++ b/cobbler/modules/cli_system.py @@ -19,7 +19,7 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _, get_random_mac import commands import cexceptions @@ -33,11 +33,14 @@ class SystemFunction(commands.CobblerFunction): return "system" def subcommands(self): - return [ "add", "edit", "copy", "rename", "remove", "report", "list" ] + return [ "add", "edit", "copy", "rename", "remove", "report", "list", "dumpvars" ] def add_options(self, p, args): - if not self.matches_args(args,["remove","report","list"]): + if self.matches_args(args,["add"]): + p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true") + + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--dhcp-tag", dest="dhcp_tag", help="for use in advanced DHCP configurations") p.add_option("--gateway", dest="gateway", help="for static IP / templating usage") p.add_option("--hostname", dest="hostname", help="ex: server.example.org") @@ -50,25 +53,30 @@ class SystemFunction(commands.CobblerFunction): p.add_option("--name", dest="name", help="a name for the system (REQUIRED)") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--netboot-enabled", dest="netboot_enabled", help="PXE on (1) or off (0)") if self.matches_args(args,["copy","rename"]): p.add_option("--newname", dest="newname", help="for use with copy/edit") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") - if not self.matches_args(args,["report","list"]): + if not self.matches_args(args,["dumpvars","report","list"]): p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") - if not self.matches_args(args,["remove","report","list"]): + if not self.matches_args(args,["dumpvars","remove","report","list"]): + p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module") p.add_option("--profile", dest="profile", help="name of cobbler profile (REQUIRED)") p.add_option("--server-override", dest="server_override", help="overrides server value in settings file") p.add_option("--subnet", dest="subnet", help="for static IP / templating usage") - p.add_option("--virt-bridge", dest="virt_bridge", help="ex: virbr0") - p.add_option("--virt-path", dest="virt_path", help="path, partition, or volume") - p.add_option("--virt-type", dest="virt_type", help="ex: xenpv, qemu, xenfv") + + p.add_option("--virt-bridge", dest="virt_bridge", help="ex: 'virbr0'") + p.add_option("--virt-cpus", dest="virt_cpus", help="integer (default: 1)") + p.add_option("--virt-file-size", dest="virt_file_size", help="size in GB") + p.add_option("--virt-path", dest="virt_path", help="path, partition, or volume") + p.add_option("--virt-ram", dest="virt_ram", help="size in MB") + p.add_option("--virt-type", dest="virt_type", help="ex: 'xenpv', 'qemu'") def run(self): @@ -76,6 +84,8 @@ class SystemFunction(commands.CobblerFunction): obj = self.object_manipulator_start(self.api.new_system,self.api.systems) if obj is None: return True + if self.matches_args(self.args,["dumpvars"]): + return self.object_manipulator_finish(obj, self.api.profiles, self.options) if self.options.profile: obj.set_profile(self.options.profile) if self.options.kopts: obj.set_kernel_options(self.options.kopts) @@ -83,8 +93,13 @@ class SystemFunction(commands.CobblerFunction): if self.options.kickstart: obj.set_kickstart(self.options.kickstart) if self.options.netboot_enabled: obj.set_netboot_enabled(self.options.netboot_enabled) if self.options.server_override: obj.set_server(self.options.server_override) - if self.options.virt_path: obj.set_virt_path(self.options.virt_path) + + if self.options.virt_file_size: obj.set_virt_file_size(self.options.virt_file_size) + if self.options.virt_ram: obj.set_virt_ram(self.options.virt_ram) if self.options.virt_type: obj.set_virt_type(self.options.virt_type) + if self.options.virt_cpus: obj.set_virt_cpus(self.options.virt_cpus) + if self.options.virt_path: obj.set_virt_path(self.options.virt_path) + if self.options.interface: my_interface = "intf%s" % self.options.interface @@ -92,15 +107,23 @@ class SystemFunction(commands.CobblerFunction): my_interface = "intf0" if self.options.hostname: obj.set_hostname(self.options.hostname, my_interface) - if self.options.mac: obj.set_mac_address(self.options.mac, my_interface) + if self.options.mac: + if self.options.mac.lower() == 'random': + obj.set_mac_address(get_random_mac(self.api), my_interface) + else: + obj.set_mac_address(self.options.mac, my_interface) if self.options.ip: obj.set_ip_address(self.options.ip, my_interface) if self.options.subnet: obj.set_subnet(self.options.subnet, my_interface) if self.options.gateway: obj.set_gateway(self.options.gateway, my_interface) if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag, my_interface) if self.options.virt_bridge: obj.set_virt_bridge(self.options.virt_bridge, my_interface) - return self.object_manipulator_finish(obj, self.api.systems, self.options) + if self.options.owners: + obj.set_owners(self.options.owners) + + rc = self.object_manipulator_finish(obj, self.api.systems, self.options) + return rc ######################################################## diff --git a/cobbler/modules/manage_bind.py b/cobbler/modules/manage_bind.py new file mode 100644 index 0000000..339fbf0 --- /dev/null +++ b/cobbler/modules/manage_bind.py @@ -0,0 +1,283 @@ +""" +This is some of the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> +John Eckersberg <jeckersb@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno +import popen2 +import re +from shlex import shlex + + +import utils +from cexceptions import * +import templar + +import item_distro +import item_profile +import item_repo +import item_system + +from utils import _ + + +def register(): + """ + The mandatory cobbler module registration hook. + """ + return "manage" + + +class BindManager: + + def what(self): + return "isc_and_bind" + + def __init__(self,config,verbose=False): + """ + 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) + + def regen_hosts(self): + pass # not used + + def __forward_zones(self): + """ + Returns a map of zones and the records that belong + in them + """ + zones = {} + for zone in self.settings.manage_forward_zones: + zones[zone] = [] + + for sys in self.systems: + for (name, interface) in sys.interfaces.iteritems(): + host = interface["hostname"] + ip = interface["ip_address"] + if not host or not ip: + # gotsta have some hostname and ip or else! + continue + if host.find(".") == -1: + continue + + # match the longest zone! + # e.g. if you have a host a.b.c.d.e + # if manage_forward_zones has: + # - c.d.e + # - b.c.d.e + # then a.b.c.d.e should go in b.c.d.e + best_match = '' + for zone in zones.keys(): + if re.search('\.%s$' % zone, host) and len(zone) > len(best_match): + best_match = zone + + if best_match == '': # no match + continue + + # strip the zone off the hostname and append the + # remainder + ip to the zone list + host = host.replace(best_match, '') + if host[-1] == '.': # strip trailing '.' if it's there + host = host[:-1] + zones[best_match].append((host, ip)) + + # axe zones that are defined in manage_forward_zones + # but don't actually match any hosts + for (k,v) in zones.items(): + if v == []: + zones.pop(k) + + return zones + + def __reverse_zones(self): + """ + Returns a map of zones and the records that belong + in them + """ + zones = {} + for zone in self.settings.manage_reverse_zones: + zones[zone] = [] + + for sys in self.systems: + for (name, interface) in sys.interfaces.iteritems(): + host = interface["hostname"] + ip = interface["ip_address"] + if not host or not ip: + # gotsta have some hostname and ip or else! + continue + + # match the longest zone! + # e.g. if you have an ip 1.2.3.4 + # if manage_reverse_zones has: + # - 1.2 + # - 1.2.3 + # then 1.2.3.4 should go in 1.2.3 + best_match = '' + for zone in zones.keys(): + if re.search('^%s\.' % zone, ip) and len(zone) > len(best_match): + best_match = zone + + if best_match == '': # no match + continue + + # strip the zone off the front of the ip + # reverse the rest of the octets + # append the remainder + hostname + ip = ip.replace(best_match, '', 1) + if ip[0] == '.': # strip leading '.' if it's there + ip = ip[1:] + tokens = ip.split('.') + tokens.reverse() + ip = '.'.join(tokens) + zones[best_match].append((ip, host + '.')) + + # axe zones that are defined in manage_forward_zones + # but don't actually match any hosts + for (k,v) in zones.items(): + if v == []: + zones.pop(k) + + return zones + + + def __write_named_conf(self): + """ + Write out the named.conf main config file from the template. + """ + settings_file = self.settings.named_conf + template_file = "/etc/cobbler/named.template" + forward_zones = self.settings.manage_forward_zones + reverse_zones = self.settings.manage_reverse_zones + + metadata = {'zone_include': ''} + for zone in self.__forward_zones().keys(): + txt = """ +zone "%(zone)s." { + type master; + file "%(zone)s"; +}; +""" % {'zone': zone} + metadata['zone_include'] = metadata['zone_include'] + txt + + for zone in self.__reverse_zones().keys(): + tokens = zone.split('.') + tokens.reverse() + arpa = '.'.join(tokens) + '.in-addr.arpa' + txt = """ +zone "%(arpa)s." { + type master; + file "%(zone)s"; +}; +""" % {'arpa': arpa, 'zone': zone} + metadata['zone_include'] = metadata['zone_include'] + txt + + try: + f2 = open(template_file,"r") + except: + raise CX(_("error reading template from file: %s") % template_file) + template_data = "" + template_data = f2.read() + f2.close() + + self.templar.render(template_data, metadata, settings_file, None) + + def __write_zone_files(self): + """ + Write out the forward and reverse zone files for all configured zones + """ + default_template_file = "/etc/cobbler/zone.template" + cobbler_server = self.settings.server + serial = int(time.time()) + forward = self.__forward_zones() + reverse = self.__reverse_zones() + + try: + f2 = open(default_template_file,"r") + except: + raise CX(_("error reading template from file: %s") % default_template_file) + default_template_data = "" + default_template_data = f2.read() + f2.close() + + for (zone, hosts) in forward.iteritems(): + metadata = { + 'cobbler_server': cobbler_server, + 'serial': serial, + 'host_record': '' + } + + # grab zone-specific template if it exists + try: + fd = open('/etc/cobbler/zone_templates/%s' % zone) + template_data = fd.read() + fd.close() + except: + template_data = default_template_data + + for host in hosts: + txt = '%s\tIN\tA\t%s\n' % host + metadata['host_record'] = metadata['host_record'] + txt + + self.templar.render(template_data, metadata, '/var/named/' + zone, None) + + for (zone, hosts) in reverse.iteritems(): + metadata = { + 'cobbler_server': cobbler_server, + 'serial': serial, + 'host_record': '' + } + + # grab zone-specific template if it exists + try: + fd = open('/etc/cobbler/zone_templates/%s' % zone) + template_data = fd.read() + fd.close() + except: + template_data = default_template_data + + for host in hosts: + txt = '%s\tIN\tPTR\t%s\n' % host + metadata['host_record'] = metadata['host_record'] + txt + + self.templar.render(template_data, metadata, '/var/named/' + zone, None) + + + def write_dns_files(self): + """ + BIND files are written when manage_dns is set in + /var/lib/cobbler/settings. + """ + + self.__write_named_conf() + self.__write_zone_files() + +def get_manager(config): + return BindManager(config) + diff --git a/cobbler/modules/manage_dnsmasq.py b/cobbler/modules/manage_dnsmasq.py new file mode 100644 index 0000000..465995f --- /dev/null +++ b/cobbler/modules/manage_dnsmasq.py @@ -0,0 +1,194 @@ + +""" +This is some of the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> +John Eckersberg <jeckersb@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno +import popen2 +from shlex import shlex +import utils +from cexceptions import * +import templar +import item_distro +import item_profile +import item_repo +import item_system +from utils import _ + +def register(): + return "manage" + +class DnsmasqManager: + """ + Handles conversion of internal state to the tftpboot tree layout + """ + + def __init__(self,config,verbose=False,dhcp=None): + """ + 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) + + def what(self): + return "dnsmasq" + + def write_dhcp_lease(self,port,host,ip,mac): + pass + + def remove_dhcp_lease(self,port,host): + pass + + + def write_dhcp_file(self): + """ + DHCP files are written when manage_dhcp is set in + /var/lib/cobbler/settings. + """ + + settings_file = self.settings.dnsmasq_conf + template_file = "/etc/cobbler/dnsmasq.template" + + try: + f2 = open(template_file,"r") + except: + raise CX(_("error writing template to file: %s") % template_file) + template_data = "" + template_data = f2.read() + f2.close() + + # build each per-system definition + # as configured, this only works for ISC, patches accepted + # from those that care about Itanium. elilo seems to be unmaintained + # so additional maintaince in other areas may be required to keep + # this working. + + elilo = os.path.basename(self.settings.bootloaders["ia64"]) + + system_definitions = {} + counter = 0 + + # we used to just loop through each system, but now we must loop + # through each network interface of each system. + + for system in self.systems: + profile = system.get_conceptual_parent() + distro = profile.get_conceptual_parent() + for (name, interface) in system.interfaces.iteritems(): + + mac = interface["mac_address"] + ip = interface["ip_address"] + host = interface["hostname"] + + if mac is None or mac == "": + # can't write a DHCP entry for this system + continue + + counter = counter + 1 + systxt = "" + + # dnsmasq. don't have to write IP and other info here, but we do tag + # each MAC based on the arch of it's distro, if it needs something other + # than pxelinux.0 -- for these arches, and these arches only, a dnsmasq + # reload (full "cobbler sync") would be required after adding the system + # to cobbler, just to tag this relationship. + + if ip is not None and ip != "": + if distro.arch.lower() == "ia64": + systxt = "dhcp-host=net:ia64," + ip + "\n" + # support for other arches needs modifications here + else: + systxt = "" + + dhcp_tag = interface["dhcp_tag"] + if dhcp_tag == "": + dhcp_tag = "default" + + if not system_definitions.has_key(dhcp_tag): + system_definitions[dhcp_tag] = "" + system_definitions[dhcp_tag] = system_definitions[dhcp_tag] + systxt + + # we are now done with the looping through each interface of each system + + metadata = { + "omapi_enabled" : self.settings.omapi_enabled, + "omapi_port" : self.settings.omapi_port, + "insert_cobbler_system_definitions" : system_definitions.get("default",""), + "date" : time.asctime(time.gmtime()), + "cobbler_server" : self.settings.server, + "next_server" : self.settings.next_server, + "elilo" : elilo + } + + # now add in other DHCP expansions that are not tagged with "default" + for x in system_definitions.keys(): + if x == "default": + continue + metadata["insert_cobbler_system_definitions_%s" % x] = system_definitions[x] + + self.templar.render(template_data, metadata, settings_file, None) + + def regen_ethers(self): + # dnsmasq knows how to read this database of MACs -> IPs, so we'll keep it up to date + # every time we add a system. + # read 'man ethers' for format info + fh = open("/etc/ethers","w+") + for sys in self.systems: + for (name, interface) in sys.interfaces.iteritems(): + mac = interface["mac_address"] + ip = interface["ip_address"] + if mac is None or mac == "": + # can't write this w/o a MAC address + continue + if ip is not None and ip != "": + fh.write(mac.upper() + "\t" + ip + "\n") + fh.close() + + def regen_hosts(self): + # dnsmasq knows how to read this database for host info + # (other things may also make use of this later) + fh = open("/var/lib/cobbler/cobbler_hosts","w+") + for sys in self.systems: + for (name, interface) in sys.interfaces.iteritems(): + mac = interface["mac_address"] + host = interface["hostname"] + ip = interface["ip_address"] + if mac is None or mac == "": + continue + if host is not None and host != "" and ip is not None and ip != "": + fh.write(ip + "\t" + host + "\n") + fh.close() + + def write_dns_files(self): + # already taken care of by the regen_hosts() + pass + +def get_manager(config): + return DnsmasqManager(config) + diff --git a/cobbler/modules/manage_isc.py b/cobbler/modules/manage_isc.py new file mode 100644 index 0000000..c398270 --- /dev/null +++ b/cobbler/modules/manage_isc.py @@ -0,0 +1,252 @@ +""" +This is some of the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> +John Eckersberg <jeckersb@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno +import popen2 +from shlex import shlex + + +import utils +from cexceptions import * +import templar + +import item_distro +import item_profile +import item_repo +import item_system + +from utils import _ + + +def register(): + """ + The mandatory cobbler module registration hook. + """ + return "manage" + + +class IscManager: + + def what(self): + return "isc_and_bind" + + def __init__(self,config,verbose=False): + """ + 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) + + def write_dhcp_lease(self,port,host,ip,mac): + """ + Use DHCP's API to create a DHCP entry in the + /var/lib/dhcpd/dhcpd.leases file + #Code from http://svn.osgdc.org/browse/kusu/kusu + # /trunk/src/kits/base/packages/kusu-base-installer/lib/kusu/nodefun.py?r=3025 + # FIXME: should use subprocess + """ + try: + fromchild, tochild = popen2.popen2(self.settings.omshell_bin) + tochild.write("port %s\n" % port) + tochild.flush() + tochild.write("connect\n") + tochild.flush() + tochild.write("new host\n") + tochild.flush() + tochild.write('set name = \"%s\"\n' % host) + tochild.flush() + tochild.write("set ip-address = %s\n" % ip) + tochild.flush() + tochild.write("set hardware-address = %s\n" % mac.lower()) + tochild.flush() + tochild.write("set hardware-type = 1\n") + tochild.flush() + tochild.write("create\n") + tochild.flush() + tochild.close() + fromchild.close() + except IOError: + # FIXME: just catch 32 (broken pipe) and show a warning + pass + + def remove_dhcp_lease(self,port,host): + """ + 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) + try: + tochild.write("port %s\n" % port) + tochild.flush() + tochild.write("connect\n") + tochild.flush() + tochild.write("new host\n") + tochild.flush() + tochild.write('set name = \"%s\"\n' % host) + tochild.flush() + tochild.write("open\n") # opens register with host information + tochild.flush() + tochild.write("remove\n") + tochild.flush() + tochild.close() + fromchild.close() + except IOError: + # FIXME: convert this to subprocess. + # FIXME: catch specific errors only (32/broken pipe) + pass + + def write_dhcp_file(self): + """ + DHCP files are written when manage_dhcp is set in + /var/lib/cobbler/settings. + """ + + settings_file = self.settings.dhcpd_conf + template_file = "/etc/cobbler/dhcp.template" + mode = self.settings.manage_dhcp_mode.lower() + + try: + f2 = open(template_file,"r") + except: + raise CX(_("error writing template to file: %s") % template_file) + template_data = "" + template_data = f2.read() + f2.close() + + # build each per-system definition + # as configured, this only works for ISC, patches accepted + # from those that care about Itanium. elilo seems to be unmaintained + # so additional maintaince in other areas may be required to keep + # this working. + + elilo = os.path.basename(self.settings.bootloaders["ia64"]) + + system_definitions = {} + counter = 0 + + + # Clean system definitions in /var/lib/dhcpd/dhcpd.leases just in + # case to avoid conflicts with the hosts we're defining and to clean + # possible removed hosts (only if using OMAPI) + if self.settings.omapi_enabled and self.settings.omapi_port: + if os.path.exists("/var/lib/dhcpd/dhcpd.leases"): + file = open('/var/lib/dhcpd/dhcpd.leases') + item = shlex(file) + while 1: + elem = item.get_token() + if not elem: + break + if elem == 'host': + hostremove = item.get_token() + self.remove_dhcp_lease(self.settings.omapi_port,hostremove) + + # we used to just loop through each system, but now we must loop + # through each network interface of each system. + + for system in self.systems: + profile = system.get_conceptual_parent() + distro = profile.get_conceptual_parent() + for (name, interface) in system.interfaces.iteritems(): + + mac = interface["mac_address"] + ip = interface["ip_address"] + host = interface["hostname"] + + if mac is None or mac == "": + # can't write a DHCP entry for this system + continue + + counter = counter + 1 + systxt = "" + + + # the label the entry after the hostname if possible + if host is not None and host != "": + systxt = "\nhost %s {\n" % host + if self.settings.isc_set_host_name: + systxt = systxt + " option host-name = \"%s\";\n" % host + else: + systxt = "\nhost generic%d {\n" % counter + + if distro.arch == "ia64": + # can't use pxelinux.0 anymore + systxt = systxt + " filename \"/%s\";\n" % elilo + systxt = systxt + " hardware ethernet %s;\n" % mac + if ip is not None and ip != "": + systxt = systxt + " fixed-address %s;\n" % ip + systxt = systxt + "}\n" + + # If we have all values defined and we're using omapi, + # we will just create entries dinamically into DHCPD + # without requiring a restart (but file will be written + # as usual for having it working after restart) + + if ip is not None and ip != "": + if mac is not None and mac != "": + if host is not None and host != "": + if self.settings.omapi_enabled and self.settings.omapi_port: + self.remove_dhcp_lease(self.settings.omapi_port,host) + self.write_dhcp_lease(self.settings.omapi_port,host,ip,mac) + + dhcp_tag = interface["dhcp_tag"] + if dhcp_tag == "": + dhcp_tag = "default" + + if not system_definitions.has_key(dhcp_tag): + system_definitions[dhcp_tag] = "" + system_definitions[dhcp_tag] = system_definitions[dhcp_tag] + systxt + + # we are now done with the looping through each interface of each system + + metadata = { + "omapi_enabled" : self.settings.omapi_enabled, + "omapi_port" : self.settings.omapi_port, + "insert_cobbler_system_definitions" : system_definitions.get("default",""), + "date" : time.asctime(time.gmtime()), + "cobbler_server" : self.settings.server, + "next_server" : self.settings.next_server, + "elilo" : elilo + } + + # now add in other DHCP expansions that are not tagged with "default" + for x in system_definitions.keys(): + if x == "default": + continue + metadata["insert_cobbler_system_definitions_%s" % x] = system_definitions[x] + + self.templar.render(template_data, metadata, settings_file, None) + + def regen_ethers(self): + pass # ISC/BIND do not use this + + +def get_manager(config): + return IscManager(config) + diff --git a/cobbler/modules/serializer_shelve.py b/cobbler/modules/serializer_shelve.py index 246a92a..09495dd 100644 --- a/cobbler/modules/serializer_shelve.py +++ b/cobbler/modules/serializer_shelve.py @@ -24,7 +24,7 @@ import os import sys import glob import traceback -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import os import shelve import time diff --git a/cobbler/modules/serializer_yaml.py b/cobbler/modules/serializer_yaml.py index 6425bd1..c4bd359 100644 --- a/cobbler/modules/serializer_yaml.py +++ b/cobbler/modules/serializer_yaml.py @@ -21,7 +21,7 @@ plib = distutils.sysconfig.get_python_lib() mod_path="%s/cobbler" % plib sys.path.insert(0, mod_path) -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import utils import yaml # Howell-Clark version import cexceptions @@ -66,7 +66,10 @@ def get_filename(collection_type): ending = collection_type if not ending.endswith("s"): ending = ending + "s" - return "/var/lib/cobbler/%s" % ending + if ending != "settings": + return "/var/lib/cobbler/%s" % ending + else: + return "/etc/cobbler/settings" def deserialize_raw(collection_type): filename = get_filename(collection_type) diff --git a/cobbler/pxegen.py b/cobbler/pxegen.py new file mode 100644 index 0000000..a438583 --- /dev/null +++ b/cobbler/pxegen.py @@ -0,0 +1,324 @@ +""" +Builds out filesystem trees/data based on the object tree. +This is the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno + +import utils +from cexceptions import * +import templar + +import item_distro +import item_profile +import item_repo +import item_system + +from utils import _ + + +class PXEGen: + """ + Handles building out PXE stuff + """ + + def __init__(self,config): + """ + Constructor + """ + 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.bootloc = utils.tftpboot_location() + + def copy_bootloaders(self): + """ + Copy bootloaders to the configured tftpboot directory + NOTE: we support different arch's if defined in + /etc/cobbler/settings. + """ + for loader in self.settings.bootloaders.keys(): + path = self.settings.bootloaders[loader] + newname = os.path.basename(path) + destpath = os.path.join(self.bootloc, newname) + utils.copyfile(path, destpath) + utils.copyfile("/var/lib/cobbler/menu.c32", os.path.join(self.bootloc, "menu.c32")) + + # Copy memtest to tftpboot if package is installed on system + for memtest in glob.glob('/boot/memtest*'): + base = os.path.basename(memtest) + utils.copyfile(memtest,os.path.join(self.bootloc,"images",base)) + + def copy_distros(self): + """ + A distro is a kernel and an initrd. Copy all of them and error + out if any files are missing. The conf file was correct if built + via the CLI or API, though it's possible files have been moved + since or perhaps they reference NFS directories that are no longer + mounted. + + NOTE: this has to be done for both tftp and http methods + """ + # copy is a 4-letter word but tftpboot runs chroot, thus it's required. + for d in self.distros: + self.copy_single_distro_files(d) + + def copy_single_distro_files(self, d): + for dirtree in [self.bootloc, self.settings.webdir]: + distros = os.path.join(dirtree, "images") + distro_dir = os.path.join(distros,d.name) + utils.mkdir(distro_dir) + kernel = utils.find_kernel(d.kernel) # full path + initrd = utils.find_initrd(d.initrd) # full path + if kernel is None or not os.path.isfile(kernel): + raise CX(_("kernel not found: %(file)s, distro: %(distro)s") % { "file" : d.kernel, "distro" : d.name }) + if initrd is None or not os.path.isfile(initrd): + raise CX(_("initrd not found: %(file)s, distro: %(distro)s") % { "file" : d.initrd, "distro" : d.name }) + b_kernel = os.path.basename(kernel) + b_initrd = os.path.basename(initrd) + if kernel.startswith(dirtree): + utils.linkfile(kernel, os.path.join(distro_dir, b_kernel)) + else: + utils.copyfile(kernel, os.path.join(distro_dir, b_kernel)) + if initrd.startswith(dirtree): + utils.linkfile(initrd, os.path.join(distro_dir, b_initrd)) + else: + utils.copyfile(initrd, os.path.join(distro_dir, b_initrd)) + + def write_all_system_files(self,system): + + profile = system.get_conceptual_parent() + if profile is None: + raise CX(_("system %(system)s references a missing profile %(profile)s") % { "system" : system.name, "profile" : system.profile}) + distro = profile.get_conceptual_parent() + if distro is None: + raise CX(_("profile %(profile)s references a missing distro %(distro)s") % { "profile" : system.profile, "distro" : profile.distro}) + + # this used to just generate a single PXE config file, but now must + # generate one record for each described NIC ... + + counter = 0 + for (name,interface) in system.interfaces.iteritems(): + + ip = interface["ip_address"] + + f1 = utils.get_config_filename(system,interface=name) + + # for tftp only ... + if distro.arch in [ "x86", "x86_64", "standard"]: + # pxelinux wants a file named $name under pxelinux.cfg + f2 = os.path.join(self.bootloc, "pxelinux.cfg", f1) + if distro.arch == "ia64": + # elilo expects files to be named "$name.conf" in the root + # and can not do files based on the MAC address + if ip is not None and ip != "": + print _("Warning: Itanium system object (%s) needs an IP address to PXE") % system.name + + filename = "%s.conf" % utils.get_config_filename(system,interface=name) + f2 = os.path.join(self.bootloc, filename) + + f3 = os.path.join(self.settings.webdir, "systems", f1) + + if system.netboot_enabled and system.is_pxe_supported(): + if distro.arch in [ "x86", "x86_64", "standard"]: + self.write_pxe_file(f2,system,profile,distro,False) + if distro.arch == "ia64": + self.write_pxe_file(f2,system,profile,distro,True) + else: + # ensure the file doesn't exist + utils.rmfile(f2) + + counter = counter + 1 + + + def make_pxe_menu(self): + # only do this if there is NOT a system named default. + default = self.systems.find(name="default") + if default is not None: + return + + fname = os.path.join(self.bootloc, "pxelinux.cfg", "default") + + # read the default template file + template_src = open("/etc/cobbler/pxedefault.template") + template_data = template_src.read() + + # 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) + + # build out the menu entries + pxe_menu_items = "" + for profile in profile_list: + distro = profile.get_conceptual_parent() + # xen distros can be ruled out as they won't boot + if distro.name.find("-xen") != -1: + continue + contents = self.write_pxe_file(None,None,profile,distro,False,include_header=False) + if contents is not None: + pxe_menu_items = pxe_menu_items + contents + "\n" + + # if we have any memtest files in images, make entries for them + # after we list the profiles + memtests = glob.glob(self.bootloc + "/images/memtest*") + if len(memtests) > 0: + pxe_menu_items = pxe_menu_items + "\n\n" + for memtest in glob.glob(self.bootloc + '/images/memtest*'): + base = os.path.basename(memtest) + contents = self.write_memtest_pxe("/images/%s" % base) + pxe_menu_items = pxe_menu_items + contents + "\n" + + # save the template. + metadata = { "pxe_menu_items" : pxe_menu_items } + outfile = os.path.join(self.bootloc, "pxelinux.cfg", "default") + self.templar.render(template_data, metadata, outfile, None) + template_src.close() + + def write_memtest_pxe(self,filename): + """ + Write a configuration file for memtest + """ + + # just some random variables + template = None + metadata = {} + buffer = "" + + template = "/etc/cobbler/pxeprofile.template" + + # store variables for templating + metadata["menu_label"] = "MENU LABEL %s" % os.path.basename(filename) + metadata["profile_name"] = os.path.basename(filename) + metadata["kernel_path"] = "/images/%s" % os.path.basename(filename) + metadata["initrd_path"] = "" + metadata["append_line"] = "" + + # get the template + template_fh = open(template) + template_data = template_fh.read() + template_fh.close() + + # return results + buffer = self.templar.render(template_data, metadata, None) + return buffer + + + + def write_pxe_file(self,filename,system,profile,distro,is_ia64, include_header=True): + """ + Write a configuration file for the boot loader(s). + More system-specific configuration may come in later, if so + that would appear inside the system object in api.py + + NOTE: relevant to tftp only + """ + + # --- + # system might have netboot_enabled set to False (see item_system.py), if so, + # don't do anything else and flag the error condition. + if system is not None and not system.netboot_enabled: + return None + + # --- + # just some random variables + template = None + metadata = {} + buffer = "" + + # --- + # find kernel and initrd + kernel_path = os.path.join("/images",distro.name,os.path.basename(distro.kernel)) + initrd_path = os.path.join("/images",distro.name,os.path.basename(distro.initrd)) + + # Find the kickstart if we inherit from another profile + kickstart_path = utils.blender(self.api, True, profile)["kickstart"] + + # --- + # choose a template + if system is None: + template = "/etc/cobbler/pxeprofile.template" + elif not is_ia64: + template = "/etc/cobbler/pxesystem.template" + else: + template = "/etc/cobbler/pxesystem_ia64.template" + + # now build the kernel command line + if system is not None: + blended = utils.blender(self.api, True, system) + else: + blended = utils.blender(self.api, True,profile) + kopts = blended["kernel_options"] + + # generate the append line + append_line = "append %s" % utils.hash_to_string(kopts) + if not is_ia64: + append_line = "%s initrd=%s" % (append_line, initrd_path) + 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 != "": + + 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: + kickstart_path = "http://%s/cblr/svc/op/ks/profile/%s" % (blended["http_server"], profile.name) + + if distro.breed is None or distro.breed == "redhat": + append_line = "%s ks=%s" % (append_line, kickstart_path) + elif distro.breed == "suse": + append_line = "%s autoyast=%s" % (append_line, kickstart_path) + elif distro.breed == "debian": + append_line = "%s auto=true url=%s" % (append_line, kickstart_path) + append_line = append_line.replace("ksdevice","interface") + + # store variables for templating + metadata["menu_label"] = "" + if not is_ia64 and system is None: + metadata["menu_label"] = "MENU LABEL %s" % profile.name + metadata["profile_name"] = profile.name + metadata["kernel_path"] = kernel_path + metadata["initrd_path"] = initrd_path + metadata["append_line"] = append_line + + # get the template + template_fh = open(template) + template_data = template_fh.read() + template_fh.close() + + # save file and/or return results, depending on how called. + buffer = self.templar.render(template_data, metadata, None) + if filename is not None: + fd = open(filename, "w") + fd.write(buffer) + fd.close() + return buffer + + + + diff --git a/cobbler/remote.py b/cobbler/remote.py index 5131323..1a414f3 100644 --- a/cobbler/remote.py +++ b/cobbler/remote.py @@ -24,6 +24,7 @@ import random import base64 import string import traceback +import glob import api as cobbler_api import utils @@ -64,6 +65,11 @@ class CobblerXMLRPCInterface: def ping(self): return True + def update(self,token=None): + # ensure the config is up to date as of /now/ + self.api.deserialize() + return True + def get_user_from_token(self,token): if not TOKEN_CACHE.has_key(token): raise CX(_("invalid token: %s") % token) @@ -158,9 +164,36 @@ class CobblerXMLRPCInterface: return self._fix_none(data) + def get_kickstart_templates(self,token): + """ + Returns all of the kickstarts that are in use by the system. + """ + self.log("get_kickstart_templates",token=token) + self.check_access(token, "get_kickstart_templates") + return utils.get_kickstart_templates(self.api) + + def is_kickstart_in_use(self,ks,token): + self.log("is_kickstart_in_use",token=token) + self.check_access(token, "is_kickstart_in_use") + for x in self.api.profiles(): + if x.kickstart is not None and x.kickstart == ks: + return True + for x in self.api.systems(): + if x.kickstart is not None and x.kickstart == ks: + return True + return False + + def generate_kickstart(self,profile=None,system=None,REMOTE_ADDR=None,REMOTE_MAC=None): + 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): """ - Return the contents of /var/lib/cobbler/settings, which is a hash. + Return the contents of /etc/cobbler/settings, which is a hash. """ self.log("get_settings",token=token) return self.__get_all("settings") @@ -184,7 +217,7 @@ class CobblerXMLRPCInterface: self.api.add_system(system) - def register_mac(self,mac,token=None): + def register_mac(self,mac,profile,token=None): """ If allow_cgi_register_mac is enabled in settings, this allows kickstarts to add new system records for per-profile-provisioned @@ -193,18 +226,35 @@ class CobblerXMLRPCInterface: READ: https://fedorahosted.org/cobbler/wiki/AutoRegistration """ - if not self.api.settings().allow_cgi_mac_registration: + if mac is None: + # don't go further if not being called by anaconda return 1 - system = self.api.find_system(mac_address=mac) - if system is not None: + if not self.api.settings().register_new_installs: + # must be enabled in settings return 2 - obj = server.new_system(token) + 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) + obj = self.api.new_system() obj.set_profile(profile) - obj.set_name(mac.replace(":","_")) + name = mac.replace(":","_") + obj.set_name(name) obj.set_mac_address(mac, "intf0") - systems.add(obj,save=True) + obj.set_netboot_enabled(False) + self.api.add_system(obj) return 0 def disable_netboot(self,name,token=None): @@ -230,26 +280,28 @@ class CobblerXMLRPCInterface: systems.add(obj,save=True,with_triggers=False,with_sync=False,quick_pxe_update=True) return True - def run_post_install_triggers(self,name,token=None): + def run_install_triggers(self,mode,objtype,name,ip,token=None): + """ - This is a feature used to run the post install trigger. - It passes the system named "name" to the trigger. Disabled by default as - this requires public API access and is technically a read-write operation. + This is a feature used to run the pre/post install triggers. + See CobblerTriggers on Wiki for details """ - self.log("run_post_install_triggers",token=token) - # used by postinstalltrigger.cgi - self.api.clear() - self.api.deserialize() - if not self.api.settings().run_post_install_trigger: - # feature disabled! + self.log("run_install_triggers",token=token) + + if mode != "pre" and mode != "post": return False - systems = self.api.systems() - obj = systems.find(name=name) - if obj == None: - # system not found! + if objtype != "system" and objtype !="profile": return False - utils.run_triggers(obj, "/var/lib/cobbler/triggers/install/post/*") + + # the trigger script is called with name,mac, and ip as arguments 1,2, and 3 + # we do not do API lookups here because they are rather expensive at install + # 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]) + + return True def _refresh(self): @@ -418,25 +470,13 @@ class CobblerXMLRPCInterface: def get_random_mac(self,token=None): """ - Generate a random MAC address. - from xend/server/netif.py - Generate a random MAC address. - Uses OUI 00-16-3E, allocated to - Xensource, Inc. Last 3 fields are random. - return: MAC address string + Wrapper for utils.get_random_mac + + Used in the webui """ self.log("get_random_mac",token=None) self._refresh() - mac = [ 0x00, 0x16, 0x3e, - random.randint(0x00, 0x7f), - random.randint(0x00, 0xff), - random.randint(0x00, 0xff) ] - mac = ':'.join(map(lambda x: "%02x" % x, mac)) - systems = self.api.systems() - while ( systems.find(mac_address=mac) ): - mac = self.get_random_mac() - - return mac + return utils.get_random_mac(self.api) def _fix_none(self,data): """ @@ -561,10 +601,6 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): FIXME: currently looks for users in /etc/cobbler/auth.conf Would be very nice to allow for PAM and/or just Kerberos. """ - if not self.auth_enabled and input_user == "<system>": - return True - if self.auth_enabled and input_user == "<system>": - return False return self.api.authenticate(input_user,input_password) def __validate_token(self,token): @@ -579,11 +615,12 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): self.__invalidate_expired_tokens() self.__invalidate_expired_objects() - if not self.auth_enabled: - user = self.get_user_from_token(token) - if user == "<system>": - self.token_cache[token] = (time.time(), user) # update to prevent timeout - return True + #if not self.auth_enabled: + # user = self.get_user_from_token(token) + # # old stuff, preserving for future usage + # # if user == "<system>": + # # self.token_cache[token] = (time.time(), user) # update to prevent timeout + # # return True if self.token_cache.has_key(token): user = self.get_user_from_token(token) @@ -596,12 +633,54 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): self.log("invalid token",token=token) raise CX(_("invalid token: %s" % token)) + def __name_to_object(self,resource,name): + if resource.find("distro") != -1: + return self.api.find_distro(name) + if resource.find("profile") != -1: + return self.api.find_profile(name) + if resource.find("system") != -1: + return self.api.find_system(name) + if resource.find("repo") != -1: + return self.api.find_repo(name) + return None + + def check_access_no_fail(self,token,resource,arg1=None,arg2=None): + """ + This is called by the WUI to decide whether an element + is editable or not. It differs form check_access in that + it is supposed to /not/ log the access checks (TBA) and does + not raise exceptions. + """ + + need_remap = False + for x in [ "distro", "profile", "system", "repo" ]: + if arg1 is not None and resource.find(x) != -1: + need_remap = True + break + + if need_remap: + # we're called with an object name, but need an object + arg1 = self.__name_to_object(resource,arg1) + + try: + self.check_access(token,resource,arg1,arg2) + return True + except: + utils.log_exc(self.logger) + return False + def check_access(self,token,resource,arg1=None,arg2=None): validated = self.__validate_token(token) + user = self.get_user_from_token(token) if not self.auth_enabled: + # for public read-only XMLRPC, permit access + self.log("permitting read-only access") return True - return self.__authorize(token,resource,arg1,arg2) - + rc = self.__authorize(token,resource,arg1,arg2) + self.log("authorization result: %s" % rc) + if not rc: + raise CX(_("authorization failure for user %s" % user)) + return rc def login(self,login_user,login_password): """ @@ -621,7 +700,11 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): def __authorize(self,token,resource,arg1=None,arg2=None): user = self.get_user_from_token(token) - if self.api.authorize(user,resource,arg1,arg2): + args = [ resource, arg1, arg2 ] + self.log("calling authorize for resource %s" % args, user=user) + + rc = self.api.authorize(user,resource,arg1,arg2) + if rc: return True else: raise CX(_("user does not have access to resource: %s") % resource) @@ -664,6 +747,16 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): return self.object_cache[object_id][0] raise CX(_("No such object for ID: %s") % object_id) + def sync(self,token): + """ + Run sync code, which should complete before XMLRPC timeout. We can't + do reposync this way. Would be nice to send output over AJAX/other + later. + """ + self.log("sync",token=token) + self.check_access(token,"sync") + return self.api.sync() + def new_distro(self,token): """ Creates a new (unconfigured) distro object. It works something like @@ -767,41 +860,56 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): found = self.api.repos().find(name) return self.__store_object(found) - def save_distro(self,object_id,token): + def save_distro(self,object_id,token,editmode="bypass"): """ Saves a newly created or modified distro object to disk. """ self.log("save_distro",object_id=object_id,token=token) - self.check_access(token,"save_distro") obj = self.__get_object(object_id) - return self.api.distros().add(obj,save=True) + self.check_access(token,"save_distro",obj) + if editmode == "new": + return self.api.distros().add(obj,save=True,check_for_duplicate_names=True) + else: + return self.api.distros().add(obj,save=True) - def save_profile(self,object_id,token): + def save_profile(self,object_id,token,editmode="bypass"): """ Saves a newly created or modified profile object to disk. """ self.log("save_profile",token=token,object_id=object_id) - self.check_access(token,"save_profile") obj = self.__get_object(object_id) - return self.api.profiles().add(obj,save=True) + self.check_access(token,"save_profile",obj) + if editmode == "new": + return self.api.profiles().add(obj,save=True,check_for_duplicate_names=True) + else: + return self.api.profiles().add(obj,save=True) - def save_system(self,object_id,token): + def save_system(self,object_id,token,editmode="bypass"): """ Saves a newly created or modified system object to disk. """ self.log("save_system",token=token,object_id=object_id) - self.check_access(token,"save_system") obj = self.__get_object(object_id) - return self.api.systems().add(obj,save=True) + self.check_access(token,"save_system",obj) + if editmode == "new": + return self.api.systems().add(obj,save=True,check_for_duplicate_names=True,check_for_duplicate_netinfo=True) + elif editmode == "edit": + return self.api.systems().add(obj,save=True,check_for_duplicate_netinfo=True) + else: + return self.api.systems().add(obj,save=True) + - def save_repo(self,object_id,token=None): + def save_repo(self,object_id,token=None,editmode="bypass"): """ Saves a newly created or modified repo object to disk. """ self.log("save_repo",object_id=object_id,token=token) - self.check_access(token,"save_repo") obj = self.__get_object(object_id) - return self.api.repos().add(obj,save=True) + self.check_access(token,"save_repo",obj) + if editmode == "new": + return self.api.repos().add(obj,save=True,check_for_duplicate_names=True) + else: + return self.api.repos().add(obj,save=True) def copy_distro(self,object_id,newname,token=None): """ @@ -874,8 +982,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): Allows modification of certain attributes on newly created or existing distro object handle. """ - self.check_access(token, "modify_distro", attribute, arg) obj = self.__get_object(object_id) + self.check_access(token, "modify_distro", obj, attribute) return self.__call_method(obj, attribute, arg) def modify_profile(self,object_id,attribute,arg,token): @@ -883,8 +991,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): Allows modification of certain attributes on newly created or existing profile object handle. """ - self.check_access(token, "modify_profile", attribute, arg) obj = self.__get_object(object_id) + self.check_access(token, "modify_profile", obj, attribute) return self.__call_method(obj, attribute, arg) def modify_system(self,object_id,attribute,arg,token): @@ -892,8 +1000,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): Allows modification of certain attributes on newly created or existing system object handle. """ - self.check_access(token, "modify_system", attribute, arg) obj = self.__get_object(object_id) + self.check_access(token, "modify_system", obj, attribute) return self.__call_method(obj, attribute, arg) def modify_repo(self,object_id,attribute,arg,token): @@ -901,8 +1009,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): Allows modification of certain attributes on newly created or existing repo object handle. """ - self.check_access(token, "modify_repo", attribute, arg) obj = self.__get_object(object_id) + self.check_access(token, "modify_repo", obj, attribute) return self.__call_method(obj, attribute, arg) def remove_distro(self,name,token,recursive=1): @@ -910,7 +1018,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): Deletes a distro from a collection. Note that this just requires the name of the distro, not a handle. """ - self.log("remove_distro",name=name,token=token) + self.log("remove_distro (%s)" % recursive,name=name,token=token) self.check_access(token, "remove_distro", name) rc = self.api._config.distros().remove(name,recursive=True) return rc @@ -918,74 +1026,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): def remove_profile(self,name,token,recursive=1): """ Deletes a profile from a collection. Note that this just requires the name - of the profile, not a handle. """ - self.log("remove_profile",name=name,token=token) - self.check_access(token, "remove_profile", name) - rc = self.api._config.profiles().remove(name,recursive=True) - return rc - - def remove_system(self,name,token): - """ - Deletes a system from a collection. Note that this just requires the name - of the system, not a handle. - """ - self.log("remove_system",name=name,token=token) - self.check_access(token, "remove_system", name) - rc = self.api._config.systems().remove(name) - return rc - - def remove_repo(self,name,token): - """ - Deletes a repo from a collection. Note that this just requires the name - of the repo, not a handle. - """ - self.log("remove_repo",name=name,token=token) - self.check_access(token, "remove_repo", name) - rc = self.api._config.repos().remove(name) - return rc - - def sync(self,token): - """ - Applies changes in Cobbler to the filesystem. - Editing a leaf-node object (like a system) does not require - this, but if updating a upper-level object or a kickstart file, - running sync at the end of operations is a good idea. A typical - cobbler sync may take anywhere between a few seconds and several - minutes, so user interfaces should be programmed accordingly. - Future versions of cobbler may understand how to do a cascade sync - on object edits making explicit calls to sync redundant. - """ - self.log("sync",token=token) - self.check_access(token, "sync") - return self.api.sync() - - def reposync(self,repos=[],token=None): - """ - Updates one or more mirrored yum repositories. - reposync is very slow and probably should not be used - through the XMLRPC API, setting up reposync on nightly cron is better. - """ - self.log("reposync",token=token,name=repos) - self.check_access(token, "reposync", repos) - return self.api.reposync(repos) - - def import_tree(self,mirror_url,mirror_name,network_root=None,token=None): - """ - I'm exposing this in the XMLRPC API for consistancy but as this - can be a very long running operation usage is /not/ recommended. - It would be better to use the CLI. See documentation in api.py. - This command may be removed from the API in a future release. - """ - self.log("import_tree",name=mirror_name,token=token) - self.check_access(token, "import_tree") - return self.api.import_tree(mirror_url,mirror_name,network_root) - - def get_kickstart_templates(self,token): - """ - Returns all of the kickstarts that are in use by the system. - """ - self.log("get_kickstart_templates",token=token) self.check_access(token, "get_kickstart_templates") files = {} for x in self.api.profiles(): @@ -998,7 +1039,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): def read_or_write_kickstart_template(self,kickstart_file,is_read,new_data,token): - """ + """ Allows the WebUI to be used as a kickstart file editor. For security reasons we will only allow kickstart files to be edited if they reside in /var/lib/cobbler/kickstarts/ or /etc/cobbler. This limits the damage @@ -1028,9 +1069,16 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): fileh.close() return data else: - fileh = open(kickstart_file,"w+") - fileh.write(new_data) - fileh.close() + if new_data == -1: + # delete requested + if not self.is_kickstart_in_use(kickstart_file,token): + os.remove(kickstart_file) + else: + raise CX(_("attempt to delete in-use file")) + else: + fileh = open(kickstart_file,"w+") + fileh.write(new_data) + fileh.close() return True diff --git a/cobbler/serializable.py b/cobbler/serializable.py index ba8f015..ad9e64e 100644 --- a/cobbler/serializable.py +++ b/cobbler/serializable.py @@ -13,7 +13,7 @@ along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. """ -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import exceptions class Serializable: diff --git a/cobbler/serializer.py b/cobbler/serializer.py index ae9f18c..ac98412 100644 --- a/cobbler/serializer.py +++ b/cobbler/serializer.py @@ -15,7 +15,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import errno import os -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ import fcntl from cexceptions import * @@ -59,10 +59,8 @@ def serialize_item(collection, item): storage_module = __get_storage_module(collection.collection_type()) save_fn = getattr(storage_module, "serialize_item", None) if save_fn is None: - # print "DEBUG: WARNING: full serializer" rc = storage_module.serialize(collection) else: - # print "DEBUG: partial serializer" rc = save_fn(collection,item) __release_lock() return rc @@ -75,10 +73,8 @@ def serialize_delete(collection, item): storage_module = __get_storage_module(collection.collection_type()) delete_fn = getattr(storage_module, "serialize_delete", None) if delete_fn is None: - # print "DEBUG: full delete" rc = storage_module.serialize(collection) else: - # print "DEBUG: partial delete" rc = delete_fn(collection,item) __release_lock() return rc diff --git a/cobbler/services.py b/cobbler/services.py new file mode 100644 index 0000000..1cb3864 --- /dev/null +++ b/cobbler/services.py @@ -0,0 +1,96 @@ +# Mod Python service functions for Cobbler's public interface +# (aka cool stuff that works with wget) +# +# Copyright 2007 Albert P. Tobey <tobert@gmail.com> +# additions: Michael DeHaan <mdehaan@redhat.com> +# +# This software may be freely redistributed under the terms of the GNU +# general public license. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import exceptions +import xmlrpclib +import os +import traceback +import string +import sys +import time + +def log_exc(apache): + """ + Log active traceback to logfile. + """ + (t, v, tb) = sys.exc_info() + apache.log_error("Exception occured: %s" % t ) + apache.log_error("Exception value: %s" % v) + apache.log_error("Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tb)))) + +class CobblerSvc(object): + """ + Interesting mod python functions are all keyed off the parameter + mode, which defaults to index. All options are passed + as parameters into the function. + """ + def __init__(self, server=None, apache=None): + self.server = server + self.apache = apache + self.remote = None + + def __xmlrpc_setup(self): + """ + Sets up the connection to the Cobbler XMLRPC server. + This is the version that does not require logins. + """ + self.remote = xmlrpclib.Server(self.server, allow_none=True) + self.remote.update() + + def modes(self): + """ + Returns a list of methods in this object that can be run as web + modes. + """ + retval = list() + for m in dir(self): + func = getattr( self, m ) + if hasattr(func, 'exposed') and getattr(func,'exposed'): + retval.append(m) + return retval + + def index(self,**args): + return "no mode specified" + + def ks(self,profile=None,system=None,REMOTE_ADDR=None,REMOTE_MAC=None,**rest): + """ + Generate kickstart files... + """ + self.__xmlrpc_setup() + data = self.remote.generate_kickstart(profile,system,REMOTE_ADDR,REMOTE_MAC) + return u"%s" % data + + def trig(self,mode="?",profile=None,system=None,REMOTE_ADDR=None,**rest): + """ + Hook to call install triggers. + """ + self.__xmlrpc_setup() + ip = REMOTE_ADDR + if profile: + rc = self.remote.run_install_triggers(mode,"profile",profile,ip) + else: + rc = self.remote.run_install_triggers(mode,"system",system,ip) + return str(rc) + + def nopxe(self,system=None,**rest): + self.__xmlrpc_setup() + return str(self.remote.disable_netboot(system)) + + # ======================================================= + # list of functions that are callable via mod_python: + modes.exposed = False + index.exposed = True + ks.exposed = True + trig.exposed = True + + diff --git a/cobbler/settings.py b/cobbler/settings.py index 8c8be03..4670ab0 100644 --- a/cobbler/settings.py +++ b/cobbler/settings.py @@ -14,7 +14,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import serializable import utils -from rhpl.translate import _, N_, textdomain, utf8 +from utils import _ TESTMODE = False @@ -22,24 +22,36 @@ TESTMODE = False # we need. DEFAULTS = { - "allow_cgi_mac_registration" : 0, - "allow_cgi_profile_change" : 0, + "allow_duplicate_macs" : 0, + "allow_duplicate_ips" : 0, + "bind_bin" : "/usr/sbin/named", "bootloaders" : { "standard" : "/usr/lib/syslinux/pxelinux.0", "ia64" : "/var/lib/cobbler/elilo-3.6-ia64.efi" }, + "cobbler_master" : '', "default_kickstart" : "/etc/cobbler/default.ks", "default_virt_bridge" : "xenbr0", "default_virt_type" : "auto", "default_virt_file_size" : "5", "default_virt_ram" : "512", + "default_ownership" : "admin", "dhcpd_conf" : "/etc/dhcpd.conf", "dhcpd_bin" : "/usr/sbin/dhcpd", "dnsmasq_bin" : "/usr/sbin/dnsmasq", "dnsmasq_conf" : "/etc/dnsmasq.conf", "httpd_bin" : "/usr/sbin/httpd", "http_port" : "80", - "kerberos_realm" : "example.org", + "isc_set_host_name" : 0, + "ldap_server" : "grimlock.devel.redhat.com", + "ldap_base_dn" : "DC=devel,DC=redhat,DC=com", + "ldap_port" : 389, + "ldap_tls" : "on", + "ldap_anonymous_bind" : 1, + "ldap_search_bind_dn" : '', + "ldap_search_passwd" : '', + "ldap_search_prefix" : 'uid=', + "kerberos_realm" : "EXAMPLE.COM", "kernel_options" : { "lang" : " ", "text" : None, @@ -47,18 +59,25 @@ DEFAULTS = { }, "manage_dhcp" : 0, "manage_dhcp_mode" : "isc", + "manage_dns" : 0, + "manage_forward_zones" : [], + "manage_reverse_zones" : [], + "named_conf" : "/etc/named.conf", "next_server" : "127.0.0.1", + "omapi_enabled" : 0, + "omapi_port" : 647, + "omshell_bin" : "/usr/bin/omshell", "pxe_just_once" : 0, - "run_post_install_trigger" : 0, + "register_new_installs" : 0, + "run_install_triggers" : 1, "server" : "127.0.0.1", "snippetsdir" : "/var/lib/cobbler/snippets", "syslog_port" : 25150, - "tftpboot" : "/tftpboot", "tftpd_bin" : "/usr/sbin/in.tftpd", "tftpd_conf" : "/etc/xinetd.d/tftp", "webdir" : "/var/www/cobbler", "xmlrpc_port" : 25151, - "xmlrpc_rw_enabled" : 0, + "xmlrpc_rw_enabled" : 1, "xmlrpc_rw_port" : 25152, "yum_post_install_mirror" : 1, "yumdownloader_flags" : "--resolve" @@ -101,7 +120,9 @@ class Settings(serializable.Serializable): if datastruct is None: print _("warning: not loading empty structure for %s") % self.filename() return + self._attributes = datastruct + return self def __getattr__(self,name): @@ -119,11 +140,3 @@ class Settings(serializable.Serializable): else: raise AttributeError, name -if __name__ == "__main__": - # used to save a settings file to /var/lib/cobbler/settings, for purposes of - # including a new updated settings file in the RPM without remembering how - # to format lots of YAML. - import yaml - print yaml.dump(DEFAULTS) - - diff --git a/cobbler/templar.py b/cobbler/templar.py new file mode 100644 index 0000000..fa401b8 --- /dev/null +++ b/cobbler/templar.py @@ -0,0 +1,189 @@ +""" +Cobbler uses Cheetah templates for lots of stuff, but there's +some additional magic around that to deal with snippets/etc. +(And it's not spelled wrong!) + +Copyright 2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import glob +import utils +from cexceptions import * +from Cheetah.Template import Template + +class Templar: + + def __init__(self,config): + """ + Constructor + """ + self.config = config + self.api = config.api + self.settings = config.settings() + self.cache = {} + + def render(self, data_input, search_table, out_path, subject=None): + """ + Render data_input back into a file. + data_input is either a string or a filename + search_table is a hash of metadata keys and values + out_path if not-none writes the results to a file + (though results are always returned) + subject is a profile or system object, if available (for snippet eval) + """ + + if type(data_input) != str: + raw_data = data_input.read() + else: + raw_data = data_input + + # backward support for Cobbler's legacy (and slightly more readable) + # template syntax. + raw_data = raw_data.replace("TEMPLATE::","$") + + # replace snippets with the proper Cheetah include directives prior to processing. + # see Wiki for full details on how snippets operate. + snippet_results = "" + for line in raw_data.split("\n"): + line = self.replace_snippets(line,subject) + snippet_results = "\n".join((snippet_results, line)) + raw_data = snippet_results + + # HACK: the ksmeta field may contain nfs://server:/mount in which + # case this is likely WRONG for kickstart, which needs the NFS + # directive instead. Do this to make the templates work. + newdata = "" + if search_table.has_key("tree") and search_table["tree"].startswith("nfs://"): + for line in data.split("\n"): + if line.find("--url") != -1 and line.find("url ") != -1: + rest = search_table["tree"][6:] # strip off "nfs://" part + try: + (server, dir) = rest.split(":",2) + except: + raise CX(_("Invalid syntax for NFS path given during import: %s" % search_table["tree"])) + line = "nfs --server %s --dir %s" % (server,dir) + # but put the URL part back in so koan can still see + # what the original value was + line = line + "\n" + "#url --url=%s" % search_table["tree"] + newdata = newdata + line + "\n" + raw_data = newdata + + # tell Cheetah not to blow up if it can't find a symbol for something + raw_data = "#errorCatcher Echo\n" + raw_data + + # now do full templating scan, where we will also templatify the snippet insertions + t = Template(source=raw_data, searchList=[search_table]) + try: + data_out = str(t) + except: + print _("There appears to be an formatting error in the template file.") + print _("For completeness, the traceback from Cheetah has been included below.") + raise + + # now apply some magic post-filtering that is used by cobbler import and some + # other places, but doesn't use Cheetah. Forcing folks to double escape + # things would be very unwelcome. + + for x in search_table: + if type(search_table[x]) == str: + data_out = data_out.replace("@@%s@@" % x, search_table[x]) + + # remove leading newlines which apparently breaks AutoYAST ? + if data_out.startswith("\n"): + data_out = data_out.strip() + + if out_path is not None: + utils.mkdir(os.path.dirname(out_path)) + fd = open(out_path, "w+") + fd.write(data_out) + fd.close() + + return data_out + + def replace_snippets(self,line,subject): + """ + Replace all SNIPPET:: syntaxes on a line with the + results of evaluating the snippet, taking care not + to replace tabs with spaces or anything like that + """ + tokens = line.split(None) + for t in tokens: + if t.startswith("SNIPPET::"): + snippet_name = t.replace("SNIPPET::","") + line = line.replace(t,self.eval_snippet(snippet_name,subject)) + return line + + def eval_snippet(self,name,subject): + """ + Replace SNIPPET::foo with contents of files: + Use /var/lib/cobbler/snippets/per_system/$name/$sysname + /var/lib/cobbler/snippets/per_profile/$name/$proname + /var/lib/cobbler/snippets/$name + in order... (first one wins) + """ + + sd = self.settings.snippetsdir + default_path = "%s/%s" % (sd,name) + + if subject is None: + if os.path.exists(default_path): + return self.slurp(default_path) + else: + return self.slurp(None) + + + if subject.COLLECTION_TYPE == "system": + profile = self.api.find_profile(name=subject.profile) + sys_path = "%s/per_system/%s/%s" % (sd,name,subject.name) + pro_path = "%s/per_profile/%s/%s" % (sd,name,profile.name) + if os.path.exists(sys_path): + return self.slurp(sys_path) + elif os.path.exists(pro_path): + return self.slurp(pro_path) + elif os.path.exists(default_path): + return self.slurp(default_path) + else: + return self.slurp(None) + + if subject.COLLECTION_TYPE == "profile": + pro_path = "%s/per_profile/%s/%s" % (sd,name,subject.name) + if os.path.exists(pro_path): + return self.slurp(pro_path) + elif os.path.exists(default_path): + return self.slurp(default_path) + else: + return self.slurp(None) + + return self.slurp(None) + + def slurp(self,filename): + """ + Get the contents of a filename but if none is specified + just include some generic error text for the rendered + template. + """ + + if filename is None: + return "# error: no snippet data found" + + # potentially eliminate a ton of system calls if syncing + # thousands of systems that use the same template + if self.cache.has_key(filename): + return self.cache[filename] + + fd = open(filename,"r") + data = fd.read() + self.cache[filename] = data + fd.close() + + return data diff --git a/cobbler/utils.py b/cobbler/utils.py index 069d440..de00150 100644 --- a/cobbler/utils.py +++ b/cobbler/utils.py @@ -17,12 +17,19 @@ import os import re import socket import glob +import random import sub_process import shutil import string import traceback +import errno +import logging from cexceptions import * -from rhpl.translate import _, N_, textdomain, utf8 + +#placeholder for translation +def _(foo): + return foo + MODULE_CACHE = {} @@ -31,6 +38,18 @@ MODULE_CACHE = {} _re_kernel = re.compile(r'vmlinuz(.*)') _re_initrd = re.compile(r'initrd(.*).img') +def setup_logger(name): + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + try: + ch = logging.FileHandler("/var/log/cobbler/cobbler.log") + except: + raise CX(_("No write permissions on log file. Are you root?")) + ch.setLevel(logging.INFO) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(message)s") + ch.setFormatter(formatter) + logger.addHandler(ch) + return logger def log_exc(logger): """ @@ -106,6 +125,26 @@ def is_mac(strdata): return True return False +def get_random_mac(api_handle): + """ + Generate a random MAC address. + from xend/server/netif.py + Generate a random MAC address. + Uses OUI 00-16-3E, allocated to + Xensource, Inc. Last 3 fields are random. + return: MAC address string + """ + mac = [ 0x00, 0x16, 0x3e, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff) ] + mac = ':'.join(map(lambda x: "%02x" % x, mac)) + systems = api_handle.systems() + while ( systems.find(mac_address=mac) ): + mac = get_random_mac(api_handle) + + return mac + def resolve_ip(strdata): """ @@ -230,6 +269,22 @@ def find_kickstart(url): return url return None +def input_string_or_list(options,delim=","): + """ + Accepts a delimited list of stuff or a list, but always returns a list. + """ + if options is None or options == "delete": + return [] + elif type(options) == list: + return options + elif type(options) == str: + tokens = options.split(delim) + if delim == ",": + tokens = [t.lstrip().rstrip() for t in tokens] + return tokens + else: + raise CX(_("invalid input type")) + def input_string_or_hash(options,delim=","): """ Older cobbler files stored configurations in a flat way, such that all values for strings. @@ -240,7 +295,7 @@ def input_string_or_hash(options,delim=","): if options == "<<inherit>>": options = {} - if options is None: + if options is None or options == "delete": return (True, {}) elif type(options) == list: raise CX(_("No idea what to do with list: %s") % options) @@ -261,7 +316,7 @@ def input_string_or_hash(options,delim=","): options.pop('',None) return (True, options) else: - raise CX(_("Foreign options type")) + raise CX(_("invalid input type")) def grab_tree(api_handle, obj): """ @@ -276,19 +331,14 @@ def grab_tree(api_handle, obj): results.append(settings) return results -def blender(api_handle,remove_hashes, root_obj, blend_cache=None): +def blender(api_handle,remove_hashes, root_obj): """ Combine all of the data in an object tree from the perspective of that point on the tree, and produce a merged hash containing consolidated data. """ - cache_enabled = False # FIXME: disabled for now as there a few bugs in this impl. - blend_key = "%s/%s/%s" % (root_obj.TYPE_NAME, root_obj.name, remove_hashes) - if cache_enabled and blend_cache is not None: - if blend_cache.has_key(blend_key): - return blend_cache[blend_key] settings = api_handle.settings() tree = grab_tree(api_handle, root_obj) @@ -334,8 +384,18 @@ def blender(api_handle,remove_hashes, root_obj, blend_cache=None): if remove_hashes: results = flatten(results) - if cache_enabled and blend_cache is not None: - blend_cache[blend_key] = results + # add in some variables for easier templating + # as these variables change based on object type + if results.has_key("interfaces"): + results["system_name"] = results["name"] + results["profile_name"] = results["profile"] + results["distro_name"] = results["distro"] + elif results.has_key("distro"): + results["profile_name"] = results["name"] + results["distro_name"] = results["distro"] + elif results.has_key("kernel"): + results["distro_name"] = results["name"] + return results def flatten(data): @@ -399,6 +459,24 @@ def __consolidate(node,results): else: results[field] = data_item + # now if we have any "!foo" results in the list, delete corresponding + # key entry "foo", and also the entry "!foo", allowing for removal + # of kernel options set in a distro later in a profile, etc. + + hash_removals(results,"kernel_options") + hash_removals(results,"ks_meta") + +def hash_removals(results,subkey): + if not results.has_key(subkey): + return + scan = results[subkey].keys() + for k in scan: + if k.startswith("!") and k != "!": + remove_me = k[1:] + if results[subkey].has_key(remove_me): + del results[subkey][remove_me] + del results[subkey][k] + def hash_to_string(hash): """ Convert a hash to a printable string. @@ -417,7 +495,7 @@ def hash_to_string(hash): buffer = buffer + str(key) + "=" + str(value) + " " return buffer -def run_triggers(ref,globber): +def run_triggers(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 @@ -434,10 +512,12 @@ def run_triggers(ref,globber): # skip .rpmnew files that may have been installed # in the triggers directory continue + arglist = [ file ] if ref: - rc = sub_process.call([file,ref.name], shell=False) - else: - rc = sub_process.call([file], shell=False) + arglist.append(ref.name) + for x in additional: + arglist.append(x) + rc = sub_process.call(arglist, shell=False) except: print _("Warning: failed to execute trigger: %s" % file) continue @@ -452,9 +532,6 @@ def fix_mod_python_select_submission(repos): which doesn't seem to happen on all versions of python/mp. """ - if str(repos).find("Field(") == -1: - return repos # no hack needed - # should be nice regex, but this is readable :) repos = str(repos) repos = repos.replace("'repos'","") @@ -468,3 +545,306 @@ def fix_mod_python_select_submission(repos): repos = repos.lstrip().rstrip() return repos +def check_dist(): + """ + Determines what distro we're running under. + """ + if os.path.exists("/etc/debian_version"): + return "debian" + else: + # valid for Fedora and all Red Hat / Fedora derivatives + return "redhat" + +def os_release(): + + if check_dist() == "redhat": + + if not os.path.exists("/bin/rpm"): + return ("unknown", 0) + args = ["/bin/rpm", "-q", "--whatprovides", "redhat-release"] + cmd = sub_process.Popen(args,shell=False,stdout=sub_process.PIPE) + data = cmd.communicate()[0] + data = data.rstrip().lower() + make = "other" + if data.find("redhat") != -1: + make = "redhat" + elif data.find("centos") != -1: + make = "centos" + elif data.find("fedora") != -1: + make = "fedora" + version = data.split("release-")[-1] + rest = 0 + if version.find("-"): + parts = version.split("-") + version = parts[0] + rest = parts[1] + return (make, float(version), rest) + elif check_dist() == "debian": + fd = open("/etc/debian_version") + parts = fd.read().split(".") + version = parts[0] + rest = parts[1] + make = "debian" + return (make, float(version), rest) + else: + return ("unknown",0) + +def tftpboot_location(): + + # if possible, read from TFTP config file to get the location + if os.path.exists("/etc/xinetd.d/tftp"): + fd = open("/etc/xinetd.d/tftp") + lines = fd.read().split("\n") + for line in lines: + if line.find("server_args") != -1: + tokens = line.split(None) + mark = False + for t in tokens: + if t == "-s": + mark = True + elif mark: + return t + + # otherwise, guess based on the distro + (make,version,rest) = os_release() + if make == "fedora" and version >= 9: + return "/var/lib/tftpboot" + return "/tftpboot" + +def linkfile(src, dst): + """ + Attempt to create a link dst that points to src. Because file + systems suck we attempt several different methods or bail to + copyfile() + """ + + try: + return os.link(src, dst) + except (IOError, OSError): + pass + + try: + return os.symlink(src, dst) + except (IOError, OSError): + pass + + return copyfile(src, dst) + +def copyfile(src,dst): + try: + return shutil.copyfile(src,dst) + except: + if not os.access(src,os.R_OK): + raise CX(_("Cannot read: %s") % src) + if not os.path.samefile(src,dst): + # accomodate for the possibility that we already copied + # the file as a symlink/hardlink + raise CX(_("Error copying %(src)s to %(dst)s") % { "src" : src, "dst" : dst}) + +def rmfile(path): + try: + os.unlink(path) + return True + except OSError, ioe: + if not ioe.errno == errno.ENOENT: # doesn't exist + traceback.print_exc() + raise CX(_("Error deleting %s") % path) + return True + +def rmtree_contents(path): + what_to_delete = glob.glob("%s/*" % path) + for x in what_to_delete: + rmtree(x) + +def rmtree(path): + try: + if os.path.isfile(path): + return rmfile(path) + else: + return shutil.rmtree(path,ignore_errors=True) + except OSError, ioe: + traceback.print_exc() + if not ioe.errno == errno.ENOENT: # doesn't exist + raise CX(_("Error deleting %s") % path) + return True + +def mkdir(path,mode=0777): + try: + return os.makedirs(path,mode) + except OSError, oe: + if not oe.errno == 17: # already exists (no constant for 17?) + traceback.print_exc() + print oe.errno + raise CX(_("Error creating") % path) + +def set_repos(self,repos,bypass_check=False): + # WARNING: hack + repos = fix_mod_python_select_submission(repos) + + # allow the magic inherit string to persist + if repos == "<<inherit>>": + # FIXME: this is not inheritable in the WebUI presently ? + self.repos = "<<inherit>>" + return + + # store as an array regardless of input type + if repos is None: + repolist = [] + elif type(repos) != list: + # allow backwards compatibility support of string input + repolist = repos.split(None) + else: + repolist = repos + + # make sure there are no empty strings + try: + repolist.remove('') + except: + pass + + self.repos = [] + + # if any repos don't exist, fail the set operation + # unless called from the deserializer stage in which + # case we have a soft error that check can report + ok = True + for r in repolist: + if bypass_check: + self.repos.append(r) + else: + if self.config.repos().find(name=r) is not None: + self.repos.append(r) + else: + raise CX(_("repo %s is not defined") % r) + + return True + +def set_virt_file_size(self,num): + """ + For Virt only. + Specifies the size of the virt image in gigabytes. + Older versions of koan (x<0.6.3) interpret 0 as "don't care" + Newer versions (x>=0.6.4) interpret 0 as "no disks" + """ + # num is a non-negative integer (0 means default) + # can also be a comma seperated list -- for usage with multiple disks + + if num == "<<inherit>>": + self.virt_file_size = "<<inherit>>" + return True + + if type(num) == str and num.find(",") != -1: + tokens = num.split(",") + for t in tokens: + # hack to run validation on each + self.set_virt_file_size(t) + # if no exceptions raised, good enough + self.virt_file_size = num + return True + + try: + inum = int(num) + if inum != float(num): + return CX(_("invalid virt file size")) + if inum >= 0: + self.virt_file_size = inum + return True + raise CX(_("invalid virt file size")) + except: + raise CX(_("invalid virt file size")) + return True + +def set_virt_ram(self,num): + """ + For Virt only. + Specifies the size of the Virt RAM in MB. + 0 tells Koan to just choose a reasonable default. + """ + + if num == "<<inherit>>": + self.virt_ram = "<<inherit>>" + return True + + # num is a non-negative integer (0 means default) + try: + inum = int(num) + if inum != float(num): + return CX(_("invalid virt ram size")) + if inum >= 0: + self.virt_ram = inum + return True + return CX(_("invalid virt ram size")) + except: + return CX(_("invalid virt ram size")) + return True + +def set_virt_type(self,vtype): + """ + Virtualization preference, can be overridden by koan. + """ + + if vtype == "<<inherit>>": + self.virt_type == "<<inherit>>" + return True + + if vtype.lower() not in [ "qemu", "xenpv", "xenfv", "vmware", "auto" ]: + raise CX(_("invalid virt type")) + self.virt_type = vtype + return True + +def set_virt_bridge(self,vbridge): + """ + The default bridge for all virtual interfaces under this profile. + """ + self.virt_bridge = vbridge + return True + +def set_virt_path(self,path): + """ + Virtual storage location suggestion, can be overriden by koan. + """ + self.virt_path = path + return True + +def set_virt_cpus(self,num): + """ + For Virt only. Set the number of virtual CPUs to give to the + virtual machine. This is fed to virtinst RAW, so cobbler + will not yelp if you try to feed it 9999 CPUs. No formatting + like 9,999 please :) + """ + if num == "<<inherit>>": + self.virt_cpus = "<<inherit>>" + return True + + try: + num = int(str(num)) + except: + raise CX(_("invalid number of virtual CPUs")) + + self.virt_cpus = num + return True + +def get_kickstart_templates(api): + files = {} + for x in api.profiles(): + if x.kickstart is not None and x.kickstart != "" and x.kickstart != "<<inherit>>": + if os.path.exists(x.kickstart): + files[x.kickstart] = 1 + for x in api.systems(): + if x.kickstart is not None and x.kickstart != "" and x.kickstart != "<<inherit>>": + if os.path.exists(x.kickstart): + files[x.kickstart] = 1 + for x in glob.glob("/var/lib/cobbler/kickstarts/*"): + files[x] = 1 + for x in glob.glob("/etc/cobbler/*.ks"): + files[x] = 1 + + return files.keys() + + + +if __name__ == "__main__": + # print redhat_release() + print tftpboot_location() + diff --git a/cobbler/webui/CobblerWeb.py b/cobbler/webui/CobblerWeb.py index 9a0cf90..d272281 100644 --- a/cobbler/webui/CobblerWeb.py +++ b/cobbler/webui/CobblerWeb.py @@ -60,6 +60,9 @@ class CobblerWeb(object): # validate that our token is still good try: self.remote.token_check(self.token) + self.username = self.remote.get_user_from_token(self.token) + # ensure config is up2date + self.remote.update(self.token) return True except Exception, e: if str(e).find("invalid token") != -1: @@ -78,6 +81,8 @@ class CobblerWeb(object): log_exc(self.apache) return False self.password = None # don't need it anymore, get rid of it + # ensure configuration is up2date + self.remote.update(self.token) return True # login failed @@ -162,15 +167,27 @@ class CobblerWeb(object): input_distro = None if name is not None: input_distro = self.remote.get_distro(name, True) + can_edit = self.remote.check_access_no_fail(self.token,"modify_distro",name) + else: + can_edit = self.remote.check_access_no_fail(self.token,"new_distro",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( 'distro_edit.tmpl', { + 'user' : self.username, 'edit' : True, + 'editable' : can_edit, 'distro': input_distro, } ) def distro_save(self,name=None,oldname=None,new_or_edit=None,editmode='edit',kernel=None, - initrd=None,kopts=None,ksmeta=None,arch=None,breed=None, - delete1=None,delete2=None,**args): + initrd=None,kopts=None,ksmeta=None,owners=None,arch=None,breed=None, + delete1=None,delete2=None,recursive=False,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -182,8 +199,12 @@ class CobblerWeb(object): # handle deletes as a special case if new_or_edit == 'edit' and delete1 and delete2: - try: - self.remote.remove_distro(name,self.token,1) # recursive + try: + if recursive is None: + self.remote.remove_distro(name,self.token,False) + else: + self.remote.remove_distro(name,self.token,True) + except Exception, e: return self.error_page("could not delete %s, %s" % (name,str(e))) return self.distro_list() @@ -220,11 +241,14 @@ class CobblerWeb(object): self.remote.modify_distro(distro, 'kopts', kopts, self.token) if ksmeta: self.remote.modify_distro(distro, 'ksmeta', ksmeta, self.token) + if owners: + self.remote.modify_distro(distro, 'owners', owners, self.token) if arch: self.remote.modify_distro(distro, 'arch', arch, self.token) if breed: self.remote.modify_distro(distro, 'breed', breed, self.token) - self.remote.save_distro(distro, self.token) + # now time to save, do we want to run duplication checks? + self.remote.save_distro(distro, self.token, editmode) except Exception, e: log_exc(self.apache) return self.error_page("Error while saving distro: %s" % str(e)) @@ -288,8 +312,9 @@ class CobblerWeb(object): def system_save(self,name=None,oldname=None,editmode="edit",profile=None, new_or_edit=None, - kopts=None, ksmeta=None, server_override=None, netboot='n', - delete1=None, delete2=None, **args): + kopts=None, ksmeta=None, owners=None, server_override=None, netboot='n', + virtpath=None,virtram=None,virttype=None,virtcpus=None,virtfilesize=None,delete1=None, delete2=None, **args): + if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -332,11 +357,26 @@ class CobblerWeb(object): self.remote.modify_system(system, 'kopts', kopts, self.token) if ksmeta: self.remote.modify_system(system, 'ksmeta', ksmeta, self.token) + if owners: + self.remote.modify_system(system, 'owners', owners, self.token) if netboot: self.remote.modify_system(system, 'netboot-enabled', netboot, self.token) if server_override: self.remote.modify_system(system, 'server', server_override, self.token) + if virtfilesize: + self.remote.modify_system(system, 'virt-file-size', virtfilesize, self.token) + if virtcpus: + self.remote.modify_system(system, 'virt-cpus', virtcpus, self.token) + if virtram: + self.remote.modify_system(system, 'virt-ram', virtram, self.token) + if virttype: + self.remote.modify_system(system, 'virt-type', virttype, self.token) + + if virtpath: + self.remote.modify_system(system, 'virt-path', virtpath, self.token) + + for x in range(0,7): interface = "intf%s" % x macaddress = args.get("macaddress-%s" % interface, "") @@ -364,8 +404,7 @@ class CobblerWeb(object): mods["gateway-%s" % interface] = gateway self.remote.modify_system(system,'modify-interface', mods, self.token) - # now commit the edits - self.remote.save_system( system, self.token) + self.remote.save_system(system, self.token, editmode) except Exception, e: log_exc(self.apache) @@ -390,9 +429,20 @@ class CobblerWeb(object): 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() } ) @@ -427,10 +477,21 @@ class CobblerWeb(object): input_profile = None if name is not None: - input_profile = self.remote.get_profile(name,True) + input_profile = self.remote.get_profile(name,True) + can_edit = self.remote.check_access_no_fail(self.token,"modify_profile",name) + else: + can_edit = self.remote.check_access_no_fail(self.token,"new_profile",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( 'profile_edit.tmpl', { + 'user' : self.username, 'edit' : True, + 'editable' : can_edit, 'profile': input_profile, 'distros': self.remote.get_distros(), 'profiles': self.remote.get_profiles(), @@ -441,9 +502,9 @@ class CobblerWeb(object): def profile_save(self,new_or_edit=None,editmode='edit',name=None,oldname=None, distro=None,kickstart=None,kopts=None, - ksmeta=None,virtfilesize=None,virtram=None,virttype=None, + ksmeta=None,owners=None,virtfilesize=None,virtram=None,virttype=None, virtpath=None,repos=None,dhcptag=None,delete1=None,delete2=None, - parent=None,virtcpus=None,virtbridge=None,subprofile=None,server_override=None,**args): + parent=None,virtcpus=None,virtbridge=None,subprofile=None,server_override=None,recursive=False,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -463,7 +524,11 @@ class CobblerWeb(object): # handle deletes as a special case if new_or_edit == 'edit' and delete1 and delete2: try: - self.remote.remove_profile(name,self.token,1) + if recursive: + self.remote.remove_profile(name,self.token,True) + else: + self.remote.remove_profile(name,self.token,False) + except Exception, e: return self.error_page("could not delete %s, %s" % (name,str(e))) return self.profile_list() @@ -495,6 +560,8 @@ class CobblerWeb(object): self.remote.modify_profile(profile, 'kickstart', kickstart, self.token) if kopts: self.remote.modify_profile(profile, 'kopts', kopts, self.token) + if owners: + self.remote.modify_profile(profile, 'owners', owners, self.token) if ksmeta: self.remote.modify_profile(profile, 'ksmeta', ksmeta, self.token) if virtfilesize: @@ -523,7 +590,7 @@ class CobblerWeb(object): if dhcptag: self.remote.modify_profile(profile, 'dhcp-tag', dhcptag, self.token) - self.remote.save_profile(profile,self.token) + self.remote.save_profile(profile,self.token, editmode) except Exception, e: log_exc(self.apache) return self.error_page("Error while saving profile: %s" % str(e)) @@ -565,13 +632,24 @@ class CobblerWeb(object): input_repo = None if name is not None: input_repo = self.remote.get_repo(name, True) + can_edit = self.remote.check_access_no_fail(self.token,"modify_repo",name) + else: + can_edit = self.remote.check_access_no_fail(self.token,"new_repo",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( 'repo_edit.tmpl', { + 'user' : self.username, 'repo': input_repo, + 'editable' : can_edit } ) def repo_save(self,name=None,oldname=None,new_or_edit=None,editmode="edit", - mirror=None,keep_updated=None,priority=99, + mirror=None,owners=None,keep_updated=None,mirror_locally=0,priority=99, rpm_list=None,createrepo_flags=None,arch=None,yumopts=None, delete1=None,delete2=None,**args): if not self.__xmlrpc_setup(): @@ -615,6 +693,7 @@ class CobblerWeb(object): self.remote.modify_repo(repo, 'mirror', mirror, self.token) self.remote.modify_repo(repo, 'keep-updated', keep_updated, self.token) self.remote.modify_repo(repo, 'priority', priority, self.token) + self.remote.modify_repo(repo, 'mirror-locally', mirror_locally, self.token) if rpm_list: self.remote.modify_repo(repo, 'rpm-list', rpm_list, self.token) @@ -624,8 +703,10 @@ class CobblerWeb(object): self.remote.modify_repo(repo, 'arch', arch, self.token) if yumopts: self.remote.modify_repo(repo, 'yumopts', yumopts, self.token) + if owners: + self.remote.modify_repo(repo, 'owners', owners, self.token) - self.remote.save_repo(repo, self.token) + self.remote.save_repo(repo, self.token, editmode) except Exception, e: log_exc(self.apache) @@ -650,22 +731,49 @@ class CobblerWeb(object): 'ksfiles': self.remote.get_kickstart_templates(self.token) } ) + def ksfile_new(self, name=None,**spam): + + + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + can_edit = self.remote.check_access_no_fail(self.token,"add_kickstart",name) + return self.__render( 'ksfile_new.tmpl', { + 'editable' : can_edit, + 'ksdata': '' + } ) + + + def ksfile_edit(self, name=None,**spam): + + if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() + + can_edit = self.remote.check_access_no_fail(self.token,"modify_kickstart",name) return self.__render( 'ksfile_edit.tmpl', { 'name': name, + 'deleteable' : not self.remote.is_kickstart_in_use(name,self.token), + 'editable' : can_edit, 'ksdata': self.remote.read_or_write_kickstart_template(name,True,"",self.token) } ) - def ksfile_save(self, name=None, ksdata=None, **args): + def ksfile_save(self, name=None, ksdata=None, delete1=None, delete2=None, isnew=None, **args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() + + try: - self.remote.read_or_write_kickstart_template(name,False,ksdata,self.token) + if delete1 and delete2: + self.remote.read_or_write_kickstart_template(name,False,-1,self.token) + if isnew is not None: + name = "/var/lib/cobbler/kickstarts/" + name + if not delete1 and not delete2: + self.remote.read_or_write_kickstart_template(name,False,ksdata,self.token) except Exception, e: return self.error_page("An error occurred while trying to save kickstart file %s:<br/><br/>%s" % (name,str(e))) - return self.ksfile_edit(name=name) + return self.ksfile_list() # ------------------------------------------------------------------------ # # Miscellaneous @@ -675,6 +783,13 @@ class CobblerWeb(object): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() + can_edit = self.remote.check_access_no_fail(self.token,"sync",None) + if not can_edit: + return self.__render('message.tmpl', { + 'message1' : "Access denied.", + 'message2' : "You do not have permission to create new objects." + }) + try: rc = self.remote.sync(self.token) if not rc: @@ -739,6 +854,7 @@ class CobblerWeb(object): settings_view.exposed = True ksfile_edit.exposed = True + ksfile_new.exposed = True ksfile_save.exposed = True ksfile_list.exposed = True diff --git a/cobbler/webui/master.py b/cobbler/webui/master.py deleted file mode 100644 index 7ef1b0a..0000000 --- a/cobbler/webui/master.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python - - - - -################################################## -## DEPENDENCIES -import sys -import os -import os.path -from os.path import getmtime, exists -import time -import types -import __builtin__ -from Cheetah.Version import MinCompatibleVersion as RequiredCheetahVersion -from Cheetah.Version import MinCompatibleVersionTuple as RequiredCheetahVersionTuple -from Cheetah.Template import Template -from Cheetah.DummyTransaction import DummyTransaction -from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valueFromFrameOrSearchList -from Cheetah.CacheRegion import CacheRegion -import Cheetah.Filters as Filters -import Cheetah.ErrorCatchers as ErrorCatchers - -################################################## -## MODULE CONSTANTS -try: - True, False -except NameError: - True, False = (1==1), (1==0) -VFFSL=valueFromFrameOrSearchList -VFSL=valueFromSearchList -VFN=valueForName -currentTime=time.time -__CHEETAH_version__ = '2.0.1' -__CHEETAH_versionTuple__ = (2, 0, 1, 'final', 0) -__CHEETAH_genTime__ = 1207686338.5978019 -__CHEETAH_genTimestamp__ = 'Tue Apr 8 16:25:38 2008' -__CHEETAH_src__ = 'webui_templates/master.tmpl' -__CHEETAH_srcLastModified__ = 'Fri Feb 15 14:47:43 2008' -__CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine' - -if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple: - raise AssertionError( - 'This template was compiled with Cheetah version' - ' %s. Templates compiled before version %s must be recompiled.'%( - __CHEETAH_version__, RequiredCheetahVersion)) - -################################################## -## CLASSES - -class master(Template): - - ################################################## - ## CHEETAH GENERATED METHODS - - - def __init__(self, *args, **KWs): - - Template.__init__(self, *args, **KWs) - if not self._CHEETAH__instanceInitialized: - cheetahKWArgs = {} - allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split() - for k,v in KWs.items(): - if k in allowedKWs: cheetahKWArgs[k] = v - self._initCheetahInstance(**cheetahKWArgs) - - - def body(self, **KWS): - - - - ## CHEETAH: generated from #block body at line 53, col 1. - trans = KWS.get("trans") - if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)): - trans = self.transaction # is None unless self.awake() was called - if not trans: - trans = DummyTransaction() - _dummyTrans = True - else: _dummyTrans = False - write = trans.response().write - SL = self._CHEETAH__searchList - _filter = self._CHEETAH__currentFilter - - ######################################## - ## START - generated method body - - write(''' - <h1 style="color: red;">Template Failure</h1> - -''') - - ######################################## - ## END - generated method body - - return _dummyTrans and trans.response().getvalue() or "" - - - def respond(self, trans=None): - - - - ## CHEETAH: main method generated for this template - if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)): - trans = self.transaction # is None unless self.awake() was called - if not trans: - trans = DummyTransaction() - _dummyTrans = True - else: _dummyTrans = False - write = trans.response().write - SL = self._CHEETAH__searchList - _filter = self._CHEETAH__currentFilter - - ######################################## - ## START - generated method body - - write('''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xml:lang="en" lang="en" xmlns="http://www.w3.org/1999/xhtml"> -<head> - <title>''') - _v = VFFSL(SL,"title",True) # '$title' on line 5, col 12 - if _v is not None: write(_filter(_v, rawExpr='$title')) # from line 5, col 12. - write('''</title> - <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> - - <link rel="stylesheet" type="text/css" media="all" href="/cobbler/webui/style.css" /> - <link rel="stylesheet" type="text/css" media="all" href="/cobbler/webui/cobblerweb.css" /> - -<script language="Javascript" src="/cobbler/webui/cobbler.js" ></script> - -</head> - - -<body onload="global_onload();"> - -<div id="wrap"> - <h1 id="masthead"> - <a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 20, col 18 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 20, col 18. - write('''/index"> - <img alt="Cobbler Logo" - src="/cobbler/webui/logo-cobbler.png"/> - </a> - </h1> -</div> - -<div id="main"> - -<div id="sidebar"> - <ul id="nav"> - <li><a href="/cobbler/webui/wui.html" class="menu">Docs</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 32, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 32, col 22. - write('''?mode=settings_view" class="menu">Settings</a></li> - <li><hr/></li> - <li>LIST</li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 35, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 35, col 22. - write('''?mode=distro_list" class="menu">Distros</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 36, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 36, col 22. - write('''?mode=profile_list" class="menu">Profiles</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 37, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 37, col 22. - write('''?mode=system_list" class="menu">Systems</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 38, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 38, col 22. - write('''?mode=ksfile_list" class="menu">Kickstarts</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 39, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 39, col 22. - write('''?mode=repo_list" class="menu">Repos</a></li> - <li><hr/></li> - <li>ADD</li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 42, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 42, col 22. - write('''?mode=distro_edit" class="menu">Distro</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 43, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 43, col 22. - write('''?mode=profile_edit" class="menu">Profile</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 44, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 44, col 22. - write('''?mode=subprofile_edit" class="menu">Subprofile</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 45, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 45, col 22. - write('''?mode=system_edit" class="menu">System</a></li> - <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 46, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 46, col 22. - write('''?mode=repo_edit" class="menu">Repo</a></li> - <li><hr/><br/></li> - <li><a class="button sync" href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 48, col 42 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 48, col 42. - write('''?mode=sync">Sync</a></li> - </ul> -</div> - -<div id="content"> -''') - self.body(trans=trans) - write('''</div><!-- content --> -</div><!-- main --> - -</body> -</html> -''') - - ######################################## - ## END - generated method body - - return _dummyTrans and trans.response().getvalue() or "" - - ################################################## - ## CHEETAH GENERATED ATTRIBUTES - - - _CHEETAH__instanceInitialized = False - - _CHEETAH_version = __CHEETAH_version__ - - _CHEETAH_versionTuple = __CHEETAH_versionTuple__ - - _CHEETAH_genTime = __CHEETAH_genTime__ - - _CHEETAH_genTimestamp = __CHEETAH_genTimestamp__ - - _CHEETAH_src = __CHEETAH_src__ - - _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__ - - title = "Cobbler Web Interface" - - _mainCheetahMethod_for_master= 'respond' - -## END CLASS DEFINITION - -if not hasattr(master, '_initCheetahAttributes'): - templateAPIClass = getattr(master, '_CHEETAH_templateClass', Template) - templateAPIClass._addCheetahPlumbingCodeToClass(master) - - -# CHEETAH was developed by Tavis Rudd and Mike Orr -# with code, advice and input from many other volunteers. -# For more information visit http://www.CheetahTemplate.org/ - -################################################## -## if run from command line: -if __name__ == '__main__': - from Cheetah.TemplateCmdLineIface import CmdLineIface - CmdLineIface(templateObj=master()).run() - - diff --git a/cobbler/yaml/__init__.py b/cobbler/yaml/__init__.py index 419d1f3..bd21b40 100644 --- a/cobbler/yaml/__init__.py +++ b/cobbler/yaml/__init__.py @@ -1,3 +1,9 @@ +"""
+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
diff --git a/cobbler/yaml/dump.py b/cobbler/yaml/dump.py index b8e9d79..eb34955 100644 --- a/cobbler/yaml/dump.py +++ b/cobbler/yaml/dump.py @@ -1,3 +1,10 @@ +"""
+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
diff --git a/cobbler/yaml/implicit.py b/cobbler/yaml/implicit.py index 6172564..49d65e0 100644 --- a/cobbler/yaml/implicit.py +++ b/cobbler/yaml/implicit.py @@ -1,3 +1,9 @@ +"""
+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
diff --git a/cobbler/yaml/inline.py b/cobbler/yaml/inline.py index 8e647de..d4f6439 100644 --- a/cobbler/yaml/inline.py +++ b/cobbler/yaml/inline.py @@ -1,3 +1,9 @@ +"""
+pyyaml legacy
+Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
+(see open source license information in docs/ directory)
+"""
+
import re
import string
diff --git a/cobbler/yaml/klass.py b/cobbler/yaml/klass.py index edcf5a8..c182fcf 100644 --- a/cobbler/yaml/klass.py +++ b/cobbler/yaml/klass.py @@ -1,3 +1,9 @@ +"""
+pyyaml legacy
+Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
+(see open source license information in docs/ directory)
+"""
+
import new
import re
diff --git a/cobbler/yaml/load.py b/cobbler/yaml/load.py index 259178d..54931d6 100644 --- a/cobbler/yaml/load.py +++ b/cobbler/yaml/load.py @@ -1,3 +1,9 @@ +"""
+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
diff --git a/cobbler/yaml/ordered_dict.py b/cobbler/yaml/ordered_dict.py index b3788b7..5bc2e3e 100644 --- a/cobbler/yaml/ordered_dict.py +++ b/cobbler/yaml/ordered_dict.py @@ -1,3 +1,10 @@ +""" +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 diff --git a/cobbler/yaml/redump.py b/cobbler/yaml/redump.py index 56ea958..eefd68e 100644 --- a/cobbler/yaml/redump.py +++ b/cobbler/yaml/redump.py @@ -1,3 +1,9 @@ +""" +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 diff --git a/cobbler/yaml/stream.py b/cobbler/yaml/stream.py index cc78c4b..dcd65c3 100644 --- a/cobbler/yaml/stream.py +++ b/cobbler/yaml/stream.py @@ -1,3 +1,9 @@ +"""
+pyyaml legacy
+Copyright (c) 2001 Steve Howell and Friends; All Rights Reserved
+(see open source license information in docs/ directory)
+"""
+
import re
import string
diff --git a/cobbler/yaml/timestamp.py b/cobbler/yaml/timestamp.py index abcb2e6..5c522f6 100644 --- a/cobbler/yaml/timestamp.py +++ b/cobbler/yaml/timestamp.py @@ -1,3 +1,10 @@ +""" +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 diff --git a/cobbler/yaml/ypath.py b/cobbler/yaml/ypath.py index 51d9d2f..b183a23 100644 --- a/cobbler/yaml/ypath.py +++ b/cobbler/yaml/ypath.py @@ -1,3 +1,10 @@ +"""
+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
diff --git a/cobbler/yumgen.py b/cobbler/yumgen.py new file mode 100644 index 0000000..8c10be3 --- /dev/null +++ b/cobbler/yumgen.py @@ -0,0 +1,110 @@ +""" +Builds out filesystem trees/data based on the object tree. +This is the code behind 'cobbler sync'. + +Copyright 2006-2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os +import os.path +import shutil +import time +import sub_process +import sys +import glob +import traceback +import errno + +import utils +from cexceptions import * +import templar + +import item_distro +import item_profile +import item_repo +import item_system + +from utils import _ + + +class YumGen: + + def __init__(self,config): + """ + Constructor + """ + 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) + + def retemplate_all_yum_repos(self): + for p in self.profiles: + self.retemplate_yum_repos(p,True) + for system in self.systems: + self.retemplate_yum_repos(system,False) + + def retemplate_yum_repos(self,obj,is_profile): + """ + Yum repository management files are in self.settings.webdir/repo_mirror/$name/config.repo + and also potentially in listed in the source_repos structure of the distro object, however + these files have server URLs in them that must be templated out. This function does this. + """ + blended = utils.blender(self.api, False, obj) + + if is_profile: + outseg = "repos_profile" + else: + outseg = "repos_system" + + input_files = [] + + # chance old versions from upgrade do not have a source_repos + # workaround for user bug + if not blended.has_key("source_repos"): + blended["source_repos"] = [] + + # tack on all the install source repos IF there is more than one. + # this is basically to support things like RHEL5 split trees + # if there is only one, then there is no need to do this. + + for r in blended["source_repos"]: + filename = self.settings.webdir + "/" + "/".join(r[0].split("/")[4:]) + input_files.append(filename) + + for repo in blended["repos"]: + input_files.append(os.path.join(self.settings.webdir, "repo_mirror", repo, "config.repo")) + + for infile in input_files: + if infile.find("ks_mirror") == -1: + dispname = infile.split("/")[-2] + else: + dispname = infile.split("/")[-1].replace(".repo","") + confdir = os.path.join(self.settings.webdir, outseg) + outdir = os.path.join(confdir, blended["name"]) + utils.mkdir(outdir) + try: + infile_h = open(infile) + except: + # file does not exist and the user needs to run reposync + # before we will use this, cobbler check will mention + # this problem + continue + infile_data = infile_h.read() + infile_h.close() + outfile = os.path.join(outdir, "%s.repo" % (dispname)) + self.templar.render(infile_data, blended, outfile, None) + + |