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
This commit is contained in:
Nate Potter 2017-01-11 16:08:22 -08:00
parent cd503ba499
commit 78da85116a
26 changed files with 512 additions and 434 deletions

View File

@ -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"
}
]
}

View File

@ -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"
}
]

View File

@ -1,8 +1,12 @@
[ {
[ "name": "test",
"[{\"flavor\": {\"disk\": 0, \"vcpus\": 0, \"ram\": 16, \"name\": \"S_irsd-Rack1Block1\", \"id\": \"321a271b-ab30-4dfb-a098-6cfb8549a143\"}}, {\"extra_specs\": {\"Rack\": \"1\", \"Block\": \"1\"}}]", "properties": {
"[{\"flavor\": {\"disk\": 0, \"vcpus\": 1, \"ram\": 32, \"name\": \"M_irsd-Rack1Block1\", \"id\": \"819ba7e5-1621-4bf1-b904-9a1a433fd338\"}}, {\"extra_specs\": {\"Rack\": \"1\", \"Block\": \"1\"}}]", "memory": {
"[{\"flavor\": {\"disk\": 0, \"vcpus\": 2, \"ram\": 64, \"name\": \"L_irsd-Rack1Block1\", \"id\": \"79e27bb9-2a7e-4c10-8ded-9ec4cdd4856d\"}}, {\"extra_specs\": {\"Rack\": \"1\", \"Block\": \"1\"}}]" "capacity_mib": "3000"
] },
] "processor": {
"total_cores": "10",
"model": "Intel"
}
}
}

View File

@ -1,4 +1,12 @@
{ {
"criteria": "cpu, storage" "name": "test",
"properties": {
"memory": {
"capacity_mib": "3000"
},
"processor": {
"total_cores": "10",
"model": "Intel"
}
}
} }

View File

@ -63,17 +63,35 @@ created_at:
in: body in: body
required: true required: true
type: string type: string
criteria_list: flavor_uuid:
description: | description: |
Criteria name for generated a new one. UUID for flavor.
in: body in: body
required: true required: false
type: string type: string
criteria_object: flavor_name:
description: | description: |
Criteria object including name and its description. Name for specified flavor.
in: body 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 type: string
id: id:
description: | description: |

View File

@ -7,13 +7,10 @@ Flavors
List, Searching of Flavors through the ``/v1/flavors`` List, Searching of Flavors through the ``/v1/flavors``
List Flavor List Flavors
============ ============
.. rest_method:: GET /v1/flavor/ .. rest_method:: GET /v1/flavors/
Leaving this empty for discussion due to there isn't a DB to keep generated flavor.
Normal response codes: 200 Normal response codes: 200
@ -32,8 +29,8 @@ Response
:language: javascript :language: javascript
Generate Flavor Create Flavor
=============== =============
.. rest_method:: POST /v1/flavors .. rest_method:: POST /v1/flavors
@ -46,7 +43,10 @@ Request
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- criterial: criteria_list - name: flavor_name
- ram: flavor_ram
- processor_model: flavor_processor_model
- cores: flavor_cores
**Example generate flavor :** **Example generate flavor :**
@ -61,30 +61,49 @@ Response
.. literalinclude:: mockup/flavor-post-response.json .. literalinclude:: mockup/flavor-post-response.json
:language: javascript :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 Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403) Error response codes: badRequest(400), unauthorized(401), forbidden(403), 404
Request Request
------- -------
.. rest_parameters:: parameters.yaml
- flavor_uuid: flavor_uuid
Response Response
-------- --------
.. rest_parameters:: parameters.yaml .. 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 .. rest_method:: DELETE /v1/flavors/{flavor_uuid}
:language: javascript
Deletes a flavor.
Normal response codes: 204
Error response codes: 401, 403, 404, 409
Request
-------
.. rest_parameters:: parameters.yaml
- flavor_ident: flavor_ident

View File

@ -80,7 +80,8 @@ api.add_resource(v1_systems.Systems, '/v1/systems/<string:systemid>',
# Flavor(s) operations # Flavor(s) operations
api.add_resource(v1_flavors.Flavors, '/v1/flavors', endpoint='flavors') api.add_resource(v1_flavors.Flavors, '/v1/flavors', endpoint='flavors')
api.add_resource(v1_flavors.Flavors, '/v1/flavors/<string:flavorid>',
endpoint='flavor')
# Storage(s) operations # Storage(s) operations
api.add_resource(v1_storages.StoragesList, '/v1/storages', endpoint='storages') api.add_resource(v1_storages.StoragesList, '/v1/storages', endpoint='storages')

View File

@ -16,8 +16,10 @@ import logging
from flask import request from flask import request
from flask_restful import Resource 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__) LOG = logging.getLogger(__name__)
@ -25,7 +27,17 @@ LOG = logging.getLogger(__name__)
class Flavors(Resource): class Flavors(Resource):
def get(self): def get(self):
return flavors.get_available_criteria() return utils.make_response(http_client.OK, flavors.list_flavors())
def post(self): 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()))

View File

@ -13,15 +13,27 @@
# under the License. # under the License.
import logging import logging
from valence.flavors.generatorbase import generatorbase
from valence.db import api as db_api
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class exampleGenerator(generatorbase): def list_flavors():
def __init__(self, nodes): flavor_models = db_api.Connection.list_flavors()
generatorbase.__init__(self, nodes) return [flavor.as_dict() for flavor in flavor_models]
def generate(self):
LOG.info("Example Flavor Generate") def create_flavor(values):
return {"Info": "Example Flavor Generator- Not Yet Implemented"} 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()

View File

@ -71,3 +71,47 @@ class Connection(object):
:returns: A list of all pod managers. :returns: A list of all pod managers.
""" """
return cls.dbdriver.list_podmanager() 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()

View File

@ -19,7 +19,8 @@ from valence.db import models
etcd_directories = [ etcd_directories = [
models.PodManager.path models.PodManager.path,
models.Flavor.path
] ]
etcd_client = etcd.Client(config.etcd_host, config.etcd_port) etcd_client = etcd.Client(config.etcd_host, config.etcd_port)

View File

@ -37,6 +37,8 @@ def translate_to_models(etcd_resp, model_type):
data = json.loads(etcd_resp.value) data = json.loads(etcd_resp.value)
if model_type == models.PodManager.path: if model_type == models.PodManager.path:
ret = models.PodManager(**data) ret = models.PodManager(**data)
elif model_type == models.Flavor.path:
ret = models.Flavor(**data)
else: else:
# TODO(lin.a.yang): after exception module got merged, raise # TODO(lin.a.yang): after exception module got merged, raise
# valence specific InvalidParameter exception here # valence specific InvalidParameter exception here
@ -102,3 +104,48 @@ class EtcdDriver(object):
podm, models.PodManager.path)) podm, models.PodManager.path))
return podmanagers 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

View File

@ -154,3 +154,38 @@ class PodManager(ModelBaseWithTimeStamp):
'validate': types.Text.validate '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
}
}

View File

@ -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

View File

@ -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()

View File

@ -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)]
"""

View File

@ -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)
]

View File

@ -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)
]

View File

@ -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)

View File

@ -44,6 +44,23 @@ class TestDBAPI(unittest.TestCase):
'/pod_managers/' + podmanager['uuid'], '/pod_managers/' + podmanager['uuid'],
json.dumps(result.as_dict())) 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') @mock.patch('etcd.Client.read')
def test_get_podmanager_by_uuid(self, mock_etcd_read): def test_get_podmanager_by_uuid(self, mock_etcd_read):
podmanager = utils.get_test_podmanager() podmanager = utils.get_test_podmanager()
@ -56,6 +73,18 @@ class TestDBAPI(unittest.TestCase):
mock_etcd_read.assert_called_with( mock_etcd_read.assert_called_with(
'/pod_managers/' + podmanager['uuid']) '/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') @mock.patch('etcd.Client.read')
def test_get_podmanager_not_found(self, mock_etcd_read): def test_get_podmanager_not_found(self, mock_etcd_read):
podmanager = utils.get_test_podmanager() podmanager = utils.get_test_podmanager()
@ -69,6 +98,18 @@ class TestDBAPI(unittest.TestCase):
mock_etcd_read.assert_called_with( mock_etcd_read.assert_called_with(
'/pod_managers/' + podmanager['uuid']) '/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.delete')
@mock.patch('etcd.Client.read') @mock.patch('etcd.Client.read')
def test_delete_podmanager(self, mock_etcd_read, mock_etcd_delete): 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( mock_etcd_delete.assert_called_with(
'/pod_managers/' + podmanager['uuid']) '/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") @freezegun.freeze_time("2017-01-01")
@mock.patch('etcd.Client.write') @mock.patch('etcd.Client.write')
@mock.patch('etcd.Client.read') @mock.patch('etcd.Client.read')
@ -103,3 +155,26 @@ class TestDBAPI(unittest.TestCase):
mock_etcd_write.assert_called_with( mock_etcd_write.assert_called_with(
'/pod_managers/' + podmanager['uuid'], '/pod_managers/' + podmanager['uuid'],
json.dumps(result.as_dict())) 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()))

View File

@ -56,3 +56,22 @@ def get_test_podmanager(**kwargs):
'created_at': kwargs.get('created_at', '2016-01-01 00:00:00 UTC'), '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'), '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'),
}

View File

@ -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

View File

@ -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"}}])]

View File

@ -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)