From f8660bd89321af0185c9e5af7b3d984b6d4e0ba0 Mon Sep 17 00:00:00 2001 From: Chad Roberts Date: Thu, 8 Oct 2015 14:52:07 -0400 Subject: [PATCH] Adding cluster template support for shares The Sahara data processing service now supports manila shares when creating or editing cluster templates. Change-Id: I32c44b6a7fa860df41e06d36c77e600b84bd5733 Partial-Implements: bp sahara-add-shares-to-clusters --- sahara_dashboard/api/sahara.py | 11 +-- .../cluster_templates/tests.py | 6 +- .../cluster_templates/workflows/copy.py | 25 +++++++ .../cluster_templates/workflows/create.py | 71 +++++++++++++++++-- .../cluster_templates/workflows/edit.py | 7 +- .../nodegroup_templates/workflows/create.py | 65 +---------------- .../data_processing/utils/workflow_helpers.py | 62 ++++++++++++++++ .../test/test_data/sahara_data.py | 1 + 8 files changed, 174 insertions(+), 74 deletions(-) diff --git a/sahara_dashboard/api/sahara.py b/sahara_dashboard/api/sahara.py index 78c56ffd..fd60f058 100644 --- a/sahara_dashboard/api/sahara.py +++ b/sahara_dashboard/api/sahara.py @@ -217,7 +217,8 @@ def nodegroup_template_update(request, ngt_id, name, plugin_name, def cluster_template_create(request, name, plugin_name, hadoop_version, description=None, cluster_configs=None, node_groups=None, anti_affinity=None, - net_id=None, use_autoconfig=None): + net_id=None, use_autoconfig=None, + shares=None): return client(request).cluster_templates.create( name=name, plugin_name=plugin_name, @@ -227,7 +228,8 @@ def cluster_template_create(request, name, plugin_name, hadoop_version, node_groups=node_groups, anti_affinity=anti_affinity, net_id=net_id, - use_autoconfig=use_autoconfig) + use_autoconfig=use_autoconfig, + shares=shares) def cluster_template_list(request, search_opts=None): @@ -246,7 +248,7 @@ def cluster_template_update(request, ct_id, name, plugin_name, hadoop_version, description=None, cluster_configs=None, node_groups=None, anti_affinity=None, net_id=None, - use_autoconfig=None): + use_autoconfig=None, shares=None): try: template = client(request).cluster_templates.update( cluster_template_id=ct_id, @@ -258,7 +260,8 @@ def cluster_template_update(request, ct_id, name, plugin_name, node_groups=node_groups, anti_affinity=anti_affinity, net_id=net_id, - use_autoconfig=use_autoconfig) + use_autoconfig=use_autoconfig, + shares=shares) except APIException as e: raise exceptions.Conflict(e) diff --git a/sahara_dashboard/content/data_processing/cluster_templates/tests.py b/sahara_dashboard/content/data_processing/cluster_templates/tests.py index 974b25f8..10d6acbc 100644 --- a/sahara_dashboard/content/data_processing/cluster_templates/tests.py +++ b/sahara_dashboard/content/data_processing/cluster_templates/tests.py @@ -21,9 +21,10 @@ from oslo_serialization import jsonutils import six from openstack_dashboard import api as dash_api -from sahara_dashboard.test import helpers as test from sahara_dashboard import api +from sahara_dashboard.test import helpers as test + INDEX_URL = reverse('horizon:project:data_processing.cluster_templates:index') DETAILS_URL = reverse( @@ -132,7 +133,8 @@ class DataProcessingClusterTemplateTests(test.TestCase): cluster_configs=ct.cluster_configs, node_groups=ct.node_groups, anti_affinity=ct.anti_affinity, - use_autoconfig=False)\ + use_autoconfig=False, + shares=ct.shares)\ .AndReturn(new_ct) self.mox.ReplayAll() diff --git a/sahara_dashboard/content/data_processing/cluster_templates/workflows/copy.py b/sahara_dashboard/content/data_processing/cluster_templates/workflows/copy.py index 537cbb83..e579ae8c 100644 --- a/sahara_dashboard/content/data_processing/cluster_templates/workflows/copy.py +++ b/sahara_dashboard/content/data_processing/cluster_templates/workflows/copy.py @@ -94,6 +94,31 @@ class CopyClusterTemplate(create_flow.ConfigureClusterTemplate): fields['use_autoconfig'].initial = ( self.template.use_autoconfig) fields["description"].initial = self.template.description + elif isinstance(step, create_flow.SelectClusterShares): + fields = step.action.fields + fields["shares"].initial = ( + self._get_share_defaults(fields["shares"].choices) + ) except Exception: exceptions.handle(request, _("Unable to fetch template to copy.")) + + def _get_share_defaults(self, choices): + values = dict() + for i, choice in enumerate(choices): + share_id = choice[0] + s = filter(lambda s: s['id'] == share_id, self.template.shares) + if len(s) > 0: + path = s[0]["path"] if "path" in s[0] else "" + values["share_id_{0}".format(i)] = { + "id": s[0]["id"], + "path": path, + "access_level": s[0]["access_level"] + } + else: + values["share_id_{0}".format(i)] = { + "id": None, + "path": None, + "access_level": None + } + return values diff --git a/sahara_dashboard/content/data_processing/cluster_templates/workflows/create.py b/sahara_dashboard/content/data_processing/cluster_templates/workflows/create.py index e1789e03..a79797f5 100644 --- a/sahara_dashboard/content/data_processing/cluster_templates/workflows/create.py +++ b/sahara_dashboard/content/data_processing/cluster_templates/workflows/create.py @@ -21,15 +21,16 @@ from saharaclient.api import base as api_base from horizon import exceptions from horizon import forms from horizon import workflows + +from sahara_dashboard.api import manila as manilaclient from sahara_dashboard.api import sahara as saharaclient -from sahara_dashboard.content.data_processing. \ - utils import helpers as helpers + +from sahara_dashboard.content.data_processing.utils import helpers as helpers from sahara_dashboard.content.data_processing. \ utils import anti_affinity as aa import sahara_dashboard.content.data_processing. \ utils.workflow_helpers as whelpers - LOG = logging.getLogger(__name__) @@ -238,6 +239,60 @@ class ConfigureNodegroups(workflows.Step): return context +class SelectClusterSharesAction(workflows.Action): + def __init__(self, request, *args, **kwargs): + super(SelectClusterSharesAction, self).__init__( + request, *args, **kwargs) + + possible_shares = self.get_possible_shares(request) + + self.fields["shares"] = whelpers.MultipleShareChoiceField( + label=_("Select Shares"), + widget=whelpers.ShareWidget(choices=possible_shares), + required=False, + choices=possible_shares + ) + + def get_possible_shares(self, request): + try: + shares = manilaclient.share_list(request) + choices = [(s.id, s.name) for s in shares] + except Exception: + exceptions.handle(request, _("Failed to get list of shares")) + choices = [] + return choices + + def clean(self): + cleaned_data = super(SelectClusterSharesAction, self).clean() + self._errors = dict() + return cleaned_data + + class Meta(object): + name = _("Shares") + help_text = _("Select the manila shares for this cluster") + + +class SelectClusterShares(workflows.Step): + action_class = SelectClusterSharesAction + + def contribute(self, data, context): + post = self.workflow.request.POST + shares_details = [] + for index in range(0, len(self.action.fields['shares'].choices) * 3): + if index % 3 == 0: + share = post.get("shares_{0}".format(index)) + if share: + path = post.get("shares_{0}".format(index + 1)) + permissions = post.get("shares_{0}".format(index + 2)) + shares_details.append({ + "id": share, + "path": path, + "access_level": permissions + }) + context['ct_shares'] = shares_details + return context + + class ConfigureClusterTemplate(whelpers.ServiceParametersWorkflow, whelpers.StatusFormatMixin): slug = "configure_cluster_template" @@ -262,6 +317,9 @@ class ConfigureClusterTemplate(whelpers.ServiceParametersWorkflow, plugin, hadoop_version) + if saharaclient.base.is_service_enabled(request, 'share'): + ConfigureClusterTemplate._register_step(self, SelectClusterShares) + self._populate_tabs(general_parameters, service_parameters) super(ConfigureClusterTemplate, self).__init__(request, @@ -308,6 +366,10 @@ class ConfigureClusterTemplate(whelpers.ServiceParametersWorkflow, plugin, hadoop_version = whelpers.\ get_plugin_and_hadoop_version(request) + ct_shares = [] + if "ct_shares" in context: + ct_shares = context["ct_shares"] + # TODO(nkonovalov): Fix client to support default_image_id saharaclient.cluster_template_create( request, @@ -318,7 +380,8 @@ class ConfigureClusterTemplate(whelpers.ServiceParametersWorkflow, configs_dict, node_groups, context["anti_affinity_info"], - use_autoconfig=context['general_use_autoconfig'] + use_autoconfig=context['general_use_autoconfig'], + shares=ct_shares ) hlps = helpers.Helpers(request) diff --git a/sahara_dashboard/content/data_processing/cluster_templates/workflows/edit.py b/sahara_dashboard/content/data_processing/cluster_templates/workflows/edit.py index 1e4a7f5a..b634d1f2 100644 --- a/sahara_dashboard/content/data_processing/cluster_templates/workflows/edit.py +++ b/sahara_dashboard/content/data_processing/cluster_templates/workflows/edit.py @@ -81,6 +81,10 @@ class EditClusterTemplate(copy_flow.CopyClusterTemplate): plugin, hadoop_version = whelpers. \ get_plugin_and_hadoop_version(request) + ct_shares = [] + if "ct_shares" in context: + ct_shares = context["ct_shares"] + saharaclient.cluster_template_update( request=request, ct_id=self.cluster_template_id, @@ -91,7 +95,8 @@ class EditClusterTemplate(copy_flow.CopyClusterTemplate): cluster_configs=configs_dict, node_groups=node_groups, anti_affinity=context["anti_affinity_info"], - use_autoconfig=context['general_use_autoconfig'] + use_autoconfig=context['general_use_autoconfig'], + shares=ct_shares ) return True except exceptions.Conflict as e: diff --git a/sahara_dashboard/content/data_processing/nodegroup_templates/workflows/create.py b/sahara_dashboard/content/data_processing/nodegroup_templates/workflows/create.py index 6a0c9131..d2ff4aa7 100644 --- a/sahara_dashboard/content/data_processing/nodegroup_templates/workflows/create.py +++ b/sahara_dashboard/content/data_processing/nodegroup_templates/workflows/create.py @@ -15,7 +15,6 @@ import itertools import logging import uuid -from django.core.exceptions import ValidationError from django.utils import encoding from django.utils import html from django.utils import safestring @@ -334,66 +333,6 @@ class SelectNodeProcessesAction(workflows.Action): help_text = _("Select node processes for the node group") -class ShareWidget(forms.MultiWidget): - def __init__(self, choices=()): - widgets = [] - for choice in choices: - widgets.append(forms.CheckboxInput( - attrs={ - "label": choice[1], - "value": choice[0], - })) - widgets.append(forms.TextInput()) - widgets.append(forms.Select( - choices=(("rw", _("Read/Write")), ("ro", _("Read only"))))) - super(ShareWidget, self).__init__(widgets) - - def decompress(self, value): - if value: - values = [] - for share in value: - values.append(value[share]["id"]) - values.append(value[share]["path"]) - values.append(value[share]["access_level"]) - return values - return [None] * len(self.widgets) - - def format_output(self, rendered_widgets): - output = [] - output.append("") - output.append("" - "") - for i, widget in enumerate(rendered_widgets): - item_widget_index = i % 3 - if item_widget_index == 0: - output.append("") - output.append( - "".format( - self.widgets[i].attrs["label"])) - # The last 2 form field td need get a larger size - if item_widget_index in [1, 2]: - size = 4 - else: - size = 2 - output.append("") - if item_widget_index == 2: - output.append("") - output.append("
ShareEnabledPathPermissions
{0}".format(size) - + widget + "
") - return safestring.mark_safe('\n'.join(output)) - - -class MultipleShareChoiceField(forms.MultipleChoiceField): - def validate(self, value): - if self.required and not value: - raise ValidationError( - self.error_messages['required'], code='required') - if not isinstance(value, list): - raise ValidationError( - _("The value of shares must be a list of values") - ) - - class SelectNodeGroupSharesAction(workflows.Action): def __init__(self, request, *args, **kwargs): super(SelectNodeGroupSharesAction, self).__init__( @@ -401,9 +340,9 @@ class SelectNodeGroupSharesAction(workflows.Action): possible_shares = self.get_possible_shares(request) - self.fields["shares"] = MultipleShareChoiceField( + self.fields["shares"] = workflow_helpers.MultipleShareChoiceField( label=_("Select Shares"), - widget=ShareWidget(choices=possible_shares), + widget=workflow_helpers.ShareWidget(choices=possible_shares), required=False, choices=possible_shares ) diff --git a/sahara_dashboard/content/data_processing/utils/workflow_helpers.py b/sahara_dashboard/content/data_processing/utils/workflow_helpers.py index 82b4be32..e914ae08 100644 --- a/sahara_dashboard/content/data_processing/utils/workflow_helpers.py +++ b/sahara_dashboard/content/data_processing/utils/workflow_helpers.py @@ -12,6 +12,8 @@ # limitations under the License. import logging +from django.core.exceptions import ValidationError +from django.utils import safestring from django.utils.translation import ugettext_lazy as _ import six @@ -324,3 +326,63 @@ class StatusFormatMixin(workflows.Workflow): return error_description else: return message % self.context[self.name_property] + + +class ShareWidget(forms.MultiWidget): + def __init__(self, choices=()): + widgets = [] + for choice in choices: + widgets.append(forms.CheckboxInput( + attrs={ + "label": choice[1], + "value": choice[0], + })) + widgets.append(forms.TextInput()) + widgets.append(forms.Select( + choices=(("rw", _("Read/Write")), ("ro", _("Read only"))))) + super(ShareWidget, self).__init__(widgets) + + def decompress(self, value): + if value: + values = [] + for share in value: + values.append(value[share]["id"]) + values.append(value[share]["path"]) + values.append(value[share]["access_level"]) + return values + return [None] * len(self.widgets) + + def format_output(self, rendered_widgets): + output = [] + output.append("") + output.append("" + "") + for i, widget in enumerate(rendered_widgets): + item_widget_index = i % 3 + if item_widget_index == 0: + output.append("") + output.append( + "".format( + self.widgets[i].attrs["label"])) + # The last 2 form field td need get a larger size + if item_widget_index in [1, 2]: + size = 4 + else: + size = 2 + output.append("") + if item_widget_index == 2: + output.append("") + output.append("
ShareEnabledPathPermissions
{0}".format(size) + + widget + "
") + return safestring.mark_safe('\n'.join(output)) + + +class MultipleShareChoiceField(forms.MultipleChoiceField): + def validate(self, value): + if self.required and not value: + raise ValidationError( + self.error_messages['required'], code='required') + if not isinstance(value, list): + raise ValidationError( + _("The value of shares must be a list of values") + ) diff --git a/sahara_dashboard/test/test_data/sahara_data.py b/sahara_dashboard/test/test_data/sahara_data.py index b4b876d5..26b75ea1 100644 --- a/sahara_dashboard/test/test_data/sahara_data.py +++ b/sahara_dashboard/test/test_data/sahara_data.py @@ -204,6 +204,7 @@ def data(TEST): "is_proxy_gateway": False } ], + "shares": [], "plugin_name": "vanilla", "tenant_id": "429ad8447c2d47bc8e0382d244e1d1df", "updated_at": None