diff options
author | Cenne <cennedee+opendev@protonmail.com> | 2021-07-08 18:37:45 +0200 |
---|---|---|
committer | Cenne <cennedee+opendev@protonmail.com> | 2021-08-23 19:38:58 +0200 |
commit | bc95c92f7c122b1217459a1d7a125fae47749e6e (patch) | |
tree | 6ffd3cc271ac841e2a2a5fc72524ee2f1f625a37 /ironic/tests | |
parent | 9f32ceda1a87ac83d4ac84faec16c01ba27c549e (diff) | |
download | ironic-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.py | 150 | ||||
-rw-r--r-- | ironic/tests/unit/api/test_rbac_legacy.yaml | 50 | ||||
-rw-r--r-- | ironic/tests/unit/api/test_rbac_project_scoped.yaml | 104 | ||||
-rw-r--r-- | ironic/tests/unit/api/test_rbac_system_scoped.yaml | 48 | ||||
-rw-r--r-- | ironic/tests/unit/conductor/test_manager.py | 426 | ||||
-rw-r--r-- | ironic/tests/unit/conductor/test_rpcapi.py | 15 |
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', |