diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index ca9adc4bac..af9d4be1f8 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -4334,3 +4334,129 @@ class ConsoleManagerTests(helpers.TestCase): def test_invalid_console_type_raise_value_error(self): self.assertRaises(exceptions.NotAvailable, console.get_console, None, 'FAKE', None) + + @helpers.update_settings( + OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'}) + @helpers.create_stubs({api.glance: ('image_list_detailed',), + api.neutron: ('network_list', + 'profile_list', + 'port_create', + 'port_delete'), + api.nova: ('extension_supported', + 'flavor_list', + 'keypair_list', + 'availability_zone_list', + 'server_create',), + api.network: ('security_group_list',), + cinder: ('volume_list', + 'volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) + def test_port_cleanup_called_on_failed_vm_launch(self): + flavor = self.flavors.first() + image = self.images.first() + keypair = self.keypairs.first() + server = self.servers.first() + sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() + customization_script = 'user data' + quota_usages = self.quota_usages.first() + + api.nova.extension_supported('BlockDeviceMappingV2Boot', + IsA(http.HttpRequest)) \ + .AndReturn(True) + volumes = [v for v in self.volumes.list() if (v.status == AVAILABLE + and v.bootable == + 'true')] + cinder.volume_list(IsA(http.HttpRequest), + search_opts=VOLUME_SEARCH_OPTS) \ + .AndReturn(volumes) + volumes = [v for v in self.volumes.list() if (v.status == AVAILABLE)] + cinder.volume_snapshot_list(IsA(http.HttpRequest), + search_opts=SNAPSHOT_SEARCH_OPTS) \ + .AndReturn(volumes) + api.nova.flavor_list(IgnoreArg()).AndReturn(self.flavors.list()) + api.nova.keypair_list(IgnoreArg()).AndReturn(self.keypairs.list()) + api.network.security_group_list(IsA(http.HttpRequest)) \ + .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) + api.glance.image_list_detailed( + IsA(http.HttpRequest), + filters={'is_public': True, 'status': 'active'}) \ + .AndReturn([self.images.list(), False, False]) + api.glance.image_list_detailed( + IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False, False]) + api.neutron.network_list(IsA(http.HttpRequest), + tenant_id=self.tenant.id, + shared=False) \ + .AndReturn(self.networks.list()[:1]) + api.neutron.network_list(IsA(http.HttpRequest), + shared=True) \ + .AndReturn(self.networks.list()[1:]) + policy_profiles = self.policy_profiles.list() + policy_profile_id = self.policy_profiles.first().id + port = self.ports.first() + api.neutron.profile_list( + IsA(http.HttpRequest), 'policy').AndReturn(policy_profiles) + api.neutron.port_create( + IsA(http.HttpRequest), + self.networks.first().id, + policy_profile_id=policy_profile_id).AndReturn(port) + nics = [{"port-id": port.id}] + api.nova.extension_supported('DiskConfig', + IsA(http.HttpRequest)) \ + .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) + api.nova.server_create(IsA(http.HttpRequest), + server.name, + image.id, + flavor.id, + keypair.name, + customization_script, + [sec_group.name], + block_device_mapping=None, + block_device_mapping_v2=None, + nics=nics, + availability_zone=avail_zone.zoneName, + instance_count=IsA(int), + admin_pass='password', + disk_config='AUTO', + config_drive=False) \ + .AndRaise(self.exceptions.neutron) + api.neutron.port_delete(IsA(http.HttpRequest), port.id) + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ + .AndReturn(quota_usages) + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(self.flavors.list()) + + self.mox.ReplayAll() + + form_data = {'flavor': flavor.id, + 'source_type': 'image_id', + 'source_id': image.id, + 'volume_size': '1', + 'image_id': image.id, + 'availability_zone': avail_zone.zoneName, + 'keypair': keypair.name, + 'name': server.name, + 'script_source': 'raw', + 'script_data': customization_script, + 'project_id': self.tenants.first().id, + 'user_id': self.user.id, + 'groups': sec_group.name, + 'volume_type': '', + 'network': self.networks.first().id, + 'count': 1, + 'admin_pass': 'password', + 'confirm_admin_pass': 'password', + 'disk_config': 'AUTO', + 'config_drive': False, + 'profile': self.policy_profiles.first().id} + url = reverse('horizon:project:instances:launch') + res = self.client.post(url, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index ddea8a57ef..fc3f23ec8d 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -879,7 +879,9 @@ class LaunchInstance(workflows.Workflow): avail_zone = context.get('availability_zone', None) - if api.neutron.is_port_profiles_supported(): + port_profiles_supported = api.neutron.is_port_profiles_supported() + + if port_profiles_supported: nics = self.set_network_port_profiles(request, context['network_id'], context['profile_id']) @@ -902,8 +904,16 @@ class LaunchInstance(workflows.Workflow): config_drive=context.get('config_drive')) return True except Exception: + if port_profiles_supported: + ports_failing_deletes = _cleanup_ports_on_failed_vm_launch( + request, nics) + if ports_failing_deletes: + ports_str = ', '.join(ports_failing_deletes) + msg = ('Port cleanup failed for these port-ids (%s).' + % ports_str) + LOG.exception(msg) exceptions.handle(request) - return False + return False def set_network_port_profiles(self, request, net_ids, profile_id): # Create port with Network ID and Port Profile @@ -941,3 +951,15 @@ class LaunchInstance(workflows.Workflow): 'profile_id': profile_id}) return nics + + +def _cleanup_ports_on_failed_vm_launch(request, nics): + ports_failing_deletes = [] + LOG.debug('Cleaning up stale VM ports.') + for nic in nics: + try: + LOG.debug('Deleting port with id: %s' % nic['port-id']) + api.neutron.port_delete(request, nic['port-id']) + except Exception: + ports_failing_deletes.append(nic['port-id']) + return ports_failing_deletes