From ddfe418f3d231a28833aa0e5ab7cc5d1006c3d9d Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 28 Jan 2013 16:41:53 -0800 Subject: [PATCH] Allow nicira plugin to handle multiple NVP API versions Bug #1126352 This patch introduces a simple framework for enabling nvlib to call the appropriate routine according to the current version. To this aim, we leverage the 'server' header which is returned by every NVP API calls (except login/logout). The patch also accounts for the changes introduced in NVP 3.0 Change-Id: Ief3a6f2b5b99ea33548353ce3bee3fa23e42752f --- .../nicira/nicira_nvp_plugin/NvpApiClient.py | 25 +++++ .../nicira/nicira_nvp_plugin/QuantumPlugin.py | 17 +++- .../nicira/nicira_nvp_plugin/nvplib.py | 97 ++++++++++++++++--- .../tests/unit/nicira/test_nicira_plugin.py | 3 +- quantum/tests/unit/nicira/test_nvplib.py | 95 ++++++++++++++++++ 5 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 quantum/tests/unit/nicira/test_nvplib.py diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py b/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py index ae511e580f..d9f491e1a3 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py @@ -26,6 +26,17 @@ LOG = logging.getLogger("NVPApiHelper") LOG.setLevel(logging.INFO) +def _find_nvp_version_in_headers(headers): + # be safe if headers is None - do not cause a failure + for (header_name, header_value) in (headers or ()): + try: + if header_name == 'server': + return header_value.split('/')[1] + except IndexError: + LOG.warning(_("Unable to fetch NVP version from response " + "headers:%s"), headers) + + class NVPApiHelper(client_eventlet.NvpApiClientEventlet): ''' Helper class to do basic login, cookie management, and provide base @@ -62,7 +73,10 @@ class NVPApiHelper(client_eventlet.NvpApiClientEventlet): self._http_timeout = http_timeout self._retries = retries self._redirects = redirects + self._nvp_version = None + # NOTE(salvatore-orlando): This method is not used anymore. Login is now + # performed automatically inside the request eventlet if necessary. def login(self, user=None, password=None): '''Login to NVP controller. @@ -130,8 +144,19 @@ class NVPApiHelper(client_eventlet.NvpApiClientEventlet): 'status': response.status, 'body': response.body}) return None + if not self._nvp_version: + self._nvp_version = _find_nvp_version_in_headers(response.headers) + return response.body + def get_nvp_version(self): + if not self._nvp_version: + # generate a simple request (/ws.v1/log) + # this will cause nvp_version to be fetched + # don't bother about response + self.request('GET', '/ws.v1/log') + return self._nvp_version + def fourZeroFour(self): raise ResourceNotFound() diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py index dbbfe5c75a..0f54173548 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py @@ -61,6 +61,8 @@ from quantum.plugins.nicira.nicira_nvp_plugin.nvp_plugin_version import ( PLUGIN_VERSION) LOG = logging.getLogger("QuantumPlugin") +NVP_FLOATINGIP_NAT_RULES_ORDER = 200 +NVP_EXTGW_NAT_RULES_ORDER = 255 # Provider network extension - allowed network types for the NVP Plugin @@ -470,7 +472,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, cluster, router_id, ip_addresses[0].split('/')[0], ip_addresses[0].split('/')[0], - source_ip_addresses=cidr) + order=NVP_EXTGW_NAT_RULES_ORDER, + match_criteria={'source_ip_addresses': cidr}) LOG.debug(_("_nvp_create_ext_gw_port completed on external network " "%(ext_net_id)s, attached to router:%(router_id)s. " @@ -1500,7 +1503,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, subnet = self._get_subnet(context, subnet_id) nvplib.create_lrouter_snat_rule( cluster, router_id, snat_ip, snat_ip, - source_ip_addresses=subnet['cidr']) + order=NVP_EXTGW_NAT_RULES_ORDER, + match_criteria={'source_ip_addresses': subnet['cidr']}) LOG.debug(_("Add_router_interface completed for subnet:%(subnet_id)s " "and router:%(router_id)s"), @@ -1678,13 +1682,16 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, try: # Create new NAT rules nvplib.create_lrouter_dnat_rule( - cluster, router_id, internal_ip, internal_ip, - destination_ip_addresses=floating_ip) + cluster, router_id, internal_ip, + order=NVP_FLOATINGIP_NAT_RULES_ORDER, + match_criteria={'destination_ip_addresses': + floating_ip}) # setup snat rule such that src ip of a IP packet when # using floating is the floating ip itself. nvplib.create_lrouter_snat_rule( cluster, router_id, floating_ip, floating_ip, - source_ip_addresses=internal_ip) + order=NVP_FLOATINGIP_NAT_RULES_ORDER, + match_criteria={'source_ip_addresses': internal_ip}) # Add Floating IP address to router_port nvplib.update_lrouter_port_ips(cluster, router_id, diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py index eac3aaf4fd..388ba68820 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py @@ -22,6 +22,7 @@ from copy import copy import hashlib +import inspect import json import logging @@ -82,6 +83,28 @@ _net_type_cache = {} # cache of {net_id: network_type} _lqueue_cache = {} +def version_dependent(func): + func_name = func.__name__ + + def dispatch_version_dependent_function(cluster, *args, **kwargs): + nvp_ver = cluster.api_client.get_nvp_version() + if nvp_ver: + ver_major = int(nvp_ver.split('.')[0]) + real_func = NVPLIB_FUNC_DICT[func_name][ver_major] + func_kwargs = kwargs + arg_spec = inspect.getargspec(real_func) + if not arg_spec.keywords and not arg_spec.varargs: + # drop args unknown to function from func_args + arg_set = set(func_kwargs.keys()) + for arg in arg_set - set(arg_spec.args): + del func_kwargs[arg] + # NOTE(salvatore-orlando): shall we fail here if a required + # argument is not passed, or let the called function raise? + real_func(cluster, *args, **func_kwargs) + + return dispatch_version_dependent_function + + def _build_uri_path(resource, resource_id=None, parent_resource_id=None, @@ -990,26 +1013,28 @@ def _create_lrouter_nat_rule(cluster, router_id, nat_rule_obj): return rule -def create_lrouter_snat_rule(cluster, router_id, - min_src_ip, max_src_ip, **kwargs): +def _build_snat_rule_obj(min_src_ip, max_src_ip, nat_match_obj): + return {"to_source_ip_address_min": min_src_ip, + "to_source_ip_address_max": max_src_ip, + "type": "SourceNatRule", + "match": nat_match_obj} - nat_match_obj = _create_nat_match_obj(**kwargs) - nat_rule_obj = { - "to_source_ip_address_min": min_src_ip, - "to_source_ip_address_max": max_src_ip, - "type": "SourceNatRule", - "match": nat_match_obj - } + +def create_lrouter_snat_rule_v2(cluster, router_id, + min_src_ip, max_src_ip, match_criteria=None): + + nat_match_obj = _create_nat_match_obj(**match_criteria) + nat_rule_obj = _build_snat_rule_obj(min_src_ip, max_src_ip, nat_match_obj) return _create_lrouter_nat_rule(cluster, router_id, nat_rule_obj) -def create_lrouter_dnat_rule(cluster, router_id, to_min_dst_ip, - to_max_dst_ip, to_dst_port=None, **kwargs): +def create_lrouter_dnat_rule_v2(cluster, router_id, dst_ip, + to_dst_port=None, match_criteria=None): - nat_match_obj = _create_nat_match_obj(**kwargs) + nat_match_obj = _create_nat_match_obj(**match_criteria) nat_rule_obj = { - "to_destination_ip_address_min": to_min_dst_ip, - "to_destination_ip_address_max": to_max_dst_ip, + "to_destination_ip_address_min": dst_ip, + "to_destination_ip_address_max": dst_ip, "type": "DestinationNatRule", "match": nat_match_obj } @@ -1018,6 +1043,41 @@ def create_lrouter_dnat_rule(cluster, router_id, to_min_dst_ip, return _create_lrouter_nat_rule(cluster, router_id, nat_rule_obj) +def create_lrouter_snat_rule_v3(cluster, router_id, min_src_ip, max_src_ip, + order=None, match_criteria=None): + nat_match_obj = _create_nat_match_obj(**match_criteria) + nat_rule_obj = _build_snat_rule_obj(min_src_ip, max_src_ip, nat_match_obj) + if order: + nat_rule_obj['order'] = order + return _create_lrouter_nat_rule(cluster, router_id, nat_rule_obj) + + +def create_lrouter_dnat_rule_v3(cluster, router_id, dst_ip, to_dst_port=None, + order=None, match_criteria=None): + + nat_match_obj = _create_nat_match_obj(**match_criteria) + nat_rule_obj = { + "to_destination_ip_address": dst_ip, + "type": "DestinationNatRule", + "match": nat_match_obj + } + if to_dst_port: + nat_rule_obj['to_destination_port'] = to_dst_port + if order: + nat_rule_obj['order'] = order + return _create_lrouter_nat_rule(cluster, router_id, nat_rule_obj) + + +@version_dependent +def create_lrouter_dnat_rule(cluster, *args, **kwargs): + pass + + +@version_dependent +def create_lrouter_snat_rule(cluster, *args, **kwargs): + pass + + def delete_nat_rules_by_match(cluster, router_id, rule_type, max_num_expected, min_num_expected=0, @@ -1113,3 +1173,12 @@ def update_lrouter_port_ips(cluster, lrouter_id, lport_id, "router logical port:%s") % str(e) LOG.exception(msg) raise nvp_exc.NvpPluginException(err_desc=msg) + + +# TODO(salvatore-orlando): Also handle changes in minor versions +NVPLIB_FUNC_DICT = { + 'create_lrouter_dnat_rule': {2: create_lrouter_dnat_rule_v2, + 3: create_lrouter_dnat_rule_v3}, + 'create_lrouter_snat_rule': {2: create_lrouter_snat_rule_v2, + 3: create_lrouter_snat_rule_v3} +} diff --git a/quantum/tests/unit/nicira/test_nicira_plugin.py b/quantum/tests/unit/nicira/test_nicira_plugin.py index ae47fb9a4b..7202dc336c 100644 --- a/quantum/tests/unit/nicira/test_nicira_plugin.py +++ b/quantum/tests/unit/nicira/test_nicira_plugin.py @@ -70,11 +70,12 @@ class NiciraPluginV2TestCase(test_plugin.QuantumDbPluginV2TestCase): self.mock_nvpapi = mock.patch('%s.NvpApiClient.NVPApiHelper' % NICIRA_PKG_PATH, autospec=True) instance = self.mock_nvpapi.start() - instance.return_value.login.return_value = "the_cookie" def _fake_request(*args, **kwargs): return self.fc.fake_request(*args, **kwargs) + # Emulate tests against NVP 2.x + instance.return_value.get_nvp_version.return_value = "2.999" instance.return_value.request.side_effect = _fake_request super(NiciraPluginV2TestCase, self).setUp(self._plugin_name) diff --git a/quantum/tests/unit/nicira/test_nvplib.py b/quantum/tests/unit/nicira/test_nvplib.py new file mode 100644 index 0000000000..24ac76d4d7 --- /dev/null +++ b/quantum/tests/unit/nicira/test_nvplib.py @@ -0,0 +1,95 @@ +# Copyright (c) 2013 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# @author: Salvatore Orlando, VMware + +import json +import os + +import mock +import unittest2 as unittest + +from quantum.openstack.common import log as logging +from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient +from quantum.plugins.nicira.nicira_nvp_plugin import nvp_cluster +from quantum.plugins.nicira.nicira_nvp_plugin import nvplib +import quantum.plugins.nicira.nicira_nvp_plugin as nvp_plugin +from quantum.tests.unit.nicira import fake_nvpapiclient +from quantum.tests.unit import test_api_v2 + +LOG = logging.getLogger(__name__) +NICIRA_PKG_PATH = nvp_plugin.__name__ +_uuid = test_api_v2._uuid + + +class TestNvplibNatRules(unittest.TestCase): + + def setUp(self): + # mock nvp api client + etc_path = os.path.join(os.path.dirname(__file__), 'etc') + self.fc = fake_nvpapiclient.FakeClient(etc_path) + self.mock_nvpapi = mock.patch('%s.NvpApiClient.NVPApiHelper' + % NICIRA_PKG_PATH, autospec=True) + instance = self.mock_nvpapi.start() + + def _fake_request(*args, **kwargs): + return self.fc.fake_request(*args, **kwargs) + + instance.return_value.request.side_effect = _fake_request + self.fake_cluster = nvp_cluster.NVPCluster('fake-cluster') + self.fake_cluster.add_controller('1.1.1.1', '999', 'foo', 'bar', + 9, 9, 9, 9, _uuid()) + self.fake_cluster.api_client = NvpApiClient.NVPApiHelper( + ('1.1.1.1', '999', True), + self.fake_cluster.user, self.fake_cluster.password, + self.fake_cluster.request_timeout, self.fake_cluster.http_timeout, + self.fake_cluster.retries, self.fake_cluster.redirects) + + super(TestNvplibNatRules, self).setUp() + + def tearDown(self): + self.fc.reset_all() + self.mock_nvpapi.stop() + + def _test_create_lrouter_dnat_rule(self, func): + tenant_id = 'pippo' + lrouter = nvplib.create_lrouter(self.fake_cluster, + tenant_id, + 'fake_router', + '192.168.0.1') + nat_rule = func(self.fake_cluster, lrouter['uuid'], '10.0.0.99', + match_criteria={'destination_ip_addresses': + '192.168.0.5'}) + uri = nvplib._build_uri_path(nvplib.LROUTERNAT_RESOURCE, + nat_rule['uuid'], + lrouter['uuid']) + return json.loads(nvplib.do_single_request("GET", uri, + cluster=self.fake_cluster)) + + def test_create_lrouter_dnat_rule_v2(self): + resp_obj = self._test_create_lrouter_dnat_rule( + nvplib.create_lrouter_dnat_rule_v2) + self.assertEquals('DestinationNatRule', resp_obj['type']) + self.assertEquals('192.168.0.5', + resp_obj['match']['destination_ip_addresses']) + + def test_create_lrouter_dnat_rule_v3(self): + resp_obj = self._test_create_lrouter_dnat_rule( + nvplib.create_lrouter_dnat_rule_v2) + # TODO(salvatore-orlando): Extend FakeNVPApiClient to deal with + # different versions of NVP API + self.assertEquals('DestinationNatRule', resp_obj['type']) + self.assertEquals('192.168.0.5', + resp_obj['match']['destination_ip_addresses'])