Skip to content

Commit

Permalink
Fix netplan static IP configuration
Browse files Browse the repository at this point in the history
This patch makes sure that debian-based OSMorphing process sets up mac address
matching in migrated netplan static configuration, thus making sure that the
migrated guest has in-place static configuration and also keeps interface names
intact.
  • Loading branch information
Dany9966 committed Dec 9, 2024
1 parent 812c0d2 commit 782dabe
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 10 deletions.
84 changes: 76 additions & 8 deletions coriolis/osmorphing/debian.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Copyright 2016 Cloudbase Solutions Srl
# All Rights Reserved.

from io import StringIO
import os
import yaml

from io import StringIO
from oslo_log import log as logging
import yaml

from coriolis import exception
from coriolis.osmorphing import base
from coriolis.osmorphing.osdetect import debian as debian_osdetect
from coriolis import utils


DEBIAN_DISTRO_IDENTIFIER = debian_osdetect.DEBIAN_DISTRO_IDENTIFIER

LO_NIC_TPL = """
Expand All @@ -24,9 +24,13 @@
iface %(device_name)s inet dhcp
"""

LOG = logging.getLogger(__name__)


class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):

netplan_base = "etc/netplan"

@classmethod
def check_os_supported(cls, detected_os_info):
if detected_os_info['distribution_name'] != (
Expand Down Expand Up @@ -96,8 +100,73 @@ def _compose_netplan_cfg(self, nics_info):
}
return yaml.dump(cfg, default_flow_style=False)

def _preserve_static_netplan_configuration(self, nics_info):
ips_info = {}
for nic in nics_info:
mac_address = nic.get('mac_address')
if not mac_address:
LOG.warning(
f"No MAC address found for NIC with ID '{nic.get('id')}. "
f"This NIC will be skipped from static IP configuration")
continue

nic_ips = nic.get('ip_addresses', [])
if not nic_ips:
LOG.warning(
f"Skipping NIC ('{mac_address}'). It has no detected IP "
f"addresses")
continue
for nic_ip in nic_ips:
ips_info[nic_ip] = mac_address
if not ips_info:
LOG.warning("No IP information found for instance. Skipping "
"static configuration")
return

for netfile in self._list_dir(self.netplan_base):
config_changed = False
if netfile.endswith('.yaml') or netfile.endswith('.yml'):
pth = f"{self.netplan_base}/{netfile}"
contents = self._read_file_sudo(pth).decode()
config = yaml.load(contents, Loader=yaml.SafeLoader)
ethernets = config.get('network', {}).get('ethernets', {})
for eth_name, eth_config in ethernets.items():
ips = eth_config.get('addresses', [])
for eth_ip in ips:
# NOTE(dvincze): addresses can also be objects, where
# netplan can set IP label and lifetime.
if isinstance(eth_ip, dict):
eth_ip_keys = list(eth_ip.keys())
if not eth_ip_keys:
LOG.warning(
"Found empty IP address object entry. "
"Skipping")
continue
eth_ip = eth_ip_keys[0]

addr = eth_ip.split('/')[0]
if ips_info.get(addr):
LOG.debug(
f"Found IP match for NIC {eth_name} for "
f"'{addr}'")
mac_addr = ips_info[addr]
if not eth_config.get('match'):
eth_config['match'] = dict()
LOG.debug(f"Setting '{mac_addr}' MAC address "
f"match field for NIC '{eth_name}'")
eth_config['match']['macaddress'] = mac_addr
eth_config['set-name'] = eth_name
config_changed = True

if config_changed:
self._exec_cmd_chroot(f'cp {pth} {pth}.bak')
new_config = yaml.dump(config, Dumper=yaml.SafeDumper)
self._write_file_sudo(pth, new_config)

def set_net_config(self, nics_info, dhcp):
if not dhcp:
if self._test_path(self.netplan_base):
self._preserve_static_netplan_configuration(nics_info)
return

self.disable_predictable_nic_names()
Expand All @@ -109,17 +178,16 @@ def set_net_config(self, nics_info, dhcp):
"cp %s %s.bak" % (ifaces_file, ifaces_file))
self._write_file_sudo(ifaces_file, contents)

netplan_base = "etc/netplan"
if self._test_path(netplan_base):
curr_files = self._list_dir(netplan_base)
if self._test_path(self.netplan_base):
curr_files = self._list_dir(self.netplan_base)
for cnf in curr_files:
if cnf.endswith(".yaml") or cnf.endswith(".yml"):
pth = "%s/%s" % (netplan_base, cnf)
pth = "%s/%s" % (self.netplan_base, cnf)
self._exec_cmd_chroot(
"mv %s %s.bak" % (pth, pth)
)
new_cfg = self._compose_netplan_cfg(nics_info)
cfg_name = "%s/coriolis_netplan.yaml" % netplan_base
cfg_name = "%s/coriolis_netplan.yaml" % self.netplan_base
self._write_file_sudo(cfg_name, new_cfg)

def get_installed_packages(self):
Expand Down
8 changes: 8 additions & 0 deletions coriolis/tests/osmorphing/data/netplan_static_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
network:
ethernets:
eth0:
addresses:
- "10.8.254.57/24":
label: maas
lifetime: 0
- "10.8.254.58/24"
48 changes: 46 additions & 2 deletions coriolis/tests/osmorphing/test_debian.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Copyright 2024 Cloudbase Solutions Srl
# All Rights Reserved.

import copy
from unittest import mock

import yaml

from coriolis import exception
from coriolis.osmorphing import base
from coriolis.osmorphing import debian
Expand Down Expand Up @@ -157,6 +159,48 @@ def test__compose_netplan_cfg(self):

self.assertEqual(result, expected_result)

@mock.patch.object(debian.BaseDebianMorphingTools, '_list_dir')
@mock.patch.object(debian.BaseDebianMorphingTools, '_read_file_sudo')
@mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
@mock.patch.object(debian.BaseDebianMorphingTools, '_write_file_sudo')
def test__preserve_static_netplan_configuration(
self, mock_write_file_sudo, mock_exec_cmd_chroot,
mock_read_file_sudo, mock_list_dir):
ipv4 = "10.8.254.57"
ipv6 = "fe80::250:56ff:feba:f3aa"
mac = "00:50:56:ba:f3:aa"
eth0_config = {
"addresses": [{f"{ipv4}/24": {"limit": 0, "label": "maas"}},
f"{ipv6}/64",
{}]}
static_netplan_config = {
"network": {
"ethernets": {
"eth0": eth0_config
}
}
}
nics_info = [
{"id": 1, "mac_address": "mac1", "ip_addresses": []},
{"id": 2, "mac_address": ""},
{"id": 3, "mac_address": mac, "ip_addresses": [ipv4, ipv6]}
]
mock_list_dir.return_value = ["static.yml"]
mock_read_file_sudo.return_value = yaml.dump(
static_netplan_config, Dumper=yaml.SafeDumper).encode()
expected_eth0 = copy.deepcopy(eth0_config)
expected_eth0['match'] = {"macaddress": mac}
expected_eth0['set-name'] = 'eth0'
expected_netplan = {"network": {"ethernets": {"eth0": expected_eth0}}}

self.morpher._preserve_static_netplan_configuration(nics_info)

mock_write_file_sudo.assert_called_once_with(
"etc/netplan/static.yml",
yaml.dump(expected_netplan, Dumper=yaml.SafeDumper))
mock_exec_cmd_chroot.assert_called_once_with(
"cp etc/netplan/static.yml etc/netplan/static.yml.bak")

@mock.patch.object(debian.BaseDebianMorphingTools, '_write_file_sudo')
@mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
@mock.patch.object(debian.BaseDebianMorphingTools, '_test_path')
Expand Down Expand Up @@ -202,11 +246,11 @@ def test_set_net_config_no_dhcp(
self, mock_disable_predictable_nic_names, mock_list_dir,
mock_test_path, mock_exec_cmd_chroot, mock_write_file_sudo):
dhcp = False
mock_test_path.return_value = True

self.morpher.set_net_config(self.nics_info, dhcp)

mock_disable_predictable_nic_names.assert_not_called()
mock_test_path.assert_not_called()
mock_list_dir.assert_not_called()
mock_write_file_sudo.assert_not_called()
mock_exec_cmd_chroot.assert_not_called()
Expand Down

0 comments on commit 782dabe

Please sign in to comment.