From 8338d8b4cdc12a041a147bcb8336a622550cd3d4 Mon Sep 17 00:00:00 2001 From: Aleksandra Bezborodova Date: Mon, 28 May 2018 20:06:58 +0300 Subject: [PATCH] Add a virtual media resource Describe virtual media resources in Python classes close to Redfish schema. Story: 1526753 Task: 12509 Co-Authored-By: Ilya Etingof Change-Id: Ifccd43036378a2808f6c89d4e15307b54e00ed6f --- doc/source/reference/index.rst | 1 + doc/source/reference/usage.rst | 39 +++++++ ...irtual-media-support-f522fbec4420341c.yaml | 4 + sushy/resources/manager/constants.py | 14 +++ sushy/resources/manager/manager.py | 20 ++++ sushy/resources/manager/mappings.py | 14 +++ sushy/resources/manager/virtual_media.py | 108 ++++++++++++++++++ .../unit/json_samples/virtual_media.json | 24 ++++ .../virtual_media_collection.json | 15 +++ .../unit/resources/manager/test_manager.py | 60 ++++++++++ .../resources/manager/test_virtual_media.py | 82 +++++++++++++ 11 files changed, 381 insertions(+) create mode 100644 releasenotes/notes/add-virtual-media-support-f522fbec4420341c.yaml create mode 100644 sushy/resources/manager/virtual_media.py create mode 100644 sushy/tests/unit/json_samples/virtual_media.json create mode 100644 sushy/tests/unit/json_samples/virtual_media_collection.json create mode 100644 sushy/tests/unit/resources/manager/test_virtual_media.py diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index 2347d156..bf264776 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -10,6 +10,7 @@ Features * Systems power management (Both soft and hard; Including NMI injection) * Changing systems boot device, frequency (Once or permanently) and mode (UEFI or BIOS) +* Virtual media management * SessionManagement .. toctree:: diff --git a/doc/source/reference/usage.rst b/doc/source/reference/usage.rst index 66ac5524..7854843a 100644 --- a/doc/source/reference/usage.rst +++ b/doc/source/reference/usage.rst @@ -209,6 +209,45 @@ Creating and using a sushy manager object # Refresh the manager object (with all its sub-resources) mgr_inst.refresh(force=True) + + # Using Virtual Media + + # Instantiate a VirtualMediaCollection object + virtmedia_col = mgr_inst.virtual_media + + # Print the ID of the VirtualMedia available in the collection + print(virtmedia_col.members_identities) + + # Get a list of VirtualMedia objects available in the collection + virtmedia_insts = virtmedia_col.get_members() + + # Instantiate a VirtualMedia object + virtmedia_inst = virtmedia_col.get_member( + virtmedia_col.members_identities[0]) + + + # Print out some of the VirtualMedia properties + print(virtmedia_inst.name, + virtmedia_inst.media_types) + + # Insert virtual media (invalidates virtmedia_inst contents) + virtmedia_inst.insert_media('https://www.dmtf.org/freeImages/Sardine.img') + + # Refresh the resource to load actual contents + virtmedia_inst.refresh() + + # Print out some of the VirtualMedia properties + print(virtmedia_inst.image, + virtmedia_inst.image_path, + virtmedia_inst.inserted, + virtmedia_inst.write_protected) + + # ... Boot the system off the virtual media... + + # Eject virtual media (invalidates virtmedia_inst contents) + virtmedia_inst.eject_media() + + ------------------------------------------------- Creating and using a sushy session service object ------------------------------------------------- diff --git a/releasenotes/notes/add-virtual-media-support-f522fbec4420341c.yaml b/releasenotes/notes/add-virtual-media-support-f522fbec4420341c.yaml new file mode 100644 index 00000000..88ecfafc --- /dev/null +++ b/releasenotes/notes/add-virtual-media-support-f522fbec4420341c.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for the virtual media resource to the library. diff --git a/sushy/resources/manager/constants.py b/sushy/resources/manager/constants.py index a1b3a9f6..69941c8f 100644 --- a/sushy/resources/manager/constants.py +++ b/sushy/resources/manager/constants.py @@ -76,3 +76,17 @@ COMMAND_SHELL_IPMI = 'command shell ipmi' COMMAND_SHELL_OEM = 'command shell oem' """Command Shell connection using an OEM-specific protocol""" + +# Virtual Media Type constants + +VIRTUAL_MEDIA_CD = 'cd' +VIRTUAL_MEDIA_DVD = 'dvd' +VIRTUAL_MEDIA_FLOPPY = 'floppy' +VIRTUAL_MEDIA_USBSTICK = 'usb' + +# Connected Via constants + +CONNECTED_VIA_APPLET = 'applet' +CONNECTED_VIA_NOT_CONNECTED = 'not_connected' +CONNECTED_VIA_OEM = 'oem' +CONNECTED_VIA_URI = 'uri' diff --git a/sushy/resources/manager/manager.py b/sushy/resources/manager/manager.py index d027947e..95684089 100644 --- a/sushy/resources/manager/manager.py +++ b/sushy/resources/manager/manager.py @@ -16,6 +16,9 @@ from sushy import exceptions from sushy.resources import base from sushy.resources import common from sushy.resources.manager import mappings as mgr_maps +from sushy.resources.manager import virtual_media +from sushy import utils + LOG = logging.getLogger(__name__) @@ -74,6 +77,8 @@ class Manager(base.ResourceBase): _actions = ActionsField('Actions', required=True) + _virtual_media = None + def __init__(self, connector, identity, redfish_version=None): """A class representing a Manager @@ -84,6 +89,10 @@ class Manager(base.ResourceBase): """ super(Manager, self).__init__(connector, identity, redfish_version) + def _do_refresh(self, force=False): + if self._virtual_media is not None: + self._virtual_media.invalidate(force) + def get_supported_graphical_console_types(self): """Get the supported values for Graphical Console connection types. @@ -178,6 +187,17 @@ class Manager(base.ResourceBase): self._conn.post(target_uri, data={'ResetType': value}) LOG.info('The Manager %s is being reset', self.identity) + @property + def virtual_media(self): + if self._virtual_media is None: + self._virtual_media = virtual_media.VirtualMediaCollection( + self._conn, + utils.get_sub_resource_path_by(self, 'VirtualMedia'), + redfish_version=self.redfish_version) + + self._virtual_media.refresh(force=False) + return self._virtual_media + class ManagerCollection(base.ResourceCollectionBase): diff --git a/sushy/resources/manager/mappings.py b/sushy/resources/manager/mappings.py index 451dc6e1..c8ea5cb9 100644 --- a/sushy/resources/manager/mappings.py +++ b/sushy/resources/manager/mappings.py @@ -59,3 +59,17 @@ COMMAND_SHELL_VALUE_MAP = { COMMAND_SHELL_VALUE_MAP_REV = ( utils.revert_dictionary(COMMAND_SHELL_VALUE_MAP)) + +MEDIA_TYPE_MAP = { + 'CD': mgr_cons.VIRTUAL_MEDIA_CD, + 'DVD': mgr_cons.VIRTUAL_MEDIA_DVD, + 'Floppy': mgr_cons.VIRTUAL_MEDIA_FLOPPY, + 'USBStick': mgr_cons.VIRTUAL_MEDIA_USBSTICK +} + +CONNECTED_VIA_MAP = { + "Applet": mgr_cons.CONNECTED_VIA_APPLET, + "NotConnected": mgr_cons.CONNECTED_VIA_NOT_CONNECTED, + "Oem": mgr_cons.CONNECTED_VIA_OEM, + "URI": mgr_cons.CONNECTED_VIA_URI +} diff --git a/sushy/resources/manager/virtual_media.py b/sushy/resources/manager/virtual_media.py new file mode 100644 index 00000000..d84d86fc --- /dev/null +++ b/sushy/resources/manager/virtual_media.py @@ -0,0 +1,108 @@ +# 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. + +# This is referred from Redfish standard schema. +# https://redfish.dmtf.org/schemas/VirtualMedia.v1_2_0.json + +from sushy import exceptions +from sushy.resources import base +from sushy.resources import common +from sushy.resources.manager import mappings as mgr_maps + + +class ActionsField(base.CompositeField): + + insert_media = common.ActionField("#VirtualMedia.InsertMedia") + eject_media = common.ActionField("#VirtualMedia.EjectMedia") + + +class VirtualMedia(base.ResourceBase): + + identity = base.Field('Id', required=True) + """Virtual Media resource identity string""" + + name = base.Field('Name', required=True) + """The name of resource""" + + image = base.Field('Image') + """A URI providing the location of the selected image""" + + image_name = base.Field('ImageName') + """The image name""" + + inserted = base.Field('Inserted') + """Indicates if virtual media is inserted in the virtual device""" + + write_protected = base.Field('WriteProtected') + """Indicates the media is write protected""" + + media_types = base.MappedField('MediaTypes', mgr_maps.MEDIA_TYPE_MAP) + """This is the media types supported as virtual media""" + + connected_via = base.MappedField('ConnectedVia', + mgr_maps.CONNECTED_VIA_MAP) + """Current virtual media connection methods + + Applet: Connected to a client application + NotConnected: No current connection + Oem: Connected via an OEM-defined method + URI: Connected to a URI location + """ + + _actions = ActionsField('Actions') + """Insert/eject action fot virtual media""" + + def _get_insert_media_element(self): + insert_media = self._actions.insert_media + if not insert_media: + raise exceptions.MissingActionError( + action='#VirtualMedia.InsertMedia', resource=self._path) + return insert_media + + def _get_eject_media_element(self): + eject_media = self._actions.eject_media + if not eject_media: + raise exceptions.MissingActionError( + action='#VirtualMedia.EjectMedia', resource=self._path) + return eject_media + + def insert_media(self, image, inserted=True, write_protected=False): + """Attach remote media to virtual media + + :param image: a URI providing the location of the selected image + :param inserted: specify if the image is to be treated as inserted upon + completion of the action. + :param write_protected: indicates the media is write protected + """ + target_uri = self._get_insert_media_element().target_uri + self._conn.post(target_uri, data={"Image": image, "Inserted": inserted, + "WriteProtected": write_protected}) + self.invalidate() + + def eject_media(self): + """Detach remote media from virtual media + + After ejecting media inserted will be False and image_name will be + empty. + """ + + target_uri = self._get_eject_media_element().target_uri + self._conn.post(target_uri) + self.invalidate() + + +class VirtualMediaCollection(base.ResourceCollectionBase): + """A collection of virtual media attached to a Manager""" + + @property + def _resource_type(self): + return VirtualMedia diff --git a/sushy/tests/unit/json_samples/virtual_media.json b/sushy/tests/unit/json_samples/virtual_media.json new file mode 100644 index 00000000..e0728724 --- /dev/null +++ b/sushy/tests/unit/json_samples/virtual_media.json @@ -0,0 +1,24 @@ +{ + "@odata.type": "#VirtualMedia.v1_1_0.VirtualMedia", + "Id": "Floppy1", + "Name": "Virtual Removable Media", + "MediaTypes": "Floppy", + "Actions": { + "#VirtualMedia.EjectMedia": { + "target": "/redfish/v1/Managers/BMC/VirtualMedia/Floppy1/Actions/VirtualMedia.EjectMedia", + "title": "Mock Eject Media" + }, + "#VirtualMedia.InsertMedia": { + "target": "/redfish/v1/Managers/BMC/VirtualMedia/Floppy1/Actions/VirtualMedia.InsertMedia", + "title": "Mock Insert Media" + } + }, + "Image": "https://www.dmtf.org/freeImages/Sardine.img", + "ImageName": "Sardine2.1.43.35.6a", + "ConnectedVia": "URI", + "Inserted": true, + "WriteProtected": false, + "@odata.context": "/redfish/v1/$metadata#VirtualMedia.VirtualMedia", + "@odata.id": "/redfish/v1/Managers/BMC/VirtualMedia/Floppy1", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} diff --git a/sushy/tests/unit/json_samples/virtual_media_collection.json b/sushy/tests/unit/json_samples/virtual_media_collection.json new file mode 100644 index 00000000..5052d829 --- /dev/null +++ b/sushy/tests/unit/json_samples/virtual_media_collection.json @@ -0,0 +1,15 @@ +{ + "@odata.type": "#VirtualMediaCollection.VirtualMediaCollection", + "Name": "Virtual Media Services", + "Description": "Redfish-BMC Virtual Media Service Settings", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/BMC/VirtualMedia/Floppy1" + } + ], + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#VirtualMediaCollection.VirtualMediaCollection", + "@odata.id": "/redfish/v1/Managers/BMC/VirtualMedia", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/resources/manager/test_manager.py b/sushy/tests/unit/resources/manager/test_manager.py index 1e78be72..19fe3934 100644 --- a/sushy/tests/unit/resources/manager/test_manager.py +++ b/sushy/tests/unit/resources/manager/test_manager.py @@ -17,6 +17,7 @@ import mock import sushy from sushy import exceptions from sushy.resources.manager import manager +from sushy.resources.manager import virtual_media from sushy.tests.unit import base @@ -53,6 +54,7 @@ class ManagerTestCase(base.TestCase): self.assertEqual(sushy.MANAGER_TYPE_BMC, self.manager.manager_type) self.assertEqual('58893887-8974-2487-2389-841168418919', self.manager.uuid) + self.assertIsNone(self.manager._virtual_media) def test_get_supported_graphical_console_types(self): # | GIVEN | @@ -206,6 +208,64 @@ class ManagerTestCase(base.TestCase): self.assertRaises(exceptions.InvalidParameterValueError, self.manager.reset_manager, 'invalid-value') + def test_virtual_media(self): + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'virtual_media_collection.json') as f: + virtual_media_collection_return_value = json.load(f) + + with open('sushy/tests/unit/json_samples/' + 'virtual_media.json') as f: + virtual_media_return_value = json.load(f) + + self.conn.get.return_value.json.side_effect = [ + virtual_media_collection_return_value, virtual_media_return_value] + + # | WHEN | + actual_virtual_media = self.manager.virtual_media + + # | THEN | + self.assertIsInstance(actual_virtual_media, + virtual_media.VirtualMediaCollection) + self.assertEqual(actual_virtual_media.name, 'Virtual Media Services') + + member = actual_virtual_media.get_member('Floppy1') + + self.assertEqual(member.image_name, "Sardine2.1.43.35.6a") + self.assertTrue(member.inserted) + self.assertFalse(member.write_protected) + + def test_virtual_media_on_refresh(self): + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'virtual_media_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + # | WHEN & THEN | + self.assertIsInstance(self.manager.virtual_media, + virtual_media.VirtualMediaCollection) + + # On refreshing the manager instance... + with open('sushy/tests/unit/json_samples/manager.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.manager.invalidate() + self.manager.refresh(force=False) + + # | WHEN & THEN | + self.assertIsNotNone(self.manager._virtual_media) + self.assertTrue(self.manager._virtual_media._is_stale) + + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'virtual_media_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + # | WHEN & THEN | + self.assertIsInstance(self.manager.virtual_media, + virtual_media.VirtualMediaCollection) + self.assertFalse(self.manager._virtual_media._is_stale) + class ManagerCollectionTestCase(base.TestCase): diff --git a/sushy/tests/unit/resources/manager/test_virtual_media.py b/sushy/tests/unit/resources/manager/test_virtual_media.py new file mode 100644 index 00000000..22eb9dd3 --- /dev/null +++ b/sushy/tests/unit/resources/manager/test_virtual_media.py @@ -0,0 +1,82 @@ +# All Rights Reserved. +# +# 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 mock + +from sushy import exceptions +from sushy.resources.manager import virtual_media +from sushy.tests.unit import base + + +class VirtualMediaTestCase(base.TestCase): + + def setUp(self): + super(VirtualMediaTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'virtual_media.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + self.sys_virtual_media = virtual_media.VirtualMedia( + self.conn, '/redfish/v1/Managers/BMC/VirtualMedia/Floppy1', + redfish_version='1.0.2') + + def test__parse_atrtributes(self): + self.sys_virtual_media._parse_attributes() + self.assertEqual('Virtual Removable Media', + self.sys_virtual_media.name) + self.assertEqual('Floppy1', self.sys_virtual_media.identity) + self.assertEqual('https://www.dmtf.org/freeImages/Sardine.img', + self.sys_virtual_media.image) + self.assertEqual('Sardine2.1.43.35.6a', + self.sys_virtual_media.image_name) + self.assertEqual('uri', self.sys_virtual_media.connected_via) + self.assertEqual('floppy', + self.sys_virtual_media.media_types) + self.assertEqual(True, self.sys_virtual_media.inserted) + self.assertEqual(False, self.sys_virtual_media.write_protected) + + def test_insert_media_none(self): + self.sys_virtual_media._actions.insert_media = None + self.assertRaisesRegex( + exceptions.MissingActionError, 'action #VirtualMedia.InsertMedia', + self.sys_virtual_media.insert_media, + "https://www.dmtf.org/freeImages/Sardine.img", True, False) + + def test_insert_media(self): + self.assertFalse(self.sys_virtual_media._is_stale) + self.sys_virtual_media.insert_media( + "https://www.dmtf.org/freeImages/Sardine.img", True, False) + self.sys_virtual_media._conn.post.assert_called_once_with( + ("/redfish/v1/Managers/BMC/VirtualMedia/Floppy1/Actions" + "/VirtualMedia.InsertMedia"), + data={"Image": "https://www.dmtf.org/freeImages/Sardine.img", + "Inserted": True, "WriteProtected": False} + ) + self.assertTrue(self.sys_virtual_media._is_stale) + + def test_eject_media_none(self): + self.sys_virtual_media._actions.eject_media = None + self.assertRaisesRegex( + exceptions.MissingActionError, 'action #VirtualMedia.EjectMedia', + self.sys_virtual_media.eject_media) + + def test_eject_media(self): + self.assertFalse(self.sys_virtual_media._is_stale) + self.sys_virtual_media.eject_media() + self.sys_virtual_media._conn.post.assert_called_once_with( + ("/redfish/v1/Managers/BMC/VirtualMedia/Floppy1/Actions" + "/VirtualMedia.EjectMedia")) + self.assertTrue(self.sys_virtual_media._is_stale)