From f177d2923de0f51bd398c9e1d994c1e870a9350b Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Thu, 25 Nov 2021 02:49:29 -0800 Subject: [PATCH] [V2T] Helper admin utils for validation and N/S cutover This patch adds an operation for the NSX-V and two operations for the NSX-T plugin. The goal of these operation are: - Find routers without any downlink. They cannot be migrated and should be removed before migration to NSX-T. - Patch and restore Neutron routers without gateway. For N/S cutover, each router must have a T0 uplink or an SR. The 'fixup' operation ensures the NSX T1 routers for these neutron routers will have a T0 uplink. (They surely do not have a SR). The 'restore' operation returns T1 routers to their original state. Change-Id: Iffd1a5e43e08fdc997a591829c87bcc0bb806c77 --- .../admin/plugins/nsxp/resources/migration.py | 162 ++++++++++++++++++ .../admin/plugins/nsxv/resources/migration.py | 27 +++ vmware_nsx/shell/resources.py | 10 +- 3 files changed, 197 insertions(+), 2 deletions(-) diff --git a/vmware_nsx/shell/admin/plugins/nsxp/resources/migration.py b/vmware_nsx/shell/admin/plugins/nsxp/resources/migration.py index db070adb80..934efaee27 100644 --- a/vmware_nsx/shell/admin/plugins/nsxp/resources/migration.py +++ b/vmware_nsx/shell/admin/plugins/nsxp/resources/migration.py @@ -18,10 +18,12 @@ import sys import netaddr from neutron_lib.callbacks import registry +from neutron_lib import context from oslo_log import log as logging from oslo_serialization import jsonutils from vmware_nsx.shell.admin.plugins.common import constants +from vmware_nsx.shell.admin.plugins.common import formatters from vmware_nsx.shell.admin.plugins.common import utils as admin_utils from vmware_nsx.shell.admin.plugins.nsxp.resources import utils as p_utils from vmware_nsx.shell.admin.plugins.nsxv3.resources import migration @@ -218,6 +220,158 @@ def migration_validate_external_cidrs(resource, event, trigger, **kwargs): sys.exit(0) +@admin_utils.output_header +@admin_utils.unpack_payload +def patch_routers_without_gateway(resource, event, trigger, **kwargs): + state_filename = None + tier0_id = None + if kwargs.get('property'): + properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) + state_filename = properties.get('state-file') + tier0_id = properties.get('tier0') + + if not state_filename: + LOG.error("Must provide a filename for saving T1 GW state") + return + if not tier0_id: + LOG.error("Cannot execute if a Tier-0 GW uuid is not provided") + return + + nsxpolicy = p_utils.get_connected_nsxpolicy() + try: + nsxpolicy.tier0.get(tier0_id) + except Exception as e: + LOG.error("An error occurred while retrieving Tier0 gw router %s: %s", + tier0_id, e) + raise SystemExit(e) + + ctx = context.get_admin_context() + fixed_routers = [] + + # Open state file, if exists, read data + try: + with open(state_filename) as f: + state_data = jsonutils.load(f) + except FileNotFoundError: + LOG.debug("State file not created yet") + state_data = {} + + with p_utils.NsxPolicyPluginWrapper() as plugin: + routers = plugin.get_routers(ctx) + try: + for router in routers: + router_id = router['id'] + if plugin._extract_external_gw(ctx, {'router': router}): + continue + + # Skip router if already fixed up + if router_id in state_data: + LOG.info("It seems router %s has already been patched. " + "Skipping it.", router_id) + continue + + # Fetch T1 + t1_data = nsxpolicy.tier1.get(router_id) + route_adv_data = t1_data.get('route_advertisement_types', []) + # append state data + state_data[router_id] = route_adv_data + # Update T1: connect to T0, disable route advertisment + nsxpolicy.tier1.update_route_advertisement( + router_id, + static_routes=False, + subnets=False, + nat=False, + lb_vip=False, + lb_snat=False, + ipsec_endpoints=False, + tier0=tier0_id) + fixed_routers.append(router) + except Exception as e: + LOG.error("Failure while patching routers without " + "gateway: %s", e) + # do not call sys.exit here + finally: + # Save state data + with open(state_filename, 'w') as f: + # Pretty print in case someone needs to insepct it + jsonutils.dump(state_data, f, indent=4) + + LOG.info(formatters.output_formatter( + "Attached following routers to T0 %s" % tier0_id, + fixed_routers, + ['id', 'name', 'project_id'])) + return fixed_routers + + +@admin_utils.output_header +@admin_utils.unpack_payload +def restore_routers_without_gateway(resource, event, trigger, **kwargs): + state_filename = None + if kwargs.get('property'): + properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) + state_filename = properties.get('state-file') + + if not state_filename: + LOG.error("Must provide a filename for saving T1 GW state") + return + + nsxpolicy = p_utils.get_connected_nsxpolicy() + ctx = context.get_admin_context() + restored_routers = [] + + # Open state file,read data + # Fail if file does not exist + try: + with open(state_filename) as f: + state_data = jsonutils.load(f) + except FileNotFoundError: + LOG.error("State file %s was not found. Aborting", state_filename) + sys.exit(1) + + with p_utils.NsxPolicyPluginWrapper() as plugin: + routers = plugin.get_routers(ctx) + try: + for router in routers: + router_id = router['id'] + if plugin._extract_external_gw(ctx, {'router': router}): + continue + + adv_info = state_data.get(router_id) + # Disconnect T0, set route adv from state file + if adv_info: + nsxpolicy.tier1.update_route_advertisement( + router_id, + static_routes=("TIER1_STATIC_ROUTES" in adv_info), + subnets=("TIER1_CONNECTED" in adv_info), + nat=("TIER1_NAT" in adv_info), + lb_vip=("TIER1_LB_VIP" in adv_info), + lb_snat=("TIER1_LB_SNAT" in adv_info), + ipsec_endpoints=("TIER1_IPSEC_LOCAL_ENDPOINT" in + adv_info), + tier0=None) + else: + # Only disconnect T0 + LOG.info("Router advertisment info not found in state " + "file for router %s", router_id) + nsxpolicy.tier1.update_route_advertisement( + router_id, tier0=None) + + state_data.pop(router_id, None) + restored_routers.append(router) + except Exception as e: + LOG.error("Failure while restoring routers without " + "gateway: %s", e) + finally: + with open(state_filename, 'w') as f: + # Pretty print in case someone needs to insepct it + jsonutils.dump(state_data, f, indent=4) + LOG.info(formatters.output_formatter( + "Restored following routers", + restored_routers, + ['id', 'name', 'project_id'])) + return restored_routers + + registry.subscribe(cleanup_db_mappings, constants.NSX_MIGRATE_T_P, shell.Operations.CLEAN_ALL.value) @@ -233,3 +387,11 @@ registry.subscribe(migration_tier0_redistribute, registry.subscribe(migration_validate_external_cidrs, constants.NSX_MIGRATE_V_T, shell.Operations.VALIDATE.value) + +registry.subscribe(patch_routers_without_gateway, + constants.NSX_MIGRATE_V_T, + shell.Operations.PATCH_RTR_NOGW.value) + +registry.subscribe(restore_routers_without_gateway, + constants.NSX_MIGRATE_V_T, + shell.Operations.RESTORE_RTR_NOGW.value) diff --git a/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py b/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py index 6afaf9c4da..c4baee23ad 100644 --- a/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py +++ b/vmware_nsx/shell/admin/plugins/nsxv/resources/migration.py @@ -650,6 +650,29 @@ def list_ports_vif_ids(resource, event, trigger, **kwargs): LOG.info("Mapping data saved into %s", filename) +@admin_utils.output_header +@admin_utils.unpack_payload +def get_routers_without_interfaces(resource, event, trigger, **kwargs): + context = n_context.get_admin_context() + empty_routers = [] + with utils.NsxVPluginWrapper() as plugin: + routers = plugin.get_routers(context) + for router in routers: + # Routers in the metadata internal project will always have an + # interface, but it won't be returned by the method below, since + # the device_owner is network:md_interface + if router['project_id'] == 'metadata_internal_project': + LOG.debug("Skipping internal router %s", router['id']) + continue + interfaces = plugin._get_router_interfaces(router['id']) + if not interfaces: + empty_routers.append(router) + LOG.info(formatters.output_formatter( + "Routers without interfaces", empty_routers, + ['id', 'name', 'project_id'])) + return empty_routers + + @admin_utils.output_header @admin_utils.unpack_payload def build_edge_mapping_file(resource, event, trigger, **kwargs): @@ -686,6 +709,10 @@ def build_edge_mapping_file(resource, event, trigger, **kwargs): LOG.info("Edge mapping data saved into %s", filename) +registry.subscribe(get_routers_without_interfaces, + constants.NSX_MIGRATE_V_T, + shell.Operations.LIST_RTR_NO_IFACE.value) + 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 16cdd25ef7..7a3221cdaf 100644 --- a/vmware_nsx/shell/resources.py +++ b/vmware_nsx/shell/resources.py @@ -85,6 +85,9 @@ class Operations(enum.Enum): SET_STATUS_ERROR = 'set-status-error' CHECK_COMPUTE_CLUSTERS = 'check-compute-clusters' CUTOVER_MAPPINGS = 'mappings-for-edge-cutover' + LIST_RTR_NO_IFACE = 'list-routers-no-interfaces' + PATCH_RTR_NOGW = 'cutover-fixup-router-nogw' + RESTORE_RTR_NOGW = 'cutover-restore-router-nogw' ops = [op.value for op in Operations] @@ -270,7 +273,8 @@ nsxv_resources = { Operations.DELETE.value]), constants.NSX_MIGRATE_V_T: Resource(constants.NSX_MIGRATE_V_T, [Operations.VALIDATE.value, - Operations.CUTOVER_MAPPINGS.value]), + Operations.CUTOVER_MAPPINGS.value, + Operations.LIST_RTR_NO_IFACE.value]), constants.PORTS: Resource(constants.PORTS, [Operations.LIST.value]), constants.LOADBALANCERS: Resource(constants.LOADBALANCERS, @@ -317,7 +321,9 @@ nsxp_resources = { constants.NSX_MIGRATE_V_T: Resource(constants.NSX_MIGRATE_V_T, [Operations.CLEAN_ALL.value, Operations.VALIDATE.value, - Operations.NSX_REDISTRIBUTE.value]), + Operations.NSX_REDISTRIBUTE.value, + Operations.PATCH_RTR_NOGW.value, + Operations.RESTORE_RTR_NOGW.value]), constants.LOADBALANCERS: Resource(constants.LOADBALANCERS, [Operations.SET_STATUS_ERROR.value]), }