Switch to allocation API instead of reservations via instance_id

Change-Id: I45882ddd18d2a91db9c3592c6ed527676b91091b
This commit is contained in:
Dmitry Tantsur 2019-05-16 13:01:24 +02:00
parent 0161effc3a
commit 814611f022
5 changed files with 392 additions and 413 deletions

View File

@ -14,11 +14,11 @@
# limitations under the License.
import logging
import random
import sys
import warnings
from openstack import connection
from openstack import exceptions as os_exc
import six
from metalsmith import _config
@ -97,48 +97,115 @@ class Provisioner(_utils.GetNodeMixin):
capabilities = capabilities or {}
self._check_hostname(hostname)
if candidates or capabilities or conductor_group or predicate:
# Predicates, capabilities and conductor groups are not supported
# by the allocation API natively, so we need to use prefiltering.
candidates = self._prefilter_nodes(resource_class,
conductor_group=conductor_group,
capabilities=capabilities,
candidates=candidates,
predicate=predicate)
node = self._reserve_node(resource_class, hostname=hostname,
candidates=candidates, traits=traits,
capabilities=capabilities)
return node
def _prefilter_nodes(self, resource_class, conductor_group, capabilities,
candidates, predicate):
"""Build a list of candidate nodes for allocation."""
if candidates:
nodes = [self._get_node(node) for node in candidates]
else:
kwargs = {}
if conductor_group:
kwargs['conductor_group'] = conductor_group
nodes = list(self.connection.baremetal.nodes(
details=True,
associated=False,
provision_state='available',
maintenance=False,
resource_class=resource_class,
details=True,
**kwargs))
conductor_group=conductor_group))
if not nodes:
raise exceptions.NodesNotFound(resource_class, conductor_group)
# Ensure parallel executions don't try nodes in the same sequence
random.shuffle(nodes)
LOG.debug('Candidate nodes: %s', nodes)
filters = [
_scheduler.NodeTypeFilter(resource_class, conductor_group),
_scheduler.CapabilitiesFilter(capabilities),
_scheduler.TraitsFilter(traits),
]
if capabilities:
filters.append(_scheduler.CapabilitiesFilter(capabilities))
if predicate is not None:
filters.append(_scheduler.CustomPredicateFilter(predicate))
instance_info = {}
if capabilities:
instance_info['capabilities'] = capabilities
if traits:
instance_info['traits'] = traits
reserver = _scheduler.IronicReserver(self.connection,
instance_info,
hostname)
return _scheduler.run_filters(filters, nodes)
def _reserve_node(self, resource_class, hostname=None, candidates=None,
traits=None, capabilities=None,
update_instance_info=True):
"""Create an allocation with given parameters."""
if candidates:
candidates = [
(node.id if not isinstance(node, six.string_types) else node)
for node in candidates
]
LOG.debug('Creating an allocation for resource class %(rsc)s '
'with traits %(traits)s and candidate nodes %(candidates)s',
{'rsc': resource_class, 'traits': traits,
'candidates': candidates})
allocation = self.connection.baremetal.create_allocation(
name=hostname, candidate_nodes=candidates,
resource_class=resource_class, traits=traits)
node = None
try:
try:
allocation = self.connection.baremetal.wait_for_allocation(
allocation)
except os_exc.SDKException as exc:
# Re-raise the expected exception class
raise exceptions.ReservationFailed(
'Failed to reserve a node: %s' % exc)
LOG.info('Successful allocation %(alloc)s for host %(host)s',
{'alloc': allocation, 'host': hostname})
node = self.connection.baremetal.get_node(allocation.node_id)
if update_instance_info:
node = self._patch_reserved_node(node, allocation, hostname,
capabilities)
except Exception as exc:
exc_info = sys.exc_info()
try:
LOG.error('Processing allocation %(alloc)s for node %(node)s '
'failed: %(exc)s; deleting allocation',
{'alloc': _utils.log_res(allocation),
'node': _utils.log_res(node), 'exc': exc})
self.connection.baremetal.delete_allocation(allocation)
except Exception:
LOG.exception('Failed to delete failed allocation')
six.reraise(*exc_info)
node = _scheduler.schedule_node(nodes, filters, reserver,
dry_run=self._dry_run)
LOG.debug('Reserved node: %s', node)
return node
def _patch_reserved_node(self, node, allocation, hostname, capabilities):
"""Make required updates on a newly reserved node."""
if not hostname:
hostname = _utils.default_hostname(node)
patch = [
{'path': '/instance_info/%s' % _utils.HOSTNAME_FIELD,
'op': 'add', 'value': hostname}
]
if capabilities:
patch.append({'path': '/instance_info/capabilities',
'op': 'add', 'value': capabilities})
LOG.debug('Patching reserved node %(node)s with %(patch)s',
{'node': _utils.log_res(node), 'patch': patch})
return self.connection.baremetal.patch_node(node, patch)
def _check_node_for_deploy(self, node):
"""Check that node is ready and reserve it if needed.
@ -154,12 +221,20 @@ class Provisioner(_utils.GetNodeMixin):
{'node': node, 'exc': exc})
if not node.instance_id:
if not node.resource_class:
raise exceptions.InvalidNode(
'Cannot create an allocation for node %s that '
'does not have a resource class set'
% _utils.log_res(node))
if not self._dry_run:
LOG.debug('Node %s not reserved yet, reserving',
_utils.log_res(node))
self.connection.baremetal.update_node(
node, instance_id=node.id)
elif node.instance_id != node.id:
# Not updating instance_info since it will be updated later
node = self._reserve_node(node.resource_class,
candidates=[node.id],
update_instance_info=False)
elif node.instance_id != node.id and not node.allocation_id:
raise exceptions.InvalidNode('Node %(node)s already reserved '
'by instance %(inst)s outside of '
'metalsmith, cannot deploy on it' %
@ -197,7 +272,7 @@ class Provisioner(_utils.GetNodeMixin):
def provision_node(self, node, image, nics=None, root_size_gb=None,
swap_size_mb=None, config=None, hostname=None,
netboot=False, capabilities=None, traits=None,
wait=None):
wait=None, clean_up_on_failure=True):
"""Provision the node with the given image.
Example::
@ -331,8 +406,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)
instance = self._get_instance(node)
return instance
@ -379,14 +453,32 @@ class Provisioner(_utils.GetNodeMixin):
extra = node.extra.copy()
for item in (_CREATED_PORTS, _ATTACHED_PORTS):
extra.pop(item, None)
kwargs = {}
if node.allocation_id and node.provision_state != 'active':
# Try to remove allocation (it will fail for active nodes)
LOG.debug('Trying to remove allocation %(alloc)s for node '
'%(node)s', {'alloc': node.allocation_id,
'node': _utils.log_res(node)})
try:
self.connection.baremetal.delete_allocation(node.allocation_id)
except Exception as exc:
LOG.debug('Failed to remove allocation %(alloc)s for %(node)s:'
' %(exc)s',
{'alloc': node.allocaiton_id,
'node': _utils.log_res(node), 'exc': exc})
elif not node.allocation_id:
# Old-style reservations have to be cleared explicitly
kwargs['instance_id'] = None
LOG.debug('Updating node %(node)s with empty instance info (was '
'%(iinfo)s) and extras %(extra)s and releasing the lock',
'%(iinfo)s) and extras %(extra)s',
{'node': _utils.log_res(node),
'iinfo': node.instance_info,
'extra': extra})
try:
self.connection.baremetal.update_node(
node, instance_info={}, extra=extra, instance_id=None)
node, instance_info={}, extra=extra, **kwargs)
except Exception as exc:
LOG.debug('Failed to clear node %(node)s extra: %(exc)s',
{'node': _utils.log_res(node), 'exc': exc})

View File

@ -17,7 +17,6 @@ import abc
import collections
import logging
from openstack import exceptions as sdk_exc
import six
from metalsmith import _utils
@ -47,38 +46,14 @@ class Filter(object):
"""
@six.add_metaclass(abc.ABCMeta)
class Reserver(object):
"""Base class for reservers."""
def run_filters(filters, nodes):
"""Filter the node list by provided filters.
@abc.abstractmethod
def __call__(self, node):
"""Reserve this node.
:param node: Node object.
:return: updated Node object if it was reserved
:raises: any Exception to indicate that the next node should be tried
"""
@abc.abstractmethod
def fail(self):
"""Fail reservation because no nodes are left.
Must raise an exception.
"""
def schedule_node(nodes, filters, reserver, dry_run=False):
"""Schedule one node.
:param nodes: List of input nodes.
:param filters: List of callable Filter objects to filter/validate nodes.
They are called in passes. If a pass yields no nodes, an error is
raised.
:param reserver: A callable Reserver object. Must return the updated node
or raise an exception.
:param dry_run: If True, reserver is not actually called.
:return: The resulting node
:param nodes: List of input nodes.
:return: The resulting nodes
"""
for f in filters:
f_name = f.__class__.__name__
@ -93,26 +68,7 @@ def schedule_node(nodes, filters, reserver, dry_run=False):
LOG.debug('Filter %(filter)s yielded %(count)d node(s)',
{'filter': f_name, 'count': len(nodes)})
if dry_run:
LOG.debug('Dry run, not reserving any nodes')
return nodes[0]
for node in nodes:
try:
result = reserver(node)
except sdk_exc.SDKException as exc:
LOG.debug('Node %(node)s was not reserved (%(exc)s), moving on '
'to the next one',
{'node': _utils.log_res(node), 'exc': exc})
else:
LOG.info('Node %s reserved for deployment',
_utils.log_res(result))
return result
LOG.debug('No nodes could be reserved')
reserver.fail()
assert False, "BUG: %s.fail did not raise" % reserver.__class__.__name__
return nodes
class NodeTypeFilter(Filter):
@ -206,41 +162,6 @@ class CapabilitiesFilter(Filter):
raise exceptions.CapabilitiesNotFound(message, self._capabilities)
class TraitsFilter(Filter):
"""Filter that checks traits."""
def __init__(self, traits):
self._traits = traits
self._counter = collections.Counter()
def __call__(self, node):
if not self._traits:
return True
traits = node.traits or []
LOG.debug('Traits for node %(node)s: %(traits)s',
{'node': _utils.log_res(node), 'traits': traits})
for trait in traits:
self._counter[trait] += 1
missing = set(self._traits) - set(traits)
if missing:
LOG.debug('Node %(node)s does not have traits %(missing)s',
{'node': _utils.log_res(node), 'missing': missing})
return False
return True
def fail(self):
existing = ", ".join("%s (%d node(s))" % item
for item in self._counter.items())
requested = ', '.join(self._traits)
message = ("No available nodes found with traits %(req)s, "
"existing traits: %(exist)s" %
{'req': requested, 'exist': existing or 'none'})
raise exceptions.TraitsNotFound(message, self._traits)
class CustomPredicateFilter(Filter):
def __init__(self, predicate):
@ -257,42 +178,3 @@ class CustomPredicateFilter(Filter):
def fail(self):
message = 'No nodes satisfied the custom predicate %s' % self.predicate
raise exceptions.CustomPredicateFailed(message, self._failed_nodes)
class IronicReserver(Reserver):
def __init__(self, connection, instance_info=None, hostname=None):
self._connection = connection
self._failed_nodes = []
self._iinfo = instance_info or {}
self.hostname = hostname
def validate(self, node):
try:
self._connection.baremetal.validate_node(
node, required=('power', 'management'))
except sdk_exc.SDKException as exc:
message = ('Node %(node)s failed validation: %(err)s' %
{'node': _utils.log_res(node), 'err': exc})
LOG.warning(message)
raise exceptions.ValidationFailed(message)
def _get_hostname(self, node):
if self.hostname is None:
return _utils.default_hostname(node)
else:
return self.hostname
def __call__(self, node):
try:
self.validate(node)
iinfo = dict(node.instance_info or {}, **self._iinfo)
iinfo[_utils.HOSTNAME_FIELD] = self._get_hostname(node)
return self._connection.baremetal.update_node(
node, instance_id=node.id, instance_info=iinfo)
except sdk_exc.SDKException:
self._failed_nodes.append(node)
raise
def fail(self):
raise exceptions.NoNodesReserved(self._failed_nodes)

View File

@ -23,7 +23,9 @@ from metalsmith import exceptions
def log_res(res):
if getattr(res, 'name', None):
if res is None:
return None
elif getattr(res, 'name', None):
return '%s (UUID %s)' % (res.name, res.id)
else:
return res.id

View File

@ -84,6 +84,7 @@ class Base(testtools.TestCase):
autospec=True))
self.api = mock.Mock(spec=['image', 'network', 'baremetal'])
self.api.baremetal.update_node.side_effect = lambda n, **kw: n
self.api.baremetal.patch_node.side_effect = lambda n, _p: n
self.api.network.ports.return_value = [
mock.Mock(spec=['id'], id=i) for i in ('000', '111')
]
@ -113,41 +114,92 @@ class TestReserveNode(Base):
self.api.baremetal.nodes.return_value = []
self.assertRaises(exceptions.NodesNotFound,
self.pr.reserve_node, self.RSC)
self.pr.reserve_node, self.RSC,
conductor_group='foo')
self.assertFalse(self.api.baremetal.update_node.called)
def test_simple_ok(self):
nodes = [self._node()]
self.api.baremetal.nodes.return_value = nodes
expected = self._node()
self.api.baremetal.get_node.return_value = expected
node = self.pr.reserve_node(self.RSC)
self.assertIn(node, nodes)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'metalsmith_hostname': node.id})
self.assertIs(expected, node)
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=None,
resource_class=self.RSC, traits=None)
self.api.baremetal.wait_for_allocation.assert_called_once_with(
self.api.baremetal.create_allocation.return_value)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_allocation_failed(self):
self.api.baremetal.wait_for_allocation.side_effect = (
os_exc.SDKException('boom'))
self.assertRaisesRegex(exceptions.ReservationFailed, 'boom',
self.pr.reserve_node, self.RSC)
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=None,
resource_class=self.RSC, traits=None)
self.api.baremetal.delete_allocation.assert_called_once_with(
self.api.baremetal.create_allocation.return_value)
self.assertFalse(self.api.baremetal.patch_node.called)
def test_node_update_failed(self):
expected = self._node()
self.api.baremetal.get_node.return_value = expected
self.api.baremetal.patch_node.side_effect = os_exc.SDKException('boom')
self.assertRaisesRegex(os_exc.SDKException, 'boom',
self.pr.reserve_node, self.RSC)
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=None,
resource_class=self.RSC, traits=None)
self.api.baremetal.delete_allocation.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value)
self.api.baremetal.patch_node.assert_called_once_with(
expected, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': expected.id}])
def test_with_hostname(self):
nodes = [self._node()]
self.api.baremetal.nodes.return_value = nodes
expected = self._node()
self.api.baremetal.get_node.return_value = expected
self.api.baremetal.nodes.return_value = [expected, self._node()]
node = self.pr.reserve_node(self.RSC, hostname='example.com')
self.assertIn(node, nodes)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'metalsmith_hostname': 'example.com'})
self.assertIs(expected, node)
self.api.baremetal.create_allocation.assert_called_once_with(
name='example.com', candidate_nodes=None,
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': 'example.com'}])
def test_with_name_as_hostname(self):
nodes = [self._node(name='example.com')]
self.api.baremetal.nodes.return_value = nodes
expected = self._node(name='example.com')
self.api.baremetal.get_node.return_value = expected
self.api.baremetal.nodes.return_value = [expected, self._node()]
node = self.pr.reserve_node(self.RSC)
self.assertIn(node, nodes)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'metalsmith_hostname': 'example.com'})
self.assertIs(expected, node)
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=None,
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': 'example.com'}])
def test_with_capabilities(self):
nodes = [
@ -156,43 +208,53 @@ class TestReserveNode(Base):
]
expected = nodes[1]
self.api.baremetal.nodes.return_value = nodes
self.api.baremetal.get_node.return_value = expected
node = self.pr.reserve_node(self.RSC, capabilities={'answer': '42'})
self.assertIs(node, expected)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'capabilities': {'answer': '42'},
'metalsmith_hostname': node.id})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[expected.id],
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id},
{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'answer': '42'}}])
def test_with_traits(self):
nodes = [self._node(properties={'local_gb': 100}, traits=traits)
for traits in [['foo', 'answer:1'], ['answer:42', 'foo'],
['answer'], None]]
expected = nodes[1]
self.api.baremetal.nodes.return_value = nodes
expected = self._node(properties={'local_gb': 100},
traits=['foo', 'answer:42'])
self.api.baremetal.get_node.return_value = expected
node = self.pr.reserve_node(self.RSC, traits=['foo', 'answer:42'])
self.assertIs(node, expected)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'traits': ['foo', 'answer:42'],
'metalsmith_hostname': node.id})
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_custom_predicate(self):
nodes = [self._node(properties={'local_gb': i})
for i in (100, 150, 200)]
self.api.baremetal.nodes.return_value = nodes[:]
self.api.baremetal.get_node.return_value = nodes[1]
node = self.pr.reserve_node(
self.RSC,
predicate=lambda node: 100 < node.properties['local_gb'] < 200)
self.assertEqual(node, nodes[1])
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'metalsmith_hostname': node.id})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[nodes[1].id],
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_custom_predicate_false(self):
nodes = [self._node() for _ in range(3)]
@ -208,25 +270,37 @@ class TestReserveNode(Base):
def test_provided_node(self):
nodes = [self._node()]
self.api.baremetal.get_node.return_value = nodes[0]
node = self.pr.reserve_node(self.RSC, candidates=nodes)
self.assertEqual(node, nodes[0])
self.assertFalse(self.api.baremetal.nodes.called)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'metalsmith_hostname': node.id})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[nodes[0].id],
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_provided_nodes(self):
nodes = [self._node(), self._node()]
nodes = [self._node(id=1), self._node(id=2)]
self.api.baremetal.get_node.return_value = nodes[0]
node = self.pr.reserve_node(self.RSC, candidates=nodes)
self.assertEqual(node, nodes[0])
self.assertFalse(self.api.baremetal.nodes.called)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'metalsmith_hostname': node.id})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[1, 2],
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_nodes_filtered(self):
nodes = [self._node(resource_class='banana'),
@ -234,16 +308,23 @@ class TestReserveNode(Base):
self._node(properties={'local_gb': 100,
'capabilities': 'cat:meow'},
resource_class='compute')]
self.api.baremetal.get_node.return_value = nodes[2]
node = self.pr.reserve_node('compute', candidates=nodes,
capabilities={'cat': 'meow'})
self.assertEqual(node, nodes[2])
self.assertFalse(self.api.baremetal.nodes.called)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'capabilities': {'cat': 'meow'},
'metalsmith_hostname': node.id})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[nodes[0].id],
resource_class='compute', traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id},
{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'cat': 'meow'}}])
def test_nodes_filtered_by_conductor_group(self):
nodes = [self._node(conductor_group='loc1'),
@ -253,6 +334,7 @@ class TestReserveNode(Base):
self._node(properties={'local_gb': 100,
'capabilities': 'cat:meow'},
conductor_group='loc1')]
self.api.baremetal.get_node.return_value = nodes[2]
node = self.pr.reserve_node(self.RSC,
conductor_group='loc1',
@ -261,10 +343,16 @@ class TestReserveNode(Base):
self.assertEqual(node, nodes[2])
self.assertFalse(self.api.baremetal.nodes.called)
self.api.baremetal.update_node.assert_called_once_with(
node, instance_id=node.id,
instance_info={'capabilities': {'cat': 'meow'},
'metalsmith_hostname': node.id})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[nodes[2].id],
resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id},
{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'cat': 'meow'}}])
def test_provided_nodes_no_match(self):
nodes = [
@ -468,18 +556,21 @@ class TestProvisionNode(Base):
def test_unreserved(self):
self.node.instance_id = None
self.api.baremetal.get_node.return_value = self.node
self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[self.node.id],
resource_class=self.node.resource_class, traits=None)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id)
self.api.baremetal.attach_vif_to_node.assert_called_once_with(
self.node, self.api.network.create_port.return_value.id)
self.api.baremetal.update_node.assert_has_calls([
mock.call(self.node, instance_id=self.node.id),
mock.call(self.node, instance_info=self.instance_info,
extra=self.extra)
])
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info=self.instance_info, extra=self.extra)
self.api.baremetal.validate_node.assert_called_once_with(self.node)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
@ -956,6 +1047,32 @@ abcd image
]
self.api.baremetal.detach_vif_from_node.assert_has_calls(
calls, any_order=True)
self.assertFalse(self.api.baremetal.delete_allocation.called)
def test_deploy_failure_with_allocation(self):
self.node.allocation_id = 'id2'
self.api.baremetal.set_node_provision_state.side_effect = (
RuntimeError('boom'))
self.assertRaisesRegex(RuntimeError, 'boom',
self.pr.provision_node, self.node,
'image', [{'network': 'n1'}, {'port': 'p1'}],
wait=3600)
self.api.baremetal.update_node.assert_any_call(
self.node, extra={}, instance_info={})
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.api.network.delete_port.assert_called_once_with(
self.api.network.create_port.return_value.id,
ignore_missing=False)
calls = [
mock.call(self.node,
self.api.network.create_port.return_value.id),
mock.call(self.node, self.api.network.find_port.return_value.id)
]
self.api.baremetal.detach_vif_from_node.assert_has_calls(
calls, any_order=True)
self.api.baremetal.delete_allocation.assert_called_once_with('id2')
def test_port_creation_failure(self):
self.api.network.create_port.side_effect = RuntimeError('boom')
@ -1310,8 +1427,86 @@ abcd and-not-image-again
class TestUnprovisionNode(Base):
def test_ok(self):
def setUp(self):
super(TestUnprovisionNode, self).setUp()
self.node.extra['metalsmith_created_ports'] = ['port1']
self.node.allocation_id = '123'
self.node.provision_state = 'active'
def test_ok(self):
# Check that unrelated extra fields are not touched.
self.node.extra['foo'] = 'bar'
result = self.pr.unprovision_node(self.node)
self.assertIs(result, self.node)
self.api.network.delete_port.assert_called_once_with(
'port1', ignore_missing=False)
self.api.baremetal.detach_vif_from_node.assert_called_once_with(
self.node, 'port1')
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'deleted', wait=False)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={'foo': 'bar'})
self.assertFalse(self.api.baremetal.delete_allocation.called)
# We cannot delete an allocation for an active node, it will be deleted
# automatically.
self.assertFalse(self.api.baremetal.delete_allocation.called)
def test_delete_allocation(self):
self.node.provision_state = 'deploy failed'
# Check that unrelated extra fields are not touched.
self.node.extra['foo'] = 'bar'
result = self.pr.unprovision_node(self.node)
self.assertIs(result, self.node)
self.api.network.delete_port.assert_called_once_with(
'port1', ignore_missing=False)
self.api.baremetal.detach_vif_from_node.assert_called_once_with(
self.node, 'port1')
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'deleted', wait=False)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={'foo': 'bar'})
self.api.baremetal.delete_allocation.assert_called_once_with('123')
def test_with_attached(self):
self.node.extra['metalsmith_attached_ports'] = ['port1', 'port2']
self.pr.unprovision_node(self.node)
self.api.network.delete_port.assert_called_once_with(
'port1', ignore_missing=False)
calls = [mock.call(self.node, 'port1'), mock.call(self.node, 'port2')]
self.api.baremetal.detach_vif_from_node.assert_has_calls(
calls, any_order=True)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'deleted', wait=False)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={})
def test_with_wait(self):
result = self.pr.unprovision_node(self.node, wait=3600)
self.assertIs(result, self.node)
self.api.network.delete_port.assert_called_once_with(
'port1', ignore_missing=False)
self.api.baremetal.detach_vif_from_node.assert_called_once_with(
self.node, 'port1')
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'deleted', wait=False)
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={})
wait_mock = self.api.baremetal.wait_for_nodes_provision_state
wait_mock.assert_called_once_with([self.node], 'available',
timeout=3600)
def test_without_allocation(self):
self.node.allocation_id = None
# Check that unrelated extra fields are not touched.
self.node.extra['foo'] = 'bar'
result = self.pr.unprovision_node(self.node)
@ -1328,44 +1523,10 @@ class TestUnprovisionNode(Base):
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={'foo': 'bar'},
instance_id=None)
def test_with_attached(self):
self.node.extra['metalsmith_created_ports'] = ['port1']
self.node.extra['metalsmith_attached_ports'] = ['port1', 'port2']
self.pr.unprovision_node(self.node)
self.api.network.delete_port.assert_called_once_with(
'port1', ignore_missing=False)
calls = [mock.call(self.node, 'port1'), mock.call(self.node, 'port2')]
self.api.baremetal.detach_vif_from_node.assert_has_calls(
calls, any_order=True)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'deleted', wait=False)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={}, instance_id=None)
def test_with_wait(self):
self.node.extra['metalsmith_created_ports'] = ['port1']
result = self.pr.unprovision_node(self.node, wait=3600)
self.assertIs(result, self.node)
self.api.network.delete_port.assert_called_once_with(
'port1', ignore_missing=False)
self.api.baremetal.detach_vif_from_node.assert_called_once_with(
self.node, 'port1')
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'deleted', wait=False)
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info={}, extra={}, instance_id=None)
wait_mock = self.api.baremetal.wait_for_nodes_provision_state
wait_mock.assert_called_once_with([self.node], 'available',
timeout=3600)
self.assertFalse(self.api.baremetal.delete_allocation.called)
def test_dry_run(self):
self.pr._dry_run = True
self.node.extra['metalsmith_created_ports'] = ['port1']
self.pr.unprovision_node(self.node)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)

View File

@ -14,28 +14,17 @@
# limitations under the License.
import mock
from openstack import exceptions as sdk_exc
import testtools
from metalsmith import _scheduler
from metalsmith import exceptions
class TestScheduleNode(testtools.TestCase):
class TestRunFilters(testtools.TestCase):
def setUp(self):
super(TestScheduleNode, self).setUp()
super(TestRunFilters, self).setUp()
self.nodes = [mock.Mock(spec=['id', 'name']) for _ in range(2)]
self.reserver = self._reserver(lambda x: x)
def _reserver(self, side_effect):
reserver = mock.Mock(spec=_scheduler.Reserver)
reserver.side_effect = side_effect
if isinstance(side_effect, Exception):
reserver.fail.side_effect = exceptions.ReservationFailed('fail')
else:
reserver.fail.side_effect = AssertionError('called fail')
return reserver
def _filter(self, side_effect, fail=AssertionError('called fail')):
fltr = mock.Mock(spec=_scheduler.Filter)
@ -44,39 +33,13 @@ class TestScheduleNode(testtools.TestCase):
return fltr
def test_no_filters(self):
result = _scheduler.schedule_node(self.nodes, [], self.reserver)
self.assertIs(result, self.nodes[0])
self.reserver.assert_called_once_with(self.nodes[0])
self.assertFalse(self.reserver.fail.called)
def test_dry_run(self):
result = _scheduler.schedule_node(self.nodes, [], self.reserver,
dry_run=True)
self.assertIs(result, self.nodes[0])
self.assertFalse(self.reserver.called)
self.assertFalse(self.reserver.fail.called)
def test_reservation_one_failed(self):
reserver = self._reserver([sdk_exc.SDKException("boom"),
self.nodes[1]])
result = _scheduler.schedule_node(self.nodes, [], reserver)
self.assertIs(result, self.nodes[1])
self.assertEqual([mock.call(n) for n in self.nodes],
reserver.call_args_list)
def test_reservation_all_failed(self):
reserver = self._reserver(sdk_exc.SDKException("boom"))
self.assertRaisesRegex(exceptions.ReservationFailed, 'fail',
_scheduler.schedule_node,
self.nodes, [], reserver)
self.assertEqual([mock.call(n) for n in self.nodes],
reserver.call_args_list)
result = _scheduler.run_filters([], self.nodes)
self.assertEqual(result, self.nodes)
def test_all_filters_pass(self):
filters = [self._filter([True, True]) for _ in range(3)]
result = _scheduler.schedule_node(self.nodes, filters, self.reserver)
self.assertIs(result, self.nodes[0])
self.reserver.assert_called_once_with(self.nodes[0])
result = _scheduler.run_filters(filters, self.nodes)
self.assertEqual(result, self.nodes)
for fltr in filters:
self.assertEqual([mock.call(n) for n in self.nodes],
fltr.call_args_list)
@ -86,9 +49,8 @@ class TestScheduleNode(testtools.TestCase):
filters = [self._filter([True, True]),
self._filter([False, True]),
self._filter([True])]
result = _scheduler.schedule_node(self.nodes, filters, self.reserver)
self.assertIs(result, self.nodes[1])
self.reserver.assert_called_once_with(self.nodes[1])
result = _scheduler.run_filters(filters, self.nodes)
self.assertEqual(result, self.nodes[1:2])
for fltr in filters:
self.assertFalse(fltr.fail.called)
for fltr in filters[:2]:
@ -101,9 +63,8 @@ class TestScheduleNode(testtools.TestCase):
self._filter([False, True]),
self._filter([False], fail=RuntimeError('failed'))]
self.assertRaisesRegex(RuntimeError, 'failed',
_scheduler.schedule_node,
self.nodes, filters, self.reserver)
self.assertFalse(self.reserver.called)
_scheduler.run_filters,
filters, self.nodes)
for fltr in filters[:2]:
self.assertEqual([mock.call(n) for n in self.nodes],
fltr.call_args_list)
@ -164,122 +125,3 @@ class TestCapabilitiesFilter(testtools.TestCase):
'No available nodes found with capabilities '
'profile=compute, existing capabilities: none',
fltr.fail)
class TestTraitsFilter(testtools.TestCase):
def test_fail_no_traits(self):
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
self.assertRaisesRegex(exceptions.TraitsNotFound,
'No available nodes found with traits '
'tr1, tr2, existing traits: none',
fltr.fail)
def test_no_traits(self):
fltr = _scheduler.TraitsFilter([])
node = mock.Mock(spec=['name', 'id'])
self.assertTrue(fltr(node))
def test_ok(self):
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
node = mock.Mock(spec=['name', 'id', 'traits'],
traits=['tr3', 'tr2', 'tr1'])
self.assertTrue(fltr(node))
def test_missing_one(self):
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
node = mock.Mock(spec=['name', 'id', 'traits'],
traits=['tr3', 'tr1'])
self.assertFalse(fltr(node))
def test_missing_all(self):
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
node = mock.Mock(spec=['name', 'id', 'traits'], traits=None)
self.assertFalse(fltr(node))
class TestIronicReserver(testtools.TestCase):
def setUp(self):
super(TestIronicReserver, self).setUp()
self.node = mock.Mock(spec=['id', 'name', 'instance_info'],
instance_info={})
self.node.id = 'abcd'
self.node.name = None
self.api = mock.Mock(spec=['baremetal'])
self.api.baremetal = mock.Mock(spec=['update_node', 'validate_node'])
self.api.baremetal.update_node.side_effect = (
lambda node, **kw: node)
self.reserver = _scheduler.IronicReserver(self.api)
def test_fail(self):
self.assertRaisesRegex(exceptions.NoNodesReserved,
'All the candidate nodes are already reserved',
self.reserver.fail)
def test_ok(self):
self.assertEqual(self.node, self.reserver(self.node))
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_id=self.node.id,
instance_info={'metalsmith_hostname': 'abcd'})
def test_name_as_hostname(self):
self.node.name = 'example.com'
self.assertEqual(self.node, self.reserver(self.node))
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_id=self.node.id,
instance_info={'metalsmith_hostname': 'example.com'})
def test_name_cannot_be_hostname(self):
# This should not ever happen, but checking just in case
self.node.name = 'banana!'
self.assertEqual(self.node, self.reserver(self.node))
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_id=self.node.id,
instance_info={'metalsmith_hostname': 'abcd'})
def test_with_instance_info(self):
self.reserver = _scheduler.IronicReserver(self.api,
{'cat': 'meow'})
self.assertEqual(self.node, self.reserver(self.node))
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_id=self.node.id,
instance_info={'cat': 'meow', 'metalsmith_hostname': 'abcd'})
def test_with_hostname(self):
self.reserver = _scheduler.IronicReserver(self.api,
hostname='example.com')
self.assertEqual(self.node, self.reserver(self.node))
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_id=self.node.id,
instance_info={'metalsmith_hostname': 'example.com'})
def test_reservation_failed(self):
self.api.baremetal.update_node.side_effect = (
sdk_exc.SDKException('conflict'))
self.assertRaisesRegex(sdk_exc.SDKException, 'conflict',
self.reserver, self.node)
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.api.baremetal.update_node.assert_called_once_with(
self.node, instance_id=self.node.id,
instance_info={'metalsmith_hostname': 'abcd'})
def test_validation_failed(self):
self.api.baremetal.validate_node.side_effect = (
sdk_exc.SDKException('fail'))
self.assertRaisesRegex(exceptions.ValidationFailed, 'fail',
self.reserver, self.node)
self.api.baremetal.validate_node.assert_called_with(
self.node, required=('power', 'management'))
self.assertFalse(self.api.baremetal.update_node.called)