# Copyright 2017 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 sys from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils from neutron.extensions import securitygroup as ext_sg from neutron_lib.callbacks import registry from neutron_lib import context as n_context from neutron_lib import exceptions from vmware_nsx.api_replay import utils as replay_utils from vmware_nsx.db import db from vmware_nsx.extensions import projectpluginmap 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 as v_utils from vmware_nsx.shell.admin.plugins.nsxv3.resources import utils as v3_utils from vmware_nsx.shell import resources as shell LOG = logging.getLogger(__name__) # list of supported objects to migrate in order of deletion (creation will be # in the opposite order) migrated_resources = ["floatingip", "router", "port", "subnet", "network", "security_group"] #TODO(asarfaty): add other resources of different service plugins like #vpnaas, fwaas, lbaas, qos, subnetpool, etc @admin_utils.output_header def import_projects(resource, event, trigger, **kwargs): """Import existing openstack projects to the current plugin""" # TODO(asarfaty): get the projects list from keystone # get the plugin name from the user if not kwargs.get('property'): LOG.error("Need to specify plugin and project parameters") return else: properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) plugin = properties.get('plugin') project = properties.get('project') if not plugin or not project: LOG.error("Need to specify plugin and project parameters") return if plugin not in projectpluginmap.VALID_TYPES: LOG.error("The supported plugins are %s", projectpluginmap.VALID_TYPES) return ctx = n_context.get_admin_context() if not db.get_project_plugin_mapping(ctx.session, project): db.add_project_plugin_mapping(ctx.session, project, plugin) def get_resource_file_name(project_id, resource): return "%s_nsxv_%ss" % (project_id, resource) def read_v_resources_to_files(context, project_id): """Read all relevant NSX-V resources from a specific project and write them into a json file """ results = {} with v_utils.NsxVPluginWrapper() as plugin: filters = {'project_id': [project_id]} for resource in migrated_resources: filename = get_resource_file_name(project_id, resource) file = open(filename, 'w') get_objects = getattr(plugin, "get_%ss" % resource) objects = get_objects(context, filters=filters) # also add router gateway ports of the relevant routers # (don't have the project id) if resource == 'port': rtr_ids = [rtr['id'] for rtr in results['router']] gw_filters = {'device_owner': ['network:router_gateway'], 'device_id': rtr_ids} gw_ports = plugin.get_ports(context, filters=gw_filters, filter_project=False) # ignore metadata gw ports objects.extend([port for port in gw_ports if not port['tenant_id']]) file.write(jsonutils.dumps(objects, sort_keys=True, indent=4)) file.close() results[resource] = objects return results def read_v_resources_from_files(project_id): """Read all relevant NSX-V resources from a json file""" results = {} for resource in migrated_resources: filename = get_resource_file_name(project_id, resource) file = open(filename, 'r') results[resource] = jsonutils.loads(file.read()) file.close() return results def delete_router_routes_and_interfaces(context, plugin, router): if router.get('routes'): plugin.update_router(context, router['id'], {'router': {'routes': []}}) interfaces = plugin._get_router_interfaces(context, router['id']) for port in interfaces: plugin.remove_router_interface(context, router['id'], {'port_id': port['id']}) def delete_v_resources(context, objects): """Delete a list of objects from the V plugin""" with v_utils.NsxVPluginWrapper() as plugin: LOG.info(">>>>Deleting all NSX-V objects of the project.") for resource in migrated_resources: get_object = getattr(plugin, "get_%s" % resource) del_object = getattr(plugin, "delete_%s" % resource) for obj in objects[resource]: # verify that this object still exists try: get_object(context, obj['id']) except exceptions.NotFound: # prevent logger from logging this exception sys.exc_clear() continue try: # handle special cases before delete if resource == 'router': delete_router_routes_and_interfaces( context, plugin, obj) elif resource == 'port': if obj['device_owner'] == 'network:dhcp': continue # delete the objects from the NSX-V plugin del_object(context, obj['id']) LOG.info(">>Deleted %(resource)s %(name)s", {'resource': resource, 'name': obj.get('name') or obj['id']}) except Exception as e: LOG.warning(">>Failed to delete %(resource)s %(name)s: " "%(e)s", {'resource': resource, 'name': obj.get('name') or obj['id'], 'e': e}) LOG.info(">>>>Done deleting all NSX-V objects.") def get_router_by_id(objects, router_id): for rtr in objects.get('router', []): if rtr['id'] == router_id: return rtr def create_t_resources(context, objects, ext_net): """Create a list of objects in the T plugin""" LOG.info(">>>>Creating all the objects of the project in NSX-T.") prepare = replay_utils.PrepareObjectForMigration() with v3_utils.NsxV3PluginWrapper() as plugin: # create the resource in the order opposite to the deletion # (but start with routers) ordered_resources = migrated_resources[::-1] ordered_resources.remove('router') ordered_resources = ['router'] + ordered_resources dhcp_subnets = [] for resource in ordered_resources: total_num = len(objects[resource]) LOG.info(">>>Creating %s %s%s.", total_num, resource, 's' if total_num > 1 else '') get_object = getattr(plugin, "get_%s" % resource) create_object = getattr(plugin, "create_%s" % resource) # go over the objects of this resource for count, obj in enumerate(objects[resource], 1): # check if this object already exists try: get_object(context, obj['id']) except exceptions.NotFound: # prevent logger from logging this exception sys.exc_clear() else: # already exists (this will happen if we rerun from files, # or if the deletion failed) LOG.info(">>Skipping %(resource)s %(name)s %(count)s/" "%(total)s as it was already created.", {'resource': resource, 'name': obj.get('name') or obj['id'], 'count': count, 'total': total_num}) continue # fix object before creation using the api replay code orig_id = obj['id'] prepare_object = getattr(prepare, "prepare_%s" % resource) obj_data = prepare_object(obj, direct_call=True) enable_dhcp = False # special cases for different objects before create: if resource == 'subnet': if obj_data['enable_dhcp']: enable_dhcp = True # disable dhcp for now, to avoid ip collisions obj_data['enable_dhcp'] = False elif resource == 'security_group': # security group rules should be added separately sg_rules = obj_data.pop('security_group_rules') elif resource == 'floatingip': # Create the floating IP on the T external network obj_data['floating_network_id'] = ext_net del obj_data['floating_ip_address'] elif resource == 'port': # remove the old subnet id field from ports fixed_ips dict # since the subnet ids are changed for fixed_ips in obj_data['fixed_ips']: del fixed_ips['subnet_id'] if obj_data['device_owner'] == 'network:dhcp': continue if obj_data['device_owner'] == 'network:floatingip': continue if obj_data['device_owner'] == 'network:router_gateway': # add a gateway on the new ext network for this router router_id = obj_data['device_id'] # keep the original enable-snat value router_data = get_router_by_id(objects, router_id) enable_snat = router_data['external_gateway_info'].get( 'enable_snat', True) rtr_body = { "external_gateway_info": {"network_id": ext_net, "enable_snat": enable_snat}} try: plugin.update_router( context, router_id, {'router': rtr_body}) LOG.info(">>Uplinked router %(rtr)s to new " "external network %(net)s", {'rtr': router_id, 'net': ext_net}) except Exception as e: LOG.error(">>Failed to add router %(rtr)s " "gateway: %(e)s", {'rtr': router_id, 'e': e}) continue if obj_data['device_owner'] == 'network:router_interface': try: # uplink router_interface ports by creating the # port, and attaching it to the router router_id = obj_data['device_id'] obj_data['device_owner'] = "" obj_data['device_id'] = "" created_port = plugin.create_port( context, {'port': obj_data}) LOG.info(">>Created interface port %(port)s, ip " "%(ip)s, mac %(mac)s)", {'port': created_port['id'], 'ip': created_port['fixed_ips'][0][ 'ip_address'], 'mac': created_port['mac_address']}) plugin.add_router_interface( context, router_id, {'port_id': created_port['id']}) LOG.info(">>Uplinked router %(rtr)s to network " "%(net)s", {'rtr': router_id, 'net': obj_data['network_id']}) except Exception as e: LOG.error(">>Failed to add router %(rtr)s " "interface port: %(e)s", {'rtr': router_id, 'e': e}) continue # create the object on the NSX-T plugin try: created_obj = create_object(context, {resource: obj_data}) LOG.info(">>Created %(resource)s %(name)s %(count)s/" "%(total)s", {'resource': resource, 'count': count, 'name': obj_data.get('name') or orig_id, 'total': total_num}) except Exception as e: # TODO(asarfaty): subnets ids are changed, so recreating a # subnet will fail on overlapping ips. LOG.error(">>Failed to create %(resource)s %(name)s: " "%(e)s", {'resource': resource, 'e': e, 'name': obj_data.get('name') or orig_id}) continue # special cases for different objects after create: if resource == 'security_group': sg_id = obj_data.get('name') or obj_data['id'] for rule in sg_rules: rule_data = prepare.prepare_security_group_rule(rule) try: plugin.create_security_group_rule( context, {'security_group_rule': rule_data}) except ext_sg.SecurityGroupRuleExists: # default rules were already created. # prevent logger from logging this exception sys.exc_clear() except Exception as e: LOG.error( ">>Failed to create security group %(name)s " "rules: %(e)s", {'name': sg_id, 'e': e}) elif resource == 'subnet': if enable_dhcp: dhcp_subnets.append(created_obj['id']) # Enable dhcp on all the relevant subnets (after creating all ports, # to maintain original IPs): if dhcp_subnets: for subnet_id in dhcp_subnets: try: plugin.update_subnet( context, subnet_id, {'subnet': {'enable_dhcp': True}}) except Exception as e: LOG.error("Failed to enable DHCP on subnet %(subnet)s:" " %(e)s", {'subnet': subnet_id, 'e': e}) # Add static routes (after all router interfaces and gateways are set) for obj_data in objects['router']: if 'routes' in obj_data: try: plugin.update_router( context, obj_data['id'], {'router': {'routes': obj_data['routes']}}) except Exception as e: LOG.error("Failed to add routes to router %(rtr)s: " "%(e)s", {'rtr': obj_data['id'], 'e': e}) LOG.info(">>>Done Creating all objects in NSX-T.") @admin_utils.output_header def migrate_v_project_to_t(resource, event, trigger, **kwargs): """Migrate 1 project from v to t with all its resources""" # filter out the plugins INFO logging # TODO(asarfaty): Consider this for all admin utils LOG.logger.setLevel(logging.INFO) logging.getLogger(None).logger.setLevel(logging.WARN) # get the configuration: tenant + public network + from file flag usage = ("Usage: nsxadmin -r projects -o %s --property project-id=<> " "--property external-net= " "<--property from-file=True>" % shell.Operations.NSX_MIGRATE_V_V3.value) if not kwargs.get('property'): LOG.error("Missing parameters: %s", usage) return properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) project = properties.get('project-id') ext_net_id = properties.get('external-net') from_file = properties.get('from-file', 'false').lower() == "true" # TODO(asarfaty): get files path if not project: LOG.error("Missing project-id parameter: %s", usage) return if not ext_net_id: LOG.error("Missing external-net parameter: %s", usage) return # check if files exist in the current directory try: filename = get_resource_file_name(project, 'network') file = open(filename, 'r') if file.read(): if not from_file: from_file = admin_utils.query_yes_no( "Use existing resources files for this project?", default="yes") file.close() except Exception: sys.exc_clear() if from_file: LOG.error("Cannot run from file: files not found") return # validate tenant id and public network ctx = n_context.get_admin_context() mapping = db.get_project_plugin_mapping(ctx.session, project) current_plugin = mapping.plugin if not mapping: LOG.error("Project %s is unknown", project) return if not from_file and current_plugin != projectpluginmap.NsxPlugins.NSX_V: LOG.error("Project %s belongs to plugin %s.", project, mapping.plugin) return with v3_utils.NsxV3PluginWrapper() as plugin: try: plugin.get_network(ctx, ext_net_id) except exceptions.NetworkNotFound: LOG.error("Network %s was not found", ext_net_id) return if not plugin._network_is_external(ctx, ext_net_id): LOG.error("Network %s is not external", ext_net_id) return if from_file: # read resources from files objects = read_v_resources_from_files(project) else: # read all V resources and dump to a file objects = read_v_resources_to_files(ctx, project) # delete all the V resources (reading it from the files) if current_plugin == projectpluginmap.NsxPlugins.NSX_V: delete_v_resources(ctx, objects) # change the mapping of this tenant to T db.update_project_plugin_mapping(ctx.session, project, projectpluginmap.NsxPlugins.NSX_T) # use api replay flag to allow keeping the IDs cfg.CONF.set_override('api_replay_mode', True) # add resources 1 by one after adapting them to T (api-replay code) create_t_resources(ctx, objects, ext_net_id) # reset api replay flag to allow keeping the IDs cfg.CONF.set_override('api_replay_mode', False) registry.subscribe(import_projects, constants.PROJECTS, shell.Operations.IMPORT.value) registry.subscribe(migrate_v_project_to_t, constants.PROJECTS, shell.Operations.NSX_MIGRATE_V_V3.value)