diff --git a/opflexagent/as_metadata_manager.py b/opflexagent/as_metadata_manager.py index 005b6a7c..5413901c 100644 --- a/opflexagent/as_metadata_manager.py +++ b/opflexagent/as_metadata_manager.py @@ -38,7 +38,9 @@ from oslo_utils import encodeutils from opflexagent._i18n import _ +from opflexagent import constants as ofcst from opflexagent import config as oscfg # noqa +from opflexagent import constants as ofcst from opflexagent.utils import utils as opflexagent_utils LOG = logging.getLogger(__name__) @@ -71,6 +73,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 +112,36 @@ 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 = list(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)))) + + +def normalize_ipv6_next_hop(ipaddr): + if not ipaddr: + return ipaddr + addr = netaddr.IPAddress(ipaddr) + if not addr.is_link_local(): + return ipaddr + words = list(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 +373,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 +418,26 @@ 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] @@ -402,9 +457,9 @@ def process(self, files): if curr_svc: updated = True + write_jsonfile(self.netsfile, new_nets) if updated: write_jsonfile(self.svcfile, new_svc) - write_jsonfile(self.netsfile, new_nets) class StateWatcher(FileWatcher): @@ -412,6 +467,7 @@ def __init__(self): root_helper = cfg.CONF.AGENT.root_helper self.mgr = AsMetadataManager(LOG, root_helper) self.svcfile = "%s/%s" % (MD_DIR, STATE_FILENAME_SVC) + self.netsfile = "%s/%s" % (MD_DIR, STATE_FILENAME_NETS) self.svc_ovsport_mac = self.mgr.get_asport_mac()[:17] self.disable_proxy = cfg.CONF.OPFLEX.disable_metadata_proxy @@ -426,6 +482,10 @@ def terminate(self, signum, frame): def process(self, files): LOG.debug("State Event: %s", files) + if not os.path.isfile(self.netsfile): + LOG.debug("Waiting for instance network state file: %s", + self.netsfile) + return curr_alloc = read_jsonfile(self.svcfile) @@ -463,16 +523,24 @@ 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"]: 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.warn("EPwatcher: Exception in deleting IP: %s", + str(e)) proxyfilename = PROXY_FILE_NAME_FORMAT % asvc["uuid"] proxyfilename = "%s/%s" % (MD_DIR, proxyfilename) @@ -483,6 +551,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 +561,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.warn("EPwatcher: Exception in adding IP: %s", + str(e)) asfilename = AS_FILE_NAME_FORMAT % asvc["uuid"] asfilename = "%s/%s" % (AS_MAPPING_DIR, asfilename) @@ -516,33 +592,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): @@ -666,7 +747,7 @@ def write_file(self, name, data): def clean_files(self): def rm_files(dirname, extension): - ignorelist = ['anycast_services.state'] + ignorelist = [STATE_FILENAME_SVC, STATE_FILENAME_NETS] try: for filename in os.listdir(dirname): if (filename.endswith('.' + extension) and @@ -695,31 +776,48 @@ 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( "ip netns exec %s ip link show %s | " - "gawk -e '/link\/ether/ {print $2}'" % + "gawk -e '/link\\/ether/ {print $2}'" % (SVC_NS, SVC_NS_PORT)) def init_host(self): @@ -747,7 +845,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..4c6626e0 100644 --- a/opflexagent/test/test_as_metadata_mgr.py +++ b/opflexagent/test/test_as_metadata_mgr.py @@ -14,6 +14,7 @@ # under the License. import copy +import os import sys from unittest import mock @@ -38,12 +39,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 +55,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 +79,30 @@ "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" + } + ] +} +legacy_link_local_fileA = { + "uuid": "44f67ef0-1fd8-7a7e-2bfb-e650cee859a9", + "interface-name": "of-svc-ovsport", + "service-mac": "02:6a:66:eb:26:6a", + "domain-policy-space": "common", + "domain-name": "sauto_k8s-bm-1_l3out-1_vrf", + "service-mapping": [ + { + "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": "fe80::a9fe:101", + "next-hop-ip": "fe80::a9fe:f003" } ] } @@ -80,6 +117,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 +136,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" } ] } @@ -129,11 +176,116 @@ def test_write_json_file(self): write_string = ''.join(write_list) self.assertEqual(write_string, JSON_FILE_DATA) + @mock.patch('opflexagent.as_metadata_manager.write_jsonfile') + @mock.patch('opflexagent.as_metadata_manager.read_jsonfile') + @mock.patch('os.listdir', return_value=['test.ep']) + def test_process_writes_instance_networks_before_services( + self, listdir_patch, read_jsonfile_patch, write_jsonfile_patch): + watcher = as_metadata_manager.EpWatcher.__new__( + as_metadata_manager.EpWatcher) + watcher.svcfile = '/state/anycast_services.state' + watcher.netsfile = '/state/instance_networks.state' + + ep_file = { + 'neutron-metadata-optimization': True, + 'domain-name': TEST_NAME, + 'domain-policy-space': TEST_TENANT, + 'neutron-network': 'net_uuid', + 'anycast-return-ip': ['fe80::f816:3eff:fe77:364b'], + } + read_jsonfile_patch.side_effect = [{}, ep_file] + + watcher.process('test') + + domain_uuid = watcher.gen_domain_uuid(TEST_TENANT, TEST_NAME) + self.assertEqual( + [ + mock.call( + watcher.netsfile, + {domain_uuid: { + 'fe80::f816:3eff:fe77:364b': 'net_uuid'}}), + mock.call( + watcher.svcfile, + {domain_uuid: { + 'domain-name': TEST_NAME, + 'domain-policy-space': TEST_TENANT, + 'next-hop-ip': '169.254.240.3', + 'next-hop-ipv6': 'fd00::a9fe:f003', + 'uuid': domain_uuid}}), + ], + write_jsonfile_patch.call_args_list) + + +class TestAsMetadataManager(base.BaseTestCase): + + def setUp(self): + super(TestAsMetadataManager, self).setUp() + + @mock.patch('os.remove') + @mock.patch('os.listdir') + def test_clean_files_preserves_state_files(self, listdir_patch, + remove_patch): + mgr = as_metadata_manager.AsMetadataManager.__new__( + as_metadata_manager.AsMetadataManager) + listdir_patch.side_effect = [ + [], + ['anycast_services.state', 'instance_networks.state', + 'old.proxy.state'], + [], + [], + ] + + mgr.clean_files() + + remove_patch.assert_called_once_with( + '%s/old.proxy.state' % as_metadata_manager.MD_DIR) + class TestStateWatcher(base.BaseTestCase): def setUp(self): super(TestStateWatcher, self).setUp() + real_isfile = os.path.isfile + netsfile = "%s/%s" % (as_metadata_manager.MD_DIR, + as_metadata_manager.STATE_FILENAME_NETS) + + def isfile(path): + if path == netsfile: + return True + return real_isfile(path) + + self.isfile_patch = mock.patch('os.path.isfile', side_effect=isfile) + self.isfile_mock = self.isfile_patch.start() + self.addCleanup(self.isfile_patch.stop) + + @mock.patch('opflexagent.as_metadata_manager.write_jsonfile') + @mock.patch('os.remove') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager' + '.update_supervisor') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager.del_ip') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager.add_ip') + @mock.patch('opflexagent.as_metadata_manager.FileProcessor.run') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager' + '.get_asport_mac', + return_value="ff-ff-ff-ff-ff-ff") + @mock.patch('opflexagent.as_metadata_manager.read_jsonfile') + @mock.patch('os.listdir') + def test_process_waits_for_instance_networks_state( + self, listdir_patch, read_jsonfile_patch, asport_mac_patch, + fileprocessor_run_patch, add_ip_patch, del_ip_patch, + update_sv_patch, os_remove_patch, write_jsonfile_patch): + watcher = as_metadata_manager.StateWatcher() + self.isfile_mock.side_effect = None + self.isfile_mock.return_value = False + watcher.process("test") + + self.assertFalse(read_jsonfile_patch.called) + self.assertFalse(listdir_patch.called) + self.assertFalse(write_jsonfile_patch.called) + self.assertFalse(add_ip_patch.called) + self.assertFalse(del_ip_patch.called) + self.assertFalse(update_sv_patch.called) + self.assertFalse(os_remove_patch.called) @mock.patch('opflexagent.as_metadata_manager.write_jsonfile') @mock.patch('os.remove') @@ -192,7 +344,51 @@ 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') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager' + '.update_supervisor') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager.del_ip') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager.add_ip') + @mock.patch('opflexagent.as_metadata_manager.FileProcessor.run') + @mock.patch('opflexagent.as_metadata_manager.AsMetadataManager' + '.get_asport_mac', + return_value="ff-ff-ff-ff-ff-ff") + @mock.patch('opflexagent.as_metadata_manager.read_jsonfile', + side_effect=[copy.deepcopy(curr_alloc_json), + copy.deepcopy(legacy_link_local_fileA), + copy.deepcopy(nochange_fileB)]) + @mock.patch('os.listdir', + return_value=["44f67ef0-1fd8-7a7e-2bfb-e650cee859a9.as", + "99e788f5-f579-83d2-6b9f-3051a21f63ab.as"]) + def test_process_legacy_link_local_file(self, listdir_patch, + read_jsonfile_patch, + asport_mac_patch, + fileprocessor_run_patch, + add_ip_patch, del_ip_patch, + update_sv_patch, + os_remove_patch, + write_jsonfile_patch): + with mock.patch(MOCK_MODULE, + new=mock.mock_open()) as open_file, mock.patch( + 'opflexagent.as_metadata_manager' + '.AsMetadataManager.sh'): + watcher = as_metadata_manager.StateWatcher() + watcher.disable_proxy = False + watcher.process("test") + 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(del_ip_patch.call_count, 2) + self.assertEqual(add_ip_patch.call_count, 2) + self.assertEqual(update_sv_patch.call_count, 1) + proxy = ''.join(call[0][0] + for call in open_file().write.call_args_list) + self.assertIn("--metadata_host fd00::a9fe:f003 --metadata_port=80", + proxy) + self.assertNotIn("--metadata_host fe80::a9fe:f003", proxy) @mock.patch('opflexagent.as_metadata_manager.write_jsonfile') @mock.patch('os.remove') @@ -219,7 +415,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 +445,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..6fe89ebc 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,43 @@ 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'}]) + self.assertEqual(['2001:db8::2', 'fe80::a8bb:ccff:fe00:1122'], + ep_file['anycast-return-ip']) + + 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..ede7e746 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) @@ -450,8 +452,15 @@ def filter_cidr_aaps(aaps): if virtual_ips: mapping_dict['virtual-ip'] = sorted(virtual_ips, key=lambda x: x['ip']) - if ips or ips_aap: - mapping_dict['anycast-return-ip'] = sorted(ips + ips_aap) + anycast_return_ips = ips + ips_aap + if (mapping['enable_metadata_optimization'] and + any(netaddr.IPNetwork(ip).version == 6 + for ip in anycast_return_ips)): + anycast_return_ips.append( + str(netaddr.EUI(mac).ipv6_link_local())) + if anycast_return_ips: + mapping_dict['anycast-return-ip'] = sorted( + set(anycast_return_ips)) if 'active_active_aap' in mapping: mapping_dict['active-active-aap'] = mapping['active_active_aap'] @@ -626,6 +635,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 +673,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 +968,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: