Switch to allocation API instead of reservations via instance_id
Change-Id: I45882ddd18d2a91db9c3592c6ed527676b91091b
This commit is contained in:
parent
0161effc3a
commit
814611f022
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue