summaryrefslogtreecommitdiffstats
path: root/dracut/parse-kickstart
blob: fcf847a1db1ba74c00e83ee8243415b6686e94fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#!/usr/bin/python
#vim: set fileencoding=utf8
# parse-kickstart - read a kickstart file and emit equivalent dracut boot args.
#
# Copyright © 2012 Red Hat, Inc.
# BLAH BLAH GPL BLAH.
#
# Designed to run inside the dracut initramfs environment.
# Requires python 2.7 or later.
#
# Authors:
#   Will Woods <wwoods@redhat.com>

import sys, os
import logging
import shutil
import uuid
from pykickstart.parser import KickstartParser, preprocessKickstart
from pykickstart.version import returnClassForVersion
from pykickstart.errors import KickstartError
from pykickstart.constants import *
from pykickstart import commands
from collections import OrderedDict

# Default logging: none
log = logging.getLogger('parse-kickstart').addHandler(logging.NullHandler())

# 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 = []
    for line in lines:
        for arg in line.split():
            k,e,v = arg.partition("=")
            args[k] = v
    return args

proc_cmdline = read_cmdline("/proc/cmdline")

# Here are the kickstart commands we care about:

class Method(commands.method.F14_Method):
    '''install methods: cdrom, harddrive, nfs, url'''
    def dracut_args(self, args, lineno, obj):
        if self.method == "cdrom":
            method="cdrom"
        elif self.method == "harddrive":
            if self.biospart:
                method="bd:%s:%s" % (self.partition, self.dir)
            else:
                method="hd:%s:%s" % (self.partition, self.dir)
        elif self.method == "nfs":
            method="nfs:%s:%s" % (self.server, self.dir)
            if self.opts:
                method += ":%s" % self.opts
        elif self.method == "url":
            # FIXME: self.proxy, self.noverifyssl
            method = self.url
        return "inst.repo=%s" % method

class Updates(commands.updates.F7_Updates):
    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):
    def dracut_args(self, args, lineno, obj):
        if self.mediacheck:
            return "rd.live.check"

class DriverDisk(commands.driverdisk.F14_DriverDisk):
    def dracut_args(self, args, lineno, obj):
        dd = self.driverdiskList[-1]
        if dd.biospart:
            location = "bd:%s" % dd.biospart
        else:
            location = dd.partition or dd.source
        if location:
            return "inst.driverdisk=%s" % location

class Network(commands.network.F16_Network):
    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

        # first 'network' line
        if len(self.network) == 1:
            net.activate = True
            if net.device == "link" or not net.device:
                # NOTE: this might still be empty (e.g. 'ks=file:...')
                # XXX FIXME: handle "link" properly?
                net.device = self.handler.ksdevice
            # tell dracut to bring this device up
            netline = ksnet_to_dracut(args, lineno, net, bootdev=True)

            # HACK: current dracut dies if you have BOOTIF= and ip= together.
            # Until that gets fixed upstream, we have to defer to dracut.
            # XXX FIXME: remove this when dracut can handle BOOTIF+ip!
            if 'BOOTIF' in proc_cmdline:
                # let dracut use BOOTIF to bring up the network
                netline = None

        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):
    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"

# TODO: keymap, lang... device? upgrade? selinux?

dracutCmds = {
    'cdrom': Method,
    'harddrive': Method,
    'nfs': Method,
    'url': Method,
    'updates': Updates,
    'mediacheck': MediaCheck,
    'driverdisk': DriverDisk,
    'network': Network,
    'cmdline': DisplayMode,
    'graphical': DisplayMode,
    'text': DisplayMode,
}
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]
        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 ksnet_to_dracut(args, lineno, net, bootdev=False):
    '''Translate the kickstart network data into dracut network data.'''
    line = []
    ip=""

    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):
        ip="dhcp"
    elif net.bootProto == BOOTPROTO_IBFT:
        ip="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".format(net)
    elif net.ipv6 == "auto":
        ip="auto6"
    elif net.ipv6 == "dhcp":
        ip="dhcp6"
    elif net.ipv6:
        ip="[{0.ipv6}]::{0.gateway}:{0.netmask}:" \
           "{0.hostname}:{0.device}:none".format(net)

    if net.device and not ip.endswith(":none"):
        line.append("ip=%s:%s" % (net.device, ip))
    else:
        line.append("ip=%s" % ip)

    for ns in net.nameserver.split(","):
        if ns:
            line.append("nameserver=%s" % ns)

    if net.mtu:
        # XXX FIXME: dracut doesn't support mtu= (yet)
        if net.device:
            line.append("mtu=%s:%s" % (net.device, net.mtu))
        else:
            line.append("mtu=%s" % net.mtu)

    if bootdev:
        if net.device:
            line.append("bootdev=%s" % net.device)
        # touch /tmp/net.ifaces to make sure dracut brings up network
        open("/tmp/net.ifaces", "a")

    if net.essid or net.wepkey or net.wpakey:
        # TODO: make dracut support wireless? (do we care?)
        log.error("'%s': dracut doesn't support wireless networks",
                      " ".join(args))

    return " ".join(line)

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):
        log.info("can't find device %s" % dev)
        return
    ifcfg = dict()
    if filename is None:
        filename = "/tmp/ifcfg/ifcfg-%s" % dev
        if not os.path.isdir("/tmp/ifcfg"):
            os.makedirs("/tmp/ifcfg")
    ifcfg['DEVICE'] = dev
    ifcfg['HWADDR'] = readsysfile("/sys/class/net/%s/address" % dev)
    if_uuid = str(uuid.uuid4())
    ifcfg['UUID'] = if_uuid
    # 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"

    # 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"
        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:
        ifcfg['DNS1'] = net.nameserver
    if net.nodefroute:
        ifcfg['DEFROUTE'] = "no"

    # TODO: dhcpclass, ethtool, essid/wepkey/wpakay, etc.

    if net.bootProto == 'dhcp':
        srcpath = "/tmp/dhclient.%s.lease" % dev
        dstdir = "/tmp/ifcfg-leases"
        dstpath = "%s/dhclient-%s-%s.lease" % (dstdir, if_uuid, dev)
        if os.path.exists(srcpath):
            if not os.path.isdir(dstdir):
                os.makedirs(dstdir)
            shutil.copyfile(srcpath, dstpath)

    try:
        log.info("writing ifcfg for %s", dev)
        outf = open(filename, "w")
        outf.write('# Generated by parse-kickstart\n')
        for k,v in ifcfg.items():
            outf.write("%s=%s\n" % (k,v))
        outf.close()
    except IOError as e:
        log.error("can't write %s: %s" % (filename, str(e)))
    else:
        return filename

def process_kickstart(ksfile):
    handler = DracutHandler()
    handler.ksdevice = os.environ.get('ksdevice')
    parser = KickstartParser(handler, missingIncludeIsFatal=False)
    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("/tmp/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()
    for path in sys.argv[1:]:
        outfile, output = process_kickstart(path)
        for line in filter(None, output):
            print line