From 78da85116a3f5f6bbac01616c946987dbd6cfef2 Mon Sep 17 00:00:00 2001 From: Nate Potter Date: Wed, 11 Jan 2017 16:08:22 -0800 Subject: [PATCH] Implement flavors This patch implements flavors, allowing users to save composition requirements to the database to be used at any time. Parameters included are the flavor's name, RAM, number of CPU cores, and processor model. Change-Id: I356ca9162559598bf1415d2c3b151596f111ac0c Implements: blueprint flavor --- .../mockup/flavor-criteria-get-response.json | 12 --- .../source/mockup/flavor-list-response.json | 39 +++++++- .../source/mockup/flavor-post-response.json | 20 +++-- api-ref/source/mockup/flavor-post.json | 12 ++- api-ref/source/parameters.yaml | 30 +++++-- api-ref/source/valence-api-v1-flavors.inc | 53 +++++++---- valence/api/route.py | 3 +- valence/api/v1/flavors.py | 18 +++- valence/{flavors => controller}/__init__.py | 0 .../example.py => controller/flavors.py} | 26 ++++-- valence/db/api.py | 44 +++++++++ valence/db/etcd_db.py | 3 +- valence/db/etcd_driver.py | 47 ++++++++++ valence/db/models.py | 35 ++++++++ valence/flavors/flavors.py | 56 ------------ valence/flavors/generatorbase.py | 37 -------- valence/flavors/plugins/__init__.py | 5 -- valence/flavors/plugins/assettag.py | 55 ------------ valence/flavors/plugins/default.py | 56 ------------ .../unit/{flavors => controller}/__init__.py | 0 valence/tests/unit/controller/test_flavors.py | 47 ++++++++++ valence/tests/unit/db/test_db_api.py | 75 ++++++++++++++++ valence/tests/unit/db/utils.py | 19 ++++ valence/tests/unit/fakes/flavor_fakes.py | 89 +++++++++++++++++++ valence/tests/unit/fakes/flavors_fakes.py | 76 ---------------- valence/tests/unit/flavors/test_flavors.py | 89 ------------------- 26 files changed, 512 insertions(+), 434 deletions(-) delete mode 100644 api-ref/source/mockup/flavor-criteria-get-response.json rename valence/{flavors => controller}/__init__.py (100%) rename valence/{flavors/plugins/example.py => controller/flavors.py} (55%) delete mode 100644 valence/flavors/flavors.py delete mode 100644 valence/flavors/generatorbase.py delete mode 100644 valence/flavors/plugins/__init__.py delete mode 100644 valence/flavors/plugins/assettag.py delete mode 100644 valence/flavors/plugins/default.py rename valence/tests/unit/{flavors => controller}/__init__.py (100%) create mode 100644 valence/tests/unit/controller/test_flavors.py create mode 100644 valence/tests/unit/fakes/flavor_fakes.py delete mode 100644 valence/tests/unit/fakes/flavors_fakes.py delete mode 100644 valence/tests/unit/flavors/test_flavors.py diff --git a/api-ref/source/mockup/flavor-criteria-get-response.json b/api-ref/source/mockup/flavor-criteria-get-response.json deleted file mode 100644 index 983b16c..0000000 --- a/api-ref/source/mockup/flavor-criteria-get-response.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "criteria": [ - { - "name": "cpu", - "description": "Generates cpu based flavors" - }, - { - "name": "default", - "description": "Generates 3 flavors(Tiny, Medium, Large) for each node considering all cpu cores, ram and storage" - } - ] -} diff --git a/api-ref/source/mockup/flavor-list-response.json b/api-ref/source/mockup/flavor-list-response.json index 0db3279..819719b 100644 --- a/api-ref/source/mockup/flavor-list-response.json +++ b/api-ref/source/mockup/flavor-list-response.json @@ -1,3 +1,36 @@ -{ - -} +[ + { + "created_at": "2017-01-19 18:46:30 UTC", + "name": "test", + "properties": { + "memory": [ + { + "capacity_mib": "3000", + "type": "DDR3" + } + ], + "processor": [ + { + "total_cores": "10" + } + ] + }, + "updated_at": "2017-01-19 18:46:30 UTC", + "uuid": "33d07db6-82c1-48ac-abca-2761433b79f9" + }, + { + "created_at": "2017-01-19 18:49:45 UTC", + "name": "test 2", + "properties": { + "memory": { + "capacity_mib": "1000" + }, + "processor": { + "model": "Intel", + "total_cores": "2" + } + }, + "updated_at": "2017-01-19 18:49:45 UTC", + "uuid": "dd561046-4372-40df-ad34-8f8c65d50e02" + } +] diff --git a/api-ref/source/mockup/flavor-post-response.json b/api-ref/source/mockup/flavor-post-response.json index ffc2b72..087bb35 100644 --- a/api-ref/source/mockup/flavor-post-response.json +++ b/api-ref/source/mockup/flavor-post-response.json @@ -1,8 +1,12 @@ -[ - [ - "[{\"flavor\": {\"disk\": 0, \"vcpus\": 0, \"ram\": 16, \"name\": \"S_irsd-Rack1Block1\", \"id\": \"321a271b-ab30-4dfb-a098-6cfb8549a143\"}}, {\"extra_specs\": {\"Rack\": \"1\", \"Block\": \"1\"}}]", - "[{\"flavor\": {\"disk\": 0, \"vcpus\": 1, \"ram\": 32, \"name\": \"M_irsd-Rack1Block1\", \"id\": \"819ba7e5-1621-4bf1-b904-9a1a433fd338\"}}, {\"extra_specs\": {\"Rack\": \"1\", \"Block\": \"1\"}}]", - "[{\"flavor\": {\"disk\": 0, \"vcpus\": 2, \"ram\": 64, \"name\": \"L_irsd-Rack1Block1\", \"id\": \"79e27bb9-2a7e-4c10-8ded-9ec4cdd4856d\"}}, {\"extra_specs\": {\"Rack\": \"1\", \"Block\": \"1\"}}]" - ] -] - +{ + "name": "test", + "properties": { + "memory": { + "capacity_mib": "3000" + }, + "processor": { + "total_cores": "10", + "model": "Intel" + } + } +} diff --git a/api-ref/source/mockup/flavor-post.json b/api-ref/source/mockup/flavor-post.json index 6968ad0..087bb35 100644 --- a/api-ref/source/mockup/flavor-post.json +++ b/api-ref/source/mockup/flavor-post.json @@ -1,4 +1,12 @@ { - "criteria": "cpu, storage" + "name": "test", + "properties": { + "memory": { + "capacity_mib": "3000" + }, + "processor": { + "total_cores": "10", + "model": "Intel" + } + } } - diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 33e12f8..50d1949 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -63,17 +63,35 @@ created_at: in: body required: true type: string -criteria_list: +flavor_uuid: description: | - Criteria name for generated a new one. + UUID for flavor. in: body - required: true + required: false type: string -criteria_object: +flavor_name: description: | - Criteria object including name and its description. + Name for specified flavor. in: body - required: true + required: false + type: string +flavor_ram: + description: | + RAM requirement for flavor. + in: body + required: false + type: string +flavor_processor_model: + description: | + Processor model specified by flavor. + in: body + required: false + type: string +flavor_cores: + description: | + Number of processor cores specified by flavor. + in: body + required: false type: string id: description: | diff --git a/api-ref/source/valence-api-v1-flavors.inc b/api-ref/source/valence-api-v1-flavors.inc index 081b203..91093b1 100644 --- a/api-ref/source/valence-api-v1-flavors.inc +++ b/api-ref/source/valence-api-v1-flavors.inc @@ -7,13 +7,10 @@ Flavors List, Searching of Flavors through the ``/v1/flavors`` -List Flavor +List Flavors ============ -.. rest_method:: GET /v1/flavor/ - - -Leaving this empty for discussion due to there isn't a DB to keep generated flavor. +.. rest_method:: GET /v1/flavors/ Normal response codes: 200 @@ -32,8 +29,8 @@ Response :language: javascript -Generate Flavor -=============== +Create Flavor +============= .. rest_method:: POST /v1/flavors @@ -46,7 +43,10 @@ Request .. rest_parameters:: parameters.yaml - - criterial: criteria_list + - name: flavor_name + - ram: flavor_ram + - processor_model: flavor_processor_model + - cores: flavor_cores **Example generate flavor :** @@ -61,30 +61,49 @@ Response .. literalinclude:: mockup/flavor-post-response.json :language: javascript -List Flavor criteria -===================== +Update Flavor +============= -.. rest_method:: GET /v1/flavors/criteria +.. rest_method:: PATCH /v1/flavors/{flavor_uuid} -Get all supported flavor generation criteria along with their description. +Updates the information stored about a flavor. Normal response codes: 200 -Error response codes: unauthorized(401), forbidden(403) +Error response codes: badRequest(400), unauthorized(401), forbidden(403), 404 Request ------- +.. rest_parameters:: parameters.yaml + + - flavor_uuid: flavor_uuid Response -------- .. rest_parameters:: parameters.yaml - - criteria: criteria_object + - uuid: flavor_uuid + - name: flavor_name + - ram: flavor_ram + - processor_model: flavor_processor_model + - cores: flavor_cores -**Example JSON representation of a Compute System:** +Delete Flavor +============= -.. literalinclude:: mockup/flavor-criteria-get-response.json - :language: javascript +.. rest_method:: DELETE /v1/flavors/{flavor_uuid} +Deletes a flavor. + +Normal response codes: 204 + +Error response codes: 401, 403, 404, 409 + +Request +------- + +.. rest_parameters:: parameters.yaml + + - flavor_ident: flavor_ident diff --git a/valence/api/route.py b/valence/api/route.py index dac8557..be0cecc 100644 --- a/valence/api/route.py +++ b/valence/api/route.py @@ -80,7 +80,8 @@ api.add_resource(v1_systems.Systems, '/v1/systems/', # Flavor(s) operations api.add_resource(v1_flavors.Flavors, '/v1/flavors', endpoint='flavors') - +api.add_resource(v1_flavors.Flavors, '/v1/flavors/', + endpoint='flavor') # Storage(s) operations api.add_resource(v1_storages.StoragesList, '/v1/storages', endpoint='storages') diff --git a/valence/api/v1/flavors.py b/valence/api/v1/flavors.py index 37a2322..943200b 100644 --- a/valence/api/v1/flavors.py +++ b/valence/api/v1/flavors.py @@ -16,8 +16,10 @@ import logging from flask import request from flask_restful import Resource +from six.moves import http_client -from valence.flavors import flavors +from valence.common import utils +from valence.controller import flavors LOG = logging.getLogger(__name__) @@ -25,7 +27,17 @@ LOG = logging.getLogger(__name__) class Flavors(Resource): def get(self): - return flavors.get_available_criteria() + return utils.make_response(http_client.OK, flavors.list_flavors()) def post(self): - return flavors.create_flavors(request.get_json()) + return utils.make_response(http_client.OK, + flavors.create_flavor(request.get_json())) + + def delete(self, flavorid): + return utils.make_response(http_client.OK, + flavors.delete_flavor(flavorid)) + + def patch(self, flavorid): + return utils.make_response(http_client.OK, + flavors.update_flavor(flavorid, + request.get_json())) diff --git a/valence/flavors/__init__.py b/valence/controller/__init__.py similarity index 100% rename from valence/flavors/__init__.py rename to valence/controller/__init__.py diff --git a/valence/flavors/plugins/example.py b/valence/controller/flavors.py similarity index 55% rename from valence/flavors/plugins/example.py rename to valence/controller/flavors.py index 7fb0bfd..78119e9 100644 --- a/valence/flavors/plugins/example.py +++ b/valence/controller/flavors.py @@ -13,15 +13,27 @@ # under the License. import logging -from valence.flavors.generatorbase import generatorbase + +from valence.db import api as db_api LOG = logging.getLogger(__name__) -class exampleGenerator(generatorbase): - def __init__(self, nodes): - generatorbase.__init__(self, nodes) +def list_flavors(): + flavor_models = db_api.Connection.list_flavors() + return [flavor.as_dict() for flavor in flavor_models] - def generate(self): - LOG.info("Example Flavor Generate") - return {"Info": "Example Flavor Generator- Not Yet Implemented"} + +def create_flavor(values): + flavor = db_api.Connection.create_flavor(values) + return flavor.as_dict() + + +def delete_flavor(flavorid): + db_api.Connection.delete_flavor(flavorid) + return "Deleted flavor {0}".format(flavorid) + + +def update_flavor(flavorid, values): + flavor = db_api.Connection.update_flavor(flavorid, values) + return flavor.as_dict() diff --git a/valence/db/api.py b/valence/db/api.py index 3be4f38..20b104a 100644 --- a/valence/db/api.py +++ b/valence/db/api.py @@ -71,3 +71,47 @@ class Connection(object): :returns: A list of all pod managers. """ return cls.dbdriver.list_podmanager() + + @classmethod + def create_flavor(cls, values): + """Create a new flavor. + + :param values: The properties of the new flavor. + :returns: The created flavor. + """ + return cls.dbdriver.create_flavor(values) + + @classmethod + def get_flavor_by_uuid(cls, flavor_uuid): + """Get specific flavor by its uuid. + + :param flavor_uuid: The uuid of the flavor. + :returns: The flavor with the specified uuid. + """ + return cls.dbdriver.get_flavor_by_uuid(flavor_uuid) + + @classmethod + def delete_flavor(cls, flavor_uuid): + """Delete a flavor by its uuid. + + :param flavor_uuid: The uuid of the flavor to delete. + """ + cls.dbdriver.delete_flavor(flavor_uuid) + + @classmethod + def update_flavor(cls, flavor_uuid, values): + """Update properties of a specified flavor. + + :param flavor_uuid: The uuid of the flavor to update. + :param values: The properties to be updated. + :returns: The updated flavor. + """ + return cls.dbdriver.update_flavor(flavor_uuid, values) + + @classmethod + def list_flavors(cls): + """Get a list of all flavors. + + :returns: A list of all flavors. + """ + return cls.dbdriver.list_flavors() diff --git a/valence/db/etcd_db.py b/valence/db/etcd_db.py index 9fcbd11..ea4ec33 100644 --- a/valence/db/etcd_db.py +++ b/valence/db/etcd_db.py @@ -19,7 +19,8 @@ from valence.db import models etcd_directories = [ - models.PodManager.path + models.PodManager.path, + models.Flavor.path ] etcd_client = etcd.Client(config.etcd_host, config.etcd_port) diff --git a/valence/db/etcd_driver.py b/valence/db/etcd_driver.py index de97f91..1e94047 100644 --- a/valence/db/etcd_driver.py +++ b/valence/db/etcd_driver.py @@ -37,6 +37,8 @@ def translate_to_models(etcd_resp, model_type): data = json.loads(etcd_resp.value) if model_type == models.PodManager.path: ret = models.PodManager(**data) + elif model_type == models.Flavor.path: + ret = models.Flavor(**data) else: # TODO(lin.a.yang): after exception module got merged, raise # valence specific InvalidParameter exception here @@ -102,3 +104,48 @@ class EtcdDriver(object): podm, models.PodManager.path)) return podmanagers + + def get_flavor_by_uuid(self, flavor_uuid): + try: + resp = self.client.read(models.Flavor.etcd_path(flavor_uuid)) + except etcd.EtcdKeyNotFound: + # TODO(ntpttr): Change this to a valence specific exception + # when the exceptions module is merged. + raise Exception('Flavor {0} not found.'.format(flavor_uuid)) + + return translate_to_models(resp, models.Flavor.path) + + def create_flavor(self, values): + values['uuid'] = uuidutils.generate_uuid() + + flavor = models.Flavor(**values) + flavor.save() + + return flavor + + def delete_flavor(self, flavor_uuid): + flavor = self.get_flavor_by_uuid(flavor_uuid) + flavor.delete() + + def update_flavor(self, flavor_uuid, values): + flavor = self.get_flavor_by_uuid(flavor_uuid) + flavor.update(values) + + return flavor + + def list_flavors(self): + try: + resp = getattr(self.client.read(models.Flavor.path), + 'children', None) + except etcd.EtcdKeyNotFound: + LOG.error("Path '/flavors' does not exist, the etcd server may " + "not have been initialized appropriately.") + raise + + flavors = [] + for flavor in resp: + if flavor.value is not None: + flavors.append(translate_to_models( + flavor, models.Flavor.path)) + + return flavors diff --git a/valence/db/models.py b/valence/db/models.py index f5f8d17..4e47a8c 100644 --- a/valence/db/models.py +++ b/valence/db/models.py @@ -154,3 +154,38 @@ class PodManager(ModelBaseWithTimeStamp): 'validate': types.Text.validate } } + + +class Flavor(ModelBaseWithTimeStamp): + + path = "/flavors" + + fields = { + 'uuid': { + 'validate': types.Text.validate + }, + 'name': { + 'validate': types.Text.validate + }, + 'properties': { + 'memory': { + 'capacity_mib': { + 'validate': types.Text.validate + }, + 'type': { + 'validate': types.Text.validate + }, + 'validate': types.Dict.validate + }, + 'processor': { + 'total_cores': { + 'validate': types.Text.validate + }, + 'model': { + 'validate': types.Text.validate + }, + 'validate': types.Dict.validate + }, + 'validate': types.Dict.validate + } + } diff --git a/valence/flavors/flavors.py b/valence/flavors/flavors.py deleted file mode 100644 index 373925d..0000000 --- a/valence/flavors/flavors.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2016 Intel, Inc. -# -# 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 importlib import import_module -import logging -import os - -from valence.redfish import redfish as rfs - -FLAVOR_PLUGIN_PATH = os.path.dirname(os.path.abspath(__file__)) + '/plugins' -LOG = logging.getLogger(__name__) - - -def get_available_criteria(): - pluginfiles = [f.split('.')[0] - for f in os.listdir(FLAVOR_PLUGIN_PATH) - if os.path.isfile(os.path.join(FLAVOR_PLUGIN_PATH, f)) - and not f.startswith('__') and f.endswith('.py')] - resp = [] - for filename in pluginfiles: - module = import_module("valence.flavors.plugins." + filename) - myclass = getattr(module, filename + 'Generator') - inst = myclass([]) - resp.append({'name': filename, 'description': inst.description()}) - return {'criteria': resp} - - -def create_flavors(data): - """criteria : comma separated generator names - - This should be same as their file name) - - """ - criteria = data["criteria"] - respjson = [] - lst_systems = rfs.systems_list() - for criteria_name in criteria.split(","): - if criteria_name: - LOG.info("Calling generator : %s ." % criteria_name) - module = __import__("valence.flavors.plugins." + criteria_name, - fromlist=["*"]) - classobj = getattr(module, criteria_name + "Generator") - inst = classobj(lst_systems) - respjson.append(inst.generate()) - return respjson diff --git a/valence/flavors/generatorbase.py b/valence/flavors/generatorbase.py deleted file mode 100644 index 0aedaad..0000000 --- a/valence/flavors/generatorbase.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) 2016 Intel, Inc. -# -# 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. - -import json -import uuid - - -class generatorbase(object): - def __init__(self, nodes): - self.nodes = nodes - self.prepend_name = 'irsd-' - - def description(self): - return "Description of plugins" - - def _flavor_template(self, name, ram, cpus, disk, extraspecs): - return json.dumps([{"flavor": - {"name": name, - "ram": int(ram), - "vcpus": int(cpus), - "disk": int(disk), - "id": str(uuid.uuid4())}}, - {"extra_specs": extraspecs}]) - - def generate(self): - raise NotImplementedError() diff --git a/valence/flavors/plugins/__init__.py b/valence/flavors/plugins/__init__.py deleted file mode 100644 index b7f932d..0000000 --- a/valence/flavors/plugins/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""from os.path import dirname, basename, isfile -import glob -modules = glob.glob(dirname(__file__)+"/*.py") -__all__ = [ basename(f)[:-3] for f in modules if isfile(f)] -""" diff --git a/valence/flavors/plugins/assettag.py b/valence/flavors/plugins/assettag.py deleted file mode 100644 index 6d1ead5..0000000 --- a/valence/flavors/plugins/assettag.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2016 Intel, Inc. -# -# 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. - -import logging -import re -from valence.flavors.generatorbase import generatorbase - -LOG = logging.getLogger() - - -class assettagGenerator(generatorbase): - def __init__(self, nodes): - generatorbase.__init__(self, nodes) - - def description(self): - return "Demo only: Generates location based on assettag" - - def generate(self): - LOG.info("Default Generator") - for node in self.nodes: - LOG.info("Node ID " + node['id']) - location = node['location'] - location = location.split('Sled')[0] - location_lst = re.split("(\d+)", location) - LOG.info(str(location_lst)) - location_lst = list(filter(None, location_lst)) - LOG.info(str(location_lst)) - extraspecs = {location_lst[i]: location_lst[i + 1] - for i in range(0, len(location_lst), 2)} - name = self.prepend_name + location - return [ - self._flavor_template("L_" + name, - node['ram'], - node['cpu']["count"], - node['storage'], extraspecs), - self._flavor_template("M_" + name, - int(node['ram']) / 2, - int(node['cpu']["count"]) / 2, - int(node['storage']) / 2, extraspecs), - self._flavor_template("S_" + name, - int(node['ram']) / 4, - int(node['cpu']["count"]) / 4, - int(node['storage']) / 4, extraspecs) - ] diff --git a/valence/flavors/plugins/default.py b/valence/flavors/plugins/default.py deleted file mode 100644 index 8d9b717..0000000 --- a/valence/flavors/plugins/default.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2016 Intel, Inc. -# -# 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. - -import logging -from valence.flavors.generatorbase import generatorbase - -LOG = logging.getLogger(__name__) - - -class defaultGenerator(generatorbase): - def __init__(self, nodes): - generatorbase.__init__(self, nodes) - - def description(self): - return ("Generates 3 flavors(Tiny, Medium, Large) for " - "each node considering all cpu cores, ram and storage") - - def generate(self): - LOG.info("Default Generator") - for node in self.nodes: - LOG.debug("Node ID " + node['id']) - location = node['location'] - LOG.debug(location) - location_lst = location.split("_") - location_lst = list(filter(None, location_lst)) - extraspecs = ({l[0]: l[1] - for l in (l.split(":") for l in location_lst)}) - name = self.prepend_name + node['id'] - return [ - self._flavor_template("L_" + name, - node['ram'], - node['cpu']["count"], - node['storage'], - extraspecs), - self._flavor_template("M_" + name, - int(node['ram']) / 2, - int(node['cpu']["count"]) / 2, - int(node['storage']) / 2, - extraspecs), - self._flavor_template("S_" + name, - int(node['ram']) / 4, - int(node['cpu']["count"]) / 4, - int(node['storage']) / 4, - extraspecs) - ] diff --git a/valence/tests/unit/flavors/__init__.py b/valence/tests/unit/controller/__init__.py similarity index 100% rename from valence/tests/unit/flavors/__init__.py rename to valence/tests/unit/controller/__init__.py diff --git a/valence/tests/unit/controller/test_flavors.py b/valence/tests/unit/controller/test_flavors.py new file mode 100644 index 0000000..1eaa4a7 --- /dev/null +++ b/valence/tests/unit/controller/test_flavors.py @@ -0,0 +1,47 @@ +# 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 unittest import TestCase + +import mock + +from valence.controller import flavors +from valence.tests.unit.fakes import flavor_fakes as fakes + + +class TestFlavors(TestCase): + + @mock.patch('valence.db.api.Connection.list_flavors') + def test_list_flavors(self, mock_db_list_flavors): + mock_db_list_flavors.return_value = fakes.fake_flavor_model_list() + result = flavors.list_flavors() + self.assertEqual(fakes.fake_flavor_list(), result) + + @mock.patch('valence.db.api.Connection.create_flavor') + def test_create_flavor(self, mock_db_create_flavor): + mock_db_create_flavor.return_value = fakes.fake_flavor_model() + result = flavors.create_flavor(fakes.fake_flavor()) + self.assertEqual(fakes.fake_flavor(), result) + + @mock.patch('valence.db.api.Connection.delete_flavor') + def test_delete_flavor(self, mock_db_delete_flavor): + expected = "Deleted flavor 00000000-0000-0000-0000-000000000000" + result = flavors.delete_flavor("00000000-0000-0000-0000-000000000000") + self.assertEqual(expected, result) + + @mock.patch('valence.db.api.Connection.update_flavor') + def test_update_flavor(self, mock_db_update_flavor): + mock_db_update_flavor.return_value = fakes.fake_flavor_model() + result = flavors.update_flavor( + "00000000-0000-0000-0000-00000000", + {"name": "Flavor 1"}) + self.assertEqual(fakes.fake_flavor(), result) diff --git a/valence/tests/unit/db/test_db_api.py b/valence/tests/unit/db/test_db_api.py index 6d0c27f..30be80c 100644 --- a/valence/tests/unit/db/test_db_api.py +++ b/valence/tests/unit/db/test_db_api.py @@ -44,6 +44,23 @@ class TestDBAPI(unittest.TestCase): '/pod_managers/' + podmanager['uuid'], json.dumps(result.as_dict())) + @freezegun.freeze_time('2017-01-01') + @mock.patch('etcd.Client.write') + @mock.patch('etcd.Client.read') + def test_create_flavor(self, mock_etcd_read, mock_etcd_write): + flavor = utils.get_test_flavor() + fake_utcnow = '2017-01-01 00:00:00 UTC' + flavor['created_at'] = fake_utcnow + flavor['updated_at'] = fake_utcnow + + mock_etcd_read.side_effect = etcd.EtcdKeyNotFound + + result = db_api.Connection.create_flavor(flavor) + self.assertEqual(flavor, result.as_dict()) + mock_etcd_read.assert_called_with('/flavors/' + flavor['uuid']) + mock_etcd_write.assert_called_with('/flavors/' + flavor['uuid'], + json.dumps(result.as_dict())) + @mock.patch('etcd.Client.read') def test_get_podmanager_by_uuid(self, mock_etcd_read): podmanager = utils.get_test_podmanager() @@ -56,6 +73,18 @@ class TestDBAPI(unittest.TestCase): mock_etcd_read.assert_called_with( '/pod_managers/' + podmanager['uuid']) + @mock.patch('etcd.Client.read') + def test_get_flavor_by_uuid(self, mock_etcd_read): + flavor = utils.get_test_flavor() + + mock_etcd_read.return_value = utils.get_etcd_read_result( + flavor['uuid'], json.dumps(flavor)) + result = db_api.Connection.get_flavor_by_uuid(flavor['uuid']) + + self.assertEqual(flavor, result.as_dict()) + mock_etcd_read.assert_called_with( + '/flavors/' + flavor['uuid']) + @mock.patch('etcd.Client.read') def test_get_podmanager_not_found(self, mock_etcd_read): podmanager = utils.get_test_podmanager() @@ -69,6 +98,18 @@ class TestDBAPI(unittest.TestCase): mock_etcd_read.assert_called_with( '/pod_managers/' + podmanager['uuid']) + @mock.patch('etcd.Client.read') + def test_get_flavor_not_found(self, mock_etcd_read): + flavor = utils.get_test_flavor() + mock_etcd_read.side_effect = etcd.EtcdKeyNotFound + + with self.assertRaises(Exception) as context: # noqa: H202 + db_api.Connection.get_flavor_by_uuid(flavor['uuid']) + + self.assertTrue('Flavor {0} not found.'.format( + flavor['uuid']) in str(context.exception)) + mock_etcd_read.assert_called_with('/flavors/' + flavor['uuid']) + @mock.patch('etcd.Client.delete') @mock.patch('etcd.Client.read') def test_delete_podmanager(self, mock_etcd_read, mock_etcd_delete): @@ -81,6 +122,17 @@ class TestDBAPI(unittest.TestCase): mock_etcd_delete.assert_called_with( '/pod_managers/' + podmanager['uuid']) + @mock.patch('etcd.Client.delete') + @mock.patch('etcd.Client.read') + def test_delete_flavor(self, mock_etcd_read, mock_etcd_delete): + flavor = utils.get_test_flavor() + + mock_etcd_read.return_value = utils.get_etcd_read_result( + flavor['uuid'], json.dumps(flavor)) + db_api.Connection.delete_flavor(flavor['uuid']) + + mock_etcd_delete.assert_called_with('/flavors/' + flavor['uuid']) + @freezegun.freeze_time("2017-01-01") @mock.patch('etcd.Client.write') @mock.patch('etcd.Client.read') @@ -103,3 +155,26 @@ class TestDBAPI(unittest.TestCase): mock_etcd_write.assert_called_with( '/pod_managers/' + podmanager['uuid'], json.dumps(result.as_dict())) + + @freezegun.freeze_time("2017-01-01") + @mock.patch('etcd.Client.write') + @mock.patch('etcd.Client.read') + def test_update_flavor(self, mock_etcd_read, mock_etcd_write): + flavor = utils.get_test_flavor() + + mock_etcd_read.return_value = utils.get_etcd_read_result( + flavor['uuid'], json.dumps(flavor)) + + fake_utcnow = '2017-01-01 00:00:00 UTC' + flavor['updated_at'] = fake_utcnow + flavor.update({'properties': {'memory': {'type': 'new_type'}}}) + + result = db_api.Connection.update_flavor( + flavor['uuid'], {'properties': {'memory': {'type': 'new_type'}}}) + + self.assertEqual(flavor, result.as_dict()) + mock_etcd_read.assert_called_with( + '/flavors/' + flavor['uuid']) + mock_etcd_write.assert_called_with( + '/flavors/' + flavor['uuid'], + json.dumps(result.as_dict())) diff --git a/valence/tests/unit/db/utils.py b/valence/tests/unit/db/utils.py index 08c2cce..faab582 100644 --- a/valence/tests/unit/db/utils.py +++ b/valence/tests/unit/db/utils.py @@ -56,3 +56,22 @@ def get_test_podmanager(**kwargs): 'created_at': kwargs.get('created_at', '2016-01-01 00:00:00 UTC'), 'updated_at': kwargs.get('updated_at', '2016-01-01 00:00:00 UTC'), } + + +def get_test_flavor(**kwargs): + return { + 'uuid': kwargs.get('uuid', 'f0565d8c-d79b-11e6-bf26-cec0c932ce01'), + 'name': kwargs.get('name', 'fake_name'), + 'properties': { + 'memory': { + 'capacity_mib': kwargs.get('capacity_mib', 'fake_capacity'), + 'type': kwargs.get('type', 'fake_type'), + }, + 'processor': { + 'total_cores': kwargs.get('total_cores', 'fake_cores'), + 'model': kwargs.get('model', 'fake_model') + } + }, + 'created_at': kwargs.get('created_at', '2016-01-01 00:00:00 UTC'), + 'updated_at': kwargs.get('updated_at', '2016-01-01 00:00:00 UTC'), + } diff --git a/valence/tests/unit/fakes/flavor_fakes.py b/valence/tests/unit/fakes/flavor_fakes.py new file mode 100644 index 0000000..b0f4653 --- /dev/null +++ b/valence/tests/unit/fakes/flavor_fakes.py @@ -0,0 +1,89 @@ +# 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 valence.db import models + + +def fake_flavor(): + return { + "uuid": "00000000-0000-0000-0000-000000000000", + "name": "Flavor 1", + "properties": { + "memory": { + "capacity_mib": "1000", + "type": "DDR2" + }, + "processor": { + "total_cores": "10", + "model": "Intel" + } + } + } + + +def fake_flavor_model(): + return models.Flavor(**fake_flavor()) + + +def fake_flavor_list(): + return [ + { + "uuid": "00000000-0000-0000-0000-000000000000", + "name": "Flavor 1", + "properties": { + "memory": { + "capacity_mib": "1000", + "type": "DDR2" + }, + "processor": { + "total_cores": "10", + "model": "Intel" + } + } + }, + { + "uuid": "11111111-1111-1111-1111-111111111111", + "name": "Flavor 2", + "properties": { + "memory": { + "capacity_mib": "2000", + "type": "DDR3" + }, + "processor": { + "total_cores": "20", + "model": "Intel" + } + } + }, + { + "uuid": "22222222-2222-2222-2222-222222222222", + "name": "Flavor 3", + "properties": { + "memory": { + "capacity_mib": "3000", + "type": "SDRAM" + }, + "processor": { + "total_cores": "30", + "model": "Intel" + } + } + } + ] + + +def fake_flavor_model_list(): + values_list = fake_flavor_list() + for i in range(len(values_list)): + values_list[i] = models.Flavor(**values_list[i]) + + return values_list diff --git a/valence/tests/unit/fakes/flavors_fakes.py b/valence/tests/unit/fakes/flavors_fakes.py deleted file mode 100644 index 8b6775b..0000000 --- a/valence/tests/unit/fakes/flavors_fakes.py +++ /dev/null @@ -1,76 +0,0 @@ -# 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. - -import json - - -def fake_flavor_nodes(): - return [ - {"id": '1', "cpu": {'count': 2}, - "ram": 1024, "storage": 256, - "nw": 'nw1', "location": 'location:1', - "uuid": 'fe542581-97fe-4dbb-a1da' - }, - {"id": '2', "cpu": {'count': 4}, - "ram": 2048, "storage": 500, - "nw": 'nw2', "location": 'location:2', - "uuid": 'f0f96c58-d3d0-4292-a191' - } - ] - - -def fake_assettag_flavors(): - return [json.dumps([{"flavor": - {"name": "L_irsd-location:2", - "ram": 2048, - "vcpus": 4, - "disk": 500, - "id": "f0f96c58-d3d0-4292-a191"}}, - {"extra_specs": {"location:": "2"}}]), - json.dumps([{"flavor": - {"name": "M_irsd-location:2", - "ram": 1024, - "vcpus": 2, - "disk": 250, - "id": "f0f96c58-d3d0-4292-a191"}}, - {"extra_specs": {"location:": "2"}}]), - json.dumps([{"flavor": - {"name": "S_irsd-location:2", - "ram": 512, - "vcpus": 1, - "disk": 125, - "id": "f0f96c58-d3d0-4292-a191"}}, - {"extra_specs": {"location:": "2"}}])] - - -def fake_default_flavors(): - return [json.dumps([{"flavor": - {"name": "L_irsd-2", - "ram": 2048, - "vcpus": 4, - "disk": 500, - "id": "f0f96c58-d3d0-4292-a191"}}, - {"extra_specs": {"location": "2"}}]), - json.dumps([{"flavor": - {"name": "M_irsd-2", - "ram": 1024, - "vcpus": 2, - "disk": 250, - "id": "f0f96c58-d3d0-4292-a191"}}, - {"extra_specs": {"location": "2"}}]), - json.dumps([{"flavor": - {"name": "S_irsd-2", - "ram": 512, - "vcpus": 1, - "disk": 125, - "id": "f0f96c58-d3d0-4292-a191"}}, - {"extra_specs": {"location": "2"}}])] diff --git a/valence/tests/unit/flavors/test_flavors.py b/valence/tests/unit/flavors/test_flavors.py deleted file mode 100644 index cf4650b..0000000 --- a/valence/tests/unit/flavors/test_flavors.py +++ /dev/null @@ -1,89 +0,0 @@ -# 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. - -import mock -import unittest - -from valence.flavors import flavors -from valence.tests.unit.fakes import flavors_fakes as fakes - - -class TestFlavors(unittest.TestCase): - - def test_get_available_criteria(self): - expected = {'criteria': [{'name': 'default', - 'description': 'Generates 3 flavors(Tiny, ' - 'Medium, Large) for each ' - 'node considering all cpu ' - 'cores, ram and storage'}, - {'name': 'assettag', - 'description': 'Demo only: Generates ' - 'location based on assettag'}, - {'name': 'example', - 'description': 'Description of plugins'}]} - result = flavors.get_available_criteria() - expected = sorted(expected['criteria'], key=lambda x: x['name']) - result = sorted(result['criteria'], key=lambda x: x['name']) - self.assertEqual(expected, result) - - @mock.patch( - 'valence.flavors.plugins.assettag.assettagGenerator.generate') - @mock.patch('uuid.uuid4') - @mock.patch('valence.redfish.redfish.systems_list') - def test_create_flavors_asserttag(self, mock_systems, - mock_uuid, - mock_generate): - fake_systems = fakes.fake_flavor_nodes() - mock_systems.return_value = fake_systems - mock_uuid.return_value = 'f0f96c58-d3d0-4292-a191' - mock_generate.return_value = fakes.fake_assettag_flavors() - result = flavors.create_flavors(data={"criteria": "assettag"}) - expected = [fakes.fake_assettag_flavors()] - self.assertEqual(expected, result) - - @mock.patch( - 'valence.flavors.plugins.default.defaultGenerator.generate') - @mock.patch('uuid.uuid4') - @mock.patch('valence.redfish.redfish.systems_list') - def test_create_flavors_default(self, mock_systems, - mock_uuid, - mock_generate): - fake_systems = fakes.fake_flavor_nodes() - mock_systems.return_value = fake_systems - mock_uuid.return_value = 'f0f96c58-d3d0-4292-a191' - mock_generate.return_value = fakes.fake_default_flavors() - result = flavors.create_flavors(data={"criteria": "default"}) - expected = [fakes.fake_default_flavors()] - self.assertEqual(expected, result) - - @mock.patch( - 'valence.flavors.plugins.default.defaultGenerator.generate') - @mock.patch( - 'valence.flavors.plugins.assettag.assettagGenerator.generate') - @mock.patch('uuid.uuid4') - @mock.patch('valence.redfish.redfish.systems_list') - def test_create_flavors_asserttag_and_default(self, mock_systems, - mock_uuid, - mock_assettag_generate, - mock_default_generate): - fake_systems = fakes.fake_flavor_nodes() - mock_systems.return_value = fake_systems - mock_uuid.return_value = 'f0f96c58-d3d0-4292-a191' - mock_assettag_generate.return_value = \ - fakes.fake_assettag_flavors() - mock_default_generate.return_value = \ - fakes.fake_default_flavors() - result = flavors.create_flavors( - data={"criteria": "assettag,default"}) - expected = [fakes.fake_assettag_flavors(), - fakes.fake_default_flavors()] - self.assertEqual(expected, result)