From aecfc71fdd88d90e433c203b2a34d0aba0650a5f Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Thu, 4 May 2017 17:27:12 +0800 Subject: [PATCH] New flavor This adds cpus, memory, nics, and disks properties to flavors. Change-Id: I8e7629c857dbb5164d596bf8e72f21e9d4bfcaba --- mogan/api/controllers/v1/flavors.py | 12 +++ mogan/api/controllers/v1/schemas/flavor.py | 4 + mogan/api/validation/parameter_types.py | 64 +++++++++++++ .../91941bf1ebc9_initial_migration.py | 56 +++++++++++ mogan/db/sqlalchemy/api.py | 94 ++++++++++++++++++- mogan/db/sqlalchemy/models.py | 68 ++++++++++++++ mogan/objects/flavor.py | 17 +++- mogan/tests/unit/db/utils.py | 7 ++ mogan/tests/unit/objects/test_flavor.py | 4 + mogan/tests/unit/objects/test_objects.py | 2 +- 10 files changed, 320 insertions(+), 8 deletions(-) diff --git a/mogan/api/controllers/v1/flavors.py b/mogan/api/controllers/v1/flavors.py index cb011707..b955c62f 100644 --- a/mogan/api/controllers/v1/flavors.py +++ b/mogan/api/controllers/v1/flavors.py @@ -56,6 +56,18 @@ class Flavor(base.APIBase): description = wtypes.text """The description of the flavor""" + cpus = {wtypes.text: types.jsontype} + """The cpus of the flavor""" + + memory = {wtypes.text: types.jsontype} + """The memory of the flavor""" + + nics = wtypes.ArrayType(types.jsontype) + """The nics of the flavor""" + + disks = wtypes.ArrayType(types.jsontype) + """The disks of the flavor""" + is_public = types.boolean """Indicates whether the flavor is public.""" diff --git a/mogan/api/controllers/v1/schemas/flavor.py b/mogan/api/controllers/v1/schemas/flavor.py index 66d8defe..a2665b9a 100644 --- a/mogan/api/controllers/v1/schemas/flavor.py +++ b/mogan/api/controllers/v1/schemas/flavor.py @@ -23,6 +23,10 @@ create_flavor = { 'name': parameter_types.name, 'description': parameter_types.description, 'is_public': parameter_types.boolean, + 'cpus': parameter_types.flavor_cpus, + 'memory': parameter_types.flavor_memory, + 'nics': parameter_types.flavor_nics, + 'disks': parameter_types.flavor_disks, }, 'required': ['name', 'description'], 'additionalProperties': False, diff --git a/mogan/api/validation/parameter_types.py b/mogan/api/validation/parameter_types.py index 9e5eb5c8..0839b478 100644 --- a/mogan/api/validation/parameter_types.py +++ b/mogan/api/validation/parameter_types.py @@ -106,3 +106,67 @@ boolean = { False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off', 'off', 'NO', 'No', 'no'], } + + +positive_integer = { + 'type': ['integer', 'string'], + 'pattern': '^[0-9]*$', 'minimum': 1 +} + + +flavor_cpus = { + 'type': 'object', + 'patternProperties': { + 'model': { + 'type': 'string', 'maxLength': 255 + }, + 'cores': positive_integer, + }, + 'required': ['model', 'cores'], + 'additionalProperties': False +} + + +flavor_memory = { + 'type': 'object', + 'patternProperties': { + 'type': { + 'type': 'string', 'maxLength': 255 + }, + 'size_mb': positive_integer, + }, + 'required': ['type', 'size_mb'], + 'additionalProperties': False +} + + +flavor_nics = { + 'type': 'array', + 'items': { + 'type': 'object', + 'patternProperties': { + 'type': { + 'type': 'string', 'maxLength': 255 + }, + 'speed': { + 'type': 'string', 'maxLength': 255 + }, + }, + 'additionalProperties': False, + }, +} + + +flavor_disks = { + 'type': 'array', + 'items': { + 'type': 'object', + 'patternProperties': { + 'type': { + 'type': 'string', 'maxLength': 255 + }, + 'size_gb': positive_integer, + }, + 'additionalProperties': False, + }, +} diff --git a/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py b/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py index 6ac55d43..e18eac77 100644 --- a/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py +++ b/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py @@ -67,6 +67,62 @@ def upgrade(): mysql_ENGINE='InnoDB', mysql_DEFAULT_CHARSET='UTF8' ) + op.create_table( + 'flavor_cpus', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('model', sa.String(length=255), nullable=True), + sa.Column('cores', sa.Integer(), nullable=False), + sa.Column('flavor_uuid', sa.String(length=36), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['flavor_uuid'], + ['flavors.uuid']), + mysql_ENGINE='InnoDB', + mysql_DEFAULT_CHARSET='UTF8' + ) + op.create_table( + 'flavor_memory', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=255), nullable=True), + sa.Column('size_mb', sa.Integer(), nullable=False), + sa.Column('flavor_uuid', sa.String(length=36), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['flavor_uuid'], + ['flavors.uuid']), + mysql_ENGINE='InnoDB', + mysql_DEFAULT_CHARSET='UTF8' + ) + op.create_table( + 'flavor_disks', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=255), nullable=True), + sa.Column('size_gb', sa.Integer(), nullable=False), + sa.Column('flavor_uuid', sa.String(length=36), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['flavor_uuid'], + ['flavors.uuid']), + mysql_ENGINE='InnoDB', + mysql_DEFAULT_CHARSET='UTF8' + ) + op.create_table( + 'flavor_nics', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=255), nullable=True), + sa.Column('speed', sa.String(length=255), nullable=False), + sa.Column('flavor_uuid', sa.String(length=36), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['flavor_uuid'], + ['flavors.uuid']), + mysql_ENGINE='InnoDB', + mysql_DEFAULT_CHARSET='UTF8' + ) op.create_table( 'servers', sa.Column('created_at', sa.DateTime(), nullable=True), diff --git a/mogan/db/sqlalchemy/api.py b/mogan/db/sqlalchemy/api.py index c5fdd188..a55b2e06 100644 --- a/mogan/db/sqlalchemy/api.py +++ b/mogan/db/sqlalchemy/api.py @@ -96,12 +96,41 @@ def add_identity_filter(query, value): raise exception.InvalidParameterValue(identity=value) -def _dict_with_extra_specs(flavor_query): +def _dict_with_extra_fields(flavor_query): """Takes a server type query and returns it as a dictionary.""" flavor_dict = dict(flavor_query) + + # extra specs extra_specs = {x['key']: x['value'] for x in flavor_query['extra_specs']} flavor_dict['extra_specs'] = extra_specs + + # cpus + cpus = {} + for c in flavor_query['cpus']: + cpus = {'model': c['model'], 'cores': c['cores']} + flavor_dict['cpus'] = cpus + + # memory + memory = {} + for m in flavor_query['memory']: + memory = {'type': m['type'], 'size_mb': m['size_mb']} + flavor_dict['memory'] = memory + + # nics + nics = [] + for n in flavor_query['nics']: + nic = {'type': n['type'], 'speed': n['speed']} + nics.append(nic) + flavor_dict['nics'] = nics + + # disks + disks = [] + for d in flavor_query['disks']: + disk = {'type': d['type'], 'size_gb': d['size_gb']} + disks.append(disk) + flavor_dict['disks'] = disks + return flavor_dict @@ -119,16 +148,45 @@ class Connection(api.Connection): if not values.get('description'): values['description'] = "" + cpus = values.pop('cpus', None) + memory = values.pop('memory', None) + nics = values.pop('nics', []) + disks = values.pop('disks', []) + flavor = models.Flavors() flavor.update(values) with _session_for_write() as session: try: session.add(flavor) + # add flavor cpus + if cpus: + flavor_cpus = models.FlavorCpus() + cpus['flavor_uuid'] = values['uuid'] + flavor_cpus.update(cpus) + session.add(flavor_cpus) + # add flavor memory + if memory: + flavor_mem = models.FlavorMemory() + memory['flavor_uuid'] = values['uuid'] + flavor_mem.update(memory) + session.add(flavor_mem) + # add flavor nics + for nic in nics: + flavor_nic = models.FlavorNics() + nic['flavor_uuid'] = values['uuid'] + flavor_nic.update(nic) + session.add(flavor_nic) + # add flavor disks + for disk in disks: + flavor_disk = models.FlavorDisks() + disk['flavor_uuid'] = values['uuid'] + flavor_disk.update(disk) + session.add(flavor_disk) session.flush() except db_exc.DBDuplicateEntry: raise exception.FlavorAlreadyExists(uuid=values['uuid']) - return _dict_with_extra_specs(flavor) + return _dict_with_extra_fields(flavor) def flavor_get(self, context, flavor_uuid): query = model_query(context, models.Flavors).filter_by( @@ -142,7 +200,7 @@ class Connection(api.Connection): query = query.filter(or_(*the_filter)) try: - return _dict_with_extra_specs(query.one()) + return _dict_with_extra_fields(query.one()) except NoResultFound: raise exception.FlavorNotFound( type_id=flavor_uuid) @@ -169,7 +227,7 @@ class Connection(api.Connection): ]) query = query.filter(or_(*the_filter)) - return [_dict_with_extra_specs(i) for i in query.all()] + return [_dict_with_extra_fields(i) for i in query.all()] def flavor_destroy(self, context, flavor_uuid): with _session_for_write(): @@ -181,6 +239,34 @@ class Connection(api.Connection): flavor_uuid=type_id) extra_query.delete() + # Clean up cpus related to this flavor + cpus_query = model_query( + context, + models.FlavorCpus).filter_by( + flavor_uuid=type_id) + cpus_query.delete() + + # Clean up memory related to this flavor + memory_query = model_query( + context, + models.FlavorMemory).filter_by( + flavor_uuid=type_id) + memory_query.delete() + + # Clean up nics related to this flavor + nics_query = model_query( + context, + models.FlavorNics).filter_by( + flavor_uuid=type_id) + nics_query.delete() + + # Clean up disks related to this flavor + disks_query = model_query( + context, + models.FlavorDisks).filter_by( + flavor_uuid=type_id) + disks_query.delete() + # Clean up all access related to this flavor project_query = model_query( context, diff --git a/mogan/db/sqlalchemy/models.py b/mogan/db/sqlalchemy/models.py index dfe70a38..7e19200e 100644 --- a/mogan/db/sqlalchemy/models.py +++ b/mogan/db/sqlalchemy/models.py @@ -231,6 +231,74 @@ class FlavorExtraSpecs(Base): '== Flavors.uuid') +class FlavorCpus(Base): + """Represents the flavor cpus.""" + + __tablename__ = 'flavor_cpus' + id = Column(Integer, primary_key=True) + model = Column(String(255), nullable=False) + cores = Column(Integer, nullable=False) + flavor_uuid = Column(String(36), ForeignKey('flavors.uuid'), + nullable=False) + flavor = orm.relationship( + Flavors, + backref='cpus', + foreign_keys=flavor_uuid, + primaryjoin='FlavorCpus.flavor_uuid ' + '== Flavors.uuid') + + +class FlavorMemory(Base): + """Represents the flavor memory.""" + + __tablename__ = 'flavor_memory' + id = Column(Integer, primary_key=True) + type = Column(String(255), nullable=False) + size_mb = Column(Integer, nullable=False) + flavor_uuid = Column(String(36), ForeignKey('flavors.uuid'), + nullable=False) + flavor = orm.relationship( + Flavors, + backref='memory', + foreign_keys=flavor_uuid, + primaryjoin='FlavorMemory.flavor_uuid ' + '== Flavors.uuid') + + +class FlavorDisks(Base): + """Represents the flavor disks.""" + + __tablename__ = 'flavor_disks' + id = Column(Integer, primary_key=True) + type = Column(String(255), nullable=False) + size_gb = Column(Integer, nullable=False) + flavor_uuid = Column(String(36), ForeignKey('flavors.uuid'), + nullable=False) + flavor = orm.relationship( + Flavors, + backref='disks', + foreign_keys=flavor_uuid, + primaryjoin='FlavorDisks.flavor_uuid ' + '== Flavors.uuid') + + +class FlavorNics(Base): + """Represents the flavor nics.""" + + __tablename__ = 'flavor_nics' + id = Column(Integer, primary_key=True) + type = Column(String(255), nullable=False) + speed = Column(String(255), nullable=False) + flavor_uuid = Column(String(36), ForeignKey('flavors.uuid'), + nullable=False) + flavor = orm.relationship( + Flavors, + backref='nics', + foreign_keys=flavor_uuid, + primaryjoin='FlavorNics.flavor_uuid ' + '== Flavors.uuid') + + class ServerFault(Base): """Represents fault info for server""" diff --git a/mogan/objects/flavor.py b/mogan/objects/flavor.py index cb3a8738..effd6628 100644 --- a/mogan/objects/flavor.py +++ b/mogan/objects/flavor.py @@ -35,6 +35,10 @@ class Flavor(base.MoganObject, object_base.VersionedObjectDictCompat): 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True), + 'cpus': object_fields.FlexibleDictField(), + 'memory': object_fields.FlexibleDictField(), + 'nics': object_fields.ListOfDictOfNullableStringsField(), + 'disks': object_fields.ListOfDictOfNullableStringsField(), 'is_public': object_fields.BooleanField(), 'extra_specs': object_fields.FlexibleDictField(), 'projects': object_fields.ListOfStringsField(), @@ -131,6 +135,12 @@ class Flavor(base.MoganObject, object_base.VersionedObjectDictCompat): updates = self.obj_get_changes() projects = updates.pop('projects', None) extra_specs = updates.pop('extra_specs', None) + updates.pop('cpus', None) + updates.pop('memory', None) + updates.pop('nics', None) + updates.pop('disks', None) + + # extra specs if extra_specs is not None: deleted_keys = (set(self._orig_extra_specs.keys()) - set(extra_specs.keys())) @@ -138,15 +148,16 @@ class Flavor(base.MoganObject, object_base.VersionedObjectDictCompat): else: added_keys = deleted_keys = None + if added_keys or deleted_keys: + self.save_extra_specs(context, self.extra_specs, deleted_keys) + + # access projects if projects is not None: deleted_projects = set(self._orig_projects) - set(projects) added_projects = set(projects) - set(self._orig_projects) else: added_projects = deleted_projects = None - if added_keys or deleted_keys: - self.save_extra_specs(context, self.extra_specs, deleted_keys) - if added_projects or deleted_projects: self.save_projects(context, added_projects, deleted_projects) diff --git a/mogan/tests/unit/db/utils.py b/mogan/tests/unit/db/utils.py index 104318e5..a62d49a1 100644 --- a/mogan/tests/unit/db/utils.py +++ b/mogan/tests/unit/db/utils.py @@ -199,6 +199,13 @@ def get_test_flavor(**kw): 'uuid': kw.get('uuid', uuidutils.generate_uuid()), 'name': kw.get('name', 'test'), 'description': kw.get('description', 'test'), + 'cpus': kw.get('cpus', {'model': 'Dual Intel Xeon E5-2650 2.00GHz', + 'cores': '16'}), + 'memory': kw.get('memory', {'type': 'DDR3', 'size_mb': '8192'}), + 'nics': kw.get('nics', [{'type': 'Ethernet', 'speed': '10 Gbps'}, + {'type': 'InfiniBand', 'speed': '40 Gbps'}]), + 'disks': kw.get('disks', [{'type': 'SSD', 'size_gb': 1024}, + {'type': 'HDD', 'size_gb': 1024}]), 'is_public': kw.get('is_public', 1), 'updated_at': kw.get('updated_at'), 'created_at': kw.get('created_at'), diff --git a/mogan/tests/unit/objects/test_flavor.py b/mogan/tests/unit/objects/test_flavor.py index be801ca3..ed6902de 100644 --- a/mogan/tests/unit/objects/test_flavor.py +++ b/mogan/tests/unit/objects/test_flavor.py @@ -77,6 +77,10 @@ class TestFlavorObject(base.DbTestCase): updates = flavor.obj_get_changes() flavor.save(self.context) updates.pop('extra_specs', None) + updates.pop('cpus', None) + updates.pop('memory', None) + updates.pop('disks', None) + updates.pop('nics', None) mock_flavor_update.return_value = self.fake_type mock_flavor_update.assert_called_once_with( self.context, uuid, updates) diff --git a/mogan/tests/unit/objects/test_objects.py b/mogan/tests/unit/objects/test_objects.py index fd47bacf..8dc7df40 100644 --- a/mogan/tests/unit/objects/test_objects.py +++ b/mogan/tests/unit/objects/test_objects.py @@ -391,7 +391,7 @@ expected_object_fingerprints = { 'ComputeDiskList': '1.0-33a2e1bb91ad4082f9f63429b77c1244', 'ServerFault': '1.0-74349ff701259e4834b4e9dc2dac1b12', 'ServerFaultList': '1.0-43e8aad0258652921f929934e9e048fd', - 'Flavor': '1.0-d1cf232312ff8101aa5a19908b476d67', + 'Flavor': '1.0-7cc125bfe2dda6e5ffa07651d58047cd', 'MyObj': '1.1-aad62eedc5a5cc8bcaf2982c285e753f', 'ServerNic': '1.0-ebbd767c2f6a7f14bd524c6067f2b382', 'ServerNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244',