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 @@ + \ 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 @@ + + +
+ + + +
+
+
+ + + + + + + + + + +
+
+ \ 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)