diff --git a/rsd_lib/resources/v2_3/fabric/endpoint.py b/rsd_lib/resources/v2_3/fabric/endpoint.py index aeb6840..a2d9bf5 100644 --- a/rsd_lib/resources/v2_3/fabric/endpoint.py +++ b/rsd_lib/resources/v2_3/fabric/endpoint.py @@ -13,11 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +import jsonschema import logging from sushy.resources import base from sushy import utils +from rsd_lib.resources.v2_3.fabric import endpoint_schemas from rsd_lib import utils as rsd_lib_utils @@ -156,3 +158,76 @@ class EndpointCollection(base.ResourceCollectionBase): """ super(EndpointCollection, self).__init__(connector, path, redfish_version) + + def _create_endpoint_request(self, identifiers, connected_entities, + protocol=None, ip_transport_details=None, + interface=None, authentication=None): + + request = {} + + jsonschema.validate(identifiers, + endpoint_schemas.identifiers_req_schema) + request['Identifiers'] = identifiers + + jsonschema.validate(connected_entities, + endpoint_schemas.connected_entities_req_schema) + request['ConnectedEntities'] = connected_entities + + if protocol is not None: + jsonschema.validate(protocol, endpoint_schemas.protocol_req_schema) + request['EndpointProtocol'] = protocol + + if ip_transport_details is not None: + jsonschema.validate( + ip_transport_details, + endpoint_schemas.ip_transport_details_req_schema) + request['IPTransportDetails'] = ip_transport_details + + if interface is not None: + jsonschema.validate(interface, + endpoint_schemas.interface_req_schema) + request['Links'] = { + "Oem": { + "Intel_RackScale": { + "Interfaces": [ + { + "@odata.id": interface + } + ] + } + } + } + + if authentication is not None: + jsonschema.validate(authentication, + endpoint_schemas.authentication_req_schema) + request['Oem'] = {"Intel_RackScale": + {"Authentication": authentication}} + + return request + + def create_endpoint(self, identifiers, connected_entities, protocol=None, + ip_transport_details=None, interface=None, + authentication=None): + """Create a new endpoint + + :param identifiers: provides iQN or NQN of created entity + :param connected_entities: provides information about entities + connected to the endpoint + :param protocol: the protocol used by the endpoint + :param ip_transport_details: the transport used for accessing the + endpoint + :param interface: the interface that should be used for the endpoint + connectivity + :param authentication: authentication data for target-initiator + authentication. Currently supported only for the + iSCSI protocol. + :returns: The uri of the new endpoint + """ + properties = self._create_endpoint_request( + identifiers, connected_entities, protocol, ip_transport_details, + interface, authentication) + resp = self._conn.post(self._path, data=properties) + LOG.info("Endpoint created at %s", resp.headers['Location']) + endpoint_url = resp.headers['Location'] + return endpoint_url[endpoint_url.find(self._path):] diff --git a/rsd_lib/resources/v2_3/fabric/endpoint_schemas.py b/rsd_lib/resources/v2_3/fabric/endpoint_schemas.py new file mode 100644 index 0000000..fa52de4 --- /dev/null +++ b/rsd_lib/resources/v2_3/fabric/endpoint_schemas.py @@ -0,0 +1,112 @@ +# Copyright (c) 2018 Intel, Corp. +# +# 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. + +identifiers_req_schema = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'DurableNameFormat': { + 'type': 'string', + 'enum': ['NQN', 'iQN'] + }, + 'DurableName': {'type': 'string'} + }, + "required": ['DurableNameFormat', 'DurableName'], + 'additionalProperties': False + } +} + +connected_entities_req_schema = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'EntityLink': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + }, + "required": ['@odata.id'], + 'additionalProperties': False + }, + 'EntityRole': { + 'type': 'string', + 'enum': ['Initiator', 'Target', 'Both'] + }, + 'Identifiers': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'DurableNameFormat': { + 'type': 'string', + 'enum': ['NQN', 'iQN', 'FC_WWN', 'UUID', 'EUI', + 'NAA', 'NSID', 'SystemPath', 'LUN'] + }, + 'DurableName': {'type': 'string'} + }, + "required": ['DurableNameFormat', 'DurableName'], + 'additionalProperties': False + } + } + }, + "required": ['EntityLink', 'EntityRole'], + 'additionalProperties': False + } +} + +protocol_req_schema = { + 'type': 'string', + 'enum': ['NVMeOverFabrics', 'iSCSI'] +} + +ip_transport_details_req_schema = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'TransportProtocol': {'type': 'string'}, + 'IPv4Address': { + 'type': 'object', + 'properties': { + 'Address': {'type': 'string'} + }, + 'additionalProperties': False + }, + 'IPv6Address': { + 'type': 'object', + 'properties': { + 'Address': {'type': 'string'} + }, + 'additionalProperties': False + }, + 'Port': {'type': 'number'} + }, + 'additionalProperties': False + } +} + +interface_req_schema = { + 'type': 'string' +} + +authentication_req_schema = { + 'type': 'object', + 'properties': { + 'Username': {'type': 'string'}, + 'Password': {'type': 'string'} + }, + 'additionalProperties': False +} diff --git a/rsd_lib/resources/v2_3/storage_service/volume.py b/rsd_lib/resources/v2_3/storage_service/volume.py index 6a8d8ae..0bc48aa 100644 --- a/rsd_lib/resources/v2_3/storage_service/volume.py +++ b/rsd_lib/resources/v2_3/storage_service/volume.py @@ -246,7 +246,7 @@ class VolumeCollection(base.ResourceCollectionBase): def create_volume(self, capacity, access_capabilities=None, capacity_sources=None, replica_infos=None, bootable=None): - """Compose a node from RackScale hardware + """Create a new volume :param capacity: Requested volume capacity in bytes :param access_capabilities: List of volume access capabilities diff --git a/rsd_lib/tests/unit/resources/v2_3/fabric/test_endpoint.py b/rsd_lib/tests/unit/resources/v2_3/fabric/test_endpoint.py index 41b9341..360726d 100644 --- a/rsd_lib/tests/unit/resources/v2_3/fabric/test_endpoint.py +++ b/rsd_lib/tests/unit/resources/v2_3/fabric/test_endpoint.py @@ -13,11 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import json +import jsonschema import mock import testtools from rsd_lib.resources.v2_3.fabric import endpoint +from rsd_lib.tests.unit.fakes import request_fakes class EndpointTestCase(testtools.TestCase): @@ -175,8 +178,13 @@ class EndpointCollectionTestCase(testtools.TestCase): with open('rsd_lib/tests/unit/json_samples/v2_3/' 'endpoint_collection.json', 'r') as f: self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.conn.post.return_value = request_fakes.fake_request_post( + None, headers={"Location": "https://localhost:8443/redfish/v1/" + "Fabrics/NVMeoE/Endpoints/3"}) + self.endpoint_col = endpoint.EndpointCollection( - self.conn, '/redfish/v1/Fabrics/PCIe/Endpoints', + self.conn, '/redfish/v1/Fabrics/NVMeoE/Endpoints', redfish_version='1.0.2') def test__parse_attributes(self): @@ -211,3 +219,242 @@ class EndpointCollectionTestCase(testtools.TestCase): mock_endpoint.assert_has_calls(calls) self.assertIsInstance(members, list) self.assertEqual(2, len(members)) + + def test_create_endpoint(self): + reqs = { + "EndpointProtocol": "NVMeOverFabrics", + "Identifiers": [ + { + "DurableNameFormat": "NQN", + "DurableName": "nqn.2014-08.org.nvmexpress:NVMf:" + "uuid:397f9b78-7e94-11e7-9ea4-001e67dfa170" + } + ], + "ConnectedEntities": [ + { + "EntityLink": { + "@odata.id": "/redfish/v1/StorageServices/1/Volumes/1" + }, + "EntityRole": "Target" + } + ], + "Links": { + "Oem": { + "Intel_RackScale": { + "Interfaces": [ + { + "@odata.id": "/redfish/v1/Systems/Target/" + "EthernetInterfaces/1" + } + ] + } + } + } + } + result = self.endpoint_col.create_endpoint( + identifiers=[ + { + "DurableNameFormat": "NQN", + "DurableName": "nqn.2014-08.org.nvmexpress:NVMf:" + "uuid:397f9b78-7e94-11e7-9ea4-001e67dfa170" + } + ], + connected_entities=[ + { + "EntityLink": { + "@odata.id": "/redfish/v1/StorageServices/1/Volumes/1" + }, + "EntityRole": "Target" + } + ], + protocol="NVMeOverFabrics", + interface="/redfish/v1/Systems/Target/EthernetInterfaces/1") + self.endpoint_col._conn.post.assert_called_once_with( + '/redfish/v1/Fabrics/NVMeoE/Endpoints', data=reqs) + self.assertEqual(result, + '/redfish/v1/Fabrics/NVMeoE/Endpoints/3') + + self.endpoint_col._conn.post.reset_mock() + reqs = { + "EndpointProtocol": "iSCSI", + "Identifiers": [ + { + "DurableNameFormat": "iQN", + "DurableName": "iqn.1986-03.com.intel:my_storage-uuid:" + "397f9b78-7e94-11e7-9ea4-001e67dfa170" + } + ], + "ConnectedEntities": [ + { + "EntityLink": { + "@odata.id": "/redfish/v1/StorageServices/1/Volumes/1" + }, + "EntityRole": "Target", + "Identifiers": [ + { + "DurableNameFormat": "LUN", + "DurableName": "1" + } + ] + } + ], + "Oem": { + "Intel_RackScale": { + "Authentication": { + "Username": "userA", + "Password": "passB" + } + } + } + } + result = self.endpoint_col.create_endpoint( + identifiers=[ + { + "DurableNameFormat": "iQN", + "DurableName": "iqn.1986-03.com.intel:my_storage-uuid:" + "397f9b78-7e94-11e7-9ea4-001e67dfa170" + } + ], + connected_entities=[ + { + "EntityLink": { + "@odata.id": "/redfish/v1/StorageServices/1/Volumes/1" + }, + "EntityRole": "Target", + "Identifiers": [ + { + "DurableNameFormat": "LUN", + "DurableName": "1" + } + ] + } + ], + protocol="iSCSI", + authentication={ + "Username": "userA", + "Password": "passB" + }) + self.endpoint_col._conn.post.assert_called_once_with( + '/redfish/v1/Fabrics/NVMeoE/Endpoints', data=reqs) + self.assertEqual(result, + '/redfish/v1/Fabrics/NVMeoE/Endpoints/3') + + def test_create_endpoint_with_invalid_reqs(self): + identifiers = [ + { + "DurableNameFormat": "iQN", + "DurableName": "iqn.1986-03.com.intel:my_storage-uuid:" + "397f9b78-7e94-11e7-9ea4-001e67dfa170" + } + ] + connected_entities = [ + { + "EntityLink": { + "@odata.id": "/redfish/v1/StorageServices/1/Volumes/1" + }, + "EntityRole": "Target", + "Identifiers": [ + { + "DurableNameFormat": "LUN", + "DurableName": "1" + } + ] + } + ] + + result = self.endpoint_col.create_endpoint( + identifiers=identifiers, connected_entities=connected_entities) + self.assertEqual(result, + '/redfish/v1/Fabrics/NVMeoE/Endpoints/3') + + # Test invalid identifiers argument + invalid_identifiers = copy.deepcopy(identifiers) + invalid_identifiers[0]['DurableNameFormat'] = 'fake-format' + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=invalid_identifiers, + connected_entities=connected_entities) + + invalid_identifiers = copy.deepcopy(identifiers) + invalid_identifiers[0].pop('DurableNameFormat') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=invalid_identifiers, + connected_entities=connected_entities) + + invalid_identifiers = copy.deepcopy(identifiers) + invalid_identifiers[0].pop('DurableName') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=invalid_identifiers, + connected_entities=connected_entities) + + invalid_identifiers = copy.deepcopy(identifiers) + invalid_identifiers[0]['invalid_key'] = 'invalid_value' + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=invalid_identifiers, + connected_entities=connected_entities) + + # Test invalid connected_entities argument + invalid_connected_entities = copy.deepcopy(connected_entities) + invalid_connected_entities[0]['EntityRole'] = 'fake-format' + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=invalid_connected_entities) + + invalid_connected_entities = copy.deepcopy(connected_entities) + invalid_connected_entities[0]['EntityLink'].pop('@odata.id') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=invalid_connected_entities) + + invalid_connected_entities = copy.deepcopy(connected_entities) + invalid_connected_entities[0].pop('EntityLink') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=invalid_connected_entities) + + invalid_connected_entities = copy.deepcopy(connected_entities) + invalid_connected_entities[0].pop('EntityRole') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=invalid_connected_entities) + + invalid_connected_entities = copy.deepcopy(connected_entities) + invalid_connected_entities[0]['invalid_key'] = 'invalid_value' + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=invalid_connected_entities) + + # Test invalid protocol argument + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=connected_entities, + protocol='invalid_potocol') + + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=connected_entities, + protocol=1) + + # Test invalid interface argument + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=connected_entities, + interface=1) + + # Test invalid authentication argument + self.assertRaises(jsonschema.exceptions.ValidationError, + self.endpoint_col.create_endpoint, + identifiers=identifiers, + connected_entities=connected_entities, + authentication={'invalid_key': 'invalid_value'})