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:
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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()
|
||||||
|
@@ -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'}})
|
||||||
|
@@ -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,
|
||||||
|
@@ -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)
|
|
||||||
|
11
releasenotes/notes/allocation-hostname-f148e8adf0aee89a.yaml
Normal file
11
releasenotes/notes/allocation-hostname-f148e8adf0aee89a.yaml
Normal 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.
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user