Use allocation name for hostname instead of a custom field

This concludes the switch to the allocation API.

Change-Id: I25cdae7d17604140f728fdbcfea4110cbd222679
This commit is contained in:
Dmitry Tantsur 2019-06-03 08:39:54 +02:00
parent b1769b01cc
commit e9c25b02e5
12 changed files with 635 additions and 396 deletions

View File

@ -5,7 +5,7 @@ fixtures==3.0.0
flake8-import-order==0.13
hacking==1.0.0
mock==2.0
openstacksdk==0.28.0
openstacksdk==0.29.0
pbr==2.0.0
Pygments==2.2.0
requests==2.18.4

View File

@ -82,7 +82,7 @@ class Instance(object):
@property
def hostname(self):
"""Node's hostname."""
return self._node.instance_info.get(_utils.HOSTNAME_FIELD)
return _utils.hostname_for(self._node, self._allocation)
def ip_addresses(self):
"""Returns IP addresses for this instance.

View File

@ -33,11 +33,10 @@ LOG = logging.getLogger(__name__)
_CREATED_PORTS = 'metalsmith_created_ports'
_ATTACHED_PORTS = 'metalsmith_attached_ports'
_PRESERVE_INSTANCE_INFO_KEYS = {'capabilities', 'traits',
_utils.HOSTNAME_FIELD}
_PRESERVE_INSTANCE_INFO_KEYS = {'capabilities', 'traits'}
class Provisioner(_utils.GetNodeMixin):
class Provisioner(object):
"""API to deploy/undeploy nodes with OpenStack.
:param session: `Session` object (from ``keystoneauth``) to use when
@ -94,7 +93,7 @@ class Provisioner(_utils.GetNodeMixin):
:raises: :py:class:`metalsmith.exceptions.ReservationFailed`
"""
capabilities = capabilities or {}
self._check_hostname(hostname)
_utils.check_hostname(hostname)
if candidates or capabilities or conductor_group or predicate:
# Predicates, capabilities and conductor groups are not supported
@ -107,7 +106,7 @@ class Provisioner(_utils.GetNodeMixin):
node = self._reserve_node(resource_class, hostname=hostname,
candidates=candidates, traits=traits,
capabilities=capabilities)
capabilities=capabilities)[0]
return node
def _prefilter_nodes(self, resource_class, conductor_group, capabilities,
@ -186,26 +185,20 @@ class Provisioner(_utils.GetNodeMixin):
six.reraise(*exc_info)
LOG.debug('Reserved node: %s', node)
return node
return node, allocation
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})
patch = [{'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)
else:
return node
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):
def _check_node_for_deploy(self, node, hostname):
"""Check that node is ready and reserve it if needed.
These checks are done outside of the try..except block in
@ -213,33 +206,6 @@ class Provisioner(_utils.GetNodeMixin):
Particularly, we don't want to try clean up nodes that were not
reserved by us or are in maintenance mode.
"""
try:
node = self._get_node(node)
except Exception as exc:
raise exceptions.InvalidNode('Cannot find node %(node)s: %(exc)s' %
{'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))
# 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' %
{'node': _utils.log_res(node),
'inst': node.instance_id})
if node.is_maintenance:
raise exceptions.InvalidNode('Refusing to deploy on node %(node)s '
'which is in maintenance mode due to '
@ -247,26 +213,98 @@ class Provisioner(_utils.GetNodeMixin):
{'node': _utils.log_res(node),
'reason': node.maintenance_reason})
return node
allocation = None
def _check_hostname(self, hostname, node=None):
"""Check the provided host name.
# Make sure the hostname does not correspond to an existing allocation
# for another node.
if hostname is not None:
allocation = self._check_allocation_for_hostname(node, hostname)
:raises: ValueError on inappropriate value of ``hostname``
"""
if hostname is None:
if node.allocation_id:
if allocation is None:
# Previously created allocation, verify/update it
allocation = self._check_and_update_allocation_for_node(
node, hostname)
elif node.instance_id:
# Old-style reservations with instance_uuid==node.uuid
if node.instance_id != node.id:
raise exceptions.InvalidNode(
'Node %(node)s already reserved by instance %(inst)s '
'outside of metalsmith, cannot deploy on it' %
{'node': _utils.log_res(node), 'inst': node.instance_id})
elif hostname:
# We have no way to update hostname without allocations
raise exceptions.InvalidNode(
'Node %s does not use allocations, cannot update '
'hostname for it' % _utils.log_res(node))
else:
# Node is not reserved at all - reserve it
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:
if not hostname:
hostname = _utils.default_hostname(node)
LOG.debug('Node %(node)s is not reserved yet, reserving for '
'hostname %(host)s',
{'node': _utils.log_res(node),
'host': hostname})
# Not updating instance_info since it will be updated later
node, allocation = self._reserve_node(
node.resource_class,
hostname=hostname,
candidates=[node.id],
update_instance_info=False)
return node, allocation
def _check_allocation_for_hostname(self, node, hostname):
try:
allocation = self.connection.baremetal.get_allocation(
hostname)
except os_exc.ResourceNotFound:
return
if not _utils.is_hostname_safe(hostname):
raise ValueError("%s cannot be used as a hostname" % hostname)
existing = self._find_node_by_hostname(hostname)
if (existing is not None and node is not None
and existing.id != node.id):
raise ValueError("The following node already uses hostname "
"%(host)s: %(node)s" %
if allocation.node_id and allocation.node_id != node.id:
raise ValueError("The following node already uses "
"hostname %(host)s: %(node)s" %
{'host': hostname,
'node': _utils.log_res(existing)})
'node': allocation.node_id})
else:
return allocation
def _check_and_update_allocation_for_node(self, node, hostname=None):
# No allocation with given hostname, find one corresponding to the
# node.
allocation = self.connection.baremetal.get_allocation(
node.allocation_id)
if allocation.name and hostname and allocation.name != hostname:
# Prevent updating of an existing hostname, since we don't
# understand the intention
raise exceptions.InvalidNode(
"Allocation %(alloc)s associated with node %(node)s "
"uses hostname %(old)s that does not match the expected "
"hostname %(new)s" %
{'alloc': _utils.log_res(allocation),
'node': _utils.log_res(node),
'old': allocation.name,
'new': hostname})
elif not allocation.name and not self._dry_run:
if not hostname:
hostname = _utils.default_hostname(node)
# Set the hostname that was not set in reserve_node.
LOG.debug('Updating allocation %(alloc)s for node '
'%(node)s with hostname %(host)s',
{'alloc': _utils.log_res(allocation),
'node': _utils.log_res(node),
'host': hostname})
allocation = self.connection.baremetal.update_allocation(
allocation, name=hostname)
return allocation
def provision_node(self, node, image, nics=None, root_size_gb=None,
swap_size_mb=None, config=None, hostname=None,
@ -331,11 +369,18 @@ class Provisioner(_utils.GetNodeMixin):
if isinstance(image, six.string_types):
image = sources.GlanceImage(image)
node = self._check_node_for_deploy(node)
_utils.check_hostname(hostname)
try:
node = self._get_node(node)
except Exception as exc:
raise exceptions.InvalidNode('Cannot find node %(node)s: %(exc)s' %
{'node': node, 'exc': exc})
node, allocation = self._check_node_for_deploy(node, hostname)
nics = _nics.NICs(self.connection, node, nics)
try:
self._check_hostname(hostname, node=node)
root_size_gb = _utils.get_root_disk(root_size_gb, node)
image._validate(self.connection)
@ -357,11 +402,6 @@ class Provisioner(_utils.GetNodeMixin):
instance_info = self._clean_instance_info(node.instance_info)
instance_info['root_gb'] = root_size_gb
instance_info['capabilities'] = capabilities
if hostname:
instance_info[_utils.HOSTNAME_FIELD] = hostname
elif not instance_info.get(_utils.HOSTNAME_FIELD):
instance_info[_utils.HOSTNAME_FIELD] = _utils.default_hostname(
node)
extra = node.extra.copy()
extra[_CREATED_PORTS] = nics.created_ports
@ -382,9 +422,10 @@ class Provisioner(_utils.GetNodeMixin):
LOG.debug('Generating a configdrive for node %s',
_utils.log_res(node))
cd = config.generate(node, _utils.hostname_for(node, allocation))
LOG.debug('Starting provisioning of node %s', _utils.log_res(node))
self.connection.baremetal.set_node_provision_state(
node, 'active', config_drive=config.generate(node))
node, 'active', config_drive=cd)
except Exception:
exc_info = sys.exc_info()
@ -408,8 +449,8 @@ class Provisioner(_utils.GetNodeMixin):
LOG.info('Deploy succeeded on node %s', _utils.log_res(node))
else:
# Update the node to return it's latest state
node = self._get_node(node, refresh=True)
instance = self._get_instance(node)
node = self.connection.baremetal.get_node(node.id)
instance = _instance.Instance(self.connection, node, allocation)
return instance
@ -425,10 +466,12 @@ class Provisioner(_utils.GetNodeMixin):
(more precisely, until the operation times out on server side).
:return: List of updated :py:class:`metalsmith.Instance` objects if
all succeeded.
:raises: :py:class:`metalsmith.exceptions.DeploymentFailure`
if the deployment failed or timed out for any nodes.
:raises: `openstack.exceptions.ResourceTimeout` if deployment times
out for any node.
:raises: `openstack.exceptions.SDKException` if deployment fails
for any node.
"""
nodes = [self._get_node(n, accept_hostname=True) for n in nodes]
nodes = [self._find_node_and_allocation(n)[0] for n in nodes]
nodes = self.connection.baremetal.wait_for_nodes_provision_state(
nodes, 'active', timeout=timeout)
# Using _get_instance in case the deployment started by something
@ -464,7 +507,7 @@ class Provisioner(_utils.GetNodeMixin):
except Exception as exc:
LOG.debug('Failed to remove allocation %(alloc)s for %(node)s:'
' %(exc)s',
{'alloc': node.allocaiton_id,
{'alloc': node.allocation_id,
'node': _utils.log_res(node), 'exc': exc})
elif not node.allocation_id:
# Old-style reservations have to be cleared explicitly
@ -491,7 +534,7 @@ class Provisioner(_utils.GetNodeMixin):
None to return immediately.
:return: the latest `Node` object.
"""
node = self._get_node(node, accept_hostname=True)
node = self._find_node_and_allocation(node)[0]
if self._dry_run:
LOG.warning("Dry run, not unprovisioning")
return
@ -519,16 +562,6 @@ 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.
@ -541,8 +574,7 @@ class Provisioner(_utils.GetNodeMixin):
:raises: :py:class:`metalsmith.exceptions.InvalidInstance`
if one of the instances are not valid instances.
"""
with self._cache_node_list_for_lookup():
result = [self._get_instance(inst) 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)
@ -562,3 +594,45 @@ class Provisioner(_utils.GetNodeMixin):
instances = [i for i in map(self._get_instance, nodes)
if i.state != _instance.InstanceState.UNKNOWN]
return instances
def _get_node(self, node, refresh=False):
"""A helper to find and return a node."""
if isinstance(node, six.string_types):
return self.connection.baremetal.get_node(node)
elif hasattr(node, 'node'):
# Instance object
node = node.node
else:
node = node
if refresh:
return self.connection.baremetal.get_node(node.id)
else:
return node
def _find_node_and_allocation(self, node, refresh=False):
if (not isinstance(node, six.string_types)
or not _utils.is_hostname_safe(node)):
return self._get_node(node, refresh=refresh), None
try:
allocation = self.connection.baremetal.get_allocation(node)
except os_exc.ResourceNotFound:
return self._get_node(node, refresh=refresh), None
else:
if allocation.node_id:
return (self.connection.baremetal.get_node(allocation.node_id),
allocation)
else:
raise exceptions.InvalidInstance(
'Allocation %s exists but is not associated '
'with a node' % node)
def _get_instance(self, ident):
node, allocation = self._find_node_and_allocation(ident)
if allocation is None and node.allocation_id:
allocation = self.connection.baremetal.get_allocation(
node.allocation_id)
return _instance.Instance(self.connection, node,
allocation=allocation)

View File

@ -13,10 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import re
from openstack import exceptions as sdk_exc
import six
from metalsmith import exceptions
@ -90,6 +88,15 @@ def is_hostname_safe(hostname):
return _HOSTNAME_RE.match(hostname) is not None
def check_hostname(hostname):
"""Check the provided host name.
:raises: ValueError on inappropriate value of ``hostname``
"""
if hostname is not None and not is_hostname_safe(hostname):
raise ValueError("%s cannot be used as a hostname" % hostname)
def parse_checksums(checksums):
"""Parse standard checksums file."""
result = {}
@ -103,15 +110,6 @@ def parse_checksums(checksums):
return result
# NOTE(dtantsur): make this private since it will no longer be possible with
# transition to allocation API.
class DuplicateHostname(sdk_exc.SDKException, exceptions.Error):
pass
HOSTNAME_FIELD = 'metalsmith_hostname'
def default_hostname(node):
if node.name and is_hostname_safe(node.name):
return node.name
@ -119,60 +117,8 @@ def default_hostname(node):
return node.id
class GetNodeMixin(object):
"""A helper mixin for getting nodes with hostnames."""
_node_list = None
def _available_nodes(self):
return self.connection.baremetal.nodes(details=True,
associated=False,
provision_state='available',
is_maintenance=False)
def _nodes_for_lookup(self):
return self.connection.baremetal.nodes(
fields=['uuid', 'name', 'instance_info'])
def _find_node_by_hostname(self, hostname):
"""A helper to find a node by metalsmith hostname."""
nodes = self._node_list or self._nodes_for_lookup()
existing = [n for n in nodes
if n.instance_info.get(HOSTNAME_FIELD) == hostname]
if len(existing) > 1:
raise DuplicateHostname(
"More than one node found with hostname %(host)s: %(nodes)s" %
{'host': hostname,
'nodes': ', '.join(log_res(n) for n in existing)})
elif not existing:
return None
else:
# Fetch the complete node information before returning
return self.connection.baremetal.get_node(existing[0].id)
def _get_node(self, node, refresh=False, accept_hostname=False):
"""A helper to find and return a node."""
if isinstance(node, six.string_types):
if accept_hostname and is_hostname_safe(node):
by_hostname = self._find_node_by_hostname(node)
if by_hostname is not None:
return by_hostname
return self.connection.baremetal.get_node(node)
elif hasattr(node, 'node'):
# Instance object
node = node.node
else:
node = node
if refresh:
return self.connection.baremetal.get_node(node.id)
else:
return node
@contextlib.contextmanager
def _cache_node_list_for_lookup(self):
if self._node_list is None:
self._node_list = list(self._nodes_for_lookup())
yield self._node_list
self._node_list = None
def hostname_for(node, allocation=None):
if allocation is not None and allocation.name:
return allocation.name
else:
return default_hostname(node)

View File

@ -68,10 +68,7 @@ class CapabilitiesNotFound(ReservationFailed):
class TraitsNotFound(ReservationFailed):
"""Requested traits do not match any nodes.
:ivar requested_traits: Requested node's traits.
"""
"""DEPRECATED."""
def __init__(self, message, traits):
self.requested_traits = traits
@ -83,10 +80,7 @@ class ValidationFailed(ReservationFailed):
class NoNodesReserved(ReservationFailed):
"""All nodes are already reserved or failed validation.
:ivar nodes: List of nodes that were checked.
"""
"""DEPRECATED."""
def __init__(self, nodes):
self.nodes = nodes
@ -112,10 +106,7 @@ class InvalidNode(Error):
class DeploymentFailure(Error):
"""One or more nodes have failed the deployment.
:ivar nodes: List of failed nodes.
"""
"""DEPRECATED."""
def __init__(self, message, nodes):
self.nodes = nodes

View File

@ -44,10 +44,11 @@ class GenericConfig(object):
self.ssh_keys = ssh_keys or []
self.user_data = user_data
def generate(self, node):
def generate(self, node, hostname=None):
"""Generate the config drive information.
:param node: `Node` object.
:param hostname: Desired hostname (defaults to node's name or ID).
:return: configdrive contents as a dictionary with keys:
``meta_data``
@ -55,7 +56,8 @@ class GenericConfig(object):
``user_data``
user data as a string
"""
hostname = node.instance_info.get(_utils.HOSTNAME_FIELD)
if not hostname:
hostname = _utils.default_hostname(node)
# NOTE(dtantsur): CirrOS does not understand lists
if isinstance(self.ssh_keys, list):
@ -85,19 +87,20 @@ class GenericConfig(object):
"""
return self.user_data
def build_configdrive(self, node):
def build_configdrive(self, node, hostname=None):
"""Make the config drive ISO.
Deprecated, use :py:meth:`generate` with openstacksdk's
``openstack.baremetal.configdrive.build`` instead.
:param node: `Node` object.
:param hostname: Desired hostname (defaults to node's name or ID).
:return: configdrive contents as a base64-encoded string.
"""
warnings.warn("build_configdrive is deprecated, use generate with "
"openstacksdk's openstack.baremetal.configdrive.build "
"instead", DeprecationWarning)
cd = self.generate(node)
cd = self.generate(node, hostname)
metadata = cd.pop('meta_data')
user_data = cd.pop('user_data')
if user_data:

View File

@ -125,12 +125,11 @@ class TestInstanceStates(test_provisioner.Base):
def test_to_dict(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']}
to_dict = self.instance.to_dict()
self.assertEqual({'allocation': None,
'hostname': 'host',
'hostname': self.node.name,
'ip_addresses': {'private': ['1.2.3.4']},
'node': {'node': 'dict'},
'state': 'deploying',
@ -143,9 +142,9 @@ class TestInstanceStates(test_provisioner.Base):
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.name = 'host'
self.instance._allocation.to_dict.return_value = {'alloc': 'dict'}
to_dict = self.instance.to_dict()

View File

@ -20,7 +20,6 @@ from openstack.baremetal import configdrive
import testtools
import metalsmith
from metalsmith import _utils
from metalsmith import instance_config
@ -33,21 +32,19 @@ class TestGenericConfig(testtools.TestCase):
self.node.name = 'node name'
def _check(self, config, expected_metadata, expected_userdata=None,
cloud_init=True):
cloud_init=True, hostname=None):
expected_m = {'public_keys': {},
'uuid': '1234',
'name': 'node name',
'hostname': 'example.com',
'uuid': self.node.id,
'name': self.node.name,
'hostname': self.node.id,
'launch_index': 0,
'availability_zone': '',
'files': [],
'meta': {}}
expected_m.update(expected_metadata)
self.node.instance_info = {_utils.HOSTNAME_FIELD:
expected_m.get('hostname')}
with mock.patch.object(configdrive, 'build', autospec=True) as mb:
result = config.build_configdrive(self.node)
result = config.build_configdrive(self.node, hostname)
mb.assert_called_once_with(expected_m, mock.ANY)
self.assertIs(result, mb.return_value)
user_data = mb.call_args[1].get('user_data')
@ -65,6 +62,16 @@ class TestGenericConfig(testtools.TestCase):
config = self.CLASS()
self._check(config, {})
def test_name_as_hostname(self):
self.node.name = 'example.com'
config = self.CLASS()
self._check(config, {'hostname': 'example.com'})
def test_explicit_hostname(self):
config = self.CLASS()
self._check(config, {'hostname': 'example.com'},
hostname='example.com')
def test_ssh_keys(self):
config = self.CLASS(ssh_keys=['abc', 'def'])
self._check(config, {'public_keys': {'0': 'abc', '1': 'def'}})

View File

@ -21,7 +21,6 @@ import testtools
from metalsmith import _instance
from metalsmith import _provisioner
from metalsmith import _utils
from metalsmith import exceptions
from metalsmith import instance_config
from metalsmith import sources
@ -76,12 +75,8 @@ class Base(testtools.TestCase):
fixtures.MockPatchObject(_provisioner.Provisioner, '_get_node',
autospec=True)).mock
self.mock_get_node.side_effect = (
lambda self, n, refresh=False, accept_hostname=False: n
lambda self, n, refresh=False: n
)
self.useFixture(
fixtures.MockPatchObject(_provisioner.Provisioner,
'_cache_node_list_for_lookup',
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
@ -95,6 +90,71 @@ class Base(testtools.TestCase):
self.pr.connection = self.api
class TestGetFindNode(testtools.TestCase):
def setUp(self):
super(TestGetFindNode, self).setUp()
self.pr = _provisioner.Provisioner(mock.Mock())
self.api = mock.Mock(spec=['baremetal'])
self.pr.connection = self.api
def test__get_node_with_node(self):
node = mock.Mock(spec=['id', 'name'])
result = self.pr._get_node(node)
self.assertIs(result, node)
self.assertFalse(self.api.baremetal.get_node.called)
def test__get_node_with_node_refresh(self):
node = mock.Mock(spec=['id', 'name'])
result = self.pr._get_node(node, refresh=True)
self.assertIs(result, self.api.baremetal.get_node.return_value)
self.api.baremetal.get_node.assert_called_once_with(node.id)
def test__get_node_with_instance(self):
node = mock.Mock(spec=['uuid', 'node'])
result = self.pr._get_node(node)
self.assertIs(result, node.node)
self.assertFalse(self.api.baremetal.get_node.called)
def test__get_node_with_instance_refresh(self):
node = mock.Mock(spec=['uuid', 'node'])
result = self.pr._get_node(node, refresh=True)
self.assertIs(result, self.api.baremetal.get_node.return_value)
self.api.baremetal.get_node.assert_called_once_with(node.node.id)
def test__get_node_with_string(self):
result = self.pr._get_node('node')
self.assertIs(result, self.api.baremetal.get_node.return_value)
self.api.baremetal.get_node.assert_called_once_with('node')
def test__find_node_and_allocation_by_node(self):
node = mock.Mock(spec=['id', 'name'])
result, alloc = self.pr._find_node_and_allocation(node)
self.assertIs(result, node)
self.assertIsNone(alloc)
def test__find_node_and_allocation_by_hostname(self):
result, alloc = self.pr._find_node_and_allocation('node')
self.assertIs(result, self.api.baremetal.get_node.return_value)
self.assertIs(alloc, self.api.baremetal.get_allocation.return_value)
self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.get_allocation.return_value.node_id)
def test__find_node_and_allocation_by_node_id(self):
self.api.baremetal.get_allocation.side_effect = (
os_exc.ResourceNotFound())
result, alloc = self.pr._find_node_and_allocation('node')
self.assertIs(result, self.api.baremetal.get_node.return_value)
self.assertIsNone(alloc)
self.api.baremetal.get_node.assert_called_once_with('node')
def test__find_node_and_allocation_by_hostname_bad_allocation(self):
self.api.baremetal.get_allocation.return_value.node_id = None
self.assertRaises(exceptions.InvalidInstance,
self.pr._find_node_and_allocation, 'node')
self.assertFalse(self.api.baremetal.get_node.called)
class TestReserveNode(Base):
RSC = 'baremetal'
@ -132,9 +192,8 @@ class TestReserveNode(Base):
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}])
self.assertFalse(self.api.baremetal.patch_node.called)
self.assertFalse(self.api.baremetal.delete_allocation.called)
def test_allocation_failed(self):
self.api.baremetal.wait_for_allocation.side_effect = (
@ -150,22 +209,22 @@ class TestReserveNode(Base):
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')
@mock.patch.object(_provisioner.LOG, 'exception', autospec=True)
def test_allocation_failed_clean_up_failed(self, mock_log):
self.api.baremetal.delete_allocation.side_effect = RuntimeError()
self.api.baremetal.wait_for_allocation.side_effect = (
os_exc.SDKException('boom'))
self.assertRaisesRegex(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.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}])
self.api.baremetal.create_allocation.return_value)
self.assertFalse(self.api.baremetal.patch_node.called)
mock_log.assert_called_once_with('Failed to delete failed allocation')
def test_with_hostname(self):
expected = self._node()
@ -180,26 +239,7 @@ class TestReserveNode(Base):
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):
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.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'}])
self.assertFalse(self.api.baremetal.patch_node.called)
def test_with_capabilities(self):
nodes = [
@ -219,11 +259,29 @@ class TestReserveNode(Base):
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',
node, [{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'answer': '42'}}])
def test_node_update_failed(self):
expected = self._node(properties={'local_gb': 100,
'capabilities': {'answer': '42'}})
self.api.baremetal.get_node.return_value = expected
self.api.baremetal.nodes.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,
capabilities={'answer': '42'})
self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=[expected.id],
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/capabilities',
'op': 'add', 'value': {'answer': '42'}}])
def test_with_traits(self):
expected = self._node(properties={'local_gb': 100},
traits=['foo', 'answer:42'])
@ -232,9 +290,7 @@ class TestReserveNode(Base):
node = self.pr.reserve_node(self.RSC, traits=['foo', 'answer:42'])
self.assertIs(node, expected)
self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
self.assertFalse(self.api.baremetal.patch_node.called)
def test_custom_predicate(self):
nodes = [self._node(properties={'local_gb': i})
@ -252,9 +308,7 @@ class TestReserveNode(Base):
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}])
self.assertFalse(self.api.baremetal.patch_node.called)
def test_custom_predicate_false(self):
nodes = [self._node() for _ in range(3)]
@ -281,9 +335,7 @@ class TestReserveNode(Base):
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}])
self.assertFalse(self.api.baremetal.patch_node.called)
def test_provided_nodes(self):
nodes = [self._node(id=1), self._node(id=2)]
@ -298,9 +350,7 @@ class TestReserveNode(Base):
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}])
self.assertFalse(self.api.baremetal.patch_node.called)
def test_nodes_filtered(self):
nodes = [self._node(resource_class='banana'),
@ -321,9 +371,7 @@ class TestReserveNode(Base):
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',
node, [{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'cat': 'meow'}}])
def test_nodes_filtered_by_conductor_group(self):
@ -349,9 +397,7 @@ class TestReserveNode(Base):
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',
node, [{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'cat': 'meow'}}])
def test_provided_nodes_no_match(self):
@ -377,14 +423,18 @@ class TestProvisionNode(Base):
def setUp(self):
super(TestProvisionNode, self).setUp()
self.image = self.api.image.find_image.return_value
self.node.instance_id = self.node.id
self.node.instance_id = '123456'
self.node.allocation_id = '123456'
self.allocation = mock.Mock(spec=['id', 'node_id', 'name'],
id='123456',
node_id=self.node.id)
self.allocation.name = 'example.com'
self.instance_info = {
'ramdisk': self.image.ramdisk_id,
'kernel': self.image.kernel_id,
'image_source': self.image.id,
'root_gb': 99, # 100 - 1
'capabilities': {'boot_option': 'local'},
_utils.HOSTNAME_FIELD: 'control-0'
}
self.extra = {
'metalsmith_created_ports': [
@ -398,6 +448,9 @@ class TestProvisionNode(Base):
fixtures.MockPatchObject(instance_config.GenericConfig,
'generate', autospec=True)
).mock
self.api.baremetal.get_node.side_effect = lambda _n: self.node
self.api.baremetal.get_allocation.side_effect = (
lambda _a: self.allocation)
def test_ok(self):
inst = self.pr.provision_node(self.node, 'image',
@ -413,7 +466,30 @@ class TestProvisionNode(Base):
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.configdrive_mock.assert_called_once_with(mock.ANY, self.node)
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
self.allocation.name)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(self.api.network.delete_port.called)
def test_old_style_reservation(self):
self.node.allocation_id = None
self.node.instance_id = self.node.id
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}])
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
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_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.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
self.node.name)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(self.api.network.delete_port.called)
@ -467,7 +543,8 @@ class TestProvisionNode(Base):
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
config.generate.assert_called_once_with(self.node)
config.generate.assert_called_once_with(self.node,
self.allocation.name)
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(
@ -481,19 +558,29 @@ class TestProvisionNode(Base):
self.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.called)
@mock.patch.object(_provisioner.Provisioner, '_find_node_by_hostname',
autospec=True)
def test_with_hostname(self, mock_find_node):
mock_find_node.return_value = None
def test_with_hostname_override(self):
self.allocation.name = None
self.api.baremetal.get_allocation.side_effect = [
os_exc.ResourceNotFound(),
self.allocation
]
def _update(allocation, name):
allocation.name = name
return allocation
self.api.baremetal.update_allocation.side_effect = _update
hostname = 'control-0.example.com'
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}],
hostname=hostname)
self.instance_info[_utils.HOSTNAME_FIELD] = hostname
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
self.assertIs(inst.allocation, self.allocation)
self.api.baremetal.update_allocation.assert_called_once_with(
self.allocation, name=hostname)
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(
@ -501,7 +588,8 @@ class TestProvisionNode(Base):
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.configdrive_mock.assert_called_once_with(mock.ANY, self.node)
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
hostname)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(
@ -510,14 +598,15 @@ class TestProvisionNode(Base):
def test_existing_hostname(self):
hostname = 'control-0.example.com'
self.node.instance_info[_utils.HOSTNAME_FIELD] = hostname
self.allocation.name = hostname
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}])
self.instance_info[_utils.HOSTNAME_FIELD] = hostname
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
self.assertIs(inst.allocation, self.allocation)
self.assertFalse(self.api.baremetal.update_allocation.called)
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(
@ -525,7 +614,90 @@ class TestProvisionNode(Base):
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.configdrive_mock.assert_called_once_with(mock.ANY, self.node)
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
hostname)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.called)
def test_existing_hostname_match(self):
hostname = 'control-0.example.com'
self.allocation.name = hostname
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}],
hostname=hostname)
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
self.assertIs(inst.allocation, self.allocation)
self.assertFalse(self.api.baremetal.update_allocation.called)
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_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.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
hostname)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.called)
def test_existing_hostname_mismatch(self):
self.api.baremetal.get_allocation.side_effect = [
# No allocation with requested hostname
os_exc.ResourceNotFound(),
# Allocation associated with the node
self.allocation
]
self.allocation.name = 'control-0.example.com'
self.assertRaisesRegex(exceptions.InvalidNode,
'does not match the expected hostname',
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}],
hostname='control-1.example.com')
self.api.baremetal.get_allocation.assert_has_calls([
mock.call('control-1.example.com'),
mock.call(self.node.allocation_id),
])
self.assertFalse(self.api.baremetal.create_allocation.called)
self.assertFalse(self.api.baremetal.update_node.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
self.assertFalse(self.api.baremetal.delete_allocation.called)
def test_node_name_as_hostname(self):
self.allocation.name = None
def _update(allocation, name):
allocation.name = name
return allocation
self.api.baremetal.update_allocation.side_effect = _update
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}])
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
self.assertIs(inst.allocation, self.allocation)
self.api.baremetal.update_allocation.assert_called_once_with(
self.allocation, name=self.node.name)
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_called_once_with(
self.node, extra=self.extra, instance_info=self.instance_info)
self.api.baremetal.validate_node.assert_called_once_with(self.node)
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
self.node.name)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(
@ -534,12 +706,19 @@ class TestProvisionNode(Base):
def test_name_not_valid_hostname(self):
self.node.name = 'node_1'
self.allocation.name = None
def _update(allocation, name):
allocation.name = name
return allocation
self.api.baremetal.update_allocation.side_effect = _update
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}])
self.instance_info[_utils.HOSTNAME_FIELD] = '000'
self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node)
self.assertIs(inst.allocation, self.allocation)
self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id)
@ -548,6 +727,8 @@ class TestProvisionNode(Base):
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra=self.extra, instance_info=self.instance_info)
self.api.baremetal.validate_node.assert_called_once_with(self.node)
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
self.node.id)
self.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY)
self.assertFalse(
@ -556,15 +737,21 @@ class TestProvisionNode(Base):
def test_unreserved(self):
self.node.instance_id = None
self.node.allocation_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],
name=self.node.name, 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.baremetal.get_node.assert_has_calls([
# After allocation
mock.call(
self.api.baremetal.wait_for_allocation.return_value.node_id),
# After deployment
mock.call(self.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(
@ -578,6 +765,54 @@ class TestProvisionNode(Base):
self.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.called)
def test_unreserved_with_hostname(self):
self.node.instance_id = None
self.node.allocation_id = None
self.api.baremetal.get_node.return_value = self.node
hostname = 'control-2.example.com'
self.pr.provision_node(self.node, 'image', [{'network': 'network'}],
hostname=hostname)
self.api.baremetal.create_allocation.assert_called_once_with(
name=hostname, candidate_nodes=[self.node.id],
resource_class=self.node.resource_class, traits=None)
self.api.baremetal.get_node.assert_has_calls([
# After allocation
mock.call(
self.api.baremetal.wait_for_allocation.return_value.node_id),
# After deployment
mock.call(self.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_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)
self.assertFalse(
self.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.called)
def test_unreserved_without_resource_class(self):
self.node.instance_id = None
self.node.allocation_id = None
self.node.resource_class = None
self.api.baremetal.get_node.return_value = self.node
self.assertRaisesRegex(exceptions.InvalidNode,
'does not have a resource class',
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.baremetal.create_allocation.called)
self.assertFalse(self.api.baremetal.update_node.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
self.assertFalse(self.api.baremetal.delete_allocation.called)
def test_with_ports(self):
port_ids = [self.api.network.find_port.return_value.id] * 2
@ -1013,11 +1248,13 @@ abcd image
def test_unreserve_dry_run(self):
self.pr._dry_run = True
self.node.allocation_id = None
self.node.instance_id = None
self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.create_allocation.called)
self.assertFalse(self.api.baremetal.attach_vif_to_node.called)
self.assertFalse(self.api.baremetal.update_node.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@ -1033,6 +1270,33 @@ abcd image
'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(
self.allocation.id)
def test_deploy_failure_without_allocation(self):
self.node.instance_id = None
self.node.allocation_id = None
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={}, instance_id=None)
self.assertFalse(
@ -1049,31 +1313,6 @@ abcd image
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_deploy_failure_no_cleanup(self):
self.node.allocation_id = 'id2'
self.api.baremetal.set_node_provision_state.side_effect = (
@ -1096,8 +1335,10 @@ abcd image
self.pr.provision_node, self.node,
'image', [{'network': 'network'}], wait=3600)
self.api.baremetal.delete_allocation.assert_called_once_with(
self.allocation.id)
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
self.assertFalse(self.api.network.delete_port.called)
self.assertFalse(self.api.baremetal.detach_vif_from_node.called)
@ -1109,8 +1350,10 @@ abcd image
self.pr.provision_node, self.node,
'image', [{'network': 'network'}], wait=3600)
self.api.baremetal.delete_allocation.assert_called_once_with(
self.allocation.id)
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
self.api.network.delete_port.assert_called_once_with(
self.api.network.create_port.return_value.id,
@ -1152,7 +1395,7 @@ abcd image
self.node, self.api.network.create_port.return_value.id)
def test_detach_failed_after_deploy_failure(self):
self.api.baremetal.detach_port_from_node.side_effect = AssertionError()
self.api.baremetal.detach_vif_from_node.side_effect = AssertionError()
self._test_failure_during_deploy_failure()
def test_update_failed_after_deploy_failure(self):
@ -1160,6 +1403,10 @@ abcd image
AssertionError()]
self._test_failure_during_deploy_failure()
def test_deallocation_failed_after_deploy_failure(self):
self.api.baremetal.delete_allocation.side_effect = AssertionError()
self._test_failure_during_deploy_failure()
def test_wait_failure(self):
self.api.baremetal.wait_for_nodes_provision_state.side_effect = (
RuntimeError('boom'))
@ -1181,7 +1428,7 @@ abcd image
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@mock.patch.object(requests, 'get', autospec=True)
@ -1207,7 +1454,7 @@ abcd and-not-image-again
self.assertFalse(self.api.image.find_image.called)
mock_get.assert_called_once_with('https://host/checksums')
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@mock.patch.object(requests, 'get', autospec=True)
@ -1233,7 +1480,7 @@ abcd and-not-image-again
self.assertFalse(self.api.image.find_image.called)
mock_get.assert_called_once_with('https://host/checksums')
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@mock.patch.object(requests, 'get', autospec=True)
@ -1257,7 +1504,7 @@ abcd and-not-image-again
self.assertFalse(self.api.image.find_image.called)
mock_get.assert_called_once_with('https://host/checksums')
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
def test_invalid_network(self):
@ -1267,7 +1514,7 @@ abcd and-not-image-again
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@ -1278,7 +1525,7 @@ abcd and-not-image-again
self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}])
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@ -1289,7 +1536,7 @@ abcd and-not-image-again
self.pr.provision_node,
self.node, 'image', [{'subnet': 'subnet'}])
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@ -1301,7 +1548,7 @@ abcd and-not-image-again
self.pr.provision_node,
self.node, 'image', [{'subnet': 'subnet'}])
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.node, extra={}, instance_info={})
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@ -1378,21 +1625,28 @@ abcd and-not-image-again
self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}],
hostname='n_1')
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@mock.patch.object(_provisioner.Provisioner, '_find_node_by_hostname',
autospec=True)
def test_duplicate_hostname(self, mock_find_node):
mock_find_node.return_value = mock.Mock(spec=['id', 'name'])
def test_duplicate_hostname(self):
allocation = mock.Mock(spec=['id', 'name', 'node_id'],
node_id='another node')
self.api.baremetal.get_allocation.side_effect = [allocation]
self.assertRaisesRegex(ValueError, 'already uses hostname host',
self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}],
hostname='host')
self.api.baremetal.update_node.assert_called_once_with(
self.node, extra={}, instance_info={}, instance_id=None)
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
def test_old_style_reservation_with_override(self):
self.node.allocation_id = None
self.node.instance_id = self.node.id
self.assertRaisesRegex(exceptions.InvalidNode,
'does not use allocations',
self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}],
hostname='host')
self.assertFalse(self.api.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@ -1407,6 +1661,7 @@ abcd and-not-image-again
def test_node_with_external_instance_id(self):
self.node.instance_id = 'nova'
self.node.allocation_id = None
self.assertRaisesRegex(exceptions.InvalidNode,
'reserved by instance nova',
self.pr.provision_node,
@ -1551,39 +1806,56 @@ class TestUnprovisionNode(Base):
self.assertFalse(self.api.baremetal.update_node.called)
class TestShowInstance(Base):
class TestShowInstance(testtools.TestCase):
def setUp(self):
super(TestShowInstance, self).setUp()
self.node.provision_state = 'active'
self.pr = _provisioner.Provisioner(mock.Mock())
self.api = mock.Mock(spec=['baremetal'])
self.pr.connection = self.api
self.node = mock.Mock(spec=NODE_FIELDS + ['to_dict'],
id='000', instance_id=None,
properties={'local_gb': 100},
instance_info={},
is_maintenance=False, extra={},
provision_state='active',
allocation_id=None)
self.node.name = 'control-0'
self.api.baremetal.get_node.return_value = self.node
def test_show_instance(self):
self.mock_get_node.side_effect = lambda n, *a, **kw: self.node
self.api.baremetal.get_allocation.side_effect = (
os_exc.ResourceNotFound())
inst = self.pr.show_instance('id1')
self.mock_get_node.assert_called_once_with(self.pr, 'id1',
accept_hostname=True)
self.assertIsInstance(inst, _instance.Instance)
self.assertIs(inst.node, self.node)
self.assertIs(inst.uuid, self.node.id)
self.api.baremetal.get_node.assert_called_once_with('id1')
def test_show_instance_with_allocation(self):
self.node.allocation_id = 'id2'
self.mock_get_node.side_effect = lambda n, *a, **kw: self.node
self.api.baremetal.get_allocation.return_value.node_id = '1234'
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.api.baremetal.get_allocation.assert_called_once_with('id1')
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)
self.api.baremetal.get_node.assert_called_once_with('1234')
def test_show_instances(self):
self.mock_get_node.side_effect = [self.node, self.node]
result = self.pr.show_instances(['1', '2'])
self.mock_get_node.assert_has_calls([
mock.call(self.pr, '1', accept_hostname=True),
mock.call(self.pr, '2', accept_hostname=True)
self.api.baremetal.get_allocation.side_effect = [
os_exc.ResourceNotFound(),
mock.Mock(node_id='4321'),
]
result = self.pr.show_instances(['inst-1', 'inst-2'])
self.api.baremetal.get_node.assert_has_calls([
mock.call('inst-1'),
mock.call('4321'),
])
self.api.baremetal.get_allocation.assert_has_calls([
mock.call('inst-1'),
mock.call('inst-2'),
])
self.assertIsInstance(result, list)
for inst in result:
@ -1591,6 +1863,14 @@ class TestShowInstance(Base):
self.assertIs(result[0].node, self.node)
self.assertIs(result[0].uuid, self.node.id)
def test_show_instance_invalid_state(self):
self.node.provision_state = 'manageable'
self.api.baremetal.get_allocation.side_effect = (
os_exc.ResourceNotFound())
self.assertRaises(exceptions.InvalidInstance,
self.pr.show_instance, 'id1')
self.api.baremetal.get_node.assert_called_once_with('id1')
class TestWaitForProvisioning(Base):
@ -1617,7 +1897,8 @@ class TestListInstances(Base):
def test_list(self):
instances = self.pr.list_instances()
self.assertTrue(isinstance(i, _instance.Instance) for i in instances)
self.assertTrue(all(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,

View File

@ -13,11 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import testtools
from metalsmith import _utils
from metalsmith import exceptions
class TestIsHostnameSafe(testtools.TestCase):
@ -68,74 +66,3 @@ class TestIsHostnameSafe(testtools.TestCase):
# Need to ensure a binary response for success or fail
self.assertIsNotNone(_utils.is_hostname_safe('spam'))
self.assertIsNotNone(_utils.is_hostname_safe('-spam'))
class TestGetNodeMixin(testtools.TestCase):
def setUp(self):
super(TestGetNodeMixin, self).setUp()
self.mixin = _utils.GetNodeMixin()
self.mixin.connection = mock.Mock(spec=['baremetal'])
self.api = self.mixin.connection.baremetal
def test__get_node_with_node(self):
node = mock.Mock(spec=['id', 'name'])
result = self.mixin._get_node(node)
self.assertIs(result, node)
self.assertFalse(self.api.get_node.called)
def test__get_node_with_node_refresh(self):
node = mock.Mock(spec=['id', 'name'])
result = self.mixin._get_node(node, refresh=True)
self.assertIs(result, self.api.get_node.return_value)
self.api.get_node.assert_called_once_with(node.id)
def test__get_node_with_instance(self):
node = mock.Mock(spec=['uuid', 'node'])
result = self.mixin._get_node(node)
self.assertIs(result, node.node)
self.assertFalse(self.api.get_node.called)
def test__get_node_with_instance_refresh(self):
node = mock.Mock(spec=['uuid', 'node'])
result = self.mixin._get_node(node, refresh=True)
self.assertIs(result, self.api.get_node.return_value)
self.api.get_node.assert_called_once_with(node.node.id)
def test__get_node_with_string(self):
result = self.mixin._get_node('node')
self.assertIs(result, self.api.get_node.return_value)
self.api.get_node.assert_called_once_with('node')
def test__get_node_with_string_hostname_allowed(self):
nodes = [
mock.Mock(instance_info={'metalsmith_hostname': host})
for host in ['host1', 'host2', 'host3']
]
self.api.nodes.return_value = nodes
result = self.mixin._get_node('host2', accept_hostname=True)
self.assertIs(result, self.api.get_node.return_value)
self.api.get_node.assert_called_once_with(nodes[1].id)
def test__get_node_with_string_hostname_allowed_fallback(self):
nodes = [
mock.Mock(instance_info={'metalsmith_hostname': host})
for host in ['host1', 'host2', 'host3']
]
self.api.nodes.return_value = nodes
result = self.mixin._get_node('node', accept_hostname=True)
self.assertIs(result, self.api.get_node.return_value)
self.api.get_node.assert_called_once_with('node')
def test__get_node_with_string_hostname_not_unique(self):
nodes = [
mock.Mock(instance_info={'metalsmith_hostname': host})
for host in ['host1', 'host2', 'host2']
]
self.api.nodes.return_value = nodes
self.assertRaises(exceptions.Error,
self.mixin._get_node,
'host2', accept_hostname=True)
self.assertFalse(self.api.get_node.called)

View File

@ -0,0 +1,11 @@
---
upgrade:
- |
An allocation name is now used for hostname instead of a custom ``extra``
field. Previously deployed instances will no longer be recognized, use
the allocation backfilling to make them recognized again.
deprecations:
- |
The exception classes ``DeploymentFailure``, ``TraitsNotFound`` and
``NoNodesReserved`` are deprecated and no longer used after transitioning
to the allocation API.

View File

@ -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.28.0 # Apache-2.0
openstacksdk>=0.29.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