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'])