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-dateutil==2.7.0
|
||||
python-glanceclient==2.9.1
|
||||
python-heatclient==1.18.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-neutronclient==6.7.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:
|
||||
path = '/' + 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})
|
||||
else:
|
||||
patch.append({'op': 'replace', 'path': path,
|
||||
@ -196,6 +196,19 @@ def cluster_show(request, 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):
|
||||
args = {}
|
||||
for (key, value) in kwargs.items():
|
||||
|
@ -12,10 +12,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.views import generic
|
||||
|
||||
from magnum_ui.api import heat
|
||||
from magnum_ui.api import magnum
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import neutron
|
||||
from openstack_dashboard.api.rest import urls
|
||||
from openstack_dashboard.api.rest import utils as rest_utils
|
||||
@ -116,6 +119,51 @@ class Cluster(generic.View):
|
||||
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
|
||||
class Clusters(generic.View):
|
||||
"""API for Magnum Clusters"""
|
||||
|
@ -34,6 +34,7 @@
|
||||
'horizon.framework.util.i18n.gettext',
|
||||
'horizon.dashboard.container-infra.clusters.create.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.show-certificate.service',
|
||||
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
||||
@ -46,6 +47,7 @@
|
||||
gettext,
|
||||
createClusterService,
|
||||
deleteClusterService,
|
||||
resizeClusterService,
|
||||
updateClusterService,
|
||||
showCertificateService,
|
||||
signCertificateService,
|
||||
@ -95,6 +97,13 @@
|
||||
text: gettext('Rotate Certificate')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'resizeClusterAction',
|
||||
service: resizeClusterService,
|
||||
template: {
|
||||
text: gettext('Resize Cluster')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'updateClusterAction',
|
||||
service: updateClusterService,
|
||||
|
@ -50,6 +50,16 @@
|
||||
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() {
|
||||
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||
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,
|
||||
getCluster: getCluster,
|
||||
getClusters: getClusters,
|
||||
getClusterNodes: getClusterNodes,
|
||||
resizeCluster: resizeCluster,
|
||||
deleteCluster: deleteCluster,
|
||||
deleteClusters: deleteClusters,
|
||||
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) {
|
||||
var promise = apiService.delete('/api/container_infra/clusters/', [id]);
|
||||
return suppressError ? promise : promise.error(function() {
|
||||
|
@ -68,6 +68,32 @@
|
||||
"path": "/api/container_infra/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",
|
||||
"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!=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…
Reference in New Issue
Block a user