From d11c1aef9bd89d7b8547010772d2cb0aa82f4921 Mon Sep 17 00:00:00 2001 From: Nate Potter Date: Wed, 21 Dec 2016 16:04:34 -0800 Subject: [PATCH] Add Rack API This commit adds API and corresponding controller functions for viewing rack resources. Change-Id: I66fd2f9cfe30b7a87ee4ea16a9f8027c34d1d0e6 Closes-bug: #1633443 --- api-ref/source/mockup/rack-get-response.json | 15 +++ api-ref/source/mockup/rack-list-response.json | 19 ++++ api-ref/source/parameters.yaml | 39 ++++++++ api-ref/source/valence-api-v1-racks.inc | 75 +++++++++++++++ valence/api/route.py | 5 + valence/api/v1/racks.py | 38 ++++++++ valence/redfish/redfish.py | 68 ++++++++++---- valence/tests/unit/api/test_route.py | 1 + valence/tests/unit/fakes/redfish_fakes.py | 36 +++++++ valence/tests/unit/redfish/test_redfish.py | 94 ++++++++++++++++++- 10 files changed, 369 insertions(+), 21 deletions(-) create mode 100644 api-ref/source/mockup/rack-get-response.json create mode 100644 api-ref/source/mockup/rack-list-response.json create mode 100644 api-ref/source/valence-api-v1-racks.inc create mode 100644 valence/api/v1/racks.py diff --git a/api-ref/source/mockup/rack-get-response.json b/api-ref/source/mockup/rack-get-response.json new file mode 100644 index 0000000..1965de7 --- /dev/null +++ b/api-ref/source/mockup/rack-get-response.json @@ -0,0 +1,15 @@ +[ + { + "description": "Rack created by PODM", + "id": "1", + "manufacturer": "Intel", + "model": "RSD_1", + "name": "Rack 1", + "serial_number": "12345", + "systems": [ + "2cd33e50-0e7a-11e7-8c14-c5fab3f6ca28", + "2a911680-0e7a-11e7-8c14-c5fab3f6ca28", + "4cadbee1-fe07-11e6-8c14-c5fab3f6ca28" + ] + } +] diff --git a/api-ref/source/mockup/rack-list-response.json b/api-ref/source/mockup/rack-list-response.json new file mode 100644 index 0000000..437fd4b --- /dev/null +++ b/api-ref/source/mockup/rack-list-response.json @@ -0,0 +1,19 @@ +[ + { + "id": "1", + "name": "Rack 1", + "systems": [ + "2cd33e50-0e7a-11e7-8c14-c5fab3f6ca28", + "2a911680-0e7a-11e7-8c14-c5fab3f6ca28", + "4cadbee1-fe07-11e6-8c14-c5fab3f6ca28" + ] + }, + { + "id": "2", + "name": "Rack 2", + "systems": [ + "7ac441b3-a4a1-44f4-8b38-469492cbfb61", + "3bf332e4-100c-11e7-93ae-92361f002671" + ] + } +] diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 0ba92a2..ad990cf 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -285,6 +285,45 @@ pod_redfish_link: pod_status: description: | Pod manager status +rack_id: + description: | + The ID of a hardware rack. + in: body + required: true + type: string +rack_name: + description: | + Name of a hardware rack. + in: body + required: true + type: string +rack_systems: + description: | + Compute systems contained by a rack. + in: body + required: true + type: string +rack_manufacturer: + description: | + The manufacturer for a rack. + in: body + required: true + type: string +rack_model: + description: | + The model for a rack. + in: body + required: true + type: string +rack_description: + description: | + The description of a rack. + in: body + required: true + type: string +rack_serial_number: + description: | + The serial number of a rack. in: body required: true type: string diff --git a/api-ref/source/valence-api-v1-racks.inc b/api-ref/source/valence-api-v1-racks.inc new file mode 100644 index 0000000..d76e65c --- /dev/null +++ b/api-ref/source/valence-api-v1-racks.inc @@ -0,0 +1,75 @@ +.. -*- rst -*- + +==== +Rack +==== + +List, Searching of hardware racks through the ``/v1/racks`` resource. + +List Racks +========== + +.. rest_method:: GET /v1/racks/ + +Return a list of Racks. +Some filtering is possible by passing in flags with the request. +By default, this query will return racks with id, name, location and +contained compute systems. + +Normal response codes: 200 +Error response codes: badRequest(400), unauthorized(401), forbidden(403) + +Request +------- + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - id: rack_id + - name: rack_name + - systems: rack_systems + +**Example list of Racks:** + +.. literalinclude:: mockup/rack-list-response.json + :language: javascript + + +Display Rack Details +==================== + +.. rest_method:: GET /v1/racks/{rack_id} + +Shows details for a Rack. +This will return the full representation of the resources. + +Normal response codes: 200 + +Error response codes: badRequest(400), unauthorized(401), forbidden(403) + +Request +------- + +.. rest_parameters:: parameters.yaml + + - id: rack_id + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - id: rack_id + - name: rack_name + - systems: rack_systems + - manufacturer: rack_manufacturer + - model: rack_model + - description: rack_description + - serial_number: rack_serial_number + +**Example JSON representation of a Rack:** + +.. literalinclude:: mockup/rack-get-response.json + :language: javascript \ No newline at end of file diff --git a/valence/api/route.py b/valence/api/route.py index 587b0c6..682f41a 100644 --- a/valence/api/route.py +++ b/valence/api/route.py @@ -24,6 +24,7 @@ import valence.api.root as api_root import valence.api.v1.flavors as v1_flavors import valence.api.v1.nodes as v1_nodes import valence.api.v1.podmanagers as v1_podmanagers +import valence.api.v1.racks as v1_racks import valence.api.v1.storages as v1_storages import valence.api.v1.systems as v1_systems import valence.api.v1.version as v1_version @@ -65,6 +66,10 @@ api.add_resource(api_root.Root, '/', endpoint='root') # V1 Root operations api.add_resource(v1_version.V1, '/v1', endpoint='v1') +# Rack operations +api.add_resource(v1_racks.RackList, '/v1/racks', endpoint='racks') +api.add_resource(v1_racks.Rack, '/v1/racks/', endpoint='rack') + # Node(s) operations api.add_resource(v1_nodes.Nodes, '/v1/nodes', endpoint='nodes') api.add_resource(v1_nodes.Node, diff --git a/valence/api/v1/racks.py b/valence/api/v1/racks.py new file mode 100644 index 0000000..2b996f5 --- /dev/null +++ b/valence/api/v1/racks.py @@ -0,0 +1,38 @@ +# 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 flask import request +import flask_restful +from six.moves import http_client + +from valence.common import utils +from valence.redfish import redfish + +LOG = logging.getLogger(__name__) + + +class RackList(flask_restful.Resource): + + def get(self): + return utils.make_response( + http_client.OK, redfish.list_racks(request.get_json())) + + +class Rack(flask_restful.Resource): + + def get(self, rack_id): + return utils.make_response( + http_client.OK, redfish.show_rack(rack_id)) diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index 6a34a6b..ee040e0 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json import logging import os @@ -81,33 +80,68 @@ def send_request(resource, method="GET", **kwargs): def filter_chassis(jsonContent, filterCondition): - returnJSONObj = {} returnMembers = [] members = jsonContent['Members'] for member in members: resource = member['@odata.id'] resp = send_request(resource) - memberJsonObj = resp.json() - chassisType = memberJsonObj['ChassisType'] + member_detail = resp.json() + chassisType = member_detail['ChassisType'] if chassisType == filterCondition: - returnMembers.append(member) - returnJSONObj["Members"] = returnMembers - returnJSONObj["Members@odata.count"] = len(returnMembers) - return returnJSONObj + returnMembers.append(member_detail) + return returnMembers -def racks(): +def list_racks(filters={}, show_detail=False): chassis_url = get_base_resource_url("Chassis") - jsonContent = send_request(chassis_url) - racks = filter_chassis(jsonContent, "Rack") - return json.dumps(racks) + resp = send_request(chassis_url) + json_content = resp.json() + raw_racks = filter_chassis(json_content, "Rack") + racks = [] + filterPassed = True + + for rack in raw_racks: + + if any(filters): + filterPassed = utils.match_conditions(rack, filters) + if not filterPassed: + continue + + rack_info = {} + + rack_id = rack["Id"] + rack_name = rack["Name"] + rack_systems = get_systems_in_chassis(rack) + rack_info.update({"id": rack_id, "name": rack_name, + "systems": rack_systems}) + if show_detail: + manufacturer = rack["Manufacturer"] + model = rack["Model"] + description = rack["Description"] + serial_number = rack["SerialNumber"] + rack_info.update({"manufacturer": manufacturer, + "model": model, + "description": description, + "serial_number": serial_number}) + racks.append(rack_info) + return racks -def pods(): - chassis_url = get_base_resource_url("Chassis") - jsonContent = send_request(chassis_url) - pods = filter_chassis(jsonContent, "Pod") - return json.dumps(pods) +def show_rack(rack_id): + return list_racks({"Id": rack_id}, show_detail=True) + + +def get_systems_in_chassis(chassis, total_systems=[]): + for chassis_link in chassis["Links"]["Contains"]: + resp = send_request(chassis_link["@odata.id"]) + chassis = resp.json() + total_systems = get_systems_in_chassis(chassis, total_systems) + for system_link in chassis["Links"]["ComputerSystems"]: + resp = send_request(system_link["@odata.id"]) + system = resp.json() + if system["UUID"] not in total_systems: + total_systems.append(system["UUID"]) + return total_systems def pod_status(pod_url, username, password): diff --git a/valence/tests/unit/api/test_route.py b/valence/tests/unit/api/test_route.py index ce9899c..ba42bb5 100644 --- a/valence/tests/unit/api/test_route.py +++ b/valence/tests/unit/api/test_route.py @@ -33,6 +33,7 @@ class TestRoute(unittest.TestCase): self.assertEqual(self.api.owns_endpoint('root'), True) self.assertEqual(self.api.owns_endpoint('v1'), True) + self.assertEqual(self.api.owns_endpoint('racks'), True) self.assertEqual(self.api.owns_endpoint('nodes'), True) self.assertEqual(self.api.owns_endpoint('node'), True) self.assertEqual(self.api.owns_endpoint('nodes_storages'), True) diff --git a/valence/tests/unit/fakes/redfish_fakes.py b/valence/tests/unit/fakes/redfish_fakes.py index 4e9bf93..50094a8 100644 --- a/valence/tests/unit/fakes/redfish_fakes.py +++ b/valence/tests/unit/fakes/redfish_fakes.py @@ -341,3 +341,39 @@ def fake_assemble_node_failed(): }] } } + + +def fake_rack_list(): + return [ + { + "Description": "Rack created by PODM", + "Id": "2", + "Manufacturer": "Intel", + "Model": "RSD_1", + "Name": "Rack 1", + "SerialNumber": "12345", + "Links": { + "Contains": [], + "ComputerSystems": [ + {"@odata.id": "/redfish/v1/Systems/1"}, + {"@odata.id": "/redfish/v1/Systems/2"}, + {"@odata.id": "/redfish/v1/Systems/3"} + ] + } + }, + { + "Description": "Rack created by PODM", + "Id": "3", + "Manufacturer": "Intel", + "Model": "RSD_1", + "Name": "Rack 2", + "SerialNumber": "12346", + "Links": { + "Contains": [], + "ComputerSystems": [ + {"@odata.id": "/redfish/v1/Systems/4"}, + {"@odata.id": "/redfish/v1/Systems/5"} + ] + } + } + ] diff --git a/valence/tests/unit/redfish/test_redfish.py b/valence/tests/unit/redfish/test_redfish.py index 38e5bfc..4fda670 100644 --- a/valence/tests/unit/redfish/test_redfish.py +++ b/valence/tests/unit/redfish/test_redfish.py @@ -119,10 +119,18 @@ class TestRedfish(TestCase): [{"@odata.id": "1"}, {"@odata.id": "2"}, {"@odata.id": "3"}]} - expected = {'Members': [ - {u'@odata.id': u'2'}, - {u'@odata.id': u'3'} - ], 'Members@odata.count': 2} + expected = [ + { + "ChassisType": "Rack", + "Name": "Rack 1", + "Id": "2" + }, + { + "ChassisType": "Rack", + "Name": "Rack 2", + "Id": "3" + } + ] result = redfish.filter_chassis(chassis, "Rack") self.assertEqual(expected, result) @@ -576,3 +584,81 @@ class TestRedfish(TestCase): redfish.node_action("1", {"Reset": {"Type": "On"}}) mock_reset_node.assert_called_once_with("1", {"Reset": {"Type": "On"}}) + + @mock.patch('valence.redfish.redfish.get_systems_in_chassis') + @mock.patch('valence.redfish.redfish.get_base_resource_url') + @mock.patch('valence.redfish.redfish.filter_chassis') + @mock.patch('valence.redfish.redfish.send_request') + def test_list_racks(self, mock_request, mock_filter, mock_base_url, + mock_system_list): + mock_base_url.return_value = "/redfish/v1/Chassis" + fake_chassis_list = fakes.fake_chassis_list() + mock_request.return_value = ( + fakes.mock_request_get(fake_chassis_list, "200")) + mock_filter.return_value = fakes.fake_rack_list() + mock_system_list.side_effect = [ + [ + "2cd33e50-0e7a-11e7-8c14-c5fab3f6ca28", + "2a911680-0e7a-11e7-8c14-c5fab3f6ca28", + "4cadbee1-fe07-11e6-8c14-c5fab3f6ca28" + ], + [ + "7ac441b3-a4a1-44f4-8b38-469492cbfb61", + "3bf332e4-100c-11e7-93ae-92361f002671" + ] + ] + expected = [ + { + "id": "2", + "name": "Rack 1", + "systems": [ + "2cd33e50-0e7a-11e7-8c14-c5fab3f6ca28", + "2a911680-0e7a-11e7-8c14-c5fab3f6ca28", + "4cadbee1-fe07-11e6-8c14-c5fab3f6ca28" + ] + }, + { + "id": "3", + "name": "Rack 2", + "systems": [ + "7ac441b3-a4a1-44f4-8b38-469492cbfb61", + "3bf332e4-100c-11e7-93ae-92361f002671" + ] + } + ] + result = redfish.list_racks() + self.assertEqual(expected, result) + + @mock.patch('valence.redfish.redfish.get_systems_in_chassis') + @mock.patch('valence.redfish.redfish.get_base_resource_url') + @mock.patch('valence.redfish.redfish.filter_chassis') + @mock.patch('valence.redfish.redfish.send_request') + def test_show_rack(self, mock_request, mock_filter, mock_base_url, + mock_system_list): + mock_base_url.return_value = "/redfish/v1/Chassis" + fake_chassis_list = fakes.fake_chassis_list() + mock_request.return_value = ( + fakes.mock_request_get(fake_chassis_list, "200")) + mock_filter.return_value = fakes.fake_rack_list() + mock_system_list.return_value = [ + "2cd33e50-0e7a-11e7-8c14-c5fab3f6ca28", + "2a911680-0e7a-11e7-8c14-c5fab3f6ca28", + "4cadbee1-fe07-11e6-8c14-c5fab3f6ca28" + ] + expected = [ + { + "description": "Rack created by PODM", + "id": "2", + "manufacturer": "Intel", + "model": "RSD_1", + "name": "Rack 1", + "serial_number": "12345", + "systems": [ + "2cd33e50-0e7a-11e7-8c14-c5fab3f6ca28", + "2a911680-0e7a-11e7-8c14-c5fab3f6ca28", + "4cadbee1-fe07-11e6-8c14-c5fab3f6ca28" + ] + } + ] + result = redfish.show_rack("2") + self.assertEqual(expected, result)