Allow nodegroups with node_count equal to 0

This change allows users to create clusters and nodegroups with
node_count equal to 0. Also adds support for resizing existing
nodegroups to 0.

Change-Id: Id63459d0fe9836e678bb7569f23d29eabc225e9e
story: 2007851
task: 40145
Signed-off-by: Diogo Guerra <diogo.filipe.tomas.guerra@cern.ch>
This commit is contained in:
Theodoros Tsioutsias 2020-06-23 16:18:05 +00:00 committed by Spyros Trigazis
parent fd79dd4fa6
commit f46923cc5e
15 changed files with 59 additions and 31 deletions

View File

@ -105,7 +105,7 @@ class Cluster(base.APIBase):
default=None)
"""The name of the nova ssh keypair"""
node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=1)
"""The node count for this cluster. Default to 1 if not set"""
master_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)

View File

@ -44,7 +44,7 @@ class ClusterResizeRequest(base.APIBase):
This class enforces type checking and value constraints.
"""
node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), mandatory=True)
node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), mandatory=True)
"""The expected node count after resize."""
nodes_to_remove = wsme.wsattr([wtypes.text], mandatory=False,

View File

@ -98,18 +98,18 @@ class NodeGroup(base.APIBase):
node_addresses = wsme.wsattr([wtypes.text], readonly=True)
"""IP addresses of nodegroup nodes"""
node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=1)
"""The node count for this nodegroup. Default to 1 if not set"""
role = wsme.wsattr(wtypes.StringType(min_length=1, max_length=255),
default='worker')
"""The role of the nodes included in this nodegroup"""
min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
"""The minimum allowed nodes for this nodegroup. Default to 1 if not set"""
min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=0)
"""The minimum allowed nodes for this nodegroup. Default to 0 if not set"""
max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=None)
"""The maximum allowed nodes for this nodegroup. Default to 1 if not set"""
max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=None)
"""The maximum allowed nodes for this nodegroup."""
is_default = types.BooleanType()
"""Specifies is a nodegroup was created by default or not"""

View File

@ -210,11 +210,11 @@ class JsonPatchType(wtypes.Base):
raise wsme.exc.ClientSideError(msg % patch.path)
if patch.op != 'remove':
if not patch.value:
if patch.value is None or patch.value == wtypes.Unset:
msg = _("'add' and 'replace' operations needs value")
raise wsme.exc.ClientSideError(msg)
ret = {'path': patch.path, 'op': patch.op}
if patch.value:
if patch.value is not None and patch.value != wtypes.Unset:
ret['value'] = patch.value
return ret

View File

@ -17,16 +17,6 @@ from webob import exc
from magnum.i18n import _
# NOTE(yuntong): v1.0 is reserved to indicate Kilo's API, but is not presently
# supported by the API service. All changes between Kilo and the
# point where we added microversioning are considered backwards-
# compatible, but are not specifically discoverable at this time.
#
# The v1.1 version indicates this "initial" version as being
# different from Kilo (v1.0), and includes the following changes:
#
# Add details of new api versions here:
#
# For each newly added microversion change, update the API version history
# string below with a one or two line description. Also update
@ -42,10 +32,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 1.7 - Add resize API
* 1.8 - Add upgrade API
* 1.9 - Add nodegroup API
* 1.10 - Allow nodegroups with 0 nodes
"""
BASE_VER = '1.1'
CURRENT_MAX_VER = '1.9'
CURRENT_MAX_VER = '1.10'
class Version(object):

View File

@ -75,3 +75,12 @@ user documentation.
An admin user can set/update/delete/list quotas for the given tenant.
A non-admin user can get self quota information.
1.10
---
Allow nodegroups with 0 nodes
Allow the cluster to be created with node_count = 0 as well as to update
existing nodegroups to have 0 nodes.

View File

@ -18,6 +18,7 @@ from pycadf import cadftaxonomy as taxonomy
from pycadf import cadftype
from pycadf import eventfactory
from pycadf import resource
from wsme import types as wtypes
from magnum.common import clients
from magnum.common import rpc
@ -176,6 +177,7 @@ def _get_nodegroup_object(context, cluster, node_count, is_master=False):
ng.image_id = cluster.cluster_template.image_id
ng.docker_volume_size = (cluster.docker_volume_size or
cluster.cluster_template.docker_volume_size)
if is_master:
ng.flavor_id = (cluster.master_flavor_id or
cluster.cluster_template.master_flavor_id)
@ -183,6 +185,11 @@ def _get_nodegroup_object(context, cluster, node_count, is_master=False):
else:
ng.flavor_id = cluster.flavor_id or cluster.cluster_template.flavor_id
ng.role = "worker"
if (cluster.labels != wtypes.Unset and cluster.labels is not None
and 'min_node_count' in cluster.labels):
ng.min_node_count = cluster.labels['min_node_count']
else:
ng.min_node_count = 0
ng.name = "default-%s" % ng.role
ng.is_default = True
ng.status = fields.ClusterStatus.CREATE_IN_PROGRESS

View File

@ -842,7 +842,7 @@ parameters:
type: number
description: >
minimum node count of cluster workers when doing scale down
default: 1
default: 0
max_node_count:
type: number

View File

@ -858,7 +858,7 @@ parameters:
type: number
description: >
minimum node count of cluster workers when doing scale down
default: 1
default: 0
max_node_count:
type: number

View File

@ -26,8 +26,9 @@ from magnum.objects import fields as m_fields
class NodeGroup(base.MagnumPersistentObject, base.MagnumObject,
base.MagnumObjectDictCompat):
# Version 1.0: Initial version
# Version 1.1: min_node_count defaults to 0
VERSION = '1.0'
VERSION = '1.1'
dbapi = dbapi.get_instance()
@ -45,7 +46,7 @@ class NodeGroup(base.MagnumPersistentObject, base.MagnumObject,
'node_count': fields.IntegerField(nullable=False, default=1),
'role': fields.StringField(),
'max_node_count': fields.IntegerField(nullable=True),
'min_node_count': fields.IntegerField(nullable=False, default=1),
'min_node_count': fields.IntegerField(nullable=False, default=0),
'is_default': fields.BooleanField(default=False),
'stack_id': fields.StringField(nullable=True),
'status': m_fields.ClusterStatusField(nullable=True),

View File

@ -41,7 +41,7 @@ class TestRootController(api_base.FunctionalTest):
[{u'href': u'http://localhost/v1/',
u'rel': u'self'}],
u'status': u'CURRENT',
u'max_version': u'1.9',
u'max_version': u'1.10',
u'min_version': u'1.1'}]}
self.v1_expected = {

View File

@ -683,8 +683,7 @@ class TestPost(api_base.FunctionalTest):
bdict['node_count'] = 0
response = self.post_json('/clusters', bdict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['errors'])
self.assertEqual(202, response.status_int)
def test_create_cluster_with_node_count_negative(self):
bdict = apiutils.cluster_post_data()

View File

@ -40,7 +40,7 @@ class TestNodegroupObject(base.TestCase):
del nodegroup_dict['max_node_count']
nodegroup = api_nodegroup.NodeGroup(**nodegroup_dict)
self.assertEqual(1, nodegroup.node_count)
self.assertEqual(1, nodegroup.min_node_count)
self.assertEqual(0, nodegroup.min_node_count)
self.assertIsNone(nodegroup.max_node_count)
@ -293,6 +293,20 @@ class TestPost(NodeGroupControllerTest):
# Verify node_count defaults to 1
self.assertEqual(1, response.json['node_count'])
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_nodegroup_with_zero_nodes(self, mock_utcnow):
ng_dict = apiutils.nodegroup_post_data()
ng_dict['node_count'] = 0
ng_dict['min_node_count'] = 0
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
response = self.post_json(self.url, ng_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(202, response.status_int)
# Verify node_count is set to zero
self.assertEqual(0, response.json['node_count'])
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_nodegroup_with_max_node_count(self, mock_utcnow):
ng_dict = apiutils.nodegroup_post_data(max_node_count=5)
@ -366,7 +380,7 @@ class TestPost(NodeGroupControllerTest):
self.assertEqual(self.cluster.project_id, response.json['project_id'])
self.assertEqual(self.cluster.labels, response.json['labels'])
self.assertEqual('worker', response.json['role'])
self.assertEqual(1, response.json['min_node_count'])
self.assertEqual(0, response.json['min_node_count'])
self.assertEqual(1, response.json['node_count'])
self.assertIsNone(response.json['max_node_count'])
@ -662,7 +676,7 @@ class TestPatch(NodeGroupControllerTest):
response = self.get_json(self.url + self.nodegroup.uuid)
# Removing the min_node_count just restores the default value
self.assertEqual(1, response['min_node_count'])
self.assertEqual(0, response['min_node_count'])
return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at)

View File

@ -364,7 +364,7 @@ object_data = {
'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c',
'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18',
'Federation': '1.0-166da281432b083f0e4b851336e12e20',
'NodeGroup': '1.0-8cb4544a28a49860d816158a7c3060b1'
'NodeGroup': '1.1-70211d19fcf53903a470607f1f4a784f'
}

View File

@ -0,0 +1,7 @@
---
features:
- |
Clusters can now be created with empty nodegroups. Existing nodegroups
can be set to node_count = 0. min_node_count defaults to 0.
This is usefull for HA or special hardware clusters with multiple
nodegroups managed by the cluster auto-scaller.