diff --git a/opflexagent/as_metadata_manager.py b/opflexagent/as_metadata_manager.py index 005b6a7c..92624350 100644 --- a/opflexagent/as_metadata_manager.py +++ b/opflexagent/as_metadata_manager.py @@ -38,6 +38,7 @@ from oslo_utils import encodeutils from opflexagent._i18n import _ +from opflexagent import constants as ofcst from opflexagent import config as oscfg # noqa from opflexagent.utils import utils as opflexagent_utils @@ -71,6 +72,10 @@ SVC_IP_SIZE = 1000 SVC_IP_CIDR = 16 SVC_NEXTHOP = "169.254.1.1" +SVC_V6_IP_DEFAULT = "fd00::a9fe:102" +SVC_V6_IP_BASE = int(netaddr.IPAddress("fd00::a9fe:f003")) +SVC_V6_IP_CIDR = 64 +SVC_V6_NEXTHOP = "fd00::a9fe:101" SVC_NS = "of-svc" SVC_NS_PORT = "of-svc-nsport" SVC_OVS_PORT = "of-svc-ovsport" @@ -106,6 +111,21 @@ def write_jsonfile(name, data): LOG.warning("Exception in writing file: %s", str(e)) +def normalize_ipv6_next_hop(ipaddr): + if not ipaddr: + return ipaddr + addr = netaddr.IPAddress(ipaddr) + if not addr.is_link_local(): + return ipaddr + words = addr.words + words[0] = 0xfd00 + words[1] = 0 + words[2] = 0 + words[3] = 0 + return str(netaddr.IPAddress(sum(word << (16 * (7 - idx)) + for idx, word in enumerate(words)))) + + class AddressPool(object): def __init__(self, base, size): self.base = base @@ -337,9 +357,15 @@ def process(self, files): curr_svc = read_jsonfile(self.svcfile) ip_pool = AddressPool(SVC_IP_BASE, SVC_IP_SIZE) + ip6_pool = AddressPool(SVC_V6_IP_BASE, SVC_IP_SIZE) for domain_uuid in curr_svc: thisip = netaddr.IPAddress(curr_svc[domain_uuid]['next-hop-ip']) ip_pool.reserve(int(thisip)) + thisip6 = curr_svc[domain_uuid].get('next-hop-ipv6') + if thisip6: + thisip6 = netaddr.IPAddress(normalize_ipv6_next_hop(thisip6)) + if not thisip6.is_link_local(): + ip6_pool.reserve(int(thisip6)) new_svc = {} new_nets = {} @@ -376,13 +402,25 @@ def process(self, files): as_uuid = domain_uuid as_addr = netaddr.IPAddress(ip_pool.get_addr()) as_addr = str(as_addr) + as_addr_v6 = netaddr.IPAddress(ip6_pool.get_addr()) + as_addr_v6 = str(as_addr_v6) new_svc[domain_uuid] = { 'domain-name': domain_name, 'domain-policy-space': domain_tenant, 'next-hop-ip': as_addr, + 'next-hop-ipv6': as_addr_v6, 'uuid': as_uuid, } else: + thisip6 = curr_svc[domain_uuid].get('next-hop-ipv6') + thisip6 = normalize_ipv6_next_hop(thisip6) + if not thisip6: + updated = True + thisip6 = str(netaddr.IPAddress(ip6_pool.get_addr())) + elif thisip6 != curr_svc[domain_uuid].get( + 'next-hop-ipv6'): + updated = True + curr_svc[domain_uuid]['next-hop-ipv6'] = thisip6 new_svc[domain_uuid] = curr_svc[domain_uuid] del curr_svc[domain_uuid] @@ -463,16 +501,27 @@ def as_equal(self, asvc, alloc): for idx in ["uuid", "domain-name", "domain-policy-space"]: if asvc[idx] != alloc[idx]: return False - if asvc["service-mapping"][0]["next-hop-ip"] != alloc["next-hop-ip"]: - return False - return True + service_map = { + svc["service-ip"]: ( + normalize_ipv6_next_hop(svc["next-hop-ip"]) + if svc["service-ip"] == ofcst.METADATA_DEFAULT_IPV6 + else svc["next-hop-ip"]) + for svc in asvc.get("service-mapping", []) + } + alloc_ipv6 = normalize_ipv6_next_hop(alloc.get("next-hop-ipv6")) + return ( + service_map.get(ofcst.METADATA_DEFAULT_IP) == + alloc["next-hop-ip"] and + service_map.get(ofcst.METADATA_DEFAULT_IPV6) == + alloc_ipv6) def as_del(self, filename, asvc): - try: - self.mgr.del_ip(asvc["service-mapping"][0]["next-hop-ip"]) - except Exception as e: - LOG.warning("EPwatcher: Exception in deleting IP: %s", - str(e)) + for svc in asvc.get("service-mapping", []): + try: + self.mgr.del_ip(svc["next-hop-ip"]) + except Exception as e: + LOG.warning("EPwatcher: Exception in deleting IP: %s", + str(e)) proxyfilename = PROXY_FILE_NAME_FORMAT % asvc["uuid"] proxyfilename = "%s/%s" % (MD_DIR, proxyfilename) @@ -483,6 +532,8 @@ def as_del(self, filename, asvc): LOG.warning("EPwatcher: Exception in deleting file: %s", str(e)) def as_create(self, alloc): + alloc_ipv6 = normalize_ipv6_next_hop(alloc["next-hop-ipv6"]) + alloc["next-hop-ipv6"] = alloc_ipv6 asvc = { "uuid": alloc["uuid"], "interface-name": SVC_OVS_PORT, @@ -491,18 +542,24 @@ def as_create(self, alloc): "domain-name": alloc["domain-name"], "service-mapping": [ { - "service-ip": "169.254.169.254", - "gateway-ip": "169.254.1.1", + "service-ip": ofcst.METADATA_DEFAULT_IP, + "gateway-ip": SVC_NEXTHOP, "next-hop-ip": alloc["next-hop-ip"], }, + { + "service-ip": ofcst.METADATA_DEFAULT_IPV6, + "gateway-ip": SVC_V6_NEXTHOP, + "next-hop-ip": alloc_ipv6, + }, ], } - try: - self.mgr.add_ip(alloc["next-hop-ip"]) - except Exception as e: - LOG.warning("EPwatcher: Exception in adding IP: %s", - str(e)) + for addr in [alloc["next-hop-ip"], alloc_ipv6]: + try: + self.mgr.add_ip(addr) + except Exception as e: + LOG.warning("EPwatcher: Exception in adding IP: %s", + str(e)) asfilename = AS_FILE_NAME_FORMAT % asvc["uuid"] asfilename = "%s/%s" % (AS_MAPPING_DIR, asfilename) @@ -516,33 +573,38 @@ def as_create(self, alloc): with open(proxyfilename, "w") as f: f.write(proxystr) pidfile = PID_FILE_NAME_FORMAT % asvc["uuid"] - self.mgr.sh("rm -f %s" % pidfile) + self.mgr.sh("rm -f %s %s-v4.pid %s-v6.pid" % ( + pidfile, pidfile[:-4], pidfile[:-4])) except Exception as e: LOG.warning("EPwatcher: Exception in writing proxy file: %s", str(e)) def proxyconfig(self, alloc): duuid = alloc["uuid"] - ipaddr = alloc["next-hop-ip"] - proxystr = "\n".join([ - "[program:opflex-ns-proxy-%s]" % duuid, - "command=ip netns exec of-svc " - "/usr/bin/opflex-ns-proxy " - "--metadata_proxy_socket=/var/lib/neutron/metadata_proxy " - "--state_path=/var/lib/neutron " - "--pid_file=/var/lib/neutron/external/pids/%s.pid " - "--domain_id=%s --metadata_host %s --metadata_port=80 " - "--log-dir=/var/log/neutron --log-file=opflex-ns-proxy-%s.log" % ( - duuid, duuid, ipaddr, duuid[:8]), - "exitcodes=0,2", - "stopasgroup=true", - "startsecs=10", - "startretries=3", - "stopwaitsecs=10", - "stdout_logfile=NONE", - "stderr_logfile=NONE", - ]) - return proxystr + proxy_configs = [] + for family, ipaddr in [("v4", alloc["next-hop-ip"]), + ("v6", normalize_ipv6_next_hop( + alloc["next-hop-ipv6"]))]: + proxy_configs.append("\n".join([ + "[program:opflex-ns-proxy-%s-%s]" % (duuid, family), + "command=ip netns exec of-svc " + "/usr/bin/opflex-ns-proxy " + "--metadata_proxy_socket=/var/lib/neutron/metadata_proxy " + "--state_path=/var/lib/neutron " + "--pid_file=/var/lib/neutron/external/pids/%s-%s.pid " + "--domain_id=%s --metadata_host %s --metadata_port=80 " + "--log-dir=/var/log/neutron " + "--log-file=opflex-ns-proxy-%s-%s.log" % ( + duuid, family, duuid, ipaddr, duuid[:8], family), + "exitcodes=0,2", + "stopasgroup=true", + "startsecs=10", + "startretries=3", + "stopwaitsecs=10", + "stdout_logfile=NONE", + "stderr_logfile=NONE", + ])) + return "\n".join(proxy_configs) class SnatConnTrackHandler(object): @@ -695,26 +757,43 @@ def stop_supervisor(self): self.sh("supervisorctl -c %s shutdown" % self.md_filename) time.sleep(30) - def add_default_route(self, nexthop): - self.sh("ip netns exec %s ip route add default via %s" % - (SVC_NS, nexthop)) + def add_default_route(self, nexthop, ip_version=4): + if ip_version == 6: + self.sh("ip netns exec %s ip -6 route add default via %s " + "dev %s" % (SVC_NS, nexthop, SVC_NS_PORT)) + else: + self.sh("ip netns exec %s ip route add default via %s" % + (SVC_NS, nexthop)) def has_ip(self, ipaddr): - outp = self.sh("ip netns exec %s ip addr show dev %s" % - (SVC_NS, SVC_NS_PORT)) + ip_version = netaddr.IPAddress(ipaddr).version + cmd = "ip netns exec %s ip addr show dev %s" + if ip_version == 6: + cmd = "ip netns exec %s ip -6 addr show dev %s" + outp = self.sh(cmd % (SVC_NS, SVC_NS_PORT)) return 'net %s/' % (ipaddr, ) in outp def add_ip(self, ipaddr): if self.has_ip(ipaddr): return - self.sh("ip netns exec %s ip addr add %s/%s dev %s" % - (SVC_NS, ipaddr, SVC_IP_CIDR, SVC_NS_PORT)) + ip_version = netaddr.IPAddress(ipaddr).version + if ip_version == 6: + self.sh("ip netns exec %s ip -6 addr add %s/%s dev %s" % + (SVC_NS, ipaddr, SVC_V6_IP_CIDR, SVC_NS_PORT)) + else: + self.sh("ip netns exec %s ip addr add %s/%s dev %s" % + (SVC_NS, ipaddr, SVC_IP_CIDR, SVC_NS_PORT)) def del_ip(self, ipaddr): if not self.has_ip(ipaddr): return - self.sh("ip netns exec %s ip addr del %s/%s dev %s" % - (SVC_NS, ipaddr, SVC_IP_CIDR, SVC_NS_PORT)) + ip_version = netaddr.IPAddress(ipaddr).version + if ip_version == 6: + self.sh("ip netns exec %s ip -6 addr del %s/%s dev %s" % + (SVC_NS, ipaddr, SVC_V6_IP_CIDR, SVC_NS_PORT)) + else: + self.sh("ip netns exec %s ip addr del %s/%s dev %s" % + (SVC_NS, ipaddr, SVC_IP_CIDR, SVC_NS_PORT)) def get_asport_mac(self): return self.sh( @@ -747,7 +826,9 @@ def init_host(self): self.sh("ip netns exec %s ip link set dev %s up" % (SVC_NS, SVC_NS_PORT)) self.add_ip(SVC_IP_DEFAULT) + self.add_ip(SVC_V6_IP_DEFAULT) self.add_default_route(SVC_NEXTHOP) + self.add_default_route(SVC_V6_NEXTHOP, ip_version=6) self.sh("ethtool --offload %s tx off" % SVC_OVS_PORT) self.sh("ip netns exec %s ethtool --offload %s tx off" % (SVC_NS, SVC_NS_PORT)) diff --git a/opflexagent/constants.py b/opflexagent/constants.py index 56296cfd..7610bb96 100644 --- a/opflexagent/constants.py +++ b/opflexagent/constants.py @@ -18,5 +18,7 @@ TYPE_OPFLEX = 'opflex' VHOST_USER_VPP_PLUG = 'vhostuser_vpp_plug' METADATA_DEFAULT_IP = '169.254.169.254' +METADATA_DEFAULT_IPV6 = 'fe80::a9fe:a9fe' METADATA_SUBNET = '169.254.0.0/16' +METADATA_SUBNET_V6 = 'fe80::a9fe:a9fe/128' VPCMODULE_NAME = 'vpc-%s-%s' diff --git a/opflexagent/test/test_as_metadata_mgr.py b/opflexagent/test/test_as_metadata_mgr.py index 266f7451..9b7b9402 100644 --- a/opflexagent/test/test_as_metadata_mgr.py +++ b/opflexagent/test/test_as_metadata_mgr.py @@ -38,12 +38,14 @@ "domain-name": "sauto_k8s-bm-1_l3out-1_vrf", "domain-policy-space": "common", "next-hop-ip": "169.254.240.3", + "next-hop-ipv6": "fd00::a9fe:f003", "uuid": "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9" }, "99e788f5-f579-83d2-6b9f-3051a21f63ab": { "domain-name": "k8s-bm-1_UnroutedVRF", "domain-policy-space": "common", "next-hop-ip": "169.254.240.4", + "next-hop-ipv6": "fd00::a9fe:f004", "uuid": "99e788f5-f579-83d2-6b9f-3051a21f63ab" } } @@ -52,6 +54,16 @@ "domain-name": "sauto_k8s-bm-1_l3out-1_vrf", "domain-policy-space": "common", "next-hop-ip": "169.254.240.3", + "next-hop-ipv6": "fd00::a9fe:f003", + "uuid": "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9" + } +} +legacy_curr_alloc_json = { + "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9": { + "domain-name": "sauto_k8s-bm-1_l3out-1_vrf", + "domain-policy-space": "common", + "next-hop-ip": "169.254.240.3", + "next-hop-ipv6": "fe80::a9fe:f003", "uuid": "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9" } } @@ -66,6 +78,11 @@ "service-ip": "169.254.169.254", "gateway-ip": "169.254.1.1", "next-hop-ip": "169.254.240.3" + }, + { + "service-ip": "fe80::a9fe:a9fe", + "gateway-ip": "fd00::a9fe:101", + "next-hop-ip": "fd00::a9fe:f003" } ] } @@ -80,6 +97,11 @@ "service-ip": "169.254.169.254", "gateway-ip": "169.254.1.1", "next-hop-ip": "169.254.240.3" + }, + { + "service-ip": "fe80::a9fe:a9fe", + "gateway-ip": "fd00::a9fe:101", + "next-hop-ip": "fd00::a9fe:f003" } ] } @@ -94,6 +116,11 @@ "service-ip": "169.254.169.254", "gateway-ip": "169.254.1.1", "next-hop-ip": "169.254.240.4" + }, + { + "service-ip": "fe80::a9fe:a9fe", + "gateway-ip": "fd00::a9fe:101", + "next-hop-ip": "fd00::a9fe:f004" } ] } @@ -192,7 +219,7 @@ def test_process_outdated_file(self, listdir_patch, read_jsonfile_patch, self.assertEqual(write_jsonfile_patch.call_count, 1) self.assertEqual(read_jsonfile_patch.call_count, 3) self.assertEqual(os_remove_patch.call_count, 2) - self.assertEqual(add_ip_patch.call_count, 1) + self.assertEqual(add_ip_patch.call_count, 2) @mock.patch('opflexagent.as_metadata_manager.write_jsonfile') @mock.patch('os.remove') @@ -219,7 +246,7 @@ def test_process_create_file(self, listdir_patch, read_jsonfile_patch, watcher.process("test") self.assertEqual(write_jsonfile_patch.call_count, 1) self.assertEqual(read_jsonfile_patch.call_count, 2) - self.assertEqual(add_ip_patch.call_count, 1) + self.assertEqual(add_ip_patch.call_count, 2) self.assertFalse(os_remove_patch.called) self.assertFalse(del_ip_patch.called) @@ -249,3 +276,26 @@ def test_process_delete_file(self, listdir_patch, read_jsonfile_patch, watcher.process("test") self.assertEqual(os_remove_patch.call_count, 2) self.assertEqual(read_jsonfile_patch.call_count, 3) + self.assertEqual(del_ip_patch.call_count, 2) + + def test_proxyconfig_dual_stack(self): + watcher = as_metadata_manager.StateWatcher.__new__( + as_metadata_manager.StateWatcher) + proxy = watcher.proxyconfig(curr_alloc_json[ + "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9"]) + self.assertIn( + "opflex-ns-proxy-44f67ef0-1fd8-7a7e-2bfb-e650cee859a9-v4", proxy) + self.assertIn( + "opflex-ns-proxy-44f67ef0-1fd8-7a7e-2bfb-e650cee859a9-v6", proxy) + self.assertIn("--metadata_host 169.254.240.3 --metadata_port=80", + proxy) + self.assertIn("--metadata_host fd00::a9fe:f003 --metadata_port=80", + proxy) + + def test_proxyconfig_legacy_link_local_next_hop(self): + watcher = as_metadata_manager.StateWatcher.__new__( + as_metadata_manager.StateWatcher) + proxy = watcher.proxyconfig(legacy_curr_alloc_json[ + "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9"]) + self.assertIn("--metadata_host fd00::a9fe:f003 --metadata_port=80", + proxy) diff --git a/opflexagent/test/test_endpoint_file_manager.py b/opflexagent/test/test_endpoint_file_manager.py index d98b88b0..eff9286f 100644 --- a/opflexagent/test/test_endpoint_file_manager.py +++ b/opflexagent/test/test_endpoint_file_manager.py @@ -165,7 +165,8 @@ def test_port_bound(self): "domain-name": 'name_of_l3p', "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) # Send same port info again self.manager._write_vrf_file.reset_mock() @@ -225,7 +226,8 @@ def test_port_bound(self): "domain-name": 'name_of_l3p', "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) self.manager.snat_iptables.setup_snat_for_es.assert_called_with( 'EXT-1', '200.0.0.10', None, '200.0.0.1/8', None, None, None, 'aa:bb:cc:00:11:44', mtu=9000) @@ -251,7 +253,8 @@ def test_port_bound(self): "domain-policy-space": 'apic_tenant', "domain-name": 'name_of_l3p', "internal-subnets": sorted(['192.170.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) self._check_call_list([mock.call('EXT-1', snat_ep_file)], self.manager._write_endpoint_file.call_args_list, False) @@ -711,7 +714,8 @@ def test_port_unbound_delete_vrf_file(self): "domain-name": 'name_of_l3p', "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) def test_port_bound_no_mapping(self): port = self._port() @@ -890,6 +894,10 @@ def test_interface_mtu(self): self.assertTrue('dhcp4' in ep_file) self.assertEqual(ep_file['dhcp4']['interface-mtu'], 1800) self.assertEqual(ep_file['dhcp4']['lease-time'], 100) + self.assertEqual(ep_file['dhcp4']['static-routes'], [{ + 'dest': '169.254.169.254', + 'dest-prefix': 32, + 'next-hop': '192.168.0.2'}]) self.assertEqual(ep_file['security-group'], [{'policy-space': 'common', 'name': 'gbp_default'}]) @@ -1023,7 +1031,8 @@ def test_endpoint_vrf_change(self): "domain-name": 'name_of_l3p', "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) self.assertEqual('l3p_id_1', self.manager.vif_to_vrf[port_1.vif_id]) self.assertEqual('l3p_id', self.manager.vif_to_vrf[port_2.vif_id]) self.assertEqual(set([port_1.vif_id]), @@ -1070,9 +1079,15 @@ def test_v6_subnets(self): 'ip_version': 6, 'dns_nameservers': [V6_DNS], 'cidr': '2001:db8::/64', + 'dhcp_server_ports': { + 'fa:16:3e:a7:a3:aa': [ + 'fe80::a9fe:1' + ] + }, 'host_routes': []}], dhcp_lease_time=100) port = self._port() + port.fixed_ips = [{'subnet_id': 'id1', 'ip_address': '2001:db8::2'}] self.manager._release_int_fip = mock.Mock() self.manager.declare_endpoint(port, mapping) # no MTU set whatsoever @@ -1085,6 +1100,41 @@ def test_v6_subnets(self): self.assertTrue('dhcp6' in ep_file) self.assertEqual(ep_file['dhcp6']['interface-mtu'], 1800) self.assertEqual(ep_file['dhcp6']['dns-servers'], [V6_DNS]) + self.assertEqual(ep_file['dhcp6']['static-routes'], [{ + 'dest': 'fe80::a9fe:a9fe', + 'dest-prefix': 128, + 'next-hop': 'fe80::a9fe:1'}]) + + def test_v6_metadata_route_without_dns(self): + mapping = self._get_gbp_details(enable_dhcp_optimization=True, + interface_mtu=1800, + subnets=[{'id': 'id1', + 'enable_dhcp': True, + 'ip_version': 6, + 'dns_nameservers': [], + 'cidr': '2001:db8::/64', + 'dhcp_server_ports': { + 'fa:16:3e:a7:a3:aa': [ + 'fe80::a9fe:1' + ] + }, + 'host_routes': []}], + dhcp_lease_time=100) + port = self._port() + port.fixed_ips = [{'subnet_id': 'id1', 'ip_address': '2001:db8::2'}] + self.manager._release_int_fip = mock.Mock() + self.manager.declare_endpoint(port, mapping) + ep_file = None + for arg in self.manager._write_endpoint_file.call_args_list: + if port.vif_id in arg[0][0]: + self.assertIsNone(ep_file) + ep_file = arg[0][1] + self.assertIsNotNone(ep_file) + self.assertTrue('dhcp6' in ep_file) + self.assertEqual(ep_file['dhcp6']['static-routes'], [{ + 'dest': 'fe80::a9fe:a9fe', + 'dest-prefix': 128, + 'next-hop': 'fe80::a9fe:1'}]) def test_port_trunk_details(self): mapping = self._get_gbp_details() diff --git a/opflexagent/test/test_gbp_ovs_agent.py b/opflexagent/test/test_gbp_ovs_agent.py index 633fea12..2bcb8eb8 100644 --- a/opflexagent/test/test_gbp_ovs_agent.py +++ b/opflexagent/test/test_gbp_ovs_agent.py @@ -281,7 +281,8 @@ def test_process_network_ports(self): "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', '1.1.1.0/24', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) def test_stale_endpoints_in_process_network_ports(self): self.agent.ep_manager.undeclare_endpoint = mock.Mock() @@ -443,7 +444,8 @@ def test_process_vrf_update(self): "domain-name": mapping['vrf_name'], "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) self.assertFalse(self.agent.ep_manager._delete_vrf_file.called) # Now simulate a deletion diff --git a/opflexagent/test/test_gbp_vpp_agent.py b/opflexagent/test/test_gbp_vpp_agent.py index a54f8b2f..7bd95b26 100755 --- a/opflexagent/test/test_gbp_vpp_agent.py +++ b/opflexagent/test/test_gbp_vpp_agent.py @@ -97,7 +97,9 @@ def start(self, interval=0): mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new=MockFixedIntervalLoopingCall), mock.patch('opflexagent.gbp_agent.GBPOpflexAgent.' - '_report_state')] + '_report_state'), + mock.patch('opflexagent.as_metadata_manager.' + 'SnatConnTrackHandler')] with base.nested_context_manager(*resources): agent = gbp_agent.GBPOpflexAgent(**kwargs) @@ -219,7 +221,8 @@ def test_process_network_ports(self): "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', '1.1.1.0/24', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) def test_dead_port(self): port = mock.Mock(ofport=1) @@ -374,7 +377,8 @@ def test_process_vrf_update(self): "domain-name": mapping['vrf_name'], "internal-subnets": sorted(['192.168.0.0/16', '192.169.0.0/16', - '169.254.0.0/16'])}) + '169.254.0.0/16', + 'fe80::a9fe:a9fe/128'])}) self.assertFalse(self.agent.ep_manager._delete_vrf_file.called) # Now simulate a deletion diff --git a/opflexagent/utils/ep_managers/endpoint_file_manager.py b/opflexagent/utils/ep_managers/endpoint_file_manager.py index 32f4d2d8..54420f31 100644 --- a/opflexagent/utils/ep_managers/endpoint_file_manager.py +++ b/opflexagent/utils/ep_managers/endpoint_file_manager.py @@ -94,6 +94,8 @@ def initialize(self, host, bridge_manager, config): 6: netaddr.IPSet(config['internal_floating_ip6_pool'])} if ofcst.METADATA_DEFAULT_IP in self.int_fip_pool[4]: self.int_fip_pool[4].remove(ofcst.METADATA_DEFAULT_IP) + if ofcst.METADATA_DEFAULT_IPV6 in self.int_fip_pool[6]: + self.int_fip_pool[6].remove(ofcst.METADATA_DEFAULT_IPV6) self.snat_iptables = snat_iptables_manager.SnatIptablesManager( bridge_manager) @@ -626,6 +628,26 @@ def _handle_host_snat_ip(self, host_snat_ips): def _map_dhcp_info(self, fixed_ips, mapping, mapping_dict): """ Add DHCP specific info to the EP file.""" + def append_static_route(routes, destination, next_hop): + cidr = netaddr.IPNetwork(destination) + route = {'dest': str(cidr.network), + 'dest-prefix': cidr.prefixlen, + 'next-hop': next_hop} + if route not in routes: + routes.append(route) + + def get_dhcp_server_ip(sn, ip_version): + for ip_addr in sn.get('dhcp_server_ips', []) or []: + if netaddr.IPAddress(ip_addr).version == ip_version: + return ip_addr, None + + for mac, ip_addrs in ( + sn.get('dhcp_server_ports', {}) or {}).items(): + for ip_addr in ip_addrs: + if netaddr.IPAddress(ip_addr).version == ip_version: + return ip_addr, mac + return None, None + subnets = mapping['subnets'] v4subnets = {k['id']: k for k in subnets if k['ip_version'] == 4 and k['enable_dhcp']} @@ -644,34 +666,41 @@ def _map_dhcp_info(self, fixed_ips, mapping, mapping_dict): 'prefix-len': netaddr.IPNetwork(sn['cidr']).prefixlen} dhcp4['static-routes'] = [] for hr in sn['host_routes']: - cidr = netaddr.IPNetwork(hr['destination']) - dhcp4['static-routes'].append( - {'dest': str(cidr.network), - 'dest-prefix': cidr.prefixlen, - 'next-hop': hr['nexthop']}) - if 'dhcp_server_ips' in sn and sn['dhcp_server_ips']: - dhcp4['server-ip'] = sn['dhcp_server_ips'][0] - if 'dhcp_server_ports' in sn and sn['dhcp_server_ports']: + append_static_route(dhcp4['static-routes'], + hr['destination'], hr['nexthop']) + dhcp_server_ip, dhcp_mac = get_dhcp_server_ip(sn, 4) + if dhcp_server_ip: + dhcp4['server-ip'] = dhcp_server_ip + append_static_route(dhcp4['static-routes'], + "%s/32" % ofcst.METADATA_DEFAULT_IP, + dhcp_server_ip) + if dhcp_mac: # REVISIT: The agent currenlty only supports a single # IP, so just use the first IP from the first entry in # the dict. Once the agent supports additional IPs, we # can provide the full dict. - dhcp_mac = list(sn['dhcp_server_ports'].keys())[0] dhcp4['server-mac'] = dhcp_mac - dhcp4['server-ip'] = sn['dhcp_server_ports'][dhcp_mac][0] if 'interface_mtu' in mapping: dhcp4['interface-mtu'] = mapping['interface_mtu'] if 'dhcp_lease_time' in mapping: dhcp4['lease-time'] = mapping['dhcp_lease_time'] mapping_dict['dhcp4'] = dhcp4 break - if len(v6subnets) > 0 and list(v6subnets.values())[0][ - 'dns_nameservers']: - mapping_dict['dhcp6'] = { - 'dns-servers': list(v6subnets.values())[0]['dns_nameservers']} + if len(v6subnets) > 0: + dhcp6 = {} + v6subnet = list(v6subnets.values())[0] + if v6subnet['dns_nameservers']: + dhcp6['dns-servers'] = v6subnet['dns_nameservers'] + dhcp_server_ip, _dhcp_mac = get_dhcp_server_ip(v6subnet, 6) + if dhcp_server_ip: + dhcp6['static-routes'] = [] + append_static_route(dhcp6['static-routes'], + "%s/128" % ofcst.METADATA_DEFAULT_IPV6, + dhcp_server_ip) if 'interface_mtu' in mapping: - mapping_dict['dhcp6']['interface-mtu'] = mapping[ - 'interface_mtu'] + dhcp6['interface-mtu'] = mapping['interface_mtu'] + if dhcp6: + mapping_dict['dhcp6'] = dhcp6 def _load_es_next_hop_info(self, es_cfg): def parse_range(val): @@ -932,7 +961,7 @@ def vrf_info_to_file(self, mapping, vif_id=None): vrf_info_copy = copy.deepcopy(vrf_info) vrf_info_copy['internal-subnets'] = sorted(list( vrf_info_copy['internal-subnets']) + - [ofcst.METADATA_SUBNET]) + [ofcst.METADATA_SUBNET, ofcst.METADATA_SUBNET_V6]) self._write_vrf_file(mapping['l3_policy_id'], vrf_info_copy) curr_vrf['info'] = vrf_info if vif_id: