diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst index ca029f6fe3..f6fefd692f 100644 --- a/doc/source/configuration/settings.rst +++ b/doc/source/configuration/settings.rst @@ -2262,47 +2262,6 @@ specified in this setting is not found in the availability zone list, the setting will be ignored and the behavior will be same as when ``Any`` is specified. -LAUNCH_INSTANCE_LEGACY_ENABLED -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 8.0.0(Liberty) - -.. versionchanged:: 9.0.0(Mitaka) - - The default value for this setting has been changed to ``False`` - -.. deprecated:: 19.1.0(Wallaby) - - The Python Launch Instance workflow is deprecated. - Consider switching to the AngujarJS workflow instead. - -Default: ``False`` - -This setting enables the Python Launch Instance workflow. - -.. note:: - - It is possible to run both the AngularJS and Python workflows simultaneously, - so the other may be need to be toggled with `LAUNCH_INSTANCE_NG_ENABLED`_ - -LAUNCH_INSTANCE_NG_ENABLED -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 8.0.0(Liberty) - -.. versionchanged:: 9.0.0(Mitaka) - - The default value for this setting has been changed to ``True`` - -Default: ``True`` - -This setting enables the AngularJS Launch Instance workflow. - -.. note:: - - It is possible to run both the AngularJS and Python workflows simultaneously, - so the other may be need to be toggled with `LAUNCH_INSTANCE_LEGACY_ENABLED`_ - OPENSTACK_ENABLE_PASSWORD_RETRIEVE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/openstack_dashboard/dashboards/project/images/images/tables.py b/openstack_dashboard/dashboards/project/images/images/tables.py index 21bea9a19c..c2c9022b09 100644 --- a/openstack_dashboard/dashboards/project/images/images/tables.py +++ b/openstack_dashboard/dashboards/project/images/images/tables.py @@ -346,11 +346,7 @@ class ImagesTable(tables.DataTable): status_columns = ["status"] verbose_name = _("Images") table_actions = (OwnerFilter, CreateImage, DeleteImage,) - launch_actions = () - if settings.LAUNCH_INSTANCE_LEGACY_ENABLED: - launch_actions = (LaunchImage,) + launch_actions - if settings.LAUNCH_INSTANCE_NG_ENABLED: - launch_actions = (LaunchImageNG,) + launch_actions + launch_actions = (LaunchImageNG,) row_actions = launch_actions + (CreateVolumeFromImage, EditImage, UpdateMetadata, DeleteImage,) diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 674ebdca28..4351e1c51d 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -413,14 +413,14 @@ class ToggleShelve(tables.BatchAction): self.current_past_action = SHELVE -class LaunchLink(tables.LinkAction): - name = "launch" +class LaunchLinkNG(tables.LinkAction): + name = "launch-ng" verbose_name = _("Launch Instance") - url = "horizon:project:instances:launch" - classes = ("ajax-modal", "btn-launch") + url = "horizon:project:instances:index" + ajax = False + classes = ("btn-launch", ) icon = "cloud-upload" policy_rules = (("compute", "os_compute_api:servers:create"),) - ajax = True def __init__(self, attrs=None, **kwargs): kwargs['preempt'] = True @@ -458,13 +458,6 @@ class LaunchLink(tables.LinkAction): self.allowed(request, None) return HttpResponse(self.render(is_table_action=True)) - -class LaunchLinkNG(LaunchLink): - name = "launch-ng" - url = "horizon:project:instances:index" - ajax = False - classes = ("btn-launch", ) - def get_default_attrs(self): url = urls.reverse(self.url) ngclick = "modal.openLaunchInstanceWizard(" \ @@ -1295,11 +1288,7 @@ class InstancesTable(tables.DataTable): status_columns = ["status", "task"] row_class = UpdateRow table_actions_menu = (StartInstance, StopInstance, SoftRebootInstance) - launch_actions = () - if settings.LAUNCH_INSTANCE_LEGACY_ENABLED: - launch_actions = (LaunchLink,) + launch_actions - if settings.LAUNCH_INSTANCE_NG_ENABLED: - launch_actions = (LaunchLinkNG,) + launch_actions + launch_actions = (LaunchLinkNG,) table_actions = launch_actions + (DeleteInstance, InstancesFilterAction) row_actions = (StartInstance, ConfirmResize, RevertResize, diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html deleted file mode 100644 index a1a83b8cd5..0000000000 --- a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html +++ /dev/null @@ -1,3 +0,0 @@ -{% load i18n %} -

{% blocktrans %}You can customize your instance after it has launched using the options available here.{% endblocktrans %}

-

{% blocktrans %}"Customization Script" is analogous to "User Data" in other systems.{% endblocktrans %}

diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 69cf67e407..075beea34a 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -19,11 +19,9 @@ import collections import json import logging -import sys from unittest import mock from django.conf import settings -from django.forms import widgets from django import http import django.test from django.test.utils import override_settings @@ -35,13 +33,11 @@ from horizon import exceptions from horizon import forms from horizon.workflows import views from openstack_dashboard import api -from openstack_dashboard.api import cinder from openstack_dashboard.dashboards.project.instances import console from openstack_dashboard.dashboards.project.instances import tables from openstack_dashboard.dashboards.project.instances import tabs from openstack_dashboard.dashboards.project.instances import workflows from openstack_dashboard.test import helpers -from openstack_dashboard.usage import quotas from openstack_dashboard.views import get_url_with_pagination @@ -249,7 +245,6 @@ class InstanceTableTests(InstanceTestBase, InstanceTableTestMixin): mock.call(helpers.IsHttpRequest())) def test_index(self): - res = self._get_index() self.assertTemplateUsed(res, INDEX_TEMPLATE) @@ -2055,1948 +2050,6 @@ class InstanceTests(InstanceTestBase): ) -class InstanceLaunchInstanceTests(InstanceTestBase, - InstanceTableTestMixin): - - @helpers.create_mocks({api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_snapshot_list', - 'volume_list',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.glance: ('image_list_detailed',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_get(self, - expect_password_fields=True, - custom_flavor_sort=None, - only_one_network=False, - config_drive_default=False): - image = self.versioned_images.first() - - self.mock_volume_list.return_value = [] - self.mock_volume_snapshot_list.return_value = [] - self._mock_glance_image_list_detailed(self.versioned_images.list()) - self.mock_network_list.side_effect = [ - self.networks.list()[:1], - [] if only_one_network else self.networks.list()[1:], - self.networks.list()[:1], - self.networks.list()[1:], - ] - self.mock_port_list_with_trunk_types.return_value = self.ports.list() - self.mock_server_group_list.return_value = self.server_groups.list() - self.mock_tenant_quota_usages.return_value = self.quota_usages.first() - self._mock_nova_lists() - - url = reverse('horizon:project:instances:launch') - params = urlencode({"source_type": "image_id", - "source_id": image.id}) - res = self.client.get("%s?%s" % (url, params)) - - workflow = res.context['workflow'] - self.assertTemplateUsed(res, views.WorkflowView.template_name) - self.assertEqual(res.context['workflow'].name, - workflows.LaunchInstance.name) - step = workflow.get_step("setinstancedetailsaction") - self.assertEqual(step.action.initial['image_id'], image.id) - self.assertQuerysetEqual( - workflow.steps, - ['', - '', - '', - '', - '', - '']) - - if custom_flavor_sort == 'id': - # Reverse sorted by id - sorted_flavors = ( - ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'), - ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'), - ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'), - ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'), - ) - elif custom_flavor_sort == 'name': - sorted_flavors = ( - ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'), - ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'), - ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'), - ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'), - ) - elif custom_flavor_sort == helpers.my_custom_sort: - sorted_flavors = ( - ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'), - ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'), - ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'), - ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'), - ) - else: - # Default - sorted by RAM - sorted_flavors = ( - ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'), - ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'), - ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'), - ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'), - ) - - select_options = ''.join([ - '' % (f[0], f[1]) - for f in sorted_flavors - ]) - self.assertContains(res, select_options) - - password_field_label = 'Admin Pass' - if expect_password_fields: - self.assertContains(res, password_field_label) - else: - self.assertNotContains(res, password_field_label) - - boot_from_image_field_label = 'Boot from image (creates a new volume)' - self.assertContains(res, boot_from_image_field_label) - - # NOTE(adriant): Django 1.11 changes the checked syntax to use html5 - # "checked" rather than XHTML's "checked='checked'". - checked_box = ( - '' - ) - if only_one_network: - self.assertContains(res, checked_box, html=True) - else: - self.assertNotContains(res, checked_box, html=True) - - self.assertContains(res, 'Disk Partition') - self.assertContains(res, 'Configuration Drive') - - step = workflow.get_step("setadvancedaction") - self.assertEqual(step.action.initial['config_drive'], - config_drive_default) - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - - self._check_glance_image_list_detailed(count=8) - - self.mock_network_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - tenant_id=self.tenant.id, shared=False), - mock.call(helpers.IsHttpRequest(), shared=True), - mock.call(helpers.IsHttpRequest(), - tenant_id=self.tenant.id, shared=False), - mock.call(helpers.IsHttpRequest(), shared=True), - ]) - self.assertEqual(4, self.mock_network_list.call_count) - self.mock_port_list_with_trunk_types.assert_has_calls( - [mock.call(helpers.IsHttpRequest(), - network_id=net.id, tenant_id=self.tenant.id) - for net in self.networks.list()]) - self.assertEqual(len(self.networks.list()), - self.mock_port_list_with_trunk_types.call_count) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')) - self._check_nova_lists(flavor_count=2) - - @helpers.update_settings( - OPENSTACK_HYPERVISOR_FEATURES={'can_set_password': False}) - def test_launch_instance_get_without_password(self): - self.test_launch_instance_get(expect_password_fields=False) - - @helpers.update_settings( - OPENSTACK_HYPERVISOR_FEATURES={'requires_keypair': True}) - def test_launch_instance_required_key(self): - flavor = self.flavors.first() - image = self.images.first() - image.min_ram = flavor.ram - image.min_disk = flavor.disk - res = self._launch_form_instance(image, flavor, keypair=None) - msg = "This field is required" - self.assertContains(res, msg) - - @django.test.utils.override_settings( - LAUNCH_INSTANCE_DEFAULTS={'config_drive': True}) - def test_launch_instance_get_with_config_drive_default(self): - self.test_launch_instance_get(config_drive_default=True) - - @django.test.utils.override_settings( - CREATE_INSTANCE_FLAVOR_SORT={ - 'key': 'id', - 'reverse': True, - }) - def test_launch_instance_get_custom_flavor_sort_by_id(self): - self.test_launch_instance_get(custom_flavor_sort='id') - - @django.test.utils.override_settings( - CREATE_INSTANCE_FLAVOR_SORT={ - 'key': 'name', - 'reverse': False, - }) - def test_launch_instance_get_custom_flavor_sort_by_name(self): - self.test_launch_instance_get(custom_flavor_sort='name') - - @django.test.utils.override_settings( - CREATE_INSTANCE_FLAVOR_SORT={ - 'key': helpers.my_custom_sort, - 'reverse': False, - }) - def test_launch_instance_get_custom_flavor_sort_by_callable(self): - self.test_launch_instance_get( - custom_flavor_sort=helpers.my_custom_sort) - - @django.test.utils.override_settings( - CREATE_INSTANCE_FLAVOR_SORT={ - 'key': 'no_such_column', - 'reverse': False, - }) - def test_launch_instance_get_custom_flavor_sort_by_missing_column(self): - self.test_launch_instance_get(custom_flavor_sort='no_such_column') - - def test_launch_instance_get_with_only_one_network(self): - self.test_launch_instance_get(only_one_network=True) - - @helpers.create_mocks({api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_snapshot_list', - 'volume_list',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.glance: ('image_list_detailed',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_get_images_snapshots(self, - block_device_mapping_v2=True, - only_one_network=False, - disk_config=True, - config_drive=True): - self.mock_volume_list.return_value = [] - self.mock_volume_snapshot_list.return_value = [] - self._mock_glance_image_list_detailed(self.versioned_images.list() + - self.versioned_snapshots.list()) - self.mock_network_list.side_effect = [ - self.networks.list()[:1], - [] if only_one_network else self.networks.list()[1:], - self.networks.list()[:1], - self.networks.list()[1:], - ] - self.mock_port_list_with_trunk_types.return_value = self.ports.list() - self.mock_server_group_list.return_value = self.server_groups.list() - self.mock_tenant_quota_usages.return_value = self.limits['absolute'] - self._mock_nova_lists() - - url = reverse('horizon:project:instances:launch') - res = self.client.get(url) - - image_sources = (res.context_data['workflow'].steps[0]. - action.fields['image_id'].choices) - - snapshot_sources = (res.context_data['workflow'].steps[0]. - action.fields['instance_snapshot_id'].choices) - - images = [image.id for image in self.versioned_images.list()] - snapshots = [s.id for s in self.versioned_snapshots.list()] - - image_sources_ids = [] - snapshot_sources_ids = [] - for image in image_sources: - self.assertTrue(image[0] in images or image[0] == '') - if image[0] != '': - image_sources_ids.append(image[0]) - - for image in images: - self.assertIn(image, image_sources_ids) - - for snapshot in snapshot_sources: - self.assertTrue(snapshot[0] in snapshots or snapshot[0] == '') - if snapshot[0] != '': - snapshot_sources_ids.append(snapshot[0]) - - for snapshot in snapshots: - self.assertIn(snapshot, snapshot_sources_ids) - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self._check_glance_image_list_detailed(count=8) - self.mock_network_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - tenant_id=self.tenant.id, shared=False), - mock.call(helpers.IsHttpRequest(), shared=True), - mock.call(helpers.IsHttpRequest(), - tenant_id=self.tenant.id, shared=False), - mock.call(helpers.IsHttpRequest(), shared=True), - ]) - self.assertEqual(4, self.mock_network_list.call_count) - self.mock_port_list_with_trunk_types.assert_has_calls( - [mock.call(helpers.IsHttpRequest(), - network_id=net.id, tenant_id=self.tenant.id) - for net in self.networks.list()]) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')) - self._check_nova_lists(flavor_count=2) - - @helpers.create_mocks({api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_snapshot_list', - 'volume_list',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.glance: ('image_list_detailed',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_get_bootable_volumes(self, - block_device_mapping_v2=True, - only_one_network=False, - disk_config=True, - config_drive=True): - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self._mock_glance_image_list_detailed(self.versioned_images.list()) - self.mock_network_list.side_effect = [ - self.networks.list()[:1], - [] if only_one_network else self.networks.list()[1:], - self.networks.list()[:1], - self.networks.list()[1:], - ] - self.mock_port_list_with_trunk_types.return_value = self.ports.list() - self.mock_server_group_list.return_value = self.server_groups.list() - self.mock_tenant_quota_usages.return_value = self.quota_usages.first() - self._mock_nova_lists() - - url = reverse('horizon:project:instances:launch') - res = self.client.get(url) - - bootable_volumes = [v.id for v in self.cinder_volumes.list() - if (v.bootable == 'true' and - v.status == 'available')] - - volume_sources = (res.context_data['workflow'].steps[0]. - action.fields['volume_id'].choices) - - volume_sources_ids = [] - for volume in volume_sources: - self.assertTrue(volume[0].split(":vol")[0] in bootable_volumes or - volume[0] == '') - if volume[0] != '': - volume_sources_ids.append(volume[0].split(":vol")[0]) - - for volume in bootable_volumes: - self.assertIn(volume, volume_sources_ids) - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self._check_glance_image_list_detailed(count=8) - self.mock_network_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - tenant_id=self.tenant.id, shared=False), - mock.call(helpers.IsHttpRequest(), shared=True), - mock.call(helpers.IsHttpRequest(), - tenant_id=self.tenant.id, shared=False), - mock.call(helpers.IsHttpRequest(), shared=True), - ]) - self.assertEqual(4, self.mock_network_list.call_count) - self.mock_port_list_with_trunk_types.assert_has_calls( - [mock.call(helpers.IsHttpRequest(), - network_id=net.id, tenant_id=self.tenant.id) - for net in self.networks.list()]) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')) - self._check_nova_lists(flavor_count=2) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_create', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list', - 'server_create',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_post(self): - flavor = self.flavors.first() - image = self.versioned_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' - nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] - quota_usages = self.quota_usages.first() - scheduler_hints = {"group": self.server_groups.first().id} - - self._mock_nova_glance_neutron_lists() - - self.mock_server_group_list.return_value = self.server_groups.list() - self.mock_volume_list.return_value = [] - self.mock_volume_snapshot_list.return_value = [] - self.mock_server_create.return_value = None - self.mock_tenant_quota_usages.return_value = quota_usages - self.mock_flavor_list.return_value = self.flavors.list() - - form_data = {'flavor': flavor.id, - 'source_type': 'image_id', - 'image_id': image.id, - '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': str(sec_group.id), - 'availability_zone': avail_zone.zoneName, - 'volume_type': '', - 'network': self.networks.first().id, - 'count': 1, - 'server_group': self.server_groups.first().id, - 'disk_config': 'AUTO', - 'config_drive': True, - } - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertNoFormErrors(res) - self.assertRedirectsNoFollow(res, INDEX_URL) - - self._check_nova_glance_neutron_lists(flavor_count=2, image_count=8) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.mock_server_create.assert_called_once_with( - helpers.IsHttpRequest(), - server.name, - image.id, - flavor.id, - keypair.name, - customization_script, - [str(sec_group.id)], - block_device_mapping=None, - block_device_mapping_v2=None, - nics=nics, - availability_zone=avail_zone.zoneName, - instance_count=helpers.IsA(int), - admin_pass='', - disk_config='AUTO', - config_drive=True, - scheduler_hints=scheduler_hints) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_create', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list', - 'server_create',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_post_boot_from_volume(self): - flavor = self.flavors.first() - keypair = self.keypairs.first() - server = self.servers.first() - volume = self.cinder_volumes.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - volume_choice = "%s:vol" % volume.id - - volume_source_id = volume.id.split(':')[0] - block_device_mapping = None - block_device_mapping_2 = [ - {'device_name': 'vda', - 'source_type': 'volume', - 'destination_type': 'volume', - 'delete_on_termination': False, - 'uuid': volume_source_id, - 'boot_index': '0', - 'volume_size': 1 - } - ] - - nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] - quota_usages = self.quota_usages.first() - - self._mock_nova_glance_neutron_lists() - - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_server_create.return_value = None - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'volume_id', - 'source_id': volume_choice, - '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': str(sec_group.id), - 'availability_zone': avail_zone.zoneName, - 'volume_size': '1', - 'volume_id': volume_choice, - 'device_name': device_name, - 'network': self.networks.first().id, - 'count': 1, - 'disk_config': 'AUTO', - 'config_drive': True} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertNoFormErrors(res) - self.assertRedirectsNoFollow(res, INDEX_URL) - - self._check_nova_glance_neutron_lists(flavor_count=2, image_count=6) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - - self.mock_server_create.assert_called_once_with( - helpers.IsHttpRequest(), - server.name, - '', - flavor.id, - keypair.name, - customization_script, - [str(sec_group.id)], - block_device_mapping=block_device_mapping, - block_device_mapping_v2=block_device_mapping_2, - nics=nics, - availability_zone=avail_zone.zoneName, - instance_count=helpers.IsA(int), - admin_pass='', - disk_config='AUTO', - config_drive=True, - scheduler_hints={}) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_create', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('server_create', - 'is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_post_no_images_available_boot_from_volume(self): - flavor = self.flavors.first() - keypair = self.keypairs.first() - server = self.servers.first() - volume = self.cinder_volumes.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - volume_choice = "%s:vol" % volume.id - block_device_mapping = [ - {'device_name': device_name, - 'source_type': 'volume', - 'destination_type': 'volume', - 'delete_on_termination': False, - 'uuid': volume.id, - 'boot_index': '0', - 'volume_size': None, - } - ] - nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] - quota_usages = self.quota_usages.first() - - self._mock_nova_glance_neutron_lists() - - self.mock_flavor_list.return_value = self.flavors.list() - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_tenant_quota_usages.return_value = quota_usages - self.mock_server_create.return_value = None - - form_data = {'flavor': flavor.id, - 'source_type': 'volume_id', - # 'image_id': '', - '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': str(sec_group.id), - 'availability_zone': avail_zone.zoneName, - 'network': self.networks.first().id, - 'volume_type': 'volume_id', - 'volume_id': volume_choice, - 'device_name': device_name, - 'count': 1, - 'disk_config': 'MANUAL', - 'config_drive': True} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertNoFormErrors(res) - self.assertRedirectsNoFollow(res, INDEX_URL) - - self._check_nova_glance_neutron_lists(flavor_count=2, - image_count=6) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )) - - self.mock_server_create.assert_called_once_with( - helpers.IsHttpRequest(), - server.name, - '', - flavor.id, - keypair.name, - customization_script, - [str(sec_group.id)], - block_device_mapping=None, - block_device_mapping_v2=block_device_mapping, - nics=nics, - availability_zone=avail_zone.zoneName, - instance_count=helpers.IsA(int), - admin_pass='', - disk_config='MANUAL', - config_drive=True, - scheduler_hints={}) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list'), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_post_no_images_available(self): - flavor = self.flavors.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() - - self.mock_tenant_quota_usages.return_value = self.quota_usages.first() - self._mock_glance_image_list_detailed([]) - self._mock_neutron_network_and_port_list() - self._mock_nova_lists() - self.mock_volume_list.return_value = [] - self.mock_volume_snapshot_list.return_value = [] - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'image_id', - 'image_id': '', - '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': str(sec_group.id), - 'availability_zone': avail_zone.zoneName, - 'volume_type': '', - 'count': 1} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertFormErrors(res, 1, "You must select an image.") - self.assertTemplateUsed(res, views.WorkflowView.template_name) - - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - self._check_glance_image_list_detailed(count=8) - self._check_neutron_network_and_port_list() - self._check_nova_lists(flavor_count=3) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - @helpers.create_mocks({ - api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_create', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list', - 'server_create',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_post_boot_from_snapshot(self): - flavor = self.flavors.first() - keypair = self.keypairs.first() - server = self.servers.first() - snapshot = self.cinder_volume_snapshots.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - snapshot_choice = "%s:snap" % snapshot.id - - snapshot_source_id = snapshot.id.split(':')[0] - block_device_mapping = None - block_device_mapping_2 = [ - {'device_name': 'vda', - 'source_type': 'snapshot', - 'destination_type': 'volume', - 'delete_on_termination': 0, - 'uuid': snapshot_source_id, - 'boot_index': '0', - 'volume_size': 1 - } - ] - - nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] - quota_usages = self.quota_usages.first() - - self._mock_nova_glance_neutron_lists() - - volumes = [v for v in self.cinder_volumes.list() - if (getattr(v, 'bootable', 'false') == 'true')] - snapshots = [v for v in self.cinder_volume_snapshots.list() - if (v.status == AVAILABLE)] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = snapshots - self.mock_server_create.return_value = None - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'volume_snapshot_id', - 'source_id': snapshot_choice, - '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': str(sec_group.id), - 'availability_zone': avail_zone.zoneName, - 'volume_size': '1', - 'volume_snapshot_id': snapshot_choice, - 'device_name': device_name, - 'network': self.networks.first().id, - 'count': 1, - 'disk_config': 'AUTO', - 'config_drive': True} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertNoFormErrors(res) - self.assertRedirectsNoFollow(res, INDEX_URL) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - - self.mock_server_create.assert_called_once_with( - helpers.IsHttpRequest(), - server.name, - '', - flavor.id, - keypair.name, - customization_script, - [str(sec_group.id)], - block_device_mapping=block_device_mapping, - block_device_mapping_v2=block_device_mapping_2, - nics=nics, - availability_zone=avail_zone.zoneName, - instance_count=helpers.IsA(int), - admin_pass='', - disk_config='AUTO', - config_drive=True, - scheduler_hints={}) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )) - - @helpers.create_mocks({ - api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_create', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list', - 'server_create'), - cinder: ('volume_list', - 'volume_snapshot_list'), - quotas: ('tenant_quota_usages',)}) - def test_launch_instance_post_boot_from_snapshot_error(self): - flavor = self.flavors.first() - keypair = self.keypairs.first() - server = self.servers.first() - avail_zone = self.availability_zones.first() - quota_usages = self.quota_usages.first() - - self.mock_image_list_detailed.return_value = [[], False, False] - self.mock_tenant_quota_usages.return_value = quota_usages - self._mock_neutron_network_and_port_list() - - bad_snapshot_id = 'a-bogus-id' - - form_data = {'flavor': flavor.id, - 'source_type': 'instance_snapshot_id', - 'instance_snapshot_id': bad_snapshot_id, - 'keypair': keypair.name, - 'name': server.name, - 'script_source': 'raw', - 'availability_zone': avail_zone.zoneName, - 'network': self.networks.first().id, - 'volume_id': '', - 'volume_snapshot_id': '', - 'image_id': '', - 'device_name': 'vda', - 'count': 1, - 'customization_script': ''} - - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertFormErrors(res, 3, "You must select a snapshot.") - - self.assertEqual(4, self.mock_image_list_detailed.call_count) - self.mock_image_list_detailed.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - filters={'is_public': True, - 'status': 'active'}), - mock.call(helpers.IsHttpRequest(), - filters={'property-owner_id': self.tenant.id, - 'status': 'active'}), - mock.call(helpers.IsHttpRequest(), - filters={'status': 'active', 'visibility': 'community'}), - mock.call(helpers.IsHttpRequest(), - filters={'status': 'active', 'visibility': 'shared'}), - ]) - - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - self._check_neutron_network_and_port_list() - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - cinder: ('volume_list', - 'volume_snapshot_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_flavorlist_error(self): - self.mock_volume_list.return_value = [] - self.mock_volume_snapshot_list.return_value = [] - self._mock_glance_image_list_detailed(self.versioned_images.list()) - self._mock_neutron_network_and_port_list() - self.mock_tenant_quota_usages.return_value = self.quota_usages.first() - self.mock_flavor_list.side_effect = self.exceptions.nova - self.mock_keypair_list.return_value = self.keypairs.list() - self.mock_security_group_list.return_value = \ - self.security_groups.list() - self.mock_availability_zone_list.return_value = \ - self.availability_zones.list() - self.mock_server_group_list.return_value = self.server_groups.list() - - url = reverse('horizon:project:instances:launch') - res = self.client.get(url) - - self.assertTemplateUsed(res, views.WorkflowView.template_name) - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - - self._check_glance_image_list_detailed(count=8) - self._check_neutron_network_and_port_list() - - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest()) - self.mock_security_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_availability_zone_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_create', - 'port_delete', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list', - 'server_create',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_form_keystone_exception(self): - flavor = self.flavors.first() - image = self.versioned_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' - nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] - quota_usages = self.quota_usages.first() - - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE)] - self.mock_volume_snapshot_list.return_value = volumes - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_keypair_list.return_value = self.keypairs.list() - self.mock_security_group_list.return_value = \ - self.security_groups.list() - self.mock_availability_zone_list.return_value = \ - self.availability_zones.list() - self._mock_glance_image_list_detailed(self.versioned_images.list()) - self._mock_neutron_network_and_port_list() - self.mock_server_create.side_effect = self.exceptions.keystone - self.mock_tenant_quota_usages.return_value = quota_usages - self.mock_flavor_list.return_value = self.flavors.list() - - 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': str(sec_group.id), - 'volume_type': '', - 'network': self.networks.first().id, - 'count': 1, - 'admin_pass': 'password', - 'confirm_admin_pass': 'password', - 'disk_config': 'AUTO', - 'config_drive': False} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertRedirectsNoFollow(res, INDEX_URL) - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest()) - self.mock_security_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_availability_zone_list.assert_called_once_with( - helpers.IsHttpRequest()) - - self._check_glance_image_list_detailed(count=8) - self._check_neutron_network_and_port_list() - - self.mock_server_create.assert_called_once_with( - helpers.IsHttpRequest(), - server.name, - image.id, - flavor.id, - keypair.name, - customization_script, - [str(sec_group.id)], - block_device_mapping=None, - block_device_mapping_v2=None, - nics=nics, - availability_zone=avail_zone.zoneName, - instance_count=helpers.IsA(int), - admin_pass='password', - disk_config='AUTO', - config_drive=False, - scheduler_hints={}) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list'), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_form_instance_count_error(self): - flavor = self.flavors.first() - image = self.versioned_images.first() - keypair = self.keypairs.first() - server = self.servers.first() - volume = self.cinder_volumes.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - volume_choice = "%s:vol" % volume.id - quota_usages = self.quota_usages.first() - - self._mock_nova_glance_neutron_lists() - - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'image_id', - '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': str(sec_group.id), - 'volume_type': 'volume_id', - 'volume_id': volume_choice, - 'device_name': device_name, - 'count': 0} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self.assertContains(res, "greater than or equal to 1") - - self._check_nova_glance_neutron_lists(flavor_count=3, - image_count=10) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 3, - mock.call(helpers.IsHttpRequest())) - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def _test_launch_form_count_error(self, resource, avail): - flavor = self.flavors.first() - image = self.versioned_images.first() - keypair = self.keypairs.first() - server = self.servers.first() - volume = self.cinder_volumes.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - volume_choice = "%s:vol" % volume.id - quota_usages = self.quota_usages.first() - if resource == 'both': - quota_usages['cores']['available'] = avail - quota_usages['ram']['available'] = 512 - else: - quota_usages[resource]['available'] = avail - - self._mock_nova_glance_neutron_lists() - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'image_id', - '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': str(sec_group.id), - 'volume_type': 'volume_id', - 'volume_id': volume_choice, - 'device_name': device_name, - 'count': 2} - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - if resource == 'ram': - msg = ("The following requested resource(s) exceed quota(s): " - "RAM(Available: %s" % avail) - if resource == 'cores': - msg = ("The following requested resource(s) exceed quota(s): " - "Cores(Available: %s" % avail) - if resource == 'both': - msg = ("The following requested resource(s) exceed quota(s): " - "Cores(Available: %(avail)s, Requested: 2), RAM(Available: " - "512, Requested: 1024)" % {'avail': avail}) - self.assertContains(res, msg) - - self._check_nova_glance_neutron_lists(flavor_count=3, - image_count=10) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 3, - mock.call(helpers.IsHttpRequest())) - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - def test_launch_form_cores_count_error_glance_v2(self): - self._test_launch_form_count_error('cores', 1) - - def test_launch_form_ram_count_error(self): - self._test_launch_form_count_error('ram', 512) - - def test_launch_form_ram_cores_count_error(self): - self._test_launch_form_count_error('both', 1) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def _launch_form_instance(self, image, flavor, keypair=None): - server = self.servers.first() - volume = self.cinder_volumes.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - volume_choice = "%s:vol" % volume.id - quota_usages = self.quota_usages.first() - - self._mock_nova_glance_neutron_lists() - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'image_id', - 'image_id': image.id, - 'availability_zone': avail_zone.zoneName, - 'name': server.name, - 'script_source': 'raw', - 'script_data': customization_script, - 'project_id': self.tenants.first().id, - 'user_id': self.user.id, - 'groups': str(sec_group.id), - 'volume_type': 'volume_id', - 'volume_id': volume_choice, - 'device_name': device_name, - 'count': 1} - if keypair: - form_data['keypair'] = keypair.name - - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - - self._check_nova_glance_neutron_lists(flavor_count=3, - image_count=10) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 3, - mock.call(helpers.IsHttpRequest())) - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - return res - - def test_launch_form_instance_requirement_error_disk(self): - flavor = self.flavors.get(id="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") - image = self.versioned_images.first() - image.min_ram = flavor.ram - image.min_disk = flavor.disk + 1 - keypair = self.keypairs.first() - res = self._launch_form_instance(image, flavor, keypair) - msg = (f"The flavor '{flavor.name}' is too small for requested " - f"image. Minimum requirements: {image.min_ram} MB of RAM and " - f"{image.min_disk} GB of Root Disk.") - self.assertContains(res, msg, html=True) - - def test_launch_form_instance_requirement_error_ram(self): - flavor = self.flavors.first() - image = self.versioned_images.first() - image.min_ram = flavor.ram + 1 - image.min_disk = flavor.disk - keypair = self.keypairs.first() - res = self._launch_form_instance(image, flavor, keypair) - msg = (f"The flavor '{flavor.name}' is too small for requested " - f"image. Minimum requirements: {image.min_ram} MB of RAM and " - f"{image.min_disk} GB of Root Disk.") - self.assertContains(res, msg, html=True) - - def test_launch_form_instance_zero_value_flavor_with_min_req(self): - flavor = self.flavors.first() - image = self.versioned_images.first() - image.min_ram = flavor.ram - image.min_disk = flavor.disk + 1 - keypair = self.keypairs.first() - res = self._launch_form_instance(image, flavor, keypair) - msg = (f"The flavor &39;{flavor.name}&39; is too small for requested " - f"image. Minimum requirements: {image.min_ram} MB of RAM and " - f"{image.min_disk} GB of Root Disk.") - self.assertNotContains(res, msg, html=True) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def _test_launch_form_instance_show_device_name(self, device_name, - widget_class, - widget_attrs): - flavor = self.flavors.first() - image = self.versioned_images.first() - keypair = self.keypairs.first() - server = self.servers.first() - volume = self.cinder_volumes.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - volume_choice = "%s:vol" % volume.id - quota_usages = self.quota_usages.first() - - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_keypair_list.return_value = self.keypairs.list() - self.mock_security_group_list.return_value = \ - self.security_groups.list() - self.mock_availability_zone_list.return_value = \ - self.availability_zones.list() - self.mock_server_group_list.return_value = self.server_groups.list() - self.mock_image_list_detailed.side_effect = [ - [self.versioned_images.list(), False, False], - [[], False, False], - ] - self.mock_network_list.side_effect = [ - self.networks.list()[:1], - self.networks.list()[1:], - self.networks.list()[:1], - self.networks.list()[1:], - ] - self.mock_port_list_with_trunk_types.return_value = self.ports.list() - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = {'flavor': flavor.id, - 'source_type': 'volume_image_id', - 'image_id': image.id, - 'availability_zone': avail_zone.zoneName, - 'keypair': keypair.name, - 'name': server.name, - 'customization_script': customization_script, - 'project_id': self.tenants.first().id, - 'user_id': self.user.id, - 'groups': str(sec_group.id), - 'volume_type': 'volume_id', - 'volume_id': volume_choice, - 'volume_size': max( - image.min_disk, image.size // 1024 ** 3), - 'device_name': device_name, - 'count': 1} - - url = reverse('horizon:project:instances:launch') - res = self.client.post(url, form_data) - self.assertNoFormErrors(res) - widget_content = widget_class().render(**widget_attrs) - # In django 1.4, the widget's html attributes are not always rendered - # in the same order and checking the fully rendered widget fails. - for widget_part in widget_content.split(): - self.assertContains(res, widget_part) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 3, - mock.call(helpers.IsHttpRequest())) - self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest()) - self.mock_security_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_availability_zone_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.assertEqual(10, self.mock_image_list_detailed.call_count) - self.mock_image_list_detailed.assert_has_calls( - [ - mock.call(helpers.IsHttpRequest(), - filters={'is_public': True, - 'status': 'active'}), - mock.call(helpers.IsHttpRequest(), - filters={'property-owner_id': self.tenant.id, - 'status': 'active'}) - ] + - [ - mock.call(helpers.IsHttpRequest(), - filters={'status': 'active', - 'visibility': 'community'}), - mock.call(helpers.IsHttpRequest(), - filters={'status': 'active', - 'visibility': 'shared'}) - ] * 3 - ) - self.assertEqual(4, self.mock_network_list.call_count) - self.mock_network_list.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - tenant_id=self.tenant.id, - shared=False), - mock.call( - helpers.IsHttpRequest(), - shared=True), - mock.call( - helpers.IsHttpRequest(), - tenant_id=self.tenant.id, - shared=False), - mock.call( - helpers.IsHttpRequest(), - shared=True), - ]) - self.assertEqual(len(self.networks.list()), - self.mock_port_list_with_trunk_types.call_count) - self.mock_port_list_with_trunk_types.assert_has_calls( - [mock.call(helpers.IsHttpRequest(), - network_id=net.id, - tenant_id=self.tenant.id) - for net in self.networks.list()]) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 3, - mock.call(helpers.IsHttpRequest())) - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - @helpers.update_settings( - OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True},) - def test_launch_form_instance_device_name_showed(self): - self._test_launch_form_instance_show_device_name( - 'vda', widgets.TextInput, { - 'name': 'device_name', 'value': 'vda', - 'attrs': {'id': 'id_device_name'}} - ) - - @helpers.update_settings( - OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': False}) - def test_launch_form_instance_device_name_hidden(self): - self._test_launch_form_instance_show_device_name( - '', widgets.HiddenInput, { - 'name': 'device_name', 'value': '', - 'attrs': {'id': 'id_device_name'}} - ) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def _test_launch_form_instance_volume_size(self, image, volume_size, msg, - avail_volumes=None): - flavor = self.flavors.get(name='m1.massive') - keypair = self.keypairs.first() - server = self.servers.first() - sec_group = self.security_groups.first() - avail_zone = self.availability_zones.first() - customization_script = 'user data' - device_name = 'vda' - quota_usages = self.quota_usages.first() - quota_usages['cores']['available'] = 2000 - if avail_volumes is not None: - quota_usages['volumes']['available'] = avail_volumes - - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_keypair_list.return_value = self.keypairs.list() - self.mock_security_group_list.return_value = \ - self.security_groups.list() - self.mock_availability_zone_list.return_value = \ - self.availability_zones.list() - self._mock_glance_image_list_detailed(self.versioned_images.list()) - self._mock_neutron_network_and_port_list() - self.mock_server_group_list.return_value = self.server_groups.list() - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_tenant_quota_usages.return_value = quota_usages - - form_data = { - 'flavor': flavor.id, - 'source_type': 'volume_image_id', - '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': str(sec_group.id), - 'volume_size': volume_size, - 'device_name': device_name, - 'count': 1 - } - url = reverse('horizon:project:instances:launch') - - res = self.client.post(url, form_data) - self.assertContains(res, msg, html=True) - - self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest()) - self.mock_security_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_availability_zone_list.assert_called_once_with( - helpers.IsHttpRequest()) - if avail_volumes is None: - image_list_count = 10 - else: - image_list_count = 8 - self._check_glance_image_list_detailed(count=image_list_count) - self._check_neutron_network_and_port_list() - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - if avail_volumes is None: - flavor_list_count = 3 - else: - flavor_list_count = 2 - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, flavor_list_count, - mock.call(helpers.IsHttpRequest())) - self.mock_tenant_quota_usages.assert_has_calls([ - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )), - mock.call( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')), - ]) - self.assertEqual(2, self.mock_tenant_quota_usages.call_count) - - def test_launch_form_instance_volume_size_error(self): - image = self.versioned_images.get(name='protected_images') - volume_size = image.min_disk // 2 - msg = ("The Volume size is too small for the '%s' image " - "and has to be greater than or equal to '%s' GB." % - (image.name, image.min_disk)) - self._test_launch_form_instance_volume_size(image, volume_size, msg) - - def test_launch_form_instance_non_int_volume_size(self): - image = self.versioned_images.get(name='protected_images') - msg = "Enter a whole number." - self._test_launch_form_instance_volume_size(image, 1.5, msg) - - def test_launch_form_instance_volume_exceed_quota(self): - image = self.versioned_images.get(name='protected_images') - msg = ("The requested instance cannot be launched. " - "Requested volume exceeds quota: Available: 0, Requested: 1.") - self._test_launch_form_instance_volume_size(image, image.min_disk, - msg, 0) - - @helpers.create_mocks({ - api.nova: ('flavor_list', - 'server_list_paged', - 'tenant_absolute_limits', - 'is_feature_available',), - api.glance: ('image_list_detailed',), - api.neutron: ('floating_ip_simple_associate_supported', - 'floating_ip_supported',), - api.network: ('servers_update_addresses',), - api.cinder: ('volume_list',), - }) - def test_launch_button_attributes(self): - servers = self.servers.list() - limits = self.limits['absolute'] - - self.mock_is_feature_available.return_value = True - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_image_list_detailed.return_value = (self.images.list(), - False, False) - self.mock_server_list_paged.return_value = [servers, False, False] - self.mock_servers_update_addresses.return_value = None - self.mock_tenant_absolute_limits.return_value = limits - self.mock_floating_ip_supported.return_value = True - self.mock_floating_ip_simple_associate_supported.return_value = True - - tables.LaunchLink() - res = self.client.get(INDEX_URL) - - launch_action = self.getAndAssertTableAction(res, 'instances', - 'launch-ng') - - self.assertEqual(set(['btn-launch']), - set(launch_action.classes)) - self.assertEqual('Launch Instance', launch_action.verbose_name) - self.assertEqual((('compute', 'os_compute_api:servers:create'),), - launch_action.policy_rules) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_is_feature_available, 10, - mock.call(helpers.IsHttpRequest(), 'locked_attribute')) - self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest()) - self._assert_mock_image_list_detailed_calls() - - search_opts = {'marker': None, 'paginate': True} - self.mock_server_list_paged.assert_called_once_with( - helpers.IsHttpRequest(), - sort_dir='desc', - search_opts=search_opts) - self.mock_servers_update_addresses.assert_called_once_with( - helpers.IsHttpRequest(), servers) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_tenant_absolute_limits, 3, - mock.call(helpers.IsHttpRequest(), reserved=True)) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_floating_ip_supported, 10, - mock.call(helpers.IsHttpRequest())) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_floating_ip_simple_associate_supported, 5, - mock.call(helpers.IsHttpRequest())) - - @helpers.create_mocks({ - api.nova: ('flavor_list', - 'server_list_paged', - 'tenant_absolute_limits', - 'is_feature_available',), - api.glance: ('image_list_detailed',), - api.neutron: ('floating_ip_simple_associate_supported', - 'floating_ip_supported',), - api.network: ('servers_update_addresses',), - api.cinder: ('volume_list',), - }) - def test_launch_button_disabled_when_quota_exceeded(self): - servers = self.servers.list() - limits = self.limits['absolute'] - limits['totalInstancesUsed'] = limits['maxTotalInstances'] - - self.mock_is_feature_available.return_value = True - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_image_list_detailed.return_value = (self.images.list(), - False, False) - self.mock_server_list_paged.return_value = [servers, False, False] - self.mock_servers_update_addresses.return_value = None - self.mock_tenant_absolute_limits.return_value = limits - self.mock_floating_ip_supported.return_value = True - self.mock_floating_ip_simple_associate_supported.return_value = True - - tables.LaunchLink() - res = self.client.get(INDEX_URL) - - launch_action = self.getAndAssertTableAction( - res, 'instances', 'launch-ng') - - self.assertIn('disabled', launch_action.classes, - 'The launch button should be disabled') - self.assertEqual('Launch Instance (Quota exceeded)', - launch_action.verbose_name) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_is_feature_available, 10, - mock.call(helpers.IsHttpRequest(), 'locked_attribute')) - self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest()) - self._assert_mock_image_list_detailed_calls() - - search_opts = {'marker': None, 'paginate': True} - self.mock_server_list_paged.assert_called_once_with( - helpers.IsHttpRequest(), - sort_dir='desc', - search_opts=search_opts) - self.mock_servers_update_addresses.assert_called_once_with( - helpers.IsHttpRequest(), servers) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_tenant_absolute_limits, 3, - mock.call(helpers.IsHttpRequest(), reserved=True)) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_floating_ip_supported, 10, - mock.call(helpers.IsHttpRequest())) - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_floating_ip_simple_associate_supported, 5, - mock.call(helpers.IsHttpRequest())) - - @helpers.create_mocks({api.glance: ('image_list_detailed',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'availability_zone_list', - 'server_group_list', - 'tenant_absolute_limits', - 'server_create',), - cinder: ('volume_list', - 'volume_snapshot_list',), - quotas: ('tenant_quota_usages',)}) - def test_launch_with_empty_device_name_allowed(self): - flavor = self.flavors.get(name='m1.massive') - image = self.versioned_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' - nics = [{'net-id': self.networks.first().id, 'v4-fixed-ip': ''}] - device_name = '' - quota_usages = self.quota_usages.first() - quota_usages['cores']['available'] = 2000 - device_mapping_v2 = [{'device_name': None, # device_name must be None - 'source_type': 'image', - 'destination_type': 'volume', - 'delete_on_termination': False, - 'uuid': image.id, - 'boot_index': '0', - 'volume_size': image.size}] - - self._mock_nova_glance_neutron_lists() - volumes = [v for v in self.cinder_volumes.list() - if (v.status == AVAILABLE and v.bootable == 'true')] - self.mock_volume_list.return_value = volumes - self.mock_volume_snapshot_list.return_value = [] - self.mock_flavor_list.return_value = self.flavors.list() - self.mock_tenant_quota_usages.return_value = quota_usages - self.mock_server_create.return_value = None - - form_data = { - 'flavor': flavor.id, - 'source_type': 'volume_image_id', - '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': str(sec_group.id), - 'volume_size': image.size, - 'device_name': device_name, - 'network': self.networks.first().id, - 'count': 1 - } - url = reverse('horizon:project:instances:launch') - - res = self.client.post(url, form_data) - self.assertNoFormErrors(res) - - self._check_nova_glance_neutron_lists(flavor_count=2, - image_count=8) - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), - search_opts=SNAPSHOT_SEARCH_OPTS) - - self.assert_mock_multiple_calls_with_same_arguments( - self.mock_flavor_list, 2, - mock.call(helpers.IsHttpRequest())) - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', )) - self.mock_server_create.assert_called_once_with( - helpers.IsHttpRequest(), - server.name, - '', - flavor.id, - keypair.name, - customization_script, - [str(sec_group.id)], - block_device_mapping=None, - block_device_mapping_v2=device_mapping_v2, - nics=nics, - availability_zone=avail_zone.zoneName, - instance_count=helpers.IsA(int), - admin_pass='', - config_drive=False, - disk_config='', - scheduler_hints={}) - - class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): @helpers.create_mocks({ @@ -4052,51 +2105,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_floating_ip_simple_associate_supported, 5, mock.call(helpers.IsHttpRequest())) - @helpers.create_mocks({api.nova: ('is_feature_available', - 'flavor_list', - 'keypair_list', - 'server_group_list', - 'availability_zone_list'), - cinder: ('volume_snapshot_list', - 'volume_list',), - api.neutron: ('network_list', - 'port_list_with_trunk_types', - 'security_group_list',), - api.glance: ('image_list_detailed',), - quotas: ('tenant_quota_usages',)}) - def test_select_default_keypair_if_only_one(self): - keypair = self.keypairs.first() - - self.mock_volume_list.return_value = [] - self.mock_volume_snapshot_list.return_value = [] - self._mock_glance_image_list_detailed(self.versioned_images.list()) - self._mock_neutron_network_and_port_list() - self.mock_tenant_quota_usages.return_value = self.quota_usages.first() - self._mock_nova_lists() - - url = reverse('horizon:project:instances:launch') - res = self.client.get(url) - self.assertContains( - res, "" % {'key': keypair.name}, - html=True, - msg_prefix="The default key pair was not selected.") - - self.mock_volume_list.assert_has_calls([ - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_SEARCH_OPTS), - mock.call(helpers.IsHttpRequest(), - search_opts=VOLUME_BOOTABLE_SEARCH_OPTS), - ]) - self.mock_volume_snapshot_list.assert_called_once_with( - helpers.IsHttpRequest(), search_opts=SNAPSHOT_SEARCH_OPTS) - self._check_glance_image_list_detailed(count=8) - self._check_neutron_network_and_port_list() - self.mock_tenant_quota_usages.assert_called_once_with( - helpers.IsHttpRequest(), - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')) - self._check_nova_lists(flavor_count=2) - @helpers.create_mocks({ api.neutron: ('floating_ip_target_list_by_instance', 'tenant_floating_ip_list', @@ -4149,24 +2157,36 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): @helpers.create_mocks({api.nova: ('server_get', 'flavor_list', - 'server_group_list', 'tenant_absolute_limits', 'is_feature_available')}) def test_instance_resize_get(self): server = self.servers.first() + flavor = self.flavors.first() self.mock_server_get.return_value = server self.mock_flavor_list.return_value = self.flavors.list() - self.mock_server_group_list.return_value = self.server_groups.list() self.mock_tenant_absolute_limits.return_value = self.limits['absolute'] url = reverse('horizon:project:instances:resize', args=[server.id]) res = self.client.get(url) + workflow = res.context['workflow'] self.assertTemplateUsed(res, views.WorkflowView.template_name) + self.assertEqual(res.context['workflow'].name, + workflows.ResizeInstance.name) + self.assertContains(res, 'Disk Partition') config_drive_field_label = 'Configuration Drive' self.assertNotContains(res, config_drive_field_label) + step = workflow.get_step("flavor_choice") + self.assertEqual(step.action.initial['old_flavor_id'], flavor.id) + + step = workflow.get_step("setadvancedaction") + self.assertEqual(step.action.fields['disk_config'].label, + 'Disk Partition') + self.assertQuerysetEqual(workflow.steps, + ['', + '']) option = '' for flavor in self.flavors.list(): if flavor.id == server.flavor['id']: @@ -4179,8 +2199,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.assert_mock_multiple_calls_with_same_arguments( self.mock_flavor_list, 2, mock.call(helpers.IsHttpRequest())) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) self.mock_tenant_absolute_limits.assert_called_once_with( helpers.IsHttpRequest(), reserved=True) @@ -4219,7 +2237,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): @helpers.create_mocks({api.nova: ('server_get', 'flavor_list', 'flavor_get', - 'server_group_list', 'tenant_absolute_limits', 'is_feature_available')}) def test_instance_resize_get_current_flavor_not_found(self): @@ -4227,7 +2244,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_server_get.return_value = server self.mock_flavor_list.return_value = [] self.mock_flavor_get.side_effect = self.exceptions.nova - self.mock_server_group_list.return_value = self.server_groups.list() self.mock_tenant_absolute_limits.return_value = self.limits['absolute'] url = reverse('horizon:project:instances:resize', args=[server.id]) @@ -4242,8 +2258,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): mock.call(helpers.IsHttpRequest())) self.mock_flavor_get.assert_called_once_with( helpers.IsHttpRequest(), server.flavor['id']) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) self.mock_tenant_absolute_limits.assert_called_once_with( helpers.IsHttpRequest(), reserved=True) @@ -4257,7 +2271,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): instance_resize_post_stubs = { api.nova: ('server_get', 'server_resize', - 'flavor_list', 'flavor_get', 'server_group_list', + 'flavor_list', 'flavor_get', 'is_feature_available')} @helpers.create_mocks(instance_resize_post_stubs) @@ -4269,7 +2283,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_server_get.return_value = server self.mock_flavor_list.return_value = self.flavors.list() - self.mock_server_group_list.return_value = self.server_groups.list() self.mock_server_resize.return_value = [] res = self._instance_resize_post(server.id, flavor.id, 'AUTO') @@ -4279,8 +2292,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_server_get.assert_called_once_with(helpers.IsHttpRequest(), server.id) self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest()) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) self.mock_server_resize.assert_called_once_with( helpers.IsHttpRequest(), server.id, flavor.id, 'AUTO') @@ -4293,7 +2304,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_server_get.return_value = server self.mock_flavor_list.return_value = self.flavors.list() - self.mock_server_group_list.return_value = self.server_groups.list() self.mock_server_resize.side_effect = self.exceptions.nova res = self._instance_resize_post(server.id, flavor.id, 'AUTO') @@ -4302,8 +2312,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_server_get.assert_called_once_with(helpers.IsHttpRequest(), server.id) self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest()) - self.mock_server_group_list.assert_called_once_with( - helpers.IsHttpRequest()) self.mock_server_resize.assert_called_once_with( helpers.IsHttpRequest(), server.id, flavor.id, 'AUTO') @@ -4538,7 +2546,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): def test_index_form_action_with_pagination(self): # The form action on the next page should have marker # object from the previous page last element. - page_size = settings.API_RESULT_PAGE_SIZE servers = self.servers.list()[:3] @@ -4664,49 +2671,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): def read(self): return self.data - def test_clean_file_upload_form_oversize_data(self): - t = workflows.create_instance.CustomizeAction(self.request, {}) - upload_str = 'user data' - files = {'script_upload': - self.SimpleFile('script_name', - upload_str, - (16 * 1024) + 1)} - - self.assertRaises( - forms.ValidationError, - t.clean_uploaded_files, - 'script', - files) - - def test_clean_file_upload_form_invalid_data(self): - t = workflows.create_instance.CustomizeAction(self.request, {}) - upload_str = b'\x81' - files = {'script_upload': - self.SimpleFile('script_name', - upload_str, - sys.getsizeof(upload_str))} - - self.assertRaises( - forms.ValidationError, - t.clean_uploaded_files, - 'script', - files) - - def test_clean_file_upload_form_valid_data(self): - t = workflows.create_instance.CustomizeAction(self.request, {}) - precleaned = 'user data' - upload_str = 'user data' - files = {'script_upload': - self.SimpleFile('script_name', - upload_str, - sys.getsizeof(upload_str))} - - cleaned = t.clean_uploaded_files('script', files) - - self.assertEqual( - cleaned, - precleaned) - def _server_rescue_post(self, server_id, image_id, password=None): form_data = {'instance_id': server_id, diff --git a/openstack_dashboard/dashboards/project/instances/urls.py b/openstack_dashboard/dashboards/project/instances/urls.py index b531ce996b..7d76c3ef82 100644 --- a/openstack_dashboard/dashboards/project/instances/urls.py +++ b/openstack_dashboard/dashboards/project/instances/urls.py @@ -26,7 +26,6 @@ INSTANCES_KEYPAIR = r'^(?P[^/]+)/(?P[^/]+)/%s$' urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), - url(r'^launch$', views.LaunchInstanceView.as_view(), name='launch'), url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), url(INSTANCES % 'update', views.UpdateView.as_view(), name='update'), diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index f767c538fa..ee8b1636c3 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -40,7 +40,6 @@ from horizon import workflows from openstack_dashboard import api from openstack_dashboard.utils import filters -from openstack_dashboard.utils import settings as setting_utils from openstack_dashboard.dashboards.project.instances \ import console as project_console @@ -260,26 +259,6 @@ def _swap_filter(resources, search_opts, fake_field, real_field): return True -class LaunchInstanceView(workflows.WorkflowView): - workflow_class = project_workflows.LaunchInstance - - def __init__(self): - super().__init__() - LOG.warning('Django version of the launch instance form is ' - 'deprecated since Wallaby release. Switch to ' - 'the AngularJS version of the form by setting ' - 'LAUNCH_INSTANCE_NG_ENABLED to True and ' - 'LAUNCH_INSTANCE_LEGACY_ENABLED to False.') - - def get_initial(self): - initial = super().get_initial() - initial['project_id'] = self.request.user.tenant_id - initial['user_id'] = self.request.user.id - initial['config_drive'] = setting_utils.get_dict_config( - 'LAUNCH_INSTANCE_DEFAULTS', 'config_drive') - return initial - - # TODO(stephenfin): Migrate to CBV def console(request, instance_id): data = _('Unable to get log for instance "%s".') % instance_id diff --git a/openstack_dashboard/dashboards/project/instances/workflows/__init__.py b/openstack_dashboard/dashboards/project/instances/workflows/__init__.py index 54ac87e84f..dde37d252a 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/__init__.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/__init__.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack_dashboard.dashboards.project.instances.workflows.\ - create_instance import LaunchInstance from openstack_dashboard.dashboards.project.instances.workflows.\ resize_instance import ResizeInstance from openstack_dashboard.dashboards.project.instances.workflows.\ @@ -20,7 +18,6 @@ from openstack_dashboard.dashboards.project.instances.workflows.\ update_port import UpdatePort __all__ = [ - 'LaunchInstance', 'ResizeInstance', 'UpdateInstance', 'UpdatePort', diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py deleted file mode 100644 index fd575e353d..0000000000 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ /dev/null @@ -1,959 +0,0 @@ -# Copyright 2012 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2012 Nebula, Inc. -# -# 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 json -import logging -import operator - -from oslo_utils import units - -from django.template.defaultfilters import filesizeformat -from django.utils.text import normalize_newlines -from django.utils.translation import ugettext_lazy as _ -from django.views.decorators.debug import sensitive_variables - -from horizon import exceptions -from horizon import forms -from horizon.utils import functions -from horizon.utils import memoized -from horizon.utils import validators -from horizon import workflows - -from openstack_dashboard import api -from openstack_dashboard.api import base -from openstack_dashboard.api import cinder -from openstack_dashboard.api import nova -from openstack_dashboard.usage import quotas - -from openstack_dashboard.dashboards.project.images.images \ - import tables as image_tables -from openstack_dashboard.dashboards.project.images \ - import utils as image_utils -from openstack_dashboard.dashboards.project.instances \ - import utils as instance_utils - - -LOG = logging.getLogger(__name__) - - -class SelectProjectUserAction(workflows.Action): - project_id = forms.ThemableChoiceField(label=_("Project")) - user_id = forms.ThemableChoiceField(label=_("User")) - - def __init__(self, request, *args, **kwargs): - super().__init__(request, *args, **kwargs) - # Set our project choices - projects = [(tenant.id, tenant.name) - for tenant in request.user.authorized_tenants] - self.fields['project_id'].choices = projects - - # Set our user options - users = [(request.user.id, request.user.username)] - self.fields['user_id'].choices = users - - class Meta(object): - name = _("Project & User") - # Unusable permission so this is always hidden. However, we - # keep this step in the workflow for validation/verification purposes. - permissions = ("!",) - - -class SelectProjectUser(workflows.Step): - action_class = SelectProjectUserAction - contributes = ("project_id", "user_id") - - -class SetInstanceDetailsAction(workflows.Action): - availability_zone = forms.ThemableChoiceField(label=_("Availability Zone"), - required=False) - - name = forms.CharField(label=_("Instance Name"), - max_length=255) - - flavor = forms.ThemableChoiceField(label=_("Flavor"), - help_text=_("Size of image to launch.")) - - count = forms.IntegerField(label=_("Number of Instances"), - min_value=1, - initial=1) - - source_type = forms.ThemableChoiceField( - label=_("Instance Boot Source"), - help_text=_("Choose Your Boot Source " - "Type.")) - - instance_snapshot_id = forms.ThemableChoiceField( - label=_("Instance Snapshot"), - required=False) - - volume_id = forms.ThemableChoiceField(label=_("Volume"), required=False) - - volume_snapshot_id = forms.ThemableChoiceField(label=_("Volume Snapshot"), - required=False) - - image_id = forms.ChoiceField( - label=_("Image Name"), - required=False, - widget=forms.ThemableSelectWidget( - data_attrs=('volume_size',), - transform=lambda x: ("%s (%s)" % (x.name, - filesizeformat(x.bytes))))) - - volume_size = forms.IntegerField(label=_("Device size (GB)"), - initial=1, - min_value=0, - required=False, - help_text=_("Volume size in gigabytes " - "(integer value).")) - - device_name = forms.CharField(label=_("Device Name"), - required=False, - initial="vda", - help_text=_("Volume mount point (e.g. 'vda' " - "mounts at '/dev/vda'). Leave " - "this field blank to let the " - "system choose a device name " - "for you.")) - - vol_delete_on_instance_delete = forms.BooleanField( - label=_("Delete Volume on Instance Delete"), - initial=False, - required=False, - help_text=_("Delete volume when the instance is deleted")) - - class Meta(object): - name = _("Details") - help_text_template = ("project/instances/" - "_launch_details_help.html") - - def __init__(self, request, context, *args, **kwargs): - self._init_images_cache() - self.request = request - self.context = context - super().__init__(request, context, *args, **kwargs) - - # Hide the device field if the hypervisor doesn't support it. - if not nova.can_set_mount_point(): - self.fields['device_name'].widget = forms.widgets.HiddenInput() - - source_type_choices = [ - ('', _("Select source")), - ("image_id", _("Boot from image")), - ("instance_snapshot_id", _("Boot from snapshot")), - ] - if cinder.is_volume_service_enabled(request): - source_type_choices += [ - ("volume_id", _("Boot from volume")), - ("volume_image_id", - _("Boot from image (creates a new volume)")), - ("volume_snapshot_id", - _("Boot from volume snapshot (creates a new volume)")), - ] - self.fields['source_type'].choices = source_type_choices - - @memoized.memoized_method - def _get_flavor(self, flavor_id): - try: - # We want to retrieve details for a given flavor, - # however flavor_list uses a memoized decorator - # so it is used instead of flavor_get to reduce the number - # of API calls. - flavors = instance_utils.flavor_list(self.request) - flavor = [x for x in flavors if x.id == flavor_id][0] - except IndexError: - flavor = None - return flavor - - @memoized.memoized_method - def _get_image(self, image_id): - try: - # We want to retrieve details for a given image, - # however get_available_images uses a cache of image list, - # so it is used instead of image_get to reduce the number - # of API calls. - images = image_utils.get_available_images( - self.request, - self.context.get('project_id'), - self._images_cache) - image = [x for x in images if x.id == image_id][0] - except IndexError: - image = None - return image - - def _check_quotas(self, cleaned_data): - count = cleaned_data.get('count', 1) - - # Prevent launching more instances than the quota allows - usages = quotas.tenant_quota_usages( - self.request, - targets=('instances', 'cores', 'ram', 'volumes', )) - available_count = usages['instances']['available'] - if available_count < count: - msg = (_('The requested instance(s) cannot be launched ' - 'as your quota will be exceeded: Available: ' - '%(avail)s, Requested: %(req)s.') - % {'avail': available_count, 'req': count}) - raise forms.ValidationError(msg) - - source_type = cleaned_data.get('source_type') - if source_type in ('volume_image_id', 'volume_snapshot_id'): - available_volume = usages['volumes']['available'] - if available_volume < count: - msg = (_('The requested instance cannot be launched. ' - 'Requested volume exceeds quota: Available: ' - '%(avail)s, Requested: %(req)s.') - % {'avail': available_volume, 'req': count}) - raise forms.ValidationError(msg) - - flavor_id = cleaned_data.get('flavor') - flavor = self._get_flavor(flavor_id) - - count_error = [] - # Validate cores and ram. - available_cores = usages['cores']['available'] - if flavor and available_cores < count * flavor.vcpus: - count_error.append(_("Cores(Available: %(avail)s, " - "Requested: %(req)s)") - % {'avail': available_cores, - 'req': count * flavor.vcpus}) - - available_ram = usages['ram']['available'] - if flavor and available_ram < count * flavor.ram: - count_error.append(_("RAM(Available: %(avail)s, " - "Requested: %(req)s)") - % {'avail': available_ram, - 'req': count * flavor.ram}) - - if count_error: - value_str = ", ".join(count_error) - msg = (_('The requested instance cannot be launched. ' - 'The following requested resource(s) exceed ' - 'quota(s): %s.') % value_str) - if count == 1: - self._errors['flavor'] = self.error_class([msg]) - else: - self._errors['count'] = self.error_class([msg]) - - def _check_flavor_for_image(self, cleaned_data): - # Prevents trying to launch an image needing more resources. - image_id = cleaned_data.get('image_id') - image = self._get_image(image_id) - flavor_id = cleaned_data.get('flavor') - flavor = self._get_flavor(flavor_id) - if not image or not flavor: - return - props_mapping = (("min_ram", "ram"), ("min_disk", "disk")) - for iprop, fprop in props_mapping: - if (getattr(image, iprop) > 0 and - getattr(flavor, fprop) > 0 and - getattr(image, iprop) > getattr(flavor, fprop)): - msg = (_("The flavor '%(flavor)s' is too small " - "for requested image.\n" - "Minimum requirements: " - "%(min_ram)s MB of RAM and " - "%(min_disk)s GB of Root Disk.") % - {'flavor': flavor.name, - 'min_ram': image.min_ram, - 'min_disk': image.min_disk}) - self._errors['image_id'] = self.error_class([msg]) - break # Not necessary to continue the tests. - - def _check_volume_for_image(self, cleaned_data): - image_id = cleaned_data.get('image_id') - image = self._get_image(image_id) - volume_size = cleaned_data.get('volume_size') - if not image or not volume_size: - return - volume_size = int(volume_size) - img_gigs = functions.bytes_to_gigabytes(image.size) - smallest_size = max(img_gigs, image.min_disk) - if volume_size < smallest_size: - msg = (_("The Volume size is too small for the" - " '%(image_name)s' image and has to be" - " greater than or equal to " - "'%(smallest_size)d' GB.") % - {'image_name': image.name, - 'smallest_size': smallest_size}) - self._errors['volume_size'] = self.error_class([msg]) - - def _check_source_image(self, cleaned_data): - if not cleaned_data.get('image_id'): - msg = _("You must select an image.") - self._errors['image_id'] = self.error_class([msg]) - else: - self._check_flavor_for_image(cleaned_data) - - def _check_source_volume_image(self, cleaned_data): - volume_size = self.data.get('volume_size', None) - if not volume_size: - msg = _("You must set volume size") - self._errors['volume_size'] = self.error_class([msg]) - if float(volume_size) <= 0: - msg = _("Volume size must be greater than 0") - self._errors['volume_size'] = self.error_class([msg]) - if not cleaned_data.get('image_id'): - msg = _("You must select an image.") - self._errors['image_id'] = self.error_class([msg]) - return - self._check_flavor_for_image(cleaned_data) - self._check_volume_for_image(cleaned_data) - - def _check_source_instance_snapshot(self, cleaned_data): - # using the array form of get blows up with KeyError - # if instance_snapshot_id is nil - if not cleaned_data.get('instance_snapshot_id'): - msg = _("You must select a snapshot.") - self._errors['instance_snapshot_id'] = self.error_class([msg]) - - def _check_source_volume(self, cleaned_data): - if not cleaned_data.get('volume_id'): - msg = _("You must select a volume.") - self._errors['volume_id'] = self.error_class([msg]) - # Prevent launching multiple instances with the same volume. - # TODO(gabriel): is it safe to launch multiple instances with - # a snapshot since it should be cloned to new volumes? - count = cleaned_data.get('count', 1) - if count > 1: - msg = _('Launching multiple instances is only supported for ' - 'images and instance snapshots.') - raise forms.ValidationError(msg) - - def _check_source_volume_snapshot(self, cleaned_data): - if not cleaned_data.get('volume_snapshot_id'): - msg = _("You must select a snapshot.") - self._errors['volume_snapshot_id'] = self.error_class([msg]) - - def _check_source(self, cleaned_data): - # Validate our instance source. - source_type = self.data.get('source_type', None) - source_check_methods = { - 'image_id': self._check_source_image, - 'volume_image_id': self._check_source_volume_image, - 'instance_snapshot_id': self._check_source_instance_snapshot, - 'volume_id': self._check_source_volume, - 'volume_snapshot_id': self._check_source_volume_snapshot - } - check_method = source_check_methods.get(source_type) - if check_method: - check_method(cleaned_data) - - def clean(self): - cleaned_data = super().clean() - - self._check_quotas(cleaned_data) - self._check_source(cleaned_data) - - return cleaned_data - - def populate_flavor_choices(self, request, context): - return instance_utils.flavor_field_data(request, False) - - def populate_availability_zone_choices(self, request, context): - try: - zones = api.nova.availability_zone_list(request) - except Exception: - zones = [] - exceptions.handle(request, - _('Unable to retrieve availability zones.')) - - zone_list = [(zone.zoneName, zone.zoneName) - for zone in zones if zone.zoneState['available']] - zone_list.sort() - if not zone_list: - zone_list.insert(0, ("", _("No availability zones found"))) - elif len(zone_list) > 1: - zone_list.insert(0, ("", _("Any Availability Zone"))) - return zone_list - - def get_help_text(self, extra_context=None): - extra = {} if extra_context is None else dict(extra_context) - try: - extra['usages'] = quotas.tenant_quota_usages( - self.request, - targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')) - extra['usages_json'] = json.dumps(extra['usages']) - extra['cinder_enabled'] = \ - base.is_service_enabled(self.request, 'volume') - flavors = json.dumps([f._info for f in - instance_utils.flavor_list(self.request)]) - extra['flavors'] = flavors - images = image_utils.get_available_images( - self.request, self.initial['project_id'], self._images_cache) - if images is not None: - attrs = [{'id': i.id, - 'min_disk': getattr(i, 'min_disk', 0), - 'min_ram': getattr(i, 'min_ram', 0), - 'size': functions.bytes_to_gigabytes(i.size)} - for i in images] - extra['images'] = json.dumps(attrs) - - except Exception: - exceptions.handle(self.request, - _("Unable to retrieve quota information.")) - return super().get_help_text(extra) - - def _init_images_cache(self): - if not hasattr(self, '_images_cache'): - self._images_cache = {} - - def _get_volume_display_name(self, volume): - if hasattr(volume, "volume_id"): - vol_type = "snap" - visible_label = _("Snapshot") - else: - vol_type = "vol" - visible_label = _("Volume") - return (("%s:%s" % (volume.id, vol_type)), - (_("%(name)s - %(size)s GB (%(label)s)") % - {'name': volume.name, - 'size': volume.size, - 'label': visible_label})) - - def populate_image_id_choices(self, request, context): - choices = [] - images = image_utils.get_available_images(request, - context.get('project_id'), - self._images_cache) - for image in images: - if image_tables.get_image_type(image) != "snapshot": - image.bytes = getattr( - image, 'virtual_size', None) or image.size - image.volume_size = max( - image.min_disk, functions.bytes_to_gigabytes(image.bytes)) - choices.append((image.id, image)) - if context.get('image_id') == image.id and \ - 'volume_size' not in context: - context['volume_size'] = image.volume_size - if choices: - choices.sort(key=lambda c: c[1].name or '') - choices.insert(0, ("", _("Select Image"))) - else: - choices.insert(0, ("", _("No images available"))) - return choices - - def populate_instance_snapshot_id_choices(self, request, context): - images = image_utils.get_available_images(request, - context.get('project_id'), - self._images_cache) - choices = [(image.id, image.name) - for image in images - if image_tables.get_image_type(image) == "snapshot"] - if choices: - choices.sort(key=operator.itemgetter(1)) - choices.insert(0, ("", _("Select Instance Snapshot"))) - else: - choices.insert(0, ("", _("No snapshots available"))) - return choices - - def populate_volume_id_choices(self, request, context): - volumes = [] - try: - if cinder.is_volume_service_enabled(request): - available = api.cinder.VOLUME_STATE_AVAILABLE - volumes = [self._get_volume_display_name(v) - for v in cinder.volume_list(self.request, - search_opts=dict(status=available, bootable=True))] - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve list of volumes.')) - if volumes: - volumes.insert(0, ("", _("Select Volume"))) - else: - volumes.insert(0, ("", _("No volumes available"))) - return volumes - - def populate_volume_snapshot_id_choices(self, request, context): - snapshots = [] - try: - if cinder.is_volume_service_enabled(request): - available = api.cinder.VOLUME_STATE_AVAILABLE - volumes = [v.id for v in cinder.volume_list( - self.request, search_opts=dict(bootable=True))] - snapshots = [self._get_volume_display_name(s) - for s in cinder.volume_snapshot_list( - self.request, search_opts=dict(status=available)) - if s.volume_id in volumes] - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve list of volume ' - 'snapshots.')) - if snapshots: - snapshots.insert(0, ("", _("Select Volume Snapshot"))) - else: - snapshots.insert(0, ("", _("No volume snapshots available"))) - return snapshots - - -class SetInstanceDetails(workflows.Step): - action_class = SetInstanceDetailsAction - depends_on = ("project_id", "user_id") - contributes = ("source_type", "source_id", - "availability_zone", "name", "count", "flavor", - "device_name", # Can be None for an image. - "vol_delete_on_instance_delete") - - def prepare_action_context(self, request, context): - if 'source_type' in context and 'source_id' in context: - context[context['source_type']] = context['source_id'] - return context - - def contribute(self, data, context): - context = super().contribute(data, context) - # Allow setting the source dynamically. - if ("source_type" in context and - "source_id" in context and - context["source_type"] not in context): - context[context["source_type"]] = context["source_id"] - - # Translate form input to context for source values. - if "source_type" in data: - if data["source_type"] in ["image_id", "volume_image_id"]: - context["source_id"] = data.get("image_id", None) - else: - context["source_id"] = data.get(data["source_type"], None) - - if "volume_size" in data: - context["volume_size"] = data["volume_size"] - - return context - - -KEYPAIR_IMPORT_URL = "horizon:project:key_pairs:import" - - -class SetAccessControlsAction(workflows.Action): - keypair = forms.ThemableDynamicChoiceField( - label=_("Key Pair"), - help_text=_("Key pair to use for " - "authentication."), - add_item_link=KEYPAIR_IMPORT_URL) - admin_pass = forms.RegexField( - label=_("Admin Password"), - required=False, - widget=forms.PasswordInput(render_value=False), - regex=validators.password_validator(), - error_messages={'invalid': validators.password_validator_msg()}) - confirm_admin_pass = forms.CharField( - label=_("Confirm Admin Password"), - strip=False, - required=False, - widget=forms.PasswordInput(render_value=False)) - groups = forms.MultipleChoiceField( - label=_("Security Groups"), - required=False, - initial=["default"], - widget=forms.ThemableCheckboxSelectMultiple(), - help_text=_("Launch instance in these " - "security groups.")) - - class Meta(object): - name = _("Access & Security") - help_text = _("Control access to your instance via key pairs, " - "security groups, and other mechanisms.") - - def __init__(self, request, *args, **kwargs): - super().__init__(request, *args, **kwargs) - if not api.nova.can_set_server_password(): - del self.fields['admin_pass'] - del self.fields['confirm_admin_pass'] - self.fields['keypair'].required = api.nova.requires_keypair() - - def populate_keypair_choices(self, request, context): - keypairs = instance_utils.keypair_field_data(request, True) - if len(keypairs) == 2: - self.fields['keypair'].initial = keypairs[1][0] - return keypairs - - def populate_groups_choices(self, request, context): - try: - groups = api.neutron.security_group_list(request) - security_group_list = [(sg.id, sg.name) for sg in groups] - except Exception: - exceptions.handle(request, - _('Unable to retrieve list of security groups')) - security_group_list = [] - return security_group_list - - def clean(self): - '''Check to make sure password fields match.''' - cleaned_data = super().clean() - if 'admin_pass' in cleaned_data: - if cleaned_data['admin_pass'] != cleaned_data.get( - 'confirm_admin_pass', None): - raise forms.ValidationError(_('Passwords do not match.')) - return cleaned_data - - -class SetAccessControls(workflows.Step): - action_class = SetAccessControlsAction - depends_on = ("project_id", "user_id") - contributes = ("keypair_id", "security_group_ids", - "admin_pass", "confirm_admin_pass") - - def contribute(self, data, context): - if data: - post = self.workflow.request.POST - context['security_group_ids'] = post.getlist("groups") - context['keypair_id'] = data.get("keypair", "") - context['admin_pass'] = data.get("admin_pass", "") - context['confirm_admin_pass'] = data.get("confirm_admin_pass", "") - return context - - -class CustomizeAction(workflows.Action): - class Meta(object): - name = _("Post-Creation") - help_text_template = ("project/instances/" - "_launch_customize_help.html") - - source_choices = [('', _('Select Script Source')), - ('raw', _('Direct Input')), - ('file', _('File'))] - - attributes = {'class': 'switchable', 'data-slug': 'scriptsource'} - script_source = forms.ChoiceField( - label=_('Customization Script Source'), - choices=source_choices, - widget=forms.ThemableSelectWidget(attrs=attributes), - required=False) - - script_help = _("A script or set of commands to be executed after the " - "instance has been built (max 16kb).") - - script_upload = forms.FileField( - label=_('Script File'), - help_text=script_help, - widget=forms.FileInput(attrs={ - 'class': 'switched', - 'data-switch-on': 'scriptsource', - 'data-scriptsource-file': _('Script File')}), - required=False) - - script_data = forms.CharField( - label=_('Script Data'), - help_text=script_help, - widget=forms.widgets.Textarea(attrs={ - 'class': 'switched', - 'data-switch-on': 'scriptsource', - 'data-scriptsource-raw': _('Script Data')}), - required=False) - - def clean(self): - cleaned = super().clean() - - files = self.request.FILES - script = self.clean_uploaded_files('script', files) - - if script is not None: - cleaned['script_data'] = script - - return cleaned - - def clean_uploaded_files(self, prefix, files): - upload_str = prefix + "_upload" - - if upload_str not in files: - return None - - upload_file = files[upload_str] - log_script_name = upload_file.name - LOG.info('got upload %s', log_script_name) - - if upload_file._size > 16 * units.Ki: # 16kb - msg = _('File exceeds maximum size (16kb)') - raise forms.ValidationError(msg) - - script = upload_file.read() - if script != "": - try: - if not isinstance(script, str): - script = script.decode() - normalize_newlines(script) - except Exception as e: - msg = _('There was a problem parsing the' - ' %(prefix)s: %(error)s') - msg = msg % {'prefix': prefix, - 'error': e} - raise forms.ValidationError(msg) - return script - - -class PostCreationStep(workflows.Step): - action_class = CustomizeAction - contributes = ("script_data",) - - -class SetNetworkAction(workflows.Action): - network = forms.MultipleChoiceField( - label=_("Networks"), - widget=forms.ThemableCheckboxSelectMultiple(), - error_messages={ - 'required': _( - "At least one network must" - " be specified.")}, - help_text=_("Launch instance with" - " these networks")) - - def __init__(self, request, *args, **kwargs): - super().__init__(request, *args, **kwargs) - - # NOTE(e0ne): we don't need 'required attribute for networks - # checkboxes to be able to select only one network - # NOTE(e0ne): we need it for compatibility with different - # Django versions (prior to 1.11) - self.use_required_attribute = False - - network_list = self.fields["network"].choices - if len(network_list) == 1: - self.fields['network'].initial = [network_list[0][0]] - - class Meta(object): - name = _("Networking") - permissions = ('openstack.services.network',) - help_text = _("Select networks for your instance.") - - def populate_network_choices(self, request, context): - return instance_utils.network_field_data(request, for_launch=True) - - -class SetNetwork(workflows.Step): - action_class = SetNetworkAction - template_name = "project/instances/_update_networks.html" - contributes = ("network_id",) - - def contribute(self, data, context): - if data: - networks = self.workflow.request.POST.getlist("network") - # If no networks are explicitly specified, network list - # contains an empty string, so remove it. - networks = [n for n in networks if n != ''] - if networks: - context['network_id'] = networks - return context - - -class SetNetworkPortsAction(workflows.Action): - ports = forms.MultipleChoiceField(label=_("Ports"), - widget=forms.CheckboxSelectMultiple(), - required=False, - help_text=_("Launch instance with" - " these ports")) - - class Meta(object): - name = _("Network Ports") - permissions = ('openstack.services.network',) - help_text_template = ("project/instances/" - "_launch_network_ports_help.html") - - def populate_ports_choices(self, request, context): - ports = instance_utils.port_field_data(request) - if not ports: - self.fields['ports'].label = _("No ports available") - self.fields['ports'].help_text = _("No ports available") - return ports - - -class SetNetworkPorts(workflows.Step): - action_class = SetNetworkPortsAction - contributes = ("ports",) - - def contribute(self, data, context): - if data: - ports = self.workflow.request.POST.getlist("ports") - if ports: - context['ports'] = ports - return context - - -class SetAdvancedAction(workflows.Action): - disk_config = forms.ThemableChoiceField( - label=_("Disk Partition"), required=False, - help_text=_("Automatic: The entire disk is a single partition and " - "automatically resizes. Manual: Results in faster build " - "times but requires manual partitioning.")) - config_drive = forms.BooleanField( - label=_("Configuration Drive"), - required=False, help_text=_("Configure OpenStack to write metadata to " - "a special configuration drive that " - "attaches to the instance when it boots.")) - server_group = forms.ThemableChoiceField( - label=_("Server Group"), required=False, - help_text=_("Server group to associate with this instance.")) - - def __init__(self, request, context, *args, **kwargs): - super().__init__(request, context, *args, **kwargs) - try: - config_choices = [("AUTO", _("Automatic")), - ("MANUAL", _("Manual"))] - self.fields['disk_config'].choices = config_choices - - # Only show the Config Drive option for the Launch Instance - # is supported. - if context.get('workflow_slug') != 'launch_instance': - del self.fields['config_drive'] - - server_group_choices = instance_utils.server_group_field_data( - request) - self.fields['server_group'].choices = server_group_choices - except Exception: - exceptions.handle(request, _('Unable to retrieve extensions ' - 'information.')) - - class Meta(object): - name = _("Advanced Options") - help_text_template = ("project/instances/" - "_launch_advanced_help.html") - - -class SetAdvanced(workflows.Step): - action_class = SetAdvancedAction - contributes = ("disk_config", "config_drive", "server_group",) - - def prepare_action_context(self, request, context): - context = super().prepare_action_context(request, context) - # Add the workflow slug to the context so that we can tell which - # workflow is being used when creating the action. This step is - # used by both the Launch Instance and Resize Instance workflows. - context['workflow_slug'] = self.workflow.slug - return context - - -class LaunchInstance(workflows.Workflow): - slug = "launch_instance" - name = _("Launch Instance") - finalize_button_name = _("Launch") - success_message = _('Request for launching %(count)s named "%(name)s" ' - 'has been submitted.') - failure_message = _('Unable to launch %(count)s named "%(name)s".') - success_url = "horizon:project:instances:index" - multipart = True - default_steps = (SelectProjectUser, - SetInstanceDetails, - SetAccessControls, - SetNetwork, - SetNetworkPorts, - PostCreationStep, - SetAdvanced) - - def format_status_message(self, message): - name = self.context.get('name', 'unknown instance') - count = self.context.get('count', 1) - if int(count) > 1: - return message % {"count": _("%s instances") % count, - "name": name} - return message % {"count": _("instance"), "name": name} - - @sensitive_variables('context') - def handle(self, request, context): - custom_script = context.get('script_data', '') - - dev_mapping_1 = None - dev_mapping_2 = None - - image_id = '' - - # Determine volume mapping options - source_type = context.get('source_type', None) - if source_type in ['image_id', 'instance_snapshot_id']: - image_id = context['source_id'] - elif source_type in ['volume_id', 'volume_snapshot_id']: - # Volume source id is extracted from the source - volume_source_id = context['source_id'].split(':')[0] - device_name = context.get('device_name', '').strip() or None - dev_source_type_mapping = { - 'volume_id': 'volume', - 'volume_snapshot_id': 'snapshot' - } - dev_mapping_2 = [ - {'device_name': device_name, - 'source_type': dev_source_type_mapping[source_type], - 'destination_type': 'volume', - 'delete_on_termination': - bool(context['vol_delete_on_instance_delete']), - 'uuid': volume_source_id, - 'boot_index': '0', - 'volume_size': context['volume_size'] - } - ] - elif source_type == 'volume_image_id': - device_name = context.get('device_name', '').strip() or None - dev_mapping_2 = [ - {'device_name': device_name, # None auto-selects device - 'source_type': 'image', - 'destination_type': 'volume', - 'delete_on_termination': - bool(context['vol_delete_on_instance_delete']), - 'uuid': context['source_id'], - 'boot_index': '0', - 'volume_size': context['volume_size'] - } - ] - - netids = context.get('network_id', None) - if netids: - nics = [{"net-id": netid, "v4-fixed-ip": ""} - for netid in netids] - else: - nics = None - - avail_zone = context.get('availability_zone', None) - - scheduler_hints = {} - server_group = context.get('server_group', None) - if server_group: - scheduler_hints['group'] = server_group - - ports = context.get('ports') - if ports: - if nics is None: - nics = [] - nics.extend([{'port-id': port} for port in ports]) - - try: - api.nova.server_create(request, - context['name'], - image_id, - context['flavor'], - context['keypair_id'], - normalize_newlines(custom_script), - context['security_group_ids'], - block_device_mapping=dev_mapping_1, - block_device_mapping_v2=dev_mapping_2, - nics=nics, - availability_zone=avail_zone, - instance_count=int(context['count']), - admin_pass=context['admin_pass'], - disk_config=context.get('disk_config'), - config_drive=context.get('config_drive'), - scheduler_hints=scheduler_hints) - return True - except Exception: - exceptions.handle(request) - return False - - -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 diff --git a/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py index bbc6906c5b..00d25938fa 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py @@ -25,8 +25,53 @@ from horizon import workflows from openstack_dashboard import api from openstack_dashboard.dashboards.project.instances \ import utils as instance_utils -from openstack_dashboard.dashboards.project.instances.workflows \ - import create_instance + + +class SetAdvancedAction(workflows.Action): + disk_config = forms.ThemableChoiceField( + label=_("Disk Partition"), required=False, + help_text=_("Automatic: The entire disk is a single partition and " + "automatically resizes. Manual: Results in faster build " + "times but requires manual partitioning.")) + config_drive = forms.BooleanField( + label=_("Configuration Drive"), + required=False, help_text=_("Configure OpenStack to write metadata to " + "a special configuration drive that " + "attaches to the instance when it boots.")) + + def __init__(self, request, context, *args, **kwargs): + super().__init__(request, context, *args, **kwargs) + try: + config_choices = [("AUTO", _("Automatic")), + ("MANUAL", _("Manual"))] + self.fields['disk_config'].choices = config_choices + + # Only show the Config Drive option for the Launch Instance + # is supported. + if context.get('workflow_slug') != 'launch_instance': + del self.fields['config_drive'] + + except Exception: + exceptions.handle(request, _('Unable to retrieve extensions ' + 'information.')) + + class Meta(object): + name = _("Advanced Options") + help_text_template = ("project/instances/" + "_launch_advanced_help.html") + + +class SetAdvanced(workflows.Step): + action_class = SetAdvancedAction + contributes = ("disk_config", "config_drive",) + + def prepare_action_context(self, request, context): + context = super().prepare_action_context(request, context) + # Add the workflow slug to the context so that we can tell which + # workflow is being used when creating the action. This step is + # used by both the Launch Instance and Resize Instance workflows. + context['workflow_slug'] = self.workflow.slug + return context class SetFlavorChoiceAction(workflows.Action): @@ -94,7 +139,7 @@ class ResizeInstance(workflows.Workflow): 'has been submitted.') failure_message = _('Unable to resize instance "%s".') success_url = "horizon:project:instances:index" - default_steps = (SetFlavorChoice, create_instance.SetAdvanced) + default_steps = (SetFlavorChoice, SetAdvanced,) def format_status_message(self, message): if "%s" in message: diff --git a/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html b/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html index a609611902..fcefd766b3 100644 --- a/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html +++ b/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html @@ -2,31 +2,17 @@
{% if launch_instance_allowed %} - {% if show_ng_launch %} - {% url 'horizon:project:network_topology:index' as networkUrl %} - - - {% if instance_quota_exceeded %} - {% trans "Launch Instance (Quota exceeded)" %} - {% else %} - {% trans "Launch Instance" %} - {% endif %} - - {% endif %} - {% if show_legacy_launch %} - - - {% if instance_quota_exceeded %} - {% trans "Launch Instance (Quota exceeded)" %} - {% else %} - {% trans "Launch Instance" %} - {% endif %} - - {% endif %} + {% url 'horizon:project:network_topology:index' as networkUrl %} + + + {% if instance_quota_exceeded %} + {% trans "Launch Instance (Quota exceeded)" %} + {% else %} + {% trans "Launch Instance" %} + {% endif %} {% endif %} {% if create_network_allowed %}
{% if launch_instance_allowed %} - {% if show_ng_launch %} {% url 'horizon:project:network_topology:index' as networkUrl %} {% if instance_quota_exceeded %}{% trans "Launch Instance (Quota exceeded)"%}{% else %}{% trans "Launch Instance"%}{% endif %} {% endif %} - {% if show_legacy_launch %} - {% if instance_quota_exceeded %}{% trans "Launch Instance (Quota exceeded)"%}{% else %}{% trans "Launch Instance"%}{% endif %} - {% endif %} - {% endif %} {% if create_network_allowed %} {% if network_quota_exceeded %}{% trans "Create Network (Quota exceeded)"%}{% else %}{% trans "Create Network"%}{% endif %} {% endif %} diff --git a/openstack_dashboard/dashboards/project/network_topology/tests.py b/openstack_dashboard/dashboards/project/network_topology/tests.py index ac7330ca19..cb40cf4141 100644 --- a/openstack_dashboard/dashboards/project/network_topology/tests.py +++ b/openstack_dashboard/dashboards/project/network_topology/tests.py @@ -234,17 +234,3 @@ class NetworkTopologyCreateTests(test.TestCase): self._test_new_button_disabled_when_quota_exceeded(expected_string, routers_quota=0) - - @test.update_settings(LAUNCH_INSTANCE_LEGACY_ENABLED=True) - @test.create_mocks({quotas: ('tenant_quota_usages',)}) - def test_launch_instance_button_disabled_when_quota_exceeded(self): - url = reverse('horizon:project:network_topology:launchinstance') - classes = 'btn btn-default btn-launch ajax-modal' - link_name = "Launch Instance (Quota exceeded)" - expected_string = "" \ - "%s" \ - % (url, classes, link_name) - - self._test_new_button_disabled_when_quota_exceeded(expected_string, - instances_quota=0) diff --git a/openstack_dashboard/dashboards/project/network_topology/urls.py b/openstack_dashboard/dashboards/project/network_topology/urls.py index 95c5453af8..1482294fe6 100644 --- a/openstack_dashboard/dashboards/project/network_topology/urls.py +++ b/openstack_dashboard/dashboards/project/network_topology/urls.py @@ -35,8 +35,6 @@ urlpatterns = [ url(r'^network/(?P[^/]+)/subnet/create$', views.NTCreateSubnetView.as_view(), name='subnet'), url(r'^json$', views.JSONView.as_view(), name='json'), - url(r'^launchinstance$', views.NTLaunchInstanceView.as_view(), - name='launchinstance'), url(r'^createnetwork$', views.NTCreateNetworkView.as_view(), name='createnetwork'), url(r'^createrouter$', views.NTCreateRouterView.as_view(), diff --git a/openstack_dashboard/dashboards/project/network_topology/utils.py b/openstack_dashboard/dashboards/project/network_topology/utils.py index 784828ee05..cbdad70c96 100644 --- a/openstack_dashboard/dashboards/project/network_topology/utils.py +++ b/openstack_dashboard/dashboards/project/network_topology/utils.py @@ -29,8 +29,10 @@ def get_context(request, context=None): if context is None: context = {} - context['launch_instance_allowed'] = policy.check( - (("compute", "os_compute_api:servers:create"),), request) + context['launch_instance_allowed'] = ( + base.is_service_enabled(request, 'compute') and + policy.check((("compute", "os_compute_api:servers:create"),), request) + ) context['instance_quota_exceeded'] = _quota_exceeded(request, 'instances') context['create_network_allowed'] = policy.check( (("network", "create_network"),), request) @@ -41,10 +43,4 @@ def get_context(request, context=None): policy.check((("network", "create_router"),), request)) context['router_quota_exceeded'] = _quota_exceeded(request, 'router') context['console_type'] = settings.CONSOLE_TYPE - context['show_ng_launch'] = ( - base.is_service_enabled(request, 'compute') and - settings.LAUNCH_INSTANCE_NG_ENABLED) - context['show_legacy_launch'] = ( - base.is_service_enabled(request, 'compute') and - settings.LAUNCH_INSTANCE_LEGACY_ENABLED) return context diff --git a/openstack_dashboard/dashboards/project/network_topology/views.py b/openstack_dashboard/dashboards/project/network_topology/views.py index 2b1530c43b..0ea922f5d0 100644 --- a/openstack_dashboard/dashboards/project/network_topology/views.py +++ b/openstack_dashboard/dashboards/project/network_topology/views.py @@ -49,8 +49,6 @@ from openstack_dashboard.dashboards.project.instances.tables import \ STATUS_DISPLAY_CHOICES as instance_choices from openstack_dashboard.dashboards.project.instances import\ views as i_views -from openstack_dashboard.dashboards.project.instances.workflows import\ - create_instance as i_workflows from openstack_dashboard.dashboards.project.networks.subnets import\ views as s_views from openstack_dashboard.dashboards.project.networks.subnets import\ @@ -139,14 +137,6 @@ class NTCreateNetworkView(n_views.CreateView): workflow_class = NTCreateNetwork -class NTLaunchInstance(i_workflows.LaunchInstance): - success_url = "horizon:project:network_topology:index" - - -class NTLaunchInstanceView(i_views.LaunchInstanceView): - workflow_class = NTLaunchInstance - - class NTCreateSubnet(s_workflows.CreateSubnet): def get_success_url(self): return reverse("horizon:project:network_topology:index") diff --git a/openstack_dashboard/dashboards/project/snapshots/tables.py b/openstack_dashboard/dashboards/project/snapshots/tables.py index 9fd925954e..61fd5f6fd6 100644 --- a/openstack_dashboard/dashboards/project/snapshots/tables.py +++ b/openstack_dashboard/dashboards/project/snapshots/tables.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf import settings from django.urls import reverse from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ @@ -242,11 +241,7 @@ class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase): prev_pagination_param = 'prev_snapshot_marker' table_actions = (VolumeSnapshotsFilterAction, DeleteVolumeSnapshot,) - launch_actions = () - if settings.LAUNCH_INSTANCE_LEGACY_ENABLED: - launch_actions = (LaunchSnapshot,) + launch_actions - if settings.LAUNCH_INSTANCE_NG_ENABLED: - launch_actions = (LaunchSnapshotNG,) + launch_actions + launch_actions = (LaunchSnapshotNG,) row_actions = ((CreateVolumeFromSnapshot,) + launch_actions + (EditVolumeSnapshot, DeleteVolumeSnapshot, CreateBackup, diff --git a/openstack_dashboard/dashboards/project/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/tables.py index 6db77eefef..f693dc57f9 100644 --- a/openstack_dashboard/dashboards/project/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/tables.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf import settings from django.http import HttpResponse from django.template import defaultfilters as filters from django.urls import NoReverseMatch @@ -572,11 +571,7 @@ class VolumesTable(VolumesTableBase): table_actions = (CreateVolume, AcceptTransfer, DeleteVolume, VolumesFilterAction) - launch_actions = () - if settings.LAUNCH_INSTANCE_LEGACY_ENABLED: - launch_actions = (LaunchVolume,) + launch_actions - if settings.LAUNCH_INSTANCE_NG_ENABLED: - launch_actions = (LaunchVolumeNG,) + launch_actions + launch_actions = (LaunchVolumeNG,) row_actions = ((EditVolume, ExtendVolume,) + launch_actions + diff --git a/openstack_dashboard/defaults.py b/openstack_dashboard/defaults.py index 693395b982..c6894332f5 100644 --- a/openstack_dashboard/defaults.py +++ b/openstack_dashboard/defaults.py @@ -240,20 +240,6 @@ IMAGES_LIST_FILTER_TENANTS = [] # The default number of lines displayed for instance console log. INSTANCE_LOG_LENGTH = 35 -# The Launch Instance user experience has been significantly enhanced. -# You can choose whether to enable the new launch instance experience, -# the legacy experience, or both. The legacy experience will be removed -# in a future release, but is available as a temporary backup setting to ensure -# compatibility with existing deployments. Further development will not be -# done on the legacy experience. Please report any problems with the new -# experience via the Launchpad tracking system. -# -# Toggle LAUNCH_INSTANCE_LEGACY_ENABLED and LAUNCH_INSTANCE_NG_ENABLED to -# determine the experience to enable. Set them both to true to enable -# both. -LAUNCH_INSTANCE_LEGACY_ENABLED = False -LAUNCH_INSTANCE_NG_ENABLED = True - # A dictionary of settings which can be used to provide the default values for # properties found in the Launch Instance modal. LAUNCH_INSTANCE_DEFAULTS = { diff --git a/openstack_dashboard/local/local_settings.d/_20_integration_tests_scaffolds.py.example b/openstack_dashboard/local/local_settings.d/_20_integration_tests_scaffolds.py.example index af36d26a92..3a01ed6a12 100644 --- a/openstack_dashboard/local/local_settings.d/_20_integration_tests_scaffolds.py.example +++ b/openstack_dashboard/local/local_settings.d/_20_integration_tests_scaffolds.py.example @@ -1,7 +1,3 @@ -# Enable both Launch Instance wizards for the sake of testing -LAUNCH_INSTANCE_LEGACY_ENABLED = True -LAUNCH_INSTANCE_NG_ENABLED = True - # Provide a global setting for switching on/off various integration tests # scaffolds INTEGRATION_TESTS_SUPPORT = True diff --git a/openstack_dashboard/management/commands/upgrade_check.py b/openstack_dashboard/management/commands/upgrade_check.py index 4b6a35ae2a..2f0c07a946 100644 --- a/openstack_dashboard/management/commands/upgrade_check.py +++ b/openstack_dashboard/management/commands/upgrade_check.py @@ -299,19 +299,6 @@ def check_chinese_locale_rename(dummy): return upgradecheck.Result(upgradecheck.Code.SUCCESS) -@register_check(_("Django launch instance form")) -def check_django_launch_instance_form(dummy): - if settings.LAUNCH_INSTANCE_LEGACY_ENABLED: - return upgradecheck.Result( - upgradecheck.Code.WARNING, - _("The Django version of the launch instance form is deprecated " - "since Wallaby release. Switch to the AngularJS version of the " - "form by setting LAUNCH_INSTANCE_NG_ENABLED to True and " - "LAUNCH_INSTANCE_LEGACY_ENABLED to False.") - ) - return upgradecheck.Result(upgradecheck.Code.SUCCESS) - - class UpgradeCheckTable(upgradecheck.UpgradeCommands): _upgrade_checks = CHECKS diff --git a/releasenotes/notes/drop-django-launch-instance-form-c6543e1d52786b79.yaml b/releasenotes/notes/drop-django-launch-instance-form-c6543e1d52786b79.yaml new file mode 100644 index 0000000000..eaa9c78af4 --- /dev/null +++ b/releasenotes/notes/drop-django-launch-instance-form-c6543e1d52786b79.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + The Django version of the launch instance form was dropped. + It was deprecated since Wallaby release. + ``LAUNCH_INSTANCE_LEGACY_ENABLED`` and ``LAUNCH_INSTANCE_NG_ENABLED`` + setting were dropped as horizon uses angular version of launch instance + by default. +