diff --git a/README.rst b/README.rst index f999cbd..06be061 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,9 @@ Installation pip install --user metalsmith +.. note:: + The current versions of *metalsmith* require Bare Metal API from the Stein + release or newer. Use the 0.11 release series for older versions. Contributing ------------ diff --git a/lower-constraints.txt b/lower-constraints.txt index 8bf064c..36e08b6 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -5,7 +5,7 @@ fixtures==3.0.0 flake8-import-order==0.13 hacking==1.0.0 mock==2.0 -openstacksdk==0.22.0 +openstacksdk==0.25.0 pbr==2.0.0 Pygments==2.2.0 requests==2.18.4 diff --git a/metalsmith/_instance.py b/metalsmith/_instance.py index ef27e09..9a5f00f 100644 --- a/metalsmith/_instance.py +++ b/metalsmith/_instance.py @@ -86,10 +86,16 @@ _DEPLOYED_STATES = frozenset([InstanceState.ACTIVE, InstanceState.MAINTENANCE]) class Instance(object): """Instance status in metalsmith.""" - def __init__(self, connection, node): + def __init__(self, connection, node, allocation=None): self._connection = connection self._uuid = node.id self._node = node + self._allocation = allocation + + @property + def allocation(self): + """Allocation object associated with the node (if any).""" + return self._allocation @property def hostname(self): @@ -163,6 +169,8 @@ class Instance(object): def to_dict(self): """Convert instance to a dict.""" return { + 'allocation': (self._allocation.to_dict() + if self._allocation is not None else None), 'hostname': self.hostname, 'ip_addresses': self.ip_addresses(), 'node': self._node.to_dict(), diff --git a/metalsmith/_provisioner.py b/metalsmith/_provisioner.py index 240ea50..e5a73cb 100644 --- a/metalsmith/_provisioner.py +++ b/metalsmith/_provisioner.py @@ -332,6 +332,7 @@ class Provisioner(_utils.GetNodeMixin): else: # Update the node to return it's latest state node = self._get_node(node, refresh=True) + # We don't create allocations yet, so don't use _get_instance. instance = _instance.Instance(self.connection, node) return instance @@ -358,7 +359,9 @@ class Provisioner(_utils.GetNodeMixin): nodes = [self._get_node(n, accept_hostname=True) for n in nodes] nodes = self.connection.baremetal.wait_for_nodes_provision_state( nodes, 'active', timeout=timeout) - return [_instance.Instance(self.connection, node) for node in nodes] + # Using _get_instance in case the deployment started by something + # external that uses allocations. + return [self._get_instance(node) for node in nodes] def _clean_instance_info(self, instance_info): return {key: value @@ -426,6 +429,16 @@ class Provisioner(_utils.GetNodeMixin): """ return self.show_instances([instance_id])[0] + def _get_instance(self, ident): + node = self._get_node(ident, accept_hostname=True) + if node.allocation_id: + allocation = self.connection.baremetal.get_allocation( + node.allocation_id) + else: + allocation = None + return _instance.Instance(self.connection, node, + allocation=allocation) + def show_instances(self, instances): """Show information about instance. @@ -439,12 +452,7 @@ class Provisioner(_utils.GetNodeMixin): if one of the instances are not valid instances. """ with self._cache_node_list_for_lookup(): - result = [ - _instance.Instance( - self.connection, - self._get_node(inst, accept_hostname=True)) - for inst in instances - ] + result = [self._get_instance(inst) for inst in instances] # NOTE(dtantsur): do not accept node names as valid instances if they # are not deployed or being deployed. missing = [inst for (res, inst) in zip(result, instances) @@ -461,8 +469,6 @@ class Provisioner(_utils.GetNodeMixin): :return: list of :py:class:`metalsmith.Instance` objects. """ nodes = self.connection.baremetal.nodes(associated=True, details=True) - instances = [i for i in - (_instance.Instance(self.connection, node) - for node in nodes) + instances = [i for i in map(self._get_instance, nodes) if i.state != _instance.InstanceState.UNKNOWN] return instances diff --git a/metalsmith/test/test_instance.py b/metalsmith/test/test_instance.py index 8b91d4a..0ac5097 100644 --- a/metalsmith/test/test_instance.py +++ b/metalsmith/test/test_instance.py @@ -129,7 +129,28 @@ class TestInstanceStates(test_provisioner.Base): mock_ips.return_value = {'private': ['1.2.3.4']} to_dict = self.instance.to_dict() - self.assertEqual({'hostname': 'host', + self.assertEqual({'allocation': None, + 'hostname': 'host', + 'ip_addresses': {'private': ['1.2.3.4']}, + 'node': {'node': 'dict'}, + 'state': 'deploying', + 'uuid': self.node.id}, + to_dict) + # States are converted to strings + self.assertIsInstance(to_dict['state'], six.string_types) + + @mock.patch.object(_instance.Instance, 'ip_addresses', autospec=True) + def test_to_dict_with_allocation(self, mock_ips): + self.node.provision_state = 'wait call-back' + self.node.to_dict.return_value = {'node': 'dict'} + self.node.instance_info = {'metalsmith_hostname': 'host'} + mock_ips.return_value = {'private': ['1.2.3.4']} + self.instance._allocation = mock.Mock() + self.instance._allocation.to_dict.return_value = {'alloc': 'dict'} + + to_dict = self.instance.to_dict() + self.assertEqual({'allocation': {'alloc': 'dict'}, + 'hostname': 'host', 'ip_addresses': {'private': ['1.2.3.4']}, 'node': {'node': 'dict'}, 'state': 'deploying', diff --git a/metalsmith/test/test_provisioner.py b/metalsmith/test/test_provisioner.py index c6ffc38..bfdd578 100644 --- a/metalsmith/test/test_provisioner.py +++ b/metalsmith/test/test_provisioner.py @@ -29,7 +29,8 @@ from metalsmith import sources NODE_FIELDS = ['name', 'id', 'instance_info', 'instance_id', 'is_maintenance', 'maintenance_reason', 'properties', 'provision_state', 'extra', - 'last_error', 'traits', 'resource_class', 'conductor_group'] + 'last_error', 'traits', 'resource_class', 'conductor_group', + 'allocation_id'] class TestInit(testtools.TestCase): @@ -66,7 +67,8 @@ class Base(testtools.TestCase): id='000', instance_id=None, properties={'local_gb': 100}, instance_info={}, - is_maintenance=False, extra={}) + is_maintenance=False, extra={}, + allocation_id=None) self.node.name = 'control-0' def _reset_api_mock(self): @@ -1386,6 +1388,19 @@ class TestShowInstance(Base): self.assertIs(inst.node, self.node) self.assertIs(inst.uuid, self.node.id) + def test_show_instance_with_allocation(self): + self.node.allocation_id = 'id2' + self.mock_get_node.side_effect = lambda n, *a, **kw: self.node + inst = self.pr.show_instance('id1') + self.mock_get_node.assert_called_once_with(self.pr, 'id1', + accept_hostname=True) + self.api.baremetal.get_allocation.assert_called_once_with('id2') + self.assertIsInstance(inst, _instance.Instance) + self.assertIs(inst.allocation, + self.api.baremetal.get_allocation.return_value) + self.assertIs(inst.node, self.node) + self.assertIs(inst.uuid, self.node.id) + def test_show_instances(self): self.mock_get_node.side_effect = [self.node, self.node] result = self.pr.show_instances(['1', '2']) @@ -1415,10 +1430,11 @@ class TestListInstances(Base): super(TestListInstances, self).setUp() self.nodes = [ mock.Mock(spec=NODE_FIELDS, provision_state=state, - instance_id='1234') + instance_id='1234', allocation_id=None) for state in ('active', 'active', 'deploying', 'wait call-back', 'deploy failed', 'available', 'available', 'enroll') ] + self.nodes[0].allocation_id = 'id2' self.nodes[6].instance_id = None self.api.baremetal.nodes.return_value = self.nodes @@ -1426,5 +1442,8 @@ class TestListInstances(Base): instances = self.pr.list_instances() self.assertTrue(isinstance(i, _instance.Instance) for i in instances) self.assertEqual(self.nodes[:6], [i.node for i in instances]) + self.assertEqual([self.api.baremetal.get_allocation.return_value] + + [None] * 5, + [i.allocation for i in instances]) self.api.baremetal.nodes.assert_called_once_with(associated=True, details=True) diff --git a/requirements.txt b/requirements.txt index 41abe2d..60d9205 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -openstacksdk>=0.22.0 # Apache-2.0 +openstacksdk>=0.25.0 # Apache-2.0 requests>=2.18.4 # Apache-2.0 six>=1.10.0 # MIT enum34>=1.0.4;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD