diff --git a/releasenotes/notes/scale-cluster-21481c69dcf78cc8.yaml b/releasenotes/notes/scale-cluster-21481c69dcf78cc8.yaml new file mode 100644 index 00000000..01d3c3f8 --- /dev/null +++ b/releasenotes/notes/scale-cluster-21481c69dcf78cc8.yaml @@ -0,0 +1,7 @@ +--- +features: + - > + Scale-in and Scale-out actions for cluster added. + These actions are added as row action for each cluster + in Clusters table view. Although, this action is only + for Angularized clusters panel. diff --git a/senlin_dashboard/api/rest/senlin.py b/senlin_dashboard/api/rest/senlin.py index 64232785..d4bfbfde 100644 --- a/senlin_dashboard/api/rest/senlin.py +++ b/senlin_dashboard/api/rest/senlin.py @@ -397,6 +397,78 @@ class Cluster(generic.View): updated_cluster.to_dict()) +@urls.register +class ClusterActions(generic.View): + """API for Senlin cluster.""" + + url_regex = r'senlin/clusters/(?P[^/]+)/(?P[^/]+)$' + + @rest_utils.ajax() + def get(self, request, cluster_id, action): + if action == "policy": + """Get policies of a single cluster with the cluster id. + + The following get parameters may be passed in the GET + + :param cluster_id: the id of the cluster + + The result is a cluster object. + """ + policies = senlin.cluster_policy_list(request, cluster_id, {}) + + return { + 'items': [p.to_dict() for p in policies], + } + elif action == "": + return None + + @rest_utils.ajax(data_required=True) + def put(self, request, cluster_id, action): + if action == "policy": + """Update policies for the cluster.""" + params = request.DATA + + new_attach_ids = params["ids"] + old_attached = senlin.cluster_policy_list(request, cluster_id, {}) + + # Extract policies should be detached and execute + for policy in old_attached: + should_detach = True + for new_id in new_attach_ids: + if new_id == policy.policy_id: + # This policy is already attached. + should_detach = False + break + if should_detach: + # If policy is not exist in new policies, + # it should be removed + senlin.cluster_detach_policy( + request, cluster_id, policy.policy_id) + + # Extract policies should be attached and execute + for new_id in new_attach_ids: + should_attach = True + for policy in old_attached: + if new_id == policy.policy_id: + # This policy is already attached. + should_attach = False + break + if should_attach: + # If policy is not exist in old policies, + # it should be added + senlin.cluster_attach_policy(request, cluster_id, + new_id, {}) + + return rest_utils.CreatedResponse( + '/api/senlin/clusters/%s/policy' % cluster_id) + elif action == "scale-in": + count = request.DATA.get("count") or None + return senlin.cluster_scale_in(request, cluster_id, count) + elif action == "scale-out": + count = request.DATA.get("count") or None + return senlin.cluster_scale_out(request, cluster_id, count) + + @urls.register class Policies(generic.View): """API for Senlin policies.""" @@ -482,62 +554,3 @@ class Policy(generic.View): return rest_utils.CreatedResponse( '/api/senlin/policies/%s' % updated_policy.id, updated_policy.to_dict()) - - -@urls.register -class ClusterPolicies(generic.View): - """API for Senlin cluster.""" - - url_regex = r'senlin/clusters/(?P[^/]+)/policy$' - - @rest_utils.ajax() - def get(self, request, cluster_id): - """Get policies of a single cluster with the cluster id. - - The following get parameters may be passed in the GET - - :param cluster_id: the id of the cluster - - The result is a cluster object. - """ - policies = senlin.cluster_policy_list(request, cluster_id, {}) - - return { - 'items': [p.to_dict() for p in policies], - } - - @rest_utils.ajax(data_required=True) - def put(self, request, cluster_id): - """Update policies for the cluster.""" - params = request.DATA - - new_attach_ids = params["ids"] - old_attached = senlin.cluster_policy_list(request, cluster_id, {}) - - # Extract policies should be detached and execute - for policy in old_attached: - should_detach = True - for new_id in new_attach_ids: - if new_id == policy.policy_id: - # This policy is already attached. - should_detach = False - break - if should_detach: - # If policy is not exist in new policies, it should be removed - senlin.cluster_detach_policy( - request, cluster_id, policy.policy_id) - - # Extract policies should be attached and execute - for new_id in new_attach_ids: - should_attach = True - for policy in old_attached: - if new_id == policy.policy_id: - # This policy is already attached. - should_attach = False - break - if should_attach: - # If policy is not exist in old policies, it should be added - senlin.cluster_attach_policy(request, cluster_id, new_id, {}) - - return rest_utils.CreatedResponse( - '/api/senlin/clusters/%s/policy' % cluster_id) diff --git a/senlin_dashboard/api/senlin.py b/senlin_dashboard/api/senlin.py index 8cf9c571..2e559f68 100644 --- a/senlin_dashboard/api/senlin.py +++ b/senlin_dashboard/api/senlin.py @@ -165,6 +165,16 @@ def cluster_recover(request, cluster, params=None): senlinclient(request).recover_cluster(cluster, **params) +def cluster_scale_in(request, cluster, count=None): + """Scale in a Cluster""" + senlinclient(request).cluster_scale_in(cluster, count) + + +def cluster_scale_out(request, cluster, count=None): + """Scale out a Cluster""" + senlinclient(request).cluster_scale_out(cluster, count) + + def cluster_delete(request, cluster): """Delete cluster.""" senlinclient(request).delete_cluster(cluster) diff --git a/senlin_dashboard/static/app/core/clusters/actions/actions.module.js b/senlin_dashboard/static/app/core/clusters/actions/actions.module.js index ed09b1b8..f21dcf2b 100644 --- a/senlin_dashboard/static/app/core/clusters/actions/actions.module.js +++ b/senlin_dashboard/static/app/core/clusters/actions/actions.module.js @@ -34,6 +34,8 @@ 'horizon.cluster.clusters.actions.manage-policy.service', 'horizon.cluster.clusters.actions.delete.service', 'horizon.cluster.clusters.actions.update.service', + 'horizon.cluster.clusters.actions.scale-in.service', + 'horizon.cluster.clusters.actions.scale-out.service', 'horizon.app.core.clusters.resourceType' ]; @@ -43,6 +45,8 @@ managePolicyService, deleteClusterService, updateClusterService, + scaleInClusterService, + scaleOutClusterService, clusterResourceType ) { var clusterResource = registry.getResourceType(clusterResourceType); @@ -84,6 +88,22 @@ type: 'row' } }) + .append({ + id: 'scaleInClusterAction', + service: scaleInClusterService, + template: { + text: gettext('Scale-in Cluster'), + type: 'row' + } + }) + .append({ + id: 'scaleOutClusterAction', + service: scaleOutClusterService, + template: { + text: gettext('Scale-out Cluster'), + type: 'row' + } + }) .append({ id: 'deleteClusterAction', service: deleteClusterService, diff --git a/senlin_dashboard/static/app/core/openstack-service-api/senlin.service.js b/senlin_dashboard/static/app/core/openstack-service-api/senlin.service.js index c393d885..be2ef10e 100644 --- a/senlin_dashboard/static/app/core/openstack-service-api/senlin.service.js +++ b/senlin_dashboard/static/app/core/openstack-service-api/senlin.service.js @@ -38,6 +38,7 @@ createCluster: createCluster, updateCluster: updateCluster, deleteCluster: deleteCluster, + scaleCluster: scaleCluster, createProfile: createProfile, updateProfile: updateProfile, deleteProfile: deleteProfile, @@ -476,7 +477,39 @@ }); } - // Policies + /** + * @name scaleCluster + * @description + * Scale a Cluster. + * + * @param {Object} id + * Cluster ID to scale. + * @param {Object} name + * Cluster name to scale. + * @param {Object} scale + * Direction of scale. 'in' or 'out'. + * @param {Object} count + * Count to scale-in a cluster. + * @param {boolean} suppressError + * If passed in, this will not show the default error handling + * @returns {Object} The result of the API call + */ + function scaleCluster(id, name, scale, count, suppressError) { + var promise = apiService.put( + '/api/senlin/clusters/' + id + '/scale-' + scale, + {count: count}); + + return suppressError ? promise : promise.error(function() { + var msg = gettext('Unable to scale-%(scale)s the cluster with name: %(name)s'); + var scaleMsg; + if (scale === 'in') { + scaleMsg = gettext('in'); + } else { + scaleMsg = gettext('out'); + } + toastService.add('error', interpolate(msg, { scale: scaleMsg, name: name }, true)); + }); + } /* * @name getClusterPolicies @@ -511,6 +544,8 @@ }); } + // Policies + /* * @name getPolicies * @description diff --git a/senlin_dashboard/static/app/core/receivers/actions/scale-in.service.js b/senlin_dashboard/static/app/core/receivers/actions/scale-in.service.js new file mode 100644 index 00000000..7feccde0 --- /dev/null +++ b/senlin_dashboard/static/app/core/receivers/actions/scale-in.service.js @@ -0,0 +1,128 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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. + */ + +(function() { + 'use strict'; + + /** + * @ngDoc factory + * @name horizon.cluster.clusters.actions.scale-in.service + * @Description + * restart container. + */ + angular + .module('horizon.cluster.clusters.actions') + .factory('horizon.cluster.clusters.actions.scale-in.service', scaleService); + + scaleService.$inject = [ + 'horizon.app.core.openstack-service-api.senlin', + 'horizon.app.core.clusters.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service' + ]; + + function scaleService( + senlin, resourceType, actionResult, gettext, $qExtensions, modal, toast + ) { + // schema + var schema = { + type: "object", + properties: { + count: { + title: gettext("Node Count"), + type: "number", + minimum: 1 + } + } + }; + + // form + var form = [ + { + type: 'section', + htmlClass: 'row', + items: [ + { + type: 'section', + htmlClass: 'col-sm-12', + items: [ + { + key: "count", + placeholder: gettext("Specify node count for scale-in.") + } + ] + } + ] + } + ]; + + // model + var model; + + var message = { + success: gettext('Cluster scale-in %s was successfully accepted.') + }; + + var service = { + initAction: initAction, + allowed: allowed, + perform: perform + }; + + return service; + + ////////////// + + // include this function in your service + // if you plan to emit events to the parent controller + function initAction() { + } + + function allowed() { + return $qExtensions.booleanAsPromise(true); + } + + function perform(selected) { + model = { + id: selected.id, + name: selected.name, + count: null + }; + // modal config + var config = { + title: gettext('Scale-In'), + submitText: gettext('Scale-In'), + schema: schema, + form: form, + model: model + }; + return modal.open(config).then(submit); + + function submit(context) { + var id = context.model.id; + var name = context.model.name; + delete context.model.id; + delete context.model.name; + return senlin.scaleCluster(id, name, 'in', context.model.count).then(function() { + toast.add('success', interpolate(message.success, [name])); + var result = actionResult.getActionResult().updated(resourceType, id); + return result.result; + }); + } + } + } +})(); diff --git a/senlin_dashboard/static/app/core/receivers/actions/scale-out.service.js b/senlin_dashboard/static/app/core/receivers/actions/scale-out.service.js new file mode 100644 index 00000000..24579e72 --- /dev/null +++ b/senlin_dashboard/static/app/core/receivers/actions/scale-out.service.js @@ -0,0 +1,128 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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. + */ + +(function() { + 'use strict'; + + /** + * @ngDoc factory + * @name horizon.cluster.clusters.actions.scale-in.service + * @Description + * restart container. + */ + angular + .module('horizon.cluster.clusters.actions') + .factory('horizon.cluster.clusters.actions.scale-out.service', scaleService); + + scaleService.$inject = [ + 'horizon.app.core.openstack-service-api.senlin', + 'horizon.app.core.clusters.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service' + ]; + + function scaleService( + senlin, resourceType, actionResult, gettext, $qExtensions, modal, toast + ) { + // schema + var schema = { + type: "object", + properties: { + count: { + title: gettext("Node Count"), + type: "number", + minimum: 1 + } + } + }; + + // form + var form = [ + { + type: 'section', + htmlClass: 'row', + items: [ + { + type: 'section', + htmlClass: 'col-sm-12', + items: [ + { + key: "count", + placeholder: gettext("Specify node count for scale-out.") + } + ] + } + ] + } + ]; + + // model + var model; + + var message = { + success: gettext('Cluster scale-out %s was successfully accepted.') + }; + + var service = { + initAction: initAction, + allowed: allowed, + perform: perform + }; + + return service; + + ////////////// + + // include this function in your service + // if you plan to emit events to the parent controller + function initAction() { + } + + function allowed() { + return $qExtensions.booleanAsPromise(true); + } + + function perform(selected) { + model = { + id: selected.id, + name: selected.name, + count: null + }; + // modal config + var config = { + title: gettext('Scale-Out'), + submitText: gettext('Scale-Out'), + schema: schema, + form: form, + model: model + }; + return modal.open(config).then(submit); + + function submit(context) { + var id = context.model.id; + var name = context.model.name; + delete context.model.id; + delete context.model.name; + return senlin.scaleCluster(id, name, 'out', context.model.count).then(function() { + toast.add('success', interpolate(message.success, [name])); + var result = actionResult.getActionResult().updated(resourceType, id); + return result.result; + }); + } + } + } +})();