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 flake8-import-order==0.13
hacking==1.0.0 hacking==1.0.0
mock==2.0 mock==2.0
openstacksdk==0.28.0 openstacksdk==0.29.0
pbr==2.0.0 pbr==2.0.0
Pygments==2.2.0 Pygments==2.2.0
requests==2.18.4 requests==2.18.4

View File

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

View File

@@ -33,11 +33,10 @@ LOG = logging.getLogger(__name__)
_CREATED_PORTS = 'metalsmith_created_ports' _CREATED_PORTS = 'metalsmith_created_ports'
_ATTACHED_PORTS = 'metalsmith_attached_ports' _ATTACHED_PORTS = 'metalsmith_attached_ports'
_PRESERVE_INSTANCE_INFO_KEYS = {'capabilities', 'traits', _PRESERVE_INSTANCE_INFO_KEYS = {'capabilities', 'traits'}
_utils.HOSTNAME_FIELD}
class Provisioner(_utils.GetNodeMixin): class Provisioner(object):
"""API to deploy/undeploy nodes with OpenStack. """API to deploy/undeploy nodes with OpenStack.
:param session: `Session` object (from ``keystoneauth``) to use when :param session: `Session` object (from ``keystoneauth``) to use when
@@ -94,7 +93,7 @@ class Provisioner(_utils.GetNodeMixin):
:raises: :py:class:`metalsmith.exceptions.ReservationFailed` :raises: :py:class:`metalsmith.exceptions.ReservationFailed`
""" """
capabilities = capabilities or {} capabilities = capabilities or {}
self._check_hostname(hostname) _utils.check_hostname(hostname)
if candidates or capabilities or conductor_group or predicate: if candidates or capabilities or conductor_group or predicate:
# Predicates, capabilities and conductor groups are not supported # Predicates, capabilities and conductor groups are not supported
@@ -107,7 +106,7 @@ class Provisioner(_utils.GetNodeMixin):
node = self._reserve_node(resource_class, hostname=hostname, node = self._reserve_node(resource_class, hostname=hostname,
candidates=candidates, traits=traits, candidates=candidates, traits=traits,
capabilities=capabilities) capabilities=capabilities)[0]
return node return node
def _prefilter_nodes(self, resource_class, conductor_group, capabilities, def _prefilter_nodes(self, resource_class, conductor_group, capabilities,
@@ -186,26 +185,20 @@ class Provisioner(_utils.GetNodeMixin):
six.reraise(*exc_info) six.reraise(*exc_info)
LOG.debug('Reserved node: %s', node) LOG.debug('Reserved node: %s', node)
return node return node, allocation
def _patch_reserved_node(self, node, allocation, hostname, capabilities): def _patch_reserved_node(self, node, allocation, hostname, capabilities):
"""Make required updates on a newly reserved node.""" """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: if capabilities:
patch.append({'path': '/instance_info/capabilities', patch = [{'path': '/instance_info/capabilities',
'op': 'add', 'value': 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', def _check_node_for_deploy(self, node, hostname):
{'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. """Check that node is ready and reserve it if needed.
These checks are done outside of the try..except block in 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 Particularly, we don't want to try clean up nodes that were not
reserved by us or are in maintenance mode. 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: if node.is_maintenance:
raise exceptions.InvalidNode('Refusing to deploy on node %(node)s ' raise exceptions.InvalidNode('Refusing to deploy on node %(node)s '
'which is in maintenance mode due to ' 'which is in maintenance mode due to '
@@ -247,26 +213,98 @@ class Provisioner(_utils.GetNodeMixin):
{'node': _utils.log_res(node), {'node': _utils.log_res(node),
'reason': node.maintenance_reason}) 'reason': node.maintenance_reason})
return node allocation = None
def _check_hostname(self, hostname, node=None): # Make sure the hostname does not correspond to an existing allocation
"""Check the provided host name. # for another node.
if hostname is not None:
allocation = self._check_allocation_for_hostname(node, hostname)
:raises: ValueError on inappropriate value of ``hostname`` if node.allocation_id:
""" if allocation is None:
if hostname 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 return
if not _utils.is_hostname_safe(hostname): if allocation.node_id and allocation.node_id != node.id:
raise ValueError("%s cannot be used as a hostname" % hostname) raise ValueError("The following node already uses "
"hostname %(host)s: %(node)s" %
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" %
{'host': hostname, {'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, def provision_node(self, node, image, nics=None, root_size_gb=None,
swap_size_mb=None, config=None, hostname=None, swap_size_mb=None, config=None, hostname=None,
@@ -331,11 +369,18 @@ class Provisioner(_utils.GetNodeMixin):
if isinstance(image, six.string_types): if isinstance(image, six.string_types):
image = sources.GlanceImage(image) 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) nics = _nics.NICs(self.connection, node, nics)
try: try:
self._check_hostname(hostname, node=node)
root_size_gb = _utils.get_root_disk(root_size_gb, node) root_size_gb = _utils.get_root_disk(root_size_gb, node)
image._validate(self.connection) image._validate(self.connection)
@@ -357,11 +402,6 @@ class Provisioner(_utils.GetNodeMixin):
instance_info = self._clean_instance_info(node.instance_info) instance_info = self._clean_instance_info(node.instance_info)
instance_info['root_gb'] = root_size_gb instance_info['root_gb'] = root_size_gb
instance_info['capabilities'] = capabilities 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 = node.extra.copy()
extra[_CREATED_PORTS] = nics.created_ports extra[_CREATED_PORTS] = nics.created_ports
@@ -382,9 +422,10 @@ class Provisioner(_utils.GetNodeMixin):
LOG.debug('Generating a configdrive for node %s', LOG.debug('Generating a configdrive for node %s',
_utils.log_res(node)) _utils.log_res(node))
cd = config.generate(node, _utils.hostname_for(node, allocation))
LOG.debug('Starting provisioning of node %s', _utils.log_res(node)) LOG.debug('Starting provisioning of node %s', _utils.log_res(node))
self.connection.baremetal.set_node_provision_state( self.connection.baremetal.set_node_provision_state(
node, 'active', config_drive=config.generate(node)) node, 'active', config_drive=cd)
except Exception: except Exception:
exc_info = sys.exc_info() exc_info = sys.exc_info()
@@ -408,8 +449,8 @@ class Provisioner(_utils.GetNodeMixin):
LOG.info('Deploy succeeded on node %s', _utils.log_res(node)) LOG.info('Deploy succeeded on node %s', _utils.log_res(node))
else: else:
# Update the node to return it's latest state # Update the node to return it's latest state
node = self._get_node(node, refresh=True) node = self.connection.baremetal.get_node(node.id)
instance = self._get_instance(node) instance = _instance.Instance(self.connection, node, allocation)
return instance return instance
@@ -425,10 +466,12 @@ class Provisioner(_utils.GetNodeMixin):
(more precisely, until the operation times out on server side). (more precisely, until the operation times out on server side).
:return: List of updated :py:class:`metalsmith.Instance` objects if :return: List of updated :py:class:`metalsmith.Instance` objects if
all succeeded. all succeeded.
:raises: :py:class:`metalsmith.exceptions.DeploymentFailure` :raises: `openstack.exceptions.ResourceTimeout` if deployment times
if the deployment failed or timed out for any nodes. 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 = self.connection.baremetal.wait_for_nodes_provision_state(
nodes, 'active', timeout=timeout) nodes, 'active', timeout=timeout)
# Using _get_instance in case the deployment started by something # Using _get_instance in case the deployment started by something
@@ -464,7 +507,7 @@ class Provisioner(_utils.GetNodeMixin):
except Exception as exc: except Exception as exc:
LOG.debug('Failed to remove allocation %(alloc)s for %(node)s:' LOG.debug('Failed to remove allocation %(alloc)s for %(node)s:'
' %(exc)s', ' %(exc)s',
{'alloc': node.allocaiton_id, {'alloc': node.allocation_id,
'node': _utils.log_res(node), 'exc': exc}) 'node': _utils.log_res(node), 'exc': exc})
elif not node.allocation_id: elif not node.allocation_id:
# Old-style reservations have to be cleared explicitly # Old-style reservations have to be cleared explicitly
@@ -491,7 +534,7 @@ class Provisioner(_utils.GetNodeMixin):
None to return immediately. None to return immediately.
:return: the latest `Node` object. :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: if self._dry_run:
LOG.warning("Dry run, not unprovisioning") LOG.warning("Dry run, not unprovisioning")
return return
@@ -519,16 +562,6 @@ class Provisioner(_utils.GetNodeMixin):
""" """
return self.show_instances([instance_id])[0] 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): def show_instances(self, instances):
"""Show information about instance. """Show information about instance.
@@ -541,8 +574,7 @@ class Provisioner(_utils.GetNodeMixin):
:raises: :py:class:`metalsmith.exceptions.InvalidInstance` :raises: :py:class:`metalsmith.exceptions.InvalidInstance`
if one of the instances are not valid instances. 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 # NOTE(dtantsur): do not accept node names as valid instances if they
# are not deployed or being deployed. # are not deployed or being deployed.
missing = [inst for (res, inst) in zip(result, instances) 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) instances = [i for i in map(self._get_instance, nodes)
if i.state != _instance.InstanceState.UNKNOWN] if i.state != _instance.InstanceState.UNKNOWN]
return instances 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import contextlib
import re import re
from openstack import exceptions as sdk_exc
import six import six
from metalsmith import exceptions from metalsmith import exceptions
@@ -90,6 +88,15 @@ def is_hostname_safe(hostname):
return _HOSTNAME_RE.match(hostname) is not None 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): def parse_checksums(checksums):
"""Parse standard checksums file.""" """Parse standard checksums file."""
result = {} result = {}
@@ -103,15 +110,6 @@ def parse_checksums(checksums):
return result 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): def default_hostname(node):
if node.name and is_hostname_safe(node.name): if node.name and is_hostname_safe(node.name):
return node.name return node.name
@@ -119,60 +117,8 @@ def default_hostname(node):
return node.id return node.id
class GetNodeMixin(object): def hostname_for(node, allocation=None):
"""A helper mixin for getting nodes with hostnames.""" if allocation is not None and allocation.name:
return allocation.name
_node_list = None else:
return default_hostname(node)
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

View File

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

View File

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

View File

@@ -125,12 +125,11 @@ class TestInstanceStates(test_provisioner.Base):
def test_to_dict(self, mock_ips): def test_to_dict(self, mock_ips):
self.node.provision_state = 'wait call-back' self.node.provision_state = 'wait call-back'
self.node.to_dict.return_value = {'node': 'dict'} self.node.to_dict.return_value = {'node': 'dict'}
self.node.instance_info = {'metalsmith_hostname': 'host'}
mock_ips.return_value = {'private': ['1.2.3.4']} mock_ips.return_value = {'private': ['1.2.3.4']}
to_dict = self.instance.to_dict() to_dict = self.instance.to_dict()
self.assertEqual({'allocation': None, self.assertEqual({'allocation': None,
'hostname': 'host', 'hostname': self.node.name,
'ip_addresses': {'private': ['1.2.3.4']}, 'ip_addresses': {'private': ['1.2.3.4']},
'node': {'node': 'dict'}, 'node': {'node': 'dict'},
'state': 'deploying', 'state': 'deploying',
@@ -143,9 +142,9 @@ class TestInstanceStates(test_provisioner.Base):
def test_to_dict_with_allocation(self, mock_ips): def test_to_dict_with_allocation(self, mock_ips):
self.node.provision_state = 'wait call-back' self.node.provision_state = 'wait call-back'
self.node.to_dict.return_value = {'node': 'dict'} self.node.to_dict.return_value = {'node': 'dict'}
self.node.instance_info = {'metalsmith_hostname': 'host'}
mock_ips.return_value = {'private': ['1.2.3.4']} mock_ips.return_value = {'private': ['1.2.3.4']}
self.instance._allocation = mock.Mock() self.instance._allocation = mock.Mock()
self.instance._allocation.name = 'host'
self.instance._allocation.to_dict.return_value = {'alloc': 'dict'} self.instance._allocation.to_dict.return_value = {'alloc': 'dict'}
to_dict = self.instance.to_dict() to_dict = self.instance.to_dict()

View File

@@ -20,7 +20,6 @@ from openstack.baremetal import configdrive
import testtools import testtools
import metalsmith import metalsmith
from metalsmith import _utils
from metalsmith import instance_config from metalsmith import instance_config
@@ -33,21 +32,19 @@ class TestGenericConfig(testtools.TestCase):
self.node.name = 'node name' self.node.name = 'node name'
def _check(self, config, expected_metadata, expected_userdata=None, def _check(self, config, expected_metadata, expected_userdata=None,
cloud_init=True): cloud_init=True, hostname=None):
expected_m = {'public_keys': {}, expected_m = {'public_keys': {},
'uuid': '1234', 'uuid': self.node.id,
'name': 'node name', 'name': self.node.name,
'hostname': 'example.com', 'hostname': self.node.id,
'launch_index': 0, 'launch_index': 0,
'availability_zone': '', 'availability_zone': '',
'files': [], 'files': [],
'meta': {}} 'meta': {}}
expected_m.update(expected_metadata) 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: 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) mb.assert_called_once_with(expected_m, mock.ANY)
self.assertIs(result, mb.return_value) self.assertIs(result, mb.return_value)
user_data = mb.call_args[1].get('user_data') user_data = mb.call_args[1].get('user_data')
@@ -65,6 +62,16 @@ class TestGenericConfig(testtools.TestCase):
config = self.CLASS() config = self.CLASS()
self._check(config, {}) 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): def test_ssh_keys(self):
config = self.CLASS(ssh_keys=['abc', 'def']) config = self.CLASS(ssh_keys=['abc', 'def'])
self._check(config, {'public_keys': {'0': 'abc', '1': '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 _instance
from metalsmith import _provisioner from metalsmith import _provisioner
from metalsmith import _utils
from metalsmith import exceptions from metalsmith import exceptions
from metalsmith import instance_config from metalsmith import instance_config
from metalsmith import sources from metalsmith import sources
@@ -76,12 +75,8 @@ class Base(testtools.TestCase):
fixtures.MockPatchObject(_provisioner.Provisioner, '_get_node', fixtures.MockPatchObject(_provisioner.Provisioner, '_get_node',
autospec=True)).mock autospec=True)).mock
self.mock_get_node.side_effect = ( 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 = mock.Mock(spec=['image', 'network', 'baremetal'])
self.api.baremetal.update_node.side_effect = lambda n, **kw: n self.api.baremetal.update_node.side_effect = lambda n, **kw: n
self.api.baremetal.patch_node.side_effect = lambda n, _p: 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 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): class TestReserveNode(Base):
RSC = 'baremetal' RSC = 'baremetal'
@@ -132,9 +192,8 @@ class TestReserveNode(Base):
self.api.baremetal.create_allocation.return_value) self.api.baremetal.create_allocation.return_value)
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
node, [{'path': '/instance_info/metalsmith_hostname', self.assertFalse(self.api.baremetal.delete_allocation.called)
'op': 'add', 'value': node.id}])
def test_allocation_failed(self): def test_allocation_failed(self):
self.api.baremetal.wait_for_allocation.side_effect = ( self.api.baremetal.wait_for_allocation.side_effect = (
@@ -150,22 +209,22 @@ class TestReserveNode(Base):
self.api.baremetal.create_allocation.return_value) self.api.baremetal.create_allocation.return_value)
self.assertFalse(self.api.baremetal.patch_node.called) self.assertFalse(self.api.baremetal.patch_node.called)
def test_node_update_failed(self): @mock.patch.object(_provisioner.LOG, 'exception', autospec=True)
expected = self._node() def test_allocation_failed_clean_up_failed(self, mock_log):
self.api.baremetal.get_node.return_value = expected self.api.baremetal.delete_allocation.side_effect = RuntimeError()
self.api.baremetal.patch_node.side_effect = os_exc.SDKException('boom') 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.pr.reserve_node, self.RSC)
self.api.baremetal.create_allocation.assert_called_once_with( self.api.baremetal.create_allocation.assert_called_once_with(
name=None, candidate_nodes=None, name=None, candidate_nodes=None,
resource_class=self.RSC, traits=None) resource_class=self.RSC, traits=None)
self.api.baremetal.delete_allocation.assert_called_once_with( self.api.baremetal.delete_allocation.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value) self.api.baremetal.create_allocation.return_value)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
expected, [{'path': '/instance_info/metalsmith_hostname', mock_log.assert_called_once_with('Failed to delete failed allocation')
'op': 'add', 'value': expected.id}])
def test_with_hostname(self): def test_with_hostname(self):
expected = self._node() expected = self._node()
@@ -180,26 +239,7 @@ class TestReserveNode(Base):
resource_class=self.RSC, traits=None) resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
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'}])
def test_with_capabilities(self): def test_with_capabilities(self):
nodes = [ nodes = [
@@ -219,11 +259,29 @@ class TestReserveNode(Base):
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname', node, [{'path': '/instance_info/capabilities',
'op': 'add', 'value': node.id},
{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'answer': '42'}}]) '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): def test_with_traits(self):
expected = self._node(properties={'local_gb': 100}, expected = self._node(properties={'local_gb': 100},
traits=['foo', 'answer:42']) traits=['foo', 'answer:42'])
@@ -232,9 +290,7 @@ class TestReserveNode(Base):
node = self.pr.reserve_node(self.RSC, traits=['foo', 'answer:42']) node = self.pr.reserve_node(self.RSC, traits=['foo', 'answer:42'])
self.assertIs(node, expected) self.assertIs(node, expected)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_custom_predicate(self): def test_custom_predicate(self):
nodes = [self._node(properties={'local_gb': i}) nodes = [self._node(properties={'local_gb': i})
@@ -252,9 +308,7 @@ class TestReserveNode(Base):
resource_class=self.RSC, traits=None) resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_custom_predicate_false(self): def test_custom_predicate_false(self):
nodes = [self._node() for _ in range(3)] nodes = [self._node() for _ in range(3)]
@@ -281,9 +335,7 @@ class TestReserveNode(Base):
resource_class=self.RSC, traits=None) resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_provided_nodes(self): def test_provided_nodes(self):
nodes = [self._node(id=1), self._node(id=2)] nodes = [self._node(id=1), self._node(id=2)]
@@ -298,9 +350,7 @@ class TestReserveNode(Base):
resource_class=self.RSC, traits=None) resource_class=self.RSC, traits=None)
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.assertFalse(self.api.baremetal.patch_node.called)
node, [{'path': '/instance_info/metalsmith_hostname',
'op': 'add', 'value': node.id}])
def test_nodes_filtered(self): def test_nodes_filtered(self):
nodes = [self._node(resource_class='banana'), 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.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname', node, [{'path': '/instance_info/capabilities',
'op': 'add', 'value': node.id},
{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'cat': 'meow'}}]) 'op': 'add', 'value': {'cat': 'meow'}}])
def test_nodes_filtered_by_conductor_group(self): 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.get_node.assert_called_once_with(
self.api.baremetal.wait_for_allocation.return_value.node_id) self.api.baremetal.wait_for_allocation.return_value.node_id)
self.api.baremetal.patch_node.assert_called_once_with( self.api.baremetal.patch_node.assert_called_once_with(
node, [{'path': '/instance_info/metalsmith_hostname', node, [{'path': '/instance_info/capabilities',
'op': 'add', 'value': node.id},
{'path': '/instance_info/capabilities',
'op': 'add', 'value': {'cat': 'meow'}}]) 'op': 'add', 'value': {'cat': 'meow'}}])
def test_provided_nodes_no_match(self): def test_provided_nodes_no_match(self):
@@ -377,14 +423,18 @@ class TestProvisionNode(Base):
def setUp(self): def setUp(self):
super(TestProvisionNode, self).setUp() super(TestProvisionNode, self).setUp()
self.image = self.api.image.find_image.return_value 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 = { self.instance_info = {
'ramdisk': self.image.ramdisk_id, 'ramdisk': self.image.ramdisk_id,
'kernel': self.image.kernel_id, 'kernel': self.image.kernel_id,
'image_source': self.image.id, 'image_source': self.image.id,
'root_gb': 99, # 100 - 1 'root_gb': 99, # 100 - 1
'capabilities': {'boot_option': 'local'}, 'capabilities': {'boot_option': 'local'},
_utils.HOSTNAME_FIELD: 'control-0'
} }
self.extra = { self.extra = {
'metalsmith_created_ports': [ 'metalsmith_created_ports': [
@@ -398,6 +448,9 @@ class TestProvisionNode(Base):
fixtures.MockPatchObject(instance_config.GenericConfig, fixtures.MockPatchObject(instance_config.GenericConfig,
'generate', autospec=True) 'generate', autospec=True)
).mock ).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): def test_ok(self):
inst = self.pr.provision_node(self.node, 'image', 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.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info=self.instance_info, extra=self.extra) self.node, instance_info=self.instance_info, extra=self.extra)
self.api.baremetal.validate_node.assert_called_once_with(self.node) 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.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY) self.node, 'active', config_drive=mock.ANY)
self.assertFalse(self.api.network.delete_port.called) 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.uuid, self.node.id)
self.assertEqual(inst.node, self.node) 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( self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id) network_id=self.api.network.find_network.return_value.id)
self.api.baremetal.attach_vif_to_node.assert_called_once_with( 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.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.called) self.assertFalse(self.api.network.delete_port.called)
@mock.patch.object(_provisioner.Provisioner, '_find_node_by_hostname', def test_with_hostname_override(self):
autospec=True) self.allocation.name = None
def test_with_hostname(self, mock_find_node): self.api.baremetal.get_allocation.side_effect = [
mock_find_node.return_value = None 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' hostname = 'control-0.example.com'
inst = self.pr.provision_node(self.node, 'image', inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}], [{'network': 'network'}],
hostname=hostname) hostname=hostname)
self.instance_info[_utils.HOSTNAME_FIELD] = hostname
self.assertEqual(inst.uuid, self.node.id) self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node) 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( self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id) network_id=self.api.network.find_network.return_value.id)
self.api.baremetal.attach_vif_to_node.assert_called_once_with( 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.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info=self.instance_info, extra=self.extra) self.node, instance_info=self.instance_info, extra=self.extra)
self.api.baremetal.validate_node.assert_called_once_with(self.node) 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.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY) self.node, 'active', config_drive=mock.ANY)
self.assertFalse( self.assertFalse(
@@ -510,14 +598,15 @@ class TestProvisionNode(Base):
def test_existing_hostname(self): def test_existing_hostname(self):
hostname = 'control-0.example.com' 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', inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}]) [{'network': 'network'}])
self.instance_info[_utils.HOSTNAME_FIELD] = hostname
self.assertEqual(inst.uuid, self.node.id) self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node) 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( self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id) network_id=self.api.network.find_network.return_value.id)
self.api.baremetal.attach_vif_to_node.assert_called_once_with( 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.api.baremetal.update_node.assert_called_once_with(
self.node, instance_info=self.instance_info, extra=self.extra) self.node, instance_info=self.instance_info, extra=self.extra)
self.api.baremetal.validate_node.assert_called_once_with(self.node) 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.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY) self.node, 'active', config_drive=mock.ANY)
self.assertFalse( self.assertFalse(
@@ -534,12 +706,19 @@ class TestProvisionNode(Base):
def test_name_not_valid_hostname(self): def test_name_not_valid_hostname(self):
self.node.name = 'node_1' 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', inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}]) [{'network': 'network'}])
self.instance_info[_utils.HOSTNAME_FIELD] = '000'
self.assertEqual(inst.uuid, self.node.id) self.assertEqual(inst.uuid, self.node.id)
self.assertEqual(inst.node, self.node) self.assertEqual(inst.node, self.node)
self.assertIs(inst.allocation, self.allocation)
self.api.network.create_port.assert_called_once_with( self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id) 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.api.baremetal.update_node.assert_called_once_with(
self.node, extra=self.extra, instance_info=self.instance_info) self.node, extra=self.extra, instance_info=self.instance_info)
self.api.baremetal.validate_node.assert_called_once_with(self.node) 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.api.baremetal.set_node_provision_state.assert_called_once_with(
self.node, 'active', config_drive=mock.ANY) self.node, 'active', config_drive=mock.ANY)
self.assertFalse( self.assertFalse(
@@ -556,15 +737,21 @@ class TestProvisionNode(Base):
def test_unreserved(self): def test_unreserved(self):
self.node.instance_id = None self.node.instance_id = None
self.node.allocation_id = None
self.api.baremetal.get_node.return_value = self.node self.api.baremetal.get_node.return_value = self.node
self.pr.provision_node(self.node, 'image', [{'network': 'network'}]) self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.api.baremetal.create_allocation.assert_called_once_with( 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) resource_class=self.node.resource_class, traits=None)
self.api.baremetal.get_node.assert_called_once_with( self.api.baremetal.get_node.assert_has_calls([
self.api.baremetal.wait_for_allocation.return_value.node_id) # 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( self.api.network.create_port.assert_called_once_with(
network_id=self.api.network.find_network.return_value.id) network_id=self.api.network.find_network.return_value.id)
self.api.baremetal.attach_vif_to_node.assert_called_once_with( 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.api.baremetal.wait_for_nodes_provision_state.called)
self.assertFalse(self.api.network.delete_port.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): def test_with_ports(self):
port_ids = [self.api.network.find_port.return_value.id] * 2 port_ids = [self.api.network.find_port.return_value.id] * 2
@@ -1013,11 +1248,13 @@ abcd image
def test_unreserve_dry_run(self): def test_unreserve_dry_run(self):
self.pr._dry_run = True self.pr._dry_run = True
self.node.allocation_id = None
self.node.instance_id = None self.node.instance_id = None
self.pr.provision_node(self.node, 'image', [{'network': 'network'}]) self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.network.create_port.called) 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.attach_vif_to_node.called)
self.assertFalse(self.api.baremetal.update_node.called) self.assertFalse(self.api.baremetal.update_node.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called) self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@@ -1033,6 +1270,33 @@ abcd image
'image', [{'network': 'n1'}, {'port': 'p1'}], 'image', [{'network': 'n1'}, {'port': 'p1'}],
wait=3600) 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.api.baremetal.update_node.assert_any_call(
self.node, extra={}, instance_info={}, instance_id=None) self.node, extra={}, instance_info={}, instance_id=None)
self.assertFalse( self.assertFalse(
@@ -1049,31 +1313,6 @@ abcd image
calls, any_order=True) calls, any_order=True)
self.assertFalse(self.api.baremetal.delete_allocation.called) 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): def test_deploy_failure_no_cleanup(self):
self.node.allocation_id = 'id2' self.node.allocation_id = 'id2'
self.api.baremetal.set_node_provision_state.side_effect = ( self.api.baremetal.set_node_provision_state.side_effect = (
@@ -1096,8 +1335,10 @@ abcd image
self.pr.provision_node, self.node, self.pr.provision_node, self.node,
'image', [{'network': 'network'}], wait=3600) '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.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.baremetal.set_node_provision_state.called)
self.assertFalse(self.api.network.delete_port.called) self.assertFalse(self.api.network.delete_port.called)
self.assertFalse(self.api.baremetal.detach_vif_from_node.called) self.assertFalse(self.api.baremetal.detach_vif_from_node.called)
@@ -1109,8 +1350,10 @@ abcd image
self.pr.provision_node, self.node, self.pr.provision_node, self.node,
'image', [{'network': 'network'}], wait=3600) '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.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.baremetal.set_node_provision_state.called)
self.api.network.delete_port.assert_called_once_with( self.api.network.delete_port.assert_called_once_with(
self.api.network.create_port.return_value.id, self.api.network.create_port.return_value.id,
@@ -1152,7 +1395,7 @@ abcd image
self.node, self.api.network.create_port.return_value.id) self.node, self.api.network.create_port.return_value.id)
def test_detach_failed_after_deploy_failure(self): 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() self._test_failure_during_deploy_failure()
def test_update_failed_after_deploy_failure(self): def test_update_failed_after_deploy_failure(self):
@@ -1160,6 +1403,10 @@ abcd image
AssertionError()] AssertionError()]
self._test_failure_during_deploy_failure() 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): def test_wait_failure(self):
self.api.baremetal.wait_for_nodes_provision_state.side_effect = ( self.api.baremetal.wait_for_nodes_provision_state.side_effect = (
RuntimeError('boom')) RuntimeError('boom'))
@@ -1181,7 +1428,7 @@ abcd image
self.pr.provision_node, self.pr.provision_node,
self.node, 'image', [{'network': 'network'}]) self.node, 'image', [{'network': 'network'}])
self.api.baremetal.update_node.assert_called_once_with( 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.baremetal.set_node_provision_state.called)
@mock.patch.object(requests, 'get', autospec=True) @mock.patch.object(requests, 'get', autospec=True)
@@ -1207,7 +1454,7 @@ abcd and-not-image-again
self.assertFalse(self.api.image.find_image.called) self.assertFalse(self.api.image.find_image.called)
mock_get.assert_called_once_with('https://host/checksums') mock_get.assert_called_once_with('https://host/checksums')
self.api.baremetal.update_node.assert_called_once_with( 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.baremetal.set_node_provision_state.called)
@mock.patch.object(requests, 'get', autospec=True) @mock.patch.object(requests, 'get', autospec=True)
@@ -1233,7 +1480,7 @@ abcd and-not-image-again
self.assertFalse(self.api.image.find_image.called) self.assertFalse(self.api.image.find_image.called)
mock_get.assert_called_once_with('https://host/checksums') mock_get.assert_called_once_with('https://host/checksums')
self.api.baremetal.update_node.assert_called_once_with( 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.baremetal.set_node_provision_state.called)
@mock.patch.object(requests, 'get', autospec=True) @mock.patch.object(requests, 'get', autospec=True)
@@ -1257,7 +1504,7 @@ abcd and-not-image-again
self.assertFalse(self.api.image.find_image.called) self.assertFalse(self.api.image.find_image.called)
mock_get.assert_called_once_with('https://host/checksums') mock_get.assert_called_once_with('https://host/checksums')
self.api.baremetal.update_node.assert_called_once_with( 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.baremetal.set_node_provision_state.called)
def test_invalid_network(self): def test_invalid_network(self):
@@ -1267,7 +1514,7 @@ abcd and-not-image-again
self.pr.provision_node, self.pr.provision_node,
self.node, 'image', [{'network': 'network'}]) self.node, 'image', [{'network': 'network'}])
self.api.baremetal.update_node.assert_called_once_with( 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.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.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.pr.provision_node,
self.node, 'image', [{'port': 'port1'}]) self.node, 'image', [{'port': 'port1'}])
self.api.baremetal.update_node.assert_called_once_with( 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.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.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.pr.provision_node,
self.node, 'image', [{'subnet': 'subnet'}]) self.node, 'image', [{'subnet': 'subnet'}])
self.api.baremetal.update_node.assert_called_once_with( 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.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.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.pr.provision_node,
self.node, 'image', [{'subnet': 'subnet'}]) self.node, 'image', [{'subnet': 'subnet'}])
self.api.baremetal.update_node.assert_called_once_with( 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.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.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.pr.provision_node,
self.node, 'image', [{'port': 'port1'}], self.node, 'image', [{'port': 'port1'}],
hostname='n_1') 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.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.called) self.assertFalse(self.api.baremetal.set_node_provision_state.called)
@mock.patch.object(_provisioner.Provisioner, '_find_node_by_hostname', def test_duplicate_hostname(self):
autospec=True) allocation = mock.Mock(spec=['id', 'name', 'node_id'],
def test_duplicate_hostname(self, mock_find_node): node_id='another node')
mock_find_node.return_value = mock.Mock(spec=['id', 'name']) self.api.baremetal.get_allocation.side_effect = [allocation]
self.assertRaisesRegex(ValueError, 'already uses hostname host', self.assertRaisesRegex(ValueError, 'already uses hostname host',
self.pr.provision_node, self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}], self.node, 'image', [{'port': 'port1'}],
hostname='host') hostname='host')
self.api.baremetal.update_node.assert_called_once_with( self.assertFalse(self.api.network.create_port.called)
self.node, extra={}, instance_info={}, instance_id=None) 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.network.create_port.called)
self.assertFalse(self.api.baremetal.set_node_provision_state.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): def test_node_with_external_instance_id(self):
self.node.instance_id = 'nova' self.node.instance_id = 'nova'
self.node.allocation_id = None
self.assertRaisesRegex(exceptions.InvalidNode, self.assertRaisesRegex(exceptions.InvalidNode,
'reserved by instance nova', 'reserved by instance nova',
self.pr.provision_node, self.pr.provision_node,
@@ -1551,39 +1806,56 @@ class TestUnprovisionNode(Base):
self.assertFalse(self.api.baremetal.update_node.called) self.assertFalse(self.api.baremetal.update_node.called)
class TestShowInstance(Base): class TestShowInstance(testtools.TestCase):
def setUp(self): def setUp(self):
super(TestShowInstance, self).setUp() 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): 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') 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.assertIsInstance(inst, _instance.Instance)
self.assertIs(inst.node, self.node) self.assertIs(inst.node, self.node)
self.assertIs(inst.uuid, self.node.id) self.assertIs(inst.uuid, self.node.id)
self.api.baremetal.get_node.assert_called_once_with('id1')
def test_show_instance_with_allocation(self): def test_show_instance_with_allocation(self):
self.node.allocation_id = 'id2' self.api.baremetal.get_allocation.return_value.node_id = '1234'
self.mock_get_node.side_effect = lambda n, *a, **kw: self.node
inst = self.pr.show_instance('id1') inst = self.pr.show_instance('id1')
self.mock_get_node.assert_called_once_with(self.pr, 'id1', self.api.baremetal.get_allocation.assert_called_once_with('id1')
accept_hostname=True)
self.api.baremetal.get_allocation.assert_called_once_with('id2')
self.assertIsInstance(inst, _instance.Instance) self.assertIsInstance(inst, _instance.Instance)
self.assertIs(inst.allocation, self.assertIs(inst.allocation,
self.api.baremetal.get_allocation.return_value) self.api.baremetal.get_allocation.return_value)
self.assertIs(inst.node, self.node) self.assertIs(inst.node, self.node)
self.assertIs(inst.uuid, self.node.id) self.assertIs(inst.uuid, self.node.id)
self.api.baremetal.get_node.assert_called_once_with('1234')
def test_show_instances(self): def test_show_instances(self):
self.mock_get_node.side_effect = [self.node, self.node] self.api.baremetal.get_allocation.side_effect = [
result = self.pr.show_instances(['1', '2']) os_exc.ResourceNotFound(),
self.mock_get_node.assert_has_calls([ mock.Mock(node_id='4321'),
mock.call(self.pr, '1', accept_hostname=True), ]
mock.call(self.pr, '2', accept_hostname=True) 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) self.assertIsInstance(result, list)
for inst in result: for inst in result:
@@ -1591,6 +1863,14 @@ class TestShowInstance(Base):
self.assertIs(result[0].node, self.node) self.assertIs(result[0].node, self.node)
self.assertIs(result[0].uuid, self.node.id) 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): class TestWaitForProvisioning(Base):
@@ -1617,7 +1897,8 @@ class TestListInstances(Base):
def test_list(self): def test_list(self):
instances = self.pr.list_instances() 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.nodes[:6], [i.node for i in instances])
self.assertEqual([self.api.baremetal.get_allocation.return_value] self.assertEqual([self.api.baremetal.get_allocation.return_value]
+ [None] * 5, + [None] * 5,

View File

@@ -13,11 +13,9 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import mock
import testtools import testtools
from metalsmith import _utils from metalsmith import _utils
from metalsmith import exceptions
class TestIsHostnameSafe(testtools.TestCase): class TestIsHostnameSafe(testtools.TestCase):
@@ -68,74 +66,3 @@ class TestIsHostnameSafe(testtools.TestCase):
# Need to ensure a binary response for success or fail # Need to ensure a binary response for success or fail
self.assertIsNotNone(_utils.is_hostname_safe('spam')) self.assertIsNotNone(_utils.is_hostname_safe('spam'))
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 # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0 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 requests>=2.18.4 # Apache-2.0
six>=1.10.0 # MIT six>=1.10.0 # MIT
enum34>=1.0.4;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD enum34>=1.0.4;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD