Merge "UI for Cluster Scaling implementation."

This commit is contained in:
Jenkins 2013-07-04 17:03:30 +00:00 committed by Gerrit Code Review
commit 9641f9260d
16 changed files with 315 additions and 131 deletions

View File

@ -18,7 +18,7 @@
import json
import logging
LOG = logging.Logger(__name__)
LOG = logging.getLogger(__name__)
class Resource(object):
@ -73,6 +73,13 @@ class ResourceManager(object):
raise RuntimeError('Unable to create %s, server returned code %s' %
(resource_name, resp.status_code))
def _update(self, url, data):
resp = self.api.client.put(url, json.dumps(data))
if resp.status_code != 202:
resource_name = self.resource_class.resource_name
raise RuntimeError('Unable to update %s, server returned code %s' %
(resource_name, resp.status_code))
def _list(self, url, response_key):
resp = self.api.client.get(url)

View File

@ -64,6 +64,9 @@ class ClusterManager(base.ResourceManager):
self._create('/clusters', data)
def scale(self, cluster_id, scale_object):
return self._put('/clusters/%s' % cluster_id, scale_object)
def list(self):
return self._list('/clusters', 'clusters')

View File

@ -32,6 +32,11 @@ class HTTPClient(object):
headers={'x-auth-token': self.token,
'content-type': 'application/json'})
def put(self, url, body):
return requests.put(self.base_url + url, body,
headers={'x-auth-token': self.token,
'content-type': 'application/json'})
def delete(self, url):
return requests.delete(self.base_url + url,
headers={'x-auth-token': self.token})

View File

@ -18,10 +18,10 @@
import logging
from django.utils.translation import ugettext as _
from horizon import forms
from savannadashboard.api import client as savannaclient
import savannadashboard.cluster_templates.workflows.create as create_flow
from savannadashboard.utils.workflow_helpers import build_node_group_fields
LOG = logging.getLogger(__name__)
@ -63,23 +63,13 @@ class CopyClusterTemplate(create_flow.ConfigureClusterTemplate):
{"name": templ_ng["name"],
"template_id": templ_ng["node_group_template_id"],
"count": templ_ng["count"],
"id": id})
"id": id,
"deletable": "true"})
ng_action.fields[group_name] = forms.CharField(
label=_("Name"),
required=True,
widget=forms.TextInput())
ng_action.fields[template_id] = forms.CharField(
label=_("Node group template"),
required=True,
widget=forms.HiddenInput())
ng_action.fields[count] = forms.IntegerField(
label=_("Count"),
required=True,
min_value=1,
widget=forms.HiddenInput())
build_node_group_fields(ng_action,
group_name,
template_id,
count)
elif isinstance(step, create_flow.GeneralConfig):
fields = step.action.fields

View File

@ -174,6 +174,8 @@ class ConfigureNodegroupsAction(workflows.Action):
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']):
@ -183,23 +185,14 @@ class ConfigureNodegroupsAction(workflows.Action):
self.groups.append({"name": request.POST[group_name],
"template_id": request.POST[template_id],
"count": request.POST[count],
"id": id})
"id": id,
"deletable": deletable.get(
request.POST[group_name], "true")})
self.fields[group_name] = forms.CharField(
label=_("Name"),
required=True,
widget=forms.TextInput())
self.fields[template_id] = forms.CharField(
label=_("Node group template"),
required=True,
widget=forms.HiddenInput())
self.fields[count] = forms.IntegerField(
label=_("Count"),
required=True,
min_value=1,
widget=forms.HiddenInput())
whelpers.build_node_group_fields(self,
group_name,
template_id,
count)
def clean(self):
cleaned_data = super(ConfigureNodegroupsAction, self).clean()

View File

@ -1,29 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 horizon import exceptions
from horizon import forms
class CreateClusterForm(forms.SelfHandlingForm):
def handle(self, request, data):
try:
#TODO(akuznetsov): launch cluster
return True
except Exception:
exceptions.handle(request)
return False

View File

@ -33,6 +33,16 @@ class CreateCluster(tables.LinkAction):
classes = ("btn-launch", "ajax-modal")
class ScaleCluster(tables.LinkAction):
name = "scale"
verbose_name = _("Scale Cluster")
url = "horizon:savanna: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")
@ -92,4 +102,5 @@ class ClustersTable(tables.DataTable):
table_actions = (CreateCluster,
ConfigureCluster,
DeleteCluster)
row_actions = (DeleteCluster,)
row_actions = (ScaleCluster,
DeleteCluster,)

View File

@ -34,4 +34,7 @@ urlpatterns = patterns('',
name='configure-cluster'),
url(r'^(?P<cluster_id>[^/]+)$',
views.ClusterDetailsView.as_view(),
name='details'))
name='details'),
url(r'^(?P<cluster_id>[^/]+)/scale$',
views.ScaleClusterView.as_view(),
name='scale'))

View File

@ -25,9 +25,8 @@ from savannadashboard.api import client as savannaclient
from savannadashboard.clusters.tables import ClustersTable
import savannadashboard.clusters.tabs as _tabs
from savannadashboard.clusters.workflows import ConfigureCluster
from savannadashboard.clusters.workflows import CreateCluster
import savannadashboard.clusters.workflows.create as create_flow
import savannadashboard.clusters.workflows.scale as scale_flow
LOG = logging.getLogger(__name__)
@ -56,7 +55,7 @@ class ClusterDetailsView(tabs.TabView):
class CreateClusterView(workflows.WorkflowView):
workflow_class = CreateCluster
workflow_class = create_flow.CreateCluster
success_url = \
"horizon:savanna:clusters:create-cluster"
classes = ("ajax-modal")
@ -64,6 +63,33 @@ class CreateClusterView(workflows.WorkflowView):
class ConfigureClusterView(workflows.WorkflowView):
workflow_class = ConfigureCluster
workflow_class = create_flow.ConfigureCluster
success_url = "horizon:savanna:clusters"
template_name = "clusters/configure.html"
class ScaleClusterView(workflows.WorkflowView):
workflow_class = scale_flow.ScaleCluster
success_url = "horizon:savanna:clusters"
classes = ("ajax-modal")
template_name = "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']
savanna = savannaclient.Client(self.request)
template = savanna.cluster_templates.get(template_id)
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

@ -164,25 +164,6 @@ class ConfigureCluster(workflows.Workflow):
success_url = "horizon:savanna:clusters:index"
default_steps = (GeneralConfig, )
def is_valid(self):
if self.context["general_hidden_configure_field"] \
== "create_nodegroup":
return False
missing = self.depends_on - set(self.context.keys())
if missing:
raise exceptions.WorkflowValidationError(
"Unable to complete the workflow. The values %s are "
"required but not present." % ", ".join(missing))
steps_valid = True
for step in self.steps:
if not step.action.is_valid():
steps_valid = False
step.has_errors = True
if not steps_valid:
return steps_valid
return self.validate(self.context)
def format_status_message(self, message):
return message % self.context["general_cluster_name"]

View File

@ -0,0 +1,149 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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.
import json
import logging
from django.utils.translation import ugettext as _
from horizon import exceptions
from savannadashboard.api import client as savannaclient
import savannadashboard.cluster_templates.workflows.create as clt_create_flow
import savannadashboard.clusters.workflows.create as cl_create_flow
from savannadashboard.utils.workflow_helpers import build_node_group_fields
LOG = logging.getLogger(__name__)
class NodeGroupsStep(clt_create_flow.ConfigureNodegroups):
pass
class ScaleCluster(cl_create_flow.ConfigureCluster):
slug = "scale_cluster"
name = _("Scale Cluster")
finalize_button_name = _("Scale")
success_url = "horizon:savanna:clusters:index"
default_steps = (NodeGroupsStep, )
def __init__(self, request, context_seed, entry_point, *args, **kwargs):
ScaleCluster._cls_registry = set([])
savanna = savannaclient.Client(request)
cluster_id = context_seed["cluster_id"]
cluster = savanna.clusters.get(cluster_id)
self.success_message = "Scaling cluster started for %s" % cluster.name
self.failure_message = "Could not start scaling for %s" % cluster.name
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})
request.GET.update({"plugin_name": plugin})
request.GET.update({"hadoop_version": hadoop_version})
request.GET.update({"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"})
build_node_group_fields(ng_action,
group_name,
template_id,
count)
def format_status_message(self, message):
return message
def handle(self, request, context):
savanna = savannaclient.Client(request)
cluster_id = request.GET["cluster_id"]
cluster = savanna.clusters.get(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)}
)
try:
savanna.clusters.scale(cluster_id, scale_object)
return True
except Exception:
exceptions.handle(request)
return False

View File

@ -1,22 +1,30 @@
<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">' +
'<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" style="padding-bottom:7px;padding-right:15px;">$template_name</div>' +
'<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>' +
'<td>' +
'<div class="input control-group" style="float:left;padding-right:5px;">' +
'<input id="count_$id" value="$node_count" type="text" max="4" maxlength="4" name="count_$id" size="4" style="width:50px">' +
'</div>' +
'<div class="input" style="float:left">' +
'<button class="btn btn-danger" data-toggle="dropdown" onclick="delete_node_group(this)">-</button>' +
'</div>' +
'</td>' +
'</tr>';
'</tr>';
function mark_element_as_wrong(id){
$("#"+id).parent("div").addClass("error");
@ -38,7 +46,7 @@
$("#forms_ids").val(JSON.stringify(ids));
}
function add_node(node_count, group_name, template_id, id) {
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).
@ -47,6 +55,10 @@
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();
}
@ -57,7 +69,8 @@
}
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());
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];
@ -104,9 +117,38 @@
<script>
{% for group in form.groups %}
add_node("{{ group.count }}", "{{ group.name }}", "{{ group.template_id }}", "{{ group.id }}");
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;
$(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 > 1) {
$("#" + id).val(val - 1);
}
$(".count-field").change();
});
}
$(".count-field").live("change", function() {
var val = $(this).val();
if (val > 1) {
$(this).parent("div").find(".dec-btn").removeClass("disabled");
} else {
$(this).parent("div").find(".dec-btn").addClass("disabled");
}
}).change();
});
</script>

View File

@ -28,32 +28,6 @@
form.submit();
});
var actions_table = $(".hidden_nodegroups_field").parent();
actions_table.attr("style", "width: 100%");
actions_table.find(".control-group").attr("style", "width: 30%; display: inline-block");
actions_table.find(".input").children().attr("style", "width: 85%");
actions_table.find(".count-field").closest(".control-group")
.after("<div style='display: inline-block' class='control-group'><div class='input'><span class='btn btn-small btn-danger ng-remove-btn'><i class='icon-remove-sign'>&nbsp&nbsp&nbsp</i></span></div></div>");
$(".ng-remove-btn").on("click", function(event) {
var div = $(this).closest(".control-group");
div.hide();
//count
var count = div.prev();
count.hide();
//template
var template = div.prev().prev();
template.hide();
//name
var name = div.prev().prev().prev();
name.hide();
var idx = count.find("input").attr("data-count-idx");
console.log(idx);
$(".hidden_to_delete_field").val($(".hidden_to_delete_field").val() + "," + idx);
});
$(".hidden_nodegroups_field").val("");
$(".hidden_configure_field").val("");

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=_("Savanna - Scale Cluster") %}
{% endblock page_header %}
{% block main %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}<!DOCTYPE html>

View File

@ -103,6 +103,24 @@ def _create_step_action(name, title, parameters, advanced_fields=None,
return step
def build_node_group_fields(action, name, template, count):
action.fields[name] = forms.CharField(
label=_("Name"),
required=True,
widget=forms.TextInput())
action.fields[template] = forms.CharField(
label=_("Node group cluster"),
required=True,
widget=forms.HiddenInput())
action.fields[count] = forms.IntegerField(
label=_("Count"),
required=True,
min_value=1,
widget=forms.HiddenInput())
def parse_configs_from_context(context, defaults):
configs_dict = dict()
for key, val in context.items():