From 6ac31e0ba8df520133d20d21e35085d9b882c04e Mon Sep 17 00:00:00 2001 From: manchandavishal Date: Tue, 2 Nov 2021 12:10:22 +0530 Subject: [PATCH] Drop Django based implementation of launch instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit horizon already deprecated launch instance Django based implementation in the wallaby cycle [1]. This patch remove code for launch instance Django based implementation as angular based implementation is the default one from long and all features gaps between angular and Django implementation is closed. It also moves SetAdvanced step code to ``resize_instance.py`` as ``workflows/create_instance.py`` file is deleted and remove server_group option from Advanced Options of resizing instance action because "server_group" is not required while resizing an instance as per nova-api reference [2]. Closes-Bug: #1869222 [1] https://review.opendev.org/c/openstack/horizon/+/779125 [2] https://docs.openstack.org/api-ref/compute/?expanded=resize-server-resize-action-detail#resize-server-resize-action Change-Id: I5e01cd81f309491f1a58ea93911030366a86e3c7 --- doc/source/configuration/settings.rst | 41 - .../project/images/images/tables.py | 6 +- .../dashboards/project/instances/tables.py | 23 +- .../instances/_launch_customize_help.html | 3 - .../dashboards/project/instances/tests.py | 2066 +---------------- .../dashboards/project/instances/urls.py | 1 - .../dashboards/project/instances/views.py | 21 - .../project/instances/workflows/__init__.py | 3 - .../instances/workflows/create_instance.py | 959 -------- .../instances/workflows/resize_instance.py | 51 +- .../network_topology/_actions_list.html | 36 +- .../templates/network_topology/index.html | 5 - .../project/network_topology/tests.py | 14 - .../project/network_topology/urls.py | 2 - .../project/network_topology/utils.py | 12 +- .../project/network_topology/views.py | 10 - .../dashboards/project/snapshots/tables.py | 7 +- .../dashboards/project/volumes/tables.py | 7 +- openstack_dashboard/defaults.py | 14 - ..._20_integration_tests_scaffolds.py.example | 4 - .../management/commands/upgrade_check.py | 13 - ...launch-instance-form-c6543e1d52786b79.yaml | 9 + 22 files changed, 96 insertions(+), 3211 deletions(-) delete mode 100644 openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html delete mode 100644 openstack_dashboard/dashboards/project/instances/workflows/create_instance.py create mode 100644 releasenotes/notes/drop-django-launch-instance-form-c6543e1d52786b79.yaml diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst index 166c76db86..91688c747d 100644 --- a/doc/source/configuration/settings.rst +++ b/doc/source/configuration/settings.rst @@ -2245,47 +2245,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. +