summaryrefslogtreecommitdiff
path: root/openstack_dashboard/dashboards
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-07-15 10:10:06 +0000
committerGerrit Code Review <review@openstack.org>2014-07-15 10:10:06 +0000
commita0ab4ebf2086833f72dc6cf69d7a82b17465194e (patch)
tree6e1142389deba4e508bcafd53371077b99e25686 /openstack_dashboard/dashboards
parent63dc652c599f6695d571c95d25b338b5e3ea7fd3 (diff)
parent636eba18964f02a1222cd335d5e40c53c2a288fb (diff)
downloadhorizon-a0ab4ebf2086833f72dc6cf69d7a82b17465194e.tar.gz
Merge "Adding nodegroup_template panel for Sahara"
Diffstat (limited to 'openstack_dashboard/dashboards')
-rw-r--r--openstack_dashboard/dashboards/project/dashboard.py3
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/panel.py27
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tables.py88
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tabs.py71
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_configure_general_help.html20
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_create_general_help.html4
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_details.html45
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_fields_help.html60
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_processes_list.html5
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/_service_confs.html23
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/configure.html11
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/create.html11
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/details.html15
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/templates/data_processing.nodegroup_templates/nodegroup_templates.html97
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/tests.py58
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/urls.py40
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/views.py110
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/copy.py86
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/nodegroup_templates/workflows/create.py297
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/utils/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/utils/helpers.py75
-rw-r--r--openstack_dashboard/dashboards/project/data_processing/utils/workflow_helpers.py259
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 }}:&nbsp{{ 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]