V2T migration: Few validation fixing

- Detect internal networks for distributed routers as neutron networks
- No identical port fixed ips and address pairs allowed
- Only 2 dns nameservers are allowed per subnet
- Only 1 ipv6 subnet is allowed per network
- Improve logging: Add summery of all issues at the end, and add an option to write it to file

Change-Id: Id195f510c3915d80755ef656912efb21b51ff9ce
This commit is contained in:
asarfaty 2021-03-03 14:34:38 +02:00 committed by Adit Sarfaty
parent f2ab4823c7
commit 95e67d45b9
2 changed files with 148 additions and 93 deletions

View File

@ -339,7 +339,7 @@ V2T migration
- Validate the configuration of the NSX-V plugin before migrating to NSX-T. When the strict flag is true. the validation will fail on warnings as well::
nsxadmin -r nsx-migrate-v2t -o validate [--property transit-network=<cidr>] [--property strict=true]
nsxadmin -r nsx-migrate-v2t -o validate [--property transit-network=<cidr>] [--property strict=true] [--property summary-file-name=<>]
- Get compute ports vif ids mapping for the migration::

View File

@ -57,6 +57,28 @@ def _get_router_from_network(context, plugin, subnet_id):
return ports[0]['device_id']
all_errors = []
all_warnings = []
n_errors = 0
n_warnings = 0
def log_error(msg):
global all_errors
global n_errors
LOG.error(msg)
all_errors.append(msg)
n_errors = n_errors + 1
def log_warning(msg):
global all_warnings
global n_warnings
LOG.warning(msg)
all_warnings.append(msg)
n_warnings = n_warnings + 1
@admin_utils.output_header
def validate_config_for_migration(resource, event, trigger, **kwargs):
"""Validate the nsxv configuration before migration to nsx-t"""
@ -64,6 +86,7 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
# Read the command line parameters
transit_networks = ["100.64.0.0/16"]
strict = False
out_file = None
if kwargs.get('property'):
# input validation
properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
@ -71,12 +94,21 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if transit_network:
transit_networks = [transit_network]
strict = bool(properties.get('strict', 'false').lower() == 'true')
out_file = properties.get('summary-file-name')
LOG.info("Running migration config validation in %sstrict mode",
'' if strict else 'non-')
admin_context = n_context.get_admin_context()
global all_errors
all_errors = []
global all_warnings
all_warnings = []
global n_errors
n_errors = 0
global n_warnings
n_warnings = 0
admin_context = n_context.get_admin_context()
# General config options / per AZ which are unsupported
config.register_nsxv_azs(cfg.CONF, cfg.CONF.nsxv.availability_zones)
@ -85,19 +117,16 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
for az in zones.list_availability_zones_objects():
for attr in unsupported_configs:
if getattr(az, attr):
LOG.warning("WARNING: \'%s\' configuration is not supported "
log_warning("WARNING: \'%s\' configuration is not supported "
"and will not be honored by NSX-T (availability "
"zone %s)", attr, az.name)
if strict:
n_errors = n_errors + 1
"zone %s)" % (attr, az.name))
with utils.NsxVPluginWrapper() as plugin:
# The migration is supported only for NSX 6.4.9 and above
nsx_ver = plugin.nsx_v.vcns.get_version()
if not c_utils.is_nsxv_version_6_4_9(nsx_ver):
LOG.error("ERROR: Migration with NSX-V version %s is not "
"supported.", nsx_ver)
n_errors = n_errors + 1
log_error("ERROR: Migration with NSX-V version %s is not "
"supported." % nsx_ver)
# Ports validations:
# Max number of allowed address pairs (allowing 1 for fixed ips)
@ -106,32 +135,36 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
for port in ports:
net_id = port['network_id']
# Too many address pairs in a port
address_pairs = port.get(addr_apidef.ADDRESS_PAIRS)
address_pairs = port.get(addr_apidef.ADDRESS_PAIRS, [])
if len(address_pairs) > num_allowed_addr_pairs:
LOG.warning("WARNING: %s allowed address pairs for port %s. "
"Only %s are allowed.",
len(address_pairs), port['id'],
num_allowed_addr_pairs)
if strict:
n_errors = n_errors + 1
log_warning("WARNING: %s allowed address pairs for port %s. "
"Only %s are allowed." %
(len(address_pairs), port['id'],
num_allowed_addr_pairs))
fixed_ips = [fixed.get('ip_address')
for fixed in port['fixed_ips']]
for pair in address_pairs:
if (port['mac_address'] == pair['mac_address'] and
pair['ip_address'] in fixed_ips):
log_error("ERROR: Port %s address pair cannot be "
"identical to the fixed ip." % port['id'])
# 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("ERROR: Compute port %s on external network %s is "
"not allowed.", port['id'], net_id)
log_error("ERROR: Compute port %s on external network %s is "
"not allowed." % (port['id'], net_id))
vnic = port.get(pbin.VNIC_TYPE)
if vnic in portbinding.VNIC_TYPES_DIRECT_PASSTHROUGH:
net = plugin.get_network(admin_context, port['network_id'])
net_type = net.get(pnet.NETWORK_TYPE)
if net_type not in portbinding.SUPPORTED_T_NETWORK_TYPES:
n_errors = n_errors + 1
LOG.error("ERROR: Port %s vnic type %s is not supported "
"with network type %s.", port['id'],
vnic, net_type)
log_error("ERROR: Port %s vnic type %s is not supported "
"with network type %s." % (port['id'],
vnic, net_type))
# Networks & subnets validations:
networks = plugin.get_networks(admin_context)
@ -149,9 +182,8 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
overlay_net = bool(net_type != c_utils.NsxVNetworkTypes.VLAN)
if (net_type == c_utils.NsxVNetworkTypes.VXLAN or
net_type == c_utils.NsxVNetworkTypes.PORTGROUP):
n_errors = n_errors + 1
LOG.error("ERROR: Network %s of type %s is not supported.",
net['id'], net_type)
log_error("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
@ -161,30 +193,27 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if subnet['enable_dhcp']:
n_dhcp_subnets = n_dhcp_subnets + 1
if n_dhcp_subnets > 1:
n_errors = n_errors + 1
LOG.error("ERROR: Network %s has %s dhcp subnets. Only 1 is "
"allowed.", net['id'], n_dhcp_subnets)
log_error("ERROR: Network %s has %s dhcp subnets. Only 1 is "
"allowed." % (net['id'], n_dhcp_subnets))
# Network attached to multiple routers
intf_ports = plugin._get_network_interface_ports(
admin_context, net['id'])
if len(intf_ports) > 1:
n_errors = n_errors + 1
LOG.error("ERROR: Network %s has interfaces on multiple "
"routers. Only 1 is allowed.", net['id'])
log_error("ERROR: Network %s has interfaces on multiple "
"routers. Only 1 is allowed." % net['id'])
if (cfg.CONF.vlan_transparent and
net.get('vlan_transparent') is True):
if len(intf_ports) > 0:
n_errors = n_errors + 1
LOG.error("ERROR: VLAN Transparent network %s cannot be "
"attached to a logical router.", net['id'])
log_error("ERROR: VLAN Transparent network %s cannot be "
"attached to a logical router." % net['id'])
if n_dhcp_subnets > 0:
n_errors = n_errors + 1
LOG.error("ERROR: DHCP is not supported for VLAN "
"transparent network %s.", net['id'])
log_error("ERROR: DHCP is not supported for VLAN "
"transparent network %s." % net['id'])
# Subnets overlapping with the transit network
ipv6_subnets = 0
for subnet in subnets:
# get the subnet IPs
if ('allocation_pools' in subnet and
@ -202,10 +231,9 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
for subnet_net in subnet_networks:
if (netaddr.IPSet(subnet_net) &
netaddr.IPSet(transit_networks)):
n_errors = n_errors + 1
LOG.error("ERROR: Subnet %s overlaps with the transit "
"network ips: %s.",
subnet['id'], transit_networks)
log_error("ERROR: Subnet %s overlaps with the transit "
"network ips: %s." %
(subnet['id'], transit_networks))
# Cannot support non-dhcp overlay subnet attached to a router
# if there is also a dhcp subnet on the same network
@ -216,11 +244,10 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if if_port['fixed_ips']:
if_sub = if_port['fixed_ips'][0]['subnet_id']
if subnet['id'] == if_sub:
n_errors = n_errors + 1
LOG.error("ERROR: Network %s has non-dhcp "
log_error("ERROR: Network %s has non-dhcp "
"subnet attached to a router, and "
"another dhcp subnet. This is not "
"allowed.", net['id'])
"allowed." % net['id'])
# Cannot use a non-gateway subnet attached to a router
if not subnet['gateway_ip']:
@ -228,10 +255,20 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if if_port['fixed_ips']:
if_sub = if_port['fixed_ips'][0]['subnet_id']
if subnet['id'] == if_sub:
n_errors = n_errors + 1
LOG.error("ERROR: Subnet %s attached to a "
"router must have a gateway IP.",
log_error("ERROR: Subnet %s attached to a "
"router must have a gateway IP." %
subnet['id'])
# only 2 dns_nameservers allowed
if len(subnet.get('dns_nameservers', [])) > 2:
log_error("ERROR: Subnet %s cannot have more than 2 "
"dns_nameservers." % subnet['id'])
if subnet.get('ip_version') == 6:
ipv6_subnets = ipv6_subnets + 1
if ipv6_subnets > 1:
log_error("ERROR: Network %s cannot have more than 1 "
"IPv6 subnets." % net['id'])
# Routers validations:
routers = plugin.get_routers(admin_context)
@ -246,9 +283,8 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if_ip_set = netaddr.IPSet(if_cidrs)
if gw_ip_set & if_ip_set:
n_errors = n_errors + 1
LOG.error("ERROR: Interface network of router %s cannot "
"overlap with router GW network", router['id'])
log_error("ERROR: Interface network of router %s cannot "
"overlap with router GW network" % router['id'])
# router without external gw cannot be attached to a vlan subnet
router_db = plugin._get_router(admin_context, router['id'])
@ -260,20 +296,17 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
net = plugin.get_network(admin_context, net_id)
net_type = net.get(pnet.NETWORK_TYPE)
if net_type == c_utils.NsxVNetworkTypes.VLAN:
n_errors = n_errors + 1
LOG.error("ERROR: Vlan network %s cannot be attached "
"to router %s without a gateway", net_id,
router['id'])
log_error("ERROR: Vlan network %s cannot be attached "
"to router %s without a gateway" % (net_id,
router['id']))
# Look for orphaned neutron networks and non neutron backend networks
backend_networks = utils.get_networks()
missing_networks = utils.get_orphaned_networks(backend_networks)
for net in missing_networks:
if strict:
n_errors = n_errors + 1
LOG.warning("WARNING: NSX backend network %s:%s is missing from "
log_warning("WARNING: NSX backend network %s:%s is missing from "
"neutron and is probably an orphaned. Please delete "
"it.", net.get('moref'), net.get('name'))
"it." % (net.get('moref'), net.get('name')))
for net in backend_networks:
moref = net['moref']
@ -299,13 +332,17 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if nsxv_db.get_network_bindings_by_physical_net(
admin_context.session, moref):
continue
if net_type == 'VirtualWire':
# Find internal networks for distributed routers
filters = {'lswitch_id': moref}
if nsxv_db.get_nsxv_router_bindings(admin_context.session,
like_filters=filters):
continue
if strict:
n_errors = n_errors + 1
LOG.warning("WARNING: NSX backend network %s:%s is not a "
log_warning("WARNING: NSX backend network %s:%s is not a "
"neutron network and cannot be migrated. "
"Please delete it or migrate it manually.",
moref, name)
"Please delete it or migrate it manually." %
(moref, name))
# Octavia loadbalancers validation:
filters = {'device_owner': [nl_constants.DEVICE_OWNER_LOADBALANCERV2,
@ -324,10 +361,9 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
# belong to an external network
if (not lb_router_id and network and
not network.get('router:external')):
n_errors = n_errors + 1
LOG.error("ERROR: Loadbalancer %s subnet %s is not "
"external nor connected to a router.",
port.get('device_id'), subnet_id)
log_error("ERROR: Loadbalancer %s subnet %s is not "
"external nor connected to a router." %
(port.get('device_id'), subnet_id))
if not lb_id:
continue
@ -349,10 +385,9 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if not vip.get('defaultPoolId'):
continue
if vip['defaultPoolId'] in pools:
LOG.error("ERROR: Found multiple listeners using the "
log_error("ERROR: Found multiple listeners using the "
"same default pool with loadbalancer %s. "
"This is not supported.", lb_id)
n_errors = n_errors + 1
"This is not supported." % lb_id)
break
pools.append(vip['defaultPoolId'])
@ -374,27 +409,25 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
router_id = _get_router_from_network(
admin_context, plugin, sub_id)
if not router_id:
LOG.error("ERROR: Found member of subnet %s not "
log_error("ERROR: Found member of subnet %s not "
"uplinked to any router on loadbalancer "
"%s. This is not supported.",
sub_id, lb_id)
n_errors = n_errors + 1
"%s. This is not supported." %
(sub_id, lb_id))
elif router_id not in lb_routers:
lb_routers.append(router_id)
if len(lb_routers) > 1:
LOG.error("ERROR: Found members/vips from different "
log_error("ERROR: Found members/vips from different "
"subnets or uplinks to different routers on "
"loadbalancer %s. This is not supported.", lb_id)
n_errors = n_errors + 1
"loadbalancer %s. This is not supported." %
lb_id)
break
# Security groups without policies
sgs = plugin.get_security_groups(admin_context)
for sg in sgs:
if plugin._is_policy_security_group(admin_context, sg['id']):
LOG.error("ERROR: Security group %s has NSX policy. This is not "
"supported.", sg['id'])
n_errors = n_errors + 1
log_error("ERROR: Security group %s has NSX policy. This is not "
"supported." % sg['id'])
# Validate QoS limits
qos_plugin_inst = qos_plugin.QoSPlugin()
@ -404,16 +437,14 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
if rule.get('type') == 'bandwidth_limit':
# Validate the limits
if rule.get('max_kbps') < qos_utils.MAX_KBPS_MIN_VALUE:
LOG.error("ERROR: QoS Policy %s has max_kbps below the "
"minimal value of %s. This is not supported.",
policy['id'], rule['max_kbps'])
n_errors = n_errors + 1
log_error("ERROR: QoS Policy %s has max_kbps below the "
"minimal value of %s. This is not supported." %
(policy['id'], rule['max_kbps']))
if rule.get('max_burst_kbps') > qos_utils.MAX_BURST_MAX_VALUE:
LOG.error("ERROR: QoS Policy %s has max_burst_kbps above "
log_error("ERROR: QoS Policy %s has max_burst_kbps above "
"the maximal value of %s. This is not "
"supported.",
policy['id'], rule['max_burst_kbps'])
n_errors = n_errors + 1
"supported." %
(policy['id'], rule['max_burst_kbps']))
# L2GW is not supported with the policy plugin
try:
@ -423,14 +454,38 @@ def validate_config_for_migration(resource, event, trigger, **kwargs):
pass
else:
if len(l2gws):
LOG.error("ERROR: Found %s L2Gws: %s. Networking-l2gw is not "
"supported.", len(l2gws), [l2gw.id for l2gw in l2gws])
n_errors = n_errors + 1
log_error("ERROR: Found %s L2Gws: %s. Networking-l2gw is not "
"supported." % (len(l2gws), [l2gw.id for l2gw in l2gws]))
LOG.info("\nPre-migration validation is complete")
if n_errors:
LOG.error("Found %s errors:", n_errors)
for msg in all_errors:
LOG.error(msg)
if n_warnings:
LOG.warning("Found %s warnings:", n_warnings)
for msg in all_warnings:
LOG.warning(msg)
if out_file:
f = open(out_file, "w")
if n_errors:
f.write("Found %s errors:\n" % n_errors)
for msg in all_errors:
f.write("%s\n" % msg)
if n_warnings:
f.write("Found %s warnings:\n" % n_warnings)
for msg in all_warnings:
f.write("%s\n" % msg)
f.close()
if strict:
n_errors = n_errors + n_warnings
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,
"migrated to NSX-T. %s issue%s found.", n_errors,
's were' if plural else ' was')
exit(n_errors)