Merge "Use node uuid instead of index for /node(s) api"

This commit is contained in:
Jenkins 2017-02-11 01:48:57 +00:00 committed by Gerrit Code Review
commit 01569d5974
14 changed files with 518 additions and 32 deletions

View File

@ -66,8 +66,10 @@ api.add_resource(api_root.Root, '/', endpoint='root')
api.add_resource(v1_version.V1, '/v1', endpoint='v1')
# Node(s) operations
api.add_resource(v1_nodes.NodesList, '/v1/nodes', endpoint='nodes')
api.add_resource(v1_nodes.Nodes, '/v1/nodes/<string:nodeid>', endpoint='node')
api.add_resource(v1_nodes.Nodes, '/v1/nodes', endpoint='nodes')
api.add_resource(v1_nodes.Node,
'/v1/nodes/<string:node_uuid>',
endpoint='node')
api.add_resource(v1_nodes.NodesStorage,
'/v1/nodes/<string:nodeid>/storages',
endpoint='nodes_storages')

View File

@ -12,39 +12,36 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
from flask import request
from flask_restful import abort
from flask_restful import Resource
from six.moves import http_client
from valence.common import utils
from valence.redfish import redfish
LOG = logging.getLogger(__name__)
class NodesList(Resource):
def get(self):
return utils.make_response(http_client.OK,
redfish.list_nodes())
def post(self):
return utils.make_response(
http_client.OK, redfish.compose_node(request.get_json()))
from valence.controller import nodes
class Nodes(Resource):
def get(self, nodeid):
return utils.make_response(http_client.OK,
redfish.get_node_by_id(nodeid))
def get(self):
return utils.make_response(
http_client.OK, nodes.Node.list_composed_nodes())
def delete(self, nodeid):
return utils.make_response(http_client.OK,
redfish.delete_composednode(nodeid))
def post(self):
return utils.make_response(
http_client.OK, nodes.Node.compose_node(request.get_json()))
class Node(Resource):
def get(self, node_uuid):
return utils.make_response(
http_client.OK,
nodes.Node.get_composed_node_by_uuid(node_uuid))
def delete(self, node_uuid):
return utils.make_response(
http_client.OK, nodes.Node.delete_composed_node(node_uuid))
class NodesStorage(Resource):

View File

@ -21,6 +21,7 @@
import logging
import flask
from oslo_utils import uuidutils
import six
LOG = logging.getLogger(__name__)
@ -106,3 +107,8 @@ def make_response(status_code, content="", headers=None):
raise ValueError("Response headers should be dict object.")
return response
def generate_uuid():
"""Generate uniform format uuid"""
return uuidutils.generate_uuid()

View File

@ -0,0 +1,99 @@
# 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 six
from valence.common import utils
from valence.db import api as db_api
from valence.redfish import redfish
class Node(object):
@staticmethod
def _show_node_brief_info(node_info):
return {key: node_info[key] for key in six.iterkeys(node_info)
if key in ["uuid", "name", "links"]}
@classmethod
def compose_node(cls, request_body):
"""Compose new node
param request_body: parameter for node composition
return: brief info of this new composed node
"""
# Call redfish to compose new node
composed_node = redfish.compose_node(request_body)
composed_node["uuid"] = utils.generate_uuid()
# Only store the minimum set of composed node info into backend db,
# since other fields like power status may be changed and valence is
# not aware.
node_db = {"uuid": composed_node["uuid"],
"name": composed_node["name"],
"index": composed_node["index"],
"links": composed_node["links"]}
db_api.Connection.create_composed_node(node_db)
return cls._show_node_brief_info(composed_node)
@classmethod
def get_composed_node_by_uuid(cls, node_uuid):
"""Get composed node details
Get the detail of specific composed node. In some cases db data may be
inconsistent with podm side, like user directly operate podm, not
through valence api. So compare it with node info from redfish, and
update db if it's inconsistent.
param node_uuid: uuid of composed node
return: detail of this composed node
"""
node_db = db_api.Connection.get_composed_node_by_uuid(node_uuid)\
.as_dict()
node_hw = redfish.get_node_by_id(node_db["index"])
# Add those fields of composed node from db
node_hw.update(node_db)
return node_hw
@classmethod
def delete_composed_node(cls, node_uuid):
"""Delete a composed node
param node_uuid: uuid of composed node
return: message of this deletion
"""
# Get node detail from db, and map node uuid to index
index = db_api.Connection.get_composed_node_by_uuid(node_uuid).index
# Call redfish to delete node, and delete corresponding entry in db
message = redfish.delete_composed_node(index)
db_api.Connection.delete_composed_node(node_uuid)
return message
@classmethod
def list_composed_nodes(cls):
"""List all composed node
return: brief info of all composed node
"""
return [cls._show_node_brief_info(node_info.as_dict())
for node_info in db_api.Connection.list_composed_nodes()]

View File

@ -115,3 +115,47 @@ class Connection(object):
:returns: A list of all flavors.
"""
return cls.dbdriver.list_flavors()
@classmethod
def create_composed_node(cls, values):
"""Create a new composed node.
:values: The properties for this new composed node.
:returns: A composed node created.
"""
return cls.dbdriver.create_composed_node(values)
@classmethod
def get_composed_node_by_uuid(cls, composed_node_uuid):
"""Get specific composed node by its uuid
:param composed_node_uuid: The uuid of composed node.
:returns: A composed node with this uuid.
"""
return cls.dbdriver.get_composed_node_by_uuid(composed_node_uuid)
@classmethod
def delete_composed_node(cls, composed_node_uuid):
"""Delete specific composed node by its uuid
:param composed_node_uuid: The uuid of composed node.
"""
cls.dbdriver.delete_composed_node(composed_node_uuid)
@classmethod
def update_composed_node(cls, composed_node_uuid, values):
"""Update properties of a composed node.
:param composed_node_uuid: The uuid of composed node.
:values: The properties to be updated.
:returns: A composed node model after updated.
"""
return cls.dbdriver.update_composed_node(composed_node_uuid, values)
@classmethod
def list_composed_nodes(cls):
"""Get a list of all composed nodes.
:returns: A list of all composed node.
"""
return cls.dbdriver.list_composed_nodes()

View File

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

View File

@ -39,6 +39,8 @@ def translate_to_models(etcd_resp, model_type):
ret = models.PodManager(**data)
elif model_type == models.Flavor.path:
ret = models.Flavor(**data)
elif model_type == models.ComposedNode.path:
ret = models.ComposedNode(**data)
else:
# TODO(lin.a.yang): after exception module got merged, raise
# valence specific InvalidParameter exception here
@ -149,3 +151,51 @@ class EtcdDriver(object):
flavor, models.Flavor.path))
return flavors
def create_composed_node(self, values):
composed_node = models.ComposedNode(**values)
composed_node.save()
return composed_node
def get_composed_node_by_uuid(self, composed_node_uuid):
try:
resp = self.client.read(models.ComposedNode.etcd_path(
composed_node_uuid))
except etcd.EtcdKeyNotFound:
# TODO(lin.a.yang): after exception module got merged, raise
# valence specific DBNotFound exception here
raise Exception(
'Composed node not found {0} in database.'.format(
composed_node_uuid))
return translate_to_models(resp, models.ComposedNode.path)
def delete_composed_node(self, composed_node_uuid):
composed_node = self.get_composed_node_by_uuid(composed_node_uuid)
composed_node.delete()
def update_composed_node(self, composed_node_uuid, values):
composed_node = self.get_composed_node_by_uuid(composed_node_uuid)
composed_node.update(values)
return composed_node
def list_composed_nodes(self):
# TODO(lin.a.yang): support filter for listing composed_node
try:
resp = getattr(self.client.read(models.ComposedNode.path),
'children', None)
except etcd.EtcdKeyNotFound:
LOG.error("Path '/nodes' does not exist, seems etcd server "
"was not initialized appropriately.")
raise
composed_nodes = []
for node in resp:
if node.value is not None:
composed_nodes.append(translate_to_models(
node, models.ComposedNode.path))
return composed_nodes

View File

@ -189,3 +189,23 @@ class Flavor(ModelBaseWithTimeStamp):
'validate': types.Dict.validate
}
}
class ComposedNode(ModelBaseWithTimeStamp):
path = "/nodes"
fields = {
'uuid': {
'validate': types.Text.validate
},
'name': {
'validate': types.Text.validate
},
'index': {
'validate': types.Text.validate
},
'links': {
'validate': types.List(types.Dict).validate
}
}

View File

@ -490,7 +490,7 @@ def compose_node(request_body):
if assemble_resp.status_code != http_client.NO_CONTENT:
# Delete node if assemble failed
delete_composednode(node_index)
delete_composed_node(node_index)
raise exception.RedfishException(assemble_resp.json(),
status_code=resp.status_code)
else:
@ -498,10 +498,10 @@ def compose_node(request_body):
LOG.debug('Successfully assembled node: ' + node_url)
# Return new composed node index
return get_node_by_id(node_index, show_detail=False)
return get_node_by_id(node_index)
def delete_composednode(nodeid):
def delete_composed_node(nodeid):
nodes_url = get_base_resource_url("Nodes")
delete_url = nodes_url + '/' + str(nodeid)
resp = send_request(delete_url, "DELETE")

View File

@ -0,0 +1,52 @@
# copyright (c) 2016 Intel, Inc.
#
# 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.
def get_test_composed_node(**kwargs):
return {
'uuid': kwargs.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'),
'name': kwargs.get('name', 'fake_name'),
'description': kwargs.get('description', 'fake_description'),
'boot_source': kwargs.get('boot_source', 'Hdd'),
'health_status': kwargs.get('health_status', 'OK'),
'index': kwargs.get('index', '1'),
'node_power_state': kwargs.get('node_power_state', 'On'),
'node_state': kwargs.get('node_state', 'Assembling'),
'pooled_group_id': kwargs.get('bookmark_link', 'None'),
'target_boot_source': kwargs.get('target_boot_source', 'Hdd'),
'links': kwargs.get(
'links',
[{'href': 'http://127.0.0.1:8181/v1/nodes/'
'7be5bc10-dcdf-11e6-bd86-934bc6947c55/',
'rel': 'self'},
{'href': 'http://127.0.0.1:8181/nodes/'
'7be5bc10-dcdf-11e6-bd86-934bc6947c55/',
'rel': 'bookmark'}]),
'metadata': kwargs.get(
'metadata',
{'memory': [{'data_width_bit': 0,
'speed_mhz': 2400,
'total_memory_mb': 8192}],
'network': [{'ipv4': [{'address': '192.168.0.10',
'gateway': '192.168.0.1',
'subnet_mask': '255.255.252.0'}],
'mac': 'e9:47:d3:60:64:66',
'speed_mbps': 0,
'status': 'Enabled',
'vlans': [{'status': 'Enabled',
'vlanid': 99}]}],
'processor': [{'instruction_set': None,
'model': None,
'speed_mhz': 3700,
'total_core': 0}]})
}

View File

@ -0,0 +1,117 @@
# copyright (c) 2016 Intel, Inc.
#
# 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 copy
import unittest
import mock
from valence.controller import nodes
from valence.tests.unit.controller import fakes
from valence.tests.unit.db import utils as test_utils
class TestAPINodes(unittest.TestCase):
def test_show_node_brief_info(self):
"""Test only show node brief info"""
node_info = fakes.get_test_composed_node()
expected = {
"name": "fake_name",
"uuid": "ea8e2a25-2901-438d-8157-de7ffd68d051",
"links": [{'href': 'http://127.0.0.1:8181/v1/nodes/'
'7be5bc10-dcdf-11e6-bd86-934bc6947c55/',
'rel': 'self'},
{'href': 'http://127.0.0.1:8181/nodes/'
'7be5bc10-dcdf-11e6-bd86-934bc6947c55/',
'rel': 'bookmark'}]
}
self.assertEqual(expected,
nodes.Node._show_node_brief_info(node_info))
@mock.patch("valence.db.api.Connection.create_composed_node")
@mock.patch("valence.common.utils.generate_uuid")
@mock.patch("valence.redfish.redfish.compose_node")
def test_compose_node(self, mock_redfish_compose_node, mock_generate_uuid,
mock_db_create_composed_node):
"""Test compose node successfully"""
node_hw = fakes.get_test_composed_node()
node_db = {"uuid": node_hw["uuid"],
"index": node_hw["index"],
"name": node_hw["name"],
"links": node_hw["links"]}
mock_redfish_compose_node.return_value = node_hw
uuid = 'ea8e2a25-2901-438d-8157-de7ffd68d051'
mock_generate_uuid.return_value = uuid
result = nodes.Node.compose_node({"name": "test"})
expected = nodes.Node._show_node_brief_info(node_hw)
self.assertEqual(expected, result)
mock_db_create_composed_node.assert_called_once_with(node_db)
@mock.patch("valence.redfish.redfish.get_node_by_id")
@mock.patch("valence.db.api.Connection.get_composed_node_by_uuid")
def test_get_composed_node_by_uuid(
self, mock_db_get_composed_node, mock_redfish_get_node):
"""Test get composed node detail"""
node_hw = fakes.get_test_composed_node()
node_db = test_utils.get_test_composed_node_db_info()
mock_db_model = mock.MagicMock()
mock_db_model.as_dict.return_value = node_db
mock_db_get_composed_node.return_value = mock_db_model
mock_redfish_get_node.return_value = node_hw
result = nodes.Node.get_composed_node_by_uuid("fake_uuid")
expected = copy.deepcopy(node_hw)
expected.update(node_db)
self.assertEqual(expected, result)
@mock.patch("valence.db.api.Connection.delete_composed_node")
@mock.patch("valence.redfish.redfish.delete_composed_node")
@mock.patch("valence.db.api.Connection.get_composed_node_by_uuid")
def test_delete_composed_node(
self, mock_db_get_composed_node, mock_redfish_delete_composed_node,
mock_db_delete_composed_node):
"""Test delete composed node"""
node_db = test_utils.get_test_composed_node_db_info()
mock_db_model = mock.MagicMock()
mock_db_model.index = node_db["index"]
mock_db_get_composed_node.return_value = mock_db_model
nodes.Node.delete_composed_node(node_db["uuid"])
mock_redfish_delete_composed_node.assert_called_once_with(
node_db["index"])
mock_db_delete_composed_node.assert_called_once_with(
node_db["uuid"])
@mock.patch("valence.db.api.Connection.list_composed_nodes")
def test_list_composed_nodes(self, mock_db_list_composed_nodes):
"""Test list all composed nodes"""
node_db = test_utils.get_test_composed_node_db_info()
mock_db_model = mock.MagicMock()
mock_db_model.as_dict.return_value = node_db
mock_db_list_composed_nodes.return_value = [mock_db_model]
expected = [nodes.Node._show_node_brief_info(node_db)]
result = nodes.Node.list_composed_nodes()
self.assertEqual(expected, result)

View File

@ -178,3 +178,83 @@ class TestDBAPI(unittest.TestCase):
mock_etcd_write.assert_called_with(
'/flavors/' + flavor['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_composed_node(self, mock_etcd_read, mock_etcd_write):
node = utils.get_test_composed_node_db_info()
fake_utcnow = '2017-01-01 00:00:00 UTC'
node['created_at'] = fake_utcnow
node['updated_at'] = fake_utcnow
# Mark this uuid don't exist in etcd db
mock_etcd_read.side_effect = etcd.EtcdKeyNotFound
result = db_api.Connection.create_composed_node(node)
self.assertEqual(node, result.as_dict())
mock_etcd_read.assert_called_once_with(
'/nodes/' + node['uuid'])
mock_etcd_write.assert_called_once_with(
'/nodes/' + node['uuid'],
json.dumps(result.as_dict()))
@mock.patch('etcd.Client.read')
def test_get_composed_node_by_uuid(self, mock_etcd_read):
node = utils.get_test_composed_node_db_info()
mock_etcd_read.return_value = utils.get_etcd_read_result(
node['uuid'], json.dumps(node))
result = db_api.Connection.get_composed_node_by_uuid(node['uuid'])
self.assertEqual(node, result.as_dict())
mock_etcd_read.assert_called_once_with(
'/nodes/' + node['uuid'])
@mock.patch('etcd.Client.read')
def test_get_composed_node_not_found(self, mock_etcd_read):
node = utils.get_test_composed_node_db_info()
mock_etcd_read.side_effect = etcd.EtcdKeyNotFound
with self.assertRaises(Exception) as context: # noqa: H202
db_api.Connection.get_composed_node_by_uuid(node['uuid'])
self.assertTrue('Composed node not found {0} in database.'.format(
node['uuid']) in str(context.exception))
mock_etcd_read.assert_called_once_with(
'/nodes/' + node['uuid'])
@mock.patch('etcd.Client.delete')
@mock.patch('etcd.Client.read')
def test_delete_composed_node(self, mock_etcd_read, mock_etcd_delete):
node = utils.get_test_composed_node_db_info()
mock_etcd_read.return_value = utils.get_etcd_read_result(
node['uuid'], json.dumps(node))
db_api.Connection.delete_composed_node(node['uuid'])
mock_etcd_delete.assert_called_with(
'/nodes/' + node['uuid'])
@freezegun.freeze_time("2017-01-01")
@mock.patch('etcd.Client.write')
@mock.patch('etcd.Client.read')
def test_update_composed_node(self, mock_etcd_read, mock_etcd_write):
node = utils.get_test_composed_node_db_info()
mock_etcd_read.return_value = utils.get_etcd_read_result(
node['uuid'], json.dumps(node))
fake_utcnow = '2017-01-01 00:00:00 UTC'
node['updated_at'] = fake_utcnow
node.update({'index': '2'})
result = db_api.Connection.update_composed_node(
node['uuid'], {'index': '2'})
self.assertEqual(node, result.as_dict())
mock_etcd_read.assert_called_with(
'/nodes/' + node['uuid'])
mock_etcd_write.assert_called_with(
'/nodes/' + node['uuid'],
json.dumps(result.as_dict()))

View File

@ -75,3 +75,21 @@ def get_test_flavor(**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_composed_node_db_info(**kwargs):
return {
'uuid': kwargs.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'),
'name': kwargs.get('name', 'fake_name'),
'index': kwargs.get('index', '1'),
'links': kwargs.get(
'links',
[{'href': 'http://127.0.0.1:8181/v1/nodes/'
'7be5bc10-dcdf-11e6-bd86-934bc6947c55/',
'rel': 'self'},
{'href': 'http://127.0.0.1:8181/nodes/'
'7be5bc10-dcdf-11e6-bd86-934bc6947c55/',
'rel': 'bookmark'}]),
'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

@ -255,7 +255,7 @@ class TestRedfish(TestCase):
fake_delete_response = fakes.mock_request_get(delete_result,
http_client.NO_CONTENT)
mock_request.return_value = fake_delete_response
result = redfish.delete_composednode(101)
result = redfish.delete_composed_node(101)
mock_request.assert_called_with('/redfish/v1/Nodes/101', 'DELETE')
expected = {
"code": "DELETED",
@ -276,7 +276,7 @@ class TestRedfish(TestCase):
http_client.INTERNAL_SERVER_ERROR)
mock_request.return_value = fake_resp
self.assertRaises(exception.RedfishException,
redfish.delete_composednode, 101)
redfish.delete_composed_node, 101)
self.assertFalse(mock_make_response.called)
@mock.patch('requests.get')
@ -340,7 +340,7 @@ class TestRedfish(TestCase):
self.assertTrue("There are no computer systems available for this "
"allocation request." in str(context.exception.detail))
@mock.patch('valence.redfish.redfish.delete_composednode')
@mock.patch('valence.redfish.redfish.delete_composed_node')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@mock.patch('valence.redfish.redfish.send_request')
def test_assemble_node_failed(self, mock_request, mock_get_url,
@ -375,7 +375,7 @@ class TestRedfish(TestCase):
mock_delete_node.assert_called_once()
@mock.patch('valence.redfish.redfish.get_node_by_id')
@mock.patch('valence.redfish.redfish.delete_composednode')
@mock.patch('valence.redfish.redfish.delete_composed_node')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@mock.patch('valence.redfish.redfish.send_request')
def test_assemble_node_success(self, mock_request, mock_get_url,