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

Azure parse_network_config uses fallback cfg when generate IMDS network cfg fails #549

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 82 additions & 63 deletions cloudinit/sources/DataSourceAzure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,76 +1387,95 @@ def load_azure_ds_dir(source_dir):
return (md, ud, cfg, {'ovf-env.xml': contents})


def parse_network_config(imds_metadata):
@azure_ds_telemetry_reporter
def parse_network_config(imds_metadata) -> dict:
"""Convert imds_metadata dictionary to network v2 configuration.

Parses network configuration from imds metadata if present or generate
fallback network config excluding mlx4_core devices.

@param: imds_metadata: Dict of content read from IMDS network service.
@return: Dictionary containing network version 2 standard configuration.
"""
with events.ReportEventStack(
name="parse_network_config",
description="",
parent=azure_ds_reporter
) as evt:
if imds_metadata != sources.UNSET and imds_metadata:
netconfig = {'version': 2, 'ethernets': {}}
LOG.debug('Azure: generating network configuration from IMDS')
network_metadata = imds_metadata['network']
for idx, intf in enumerate(network_metadata['interface']):
# First IPv4 and/or IPv6 address will be obtained via DHCP.
# Any additional IPs of each type will be set as static
# addresses.
nicname = 'eth{idx}'.format(idx=idx)
dhcp_override = {'route-metric': (idx + 1) * 100}
dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override,
'dhcp6': False}
for addr_type in ('ipv4', 'ipv6'):
addresses = intf.get(addr_type, {}).get('ipAddress', [])
if addr_type == 'ipv4':
default_prefix = '24'
else:
default_prefix = '128'
if addresses:
dev_config['dhcp6'] = True
# non-primary interfaces should have a higher
# route-metric (cost) so default routes prefer
# primary nic due to lower route-metric value
dev_config['dhcp6-overrides'] = dhcp_override
for addr in addresses[1:]:
# Append static address config for ip > 1
netPrefix = intf[addr_type]['subnet'][0].get(
'prefix', default_prefix)
privateIp = addr['privateIpAddress']
if not dev_config.get('addresses'):
dev_config['addresses'] = []
dev_config['addresses'].append(
'{ip}/{prefix}'.format(
ip=privateIp, prefix=netPrefix))
if dev_config:
mac = ':'.join(re.findall(r'..', intf['macAddress']))
dev_config.update({
'match': {'macaddress': mac.lower()},
'set-name': nicname
})
# With netvsc, we can get two interfaces that
# share the same MAC, so we need to make sure
# our match condition also contains the driver
driver = device_driver(nicname)
if driver and driver == 'hv_netvsc':
dev_config['match']['driver'] = driver
netconfig['ethernets'][nicname] = dev_config
evt.description = "network config from imds"
else:
blacklist = ['mlx4_core']
LOG.debug('Azure: generating fallback configuration')
# generate a network config, blacklist picking mlx4_core devs
netconfig = net.generate_fallback_config(
blacklist_drivers=blacklist, config_driver=True)
evt.description = "network config from fallback"
return netconfig
if imds_metadata != sources.UNSET and imds_metadata:
try:
return _generate_network_config_from_imds_metadata(imds_metadata)
except Exception as e:
LOG.error(
'Failed generating network config '
'from IMDS network metadata: %s', str(e))
try:
return _generate_network_config_from_fallback_config()
except Exception as e:
LOG.error('Failed generating fallback network config: %s', str(e))
return {}


@azure_ds_telemetry_reporter
def _generate_network_config_from_imds_metadata(imds_metadata) -> dict:
"""Convert imds_metadata dictionary to network v2 configuration.
Parses network configuration from imds metadata.

@param: imds_metadata: Dict of content read from IMDS network service.
@return: Dictionary containing network version 2 standard configuration.
"""
netconfig = {'version': 2, 'ethernets': {}}
network_metadata = imds_metadata['network']
for idx, intf in enumerate(network_metadata['interface']):
# First IPv4 and/or IPv6 address will be obtained via DHCP.
# Any additional IPs of each type will be set as static
# addresses.
nicname = 'eth{idx}'.format(idx=idx)
dhcp_override = {'route-metric': (idx + 1) * 100}
dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override,
'dhcp6': False}
for addr_type in ('ipv4', 'ipv6'):
addresses = intf.get(addr_type, {}).get('ipAddress', [])
if addr_type == 'ipv4':
default_prefix = '24'
else:
default_prefix = '128'
if addresses:
dev_config['dhcp6'] = True
# non-primary interfaces should have a higher
# route-metric (cost) so default routes prefer
# primary nic due to lower route-metric value
dev_config['dhcp6-overrides'] = dhcp_override
for addr in addresses[1:]:
# Append static address config for ip > 1
netPrefix = intf[addr_type]['subnet'][0].get(
'prefix', default_prefix)
privateIp = addr['privateIpAddress']
if not dev_config.get('addresses'):
dev_config['addresses'] = []
dev_config['addresses'].append(
'{ip}/{prefix}'.format(
ip=privateIp, prefix=netPrefix))
if dev_config:
mac = ':'.join(re.findall(r'..', intf['macAddress']))
dev_config.update({
'match': {'macaddress': mac.lower()},
'set-name': nicname
})
# With netvsc, we can get two interfaces that
# share the same MAC, so we need to make sure
# our match condition also contains the driver
driver = device_driver(nicname)
if driver and driver == 'hv_netvsc':
dev_config['match']['driver'] = driver
netconfig['ethernets'][nicname] = dev_config
return netconfig


@azure_ds_telemetry_reporter
def _generate_network_config_from_fallback_config() -> dict:
"""Generate fallback network config excluding mlx4_core & mlx5_core devices.

@return: Dictionary containing network version 2 standard configuration.
"""
blacklist = ['mlx4_core', 'mlx5_core']
johnsonshi marked this conversation as resolved.
Show resolved Hide resolved
# generate a network config, blacklist picking mlx4_core devs
return net.generate_fallback_config(
blacklist_drivers=blacklist, config_driver=True)


@azure_ds_telemetry_reporter
Expand Down
100 changes: 80 additions & 20 deletions tests/unittests/test_datasource/test_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,19 @@ def construct_valid_ovf_env(data=None, pubkeys=None,
class TestParseNetworkConfig(CiTestCase):

maxDiff = None
fallback_config = {
'version': 1,
'config': [{
'type': 'physical', 'name': 'eth0',
'mac_address': '00:11:22:33:44:55',
'params': {'driver': 'hv_netsvc'},
'subnets': [{'type': 'dhcp'}],
}]
}

def test_single_ipv4_nic_configuration(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
johnsonshi marked this conversation as resolved.
Show resolved Hide resolved
return_value=None)
def test_single_ipv4_nic_configuration(self, m_driver):
"""parse_network_config emits dhcp on single nic with ipv4"""
expected = {'ethernets': {
'eth0': {'dhcp4': True,
Expand All @@ -173,7 +184,9 @@ def test_single_ipv4_nic_configuration(self):
'set-name': 'eth0'}}, 'version': 2}
self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))

def test_increases_route_metric_for_non_primary_nics(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
def test_increases_route_metric_for_non_primary_nics(self, m_driver):
"""parse_network_config increases route-metric for each nic"""
expected = {'ethernets': {
'eth0': {'dhcp4': True,
Expand All @@ -200,7 +213,9 @@ def test_increases_route_metric_for_non_primary_nics(self):
imds_data['network']['interface'].append(third_intf)
self.assertEqual(expected, dsaz.parse_network_config(imds_data))

def test_ipv4_and_ipv6_route_metrics_match_for_nics(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
def test_ipv4_and_ipv6_route_metrics_match_for_nics(self, m_driver):
"""parse_network_config emits matching ipv4 and ipv6 route-metrics."""
expected = {'ethernets': {
'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/128'],
Expand Down Expand Up @@ -242,7 +257,9 @@ def test_ipv4_and_ipv6_route_metrics_match_for_nics(self):
imds_data['network']['interface'].append(third_intf)
self.assertEqual(expected, dsaz.parse_network_config(imds_data))

def test_ipv4_secondary_ips_will_be_static_addrs(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
def test_ipv4_secondary_ips_will_be_static_addrs(self, m_driver):
"""parse_network_config emits primary ipv4 as dhcp others are static"""
expected = {'ethernets': {
'eth0': {'addresses': ['10.0.0.5/24'],
Expand All @@ -262,7 +279,9 @@ def test_ipv4_secondary_ips_will_be_static_addrs(self):
}
self.assertEqual(expected, dsaz.parse_network_config(imds_data))

def test_ipv6_secondary_ips_will_be_static_cidrs(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
def test_ipv6_secondary_ips_will_be_static_cidrs(self, m_driver):
"""parse_network_config emits primary ipv6 as dhcp others are static"""
expected = {'ethernets': {
'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/10'],
Expand Down Expand Up @@ -301,6 +320,42 @@ def test_match_driver_for_netvsc(self, m_driver):
}}, 'version': 2}
self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))

@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
@mock.patch('cloudinit.net.generate_fallback_config')
def test_parse_network_config_uses_fallback_cfg_when_no_network_metadata(
self, m_fallback_config, m_driver):
"""parse_network_config generates fallback network config when the
IMDS instance metadata is corrupted/invalid, such as when
network metadata is not present.
"""
imds_metadata_missing_network_metadata = copy.deepcopy(
NETWORK_METADATA)
del imds_metadata_missing_network_metadata['network']
m_fallback_config.return_value = self.fallback_config
self.assertEqual(
self.fallback_config,
dsaz.parse_network_config(
imds_metadata_missing_network_metadata))

@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
@mock.patch('cloudinit.net.generate_fallback_config')
def test_parse_network_config_uses_fallback_cfg_when_no_interface_metadata(
self, m_fallback_config, m_driver):
"""parse_network_config generates fallback network config when the
IMDS instance metadata is corrupted/invalid, such as when
network interface metadata is not present.
"""
imds_metadata_missing_interface_metadata = copy.deepcopy(
NETWORK_METADATA)
del imds_metadata_missing_interface_metadata['network']['interface']
m_fallback_config.return_value = self.fallback_config
self.assertEqual(
self.fallback_config,
dsaz.parse_network_config(
imds_metadata_missing_interface_metadata))


class TestGetMetadataFromIMDS(HttprettyTestCase):

Expand Down Expand Up @@ -783,7 +838,9 @@ def test_user_cfg_set_agent_command_plain(self):
self.assertTrue(ret)
self.assertEqual(data['agent_invoked'], cfg['agent_command'])

def test_network_config_set_from_imds(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
def test_network_config_set_from_imds(self, m_driver):
"""Datasource.network_config returns IMDS network data."""
sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
odata = {}
Expand All @@ -801,7 +858,10 @@ def test_network_config_set_from_imds(self):
dsrc.get_data()
self.assertEqual(expected_network_config, dsrc.network_config)

def test_network_config_set_from_imds_route_metric_for_secondary_nic(self):
@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
def test_network_config_set_from_imds_route_metric_for_secondary_nic(
self, m_driver):
"""Datasource.network_config adds route-metric to secondary nics."""
sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
odata = {}
Expand Down Expand Up @@ -1157,8 +1217,10 @@ def test_list_possible_azure_ds_devs(self, m_check_fbsd_cdrom,
self.assertEqual(
[mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list)

@mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
return_value=None)
@mock.patch('cloudinit.net.generate_fallback_config')
def test_imds_network_config(self, mock_fallback):
def test_imds_network_config(self, mock_fallback, m_driver):
"""Network config is generated from IMDS network data when present."""
sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
odata = {'HostName': "myhost", 'UserName': "myuser"}
Expand Down Expand Up @@ -1245,8 +1307,9 @@ def test_fallback_network_config(self, mock_fallback, mock_dd,

netconfig = dsrc.network_config
self.assertEqual(netconfig, fallback_config)
mock_fallback.assert_called_with(blacklist_drivers=['mlx4_core'],
config_driver=True)
mock_fallback.assert_called_with(
blacklist_drivers=['mlx4_core', 'mlx5_core'],
config_driver=True)

@mock.patch('cloudinit.net.get_interface_mac')
@mock.patch('cloudinit.net.get_devicelist')
Expand All @@ -1268,19 +1331,15 @@ def test_fallback_network_config_blacklist(self, mock_fallback, mock_dd,
'subnets': [{'type': 'dhcp'}],
}]
}
blacklist_config = {
'type': 'physical',
'name': 'eth1',
'mac_address': '00:11:22:33:44:55',
'params': {'driver': 'mlx4_core'}
}
mock_fallback.return_value = fallback_config

mock_devlist.return_value = ['eth0', 'eth1']
mock_devlist.return_value = ['eth0', 'eth1', 'eth2']
mock_dd.side_effect = [
'hv_netsvc', # list composition, skipped
'mlx4_core', # list composition, match
'mlx4_core', # config get driver name
'mlx5_core', # list composition, match
'mlx5_core', # config get driver name
]
mock_get_mac.return_value = '00:11:22:33:44:55'

Expand All @@ -1291,9 +1350,10 @@ def test_fallback_network_config_blacklist(self, mock_fallback, mock_dd,
self.assertTrue(ret)

netconfig = dsrc.network_config
expected_config = fallback_config
expected_config['config'].append(blacklist_config)
johnsonshi marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(netconfig, expected_config)
self.assertEqual(netconfig, fallback_config)
mock_fallback.assert_called_with(
blacklist_drivers=['mlx4_core', 'mlx5_core'],
config_driver=True)

@mock.patch(MOCKPATH + 'subp.subp')
def test_get_hostname_with_no_args(self, m_subp):
Expand Down