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:
parent
fd79dd4fa6
commit
f46923cc5e
@ -105,7 +105,7 @@ class Cluster(base.APIBase):
|
|||||||
default=None)
|
default=None)
|
||||||
"""The name of the nova ssh keypair"""
|
"""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"""
|
"""The node count for this cluster. Default to 1 if not set"""
|
||||||
|
|
||||||
master_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
|
master_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
|
||||||
|
@ -44,7 +44,7 @@ class ClusterResizeRequest(base.APIBase):
|
|||||||
This class enforces type checking and value constraints.
|
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."""
|
"""The expected node count after resize."""
|
||||||
|
|
||||||
nodes_to_remove = wsme.wsattr([wtypes.text], mandatory=False,
|
nodes_to_remove = wsme.wsattr([wtypes.text], mandatory=False,
|
||||||
|
@ -98,18 +98,18 @@ class NodeGroup(base.APIBase):
|
|||||||
node_addresses = wsme.wsattr([wtypes.text], readonly=True)
|
node_addresses = wsme.wsattr([wtypes.text], readonly=True)
|
||||||
"""IP addresses of nodegroup nodes"""
|
"""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"""
|
"""The node count for this nodegroup. Default to 1 if not set"""
|
||||||
|
|
||||||
role = wsme.wsattr(wtypes.StringType(min_length=1, max_length=255),
|
role = wsme.wsattr(wtypes.StringType(min_length=1, max_length=255),
|
||||||
default='worker')
|
default='worker')
|
||||||
"""The role of the nodes included in this nodegroup"""
|
"""The role of the nodes included in this nodegroup"""
|
||||||
|
|
||||||
min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
|
min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=0)
|
||||||
"""The minimum allowed nodes for this nodegroup. Default to 1 if not set"""
|
"""The minimum allowed nodes for this nodegroup. Default to 0 if not set"""
|
||||||
|
|
||||||
max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=None)
|
max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=0), default=None)
|
||||||
"""The maximum allowed nodes for this nodegroup. Default to 1 if not set"""
|
"""The maximum allowed nodes for this nodegroup."""
|
||||||
|
|
||||||
is_default = types.BooleanType()
|
is_default = types.BooleanType()
|
||||||
"""Specifies is a nodegroup was created by default or not"""
|
"""Specifies is a nodegroup was created by default or not"""
|
||||||
|
@ -210,11 +210,11 @@ class JsonPatchType(wtypes.Base):
|
|||||||
raise wsme.exc.ClientSideError(msg % patch.path)
|
raise wsme.exc.ClientSideError(msg % patch.path)
|
||||||
|
|
||||||
if patch.op != 'remove':
|
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")
|
msg = _("'add' and 'replace' operations needs value")
|
||||||
raise wsme.exc.ClientSideError(msg)
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
ret = {'path': patch.path, 'op': patch.op}
|
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
|
ret['value'] = patch.value
|
||||||
return ret
|
return ret
|
||||||
|
@ -17,16 +17,6 @@ from webob import exc
|
|||||||
|
|
||||||
from magnum.i18n import _
|
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
|
# For each newly added microversion change, update the API version history
|
||||||
# string below with a one or two line description. Also update
|
# 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.7 - Add resize API
|
||||||
* 1.8 - Add upgrade API
|
* 1.8 - Add upgrade API
|
||||||
* 1.9 - Add nodegroup API
|
* 1.9 - Add nodegroup API
|
||||||
|
* 1.10 - Allow nodegroups with 0 nodes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
BASE_VER = '1.1'
|
BASE_VER = '1.1'
|
||||||
CURRENT_MAX_VER = '1.9'
|
CURRENT_MAX_VER = '1.10'
|
||||||
|
|
||||||
|
|
||||||
class Version(object):
|
class Version(object):
|
||||||
|
@ -75,3 +75,12 @@ user documentation.
|
|||||||
|
|
||||||
An admin user can set/update/delete/list quotas for the given tenant.
|
An admin user can set/update/delete/list quotas for the given tenant.
|
||||||
A non-admin user can get self quota information.
|
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.
|
||||||
|
@ -18,6 +18,7 @@ from pycadf import cadftaxonomy as taxonomy
|
|||||||
from pycadf import cadftype
|
from pycadf import cadftype
|
||||||
from pycadf import eventfactory
|
from pycadf import eventfactory
|
||||||
from pycadf import resource
|
from pycadf import resource
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from magnum.common import clients
|
from magnum.common import clients
|
||||||
from magnum.common import rpc
|
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.image_id = cluster.cluster_template.image_id
|
||||||
ng.docker_volume_size = (cluster.docker_volume_size or
|
ng.docker_volume_size = (cluster.docker_volume_size or
|
||||||
cluster.cluster_template.docker_volume_size)
|
cluster.cluster_template.docker_volume_size)
|
||||||
|
|
||||||
if is_master:
|
if is_master:
|
||||||
ng.flavor_id = (cluster.master_flavor_id or
|
ng.flavor_id = (cluster.master_flavor_id or
|
||||||
cluster.cluster_template.master_flavor_id)
|
cluster.cluster_template.master_flavor_id)
|
||||||
@ -183,6 +185,11 @@ def _get_nodegroup_object(context, cluster, node_count, is_master=False):
|
|||||||
else:
|
else:
|
||||||
ng.flavor_id = cluster.flavor_id or cluster.cluster_template.flavor_id
|
ng.flavor_id = cluster.flavor_id or cluster.cluster_template.flavor_id
|
||||||
ng.role = "worker"
|
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.name = "default-%s" % ng.role
|
||||||
ng.is_default = True
|
ng.is_default = True
|
||||||
ng.status = fields.ClusterStatus.CREATE_IN_PROGRESS
|
ng.status = fields.ClusterStatus.CREATE_IN_PROGRESS
|
||||||
|
@ -842,7 +842,7 @@ parameters:
|
|||||||
type: number
|
type: number
|
||||||
description: >
|
description: >
|
||||||
minimum node count of cluster workers when doing scale down
|
minimum node count of cluster workers when doing scale down
|
||||||
default: 1
|
default: 0
|
||||||
|
|
||||||
max_node_count:
|
max_node_count:
|
||||||
type: number
|
type: number
|
||||||
|
@ -858,7 +858,7 @@ parameters:
|
|||||||
type: number
|
type: number
|
||||||
description: >
|
description: >
|
||||||
minimum node count of cluster workers when doing scale down
|
minimum node count of cluster workers when doing scale down
|
||||||
default: 1
|
default: 0
|
||||||
|
|
||||||
max_node_count:
|
max_node_count:
|
||||||
type: number
|
type: number
|
||||||
|
@ -26,8 +26,9 @@ from magnum.objects import fields as m_fields
|
|||||||
class NodeGroup(base.MagnumPersistentObject, base.MagnumObject,
|
class NodeGroup(base.MagnumPersistentObject, base.MagnumObject,
|
||||||
base.MagnumObjectDictCompat):
|
base.MagnumObjectDictCompat):
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
|
# Version 1.1: min_node_count defaults to 0
|
||||||
|
|
||||||
VERSION = '1.0'
|
VERSION = '1.1'
|
||||||
|
|
||||||
dbapi = dbapi.get_instance()
|
dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ class NodeGroup(base.MagnumPersistentObject, base.MagnumObject,
|
|||||||
'node_count': fields.IntegerField(nullable=False, default=1),
|
'node_count': fields.IntegerField(nullable=False, default=1),
|
||||||
'role': fields.StringField(),
|
'role': fields.StringField(),
|
||||||
'max_node_count': fields.IntegerField(nullable=True),
|
'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),
|
'is_default': fields.BooleanField(default=False),
|
||||||
'stack_id': fields.StringField(nullable=True),
|
'stack_id': fields.StringField(nullable=True),
|
||||||
'status': m_fields.ClusterStatusField(nullable=True),
|
'status': m_fields.ClusterStatusField(nullable=True),
|
||||||
|
@ -41,7 +41,7 @@ class TestRootController(api_base.FunctionalTest):
|
|||||||
[{u'href': u'http://localhost/v1/',
|
[{u'href': u'http://localhost/v1/',
|
||||||
u'rel': u'self'}],
|
u'rel': u'self'}],
|
||||||
u'status': u'CURRENT',
|
u'status': u'CURRENT',
|
||||||
u'max_version': u'1.9',
|
u'max_version': u'1.10',
|
||||||
u'min_version': u'1.1'}]}
|
u'min_version': u'1.1'}]}
|
||||||
|
|
||||||
self.v1_expected = {
|
self.v1_expected = {
|
||||||
|
@ -683,8 +683,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
bdict['node_count'] = 0
|
bdict['node_count'] = 0
|
||||||
response = self.post_json('/clusters', bdict, expect_errors=True)
|
response = self.post_json('/clusters', bdict, expect_errors=True)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(400, response.status_int)
|
self.assertEqual(202, response.status_int)
|
||||||
self.assertTrue(response.json['errors'])
|
|
||||||
|
|
||||||
def test_create_cluster_with_node_count_negative(self):
|
def test_create_cluster_with_node_count_negative(self):
|
||||||
bdict = apiutils.cluster_post_data()
|
bdict = apiutils.cluster_post_data()
|
||||||
|
@ -40,7 +40,7 @@ class TestNodegroupObject(base.TestCase):
|
|||||||
del nodegroup_dict['max_node_count']
|
del nodegroup_dict['max_node_count']
|
||||||
nodegroup = api_nodegroup.NodeGroup(**nodegroup_dict)
|
nodegroup = api_nodegroup.NodeGroup(**nodegroup_dict)
|
||||||
self.assertEqual(1, nodegroup.node_count)
|
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)
|
self.assertIsNone(nodegroup.max_node_count)
|
||||||
|
|
||||||
|
|
||||||
@ -293,6 +293,20 @@ class TestPost(NodeGroupControllerTest):
|
|||||||
# Verify node_count defaults to 1
|
# Verify node_count defaults to 1
|
||||||
self.assertEqual(1, response.json['node_count'])
|
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')
|
@mock.patch('oslo_utils.timeutils.utcnow')
|
||||||
def test_create_nodegroup_with_max_node_count(self, mock_utcnow):
|
def test_create_nodegroup_with_max_node_count(self, mock_utcnow):
|
||||||
ng_dict = apiutils.nodegroup_post_data(max_node_count=5)
|
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.project_id, response.json['project_id'])
|
||||||
self.assertEqual(self.cluster.labels, response.json['labels'])
|
self.assertEqual(self.cluster.labels, response.json['labels'])
|
||||||
self.assertEqual('worker', response.json['role'])
|
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.assertEqual(1, response.json['node_count'])
|
||||||
self.assertIsNone(response.json['max_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)
|
response = self.get_json(self.url + self.nodegroup.uuid)
|
||||||
# Removing the min_node_count just restores the default value
|
# 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(
|
return_updated_at = timeutils.parse_isotime(
|
||||||
response['updated_at']).replace(tzinfo=None)
|
response['updated_at']).replace(tzinfo=None)
|
||||||
self.assertEqual(test_time, return_updated_at)
|
self.assertEqual(test_time, return_updated_at)
|
||||||
|
@ -364,7 +364,7 @@ object_data = {
|
|||||||
'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c',
|
'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c',
|
||||||
'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18',
|
'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18',
|
||||||
'Federation': '1.0-166da281432b083f0e4b851336e12e20',
|
'Federation': '1.0-166da281432b083f0e4b851336e12e20',
|
||||||
'NodeGroup': '1.0-8cb4544a28a49860d816158a7c3060b1'
|
'NodeGroup': '1.1-70211d19fcf53903a470607f1f4a784f'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user