diff --git a/shade/__init__.py b/shade/__init__.py index 0deb56821..6592b2fd7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3477,7 +3477,8 @@ class OperatorCloud(OpenStackCloud): except ironic_exceptions.ClientException: return None - def register_machine(self, nics, **kwargs): + def register_machine(self, nics, wait=False, timeout=3600, + lock_timeout=600, **kwargs): """Register Baremetal with Ironic Allows for the registration of Baremetal nodes with Ironic @@ -3501,6 +3502,18 @@ class OperatorCloud(OpenStackCloud): {'mac': 'aa:bb:cc:dd:ee:02'} ] + :param wait: Boolean value, defaulting to false, to wait for the + node to reach the available state where the node can be + provisioned. It must be noted, when set to false, the + method will still wait for locks to clear before sending + the next required command. + + :param timeout: Integer value, defautling to 3600 seconds, for the + wait state to reach completion. + + :param lock_timeout: Integer value, defaulting to 600 seconds, for + locks to clear. + :param kwargs: Key value pairs to be passed to the Ironic API, including uuid, name, chassis_uuid, driver_info, parameters. @@ -3511,8 +3524,9 @@ class OperatorCloud(OpenStackCloud): baremetal node. """ try: - machine = self.manager.submitTask( - _tasks.MachineCreate(**kwargs)) + machine = meta.obj_to_dict( + self.manager.submitTask(_tasks.MachineCreate(**kwargs))) + except Exception as e: self.log.debug("ironic machine registration failed", exc_info=True) raise OpenStackCloudException( @@ -3523,7 +3537,7 @@ class OperatorCloud(OpenStackCloud): for row in nics: nic = self.manager.submitTask( _tasks.MachinePortCreate(address=row['mac'], - node_uuid=machine.uuid)) + node_uuid=machine['uuid'])) created_nics.append(nic.uuid) except Exception as e: @@ -3539,11 +3553,77 @@ class OperatorCloud(OpenStackCloud): pass finally: self.manager.submitTask( - _tasks.MachineDelete(node_id=machine.uuid)) + _tasks.MachineDelete(node_id=machine['uuid'])) raise OpenStackCloudException( "Error registering NICs with the baremetal service: %s" % str(e)) - return meta.obj_to_dict(machine) + + try: + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "available state"): + + machine = self.get_machine(machine['uuid']) + + # Note(TheJulia): Per the Ironic state code, a node + # that fails returns to enroll state, which means a failed + # node cannot be determined at this point in time. + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state( + machine['uuid'], 'manage') + elif machine['provision_state'] in ['manageable']: + self.node_set_provision_state( + machine['uuid'], 'provide') + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + # Note(TheJulia): Earlier versions of Ironic default to + # None and later versions default to available up until + # the introduction of enroll state. + # Note(TheJulia): The node will transition through + # cleaning if it is enabled, and we will wait for + # completion. + elif machine['provision_state'] in ['available', None]: + break + + else: + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state(machine['uuid'], 'manage') + # Note(TheJulia): We need to wait for the lock to clear + # before we attempt to set the machine into provide state + # which allows for the transition to available. + for count in _utils._iterate_timeout( + lock_timeout, + "Timeout waiting for reservation to clear " + "before setting provide state"): + machine = self.get_machine(machine['uuid']) + if (machine['reservation'] is None and + machine['provision_state'] is not 'enroll'): + + self.node_set_provision_state( + machine['uuid'], 'provide') + machine = self.get_machine(machine['uuid']) + break + + elif machine['provision_state'] in [ + 'cleaning', + 'available']: + break + + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + except Exception as e: + raise OpenStackCloudException( + "Error transitioning node to available state: %s" + % e) + return machine def unregister_machine(self, nics, uuid): """Unregister Baremetal from Ironic diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 47ac46a3a..3d5a727e0 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -367,20 +367,155 @@ class TestShadeOperator(base.TestCase): def test_register_machine(self, mock_client): class fake_node: uuid = "00000000-0000-0000-0000-000000000000" + provision_state = "available" + reservation = None + last_error = None expected_return_value = dict( uuid="00000000-0000-0000-0000-000000000000", + provision_state="available", + reservation=None, + last_error=None ) mock_client.node.create.return_value = fake_node + mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] return_value = self.cloud.register_machine(nics) self.assertDictEqual(expected_return_value, return_value) self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) + self.assertFalse(mock_client.node.get.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'node_set_provision_state') + def test_register_machine_enroll( + self, + mock_set_state, + mock_client): + machine_uuid = "00000000-0000-0000-0000-000000000000" + + class fake_node_init_state: + uuid = machine_uuid + provision_state = "enroll" + reservation = None + last_error = None + + class fake_node_post_manage: + uuid = machine_uuid + provision_state = "enroll" + reservation = "do you have a flag?" + last_error = None + + class fake_node_post_manage_done: + uuid = machine_uuid + provision_state = "manage" + reservation = None + last_error = None + + class fake_node_post_provide: + uuid = machine_uuid + provision_state = "available" + reservation = None + last_error = None + + class fake_node_post_enroll_failure: + uuid = machine_uuid + provision_state = "enroll" + reservation = None + last_error = "insufficent lolcats" + + expected_return_value = dict( + uuid=machine_uuid, + provision_state="available", + reservation=None, + last_error=None + ) + + mock_client.node.get.side_effect = iter([ + fake_node_init_state, + fake_node_post_manage, + fake_node_post_manage_done, + fake_node_post_provide]) + mock_client.node.create.return_value = fake_node_init_state + nics = [{'mac': '00:00:00:00:00:00'}] + return_value = self.cloud.register_machine(nics) + self.assertDictEqual(expected_return_value, return_value) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) + mock_client.reset_mock() + mock_client.node.get.side_effect = iter([ + fake_node_init_state, + fake_node_post_manage, + fake_node_post_manage_done, + fake_node_post_provide]) + return_value = self.cloud.register_machine(nics, wait=True) + self.assertDictEqual(expected_return_value, return_value) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) + mock_client.reset_mock() + mock_client.node.get.side_effect = iter([ + fake_node_init_state, + fake_node_post_manage, + fake_node_post_enroll_failure]) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics, + wait=True) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'node_set_provision_state') + def test_register_machine_enroll_timeout( + self, + mock_set_state, + mock_client): + machine_uuid = "00000000-0000-0000-0000-000000000000" + + class fake_node_init_state: + uuid = machine_uuid + provision_state = "enroll" + reservation = "do you have a flag?" + last_error = None + + mock_client.node.get.return_value = fake_node_init_state + mock_client.node.create.return_value = fake_node_init_state + nics = [{'mac': '00:00:00:00:00:00'}] + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics, + lock_timeout=0.001) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) + mock_client.node.get.reset_mock() + mock_client.node.create.reset_mock() + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics, + wait=True, + timeout=0.001) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine_port_create_failed(self, mock_client): + class fake_node: + uuid = "00000000-0000-0000-0000-000000000000" + provision_state = "available" + resevation = None + last_error = None + nics = [{'mac': '00:00:00:00:00:00'}] + mock_client.node.create.return_value = fake_node mock_client.port.create.side_effect = ( exc.OpenStackCloudException("Error")) self.assertRaises(exc.OpenStackCloudException,