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)