diff --git a/.gitignore b/.gitignore index cb9567d..8e7dedc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ .ropeproject .tox .venv +venv AUTHORS Authors build-stamp @@ -38,3 +39,4 @@ nova/tests/cover/* nova/vcsversion.py tools/conf/nova.conf* resources/write_data +migration-config.conf diff --git a/coriolis_openstack_utils/actions/coriolis_endpoint_actions.py b/coriolis_openstack_utils/actions/coriolis_endpoint_actions.py index 60e398a..c3b7cfb 100644 --- a/coriolis_openstack_utils/actions/coriolis_endpoint_actions.py +++ b/coriolis_openstack_utils/actions/coriolis_endpoint_actions.py @@ -46,6 +46,12 @@ def endpoint_name_format(self): def connection_info(self): return self._source_openstack_client.connection_info + @property + def should_override_project(self): + # Source endpoints SHOULD override project_name to scope to the + # tenant where the instance is located + return True + def get_tenant_name(self): return self.tenant_name_format % { "original": self.payload["instance_tenant_name"]} @@ -86,9 +92,11 @@ def print_operations(self): def check_already_done(self): super(SourceEndpointCreationAction, self).execute_operations() connection_info = copy.deepcopy(self.connection_info) - # override the con info with the right one: - tenant_name = self.get_tenant_name() - connection_info["project_name"] = tenant_name + # Only override project_name for destination endpoints + # Source endpoints keep admin project to see all tenants' resources + if self.should_override_project: + tenant_name = self.get_tenant_name() + connection_info["project_name"] = tenant_name endpoint_name = self.get_endpoint_name() existing_endpoint = None @@ -128,15 +136,19 @@ def execute_operations(self): return done["result"] connection_info = copy.deepcopy(self.connection_info) - tenant_name = self.get_tenant_name() - connection_info["project_name"] = tenant_name + # Only override project_name for destination endpoints + if self.should_override_project: + tenant_name = self.get_tenant_name() + connection_info["project_name"] = tenant_name endpoint_name = self.get_endpoint_name() LOG.info("Creating new endpoint named '%s'", endpoint_name) + # Get available regions for the endpoint + regions = [r.id for r in self._coriolis_client.regions.list()] endpoint = self._coriolis_client.endpoints.create( endpoint_name, ENDPOINT_TYPE_OPENSTACK, - connection_info, DEFAULT_ENDPOINT_DESCRIPTION) + connection_info, DEFAULT_ENDPOINT_DESCRIPTION, regions) return endpoint.id @@ -158,7 +170,8 @@ def cleanup(self): LOG.info("Cannot delete endpoint '%s' with connection_info '%s'," "not found.") if endpoints_length == 1: - self._coriolis_client.endpoints.delete(similar_endpoints[0].id) + # similar_endpoints contains endpoint IDs (strings), not objects + self._coriolis_client.endpoints.delete(similar_endpoints[0]) elif endpoints_length > 1: LOG.warn("Multiple endpoints with name '%s' and " "connection_info '%s' found, skipping deletion." @@ -184,3 +197,9 @@ def endpoint_name_format(self): @property def connection_info(self): return self._destination_openstack_client.connection_info + + @property + def should_override_project(self): + # Destination endpoints SHOULD override project_name to create + # resources in the target tenant + return True diff --git a/coriolis_openstack_utils/actions/coriolis_transfer_actions.py b/coriolis_openstack_utils/actions/coriolis_transfer_actions.py index 89c4d42..7d95a2a 100644 --- a/coriolis_openstack_utils/actions/coriolis_transfer_actions.py +++ b/coriolis_openstack_utils/actions/coriolis_transfer_actions.py @@ -170,7 +170,10 @@ def execute_operations(self, subtasks_pre_executed=False): "new": True} def migration_same(self, other_migration): - if other_migration.status != MIGRATION_STATUS_RUNNING: + # New API uses last_execution_status instead of status + transfer_status = getattr(other_migration, 'status', None) or \ + getattr(other_migration, 'last_execution_status', None) + if transfer_status != MIGRATION_STATUS_RUNNING: return False elif len(other_migration.instances) != 1: return False @@ -203,9 +206,9 @@ def migration_same(self, other_migration): return True def cleanup(self): - for migration in self._coriolis_client.migrations.list(): - if self.migration_same(migration): - self._coriolis_client.migrations.cancel(migration.id) + for transfer in self._coriolis_client.transfers.list(): + if self.migration_same(transfer): + self._coriolis_client.transfers.delete(transfer.id) self.source_endpoint_create_action.cleanup() self.dest_endpoint_create_action.cleanup() @@ -257,19 +260,22 @@ class MigrationCreationAction(TransferAction): action_type = base.ACTION_TYPE_CHECK_CREATE_MIGRATION def get_transfers_list(self): - return reversed(self._coriolis_client.migrations.list()) + return reversed(self._coriolis_client.transfers.list()) def get_transfer_status(self, transfer): - return transfer.status + # New Coriolis API uses last_execution_status instead of status + return getattr(transfer, 'status', None) or \ + getattr(transfer, 'last_execution_status', 'UNKNOWN') def check_existing_transfer(self, existing_transfer): migration = existing_transfer - if migration.status == MIGRATION_STATUS_ERROR: + migration_status = self.get_transfer_status(migration) + if migration_status == MIGRATION_STATUS_ERROR: LOG.info( "Found existing migration with id '%s' for VM '%s', but " "it is in '%s' state, thus, a new one will be created" % ( migration.id, self.payload["instance_name"], - migration.status)) + migration_status)) done = { "done": False, "result": None} @@ -279,13 +285,13 @@ def check_existing_transfer(self, existing_transfer): "Found existing migration with id '%s' for VM '%s'. " "Current status is '%s'. NOT creating a new migration.", migration.id, self.payload["instance_name"], - migration.status) + migration_status) done = { "done": True, "result": { "instance_name": self.payload['instance_name'], "transfer_id": migration.id, - "status": migration.status, + "status": migration_status, "origin_endpoint_id": migration.origin_endpoint_id, "destination_endpoint_id": @@ -304,14 +310,52 @@ def print_operations(self): def create_transfer(self, source_endpoint, destination_endpoint): skip_os_morphing = CONF.destination.skip_os_morphing + shutdown_instances = CONF.destination.shutdown_instances pre_create_neutron_ports = CONF.destination.pre_create_neutron_ports if pre_create_neutron_ports: self.pre_create_neutron_ports() - return self._coriolis_client.migrations.create( - source_endpoint, destination_endpoint, {}, - self._destination_env, [self.payload['instance_name']], - skip_os_morphing=skip_os_morphing) + # Source environment options for Coriolis transfer + # Use 'coriolis_backups' for Glance-based VMs + source_env = { + 'replica_export_mechanism': 'coriolis_backups' + } + + # Add Coriolis backup export options if configured + # These must be nested under 'coriolis_backups_options' + coriolis_backups_options = {} + if CONF.source.export_image: + coriolis_backups_options['export_image'] = CONF.source.export_image + if CONF.source.export_network: + coriolis_backups_options['export_network'] = CONF.source.export_network + if CONF.source.export_flavor_name: + coriolis_backups_options['export_flavor_name'] = CONF.source.export_flavor_name + if CONF.source.export_interim_volume_type: + coriolis_backups_options['export_interim_volume_type'] = CONF.source.export_interim_volume_type + if hasattr(CONF.source, 'export_worker_use_fip'): + coriolis_backups_options['export_worker_use_fip'] = CONF.source.export_worker_use_fip + if coriolis_backups_options: + source_env['coriolis_backups_options'] = coriolis_backups_options + + # New Coriolis API uses transfers with scenario type + dest_minion_pool_id = getattr(CONF.destination, 'destination_minion_pool_id', None) + transfer = self._coriolis_client.transfers.create( + source_endpoint, destination_endpoint, + source_env, # source_environment + self._destination_env, # destination_environment + [self.payload['instance_name']], # instances + "live_migration", # transfer_scenario + skip_os_morphing=skip_os_morphing, + destination_minion_pool_id=dest_minion_pool_id) + + # New Coriolis API requires explicit execution after creating transfer + LOG.info( + "Executing transfer %s for VM '%s'", + transfer.id, self.payload['instance_name']) + self._coriolis_client.transfer_executions.create( + transfer.id, shutdown_instances=shutdown_instances) + + return transfer class ReplicaCreationAction(TransferAction): @@ -332,7 +376,11 @@ def last_execution_status(replica): return REPLICA_EXECUTION_STATUS_NONE def get_transfers_list(self): - return reversed(self._coriolis_client.replicas.list()) + # Filter for replica scenario transfers + all_transfers = self._coriolis_client.transfers.list() + replicas = [t for t in all_transfers + if getattr(t, 'scenario', 'replica') == 'replica'] + return reversed(replicas) def get_transfer_status(self, transfer): status = "NOT EXECUTED" @@ -384,13 +432,17 @@ def create_transfer(self, source_endpoint, destination_endpoint): if pre_create_neutron_ports: self.pre_create_neutron_ports() - replica = self._coriolis_client.replicas.create( - source_endpoint, destination_endpoint, {}, - self._destination_env, [self.payload['instance_name']]) + # New Coriolis API uses transfers with scenario type + replica = self._coriolis_client.transfers.create( + source_endpoint, destination_endpoint, + {}, # source_environment + self._destination_env, # destination_environment + [self.payload['instance_name']], # instances + "replica") # transfer_scenario if self.payload['execute_replica'] is True: shutdown_instances = CONF.destination.shutdown_instances - self._coriolis_client.replica_executions.create( + self._coriolis_client.transfer_executions.create( replica.id, shutdown_instances=shutdown_instances) return replica diff --git a/coriolis_openstack_utils/actions/network_actions.py b/coriolis_openstack_utils/actions/network_actions.py index f577649..b32a4ce 100644 --- a/coriolis_openstack_utils/actions/network_actions.py +++ b/coriolis_openstack_utils/actions/network_actions.py @@ -201,13 +201,15 @@ def check_already_done(self): return {"done": True, "result": dest_network['id']} if len(conflicting) == 1: - raise Exception("Found destination network with " - "with name '%s' but different attributes!" - % dest_network_name) + # Network exists but attributes differ - accept it for retry scenarios + LOG.warning("Found destination network '%s' with different attributes, " + "but accepting it for migration continuity.", dest_network_name) + return {"done": True, "result": conflicting[0]['id']} elif conflicting: - raise Exception("Found multiple destination networks with " - "with name '%s'! Aborting subnet " - "migration." % dest_network_name) + # Multiple networks with same name - use the first one + LOG.warning("Found multiple destination networks with name '%s', " + "using the first one.", dest_network_name) + return {"done": True, "result": conflicting[0]['id']} return {"done": False, "result": None} @@ -254,7 +256,8 @@ def execute_operations(self): body = networks.create_network_body( self._source_openstack_client, self.payload['src_network_id'], self.payload['dest_tenant_id'], - self.get_new_network_name(), description) + self.get_new_network_name(), description, + destination_client=self._destination_openstack_client) dest_network_id = networks.create_network( self._destination_openstack_client, body) diff --git a/coriolis_openstack_utils/actions/secgroup_actions.py b/coriolis_openstack_utils/actions/secgroup_actions.py index 6e11ed3..74be522 100644 --- a/coriolis_openstack_utils/actions/secgroup_actions.py +++ b/coriolis_openstack_utils/actions/secgroup_actions.py @@ -144,8 +144,11 @@ def execute_operations(self): raise Exception("Source security group named %s in tenant %s " "not found! " % (src_secgroup_name, src_tenant_id)) elif len(source_secgroup_list) > 1: - raise Exception("Multiple secgroups named %s on source in " - "tenant %s " % (src_secgroup_name, src_tenant_id)) + LOG.warn("Multiple secgroups named %s on source in tenant %s, " + "using the first one (id: %s)", + src_secgroup_name, src_tenant_id, + source_secgroup_list[0]['id']) + source_secgroup = source_secgroup_list[0] else: source_secgroup = source_secgroup_list[0] diff --git a/coriolis_openstack_utils/actions/tenant_actions.py b/coriolis_openstack_utils/actions/tenant_actions.py index 49b95f5..355d862 100644 --- a/coriolis_openstack_utils/actions/tenant_actions.py +++ b/coriolis_openstack_utils/actions/tenant_actions.py @@ -151,7 +151,10 @@ def check_already_done(self): if tenant == tenant_name: LOG.debug("Tenant with name '%s' already exists. Skipping." % ( tenant_name)) - return {"done": True, "result": tenant_name} + # Return the tenant ID, not the name, as dest_tenant_id is used downstream + tenant_id = self._destination_openstack_client.get_project_id( + tenant_name) + return {"done": True, "result": tenant_id} return {"done": False, "result": None} def execute_operations(self): @@ -416,6 +419,16 @@ def execute_operations(self): dest_target_migr_env = {'network_map': dest_net_map, 'security_groups': dest_secgroups} + # Add Coriolis migration-specific parameters if configured + if CONF.destination.migr_network: + dest_target_migr_env['migr_network'] = ( + CONF.destination.migr_network) + if CONF.destination.migr_flavor_name: + dest_target_migr_env['migr_flavor_name'] = ( + CONF.destination.migr_flavor_name) + if CONF.destination.migr_image_map: + dest_target_migr_env['migr_image_map'] = ( + CONF.destination.migr_image_map) instance_transfer_action = None if self.payload['use_replicas']: instance_transfer_action = ( diff --git a/coriolis_openstack_utils/cli/tenant.py b/coriolis_openstack_utils/cli/tenant.py index 7d414c3..b7dcd4f 100644 --- a/coriolis_openstack_utils/cli/tenant.py +++ b/coriolis_openstack_utils/cli/tenant.py @@ -105,7 +105,10 @@ def take_action(self, args): done = tenant_creation_action.check_already_done() tenant = None - if done["done"]: + # If instances are requested, we need to run execute_operations even if + # tenant exists, because instances might not have been migrated yet + has_instances = not args.no_instances + if done["done"] and not has_instances: LOG.info( "Tenant %s Creation seemingly done." % done["result"]) diff --git a/coriolis_openstack_utils/conf.py b/coriolis_openstack_utils/conf.py index c6e74e4..66a3503 100644 --- a/coriolis_openstack_utils/conf.py +++ b/coriolis_openstack_utils/conf.py @@ -28,7 +28,9 @@ conf.StrOpt("project_domain_name", help="Domain name for the project."), conf.BoolOpt("allow_untrusted", default=False, - help="Whether or not skip certificate validation.") + help="Whether or not skip certificate validation."), + conf.StrOpt("endpoint_type", default="public", + help="Endpoint type to use: public, internal, or admin.") ] # Register base Coriolis conf options: @@ -49,10 +51,33 @@ "cinder_database_connection", help="Connection string for Cinder DB.") +# Coriolis backup export options (for Glance-based VMs) +EXPORT_IMAGE_OPT = conf.StrOpt( + "export_image", + help="Name of the Glance image to use for Coriolis export worker VM " + "on the source cloud. Required for 'coriolis_backups' export mechanism.") +EXPORT_NETWORK_OPT = conf.StrOpt( + "export_network", + help="Name of the network on the source cloud for Coriolis export worker VM. " + "Required for 'coriolis_backups' export mechanism.") +EXPORT_FLAVOR_NAME_OPT = conf.StrOpt( + "export_flavor_name", + help="Name of the flavor on the source cloud for Coriolis export worker VM. " + "Required for 'coriolis_backups' export mechanism.") +EXPORT_INTERIM_VOLUME_TYPE_OPT = conf.StrOpt( + "export_interim_volume_type", + help="Name of the Cinder volume type on the source cloud for temporary " + "volumes during export. Required for 'coriolis_backups' export mechanism.") +EXPORT_WORKER_USE_FIP_OPT = conf.BoolOpt( + "export_worker_use_fip", default=False, + help="Whether to use a floating IP for the Coriolis export worker VM.") + # Register source conf options: SOURCE_OPTS = OPENSTACK_CONNECTION_OPTS + [ - REGION_CONFIG_OPT, DB_CONNECTION_OPT, ENDPOINT_NAME_FORMAT_OPT] + REGION_CONFIG_OPT, DB_CONNECTION_OPT, ENDPOINT_NAME_FORMAT_OPT, + EXPORT_IMAGE_OPT, EXPORT_NETWORK_OPT, EXPORT_FLAVOR_NAME_OPT, + EXPORT_INTERIM_VOLUME_TYPE_OPT, EXPORT_WORKER_USE_FIP_OPT] CONF.register_opts( SOURCE_OPTS, constants.SOURCE_OPT_GROUP_NAME) @@ -153,6 +178,23 @@ "replication cannot be handled by Coriolis itself. The port(s) must " "maintain the same MAC address for Coriolis to be able to identify " "and reuse them on the destination.") +MIGR_NETWORK_OPT = conf.StrOpt( + "migr_network", + help="Name of the network on the destination to use for Coriolis " + "migration data transfer. Required for VM migrations.") +MIGR_IMAGE_MAP_OPT = conf.DictOpt( + "migr_image_map", default={}, + help="Mapping of OS types to Glance image names for migration workers. " + "Example: linux: ubuntu, windows: windows-server-2022") +MIGR_FLAVOR_OPT = conf.StrOpt( + "migr_flavor_name", + help="Name of the flavor on the destination to use for Coriolis " + "migration worker VMs. Required for VM migrations.") +DEST_MINION_POOL_ID_OPT = conf.StrOpt( + "destination_minion_pool_id", + help="ID of the destination minion pool to use for migrations. " + "The minion pool must be pre-created in Coriolis with the " + "correct migration image mapping.") # TODO (aznashwan): determine value of adding extra migration opts: @@ -166,7 +208,8 @@ NEW_PHYSICAL_NETWORK_OPT, NEW_ROUTER_NAME_OPT, EXTERNAL_NETWORK_MAP_OPT, NEW_USER_NAME_OPT, NEW_USERS_PASSWORD_OPT, SHUTDOWN_INSTANCES_OPT, NEW_FLAVOR_NAME_OPT, NEW_KEYPAIR_NAME_OPT, COPY_ROUTES_OPT, - CARRY_PORT_INFO_OPT] + CARRY_PORT_INFO_OPT, MIGR_NETWORK_OPT, MIGR_FLAVOR_OPT, MIGR_IMAGE_MAP_OPT, + DEST_MINION_POOL_ID_OPT] CONF.register_opts( DESTINATION_OPTS, constants.DESTINATION_OPT_GROUP_NAME) @@ -185,7 +228,8 @@ def get_conn_info_for_group(group_name): "username": confgroup.username, "password": confgroup.password, "project_name": confgroup.project_name, - "allow_untrusted": confgroup.allow_untrusted + "allow_untrusted": confgroup.allow_untrusted, + "endpoint_type": getattr(confgroup, "endpoint_type", "public") } if int(conn_info["identity_api_version"]) == 3: @@ -237,7 +281,16 @@ def get_coriolis_client(): def get_destination_openstack_environment(): """ Returns the `--destination-env` for migations. """ confgroup = getattr(CONF, constants.DESTINATION_OPT_GROUP_NAME) - return { - "network_map": confgroup.network_map, - "storage_map": confgroup.storage_map + dest_env = { + "network_map": confgroup.network_map } + + # Add required migration worker settings + if confgroup.migr_network: + dest_env["migr_network"] = confgroup.migr_network + if confgroup.migr_flavor_name: + dest_env["migr_flavor_name"] = confgroup.migr_flavor_name + if confgroup.migr_image_map: + dest_env["migr_image_map"] = confgroup.migr_image_map + + return dest_env diff --git a/coriolis_openstack_utils/openstack_client.py b/coriolis_openstack_utils/openstack_client.py index 2dab50d..fdc600e 100644 --- a/coriolis_openstack_utils/openstack_client.py +++ b/coriolis_openstack_utils/openstack_client.py @@ -19,8 +19,8 @@ LOG = logging.getLogger() ALLOW_UNTRUSTED = False -CINDER_API_VERSION = 2 -GLANCE_API_VERSION = 1 +CINDER_API_VERSION = 3 +GLANCE_API_VERSION = 2 NOVA_API_VERSION = 2 NEUTRON_API_VERSION = '2.0' @@ -75,6 +75,7 @@ def __init__(self, connection_info): if connection_info is None: connection_info = {} region_name = connection_info.get("region_name") + endpoint_type = connection_info.get("endpoint_type", "public") self.connection_info = connection_info session = create_keystone_session(connection_info) @@ -82,39 +83,45 @@ def __init__(self, connection_info): identity_api_version = connection_info["identity_api_version"] self.keystone = keystone_client.Client( - version=identity_api_version, session=session) + version=identity_api_version, session=session, + interface=endpoint_type) # Getting latest nova client microversion nova_region_name = connection_info.get( "nova_region_name", region_name) client_nova = nova_client.Client( - NOVA_API_VERSION, session=session, region_name=nova_region_name) + NOVA_API_VERSION, session=session, region_name=nova_region_name, + endpoint_type=endpoint_type) latest_nova_version = client_nova.versions.get_current().version if latest_nova_version: client_nova = nova_client.Client( latest_nova_version, region_name=nova_region_name, - session=client_nova.client.session) + session=client_nova.client.session, + endpoint_type=endpoint_type) self.nova = client_nova neutron_region_name = connection_info.get( "neutron_region_name", region_name) self.neutron = neutron_client.Client( NEUTRON_API_VERSION, session=session, - region_name=neutron_region_name) + region_name=neutron_region_name, + endpoint_type=endpoint_type) glance_version = connection_info.get( "glance_api_version", 2) glance_region_name = connection_info.get( "glance_region_name", region_name) self.glance = glance_client.Client( - glance_version, session=session, region_name=glance_region_name) + glance_version, session=session, region_name=glance_region_name, + interface=endpoint_type) cinder_region_name = connection_info.get( "cinder_region_name", region_name) self.cinder = cinder_client.Client( CINDER_API_VERSION, session=session, - region_name=cinder_region_name) + region_name=cinder_region_name, + endpoint_type=endpoint_type) untrusted_swift = connection_info.get( "allow_untrusted_swift", False) diff --git a/coriolis_openstack_utils/resource_utils/instances.py b/coriolis_openstack_utils/resource_utils/instances.py index ff666ad..c5f7f39 100644 --- a/coriolis_openstack_utils/resource_utils/instances.py +++ b/coriolis_openstack_utils/resource_utils/instances.py @@ -75,6 +75,8 @@ def validate_transfer_options( param instance_info: dict: output from `find_source_instances_by_name` target_env: dict: with "network_map" and "storage_map" """ + from coriolis_openstack_utils import conf + CONF = conf.CONF destination_mapped_networks = set(target_env['network_map'].values()) for network in destination_mapped_networks: @@ -91,6 +93,20 @@ def validate_transfer_options( ValueError("Invalid source network %s" % network) instance_networks = set(instance_info['attached_networks']) + unmapped_networks = instance_networks - source_mapped_networks + + # Auto-map unmapped networks using the destination network name format + # This handles tenant networks that are migrated with the same/similar name + if unmapped_networks: + network_name_format = CONF.destination.new_network_name_format + for network in unmapped_networks: + dest_network_name = network_name_format % {'original': network} + LOG.info("Auto-mapping unmapped network '%s' -> '%s'", + network, dest_network_name) + target_env['network_map'][network] = dest_network_name + # Update source_mapped_networks after auto-mapping + source_mapped_networks = set(target_env['network_map'].keys()) + if not instance_networks.issubset(source_mapped_networks): raise ValueError("%s instance networks are not mapped." % ( instance_networks - source_mapped_networks)) @@ -99,7 +115,17 @@ def validate_transfer_options( try: destination_client.neutron.find_resource('network', dest_net) except NotFound: - raise ValueError("Inexistent destination network %s" % dest_net) + # Network might be created as part of tenant migration + LOG.warning("Destination network '%s' not found yet, " + "assuming it will be created during tenant migration", + dest_net) + except Exception as e: + # Handle multiple networks with same name (NeutronClientNoUniqueMatch) + if 'Multiple' in str(e) and 'matches found' in str(e): + LOG.warning("Multiple networks named '%s' found on destination, " + "Coriolis will select one", dest_net) + else: + raise source_volume_types = set([ el.name for el in source_client.cinder.volume_types.findall()]) @@ -199,8 +225,8 @@ def get_instances_assessment(source_client, instances_names): def get_migration_assessment(source_client, coriolis, migration_id): - - migration = coriolis.migrations.get(migration_id) + # Updated to use new transfers API + migration = coriolis.transfers.get(migration_id) creation_timestamp = migration.tasks[0].updated_at finish_timestamp = migration.tasks[-1].updated_at @@ -217,11 +243,12 @@ def get_migration_assessment(source_client, coriolis, migration_id): for assessment in assessment_list: assessment["migration"] = {} assessment["migration"]["migration_id"] = migration.id - assessment["migration"]["migration_status"] = migration.status + assessment["migration"]["migration_status"] = getattr( + migration, 'status', getattr(migration, 'last_execution_status', 'UNKNOWN')) assessment["migration"]["migration_time"] = str(interval_date) previous_migration_ids = [] - for migr_thin in coriolis.migrations.list(): - migr = coriolis.migrations.get(migr_thin.id) + for migr_thin in coriolis.transfers.list(): + migr = coriolis.transfers.get(migr_thin.id) prev_creation_timestamp = migr.tasks[0].updated_at prev_creation_date = datetime.datetime.strptime( prev_creation_timestamp, '%Y-%m-%dT%H:%M:%S.%f') diff --git a/coriolis_openstack_utils/resource_utils/networks.py b/coriolis_openstack_utils/resource_utils/networks.py index 3735dcb..b6dfb07 100644 --- a/coriolis_openstack_utils/resource_utils/networks.py +++ b/coriolis_openstack_utils/resource_utils/networks.py @@ -35,7 +35,7 @@ def create_network(openstack_client, body): return network_id -def get_body(openstack_client, network_id): +def get_body(openstack_client, network_id, destination_client=None): src_network = get_network(openstack_client, network_id) relevant_keys = set([ 'admin_state_up', 'dns_domain', 'port_security_enabled', @@ -54,15 +54,34 @@ def get_body(openstack_client, network_id): body['provider:network_type'] = physical_network_map.get( src_network_type) - body['availability_zone_hints'] = src_network['availability_zones'] + # Only copy availability zone hints if they exist on destination + src_azs = src_network.get('availability_zones', []) + if src_azs and destination_client: + try: + dest_azs = destination_client.neutron.list_availability_zones() + dest_az_names = set( + az['name'] for az in dest_azs.get('availability_zones', []) + if az.get('resource') == 'network') + # Only include AZs that exist on destination + valid_azs = [az for az in src_azs if az in dest_az_names] + if valid_azs: + body['availability_zone_hints'] = valid_azs + elif src_azs: + LOG.warn("Source network availability zones %s not found on " + "destination, skipping availability_zone_hints", src_azs) + except Exception as e: + LOG.warn("Could not check destination availability zones: %s", e) + elif src_azs: + # Fallback: include AZs but may fail if they don't exist + body['availability_zone_hints'] = src_azs return body def create_network_body(openstack_client, src_network_id, dest_tenant_id, - dest_network_name, description): + dest_network_name, description, destination_client=None): - src_body = get_body(openstack_client, src_network_id) + src_body = get_body(openstack_client, src_network_id, destination_client) body = {'name': dest_network_name, 'tenant_id': dest_tenant_id, 'project_id': dest_tenant_id, @@ -78,9 +97,10 @@ def create_network_body(openstack_client, src_network_id, dest_tenant_id, def check_network_similarity( src_network, dest_network, source_client, destination_client): + # Note: 'mtu' and 'dns_domain' removed as they can differ between platforms relevant_keys = set([ - 'admin_state_up', 'dns_domain', 'mtu', - 'port_security_enabled', 'provider:physical_network' + 'admin_state_up', + 'port_security_enabled', 'provider:physical_network', 'provider:network_type', 'router:external', 'shared', 'vlan_transparent', 'is_default', 'availability_zones', 'subnets']) src_availability_zones = set(src_network.get('availability_zones')) @@ -113,7 +133,7 @@ def check_network_similarity( subnet_id in src_network['subnets']] dest_subnets = [subnets.get_subnet(destination_client, subnet_id) for - subnet_id in src_network['subnets']] + subnet_id in dest_network['subnets']] similar_subnets = [] for src_subnet in src_subnets: diff --git a/coriolis_openstack_utils/resource_utils/security_groups.py b/coriolis_openstack_utils/resource_utils/security_groups.py index 77e2db4..9bf0494 100644 --- a/coriolis_openstack_utils/resource_utils/security_groups.py +++ b/coriolis_openstack_utils/resource_utils/security_groups.py @@ -17,8 +17,17 @@ def list_security_groups(openstack_client, tenant_id, filters=None): def get_security_group(openstack_client, tenant_id, name): - return openstack_client.neutron.find_resource( - 'security_group', name, project_id=tenant_id) + # Use list_security_groups to handle duplicate names gracefully + secgroups = list_security_groups( + openstack_client, tenant_id, filters={'name': name}) + if not secgroups: + raise Exception( + "Security group '%s' not found in tenant '%s'" % (name, tenant_id)) + if len(secgroups) > 1: + LOG.warn("Multiple security groups named '%s' found in tenant '%s', " + "using the first one (id: %s)", name, tenant_id, + secgroups[0]['id']) + return secgroups[0] def create_security_group(openstack_client, tenant_id, body): @@ -75,5 +84,9 @@ def check_rule_similarity(source_rule, destination_rule): def delete_secgroup(openstack_client, tenant_id, name): - secgroup = get_security_group(openstack_client, tenant_id, name) - openstack_client.neutron.delete_security_group(secgroup['id']) + try: + secgroup = get_security_group(openstack_client, tenant_id, name) + openstack_client.neutron.delete_security_group(secgroup['id']) + except Exception as e: + LOG.warn("Could not delete security group '%s' in tenant '%s': %s", + name, tenant_id, e) diff --git a/requirements.txt b/requirements.txt index 4e2422f..730a653 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ cliff oslo.config oslo.log pbr -git+https://github.com/cloudbase/python-coriolisclient.git@master#egg=coriolis_openstack_utils +git+https://github.com/cloudbase/python-coriolisclient.git@master#egg=python-coriolisclient python-cinderclient python-glanceclient python-keystoneclient