diff --git a/koan/app.py b/koan/app.py index e2bc6ee..0d80665 100755 --- a/koan/app.py +++ b/koan/app.py @@ -33,6 +33,7 @@ import xmlrpclib import string import re import glob +import socket # the version of cobbler needed to interact with this version of koan # this is an decimal value (major + 0.1 * minor + 0.01 * maint) @@ -617,7 +618,10 @@ class Koan: def after_download(self, profile_data): for x in DISPLAY_PARAMS: if profile_data.has_key(x): - print "%20s : %s" % (x, profile_data[x]) + value = profile_data[x] + if x == 'kernel_options': + value = self.calc_kernel_args(profile_data) + print "%20s : %s" % (x, value) return self.net_install(after_download) #--------------------------------------------------- @@ -657,11 +661,17 @@ class Koan: k_args = self.safe_load(profile_data,'kernel_options') k_args = k_args + " ks=file:ks.cfg" + kickstart = self.safe_load(profile_data,'kickstart') + self.build_initrd( self.safe_load(profile_data,'initrd_local'), - self.safe_load(profile_data,'kickstart'), + kickstart, profile_data ) + + if k_args.find('koan_autonet') != -1: + k_args = k_args.replace('koan_autonet', self.get_netconfig(kickstart)) + k_args = k_args.replace("lang ","lang= ") cmd = [ "/sbin/grubby", @@ -785,6 +795,156 @@ class Koan: shutil.copyfile("/var/spool/koan/initrd_final", initrd) #--------------------------------------------------- + def get_netconfig_from_running_system(self): + """ + Get the network configuration from the running system (as opposed + to config files, which are distribution-specific). This isn't going to + be 100% correct on multi-homed machines, but should work in 98% of the + sane cases in the wild. + """ + ret = dict() + + # get the hostname from the system + ret['hostname'] = socket.gethostname() + try: + ret['ip'] = socket.gethostbyname( ret['hostname'] ) + except: + # ignore the error, but still print the trace + traceback.print_exc() + + if ret.has_key('ip'): + iface = self.get_local_interface( ip=ret['ip'] ) + ret.update(**iface) + else: + iface = self.get_local_interface() + ret.update(**iface) + + # if cobbler knows about this system, prefer cobbler's data + if self.system is not None and (ret['mac'] or ret['ip'] or ret['hostname']): + try: + systems = self.get_systems_xmlrpc() + + for system in systems: + if ret.has_key('mac') and system['mac_address'].upper() == ret['mac'].upper(): + ret.update(system) + break + if ret.has_key('ip') and system['ip_address'] == ret['ip']: + ret.update(system) + break + if ret.has_key('hostname') and system['hostname'] == ret['hostname']: + ret.update(system) + break + except: + # ignore the error, but still print the trace + traceback.print_exc() + + # get the first nameserver configured on the local system + fd = open("/etc/resolv.conf", "r") + for line in fd.readlines(): + if line.startswith('nameserver'): + ret['nameserver'] = line.split()[1] + break + fd.close() + + return ret + + def get_netconfig_from_kickstart(self,kickstart): + """ + Scan the kickstart configuration for a network --bootproto static line. + """ + ret = dict() + lines = [] + + if os.path.exists("/var/spool/koan/ks.cfg"): + fd = open("/var/spool/koan/ks.cfg", "r") + lines = fd.readlines() + fd.close() + else: + ksdata = self.get_kickstart_data(kickstart,None) + lines = ksdata.splitlines() + + for line in lines: + if line.startswith('network '): + p = opt_parse.OptionParser() + p.add_option("--bootproto", dest="bootproto") + p.add_option("--ip", dest="ip") + p.add_option("--netmask", dest="netmask") + p.add_option("--gateway", dest="gateway") + p.add_option("--hostname", dest="hostname") + p.add_option("--nameserver", dest="nameserver") + # ignored, but required for clean parsing + p.add_option("--device", dest="device") + p.add_option("--onboot", dest="onboot") + p.add_option("--nodns", dest="nodns") + (options,args) = p.parse_args( line.split()[1:] ) + + if options.bootproto and options.bootproto == 'static': + ret['ip'] = options.ip + ret['netmask'] = options.netmask + ret['gateway'] = options.gateway + ret['hostname'] = options.hostname + ret['nameserver'] = options.nameserver + + break + return ret + + def get_netconfig(self,kickstart): + """ + If this is a --replace-self run, get the running network configuration and + put it on the kernel command line so DHCP isn't required. + + If it's a new kickstart, check the kickstart for a static network + configuration and push that into the kernel command line. + """ + kargs = "" + hostname = None + ip = None + netmask = None + nameserver = None + gateway = None + + # Always try to get the network configuration kickstart. This will + # override anything set in the 'if self.is_replace' block above. + ksconf = self.get_netconfig_from_kickstart(kickstart) + if ksconf.has_key('ip') and ksconf['ip'] is not None: + ip = ksconf['ip'] + if ksconf.has_key('netmask') and ksconf['netmask'] is not None: + netmask = ksconf['netmask'] + if ksconf.has_key('hostname') and ksconf['hostname'] is not None: + hostname = ksconf['hostname'] + if ksconf.has_key('nameserver') and ksconf['nameserver'] is not None: + nameserver = ksconf['nameserver'] + if ksconf.has_key('gateway') and ksconf['gateway'] is not None: + gateway = ksconf['gateway'] + + if self.is_replace: + conf = self.get_netconfig_from_running_system() + if conf is not None: + if ip is None and conf['ip'] is not None: + ip = conf['ip'] + if netmask is None and conf['netmask'] is not None: + netmask = conf['netmask'] + if hostname is None and conf['hostname'] is not None: + hostname = conf['hostname'] + if nameserver is None and conf['nameserver'] is not None: + nameserver = conf['nameserver'] + if gateway is None and conf['gateway'] is not None: + gateway = conf['gateway'] + + if ip is not None: + kargs = kargs + " ip=%s" % ip + if netmask is not None: + kargs = kargs + " netmask=%s" % netmask + if hostname is not None: + kargs = kargs + " hostname=%s" % hostname + if nameserver is not None: + kargs = kargs + " nameserver=%s" % nameserver + if gateway is not None: + kargs = kargs + " gateway=%s" % gateway + + return kargs + + #--------------------------------------------------- def connect_fail(self): raise InfoException, "Could not communicate with %s:%s" % (self.server, self.port) @@ -827,6 +987,104 @@ class Koan: #--------------------------------------------------- + def get_local_interface(self,ip=None,mac=None,iface=None,want_all=None): + """ + Fetches interfaces, macs, ips, netmask (in cidr notation), and optionally + default route using iproute2. + + The default is to return a best-guess of the primary interface on the + system. If a system is hooked up with a default route, it will be chosen. + Otherwise, eth0 is preferred. After that, the "most configured" interface + will be returned. If no appropriate-looking interfaces are found, None will + be returned. + + Options: + ip: specify an ip to search for, return only that interface + mac: sepcify a mac to search for, return only that interface + iface: return only the specified interface + want_all: (boolean) return all information in a dict + + """ + interfaces = dict() + iface_re = re.compile('\d+: ([a-z]+[0-9]+): <.*> mtu \d+', re.IGNORECASE) + + mac_re = re.compile('.*link/ether ([a-z0-9:]{17}) ', re.IGNORECASE) + if mac is not None: + mac_re = re.compile('.*link/ether (%s) ' % mac, re.IGNORECASE) + + ip_re = re.compile('.*inet ([\.0-9]+)/(\d+) ') + if ip is not None: + ip_re = re.compile('.*inet (%s)/(\d+) ' % ip) + + sys_iface = None + fd = os.popen("/sbin/ip address show") + for line in fd.readlines(): + mac_match = mac_re.match(line) + iface_match = iface_re.match(line) + ip_match = ip_re.match(line) + + if iface_match: + sys_iface = iface_match.group(1) + interfaces[sys_iface] = { 'iface': sys_iface } + if mac_match and sys_iface: + interfaces[sys_iface]['mac'] = mac_match.group(1) + if ip_match and sys_iface: + interfaces[sys_iface]['ip'] = ip_match.group(1) + interfaces[sys_iface]['netmask_int'] = ip_match.group(2) + interfaces[sys_iface]['netmask'] = self.netmask_i2dq( ip_match.group(2) ) + + fd.close() + + # get the default route + fd = os.popen("/sbin/ip route show") + for line in fd.readlines(): + if line.startswith('default via '): + parts = line.split() + interfaces[parts[4]]['gateway'] = parts[2] + break + fd.close() + + if want_all is not None: + return interfaces + elif mac: + for i in interfaces.values(): + if i[mac] == mac: return i + elif ip: + for i in interfaces.values(): + if i[ip] == ip: return i + elif iface: + # this can and should throw an error if the interface doesn't exist + return interfaces[iface] + else: + ret = None + + # but even more likely, we want the device the default route is on + for i in interfaces.values(): + if i.has_key('gateway'): ret = i + + if ret is None: + for i in interfaces.values(): + if i.has_key('ip') and i.has_key('mac'): ret = i + + # eth0 is the most likely cnadidate + if ret is None and interfaces.has_key('eth0'): + ret = interfaces['eth0'] + + return ret + + def netmask_i2dq(self,nm): + """ + Convert a netmask from integer to dotted-quad notation. + """ + intval = 0xffffffff << (32 - int(nm)) + ret = "%u.%u.%u.%u" % ((intval >> 24) & 0x000000ff, + ((intval & 0x00ff0000) >> 16), + ((intval & 0x0000ff00) >> 8), + (intval & 0x000000ff)) + return ret + + #--------------------------------------------------- + def is_ip(self,strdata): """ Is strdata an IP? @@ -901,6 +1159,9 @@ class Koan: def calc_kernel_args(self, pd): kickstart = self.safe_load(pd,'kickstart') options = self.safe_load(pd,'kernel_options') + + options = options.replace('koan_autonet', self.get_netconfig(kickstart)) + kextra = "" if kickstart != "": kextra = kextra + "ks=" + kickstart @@ -910,6 +1171,7 @@ class Koan: kextra = kextra + options # parser issues? lang needs a trailing = and somehow doesn't have it. kextra = kextra.replace("lang ","lang= ") + return kextra #---------------------------------------------------