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
This commit is contained in:
Salvatore Orlando 2013-01-28 16:41:53 -08:00 committed by Salvatore Orlando
parent df77f43b26
commit ddfe418f3d
5 changed files with 217 additions and 20 deletions

View File

@ -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()

View File

@ -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,

View File

@ -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}
}

View File

@ -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)

View File

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