Optimizations for setting boot device

By default, OneView doesn't provide on its API a facility to set one-time boot
on managed ServerHardware. This patch aims to allow it through bypassing OneView
and using iLO's REST interface directly. Also, some optimization is made to not
try to set again the boot device if the selected one is already the primary.

Change-Id: I1163048b6edf8778c3a84414804b73eef2c0fd5f
This commit is contained in:
Thiago Paiva 2016-07-11 19:12:09 -03:00
parent 2100accffd
commit dca44ab0dd
4 changed files with 243 additions and 27 deletions

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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')