From a09d5e54b9377b2f6a5481e188f332f9bc504214 Mon Sep 17 00:00:00 2001 From: "Brad P. Crochet" Date: Mon, 23 Feb 2015 10:03:30 -0500 Subject: [PATCH] Remove kernel/image from flavor and put it on node Starting with Juno, setting the kernel and image id on a flavor is deprecated. This moves that setting from the flavor, and puts it on the ironic node. Change-Id: I1f6072213d01adbc95e9d70d4faf95b376b5a566 --- tuskar_ui/api/node.py | 14 +++- tuskar_ui/infrastructure/flavors/tables.py | 63 +++------------- .../flavors/templates/flavors/details.html | 9 --- tuskar_ui/infrastructure/flavors/tests.py | 44 ++--------- tuskar_ui/infrastructure/flavors/utils.py | 5 +- tuskar_ui/infrastructure/flavors/views.py | 8 -- tuskar_ui/infrastructure/flavors/workflows.py | 36 +-------- tuskar_ui/infrastructure/nodes/forms.py | 33 ++++++++- tuskar_ui/infrastructure/nodes/tabs.py | 9 +++ tuskar_ui/infrastructure/nodes/tests.py | 73 ++++++++++++++++--- tuskar_ui/infrastructure/nodes/views.py | 45 +++++++++++- .../nodes/_detail_overview.html | 8 ++ .../nodes/_nodes_formset_form.html | 5 ++ .../infrastructure/nodes/register.html | 2 +- tuskar_ui/test/test_data/heat_data.py | 20 ++++- tuskar_ui/test/test_data/node_data.py | 36 ++++++--- 16 files changed, 234 insertions(+), 176 deletions(-) diff --git a/tuskar_ui/api/node.py b/tuskar_ui/api/node.py index 9ff888466..53374075f 100644 --- a/tuskar_ui/api/node.py +++ b/tuskar_ui/api/node.py @@ -100,7 +100,9 @@ class IronicNode(base.APIResourceWrapper): def create(cls, request, ipmi_address=None, cpu_arch=None, cpus=None, memory_mb=None, local_gb=None, mac_addresses=[], ipmi_username=None, ipmi_password=None, ssh_address=None, - ssh_username=None, ssh_key_contents=None, driver=None): + ssh_username=None, ssh_key_contents=None, + deployment_kernel=None, deployment_ramdisk=None, + driver=None): """Create a Node in Ironic.""" if driver == 'pxe_ssh': driver_info = { @@ -115,6 +117,10 @@ class IronicNode(base.APIResourceWrapper): 'ipmi_username': ipmi_username, 'ipmi_password': ipmi_password } + driver_info.update( + pxe_deploy_kernel=deployment_kernel, + pxe_deploy_ramdisk=deployment_ramdisk + ) properties = {} if cpus: @@ -342,6 +348,7 @@ class BareMetalNode(base.APIResourceWrapper): def create(cls, request, ipmi_address, cpu_arch, cpus, memory_mb, local_gb, mac_addresses, ipmi_username=None, ipmi_password=None, ssh_address=None, ssh_username=None, ssh_key_contents=None, + deployment_kernel=None, deployment_ramdisk=None, driver=None): """Create a Nova BareMetalNode.""" node = baremetalclient(request).create( @@ -567,12 +574,15 @@ class Node(base.APIResourceWrapper): def create(cls, request, ipmi_address=None, cpu_arch=None, cpus=None, memory_mb=None, local_gb=None, mac_addresses=[], ipmi_username=None, ipmi_password=None, ssh_address=None, - ssh_username=None, ssh_key_contents=None, driver=None): + ssh_username=None, ssh_key_contents=None, + deployment_kernel=None, deployment_ramdisk=None, driver=None): return cls(NodeClient(request).node_class.create( request, ipmi_address, cpu_arch, cpus, memory_mb, local_gb, mac_addresses, ipmi_username=ipmi_username, ipmi_password=ipmi_password, ssh_address=ssh_address, ssh_username=ssh_username, ssh_key_contents=ssh_key_contents, + deployment_kernel=deployment_kernel, + deployment_ramdisk=deployment_ramdisk, driver=driver)) @classmethod diff --git a/tuskar_ui/infrastructure/flavors/tables.py b/tuskar_ui/infrastructure/flavors/tables.py index 10d749103..68431a5bb 100644 --- a/tuskar_ui/infrastructure/flavors/tables.py +++ b/tuskar_ui/infrastructure/flavors/tables.py @@ -16,41 +16,11 @@ from django.utils.translation import ugettext_lazy as _ import horizon.exceptions import horizon.messages import horizon.tables -from openstack_dashboard.api import glance from openstack_dashboard.dashboards.admin.flavors import ( tables as flavor_tables) from tuskar_ui import api from tuskar_ui.infrastructure.flavors import utils -import tuskar_ui.utils.utils - -DEFAULT_KERNEL_IMAGE_NAME = 'discovery-kernel' -DEFAULT_RAMDISK_IMAGE_NAME = 'discovery-ramdisk' - - -def _guess_default_image_ids(request): - try: - images = glance.image_list_detailed(request)[0] - except Exception: - horizon.exceptions.handle(request, - _('Unable to retrieve images list.')) - kernel_images = [] - ramdisk_images = [] - else: - has_type = tuskar_ui.utils.utils.check_image_type - kernel_images = [ - image for image in images - if has_type(image, 'discovery kernel') - and image.name == DEFAULT_KERNEL_IMAGE_NAME - ] - ramdisk_images = [ - image for image in images - if has_type(image, 'discovery ramdisk') - and image.name == DEFAULT_RAMDISK_IMAGE_NAME - ] - if not kernel_images or not ramdisk_images: - raise ValueError("No default images") - return kernel_images[0].id, ramdisk_images[0].id class CreateFlavor(flavor_tables.CreateFlavor): @@ -65,33 +35,20 @@ class CreateSuggestedFlavor(horizon.tables.Action): method = 'POST' icon = 'plus' - def create_flavor(self, request, node_id, images): + def create_flavor(self, request, node_id): node = api.node.Node.get(request, node_id) suggestion = utils.FlavorSuggestion.from_node(node) - return suggestion.create_flavor(request, *images) + return suggestion.create_flavor(request) def handle(self, data_table, request, node_ids): - try: - images = _guess_default_image_ids(request) - except ValueError: - horizon.messages.error(request, _( - "No default images available. " - "Create images called \"{0}\" and \"{1}\" or " - "use the \"{2}\" action to choose different image names." - ).format( - DEFAULT_KERNEL_IMAGE_NAME, - DEFAULT_RAMDISK_IMAGE_NAME, - EditAndCreateSuggestedFlavor.verbose_name, - )) - else: - for node_id in node_ids: - try: - self.create_flavor(request, node_id, images) - except Exception: - horizon.exceptions.handle( - request, - _(u"Unable to create flavor for node %r") % node_id, - ) + for node_id in node_ids: + try: + self.create_flavor(request, node_id) + except Exception: + horizon.exceptions.handle( + request, + _(u"Unable to create flavor for node %r") % node_id, + ) return django.shortcuts.redirect(request.get_full_path()) diff --git a/tuskar_ui/infrastructure/flavors/templates/flavors/details.html b/tuskar_ui/infrastructure/flavors/templates/flavors/details.html index 72c39c5d1..41d59603c 100644 --- a/tuskar_ui/infrastructure/flavors/templates/flavors/details.html +++ b/tuskar_ui/infrastructure/flavors/templates/flavors/details.html @@ -21,15 +21,6 @@
{{ flavor.disk_bytes|filesizeformat|default:"—" }}
-
-

{% trans "Deploy Images" %}

-
-
{% trans "Kernel" %}
-
{{ kernel_image.name|default:"—" }}
-
{% trans "Ramdisk" %}
-
{{ ramdisk_image.name|default:"—" }}
-
-
diff --git a/tuskar_ui/infrastructure/flavors/tests.py b/tuskar_ui/infrastructure/flavors/tests.py index 266e0926f..002c7656d 100644 --- a/tuskar_ui/infrastructure/flavors/tests.py +++ b/tuskar_ui/infrastructure/flavors/tests.py @@ -43,19 +43,14 @@ DETAILS_VIEW = 'horizon:infrastructure:flavors:details' def _prepare_create(): flavor = TEST_DATA.novaclient_flavors.first() all_flavors = TEST_DATA.novaclient_flavors.list() - images = TEST_DATA.glanceclient_images.list() data = {'name': 'foobar', 'vcpus': 3, 'memory_mb': 1024, 'disk_gb': 40, - 'arch': 'amd64', - 'kernel_image_id': images[5].id, - 'ramdisk_image_id': images[4].id} + 'arch': 'amd64'} with contextlib.nested( patch('tuskar_ui.api.flavor.Flavor.create', return_value=flavor), - patch('openstack_dashboard.api.glance.image_list_detailed', - return_value=(TEST_DATA.glanceclient_images.list(), False)), # Inherited code calls this directly patch('openstack_dashboard.api.nova.flavor_list', return_value=all_flavors), @@ -108,26 +103,10 @@ class FlavorsTest(test.BaseAdminViewTests): self.assertMessageCount(response=res, error=2, warning=0) def test_create_get(self): - with patch('openstack_dashboard.api.glance.image_list_detailed', - return_value=([], False)) as mock: - res = self.client.get(CREATE_URL) - self.assertEqual(mock.call_count, 2) + res = self.client.get(CREATE_URL) self.assertTemplateUsed(res, 'infrastructure/flavors/create.html') - def test_create_get_recoverable_failure(self): - with patch('openstack_dashboard.api.glance.image_list_detailed', - side_effect=_raise_nova_client_exception): - res = self.client.get(CREATE_URL) - self.assertEqual( - [(m.message, m.tags) for m in res.context['messages']], - [ - (u'Unable to retrieve images list.', u'error'), - ], - ) - self.assertMessageCount(response=res, error=1, warning=0) - def test_create_post_ok(self): - images = TEST_DATA.glanceclient_images.list() with _prepare_create() as (create_mock, data): res = self.client.post(CREATE_URL, data) self.assertNoFormErrors(res) @@ -135,8 +114,7 @@ class FlavorsTest(test.BaseAdminViewTests): request = create_mock.call_args_list[0][0][0] self.assertListEqual(create_mock.call_args_list, [ call(request, name=u'foobar', memory=1024, vcpus=3, disk=40, - cpu_arch='amd64', kernel_image_id=images[5].id, - ramdisk_image_id=images[4].id) + cpu_arch='amd64') ]) def test_create_post_name_exists(self): @@ -158,8 +136,6 @@ class FlavorsTest(test.BaseAdminViewTests): return_value=[]), patch('tuskar_ui.api.tuskar.Plan.list', return_value=[]), - patch('openstack_dashboard.api.glance.image_list_detailed', - return_value=([], False)), patch('openstack_dashboard.api.nova.flavor_list', return_value=TEST_DATA.novaclient_flavors.list()) ): @@ -187,8 +163,6 @@ class FlavorsTest(test.BaseAdminViewTests): return_value=[]), patch('tuskar_ui.api.tuskar.Plan.list', return_value=[]), - patch('openstack_dashboard.api.glance.image_list_detailed', - return_value=([], False)), patch('openstack_dashboard.api.nova.flavor_list', return_value=TEST_DATA.novaclient_flavors.list()), patch('tuskar_ui.api.node.Node.list', @@ -201,24 +175,20 @@ class FlavorsTest(test.BaseAdminViewTests): def test_details_no_overcloud(self): flavor = api.flavor.Flavor(TEST_DATA.novaclient_flavors.first()) - images = TEST_DATA.glanceclient_images.list()[:2] plan = api.tuskar.Plan(TEST_DATA.tuskarclient_plans.first()) roles = [api.tuskar.Role(role) for role in self.tuskarclient_roles.list()] with contextlib.nested( - patch('openstack_dashboard.api.glance.image_get', - side_effect=images), patch('tuskar_ui.api.flavor.Flavor.get', return_value=flavor), patch('tuskar_ui.api.tuskar.Plan.get_the_plan', return_value=plan), patch('tuskar_ui.api.tuskar.Role.list', return_value=roles), patch('tuskar_ui.api.tuskar.Role.flavor', return_value=flavor), - ) as (image_mock, get_mock, plan_mock, roles_mock, role_flavor_mock): + ) as (get_mock, plan_mock, roles_mock, role_flavor_mock): res = self.client.get(urlresolvers.reverse(DETAILS_VIEW, args=(flavor.id,))) - self.assertEqual(image_mock.call_count, 1) self.assertEqual(get_mock.call_count, 1) self.assertEqual(plan_mock.call_count, 2) self.assertEqual(roles_mock.call_count, 1) @@ -227,15 +197,12 @@ class FlavorsTest(test.BaseAdminViewTests): def test_details(self): flavor = api.flavor.Flavor(TEST_DATA.novaclient_flavors.first()) - images = TEST_DATA.glanceclient_images.list()[:2] plan = api.tuskar.Plan(TEST_DATA.tuskarclient_plans.first()) roles = [api.tuskar.Role(role) for role in self.tuskarclient_roles.list()] stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) with contextlib.nested( - patch('openstack_dashboard.api.glance.image_get', - side_effect=images), patch('tuskar_ui.api.flavor.Flavor.get', return_value=flavor), patch('tuskar_ui.api.tuskar.Plan.get_the_plan', @@ -247,11 +214,10 @@ class FlavorsTest(test.BaseAdminViewTests): # __name__ is required for horizon.tables patch('tuskar_ui.api.heat.Stack.resources_count', return_value=42, __name__='') - ) as (image_mock, flavor_mock, plan_mock, roles_mock, role_flavor_mock, + ) as (flavor_mock, plan_mock, roles_mock, role_flavor_mock, stack_mock, count_mock): res = self.client.get(urlresolvers.reverse(DETAILS_VIEW, args=(flavor.id,))) - self.assertEqual(image_mock.call_count, 1) self.assertEqual(flavor_mock.call_count, 1) self.assertEqual(plan_mock.call_count, 2) self.assertEqual(roles_mock.call_count, 1) diff --git a/tuskar_ui/infrastructure/flavors/utils.py b/tuskar_ui/infrastructure/flavors/utils.py index e84831b27..cf2d63ddd 100644 --- a/tuskar_ui/infrastructure/flavors/utils.py +++ b/tuskar_ui/infrastructure/flavors/utils.py @@ -110,8 +110,7 @@ class FlavorSuggestion(object): ) ) - def create_flavor(self, request, - kernel_image_id=None, ramdisk_image_id=None): + def create_flavor(self, request): return api.flavor.Flavor.create( request, name=self.name, @@ -119,6 +118,4 @@ class FlavorSuggestion(object): vcpus=self.vcpus, disk=self.disk, cpu_arch=self.cpu_arch, - kernel_image_id=kernel_image_id, - ramdisk_image_id=ramdisk_image_id, ) diff --git a/tuskar_ui/infrastructure/flavors/views.py b/tuskar_ui/infrastructure/flavors/views.py index be31f9b26..b66d4aca9 100644 --- a/tuskar_ui/infrastructure/flavors/views.py +++ b/tuskar_ui/infrastructure/flavors/views.py @@ -92,14 +92,6 @@ class DetailView(horizon.tables.DataTableView): kwargs.get('flavor_id'), _error_redirect=self.error_redirect ) - context['kernel_image'] = api.node.image_get( - self.request, - context['flavor'].kernel_image_id - ) - context['ramdisk_image'] = api.node.image_get( - self.request, - context['flavor'].ramdisk_image_id - ) return context def get_data(self): diff --git a/tuskar_ui/infrastructure/flavors/workflows.py b/tuskar_ui/infrastructure/flavors/workflows.py index bfdacbb0b..6f1e1dee8 100644 --- a/tuskar_ui/infrastructure/flavors/workflows.py +++ b/tuskar_ui/infrastructure/flavors/workflows.py @@ -16,47 +16,19 @@ from django.forms import fields from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import workflows -from openstack_dashboard.api import glance from openstack_dashboard.dashboards.admin.flavors import ( workflows as flavor_workflows) from tuskar_ui import api -from tuskar_ui.utils import utils class CreateFlavorAction(flavor_workflows.CreateFlavorInfoAction): arch = fields.ChoiceField(choices=(('i386', 'i386'), ('amd64', 'amd64'), ('x86_64', 'x86_64')), label=_("Architecture")) - kernel_image_id = fields.ChoiceField(choices=(), - label=_("Deploy Kernel Image")) - ramdisk_image_id = fields.ChoiceField(choices=(), - label=_("Deploy Ramdisk Image")) def __init__(self, *args, **kwrds): super(CreateFlavorAction, self).__init__(*args, **kwrds) - try: - kernel_images = glance.image_list_detailed( - self.request, - )[0] - kernel_images = [image for image in kernel_images - if utils.check_image_type(image, - 'discovery kernel')] - ramdisk_images = glance.image_list_detailed( - self.request, - )[0] - ramdisk_images = [image for image in ramdisk_images - if utils.check_image_type(image, - 'discovery ramdisk')] - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve images list.')) - kernel_images = [] - ramdisk_images = [] - self.fields['kernel_image_id'].choices = [(img.id, img.name) - for img in kernel_images] - self.fields['ramdisk_image_id'].choices = [(img.id, img.name) - for img in ramdisk_images] # Delete what is not applicable to hardware del self.fields['eph_gb'] del self.fields['swap_mb'] @@ -79,9 +51,7 @@ class CreateFlavorStep(workflows.Step): "vcpus", "memory_mb", "disk_gb", - "arch", - "kernel_image_id", - "ramdisk_image_id") + "arch") class CreateFlavor(flavor_workflows.CreateFlavor): @@ -101,9 +71,7 @@ class CreateFlavor(flavor_workflows.CreateFlavor): memory=data['memory_mb'], vcpus=data['vcpus'], disk=data['disk_gb'], - cpu_arch=data['arch'], - kernel_image_id=data['kernel_image_id'], - ramdisk_image_id=data['ramdisk_image_id'] + cpu_arch=data['arch'] ) except Exception: exceptions.handle(request, _("Unable to create flavor")) diff --git a/tuskar_ui/infrastructure/nodes/forms.py b/tuskar_ui/infrastructure/nodes/forms.py index c4f721ef0..12d365cd1 100644 --- a/tuskar_ui/infrastructure/nodes/forms.py +++ b/tuskar_ui/infrastructure/nodes/forms.py @@ -23,6 +23,9 @@ from tuskar_ui import api import tuskar_ui.forms +DEFAULT_KERNEL_IMAGE_NAME = 'bm-deploy-kernel' +DEFAULT_RAMDISK_IMAGE_NAME = 'bm-deploy-ramdisk' + CPU_ARCH_CHOICES = [ ('', _("unspecified")), ('amd64', _("amd64")), @@ -37,7 +40,10 @@ DRIVER_CHOICES = [ def get_driver_info_dict(data): driver = data['driver'] - driver_dict = {'driver': driver} + driver_dict = {'driver': driver, + 'deployment_kernel': data['deployment_kernel'], + 'deployment_ramdisk': data['deployment_ramdisk'], + } if driver == 'pxe_ipmitool': driver_dict.update( ipmi_address=data['ipmi_address'], @@ -203,6 +209,18 @@ class NodeForm(django.forms.Form): widget=tuskar_ui.forms.NumberInput( attrs={'placeholder': _('unspecified')}), ) + deployment_kernel = django.forms.ChoiceField( + label=_("Kernel"), + required=False, + choices=[], + widget=django.forms.Select(), + ) + deployment_ramdisk = django.forms.ChoiceField( + label=_("Ramdisk"), + required=False, + choices=[], + widget=django.forms.Select(), + ) def get_name(self): try: @@ -244,6 +262,19 @@ class NodeForm(django.forms.Form): class BaseNodeFormset(tuskar_ui.forms.SelfHandlingFormset): + def __init__(self, *args, **kwargs): + self.kernel_images = kwargs.pop('kernel_images') + self.ramdisk_images = kwargs.pop('ramdisk_images') + super(BaseNodeFormset, self).__init__(*args, **kwargs) + + def add_fields(self, form, index): + deployment_kernel_choices = [(kernel.id, kernel.name) + for kernel in self.kernel_images] + deployment_ramdisk_choices = [(ramdisk.id, ramdisk.name) + for ramdisk in self.ramdisk_images] + form.fields['deployment_kernel'].choices = deployment_kernel_choices + form.fields['deployment_ramdisk'].choices = deployment_ramdisk_choices + def clean(self): all_macs = api.node.Node.get_all_mac_addresses(self.request) bad_macs = set() diff --git a/tuskar_ui/infrastructure/nodes/tabs.py b/tuskar_ui/infrastructure/nodes/tabs.py index 1931751d5..aa085ce64 100644 --- a/tuskar_ui/infrastructure/nodes/tabs.py +++ b/tuskar_ui/infrastructure/nodes/tabs.py @@ -307,6 +307,15 @@ class DetailOverviewTab(tabs.Tab): context['role'] = resource.role context['stack'] = resource.stack + context['kernel_image'] = api.node.image_get( + request, + node.driver_info['pxe_deploy_kernel'] + ) + context['ramdisk_image'] = api.node.image_get( + request, + node.driver_info['pxe_deploy_ramdisk'] + ) + if node.instance_uuid: if api_base.is_service_enabled(self.request, 'metering'): # Meter configuration in the following format: diff --git a/tuskar_ui/infrastructure/nodes/tests.py b/tuskar_ui/infrastructure/nodes/tests.py index b8bed8588..ee3a9dbbc 100644 --- a/tuskar_ui/infrastructure/nodes/tests.py +++ b/tuskar_ui/infrastructure/nodes/tests.py @@ -19,6 +19,7 @@ from ceilometerclient.v2 import client as ceilometer_client from django.core import urlresolvers from horizon import exceptions as horizon_exceptions from mock import patch, call, ANY # noqa +from novaclient import exceptions as nova_exceptions from openstack_dashboard.test.test_data import utils from tuskar_ui import api @@ -40,6 +41,10 @@ heat_data.data(TEST_DATA) tuskar_data.data(TEST_DATA) +def _raise_nova_client_exception(*args, **kwargs): + raise nova_exceptions.ClientException("Boom!") + + class NodesTests(test.BaseAdminViewTests): @handle_errors("Error!", []) def _raise_tuskar_exception(self, request, *args, **kwargs): @@ -127,13 +132,17 @@ class NodesTests(test.BaseAdminViewTests): self._test_index_tab_list_exception('maintenance') def test_register_get(self): - res = self.client.get(REGISTER_URL) + with patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=([], False)) as mock: + res = self.client.get(REGISTER_URL) + self.assertEqual(mock.call_count, 2) self.assertTemplateUsed( res, 'infrastructure/nodes/register.html') def test_register_post(self): node = TEST_DATA.ironicclient_nodes.first nodes = self._all_mocked_nodes() + images = self.glanceclient_images.list() data = { 'register_nodes-TOTAL_FORMS': 2, 'register_nodes-INITIAL_FORMS': 1, @@ -148,6 +157,8 @@ class NodesTests(test.BaseAdminViewTests): 'register_nodes-0-cpus': '1', 'register_nodes-0-memory_mb': '2', 'register_nodes-0-local_gb': '3', + 'register_nodes-0-deployment_kernel': images[6].id, + 'register_nodes-0-deployment_ramdisk': images[7].id, 'register_nodes-1-driver': 'pxe_ipmitool', 'register_nodes-1-ipmi_address': '127.0.0.2', @@ -156,12 +167,18 @@ class NodesTests(test.BaseAdminViewTests): 'register_nodes-1-cpus': '4', 'register_nodes-1-memory_mb': '5', 'register_nodes-1-local_gb': '6', + 'register_nodes-1-deployment_kernel': images[6].id, + 'register_nodes-1-deployment_ramdisk': images[7].id, } - with patch('tuskar_ui.api.node.Node', **{ - 'spec_set': ['create', 'get_all_mac_addresses'], - 'create.return_value': node, - 'get_all_mac_addresses.return_value': set(nodes), - }) as Node: + with contextlib.nested( + patch('tuskar_ui.api.node.Node', **{ + 'spec_set': ['create', 'get_all_mac_addresses'], + 'create.return_value': node, + 'get_all_mac_addresses.return_value': set(nodes), + }), + patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=[images, False, False]), + ) as (Node, mock_glance_images): res = self.client.post(REGISTER_URL, data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -177,6 +194,8 @@ class NodesTests(test.BaseAdminViewTests): ipmi_username=u'username', ipmi_password=u'password', driver='pxe_ipmitool', + deployment_kernel=images[6].id, + deployment_ramdisk=images[7].id, ), call( ANY, @@ -189,11 +208,14 @@ class NodesTests(test.BaseAdminViewTests): ipmi_username=None, ipmi_password=None, driver='pxe_ipmitool', + deployment_kernel=images[6].id, + deployment_ramdisk=images[7].id, ), ]) def test_register_post_exception(self): nodes = self._all_mocked_nodes() + images = self.glanceclient_images.list() data = { 'register_nodes-TOTAL_FORMS': 2, 'register_nodes-INITIAL_FORMS': 1, @@ -208,6 +230,8 @@ class NodesTests(test.BaseAdminViewTests): 'register_nodes-0-cpus': '1', 'register_nodes-0-memory_mb': '2', 'register_nodes-0-local_gb': '3', + 'register_nodes-0-deployment_kernel': images[6].id, + 'register_nodes-0-deployment_ramdisk': images[7].id, 'register_nodes-1-driver': 'pxe_ipmitool', 'register_nodes-1-ipmi_address': '127.0.0.2', @@ -216,12 +240,18 @@ class NodesTests(test.BaseAdminViewTests): 'register_nodes-1-cpus': '4', 'register_nodes-1-memory_mb': '5', 'register_nodes-1-local_gb': '6', + 'register_nodes-1-deployment_kernel': images[6].id, + 'register_nodes-1-deployment_ramdisk': images[7].id, } - with patch('tuskar_ui.api.node.Node', **{ - 'spec_set': ['create', 'get_all_mac_addresses'], - 'create.side_effect': self.exceptions.tuskar, - 'get_all_mac_addresses.return_value': set(nodes), - }) as Node: + with contextlib.nested( + patch('tuskar_ui.api.node.Node', **{ + 'spec_set': ['create', 'get_all_mac_addresses'], + 'create.side_effect': self.exceptions.tuskar, + 'get_all_mac_addresses.return_value': set(nodes), + }), + patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=[images, False, False]), + ) as (Node, mock_glance_images): res = self.client.post(REGISTER_URL, data) self.assertEqual(res.status_code, 200) self.assertListEqual(Node.create.call_args_list, [ @@ -236,6 +266,8 @@ class NodesTests(test.BaseAdminViewTests): ipmi_username=u'username', ipmi_password=u'password', driver='pxe_ipmitool', + deployment_kernel=images[6].id, + deployment_ramdisk=images[7].id, ), call( ANY, @@ -248,6 +280,8 @@ class NodesTests(test.BaseAdminViewTests): ipmi_username=None, ipmi_password=None, driver='pxe_ipmitool', + deployment_kernel=images[6].id, + deployment_ramdisk=images[7].id, ), ]) self.assertTemplateUsed( @@ -255,6 +289,7 @@ class NodesTests(test.BaseAdminViewTests): def test_node_detail(self): node = api.node.Node(self.ironicclient_nodes.list()[0]) + image = self.glanceclient_images.first() with contextlib.nested( patch('tuskar_ui.api.node.Node', **{ @@ -266,7 +301,9 @@ class NodesTests(test.BaseAdminViewTests): 'get_by_node.side_effect': lambda *args, **kwargs: {}[None], # Raises LookupError }), - ) as (mock_node, mock_heat): + patch('openstack_dashboard.api.glance.image_get', + return_value=image), + ) as (mock_node, mock_heat, mock_glance): res = self.client.get( urlresolvers.reverse(DETAIL_VIEW, args=(node.uuid,)) ) @@ -421,6 +458,8 @@ class NodesTests(test.BaseAdminViewTests): 'ipmi_address': '127.0.0.1', 'ipmi_username': 'root', 'ipmi_password': 'P@55W0rd', + 'deployment_kernel': '7', + 'deployment_ramdisk': '8', } ret = forms.get_driver_info_dict(data) self.assertEqual(ret, { @@ -428,12 +467,16 @@ class NodesTests(test.BaseAdminViewTests): 'ipmi_address': '127.0.0.1', 'ipmi_username': 'root', 'ipmi_password': 'P@55W0rd', + 'deployment_kernel': '7', + 'deployment_ramdisk': '8', }) data = { 'driver': 'pxe_ssh', 'ssh_address': '127.0.0.1', 'ssh_username': 'root', 'ssh_key_contents': 'P@55W0rd', + 'deployment_kernel': '7', + 'deployment_ramdisk': '8', } ret = forms.get_driver_info_dict(data) self.assertEqual(ret, { @@ -441,6 +484,8 @@ class NodesTests(test.BaseAdminViewTests): 'ssh_address': '127.0.0.1', 'ssh_username': 'root', 'ssh_key_contents': 'P@55W0rd', + 'deployment_kernel': '7', + 'deployment_ramdisk': '8', }) def test_create_node(self): @@ -454,6 +499,8 @@ class NodesTests(test.BaseAdminViewTests): 'ipmi_username': 'username', 'ipmi_password': 'password', 'driver': 'pxe_ipmitool', + 'deployment_kernel': '7', + 'deployment_ramdisk': '8', } with patch('tuskar_ui.api.node.Node', **{ 'spec_set': ['create', 'set_maintenance', 'discover'], @@ -472,5 +519,7 @@ class NodesTests(test.BaseAdminViewTests): ipmi_username=u'username', ipmi_password=u'password', driver='pxe_ipmitool', + deployment_kernel='7', + deployment_ramdisk='8', ), ]) diff --git a/tuskar_ui/infrastructure/nodes/views.py b/tuskar_ui/infrastructure/nodes/views.py index c962ed73f..48221ac23 100644 --- a/tuskar_ui/infrastructure/nodes/views.py +++ b/tuskar_ui/infrastructure/nodes/views.py @@ -19,9 +19,11 @@ import django.forms import django.http from django.utils.translation import ugettext_lazy as _ from django.views.generic import base +from horizon import exceptions from horizon import forms as horizon_forms from horizon import tabs as horizon_tabs from horizon.utils import memoized +from openstack_dashboard.api import glance from tuskar_ui import api from tuskar_ui.infrastructure.nodes import forms @@ -29,6 +31,36 @@ from tuskar_ui.infrastructure.nodes import tables from tuskar_ui.infrastructure.nodes import tabs import tuskar_ui.infrastructure.views as infrastructure_views from tuskar_ui.utils import metering as metering_utils +from tuskar_ui.utils import utils + + +def get_kernel_images(request): + try: + kernel_images = glance.image_list_detailed( + request, + )[0] + kernel_images = [image for image in kernel_images + if utils.check_image_type(image, 'deploy kernel')] + except Exception: + exceptions.handle(request, + _('Unable to retrieve kernel image list.')) + kernel_images = [] + return kernel_images + + +def get_ramdisk_images(request): + try: + ramdisk_images = glance.image_list_detailed( + request, + )[0] + ramdisk_images = [image for image in ramdisk_images + if utils.check_image_type( + image, 'deploy ramdisk')] + except Exception: + exceptions.handle(request, + _('Unable to retrieve ramdisk image list.')) + ramdisk_images = [] + return ramdisk_images class IndexView(infrastructure_views.ItemCountMixin, @@ -78,7 +110,9 @@ class RegisterView(horizon_forms.ModalFormView): formset = forms.RegisterNodeFormset( self.request.POST, prefix=self.form_prefix, - request=self.request + request=self.request, + kernel_images=get_kernel_images(self.request), + ramdisk_images=get_ramdisk_images(self.request) ) if formset.is_valid(): initial += formset.cleaned_data @@ -86,7 +120,9 @@ class RegisterView(horizon_forms.ModalFormView): None, initial=initial, prefix=self.form_prefix, - request=self.request + request=self.request, + kernel_images=get_kernel_images(self.request), + ramdisk_images=get_ramdisk_images(self.request) ) formset.extra = 0 return formset @@ -94,7 +130,9 @@ class RegisterView(horizon_forms.ModalFormView): self.request.POST or None, initial=initial, prefix=self.form_prefix, - request=self.request + request=self.request, + kernel_images=get_kernel_images(self.request), + ramdisk_images=get_ramdisk_images(self.request) ) def get_context_data(self, **kwargs): @@ -109,6 +147,7 @@ class DetailView(horizon_tabs.TabView): def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) + node = self.get_data() if node.maintenance: diff --git a/tuskar_ui/infrastructure/templates/infrastructure/nodes/_detail_overview.html b/tuskar_ui/infrastructure/templates/infrastructure/nodes/_detail_overview.html index 34a0e1e98..e000da055 100644 --- a/tuskar_ui/infrastructure/templates/infrastructure/nodes/_detail_overview.html +++ b/tuskar_ui/infrastructure/templates/infrastructure/nodes/_detail_overview.html @@ -85,6 +85,14 @@
{{ node.instance_uuid|default:"—" }}
+

{% trans "Deployment Images" %}

+
+
{% trans "Kernel" %}
+
{{ kernel_image.name|default:"—" }}
+
{% trans "Ramdisk" %}
+
{{ ramdisk_image.name|default:"—" }}
+
+

{% trans "Performance & Metrics" %}

diff --git a/tuskar_ui/infrastructure/templates/infrastructure/nodes/_nodes_formset_form.html b/tuskar_ui/infrastructure/templates/infrastructure/nodes/_nodes_formset_form.html index 270b41367..c828125db 100644 --- a/tuskar_ui/infrastructure/templates/infrastructure/nodes/_nodes_formset_form.html +++ b/tuskar_ui/infrastructure/templates/infrastructure/nodes/_nodes_formset_form.html @@ -27,6 +27,11 @@ {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.memory_mb extra_text=_('MB') %} {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.local_gb extra_text=_('GB') %}
+
+
{% trans "Deployment Images" %}
+ {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.deployment_kernel %} + {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.deployment_ramdisk %} +
diff --git a/tuskar_ui/infrastructure/templates/infrastructure/nodes/register.html b/tuskar_ui/infrastructure/templates/infrastructure/nodes/register.html index e83d3e7a1..3e9cb1f5a 100644 --- a/tuskar_ui/infrastructure/templates/infrastructure/nodes/register.html +++ b/tuskar_ui/infrastructure/templates/infrastructure/nodes/register.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'infrastructure/base.html' %} {% load i18n %} {% block title %}{% trans "Register Nodes" %}{% endblock %} diff --git a/tuskar_ui/test/test_data/heat_data.py b/tuskar_ui/test/test_data/heat_data.py index a8aa7135b..6af939b07 100644 --- a/tuskar_ui/test/test_data/heat_data.py +++ b/tuskar_ui/test/test_data/heat_data.py @@ -254,5 +254,23 @@ def data(TEST): 'properties': { 'type': 'discovery kernel' }}) + image_7 = images.Image( + images.ImageManager(None), + {'id': '7', + 'name': 'Baremetal Deployment Kernel', + 'is_public': True, + 'protected': False, + 'properties': { + 'type': 'deploy kernel' + }}) + image_8 = images.Image( + images.ImageManager(None), + {'id': '8', + 'name': 'Baremetal Deployment Ramdisk', + 'is_public': True, + 'protected': False, + 'properties': { + 'type': 'deploy ramdisk' + }}) TEST.glanceclient_images.add(image_1, image_2, image_3, image_4, - image_5, image_6) + image_5, image_6, image_7, image_8) diff --git a/tuskar_ui/test/test_data/node_data.py b/tuskar_ui/test/test_data/node_data.py index e3f6fb511..dcbdec179 100644 --- a/tuskar_ui/test/test_data/node_data.py +++ b/tuskar_ui/test/test_data/node_data.py @@ -124,7 +124,9 @@ def data(TEST): 'ipmi_address': '1.1.1.1', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.2' + 'ip_address': '1.2.2.2', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '8', @@ -149,7 +151,9 @@ def data(TEST): 'ipmi_address': '2.2.2.2', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.3' + 'ip_address': '1.2.2.3', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '16', @@ -174,7 +178,9 @@ def data(TEST): 'ipmi_address': '3.3.3.3', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.4' + 'ip_address': '1.2.2.4', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '32', @@ -199,7 +205,9 @@ def data(TEST): 'ipmi_address': '4.4.4.4', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.5' + 'ip_address': '1.2.2.5', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '8', @@ -224,7 +232,9 @@ def data(TEST): 'ipmi_address': '5.5.5.5', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.6' + 'ip_address': '1.2.2.6', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '8', @@ -249,7 +259,9 @@ def data(TEST): 'ipmi_address': '5.5.5.5', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.6' + 'ip_address': '1.2.2.6', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '8', @@ -274,7 +286,9 @@ def data(TEST): 'ipmi_address': '7.7.7.7', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.7' + 'ip_address': '1.2.2.7', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '8', @@ -299,7 +313,9 @@ def data(TEST): 'ipmi_address': '8.8.8.8', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.8' + 'ip_address': '1.2.2.8', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '8', @@ -324,7 +340,9 @@ def data(TEST): 'ipmi_address': '9.9.9.9', 'ipmi_username': 'admin', 'ipmi_password': 'password', - 'ip_address': '1.2.2.9' + 'ip_address': '1.2.2.9', + 'pxe_deploy_kernel': 'deploy-kernel-uuid', + 'pxe_deploy_ramdisk': 'deploy-ramdisk-uuid', }, 'properties': { 'cpus': '16',