#!/usr/bin/python3 #vim: set fileencoding=utf8 # parse-kickstart - read a kickstart file and emit equivalent dracut boot args # # Designed to run inside the dracut initramfs environment. # Requires python 2.7 or later. # # # Copyright (C) 2012-2014 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. 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., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # # Authors: # Will Woods import sys, os import logging import shutil import uuid import glob from pykickstart.parser import KickstartParser, preprocessKickstart from pykickstart.sections import NullSection from pykickstart.version import returnClassForVersion from pykickstart.errors import KickstartError # pylint: disable=wildcard-import,unused-wildcard-import from pykickstart.constants import * from pykickstart import commands from collections import OrderedDict # Default logging: none log = logging.getLogger('parse-kickstart').addHandler(logging.NullHandler()) TMPDIR = "/tmp" import functools def eintr_retry_call(func, *args, **kwargs): """Retry an interruptible system call if interrupted.""" while True: try: return func(*args, **kwargs) except InterruptedError: continue open = functools.partial(eintr_retry_call, open) # pylint: disable=redefined-builtin # Helper function for reading simple files in /sys def readsysfile(f): '''Return the contents of f, or "" if missing.''' try: val = open(f).readline().strip() except IOError: val = "" return val def read_cmdline(f): '''Returns an OrderedDict containing key-value pairs from a file with boot arguments (e.g. /proc/cmdline).''' args = OrderedDict() try: lines = open(f).readlines() except IOError: lines = [] # pylint: disable=redefined-outer-name for line in lines: for arg in line.split(): k,_,v = arg.partition("=") args[k] = v return args def first_device_with_link(): for dev_dir in sorted(glob.glob("/sys/class/net/*")): ##define ARPHRD_ETHER 1 /* Ethernet 10Mbps */ ##define ARPHRD_INFINIBAND 32 /* InfiniBand */ try: with open(dev_dir+"/type") as f: if f.read().strip() not in ("1", "32"): continue with open(dev_dir+"/carrier") as f: if f.read().strip() == "1": return os.path.basename(dev_dir) except IOError: pass return "" def setting_only_hostname(net, args): return net.hostname and (len(args) == 2 or (len(args) == 3 and "--hostname" in args)) proc_cmdline = read_cmdline("/proc/cmdline") class DracutArgsMixin(object): """A mixin class to make a Command generate dracut args.""" def dracut_args(self, args, lineno, obj): raise NotImplementedError # Here are the kickstart commands we care about: class Cdrom(commands.cdrom.FC3_Cdrom, DracutArgsMixin): def dracut_args(self, args, lineno, obj): return "inst.repo=cdrom" class HardDrive(commands.harddrive.FC3_HardDrive, DracutArgsMixin): def dracut_args(self, args, lineno, obj): if self.biospart: return "inst.repo=bd:%s:%s" % (self.partition, self.dir) else: return "inst.repo=hd:%s:%s" % (self.partition, self.dir) class NFS(commands.nfs.FC6_NFS, DracutArgsMixin): def dracut_args(self, args, lineno, obj): if self.opts: method = "nfs:%s:%s:%s" % (self.opts, self.server, self.dir) else: method="nfs:%s:%s" % (self.server, self.dir) # Spaces on the cmdline need to be '\ ' method = method.replace(" ", "\\ ") return "inst.repo=%s" % method class URL(commands.url.F18_Url, DracutArgsMixin): def dracut_args(self, args, lineno, obj): # Spaces in the url need to be %20 if self.url: method = self.url.replace(" ", "%20") else: method = None args = ["inst.repo=%s" % method] if self.noverifyssl: args.append("rd.noverifyssl") if self.proxy: args.append("proxy=%s" % self.proxy) return "\n".join(args) class Updates(commands.updates.F7_Updates, DracutArgsMixin): def dracut_args(self, args, lineno, obj): if self.url == "floppy": return "live.updates=/dev/fd0" elif self.url: return "live.updates=%s" % self.url class MediaCheck(commands.mediacheck.FC4_MediaCheck, DracutArgsMixin): def dracut_args(self, args, lineno, obj): if self.mediacheck: return "rd.live.check" class DriverDisk(commands.driverdisk.F14_DriverDisk, DracutArgsMixin): def dracut_args(self, args, lineno, obj): dd_args = [] for dd in self.driverdiskList: if dd.partition: dd_args.append("inst.dd=hd:%s" % dd.partition) elif dd.source: dd_args.append("inst.dd=%s" % dd.source) elif dd.biospart: dd_args.append("inst.dd=bd:%s" % dd.biospart) return "\n".join(dd_args) class Network(commands.network.F22_Network, DracutArgsMixin): def dracut_args(self, args, lineno, net): ''' NOTE: The first 'network' line get special treatment: * '--activate' is always enabled * '--device' is optional (defaults to the 'ksdevice=' boot arg) * the device gets brought online in initramfs ''' netline = None # Setting only hostname in kickstart if not net.device and not self.handler.ksdevice \ and setting_only_hostname(net, args): return None # first 'network' line if len(self.network) == 1: net.activate = True # Note that there may be no net.device and no ksdevice if inst.ks=file:/ks.cfg # If that is the case, fall into ksnet_to_dracut with net.device=None and let # it handle things. if not net.device: if self.handler.ksdevice: net.device = self.handler.ksdevice log.info("Using ksdevice %s for missing --device in first kickstart network command", self.handler.ksdevice) if net.device == "link": net.device = first_device_with_link() if not net.device: log.warn("No device with link found for --device=link") return else: log.info("Using %s as first device with link found", net.device) # tell dracut to bring this device up netline = ksnet_to_dracut(args, lineno, net, bootdev=True) else: # all subsequent 'network' lines require '--device' if not net.device or net.device == "link": log.error("'%s': missing --device", " ".join(args)) return # write ifcfg so NM will set up the device correctly later ksnet_to_ifcfg(net) return netline class DisplayMode(commands.displaymode.FC3_DisplayMode, DracutArgsMixin): def dracut_args(self, args, lineno, obj): if self.displayMode == DISPLAY_MODE_CMDLINE: return "inst.cmdline" elif self.displayMode == DISPLAY_MODE_TEXT: return "inst.text" elif self.displayMode == DISPLAY_MODE_GRAPHICAL: return "inst.graphical" class Bootloader(commands.bootloader.F21_Bootloader, DracutArgsMixin): def dracut_args(self, args, lineno, obj): if self.extlinux: return "extlinux" # FUTURE: keymap, lang... device? selinux? dracutCmds = { 'cdrom': Cdrom, 'harddrive': HardDrive, 'nfs': NFS, 'url': URL, 'updates': Updates, 'mediacheck': MediaCheck, 'driverdisk': DriverDisk, 'network': Network, 'cmdline': DisplayMode, 'graphical': DisplayMode, 'text': DisplayMode, 'bootloader': Bootloader, } handlerclass = returnClassForVersion() class DracutHandler(handlerclass): def __init__(self): handlerclass.__init__(self, commandUpdates=dracutCmds) self.output = [] self.ksdevice = None def dispatcher(self, args, lineno): obj = handlerclass.dispatcher(self, args, lineno) # and execute any specified dracut_args cmd = args[0] # the commands member is implemented by the class returned # by returnClassForVersion # pylint: disable=no-member command = self.commands[cmd] if hasattr(command, "dracut_args"): log.debug("kickstart line %u: handling %s", lineno, cmd) self.output.append(command.dracut_args(args, lineno, obj)) return obj def init_logger(level=None): if level is None and 'rd.debug' in proc_cmdline: level = logging.DEBUG logfmt = "%(name)s %(levelname)s: %(message)s" logging.basicConfig(format=logfmt, level=level) logger = logging.getLogger('parse-kickstart') return logger def is_mac(addr): return addr and len(addr) == 17 and addr.count(":") == 5 # good enough def find_devname(mac): for netif in os.listdir("/sys/class/net"): thismac = readsysfile("/sys/class/net/%s/address" % netif) if thismac.lower() == mac.lower(): return netif def s390_settings(device): cfg = { 'SUBCHANNELS': '', 'NETTYPE': '', 'OPTIONS': '' } subchannels = [] for symlink in sorted(glob.glob("/sys/class/net/%s/device/cdev[0-9]*" % device)): subchannels.append(os.path.basename(os.readlink(symlink))) if not subchannels: return cfg cfg['SUBCHANNELS'] = ','.join(subchannels) ## cat /etc/ccw.conf #qeth,0.0.0900,0.0.0901,0.0.0902,layer2=0,portname=FOOBAR,portno=0 # #SUBCHANNELS="0.0.0900,0.0.0901,0.0.0902" #NETTYPE="qeth" #OPTIONS="layer2=1 portname=FOOBAR portno=0" with open('/etc/ccw.conf') as f: # pylint: disable=redefined-outer-name for line in f: if cfg['SUBCHANNELS'] in line: items = line.strip().split(',') cfg['NETTYPE'] = items[0] cfg['OPTIONS'] = " ".join(i for i in items[1:] if '=' in i) break return cfg def ksnet_to_dracut(args, lineno, net, bootdev=False): '''Translate the kickstart network data into dracut network data.''' # pylint: disable=redefined-outer-name line = [] ip="" autoconf="" if is_mac(net.device): # this is a MAC - find the interface name mac = net.device net.device = find_devname(mac) if net.device is None: # iface not active - pick a name for it net.device = "ksdev0" # we only get called once, so this is OK line.append("ifname=%s:%s" % (net.device, mac.lower())) # NOTE: dracut currently only does ipv4 *or* ipv6, so only one ip=arg.. if net.bootProto in (BOOTPROTO_DHCP, BOOTPROTO_BOOTP): autoconf="dhcp" elif net.bootProto == BOOTPROTO_IBFT: autoconf="ibft" elif net.bootProto == BOOTPROTO_QUERY: log.error("'%s': --bootproto=query is deprecated", " ".join(args)) elif net.bootProto == BOOTPROTO_STATIC: req = ("gateway", "netmask", "nameserver", "ip") missing = ", ".join("--%s" % i for i in req if not hasattr(net, i)) if missing: log.warn("line %u: network missing %s", lineno, missing) else: ip="{0.ip}::{0.gateway}:{0.netmask}:" \ "{0.hostname}:{0.device}:none:{0.mtu}".format(net) elif net.ipv6 == "auto": autoconf="auto6" elif net.ipv6 == "dhcp": autoconf="dhcp6" elif net.ipv6: ip="[{0.ipv6}]::{0.ipv6gateway}:{0.netmask}:" \ "{0.hostname}:{0.device}:none:{0.mtu}".format(net) if autoconf: if net.device or net.mtu: ip="%s:%s:%s" % (net.device, autoconf, net.mtu) else: ip=autoconf if ip: line.append("ip=%s" % ip) for ns in net.nameserver.split(","): if ns: line.append("nameserver=%s" % ns) if bootdev: if net.device: line.append("bootdev=%s" % net.device) # touch /tmp/net.ifaces to make sure dracut brings up network open(TMPDIR+"/net.ifaces", "a") if net.essid or net.wepkey or net.wpakey: # NOTE: does dracut actually support wireless? (do we care?) log.error("'%s': dracut doesn't support wireless networks", " ".join(args)) return " ".join(line) def add_s390_settings(dev, ifcfg): s390cfg = s390_settings(dev) if s390cfg['SUBCHANNELS']: ifcfg.pop('HWADDR', None) ifcfg['SUBCHANNELS'] = s390cfg['SUBCHANNELS'] if s390cfg['NETTYPE']: ifcfg['NETTYPE'] = s390cfg['NETTYPE'] if s390cfg['OPTIONS']: ifcfg['OPTIONS'] = s390cfg['OPTIONS'] def ksnet_to_ifcfg(net, filename=None): '''Write an ifcfg file for the given kickstart network config''' dev = net.device if is_mac(dev): dev = find_devname(dev) if not dev: return if (not os.path.isdir("/sys/class/net/%s" % dev) and not net.bondslaves and not net.teamslaves and not net.bridgeslaves): log.info("can't find device %s", dev) return ifcfg = dict() if filename is None: filename = TMPDIR+"/ifcfg/ifcfg-%s" % dev if not os.path.isdir(TMPDIR+"/ifcfg"): os.makedirs(TMPDIR+"/ifcfg") ifcfg['DEVICE'] = dev hwaddr = readsysfile("/sys/class/net/%s/address" % dev) if "ifname={0}:{1}".format(dev, hwaddr).upper() in open("/proc/cmdline").read().upper(): # rename by initscript's 60-net-rules on target system after switchroot ifcfg['HWADDR'] = hwaddr ifcfg['UUID'] = str(uuid.uuid4()) # we set real ONBOOT value in anaconda, here # we use it to activate devcies by NM on start ifcfg['ONBOOT'] = "yes" if net.activate else "no" add_s390_settings(dev, ifcfg) # dhcp etc. ifcfg['BOOTPROTO'] = net.bootProto if net.bootProto == 'static': ifcfg['IPADDR'] = net.ip ifcfg['NETMASK'] = net.netmask ifcfg['GATEWAY'] = net.gateway if net.bootProto == 'dhcp': if net.hostname: ifcfg['DHCP_HOSTNAME'] = net.hostname # ipv6 settings if net.noipv6: ifcfg['IPV6INIT'] = "no" else: ifcfg['IPV6INIT'] = "yes" if net.ipv6 == 'dhcp': ifcfg['DHCPV6C'] = "yes" ifcfg['IPV6_AUTOCONF'] = "no" if net.ipv6gateway: ifcfg['IPV6_DEFAULTGW'] = net.ipv6gateway elif net.ipv6 == 'auto': ifcfg['IPV6_AUTOCONF'] = "yes" # NOTE: redundant (this is the default) elif ':' in net.ipv6: ifcfg['IPV6ADDR'] = net.ipv6 ifcfg['IPV6_AUTOCONF'] = "no" # misc stuff if net.mtu: ifcfg['MTU'] = net.mtu if net.nameserver: for i, ip in enumerate(net.nameserver.split(",")): ifcfg["DNS%d" % (i+1)] = ip if net.nodefroute: ifcfg['DEFROUTE'] = "no" # FUTURE: dhcpclass, ethtool, essid/wepkey/wpakay, etc. if net.bootProto == 'dhcp': srcpath = TMPDIR+"/dhclient.%s.lease" % dev dstdir = TMPDIR+"/ifcfg-leases" dstpath = "%s/dhclient-%s-%s.lease" % (dstdir, ifcfg['UUID'], dev) if os.path.exists(srcpath): if not os.path.isdir(dstdir): os.makedirs(dstdir) shutil.copyfile(srcpath, dstpath) if net.bondslaves: ifcfg.pop('HWADDR', None) ifcfg['TYPE'] = "Bond" ifcfg['BONDING_MASTER'] = "yes" ifcfg['NAME'] = "Bond connection %s" % dev if ';' in net.bondopts: sep = ";" else: sep = "," ifcfg['BONDING_OPTS'] = '"' + " ".join(net.bondopts.split(sep)) + '"' for i, slave in enumerate(net.bondslaves.split(","), 1): slave_ifcfg = { 'TYPE' : "Ethernet", 'NAME' : "%s slave %s" % (dev, i), 'UUID' : str(uuid.uuid4()), 'ONBOOT' : "yes", 'MASTER' : ifcfg['UUID'], 'HWADDR' : readsysfile("/sys/class/net/%s/address" % slave), } add_s390_settings(slave, slave_ifcfg) slave_filename = TMPDIR+"/ifcfg/ifcfg-%s" % "_".join(slave_ifcfg['NAME'].split(" ")) log.info("writing ifcfg %s for slave %s of bond %s", slave_filename, slave, dev) write_ifcfg(slave_filename, slave_ifcfg) if net.teamslaves: ifcfg.pop('HWADDR', None) ifcfg['TYPE'] = "Team" ifcfg['NAME'] = "Team connection %s" % dev ifcfg['TEAM_CONFIG'] = "'" + net.teamconfig + "'" for i, (slave, cfg) in enumerate(net.teamslaves): slave_ifcfg = { 'DEVICE': slave, 'DEVICETYPE' : "TeamPort", 'NAME' : "%s slave %s" % (dev, i), 'UUID' : str(uuid.uuid4()), 'ONBOOT' : "yes", 'TEAM_MASTER' : dev, # 'HWADDR' : readsysfile("/sys/class/net/%s/address" % slave), } if cfg: slave_ifcfg['TEAM_PORT_CONFIG'] = "'" + cfg + "'" slave_filename = TMPDIR+"/ifcfg/ifcfg-%s" % "_".join(slave_ifcfg['NAME'].split(" ")) log.info("writing ifcfg %s for slave %s of team %s", slave_filename, slave, dev) write_ifcfg(slave_filename, slave_ifcfg) if net.bridgeslaves: ifcfg.pop('HWADDR', None) ifcfg['TYPE'] = "Bridge" ifcfg['NAME'] = "Bridge connection %s" % dev options = {} for opt in net.bridgeopts.split(","): key, _, value = opt.partition("=") if not value: log.error("Invalid bridge option %s", opt) continue key = key.replace('-', '_') options[key] = value stp = options.pop("stp", None) if stp: ifcfg['STP'] = stp delay = options.pop("forward_delay", None) if delay: ifcfg['DELAY'] = delay if options: keyvalues = ["%s=%s" % (key, options[key]) for key in options] ifcfg['BRIDGING_OPTS'] = '"' + " ".join(keyvalues) + '"' for i, slave in enumerate(net.bridgeslaves.split(","), 1): slave_ifcfg = { 'TYPE' : "Ethernet", 'NAME' : "%s slave %s" % (dev, i), 'UUID' : str(uuid.uuid4()), 'ONBOOT' : "yes", 'BRIDGE' : dev, 'HWADDR' : readsysfile("/sys/class/net/%s/address" % slave), } slave_filename = TMPDIR+"/ifcfg/ifcfg-%s" % "_".join(slave_ifcfg['NAME'].split(" ")) log.info("writing ifcfg %s for slave %s of bridge %s", slave_filename, slave, dev) write_ifcfg(slave_filename, slave_ifcfg) if net.vlanid: interface_name = net.interfacename or "%s.%s" % (dev, net.vlanid) ifcfg.pop('HWADDR', None) ifcfg['TYPE'] = "Vlan" ifcfg['VLAN'] = "yes" ifcfg['VLAN_ID'] = net.vlanid ifcfg['NAME'] = "VLAN connection %s" % interface_name ifcfg['DEVICE'] = interface_name ifcfg['PHYSDEV'] = dev filename = TMPDIR+"/ifcfg/ifcfg-%s" % interface_name log.info("writing ifcfg %s for %s", filename, dev) if write_ifcfg(filename, ifcfg): return filename def write_ifcfg(filename, ifcfg): try: with open(filename, "w") as f: f.write('# Generated by parse-kickstart\n') for k,v in list(ifcfg.items()): f.write("%s=%s\n" % (k,v)) except IOError as e: log.error("can't write %s: %s", filename, e) return False return True def process_kickstart(ksfile): handler = DracutHandler() try: handler.ksdevice = proc_cmdline['ksdevice'] except KeyError: log.debug("ksdevice argument is not available") parser = KickstartParser(handler, missingIncludeIsFatal=False, errorsAreFatal=False) parser.registerSection(NullSection(handler, sectionOpen="%addon")) parser.registerSection(NullSection(handler, sectionOpen="%anaconda")) log.info("processing kickstart file %s", ksfile) processed_file = preprocessKickstart(ksfile) try: parser.readKickstart(processed_file) except KickstartError as e: log.error(str(e)) with open(TMPDIR+"/ks.info", "a") as f: f.write('parsed_kickstart="%s"\n' % processed_file) log.info("finished parsing kickstart") return processed_file, handler.output if __name__ == '__main__': log = init_logger() # Override tmp directory path for testing. Don't use argparse because we don't want to # include that dependency in the initramfs. Pass '--tmpdir /path/to/tmp/' if "--tmpdir" in sys.argv: idx = sys.argv.index("--tmpdir") try: sys.argv.pop(idx) TMPDIR = os.path.normpath(sys.argv.pop(idx)) except IndexError: pass for path in sys.argv[1:]: outfile, output = process_kickstart(path) for line in (l for l in output if l): print(line)