Blame cloudinit/net/cmdline.py

Packit Service a04d08
# Copyright (C) 2013-2014 Canonical Ltd.
Packit Service a04d08
#
Packit Service a04d08
# Author: Scott Moser <scott.moser@canonical.com>
Packit Service a04d08
# Author: Blake Rouse <blake.rouse@canonical.com>
Packit Service a04d08
#
Packit Service a04d08
# This file is part of cloud-init. See LICENSE file for license information.
Packit Service a04d08
Packit Service a04d08
import abc
Packit Service a04d08
import base64
Packit Service a04d08
import glob
Packit Service a04d08
import gzip
Packit Service a04d08
import io
Packit Service 9bfd13
import logging
Packit Service a04d08
import os
Packit Service a04d08
Packit Service a04d08
from cloudinit import util
Packit Service a04d08
Packit Service a04d08
from . import get_devicelist
Packit Service a04d08
from . import read_sys_net_safe
Packit Service a04d08
Packit Service a04d08
_OPEN_ISCSI_INTERFACE_FILE = "/run/initramfs/open-iscsi.interface"
Packit Service a04d08
Packit Service 9bfd13
KERNEL_CMDLINE_NETWORK_CONFIG_DISABLED = "disabled"
Packit Service 9bfd13
Packit Service a04d08
Packit Service 9bfd13
class InitramfsNetworkConfigSource(metaclass=abc.ABCMeta):
Packit Service a04d08
    """ABC for net config sources that read config written by initramfses"""
Packit Service a04d08
Packit Service a04d08
    @abc.abstractmethod
Packit Service 9bfd13
    def is_applicable(self) -> bool:
Packit Service a04d08
        """Is this initramfs config source applicable to the current system?"""
Packit Service a04d08
Packit Service a04d08
    @abc.abstractmethod
Packit Service 9bfd13
    def render_config(self) -> dict:
Packit Service a04d08
        """Render a v1 network config from the initramfs configuration"""
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class KlibcNetworkConfigSource(InitramfsNetworkConfigSource):
Packit Service a04d08
    """InitramfsNetworkConfigSource for klibc initramfs (i.e. Debian/Ubuntu)
Packit Service a04d08
Packit Service a04d08
    Has three parameters, but they are intended to make testing simpler, _not_
Packit Service a04d08
    for use in production code.  (This is indicated by the prepended
Packit Service a04d08
    underscores.)
Packit Service a04d08
    """
Packit Service a04d08
Packit Service a04d08
    def __init__(self, _files=None, _mac_addrs=None, _cmdline=None):
Packit Service a04d08
        self._files = _files
Packit Service a04d08
        self._mac_addrs = _mac_addrs
Packit Service a04d08
        self._cmdline = _cmdline
Packit Service a04d08
Packit Service a04d08
        # Set defaults here, as they require computation that we don't want to
Packit Service a04d08
        # do at method definition time
Packit Service a04d08
        if self._files is None:
Packit Service a04d08
            self._files = _get_klibc_net_cfg_files()
Packit Service a04d08
        if self._cmdline is None:
Packit Service a04d08
            self._cmdline = util.get_cmdline()
Packit Service a04d08
        if self._mac_addrs is None:
Packit Service a04d08
            self._mac_addrs = {}
Packit Service a04d08
            for k in get_devicelist():
Packit Service a04d08
                mac_addr = read_sys_net_safe(k, 'address')
Packit Service a04d08
                if mac_addr:
Packit Service a04d08
                    self._mac_addrs[k] = mac_addr
Packit Service a04d08
Packit Service 9bfd13
    def is_applicable(self) -> bool:
Packit Service a04d08
        """
Packit Service a04d08
        Return whether this system has klibc initramfs network config or not
Packit Service a04d08
Packit Service a04d08
        Will return True if:
Packit Service a04d08
            (a) klibc files exist in /run, AND
Packit Service a04d08
            (b) either:
Packit Service a04d08
                (i) ip= or ip6= are on the kernel cmdline, OR
Packit Service a04d08
                (ii) an open-iscsi interface file is present in the system
Packit Service a04d08
        """
Packit Service a04d08
        if self._files:
Packit Service a04d08
            if 'ip=' in self._cmdline or 'ip6=' in self._cmdline:
Packit Service a04d08
                return True
Packit Service a04d08
            if os.path.exists(_OPEN_ISCSI_INTERFACE_FILE):
Packit Service a04d08
                # iBft can configure networking without ip=
Packit Service a04d08
                return True
Packit Service a04d08
        return False
Packit Service a04d08
Packit Service 9bfd13
    def render_config(self) -> dict:
Packit Service a04d08
        return config_from_klibc_net_cfg(
Packit Service a04d08
            files=self._files, mac_addrs=self._mac_addrs,
Packit Service a04d08
        )
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
_INITRAMFS_CONFIG_SOURCES = [KlibcNetworkConfigSource]
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _klibc_to_config_entry(content, mac_addrs=None):
Packit Service a04d08
    """Convert a klibc written shell content file to a 'config' entry
Packit Service a04d08
    When ip= is seen on the kernel command line in debian initramfs
Packit Service a04d08
    and networking is brought up, ipconfig will populate
Packit Service a04d08
    /run/net-<name>.cfg.
Packit Service a04d08
Packit Service a04d08
    The files are shell style syntax, and examples are in the tests
Packit Service a04d08
    provided here.  There is no good documentation on this unfortunately.
Packit Service a04d08
Packit Service a04d08
    DEVICE=<name> is expected/required and PROTO should indicate if
Packit Service 9bfd13
    this is 'none' (static) or 'dhcp' or 'dhcp6' (LP: #1621507).
Packit Service a04d08
    note that IPV6PROTO is also written by newer code to address the
Packit Service a04d08
    possibility of both ipv4 and ipv6 getting addresses.
Packit Service 9bfd13
Packit Service 9bfd13
    Full syntax is documented at:
Packit Service 9bfd13
    https://git.kernel.org/pub/scm/libs/klibc/klibc.git/plain/usr/kinit/ipconfig/README.ipconfig
Packit Service a04d08
    """
Packit Service a04d08
Packit Service a04d08
    if mac_addrs is None:
Packit Service a04d08
        mac_addrs = {}
Packit Service a04d08
Packit Service a04d08
    data = util.load_shell_content(content)
Packit Service a04d08
    try:
Packit Service a04d08
        name = data['DEVICE'] if 'DEVICE' in data else data['DEVICE6']
Packit Service 9bfd13
    except KeyError as e:
Packit Service 9bfd13
        raise ValueError("no 'DEVICE' or 'DEVICE6' entry in data") from e
Packit Service a04d08
Packit Service a04d08
    # ipconfig on precise does not write PROTO
Packit Service a04d08
    # IPv6 config gives us IPV6PROTO, not PROTO.
Packit Service a04d08
    proto = data.get('PROTO', data.get('IPV6PROTO'))
Packit Service a04d08
    if not proto:
Packit Service a04d08
        if data.get('filename'):
Packit Service a04d08
            proto = 'dhcp'
Packit Service a04d08
        else:
Packit Service 9bfd13
            proto = 'none'
Packit Service a04d08
Packit Service 9bfd13
    if proto not in ('none', 'dhcp', 'dhcp6'):
Packit Service a04d08
        raise ValueError("Unexpected value for PROTO: %s" % proto)
Packit Service a04d08
Packit Service a04d08
    iface = {
Packit Service a04d08
        'type': 'physical',
Packit Service a04d08
        'name': name,
Packit Service a04d08
        'subnets': [],
Packit Service a04d08
    }
Packit Service a04d08
Packit Service a04d08
    if name in mac_addrs:
Packit Service a04d08
        iface['mac_address'] = mac_addrs[name]
Packit Service a04d08
Packit Service a04d08
    # Handle both IPv4 and IPv6 values
Packit Service a04d08
    for pre in ('IPV4', 'IPV6'):
Packit Service a04d08
        # if no IPV4ADDR or IPV6ADDR, then go on.
Packit Service a04d08
        if pre + "ADDR" not in data:
Packit Service a04d08
            continue
Packit Service a04d08
Packit Service a04d08
        # PROTO for ipv4, IPV6PROTO for ipv6
Packit Service a04d08
        cur_proto = data.get(pre + 'PROTO', proto)
Packit Service 9bfd13
        # ipconfig's 'none' is called 'static'
Packit Service 9bfd13
        if cur_proto == 'none':
Packit Service 9bfd13
            cur_proto = 'static'
Packit Service a04d08
        subnet = {'type': cur_proto, 'control': 'manual'}
Packit Service a04d08
Packit Service a04d08
        # only populate address for static types. While the rendered config
Packit Service a04d08
        # may have an address for dhcp, that is not really expected.
Packit Service a04d08
        if cur_proto == 'static':
Packit Service a04d08
            subnet['address'] = data[pre + 'ADDR']
Packit Service a04d08
Packit Service a04d08
        # these fields go right on the subnet
Packit Service a04d08
        for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
Packit Service a04d08
            if pre + key in data:
Packit Service a04d08
                subnet[key.lower()] = data[pre + key]
Packit Service a04d08
Packit Service a04d08
        dns = []
Packit Service a04d08
        # handle IPV4DNS0 or IPV6DNS0
Packit Service a04d08
        for nskey in ('DNS0', 'DNS1'):
Packit Service a04d08
            ns = data.get(pre + nskey)
Packit Service a04d08
            # verify it has something other than 0.0.0.0 (or ipv6)
Packit Service a04d08
            if ns and len(ns.strip(":.0")):
Packit Service a04d08
                dns.append(data[pre + nskey])
Packit Service a04d08
        if dns:
Packit Service a04d08
            subnet['dns_nameservers'] = dns
Packit Service a04d08
            # add search to both ipv4 and ipv6, as it has no namespace
Packit Service a04d08
            search = data.get('DOMAINSEARCH')
Packit Service a04d08
            if search:
Packit Service a04d08
                if ',' in search:
Packit Service a04d08
                    subnet['dns_search'] = search.split(",")
Packit Service a04d08
                else:
Packit Service a04d08
                    subnet['dns_search'] = search.split()
Packit Service a04d08
Packit Service a04d08
        iface['subnets'].append(subnet)
Packit Service a04d08
Packit Service a04d08
    return name, iface
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _get_klibc_net_cfg_files():
Packit Service a04d08
    return glob.glob('/run/net-*.conf') + glob.glob('/run/net6-*.conf')
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def config_from_klibc_net_cfg(files=None, mac_addrs=None):
Packit Service a04d08
    if files is None:
Packit Service a04d08
        files = _get_klibc_net_cfg_files()
Packit Service a04d08
Packit Service a04d08
    entries = []
Packit Service a04d08
    names = {}
Packit Service a04d08
    for cfg_file in files:
Packit Service a04d08
        name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
Packit Service a04d08
                                             mac_addrs=mac_addrs)
Packit Service a04d08
        if name in names:
Packit Service a04d08
            prev = names[name]['entry']
Packit Service a04d08
            if prev.get('mac_address') != entry.get('mac_address'):
Packit Service a04d08
                raise ValueError(
Packit Service a04d08
                    "device '{name}' was defined multiple times ({files})"
Packit Service a04d08
                    " but had differing mac addresses: {old} -> {new}.".format(
Packit Service a04d08
                        name=name, files=' '.join(names[name]['files']),
Packit Service a04d08
                        old=prev.get('mac_address'),
Packit Service a04d08
                        new=entry.get('mac_address')))
Packit Service a04d08
            prev['subnets'].extend(entry['subnets'])
Packit Service a04d08
            names[name]['files'].append(cfg_file)
Packit Service a04d08
        else:
Packit Service a04d08
            names[name] = {'files': [cfg_file], 'entry': entry}
Packit Service a04d08
            entries.append(entry)
Packit Service a04d08
Packit Service a04d08
    return {'config': entries, 'version': 1}
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def read_initramfs_config():
Packit Service a04d08
    """
Packit Service a04d08
    Return v1 network config for initramfs-configured networking (or None)
Packit Service a04d08
Packit Service a04d08
    This will consider each _INITRAMFS_CONFIG_SOURCES entry in turn, and return
Packit Service a04d08
    v1 network configuration for the first one that is applicable.  If none are
Packit Service a04d08
    applicable, return None.
Packit Service a04d08
    """
Packit Service a04d08
    for src_cls in _INITRAMFS_CONFIG_SOURCES:
Packit Service a04d08
        cfg_source = src_cls()
Packit Service a04d08
Packit Service a04d08
        if not cfg_source.is_applicable():
Packit Service a04d08
            continue
Packit Service a04d08
Packit Service a04d08
        return cfg_source.render_config()
Packit Service a04d08
    return None
Packit Service a04d08
Packit Service a04d08
Packit Service 9bfd13
def _decomp_gzip(blob):
Packit Service 9bfd13
    # decompress blob or return original blob
Packit Service a04d08
    with io.BytesIO(blob) as iobuf:
Packit Service a04d08
        gzfp = None
Packit Service a04d08
        try:
Packit Service a04d08
            gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
Packit Service a04d08
            return gzfp.read()
Packit Service a04d08
        except IOError:
Packit Service a04d08
            return blob
Packit Service a04d08
        finally:
Packit Service a04d08
            if gzfp:
Packit Service a04d08
                gzfp.close()
Packit Service a04d08
Packit Service a04d08
Packit Service 9bfd13
def _b64dgz(data):
Packit Service 9bfd13
    """Decode a string base64 encoding, if gzipped, uncompress as well
Packit Service b1601c
Packit Service 9bfd13
    :return: decompressed unencoded string of the data or empty string on
Packit Service 9bfd13
       unencoded data.
Packit Service 9bfd13
    """
Packit Service 9bfd13
    try:
Packit Service 9bfd13
        blob = base64.b64decode(data)
Packit Service 9bfd13
    except (TypeError, ValueError):
Packit Service 9bfd13
        logging.error(
Packit Service 9bfd13
            "Expected base64 encoded kernel commandline parameter"
Packit Service 9bfd13
            " network-config. Ignoring network-config=%s.", data)
Packit Service 9bfd13
        return ''
Packit Service a04d08
Packit Service 9bfd13
    return _decomp_gzip(blob)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def read_kernel_cmdline_config(cmdline=None):
Packit Service a04d08
    if cmdline is None:
Packit Service a04d08
        cmdline = util.get_cmdline()
Packit Service a04d08
Packit Service a04d08
    if 'network-config=' in cmdline:
Packit Service a04d08
        data64 = None
Packit Service a04d08
        for tok in cmdline.split():
Packit Service a04d08
            if tok.startswith("network-config="):
Packit Service a04d08
                data64 = tok.split("=", 1)[1]
Packit Service a04d08
        if data64:
Packit Service 9bfd13
            if data64 == KERNEL_CMDLINE_NETWORK_CONFIG_DISABLED:
Packit Service 9bfd13
                return {"config": "disabled"}
Packit Service a04d08
            return util.load_yaml(_b64dgz(data64))
Packit Service a04d08
Packit Service a04d08
    return None
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab