diff --git a/oneview_client/client.py b/oneview_client/client.py index 21360f0..90569d0 100644 --- a/oneview_client/client.py +++ b/oneview_client/client.py @@ -25,6 +25,7 @@ from oneview_client import exceptions from oneview_client import ilo_utils from oneview_client import managers from oneview_client import states +from oneview_client import utils SUPPORTED_ONEVIEW_VERSION = 200 @@ -237,6 +238,28 @@ class BaseClient(object): finally: ilo_utils.ilo_logout(host_ip, ilo_token) + def _set_onetime_boot(self, server_hardware_uuid, boot_device): + host_ip, ilo_token = self._get_ilo_access(server_hardware_uuid) + oneview_ilo_mapping = { + 'HardDisk': 'Hdd', + 'PXE': 'Pxe', + 'CD': 'Cd', + 'USB': 'Usb', + } + try: + ilo_device = oneview_ilo_mapping[boot_device] + return ilo_utils.set_onetime_boot(host_ip, ilo_token, + ilo_device, + self.allow_insecure_connections) + except exceptions.IloException as e: + raise e + except KeyError as e: + raise exceptions.IloException( + "Set one-time boot to %s is not supported." % boot_device + ) + finally: + ilo_utils.ilo_logout(host_ip, ilo_token) + class ClientV2(BaseClient): @@ -357,12 +380,32 @@ class Client(BaseClient): ) return server_profile.boot.get("order") - def set_boot_device(self, node_info, new_primary_boot_device): - boot_order = self.get_boot_order(node_info) - + def set_boot_device(self, node_info, new_primary_boot_device, + onetime=False): if new_primary_boot_device is None: raise exceptions.OneViewBootDeviceInvalidError() + boot_order = self.get_boot_order(node_info) + if new_primary_boot_device == boot_order[0]: + return + + if onetime: + try: + sh_uuid = utils.get_uuid_from_uri( + node_info['server_hardware_uri'] + ) + self._set_onetime_boot(sh_uuid, new_primary_boot_device) + return + except exceptions.IloException: + # Falls back to persistent in case of failure + pass + + self._persistent_set_boot_device(node_info, boot_order, + new_primary_boot_device) + + def _persistent_set_boot_device(self, node_info, boot_order, + new_primary_boot_device): + if new_primary_boot_device in boot_order: boot_order.remove(new_primary_boot_device) diff --git a/oneview_client/ilo_utils.py b/oneview_client/ilo_utils.py index 3466ae0..3831708 100644 --- a/oneview_client/ilo_utils.py +++ b/oneview_client/ilo_utils.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import requests from oneview_client import exceptions @@ -33,11 +34,20 @@ def rest_op(operation, host, suburi, request_headers, request_body, if operation == "GET": response = requests.get(url, headers=request_headers, verify=enforce_SSL) + return_value = (response.status_code, response.headers, + response.json()) elif operation == "DELETE": response = requests.delete(url, headers=request_headers, verify=enforce_SSL) + return_value = (response.status_code, response.headers, + response.json()) + elif operation == "PATCH": + response = requests.patch(url, data=json.dumps(request_body), + headers=request_headers, verify=enforce_SSL) + return_value = (response.status_code, response.headers, + response.json()) - return response.status_code, response.headers, response.json() + return return_value # REST GET @@ -47,6 +57,14 @@ def rest_get(host, suburi, request_headers, x_auth_token, enforce_SSL=True): # NOTE: be prepared for various HTTP responses including 500, 404, etc. +# REST PATCH +def rest_patch(host, suburi, request_headers, request_body, x_auth_token, + enforce_SSL=True): + return rest_op('PATCH', host, suburi, request_headers, request_body, + x_auth_token, enforce_SSL=enforce_SSL) + # NOTE: be prepared for various HTTP responses including 500, 404, etc. + + # REST DELETE def rest_delete(host, suburi, request_headers, x_auth_token, enforce_SSL=True): return rest_op('DELETE', host, suburi, request_headers, None, x_auth_token, @@ -67,7 +85,6 @@ def collection(host, collection_uri, request_headers, x_auth_token, enforce_SSL) while status < 300: - # verify expected type # NOTE: Because of the Redfish standards effort, we have versioned @@ -95,7 +112,6 @@ def collection(host, collection_uri, request_headers, x_auth_token, # vs. the Items in case a collection contains both. if 'Items' in thecollection: - # iterate items for item in thecollection['Items']: # if the item has a self uri pointer, supply that for @@ -143,12 +159,12 @@ def collection(host, collection_uri, request_headers, x_auth_token, raise exceptions.IloException("HTTP %s" % status) -def get_mac_from_ilo(host_ip, x_auth_token, nic_index=0): +def get_mac_from_ilo(host_ip, x_auth_token, nic_index=0, allow_insecure=False): # for each system in the systems collection at /rest/v1/Systems ilo_hardware = collection(host_ip, '/rest/v1/Systems', None, - x_auth_token, enforce_SSL=False) + x_auth_token, enforce_SSL=not allow_insecure) - for status, headers, system, memberuri in ilo_hardware: + for status, headers, system, member_uri in ilo_hardware: # verify expected type # hint: don't limit to version 0 here as we will rev to 1.0 at # some point hopefully with minimal changes @@ -164,12 +180,37 @@ def get_mac_from_ilo(host_ip, x_auth_token, nic_index=0): return system['HostCorrelation']['HostMACAddress'][nic_index] +def set_onetime_boot(host_ip, x_auth_token, boot_target, allow_insecure=False): + # for each system in the systems collection at /rest/v1/Systems + ilo_hardware = collection(host_ip, '/rest/v1/Systems', None, + x_auth_token, enforce_SSL=not allow_insecure) + + for status, headers, system, member_uri in ilo_hardware: + # verify expected type + # hint: don't limit to version 0 here as we will rev to 1.0 at + # some point hopefully with minimal changes + assert(get_type(system) == 'ComputerSystem.0' or + get_type(system) == 'ComputerSystem.1') + + if boot_target not in system["Boot"]["BootSourceOverrideSupported"]: + raise exceptions.IloException( + "ERROR: %s is not a supported boot option.\n" % boot_target) + else: + body = {"Boot": {"BootSourceOverrideTarget": boot_target}} + headers = {"Content-Type": "application/json"} + status_code, _, response = rest_patch(host_ip, member_uri, headers, + body, x_auth_token, + not allow_insecure) + if status_code != 200: + raise exceptions.IloException(response) + + def ilo_logout(host_ip, x_auth_token): sessions = collection(host_ip, '/rest/v1/sessions', None, x_auth_token, enforce_SSL=False) - for status, headers, member, memberuri in sessions: + for status, headers, member, member_uri in sessions: if member.get('Oem').get('Hp').get('MySession') is True: - status, headers, member = rest_delete(host_ip, memberuri, None, + status, headers, member = rest_delete(host_ip, member_uri, None, x_auth_token, False) if status != 200: message = "iLO returned HTTP %s" % status diff --git a/oneview_client/tests/functional/test_oneview_client.py b/oneview_client/tests/functional/test_oneview_client.py index 226f763..b1af2a6 100644 --- a/oneview_client/tests/functional/test_oneview_client.py +++ b/oneview_client/tests/functional/test_oneview_client.py @@ -22,6 +22,7 @@ import unittest from oneview_client import client from oneview_client import exceptions +from oneview_client import ilo_utils from oneview_client import models from oneview_client.tests import fixtures from oneview_client import utils @@ -287,6 +288,113 @@ class OneViewClientTestCase(unittest.TestCase): utils.get_uuid_from_uri(spt.get('uri')) ) + @mock.patch.object(client.Client, '_wait_for_task_to_complete') + @mock.patch.object(requests, 'get', autospec=True) + @mock.patch.object(requests, 'put', autospec=True) + def test_set_boot_device(self, mock_put, mock_get, mock__wait_for_task, + mock__authenticate): + oneview_client = client.Client(self.manager_url, + self.username, + self.password) + response = mock_put.return_value + response.status_code = http_client.OK + mock_put.return_value = response + + hardware = mock.MagicMock() + hardware.status_code = http_client.OK + hardware.json = mock.MagicMock( + return_value=fixtures.SERVER_HARDWARE_LIST_JSON['members'][0] + ) + profile = mock.MagicMock() + profile.status_code = http_client.OK + profile.json = mock.MagicMock( + return_value=fixtures.SERVER_PROFILE_JSON + ) + mock_get.side_effect = [hardware, profile, hardware, profile] + oneview_client._wait_for_task_to_complete = mock__wait_for_task + + node_info = { + 'server_hardware_uri': + '/rest/server-hardware/30303437-3933-4753-4831-31315835524E' + } + + oneview_client.set_boot_device(node_info, 'PXE') + mock_put.assert_called_once_with( + self.manager_url + fixtures.SERVER_PROFILE_JSON.get('uri'), + data=mock.ANY, + headers=mock.ANY, + verify=True + ) + self.assertIn('["PXE", "CD", "Floppy", "USB", "HardDisk"]', + mock_put.call_args[1]['data']) + + @mock.patch.object(ilo_utils, 'ilo_logout') + @mock.patch.object(client.Client, '_get_ilo_access') + @mock.patch.object(client.Client, '_wait_for_task_to_complete') + @mock.patch.object(requests, 'get', autospec=True) + @mock.patch.object(requests, 'patch', autospec=True) + def test_set_boot_device_onetime(self, mock_patch, mock_get, + mock__wait_for_task, mock_get_ilo_access, + mock_ilo_logout, mock__authenticate): + oneview_client = client.Client(self.manager_url, + self.username, + self.password) + + hardware = mock.MagicMock() + hardware.status_code = http_client.OK + hardware.json = mock.MagicMock( + return_value=fixtures.SERVER_HARDWARE_LIST_JSON['members'][0] + ) + profile = mock.MagicMock() + profile.status_code = http_client.OK + profile.json = mock.MagicMock( + return_value=fixtures.SERVER_PROFILE_JSON + ) + ilo_system = mock.MagicMock() + ilo_system.status_code = http_client.OK + ilo_system.json = mock.MagicMock( + return_value={ + "Type": "Collection.0", + "Items": [ + { + "links": {"self": {"href": "/rest/v1/Systems/1"}}, + "Type": "ComputerSystem.0", + "Boot": { + "BootSourceOverrideSupported": ["Hdd", "Cd"], + } + }, + ] + } + ) + mock_get.side_effect = [hardware, profile, # hardware, profile, + ilo_system] + + response2 = mock_patch.return_value + response2.status_code = http_client.OK + mock_patch.return_value = response2 + + my_host = 'my-host' + key = '123' + mock_get_ilo_access.return_value = (my_host, key) + + oneview_client._wait_for_task_to_complete = mock__wait_for_task + + node_info = { + 'server_hardware_uri': + '/rest/server-hardware/30303437-3933-4753-4831-31315835524E' + } + + oneview_client.set_boot_device(node_info, 'HardDisk', onetime=True) + mock_patch.assert_called_once_with( + 'https://' + my_host + '/rest/v1/Systems/1', + data='{"Boot": {"BootSourceOverrideTarget": "Hdd"}}', + headers={ + 'Content-Type': 'application/json', + 'X-Auth-Token': key}, + verify=True + ) + mock_ilo_logout.assert_called() + @mock.patch.object(client.ClientV2, '_authenticate', autospec=True) class OneViewClientV2TestCase(unittest.TestCase): diff --git a/oneview_client/tests/unit/test_oneview_client.py b/oneview_client/tests/unit/test_oneview_client.py index bf966a8..e61b34d 100644 --- a/oneview_client/tests/unit/test_oneview_client.py +++ b/oneview_client/tests/unit/test_oneview_client.py @@ -287,6 +287,7 @@ class OneViewClientTestCase(unittest.TestCase): self.oneview_client, uri="/rest/server-hardware/555" ) + @mock.patch.object(client.Client, '_set_onetime_boot') @mock.patch.object(client.Client, '_wait_for_task_to_complete', autospec=True) @mock.patch.object(client.Client, '_prepare_and_do_request', autospec=True) @@ -295,22 +296,25 @@ class OneViewClientTestCase(unittest.TestCase): @mock.patch.object(client.Client, 'get_server_hardware', autospec=True) def test_set_boot_device( self, mock_get_server_hardware, mock_get_server_profile, - mock__prepare_do_request, mock__wait_for_task + mock__prepare_do_request, mock__wait_for_task, mock_set_onetime_boot ): - mock_get_server_hardware.return_value = ( + + server_hardware = ( models.ServerHardware.from_json(fixtures.SERVER_HARDWARE_JSON) ) + mock_get_server_hardware.return_value = server_hardware mock_get_server_profile.return_value = ( models.ServerProfile.from_json(fixtures.SERVER_PROFILE_JSON) ) expected_profile = copy.deepcopy(fixtures.SERVER_PROFILE_JSON) expected_profile['boot'] = { 'manageBoot': True, - 'order': ["USB", "CD", "Floppy", "HardDisk", "PXE"] + # Original boot order is ["CD", "Floppy", "USB", "HardDisk", "PXE"] + 'order': ["PXE", "CD", "Floppy", "USB", "HardDisk"] } driver_info = {"server_hardware_uri": "/any"} - new_first_boot_device = "USB" - + new_first_boot_device = "PXE" + # Persistent self.oneview_client.set_boot_device(driver_info, new_first_boot_device) mock__prepare_do_request.assert_called_once_with( self.oneview_client, @@ -318,6 +322,32 @@ class OneViewClientTestCase(unittest.TestCase): request_type='PUT', uri='/rest/server-profiles/f2160e28-8107-45f9-b4b2-3119a622a3a1' ) + # Onetime + new_first_boot_device = "USB" + self.oneview_client.set_boot_device(driver_info, new_first_boot_device, + onetime=True) + mock_set_onetime_boot.assert_called_once_with( + 'any', + new_first_boot_device + ) + # Fallback in case onetime fails + new_first_boot_device = "HardDisk" + mock__prepare_do_request.reset_mock() + mock_set_onetime_boot.reset_mock() + expected_profile['boot'] = { + 'manageBoot': True, + # Boot order should be ["PXE", "CD", "Floppy", "USB", "HardDisk"] + 'order': ["HardDisk", "PXE", "CD", "Floppy", "USB"] + } + mock_set_onetime_boot.side_effect = [exceptions.IloException("BOOM")] + self.oneview_client.set_boot_device(driver_info, new_first_boot_device, + onetime=True) + mock__prepare_do_request.assert_called_once_with( + self.oneview_client, + body=expected_profile, + request_type='PUT', + uri='/rest/server-profiles/f2160e28-8107-45f9-b4b2-3119a622a3a1' + ) @mock.patch.object(client.Client, '_prepare_and_do_request', autospec=True) @mock.patch.object(client.Client, 'get_server_profile_from_hardware', @@ -356,23 +386,18 @@ class OneViewClientTestCase(unittest.TestCase): @mock.patch.object(client.Client, '_prepare_and_do_request', autospec=True) @mock.patch.object(client.Client, 'get_server_hardware', autospec=True) - @mock.patch.object(client.Client, 'get_boot_order', autospec=True) def test_get_server_profile_from_hardware( - self, mock_get_boot_order, mock_get_server_hardware, - mock__prepare_do_request + self, mock_get_server_hardware, mock__prepare_do_request ): driver_info = {} - new_first_boot_device = "any_boot_device" - mock_get_boot_order.return_value = [] server_hardware = models.ServerHardware() setattr(server_hardware, 'server_profile_uri', None) mock_get_server_hardware.return_value = server_hardware self.assertRaises( exceptions.OneViewServerProfileAssociatedError, - self.oneview_client.set_boot_device, - driver_info, - new_first_boot_device + self.oneview_client.get_server_profile_from_hardware, + driver_info ) setattr( server_hardware, @@ -384,9 +409,8 @@ class OneViewClientTestCase(unittest.TestCase): self.assertRaises( exceptions.OneViewResourceNotFoundError, - self.oneview_client.set_boot_device, - driver_info, - new_first_boot_device + self.oneview_client.get_server_profile_from_hardware, + driver_info ) @mock.patch.object(requests, 'get')