Add ui for resizing clusters
Create new row action on clusters panel Create new modal form for resizing cluster:wq Create REST endpoint for resizing cluster Bump python-magnumclient lower constraint Add heatclient lower constraint Change-Id: I591d4e6ebe85adac0bcefb3f95b1a7d2abf0ba88
This commit is contained in:
parent
2d2ea7422a
commit
0bfd85d4c1
@ -79,8 +79,9 @@ pyScss==1.3.4
|
|||||||
python-cinderclient==3.5.0
|
python-cinderclient==3.5.0
|
||||||
python-dateutil==2.7.0
|
python-dateutil==2.7.0
|
||||||
python-glanceclient==2.9.1
|
python-glanceclient==2.9.1
|
||||||
|
python-heatclient==1.18.0
|
||||||
python-keystoneclient==3.15.0
|
python-keystoneclient==3.15.0
|
||||||
python-magnumclient==2.11.0
|
python-magnumclient==2.15.0 # Apache-2.0
|
||||||
python-mimeparse==1.6.0
|
python-mimeparse==1.6.0
|
||||||
python-neutronclient==6.7.0
|
python-neutronclient==6.7.0
|
||||||
python-novaclient==10.1.0
|
python-novaclient==10.1.0
|
||||||
|
62
magnum_ui/api/heat.py
Normal file
62
magnum_ui/api/heat.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Copyright 2015 Cisco Systems.
|
||||||
|
#
|
||||||
|
# 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 __future__ import absolute_import
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from horizon.utils.memoized import memoized
|
||||||
|
from openstack_dashboard.api import base
|
||||||
|
|
||||||
|
from heatclient import client as heat_client
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def heatclient(request, password=None):
|
||||||
|
|
||||||
|
service_type = 'orchestration'
|
||||||
|
openstack_api_versions = getattr(settings, 'OPENSTACK_API_VERSIONS', {})
|
||||||
|
api_version = openstack_api_versions.get(service_type, 1)
|
||||||
|
|
||||||
|
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
|
||||||
|
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
|
||||||
|
|
||||||
|
endpoint = base.url_for(request, 'orchestration')
|
||||||
|
kwargs = {
|
||||||
|
'token': request.user.token.id,
|
||||||
|
'insecure': insecure,
|
||||||
|
'ca_file': cacert,
|
||||||
|
'username': request.user.username,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
|
||||||
|
client = heat_client.Client(api_version, endpoint, **kwargs)
|
||||||
|
client.format_parameters = format_parameters
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def format_parameters(params):
|
||||||
|
parameters = {}
|
||||||
|
for count, p in enumerate(params, 1):
|
||||||
|
parameters['Parameters.member.%d.ParameterKey' % count] = p
|
||||||
|
parameters['Parameters.member.%d.ParameterValue' % count] = params[p]
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
|
||||||
|
def stack_get(request, stack_id):
|
||||||
|
return heatclient(request).stacks.get(stack_id)
|
@ -84,7 +84,7 @@ def _create_patches(old, new):
|
|||||||
for key in new:
|
for key in new:
|
||||||
path = '/' + key
|
path = '/' + key
|
||||||
if key in old and old[key] != new[key]:
|
if key in old and old[key] != new[key]:
|
||||||
if new[key] is None or new[key] is '':
|
if new[key] is None or new[key] == '':
|
||||||
patch.append({'op': 'remove', 'path': path})
|
patch.append({'op': 'remove', 'path': path})
|
||||||
else:
|
else:
|
||||||
patch.append({'op': 'replace', 'path': path,
|
patch.append({'op': 'replace', 'path': path,
|
||||||
@ -196,6 +196,19 @@ def cluster_show(request, id):
|
|||||||
return magnumclient(request).clusters.get(id)
|
return magnumclient(request).clusters.get(id)
|
||||||
|
|
||||||
|
|
||||||
|
def cluster_resize(request, cluster_id, node_count,
|
||||||
|
nodes_to_remove=None, nodegroup=None):
|
||||||
|
|
||||||
|
if nodes_to_remove is None:
|
||||||
|
nodes_to_remove = []
|
||||||
|
|
||||||
|
# Note: Magnum client does not use any return statement so result will
|
||||||
|
# be None unless an exception is raised.
|
||||||
|
return magnumclient(request).clusters.resize(
|
||||||
|
cluster_id, node_count,
|
||||||
|
nodes_to_remove=nodes_to_remove, nodegroup=nodegroup)
|
||||||
|
|
||||||
|
|
||||||
def certificate_create(request, **kwargs):
|
def certificate_create(request, **kwargs):
|
||||||
args = {}
|
args = {}
|
||||||
for (key, value) in kwargs.items():
|
for (key, value) in kwargs.items():
|
||||||
|
@ -12,10 +12,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from django.http import HttpResponseNotFound
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
|
from magnum_ui.api import heat
|
||||||
from magnum_ui.api import magnum
|
from magnum_ui.api import magnum
|
||||||
|
|
||||||
|
from openstack_dashboard import api
|
||||||
from openstack_dashboard.api import neutron
|
from openstack_dashboard.api import neutron
|
||||||
from openstack_dashboard.api.rest import urls
|
from openstack_dashboard.api.rest import urls
|
||||||
from openstack_dashboard.api.rest import utils as rest_utils
|
from openstack_dashboard.api.rest import utils as rest_utils
|
||||||
@ -116,6 +119,51 @@ class Cluster(generic.View):
|
|||||||
updated_cluster.to_dict())
|
updated_cluster.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@urls.register
|
||||||
|
class ClusterResize(generic.View):
|
||||||
|
|
||||||
|
url_regex = r'container_infra/clusters/(?P<cluster_id>[^/]+)/resize$'
|
||||||
|
|
||||||
|
@rest_utils.ajax()
|
||||||
|
def get(self, request, cluster_id):
|
||||||
|
"""Get cluster details for resize"""
|
||||||
|
try:
|
||||||
|
cluster = magnum.cluster_show(request, cluster_id).to_dict()
|
||||||
|
except AttributeError as e:
|
||||||
|
print(e)
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
stack = heat.stack_get(request, cluster["stack_id"])
|
||||||
|
search_opts = {"name": "%s-minion" % stack.stack_name}
|
||||||
|
servers = api.nova.server_list(request, search_opts=search_opts)[0]
|
||||||
|
|
||||||
|
worker_nodes = []
|
||||||
|
for server in servers:
|
||||||
|
worker_nodes.append({"name": server.name, "id": server.id})
|
||||||
|
|
||||||
|
return {"cluster": change_to_id(cluster),
|
||||||
|
"worker_nodes": worker_nodes}
|
||||||
|
|
||||||
|
@rest_utils.ajax(data_required=True)
|
||||||
|
def post(self, request, cluster_id):
|
||||||
|
"""Resize a cluster"""
|
||||||
|
|
||||||
|
nodes_to_remove = request.DATA.get("nodes_to_remove", None)
|
||||||
|
nodegroup = request.DATA.get("nodegroup", None)
|
||||||
|
node_count = request.DATA.get("node_count")
|
||||||
|
|
||||||
|
# Result will be 'None' unless error is raised response will be '204'
|
||||||
|
try:
|
||||||
|
return magnum.cluster_resize(
|
||||||
|
request, cluster_id, node_count,
|
||||||
|
nodes_to_remove=nodes_to_remove, nodegroup=nodegroup)
|
||||||
|
except AttributeError as e:
|
||||||
|
# If cluster is not found magnum-client throws Attribute error
|
||||||
|
# catch and respond with 404
|
||||||
|
print(e)
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
@urls.register
|
||||||
class Clusters(generic.View):
|
class Clusters(generic.View):
|
||||||
"""API for Magnum Clusters"""
|
"""API for Magnum Clusters"""
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
'horizon.framework.util.i18n.gettext',
|
'horizon.framework.util.i18n.gettext',
|
||||||
'horizon.dashboard.container-infra.clusters.create.service',
|
'horizon.dashboard.container-infra.clusters.create.service',
|
||||||
'horizon.dashboard.container-infra.clusters.delete.service',
|
'horizon.dashboard.container-infra.clusters.delete.service',
|
||||||
|
'horizon.dashboard.container-infra.clusters.resize.service',
|
||||||
'horizon.dashboard.container-infra.clusters.update.service',
|
'horizon.dashboard.container-infra.clusters.update.service',
|
||||||
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
||||||
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
||||||
@ -46,6 +47,7 @@
|
|||||||
gettext,
|
gettext,
|
||||||
createClusterService,
|
createClusterService,
|
||||||
deleteClusterService,
|
deleteClusterService,
|
||||||
|
resizeClusterService,
|
||||||
updateClusterService,
|
updateClusterService,
|
||||||
showCertificateService,
|
showCertificateService,
|
||||||
signCertificateService,
|
signCertificateService,
|
||||||
@ -95,6 +97,13 @@
|
|||||||
text: gettext('Rotate Certificate')
|
text: gettext('Rotate Certificate')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.append({
|
||||||
|
id: 'resizeClusterAction',
|
||||||
|
service: resizeClusterService,
|
||||||
|
template: {
|
||||||
|
text: gettext('Resize Cluster')
|
||||||
|
}
|
||||||
|
})
|
||||||
.append({
|
.append({
|
||||||
id: 'updateClusterAction',
|
id: 'updateClusterAction',
|
||||||
service: updateClusterService,
|
service: updateClusterService,
|
||||||
|
@ -50,6 +50,16 @@
|
|||||||
expect(actionHasId(actions, 'rotateCertificateAction')).toBe(true);
|
expect(actionHasId(actions, 'rotateCertificateAction')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('registers Resize Cluster as an item action', function() {
|
||||||
|
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||||
|
expect(actionHasId(actions, 'resizeClusterAction')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registers Update Cluster as an item action', function() {
|
||||||
|
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||||
|
expect(actionHasId(actions, 'updateClusterAction')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('registers Delete Cluster as an item action', function() {
|
it('registers Delete Cluster as an item action', function() {
|
||||||
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||||
expect(actionHasId(actions, 'deleteClusterAction')).toBe(true);
|
expect(actionHasId(actions, 'deleteClusterAction')).toBe(true);
|
||||||
|
@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2017 NEC Corporation
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @name horizon.dashboard.container-infra.clusters.resize.service
|
||||||
|
* @description Service for the container-infra cluster resize modal.
|
||||||
|
* Allows user to select new number of worker nodes and if the number
|
||||||
|
* is reduced, nodes to be removed can be selected from the list.
|
||||||
|
*/
|
||||||
|
angular
|
||||||
|
.module('horizon.dashboard.container-infra.clusters')
|
||||||
|
.factory('horizon.dashboard.container-infra.clusters.resize.service',
|
||||||
|
resizeService);
|
||||||
|
|
||||||
|
resizeService.$inject = [
|
||||||
|
'$rootScope',
|
||||||
|
'$q',
|
||||||
|
'horizon.app.core.openstack-service-api.magnum',
|
||||||
|
'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',
|
||||||
|
'horizon.framework.widgets.modal-wait-spinner.service',
|
||||||
|
'horizon.dashboard.container-infra.clusters.resourceType'
|
||||||
|
];
|
||||||
|
|
||||||
|
function resizeService(
|
||||||
|
$rootScope, $q, magnum, actionResult, gettext, $qExtensions, modal, toast, spinnerModal,
|
||||||
|
resourceType
|
||||||
|
) {
|
||||||
|
|
||||||
|
var modalConfig, formModel;
|
||||||
|
|
||||||
|
var service = {
|
||||||
|
perform: perform,
|
||||||
|
allowed: allowed
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
function perform(selected, $scope) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
spinnerModal.showModalSpinner(gettext('Loading'));
|
||||||
|
|
||||||
|
magnum.getClusterNodes(selected.id)
|
||||||
|
.then(onLoad)
|
||||||
|
.catch(hideSpinnerOnError);
|
||||||
|
|
||||||
|
function onLoad(response) {
|
||||||
|
formModel = getFormModelDefaults();
|
||||||
|
formModel.id = selected.id;
|
||||||
|
|
||||||
|
modalConfig = constructModalConfig(response.data.worker_nodes);
|
||||||
|
|
||||||
|
deferred.resolve(modal.open(modalConfig).then(onModalSubmit));
|
||||||
|
$scope.model = formModel;
|
||||||
|
|
||||||
|
spinnerModal.hideModalSpinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSpinnerOnError(error) {
|
||||||
|
spinnerModal.hideModalSpinner();
|
||||||
|
return deferred.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowed() {
|
||||||
|
return $qExtensions.booleanAsPromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructModalConfig(workerNodesList) {
|
||||||
|
formModel.original_node_count = workerNodesList.length;
|
||||||
|
formModel.node_count = workerNodesList.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: gettext('Resize Cluster'),
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
'node_count': {
|
||||||
|
type: 'number',
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
'nodes_to_remove': {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
minItems: 0 // Must be specified to avoid obsolete validation errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
key: 'node_count',
|
||||||
|
title: gettext('Node Count'),
|
||||||
|
placeholder: gettext('The cluster node count.'),
|
||||||
|
required: true,
|
||||||
|
validationMessage: {
|
||||||
|
101: gettext('You cannot resize to less than a single Worker Node.')
|
||||||
|
},
|
||||||
|
onChange: validateNodeRemovalCount
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nodes_to_remove',
|
||||||
|
type: 'checkboxes',
|
||||||
|
title: gettext('Choose nodes to remove (Optional)'),
|
||||||
|
titleMap: generateNodesTitleMap(workerNodesList),
|
||||||
|
condition: 'model.node_count < model.original_node_count',
|
||||||
|
onChange: validateNodeRemovalCount,
|
||||||
|
validationMessage: {
|
||||||
|
nodeRemovalCountExceeded: gettext('You may only select as many nodes ' +
|
||||||
|
'as you are reducing the original node count by.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
model: formModel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid when user selects more Worker Nodes (checkboxes) than is allowed to be removed
|
||||||
|
function validateNodeRemovalCount() {
|
||||||
|
var selectedNodesCount = formModel.nodes_to_remove ? formModel.nodes_to_remove.length : 0;
|
||||||
|
var maximumNodesCount = formModel.original_node_count - formModel.node_count;
|
||||||
|
|
||||||
|
if (selectedNodesCount <= maximumNodesCount) {
|
||||||
|
broadcastNodeRemovalValid();
|
||||||
|
} else {
|
||||||
|
broadcastNodeRemovalInvalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastNodeRemovalInvalid() {
|
||||||
|
$rootScope.$broadcast('schemaForm.error.nodes_to_remove',
|
||||||
|
'nodeRemovalCountExceeded', false);
|
||||||
|
}
|
||||||
|
function broadcastNodeRemovalValid() {
|
||||||
|
$rootScope.$broadcast('schemaForm.error.nodes_to_remove',
|
||||||
|
'nodeRemovalCountExceeded', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormModelDefaults() {
|
||||||
|
return {
|
||||||
|
original_node_count: null,
|
||||||
|
node_count: null,
|
||||||
|
nodes_to_remove: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNodesTitleMap(nodesList) {
|
||||||
|
return nodesList.map(function(node) {
|
||||||
|
return {
|
||||||
|
value: node.id,
|
||||||
|
name: node.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onModalSubmit() {
|
||||||
|
var postRequestObject = {
|
||||||
|
node_count: formModel.node_count,
|
||||||
|
nodegroup: 'production_group'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formModel.node_count < formModel.original_node_count &&
|
||||||
|
formModel.nodes_to_remove && formModel.nodes_to_remove.length > 0) {
|
||||||
|
postRequestObject.nodes_to_remove = formModel.nodes_to_remove;
|
||||||
|
}
|
||||||
|
|
||||||
|
return magnum.resizeCluster(formModel.id, postRequestObject)
|
||||||
|
.then(onRequestSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRequestSuccess() {
|
||||||
|
toast.add('success', gettext('Cluster is being resized.'));
|
||||||
|
return actionResult.getActionResult()
|
||||||
|
.updated(resourceType, formModel.id)
|
||||||
|
.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2017 NEC Corporation
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('horizon.dashboard.container-infra.clusters.resize.service', function() {
|
||||||
|
|
||||||
|
var service, $scope, $q, deferred, magnum, spinnerModal, modalConfig;
|
||||||
|
var selected = {
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
var modal = {
|
||||||
|
open: function(config) {
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.resolve(config);
|
||||||
|
modalConfig = config;
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
beforeEach(module('horizon.app.core'));
|
||||||
|
beforeEach(module('horizon.framework'));
|
||||||
|
beforeEach(module('horizon.dashboard.container-infra.clusters'));
|
||||||
|
|
||||||
|
beforeEach(module(function($provide) {
|
||||||
|
$provide.value('horizon.framework.widgets.form.ModalFormService', modal);
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
|
||||||
|
$q = _$q_;
|
||||||
|
$scope = _$rootScope_.$new();
|
||||||
|
service = $injector.get(
|
||||||
|
'horizon.dashboard.container-infra.clusters.resize.service');
|
||||||
|
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
||||||
|
spinnerModal = $injector.get('horizon.framework.widgets.modal-wait-spinner.service');
|
||||||
|
|
||||||
|
spyOn(spinnerModal, 'showModalSpinner').and.callFake(function() {});
|
||||||
|
spyOn(spinnerModal, 'hideModalSpinner').and.callFake(function() {});
|
||||||
|
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.resolve({data: {uuid: '1'}});
|
||||||
|
spyOn(magnum, 'resizeCluster').and.returnValue(deferred.promise);
|
||||||
|
spyOn(modal, 'open').and.callThrough();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should check the policy if the user is allowed to update cluster', function() {
|
||||||
|
var allowed = service.allowed();
|
||||||
|
expect(allowed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the modal, hide the loading spinner and check the form model',
|
||||||
|
inject(function($timeout) {
|
||||||
|
var mockWorkerNodes = [{id: "456", name: "Worker Node 1"}];
|
||||||
|
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.resolve({data: {cluster: {}, worker_nodes: mockWorkerNodes}});
|
||||||
|
spyOn(magnum, 'getClusterNodes').and.returnValue(deferred.promise);
|
||||||
|
|
||||||
|
service.perform(selected, $scope);
|
||||||
|
|
||||||
|
$timeout(function() {
|
||||||
|
expect(modal.open).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.showModalSpinner).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.hideModalSpinner).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Check if the form's model skeleton is correct
|
||||||
|
expect(modalConfig.model.id).toBe(selected.id);
|
||||||
|
expect(modalConfig.model.original_node_count).toBe(mockWorkerNodes.length);
|
||||||
|
expect(modalConfig.model.node_count).toBe(mockWorkerNodes.length);
|
||||||
|
expect(modalConfig.title).toBeDefined();
|
||||||
|
expect(modalConfig.schema).toBeDefined();
|
||||||
|
expect(modalConfig.form).toBeDefined();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$timeout.flush();
|
||||||
|
$scope.$apply();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not open the modal due to a request error and should hide the loading spinner',
|
||||||
|
inject(function($timeout) {
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.reject();
|
||||||
|
spyOn(magnum, 'getClusterNodes').and.returnValue(deferred.promise);
|
||||||
|
|
||||||
|
service.perform(selected, $scope);
|
||||||
|
|
||||||
|
$timeout(function() {
|
||||||
|
expect(modal.open).not.toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.showModalSpinner).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.hideModalSpinner).toHaveBeenCalled();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$timeout.flush();
|
||||||
|
$scope.$apply();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
})();
|
@ -33,6 +33,8 @@
|
|||||||
updateCluster: updateCluster,
|
updateCluster: updateCluster,
|
||||||
getCluster: getCluster,
|
getCluster: getCluster,
|
||||||
getClusters: getClusters,
|
getClusters: getClusters,
|
||||||
|
getClusterNodes: getClusterNodes,
|
||||||
|
resizeCluster: resizeCluster,
|
||||||
deleteCluster: deleteCluster,
|
deleteCluster: deleteCluster,
|
||||||
deleteClusters: deleteClusters,
|
deleteClusters: deleteClusters,
|
||||||
createClusterTemplate: createClusterTemplate,
|
createClusterTemplate: createClusterTemplate,
|
||||||
@ -87,6 +89,21 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClusterNodes(id) {
|
||||||
|
return apiService.get('/api/container_infra/clusters/' + id + '/resize')
|
||||||
|
.error(function() {
|
||||||
|
toastService.add('error', gettext('Unable to get cluster\'s working nodes.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCluster(id, params) {
|
||||||
|
return apiService.post('/api/container_infra/clusters/' + id + '/resize', params)
|
||||||
|
.error(function() {
|
||||||
|
var msg = gettext('Unable to resize given cluster id: %(id)s.');
|
||||||
|
toastService.add('error', interpolate(msg, { id: id }, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function deleteCluster(id, suppressError) {
|
function deleteCluster(id, suppressError) {
|
||||||
var promise = apiService.delete('/api/container_infra/clusters/', [id]);
|
var promise = apiService.delete('/api/container_infra/clusters/', [id]);
|
||||||
return suppressError ? promise : promise.error(function() {
|
return suppressError ? promise : promise.error(function() {
|
||||||
|
@ -68,6 +68,32 @@
|
|||||||
"path": "/api/container_infra/clusters/",
|
"path": "/api/container_infra/clusters/",
|
||||||
"error": "Unable to retrieve the clusters."
|
"error": "Unable to retrieve the clusters."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"func": "getClusterNodes",
|
||||||
|
"method": "get",
|
||||||
|
"path": "/api/container_infra/clusters/123/resize",
|
||||||
|
"error": "Unable to get cluster\'s working nodes.",
|
||||||
|
"testInput": ["123"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"func": "resizeCluster",
|
||||||
|
"method": "post",
|
||||||
|
"path": "/api/container_infra/clusters/123/resize",
|
||||||
|
"data": {
|
||||||
|
"node_count": 2,
|
||||||
|
"nodes_to_remove": ["456"],
|
||||||
|
"nodegroup": "production_group"
|
||||||
|
},
|
||||||
|
"error": "Unable to resize given cluster id: 123.",
|
||||||
|
"testInput": [
|
||||||
|
"123",
|
||||||
|
{
|
||||||
|
"node_count": 2,
|
||||||
|
"nodes_to_remove": ["456"],
|
||||||
|
"nodegroup": "production_group"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"func": "deleteCluster",
|
"func": "deleteCluster",
|
||||||
"method": "delete",
|
"method": "delete",
|
||||||
|
10
releasenotes/notes/resize-actions-1436a2a0dccbd13b.yaml
Normal file
10
releasenotes/notes/resize-actions-1436a2a0dccbd13b.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- >
|
||||||
|
REST Api and Angular service for resizing clusters is addedd. Angular view
|
||||||
|
supports resizing number of worker nodes only.
|
||||||
|
other:
|
||||||
|
- >
|
||||||
|
Bump python-magnumclient lowerconstraint to >= 2.15.0
|
||||||
|
- >
|
||||||
|
Adds python-heatclient >= 1.18.0 dependency
|
@ -8,6 +8,7 @@
|
|||||||
#
|
#
|
||||||
# PBR should always appear first
|
# PBR should always appear first
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
python-magnumclient>=2.11.0 # Apache-2.0
|
python-magnumclient>=2.15.0 # Apache-2.0
|
||||||
|
python-heatclient>=1.18.0
|
||||||
|
|
||||||
horizon>=15.0.0.0b1 # Apache-2.0
|
horizon>=15.0.0.0b1 # Apache-2.0
|
Loading…
x
Reference in New Issue
Block a user