diff options
author | Jenkins <jenkins@review.openstack.org> | 2014-07-15 10:10:06 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2014-07-15 10:10:06 +0000 |
commit | a0ab4ebf2086833f72dc6cf69d7a82b17465194e (patch) | |
tree | 6e1142389deba4e508bcafd53371077b99e25686 /openstack_dashboard/dashboards | |
parent | 63dc652c599f6695d571c95d25b338b5e3ea7fd3 (diff) | |
parent | 636eba18964f02a1222cd335d5e40c53c2a288fb (diff) | |
download | horizon-a0ab4ebf2086833f72dc6cf69d7a82b17465194e.tar.gz |
Merge "Adding nodegroup_template panel for Sahara"
Diffstat (limited to 'openstack_dashboard/dashboards')
24 files changed, 1404 insertions, 1 deletions
diff --git a/openstack_dashboard/dashboards/project/dashboard.py b/openstack_dashboard/dashboards/project/dashboard.py index ee50e5c38..ff7f522a8 100644 --- a/openstack_dashboard/dashboards/project/dashboard.py +++ b/openstack_dashboard/dashboards/project/dashboard.py @@ -61,7 +61,8 @@ class DataProcessingPanels(horizon.PanelGroup): name = _("Data Processing") slug = "data_processing" panels = ('data_processing.data_plugins', - 'data_processing.data_image_registry', ) + 'data_processing.data_image_registry', + 'data_processing.nodegroup_templates', ) class Project(horizon.Dashboard): diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/__init__.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/__init__.py diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/panel.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/panel.py new file mode 100644 index 000000000..6f0e54414 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/panel.py @@ -0,0 +1,27 @@ +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from openstack_dashboard.dashboards.project import dashboard + + +class NodegroupTemplatesPanel(horizon.Panel): + name = _("Node Group Templates") + slug = 'data_processing.nodegroup_templates' + permissions = ('openstack.services.data_processing',) + + +dashboard.Project.register(NodegroupTemplatesPanel) diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tables.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tables.py new file mode 100644 index 000000000..d6a37aff1 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tables.py @@ -0,0 +1,88 @@ +# 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. +from django import template + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables +from openstack_dashboard.api import sahara as saharaclient + +LOG = logging.getLogger(__name__) + + +class CreateNodegroupTemplate(tables.LinkAction): + name = "create" + verbose_name = _("Create Template") + url = ("horizon:project:data_processing.nodegroup_templates:" + "create-nodegroup-template") + classes = ("ajax-modal", "btn-create", "create-nodegrouptemplate-btn") + + +class ConfigureNodegroupTemplate(tables.LinkAction): + name = "configure" + verbose_name = _("Configure Template") + url = ("horizon:project:data_processing.nodegroup_templates:" + "configure-nodegroup-template") + classes = ("ajax-modal", "btn-create", "configure-nodegrouptemplate-btn") + attrs = {"style": "display: none"} + + +class CopyTemplate(tables.LinkAction): + name = "copy" + verbose_name = _("Copy Template") + url = "horizon:project:data_processing.nodegroup_templates:copy" + classes = ("ajax-modal", ) + + +class DeleteTemplate(tables.BatchAction): + name = "delete_nodegroup_template" + verbose_name = _("Delete") + classes = ("btn-terminate", "btn-danger") + + action_present = _("Delete") + action_past = _("Deleted") + data_type_singular = _("Template") + data_type_plural = _("Templates") + + def action(self, request, template_id): + saharaclient.nodegroup_template_delete(request, template_id) + + +def render_processes(nodegroup_template): + template_name = ( + 'project/data_processing.nodegroup_templates/_processes_list.html') + context = {"processes": nodegroup_template.node_processes} + return template.loader.render_to_string(template_name, context) + + +class NodegroupTemplatesTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link=("horizon:project:data_processing.nodegroup_templates:details")) + plugin_name = tables.Column("plugin_name", + verbose_name=_("Plugin")) + hadoop_version = tables.Column("hadoop_version", + verbose_name=_("Hadoop Version")) + node_processes = tables.Column(render_processes, + verbose_name=_("Node Processes")) + + class Meta: + name = "nodegroup_templates" + verbose_name = _("Node Group Templates") + table_actions = (CreateNodegroupTemplate, + ConfigureNodegroupTemplate, + DeleteTemplate) + row_actions = (CopyTemplate, + DeleteTemplate,) diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tabs.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tabs.py new file mode 100644 index 000000000..b670f5676 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tabs.py @@ -0,0 +1,71 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs + +from openstack_dashboard.api import nova +from openstack_dashboard.api import sahara as saharaclient + + +LOG = logging.getLogger(__name__) + + +class GeneralTab(tabs.Tab): + name = _("General Info") + slug = "nodegroup_template_details_tab" + template_name = ( + "project/data_processing.nodegroup_templates/_details.html") + + def get_context_data(self, request): + template_id = self.tab_group.kwargs['template_id'] + try: + template = saharaclient.nodegroup_template_get(request, template_id) + except Exception: + template = {} + exceptions.handle(request, + _("Unable to fetch node group template.")) + try: + flavor = nova.flavor_get(request, template.flavor_id) + except Exception: + flavor = {} + exceptions.handle(request, + _("Unable to fetch flavor for template.")) + return {"template": template, "flavor": flavor} + + +class ConfigsTab(tabs.Tab): + name = _("Service Configurations") + slug = "nodegroup_template_service_configs_tab" + template_name = ( + "project/data_processing.nodegroup_templates/_service_confs.html") + + def get_context_data(self, request): + template_id = self.tab_group.kwargs['template_id'] + try: + template = saharaclient.nodegroup_template_get(request, template_id) + except Exception: + template = {} + exceptions.handle(request, + _("Unable to fetch node group template.")) + return {"template": template} + + +class NodegroupTemplateDetailsTabs(tabs.TabGroup): + slug = "nodegroup_template_details" + tabs = (GeneralTab, ConfigsTab, ) + sticky = True diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_configure_general_help.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_configure_general_help.html new file mode 100644 index 000000000..10675b616 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_configure_general_help.html @@ -0,0 +1,20 @@ +{% load i18n horizon %} +<div class="well"> +<p> + {% blocktrans %}This Node Group Template will be created for:{% endblocktrans %} + <br > + <b>{% blocktrans %}Plugin{% endblocktrans %}</b>: {{ plugin_name }} + <br /> + <b>{% blocktrans %}Hadoop version{% endblocktrans %}</b>: {{ hadoop_version }} + <br /> +</p> +<p> + {% blocktrans %}The Node Group Template object should specify processes that will be launched on each instance. Also an OpenStack flavor is required to boot VMs.{% endblocktrans %} +</p> +<p> + {% blocktrans %}Data Processing provides different storage location options. You may choose Ephemeral Drive or a Cinder Volume to be attached to instances.{% endblocktrans %} +</p> +<p> + {% blocktrans %}When processes are selected, you may set <b>node</b> scoped Hadoop configurations on corresponding tabs.{% endblocktrans %} +</p> +</div>
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_create_general_help.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_create_general_help.html new file mode 100644 index 000000000..6b21b603f --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_create_general_help.html @@ -0,0 +1,4 @@ +{% load i18n horizon %} +<p class="well"> + {% blocktrans %}Select a plugin and Hadoop version for a new Node group template.{% endblocktrans %} +</p>
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_details.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_details.html new file mode 100644 index 000000000..06621c338 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_details.html @@ -0,0 +1,45 @@ +{% load i18n sizeformat %} +{% load url from future %} +<h3>{% trans "Template Overview" %}</h3> +<div class="status row-fluid detail"> + <dl> + <dt>{% trans "Name" %}</dt> + <dd>{{ template.name }}</dd> + <dt>{% trans "ID" %}</dt> + <dd>{{ template.id }}</dd> + <dt>{% trans "Description" %}</dt> + <dd>{{ template.description|default:"None" }}</dd> + </dl> + <dl> + <dt>{% trans "Flavor" %}</dt> + <dd>{{ flavor.name }}</dd> + </dl> + <dl> + <dt>{% trans "Plugin" %}</dt> + <dd><a href="{% url 'horizon:project:data_processing.data_plugins:details' template.plugin_name %}">{{ template.plugin_name }}</a></dd> + <dt>{% trans "Hadoop Version" %}</dt> + <dd>{{ template.hadoop_version }}</dd> + </dl> + <dl> + <dt>{% trans "Node Processes" %}</dt> + <dd> + <ul> + {% for process in template.node_processes %} + <li>{{ process }}</li> + {% endfor %} + </ul> + </dd> + </dl> + <dl> + <h4>{% trans "HDFS placement" %}</h4> + {% if template.volumes_per_node %} + <h6>{% trans "Cinder volumes" %}</h6> + <dt>{% trans "Volumes per node" %}</dt> + <dd>{{ template.volumes_per_node }}</dd> + <dt>{% trans "Volumes size" %}</dt> + <dd>{{ template.volumes_size }}</dd> + {% else %} + <h6>{% trans "Ephemeral drive" %}</h6> + {% endif %} + </dl> +</div>
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_fields_help.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_fields_help.html new file mode 100644 index 000000000..11b9c8af0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_fields_help.html @@ -0,0 +1,60 @@ +<div class="field-filter"> + <input type="text" class="field-filter" placeholder="Filter" style="background:no-repeat scroll 195px 5px transparent url('/static/dashboard/img/search.png');"/><br> + <a class="full-config-show" onclick="show_not_important_fields(this);">Show full configuration</a> + <a class="full-config-hide" onclick="hide_not_important_fields(this);">Hide full configuration</a> +</div> + +<script type="text/javascript"> + function hide_not_important_fields(element) { + var fieldset = $(element).parents("fieldset")[0]; + $("[priority='2']", fieldset).parents("div.control-group").hide(); + $("[priority='1']", fieldset).parents("div.control-group").show(); + var div = $(element).parents("div.field-filter")[0]; + $("a.full-config-show",div).show(); + $("a.full-config-hide",div).hide(); + } + + function show_not_important_fields(element) { + var fieldset = $(element).parents("fieldset")[0]; + $("[priority='2']", fieldset).parents("div.control-group").show(); + var div = $(element).parents("div.field-filter")[0]; + $("a.full-config-show",div).hide(); + $("a.full-config-hide",div).show(); + } + + function create_auto_search(inpt) { + if (inpt.has_filter) { + return; + } + var input = inpt; + inpt.has_filter = true; + var oldValue = ""; + setInterval(function () { + var val = $(input).val(); + if (val == oldValue) { + return; + } + oldValue = val; + var fieldset = $(input).parents("fieldset")[0]; + if (val == "") { + $("label", fieldset).parents("div.control-group").show(); + if ($("a.full-config-show")[0].style.display == "inline") { + hide_not_important_fields(this); + } + } else { + $("label", fieldset).filter(function (idx, e) { + return $(e).text().toLowerCase().indexOf(val.toLowerCase()) == -1; + }).parents("div.control-group").hide(); + $("label", fieldset).filter(function (idx, e) { + return $(e).text().toLowerCase().indexOf(val.toLowerCase()) > -1; + }).parents("div.control-group").show(); + } + }, 300); + } + + $("input.field-filter").each(function () { + create_auto_search(this); + }); + + $("a.full-config-hide").click(); +</script> diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_processes_list.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_processes_list.html new file mode 100644 index 000000000..52854fdbe --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_processes_list.html @@ -0,0 +1,5 @@ +<ul> + {% for process in processes %} + <li>{{ process }}</li> + {% endfor %} +</ul>
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_service_confs.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_service_confs.html new file mode 100644 index 000000000..74517b18e --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_service_confs.html @@ -0,0 +1,23 @@ +{% load i18n sizeformat %} +<h3>{% trans "Service Configurations" %}</h3> +<div class="status row-fluid detail"> + <dl> + {% for service, config in template.node_configs.items %} + <dt>{{ service }}</dt> + <dd> + {% if config %} + <ul> + {% for conf_name, conf_val in config.items %} + <li> + {{ conf_name }}: {{ conf_val }} + </li> + {% endfor %} + </ul> + {% else %} + <h6>No configurations</h6> + {% endif %} + </dd> + {% endfor %} + </dl> + +</div>
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/configure.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/configure.html new file mode 100644 index 000000000..1b03aa44d --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/configure.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Node Group Template" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Node Group Template") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/create.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/create.html new file mode 100644 index 000000000..1b03aa44d --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Node Group Template" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Node Group Template") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/details.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/details.html new file mode 100644 index 000000000..2877c30ce --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/details.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Nodegroup Template Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Node Group Template Details") %} +{% endblock page_header %} + +{% block main %} + <div class="row-fluid"> + <div class="span12"> + {{ tab_group.render }} + </div> + </div> +{% endblock %}
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/nodegroup_templates.html b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/nodegroup_templates.html new file mode 100644 index 000000000..da9214807 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/nodegroup_templates.html @@ -0,0 +1,97 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Data Processing" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Node Group Templates") %} +{% endblock page_header %} + +{% block main %} + + <div class="nodegroup_templates"> + {{ nodegroup_templates_table.render }} + </div> + + <script type="text/javascript"> + addHorizonLoadEvent(function () { + function get_service_tab(service) { + return $("a").filter(function (idx, e) { + return $(e).attr("data-target") && $(e).attr("data-target").indexOf(service.toLowerCase()) != -1 + }).closest("li"); + } + + // replace form submit with ajax POST and trigger next workflow + horizon.modals.addModalInitFunction(function (modal) { + if ($(modal).find(".nav-tabs").find("li").size() == 1) { + // hide tab bar for plugin/version modal wizard + $('div#modal_wrapper ul.nav-tabs').hide(); + } + + if ($(modal).find(".hidden_create_field").length > 0) { + var form = $(".hidden_create_field").closest("form"); + var successful = false; + form.submit(function (e) { + var oldHref = $(".configure-nodegrouptemplate-btn")[0].href; + var plugin = $("#id_plugin_name option:selected").val(); + var version = $("#id_" + plugin + "_version option:selected").val(); + form.find(".close").click(); + $(".configure-nodegrouptemplate-btn")[0].href = oldHref + + "?plugin_name=" + encodeURIComponent(plugin) + + "&hadoop_version=" + encodeURIComponent(version); + $(".configure-nodegrouptemplate-btn").click(); + $(".configure-nodegrouptemplate-btn")[0].href = oldHref; + return false; + }); + $(".plugin_version_choice").closest(".control-group").hide(); + } + + //display version for selected plugin + $(document).on('change', '.plugin_name_choice', switch_versions); + function switch_versions() { + $(".plugin_version_choice").closest(".control-group").hide(); + var plugin = $(this); + $("." + plugin.val() + "_version_choice").closest(".control-group").show(); + } + + $(".storage_field").change(switch_storage).change(); + + function switch_storage() { + var show = $(".storage_field").val() == "cinder_volume"; + if (show) { + $(".volume_per_node_field").closest(".control-group").show(); + $(".volume_size_field").closest(".control-group").show(); + } else { + $(".volume_per_node_field").closest(".control-group").hide(); + $(".volume_size_field").closest(".control-group").hide(); + } + } + + $(".plugin_name_choice").change(); + + //handle node processes change + $("input").filter(function (idx, e) { + return $(e).attr("name") && $(e).attr("name").indexOf("processes") != -1 + }) + .change(function () { + var process_service = $(this).val(); + var service = $(this).val().split(":")[0]; + var enabled = false; + $(this).closest("ul").find("input").each(function (idx, el) { + if ($(el).val().split(":")[0] != service) { + return; + } + enabled |= $(el).is(':checked'); + }); + if (enabled) { + get_service_tab(service).show(); + } else { + get_service_tab(service).hide(); + } + }).change(); + //general tab should be active + get_service_tab("generalconfigaction").find("a").click(); + }); + }); + </script> + +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tests.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tests.py new file mode 100644 index 000000000..b9c542047 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tests.py @@ -0,0 +1,58 @@ +# 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. + +from django.core.urlresolvers import reverse +from django import http + +from mox import IsA # noqa + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + + +INDEX_URL = reverse( + 'horizon:project:data_processing.nodegroup_templates:index') +DETAILS_URL = reverse( + 'horizon:project:data_processing.nodegroup_templates:details', + args=['id']) + + +class DataProcessingNodeGroupTests(test.TestCase): + @test.create_stubs({api.sahara: ('nodegroup_template_list',)}) + def test_index(self): + api.sahara.nodegroup_template_list(IsA(http.HttpRequest)) \ + .AndReturn(self.nodegroup_templates.list()) + self.mox.ReplayAll() + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, + 'project/data_processing.nodegroup_templates/' + 'nodegroup_templates.html') + self.assertContains(res, 'Node Group Templates') + self.assertContains(res, 'Name') + self.assertContains(res, 'Plugin') + + @test.create_stubs({api.sahara: ('nodegroup_template_get',), + api.nova: ('flavor_get',)}) + def test_details(self): + flavor = self.flavors.first() + ngt = self.nodegroup_templates.first() + api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor) + api.sahara.nodegroup_template_get(IsA(http.HttpRequest), + IsA(unicode)) \ + .MultipleTimes().AndReturn(ngt) + self.mox.ReplayAll() + res = self.client.get(DETAILS_URL) + self.assertTemplateUsed(res, + 'project/data_processing.nodegroup_templates/' + 'details.html') + self.assertContains(res, 'sample-template') + self.assertContains(res, 'Template Overview') diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/urls.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/urls.py new file mode 100644 index 000000000..2bcd21dd3 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/urls.py @@ -0,0 +1,40 @@ +# 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. + + +from django.conf.urls import patterns # noqa +from django.conf.urls import url # noqa + +import openstack_dashboard.dashboards.project. \ + data_processing.nodegroup_templates.views as views + + +urlpatterns = patterns('sahara.nodegroup_templates.views', + url(r'^$', views.NodegroupTemplatesView.as_view(), + name='index'), + url(r'^nodegroup-templates$', + views.NodegroupTemplatesView.as_view(), + name='nodegroup-templates'), + url(r'^create-nodegroup-template$', + views.CreateNodegroupTemplateView.as_view(), + name='create-nodegroup-template'), + url(r'^configure-nodegroup-template$', + views.ConfigureNodegroupTemplateView.as_view(), + name='configure-nodegroup-template'), + url(r'^(?P<template_id>[^/]+)$', + views.NodegroupTemplateDetailsView.as_view(), + name='details'), + url(r'^(?P<template_id>[^/]+)/copy$', + views.CopyNodegroupTemplateView.as_view(), + name='copy') + ) diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/views.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/views.py new file mode 100644 index 000000000..62fb88eeb --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/views.py @@ -0,0 +1,110 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tables +from horizon import tabs +from horizon import workflows + +from openstack_dashboard.api import sahara as saharaclient + +import openstack_dashboard.dashboards.project.data_processing. \ + nodegroup_templates.tables as _tables +import openstack_dashboard.dashboards.project.data_processing. \ + nodegroup_templates.tabs as _tabs +import openstack_dashboard.dashboards.project.data_processing. \ + nodegroup_templates.workflows.copy as copy_flow +import openstack_dashboard.dashboards.project.data_processing. \ + nodegroup_templates.workflows.create as create_flow + +LOG = logging.getLogger(__name__) + + +class NodegroupTemplatesView(tables.DataTableView): + table_class = _tables.NodegroupTemplatesTable + template_name = ( + 'project/data_processing.nodegroup_templates/nodegroup_templates.html') + + def get_data(self): + try: + data = saharaclient.nodegroup_template_list(self.request) + except Exception: + data = [] + exceptions.handle(self.request, + _("Unable to fetch node group template list.")) + return data + + +class NodegroupTemplateDetailsView(tabs.TabView): + tab_group_class = _tabs.NodegroupTemplateDetailsTabs + template_name = 'project/data_processing.nodegroup_templates/details.html' + + def get_context_data(self, **kwargs): + context = super(NodegroupTemplateDetailsView, self)\ + .get_context_data(**kwargs) + return context + + def get_data(self): + pass + + +class CreateNodegroupTemplateView(workflows.WorkflowView): + workflow_class = create_flow.CreateNodegroupTemplate + success_url = ( + "horizon:project:data_processing.nodegroup_templates:" + "create-nodegroup-template") + classes = ("ajax-modal") + template_name = "project/data_processing.nodegroup_templates/create.html" + + +class ConfigureNodegroupTemplateView(workflows.WorkflowView): + workflow_class = create_flow.ConfigureNodegroupTemplate + success_url = "horizon:project:data_processing.nodegroup_templates" + template_name = ( + "project/data_processing.nodegroup_templates/configure.html") + + +class CopyNodegroupTemplateView(workflows.WorkflowView): + workflow_class = copy_flow.CopyNodegroupTemplate + success_url = "horizon:project:data_processing.nodegroup_templates" + template_name = ( + "project/data_processing.nodegroup_templates/configure.html") + + def get_context_data(self, **kwargs): + context = super(CopyNodegroupTemplateView, self)\ + .get_context_data(**kwargs) + + context["template_id"] = kwargs["template_id"] + return context + + def get_object(self, *args, **kwargs): + if not hasattr(self, "_object"): + template_id = self.kwargs['template_id'] + try: + template = saharaclient.nodegroup_template_get(self.request, + template_id) + except Exception: + template = None + exceptions.handle(self.request, + _("Unable to fetch template object.")) + self._object = template + return self._object + + def get_initial(self): + initial = super(CopyNodegroupTemplateView, self).get_initial() + initial['template_id'] = self.kwargs['template_id'] + return initial diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/__init__.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/__init__.py diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/copy.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/copy.py new file mode 100644 index 000000000..f09a20959 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/copy.py @@ -0,0 +1,86 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions + +from openstack_dashboard.api import sahara as saharaclient + +import openstack_dashboard.dashboards.project.data_processing. \ + nodegroup_templates.workflows.create as create_flow + +LOG = logging.getLogger(__name__) + + +class CopyNodegroupTemplate(create_flow.ConfigureNodegroupTemplate): + success_message = _("Node Group Template copy %s created") + + def __init__(self, request, context_seed, entry_point, *args, **kwargs): + template_id = context_seed["template_id"] + template = saharaclient.nodegroup_template_get(request, template_id) + self._set_configs_to_copy(template.node_configs) + + plugin = template.plugin_name + hadoop_version = template.hadoop_version + + request.GET = request.GET.copy() + request.GET.update( + {"plugin_name": plugin, "hadoop_version": hadoop_version}) + + super(CopyNodegroupTemplate, self).__init__(request, context_seed, + entry_point, *args, + **kwargs) + + for step in self.steps: + if not isinstance(step, create_flow.GeneralConfig): + continue + fields = step.action.fields + + fields["nodegroup_name"].initial = template.name + "-copy" + fields["description"].initial = template.description + fields["flavor"].initial = template.flavor_id + + storage = "cinder_volume" if template.volumes_per_node > 0 \ + else "ephemeral_drive" + volumes_per_node = template.volumes_per_node + volumes_size = template.volumes_size + fields["storage"].initial = storage + fields["volumes_per_node"].initial = volumes_per_node + fields["volumes_size"].initial = volumes_size + + if template.floating_ip_pool: + fields['floating_ip_pool'].initial = template.floating_ip_pool + + processes_dict = dict() + try: + plugin_details = saharaclient.plugin_get_version_details( + request, + plugin, + hadoop_version) + plugin_node_processes = plugin_details.node_processes + except Exception: + plugin_node_processes = dict() + exceptions.handle(request, + _("Unable to fetch plugin details.")) + for process in template.node_processes: + #need to know the service + _service = None + for service, processes in plugin_node_processes.items(): + if process in processes: + _service = service + break + processes_dict["%s:%s" % (_service, process)] = process + fields["processes"].initial = processes_dict diff --git a/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/create.py b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/create.py new file mode 100644 index 000000000..da7a0074e --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/create.py @@ -0,0 +1,297 @@ +# 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. + +from horizon import forms +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import workflows + +from openstack_dashboard.api import network +from openstack_dashboard.api import nova +from openstack_dashboard.api import sahara as saharaclient + +import openstack_dashboard.dashboards.project.data_processing. \ + utils.helpers as helpers +import openstack_dashboard.dashboards.project.data_processing. \ + utils.workflow_helpers as whelpers + +from saharaclient.api import base as api_base + + +LOG = logging.getLogger(__name__) + + +class GeneralConfigAction(workflows.Action): + nodegroup_name = forms.CharField(label=_("Template Name")) + + description = forms.CharField(label=_("Description"), + required=False, + widget=forms.Textarea) + + flavor = forms.ChoiceField(label=_("OpenStack Flavor")) + + storage = forms.ChoiceField( + label=_("Storage location"), + help_text=_("Storage"), + choices=[("ephemeral_drive", "Ephemeral Drive"), + ("cinder_volume", "Cinder Volume")], + widget=forms.Select(attrs={"class": "storage_field"})) + + volumes_per_node = forms.IntegerField( + label=_("Volumes per node"), + required=False, + initial=1, + widget=forms.TextInput(attrs={"class": "volume_per_node_field"}) + ) + + volumes_size = forms.IntegerField( + label=_("Volumes size (GB)"), + required=False, + initial=10, + widget=forms.TextInput(attrs={"class": "volume_size_field"}) + ) + + hidden_configure_field = forms.CharField( + required=False, + widget=forms.HiddenInput(attrs={"class": "hidden_configure_field"})) + + def __init__(self, request, *args, **kwargs): + super(GeneralConfigAction, self).__init__(request, *args, **kwargs) + + sahara = saharaclient.client(request) + hlps = helpers.Helpers(sahara) + + plugin, hadoop_version = whelpers.\ + get_plugin_and_hadoop_version(request) + process_choices = [] + try: + version_details = saharaclient.plugin_get_version_details(request, + plugin, + hadoop_version) + for service, processes in version_details.node_processes.items(): + for process in processes: + process_choices.append( + (str(service) + ":" + str(process), process)) + except Exception: + exceptions.handle(request, + _("Unable to generate process choices.")) + + if not saharaclient.SAHARA_AUTO_IP_ALLOCATION_ENABLED: + pools = network.floating_ip_pools_list(request) + pool_choices = [(pool.id, pool.name) for pool in pools] + pool_choices.insert(0, (None, "Do not assign floating IPs")) + + self.fields['floating_ip_pool'] = forms.ChoiceField( + label=_("Floating IP pool"), + choices=pool_choices, + required=False) + + self.fields["processes"] = forms.MultipleChoiceField( + label=_("Processes"), + widget=forms.CheckboxSelectMultiple(), + help_text=_("Processes to be launched in node group"), + choices=process_choices) + + self.fields["plugin_name"] = forms.CharField( + widget=forms.HiddenInput(), + initial=plugin + ) + self.fields["hadoop_version"] = forms.CharField( + widget=forms.HiddenInput(), + initial=hadoop_version + ) + + node_parameters = hlps.get_general_node_group_configs(plugin, + hadoop_version) + for param in node_parameters: + self.fields[param.name] = whelpers.build_control(param) + + def populate_flavor_choices(self, request, context): + try: + flavors = nova.flavor_list(request) + flavor_list = [(flavor.id, "%s" % flavor.name) + for flavor in flavors] + except Exception: + flavor_list = [] + exceptions.handle(request, + _('Unable to retrieve instance flavors.')) + return sorted(flavor_list) + + def get_help_text(self): + extra = dict() + plugin, hadoop_version = whelpers.\ + get_plugin_and_hadoop_version(self.request) + extra["plugin_name"] = plugin + extra["hadoop_version"] = hadoop_version + return super(GeneralConfigAction, self).get_help_text(extra) + + class Meta: + name = _("Configure Node Group Template") + help_text_template = ( + "project/data_processing.nodegroup_templates" + "/_configure_general_help.html") + + +class GeneralConfig(workflows.Step): + action_class = GeneralConfigAction + contributes = ("general_nodegroup_name", ) + + def contribute(self, data, context): + for k, v in data.items(): + if "hidden" in k: + continue + context["general_" + k] = v if v != "None" else None + + post = self.workflow.request.POST + context['general_processes'] = post.getlist("processes") + return context + + +class ConfigureNodegroupTemplate(whelpers.ServiceParametersWorkflow, + whelpers.StatusFormatMixin): + slug = "configure_nodegroup_template" + name = _("Create Node Group Template") + finalize_button_name = _("Create") + success_message = _("Created Node Group Template %s") + name_property = "general_nodegroup_name" + success_url = "horizon:project:data_processing.nodegroup_templates:index" + default_steps = (GeneralConfig,) + + def __init__(self, request, context_seed, entry_point, *args, **kwargs): + sahara = saharaclient.client(request) + hlps = helpers.Helpers(sahara) + + plugin, hadoop_version = whelpers.\ + get_plugin_and_hadoop_version(request) + + general_parameters = hlps.get_general_node_group_configs( + plugin, + hadoop_version) + service_parameters = hlps.get_targeted_node_group_configs( + plugin, + hadoop_version) + + self._populate_tabs(general_parameters, service_parameters) + + super(ConfigureNodegroupTemplate, self).__init__(request, + context_seed, + entry_point, + *args, **kwargs) + + def is_valid(self): + missing = self.depends_on - set(self.context.keys()) + if missing: + raise exceptions.WorkflowValidationError( + "Unable to complete the workflow. The values %s are " + "required but not present." % ", ".join(missing)) + checked_steps = [] + + if "general_processes" in self.context: + checked_steps = self.context["general_processes"] + enabled_services = set([]) + for process_name in checked_steps: + enabled_services.add(str(process_name).split(":")[0]) + + steps_valid = True + for step in self.steps: + process_name = str(getattr(step, "process_name", None)) + if process_name not in enabled_services and \ + not isinstance(step, GeneralConfig): + continue + if not step.action.is_valid(): + steps_valid = False + step.has_errors = True + if not steps_valid: + return steps_valid + return self.validate(self.context) + + def handle(self, request, context): + try: + processes = [] + for service_process in context["general_processes"]: + processes.append(str(service_process).split(":")[1]) + + configs_dict = whelpers.parse_configs_from_context(context, + self.defaults) + + plugin, hadoop_version = whelpers.\ + get_plugin_and_hadoop_version(request) + + volumes_per_node = None + volumes_size = None + + if context["general_storage"] == "cinder_volume": + volumes_per_node = context["general_volumes_per_node"] + volumes_size = context["general_volumes_size"] + + saharaclient.nodegroup_template_create( + request, + name=context["general_nodegroup_name"], + plugin_name=plugin, + hadoop_version=hadoop_version, + description=context["general_description"], + flavor_id=context["general_flavor"], + volumes_per_node=volumes_per_node, + volumes_size=volumes_size, + node_processes=processes, + node_configs=configs_dict, + floating_ip_pool=context.get("general_floating_ip_pool", None)) + return True + except api_base.APIException as e: + self.error_description = str(e) + return False + except Exception: + exceptions.handle(request) + + +class SelectPluginAction(workflows.Action, + whelpers.PluginAndVersionMixin): + hidden_create_field = forms.CharField( + required=False, + widget=forms.HiddenInput(attrs={"class": "hidden_create_field"})) + + def __init__(self, request, *args, **kwargs): + super(SelectPluginAction, self).__init__(request, *args, **kwargs) + + sahara = saharaclient.client(request) + self._generate_plugin_version_fields(sahara) + + class Meta: + name = _("Select plugin and hadoop version") + help_text_template = ("project/data_processing.nodegroup_templates" + "/_create_general_help.html") + + +class SelectPlugin(workflows.Step): + action_class = SelectPluginAction + contributes = ("plugin_name", "hadoop_version") + + def contribute(self, data, context): + context = super(SelectPlugin, self).contribute(data, context) + context["plugin_name"] = data.get('plugin_name', None) + context["hadoop_version"] = \ + data.get(context["plugin_name"] + "_version", None) + return context + + +class CreateNodegroupTemplate(workflows.Workflow): + slug = "create_nodegroup_template" + name = _("Create Node Group Template") + finalize_button_name = _("Create") + success_message = _("Created") + failure_message = _("Could not create") + success_url = "horizon:project:data_processing.nodegroup_templates:index" + default_steps = (SelectPlugin,) diff --git a/openstack_dashboard/dashboards/project/data_processing/utils/__init__.py b/openstack_dashboard/dashboards/project/data_processing/utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/utils/__init__.py diff --git a/openstack_dashboard/dashboards/project/data_processing/utils/helpers.py b/openstack_dashboard/dashboards/project/data_processing/utils/helpers.py new file mode 100644 index 000000000..1a5f9d8e8 --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/utils/helpers.py @@ -0,0 +1,75 @@ +# 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 openstack_dashboard.dashboards.project.data_processing. \ + utils.workflow_helpers as work_helpers + + +class Helpers(object): + def __init__(self, sahara_client): + self.sahara = sahara_client + self.plugins = self.sahara.plugins + + def _get_node_processes(self, plugin): + processes = [] + for proc_lst in plugin.node_processes.values(): + processes += proc_lst + + return [(proc_name, proc_name) for proc_name in processes] + + def get_node_processes(self, plugin_name, hadoop_version): + plugin = self.plugins.get_version_details(plugin_name, hadoop_version) + + return self._get_node_processes(plugin) + + def _extract_parameters(self, configs, scope, applicable_target): + parameters = [] + for config in configs: + if (config['scope'] == scope and + config['applicable_target'] == applicable_target): + + parameters.append(work_helpers.Parameter(config)) + + return parameters + + def get_cluster_general_configs(self, plugin_name, hadoop_version): + plugin = self.plugins.get_version_details(plugin_name, hadoop_version) + + return self._extract_parameters(plugin.configs, 'cluster', "general") + + def get_general_node_group_configs(self, plugin_name, hadoop_version): + plugin = self.plugins.get_version_details(plugin_name, hadoop_version) + + return self._extract_parameters(plugin.configs, 'node', 'general') + + def get_targeted_node_group_configs(self, plugin_name, hadoop_version): + plugin = self.plugins.get_version_details(plugin_name, hadoop_version) + + parameters = {} + + for service in plugin.node_processes.keys(): + parameters[service] = self._extract_parameters(plugin.configs, + 'node', service) + + return parameters + + def get_targeted_cluster_configs(self, plugin_name, hadoop_version): + plugin = self.plugins.get_version_details(plugin_name, hadoop_version) + + parameters = {} + + for service in plugin.node_processes.keys(): + parameters[service] = self._extract_parameters(plugin.configs, + 'cluster', service) + + return parameters diff --git a/openstack_dashboard/dashboards/project/data_processing/utils/workflow_helpers.py b/openstack_dashboard/dashboards/project/data_processing/utils/workflow_helpers.py new file mode 100644 index 000000000..e058eabdb --- /dev/null +++ b/openstack_dashboard/dashboards/project/data_processing/utils/workflow_helpers.py @@ -0,0 +1,259 @@ +# 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. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import workflows + + +class Parameter(object): + def __init__(self, config): + self.name = config['name'] + self.description = config.get('description', "No description") + self.required = not config['is_optional'] + self.default_value = config.get('default_value', None) + self.initial_value = self.default_value + self.param_type = config['config_type'] + self.priority = int(config.get('priority', 2)) + + +def build_control(parameter): + attrs = {"priority": parameter.priority, + "placeholder": parameter.default_value} + if parameter.param_type == "string": + return forms.CharField( + widget=forms.TextInput(attrs=attrs), + label=parameter.name, + required=(parameter.required and + parameter.default_value is None), + help_text=parameter.description, + initial=parameter.initial_value) + + if parameter.param_type == "int": + return forms.IntegerField( + widget=forms.TextInput(attrs=attrs), + label=parameter.name, + required=parameter.required, + help_text=parameter.description, + initial=parameter.initial_value) + + elif parameter.param_type == "bool": + return forms.BooleanField( + widget=forms.CheckboxInput(attrs=attrs), + label=parameter.name, + required=False, + initial=parameter.initial_value, + help_text=parameter.description) + + elif parameter.param_type == "dropdown": + return forms.ChoiceField( + widget=forms.CheckboxInput(attrs=attrs), + label=parameter.name, + required=parameter.required, + choices=parameter.choices, + help_text=parameter.description) + + +def _create_step_action(name, title, parameters, advanced_fields=None, + service=None): + class_fields = {} + contributes_field = () + for param in parameters: + field_name = "CONF:" + service + ":" + param.name + contributes_field += (field_name,) + class_fields[field_name] = build_control(param) + + if advanced_fields is not None: + for ad_field_name, ad_field_value in advanced_fields: + class_fields[ad_field_name] = ad_field_value + + action_meta = type('Meta', (object, ), + dict(help_text_template=("project" + "/data_processing." + "nodegroup_templates/" + "_fields_help.html"))) + + class_fields['Meta'] = action_meta + action = type(str(title), + (workflows.Action,), + class_fields) + + step_meta = type('Meta', (object,), dict(name=title)) + step = type(str(name), + (workflows.Step, ), + dict(name=name, + process_name=name, + action_class=action, + contributes=contributes_field, + Meta=step_meta)) + + return step + + +def build_node_group_fields(action, name, template, count): + action.fields[name] = forms.CharField( + label=_("Name"), + required=True, + widget=forms.TextInput()) + + action.fields[template] = forms.CharField( + label=_("Node group cluster"), + required=True, + widget=forms.HiddenInput()) + + action.fields[count] = forms.IntegerField( + label=_("Count"), + required=True, + min_value=0, + widget=forms.HiddenInput()) + + +def parse_configs_from_context(context, defaults): + configs_dict = dict() + for key, val in context.items(): + if str(key).startswith("CONF"): + key_split = str(key).split(":") + service = key_split[1] + config = key_split[2] + if service not in configs_dict: + configs_dict[service] = dict() + if (val is None or + unicode(defaults[service][config]) == unicode(val)): + continue + configs_dict[service][config] = val + return configs_dict + + +def safe_call(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + return None + + +def get_plugin_and_hadoop_version(request): + plugin_name = request.REQUEST["plugin_name"] + hadoop_version = request.REQUEST["hadoop_version"] + return (plugin_name, hadoop_version) + + +class PluginAndVersionMixin(object): + def _generate_plugin_version_fields(self, sahara): + plugins = sahara.plugins.list() + plugin_choices = [(plugin.name, plugin.title) for plugin in plugins] + + self.fields["plugin_name"] = forms.ChoiceField( + label=_("Plugin Name"), + required=True, + choices=plugin_choices, + widget=forms.Select(attrs={"class": "plugin_name_choice"})) + + for plugin in plugins: + field_name = plugin.name + "_version" + choice_field = forms.ChoiceField( + label=_("Hadoop Version"), + required=True, + choices=[(version, version) for version in plugin.versions], + widget=forms.Select( + attrs={"class": "plugin_version_choice " + + field_name + "_choice"}) + ) + self.fields[field_name] = choice_field + + +class PatchedDynamicWorkflow(workflows.Workflow): + """Overrides Workflow to fix its issues.""" + + def _ensure_dynamic_exist(self): + if not hasattr(self, 'dynamic_steps'): + self.dynamic_steps = list() + + def _register_step(self, step): + # Use that method instead of 'register' to register step. + # Note that a step could be registered in descendant class constructor + # only before this class constructor is invoked. + self._ensure_dynamic_exist() + self.dynamic_steps.append(step) + + def _order_steps(self): + # overrides method of Workflow + # crutch to fix https://bugs.launchpad.net/horizon/+bug/1196717 + # and another not filed issue that dynamic creation of tabs is + # not thread safe + self._ensure_dynamic_exist() + + self._registry = dict([(step, step(self)) + for step in self.dynamic_steps]) + + return list(self.default_steps) + self.dynamic_steps + + +class ServiceParametersWorkflow(PatchedDynamicWorkflow): + """Base class for Workflows having services tabs with parameters.""" + + def _populate_tabs(self, general_parameters, service_parameters): + # Populates tabs for 'general' and service parameters + # Also populates defaults and initial values + self.defaults = dict() + + self._init_step('general', 'General Parameters', general_parameters) + + for service, parameters in service_parameters.items(): + self._init_step(service, service + ' Parameters', parameters) + + def _init_step(self, service, title, parameters): + if not parameters: + return + + self._populate_initial_values(service, parameters) + + step = _create_step_action(service, title=title, parameters=parameters, + service=service) + + self.defaults[service] = dict() + for param in parameters: + self.defaults[service][param.name] = param.default_value + + self._register_step(step) + + def _set_configs_to_copy(self, configs): + self.configs_to_copy = configs + + def _populate_initial_values(self, service, parameters): + if not hasattr(self, 'configs_to_copy'): + return + + configs = self.configs_to_copy + + for param in parameters: + if (service in configs and + param.name in configs[service]): + param.initial_value = configs[service][param.name] + + +class StatusFormatMixin(workflows.Workflow): + def __init__(self, request, context_seed, entry_point, *args, **kwargs): + super(StatusFormatMixin, self).__init__(request, + context_seed, + entry_point, + *args, + **kwargs) + + def format_status_message(self, message): + error_description = getattr(self, 'error_description', None) + + if error_description: + return error_description + else: + return message % self.context[self.name_property] |