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:
parent
cd503ba499
commit
78da85116a
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,12 @@
|
||||
{
|
||||
"criteria": "cpu, storage"
|
||||
"name": "test",
|
||||
"properties": {
|
||||
"memory": {
|
||||
"capacity_mib": "3000"
|
||||
},
|
||||
"processor": {
|
||||
"total_cores": "10",
|
||||
"model": "Intel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: |
|
||||
|
@ -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
|
||||
|
@ -80,7 +80,8 @@ api.add_resource(v1_systems.Systems, '/v1/systems/<string:systemid>',
|
||||
|
||||
# Flavor(s) operations
|
||||
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
|
||||
api.add_resource(v1_storages.StoragesList, '/v1/storages', endpoint='storages')
|
||||
|
@ -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()))
|
||||
|
@ -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()
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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()
|
@ -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)]
|
||||
"""
|
@ -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)
|
||||
]
|
@ -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)
|
||||
]
|
47
valence/tests/unit/controller/test_flavors.py
Normal file
47
valence/tests/unit/controller/test_flavors.py
Normal 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)
|
@ -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()))
|
||||
|
@ -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'),
|
||||
}
|
||||
|
89
valence/tests/unit/fakes/flavor_fakes.py
Normal file
89
valence/tests/unit/fakes/flavor_fakes.py
Normal 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
|
@ -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"}}])]
|
@ -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)
|
Loading…
Reference in New Issue
Block a user