Merge "AgentVendorInterface: Move to a common place"
This commit is contained in:
commit
dcff0511c1
@ -15,14 +15,10 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo.utils import excutils
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from ironic.common import dhcp_factory
|
from ironic.common import dhcp_factory
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
from ironic.common.i18n import _LE
|
|
||||||
from ironic.common.i18n import _LW
|
|
||||||
from ironic.common import image_service
|
from ironic.common import image_service
|
||||||
from ironic.common import keystone
|
from ironic.common import keystone
|
||||||
from ironic.common import paths
|
from ironic.common import paths
|
||||||
@ -32,10 +28,10 @@ from ironic.common import utils
|
|||||||
from ironic.conductor import task_manager
|
from ironic.conductor import task_manager
|
||||||
from ironic.conductor import utils as manager_utils
|
from ironic.conductor import utils as manager_utils
|
||||||
from ironic.drivers import base
|
from ironic.drivers import base
|
||||||
|
from ironic.drivers.modules import agent_base_vendor
|
||||||
from ironic.drivers.modules import agent_client
|
from ironic.drivers.modules import agent_client
|
||||||
from ironic.drivers.modules import deploy_utils
|
from ironic.drivers.modules import deploy_utils
|
||||||
from ironic.drivers.modules import image_cache
|
from ironic.drivers.modules import image_cache
|
||||||
from ironic import objects
|
|
||||||
from ironic.openstack.common import fileutils
|
from ironic.openstack.common import fileutils
|
||||||
from ironic.openstack.common import log
|
from ironic.openstack.common import log
|
||||||
|
|
||||||
@ -51,9 +47,6 @@ agent_opts = [
|
|||||||
cfg.StrOpt('agent_pxe_bootfile_name',
|
cfg.StrOpt('agent_pxe_bootfile_name',
|
||||||
default='pxelinux.0',
|
default='pxelinux.0',
|
||||||
help='Neutron bootfile DHCP parameter.'),
|
help='Neutron bootfile DHCP parameter.'),
|
||||||
cfg.IntOpt('heartbeat_timeout',
|
|
||||||
default=300,
|
|
||||||
help='Maximum interval (in seconds) for agent heartbeats.'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -281,98 +274,13 @@ class AgentDeploy(base.DeployInterface):
|
|||||||
provider.update_dhcp(task, CONF.agent.agent_pxe_bootfile_name)
|
provider.update_dhcp(task, CONF.agent.agent_pxe_bootfile_name)
|
||||||
|
|
||||||
|
|
||||||
class AgentVendorInterface(base.VendorInterface):
|
class AgentVendorInterface(agent_base_vendor.BaseAgentVendor):
|
||||||
|
|
||||||
def __init__(self):
|
def deploy_is_done(self, task):
|
||||||
self.supported_payload_versions = ['2']
|
return self._client.deploy_is_done(task.node)
|
||||||
self._client = _get_client()
|
|
||||||
|
|
||||||
def get_properties(self):
|
|
||||||
"""Return the properties of the interface.
|
|
||||||
|
|
||||||
:returns: dictionary of <property name>:<property description> entries.
|
|
||||||
"""
|
|
||||||
# NOTE(jroll) all properties are set by the driver,
|
|
||||||
# not by the operator.
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def validate(self, task, method, **kwargs):
|
|
||||||
"""Validate the driver-specific Node deployment info.
|
|
||||||
|
|
||||||
No validation necessary.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance
|
|
||||||
:param method: method to be validated
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def driver_validate(self, method, **kwargs):
|
|
||||||
"""Validate the driver deployment info.
|
|
||||||
|
|
||||||
:param method: method to be validated.
|
|
||||||
"""
|
|
||||||
version = kwargs.get('version')
|
|
||||||
|
|
||||||
if not version:
|
|
||||||
raise exception.MissingParameterValue(_('Missing parameter '
|
|
||||||
'version'))
|
|
||||||
if version not in self.supported_payload_versions:
|
|
||||||
raise exception.InvalidParameterValue(_('Unknown lookup '
|
|
||||||
'payload version: %s')
|
|
||||||
% version)
|
|
||||||
|
|
||||||
@base.passthru(['POST'])
|
|
||||||
def heartbeat(self, task, **kwargs):
|
|
||||||
"""Method for agent to periodically check in.
|
|
||||||
|
|
||||||
The agent should be sending its agent_url (so Ironic can talk back)
|
|
||||||
as a kwarg. kwargs should have the following format::
|
|
||||||
|
|
||||||
{
|
|
||||||
'agent_url': 'http://AGENT_HOST:AGENT_PORT'
|
|
||||||
}
|
|
||||||
|
|
||||||
AGENT_PORT defaults to 9999.
|
|
||||||
"""
|
|
||||||
node = task.node
|
|
||||||
driver_internal_info = node.driver_internal_info
|
|
||||||
LOG.debug(
|
|
||||||
'Heartbeat from %(node)s, last heartbeat at %(heartbeat)s.',
|
|
||||||
{'node': node.uuid,
|
|
||||||
'heartbeat': driver_internal_info.get('agent_last_heartbeat')})
|
|
||||||
driver_internal_info['agent_last_heartbeat'] = int(_time())
|
|
||||||
try:
|
|
||||||
driver_internal_info['agent_url'] = kwargs['agent_url']
|
|
||||||
except KeyError:
|
|
||||||
raise exception.MissingParameterValue(_('For heartbeat operation, '
|
|
||||||
'"agent_url" must be '
|
|
||||||
'specified.'))
|
|
||||||
|
|
||||||
node.driver_internal_info = driver_internal_info
|
|
||||||
node.save()
|
|
||||||
|
|
||||||
# Async call backs don't set error state on their own
|
|
||||||
# TODO(jimrollenhagen) improve error messages here
|
|
||||||
msg = _('Failed checking if deploy is done.')
|
|
||||||
try:
|
|
||||||
if node.provision_state == states.DEPLOYWAIT:
|
|
||||||
msg = _('Node failed to get image for deploy.')
|
|
||||||
self._continue_deploy(task, **kwargs)
|
|
||||||
elif (node.provision_state == states.DEPLOYING
|
|
||||||
and self._deploy_is_done(node)):
|
|
||||||
msg = _('Node failed to move to active state.')
|
|
||||||
self._reboot_to_instance(task, **kwargs)
|
|
||||||
except Exception:
|
|
||||||
LOG.exception(_LE('Async exception for %(node)s: %(msg)s'),
|
|
||||||
{'node': node,
|
|
||||||
'msg': msg})
|
|
||||||
deploy_utils.set_failed_state(task, msg)
|
|
||||||
|
|
||||||
def _deploy_is_done(self, node):
|
|
||||||
return self._client.deploy_is_done(node)
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
@task_manager.require_exclusive_lock
|
||||||
def _continue_deploy(self, task, **kwargs):
|
def continue_deploy(self, task, **kwargs):
|
||||||
task.process_event('resume')
|
task.process_event('resume')
|
||||||
node = task.node
|
node = task.node
|
||||||
image_source = node.instance_info.get('image_source')
|
image_source = node.instance_info.get('image_source')
|
||||||
@ -403,7 +311,7 @@ class AgentVendorInterface(base.VendorInterface):
|
|||||||
if command['command_status'] == 'FAILED':
|
if command['command_status'] == 'FAILED':
|
||||||
return command['command_error']
|
return command['command_error']
|
||||||
|
|
||||||
def _reboot_to_instance(self, task, **kwargs):
|
def reboot_to_instance(self, task, **kwargs):
|
||||||
node = task.node
|
node = task.node
|
||||||
LOG.debug('Preparing to reboot to instance for node %s',
|
LOG.debug('Preparing to reboot to instance for node %s',
|
||||||
node.uuid)
|
node.uuid)
|
||||||
@ -423,162 +331,3 @@ class AgentVendorInterface(base.VendorInterface):
|
|||||||
manager_utils.node_power_action(task, states.REBOOT)
|
manager_utils.node_power_action(task, states.REBOOT)
|
||||||
|
|
||||||
task.process_event('done')
|
task.process_event('done')
|
||||||
|
|
||||||
@base.driver_passthru(['POST'], async=False)
|
|
||||||
def lookup(self, context, **kwargs):
|
|
||||||
"""Find a matching node for the agent.
|
|
||||||
|
|
||||||
Method to be called the first time a ramdisk agent checks in. This
|
|
||||||
can be because this is a node just entering decom or a node that
|
|
||||||
rebooted for some reason. We will use the mac addresses listed in the
|
|
||||||
kwargs to find the matching node, then return the node object to the
|
|
||||||
agent. The agent can that use that UUID to use the node vendor
|
|
||||||
passthru method.
|
|
||||||
|
|
||||||
Currently, we don't handle the instance where the agent doesn't have
|
|
||||||
a matching node (i.e. a brand new, never been in Ironic node).
|
|
||||||
|
|
||||||
kwargs should have the following format::
|
|
||||||
|
|
||||||
{
|
|
||||||
"version": "2"
|
|
||||||
"inventory": {
|
|
||||||
"interfaces": [
|
|
||||||
{
|
|
||||||
"name": "eth0",
|
|
||||||
"mac_address": "00:11:22:33:44:55",
|
|
||||||
"switch_port_descr": "port24"
|
|
||||||
"switch_chassis_descr": "tor1"
|
|
||||||
}, ...
|
|
||||||
], ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The interfaces list should include a list of the non-IPMI MAC addresses
|
|
||||||
in the form aa:bb:cc:dd:ee:ff.
|
|
||||||
|
|
||||||
This method will also return the timeout for heartbeats. The driver
|
|
||||||
will expect the agent to heartbeat before that timeout, or it will be
|
|
||||||
considered down. This will be in a root level key called
|
|
||||||
'heartbeat_timeout'
|
|
||||||
|
|
||||||
:raises: NotFound if no matching node is found.
|
|
||||||
:raises: InvalidParameterValue with unknown payload version
|
|
||||||
"""
|
|
||||||
inventory = kwargs.get('inventory')
|
|
||||||
interfaces = self._get_interfaces(inventory)
|
|
||||||
mac_addresses = self._get_mac_addresses(interfaces)
|
|
||||||
|
|
||||||
node = self._find_node_by_macs(context, mac_addresses)
|
|
||||||
|
|
||||||
LOG.debug('Initial lookup for node %s succeeded.', node.uuid)
|
|
||||||
|
|
||||||
# Only support additional hardware in v2 and above. Grab all the
|
|
||||||
# top level keys in inventory that aren't interfaces and add them.
|
|
||||||
# Nest it in 'hardware' to avoid namespace issues
|
|
||||||
hardware = {
|
|
||||||
'hardware': {
|
|
||||||
'network': interfaces
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
if key != 'interfaces':
|
|
||||||
hardware['hardware'][key] = value
|
|
||||||
|
|
||||||
return {
|
|
||||||
'heartbeat_timeout': CONF.agent.heartbeat_timeout,
|
|
||||||
'node': node
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_interfaces(self, inventory):
|
|
||||||
interfaces = []
|
|
||||||
try:
|
|
||||||
interfaces = inventory['interfaces']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
'Malformed network interfaces lookup: %s') % inventory)
|
|
||||||
|
|
||||||
return interfaces
|
|
||||||
|
|
||||||
def _get_mac_addresses(self, interfaces):
|
|
||||||
"""Returns MACs for the network devices."""
|
|
||||||
mac_addresses = []
|
|
||||||
|
|
||||||
for interface in interfaces:
|
|
||||||
try:
|
|
||||||
mac_addresses.append(utils.validate_and_normalize_mac(
|
|
||||||
interface.get('mac_address')))
|
|
||||||
except exception.InvalidMAC:
|
|
||||||
LOG.warning(_LW('Malformed MAC: %s'), interface.get(
|
|
||||||
'mac_address'))
|
|
||||||
return mac_addresses
|
|
||||||
|
|
||||||
def _find_node_by_macs(self, context, mac_addresses):
|
|
||||||
"""Get nodes for a given list of MAC addresses.
|
|
||||||
|
|
||||||
Given a list of MAC addresses, find the ports that match the MACs
|
|
||||||
and return the node they are all connected to.
|
|
||||||
|
|
||||||
:raises: NodeNotFound if the ports point to multiple nodes or no
|
|
||||||
nodes.
|
|
||||||
"""
|
|
||||||
ports = self._find_ports_by_macs(context, mac_addresses)
|
|
||||||
if not ports:
|
|
||||||
raise exception.NodeNotFound(_(
|
|
||||||
'No ports matching the given MAC addresses %sexist in the '
|
|
||||||
'database.') % mac_addresses)
|
|
||||||
node_id = self._get_node_id(ports)
|
|
||||||
try:
|
|
||||||
node = objects.Node.get_by_id(context, node_id)
|
|
||||||
except exception.NodeNotFound:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.exception(_LE('Could not find matching node for the '
|
|
||||||
'provided MACs %s.'), mac_addresses)
|
|
||||||
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _find_ports_by_macs(self, context, mac_addresses):
|
|
||||||
"""Get ports for a given list of MAC addresses.
|
|
||||||
|
|
||||||
Given a list of MAC addresses, find the ports that match the MACs
|
|
||||||
and return them as a list of Port objects, or an empty list if there
|
|
||||||
are no matches
|
|
||||||
"""
|
|
||||||
ports = []
|
|
||||||
for mac in mac_addresses:
|
|
||||||
# Will do a search by mac if the mac isn't malformed
|
|
||||||
try:
|
|
||||||
port_ob = objects.Port.get_by_address(context, mac)
|
|
||||||
ports.append(port_ob)
|
|
||||||
|
|
||||||
except exception.PortNotFound:
|
|
||||||
LOG.warning(_LW('MAC address %s not found in database'), mac)
|
|
||||||
|
|
||||||
return ports
|
|
||||||
|
|
||||||
def _get_node_id(self, ports):
|
|
||||||
"""Get a node ID for a list of ports.
|
|
||||||
|
|
||||||
Given a list of ports, either return the node_id they all share or
|
|
||||||
raise a NotFound if there are multiple node_ids, which indicates some
|
|
||||||
ports are connected to one node and the remaining port(s) are connected
|
|
||||||
to one or more other nodes.
|
|
||||||
|
|
||||||
:raises: NodeNotFound if the MACs match multiple nodes. This
|
|
||||||
could happen if you swapped a NIC from one server to another and
|
|
||||||
don't notify Ironic about it or there is a MAC collision (since
|
|
||||||
they're not guaranteed to be unique).
|
|
||||||
"""
|
|
||||||
# See if all the ports point to the same node
|
|
||||||
node_ids = set(port_ob.node_id for port_ob in ports)
|
|
||||||
if len(node_ids) > 1:
|
|
||||||
raise exception.NodeNotFound(_(
|
|
||||||
'Ports matching mac addresses match multiple nodes. MACs: '
|
|
||||||
'%(macs)s. Port ids: %(port_ids)s') %
|
|
||||||
{'macs': [port_ob.address for port_ob in ports], 'port_ids':
|
|
||||||
[port_ob.uuid for port_ob in ports]}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only have one node_id left, return it.
|
|
||||||
return node_ids.pop()
|
|
||||||
|
330
ironic/drivers/modules/agent_base_vendor.py
Normal file
330
ironic/drivers/modules/agent_base_vendor.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright 2014 Rackspace, Inc.
|
||||||
|
# Copyright 2015 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo.utils import excutils
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common.i18n import _
|
||||||
|
from ironic.common.i18n import _LE
|
||||||
|
from ironic.common.i18n import _LW
|
||||||
|
from ironic.common import states
|
||||||
|
from ironic.common import utils
|
||||||
|
from ironic.drivers import base
|
||||||
|
from ironic.drivers.modules import agent_client
|
||||||
|
from ironic.drivers.modules import deploy_utils
|
||||||
|
from ironic import objects
|
||||||
|
from ironic.openstack.common import log
|
||||||
|
|
||||||
|
agent_opts = [
|
||||||
|
cfg.IntOpt('heartbeat_timeout',
|
||||||
|
default=300,
|
||||||
|
help='Maximum interval (in seconds) for agent heartbeats.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(agent_opts, group='agent')
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _time():
|
||||||
|
"""Broken out for testing."""
|
||||||
|
return time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client():
|
||||||
|
client = agent_client.AgentClient()
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgentVendor(base.VendorInterface):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.supported_payload_versions = ['2']
|
||||||
|
self._client = _get_client()
|
||||||
|
|
||||||
|
def continue_deploy(self, task, **kwargs):
|
||||||
|
"""Continues the deployment of baremetal node.
|
||||||
|
|
||||||
|
This method continues the deployment of the baremetal node after
|
||||||
|
the ramdisk have been booted.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def deploy_is_done(self, task):
|
||||||
|
"""Check if the deployment is already completed.
|
||||||
|
|
||||||
|
:returns: True if the deployment is completed. False otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reboot_to_instance(self, task, **kwargs):
|
||||||
|
"""Method invoked after the deployment is completed.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_properties(self):
|
||||||
|
"""Return the properties of the interface.
|
||||||
|
|
||||||
|
:returns: dictionary of <property name>:<property description> entries.
|
||||||
|
"""
|
||||||
|
# NOTE(jroll) all properties are set by the driver,
|
||||||
|
# not by the operator.
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def validate(self, task, method, **kwargs):
|
||||||
|
"""Validate the driver-specific Node deployment info.
|
||||||
|
|
||||||
|
No validation necessary.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance
|
||||||
|
:param method: method to be validated
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def driver_validate(self, method, **kwargs):
|
||||||
|
"""Validate the driver deployment info.
|
||||||
|
|
||||||
|
:param method: method to be validated.
|
||||||
|
"""
|
||||||
|
version = kwargs.get('version')
|
||||||
|
|
||||||
|
if not version:
|
||||||
|
raise exception.MissingParameterValue(_('Missing parameter '
|
||||||
|
'version'))
|
||||||
|
if version not in self.supported_payload_versions:
|
||||||
|
raise exception.InvalidParameterValue(_('Unknown lookup '
|
||||||
|
'payload version: %s')
|
||||||
|
% version)
|
||||||
|
|
||||||
|
@base.passthru(['POST'])
|
||||||
|
def heartbeat(self, task, **kwargs):
|
||||||
|
"""Method for agent to periodically check in.
|
||||||
|
|
||||||
|
The agent should be sending its agent_url (so Ironic can talk back)
|
||||||
|
as a kwarg. kwargs should have the following format::
|
||||||
|
|
||||||
|
{
|
||||||
|
'agent_url': 'http://AGENT_HOST:AGENT_PORT'
|
||||||
|
}
|
||||||
|
|
||||||
|
AGENT_PORT defaults to 9999.
|
||||||
|
"""
|
||||||
|
node = task.node
|
||||||
|
driver_internal_info = node.driver_internal_info
|
||||||
|
LOG.debug(
|
||||||
|
'Heartbeat from %(node)s, last heartbeat at %(heartbeat)s.',
|
||||||
|
{'node': node.uuid,
|
||||||
|
'heartbeat': driver_internal_info.get('agent_last_heartbeat')})
|
||||||
|
driver_internal_info['agent_last_heartbeat'] = int(_time())
|
||||||
|
try:
|
||||||
|
driver_internal_info['agent_url'] = kwargs['agent_url']
|
||||||
|
except KeyError:
|
||||||
|
raise exception.MissingParameterValue(_('For heartbeat operation, '
|
||||||
|
'"agent_url" must be '
|
||||||
|
'specified.'))
|
||||||
|
|
||||||
|
node.driver_internal_info = driver_internal_info
|
||||||
|
node.save()
|
||||||
|
|
||||||
|
# Async call backs don't set error state on their own
|
||||||
|
# TODO(jimrollenhagen) improve error messages here
|
||||||
|
msg = _('Failed checking if deploy is done.')
|
||||||
|
try:
|
||||||
|
if node.provision_state == states.DEPLOYWAIT:
|
||||||
|
msg = _('Node failed to get image for deploy.')
|
||||||
|
self.continue_deploy(task, **kwargs)
|
||||||
|
elif (node.provision_state == states.DEPLOYING and
|
||||||
|
self.deploy_is_done(task)):
|
||||||
|
msg = _('Node failed to move to active state.')
|
||||||
|
self.reboot_to_instance(task, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception(_LE('Async exception for %(node)s: %(msg)s'),
|
||||||
|
{'node': node,
|
||||||
|
'msg': msg})
|
||||||
|
deploy_utils.set_failed_state(task, msg)
|
||||||
|
|
||||||
|
@base.driver_passthru(['POST'], async=False)
|
||||||
|
def lookup(self, context, **kwargs):
|
||||||
|
"""Find a matching node for the agent.
|
||||||
|
|
||||||
|
Method to be called the first time a ramdisk agent checks in. This
|
||||||
|
can be because this is a node just entering decom or a node that
|
||||||
|
rebooted for some reason. We will use the mac addresses listed in the
|
||||||
|
kwargs to find the matching node, then return the node object to the
|
||||||
|
agent. The agent can that use that UUID to use the node vendor
|
||||||
|
passthru method.
|
||||||
|
|
||||||
|
Currently, we don't handle the instance where the agent doesn't have
|
||||||
|
a matching node (i.e. a brand new, never been in Ironic node).
|
||||||
|
|
||||||
|
kwargs should have the following format::
|
||||||
|
|
||||||
|
{
|
||||||
|
"version": "2"
|
||||||
|
"inventory": {
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"name": "eth0",
|
||||||
|
"mac_address": "00:11:22:33:44:55",
|
||||||
|
"switch_port_descr": "port24"
|
||||||
|
"switch_chassis_descr": "tor1"
|
||||||
|
}, ...
|
||||||
|
], ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The interfaces list should include a list of the non-IPMI MAC addresses
|
||||||
|
in the form aa:bb:cc:dd:ee:ff.
|
||||||
|
|
||||||
|
This method will also return the timeout for heartbeats. The driver
|
||||||
|
will expect the agent to heartbeat before that timeout, or it will be
|
||||||
|
considered down. This will be in a root level key called
|
||||||
|
'heartbeat_timeout'
|
||||||
|
|
||||||
|
:raises: NotFound if no matching node is found.
|
||||||
|
:raises: InvalidParameterValue with unknown payload version
|
||||||
|
"""
|
||||||
|
inventory = kwargs.get('inventory')
|
||||||
|
interfaces = self._get_interfaces(inventory)
|
||||||
|
mac_addresses = self._get_mac_addresses(interfaces)
|
||||||
|
|
||||||
|
node = self._find_node_by_macs(context, mac_addresses)
|
||||||
|
|
||||||
|
LOG.debug('Initial lookup for node %s succeeded.', node.uuid)
|
||||||
|
|
||||||
|
# Only support additional hardware in v2 and above. Grab all the
|
||||||
|
# top level keys in inventory that aren't interfaces and add them.
|
||||||
|
# Nest it in 'hardware' to avoid namespace issues
|
||||||
|
hardware = {
|
||||||
|
'hardware': {
|
||||||
|
'network': interfaces
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if key != 'interfaces':
|
||||||
|
hardware['hardware'][key] = value
|
||||||
|
|
||||||
|
return {
|
||||||
|
'heartbeat_timeout': CONF.agent.heartbeat_timeout,
|
||||||
|
'node': node
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_interfaces(self, inventory):
|
||||||
|
interfaces = []
|
||||||
|
try:
|
||||||
|
interfaces = inventory['interfaces']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
'Malformed network interfaces lookup: %s') % inventory)
|
||||||
|
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
def _get_mac_addresses(self, interfaces):
|
||||||
|
"""Returns MACs for the network devices."""
|
||||||
|
mac_addresses = []
|
||||||
|
|
||||||
|
for interface in interfaces:
|
||||||
|
try:
|
||||||
|
mac_addresses.append(utils.validate_and_normalize_mac(
|
||||||
|
interface.get('mac_address')))
|
||||||
|
except exception.InvalidMAC:
|
||||||
|
LOG.warning(_LW('Malformed MAC: %s'), interface.get(
|
||||||
|
'mac_address'))
|
||||||
|
return mac_addresses
|
||||||
|
|
||||||
|
def _find_node_by_macs(self, context, mac_addresses):
|
||||||
|
"""Get nodes for a given list of MAC addresses.
|
||||||
|
|
||||||
|
Given a list of MAC addresses, find the ports that match the MACs
|
||||||
|
and return the node they are all connected to.
|
||||||
|
|
||||||
|
:raises: NodeNotFound if the ports point to multiple nodes or no
|
||||||
|
nodes.
|
||||||
|
"""
|
||||||
|
ports = self._find_ports_by_macs(context, mac_addresses)
|
||||||
|
if not ports:
|
||||||
|
raise exception.NodeNotFound(_(
|
||||||
|
'No ports matching the given MAC addresses %sexist in the '
|
||||||
|
'database.') % mac_addresses)
|
||||||
|
node_id = self._get_node_id(ports)
|
||||||
|
try:
|
||||||
|
node = objects.Node.get_by_id(context, node_id)
|
||||||
|
except exception.NodeNotFound:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Could not find matching node for the '
|
||||||
|
'provided MACs %s.'), mac_addresses)
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
def _find_ports_by_macs(self, context, mac_addresses):
|
||||||
|
"""Get ports for a given list of MAC addresses.
|
||||||
|
|
||||||
|
Given a list of MAC addresses, find the ports that match the MACs
|
||||||
|
and return them as a list of Port objects, or an empty list if there
|
||||||
|
are no matches
|
||||||
|
"""
|
||||||
|
ports = []
|
||||||
|
for mac in mac_addresses:
|
||||||
|
# Will do a search by mac if the mac isn't malformed
|
||||||
|
try:
|
||||||
|
port_ob = objects.Port.get_by_address(context, mac)
|
||||||
|
ports.append(port_ob)
|
||||||
|
|
||||||
|
except exception.PortNotFound:
|
||||||
|
LOG.warning(_LW('MAC address %s not found in database'), mac)
|
||||||
|
|
||||||
|
return ports
|
||||||
|
|
||||||
|
def _get_node_id(self, ports):
|
||||||
|
"""Get a node ID for a list of ports.
|
||||||
|
|
||||||
|
Given a list of ports, either return the node_id they all share or
|
||||||
|
raise a NotFound if there are multiple node_ids, which indicates some
|
||||||
|
ports are connected to one node and the remaining port(s) are connected
|
||||||
|
to one or more other nodes.
|
||||||
|
|
||||||
|
:raises: NodeNotFound if the MACs match multiple nodes. This
|
||||||
|
could happen if you swapped a NIC from one server to another and
|
||||||
|
don't notify Ironic about it or there is a MAC collision (since
|
||||||
|
they're not guaranteed to be unique).
|
||||||
|
"""
|
||||||
|
# See if all the ports point to the same node
|
||||||
|
node_ids = set(port_ob.node_id for port_ob in ports)
|
||||||
|
if len(node_ids) > 1:
|
||||||
|
raise exception.NodeNotFound(_(
|
||||||
|
'Ports matching mac addresses match multiple nodes. MACs: '
|
||||||
|
'%(macs)s. Port ids: %(port_ids)s') %
|
||||||
|
{'macs': [port_ob.address for port_ob in ports], 'port_ids':
|
||||||
|
[port_ob.uuid for port_ob in ports]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only have one node_id left, return it.
|
||||||
|
return node_ids.pop()
|
@ -22,8 +22,6 @@ from ironic.common import pxe_utils
|
|||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
from ironic.conductor import task_manager
|
from ironic.conductor import task_manager
|
||||||
from ironic.drivers.modules import agent
|
from ironic.drivers.modules import agent
|
||||||
from ironic.drivers.modules import deploy_utils
|
|
||||||
from ironic import objects
|
|
||||||
from ironic.tests.conductor import utils as mgr_utils
|
from ironic.tests.conductor import utils as mgr_utils
|
||||||
from ironic.tests.db import base as db_base
|
from ironic.tests.db import base as db_base
|
||||||
from ironic.tests.db import utils as db_utils
|
from ironic.tests.db import utils as db_utils
|
||||||
@ -134,6 +132,7 @@ class TestAgentDeploy(db_base.DbTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestAgentVendor(db_base.DbTestCase):
|
class TestAgentVendor(db_base.DbTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestAgentVendor, self).setUp()
|
super(TestAgentVendor, self).setUp()
|
||||||
mgr_utils.mock_the_extension_manager(driver="fake_agent")
|
mgr_utils.mock_the_extension_manager(driver="fake_agent")
|
||||||
@ -146,30 +145,6 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
}
|
}
|
||||||
self.node = object_utils.create_test_node(self.context, **n)
|
self.node = object_utils.create_test_node(self.context, **n)
|
||||||
|
|
||||||
def test_validate(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
method = 'heartbeat'
|
|
||||||
self.passthru.validate(task, method)
|
|
||||||
|
|
||||||
def test_driver_validate(self):
|
|
||||||
kwargs = {'version': '2'}
|
|
||||||
method = 'lookup'
|
|
||||||
self.passthru.driver_validate(method, **kwargs)
|
|
||||||
|
|
||||||
def test_driver_validate_invalid_paremeter(self):
|
|
||||||
method = 'lookup'
|
|
||||||
kwargs = {'version': '1'}
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
self.passthru.driver_validate,
|
|
||||||
method, **kwargs)
|
|
||||||
|
|
||||||
def test_driver_validate_missing_parameter(self):
|
|
||||||
method = 'lookup'
|
|
||||||
kwargs = {}
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
self.passthru.driver_validate,
|
|
||||||
method, **kwargs)
|
|
||||||
|
|
||||||
def test_continue_deploy(self):
|
def test_continue_deploy(self):
|
||||||
self.node.provision_state = states.DEPLOYWAIT
|
self.node.provision_state = states.DEPLOYWAIT
|
||||||
self.node.target_provision_state = states.ACTIVE
|
self.node.target_provision_state = states.ACTIVE
|
||||||
@ -188,7 +163,7 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.passthru._continue_deploy(task)
|
self.passthru.continue_deploy(task)
|
||||||
|
|
||||||
client_mock.prepare_image.assert_called_with(task.node,
|
client_mock.prepare_image.assert_called_with(task.node,
|
||||||
expected_image_info)
|
expected_image_info)
|
||||||
@ -215,7 +190,7 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.passthru._continue_deploy(task)
|
self.passthru.continue_deploy(task)
|
||||||
|
|
||||||
client_mock.prepare_image.assert_called_with(task.node,
|
client_mock.prepare_image.assert_called_with(task.node,
|
||||||
expected_image_info)
|
expected_image_info)
|
||||||
@ -227,7 +202,7 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
@mock.patch('ironic.conductor.utils.node_set_boot_device')
|
@mock.patch('ironic.conductor.utils.node_set_boot_device')
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
||||||
'._check_deploy_success')
|
'._check_deploy_success')
|
||||||
def test__reboot_to_instance(self, check_deploy_mock, bootdev_mock,
|
def test_reboot_to_instance(self, check_deploy_mock, bootdev_mock,
|
||||||
power_mock):
|
power_mock):
|
||||||
check_deploy_mock.return_value = None
|
check_deploy_mock.return_value = None
|
||||||
|
|
||||||
@ -237,7 +212,7 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.passthru._reboot_to_instance(task)
|
self.passthru.reboot_to_instance(task)
|
||||||
|
|
||||||
check_deploy_mock.assert_called_once_with(task.node)
|
check_deploy_mock.assert_called_once_with(task.node)
|
||||||
bootdev_mock.assert_called_once_with(task, 'disk', persistent=True)
|
bootdev_mock.assert_called_once_with(task, 'disk', persistent=True)
|
||||||
@ -245,222 +220,11 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
self.assertEqual(states.ACTIVE, task.node.provision_state)
|
self.assertEqual(states.ACTIVE, task.node.provision_state)
|
||||||
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
|
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
|
||||||
|
|
||||||
def test_lookup_version_not_found(self):
|
def test_deploy_is_done(self):
|
||||||
kwargs = {
|
client_mock = mock.Mock()
|
||||||
'version': '999',
|
self.passthru._client = client_mock
|
||||||
}
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
self.passthru.deploy_is_done(task)
|
||||||
self.passthru.lookup,
|
self.passthru._client.deploy_is_done.assert_called_once_with(
|
||||||
task.context,
|
task.node)
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
|
||||||
'._find_node_by_macs')
|
|
||||||
def test_lookup_v2(self, find_mock):
|
|
||||||
kwargs = {
|
|
||||||
'version': '2',
|
|
||||||
'inventory': {
|
|
||||||
'interfaces': [
|
|
||||||
{
|
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
|
||||||
'name': 'eth0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'mac_address': 'ff:ee:dd:cc:bb:aa',
|
|
||||||
'name': 'eth1'
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
find_mock.return_value = self.node
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
node = self.passthru.lookup(task.context, **kwargs)
|
|
||||||
self.assertEqual(self.node, node['node'])
|
|
||||||
|
|
||||||
def test_lookup_v2_missing_inventory(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
self.passthru.lookup,
|
|
||||||
task.context)
|
|
||||||
|
|
||||||
def test_lookup_v2_empty_inventory(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
self.passthru.lookup,
|
|
||||||
task.context,
|
|
||||||
inventory={})
|
|
||||||
|
|
||||||
def test_lookup_v2_empty_interfaces(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
self.assertRaises(exception.NodeNotFound,
|
|
||||||
self.passthru.lookup,
|
|
||||||
task.context,
|
|
||||||
version='2',
|
|
||||||
inventory={'interfaces': []})
|
|
||||||
|
|
||||||
@mock.patch.object(objects.Port, 'get_by_address')
|
|
||||||
def test_find_ports_by_macs(self, mock_get_port):
|
|
||||||
fake_port = object_utils.get_test_port(self.context)
|
|
||||||
mock_get_port.return_value = fake_port
|
|
||||||
|
|
||||||
macs = ['aa:bb:cc:dd:ee:ff']
|
|
||||||
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
ports = self.passthru._find_ports_by_macs(task, macs)
|
|
||||||
self.assertEqual(1, len(ports))
|
|
||||||
self.assertEqual(fake_port.uuid, ports[0].uuid)
|
|
||||||
self.assertEqual(fake_port.node_id, ports[0].node_id)
|
|
||||||
|
|
||||||
@mock.patch.object(objects.Port, 'get_by_address')
|
|
||||||
def test_find_ports_by_macs_bad_params(self, mock_get_port):
|
|
||||||
mock_get_port.side_effect = exception.PortNotFound(port="123")
|
|
||||||
|
|
||||||
macs = ['aa:bb:cc:dd:ee:ff']
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
empty_ids = self.passthru._find_ports_by_macs(task, macs)
|
|
||||||
self.assertEqual([], empty_ids)
|
|
||||||
|
|
||||||
@mock.patch('ironic.objects.node.Node.get_by_id')
|
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
|
||||||
'._get_node_id')
|
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
|
||||||
'._find_ports_by_macs')
|
|
||||||
def test_find_node_by_macs(self, ports_mock, node_id_mock, node_mock):
|
|
||||||
ports_mock.return_value = object_utils.get_test_port(self.context)
|
|
||||||
node_id_mock.return_value = '1'
|
|
||||||
node_mock.return_value = self.node
|
|
||||||
|
|
||||||
macs = ['aa:bb:cc:dd:ee:ff']
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
node = self.passthru._find_node_by_macs(task, macs)
|
|
||||||
self.assertEqual(node, node)
|
|
||||||
|
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
|
||||||
'._find_ports_by_macs')
|
|
||||||
def test_find_node_by_macs_no_ports(self, ports_mock):
|
|
||||||
ports_mock.return_value = []
|
|
||||||
|
|
||||||
macs = ['aa:bb:cc:dd:ee:ff']
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
self.assertRaises(exception.NodeNotFound,
|
|
||||||
self.passthru._find_node_by_macs,
|
|
||||||
task,
|
|
||||||
macs)
|
|
||||||
|
|
||||||
@mock.patch('ironic.objects.node.Node.get_by_uuid')
|
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
|
||||||
'._get_node_id')
|
|
||||||
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
|
|
||||||
'._find_ports_by_macs')
|
|
||||||
def test_find_node_by_macs_nodenotfound(self, ports_mock, node_id_mock,
|
|
||||||
node_mock):
|
|
||||||
port = object_utils.get_test_port(self.context)
|
|
||||||
ports_mock.return_value = [port]
|
|
||||||
node_id_mock.return_value = self.node['uuid']
|
|
||||||
node_mock.side_effect = [self.node,
|
|
||||||
exception.NodeNotFound(node=self.node)]
|
|
||||||
|
|
||||||
macs = ['aa:bb:cc:dd:ee:ff']
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
self.assertRaises(exception.NodeNotFound,
|
|
||||||
self.passthru._find_node_by_macs,
|
|
||||||
task,
|
|
||||||
macs)
|
|
||||||
|
|
||||||
def test_get_node_id(self):
|
|
||||||
fake_port1 = object_utils.get_test_port(self.context,
|
|
||||||
node_id=123,
|
|
||||||
address="aa:bb:cc:dd:ee:fe")
|
|
||||||
fake_port2 = object_utils.get_test_port(self.context,
|
|
||||||
node_id=123,
|
|
||||||
id=42,
|
|
||||||
address="aa:bb:cc:dd:ee:fb",
|
|
||||||
uuid='1be26c0b-03f2-4d2e-ae87-'
|
|
||||||
'c02d7f33c782')
|
|
||||||
|
|
||||||
node_id = self.passthru._get_node_id([fake_port1, fake_port2])
|
|
||||||
self.assertEqual(fake_port2.node_id, node_id)
|
|
||||||
|
|
||||||
def test_get_node_id_exception(self):
|
|
||||||
fake_port1 = object_utils.get_test_port(self.context,
|
|
||||||
node_id=123,
|
|
||||||
address="aa:bb:cc:dd:ee:fc")
|
|
||||||
fake_port2 = object_utils.get_test_port(self.context,
|
|
||||||
node_id=321,
|
|
||||||
id=42,
|
|
||||||
address="aa:bb:cc:dd:ee:fd",
|
|
||||||
uuid='1be26c0b-03f2-4d2e-ae87-'
|
|
||||||
'c02d7f33c782')
|
|
||||||
|
|
||||||
self.assertRaises(exception.NodeNotFound,
|
|
||||||
self.passthru._get_node_id,
|
|
||||||
[fake_port1, fake_port2])
|
|
||||||
|
|
||||||
def test_get_interfaces(self):
|
|
||||||
fake_inventory = {
|
|
||||||
'interfaces': [
|
|
||||||
{
|
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
|
||||||
'name': 'eth0'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
interfaces = self.passthru._get_interfaces(fake_inventory)
|
|
||||||
self.assertEqual(fake_inventory['interfaces'], interfaces)
|
|
||||||
|
|
||||||
def test_get_interfaces_bad(self):
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
self.passthru._get_interfaces,
|
|
||||||
inventory={})
|
|
||||||
|
|
||||||
def test_heartbeat(self):
|
|
||||||
kwargs = {
|
|
||||||
'agent_url': 'http://127.0.0.1:9999/bar'
|
|
||||||
}
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
self.passthru.heartbeat(task, **kwargs)
|
|
||||||
|
|
||||||
def test_heartbeat_bad(self):
|
|
||||||
kwargs = {}
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
self.passthru.heartbeat, task, **kwargs)
|
|
||||||
|
|
||||||
@mock.patch.object(deploy_utils, 'set_failed_state')
|
|
||||||
@mock.patch.object(agent.AgentVendorInterface, '_deploy_is_done')
|
|
||||||
def test_heartbeat_deploy_done_fails(self, done_mock, failed_mock):
|
|
||||||
kwargs = {
|
|
||||||
'agent_url': 'http://127.0.0.1:9999/bar'
|
|
||||||
}
|
|
||||||
done_mock.side_effect = Exception
|
|
||||||
with task_manager.acquire(
|
|
||||||
self.context, self.node['uuid'], shared=True) as task:
|
|
||||||
task.node.provision_state = states.DEPLOYING
|
|
||||||
task.node.target_provision_state = states.ACTIVE
|
|
||||||
self.passthru.heartbeat(task, **kwargs)
|
|
||||||
failed_mock.assert_called_once_with(task, mock.ANY)
|
|
||||||
|
|
||||||
def test_vendor_passthru_vendor_routes(self):
|
|
||||||
expected = ['heartbeat']
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
vendor_routes = task.driver.vendor.vendor_routes
|
|
||||||
self.assertIsInstance(vendor_routes, dict)
|
|
||||||
self.assertEqual(expected, list(vendor_routes))
|
|
||||||
|
|
||||||
def test_vendor_passthru_driver_routes(self):
|
|
||||||
expected = ['lookup']
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
driver_routes = task.driver.vendor.driver_routes
|
|
||||||
self.assertIsInstance(driver_routes, dict)
|
|
||||||
self.assertEqual(expected, list(driver_routes))
|
|
||||||
|
292
ironic/tests/drivers/test_agent_base_vendor.py
Normal file
292
ironic/tests/drivers/test_agent_base_vendor.py
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright 2015 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common import states
|
||||||
|
from ironic.conductor import task_manager
|
||||||
|
from ironic.drivers.modules import agent_base_vendor
|
||||||
|
from ironic.drivers.modules import deploy_utils
|
||||||
|
from ironic import objects
|
||||||
|
from ironic.tests.conductor import utils as mgr_utils
|
||||||
|
from ironic.tests.db import base as db_base
|
||||||
|
from ironic.tests.db import utils as db_utils
|
||||||
|
from ironic.tests.objects import utils as object_utils
|
||||||
|
|
||||||
|
INSTANCE_INFO = db_utils.get_test_agent_instance_info()
|
||||||
|
DRIVER_INFO = db_utils.get_test_agent_driver_info()
|
||||||
|
DRIVER_INTERNAL_INFO = db_utils.get_test_agent_driver_internal_info()
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseAgentVendor(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestBaseAgentVendor, self).setUp()
|
||||||
|
mgr_utils.mock_the_extension_manager(driver="fake_agent")
|
||||||
|
self.passthru = agent_base_vendor.BaseAgentVendor()
|
||||||
|
n = {
|
||||||
|
'driver': 'fake_agent',
|
||||||
|
'instance_info': INSTANCE_INFO,
|
||||||
|
'driver_info': DRIVER_INFO,
|
||||||
|
'driver_internal_info': DRIVER_INTERNAL_INFO,
|
||||||
|
}
|
||||||
|
self.node = object_utils.create_test_node(self.context, **n)
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
method = 'heartbeat'
|
||||||
|
self.passthru.validate(task, method)
|
||||||
|
|
||||||
|
def test_driver_validate(self):
|
||||||
|
kwargs = {'version': '2'}
|
||||||
|
method = 'lookup'
|
||||||
|
self.passthru.driver_validate(method, **kwargs)
|
||||||
|
|
||||||
|
def test_driver_validate_invalid_paremeter(self):
|
||||||
|
method = 'lookup'
|
||||||
|
kwargs = {'version': '1'}
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.passthru.driver_validate,
|
||||||
|
method, **kwargs)
|
||||||
|
|
||||||
|
def test_driver_validate_missing_parameter(self):
|
||||||
|
method = 'lookup'
|
||||||
|
kwargs = {}
|
||||||
|
self.assertRaises(exception.MissingParameterValue,
|
||||||
|
self.passthru.driver_validate,
|
||||||
|
method, **kwargs)
|
||||||
|
|
||||||
|
def test_lookup_version_not_found(self):
|
||||||
|
kwargs = {
|
||||||
|
'version': '999',
|
||||||
|
}
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.passthru.lookup,
|
||||||
|
task.context,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
@mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor'
|
||||||
|
'._find_node_by_macs')
|
||||||
|
def test_lookup_v2(self, find_mock):
|
||||||
|
kwargs = {
|
||||||
|
'version': '2',
|
||||||
|
'inventory': {
|
||||||
|
'interfaces': [
|
||||||
|
{
|
||||||
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
|
'name': 'eth0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'mac_address': 'ff:ee:dd:cc:bb:aa',
|
||||||
|
'name': 'eth1'
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
find_mock.return_value = self.node
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
node = self.passthru.lookup(task.context, **kwargs)
|
||||||
|
self.assertEqual(self.node, node['node'])
|
||||||
|
|
||||||
|
def test_lookup_v2_missing_inventory(self):
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.passthru.lookup,
|
||||||
|
task.context)
|
||||||
|
|
||||||
|
def test_lookup_v2_empty_inventory(self):
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.passthru.lookup,
|
||||||
|
task.context,
|
||||||
|
inventory={})
|
||||||
|
|
||||||
|
def test_lookup_v2_empty_interfaces(self):
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
self.assertRaises(exception.NodeNotFound,
|
||||||
|
self.passthru.lookup,
|
||||||
|
task.context,
|
||||||
|
version='2',
|
||||||
|
inventory={'interfaces': []})
|
||||||
|
|
||||||
|
@mock.patch.object(objects.Port, 'get_by_address')
|
||||||
|
def test_find_ports_by_macs(self, mock_get_port):
|
||||||
|
fake_port = object_utils.get_test_port(self.context)
|
||||||
|
mock_get_port.return_value = fake_port
|
||||||
|
|
||||||
|
macs = ['aa:bb:cc:dd:ee:ff']
|
||||||
|
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
ports = self.passthru._find_ports_by_macs(task, macs)
|
||||||
|
self.assertEqual(1, len(ports))
|
||||||
|
self.assertEqual(fake_port.uuid, ports[0].uuid)
|
||||||
|
self.assertEqual(fake_port.node_id, ports[0].node_id)
|
||||||
|
|
||||||
|
@mock.patch.object(objects.Port, 'get_by_address')
|
||||||
|
def test_find_ports_by_macs_bad_params(self, mock_get_port):
|
||||||
|
mock_get_port.side_effect = exception.PortNotFound(port="123")
|
||||||
|
|
||||||
|
macs = ['aa:bb:cc:dd:ee:ff']
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
empty_ids = self.passthru._find_ports_by_macs(task, macs)
|
||||||
|
self.assertEqual([], empty_ids)
|
||||||
|
|
||||||
|
@mock.patch('ironic.objects.node.Node.get_by_id')
|
||||||
|
@mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor'
|
||||||
|
'._get_node_id')
|
||||||
|
@mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor'
|
||||||
|
'._find_ports_by_macs')
|
||||||
|
def test_find_node_by_macs(self, ports_mock, node_id_mock, node_mock):
|
||||||
|
ports_mock.return_value = object_utils.get_test_port(self.context)
|
||||||
|
node_id_mock.return_value = '1'
|
||||||
|
node_mock.return_value = self.node
|
||||||
|
|
||||||
|
macs = ['aa:bb:cc:dd:ee:ff']
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
node = self.passthru._find_node_by_macs(task, macs)
|
||||||
|
self.assertEqual(node, node)
|
||||||
|
|
||||||
|
@mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor'
|
||||||
|
'._find_ports_by_macs')
|
||||||
|
def test_find_node_by_macs_no_ports(self, ports_mock):
|
||||||
|
ports_mock.return_value = []
|
||||||
|
|
||||||
|
macs = ['aa:bb:cc:dd:ee:ff']
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
self.assertRaises(exception.NodeNotFound,
|
||||||
|
self.passthru._find_node_by_macs,
|
||||||
|
task,
|
||||||
|
macs)
|
||||||
|
|
||||||
|
@mock.patch('ironic.objects.node.Node.get_by_uuid')
|
||||||
|
@mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor'
|
||||||
|
'._get_node_id')
|
||||||
|
@mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor'
|
||||||
|
'._find_ports_by_macs')
|
||||||
|
def test_find_node_by_macs_nodenotfound(self, ports_mock, node_id_mock,
|
||||||
|
node_mock):
|
||||||
|
port = object_utils.get_test_port(self.context)
|
||||||
|
ports_mock.return_value = [port]
|
||||||
|
node_id_mock.return_value = self.node['uuid']
|
||||||
|
node_mock.side_effect = [self.node,
|
||||||
|
exception.NodeNotFound(node=self.node)]
|
||||||
|
|
||||||
|
macs = ['aa:bb:cc:dd:ee:ff']
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
self.assertRaises(exception.NodeNotFound,
|
||||||
|
self.passthru._find_node_by_macs,
|
||||||
|
task,
|
||||||
|
macs)
|
||||||
|
|
||||||
|
def test_get_node_id(self):
|
||||||
|
fake_port1 = object_utils.get_test_port(self.context,
|
||||||
|
node_id=123,
|
||||||
|
address="aa:bb:cc:dd:ee:fe")
|
||||||
|
fake_port2 = object_utils.get_test_port(self.context,
|
||||||
|
node_id=123,
|
||||||
|
id=42,
|
||||||
|
address="aa:bb:cc:dd:ee:fb",
|
||||||
|
uuid='1be26c0b-03f2-4d2e-ae87-'
|
||||||
|
'c02d7f33c782')
|
||||||
|
|
||||||
|
node_id = self.passthru._get_node_id([fake_port1, fake_port2])
|
||||||
|
self.assertEqual(fake_port2.node_id, node_id)
|
||||||
|
|
||||||
|
def test_get_node_id_exception(self):
|
||||||
|
fake_port1 = object_utils.get_test_port(self.context,
|
||||||
|
node_id=123,
|
||||||
|
address="aa:bb:cc:dd:ee:fc")
|
||||||
|
fake_port2 = object_utils.get_test_port(self.context,
|
||||||
|
node_id=321,
|
||||||
|
id=42,
|
||||||
|
address="aa:bb:cc:dd:ee:fd",
|
||||||
|
uuid='1be26c0b-03f2-4d2e-ae87-'
|
||||||
|
'c02d7f33c782')
|
||||||
|
|
||||||
|
self.assertRaises(exception.NodeNotFound,
|
||||||
|
self.passthru._get_node_id,
|
||||||
|
[fake_port1, fake_port2])
|
||||||
|
|
||||||
|
def test_get_interfaces(self):
|
||||||
|
fake_inventory = {
|
||||||
|
'interfaces': [
|
||||||
|
{
|
||||||
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
|
'name': 'eth0'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
interfaces = self.passthru._get_interfaces(fake_inventory)
|
||||||
|
self.assertEqual(fake_inventory['interfaces'], interfaces)
|
||||||
|
|
||||||
|
def test_get_interfaces_bad(self):
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.passthru._get_interfaces,
|
||||||
|
inventory={})
|
||||||
|
|
||||||
|
def test_heartbeat(self):
|
||||||
|
kwargs = {
|
||||||
|
'agent_url': 'http://127.0.0.1:9999/bar'
|
||||||
|
}
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
self.passthru.heartbeat(task, **kwargs)
|
||||||
|
|
||||||
|
def test_heartbeat_bad(self):
|
||||||
|
kwargs = {}
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
self.assertRaises(exception.MissingParameterValue,
|
||||||
|
self.passthru.heartbeat, task, **kwargs)
|
||||||
|
|
||||||
|
@mock.patch.object(deploy_utils, 'set_failed_state')
|
||||||
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor, 'deploy_is_done')
|
||||||
|
def test_heartbeat_deploy_done_fails(self, done_mock, failed_mock):
|
||||||
|
kwargs = {
|
||||||
|
'agent_url': 'http://127.0.0.1:9999/bar'
|
||||||
|
}
|
||||||
|
done_mock.side_effect = Exception
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=True) as task:
|
||||||
|
task.node.provision_state = states.DEPLOYING
|
||||||
|
task.node.target_provision_state = states.ACTIVE
|
||||||
|
self.passthru.heartbeat(task, **kwargs)
|
||||||
|
failed_mock.assert_called_once_with(task, mock.ANY)
|
||||||
|
|
||||||
|
def test_vendor_passthru_vendor_routes(self):
|
||||||
|
expected = ['heartbeat']
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
vendor_routes = task.driver.vendor.vendor_routes
|
||||||
|
self.assertIsInstance(vendor_routes, dict)
|
||||||
|
self.assertEqual(expected, list(vendor_routes))
|
||||||
|
|
||||||
|
def test_vendor_passthru_driver_routes(self):
|
||||||
|
expected = ['lookup']
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
driver_routes = task.driver.vendor.driver_routes
|
||||||
|
self.assertIsInstance(driver_routes, dict)
|
||||||
|
self.assertEqual(expected, list(driver_routes))
|
Loading…
x
Reference in New Issue
Block a user