diff options
| author | zsdc <taras@vyos.io> | 2023-05-04 22:26:17 +0300 | 
|---|---|---|
| committer | zsdc <taras@vyos.io> | 2023-05-04 22:26:17 +0300 | 
| commit | 3c229a3646a58e47d4d69c011f82c45ef3bb3c77 (patch) | |
| tree | 5c805e4e0d0d3131585cc5a7d0a8edf8167f5294 /src | |
| parent | ba81c15185d7a59ab0ec6705b53b311b4dda721d (diff) | |
| download | vyos-1x-3c229a3646a58e47d4d69c011f82c45ef3bb3c77.tar.gz vyos-1x-3c229a3646a58e47d4d69c011f82c45ef3bb3c77.zip | |
cloud-init: T5190: Added Cloud-init pre-configurator
Added a new service that starts before Cloud-init, waits for all network
interfaces initialization, and if requested by config, checks which interfaces
can get configuration via DHCP server and creates a corresponding Cloud-init
network configuration.
This protects from two situations:
* when Cloud-init tries to get meta-data via eth0 (default and fallback variant
for any data source which depends on network), but the real network is connected
to another interface
* when Cloud-init starts simultaneously with udev and initializes the first
interface to get meta-data before it is renamed to eth0 by udev
Diffstat (limited to 'src')
| -rwxr-xr-x | src/system/vyos-config-cloud-init.py | 169 | ||||
| -rw-r--r-- | src/systemd/vyos-config-cloud-init.service | 19 | 
2 files changed, 188 insertions, 0 deletions
| diff --git a/src/system/vyos-config-cloud-init.py b/src/system/vyos-config-cloud-init.py new file mode 100755 index 000000000..0a6c1f9bc --- /dev/null +++ b/src/system/vyos-config-cloud-init.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. + +import logging +from concurrent.futures import ProcessPoolExecutor +from pathlib import Path +from subprocess import run, TimeoutExpired +from sys import exit + +from psutil import net_if_addrs, AF_LINK +from systemd.journal import JournalHandler +from yaml import safe_load + +from vyos.template import render + +# define a path to the configuration file and template +config_file = '/etc/cloud/cloud.cfg.d/20_vyos_network.cfg' +template_file = 'system/cloud_init_networking.j2' + + +def check_interface_dhcp(iface_name: str) -> bool: +    """Check DHCP client can work on an interface + +    Args: +        iface_name (str): interface name + +    Returns: +        bool: check result +    """ +    dhclient_command: list[str] = [ +        'dhclient', '-4', '-1', '-q', '--no-pid', '-sf', '/bin/true', iface_name +    ] +    check_result = False +    # try to get an IP address +    # we use dhclient behavior here to speedup detection +    # if dhclient receives a configuration and configure an interface +    # it switch to background +    # If no - it will keep running in foreground +    try: +        run(['ip', 'l', 'set', iface_name, 'up']) +        run(dhclient_command, timeout=5) +        check_result = True +    except TimeoutExpired: +        pass +    finally: +        run(['ip', 'l', 'set', iface_name, 'down']) + +    logger.info(f'DHCP server was found on {iface_name}: {check_result}') +    return check_result + + +def dhclient_cleanup() -> None: +    """Clean up after dhclients +    """ +    run(['killall', 'dhclient']) +    leases_file: Path = Path('/var/lib/dhcp/dhclient.leases') +    leases_file.unlink(missing_ok=True) +    logger.debug('cleaned up after dhclients') + + +def dict_interfaces() -> dict[str, str]: +    """Return list of available network interfaces except loopback + +    Returns: +        list[str]: a list of interfaces +    """ +    interfaces_dict: dict[str, str] = {} +    ifaces = net_if_addrs() +    for iface_name, iface_addresses in ifaces.items(): +        # we do not need loopback interface +        if iface_name == 'lo': +            continue +        # check other interfaces for MAC addresses +        for iface_addr in iface_addresses: +            if iface_addr.family == AF_LINK and iface_addr.address: +                interfaces_dict[iface_name] = iface_addr.address +                continue + +    logger.debug(f'found interfaces: {interfaces_dict}') +    return interfaces_dict + + +def need_to_check() -> bool: +    """Check if we need to perform DHCP checks + +    Returns: +        bool: check result +    """ +    # if cloud-init config does not exist, we do not need to do anything +    ci_config_vyos = Path('/etc/cloud/cloud.cfg.d/20_vyos_custom.cfg') +    if not ci_config_vyos.exists(): +        logger.info( +            'No need to check interfaces: Cloud-init config file was not found') +        return False + +    # load configuration file +    try: +        config = safe_load(ci_config_vyos.read_text()) +    except: +        logger.error('Cloud-init config file has a wrong format') +        return False + +    # check if we have in config configured option +    # vyos_config_options: +    #   network_preconfigure: true +    if not config.get('vyos_config_options', {}).get('network_preconfigure'): +        logger.info( +            'No need to check interfaces: Cloud-init config option "network_preconfigure" is not set' +        ) +        return False + +    return True + + +if __name__ == '__main__': +    # prepare logger +    logger = logging.getLogger(__name__) +    logger.addHandler(JournalHandler(SYSLOG_IDENTIFIER=Path(__file__).name)) +    logger.setLevel(logging.INFO) + +    # we need to give udev some time to rename all interfaces +    # this is placed before need_to_check() call, because we are not always +    # need to preconfigure cloud-init, but udev always need to finish its work +    # before cloud-init start +    run(['udevadm', 'settle']) +    logger.info('udev finished its work, we continue') + +    # do not perform any checks if this is not required +    if not need_to_check(): +        exit() + +    # get list of interfaces and check them +    interfaces_dhcp: list[dict[str, str]] = [] +    interfaces_dict: dict[str, str] = dict_interfaces() + +    with ProcessPoolExecutor(max_workers=len(interfaces_dict)) as executor: +        iface_check_results = [{ +            'dhcp': executor.submit(check_interface_dhcp, iface_name), +            'append': { +                'name': iface_name, +                'mac': iface_mac +            } +        } for iface_name, iface_mac in interfaces_dict.items()] + +    dhclient_cleanup() + +    for iface_check_result in iface_check_results: +        if iface_check_result.get('dhcp').result(): +            interfaces_dhcp.append(iface_check_result.get('append')) + +    # render cloud-init config +    if interfaces_dhcp: +        logger.debug('rendering cloud-init network configuration') +        render(config_file, template_file, {'ifaces_list': interfaces_dhcp}) + +    exit() diff --git a/src/systemd/vyos-config-cloud-init.service b/src/systemd/vyos-config-cloud-init.service new file mode 100644 index 000000000..ba6f90e6d --- /dev/null +++ b/src/systemd/vyos-config-cloud-init.service @@ -0,0 +1,19 @@ +[Unit] +Description=Pre-configure Cloud-init +DefaultDependencies=no +Requires=systemd-remount-fs.service +Requires=systemd-udevd.service +Wants=network-pre.target +After=systemd-remount-fs.service +After=systemd-udevd.service +Before=cloud-init-local.service + +[Service] +Type=oneshot +ExecStart=/usr/libexec/vyos/system/vyos-config-cloud-init.py +TimeoutSec=120 +KillMode=process +StandardOutput=journal+console + +[Install] +WantedBy=cloud-init-local.service | 
