diff --git a/savannadashboard/api/base.py b/savannadashboard/api/base.py index 6c07f304..fc63436b 100644 --- a/savannadashboard/api/base.py +++ b/savannadashboard/api/base.py @@ -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) diff --git a/savannadashboard/api/clusters.py b/savannadashboard/api/clusters.py index b4fa061d..a748d0be 100644 --- a/savannadashboard/api/clusters.py +++ b/savannadashboard/api/clusters.py @@ -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') diff --git a/savannadashboard/api/httpclient.py b/savannadashboard/api/httpclient.py index 1a244d2c..8d691d27 100644 --- a/savannadashboard/api/httpclient.py +++ b/savannadashboard/api/httpclient.py @@ -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}) diff --git a/savannadashboard/cluster_templates/workflows/copy.py b/savannadashboard/cluster_templates/workflows/copy.py index f1b57c43..3b5ca7c6 100644 --- a/savannadashboard/cluster_templates/workflows/copy.py +++ b/savannadashboard/cluster_templates/workflows/copy.py @@ -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 diff --git a/savannadashboard/cluster_templates/workflows/create.py b/savannadashboard/cluster_templates/workflows/create.py index 47c48702..15b8e54e 100644 --- a/savannadashboard/cluster_templates/workflows/create.py +++ b/savannadashboard/cluster_templates/workflows/create.py @@ -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() diff --git a/savannadashboard/clusters/forms.py b/savannadashboard/clusters/forms.py deleted file mode 100644 index 9f5c3c41..00000000 --- a/savannadashboard/clusters/forms.py +++ /dev/null @@ -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 diff --git a/savannadashboard/clusters/tables.py b/savannadashboard/clusters/tables.py index e6071e05..bda7da2f 100644 --- a/savannadashboard/clusters/tables.py +++ b/savannadashboard/clusters/tables.py @@ -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,) diff --git a/savannadashboard/clusters/urls.py b/savannadashboard/clusters/urls.py index f3afd3b3..c12b7969 100644 --- a/savannadashboard/clusters/urls.py +++ b/savannadashboard/clusters/urls.py @@ -34,4 +34,7 @@ urlpatterns = patterns('', name='configure-cluster'), url(r'^(?P[^/]+)$', views.ClusterDetailsView.as_view(), - name='details')) + name='details'), + url(r'^(?P[^/]+)/scale$', + views.ScaleClusterView.as_view(), + name='scale')) diff --git a/savannadashboard/clusters/views.py b/savannadashboard/clusters/views.py index 11d7bb00..21a6adce 100644 --- a/savannadashboard/clusters/views.py +++ b/savannadashboard/clusters/views.py @@ -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 diff --git a/savannadashboard/clusters/workflows/__init__.py b/savannadashboard/clusters/workflows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/savannadashboard/clusters/workflows.py b/savannadashboard/clusters/workflows/create.py similarity index 90% rename from savannadashboard/clusters/workflows.py rename to savannadashboard/clusters/workflows/create.py index 2372fc44..04402df4 100644 --- a/savannadashboard/clusters/workflows.py +++ b/savannadashboard/clusters/workflows/create.py @@ -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"] diff --git a/savannadashboard/clusters/workflows/scale.py b/savannadashboard/clusters/workflows/scale.py new file mode 100644 index 00000000..289b3542 --- /dev/null +++ b/savannadashboard/clusters/workflows/scale.py @@ -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 diff --git a/savannadashboard/templates/cluster_templates/cluster_node_groups_template.html b/savannadashboard/templates/cluster_templates/cluster_node_groups_template.html index 830419aa..321e8373 100644 --- a/savannadashboard/templates/cluster_templates/cluster_node_groups_template.html +++ b/savannadashboard/templates/cluster_templates/cluster_node_groups_template.html @@ -1,22 +1,30 @@ \ No newline at end of file diff --git a/savannadashboard/templates/cluster_templates/cluster_templates.html b/savannadashboard/templates/cluster_templates/cluster_templates.html index d9bdcf50..d0e69c20 100644 --- a/savannadashboard/templates/cluster_templates/cluster_templates.html +++ b/savannadashboard/templates/cluster_templates/cluster_templates.html @@ -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("
   
"); - - $(".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(""); diff --git a/savannadashboard/templates/clusters/scale.html b/savannadashboard/templates/clusters/scale.html new file mode 100644 index 00000000..e526decc --- /dev/null +++ b/savannadashboard/templates/clusters/scale.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Scale Cluster" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Savanna - Scale Cluster") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/savannadashboard/utils/workflow_helpers.py b/savannadashboard/utils/workflow_helpers.py index b87ce39a..5b2e6506 100644 --- a/savannadashboard/utils/workflow_helpers.py +++ b/savannadashboard/utils/workflow_helpers.py @@ -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():