Merge "Adding cluster_template and cluster panels for Sahara"

This commit is contained in:
Jenkins 2014-07-23 04:39:43 +00:00 committed by Gerrit Code Review
commit 49f8b944e3
50 changed files with 2776 additions and 1 deletions

View File

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

View File

@ -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):

View File

@ -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

View File

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

View File

@ -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,)

View File

@ -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

View File

@ -0,0 +1,22 @@
{% load i18n horizon %}
<div class="well">
<p>
{% blocktrans %}This Cluster Template will be created for:{% endblocktrans %}
<br >
<b>{% blocktrans %}Plugin{% endblocktrans %}</b>: {{ plugin_name }}
<br />
<b>{% blocktrans %}Hadoop version{% endblocktrans %}</b>: {{ hadoop_version }}
<br />
</p>
<p>
{% blocktrans %}The 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 &quotNode Groups&quot tab.{% endblocktrans %}
</p>
<p>
{% blocktrans %}You may set <b>cluster</b> scoped Hadoop configurations on corresponding tabs.{% endblocktrans %}
</p>
<p>
{% 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 %}
</p>
</div>

View File

@ -0,0 +1,4 @@
{% load i18n horizon %}
<p class="well">
{% blocktrans %}Select a plugin and Hadoop version for a new Cluster template.{% endblocktrans %}
</p>

View File

@ -0,0 +1,54 @@
{% load i18n sizeformat %}
{% load url from future %}
<h3>{% trans "Template Overview" %}</h3>
<div class="status row-fluid detail">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ template.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ template.id }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ template.description|default:"None" }}</dd>
</dl>
<dl>
<dt>{% trans "Plugin" %}</dt>
<dd><a href="{% url 'horizon:project:data_processing.data_plugins:details' template.plugin_name %}">{{ template.plugin_name }}</a></dd>
<dt>{% trans "Hadoop Version" %}</dt>
<dd>{{ template.hadoop_version }}</dd>
</dl>
<dl>
<dt>{% trans "Anti-affinity enabled for" %}</dt>
{% if template.anti_affinity %}
<dd>
<ul>
{% for process in template.anti_affinity %}
<li>{{ process }}</li>
{% endfor %}
</ul>
</dd>
{% else %}
<h6>{% trans "no processes" %}</h6>
{% endif %}
</dl>
<dl>
<dt>{% trans "Node Configurations" %}</dt>
{% if template.cluster_configs %}
<dd>
{% for service, service_conf in template.cluster_configs.items %}
<h4>{{ service }}</h4>
{% if service_conf %}
<ul>
{% for conf_name, conf_value in service_conf.items %}
<li>{{ conf_name }}:&nbsp{{ conf_value }}</li>
{% endfor %}
</ul>
{% else %}
<h6>{% trans "No configurations" }%</h6>
{% endif %}
{% endfor %}
</dd>
{% else %}
<dd>{% trans "Cluster configurations are not specified" %}</dd>
{% endif %}
</dl>
</div>

View File

@ -0,0 +1,55 @@
{% load i18n sizeformat %}
{% load url from future %}
<h3>{% trans "Node Groups" %}</h3>
<div class="status row-fluid detail">
{% for node_group in template.node_groups %}
<dl class="well">
<h4>{% trans "Node Group" %}: {{ node_group.name }}</h4>
<dt>{% trans "Nodes Count" %}</dt>
<dd>{{ node_group.count }}</dd>
<dt>{% trans "Flavor" %}</dt>
<dd>{{ node_group.flavor_id|default:"Flavor is not specified" }}</dd>
<dt>{% trans "Template" %}</dt>
{% if node_group.node_group_template_id %}
<dd><a href="{% url 'horizon:project:data_processing.nodegroup_templates:details' node_group.node_group_template_id %}">{{ node_group.node_group_template.name }} </a></dd>
{% else %}
<dd>{% trans "Template not specified" %}</dd>
{% endif %}
<dt>{% trans "Node Processes" %}</dt>
{% if node_group.node_processes %}
<dd>
<ul>
{% for process in node_group.node_processes %}
<li>{{ process }}</li>
{% endfor %}
</ul>
</dd>
{% else %}
<dd>{% trans "Node processes are not specified" %}</dd>
{% endif %}
<dt>{% trans "Node Configurations" %}</dt>
{% if node_group.node_configs %}
<dd>
{% for service, service_conf in node_group.node_configs.items %}
<h6>{{ service }}</h6>
{% if service_conf %}
<ul>
{% for conf_name, conf_value in service_conf.items %}
<li>{{ conf_name }}:&nbsp{{ conf_value }}</li>
{% endfor %}
</ul>
{% else %}
<h6>{% trans "No configurations" %}</h6>
{% endif %}
{% endfor %}
</dd>
{% else %}
<dd>{% trans "Node configurations are not specified" %}</dd>
{% endif %}
</dl>
{% endfor %}
</div>

View File

@ -0,0 +1,5 @@
<ul>
{% for group in node_groups %}
<li>{{ group.name }}:&nbsp{{ group.count }}</li>
{% endfor %}
</ul>

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" id="upload_file_btn" type="submit" value="{% trans "Upload" %}"/>
<a href="{% url 'horizon:project:data_processing.cluster_templates:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,155 @@
<script>
var template = '<tr id_attr="$id"><td>' +
'<div class="input control-group" style="padding-right:15px;">' +
'<input id="template_id_$id" value="$template_id" type="hidden" name="template_id_$id">' +
'<input id="group_name_$id" value="$group_name" type="text" name="group_name_$id" class="input-medium">' +
'</div>' +
'</td>' +
'<td>' +
'<div class="input control-group" style="padding-right:15px;"><input disabled value="$template_name" class="input-medium" /></div>' +
'</td>' +
'<td>' +
'<div class="input control-group btn-group input-append" style="float:left;padding-right:5px;">' +
'<input id="count_$id" class="count-field" value="$node_count" type="text" max="4" maxlength="4" name="count_$id" size="4" style="width:50px">' +
'<div class="btn dec-btn" data-count-id="count_$id"><i class="icon-minus"></i></div>' +
'<div class="btn inc-btn" data-count-id="count_$id"><i class="icon-plus"></i></div>' +
'</div>' +
'<div class="input" style="float:left">' +
'</div>' +
'</td>' +
'<td>' +
'<div class="input control-group" style="float:left;padding-right:5px;">' +
'<input type="button" class="btn btn-danger" id="delete_btn_$id" data-toggle="dropdown" onclick="delete_node_group(this)" value="Remove" style="margin-bottom: 10px"/>' +
'</div>' +
'</td>' +
'</tr>';
function mark_element_as_wrong(id){
$("#"+id).parent("div").addClass("error");
}
function get_next_id() {
var max = -1;
$("#node-templates tbody tr").each(function () {
max = Math.max(max, parseInt($(this).attr("id_attr")));
});
return max + 1;
}
function set_nodes_ids() {
var ids = [];
$("#node-templates tbody tr").each(function () {
ids.push(parseInt($(this).attr("id_attr")));
});
$("#forms_ids").val(JSON.stringify(ids));
}
function add_node(node_count, group_name, template_id, id, deletable) {
var template_name = $("select option[value='" + template_id + "']").html();
var tmp = template.
replace(/\$id/g, id).
replace(/\$group_name/g, group_name).
replace(/\$template_id/g, template_id).
replace(/\$node_count/g, node_count).
replace(/\$template_name/g, template_name);
$("#node-templates tbody").append(tmp);
if (!deletable) {
$("#delete_btn_" + id).remove();
$("#group_name_" + id).prop('readonly', true);
}
$("#node-templates").show();
set_nodes_ids();
}
function add_node_group_template(node_count) {
if ($("select option:selected").html() == "Select") {
return;
}
var template_id = $("#template_id option:selected").val();
var template_name = $("#template_id option:selected").html();
add_node(node_count, template_name, template_id, get_next_id(), true);
$(".count-field").change();
}
function delete_node_group(el) {
var tr = $(el).parents("tr")[0];
tr.parentNode.removeChild(tr);
var id = get_next_id();
if (id == 0) {
$("#node-templates").hide();
}
set_nodes_ids();
}
$("#template_id").change(function () {
if ($("select option:selected").html() == "Select") {
$("#add_group_button").addClass("disabled");
} else {
$("#add_group_button").removeClass("disabled");
}
});
$("#node-templates").hide();
</script>
<input type="hidden" value="[]" name="forms_ids" id="forms_ids">
<div class="input">
<select id="template_id" name="template">
<option>Select</option>
{% for template in form.templates %}
<option value="{{ template.id }}">{{ template.name }}</option>
{% endfor %}
</select>
<a id="add_group_button" class="btn disabled btn-inline" onclick="add_node_group_template(1);">+</a>
</div>
<br/>
<div id="node-templates">
<table>
<thead>
<tr>
<td><label style="padding-bottom:5px">Group Name</label></td>
<td><label style="padding-bottom:5px">Template&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</label></td>
<td><label style="padding-bottom:5px">Count</label></td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<script>
{% for group in form.groups %}
add_node("{{ group.count }}", "{{ group.name }}", "{{ group.template_id }}", "{{ group.id }}", {{ group.deletable }});
{% endfor %}
{% for field_id in form.errors_fields %}
mark_element_as_wrong("{{ field_id }}");
{% endfor %}
var handlers_registred;
var lower_limit = 1;
$(function() {
if (!handlers_registred) {
handlers_registred = true;
$(".inc-btn").live("click", function(e) {
var id = $(this).attr("data-count-id");
$("#" + id).val(parseInt($("#" + id).val()) + 1);
$(".count-field").change();
});
$(".dec-btn").live("click", function(e) {
var id = $(this).attr("data-count-id");
var val = parseInt($("#" + id).val());
if (val > lower_limit) {
$("#" + id).val(val - 1);
}
$(".count-field").change();
});
}
$(".count-field").live("change", function() {
var val = $(this).val();
if (val > lower_limit) {
$(this).parent("div").find(".dec-btn").removeClass("disabled");
} else {
$(this).parent("div").find(".dec-btn").addClass("disabled");
}
}).change();
});
</script>

View File

@ -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 %}
<div class="cluster_templates">
{{ cluster_templates_table.render }}
</div>
<script type="text/javascript">
addHorizonLoadEvent(function () {
horizon.modals.addModalInitFunction(function (modal) {
if ($(modal).find(".nav-tabs").find("li").size() == 1) {
// hide tab bar for plugin/version modal wizard
$('div#modal_wrapper ul.nav-tabs').hide();
}
$(".hidden_nodegroups_field").after("<input type='button' id='add_nodegroup' value='Add Node group'/><br/>");
$("#add_nodegroup").click(function() {
$(".hidden_nodegroups_field").val("create_nodegroup");
$(".hidden_configure_field").val("create_nodegroup");
var form = $(".hidden_nodegroups_field").closest("form");
form.submit();
});
$(".hidden_nodegroups_field").val("");
$(".hidden_configure_field").val("");
if ($(modal).find(".hidden_create_field").length > 0) {
var form = $(".hidden_create_field").closest("form");
var successful = false;
form.submit(function (e) {
var oldHref = $(".configure-clustertemplate-btn")[0].href;
var plugin = $("#id_plugin_name option:selected").val();
var version = $("#id_" + plugin + "_version option:selected").val();
form.find(".close").click();
$(".configure-clustertemplate-btn")[0].href = oldHref +
"?plugin_name=" + encodeURIComponent(plugin) +
"&hadoop_version=" + encodeURIComponent(version);
$(".configure-clustertemplate-btn").click();
$(".configure-clustertemplate-btn")[0].href = oldHref;
return false;
});
$(".plugin_version_choice").closest(".control-group").hide();
}
//display version for selected plugin
$(document).on('change', '.plugin_name_choice', switch_versions);
function switch_versions() {
$(".plugin_version_choice").closest(".control-group").hide();
var plugin = $(this);
$("." + plugin.val() + "_version_choice").closest(".control-group").show();
}
$(".plugin_name_choice").change();
});
});
</script>
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -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 %}

View File

@ -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')

View File

@ -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<template_id>[^/]+)$',
views.ClusterTemplateDetailsView.as_view(),
name='details'),
url(r'^(?P<template_id>[^/]+)/copy$',
views.CopyClusterTemplateView.as_view(),
name='copy'))

View File

@ -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

View File

@ -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."))

View File

@ -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

View File

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

View File

@ -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,)

View File

@ -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 "<a href='" + url + "' target=\"_blank\">" + url + "</a>"
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

View File

@ -0,0 +1,20 @@
{% load i18n horizon %}
<div class="well">
<p>
{% blocktrans %}This Cluster will be started with:{% endblocktrans %}
<br >
<b>{% blocktrans %}Plugin{% endblocktrans %}</b>: {{ plugin_name }}
<br />
<b>{% blocktrans %}Hadoop version{% endblocktrans %}</b>: {{ hadoop_version }}
<br />
</p>
<p>
{% blocktrans %}Cluster can be launched using existing Cluster Templates.{% endblocktrans %}
</p>
<p>
{% blocktrans %}The Cluster object should specify OpenStack Image to boot instances for Hadoop Cluster.{% endblocktrans %}
</p>
<p>
{% blocktrans %}User has to choose a keypair to have access to clusters instances.{% endblocktrans %}
</p>
</div>

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" id="create_cluster_btn" type="submit" value="{% trans " Done" %}"/>
<a href="{% url 'horizon:project:data_processing.clusters:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,4 @@
{% load i18n %}
<p class="well">
{% trans "Select a plugin and Hadoop version for a new Cluster." %}
</p>

View File

@ -0,0 +1,89 @@
{% load i18n sizeformat %}
{% load url from future %}
<h3>{% trans "Cluster Overview" %}</h3>
<div class="status row-fluid detail">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ cluster.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ cluster.id }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ cluster.description|default:"None" }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ cluster.status }}</dd>
</dl>
{% if cluster.error_description %}
<h4>{% trans "Error Details" %}</h4>
<p class="well">
{{ cluster.error_description }}
</p>
{% endif %}
<dl>
<dt>{% trans "Plugin" %}</dt>
<dd><a href="{% url 'horizon:project:data_processing.data_plugins:details' cluster.plugin_name %}">{{ cluster.plugin_name }}</a></dd>
<dt>{% trans "Hadoop Version" %}</dt>
<dd>{{ cluster.hadoop_version }}</dd>
</dl>
<dl>
<dt>{% trans "Template" %}</dt>
{% if cluster_template %}
<dd><a href="{% url 'horizon:project:data_processing.cluster_templates:details' cluster_template.id %}">{{ cluster_template.name }} </a></dd>
{% else %}
<dd>{% trans "Template not specified" %}</dd>
{% endif %}
<dt>{% trans "Base Image" %}</dt>
<dd>{{ base_image.name }}</dd>
{% if network %}
<dt>{% trans "Neutron Management Network" %}</dt>
<dd>{{ network }}</dd>
{% endif %}
<dt>{% trans "Keypair" %}</dt>
<dd>{{ cluster.user_keypair_id }}</dd>
</dl>
<dl>
<dt>{% trans "Anti-affinity enabled for" %}</dt>
{% if cluster.anti_affinity %}
<dd>
<ul>
{% for process in cluster.anti_affinity %}
<li>{{ process }}</li>
{% endfor %}
</ul>
</dd>
{% else %}
<h6>{% trans "no processes" %}</h6>
{% endif %}
</dl>
<dl>
<dt>{% trans "Node Configurations" %}</dt>
{% if cluster.cluster_configs %}
<dd>
{% for service, service_conf in cluster.cluster_configs.items %}
<h4>{{ service }}</h4>
{% if service_conf %}
<ul>
{% for conf_name, conf_value in service_conf.items %}
<li>{{ conf_name }}:&nbsp{{ conf_value }}</li>
{% endfor %}
</ul>
{% else %}
<h6>{% trans "No configurations" %}</h6>
{% endif %}
{% endfor %}
</dd>
{% else %}
<dd>{% trans "Cluster configurations are not specified" %}</dd>
{% endif %}
</dl>
<dl>
{% for info_key, info_val in cluster.info.items %}
<dt>{{ info_key }}</dt>
{% for key, val in info_val.items %}
<dd>{{ key }}:&nbsp{% autoescape off %}{{ val }}{% endautoescape %}</dd>
{% endfor %}
{% endfor %}
</dl>
</div>

View File

@ -0,0 +1,5 @@
{% load i18n sizeformat %}
<h3>{% trans "Cluster Instances" %}</h3>
<div class="status row-fluid detail">
{{ cluster_instances_table.render }}
</div>

View File

@ -0,0 +1,63 @@
{% load i18n sizeformat %}
{% load url from future %}
<h3>{% trans "Node Groups" %}</h3>
<div class="status row-fluid detail">
{% for node_group in cluster.node_groups %}
<dl class="well">
<h4>{% trans "Name" %}: {{ node_group.name }}</h4>
<dt>{% trans "Number of Nodes" %}</dt>
<dd>{{ node_group.count }}</dd>
<dt>{% trans "Flavor" %}</dt>
<dd>{{ node_group.flavor_name|default:"Flavor is not specified" }}</dd>
{% if node_group.floating_ip_pool %}
<dt>{% trans "Floating IP Pool" %}</dt>
<dd>{{ node_group.floating_ip_pool }}</dd>
{% endif %}
<dt>{% trans "Template" %}</dt>
{% if node_group.node_group_template_id %}
<dd><a href="{% url 'horizon:project:data_processing.nodegroup_templates:details' node_group.node_group_template_id %}">{{ node_group.node_group_template.name }} </a></dd>
{% else %}
<dd>{% trans "Template not specified" %}</dd>
{% endif %}
<dt>{% trans "Node Processes" %}</dt>
{% if node_group.node_processes %}
<dd>
<ul>
{% for process in node_group.node_processes %}
<li>{{ process }}</li>
{% endfor %}
</ul>
</dd>
{% else %}
<dd>{% trans "Node processes are not specified" %}</dd>
{% endif %}
<dt>{% trans "Node Configurations" %}</dt>
{% if node_group.node_configs %}
<dd>
{% for service, service_conf in node_group.node_configs.items %}
<h4>{{ service }}</h4>
{% if service_conf %}
<ul>
{% for conf_name, conf_value in service_conf.items %}
<li>{{ conf_name }}:&nbsp{{ conf_value }}</li>
{% endfor %}
</ul>
{% else %}
<h6>{% trans "No configurations" %}</h6>
{% endif %}
{% endfor %}
</dd>
{% else %}
<dd>{% trans "Node configurations are not specified" %}</dd>
{% endif %}
</dl>
{% endfor %}
</div>

View File

@ -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 %}
<div class="cluster_templates">
{{ clusters_table.render }}
</div>
<script type="text/javascript">
addHorizonLoadEvent(function () {
horizon.modals.addModalInitFunction(function (modal) {
if ($(modal).find(".nav-tabs").find("li").size() == 1) {
// hide tab bar for plugin/version modal wizard
$('div#modal_wrapper ul.nav-tabs').hide();
}
$(".hidden_nodegroups_field").val("");
$(".hidden_configure_field").val("");
lower_limit = 0;
$(".count-field").change();
if ($(modal).find(".hidden_create_field").length > 0) {
var form = $(".hidden_create_field").closest("form");
var successful = false;
form.submit(function (e) {
var oldHref = $(".configure-cluster-btn")[0].href;
var plugin = $("#id_plugin_name option:selected").val();
var version = $("#id_" + plugin + "_version option:selected").val();
form.find(".close").click();
$(".configure-cluster-btn")[0].href = oldHref +
"?plugin_name=" + encodeURIComponent(plugin) +
"&hadoop_version=" + encodeURIComponent(version);
$(".configure-cluster-btn").click();
$(".configure-cluster-btn")[0].href = oldHref;
return false;
});
$(".plugin_version_choice").closest(".control-group").hide();
}
//display version for selected plugin
$(document).on('change', '.plugin_name_choice', switch_versions);
function switch_versions() {
$(".plugin_version_choice").closest(".control-group").hide();
var plugin = $(this);
$("." + plugin.val() + "_version_choice").closest(".control-group").show();
}
$(".plugin_name_choice").change();
});
});
</script>
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -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 %}

View File

@ -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')

View File

@ -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<cluster_id>[^/]+)$',
views.ClusterDetailsView.as_view(),
name='details'),
url(r'^(?P<cluster_id>[^/]+)/scale$',
views.ScaleClusterView.as_view(),
name='scale'))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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