diff --git a/doc/source/admin_util.rst b/doc/source/admin_util.rst index 3572e135be..f2f642ad8b 100644 --- a/doc/source/admin_util.rst +++ b/doc/source/admin_util.rst @@ -313,6 +313,13 @@ Metadata nsxadmin -r metadata -o status [--property network_id=] +V2T migration +~~~~~~~~~~~~~ + +- Validate the configuration of the NSX-V plugin befor migrating to NSX-T:: + + nsxadmin -r nsx-migrate-v2t -o validate [--property transit-network=] + Config ~~~~~~ diff --git a/vmware_nsx/shell/admin/plugins/common/constants.py b/vmware_nsx/shell/admin/plugins/common/constants.py index d2cf62adcb..a4b3e19040 100644 --- a/vmware_nsx/shell/admin/plugins/common/constants.py +++ b/vmware_nsx/shell/admin/plugins/common/constants.py @@ -67,6 +67,7 @@ BGP_GW_EDGE = 'bgp-gw-edge' ROUTING_REDIS_RULE = 'routing-redistribution-rule' BGP_NEIGHBOUR = 'bgp-neighbour' NSX_PORTGROUPS = 'nsx-portgroups' +NSX_MIGRATE_V_T = 'nsx-migrate-v2t' # NSXTV only Resource Constants PROJECTS = 'projects' diff --git a/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py b/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py new file mode 100644 index 0000000000..0bf49b64e5 --- /dev/null +++ b/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py @@ -0,0 +1,178 @@ +# Copyright 2019 VMware, Inc. All rights reserved. +# +# 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 netaddr +from oslo_log import log as logging + +from neutron.db import l3_db +from neutron_lib.api.definitions import allowedaddresspairs as addr_apidef +from neutron_lib.api.definitions import provider_net as pnet +from neutron_lib.api import validators +from neutron_lib.callbacks import registry +from neutron_lib import constants as nl_constants +from neutron_lib import context as n_context + +from vmware_nsx.common import nsxv_constants +from vmware_nsx.common import utils as c_utils +from vmware_nsx.shell.admin.plugins.common import constants +from vmware_nsx.shell.admin.plugins.common import utils as admin_utils +from vmware_nsx.shell.admin.plugins.nsxv.resources import utils +from vmware_nsx.shell import resources as shell +from vmware_nsxlib.v3 import nsx_constants as nsxlib_consts + +LOG = logging.getLogger(__name__) + + +@admin_utils.output_header +def validate_config_for_migration(resource, event, trigger, **kwargs): + """Validate the nsxv configuration before migration to nsx-t""" + + transit_networks = ["100.64.0.0/16"] + if kwargs.get('property'): + # input validation + properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) + transit_network = properties.get('transit-network') + if transit_network: + transit_networks = [transit_network] + + # Max number of allowed address pairs (allowing 3 for fixed ips) + num_allowed_addr_pairs = nsxlib_consts.NUM_ALLOWED_IP_ADDRESSES - 3 + + admin_context = n_context.get_admin_context() + n_errors = 0 + + with utils.NsxVPluginWrapper() as plugin: + # Ports validations: + ports = plugin.get_ports(admin_context) + for port in ports: + net_id = port['network_id'] + # Too many address pairs in a port + address_pairs = port.get(addr_apidef.ADDRESS_PAIRS) + if len(address_pairs) > num_allowed_addr_pairs: + n_errors = n_errors + 1 + LOG.error("%s allowed address pairs for port %s. Only %s are " + "allowed.", + len(address_pairs), port['id'], + num_allowed_addr_pairs) + + # Compute port on external network + if (port.get('device_owner', '').startswith( + nl_constants.DEVICE_OWNER_COMPUTE_PREFIX) and + plugin._network_is_external(admin_context, net_id)): + n_errors = n_errors + 1 + LOG.error("Compute port %s on external network %s is not " + "allowed.", port['id'], net_id) + + # Networks & subnets validations: + networks = plugin.get_networks(admin_context) + for net in networks: + # skip internal networks + if net['project_id'] == nsxv_constants.INTERNAL_TENANT_ID: + continue + + # VXLAN or portgroup provider networks + net_type = net.get(pnet.NETWORK_TYPE) + if (net_type == c_utils.NsxVNetworkTypes.VXLAN or + net_type == c_utils.NsxVNetworkTypes.PORTGROUP): + n_errors = n_errors + 1 + LOG.error("Network %s of type %s is not supported.", + net['id'], net_type) + + subnets = plugin._get_subnets_by_network(admin_context, net['id']) + n_dhcp_subnets = 0 + + # Multiple DHCP subnets per network + for subnet in subnets: + if subnet['enable_dhcp']: + n_dhcp_subnets = n_dhcp_subnets + 1 + if n_dhcp_subnets > 1: + n_errors = n_errors + 1 + LOG.error("Network %s has %s dhcp subnets. Only 1 is allowed.", + net['id'], n_dhcp_subnets) + + # Subnets overlapping with the transit network + for subnet in subnets: + # get the subnet IPs + if ('allocation_pools' in subnet and + validators.is_attr_set(subnet['allocation_pools'])): + # use the pools instead of the cidr + subnet_networks = [ + netaddr.IPRange(pool.get('start'), pool.get('end')) + for pool in subnet.get('allocation_pools')] + else: + cidr = subnet.get('cidr') + if not validators.is_attr_set(cidr): + return + subnet_networks = [netaddr.IPNetwork(subnet['cidr'])] + + for subnet_net in subnet_networks: + if (netaddr.IPSet(subnet_net) & + netaddr.IPSet(transit_networks)): + n_errors = n_errors + 1 + LOG.error("Subnet %s overlaps with the transit " + "network ips: %s.", + subnet['id'], transit_networks) + + # Network attached to multiple routers + port_filters = {'device_owner': [l3_db.DEVICE_OWNER_ROUTER_INTF], + 'network_id': [net['id']]} + intf_ports = plugin.get_ports(admin_context, filters=port_filters) + if len(intf_ports) > 1: + n_errors = n_errors + 1 + LOG.error("Network %s has interfaces on multiple routers. " + "Only 1 is allowed.", net['id']) + + # Routers validations: + routers = plugin.get_routers(admin_context) + for router in routers: + # Interface subnets overlap with the GW subnet + gw_subnets = plugin._find_router_gw_subnets(admin_context, router) + gw_cidrs = [subnet['cidr'] for subnet in gw_subnets] + gw_ip_set = netaddr.IPSet(gw_cidrs) + + if_cidrs = plugin._find_router_subnets_cidrs( + admin_context, router['id']) + if_ip_set = netaddr.IPSet(if_cidrs) + + if gw_ip_set & if_ip_set: + n_errors = n_errors + 1 + LOG.error("Interface network of router %s cannot overlap with " + "router GW network", router['id']) + + # TODO(asarfaty): missing validations: + # - Vlan provider network with the same VLAN tag as the uplink + # profile tag used in the relevant transport node + # (cannot check this without access to the T manager) + # - Unsupported load balancing topologies + # (e.g.: Load Balancer with members from various subnets + # not uplinked to the same edge router) + # First need to decide if this is for nlbaas or Octavia + + # General validations: + # TODO(asarfaty): multiple transport zones (migrator limitation)? + + if n_errors > 0: + plural = n_errors > 1 + LOG.error("The NSX-V plugin configuration is not ready to be " + "migrated to NSX-T. %s error%s found.", n_errors, + 's were' if plural else ' was') + exit(n_errors) + + LOG.info("The NSX-V plugin configuration is ready to be migrated to " + "NSX-T.") + + +registry.subscribe(validate_config_for_migration, + constants.NSX_MIGRATE_V_T, + shell.Operations.VALIDATE.value) diff --git a/vmware_nsx/shell/resources.py b/vmware_nsx/shell/resources.py index 9fae34dd20..b80790d2dd 100644 --- a/vmware_nsx/shell/resources.py +++ b/vmware_nsx/shell/resources.py @@ -243,6 +243,8 @@ nsxv_resources = { constants.BGP_NEIGHBOUR: Resource(constants.BGP_NEIGHBOUR, [Operations.CREATE.value, Operations.DELETE.value]), + constants.NSX_MIGRATE_V_T: Resource(constants.NSX_MIGRATE_V_T, + [Operations.VALIDATE.value]), } diff --git a/vmware_nsx/tests/unit/shell/test_admin_utils.py b/vmware_nsx/tests/unit/shell/test_admin_utils.py index 760bc91138..56752956ca 100644 --- a/vmware_nsx/tests/unit/shell/test_admin_utils.py +++ b/vmware_nsx/tests/unit/shell/test_admin_utils.py @@ -34,6 +34,7 @@ from vmware_nsx.common import config # noqa from vmware_nsx.db import nsxv_db from vmware_nsx.dvs import dvs_utils from vmware_nsx.shell.admin.plugins.nsxp.resources import utils as nsxp_utils +from vmware_nsx.shell.admin.plugins.nsxv.resources import migration from vmware_nsx.shell.admin.plugins.nsxv.resources import utils as nsxv_utils from vmware_nsx.shell.admin.plugins.nsxv3.resources import utils as nsxv3_utils from vmware_nsx.shell import resources @@ -61,6 +62,7 @@ class AbstractTestAdminUtils(base.BaseTestCase): # remove resource registration conflicts resource_registry.unregister_all_resources() + self.edgeapi = nsxv_utils.NeutronDbClient() # Init the neutron config neutron_config.init(args=['--config-file', BASE_CONF_PATH, '--config-file', NSX_INI_PATH]) @@ -115,9 +117,7 @@ class AbstractTestAdminUtils(base.BaseTestCase): data = {'router': {'tenant_id': tenant_id}} data['router']['name'] = 'dummy' data['router']['admin_state_up'] = True - - edgeapi = nsxv_utils.NeutronDbClient() - return self._plugin.create_router(edgeapi.context, data) + return self._plugin.create_router(self.edgeapi.context, data) class TestNsxvAdminUtils(AbstractTestAdminUtils, @@ -160,12 +160,16 @@ class TestNsxvAdminUtils(AbstractTestAdminUtils, side_effect=get_plugin_mock).start() # Create a router to make sure we have deployed an edge - self.router = self.create_router() + self.router = self._create_router() + self.network = self._create_net() def tearDown(self): if self.router and self.router.get('id'): - edgeapi = nsxv_utils.NeutronDbClient() - self._plugin.delete_router(edgeapi.context, self.router['id']) + self._plugin.delete_router( + self.edgeapi.context, self.router['id']) + if self.network and self.network.get('id'): + self._plugin.delete_network( + self.edgeapi.context, self.network['id']) super(TestNsxvAdminUtils, self).tearDown() def test_nsxv_resources(self): @@ -176,7 +180,7 @@ class TestNsxvAdminUtils(AbstractTestAdminUtils, args['property'].extend(params) self._test_resource('edges', 'nsx-update', **args) - def create_router(self): + def _create_router(self): # Create an exclusive router (with an edge) tenant_id = uuidutils.generate_uuid() data = {'router': {'tenant_id': tenant_id}} @@ -184,12 +188,31 @@ class TestNsxvAdminUtils(AbstractTestAdminUtils, data['router']['admin_state_up'] = True data['router']['router_type'] = 'exclusive' - edgeapi = nsxv_utils.NeutronDbClient() - return self._plugin.create_router(edgeapi.context, data) + return self._plugin.create_router(self.edgeapi.context, data) + + def _create_net(self): + tenant_id = uuidutils.generate_uuid() + data = {'network': {'tenant_id': tenant_id, + 'name': 'dummy', + 'admin_state_up': True, + 'shared': False}} + net = self._plugin.create_network(self.edgeapi.context, data) + data = {'subnet': {'tenant_id': tenant_id, + 'name': 'dummy', + 'admin_state_up': True, + 'network_id': net['id'], + 'cidr': '1.1.1.0/16', + 'enable_dhcp': True, + 'ip_version': 4, + 'dns_nameservers': None, + 'host_routes': None, + 'allocation_pools': None}} + self._plugin.create_subnet(self.edgeapi.context, data) + return net def get_edge_id(self): - edgeapi = nsxv_utils.NeutronDbClient() - bindings = nsxv_db.get_nsxv_router_bindings(edgeapi.context.session) + bindings = nsxv_db.get_nsxv_router_bindings( + self.edgeapi.context.session) for binding in bindings: if binding.edge_id: return binding.edge_id @@ -243,6 +266,17 @@ class TestNsxvAdminUtils(AbstractTestAdminUtils, args = {'property': ["edge-id=%s" % edge_id]} self._test_resource('routers', 'nsx-recreate', **args) + def test_migration_validation(self): + # check that validation fails + args = {'property': ["transit-network=1.1.1.0/24"]} + try: + migration.validate_config_for_migration( + 'nsx-migrate-v2t', 'validate', None, **args) + except SystemExit: + return + else: + self.assertTrue(False) + class TestNsxv3AdminUtils(AbstractTestAdminUtils, test_v3_plugin.NsxV3PluginTestCaseMixin):