diff --git a/setup.cfg b/setup.cfg index 8b204adf8e..ea60955889 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ packages = console_scripts = neutron-check-nsx-config = vmware_nsx.check_nsx_config:main nsxadmin = vmware_nsx.shell.nsxadmin:main + neutron-api-replay = vmware_nsx.api_replay.cli:main neutron.db.alembic_migrations = vmware-nsx = vmware_nsx.db.migration:alembic_migrations neutron.core_plugins = diff --git a/vmware_nsx/api_replay/cli.py b/vmware_nsx/api_replay/cli.py new file mode 100644 index 0000000000..e859f59b09 --- /dev/null +++ b/vmware_nsx/api_replay/cli.py @@ -0,0 +1,87 @@ +# 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 argparse + +from vmware_nsx.plugins.nsx_v3.api_replay import client + + +class ApiReplayCli(object): + + def __init__(self): + args = self._setup_argparse() + client.ApiReplayClient( + source_os_tenant_name=args.source_os_tenant_name, + source_os_username=args.source_os_username, + source_os_password=args.source_os_password, + source_os_auth_url=args.source_os_auth_url, + dest_os_username=args.dest_os_username, + dest_os_tenant_name=args.dest_os_tenant_name, + dest_os_password=args.dest_os_password, + dest_os_auth_url=args.dest_os_auth_url) + + def _setup_argparse(self): + parser = argparse.ArgumentParser() + + # Arguements required to connect to source + # neutron which we will fetch all of the data from. + parser.add_argument( + "--source-os-username", + required=True, + help="The source os-username to use to " + "gather neutron resources with.") + parser.add_argument( + "--source-os-tenant-name", + required=True, + help="The source os-tenant-name to use to " + "gather neutron resource with.") + parser.add_argument( + "--source-os-password", + required=True, + help="The password for this user.") + parser.add_argument( + "--source-os-auth-url", + required=True, + help="They keystone api endpoint for this user.") + + # Arguements required to connect to the dest neutron which + # we will recreate all of these resources over. + parser.add_argument( + "--dest-os-username", + required=True, + help="The dest os-username to use to" + "gather neutron resources with.") + parser.add_argument( + "--dest-os-tenant-name", + required=True, + help="The dest os-tenant-name to use to " + "gather neutron resource with.") + parser.add_argument( + "--dest-os-password", + required=True, + help="The password for this user.") + parser.add_argument( + "--dest-os-auth-url", + required=True, + help="They keystone api endpoint for this user.") + + # NOTE: this will return an error message if any of the + # require options are missing. + return parser.parse_args() + + +def main(): + ApiReplayCli() + + +if __name__ == '__main__': + main() diff --git a/vmware_nsx/api_replay/client.py b/vmware_nsx/api_replay/client.py new file mode 100644 index 0000000000..f8b73c6bf0 --- /dev/null +++ b/vmware_nsx/api_replay/client.py @@ -0,0 +1,263 @@ +# 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. + + +from neutronclient.common import exceptions as n_exc +from neutronclient.v2_0 import client + + +class ApiReplayClient(object): + + def __init__(self, source_os_username, source_os_tenant_name, + source_os_password, source_os_auth_url, + dest_os_username, dest_os_tenant_name, + dest_os_password, dest_os_auth_url): + + self._source_os_username = source_os_username + self._source_os_tenant_name = source_os_tenant_name + self._source_os_password = source_os_password + self._source_os_auth_url = source_os_auth_url + + self._dest_os_username = dest_os_username + self._dest_os_tenant_name = dest_os_tenant_name + self._dest_os_password = dest_os_password + self._dest_os_auth_url = dest_os_auth_url + + self.source_neutron = client.Client( + username=self._source_os_username, + tenant_name=self._source_os_tenant_name, + password=self._source_os_password, + auth_url=self._source_os_auth_url) + + self.dest_neutron = client.Client( + username=self._dest_os_username, + tenant_name=self._dest_os_tenant_name, + password=self._dest_os_password, + auth_url=self._dest_os_auth_url) + + self.migrate_security_groups() + self.migrate_routers() + self.migrate_networks_subnets_ports() + + def find_subnet_by_id(self, subnet_id, subnets): + for subnet in subnets: + if subnet['id'] == subnet_id: + return subnet + + def subnet_drop_ipv6_fields_if_v4(self, body): + """ + Drops v6 fields on subnets that are v4 as server doesn't allow them. + """ + v6_fields_to_remove = ['ipv6_address_mode', 'ipv6_ra_mode'] + if body['ip_version'] != 4: + return + + for field in v6_fields_to_remove: + if field in body: + body.pop(field) + + def get_ports_on_network(self, network_id, ports): + """Returns all the ports on a given network_id.""" + ports_on_network = [] + for port in ports: + if port['network_id'] == network_id: + ports_on_network.append(port) + return ports_on_network + + def have_id(self, id, groups): + """If the sg_id is in groups return true else false.""" + for group in groups: + if id == group['id']: + return group + + return False + + def drop_fields(self, item, drop_fields): + body = {} + for k, v in item.items(): + if k in drop_fields: + continue + body[k] = v + return body + + def migrate_security_groups(self): + """Migrates security groups from source to dest neutron.""" + + # first fetch the security groups from both the + # source and dest neutron server + source_sec_groups = self.source_neutron.list_security_groups() + dest_sec_groups = self.dest_neutron.list_security_groups() + + source_sec_groups = source_sec_groups['security_groups'] + dest_sec_groups = dest_sec_groups['security_groups'] + + for sg in source_sec_groups: + dest_sec_group = self.have_id(sg['id'], dest_sec_groups) + # If the security group already exists on the the dest_neutron + if dest_sec_group: + # make sure all the security group rules are theree and + # create them if not + for sg_rule in sg['security_group_rules']: + if(self.have_id(sg_rule['id'], + dest_sec_group['security_group_rules']) + is False): + try: + print ( + self.dest_neutron.create_security_group_rule( + {'security_group_rule': sg_rule})) + except n_exc.Conflict: + # NOTE(arosen): when you create a default + # security group it is automatically populated + # with some rules. When we go to create the rules + # that already exist because of a match an error + # is raised here but thats okay. + pass + + # dest server doesn't have the group so we create it here. + else: + sg_rules = sg.pop('security_group_rules') + try: + print(self.dest_neutron.create_security_group( + {'security_group': sg})) + except Exception as e: + # TODO(arosen): improve exception handing here. + print (e) + pass + + for sg_rule in sg_rules: + try: + print (self.dest_neutron.create_security_group_rule( + {'security_group_rule': sg_rule})) + except n_exc.Conflict: + # NOTE(arosen): when you create a default + # security group it is automatically populated + # with some rules. When we go to create the rules + # that already exist because of a match an error + # is raised here but thats okay. + pass + + def migrate_routers(self): + """Migrates routers from source to dest neutron.""" + source_routers = self.source_neutron.list_routers()['routers'] + dest_routers = self.dest_neutron.list_routers()['routers'] + + for router in source_routers: + dest_router = self.have_id(router['id'], dest_routers) + if dest_router is False: + drop_router_fields = ['status', + 'routes', + 'external_gateway_info'] + body = self.drop_fields(router, drop_router_fields) + print (self.dest_neutron.create_router( + {'router': body})) + + def migrate_networks_subnets_ports(self): + """Migrates routers from source to dest neutron.""" + source_ports = self.source_neutron.list_ports()['ports'] + source_subnets = self.source_neutron.list_subnets()['subnets'] + source_networks = self.source_neutron.list_networks()['networks'] + dest_networks = self.dest_neutron.list_networks()['networks'] + dest_ports = self.dest_neutron.list_ports()['ports'] + + # NOTE: These are fields we drop of when creating a subnet as the + # network api doesn't allow us to specify them. + # TODO(arosen): revisit this to make these fields passable. + drop_subnet_fields = ['updated_at', + 'created_at', + 'network_id', + 'id'] + + # NOTE: These are fields we drop of when creating a subnet as the + # network api doesn't allow us to specify them. + # TODO(arosen): revisit this to make these fields passable. + drop_port_fields = ['updated_at', + 'created_at', + 'status', + 'port_security_enabled', + 'binding:vif_details', + 'binding:vif_type', + 'binding:host_id'] + + drop_network_fields = ['status', 'subnets', 'availability_zones', + 'created_at', 'updated_at', 'tags'] + + for network in source_networks: + body = self.drop_fields(network, drop_network_fields) + + # neutron doesn't like description being None even though its + # what it returns to us. + if 'description' in body and body['description'] is None: + body['description'] = '' + + # only create network if the dest server doesn't have it + if self.have_id(network['id'], dest_networks) is False: + created_net = self.dest_neutron.create_network( + {'network': body})['network'] + print ("Created network: " + created_net['id']) + + for subnet_id in network['subnets']: + subnet = self.find_subnet_by_id(subnet_id, source_subnets) + body = self.drop_fields(subnet, drop_subnet_fields) + + # specify the network_id that we just created above + body['network_id'] = network['id'] + self.subnet_drop_ipv6_fields_if_v4(body) + if 'description' in body and body['description'] is None: + body['description'] = '' + try: + created_subnet = self.dest_neutron.create_subnet( + {'subnet': body})['subnet'] + print ("Created subnet: " + created_subnet['id']) + except n_exc.BadRequest as e: + print (e) + # NOTE(arosen): this occurs here if you run the script + # multiple times as we don't currently + # perserve the subnet_id. Also, 409 would be a better + # response code for this in neutron :( + pass + + # create the ports on the network + ports = self.get_ports_on_network(network['id'], source_ports) + for port in ports: + + body = self.drop_fields(port, drop_port_fields) + + # specify the network_id that we just created above + port['network_id'] = network['id'] + + # remove the subnet id field from fixed_ips dict + for fixed_ips in body['fixed_ips']: + del fixed_ips['subnet_id'] + + # only create port if the dest server doesn't have it + if self.have_id(port['id'], dest_ports) is False: + + if port['device_owner'] in ['network:router_interface', + 'network:router_gateway']: + if port['allowed_address_pairs'] == []: + del body['allowed_address_pairs'] + created_port = self.dest_neutron.create_port( + {'port': body})['port'] + print ("Created port: " + created_port['id']) + + if port['device_owner'] == 'network:router_interface': + try: + print (self.dest_neutron.add_interface_router( + port['device_id'], + {'port_id': port['id']})) + + except n_exc.BadRequest as e: + # NOTE(arosen): this occurs here if you run the + # script multiple times as we don't track this. + print (e) + + # TODO(arosen): handle 'network:router_gateway' uplinking