diff --git a/openstack_dashboard/api/sahara.py b/openstack_dashboard/api/sahara.py
index 77438c5b5a..86e2946847 100644
--- a/openstack_dashboard/api/sahara.py
+++ b/openstack_dashboard/api/sahara.py
@@ -109,6 +109,10 @@ def nodegroup_template_get(request, ngt_id):
return client(request).node_group_templates.get(ngt_id)
+def nodegroup_template_find(request, **kwargs):
+ return client(request).node_group_templates.find(**kwargs)
+
+
def nodegroup_template_delete(request, ngt_id):
client(request).node_group_templates.delete(ngt_id)
diff --git a/openstack_dashboard/dashboards/project/dashboard.py b/openstack_dashboard/dashboards/project/dashboard.py
index ff7f522a83..62f7cd5962 100644
--- a/openstack_dashboard/dashboards/project/dashboard.py
+++ b/openstack_dashboard/dashboards/project/dashboard.py
@@ -62,7 +62,9 @@ class DataProcessingPanels(horizon.PanelGroup):
slug = "data_processing"
panels = ('data_processing.data_plugins',
'data_processing.data_image_registry',
- 'data_processing.nodegroup_templates', )
+ 'data_processing.nodegroup_templates',
+ 'data_processing.cluster_templates',
+ 'data_processing.clusters', )
class Project(horizon.Dashboard):
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/__init__.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/forms.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/forms.py
new file mode 100644
index 0000000000..cc4fd890e5
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/forms.py
@@ -0,0 +1,59 @@
+# 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 forms
+
+from openstack_dashboard.api import sahara as saharaclient
+from openstack_dashboard.dashboards.project.data_processing. \
+ utils import workflow_helpers
+
+LOG = logging.getLogger(__name__)
+
+
+class UploadFileForm(forms.SelfHandlingForm,
+ workflow_helpers.PluginAndVersionMixin):
+ template_name = forms.CharField(max_length=80,
+ label=_("Cluster Template Name"))
+
+ def __init__(self, request, *args, **kwargs):
+ super(UploadFileForm, self).__init__(request, *args, **kwargs)
+
+ sahara = saharaclient.client(request)
+ self._generate_plugin_version_fields(sahara)
+
+ self.fields['template_file'] = forms.FileField(label=_("Template"),
+ required=True)
+
+ def handle(self, request, data):
+ try:
+ # we can set a limit on file size, but should we?
+ filecontent = self.files['template_file'].read()
+
+ plugin_name = data['plugin_name']
+ hadoop_version = data.get(plugin_name + "_version")
+
+ saharaclient.plugin_convert_to_template(request,
+ plugin_name,
+ hadoop_version,
+ data['template_name'],
+ filecontent)
+ return True
+ except Exception:
+ exceptions.handle(request,
+ _("Unable to upload cluster template file"))
+ return False
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/panel.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/panel.py
new file mode 100644
index 0000000000..b4ce2040c6
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_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 ClusterTemplatesPanel(horizon.Panel):
+ name = _("Cluster Templates")
+ slug = 'data_processing.cluster_templates'
+ permissions = ('openstack.services.data_processing',)
+
+
+dashboard.Project.register(ClusterTemplatesPanel)
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tables.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tables.py
new file mode 100644
index 0000000000..ed7896346b
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tables.py
@@ -0,0 +1,118 @@
+# 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.core import urlresolvers
+from django import template
+from django.utils import http
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import tables
+
+from openstack_dashboard.api import sahara as saharaclient
+
+LOG = logging.getLogger(__name__)
+
+
+def render_node_groups(cluster_template):
+ template_name = (
+ 'project/data_processing.cluster_templates/_nodegroups_list.html')
+ context = {"node_groups": cluster_template.node_groups}
+ return template.loader.render_to_string(template_name, context)
+
+
+class UploadFile(tables.LinkAction):
+ name = 'upload_file'
+ verbose_name = _("Upload Template")
+ url = 'horizon:project:data_processing.cluster_templates:upload_file'
+ classes = ("btn-launch", "ajax-modal")
+
+
+class CreateCluster(tables.LinkAction):
+ name = "create cluster"
+ verbose_name = _("Launch Cluster")
+ url = "horizon:project:data_processing.clusters:configure-cluster"
+ classes = ("btn-launch", "ajax-modal")
+
+ def get_link_url(self, datum):
+ base_url = urlresolvers.reverse(self.url)
+
+ params = http.urlencode({"hadoop_version": datum.hadoop_version,
+ "plugin_name": datum.plugin_name,
+ "cluster_template_id": datum.id})
+ return "?".join([base_url, params])
+
+
+class CopyTemplate(tables.LinkAction):
+ name = "copy"
+ verbose_name = _("Copy Template")
+ url = "horizon:project:data_processing.cluster_templates:copy"
+ classes = ("ajax-modal", )
+
+
+class DeleteTemplate(tables.BatchAction):
+ name = "delete_cluster_template"
+ verbose_name = _("Delete Template")
+ 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.cluster_template_delete(request, template_id)
+
+
+class CreateClusterTemplate(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Template")
+ url = ("horizon:project:data_processing.cluster_templates:"
+ "create-cluster-template")
+ classes = ("ajax-modal", "btn-create", "create-clustertemplate-btn")
+
+
+class ConfigureClusterTemplate(tables.LinkAction):
+ name = "configure"
+ verbose_name = _("Configure Cluster Template")
+ url = ("horizon:project:data_processing.cluster_templates:"
+ "configure-cluster-template")
+ classes = ("ajax-modal", "btn-create", "configure-clustertemplate-btn")
+ attrs = {"style": "display: none"}
+
+
+class ClusterTemplatesTable(tables.DataTable):
+ name = tables.Column("name",
+ verbose_name=_("Name"),
+ link=("horizon:project:data_processing.cluster_templates:details"))
+ plugin_name = tables.Column("plugin_name",
+ verbose_name=_("Plugin"))
+ hadoop_version = tables.Column("hadoop_version",
+ verbose_name=_("Hadoop Version"))
+ node_groups = tables.Column(render_node_groups,
+ verbose_name=_("Node Groups"))
+ description = tables.Column("description",
+ verbose_name=_("Description"))
+
+ class Meta:
+ name = "cluster_templates"
+ verbose_name = _("Cluster Templates")
+ table_actions = (UploadFile,
+ CreateClusterTemplate,
+ ConfigureClusterTemplate,
+ DeleteTemplate,)
+
+ row_actions = (CreateCluster,
+ CopyTemplate,
+ DeleteTemplate,)
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tabs.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tabs.py
new file mode 100644
index 0000000000..1ee07acbd8
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tabs.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 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
+from openstack_dashboard.dashboards.project. \
+ data_processing.utils import workflow_helpers as helpers
+
+
+LOG = logging.getLogger(__name__)
+
+
+class GeneralTab(tabs.Tab):
+ name = _("General Info")
+ slug = "cluster_template_details_tab"
+ template_name = (
+ "project/data_processing.cluster_templates/_details.html")
+
+ def get_context_data(self, request):
+ template_id = self.tab_group.kwargs['template_id']
+ try:
+ template = saharaclient.cluster_template_get(request, template_id)
+ except Exception:
+ template = {}
+ exceptions.handle(request,
+ _("Unable to fetch cluster template details."))
+ return {"template": template}
+
+
+class NodeGroupsTab(tabs.Tab):
+ name = _("Node Groups")
+ slug = "cluster_template_nodegroups_tab"
+ template_name = (
+ "project/data_processing.cluster_templates/_nodegroups_details.html")
+
+ def get_context_data(self, request):
+ template_id = self.tab_group.kwargs['template_id']
+ try:
+ template = saharaclient.cluster_template_get(request, template_id)
+ for ng in template.node_groups:
+ if not ng["flavor_id"]:
+ continue
+ ng["flavor_name"] = (
+ nova.flavor_get(request, ng["flavor_id"]).name)
+ ng["node_group_template"] = helpers.safe_call(
+ saharaclient.nodegroup_template_get,
+ request, ng.get("node_group_template_id", None))
+ except Exception:
+ template = {}
+ exceptions.handle(request,
+ _("Unable to fetch node group details."))
+ return {"template": template}
+
+
+class ClusterTemplateDetailsTabs(tabs.TabGroup):
+ slug = "cluster_template_details"
+ tabs = (GeneralTab, NodeGroupsTab, )
+ sticky = True
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_configure_general_help.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_configure_general_help.html
new file mode 100644
index 0000000000..78bbe302d1
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_configure_general_help.html
@@ -0,0 +1,22 @@
+{% load i18n horizon %}
+
+
+ {% blocktrans %}This Cluster Template will be created for:{% endblocktrans %}
+
+ {% blocktrans %}Plugin{% endblocktrans %} : {{ plugin_name }}
+
+ {% blocktrans %}Hadoop version{% endblocktrans %} : {{ hadoop_version }}
+
+
+
+ {% blocktrans %}The Cluster Template object should specify Node Group Templates that will be used to build a Hadoop Cluster.
+ You can add Node Groups using Node Group Templates on a "Node Groups" tab.{% endblocktrans %}
+
+
+ {% blocktrans %}You may set cluster scoped Hadoop configurations on corresponding tabs.{% endblocktrans %}
+
+
+ {% blocktrans %}The Cluster Template object may specify a list of processes in anti-affinity group.
+ That means these processes may not be launched more than once on a single host.{% endblocktrans %}
+
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_create_general_help.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_create_general_help.html
new file mode 100644
index 0000000000..09245e12f4
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_create_general_help.html
@@ -0,0 +1,4 @@
+{% load i18n horizon %}
+
+ {% blocktrans %}Select a plugin and Hadoop version for a new Cluster template.{% endblocktrans %}
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_details.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_details.html
new file mode 100644
index 0000000000..9e76151265
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_details.html
@@ -0,0 +1,54 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+{% trans "Template Overview" %}
+
+
+ {% trans "Name" %}
+ {{ template.name }}
+ {% trans "ID" %}
+ {{ template.id }}
+ {% trans "Description" %}
+ {{ template.description|default:"None" }}
+
+
+ {% trans "Plugin" %}
+ {{ template.plugin_name }}
+ {% trans "Hadoop Version" %}
+ {{ template.hadoop_version }}
+
+
+ {% trans "Anti-affinity enabled for" %}
+ {% if template.anti_affinity %}
+
+
+ {% for process in template.anti_affinity %}
+ {{ process }}
+ {% endfor %}
+
+
+ {% else %}
+ {% trans "no processes" %}
+ {% endif %}
+
+
+ {% trans "Node Configurations" %}
+ {% if template.cluster_configs %}
+
+ {% for service, service_conf in template.cluster_configs.items %}
+ {{ service }}
+ {% if service_conf %}
+
+ {% for conf_name, conf_value in service_conf.items %}
+ {{ conf_name }}: {{ conf_value }}
+ {% endfor %}
+
+ {% else %}
+ {% trans "No configurations" }%
+ {% endif %}
+ {% endfor %}
+
+ {% else %}
+ {% trans "Cluster configurations are not specified" %}
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_nodegroups_details.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_nodegroups_details.html
new file mode 100644
index 0000000000..6c80bd823c
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_nodegroups_details.html
@@ -0,0 +1,55 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+{% trans "Node Groups" %}
+
+ {% for node_group in template.node_groups %}
+
+ {% trans "Node Group" %}: {{ node_group.name }}
+ {% trans "Nodes Count" %}
+ {{ node_group.count }}
+
+ {% trans "Flavor" %}
+ {{ node_group.flavor_id|default:"Flavor is not specified" }}
+
+ {% trans "Template" %}
+ {% if node_group.node_group_template_id %}
+ {{ node_group.node_group_template.name }}
+ {% else %}
+ {% trans "Template not specified" %}
+ {% endif %}
+
+ {% trans "Node Processes" %}
+ {% if node_group.node_processes %}
+
+
+ {% for process in node_group.node_processes %}
+ {{ process }}
+ {% endfor %}
+
+
+ {% else %}
+ {% trans "Node processes are not specified" %}
+ {% endif %}
+
+ {% trans "Node Configurations" %}
+ {% if node_group.node_configs %}
+
+ {% for service, service_conf in node_group.node_configs.items %}
+ {{ service }}
+ {% if service_conf %}
+
+ {% for conf_name, conf_value in service_conf.items %}
+ {{ conf_name }}: {{ conf_value }}
+ {% endfor %}
+
+ {% else %}
+ {% trans "No configurations" %}
+ {% endif %}
+ {% endfor %}
+
+ {% else %}
+ {% trans "Node configurations are not specified" %}
+ {% endif %}
+
+ {% endfor %}
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_nodegroups_list.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_nodegroups_list.html
new file mode 100644
index 0000000000..7342684cdc
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_nodegroups_list.html
@@ -0,0 +1,5 @@
+
+ {% for group in node_groups %}
+ {{ group.name }}: {{ group.count }}
+ {% endfor %}
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_upload_file.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_upload_file.html
new file mode 100644
index 0000000000..90b460e004
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/_upload_file.html
@@ -0,0 +1,24 @@
+{% extends "horizon/common/_modal_form.html" %}
+
+{% load url from future %}
+
+{% load i18n %}
+
+{% block form_id %}upload_file{% endblock %}
+{% block form_action %}{% url 'horizon:project:data_processing.cluster_templates:upload_file' %}{% endblock %}
+{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
+
+{% block modal-header %}{% trans "Upload Template" %}{% endblock %}
+
+{% block modal-body %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/cluster_node_groups_template.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/cluster_node_groups_template.html
new file mode 100644
index 0000000000..50f68fab9d
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/cluster_node_groups_template.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+ Group Name
+ Template
+ Count
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/cluster_templates.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/cluster_templates.html
new file mode 100644
index 0000000000..2aa56dcde2
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/cluster_templates.html
@@ -0,0 +1,66 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Data Processing" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Data Processing - Cluster Templates") %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+ {{ cluster_templates_table.render }}
+
+
+
+
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/configure.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/configure.html
new file mode 100644
index 0000000000..34c30d5dc7
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/configure.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Cluster Template" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Cluster Template") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'horizon/common/_workflow.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/create.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/create.html
new file mode 100644
index 0000000000..34c30d5dc7
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Cluster Template" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Cluster Template") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'horizon/common/_workflow.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/details.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/details.html
new file mode 100644
index 0000000000..7becc22921
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/details.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Cluster Template Details" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Cluster Template Details") %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+ {{ tab_group.render }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/upload_file.html b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/upload_file.html
new file mode 100644
index 0000000000..45c6f890ad
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/templates/data_processing.cluster_templates/upload_file.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Upload Template" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Upload Template") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'project/data_processing.cluster_templates/_upload_file.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tests.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tests.py
new file mode 100644
index 0000000000..048eb63fa4
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/tests.py
@@ -0,0 +1,54 @@
+# 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.cluster_templates:index')
+DETAILS_URL = reverse(
+ 'horizon:project:data_processing.cluster_templates:details', args=['id'])
+
+
+class DataProcessingClusterTemplateTests(test.TestCase):
+ @test.create_stubs({api.sahara: ('cluster_template_list',)})
+ def test_index(self):
+ api.sahara.cluster_template_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.cluster_templates.list())
+ self.mox.ReplayAll()
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res,
+ 'project/data_processing.cluster_templates/'
+ 'cluster_templates.html')
+ self.assertContains(res, 'Cluster Templates')
+ self.assertContains(res, 'Name')
+
+ @test.create_stubs({api.sahara: ('cluster_template_get',),
+ api.nova: ('flavor_get',)})
+ def test_details(self):
+ flavor = self.flavors.first()
+ ct = self.cluster_templates.first()
+ api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
+ .MultipleTimes().AndReturn(flavor)
+ api.sahara.cluster_template_get(IsA(http.HttpRequest),
+ IsA(unicode)) \
+ .MultipleTimes().AndReturn(ct)
+ self.mox.ReplayAll()
+ res = self.client.get(DETAILS_URL)
+ self.assertTemplateUsed(res,
+ 'project/data_processing.cluster_templates/'
+ 'details.html')
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/urls.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/urls.py
new file mode 100644
index 0000000000..dd17d92b8e
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_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.cluster_templates.views as views
+
+
+urlpatterns = patterns('',
+ url(r'^$', views.ClusterTemplatesView.as_view(),
+ name='index'),
+ url(r'^$', views.ClusterTemplatesView.as_view(),
+ name='cluster-templates'),
+ url(r'^upload_file$',
+ views.UploadFileView.as_view(),
+ name='upload_file'),
+ url(r'^create-cluster-template$',
+ views.CreateClusterTemplateView.as_view(),
+ name='create-cluster-template'),
+ url(r'^configure-cluster-template$',
+ views.ConfigureClusterTemplateView.as_view(),
+ name='configure-cluster-template'),
+ url(r'^(?P[^/]+)$',
+ views.ClusterTemplateDetailsView.as_view(),
+ name='details'),
+ url(r'^(?P[^/]+)/copy$',
+ views.CopyClusterTemplateView.as_view(),
+ name='copy'))
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/views.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/views.py
new file mode 100644
index 0000000000..96a5204a00
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/views.py
@@ -0,0 +1,113 @@
+# 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.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import tables
+from horizon import tabs
+from horizon import workflows
+
+from openstack_dashboard.api import sahara as saharaclient
+from openstack_dashboard.dashboards.project.data_processing. \
+ cluster_templates import forms as cluster_forms
+import openstack_dashboard.dashboards.project.data_processing. \
+ cluster_templates.tables as ct_tables
+import openstack_dashboard.dashboards.project.data_processing. \
+ cluster_templates.tabs as _tabs
+import openstack_dashboard.dashboards.project.data_processing. \
+ cluster_templates.workflows.copy as copy_flow
+import openstack_dashboard.dashboards.project.data_processing. \
+ cluster_templates.workflows.create as create_flow
+
+LOG = logging.getLogger(__name__)
+
+
+class ClusterTemplatesView(tables.DataTableView):
+ table_class = ct_tables.ClusterTemplatesTable
+ template_name = (
+ 'project/data_processing.cluster_templates/cluster_templates.html')
+
+ def get_data(self):
+ cluster_templates = saharaclient.cluster_template_list(self.request)
+ return cluster_templates
+
+
+class ClusterTemplateDetailsView(tabs.TabView):
+ tab_group_class = _tabs.ClusterTemplateDetailsTabs
+ template_name = 'project/data_processing.cluster_templates/details.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(ClusterTemplateDetailsView, self)\
+ .get_context_data(**kwargs)
+ return context
+
+ def get_data(self):
+ pass
+
+
+class UploadFileView(forms.ModalFormView):
+ form_class = cluster_forms.UploadFileForm
+ template_name = (
+ 'project/data_processing.cluster_templates/upload_file.html')
+ success_url = reverse_lazy(
+ 'horizon:project:data_processing.cluster_templates:index')
+
+
+class CreateClusterTemplateView(workflows.WorkflowView):
+ workflow_class = create_flow.CreateClusterTemplate
+ success_url = ("horizon:project:data_processing.cluster_templates"
+ ":create-cluster-template")
+ classes = ("ajax-modal")
+ template_name = "project/data_processing.cluster_templates/create.html"
+
+
+class ConfigureClusterTemplateView(workflows.WorkflowView):
+ workflow_class = create_flow.ConfigureClusterTemplate
+ success_url = "horizon:project:data_processing.cluster_templates"
+ template_name = "project/data_processing.cluster_templates/configure.html"
+
+
+class CopyClusterTemplateView(workflows.WorkflowView):
+ workflow_class = copy_flow.CopyClusterTemplate
+ success_url = "horizon:project:data_processing.cluster_templates"
+ template_name = "project/data_processing.cluster_templates/configure.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(CopyClusterTemplateView, 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.cluster_template_get(self.request,
+ template_id)
+ except Exception:
+ template = {}
+ exceptions.handle(self.request,
+ _("Unable to fetch cluster template."))
+ self._object = template
+ return self._object
+
+ def get_initial(self):
+ initial = super(CopyClusterTemplateView, self).get_initial()
+ initial['template_id'] = self.kwargs['template_id']
+ return initial
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/__init__.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/copy.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/copy.py
new file mode 100644
index 0000000000..4b81e364f5
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/copy.py
@@ -0,0 +1,80 @@
+# 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. \
+ cluster_templates.workflows.create as create_flow
+import openstack_dashboard.dashboards.project.data_processing.utils. \
+ workflow_helpers as wf_helpers
+
+LOG = logging.getLogger(__name__)
+
+
+class CopyClusterTemplate(create_flow.ConfigureClusterTemplate):
+ success_message = _("Cluster Template copy %s created")
+
+ def __init__(self, request, context_seed, entry_point, *args, **kwargs):
+ template_id = context_seed["template_id"]
+ try:
+ template = saharaclient.cluster_template_get(request, template_id)
+ self._set_configs_to_copy(template.cluster_configs)
+
+ request.GET = request.GET.copy()
+ request.GET.update({"plugin_name": template.plugin_name,
+ "hadoop_version": template.hadoop_version,
+ "aa_groups": template.anti_affinity})
+
+ super(CopyClusterTemplate, self).__init__(request, context_seed,
+ entry_point, *args,
+ **kwargs)
+ #init Node Groups
+ for step in self.steps:
+ if isinstance(step, create_flow.ConfigureNodegroups):
+ ng_action = step.action
+ template_ngs = template.node_groups
+
+ if 'forms_ids' not in request.POST:
+ ng_action.groups = []
+ for id in range(0, len(template_ngs), 1):
+ group_name = "group_name_" + str(id)
+ template_id = "template_id_" + str(id)
+ count = "count_" + str(id)
+ templ_ng = template_ngs[id]
+ ng_action.groups.append(
+ {"name": templ_ng["name"],
+ "template_id":
+ templ_ng["node_group_template_id"],
+ "count": templ_ng["count"],
+ "id": id,
+ "deletable": "true"})
+
+ wf_helpers.build_node_group_fields(ng_action,
+ group_name,
+ template_id,
+ count)
+
+ elif isinstance(step, create_flow.GeneralConfig):
+ fields = step.action.fields
+ fields["cluster_template_name"].initial = \
+ template.name + "-copy"
+
+ fields["description"].initial = template.description
+ except Exception:
+ exceptions.handle(request,
+ _("Unable to fetch template to copy."))
diff --git a/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/create.py b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/create.py
new file mode 100644
index 0000000000..363b2083fd
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/cluster_templates/workflows/create.py
@@ -0,0 +1,306 @@
+# 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 _
+import json
+
+from horizon import exceptions
+from horizon import forms
+from horizon import workflows
+
+from openstack_dashboard.api import sahara as saharaclient
+from openstack_dashboard.dashboards.project.data_processing. \
+ utils import helpers as helpers
+from openstack_dashboard.dashboards.project.data_processing. \
+ utils import anti_affinity as aa
+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 SelectPluginAction(workflows.Action):
+ 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)
+
+ try:
+ plugins = saharaclient.plugin_list(request)
+ except Exception:
+ plugins = []
+ exceptions.handle(request,
+ _("Unable to fetch plugin 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 Meta:
+ name = _("Select plugin and hadoop version for cluster template")
+ help_text_template = ("project/data_processing.cluster_templates/"
+ "_create_general_help.html")
+
+
+class SelectPlugin(workflows.Step):
+ action_class = SelectPluginAction
+
+
+class CreateClusterTemplate(workflows.Workflow):
+ slug = "create_cluster_template"
+ name = _("Create Cluster Template")
+ finalize_button_name = _("Create")
+ success_message = _("Created")
+ failure_message = _("Could not create")
+ success_url = "horizon:project:data_processing.cluster_templates:index"
+ default_steps = (SelectPlugin,)
+
+
+class GeneralConfigAction(workflows.Action):
+ hidden_configure_field = forms.CharField(
+ required=False,
+ widget=forms.HiddenInput(attrs={"class": "hidden_configure_field"}))
+
+ hidden_to_delete_field = forms.CharField(
+ required=False,
+ widget=forms.HiddenInput(attrs={"class": "hidden_to_delete_field"}))
+
+ cluster_template_name = forms.CharField(label=_("Template Name"),
+ required=True)
+
+ description = forms.CharField(label=_("Description"),
+ required=False,
+ widget=forms.Textarea)
+
+ anti_affinity = aa.anti_affinity_field()
+
+ def __init__(self, request, *args, **kwargs):
+ super(GeneralConfigAction, self).__init__(request, *args, **kwargs)
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ self.fields["plugin_name"] = forms.CharField(
+ widget=forms.HiddenInput(),
+ initial=plugin
+ )
+ self.fields["hadoop_version"] = forms.CharField(
+ widget=forms.HiddenInput(),
+ initial=hadoop_version
+ )
+
+ populate_anti_affinity_choices = aa.populate_anti_affinity_choices
+
+ 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)
+
+ def clean(self):
+ cleaned_data = super(GeneralConfigAction, self).clean()
+ if cleaned_data.get("hidden_configure_field", None) \
+ == "create_nodegroup":
+ self._errors = dict()
+ return cleaned_data
+
+ class Meta:
+ name = _("Details")
+ help_text_template = ("project/data_processing.cluster_templates/"
+ "_configure_general_help.html")
+
+
+class GeneralConfig(workflows.Step):
+ action_class = GeneralConfigAction
+ contributes = ("hidden_configure_field", )
+
+ def contribute(self, data, context):
+ for k, v in data.items():
+ context["general_" + k] = v
+
+ post = self.workflow.request.POST
+ context['anti_affinity_info'] = post.getlist("anti_affinity")
+
+ return context
+
+
+class ConfigureNodegroupsAction(workflows.Action):
+ hidden_nodegroups_field = forms.CharField(
+ required=False,
+ widget=forms.HiddenInput(attrs={"class": "hidden_nodegroups_field"}))
+ forms_ids = forms.CharField(
+ required=False,
+ widget=forms.HiddenInput())
+
+ def __init__(self, request, *args, **kwargs):
+ super(ConfigureNodegroupsAction, self). \
+ __init__(request, *args, **kwargs)
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ self.templates = saharaclient.nodegroup_template_find(request,
+ plugin_name=plugin,
+ hadoop_version=hadoop_version)
+
+ deletable = request.REQUEST.get("deletable", dict())
+
+ if 'forms_ids' in request.POST:
+ self.groups = []
+ for id in json.loads(request.POST['forms_ids']):
+ group_name = "group_name_" + str(id)
+ template_id = "template_id_" + str(id)
+ count = "count_" + str(id)
+ self.groups.append({"name": request.POST[group_name],
+ "template_id": request.POST[template_id],
+ "count": request.POST[count],
+ "id": id,
+ "deletable": deletable.get(
+ request.POST[group_name], "true")})
+
+ whelpers.build_node_group_fields(self,
+ group_name,
+ template_id,
+ count)
+
+ def clean(self):
+ cleaned_data = super(ConfigureNodegroupsAction, self).clean()
+ if cleaned_data.get("hidden_nodegroups_field", None) \
+ == "create_nodegroup":
+ self._errors = dict()
+ return cleaned_data
+
+ class Meta:
+ name = _("Node Groups")
+
+
+class ConfigureNodegroups(workflows.Step):
+ action_class = ConfigureNodegroupsAction
+ contributes = ("hidden_nodegroups_field", )
+ template_name = ("project/data_processing.cluster_templates/"
+ "cluster_node_groups_template.html")
+
+ def contribute(self, data, context):
+ for k, v in data.items():
+ context["ng_" + k] = v
+ return context
+
+
+class ConfigureClusterTemplate(whelpers.ServiceParametersWorkflow,
+ whelpers.StatusFormatMixin):
+ slug = "configure_cluster_template"
+ name = _("Create Cluster Template")
+ finalize_button_name = _("Create")
+ success_message = _("Created Cluster Template %s")
+ name_property = "general_cluster_template_name"
+ success_url = "horizon:project:data_processing.cluster_templates:index"
+ default_steps = (GeneralConfig,
+ ConfigureNodegroups)
+
+ def __init__(self, request, context_seed, entry_point, *args, **kwargs):
+ ConfigureClusterTemplate._cls_registry = set([])
+
+ sahara = saharaclient.client(request)
+ hlps = helpers.Helpers(sahara)
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ general_parameters = hlps.get_cluster_general_configs(
+ plugin,
+ hadoop_version)
+ service_parameters = hlps.get_targeted_cluster_configs(
+ plugin,
+ hadoop_version)
+
+ self._populate_tabs(general_parameters, service_parameters)
+
+ super(ConfigureClusterTemplate, self).__init__(request,
+ context_seed,
+ entry_point,
+ *args, **kwargs)
+
+ def is_valid(self):
+ steps_valid = True
+ for step in self.steps:
+ if not step.action.is_valid():
+ steps_valid = False
+ step.has_errors = True
+ errors_fields = list(step.action.errors.keys())
+ step.action.errors_fields = errors_fields
+ if not steps_valid:
+ return steps_valid
+ return self.validate(self.context)
+
+ def handle(self, request, context):
+ try:
+ node_groups = []
+ configs_dict = whelpers.parse_configs_from_context(context,
+ self.defaults)
+
+ ids = json.loads(context['ng_forms_ids'])
+ for id in ids:
+ name = context['ng_group_name_' + str(id)]
+ template_id = context['ng_template_id_' + str(id)]
+ count = context['ng_count_' + str(id)]
+
+ ng = {"name": name,
+ "node_group_template_id": template_id,
+ "count": count}
+ node_groups.append(ng)
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ #TODO(nkonovalov): Fix client to support default_image_id
+ saharaclient.cluster_template_create(
+ request,
+ context["general_cluster_template_name"],
+ plugin,
+ hadoop_version,
+ context["general_description"],
+ configs_dict,
+ node_groups,
+ context["anti_affinity_info"])
+ return True
+ except api_base.APIException as e:
+ self.error_description = str(e)
+ return False
+ except Exception:
+ exceptions.handle(request,
+ _("Cluster template creation failed"))
+ return False
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/__init__.py b/openstack_dashboard/dashboards/project/data_processing/clusters/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/panel.py b/openstack_dashboard/dashboards/project/data_processing/clusters/panel.py
new file mode 100644
index 0000000000..90c908e46c
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/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 ClustersPanel(horizon.Panel):
+ name = _("Clusters")
+ slug = 'data_processing.clusters'
+ permissions = ('openstack.services.data_processing',)
+
+
+dashboard.Project.register(ClustersPanel)
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/tables.py b/openstack_dashboard/dashboards/project/data_processing/clusters/tables.py
new file mode 100644
index 0000000000..0d25e525d1
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/tables.py
@@ -0,0 +1,100 @@
+# 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 tables
+
+from openstack_dashboard.api import sahara as saharaclient
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateCluster(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Launch Cluster")
+ url = "horizon:project:data_processing.clusters:create-cluster"
+ classes = ("btn-launch", "ajax-modal")
+
+
+class ScaleCluster(tables.LinkAction):
+ name = "scale"
+ verbose_name = _("Scale Cluster")
+ url = "horizon:project:data_processing.clusters:scale"
+ classes = ("ajax-modal", "btn-edit")
+
+ def allowed(self, request, cluster=None):
+ return cluster.status == "Active"
+
+
+class DeleteCluster(tables.BatchAction):
+ name = "delete"
+ action_present = _("Delete")
+ action_past = _("Deleted")
+ data_type_singular = _("Cluster")
+ data_type_plural = _("Clusters")
+ classes = ('btn-danger', 'btn-terminate')
+
+ def action(self, request, obj_id):
+ saharaclient.cluster_delete(request, obj_id)
+
+
+class UpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, instance_id):
+ instance = saharaclient.cluster_get(request, instance_id)
+ return instance
+
+
+def get_instances_count(cluster):
+ return sum([len(ng["instances"])
+ for ng in cluster.node_groups])
+
+
+class ConfigureCluster(tables.LinkAction):
+ name = "configure"
+ verbose_name = _("Configure Cluster")
+ url = "horizon:project:data_processing.clusters:configure-cluster"
+ classes = ("ajax-modal", "btn-create", "configure-cluster-btn")
+ attrs = {"style": "display: none"}
+
+
+class ClustersTable(tables.DataTable):
+ STATUS_CHOICES = (
+ ("active", True),
+ ("error", False)
+ )
+
+ name = tables.Column("name",
+ verbose_name=_("Name"),
+ link=("horizon:project:data_processing.clusters:details"))
+ status = tables.Column("status",
+ verbose_name=_("Status"),
+ status=True,
+ status_choices=STATUS_CHOICES)
+ instances_count = tables.Column(get_instances_count,
+ verbose_name=_("Instances Count"))
+
+ class Meta:
+ name = "clusters"
+ verbose_name = _("Clusters")
+ row_class = UpdateRow
+ status_columns = ["status"]
+ table_actions = (CreateCluster,
+ ConfigureCluster,
+ DeleteCluster)
+ row_actions = (ScaleCluster,
+ DeleteCluster,)
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/tabs.py b/openstack_dashboard/dashboards/project/data_processing/clusters/tabs.py
new file mode 100644
index 0000000000..d18b6cdf02
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/tabs.py
@@ -0,0 +1,170 @@
+# 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 openstack_dashboard.dashboards.project. \
+ data_processing.utils import workflow_helpers as helpers
+
+from openstack_dashboard.api import glance
+from openstack_dashboard.api import neutron
+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 = "cluster_details_tab"
+ template_name = "project/data_processing.clusters/_details.html"
+
+ def get_context_data(self, request):
+ cluster_id = self.tab_group.kwargs['cluster_id']
+ cluster_info = {}
+ try:
+ sahara = saharaclient.client(request)
+ cluster = sahara.clusters.get(cluster_id)
+
+ for info_key, info_val in cluster.info.items():
+ for key, val in info_val.items():
+ if str(val).startswith(('http://', 'https://')):
+ cluster.info[info_key][key] = build_link(val)
+
+ base_image = glance.image_get(request,
+ cluster.default_image_id)
+
+ if getattr(cluster, 'cluster_template_id', None):
+ cluster_template = helpers.safe_call(
+ sahara.cluster_templates.get,
+ cluster.cluster_template_id)
+ else:
+ cluster_template = None
+
+ if getattr(cluster, 'neutron_management_network', None):
+ net_id = cluster.neutron_management_network
+ network = neutron.network_get(request, net_id)
+ network.set_id_as_name_if_empty()
+ net_name = network.name
+ else:
+ net_name = None
+
+ cluster_info.update({"cluster": cluster,
+ "base_image": base_image,
+ "cluster_template": cluster_template,
+ "network": net_name})
+ except Exception as e:
+ LOG.error("Unable to fetch cluster details: %s" % str(e))
+
+ return cluster_info
+
+
+def build_link(url):
+ return "" + url + " "
+
+
+class NodeGroupsTab(tabs.Tab):
+ name = _("Node Groups")
+ slug = "cluster_nodegroups_tab"
+ template_name = (
+ "project/data_processing.clusters/_nodegroups_details.html")
+
+ def get_context_data(self, request):
+ cluster_id = self.tab_group.kwargs['cluster_id']
+ try:
+ sahara = saharaclient.client(request)
+ cluster = sahara.clusters.get(cluster_id)
+ for ng in cluster.node_groups:
+ if not ng["flavor_id"]:
+ continue
+ ng["flavor_name"] = (
+ nova.flavor_get(request, ng["flavor_id"]).name)
+ ng["node_group_template"] = helpers.safe_call(
+ sahara.node_group_templates.get,
+ ng.get("node_group_template_id", None))
+ except Exception:
+ cluster = {}
+ exceptions.handle(request,
+ _("Unable to get node group details."))
+
+ return {"cluster": cluster}
+
+
+class Instance(object):
+ def __init__(self, name=None, id=None, internal_ip=None,
+ management_ip=None):
+ self.name = name
+ self.id = id
+ self.internal_ip = internal_ip
+ self.management_ip = management_ip
+
+
+class InstancesTable(tables.DataTable):
+ name = tables.Column("name",
+ link=("horizon:project:instances:detail"),
+ verbose_name=_("Name"))
+
+ internal_ip = tables.Column("internal_ip",
+ verbose_name=_("Internal IP"))
+
+ management_ip = tables.Column("management_ip",
+ verbose_name=_("Management IP"))
+
+ class Meta:
+ name = "cluster_instances"
+ #just ignoring the name
+ verbose_name = _(" ")
+
+
+class InstancesTab(tabs.TableTab):
+ name = _("Instances")
+ slug = "cluster_instances_tab"
+ template_name = "project/data_processing.clusters/_instances_details.html"
+ table_classes = (InstancesTable, )
+
+ def get_cluster_instances_data(self):
+ cluster_id = self.tab_group.kwargs['cluster_id']
+
+ try:
+ sahara = saharaclient.client(self.request)
+ cluster = sahara.clusters.get(cluster_id)
+
+ instances = []
+ for ng in cluster.node_groups:
+ for instance in ng["instances"]:
+ instances.append(Instance(
+ name=instance["instance_name"],
+ id=instance["instance_id"],
+ internal_ip=instance.get("internal_ip",
+ "Not assigned"),
+ management_ip=instance.get("management_ip",
+ "Not assigned")))
+ except Exception:
+ instances = []
+ exceptions.handle(self.request,
+ _("Unable to fetch instance details."))
+ return instances
+
+
+class ClusterDetailsTabs(tabs.TabGroup):
+ slug = "cluster_details"
+ tabs = (GeneralTab, NodeGroupsTab, InstancesTab, )
+ sticky = True
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_configure_general_help.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_configure_general_help.html
new file mode 100644
index 0000000000..39ee29e4e3
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_configure_general_help.html
@@ -0,0 +1,20 @@
+{% load i18n horizon %}
+
+
+ {% blocktrans %}This Cluster will be started with:{% endblocktrans %}
+
+ {% blocktrans %}Plugin{% endblocktrans %} : {{ plugin_name }}
+
+ {% blocktrans %}Hadoop version{% endblocktrans %} : {{ hadoop_version }}
+
+
+
+ {% blocktrans %}Cluster can be launched using existing Cluster Templates.{% endblocktrans %}
+
+
+ {% blocktrans %}The Cluster object should specify OpenStack Image to boot instances for Hadoop Cluster.{% endblocktrans %}
+
+
+ {% blocktrans %}User has to choose a keypair to have access to clusters instances.{% endblocktrans %}
+
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_create_cluster.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_create_cluster.html
new file mode 100644
index 0000000000..f22b7a5e60
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_create_cluster.html
@@ -0,0 +1,23 @@
+{% extends "horizon/common/_modal_form.html" %}
+
+{% load url from future %}
+
+{% load i18n %}
+
+{% block form_id %}create_cluster_form{% endblock %}
+{% block form_action %}{% url 'horizon:project:data_processing.clusters:create' %}{% endblock %}
+
+{% block modal-header %}{% trans "Launch Cluster" %}{% endblock %}
+
+{% block modal-body %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_create_general_help.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_create_general_help.html
new file mode 100644
index 0000000000..0734c0d9be
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_create_general_help.html
@@ -0,0 +1,4 @@
+{% load i18n %}
+
+ {% trans "Select a plugin and Hadoop version for a new Cluster." %}
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_details.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_details.html
new file mode 100644
index 0000000000..225892c400
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_details.html
@@ -0,0 +1,89 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+{% trans "Cluster Overview" %}
+
+
+ {% trans "Name" %}
+ {{ cluster.name }}
+ {% trans "ID" %}
+ {{ cluster.id }}
+ {% trans "Description" %}
+ {{ cluster.description|default:"None" }}
+ {% trans "Status" %}
+ {{ cluster.status }}
+
+
+ {% if cluster.error_description %}
+
{% trans "Error Details" %}
+
+ {{ cluster.error_description }}
+
+ {% endif %}
+
+
+ {% trans "Plugin" %}
+ {{ cluster.plugin_name }}
+ {% trans "Hadoop Version" %}
+ {{ cluster.hadoop_version }}
+
+
+ {% trans "Template" %}
+ {% if cluster_template %}
+ {{ cluster_template.name }}
+ {% else %}
+ {% trans "Template not specified" %}
+ {% endif %}
+ {% trans "Base Image" %}
+ {{ base_image.name }}
+ {% if network %}
+ {% trans "Neutron Management Network" %}
+ {{ network }}
+ {% endif %}
+ {% trans "Keypair" %}
+ {{ cluster.user_keypair_id }}
+
+
+ {% trans "Anti-affinity enabled for" %}
+ {% if cluster.anti_affinity %}
+
+
+ {% for process in cluster.anti_affinity %}
+ {{ process }}
+ {% endfor %}
+
+
+ {% else %}
+ {% trans "no processes" %}
+ {% endif %}
+
+
+ {% trans "Node Configurations" %}
+ {% if cluster.cluster_configs %}
+
+ {% for service, service_conf in cluster.cluster_configs.items %}
+ {{ service }}
+ {% if service_conf %}
+
+ {% for conf_name, conf_value in service_conf.items %}
+ {{ conf_name }}: {{ conf_value }}
+ {% endfor %}
+
+ {% else %}
+ {% trans "No configurations" %}
+ {% endif %}
+ {% endfor %}
+
+ {% else %}
+ {% trans "Cluster configurations are not specified" %}
+ {% endif %}
+
+
+
+ {% for info_key, info_val in cluster.info.items %}
+ {{ info_key }}
+ {% for key, val in info_val.items %}
+ {{ key }}: {% autoescape off %}{{ val }}{% endautoescape %}
+ {% endfor %}
+ {% endfor %}
+
+
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_instances_details.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_instances_details.html
new file mode 100644
index 0000000000..5f68a67fe8
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_instances_details.html
@@ -0,0 +1,5 @@
+{% load i18n sizeformat %}
+{% trans "Cluster Instances" %}
+
+ {{ cluster_instances_table.render }}
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_nodegroups_details.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_nodegroups_details.html
new file mode 100644
index 0000000000..a88f3aa81e
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/_nodegroups_details.html
@@ -0,0 +1,63 @@
+{% load i18n sizeformat %}
+
+{% load url from future %}
+
+
+{% trans "Node Groups" %}
+
+ {% for node_group in cluster.node_groups %}
+
+ {% trans "Name" %}: {{ node_group.name }}
+ {% trans "Number of Nodes" %}
+ {{ node_group.count }}
+
+ {% trans "Flavor" %}
+ {{ node_group.flavor_name|default:"Flavor is not specified" }}
+
+ {% if node_group.floating_ip_pool %}
+ {% trans "Floating IP Pool" %}
+ {{ node_group.floating_ip_pool }}
+ {% endif %}
+
+ {% trans "Template" %}
+ {% if node_group.node_group_template_id %}
+ {{ node_group.node_group_template.name }}
+ {% else %}
+ {% trans "Template not specified" %}
+ {% endif %}
+
+ {% trans "Node Processes" %}
+ {% if node_group.node_processes %}
+
+
+ {% for process in node_group.node_processes %}
+ {{ process }}
+ {% endfor %}
+
+
+ {% else %}
+ {% trans "Node processes are not specified" %}
+ {% endif %}
+
+ {% trans "Node Configurations" %}
+ {% if node_group.node_configs %}
+
+ {% for service, service_conf in node_group.node_configs.items %}
+ {{ service }}
+ {% if service_conf %}
+
+ {% for conf_name, conf_value in service_conf.items %}
+ {{ conf_name }}: {{ conf_value }}
+ {% endfor %}
+
+ {% else %}
+ {% trans "No configurations" %}
+ {% endif %}
+ {% endfor %}
+
+ {% else %}
+ {% trans "Node configurations are not specified" %}
+ {% endif %}
+
+ {% endfor %}
+
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/clusters.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/clusters.html
new file mode 100644
index 0000000000..2fa0cef869
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/clusters.html
@@ -0,0 +1,61 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Data Processing" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Data Processing - Clusters") %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+ {{ clusters_table.render }}
+
+
+
+
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/configure.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/configure.html
new file mode 100644
index 0000000000..cbe7258511
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/configure.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Launch Cluster" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Launch Cluster") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'horizon/common/_workflow.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/create.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/create.html
new file mode 100644
index 0000000000..cbe7258511
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Launch Cluster" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Launch Cluster") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'horizon/common/_workflow.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/create_cluster.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/create_cluster.html
new file mode 100644
index 0000000000..1933aa2f44
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/create_cluster.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Launch Cluster" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Launch Cluster") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'templates/data_processing.clusters/_create_cluster.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/details.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/details.html
new file mode 100644
index 0000000000..ef8fea80d0
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/details.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Cluster Details" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Cluster Details") %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+ {{ tab_group.render }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/scale.html b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/scale.html
new file mode 100644
index 0000000000..5672d26ead
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/templates/data_processing.clusters/scale.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Scale Cluster" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Scale Cluster") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'horizon/common/_workflow.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/tests.py b/openstack_dashboard/dashboards/project/data_processing/clusters/tests.py
new file mode 100644
index 0000000000..40bc4da800
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/tests.py
@@ -0,0 +1,37 @@
+# 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.clusters:index')
+DETAILS_URL = reverse(
+ 'horizon:project:data_processing.clusters:details', args=['id'])
+
+
+class DataProcessingClusterTests(test.TestCase):
+ @test.create_stubs({api.sahara: ('cluster_list',)})
+ def test_index(self):
+ api.sahara.cluster_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.clusters.list())
+ self.mox.ReplayAll()
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res,
+ 'project/data_processing.clusters/clusters.html')
+ self.assertContains(res, 'Clusters')
+ self.assertContains(res, 'Name')
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/urls.py b/openstack_dashboard/dashboards/project/data_processing/clusters/urls.py
new file mode 100644
index 0000000000..d252289c46
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/urls.py
@@ -0,0 +1,38 @@
+# 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. \
+ clusters.views as views
+
+
+urlpatterns = patterns('',
+ url(r'^$', views.ClustersView.as_view(),
+ name='index'),
+ url(r'^$', views.ClustersView.as_view(),
+ name='clusters'),
+ url(r'^create-cluster$',
+ views.CreateClusterView.as_view(),
+ name='create-cluster'),
+ url(r'^configure-cluster$',
+ views.ConfigureClusterView.as_view(),
+ name='configure-cluster'),
+ url(r'^(?P[^/]+)$',
+ views.ClusterDetailsView.as_view(),
+ name='details'),
+ url(r'^(?P[^/]+)/scale$',
+ views.ScaleClusterView.as_view(),
+ name='scale'))
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/views.py b/openstack_dashboard/dashboards/project/data_processing/clusters/views.py
new file mode 100644
index 0000000000..332d67dc1d
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/views.py
@@ -0,0 +1,104 @@
+# 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.clusters. \
+ tables as c_tables
+import openstack_dashboard.dashboards.project.data_processing.clusters.tabs \
+ as _tabs
+import openstack_dashboard.dashboards.project.data_processing.clusters. \
+ workflows.create as create_flow
+import openstack_dashboard.dashboards.project.data_processing.clusters. \
+ workflows.scale as scale_flow
+
+LOG = logging.getLogger(__name__)
+
+
+class ClustersView(tables.DataTableView):
+ table_class = c_tables.ClustersTable
+ template_name = 'project/data_processing.clusters/clusters.html'
+
+ def get_data(self):
+ try:
+ clusters = saharaclient.cluster_list(self.request)
+ except Exception:
+ clusters = []
+ exceptions.handle(self.request,
+ _("Unable to fetch cluster list"))
+ return clusters
+
+
+class ClusterDetailsView(tabs.TabView):
+ tab_group_class = _tabs.ClusterDetailsTabs
+ template_name = 'project/data_processing.clusters/details.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(ClusterDetailsView, self)\
+ .get_context_data(**kwargs)
+ return context
+
+
+class CreateClusterView(workflows.WorkflowView):
+ workflow_class = create_flow.CreateCluster
+ success_url = \
+ "horizon:project:data_processing.clusters:create-cluster"
+ classes = ("ajax-modal")
+ template_name = "project/data_processing.clusters/create.html"
+
+
+class ConfigureClusterView(workflows.WorkflowView):
+ workflow_class = create_flow.ConfigureCluster
+ success_url = "horizon:project:data_processing.clusters"
+ template_name = "project/data_processing.clusters/configure.html"
+
+
+class ScaleClusterView(workflows.WorkflowView):
+ workflow_class = scale_flow.ScaleCluster
+ success_url = "horizon:project:data_processing.clusters"
+ classes = ("ajax-modal")
+ template_name = "project/data_processing.clusters/scale.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(ScaleClusterView, self)\
+ .get_context_data(**kwargs)
+
+ context["cluster_id"] = kwargs["cluster_id"]
+ return context
+
+ def get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ template_id = self.kwargs['cluster_id']
+ try:
+ template = saharaclient.cluster_template_get(self.request,
+ template_id)
+ except Exception:
+ template = None
+ exceptions.handle(self.request,
+ _("Unable to fetch cluster template."))
+ self._object = template
+ return self._object
+
+ def get_initial(self):
+ initial = super(ScaleClusterView, self).get_initial()
+ initial.update({'cluster_id': self.kwargs['cluster_id']})
+ return initial
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/__init__.py b/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/create.py b/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/create.py
new file mode 100644
index 0000000000..e7a0a540ae
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/create.py
@@ -0,0 +1,230 @@
+# 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 exceptions
+from horizon import forms
+from horizon import workflows
+
+from openstack_dashboard.api import nova
+
+from openstack_dashboard.dashboards.project.data_processing.utils \
+ import neutron_support
+import openstack_dashboard.dashboards.project.data_processing.utils. \
+ workflow_helpers as whelpers
+
+
+from django.utils.translation import ugettext_lazy as _
+
+from openstack_dashboard.api import sahara as saharaclient
+import openstack_dashboard.dashboards.project.data_processing. \
+ cluster_templates. workflows.create as t_flows
+
+from saharaclient.api import base as api_base
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+class SelectPluginAction(t_flows.SelectPluginAction):
+ class Meta:
+ name = _("Select plugin and hadoop version for cluster")
+ help_text_template = (
+ "project/data_processing.clusters/_create_general_help.html")
+
+
+class SelectPlugin(t_flows.SelectPlugin):
+ pass
+
+
+class CreateCluster(t_flows.CreateClusterTemplate):
+ slug = "create_cluster"
+ name = _("Launch Cluster")
+ success_url = "horizon:project:data_processing.cluster_templates:index"
+
+
+class GeneralConfigAction(workflows.Action):
+ populate_neutron_management_network_choices = \
+ neutron_support.populate_neutron_management_network_choices
+
+ hidden_configure_field = forms.CharField(
+ required=False,
+ widget=forms.HiddenInput(attrs={"class": "hidden_configure_field"}))
+
+ hidden_to_delete_field = forms.CharField(
+ required=False,
+ widget=forms.HiddenInput(attrs={"class": "hidden_to_delete_field"}))
+
+ cluster_name = forms.CharField(label=_("Cluster Name"),
+ required=True)
+
+ description = forms.CharField(label=_("Description"),
+ required=False,
+ widget=forms.Textarea)
+ cluster_template = forms.ChoiceField(label=_("Cluster Template"),
+ initial=(None, "None"),
+ required=False)
+
+ image = forms.ChoiceField(label=_("Base Image"),
+ required=True)
+
+ keypair = forms.ChoiceField(
+ label=_("Keypair"),
+ required=False,
+ help_text=_("Which keypair to use for authentication."))
+
+ def __init__(self, request, *args, **kwargs):
+ super(GeneralConfigAction, self).__init__(request, *args, **kwargs)
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ if saharaclient.base.is_service_enabled(request, 'network'):
+ self.fields["neutron_management_network"] = forms.ChoiceField(
+ label=_("Neutron Management Network"),
+ required=True,
+ choices=self.populate_neutron_management_network_choices(
+ request, {})
+ )
+
+ self.fields["plugin_name"] = forms.CharField(
+ widget=forms.HiddenInput(),
+ initial=plugin
+ )
+ self.fields["hadoop_version"] = forms.CharField(
+ widget=forms.HiddenInput(),
+ initial=hadoop_version
+ )
+
+ def populate_image_choices(self, request, context):
+ try:
+ all_images = saharaclient.image_list(request)
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ details = saharaclient.plugin_get_version_details(request,
+ plugin,
+ hadoop_version)
+
+ return [(image.id, image.name) for image in all_images
+ if set(details.required_image_tags).
+ issubset(set(image.tags))]
+ except Exception:
+ exceptions.handle(request,
+ _("Unable to fetch image choices."))
+ return []
+
+ def populate_keypair_choices(self, request, context):
+ try:
+ keypairs = nova.keypair_list(request)
+ except Exception:
+ keypairs = []
+ exceptions.handle(request,
+ _("Unable to fetch keypair choices."))
+ keypair_list = [(kp.name, kp.name) for kp in keypairs]
+ keypair_list.insert(0, ("", _("No keypair")))
+
+ return keypair_list
+
+ def populate_cluster_template_choices(self, request, context):
+ templates = saharaclient.cluster_template_list(request)
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ choices = [(template.id, template.name)
+ for template in templates
+ if (template.hadoop_version == hadoop_version and
+ template.plugin_name == plugin)]
+
+ # cluster_template_id comes from cluster templates table, when
+ # Create Cluster from template is clicked there
+ selected_template_id = request.REQUEST.get("cluster_template_id", None)
+
+ for template in templates:
+ if template.id == selected_template_id:
+ self.fields['cluster_template'].initial = template.id
+
+ return choices
+
+ 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)
+
+ def clean(self):
+ cleaned_data = super(GeneralConfigAction, self).clean()
+ if cleaned_data.get("hidden_configure_field", None) \
+ == "create_nodegroup":
+ self._errors = dict()
+ return cleaned_data
+
+ class Meta:
+ name = _("Configure Cluster")
+ help_text_template = \
+ ("project/data_processing.clusters/_configure_general_help.html")
+
+
+class GeneralConfig(workflows.Step):
+ action_class = GeneralConfigAction
+ contributes = ("hidden_configure_field", )
+
+ def contribute(self, data, context):
+ for k, v in data.items():
+ context["general_" + k] = v
+
+ return context
+
+
+class ConfigureCluster(whelpers.StatusFormatMixin, workflows.Workflow):
+ slug = "configure_cluster"
+ name = _("Launch Cluster")
+ finalize_button_name = _("Create")
+ success_message = _("Created Cluster %s")
+ name_property = "general_cluster_name"
+ success_url = "horizon:project:data_processing.clusters:index"
+ default_steps = (GeneralConfig, )
+
+ def handle(self, request, context):
+ try:
+ #TODO(nkonovalov) Implement AJAX Node Groups
+ node_groups = None
+
+ plugin, hadoop_version = whelpers.\
+ get_plugin_and_hadoop_version(request)
+
+ cluster_template_id = context["general_cluster_template"] or None
+ user_keypair = context["general_keypair"] or None
+
+ saharaclient.cluster_create(
+ request,
+ context["general_cluster_name"],
+ plugin, hadoop_version,
+ cluster_template_id=cluster_template_id,
+ default_image_id=context["general_image"],
+ description=context["general_description"],
+ node_groups=node_groups,
+ user_keypair_id=user_keypair,
+ net_id=context.get("general_neutron_management_network", None))
+ return True
+ except api_base.APIException as e:
+ self.error_description = str(e)
+ return False
+ except Exception:
+ exceptions.handle(request,
+ _('Unable to create the cluster'))
+ return False
diff --git a/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/scale.py b/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/scale.py
new file mode 100644
index 0000000000..3029df74e6
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/clusters/workflows/scale.py
@@ -0,0 +1,166 @@
+# 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 json
+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. \
+ cluster_templates.workflows.create as clt_create_flow
+import openstack_dashboard.dashboards.project.data_processing. \
+ clusters.workflows.create as cl_create_flow
+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 NodeGroupsStep(clt_create_flow.ConfigureNodegroups):
+ pass
+
+
+class ScaleCluster(cl_create_flow.ConfigureCluster,
+ whelpers.StatusFormatMixin):
+ slug = "scale_cluster"
+ name = _("Scale Cluster")
+ finalize_button_name = _("Scale")
+ success_url = "horizon:project:data_processing.clusters:index"
+ default_steps = (NodeGroupsStep, )
+
+ def __init__(self, request, context_seed, entry_point, *args, **kwargs):
+ ScaleCluster._cls_registry = set([])
+
+ self.success_message = _("Scaled cluster successfully started.")
+
+ cluster_id = context_seed["cluster_id"]
+ try:
+ cluster = saharaclient.cluster_get(request, cluster_id)
+ plugin = cluster.plugin_name
+ hadoop_version = cluster.hadoop_version
+
+ #init deletable nodegroups
+ deletable = dict()
+ for group in cluster.node_groups:
+ deletable[group["name"]] = "false"
+ request.GET = request.GET.copy()
+ request.GET.update({
+ "cluster_id": cluster_id,
+ "plugin_name": plugin,
+ "hadoop_version": hadoop_version,
+ "deletable": deletable
+ })
+
+ super(ScaleCluster, self).__init__(request, context_seed,
+ entry_point, *args,
+ **kwargs)
+
+ #init Node Groups
+
+ for step in self.steps:
+ if isinstance(step, clt_create_flow.ConfigureNodegroups):
+ ng_action = step.action
+ template_ngs = cluster.node_groups
+
+ if 'forms_ids' not in request.POST:
+ ng_action.groups = []
+ for id in range(0, len(template_ngs), 1):
+ group_name = "group_name_" + str(id)
+ template_id = "template_id_" + str(id)
+ count = "count_" + str(id)
+ templ_ng = template_ngs[id]
+ ng_action.groups.append(
+ {"name": templ_ng["name"],
+ "template_id": templ_ng["node_group_template_id"],
+ "count": templ_ng["count"],
+ "id": id,
+ "deletable": "false"})
+
+ whelpers.build_node_group_fields(ng_action,
+ group_name,
+ template_id,
+ count)
+ except Exception:
+ exceptions.handle(request,
+ _("Unable to fetch cluster to scale"))
+
+ def format_status_message(self, message):
+ # Scaling form requires special handling because it has no Cluster name
+ # in it's context
+
+ error_description = getattr(self, 'error_description', None)
+ if error_description:
+ return error_description
+ else:
+ return self.success_message
+
+ def handle(self, request, context):
+ cluster_id = request.GET["cluster_id"]
+ try:
+ cluster = saharaclient.cluster_get(request, cluster_id)
+ existing_node_groups = set([])
+ for ng in cluster.node_groups:
+ existing_node_groups.add(ng["name"])
+
+ scale_object = dict()
+
+ ids = json.loads(context["ng_forms_ids"])
+
+ for _id in ids:
+ name = context["ng_group_name_%s" % _id]
+ template_id = context["ng_template_id_%s" % _id]
+ count = context["ng_count_%s" % _id]
+
+ if name not in existing_node_groups:
+ if "add_node_groups" not in scale_object:
+ scale_object["add_node_groups"] = []
+
+ scale_object["add_node_groups"].append(
+ {"name": name,
+ "node_group_template_id": template_id,
+ "count": int(count)})
+ else:
+ old_count = None
+ for ng in cluster.node_groups:
+ if name == ng["name"]:
+ old_count = ng["count"]
+ break
+
+ if old_count != count:
+ if "resize_node_groups" not in scale_object:
+ scale_object["resize_node_groups"] = []
+
+ scale_object["resize_node_groups"].append(
+ {"name": name,
+ "count": int(count)}
+ )
+ except Exception:
+ scale_object = {}
+ exceptions.handle(request,
+ _("Unable to fetch cluster to scale."))
+
+ try:
+ saharaclient.cluster_scale(request, cluster_id, scale_object)
+ return True
+ except api_base.APIException as e:
+ self.error_description = str(e)
+ return False
+ except Exception:
+ exceptions.handle(request,
+ _("Scale cluster operation failed"))
+ return False
diff --git a/openstack_dashboard/dashboards/project/data_processing/utils/anti_affinity.py b/openstack_dashboard/dashboards/project/data_processing/utils/anti_affinity.py
new file mode 100644
index 0000000000..31defc5bd2
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/utils/anti_affinity.py
@@ -0,0 +1,67 @@
+# 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 forms
+
+from openstack_dashboard.api import sahara as saharaclient
+import openstack_dashboard.dashboards.project.data_processing. \
+ utils.workflow_helpers as whelpers
+
+
+LOG = logging.getLogger(__name__)
+
+
+def anti_affinity_field():
+ return forms.MultipleChoiceField(
+ label=_("Use anti-affinity groups for: "),
+ required=False,
+ help_text=_("Use anti-affinity groups for processes"),
+ widget=forms.CheckboxSelectMultiple()
+ )
+
+
+def populate_anti_affinity_choices(self, request, context):
+ try:
+ sahara = saharaclient.client(request)
+ plugin, version = whelpers.get_plugin_and_hadoop_version(request)
+
+ version_details = sahara.plugins.get_version_details(plugin, version)
+ process_choices = []
+ for processes in version_details.node_processes.values():
+ for process in processes:
+ process_choices.append((process, process))
+
+ cluster_template_id = request.REQUEST.get("cluster_template_id", None)
+ if cluster_template_id is None:
+ selected_processes = request.REQUEST.get("aa_groups", [])
+ else:
+ cluster_template = (
+ sahara.cluster_templates.get(cluster_template_id))
+ selected_processes = cluster_template.anti_affinity
+
+ checked_dict = dict()
+
+ for process in selected_processes:
+ checked_dict[process] = process
+
+ self.fields['anti_affinity'].initial = checked_dict
+ except Exception:
+ process_choices = []
+ exceptions.handle(request,
+ _("Unable to populate anti-affinity processes."))
+ return process_choices
diff --git a/openstack_dashboard/dashboards/project/data_processing/utils/neutron_support.py b/openstack_dashboard/dashboards/project/data_processing/utils/neutron_support.py
new file mode 100644
index 0000000000..4128ee7fdd
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/data_processing/utils/neutron_support.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2013 Mirantis Inc.
+#
+# 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 exceptions
+
+from openstack_dashboard.api import neutron
+
+
+def populate_neutron_management_network_choices(self, request, context):
+ try:
+ tenant_id = self.request.user.tenant_id
+ networks = neutron.network_list_for_tenant(request, tenant_id)
+ for n in networks:
+ n.set_id_as_name_if_empty()
+ network_list = [(network.id, network.name) for network in networks]
+ except Exception:
+ network_list = []
+ exceptions.handle(request,
+ _('Unable to retrieve networks.'))
+ return network_list
diff --git a/openstack_dashboard/test/test_data/sahara_data.py b/openstack_dashboard/test/test_data/sahara_data.py
index a59d192331..c922ef291f 100644
--- a/openstack_dashboard/test/test_data/sahara_data.py
+++ b/openstack_dashboard/test/test_data/sahara_data.py
@@ -12,6 +12,8 @@
from openstack_dashboard.test.test_data import utils
+from saharaclient.api import cluster_templates
+from saharaclient.api import clusters
from saharaclient.api import node_group_templates
from saharaclient.api import plugins
@@ -19,6 +21,8 @@ from saharaclient.api import plugins
def data(TEST):
TEST.plugins = utils.TestDataContainer()
TEST.nodegroup_templates = utils.TestDataContainer()
+ TEST.cluster_templates = utils.TestDataContainer()
+ TEST.clusters = utils.TestDataContainer()
plugin1_dict = {
"description": "vanilla plugin",
@@ -61,3 +65,167 @@ def data(TEST):
node_group_templates.NodeGroupTemplateManager(None), ngt1_dict)
TEST.nodegroup_templates.add(ngt1)
+
+ #Cluster_templates
+ ct1_dict = {
+ "anti_affinity": [],
+ "cluster_configs": {},
+ "created_at": "2014-06-04 14:01:06.460711",
+ "default_image_id": None,
+ "description": None,
+ "hadoop_version": "1.2.1",
+ "id": "a2c3743f-31a2-4919-8d02-792138a87a98",
+ "name": "sample-cluster-template",
+ "neutron_management_network": None,
+ "node_groups": [
+ {
+ "count": 1,
+ "created_at": "2014-06-04 14:01:06.462512",
+ "flavor_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
+ "floating_ip_pool": None,
+ "image_id": None,
+ "name": "master",
+ "node_configs": {},
+ "node_group_template_id": "c166dfcc-9cc7-4b48-adc9",
+ "node_processes": [
+ "namenode",
+ "jobtracker",
+ "secondarynamenode",
+ "hiveserver",
+ "oozie"
+ ],
+ "updated_at": None,
+ "volume_mount_prefix": "/volumes/disk",
+ "volumes_per_node": 0,
+ "volumes_size": 0
+ },
+ {
+ "count": 2,
+ "created_at": "2014-06-04 14:01:06.463214",
+ "flavor_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
+ "floating_ip_pool": None,
+ "image_id": None,
+ "name": "workers",
+ "node_configs": {},
+ "node_group_template_id": "4eb5504c-94c9-4049-a440",
+ "node_processes": [
+ "datanode",
+ "tasktracker"
+ ],
+ "updated_at": None,
+ "volume_mount_prefix": "/volumes/disk",
+ "volumes_per_node": 0,
+ "volumes_size": 0
+ }
+ ],
+ "plugin_name": "vanilla",
+ "tenant_id": "429ad8447c2d47bc8e0382d244e1d1df",
+ "updated_at": None
+ }
+
+ ct1 = cluster_templates.ClusterTemplate(
+ cluster_templates.ClusterTemplateManager(None), ct1_dict)
+ TEST.cluster_templates.add(ct1)
+
+ #Clusters
+ cluster1_dict = {
+ "anti_affinity": [],
+ "cluster_configs": {},
+ "cluster_template_id": "a2c3743f-31a2-4919-8d02-792138a87a98",
+ "created_at": "2014-06-04 20:02:14.051328",
+ "default_image_id": "9eb4643c-dca8-4ea7-92d2-b773f88a8dc6",
+ "description": "",
+ "hadoop_version": "1.2.1",
+ "id": "ec9a0d28-5cfb-4028-a0b5-40afe23f1533",
+ "info": {},
+ "is_transient": False,
+ "management_public_key": "fakekey",
+ "name": "cercluster",
+ "neutron_management_network": None,
+ "node_groups": [
+ {
+ "count": 1,
+ "created_at": "2014-06-04 20:02:14.053153",
+ "flavor_id": "0",
+ "floating_ip_pool": None,
+ "image_id": None,
+ "instances": [
+ {
+ "created_at": "2014-06-04 20:02:14.834529",
+ "id": "c3b8004b-7063-4b99-a082-820cdc6e961c",
+ "instance_id": "a45f5495-4a10-4f17-8fae",
+ "instance_name": "cercluster-master-001",
+ "internal_ip": None,
+ "management_ip": None,
+ "updated_at": None,
+ "volumes": []
+ }
+ ],
+ "name": "master",
+ "node_configs": {},
+ "node_group_template_id": "c166dfcc-9cc7-4b48-adc9",
+ "node_processes": [
+ "namenode",
+ "jobtracker",
+ "secondarynamenode",
+ "hiveserver",
+ "oozie"
+ ],
+ "updated_at": "2014-06-04 20:02:14.841760",
+ "volume_mount_prefix": "/volumes/disk",
+ "volumes_per_node": 0,
+ "volumes_size": 0
+ },
+ {
+ "count": 2,
+ "created_at": "2014-06-04 20:02:14.053849",
+ "flavor_id": "0",
+ "floating_ip_pool": None,
+ "image_id": None,
+ "instances": [
+ {
+ "created_at": "2014-06-04 20:02:15.097655",
+ "id": "6a8ae0b1-bb28-4de2-bfbb-bdd3fd2d72b2",
+ "instance_id": "38bf8168-fb30-483f-8d52",
+ "instance_name": "cercluster-workers-001",
+ "internal_ip": None,
+ "management_ip": None,
+ "updated_at": None,
+ "volumes": []
+ },
+ {
+ "created_at": "2014-06-04 20:02:15.344515",
+ "id": "17b98ed3-a776-467a-90cf-9f46a841790b",
+ "instance_id": "85606938-8e53-46a5-a50b",
+ "instance_name": "cercluster-workers-002",
+ "internal_ip": None,
+ "management_ip": None,
+ "updated_at": None,
+ "volumes": []
+ }
+ ],
+ "name": "workers",
+ "node_configs": {},
+ "node_group_template_id": "4eb5504c-94c9-4049-a440",
+ "node_processes": [
+ "datanode",
+ "tasktracker"
+ ],
+ "updated_at": "2014-06-04 20:02:15.355745",
+ "volume_mount_prefix": "/volumes/disk",
+ "volumes_per_node": 0,
+ "volumes_size": 0
+ }
+ ],
+ "plugin_name": "vanilla",
+ "status": "Active",
+ "status_description": "",
+ "tenant_id": "429ad8447c2d47bc8e0382d244e1d1df",
+ "trust_id": None,
+ "updated_at": "2014-06-04 20:02:15.446087",
+ "user_keypair_id": "stackboxkp"
+ }
+
+ cluster1 = clusters.Cluster(
+ clusters.ClusterManager(None), cluster1_dict)
+ TEST.clusters.add(cluster1)