diff --git a/cloudbaseinit/plugins/common/networkconfig.py b/cloudbaseinit/plugins/common/networkconfig.py index 56a54ccc..af492636 100644 --- a/cloudbaseinit/plugins/common/networkconfig.py +++ b/cloudbaseinit/plugins/common/networkconfig.py @@ -12,9 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. - import re +import netaddr from oslo_log import log as oslo_logging from cloudbaseinit import exception @@ -37,6 +37,8 @@ NET_REQUIRE = { "dnsnameservers": False } +BOND_FORMAT_STR = "bond_%s" + def _name2idx(name): """Get the position of a network interface by its name.""" @@ -120,13 +122,8 @@ def _preprocess_nics(network_details, network_adapters): class NetworkConfigPlugin(plugin_base.BasePlugin): - - def execute(self, service, shared_data): + def _process_network_details(self, network_details): osutils = osutils_factory.get_os_utils() - network_details = service.get_network_details() - if not network_details: - return plugin_base.PLUGIN_EXECUTION_DONE, False - # Check and save NICs by MAC. network_adapters = osutils.get_network_adapters() network_details = _preprocess_nics(network_details, @@ -148,6 +145,7 @@ class NetworkConfigPlugin(plugin_base.BasePlugin): name = osutils.get_network_adapter_name_by_mac_address(mac) LOG.info("Configuring network adapter: %s", name) + reboot = osutils.set_static_network_config( name, nic.address, @@ -173,3 +171,141 @@ class NetworkConfigPlugin(plugin_base.BasePlugin): LOG.error("No adapters were configured") return plugin_base.PLUGIN_EXECUTION_DONE, reboot_required + + @staticmethod + def _process_link_common(osutils, link): + LOG.debug( + "Enable network adapter \"%(name)s\": %(enabled)s", + {"name": link.name, "enabled": link.enabled}) + osutils.enable_network_adapter(link.name, link.enabled) + + if link.mtu: + LOG.debug( + "Setting MTU on network adapter \"%(name)s\": %(mtu)s", + {"name": link.name, "mtu": link.mtu}) + osutils.set_network_adapter_mtu(link.name, link.mtu) + + @staticmethod + def _process_physical_links(osutils, network_details): + physical_links = [ + link for link in network_details.links if + link.type == network_model.LINK_TYPE_PHYSICAL] + + for link in physical_links: + adapter_name = osutils.get_network_adapter_name_by_mac_address( + link.mac_address) + + if adapter_name != link.name: + LOG.info( + "Renaming network adapter \"%(old_name)s\" to " + "\"%(new_name)s\"", + {"old_name": adapter_name, "new_name": link.name}) + osutils.rename_network_adapter(adapter_name, link.name) + + NetworkConfigPlugin._process_link_common(osutils, link) + + @staticmethod + def _process_bond_links(osutils, network_details): + bond_links = [ + link for link in network_details.links if + link.type == network_model.LINK_TYPE_BOND] + + for link in bond_links: + bond_name = BOND_FORMAT_STR % link.id + primary_nic_vlan_id = None + LOG.info("Creating network team: %s", bond_name) + osutils.create_network_team( + bond_name, link.bond.type, link.bond.lb_algorithm, + link.bond.members, link.mac_address, link.name, + primary_nic_vlan_id, link.bond.lacp_rate) + + NetworkConfigPlugin._process_link_common(osutils, link) + + @staticmethod + def _process_vlan_links(osutils, network_details): + vlan_links = [ + link for link in network_details.links if + link.type == network_model.LINK_TYPE_VLAN] + + for link in vlan_links: + bond_name = BOND_FORMAT_STR % link.vlan_link + LOG.info( + "Creating bond network adapter \"%(nic_name)s\" on team " + "\"%(bond_name)s\" with VLAN: %(vlan_id)s", + {"nic_name": link.name, "bond_name": bond_name, + "vlan_id": link.vlan_id}) + osutils.add_network_team_nic(bond_name, link.name, link.vlan_id) + + NetworkConfigPlugin._process_link_common(osutils, link) + + @staticmethod + def _get_default_dns_nameservers(network_details): + ipv4_nameservers = [] + ipv6_nameservers = [] + for s in network_details.services: + if isinstance(s, network_model.NameServerService): + for nameserver in s.addresses: + if netaddr.valid_ipv6(nameserver): + ipv6_nameservers.append(nameserver) + else: + ipv4_nameservers.append(nameserver) + return (ipv4_nameservers, ipv6_nameservers) + + @staticmethod + def _process_networks(osutils, network_details): + reboot_required = False + ipv4_ns, ipv6_ns = NetworkConfigPlugin._get_default_dns_nameservers( + network_details) + + for net in network_details.networks: + ip_address, prefix_len = net.address_cidr.split("/") + + gateway = None + default_gw_route = [ + r for r in net.routes if + netaddr.IPNetwork(r.network_cidr).prefixlen == 0] + if default_gw_route: + gateway = default_gw_route[0].gateway + + nameservers = net.dns_nameservers + if not nameservers: + if netaddr.valid_ipv6(ip_address): + nameservers = ipv6_ns + else: + nameservers = ipv4_ns + + LOG.info( + "Setting static IP configuration on network adapter " + "\"%(name)s\". IP: %(ip)s, prefix length: %(prefix_len)s, " + "gateway: %(gateway)s, dns: %(dns)s", + {"name": net.link, "ip": ip_address, "prefix_len": prefix_len, + "gateway": gateway, "dns": nameservers}) + reboot = osutils.set_static_network_config( + net.link, ip_address, prefix_len, gateway, nameservers) + reboot_required = reboot or reboot_required + + return reboot_required + + @staticmethod + def _process_network_details_v2(network_details): + osutils = osutils_factory.get_os_utils() + + NetworkConfigPlugin._process_physical_links( + osutils, network_details) + NetworkConfigPlugin._process_bond_links(osutils, network_details) + NetworkConfigPlugin._process_vlan_links(osutils, network_details) + reboot_required = NetworkConfigPlugin._process_networks( + osutils, network_details) + + return plugin_base.PLUGIN_EXECUTION_DONE, reboot_required + + def execute(self, service, shared_data): + network_details = service.get_network_details_v2() + if network_details: + return self._process_network_details_v2(network_details) + + network_details = service.get_network_details() + if network_details: + return self._process_network_details(network_details) + + return plugin_base.PLUGIN_EXECUTION_DONE, False diff --git a/cloudbaseinit/tests/plugins/common/test_networkconfig.py b/cloudbaseinit/tests/plugins/common/test_networkconfig.py index 965b2a05..eaed4d00 100644 --- a/cloudbaseinit/tests/plugins/common/test_networkconfig.py +++ b/cloudbaseinit/tests/plugins/common/test_networkconfig.py @@ -31,20 +31,21 @@ from cloudbaseinit.tests import testutils class TestNetworkConfigPlugin(unittest.TestCase): def setUp(self): - self._setUp() + self._setup_network_details_v1() @mock.patch("cloudbaseinit.osutils.factory.get_os_utils") - def _test_execute(self, mock_get_os_utils, - network_adapters=None, - network_details=None, - invalid_details=False, - missed_adapters=[], - extra_network_details=[]): + def _test_execute_network_details_v1(self, mock_get_os_utils, + network_adapters=None, + network_details=None, + invalid_details=False, + missed_adapters=[], + extra_network_details=[]): # Prepare mock environment. mock_service = mock.MagicMock() mock_shared_data = mock.Mock() mock_osutils = mock.MagicMock() mock_service.get_network_details.return_value = network_details + mock_service.get_network_details_v2.return_value = None mock_get_os_utils.return_value = mock_osutils mock_osutils.get_network_adapters.return_value = network_adapters mock_osutils.set_static_network_config.return_value = True @@ -102,7 +103,8 @@ class TestNetworkConfigPlugin(unittest.TestCase): reboot = len(missed_adapters) != self._count self.assertEqual((plugin_base.PLUGIN_EXECUTION_DONE, reboot), ret) - def _setUp(self, same_names=True, wrong_names=False, no_macs=False): + def _setup_network_details_v1(self, same_names=True, wrong_names=False, + no_macs=False): # Generate fake pairs of NetworkDetails objects and # local ethernet network adapters. iface_name = "Ethernet" if wrong_names else "eth" @@ -179,42 +181,43 @@ class TestNetworkConfigPlugin(unittest.TestCase): # Get the network config plugin. self._network_plugin = networkconfig.NetworkConfigPlugin() # Execution wrapper. - self._partial_test_execute = functools.partial( - self._test_execute, + self._partial_test_execute_network_details_v1 = functools.partial( + self._test_execute_network_details_v1, network_adapters=self._network_adapters, network_details=self._network_details ) def test_execute_no_network_details(self): self._network_details[:] = [] - self._partial_test_execute() + self._partial_test_execute_network_details_v1() def test_execute_no_network_adapters(self): self._network_adapters[:] = [] - self._partial_test_execute() + self._partial_test_execute_network_details_v1() def test_execute_invalid_network_details(self): self._network_details.append([None] * 6) - self._partial_test_execute(invalid_details=True) + self._partial_test_execute_network_details_v1(invalid_details=True) def test_execute_invalid_network_details_name(self): - self._setUp(wrong_names=True, no_macs=True) - self._partial_test_execute(invalid_details=True) + self._setup_network_details_v1(wrong_names=True, no_macs=True) + self._partial_test_execute_network_details_v1(invalid_details=True) def test_execute_single(self): for _ in range(self._count - 1): self._network_adapters.pop() self._network_details.pop() - self._partial_test_execute() + self._partial_test_execute_network_details_v1() def test_execute_multiple(self): - self._partial_test_execute() + self._partial_test_execute_network_details_v1() def test_execute_missing_one(self): self.assertGreater(self._count, 1) self._network_details.pop(0) adapter = self._network_adapters[0] - self._partial_test_execute(missed_adapters=[adapter]) + self._partial_test_execute_network_details_v1( + missed_adapters=[adapter]) def test_execute_missing_all(self): nic = self._network_details[0] @@ -231,7 +234,8 @@ class TestNetworkConfigPlugin(unittest.TestCase): nic.dnsnameservers ) self._network_details[:] = [nic] - self._partial_test_execute(missed_adapters=self._network_adapters) + self._partial_test_execute_network_details_v1( + missed_adapters=self._network_adapters) def _test_execute_missing_smth(self, name=False, mac=False, address=False, address6=False, @@ -262,7 +266,7 @@ class TestNetworkConfigPlugin(unittest.TestCase): # Or other vital details. missed_adapters = [self._network_adapters[ind]] extra_network_details = [] - self._partial_test_execute( + self._partial_test_execute_network_details_v1( missed_adapters=missed_adapters, extra_network_details=extra_network_details ) @@ -271,7 +275,7 @@ class TestNetworkConfigPlugin(unittest.TestCase): self._test_execute_missing_smth(mac=True) def test_execute_missing_mac2(self): - self._setUp(same_names=False) + self._setup_network_details_v1(same_names=False) self._test_execute_missing_smth(mac=True) def test_execute_missing_name_mac(self): @@ -295,3 +299,174 @@ class TestNetworkConfigPlugin(unittest.TestCase): def test_execute_missing_gateway(self): self._test_execute_missing_smth(gateway=True) + + def _get_network_details_v2(self): + links = [] + link1 = network_model.Link( + id=mock.sentinel.link_id1, + name=mock.sentinel.link_name1, + type=network_model.LINK_TYPE_PHYSICAL, + enabled=mock.sentinel.link_enabled1, + mac_address=mock.sentinel.link_mac1, + mtu=mock.sentinel.link_mtu1, + bond=None, + vlan_link=None, + vlan_id=None) + links.append(link1) + + bond1 = network_model.Bond( + members=[mock.sentinel.link_id1], + type=mock.sentinel.bond_type1, + lb_algorithm=mock.sentinel.bond_lb_algo1, + lacp_rate=mock.sentinel.lacp_rate1) + + bond_link1 = network_model.Link( + id=mock.sentinel.bond_link_id1, + name=mock.sentinel.bond_link_name1, + type=network_model.LINK_TYPE_BOND, + enabled=mock.sentinel.bond_link_enabled1, + mac_address=mock.sentinel.bond_link_mac1, + mtu=mock.sentinel.bond_link_mtu1, + bond=bond1, + vlan_link=None, + vlan_id=None) + links.append(bond_link1) + + vlan_link1 = network_model.Link( + id=mock.sentinel.vlan_link_id1, + name=mock.sentinel.vlan_link_name1, + type=network_model.LINK_TYPE_VLAN, + enabled=mock.sentinel.vlan_link_enabled1, + mac_address=mock.sentinel.vlan_link_mac1, + mtu=mock.sentinel.vlan_link_mtu1, + bond=None, + vlan_link=mock.sentinel.bond_link_id1, + vlan_id=mock.sentinel.vlan_id1) + links.append(vlan_link1) + + networks = [] + route1 = network_model.Route( + network_cidr=mock.sentinel.network_cidr1, + gateway=mock.sentinel.gateway1) + + route2 = network_model.Route( + network_cidr=mock.sentinel.network_cidr2, + gateway=mock.sentinel.gateway2) + + network1 = network_model.Network( + link=mock.sentinel.link_id1, + address_cidr=mock.sentinel.address_cidr1, + dns_nameservers=mock.sentinel.network_dns_list1, + routes=[route1, route2]) + networks.append(network1) + + services = [] + service1 = network_model.NameServerService( + addresses=[mock.sentinel.dns1, mock.sentinel.dns3], + search=mock.sentinel.dns_search1) + services.append(service1) + + return network_model.NetworkDetailsV2( + links=links, networks=networks, services=services) + + @mock.patch("cloudbaseinit.osutils.factory.get_os_utils") + def _test_execute_network_details_v2(self, mock_get_os_utils, + empty_network_dns_list=False, + both_ipv4_dns_list=False, + both_ipv6_dns_list=False): + mock.sentinel.link_mac1 = u"00:00:00:00:00:01" + mock.sentinel.network_cidr1 = u"0.0.0.0/0" + mock.sentinel.gateway1 = u"10.0.0.254" + mock.sentinel.network_cidr2 = u"172.16.0.0/16" + mock.sentinel.gateway2 = u"172.16.1.1" + mock.sentinel.address_cidr1 = u"10.0.0.1/24" + mock.sentinel.dns1 = "10.0.0.1" + mock.sentinel.dns2 = "10.0.0.2" + mock.sentinel.network_dns_list1 = [] + + if empty_network_dns_list: + mock.sentinel.dns3 = "10.0.0.3" + expected_dns_list = [mock.sentinel.dns1, mock.sentinel.dns3] + elif both_ipv4_dns_list: + mock.sentinel.dns3 = "2001:db8::3" + expected_dns_list = [mock.sentinel.dns1] + elif both_ipv6_dns_list: + mock.sentinel.address_cidr1 = u"2001:db8::3/24" + mock.sentinel.dns3 = "2001:db8::4" + expected_dns_list = [mock.sentinel.dns3] + else: + mock.sentinel.network_dns_list1 = [ + mock.sentinel.dns1, mock.sentinel.dns2] + expected_dns_list = mock.sentinel.network_dns_list1 + + service = mock.Mock() + network_details = self._get_network_details_v2() + service.get_network_details_v2.return_value = network_details + + mock_os_utils = mock.Mock() + mock_get_os_utils.return_value = mock_os_utils + + m = mock_os_utils.get_network_adapter_name_by_mac_address + m.return_value = mock.sentinel.adapter_old_name1 + + plugin = networkconfig.NetworkConfigPlugin() + plugin.execute(service, {}) + + service.get_network_details_v2.assert_called_once_with() + service.get_network_details.assert_not_called() + + m.assert_called_once_with(mock.sentinel.link_mac1) + + mock_os_utils.rename_network_adapter.assert_called_once_with( + mock.sentinel.adapter_old_name1, mock.sentinel.link_name1) + + bond_name = (networkconfig.BOND_FORMAT_STR % + mock.sentinel.bond_link_id1) + mock_os_utils.create_network_team.assert_called_once_with( + bond_name, mock.sentinel.bond_type1, + mock.sentinel.bond_lb_algo1, + [mock.sentinel.link_id1], + mock.sentinel.bond_link_mac1, + mock.sentinel.bond_link_name1, + None, + mock.sentinel.lacp_rate1) + + mock_os_utils.add_network_team_nic.assert_called_once_with( + bond_name, + mock.sentinel.vlan_link_name1, + mock.sentinel.vlan_id1) + + mock_os_utils.set_network_adapter_mtu.assert_has_calls( + [mock.call(mock.sentinel.link_name1, mock.sentinel.link_mtu1), + mock.call( + mock.sentinel.bond_link_name1, mock.sentinel.bond_link_mtu1), + mock.call( + mock.sentinel.vlan_link_name1, mock.sentinel.vlan_link_mtu1)], + any_order=False) + + mock_os_utils.enable_network_adapter.assert_has_calls( + [mock.call(mock.sentinel.link_name1, mock.sentinel.link_enabled1), + mock.call( + mock.sentinel.bond_link_name1, + mock.sentinel.bond_link_enabled1), + mock.call( + mock.sentinel.vlan_link_name1, + mock.sentinel.vlan_link_enabled1)], + any_order=False) + + ip_address, prefix_len = mock.sentinel.address_cidr1.split("/") + mock_os_utils.set_static_network_config.assert_called_once_with( + mock.sentinel.link_id1, ip_address, prefix_len, + mock.sentinel.gateway1, expected_dns_list) + + def test_execute_network_details_v2(self): + self._test_execute_network_details_v2() + + def test_execute_network_details_v2_empty_network_dns_list(self): + self._test_execute_network_details_v2(empty_network_dns_list=True) + + def test_execute_network_details_v2_ipv4_dns_list(self): + self._test_execute_network_details_v2(both_ipv4_dns_list=True) + + def test_execute_network_details_v2_ipv6_dns_list(self): + self._test_execute_network_details_v2(both_ipv6_dns_list=True)