nsx v3 lport refactor
this patch refactors the nsx v3 logical switch port functions into the rest client/resource model and updates all refs to these functions to use an nsx v3 client. the patch also adds support for passing switch profile ids to the lport create. this functionality will be leveraged in subsequent port security change sets. Change-Id: I1cf86a7f3127f637735560f1ad60eb36edac7500
This commit is contained in:
parent
a7c909536e
commit
22618a005b
@ -124,73 +124,6 @@ def update_logical_switch(lswitch_id, name=None, admin_state=None):
|
||||
return client.update_resource(resource, lswitch)
|
||||
|
||||
|
||||
def create_logical_port(lswitch_id, vif_uuid, tags,
|
||||
attachment_type=nsx_constants.ATTACHMENT_VIF,
|
||||
admin_state=True, name=None, address_bindings=None,
|
||||
parent_name=None, parent_tag=None):
|
||||
|
||||
# NOTE(arosen): if a parent_name is specified we need to use the
|
||||
# CIF's attachment.
|
||||
key_values = None
|
||||
if parent_name:
|
||||
attachment_type = nsx_constants.ATTACHMENT_CIF
|
||||
key_values = [
|
||||
{'key': 'VLAN_ID', 'value': parent_tag},
|
||||
{'key': 'Host_VIF_ID', 'value': parent_name},
|
||||
{'key': 'IP', 'value': address_bindings[0]['ip_address']},
|
||||
{'key': 'MAC', 'value': address_bindings[0]['mac_address']}]
|
||||
# NOTE(arosen): The above api body structure might change in the future
|
||||
|
||||
resource = 'logical-ports'
|
||||
body = {'logical_switch_id': lswitch_id,
|
||||
'attachment': {'attachment_type': attachment_type,
|
||||
'id': vif_uuid},
|
||||
'tags': tags}
|
||||
if name:
|
||||
body['display_name'] = name
|
||||
if admin_state:
|
||||
body['admin_state'] = nsx_constants.ADMIN_STATE_UP
|
||||
else:
|
||||
body['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
|
||||
|
||||
if key_values:
|
||||
body['attachment']['context'] = {'key_values': key_values}
|
||||
body['attachment']['context']['resource_type'] = \
|
||||
nsx_constants.CIF_RESOURCE_TYPE
|
||||
if address_bindings:
|
||||
body['address_bindings'] = address_bindings
|
||||
|
||||
return client.create_resource(resource, body)
|
||||
|
||||
|
||||
def delete_logical_port(logical_port_id):
|
||||
resource = 'logical-ports/%s?detach=true' % logical_port_id
|
||||
client.delete_resource(resource)
|
||||
|
||||
|
||||
def get_logical_port(logical_port_id):
|
||||
resource = "logical-ports/%s" % logical_port_id
|
||||
return client.get_resource(resource)
|
||||
|
||||
|
||||
@utils.retry_upon_exception_nsxv3(nsx_exc.StaleRevision,
|
||||
max_attempts=cfg.CONF.nsx_v3.retries)
|
||||
def update_logical_port(lport_id, name=None, admin_state=None):
|
||||
resource = "logical-ports/%s" % lport_id
|
||||
lport = get_logical_port(lport_id)
|
||||
if name is not None:
|
||||
lport['display_name'] = name
|
||||
if admin_state is not None:
|
||||
if admin_state:
|
||||
lport['admin_state'] = nsx_constants.ADMIN_STATE_UP
|
||||
else:
|
||||
lport['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
|
||||
# If revision_id of the payload that we send is older than what NSX has
|
||||
# then we will get a 412: Precondition Failed. In that case we need to
|
||||
# re-fetch, patch the response and send it again with the new revision_id
|
||||
return client.update_resource(resource, lport)
|
||||
|
||||
|
||||
def create_logical_router(display_name, tags,
|
||||
edge_cluster_uuid=None, tier_0=False):
|
||||
# TODO(salv-orlando): If possible do not manage edge clusters in the main
|
||||
|
@ -16,6 +16,11 @@
|
||||
import abc
|
||||
import six
|
||||
|
||||
from oslo_config import cfg
|
||||
from vmware_nsx.common import exceptions as nsx_exc
|
||||
from vmware_nsx.common import nsx_constants
|
||||
from vmware_nsx.common import utils
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AbstractRESTResource(object):
|
||||
@ -105,3 +110,86 @@ class SwitchingProfile(AbstractRESTResource):
|
||||
description=description,
|
||||
white_list_providers=whitelist_providers,
|
||||
tags=tags or [])
|
||||
|
||||
def build_switch_profile_ids(self, *profiles):
|
||||
ids = []
|
||||
for profile in profiles:
|
||||
if type(profile) is str:
|
||||
profile = self.get(profile)
|
||||
ids.append({
|
||||
'value': profile['id'],
|
||||
'key': profile['resource_type']
|
||||
})
|
||||
return ids
|
||||
|
||||
|
||||
class LogicalPort(AbstractRESTResource):
|
||||
|
||||
@property
|
||||
def uri_segment(self):
|
||||
return 'logical-ports'
|
||||
|
||||
def create(self, lswitch_id, vif_uuid, tags=[],
|
||||
attachment_type=nsx_constants.ATTACHMENT_VIF,
|
||||
admin_state=True, name=None, address_bindings=None,
|
||||
parent_name=None, parent_tag=None,
|
||||
switch_profile_ids=None):
|
||||
|
||||
# NOTE(arosen): if a parent_name is specified we need to use the
|
||||
# CIF's attachment.
|
||||
key_values = None
|
||||
if parent_name:
|
||||
attachment_type = nsx_constants.ATTACHMENT_CIF
|
||||
key_values = [
|
||||
{'key': 'VLAN_ID', 'value': parent_tag},
|
||||
{'key': 'Host_VIF_ID', 'value': parent_name},
|
||||
{'key': 'IP', 'value': address_bindings[0]['ip_address']},
|
||||
{'key': 'MAC', 'value': address_bindings[0]['mac_address']}]
|
||||
# NOTE(arosen): The above api body structure might change
|
||||
# in the future
|
||||
|
||||
body = {'logical_switch_id': lswitch_id,
|
||||
'attachment': {'attachment_type': attachment_type,
|
||||
'id': vif_uuid}}
|
||||
|
||||
if tags:
|
||||
body['tags'] = tags
|
||||
if name:
|
||||
body['display_name'] = name
|
||||
if admin_state:
|
||||
body['admin_state'] = nsx_constants.ADMIN_STATE_UP
|
||||
else:
|
||||
body['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
|
||||
|
||||
if key_values:
|
||||
body['attachment']['context'] = {'key_values': key_values}
|
||||
body['attachment']['context']['resource_type'] = \
|
||||
nsx_constants.CIF_RESOURCE_TYPE
|
||||
if address_bindings:
|
||||
body['address_bindings'] = address_bindings
|
||||
|
||||
if switch_profile_ids:
|
||||
body['switching_profile_ids'] = switch_profile_ids
|
||||
|
||||
return self._client.create(body=body)
|
||||
|
||||
def delete(self, lport_id):
|
||||
return self._client.url_delete('%s?detach=true' % lport_id)
|
||||
|
||||
@utils.retry_upon_exception_nsxv3(
|
||||
nsx_exc.StaleRevision,
|
||||
max_attempts=cfg.CONF.nsx_v3.retries)
|
||||
def update(self, lport_id, name=None, admin_state=None):
|
||||
lport = self.get(lport_id)
|
||||
if name is not None:
|
||||
lport['display_name'] = name
|
||||
if admin_state is not None:
|
||||
if admin_state:
|
||||
lport['admin_state'] = nsx_constants.ADMIN_STATE_UP
|
||||
else:
|
||||
lport['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
|
||||
# If revision_id of the payload that we send is older than what NSX has
|
||||
# then we will get a 412: Precondition Failed. In that case we need to
|
||||
# re-fetch, patch the response and send it again with the
|
||||
# new revision_id
|
||||
return self._client.update(lport_id, body=lport)
|
||||
|
@ -62,7 +62,9 @@ from vmware_nsx.common import nsx_constants
|
||||
from vmware_nsx.common import utils
|
||||
from vmware_nsx.db import db as nsx_db
|
||||
from vmware_nsx.nsxlib import v3 as nsxlib
|
||||
from vmware_nsx.nsxlib.v3 import client as nsx_client
|
||||
from vmware_nsx.nsxlib.v3 import dfw_api as firewall
|
||||
from vmware_nsx.nsxlib.v3 import resources as nsx_resources
|
||||
from vmware_nsx.nsxlib.v3 import router as routerlib
|
||||
from vmware_nsx.nsxlib.v3 import security
|
||||
|
||||
@ -104,6 +106,7 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
'security-group' in self.supported_extension_aliases}}
|
||||
self.tier0_groups_dict = {}
|
||||
self._setup_rpc()
|
||||
self._nsx_client = nsx_client.NSX3Client()
|
||||
self.nsgroup_container, self.default_section = (
|
||||
security.init_nsgroup_container_and_default_section_rules())
|
||||
|
||||
@ -426,9 +429,11 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
# for ports plugged into a Bridge Endpoint.
|
||||
vif_uuid = port_data.get('device_id')
|
||||
attachment_type = port_data.get('device_owner')
|
||||
result = nsxlib.create_logical_port(
|
||||
lswitch_id=port_data['network_id'],
|
||||
vif_uuid=vif_uuid, name=port_data['name'], tags=tags,
|
||||
port_client = nsx_resources.LogicalPort(self._nsx_client)
|
||||
result = port_client.create(
|
||||
port_data['network_id'], vif_uuid,
|
||||
tags=tags,
|
||||
name=port_data['name'],
|
||||
admin_state=port_data['admin_state_up'],
|
||||
address_bindings=address_bindings,
|
||||
attachment_type=attachment_type,
|
||||
@ -517,7 +522,7 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
updated_port = {'port': {ext_sg.SECURITYGROUPS: [],
|
||||
'admin_state_up': False}}
|
||||
self.update_port(context, port_id, updated_port)
|
||||
nsxlib.delete_logical_port(nsx_port_id)
|
||||
nsx_resources.LogicalPort(self._nsx_client).delete(nsx_port_id)
|
||||
self.disassociate_floatingips(context, port_id)
|
||||
ret_val = super(NsxV3Plugin, self).delete_port(context, port_id)
|
||||
|
||||
@ -535,7 +540,7 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
sec_grp_updated = self.update_security_group_on_port(
|
||||
context, id, port, original_port, updated_port)
|
||||
try:
|
||||
nsxlib.update_logical_port(
|
||||
nsx_resources.LogicalPort(self._nsx_client).update(
|
||||
nsx_lport_id, name=port['port'].get('name'),
|
||||
admin_state=port['port'].get('admin_state_up'))
|
||||
security.update_lport_with_security_groups(
|
||||
|
@ -87,7 +87,7 @@ def update_logical_switch(lswitch_id, name=None, admin_state=None):
|
||||
return lswitch
|
||||
|
||||
|
||||
def create_logical_port(lswitch_id, vif_uuid, tags,
|
||||
def create_logical_port(client, lswitch_id, vif_uuid, tags=[],
|
||||
attachment_type=nsx_constants.ATTACHMENT_VIF,
|
||||
admin_state=True, name=None, address_bindings=None,
|
||||
parent_name=None, parent_tag=None):
|
||||
@ -130,7 +130,7 @@ def create_logical_port(lswitch_id, vif_uuid, tags,
|
||||
return FAKE_PORT
|
||||
|
||||
|
||||
def get_logical_port(lport_id):
|
||||
def get_logical_port(client, lport_id):
|
||||
FAKE_SWITCH_UUID = uuidutils.generate_uuid()
|
||||
FAKE_PORT = {
|
||||
"id": lport_id,
|
||||
@ -169,8 +169,8 @@ def get_logical_port(lport_id):
|
||||
return FAKE_PORT
|
||||
|
||||
|
||||
def update_logical_port(lport_id, name=None, admin_state=None):
|
||||
lport = get_logical_port(lport_id)
|
||||
def update_logical_port(client, lport_id, name=None, admin_state=None):
|
||||
lport = get_logical_port(client, lport_id)
|
||||
if name:
|
||||
lport['display_name'] = name
|
||||
if admin_state is not None:
|
||||
|
@ -1,71 +0,0 @@
|
||||
# Copyright (c) 2015 VMware, Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import mock
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from vmware_nsx.nsxlib import v3 as nsxlib
|
||||
from vmware_nsx.tests.unit.vmware.nsxlib.v3 import nsxlib_testcase
|
||||
from vmware_nsx.tests.unit.vmware import test_constants_v3
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NsxLibPortTestCase(nsxlib_testcase.NsxLibTestCase):
|
||||
|
||||
@mock.patch("vmware_nsx.nsxlib.v3"
|
||||
".client.create_resource")
|
||||
def test_create_logical_port(self, mock_create_resource):
|
||||
"""
|
||||
Test creating a port returns the correct response and 200 status
|
||||
"""
|
||||
mock_create_resource.return_value = test_constants_v3.FAKE_PORT
|
||||
|
||||
result = nsxlib.create_logical_port(
|
||||
test_constants_v3.FAKE_PORT['logical_switch_id'],
|
||||
test_constants_v3.FAKE_PORT['attachment']['id'],
|
||||
tags={})
|
||||
|
||||
self.assertEqual(test_constants_v3.FAKE_PORT, result)
|
||||
|
||||
@mock.patch("vmware_nsx.nsxlib.v3"
|
||||
".client.create_resource")
|
||||
def test_create_logical_port_admin_down(self, mock_create_resource):
|
||||
"""
|
||||
Test creating port with admin_state down
|
||||
"""
|
||||
fake_port = test_constants_v3.FAKE_PORT
|
||||
fake_port['admin_state'] = "DOWN"
|
||||
mock_create_resource.return_value = fake_port
|
||||
|
||||
result = nsxlib.create_logical_port(
|
||||
test_constants_v3.FAKE_PORT['logical_switch_id'],
|
||||
test_constants_v3.FAKE_PORT['attachment']['id'],
|
||||
tags={}, admin_state=False)
|
||||
|
||||
self.assertEqual(fake_port, result)
|
||||
|
||||
@mock.patch("vmware_nsx.nsxlib.v3"
|
||||
".client.delete_resource")
|
||||
def test_delete_logical_port(self, mock_delete_resource):
|
||||
"""
|
||||
Test deleting port
|
||||
"""
|
||||
mock_delete_resource.return_value = None
|
||||
|
||||
result = nsxlib.delete_logical_port(test_constants_v3.FAKE_PORT['id'])
|
||||
self.assertIsNone(result)
|
@ -20,6 +20,7 @@ from oslo_serialization import jsonutils
|
||||
from vmware_nsx.nsxlib.v3 import client
|
||||
from vmware_nsx.nsxlib.v3 import resources
|
||||
from vmware_nsx.tests.unit.vmware.nsxlib.v3 import test_client
|
||||
from vmware_nsx.tests.unit.vmware import test_constants_v3
|
||||
|
||||
|
||||
CLIENT_PKG = test_client.CLIENT_PKG
|
||||
@ -142,3 +143,87 @@ class TestSwitchingProfileTestCase(test_client.BaseClientTestCase):
|
||||
api = resources.SwitchingProfile(client.NSX3Client())
|
||||
self.assertEqual(resp_resources['results'],
|
||||
api.find_by_display_name('resource-1'))
|
||||
|
||||
|
||||
class LogicalPortTestCase(test_client.BaseClientTestCase):
|
||||
|
||||
@mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post'))
|
||||
@mock.patch(CLIENT_PKG + '.RESTClient._validate_result')
|
||||
def test_create_logical_port(self, mock_validate, mock_post):
|
||||
"""
|
||||
Test creating a port returns the correct response and 200 status
|
||||
"""
|
||||
fake_port = test_constants_v3.FAKE_PORT
|
||||
mock_post.return_value = mocks.MockRequestsResponse(
|
||||
200, jsonutils.dumps(fake_port))
|
||||
|
||||
profile_client = resources.SwitchingProfile(client.NSX3Client())
|
||||
profile_dicts = []
|
||||
for profile_id in fake_port['switching_profile_ids']:
|
||||
profile_dicts.append({'resource_type': profile_id['key'],
|
||||
'id': profile_id['value']})
|
||||
|
||||
result = resources.LogicalPort(client.NSX3Client()).create(
|
||||
fake_port['logical_switch_id'],
|
||||
fake_port['attachment']['id'],
|
||||
switch_profile_ids=profile_client.build_switch_profile_ids(
|
||||
*profile_dicts))
|
||||
|
||||
resp_body = {
|
||||
'logical_switch_id': fake_port['logical_switch_id'],
|
||||
'attachment': {
|
||||
'attachment_type': 'VIF',
|
||||
'id': fake_port['attachment']['id']
|
||||
},
|
||||
'admin_state': 'UP',
|
||||
'switching_profile_ids': fake_port['switching_profile_ids']
|
||||
}
|
||||
|
||||
self.assertEqual(fake_port, result)
|
||||
self.assertEqual(fake_port['switching_profile_ids'],
|
||||
resp_body['switching_profile_ids'])
|
||||
test_client.assert_session_call(
|
||||
mock_post,
|
||||
'https://1.2.3.4/api/v1/logical-ports',
|
||||
False,
|
||||
jsonutils.dumps(resp_body),
|
||||
client.JSONRESTClient._DEFAULT_HEADERS,
|
||||
test_client.BaseClientTestCase.ca_file)
|
||||
|
||||
@mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post'))
|
||||
@mock.patch(CLIENT_PKG + '.RESTClient._validate_result')
|
||||
def test_create_logical_port_admin_down(self, mock_validate, mock_post):
|
||||
"""
|
||||
Test creating port with admin_state down
|
||||
"""
|
||||
fake_port = test_constants_v3.FAKE_PORT
|
||||
fake_port['admin_state'] = "DOWN"
|
||||
mock_post.return_value = mocks.MockRequestsResponse(
|
||||
200, jsonutils.dumps(fake_port))
|
||||
|
||||
result = resources.LogicalPort(client.NSX3Client()).create(
|
||||
test_constants_v3.FAKE_PORT['logical_switch_id'],
|
||||
test_constants_v3.FAKE_PORT['attachment']['id'],
|
||||
tags={}, admin_state=False)
|
||||
|
||||
self.assertEqual(fake_port, result)
|
||||
|
||||
@mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.delete'))
|
||||
@mock.patch(CLIENT_PKG + '.RESTClient._validate_result')
|
||||
def test_delete_logical_port(self, mock_validate, mock_delete):
|
||||
"""
|
||||
Test deleting port
|
||||
"""
|
||||
mock_delete.return_value = mocks.MockRequestsResponse(
|
||||
200, None)
|
||||
|
||||
uuid = test_constants_v3.FAKE_PORT['id']
|
||||
result = resources.LogicalPort(client.NSX3Client()).delete(uuid)
|
||||
self.assertIsNone(result.content)
|
||||
test_client.assert_session_call(
|
||||
mock_delete,
|
||||
'https://1.2.3.4/api/v1/logical-ports/%s?detach=true' % uuid,
|
||||
False,
|
||||
None,
|
||||
client.JSONRESTClient._DEFAULT_HEADERS,
|
||||
test_client.BaseClientTestCase.ca_file)
|
||||
|
@ -36,6 +36,7 @@ from neutron import version
|
||||
from vmware_nsx.common import utils
|
||||
from vmware_nsx.nsxlib import v3 as nsxlib
|
||||
from vmware_nsx.nsxlib.v3 import dfw_api as firewall
|
||||
from vmware_nsx.nsxlib.v3 import resources as nsx_resources
|
||||
from vmware_nsx.tests.unit import vmware
|
||||
from vmware_nsx.tests.unit.vmware import nsx_v3_mocks
|
||||
|
||||
@ -56,10 +57,10 @@ class NsxPluginV3TestCase(test_plugin.NeutronDbPluginV2TestCase):
|
||||
nsxlib.delete_logical_switch = mock.Mock()
|
||||
nsxlib.get_logical_switch = nsx_v3_mocks.get_logical_switch
|
||||
nsxlib.update_logical_switch = nsx_v3_mocks.update_logical_switch
|
||||
nsxlib.create_logical_port = nsx_v3_mocks.create_logical_port
|
||||
nsxlib.delete_logical_port = mock.Mock()
|
||||
nsxlib.get_logical_port = nsx_v3_mocks.get_logical_port
|
||||
nsxlib.update_logical_port = nsx_v3_mocks.update_logical_port
|
||||
nsx_resources.LogicalPort.create = nsx_v3_mocks.create_logical_port
|
||||
nsx_resources.LogicalPort.delete = mock.Mock()
|
||||
nsx_resources.LogicalPort.get = nsx_v3_mocks.get_logical_port
|
||||
nsx_resources.LogicalPort.update = nsx_v3_mocks.update_logical_port
|
||||
firewall.add_rules_in_section = nsx_v3_mocks.add_rules_in_section
|
||||
firewall.nsxclient.create_resource = nsx_v3_mocks.create_resource
|
||||
firewall.nsxclient.update_resource = nsx_v3_mocks.update_resource
|
||||
@ -114,12 +115,11 @@ class SecurityGroupsTestCase(ext_sg.SecurityGroupDBTestCase):
|
||||
plugin=PLUGIN_NAME,
|
||||
ext_mgr=None):
|
||||
nsxlib.create_logical_switch = nsx_v3_mocks.create_logical_switch
|
||||
nsxlib.create_logical_port = nsx_v3_mocks.create_logical_port
|
||||
nsxlib.update_logical_port = nsx_v3_mocks.update_logical_port
|
||||
nsxlib.delete_logical_port = mock.Mock()
|
||||
nsxlib.delete_logical_switch = mock.Mock()
|
||||
nsxlib.get_logical_port = nsx_v3_mocks.get_logical_port
|
||||
nsxlib.update_logical_port = nsx_v3_mocks.update_logical_port
|
||||
nsx_resources.LogicalPort.create = nsx_v3_mocks.create_logical_port
|
||||
nsx_resources.LogicalPort.delete = mock.Mock()
|
||||
nsx_resources.LogicalPort.get = nsx_v3_mocks.get_logical_port
|
||||
nsx_resources.LogicalPort.update = nsx_v3_mocks.update_logical_port
|
||||
firewall.add_rules_in_section = nsx_v3_mocks.add_rules_in_section
|
||||
firewall.nsxclient.create_resource = nsx_v3_mocks.create_resource
|
||||
firewall.nsxclient.update_resource = nsx_v3_mocks.update_resource
|
||||
@ -193,7 +193,7 @@ class L3NatTest(test_l3_plugin.L3BaseForIntTests, NsxPluginV3TestCase):
|
||||
self.plugin_instance.__module__,
|
||||
self.plugin_instance.__class__.__name__)
|
||||
self._plugin_class = self.plugin_instance.__class__
|
||||
nsxlib.create_logical_port = self.v3_mock.create_logical_port
|
||||
nsx_resources.LogicalPort.create = self.v3_mock.create_logical_port
|
||||
nsxlib.create_logical_router = self.v3_mock.create_logical_router
|
||||
nsxlib.update_logical_router = self.v3_mock.update_logical_router
|
||||
nsxlib.delete_logical_router = self.v3_mock.delete_logical_router
|
||||
|
Loading…
x
Reference in New Issue
Block a user