From 69e6676588a7f972a0818bdc7233d75f8fd9b296 Mon Sep 17 00:00:00 2001 From: Justin Pomeroy Date: Mon, 8 Sep 2014 16:52:06 -0500 Subject: [PATCH] Allow setting config drive option when launching instance This patch adds a "Configuration Drive" checkbox to the Advanced tab of the Launch Instance workflow, if the config drive extension is supported. This allows setting the config drive option when launching an instance. Change-Id: Ib4e4e18251f4f024a61dabb646cda79a7c88582d Closes-Bug: #1366842 --- openstack_dashboard/api/nova.py | 5 +- .../instances/_launch_advanced_help.html | 3 +- .../dashboards/project/instances/tests.py | 71 +++++++++++++++++-- .../instances/workflows/create_instance.py | 35 +++++++-- 4 files changed, 97 insertions(+), 17 deletions(-) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 5a92c961f2..3544a4cb56 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -535,7 +535,7 @@ def server_create(request, name, image, flavor, key_name, user_data, security_groups, block_device_mapping=None, block_device_mapping_v2=None, nics=None, availability_zone=None, instance_count=1, admin_pass=None, - disk_config=None, meta=None): + disk_config=None, config_drive=None, meta=None): return Server(novaclient(request).servers.create( name, image, flavor, userdata=user_data, security_groups=security_groups, @@ -543,7 +543,8 @@ def server_create(request, name, image, flavor, key_name, user_data, block_device_mapping_v2=block_device_mapping_v2, nics=nics, availability_zone=availability_zone, min_count=instance_count, admin_pass=admin_pass, - disk_config=disk_config, meta=meta), request) + disk_config=disk_config, config_drive=config_drive, + meta=meta), request) def server_delete(request, instance): diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_advanced_help.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_advanced_help.html index a8b7885779..0b43a16c2c 100644 --- a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_advanced_help.html +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_advanced_help.html @@ -1,3 +1,2 @@ {% load i18n %} -

{% blocktrans %}Automatic: Entire disk is single partition and automatically resizes.{% endblocktrans %}

-

{% blocktrans %}Manual: Faster build times but requires manual partitioning.{% endblocktrans %}

+

{% blocktrans %}Specify advanced options to use when launching an instance.{% endblocktrans %}

diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index dfa3b8e2bf..09a9b1f39e 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -1209,6 +1209,7 @@ class InstanceTests(helpers.TestCase): custom_flavor_sort=None, only_one_network=False, disk_config=True, + config_drive=True, test_with_profile=False): image = self.images.first() @@ -1245,6 +1246,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(disk_config) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(config_drive) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -1335,6 +1338,12 @@ class InstanceTests(helpers.TestCase): else: self.assertNotContains(res, disk_config_field_label) + config_drive_field_label = 'Configuration Drive' + if config_drive: + self.assertContains(res, config_drive_field_label) + else: + self.assertNotContains(res, config_drive_field_label) + @django.test.utils.override_settings( OPENSTACK_HYPERVISOR_FEATURES={'can_set_password': False}) def test_launch_instance_get_without_password(self): @@ -1346,6 +1355,9 @@ class InstanceTests(helpers.TestCase): def test_launch_instance_get_no_disk_config_supported(self): self.test_launch_instance_get(disk_config=False) + def test_launch_instance_get_no_config_drive_supported(self): + self.test_launch_instance_get(config_drive=False) + @django.test.utils.override_settings( CREATE_INSTANCE_FLAVOR_SORT={ 'key': 'id', @@ -1402,6 +1414,7 @@ class InstanceTests(helpers.TestCase): block_device_mapping_v2=True, only_one_network=False, disk_config=True, + config_drive=True, test_with_profile=False): api.nova.extension_supported('BlockDeviceMappingV2Boot', IsA(http.HttpRequest)) \ @@ -1437,6 +1450,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(disk_config) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(config_drive) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -1491,6 +1506,7 @@ class InstanceTests(helpers.TestCase): quotas: ('tenant_quota_usages',)}) def test_launch_instance_post(self, disk_config=True, + config_drive=True, test_with_profile=False): flavor = self.flavors.first() image = self.images.first() @@ -1543,6 +1559,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(disk_config) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(config_drive) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn([]) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -1550,6 +1568,10 @@ class InstanceTests(helpers.TestCase): disk_config_value = u'AUTO' else: disk_config_value = None + if config_drive: + config_drive_value = True + else: + config_drive_value = None api.nova.server_create(IsA(http.HttpRequest), server.name, image.id, @@ -1563,7 +1585,8 @@ class InstanceTests(helpers.TestCase): availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'', - disk_config=disk_config_value) + disk_config=disk_config_value, + config_drive=config_drive_value) quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(quota_usages) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -1586,6 +1609,8 @@ class InstanceTests(helpers.TestCase): 'count': 1} if disk_config: form_data['disk_config'] = 'AUTO' + if config_drive: + form_data['config_drive'] = True if test_with_profile: form_data['profile'] = self.policy_profiles.first().id url = reverse('horizon:project:instances:launch') @@ -1597,6 +1622,9 @@ class InstanceTests(helpers.TestCase): def test_launch_instance_post_no_disk_config_supported(self): self.test_launch_instance_post(disk_config=False) + def test_launch_instance_post_no_config_drive_supported(self): + self.test_launch_instance_post(config_drive=False) + @helpers.update_settings( OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'}) def test_launch_instance_post_with_profile(self): @@ -1673,6 +1701,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -1689,7 +1719,8 @@ class InstanceTests(helpers.TestCase): availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'', - disk_config=u'AUTO') + disk_config=u'AUTO', + config_drive=True) quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(quota_usages) @@ -1710,7 +1741,8 @@ class InstanceTests(helpers.TestCase): 'device_name': device_name, 'network': self.networks.first().id, 'count': 1, - 'disk_config': 'AUTO'} + 'disk_config': 'AUTO', + 'config_drive': True} if test_with_profile: form_data['profile'] = self.policy_profiles.first().id url = reverse('horizon:project:instances:launch') @@ -1799,6 +1831,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -1818,7 +1852,8 @@ class InstanceTests(helpers.TestCase): availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'', - disk_config='MANUAL') + disk_config='MANUAL', + config_drive=True) self.mox.ReplayAll() @@ -1837,7 +1872,8 @@ class InstanceTests(helpers.TestCase): 'volume_id': volume_choice, 'device_name': device_name, 'count': 1, - 'disk_config': 'MANUAL'} + 'disk_config': 'MANUAL', + 'config_drive': True} if test_with_profile: form_data['profile'] = self.policy_profiles.first().id url = reverse('horizon:project:instances:launch') @@ -1905,6 +1941,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.keypair_list(IsA(http.HttpRequest)) \ @@ -1987,6 +2025,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \ .AndReturn(self.limits['absolute']) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -2078,6 +2118,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IgnoreArg()).AndReturn(self.volumes.list()) api.nova.server_create(IsA(http.HttpRequest), server.name, @@ -2092,7 +2134,8 @@ class InstanceTests(helpers.TestCase): availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass='password', - disk_config='AUTO') \ + disk_config='AUTO', + config_drive=False) \ .AndRaise(self.exceptions.keystone) quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(quota_usages) @@ -2118,7 +2161,8 @@ class InstanceTests(helpers.TestCase): 'count': 1, 'admin_pass': 'password', 'confirm_admin_pass': 'password', - 'disk_config': 'AUTO'} + 'disk_config': 'AUTO', + 'config_drive': False} if test_with_profile: form_data['profile'] = self.policy_profiles.first().id url = reverse('horizon:project:instances:launch') @@ -2190,6 +2234,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -2288,6 +2334,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -2403,6 +2451,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -2529,6 +2579,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -2725,6 +2777,8 @@ class InstanceTests(helpers.TestCase): api.nova.extension_supported('DiskConfig', IsA(http.HttpRequest)) \ .AndReturn(True) + api.nova.extension_supported('ConfigDrive', + IsA(http.HttpRequest)).AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -2845,6 +2899,9 @@ class InstanceTests(helpers.TestCase): self.assertTemplateUsed(res, views.WorkflowView.template_name) + config_drive_field_label = 'Configuration Drive' + self.assertNotContains(res, config_drive_field_label) + @helpers.create_stubs({api.nova: ('server_get', 'flavor_list',)}) def test_instance_resize_get_server_get_exception(self): diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index ea62a6d421..33b2bb21e6 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -663,11 +663,18 @@ class SetNetwork(workflows.Step): class SetAdvancedAction(workflows.Action): - disk_config = forms.ChoiceField(label=_("Disk Partition"), - required=False) + disk_config = forms.ChoiceField(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, *args, **kwargs): - super(SetAdvancedAction, self).__init__(request, *args, **kwargs) + def __init__(self, request, context, *args, **kwargs): + super(SetAdvancedAction, self).__init__(request, context, + *args, **kwargs) try: if not api.nova.extension_supported("DiskConfig", request): del self.fields['disk_config'] @@ -676,6 +683,12 @@ class SetAdvancedAction(workflows.Action): config_choices = [("AUTO", _("Automatic")), ("MANUAL", _("Manual"))] self.fields['disk_config'].choices = config_choices + # Only show the Config Drive option for the Launch Instance + # workflow (not Resize Instance) and only if the extension + # is supported. + if context.get('workflow_slug') != 'launch_instance' or ( + not api.nova.extension_supported("ConfigDrive", request)): + del self.fields['config_drive'] except Exception: exceptions.handle(request, _('Unable to retrieve extensions ' 'information.')) @@ -688,7 +701,16 @@ class SetAdvancedAction(workflows.Action): class SetAdvanced(workflows.Step): action_class = SetAdvancedAction - contributes = ("disk_config",) + contributes = ("disk_config", "config_drive",) + + def prepare_action_context(self, request, context): + context = super(SetAdvanced, self).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): @@ -788,7 +810,8 @@ class LaunchInstance(workflows.Workflow): availability_zone=avail_zone, instance_count=int(context['count']), admin_pass=context['admin_pass'], - disk_config=context.get('disk_config')) + disk_config=context.get('disk_config'), + config_drive=context.get('config_drive')) return True except Exception: exceptions.handle(request)