diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py index 1254c6dd5..d5679022a 100644 --- a/tuskar_ui/api.py +++ b/tuskar_ui/api.py @@ -769,3 +769,14 @@ class OvercloudRole(base.APIResourceWrapper): """ role = tuskarclient(request).overcloud_roles.get(role_id) return cls(role) + + def update(self, request, **kwargs): + """Update the selected attributes of Tuskar OvercloudRole. + + :param request: request object + :type request: django.http.HttpRequest + """ + for attr in kwargs: + if attr not in self._attrs: + raise TypeError('Invalid parameter %r' % attr) + tuskarclient(request).overcloud_roles.update(self.id, **kwargs) diff --git a/tuskar_ui/infrastructure/overcloud/forms.py b/tuskar_ui/infrastructure/overcloud/forms.py index f150d6d69..e9b9efc2e 100644 --- a/tuskar_ui/infrastructure/overcloud/forms.py +++ b/tuskar_ui/infrastructure/overcloud/forms.py @@ -17,6 +17,7 @@ from django.utils.translation import ugettext_lazy as _ import horizon.exceptions import horizon.forms import horizon.messages +from openstack_dashboard import api as horizon_api from tuskar_ui import api @@ -35,11 +36,15 @@ class UndeployOvercloud(horizon.forms.SelfHandlingForm): return True -# TODO(rdopieralski) Get the list of flavors -def get_flavors(): - yield (None, '----') - yield ('xxx', 'Some Hardware Profile') - yield ('yyy', 'Other Hardware Profile') +def get_flavor_choices(request): + empty = [('', '----')] + try: + flavors = horizon_api.nova.flavor_list(request, None) + except Exception: + horizon.exceptions.handle(request, + _('Unable to retrieve flavor list.')) + return empty + return empty + [(flavor.id, flavor.name) for flavor in flavors] class OvercloudRoleForm(horizon.forms.SelfHandlingForm): @@ -58,8 +63,18 @@ class OvercloudRoleForm(horizon.forms.SelfHandlingForm): widget=django.forms.TextInput( attrs={'readonly': 'readonly', 'disabled': 'disabled'})) flavor_id = django.forms.ChoiceField( - label=_("Node Profile"), required=False, choices=get_flavors()) + label=_("Node Profile"), required=False, choices=()) + + def __init__(self, *args, **kwargs): + super(OvercloudRoleForm, self).__init__(*args, **kwargs) + self.fields['flavor_id'].choices = get_flavor_choices(self.request) def handle(self, request, context): - # TODO(rdopieralski) Associate the flavor with the role + try: + role = api.OvercloudRole.get(request, context['id']) + role.update(request, flavor_id=context['flavor_id']) + except Exception: + horizon.exceptions.handle(request, + _('Unable to update the role.')) + return False return True diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html index 210cd26fa..e69403673 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html @@ -16,21 +16,25 @@ {% if forloop.first %} - + {% if editable %} + + {% endif %} {{ label }} {% endif %} {% if field.field.label %} {{ field.label }} - {% else %} + {% elif editable %} ({% trans "Add a node profile" %}) + {% else %} + ({% trans "No node profile" %}) {% endif %} diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html index cb09a429d..5c02e2ecd 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html @@ -11,7 +11,7 @@

{% trans "Roles" %}

- {% include 'infrastructure/overcloud/node_counts.html' with form=form %} + {% include 'infrastructure/overcloud/node_counts.html' with form=form editable=True %}
diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py index a5ffc77ee..69fef3bdc 100644 --- a/tuskar_ui/infrastructure/overcloud/tests.py +++ b/tuskar_ui/infrastructure/overcloud/tests.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import contextlib from django.core import urlresolvers @@ -101,13 +102,20 @@ class OvercloudTests(test.BaseAdminViewTests): def test_create_get(self): roles = TEST_DATA.tuskarclient_overcloud_roles.list() - with contextlib.nested(patch('tuskar_ui.api.OvercloudRole', **{ - 'spec_set': ['list'], - 'list.return_value': roles, - }), patch('tuskar_ui.api.Node', **{ - 'spec_set': ['list'], - 'list.return_value': [], - })): + with contextlib.nested( + patch('tuskar_ui.api.OvercloudRole', **{ + 'spec_set': ['list'], + 'list.return_value': roles, + }), + patch('tuskar_ui.api.Node', **{ + 'spec_set': ['list'], + 'list.return_value': [], + }), + patch('openstack_dashboard.api.nova', **{ + 'spec_set': ['flavor_list'], + 'flavor_list.return_value': [], + }), + ): res = self.client.get(CREATE_URL) self.assertTemplateUsed( res, 'infrastructure/_fullscreen_workflow_base.html') @@ -117,22 +125,30 @@ class OvercloudTests(test.BaseAdminViewTests): def test_create_post(self): oc = None roles = TEST_DATA.tuskarclient_overcloud_roles.list() + old_flavor_id = roles[0].flavor_id + roles[0].flavor_id = 'default' data = { 'count__1__default': '1', - 'count__2__default': '0', - 'count__3__default': '0', - 'count__4__default': '0', + 'count__2__': '0', + 'count__3__': '0', + 'count__4__': '0', } with contextlib.nested( patch('tuskar_ui.api.OvercloudRole', **{ 'spec_set': ['list'], - 'list.side_effect': lambda request: roles, - }), - patch('tuskar_ui.api.Overcloud', **{ + 'list.return_value': roles, + }), patch('tuskar_ui.api.Overcloud', **{ 'spec_set': ['create'], - 'create.return_value': oc, + 'create.side_effect': lambda *args: oc, + }), patch('tuskar_ui.api.Node', **{ + 'spec_set': ['list'], + 'list.return_value': [], }), - ) as (OvercloudRole, Overcloud): + patch('openstack_dashboard.api.nova', **{ + 'spec_set': ['flavor_list'], + 'flavor_list.return_value': [], + }), + ) as (OvercloudRole, Overcloud, Node, nova): oc = Overcloud res = self.client.post(CREATE_URL, data) request = Overcloud.create.call_args_list[0][0][0] @@ -141,9 +157,9 @@ class OvercloudTests(test.BaseAdminViewTests): [ call(request, { ('1', 'default'): 1, - ('2', 'default'): 0, - ('3', 'default'): 0, - ('4', 'default'): 0, + ('2', ''): 0, + ('3', ''): 0, + ('4', ''): 0, }, { 'NeutronPublicInterfaceRawDevice': '', 'NovaComputeDriver': '', @@ -175,6 +191,7 @@ class OvercloudTests(test.BaseAdminViewTests): 'Flavor': '', }), ]) + roles[0].flavor_id = old_flavor_id self.assertRedirectsNoFollow(res, INDEX_URL) def test_detail_get(self): @@ -308,7 +325,11 @@ class OvercloudTests(test.BaseAdminViewTests): for role in roles ], }), - ) as (OvercloudRole, Overcloud): + patch('openstack_dashboard.api.nova', **{ + 'spec_set': ['flavor_list'], + 'flavor_list.return_value': [], + }), + ) as (OvercloudRole, Overcloud, nova): oc = Overcloud url = urlresolvers.reverse( 'horizon:infrastructure:overcloud:scale', args=(oc.id,)) @@ -319,12 +340,14 @@ class OvercloudTests(test.BaseAdminViewTests): def test_scale_post(self): oc = None roles = TEST_DATA.tuskarclient_overcloud_roles.list() + old_flavor_id = roles[0].flavor_id + roles[0].flavor_id = 'default' data = { 'overcloud_id': '1', 'count__1__default': '1', - 'count__2__default': '0', - 'count__3__default': '0', - 'count__4__default': '0', + 'count__2__': '0', + 'count__3__': '0', + 'count__4__': '0', } with contextlib.nested( patch('tuskar_ui.api.OvercloudRole', **{ @@ -341,7 +364,11 @@ class OvercloudTests(test.BaseAdminViewTests): for role in roles ], }), - ) as (OvercloudRole, Overcloud): + patch('openstack_dashboard.api.nova', **{ + 'spec_set': ['flavor_list'], + 'flavor_list.return_value': [], + }), + ) as (OvercloudRole, Overcloud, nova): oc = Overcloud url = urlresolvers.reverse( 'horizon:infrastructure:overcloud:scale', args=(oc.id,)) @@ -353,21 +380,28 @@ class OvercloudTests(test.BaseAdminViewTests): # [ # call(request, { # ('1', 'default'): 1, - # ('2', 'default'): 0, - # ('3', 'default'): 0, - # ('4', 'default'): 0, + # ('2', ''): 0, + # ('3', ''): 0, + # ('4', ''): 0, # }), # ]) + roles[0].flavor_id = old_flavor_id self.assertRedirectsNoFollow(res, DETAIL_URL) def test_role_edit_get(self): role = TEST_DATA.tuskarclient_overcloud_roles.first() url = urlresolvers.reverse( 'horizon:infrastructure:overcloud:role_edit', args=(role.id,)) - with patch('tuskar_ui.api.OvercloudRole', **{ - 'spec_set': ['get'], - 'get.return_value': role, - }): + with contextlib.nested( + patch('tuskar_ui.api.OvercloudRole', **{ + 'spec_set': ['get'], + 'get.return_value': role, + }), + patch('openstack_dashboard.api.nova', **{ + 'spec_set': ['flavor_list'], + 'flavor_list.return_value': [], + }), + ): res = self.client.get(url) self.assertTemplateUsed( res, 'infrastructure/overcloud/role_edit.html') @@ -375,17 +409,42 @@ class OvercloudTests(test.BaseAdminViewTests): res, 'infrastructure/overcloud/_role_edit.html') def test_role_edit_post(self): - role = TEST_DATA.tuskarclient_overcloud_roles.first() - url = urlresolvers.reverse( - 'horizon:infrastructure:overcloud:role_edit', args=(role.id,)) - data = { - 'id': '1', - 'flavor_id': 'xxx', - } - with patch('tuskar_ui.api.OvercloudRole', **{ - 'spec_set': ['get'], - 'get.return_value': role, - }): - # TODO(rdopieralski) Check if the role got associated with flavor. + role = None + Flavor = collections.namedtuple('Flavor', 'id name') + flavor = Flavor('xxx', 'Xxx') + with contextlib.nested( + patch('tuskar_ui.api.OvercloudRole', **{ + 'spec_set': [ + 'get', + 'update', + 'id', + 'name', + 'description', + 'image_name', + 'flavor_id', + ], + 'get.side_effect': lambda *args: role, + 'name': 'Compute', + 'description': '...', + 'image_name': '', + 'id': 1, + 'flavor_id': '', + }), + patch('openstack_dashboard.api.nova', **{ + 'spec_set': ['flavor_list'], + 'flavor_list.return_value': [flavor], + }), + ) as (OvercloudRole, nova): + role = OvercloudRole + url = urlresolvers.reverse( + 'horizon:infrastructure:overcloud:role_edit', args=(role.id,)) + data = { + 'id': str(role.id), + 'flavor_id': flavor.id, + } res = self.client.post(url, data) + request = OvercloudRole.update.call_args_list[0][0][0] + self.assertListEqual( + OvercloudRole.update.call_args_list, + [call(request, flavor_id=flavor.id)]) self.assertRedirectsNoFollow(res, CREATE_URL) diff --git a/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py b/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py index b9552e112..a66f9c466 100644 --- a/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py +++ b/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py @@ -16,6 +16,7 @@ import django.forms from django.utils.translation import ugettext_lazy as _ from horizon.utils import memoized import horizon.workflows +from openstack_dashboard import api as horizon_api from tuskar_ui import api import tuskar_ui.forms @@ -39,8 +40,31 @@ class Action(horizon.workflows.Action): slug = 'undeployed_overview' name = _("Overview") + def _get_profile_names(self): + # Get all flavors in one call, instead of getting them one by one. + try: + flavors = horizon_api.nova.flavor_list(self.request, None) + except Exception: + horizon.exceptions.handle(self.request, + _('Unable to retrieve flavor list.')) + flavors = [] + return dict((str(flavor.id), flavor.name) for flavor in flavors) + + def _get_profiles(self, role, profile_names): + # TODO(rdopieralski) Get a list of hardware profiles for each + # role here, when we support multiple profiles per role. + if role.flavor_id: + profiles = [( + role.flavor_id, + profile_names.get(str(role.flavor_id), role.flavor_id), + )] + else: + profiles = [] + return profiles + def __init__(self, *args, **kwargs): super(Action, self).__init__(*args, **kwargs) + profile_names = self._get_profile_names() for role in self._get_roles(): if role.name == 'Controller': initial = 1 @@ -48,18 +72,16 @@ class Action(horizon.workflows.Action): else: initial = 0 attrs = {} - # TODO(rdopieralski) Get a list of hardware profiles for each - # role here. - profiles = [(_("Default"), 'default')] + profiles = self._get_profiles(role, profile_names) if not profiles: name = get_field_name_from_role_id_and_profile_id(str(role.id)) attrs = {'readonly': 'readonly'} self.fields[name] = django.forms.IntegerField( label='', initial=initial, min_value=initial, widget=tuskar_ui.forms.NumberPickerInput(attrs=attrs)) - for label, profile in profiles: + for profile_id, label in profiles: name = get_field_name_from_role_id_and_profile_id( - str(role.id), profile) + str(role.id), profile_id) self.fields[name] = django.forms.IntegerField( label=label, initial=initial, min_value=initial, widget=tuskar_ui.forms.NumberPickerInput(attrs=attrs)) diff --git a/tuskar_ui/test/test_data/tuskar_data.py b/tuskar_ui/test/test_data/tuskar_data.py index 733a4f537..0567623c3 100644 --- a/tuskar_ui/test/test_data/tuskar_data.py +++ b/tuskar_ui/test/test_data/tuskar_data.py @@ -369,25 +369,28 @@ def data(TEST): 'name': 'Controller', 'description': 'controller overcloud role', 'image_name': 'overcloud-control', - 'flavor_id': None, + 'flavor_id': '', }) r_2 = overcloud_roles.OvercloudRole( overcloud_roles.OvercloudRoleManager(None), {'id': 2, 'name': 'Compute', 'description': 'compute overcloud role', + 'flavor_id': '', 'image_name': 'overcloud-compute'}) r_3 = overcloud_roles.OvercloudRole( overcloud_roles.OvercloudRoleManager(None), {'id': 3, 'name': 'Object Storage', 'description': 'object storage overcloud role', + 'flavor_id': '', 'image_name': 'overcloud-object-storage'}) r_4 = overcloud_roles.OvercloudRole( overcloud_roles.OvercloudRoleManager(None), {'id': 4, 'name': 'Block Storage', 'description': 'block storage overcloud role', + 'flavor_id': '', 'image_name': 'overcloud-block-storage'}) TEST.tuskarclient_overcloud_roles.add(r_1, r_2, r_3, r_4)