summaryrefslogtreecommitdiff
path: root/ironic/tests
diff options
context:
space:
mode:
authorCenne <cennedee+opendev@protonmail.com>2021-07-08 18:37:45 +0200
committerCenne <cennedee+opendev@protonmail.com>2021-08-23 19:38:58 +0200
commitbc95c92f7c122b1217459a1d7a125fae47749e6e (patch)
tree6ffd3cc271ac841e2a2a5fc72524ee2f1f625a37 /ironic/tests
parent9f32ceda1a87ac83d4ac84faec16c01ba27c549e (diff)
downloadironic-bc95c92f7c122b1217459a1d7a125fae47749e6e.tar.gz
Add api endpoints for changing boot_mode and secure_boot state
Done: - Node API endpoints expose - RPC methods - Conductor Manager methods - Conductor utils new methods - RBAC new policies - Node API tests - Manager Tests (+ some testing for utils methods) - RBAC tests - Docs (api-ref) - REST API version history - Releasenotes Story: 2008567 Task: 41709 Change-Id: I2d72389edf546b99c536c6b130ca85ababf80591
Diffstat (limited to 'ironic/tests')
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py150
-rw-r--r--ironic/tests/unit/api/test_rbac_legacy.yaml50
-rw-r--r--ironic/tests/unit/api/test_rbac_project_scoped.yaml104
-rw-r--r--ironic/tests/unit/api/test_rbac_system_scoped.yaml48
-rw-r--r--ironic/tests/unit/conductor/test_manager.py426
-rw-r--r--ironic/tests/unit/conductor/test_rpcapi.py15
6 files changed, 793 insertions, 0 deletions
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index 0ae598026..9b37a542f 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -35,6 +35,7 @@ from ironic.api.controllers.v1 import notification_utils
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import versions
from ironic.common import boot_devices
+from ironic.common import boot_modes
from ironic.common import components
from ironic.common import driver_factory
from ironic.common import exception
@@ -5136,6 +5137,14 @@ class TestPut(test_api_base.BaseApiTest):
autospec=True)
self.mock_cnps = p.start()
self.addCleanup(p.stop)
+ p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_boot_mode',
+ autospec=True)
+ self.mock_cnbm = p.start()
+ self.addCleanup(p.stop)
+ p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_secure_boot',
+ autospec=True)
+ self.mock_cnsb = p.start()
+ self.addCleanup(p.stop)
p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_deploy',
autospec=True)
self.mock_dnd = p.start()
@@ -5301,6 +5310,147 @@ class TestPut(test_api_base.BaseApiTest):
{'target': 'not-supported'}, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
+ def _test_boot_mode_success(self, target_state, api_version):
+
+ body = {'target': target_state}
+
+ if api_version is None:
+ response = self.put_json(
+ '/nodes/%s/states/boot_mode' % self.node.uuid, body)
+ else:
+ response = self.put_json(
+ '/nodes/%s/states/boot_mode' % self.node.uuid, body,
+ headers={api_base.Version.string: api_version})
+
+ self.assertEqual(http_client.ACCEPTED, response.status_code)
+ self.assertEqual(b'', response.body)
+ self.mock_cnbm.assert_called_once_with(mock.ANY,
+ mock.ANY,
+ self.node.uuid,
+ target_state,
+ topic='test-topic')
+ # Check location header
+ self.assertIsNotNone(response.location)
+ expected_location = '/v1/nodes/%s/states' % self.node.uuid
+ self.assertEqual(urlparse.urlparse(response.location).path,
+ expected_location)
+
+ def _test_boot_mode_failure(self, target_state, http_status_code,
+ api_version):
+
+ body = {'target': target_state}
+
+ if api_version is None:
+ response = self.put_json(
+ '/nodes/%s/states/boot_mode' % self.node.uuid, body,
+ expect_errors=True)
+ else:
+ response = self.put_json(
+ '/nodes/%s/states/boot_mode' % self.node.uuid, body,
+ headers={api_base.Version.string: api_version},
+ expect_errors=True)
+
+ self.assertEqual(http_status_code, response.status_code)
+ self.assertEqual('application/json', response.content_type)
+ self.assertTrue(response.json['error_message'])
+
+ def test_boot_mode_uefi_valid_soft_ver(self):
+ self._test_boot_mode_success(boot_modes.UEFI, "1.76")
+
+ def test_boot_mode_uefi_older_soft_ver(self):
+ self._test_boot_mode_failure(
+ boot_modes.UEFI, http_client.NOT_FOUND, "1.75")
+
+ def test_boot_mode_bios_valid_soft_ver(self):
+ self._test_boot_mode_success(boot_modes.LEGACY_BIOS, "1.76")
+
+ def test_boot_mode_bios_older_soft_ver(self):
+ self._test_boot_mode_failure(
+ boot_modes.LEGACY_BIOS, http_client.NOT_FOUND, "1.75")
+
+ def test_boot_mode_invalid_request(self):
+ self._test_boot_mode_failure(
+ 'unsupported-efi', http_client.BAD_REQUEST, "1.76")
+
+ def _test_secure_boot_success(self, target_state, api_version):
+
+ body = {'target': target_state}
+
+ if api_version is None:
+ response = self.put_json(
+ '/nodes/%s/states/secure_boot' % self.node.uuid, body)
+ else:
+ response = self.put_json(
+ '/nodes/%s/states/secure_boot' % self.node.uuid, body,
+ headers={api_base.Version.string: api_version})
+
+ self.assertEqual(http_client.ACCEPTED, response.status_code)
+ self.assertEqual(b'', response.body)
+ self.mock_cnsb.assert_called_once_with(mock.ANY,
+ mock.ANY,
+ self.node.uuid,
+ target_state,
+ topic='test-topic')
+ # Check location header
+ self.assertIsNotNone(response.location)
+ expected_location = '/v1/nodes/%s/states' % self.node.uuid
+ self.assertEqual(urlparse.urlparse(response.location).path,
+ expected_location)
+
+ def _test_secure_boot_failure(self, target_state, http_status_code,
+ api_version):
+
+ body = {'target': target_state}
+
+ if api_version is None:
+ response = self.put_json(
+ '/nodes/%s/states/secure_boot' % self.node.uuid, body,
+ expect_errors=True)
+ else:
+ response = self.put_json(
+ '/nodes/%s/states/secure_boot' % self.node.uuid, body,
+ headers={api_base.Version.string: api_version},
+ expect_errors=True)
+
+ self.assertEqual(http_status_code, response.status_code)
+ self.assertEqual('application/json', response.content_type)
+ self.assertTrue(response.json['error_message'])
+
+ def test_secure_boot_on_valid_soft_ver(self):
+ self._test_secure_boot_success(True, "1.76")
+
+ def test_secure_boot_on_older_soft_ver(self):
+ self._test_secure_boot_failure(
+ True, http_client.NOT_FOUND, "1.75")
+
+ def test_secure_boot_off_valid_soft_ver(self):
+ self._test_secure_boot_success(False, "1.76")
+
+ def test_secure_boot_off_older_soft_ver(self):
+ self._test_secure_boot_failure(
+ False, http_client.NOT_FOUND, "1.75")
+
+ def test_secure_boot_off_valid_undocumented_request_zero(self):
+ self._test_secure_boot_success(0, "1.76")
+
+ def test_secure_boot_on_valid_undocumented_request_one(self):
+ self._test_secure_boot_success(1, "1.76")
+
+ def test_secure_boot_on_invalid_request_two(self):
+ self._test_secure_boot_failure(2, http_client.BAD_REQUEST, "1.76")
+
+ def test_secure_boot_invalid_request_nullstr(self):
+ self._test_secure_boot_failure(
+ '', http_client.BAD_REQUEST, "1.76")
+
+ def test_secure_boot_invalid_request_boo(self):
+ self._test_secure_boot_failure(
+ 'boo!', http_client.BAD_REQUEST, "1.76")
+
+ def test_secure_boot_invalid_request_None(self):
+ self._test_secure_boot_failure(
+ None, http_client.BAD_REQUEST, "1.76")
+
def test_power_change_when_being_cleaned(self):
for state in (states.CLEANING, states.CLEANWAIT):
self.node.provision_state = state
diff --git a/ironic/tests/unit/api/test_rbac_legacy.yaml b/ironic/tests/unit/api/test_rbac_legacy.yaml
index 2f981e68f..a665d15fc 100644
--- a/ironic/tests/unit/api/test_rbac_legacy.yaml
+++ b/ironic/tests/unit/api/test_rbac_legacy.yaml
@@ -412,6 +412,56 @@ nodes_states_power_put_observer:
assert_status: 403
deprecated: true
+nodes_states_boot_mode_put_admin:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *admin_headers
+ body: &boot_mode_body
+ target: "uefi"
+ assert_status: 503
+ deprecated: true
+
+nodes_states_boot_mode_put_member:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *member_headers
+ body: *boot_mode_body
+ assert_status: 404
+ deprecated: true
+
+nodes_states_boot_mode_put_observer:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *observer_headers
+ body: *boot_mode_body
+ assert_status: 403
+ deprecated: true
+
+nodes_states_secure_boot_put_admin:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *admin_headers
+ body: &secure_boot_body
+ target: "true"
+ assert_status: 503
+ deprecated: true
+
+nodes_states_secure_boot_put_member:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *member_headers
+ body: *secure_boot_body
+ assert_status: 404
+ deprecated: true
+
+nodes_states_secure_boot_put_observer:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *observer_headers
+ body: *secure_boot_body
+ assert_status: 403
+ deprecated: true
+
nodes_states_provision_put_admin:
path: '/v1/nodes/{node_ident}/states/provision'
method: put
diff --git a/ironic/tests/unit/api/test_rbac_project_scoped.yaml b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
index 6ec74d2dd..81e7f646f 100644
--- a/ironic/tests/unit/api/test_rbac_project_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
@@ -863,6 +863,110 @@ third_party_admin_cannot_put_power_state_change:
body: *power_body
assert_status: 404
+# Boot mode state
+
+owner_admin_can_put_boot_mode_state_change:
+ path: '/v1/nodes/{owner_node_ident}/states/boot_mode'
+ method: put
+ headers: *owner_admin_headers
+ body: &boot_mode_body
+ target: "uefi"
+ assert_status: 503
+
+lessee_admin_can_put_boot_mode_state_change:
+ path: '/v1/nodes/{lessee_node_ident}/states/boot_mode'
+ method: put
+ headers: *lessee_admin_headers
+ body: *boot_mode_body
+ assert_status: 503
+
+owner_member_can_put_boot_mode_state_change:
+ path: '/v1/nodes/{owner_node_ident}/states/boot_mode'
+ method: put
+ headers: *owner_member_headers
+ body: *boot_mode_body
+ assert_status: 503
+
+lessee_member_can_put_boot_mode_state_change:
+ path: '/v1/nodes/{lessee_node_ident}/states/boot_mode'
+ method: put
+ headers: *lessee_member_headers
+ body: *boot_mode_body
+ assert_status: 503
+
+owner_reader_cannot_put_boot_mode_state_change:
+ path: '/v1/nodes/{owner_node_ident}/states/boot_mode'
+ method: put
+ headers: *owner_reader_headers
+ body: *boot_mode_body
+ assert_status: 403
+
+lessee_reader_cannot_put_boot_mode_state_change:
+ path: '/v1/nodes/{lessee_node_ident}/states/boot_mode'
+ method: put
+ headers: *lessee_reader_headers
+ body: *boot_mode_body
+ assert_status: 403
+
+third_party_admin_cannot_put_boot_mode_state_change:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *third_party_admin_headers
+ body: *boot_mode_body
+ assert_status: 404
+
+# Secure Boot state
+
+owner_admin_can_put_secure_boot_state_change:
+ path: '/v1/nodes/{owner_node_ident}/states/secure_boot'
+ method: put
+ headers: *owner_admin_headers
+ body: &secure_boot_body
+ target: "true"
+ assert_status: 503
+
+lessee_admin_can_put_secure_boot_state_change:
+ path: '/v1/nodes/{lessee_node_ident}/states/secure_boot'
+ method: put
+ headers: *lessee_admin_headers
+ body: *secure_boot_body
+ assert_status: 503
+
+owner_member_can_put_secure_boot_state_change:
+ path: '/v1/nodes/{owner_node_ident}/states/secure_boot'
+ method: put
+ headers: *owner_member_headers
+ body: *secure_boot_body
+ assert_status: 503
+
+lessee_member_can_put_secure_boot_state_change:
+ path: '/v1/nodes/{lessee_node_ident}/states/secure_boot'
+ method: put
+ headers: *lessee_member_headers
+ body: *secure_boot_body
+ assert_status: 503
+
+owner_reader_cannot_put_secure_boot_state_change:
+ path: '/v1/nodes/{owner_node_ident}/states/secure_boot'
+ method: put
+ headers: *owner_reader_headers
+ body: *secure_boot_body
+ assert_status: 403
+
+lessee_reader_cannot_put_secure_boot_state_change:
+ path: '/v1/nodes/{lessee_node_ident}/states/secure_boot'
+ method: put
+ headers: *lessee_reader_headers
+ body: *secure_boot_body
+ assert_status: 403
+
+third_party_admin_cannot_put_secure_boot_state_change:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *third_party_admin_headers
+ body: *secure_boot_body
+ assert_status: 404
+
# Provision states
owner_admin_can_change_provision_state:
diff --git a/ironic/tests/unit/api/test_rbac_system_scoped.yaml b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
index c0126b04d..032001fb2 100644
--- a/ironic/tests/unit/api/test_rbac_system_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
@@ -378,6 +378,54 @@ nodes_states_power_put_reader:
body: *power_body
assert_status: 403
+# Boot mode state
+
+nodes_states_boot_mode_put_admin:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *admin_headers
+ body: &boot_mode_body
+ target: "uefi"
+ assert_status: 503
+
+nodes_states_boot_mode_put_member:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *scoped_member_headers
+ body: *boot_mode_body
+ assert_status: 503
+
+nodes_states_boot_mode_put_reader:
+ path: '/v1/nodes/{node_ident}/states/boot_mode'
+ method: put
+ headers: *reader_headers
+ body: *boot_mode_body
+ assert_status: 403
+
+# Secure Boot state
+
+nodes_states_secure_boot_put_admin:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *admin_headers
+ body: &secure_boot_body
+ target: "true"
+ assert_status: 503
+
+nodes_states_secure_boot_put_member:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *scoped_member_headers
+ body: *secure_boot_body
+ assert_status: 503
+
+nodes_states_secure_boot_put_reader:
+ path: '/v1/nodes/{node_ident}/states/secure_boot'
+ method: put
+ headers: *reader_headers
+ body: *secure_boot_body
+ assert_status: 403
+
nodes_states_provision_put_admin:
path: '/v1/nodes/{node_ident}/states/provision'
method: put
diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py
index 897e19d3e..1603b19be 100644
--- a/ironic/tests/unit/conductor/test_manager.py
+++ b/ironic/tests/unit/conductor/test_manager.py
@@ -34,6 +34,7 @@ from oslo_versionedobjects import fields
import tenacity
from ironic.common import boot_devices
+from ironic.common import boot_modes
from ironic.common import components
from ironic.common import driver_factory
from ironic.common import exception
@@ -448,6 +449,431 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
@mgr_utils.mock_record_keepalive
+class ChangeNodeBootModeTestCase(mgr_utils.ServiceSetUpMixin,
+ db_base.DbTestCase):
+ @mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
+ def test_change_node_boot_mode_valid(self, get_boot_mock, set_boot_mock):
+ # Test change_node_boot_mode including integration with
+ # conductor.utils.node_change_boot_mode
+ get_boot_mock.side_effect = [boot_modes.LEGACY_BIOS, # before setting
+ boot_modes.UEFI] # after setting
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ boot_mode=boot_modes.LEGACY_BIOS)
+ self._start_service()
+
+ self.service.change_node_boot_mode(self.context,
+ node.uuid,
+ boot_modes.UEFI)
+ self._stop_service()
+
+ set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ mode=boot_modes.UEFI)
+ self.assertEqual(get_boot_mock.call_count, 1)
+ # Call once before setting to see if it's required
+
+ node.refresh()
+ self.assertEqual(boot_modes.UEFI, node.boot_mode)
+ self.assertIsNone(node.last_error)
+ # Verify the reservation has been cleared by
+ # background task's link callback.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
+ def test_change_node_boot_mode_existing(self, get_boot_mock,
+ set_boot_mock):
+ # Test change_node_boot_mode including integration with
+ # conductor.utils.node_change_boot_mode when target==current
+ get_boot_mock.return_value = boot_modes.LEGACY_BIOS
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ boot_mode=boot_modes.LEGACY_BIOS)
+ self._start_service()
+
+ self.service.change_node_boot_mode(self.context,
+ node.uuid,
+ boot_modes.LEGACY_BIOS)
+ self._stop_service()
+
+ set_boot_mock.assert_not_called()
+ self.assertEqual(get_boot_mock.call_count, 1)
+ # Called once before setting to see if it's even required
+
+ node.refresh()
+ self.assertEqual(boot_modes.LEGACY_BIOS, node.boot_mode)
+ self.assertIsNone(node.last_error)
+ # Verify the reservation has been cleared by
+ # background task's link callback.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(conductor_utils, 'node_change_boot_mode',
+ autospec=True)
+ def test_change_node_boot_mode_node_already_locked(self, ncbm_mock):
+ # Test change_node_boot_mode with mocked
+ # conductor.utils.node_change_boot_mode.
+ fake_reservation = 'fake-reserv'
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ boot_mode=boot_modes.LEGACY_BIOS,
+ reservation=fake_reservation)
+ self._start_service()
+
+ exc = self.assertRaises(messaging.rpc.ExpectedException,
+ self.service.change_node_boot_mode,
+ self.context,
+ node.uuid,
+ boot_modes.LEGACY_BIOS)
+ # Compare true exception hidden by @messaging.expected_exceptions
+ self.assertEqual(exception.NodeLocked, exc.exc_info[0])
+
+ # In this test worker should not be spawned, but waiting to make sure
+ # the below perform_mock assertion is valid.
+ self._stop_service()
+ self.assertFalse(ncbm_mock.called, 'node_change_boot_mode has been '
+ 'unexpectedly called.')
+ # Verify existing reservation wasn't broken.
+ node.refresh()
+ self.assertEqual(fake_reservation, node.reservation)
+
+ def test_change_node_boot_mode_worker_pool_full(self):
+ # Test change_node_boot_mode including integration with
+ # conductor.utils.change_node_boot_mode.
+ initial_state = boot_modes.LEGACY_BIOS
+ node = obj_utils.create_test_node(self.context, driver='fake-hardware',
+ boot_mode=initial_state)
+ self._start_service()
+
+ with mock.patch.object(self.service,
+ '_spawn_worker', autospec=True) as spawn_mock:
+ spawn_mock.side_effect = exception.NoFreeConductorWorker()
+
+ exc = self.assertRaises(messaging.rpc.ExpectedException,
+ self.service.change_node_boot_mode,
+ self.context,
+ node.uuid,
+ boot_modes.UEFI)
+ # Compare true exception hidden by @messaging.expected_exceptions
+ self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0])
+
+ spawn_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ mock.ANY)
+ node.refresh()
+ self.assertEqual(initial_state, node.boot_mode)
+ self.assertIsNotNone(node.last_error)
+ # Verify the picked reservation has been cleared due to full pool.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
+ def test_change_node_boot_mode_exception_in_background_task(
+ self, get_boot_mock, set_boot_mock):
+ # Test change_node_boot_mode including integration with
+ # conductor.utils.node_change_boot_mode.
+ initial_state = boot_modes.LEGACY_BIOS
+ node = obj_utils.create_test_node(self.context, driver='fake-hardware',
+ boot_mode=initial_state)
+ self._start_service()
+
+ get_boot_mock.return_value = boot_modes.LEGACY_BIOS
+ new_state = boot_modes.UEFI
+ set_boot_mock.side_effect = exception.UnsupportedDriverExtension(
+ driver=fake, extension='set_boot_mode')
+
+ self.service.change_node_boot_mode(self.context,
+ node.uuid,
+ new_state)
+ self._stop_service()
+
+ # Call once before setting to see if it was required
+ self.assertEqual(get_boot_mock.call_count, 1)
+ set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ new_state)
+ node.refresh()
+ self.assertEqual(initial_state, node.boot_mode)
+ self.assertIsNotNone(node.last_error)
+ # Verify the reservation has been cleared by background task's
+ # link callback despite exception in background task.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(fake.FakeManagement, 'validate', autospec=True)
+ def test_change_node_boot_mode_validate_fail(self, validate_mock):
+ # Test change_node_power_state where task.driver.management.validate
+ # fails
+ initial_state = boot_modes.UEFI
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ boot_mode=initial_state)
+ self._start_service()
+
+ validate_mock.side_effect = exception.InvalidParameterValue(
+ 'wrong management driver info')
+ new_state = boot_modes.LEGACY_BIOS
+ exc = self.assertRaises(messaging.rpc.ExpectedException,
+ self.service.change_node_boot_mode,
+ self.context,
+ node.uuid,
+ new_state)
+
+ self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
+
+ node.refresh()
+ validate_mock.assert_called_once_with(mock.ANY, mock.ANY)
+ self.assertEqual(initial_state, node.boot_mode)
+ self.assertIsNone(node.last_error)
+
+ @mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
+ def test_change_node_boot_mode_exception_getting_current(self,
+ get_boot_mock,
+ set_boot_mock):
+ # Test change_node_boot_mode smooth opertion when get_boot_mode mode
+ # raises an exception
+ initial_state = boot_modes.LEGACY_BIOS
+ node = obj_utils.create_test_node(self.context, driver='fake-hardware',
+ boot_mode=initial_state)
+ self._start_service()
+
+ get_boot_mock.side_effect = exception.UnsupportedDriverExtension(
+ driver=fake, extension='get_boot_mode')
+ new_state = boot_modes.UEFI
+
+ self.service.change_node_boot_mode(self.context,
+ node.uuid,
+ new_state)
+ self._stop_service()
+
+ # Call once before setting to see if it is required
+ self.assertEqual(get_boot_mock.call_count, 1)
+ set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ new_state)
+ node.refresh()
+ self.assertEqual(new_state, node.boot_mode)
+ self.assertIsNone(node.last_error)
+ # Verify the reservation has been cleared by
+ # background task's link callback.
+ self.assertIsNone(node.reservation)
+
+
+@mgr_utils.mock_record_keepalive
+class ChangeNodeSecureBootTestCase(mgr_utils.ServiceSetUpMixin,
+ db_base.DbTestCase):
+ @mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
+ autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
+ autospec=True)
+ def test_change_node_secure_boot_valid(self, get_boot_mock, set_boot_mock):
+ # Test change_node_secure_boot including integration with
+ # conductor.utils.node_change_secure_boot
+ get_boot_mock.side_effect = [False, # before setting
+ True] # after setting
+ initial_state = False
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ secure_boot=initial_state)
+ self._start_service()
+ target_state = True
+ self.service.change_node_secure_boot(self.context,
+ node.uuid,
+ target_state)
+ self._stop_service()
+
+ set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ target_state)
+ self.assertEqual(get_boot_mock.call_count, 1)
+ # Call once before setting to see if it's required
+
+ node.refresh()
+ self.assertEqual(target_state, node.secure_boot)
+ self.assertIsNone(node.last_error)
+ # Verify the reservation has been cleared by
+ # background task's link callback.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
+ autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
+ autospec=True)
+ def test_change_node_secure_boot_existing(self, get_boot_mock,
+ set_boot_mock):
+ # Test change_node_secure_boot including integration with
+ # conductor.utils.node_change_secure_boot when target==current
+ get_boot_mock.return_value = False
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ secure_boot=False)
+ self._start_service()
+
+ self.service.change_node_secure_boot(self.context,
+ node.uuid,
+ False)
+ self._stop_service()
+
+ set_boot_mock.assert_not_called()
+ self.assertEqual(get_boot_mock.call_count, 1)
+ # Called once before setting to see if it's even required
+
+ node.refresh()
+ self.assertEqual(False, node.secure_boot)
+ self.assertIsNone(node.last_error)
+ # Verify the reservation has been cleared by
+ # background task's link callback.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(conductor_utils, 'node_change_secure_boot',
+ autospec=True)
+ def test_change_node_secure_boot_node_already_locked(self, ncbm_mock):
+ # Test change_node_secure_boot with mocked
+ # conductor.utils.node_change_secure_boot.
+ fake_reservation = 'fake-reserv'
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ secure_boot=False,
+ reservation=fake_reservation)
+ self._start_service()
+
+ exc = self.assertRaises(messaging.rpc.ExpectedException,
+ self.service.change_node_secure_boot,
+ self.context,
+ node.uuid,
+ False)
+ # Compare true exception hidden by @messaging.expected_exceptions
+ self.assertEqual(exception.NodeLocked, exc.exc_info[0])
+
+ # In this test worker should not be spawned, but waiting to make sure
+ # the below perform_mock assertion is valid.
+ self._stop_service()
+ self.assertFalse(ncbm_mock.called, 'node_change_secure_boot has been '
+ 'unexpectedly called.')
+ # Verify existing reservation wasn't broken.
+ node.refresh()
+ self.assertEqual(fake_reservation, node.reservation)
+
+ def test_change_node_secure_boot_worker_pool_full(self):
+ # Test change_node_secure_boot including integration with
+ # conductor.utils.change_node_secure_boot.
+ initial_state = False
+ node = obj_utils.create_test_node(self.context, driver='fake-hardware',
+ secure_boot=initial_state)
+ self._start_service()
+
+ with mock.patch.object(self.service,
+ '_spawn_worker', autospec=True) as spawn_mock:
+ spawn_mock.side_effect = exception.NoFreeConductorWorker()
+
+ exc = self.assertRaises(messaging.rpc.ExpectedException,
+ self.service.change_node_secure_boot,
+ self.context,
+ node.uuid,
+ True)
+ # Compare true exception hidden by @messaging.expected_exceptions
+ self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0])
+
+ spawn_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ mock.ANY)
+ node.refresh()
+ self.assertEqual(initial_state, node.secure_boot)
+ self.assertIsNotNone(node.last_error)
+ # Verify the picked reservation has been cleared due to full pool.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
+ autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
+ autospec=True)
+ def test_change_node_secure_boot_exception_in_background_task(
+ self, get_boot_mock, set_boot_mock):
+ # Test change_node_secure_boot including integration with
+ # conductor.utils.node_change_secure_boot.
+ initial_state = False
+ node = obj_utils.create_test_node(self.context, driver='fake-hardware',
+ secure_boot=initial_state)
+ self._start_service()
+
+ get_boot_mock.return_value = False
+ new_state = True
+ set_boot_mock.side_effect = exception.UnsupportedDriverExtension(
+ driver=fake, extension='set_secure_boot_state')
+
+ self.service.change_node_secure_boot(self.context,
+ node.uuid,
+ new_state)
+ self._stop_service()
+
+ # Call once before setting to see if it was required
+ self.assertEqual(get_boot_mock.call_count, 1)
+ set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ new_state)
+ node.refresh()
+ self.assertEqual(initial_state, node.secure_boot)
+ self.assertIsNotNone(node.last_error)
+ # Verify the reservation has been cleared by background task's
+ # link callback despite exception in background task.
+ self.assertIsNone(node.reservation)
+
+ @mock.patch.object(fake.FakeManagement, 'validate', autospec=True)
+ def test_change_node_secure_boot_validate_fail(self, validate_mock):
+ # Test change_node_power_state where task.driver.management.validate
+ # fails
+ initial_state = True
+ node = obj_utils.create_test_node(self.context,
+ driver='fake-hardware',
+ secure_boot=initial_state)
+ self._start_service()
+
+ validate_mock.side_effect = exception.InvalidParameterValue(
+ 'wrong management driver info')
+ new_state = False
+ exc = self.assertRaises(messaging.rpc.ExpectedException,
+ self.service.change_node_secure_boot,
+ self.context,
+ node.uuid,
+ new_state)
+
+ self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
+
+ node.refresh()
+ validate_mock.assert_called_once_with(mock.ANY, mock.ANY)
+ self.assertEqual(initial_state, node.secure_boot)
+ self.assertIsNone(node.last_error)
+
+ @mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
+ autospec=True)
+ @mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
+ autospec=True)
+ def test_change_node_secure_boot_exception_getting_current(self,
+ get_boot_mock,
+ set_boot_mock):
+ # Test change_node_secure_boot smooth opertion when
+ # get_secure_boot_state raises an exception
+ initial_state = False
+ node = obj_utils.create_test_node(self.context, driver='fake-hardware',
+ secure_boot=initial_state)
+ self._start_service()
+
+ get_boot_mock.side_effect = exception.UnsupportedDriverExtension(
+ driver=fake, extension='get_secure_boot_state')
+ new_state = True
+
+ self.service.change_node_secure_boot(self.context,
+ node.uuid,
+ new_state)
+ self._stop_service()
+
+ # Call once before setting to see if it is required
+ self.assertEqual(get_boot_mock.call_count, 1)
+ set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
+ new_state)
+ node.refresh()
+ self.assertEqual(new_state, node.secure_boot)
+ self.assertIsNone(node.last_error)
+ # Verify the reservation has been cleared by
+ # background task's link callback.
+ self.assertIsNone(node.reservation)
+
+
+@mgr_utils.mock_record_keepalive
class CreateNodeTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test_create_node(self):
node = obj_utils.get_test_node(self.context, driver='fake-hardware',
diff --git a/ironic/tests/unit/conductor/test_rpcapi.py b/ironic/tests/unit/conductor/test_rpcapi.py
index 0c7763e83..314199498 100644
--- a/ironic/tests/unit/conductor/test_rpcapi.py
+++ b/ironic/tests/unit/conductor/test_rpcapi.py
@@ -26,6 +26,7 @@ import oslo_messaging as messaging
from oslo_messaging import _utils as messaging_utils
from ironic.common import boot_devices
+from ironic.common import boot_modes
from ironic.common import components
from ironic.common import exception
from ironic.common import indicator_states
@@ -301,6 +302,20 @@ class RPCAPITestCase(db_base.DbTestCase):
node_id=self.fake_node['uuid'],
new_state=states.POWER_ON)
+ def test_change_node_boot_mode(self):
+ self._test_rpcapi('change_node_boot_mode',
+ 'call',
+ version='1.55',
+ node_id=self.fake_node['uuid'],
+ new_state=boot_modes.LEGACY_BIOS)
+
+ def test_change_node_secure_boot(self):
+ self._test_rpcapi('change_node_secure_boot',
+ 'call',
+ version='1.55',
+ node_id=self.fake_node['uuid'],
+ new_state=True)
+
def test_vendor_passthru(self):
self._test_rpcapi('vendor_passthru',
'call',