Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cloud-init: T5190: Added Cloud-init pre-configurator #1978

Merged
merged 1 commit into from May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions data/templates/system/cloud_init_networking.j2
@@ -0,0 +1,9 @@
network:
version: 2
ethernets:
{% for iface in ifaces_list %}
{{ iface['name'] }}:
dhcp4: true
match:
macaddress: "{{ iface['mac'] }}"
{% endfor %}
3 changes: 3 additions & 0 deletions debian/vyos-1x.postinst
Expand Up @@ -122,5 +122,8 @@ if test -f /etc/pam.d/frr; then
fi
fi

# Enable Cloud-init pre-configuration service
systemctl enable vyos-config-cloud-init.service
c-po marked this conversation as resolved.
Show resolved Hide resolved

# Generate API GraphQL schema
/usr/libexec/vyos/services/api/graphql/generate/generate_schema.py
169 changes: 169 additions & 0 deletions 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()
19 changes: 19 additions & 0 deletions 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