""" A Cobbler System. Copyright 2006, Red Hat, Inc Michael DeHaan 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 utils import item from cexceptions import * from rhpl.translate import _, N_, textdomain, utf8 class System(item.Item): TYPE_NAME = _("system") COLLECTION_TYPE = "system" def make_clone(self): ds = self.to_datastruct() cloned = System(self.config) cloned.from_datastruct(ds) return cloned def clear(self,is_subobject=False): self.name = None self.profile = (None, '<>')[is_subobject] self.kernel_options = ({}, '<>')[is_subobject] self.ks_meta = ({}, '<>')[is_subobject] # the empty interface is just set up so we don't break anything self.interfaces = [{ "mac_address" : "", "ip_address" : "", "gateway" : "", "hostname" : "", "subnet" : "", "virt_bridge" : "", "dhcp_tag" : "" }] # these are obsolete: # self.ip_address = ("", '<>')[is_subobject] # self.mac_address = ("", '<>')[is_subobject] # self.hostname = ("", '<>')[is_subobject] self.netboot_enabled = (1, '<>')[is_subobject] self.depth = 2 self.kickstart = "<>" # use value in profile self.virt_path = "<>" # use value in profile self.virt_type = "<>" # use value in profile def from_datastruct(self,seed_data): # I apologize in advance for how gnarly this is, though the intent is to be # able to load a cobbbler config from any version of cobbler, allowing upgrades # as needed. As such, there is code to perform migrations between data structures. # over time, some flat fields have migrated to lists. The biggest change is the # consolidating of network related fields under the "interfaces" array to support # multiple NICs. Most additions of simple flat fields/settings are simple, however # and this expansion should be largely complete. self.parent = self.load_item(seed_data, 'parent') self.name = self.load_item(seed_data, 'name') 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', '<>') self.virt_path = self.load_item(seed_data, 'virt_path', '<>') self.virt_type = self.load_item(seed_data, 'virt_type', '<>') # backwards compat, this is now a per-NIC setting __dhcp_tag = self.load_item(seed_data, 'dhcp_tag', 'default') # backwards compat, load --ip-address from two possible sources. # the old --pxe-address was a bit of a misnomer, new value is --ip-address # though the ultimate trick is that we don't even use this parameter anymore for # storage reasons. We now use self.interfaces which is an array of hashes. __oldvar = self.load_item(seed_data, 'pxe_address', "") if __oldvar == "": # newer version, yay __ip_address = self.load_item(seed_data, 'ip_address', "") else: __ip_address = __oldvar # the following two parameters are also here for backwards compatibility reasons __hostname = self.load_item(seed_data, 'hostname', "") __mac_address = self.load_item(seed_data, 'mac_address', "") # now load the new-style interface definition data structure self.interfaces = self.load_item(seed_data, 'interfaces', [{ "mac_address" : "", "ip_address" : "", "gateway" : "", "hostname" : "", "subnet" : "", "virt_bridge" : "", "dhcp_tag" : "" }]) # now backfill the interface structure with any old values # backwards compatibility here is complex/ugly though we don't want to # break anyone. So this code needs to stay here. if (__hostname != "" or __mac_address != "" or __ip_address != ""): if __hostname is not None and __hostname != "": self.interfaces[0]["hostname"] = __hostname if __mac_address is not None and __mac_address != "": self.interfaces[0]["mac_address"] = __mac_address if __ip_address is not None and __ip_address != "": self.interfaces[0]["ip_address"] = __ip_address if __dhcp_tag is not None and __dhcp_tag != "": self.interfaces[0]["dhcp_tag"] = __dhcp_tag # now if no interfaces are STILL defined, add in one only under certain # conditions .. this emulates legacy behavior in the new interface format # FIXME: the following may be a bit ...quirky if __mac_address == "" and not self.interfaces[0].has_key("mac_address") and utils.is_mac(self.name): self.interfaces[0]["mac_address"] = self.name elif __ip_address == "" and not self.interfaces[0].has_key("ip_address") and utils.is_ip(self.name): self.interfaces[0]["ip_address"] = self.name # now for each interface, if any fields are missing, add them. # if any new interface fields are ever added, they must be duplicated here. # FIXME: cleanup for x in self.interfaces: if not x.has_key("mac_address"): x["mac_address"] = "" if not x.has_key("ip_address"): x["ip_address"] = "" if not x.has_key("subnet"): x["subnet"] = "" if not x.has_key("hostname"): x["hostname"] = "" if not x.has_key("gateway"): x["gateway"] = "" if not x.has_key("virt_bridge"): x["virt_bridge"] = "" if not x.has_key("dhcp_tag"): x["dhcp_tag"] = "" # question: is there any case where we'd want to PXE-enable one interface # but not another. answer: probably not. self.netboot_enabled = self.load_item(seed_data, 'netboot_enabled', 1) # backwards compatibility -- convert string entries to dicts for storage # this allows for better usage from the API. if self.kernel_options != "<>" and type(self.kernel_options) != dict: self.set_kernel_options(self.kernel_options) if self.ks_meta != "<>" and type(self.ks_meta) != dict: self.set_ksmeta(self.ks_meta) return self def get_parent(self): """ Return object next highest up the tree. """ if self.parent is None or self.parent == '': return self.config.profiles().find(name=self.profile) else: return self.config.systems().find(name=self.parent) def set_name(self,name): """ In Cobbler 0.4.9, any name given is legal, but if it's not an IP or MAC, --ip-address of --mac-address must be given for PXE options to work. """ # set appropriate fields if the name implicitly is a MAC or IP. # if the name is a hostname though, don't intuit that, as that's hard to determine if utils.is_mac(name): if self.interfaces[0]["mac_address"] == "": self.interfaces[0]["mac_address"] = name elif utils.is_ip(name): if self.interfaces[0]["ip_address"] == "": self.interfaces[0]["ip_address"] = name self.name = name return True def get_mac_address(self,interface=0): """ Get the mac address, which may be implicit in the object name or explicit with --mac-address. Use the explicit location first. """ if len(self.interfaces) -1 < interface: raise CX(_("internal error: probing an interface that does not exist")) intf = self.interfaces[interface] if intf["mac_address"] != "": return intf["mac_address"] elif utils.is_mac(self.name) and interface == 0: return self.name else: return None def get_ip_address(self,interface=0): """ Get the IP address, which may be implicit in the object name or explict with --ip-address. Use the explicit location first. """ if len(self.interfaces) -1 < interface: raise CX(_("internal error: probing an interface that does not exist")) intf = self.interfaces[interface] if intf["ip_address"] != "": return intf["ip_address"] elif utils.is_ip(self.name) and interface == 0: return self.name else: return None def is_pxe_supported(self,interface=0): """ Can only add system PXE records if a MAC or IP address is available, else it's a koan only record. Actually Itanium goes beyond all this and needs the IP all of the time though this is enforced elsewhere (action_sync.py). """ if self.name == "default": return True counter = 0 mac = self.get_mac_address(counter) ip = self.get_ip_address(counter) if mac is None and ip is None: return False return True def __get_interface(self,interface=0,enforcing=False): if not ( interface <= len(self.interfaces) -1 ): if enforcing: raise CX(_("internal error: accessing interface that does not exist: %s" % interface)) else: # if there are two interfaces and the API requests the fifth, that's an error. # you can't set an interface that is more than +1 away from the current count. # FIXME: this may cause problems. # FIXME: there should be a function that generates this. new_item = { "hostname" : "", "ip_address" : "", "mac_address" : "", "subnet" : "", "gateway" : "", "virt_bridge" : "", "dhcp_tag" : "" } self.interfaces.append(new_item) return new_item return self.interfaces[interface] def set_dhcp_tag(self,dhcp_tag,interface=0): intf = self.__get_interface(interface) intf["dhcp_tag"] = dhcp_tag return True def set_hostname(self,hostname,interface=0): intf = self.__get_interface(interface) intf["hostname"] = hostname return True def set_ip_address(self,address,interface=0): """ Assign a IP or hostname in DHCP when this MAC boots. Only works if manage_dhcp is set in /var/lib/cobbler/settings """ # FIXME: interface intf = self.__get_interface(interface) if utils.is_ip(address): intf["ip_address"] = address return True raise CX(_("invalid format for IP address (%s)") % address) def set_mac_address(self,address,interface=0): # FIXME: interface intf = self.__get_interface(interface) if utils.is_mac(address): intf["mac_address"] = address return True raise CX(_("invalid format for MAC address (%s)" % address)) def set_gateway(self,gateway,interface=0): # FIXME: validate + interface intf = self.__get_interface(interface) intf["gateway"] = gateway return True def set_subnet(self,subnet,interface=0): # FIXME: validate + interface intf = self.__get_interface(interface) intf["subnet"] = subnet return True def set_virt_bridge(self,bridge,interface=0): # FIXME: validate + interface intf = self.__get_interface(interface) intf["bridge"] = bridge return True def set_profile(self,profile_name): """ Set the system to use a certain named profile. The profile must have already been loaded into the Profiles collection. """ p = self.config.profiles().find(name=profile_name) if p is not None: self.profile = profile_name self.depth = p.depth + 1 # subprofiles have varying depths. 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_type(self,vtype): """ Virtualization preference, can be overridden by koan. """ if vtype.lower() not in [ "qemu", "xenpv", "auto" ]: raise CX(_("invalid virt type")) self.virt_type = vtype return True def set_netboot_enabled(self,netboot_enabled): """ If true, allows per-system PXE files to be generated on sync (or add). If false, these files are not generated, thus eliminating the potential for an infinite install loop when systems are set to PXE boot first in the boot order. In general, users who are PXE booting first in the boot order won't create system definitions, so this feature primarily comes into play for programmatic users of the API, who want to initially create a system with netboot enabled and then disable it after the system installs, as triggered by some action in kickstart %post. For this reason, this option is not surfaced in the CLI, output, or documentation (yet). Use of this option does not affect the ability to use PXE menus. If an admin has machines set up to PXE only after local boot fails, this option isn't even relevant. """ if netboot_enabled in [ True, "True", "true", 1, "1", "on", "yes", "y", "ON", "YES", "Y" ]: # this is a bit lame, though we don't know what the user will enter YAML wise... self.netboot_enabled = 1 else: self.netboot_enabled = 0 return True def is_valid(self): """ A system is valid when it contains a valid name and a profile. """ # NOTE: this validation code does not support inheritable distros at this time. # this is by design as inheritable systems don't make sense. if self.name is None: raise CX(_("need to specify a name for this object")) return False if self.profile is None: raise CX(_("need to specify a profile for this system")) return False return True def set_kickstart(self,kickstart): """ Sets the kickstart. This must be a NFS, HTTP, or FTP URL. Or filesystem path. Minor checking of the URL is performed here. NOTE -- usage of the --kickstart parameter in the profile is STRONGLY encouraged. This is only for exception cases where a user already has kickstarts made for each system and can't leverage templating. Profiles provide an important abstraction layer -- assigning systems to defined and repeatable roles. """ if utils.find_kickstart(kickstart): self.kickstart = kickstart return True raise CX(_("kickstart not found")) def to_datastruct(self): return { 'name' : self.name, 'profile' : self.profile, 'kernel_options' : self.kernel_options, 'ks_meta' : self.ks_meta, #'ip_address' : self.ip_address, 'netboot_enabled' : self.netboot_enabled, #'hostname' : self.hostname, #'mac_address' : self.mac_address, 'parent' : self.parent, 'depth' : self.depth, 'kickstart' : self.kickstart, 'virt_type' : self.virt_type, 'virt_path' : self.virt_path, #'dhcp_tag' : self.dhcp_tag, 'interfaces' : self.interfaces } 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 + _("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 counter = 0 for x in self.interfaces: buf = buf + _("interface : #%s\n") % (counter) buf = buf + _(" mac address : %s\n") % x.get("mac_address","") buf = buf + _(" ip address : %s\n") % x.get("ip_address","") buf = buf + _(" hostname : %s\n") % x.get("hostname","") buf = buf + _(" gateway : %s\n") % x.get("gateway","") buf = buf + _(" subnet : %s\n") % x.get("subnet","") buf = buf + _(" virt bridge : %s\n") % x.get("virt_bridge","") buf = buf + _(" dhcp tag : %s\n") % x.get("dhcp_tag","") buf = buf + _(" config id : %s\n") % utils.get_config_filename(self,counter) counter = counter + 1 return buf def remote_methods(self): return { 'name' : self.set_name, 'profile' : self.set_profile, 'kopts' : self.set_kernel_options, 'ksmeta' : self.set_ksmeta, 'hostname' : self.set_hostname, 'ip-address' : self.set_ip_address, 'ip' : self.set_ip_address, # alias 'mac-address' : self.set_mac_address, 'mac' : self.set_mac_address, # alias 'kickstart' : self.set_kickstart, 'netboot-enabled' : self.set_netboot_enabled, 'virt-path' : self.set_virt_path, 'virt-type' : self.set_virt_type, 'dhcp-tag' : self.set_dhcp_tag, 'gateway' : self.set_gateway, 'virt-bridge' : self.set_virt_bridge, 'subnet' : self.set_subnet }