Allow flavor_id on cluster create

Add flavor_id as an option during cluster create. If not given,
the default is taken from the cluster template.

Add flavor_id in the Cluster object and use that instead
of the one from ClusterTemplate.

Update both magnum and magnum cli documentation to reflect the above changes.

Partial-Bug: #1699245
Change-Id: Ib60c05cce1cf2639ca4740abdd264403033433f9
This commit is contained in:
Ricardo Rocha 2017-07-31 13:08:56 +00:00
parent 28fff8006a
commit 84006f63d7
20 changed files with 101 additions and 27 deletions

View File

@ -41,6 +41,7 @@ Request
- keypair: keypair_id - keypair: keypair_id
- master_flavor_id: master_flavor_id - master_flavor_id: master_flavor_id
- labels: labels - labels: labels
- flavor_id: flavor_id
.. note:: .. note::

View File

@ -8,5 +8,6 @@
"keypair":"my_keypair", "keypair":"my_keypair",
"master_flavor_id":null, "master_flavor_id":null,
"labels":{ "labels":{
} },
"flavor_id":null
} }

View File

@ -200,7 +200,7 @@ They are loosely grouped as: mandatory, infrastructure, COE specific.
--flavor \<flavor\> --flavor \<flavor\>
The nova flavor id for booting the node servers. The default The nova flavor id for booting the node servers. The default
is 'm1.small'. is 'm1.small'. This value can be overridden at cluster creation.
--master-flavor \<master-flavor\> --master-flavor \<master-flavor\>
The nova flavor id for booting the master or manager servers. The The nova flavor id for booting the master or manager servers. The

View File

@ -188,7 +188,12 @@ def validate_os_resources(context, cluster_template, cluster=None):
cli = clients.OpenStackClients(context) cli = clients.OpenStackClients(context)
for attr, validate_method in validators.items(): for attr, validate_method in validators.items():
if attr in cluster_template and cluster_template[attr] is not None: if cluster and attr in cluster and cluster[attr]:
if attr != 'labels':
validate_method(cli, cluster[attr])
else:
validate_method(cluster[attr])
elif attr in cluster_template and cluster_template[attr] is not None:
if attr != 'labels': if attr != 'labels':
validate_method(cli, cluster_template[attr]) validate_method(cli, cluster_template[attr])
else: else:

View File

@ -98,6 +98,9 @@ class Bay(base.APIBase):
master_flavor_id = wtypes.StringType(min_length=1, max_length=255) master_flavor_id = wtypes.StringType(min_length=1, max_length=255)
"""The master flavor of this Bay""" """The master flavor of this Bay"""
flavor_id = wtypes.StringType(min_length=1, max_length=255)
"""The flavor of this Bay"""
bay_create_timeout = wsme.wsattr(wtypes.IntegerType(minimum=0), default=60) bay_create_timeout = wsme.wsattr(wtypes.IntegerType(minimum=0), default=60)
"""Timeout for creating the bay in minutes. Default to 60 if not set""" """Timeout for creating the bay in minutes. Default to 60 if not set"""
@ -181,7 +184,7 @@ class Bay(base.APIBase):
if not expand: if not expand:
bay.unset_fields_except(['uuid', 'name', 'baymodel_id', bay.unset_fields_except(['uuid', 'name', 'baymodel_id',
'docker_volume_size', 'labels', 'docker_volume_size', 'labels',
'master_flavor_id', 'master_flavor_id', 'flavor_id',
'node_count', 'status', 'node_count', 'status',
'bay_create_timeout', 'master_count', 'bay_create_timeout', 'master_count',
'stack_id']) 'stack_id'])
@ -208,6 +211,7 @@ class Bay(base.APIBase):
docker_volume_size=1, docker_volume_size=1,
labels={}, labels={},
master_flavor_id=None, master_flavor_id=None,
flavor_id=None,
bay_create_timeout=15, bay_create_timeout=15,
stack_id='49dc23f5-ffc9-40c3-9d34-7be7f9e34d63', stack_id='49dc23f5-ffc9-40c3-9d34-7be7f9e34d63',
status=fields.ClusterStatus.CREATE_COMPLETE, status=fields.ClusterStatus.CREATE_COMPLETE,
@ -441,6 +445,10 @@ class BaysController(base.Controller):
if bay.master_flavor_id == wtypes.Unset or not bay.master_flavor_id: if bay.master_flavor_id == wtypes.Unset or not bay.master_flavor_id:
bay.master_flavor_id = baymodel.master_flavor_id bay.master_flavor_id = baymodel.master_flavor_id
# If flavor_id is not present, use baymodel value
if bay.flavor_id == wtypes.Unset or not bay.flavor_id:
bay.flavor_id = baymodel.flavor_id
bay_dict = bay.as_dict() bay_dict = bay.as_dict()
bay_dict['keypair'] = baymodel.keypair_id bay_dict['keypair'] = baymodel.keypair_id
attr_validator.validate_os_resources(context, baymodel.as_dict(), attr_validator.validate_os_resources(context, baymodel.as_dict(),

View File

@ -116,6 +116,9 @@ class Cluster(base.APIBase):
master_flavor_id = wtypes.StringType(min_length=1, max_length=255) master_flavor_id = wtypes.StringType(min_length=1, max_length=255)
"""The flavor of the master node for this Cluster""" """The flavor of the master node for this Cluster"""
flavor_id = wtypes.StringType(min_length=1, max_length=255)
"""The flavor of this Cluster"""
create_timeout = wsme.wsattr(wtypes.IntegerType(minimum=0), default=60) create_timeout = wsme.wsattr(wtypes.IntegerType(minimum=0), default=60)
"""Timeout for creating the cluster in minutes. Default to 60 if not set""" """Timeout for creating the cluster in minutes. Default to 60 if not set"""
@ -169,7 +172,7 @@ class Cluster(base.APIBase):
cluster.unset_fields_except(['uuid', 'name', 'cluster_template_id', cluster.unset_fields_except(['uuid', 'name', 'cluster_template_id',
'keypair', 'docker_volume_size', 'keypair', 'docker_volume_size',
'labels', 'node_count', 'status', 'labels', 'node_count', 'status',
'master_flavor_id', 'master_flavor_id', 'flavor_id',
'create_timeout', 'master_count', 'create_timeout', 'master_count',
'stack_id']) 'stack_id'])
@ -197,6 +200,7 @@ class Cluster(base.APIBase):
docker_volume_size=1, docker_volume_size=1,
labels={}, labels={},
master_flavor_id='m1.small', master_flavor_id='m1.small',
flavor_id='m1.small',
create_timeout=15, create_timeout=15,
stack_id='49dc23f5-ffc9-40c3-9d34-7be7f9e34d63', stack_id='49dc23f5-ffc9-40c3-9d34-7be7f9e34d63',
status=fields.ClusterStatus.CREATE_COMPLETE, status=fields.ClusterStatus.CREATE_COMPLETE,
@ -421,6 +425,10 @@ class ClustersController(base.Controller):
not cluster.master_flavor_id): not cluster.master_flavor_id):
cluster.master_flavor_id = cluster_template.master_flavor_id cluster.master_flavor_id = cluster_template.master_flavor_id
# If flavor_id is not present, use cluster_template value
if cluster.flavor_id == wtypes.Unset or not cluster.flavor_id:
cluster.flavor_id = cluster_template.flavor_id
cluster_dict = cluster.as_dict() cluster_dict = cluster.as_dict()
attr_validator.validate_os_resources(context, attr_validator.validate_os_resources(context,

View File

@ -0,0 +1,30 @@
# 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.
"""add flavor_id to cluster
Revision ID: 041d9a0f1159
Revises: 04c625aa95ba
Create Date: 2017-07-31 12:46:00.777841
"""
# revision identifiers, used by Alembic.
revision = '041d9a0f1159'
down_revision = '04c625aa95ba'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('cluster', sa.Column('flavor_id',
sa.String(length=255), nullable=True))

View File

@ -118,6 +118,7 @@ class Cluster(Base):
docker_volume_size = Column(Integer()) docker_volume_size = Column(Integer())
labels = Column(JSONEncodedDict) labels = Column(JSONEncodedDict)
master_flavor_id = Column(String(255)) master_flavor_id = Column(String(255))
flavor_id = Column(String(255))
stack_id = Column(String(255)) stack_id = Column(String(255))
api_address = Column(String(255)) api_address = Column(String(255))
node_addresses = Column(JSONEncodedList) node_addresses = Column(JSONEncodedList)

View File

@ -54,7 +54,7 @@ class K8sTemplateDefinition(template_def.BaseTemplateDefinition):
self.add_parameter('master_flavor', self.add_parameter('master_flavor',
cluster_attr='master_flavor_id') cluster_attr='master_flavor_id')
self.add_parameter('minion_flavor', self.add_parameter('minion_flavor',
cluster_template_attr='flavor_id') cluster_attr='flavor_id')
self.add_parameter('number_of_minions', self.add_parameter('number_of_minions',
cluster_attr='node_count') cluster_attr='node_count')
self.add_parameter('external_network', self.add_parameter('external_network',

View File

@ -50,7 +50,7 @@ class SwarmFedoraTemplateDefinition(template_def.BaseTemplateDefinition):
self.add_parameter('master_flavor', self.add_parameter('master_flavor',
cluster_attr='master_flavor_id') cluster_attr='master_flavor_id')
self.add_parameter('node_flavor', self.add_parameter('node_flavor',
cluster_template_attr='flavor_id') cluster_attr='flavor_id')
self.add_parameter('docker_volume_size', self.add_parameter('docker_volume_size',
cluster_attr='docker_volume_size') cluster_attr='docker_volume_size')
self.add_parameter('volume_driver', self.add_parameter('volume_driver',

View File

@ -63,7 +63,7 @@ class SwarmModeTemplateDefinition(template_def.BaseTemplateDefinition):
self.add_parameter('master_flavor', self.add_parameter('master_flavor',
cluster_attr='master_flavor_id') cluster_attr='master_flavor_id')
self.add_parameter('node_flavor', self.add_parameter('node_flavor',
cluster_template_attr='flavor_id') cluster_attr='flavor_id')
self.add_parameter('docker_volume_size', self.add_parameter('docker_volume_size',
cluster_attr='docker_volume_size') cluster_attr='docker_volume_size')
self.add_parameter('volume_driver', self.add_parameter('volume_driver',

View File

@ -33,7 +33,7 @@ class UbuntuMesosTemplateDefinition(template_def.BaseTemplateDefinition):
self.add_parameter('master_flavor', self.add_parameter('master_flavor',
cluster_attr='master_flavor_id') cluster_attr='master_flavor_id')
self.add_parameter('slave_flavor', self.add_parameter('slave_flavor',
cluster_template_attr='flavor_id') cluster_attr='flavor_id')
self.add_parameter('cluster_name', self.add_parameter('cluster_name',
cluster_attr='name') cluster_attr='name')
self.add_parameter('volume_driver', self.add_parameter('volume_driver',

View File

@ -45,8 +45,9 @@ class Cluster(base.MagnumPersistentObject, base.MagnumObject,
# Version 1.14: Added 'docker_volume_size' field # Version 1.14: Added 'docker_volume_size' field
# Version 1.15: Added 'labels' field # Version 1.15: Added 'labels' field
# Version 1.16: Added 'master_flavor_id' field # Version 1.16: Added 'master_flavor_id' field
# Version 1.17: Added 'flavor_id' field
VERSION = '1.16' VERSION = '1.17'
dbapi = dbapi.get_instance() dbapi = dbapi.get_instance()
@ -61,6 +62,7 @@ class Cluster(base.MagnumPersistentObject, base.MagnumObject,
'docker_volume_size': fields.IntegerField(nullable=True), 'docker_volume_size': fields.IntegerField(nullable=True),
'labels': fields.DictOfStringsField(nullable=True), 'labels': fields.DictOfStringsField(nullable=True),
'master_flavor_id': fields.StringField(nullable=True), 'master_flavor_id': fields.StringField(nullable=True),
'flavor_id': fields.StringField(nullable=True),
'stack_id': fields.StringField(nullable=True), 'stack_id': fields.StringField(nullable=True),
'status': m_fields.ClusterStatusField(nullable=True), 'status': m_fields.ClusterStatusField(nullable=True),
'status_reason': fields.StringField(nullable=True), 'status_reason': fields.StringField(nullable=True),

View File

@ -856,6 +856,24 @@ class TestPost(api_base.FunctionalTest):
# Verify master_flavor_id from ClusterTemplate is used # Verify master_flavor_id from ClusterTemplate is used
self.assertEqual('m1.small', cluster[0].master_flavor_id) self.assertEqual('m1.small', cluster[0].master_flavor_id)
def test_create_cluster_with_flavor_id(self):
bdict = apiutils.cluster_post_data()
bdict['flavor_id'] = 'm2.small'
response = self.post_json('/clusters', bdict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(202, response.status_int)
cluster, timeout = self.mock_cluster_create.call_args
self.assertEqual('m2.small', cluster[0].flavor_id)
def test_create_cluster_without_flavor_id(self):
bdict = apiutils.cluster_post_data()
response = self.post_json('/clusters', bdict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(202, response.status_int)
cluster, timeout = self.mock_cluster_create.call_args
# Verify flavor_id from ClusterTemplate is used
self.assertEqual('m1.small', cluster[0].flavor_id)
class TestDelete(api_base.FunctionalTest): class TestDelete(api_base.FunctionalTest):
def setUp(self): def setUp(self):

View File

@ -315,11 +315,18 @@ class TestAttrValidator(base.BaseTestCase):
@mock.patch('magnum.common.clients.OpenStackClients') @mock.patch('magnum.common.clients.OpenStackClients')
def test_validate_os_resources_with_cluster(self, mock_os_cli): def test_validate_os_resources_with_cluster(self, mock_os_cli):
mock_cluster_template = {} mock_cluster_template = {}
mock_cluster = {'keypair': 'test-keypair'} mock_cluster = {
'keypair': 'test-keypair', 'labels': {'lab1': 'val1'},
'image_id': 'e33f0988-1730-405e-8401-30cbc8535302'
}
mock_keypair = mock.MagicMock() mock_keypair = mock.MagicMock()
mock_keypair.id = 'test-keypair' mock_keypair.id = 'test-keypair'
mock_image = {'name': 'fedora-21-atomic-5',
'id': 'e33f0988-1730-405e-8401-30cbc8535302',
'os_distro': 'fedora-atomic'}
mock_nova = mock.MagicMock() mock_nova = mock.MagicMock()
mock_nova.keypairs.get.return_value = mock_keypair mock_nova.keypairs.get.return_value = mock_keypair
mock_nova.images.get.return_value = mock_image
mock_os_cli = mock.MagicMock() mock_os_cli = mock.MagicMock()
mock_os_cli.nova.return_value = mock_nova mock_os_cli.nova.return_value = mock_nova
mock_context = mock.MagicMock() mock_context = mock.MagicMock()

View File

@ -77,6 +77,7 @@ class TestClusterConductorWithK8s(base.TestCase):
'master_count': 1, 'master_count': 1,
'discovery_url': 'https://discovery.etcd.io/test', 'discovery_url': 'https://discovery.etcd.io/test',
'docker_volume_size': 20, 'docker_volume_size': 20,
'flavor_id': 'flavor_id',
'master_addresses': ['172.17.2.18'], 'master_addresses': ['172.17.2.18'],
'ca_cert_ref': 'http://barbican/v1/containers/xx-xx-xx-xx', 'ca_cert_ref': 'http://barbican/v1/containers/xx-xx-xx-xx',
'magnum_cert_ref': 'http://barbican/v1/containers/xx-xx-xx-xx', 'magnum_cert_ref': 'http://barbican/v1/containers/xx-xx-xx-xx',
@ -96,6 +97,7 @@ class TestClusterConductorWithK8s(base.TestCase):
'kube_dashboard_enabled': 'True', 'kube_dashboard_enabled': 'True',
'docker_volume_type': 'lvmdriver-1'}, 'docker_volume_type': 'lvmdriver-1'},
'master_flavor_id': 'master_flavor_id', 'master_flavor_id': 'master_flavor_id',
'flavor_id': 'flavor_id',
} }
self.context.user_name = 'fake_user' self.context.user_name = 'fake_user'
self.context.tenant = 'fake_tenant' self.context.tenant = 'fake_tenant'
@ -374,6 +376,7 @@ class TestClusterConductorWithK8s(base.TestCase):
'discovery_url': 'https://discovery.etcd.io/test', 'discovery_url': 'https://discovery.etcd.io/test',
'docker_volume_size': 20, 'docker_volume_size': 20,
'master_flavor': 'master_flavor_id', 'master_flavor': 'master_flavor_id',
'minion_flavor': 'flavor_id',
'external_network': 'external_network_id', 'external_network': 'external_network_id',
'flannel_backend': 'vxlan', 'flannel_backend': 'vxlan',
'flannel_network_cidr': '10.101.0.0/16', 'flannel_network_cidr': '10.101.0.0/16',
@ -589,21 +592,6 @@ class TestClusterConductorWithK8s(base.TestCase):
mock_get, mock_get,
missing_attr='image_id') missing_attr='image_id')
@patch('requests.get')
@patch('magnum.objects.ClusterTemplate.get_by_uuid')
@patch('magnum.drivers.common.driver.Driver.get_driver')
def test_extract_template_definition_without_minion_flavor(
self,
mock_driver,
mock_objects_cluster_template_get_by_uuid,
mock_get):
mock_driver.return_value = k8s_dr.Driver()
self._test_extract_template_definition(
mock_driver,
mock_objects_cluster_template_get_by_uuid,
mock_get,
missing_attr='flavor_id')
@patch('requests.get') @patch('requests.get')
@patch('magnum.objects.ClusterTemplate.get_by_uuid') @patch('magnum.objects.ClusterTemplate.get_by_uuid')
@patch('magnum.drivers.common.driver.Driver.get_driver') @patch('magnum.drivers.common.driver.Driver.get_driver')

View File

@ -57,6 +57,7 @@ class TestClusterConductorWithMesos(base.TestCase):
'cluster_template_id': 'xx-xx-xx-xx', 'cluster_template_id': 'xx-xx-xx-xx',
'keypair': 'keypair_id', 'keypair': 'keypair_id',
'master_flavor_id': 'master_flavor_id', 'master_flavor_id': 'master_flavor_id',
'flavor_id': 'flavor_id',
'name': 'cluster1', 'name': 'cluster1',
'stack_id': 'xx-xx-xx-xx', 'stack_id': 'xx-xx-xx-xx',
'api_address': '172.17.2.3', 'api_address': '172.17.2.3',
@ -194,6 +195,7 @@ class TestClusterConductorWithMesos(base.TestCase):
'mesos_slave_image_providers': 'docker', 'mesos_slave_image_providers': 'docker',
'master_flavor': 'master_flavor_id', 'master_flavor': 'master_flavor_id',
'verify_ca': True, 'verify_ca': True,
'slave_flavor': 'flavor_id',
} }
self.assertEqual(expected, definition) self.assertEqual(expected, definition)
self.assertEqual( self.assertEqual(

View File

@ -62,6 +62,7 @@ class TestClusterConductorWithSwarm(base.TestCase):
'uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a52', 'uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a52',
'cluster_template_id': 'xx-xx-xx-xx', 'cluster_template_id': 'xx-xx-xx-xx',
'keypair': 'keypair_id', 'keypair': 'keypair_id',
'flavor_id': 'flavor_id',
'docker_volume_size': 20, 'docker_volume_size': 20,
'master_flavor_id': 'master_flavor_id', 'master_flavor_id': 'master_flavor_id',
'name': 'cluster1', 'name': 'cluster1',
@ -309,6 +310,7 @@ class TestClusterConductorWithSwarm(base.TestCase):
'docker_volume_size': 20, 'docker_volume_size': 20,
'master_flavor': 'master_flavor_id', 'master_flavor': 'master_flavor_id',
'verify_ca': True, 'verify_ca': True,
'node_flavor': 'flavor_id',
} }
self.assertEqual(expected, definition) self.assertEqual(expected, definition)
self.assertEqual( self.assertEqual(

View File

@ -99,6 +99,7 @@ def get_test_cluster(**kw):
'docker_volume_size': kw.get('docker_volume_size'), 'docker_volume_size': kw.get('docker_volume_size'),
'labels': kw.get('labels'), 'labels': kw.get('labels'),
'master_flavor_id': kw.get('master_flavor_id', None), 'master_flavor_id': kw.get('master_flavor_id', None),
'flavor_id': kw.get('flavor_id', None),
} }
# Only add Keystone trusts related attributes on demand since they may # Only add Keystone trusts related attributes on demand since they may

View File

@ -355,7 +355,7 @@ class TestObject(test_base.TestCase, _TestObject):
# For more information on object version testing, read # For more information on object version testing, read
# http://docs.openstack.org/developer/magnum/objects.html # http://docs.openstack.org/developer/magnum/objects.html
object_data = { object_data = {
'Cluster': '1.16-7a544c5059697c464810470980f81ba1', 'Cluster': '1.17-c32c07425ab0042c7370bef2902b4d21',
'ClusterTemplate': '1.18-7fa94f4fdd027acfb4f022f202afdfb5', 'ClusterTemplate': '1.18-7fa94f4fdd027acfb4f022f202afdfb5',
'Certificate': '1.1-1924dc077daa844f0f9076332ef96815', 'Certificate': '1.1-1924dc077daa844f0f9076332ef96815',
'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd', 'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',