Add network interface to base driver class

This change also introduces two network interfaces:

* flat: Copies current neutron DHCP provider logic to work with
  cleaning ports;
* noop: noop interface.

The default value of the network_interface is None, meaning that the
node will be using the default network interface. The default network
interface is determined the following way:

* if [DEFAULT]default_network_interface configuration option is set
  (the default for it is None), the specified interface becomes the
  default for all nodes;

* if it is not set, 'flat' interface will be used if the deployment
  currently uses 'neutron' DHCP provider, otherwise 'noop' interface
  will be used.

create_cleaning_ports and delete_cleaning_ports methods of the DHCP
providers are still being called in case of out-of-tree DHCP
providers, but this possibility will be removed completely in the
next release. If the DHCP provider logic is rewritten into a custom
network interface, please remove those methods from the provider, so
that network interface is called instead.

Partial-bug: #1526403
Co-Authored-By: Om Kumar <om.kumar@hp.com>
Co-Authored-By: Vasyl Saienko <vsaienko@mirantis.com>
Co-Authored-By: Sivaramakrishna Garimella <sivaramakrishna.garimella@hp.com>
Co-Authored-By: Vladyslav Drok <vdrok@mirantis.com>
Co-Authored-By: Zhenguo Niu <Niu.ZGlinux@gmail.com>
Change-Id: I0c26582b6b6e9d32650ff3e2b9a3269c3c2d5454
This commit is contained in:
Vasyl Saienko 2016-05-17 13:59:39 +03:00 committed by Vladyslav Drok
parent 1825267b3a
commit cde11611d9
24 changed files with 1219 additions and 262 deletions

View File

@ -30,6 +30,24 @@
# developer documentation online. (list value)
#enabled_drivers = pxe_ipmitool
# Specify the list of network interfaces to load during
# service initialization. Missing network interfaces, or
# network interfaces which fail to initialize, will prevent
# the conductor service from starting. The option default is a
# recommended set of production-oriented network interfaces. A
# complete list of network interfaces present on your system
# may be found by enumerating the
# "ironic.hardware.interfaces.network" entrypoint. (list
# value)
#enabled_network_interfaces = flat,noop
# Default network interface to be used for nodes that do not
# have network_interface field set. A complete list of network
# interfaces present on your system may be found by
# enumerating the "ironic.hardware.interfaces.network"
# entrypoint. (string value)
#default_network_interface = <None>
# Used if there is a formatting error when generating an
# exception message (a programming error). If True, raise an
# exception; if False, use the unformatted message. (boolean
@ -1410,12 +1428,12 @@
# (list value)
#hash_algorithms = md5
# Authentication type to load (string value)
# Authentication type to load (unknown value)
# Deprecated group/name - [keystone_authtoken]/auth_plugin
#auth_type = <None>
# Config Section from which to load plugin specific options
# (string value)
# (unknown value)
#auth_section = <None>
@ -1499,8 +1517,11 @@
# Allowed values: keystone, noauth
#auth_strategy = keystone
# UUID of the network to create Neutron ports on, when booting
# to a ramdisk for cleaning using Neutron DHCP. (string value)
# Neutron network UUID for the ramdisk to be booted into for
# cleaning nodes. Required if cleaning (either automatic or
# manual) is run for flat network interface, and, if DHCP
# providers are still being used, for neutron DHCP provider.
# (string value)
#cleaning_network_uuid = <None>

View File

@ -41,6 +41,23 @@ driver_opts = [
'be found by enumerating the "ironic.drivers" '
'entrypoint. An example may be found in the '
'developer documentation online.')),
cfg.ListOpt('enabled_network_interfaces',
default=['flat', 'noop'],
help=_('Specify the list of network interfaces to load during '
'service initialization. Missing network interfaces, '
'or network interfaces which fail to initialize, will '
'prevent the conductor service from starting. The '
'option default is a recommended set of '
'production-oriented network interfaces. A complete '
'list of network interfaces present on your system may '
'be found by enumerating the '
'"ironic.hardware.interfaces.network" entrypoint.')),
cfg.StrOpt('default_network_interface',
help=_('Default network interface to be used for nodes that '
'do not have network_interface field set. A complete '
'list of network interfaces present on your system may '
'be found by enumerating the '
'"ironic.hardware.interfaces.network" entrypoint.'))
]
CONF = cfg.CONF
@ -76,6 +93,20 @@ def _attach_interfaces_to_driver(driver, node, driver_name=None):
impl = getattr(driver_singleton, iface, None)
setattr(driver, iface, impl)
network_iface = node.network_interface
if network_iface is None:
network_iface = (CONF.default_network_interface or
('flat' if CONF.dhcp.dhcp_provider == 'neutron'
else 'noop'))
network_factory = NetworkInterfaceFactory()
try:
net_driver = network_factory.get_driver(network_iface)
except KeyError:
raise exception.DriverNotFoundInEntrypoint(
driver_name=network_iface,
entrypoint=network_factory._entrypoint_name)
driver.network = net_driver
def get_driver(driver_name):
"""Simple method to get a ref to an instance of a driver.
@ -93,7 +124,7 @@ def get_driver(driver_name):
try:
factory = DriverFactory()
return factory[driver_name].obj
return factory.get_driver(driver_name)
except KeyError:
raise exception.DriverNotFound(driver_name=driver_name)
@ -109,8 +140,11 @@ def drivers():
for name in factory.names)
class DriverFactory(object):
"""Discover, load and manage the drivers available."""
class BaseDriverFactory(object):
"""Discover, load and manage the drivers available.
This is subclassed to load both main drivers and extra interfaces.
"""
# NOTE(deva): loading the _extension_manager as a class member will break
# stevedore when it loads a driver, because the driver will
@ -119,13 +153,25 @@ class DriverFactory(object):
# once, the first time DriverFactory.__init__ is called.
_extension_manager = None
# Entrypoint name containing the list of all available drivers/interfaces
_entrypoint_name = None
# Name of the [DEFAULT] section config option containing a list of enabled
# drivers/interfaces
_enabled_driver_list_config_option = ''
# This field will contain the list of the enabled drivers/interfaces names
# without duplicates
_enabled_driver_list = None
def __init__(self):
if not DriverFactory._extension_manager:
DriverFactory._init_extension_manager()
if not self.__class__._extension_manager:
self.__class__._init_extension_manager()
def __getitem__(self, name):
return self._extension_manager[name]
def get_driver(self, name):
return self[name].obj
# NOTE(deva): Use lockutils to avoid a potential race in eventlet
# that might try to create two driver factories.
@classmethod
@ -136,19 +182,24 @@ class DriverFactory(object):
# creation of multiple NameDispatchExtensionManagers.
if cls._extension_manager:
return
enabled_drivers = getattr(CONF, cls._enabled_driver_list_config_option,
[])
# Check for duplicated driver entries and warn the operator
# about them
counter = collections.Counter(CONF.enabled_drivers).items()
duplicated_drivers = list(dup for (dup, i) in counter if i > 1)
counter = collections.Counter(enabled_drivers).items()
duplicated_drivers = []
cls._enabled_driver_list = []
for item, cnt in counter:
if cnt > 1:
duplicated_drivers.append(item)
cls._enabled_driver_list.append(item)
if duplicated_drivers:
LOG.warning(_LW('The driver(s) "%s" is/are duplicated in the '
'list of enabled_drivers. Please check your '
'configuration file.'),
', '.join(duplicated_drivers))
enabled_drivers = set(CONF.enabled_drivers)
# NOTE(deva): Drivers raise "DriverLoadError" if they are unable to be
# loaded, eg. due to missing external dependencies.
# We capture that exception, and, only if it is for an
@ -160,30 +211,31 @@ class DriverFactory(object):
def _catch_driver_not_found(mgr, ep, exc):
# NOTE(deva): stevedore loads plugins *before* evaluating
# _check_func, so we need to check here, too.
if ep.name in enabled_drivers:
if ep.name in cls._enabled_driver_list:
if not isinstance(exc, exception.DriverLoadError):
raise exception.DriverLoadError(driver=ep.name, reason=exc)
raise exc
def _check_func(ext):
return ext.name in enabled_drivers
return ext.name in cls._enabled_driver_list
cls._extension_manager = (
dispatch.NameDispatchExtensionManager(
'ironic.drivers',
cls._entrypoint_name,
_check_func,
invoke_on_load=True,
on_load_failure_callback=_catch_driver_not_found))
# NOTE(deva): if we were unable to load any configured driver, perhaps
# because it is not present on the system, raise an error.
if (sorted(enabled_drivers) !=
if (sorted(cls._enabled_driver_list) !=
sorted(cls._extension_manager.names())):
found = cls._extension_manager.names()
names = [n for n in enabled_drivers if n not in found]
names = [n for n in cls._enabled_driver_list if n not in found]
# just in case more than one could not be found ...
names = ', '.join(names)
raise exception.DriverNotFound(driver_name=names)
raise exception.DriverNotFoundInEntrypoint(
driver_name=names, entrypoint=cls._entrypoint_name)
LOG.info(_LI("Loaded the following drivers: %s"),
cls._extension_manager.names())
@ -192,3 +244,13 @@ class DriverFactory(object):
def names(self):
"""The list of driver names available."""
return self._extension_manager.names()
class DriverFactory(BaseDriverFactory):
_entrypoint_name = 'ironic.drivers'
_enabled_driver_list_config_option = 'enabled_drivers'
class NetworkInterfaceFactory(BaseDriverFactory):
_entrypoint_name = 'ironic.hardware.interfaces.network'
_enabled_driver_list_config_option = 'enabled_network_interfaces'

View File

@ -241,6 +241,11 @@ class DriverNotFound(NotFound):
_msg_fmt = _("Could not find the following driver(s): %(driver_name)s.")
class DriverNotFoundInEntrypoint(DriverNotFound):
_msg_fmt = _("Could not find the following driver(s) in the "
"'%(entrypoint)s' entrypoint: %(driver_name)s.")
class ImageNotFound(NotFound):
_msg_fmt = _("Image %(image_id)s could not be found.")
@ -591,3 +596,7 @@ class OneViewError(IronicException):
class NodeTagNotFound(IronicException):
_msg_fmt = _("Node %(node_id)s doesn't have a tag '%(tag)s'")
class NetworkError(IronicException):
_msg_fmt = _("Network operation failure.")

View File

@ -10,12 +10,20 @@
# License for the specific language governing permissions and limitations
# under the License.
from neutronclient.common import exceptions as neutron_exceptions
from neutronclient.v2_0 import client as clientv20
from oslo_config import cfg
from oslo_log import log
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common.i18n import _LI
from ironic.common.i18n import _LW
from ironic.common import keystone
LOG = log.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('my_ip', 'ironic.netconf')
@ -42,8 +50,11 @@ neutron_opts = [
'but not affected by this setting) is insecure and '
'should only be used for testing.')),
cfg.StrOpt('cleaning_network_uuid',
help=_('UUID of the network to create Neutron ports on, when '
'booting to a ramdisk for cleaning using Neutron DHCP.'))
help=_('Neutron network UUID for the ramdisk to be booted '
'into for cleaning nodes. Required if cleaning (either '
'automatic or manual) is run for flat network interface,'
' and, if DHCP providers are still being used, for '
'neutron DHCP provider.'))
]
CONF.register_opts(neutron_opts, group='neutron')
@ -73,3 +84,191 @@ def get_client(token=None):
params['token'] = token
return clientv20.Client(**params)
def add_ports_to_network(task, network_uuid, is_flat=False):
"""Create neutron ports to boot the ramdisk.
Create neutron ports for each pxe_enabled port on task.node to boot
the ramdisk.
:param task: a TaskManager instance.
:param network_uuid: UUID of a neutron network where ports will be
created.
:param is_flat: Indicates whether it is a flat network or not.
:raises: NetworkError
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
"""
client = get_client(task.context.auth_token)
node = task.node
LOG.debug('For node %(node)s, creating neutron ports on network '
'%(network_uuid)s using %(net_iface)s network interface.',
{'net_iface': task.driver.network.__class__.__name__,
'node': node.uuid, 'network_uuid': network_uuid})
body = {
'port': {
'network_id': network_uuid,
'admin_state_up': True,
'binding:vnic_type': 'baremetal',
'device_owner': 'baremetal:none',
}
}
if not is_flat:
# NOTE(vdrok): It seems that change
# I437290affd8eb87177d0626bf7935a165859cbdd to neutron broke the
# possibility to always bind port. Set binding:host_id only in
# case of non flat network.
body['port']['binding:host_id'] = node.uuid
# Since instance_uuid will not be available during cleaning
# operations, we need to check that and populate them only when
# available
body['port']['device_id'] = node.instance_uuid or node.uuid
ports = {}
failures = []
portmap = get_node_portmap(task)
pxe_enabled_ports = [p for p in task.ports if p.pxe_enabled]
for ironic_port in pxe_enabled_ports:
body['port']['mac_address'] = ironic_port.address
binding_profile = {'local_link_information':
[portmap[ironic_port.uuid]]}
body['port']['binding:profile'] = binding_profile
try:
port = client.create_port(body)
except neutron_exceptions.NeutronClientException as e:
rollback_ports(task, network_uuid)
msg = (_('Could not create neutron port for ironic port '
'%(ir-port)s on given network %(net)s from node '
'%(node)s. %(exc)s') %
{'net': network_uuid, 'node': node.uuid,
'ir-port': ironic_port.uuid, 'exc': e})
LOG.exception(msg)
raise exception.NetworkError(msg)
try:
ports[ironic_port.uuid] = port['port']['id']
except KeyError:
failures.append(ironic_port.uuid)
if failures:
if len(failures) == len(pxe_enabled_ports):
raise exception.NetworkError(_(
"Failed to update vif_port_id for any PXE enabled port "
"on node %s.") % node.uuid)
else:
LOG.warning(_LW("Some errors were encountered when updating "
"vif_port_id for node %(node)s on "
"the following ports: %(ports)s."),
{'node': node.uuid, 'ports': failures})
else:
LOG.info(_LI('Successfully created ports for node %(node_uuid)s in '
'network %(net)s.'),
{'node_uuid': node.uuid, 'net': network_uuid})
return ports
def remove_ports_from_network(task, network_uuid):
"""Deletes the neutron ports created for booting the ramdisk.
:param task: a TaskManager instance.
:param network_uuid: UUID of a neutron network ports will be deleted from.
:raises: NetworkError
"""
macs = [p.address for p in task.ports if p.pxe_enabled]
if macs:
params = {
'network_id': network_uuid,
'mac_address': macs,
}
LOG.debug("Removing ports on network %(net)s on node %(node)s.",
{'net': network_uuid, 'node': task.node.uuid})
remove_neutron_ports(task, params)
def remove_neutron_ports(task, params):
"""Deletes the neutron ports matched by params.
:param task: a TaskManager instance.
:param params: Dict of params to filter ports.
:raises: NetworkError
"""
client = get_client(task.context.auth_token)
node_uuid = task.node.uuid
try:
response = client.list_ports(**params)
except neutron_exceptions.NeutronClientException as e:
msg = (_('Could not get given network VIF for %(node)s '
'from neutron, possible network issue. %(exc)s') %
{'node': node_uuid, 'exc': e})
LOG.exception(msg)
raise exception.NetworkError(msg)
ports = response.get('ports', [])
if not ports:
LOG.debug('No ports to remove for node %s', node_uuid)
return
for port in ports:
if not port['id']:
# TODO(morgabra) client.list_ports() sometimes returns
# port objects with null ids. It's unclear why this happens.
LOG.warning(_LW("Deleting neutron port failed, missing 'id'. "
"Node: %(node)s, neutron port: %(port)s."),
{'node': node_uuid, 'port': port})
continue
LOG.debug('Deleting neutron port %(vif_port_id)s of node '
'%(node_id)s.',
{'vif_port_id': port['id'], 'node_id': node_uuid})
try:
client.delete_port(port['id'])
except neutron_exceptions.NeutronClientException as e:
msg = (_('Could not remove VIF %(vif)s of node %(node)s, possibly '
'a network issue: %(exc)s') %
{'vif': port['id'], 'node': node_uuid, 'exc': e})
LOG.exception(msg)
raise exception.NetworkError(msg)
LOG.info(_LI('Successfully removed node %(node_uuid)s neutron ports.'),
{'node_uuid': node_uuid})
def get_node_portmap(task):
"""Extract the switch port information for the node.
:param task: a task containing the Node object.
:returns: a dictionary in the form {port.uuid: port.local_link_connection}
"""
portmap = {}
for port in task.ports:
portmap[port.uuid] = port.local_link_connection
return portmap
# TODO(jroll) raise InvalidParameterValue if a port doesn't have the
# necessary info? (probably)
def rollback_ports(task, network_uuid):
"""Attempts to delete any ports created by cleaning/provisioning
Purposefully will not raise any exceptions so error handling can
continue.
:param task: a TaskManager instance.
:param network_uuid: UUID of a neutron network.
"""
try:
remove_ports_from_network(task, network_uuid)
except exception.NetworkError:
# Only log the error
LOG.exception(_LE(
'Failed to rollback port changes for node %(node)s '
'on network %(network)s'), {'node': task.node.uuid,
'network': network_uuid})

View File

@ -83,8 +83,12 @@ class BaseConductorManager(object):
self.ring_manager = hash.HashRingManager()
"""Consistent hash ring which maps drivers to conductors."""
# NOTE(deva): this call may raise DriverLoadError or DriverNotFound
# NOTE(deva): these calls may raise DriverLoadError or DriverNotFound
# NOTE(vdrok): instantiate network interface factory on startup so that
# all the network interfaces are loaded at the very beginning, and
# failures prevent the conductor from starting.
drivers = driver_factory.drivers()
driver_factory.NetworkInterfaceFactory()
if not drivers:
msg = _LE("Conductor %s cannot be started because no drivers "
"were loaded. This could be because no drivers were "

View File

@ -34,6 +34,9 @@ from ironic import objects
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
create_cleaning_ports_deprecation = False
delete_cleaning_ports_deprecation = False
class NeutronDHCPApi(base.BaseDHCP):
"""API for communicating to neutron 2.x API."""
@ -271,95 +274,37 @@ class NeutronDHCPApi(base.BaseDHCP):
return port_ip_addresses + portgroup_ip_addresses
# TODO(vsaienko) Remove this method when deprecation period is passed
# in Ocata.
def create_cleaning_ports(self, task):
"""Create neutron ports for each port on task.node to boot the ramdisk.
:param task: a TaskManager instance.
:raises: InvalidParameterValue if the cleaning network is None
:raises: NetworkError, InvalidParameterValue
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
"""
if not CONF.neutron.cleaning_network_uuid:
raise exception.InvalidParameterValue(_('Valid cleaning network '
'UUID not provided'))
neutron_client = neutron.get_client(task.context.auth_token)
body = {
'port': {
'network_id': CONF.neutron.cleaning_network_uuid,
'admin_state_up': True,
}
}
ports = {}
for ironic_port in task.ports:
body['port']['mac_address'] = ironic_port.address
try:
port = neutron_client.create_port(body)
except neutron_client_exc.ConnectionFailed as e:
self._rollback_cleaning_ports(task)
msg = (_('Could not create cleaning port on network %(net)s '
'from %(node)s. %(exc)s') %
{'net': CONF.neutron.cleaning_network_uuid,
'node': task.node.uuid,
'exc': e})
LOG.exception(msg)
raise exception.NodeCleaningFailure(msg)
if not port.get('port') or not port['port'].get('id'):
self._rollback_cleaning_ports(task)
msg = (_('Failed to create cleaning ports for node '
'%(node)s') % {'node': task.node.uuid})
LOG.error(msg)
raise exception.NodeCleaningFailure(msg)
# Match return value of get_node_vif_ids()
ports[ironic_port.uuid] = port['port']['id']
return ports
global create_cleaning_ports_deprecation
if not create_cleaning_ports_deprecation:
LOG.warning(_LW('create_cleaning_ports via dhcp provider is '
'deprecated. The node.network_interface setting '
'should be used instead.'))
create_cleaning_ports_deprecation = True
return task.driver.network.add_cleaning_network(task)
# TODO(vsaienko) Remove this method when deprecation period is passed
# in Ocata.
def delete_cleaning_ports(self, task):
"""Deletes the neutron port created for booting the ramdisk.
:param task: a TaskManager instance.
:raises: NetworkError, InvalidParameterValue
"""
neutron_client = neutron.get_client(task.context.auth_token)
macs = [p.address for p in task.ports]
params = {
'network_id': CONF.neutron.cleaning_network_uuid
}
try:
ports = neutron_client.list_ports(**params)
except neutron_client_exc.ConnectionFailed as e:
msg = (_('Could not get cleaning network vif for %(node)s '
'from Neutron, possible network issue. %(exc)s') %
{'node': task.node.uuid,
'exc': e})
LOG.exception(msg)
raise exception.NodeCleaningFailure(msg)
global delete_cleaning_ports_deprecation
if not delete_cleaning_ports_deprecation:
LOG.warning(_LW('delete_cleaning_ports via dhcp provider is '
'deprecated. The node.network_interface setting '
'should be used instead.'))
delete_cleaning_ports_deprecation = True
# Iterate the list of Neutron port dicts, remove the ones we added
for neutron_port in ports.get('ports', []):
# Only delete ports using the node's mac addresses
if neutron_port.get('mac_address') in macs:
try:
neutron_client.delete_port(neutron_port.get('id'))
except neutron_client_exc.ConnectionFailed as e:
msg = (_('Could not remove cleaning ports on network '
'%(net)s from %(node)s, possible network issue. '
'%(exc)s') %
{'net': CONF.neutron.cleaning_network_uuid,
'node': task.node.uuid,
'exc': e})
LOG.exception(msg)
raise exception.NodeCleaningFailure(msg)
def _rollback_cleaning_ports(self, task):
"""Attempts to delete any ports created by cleaning
Purposefully will not raise any exceptions so error handling can
continue.
:param task: a TaskManager instance.
"""
try:
self.delete_cleaning_ports(task)
except Exception:
# Log the error, but let the caller invoke the
# manager.cleaning_error_handler().
LOG.exception(_LE('Failed to rollback cleaning port '
'changes for node %s') % task.node.uuid)
task.driver.network.remove_cleaning_network(task)

View File

@ -158,8 +158,14 @@ class BareDriver(BaseDriver):
Any composable interfaces should be added as class attributes of this
class, as well as appended to core_interfaces or standard_interfaces here.
"""
def __init__(self):
pass
self.network = None
"""`Core` attribute for network connectivity.
A reference to an instance of :class:NetworkInterface.
"""
self.core_interfaces.append('network')
class BaseInterface(object):
@ -1020,6 +1026,74 @@ class RAIDInterface(BaseInterface):
return raid.get_logical_disk_properties(self.raid_schema)
@six.add_metaclass(abc.ABCMeta)
class NetworkInterface(object):
"""Base class for network interfaces."""
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
return {}
def validate(self, task):
"""Validates the network interface.
:param task: a TaskManager instance.
:raises: InvalidParameterValue, if the network interface configuration
is invalid.
:raises: MissingParameterValue, if some parameters are missing.
"""
@abc.abstractmethod
def add_provisioning_network(self, task):
"""Add the provisioning network to a node.
:param task: A TaskManager instance.
:raises: NetworkError
"""
@abc.abstractmethod
def remove_provisioning_network(self, task):
"""Remove the provisioning network from a node.
:param task: A TaskManager instance.
"""
@abc.abstractmethod
def configure_tenant_networks(self, task):
"""Configure tenant networks for a node.
:param task: A TaskManager instance.
:raises: NetworkError
"""
@abc.abstractmethod
def unconfigure_tenant_networks(self, task):
"""Unconfigure tenant networks for a node.
:param task: A TaskManager instance.
"""
@abc.abstractmethod
def add_cleaning_network(self, task):
"""Add the cleaning network to a node.
:param task: A TaskManager instance.
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
:raises: NetworkError
"""
@abc.abstractmethod
def remove_cleaning_network(self, task):
"""Remove the cleaning network from a node.
:param task: A TaskManager instance.
:raises: NetworkError
"""
def _validate_argsinfo(argsinfo):
"""Validate args info.

View File

@ -380,8 +380,10 @@ class AgentDeploy(base.DeployInterface):
"""Boot into the agent to prepare for cleaning.
:param task: a TaskManager object containing the node
:raises NodeCleaningFailure: if the previous cleaning ports cannot
be removed or if new cleaning ports cannot be created
:raises: NodeCleaningFailure, NetworkError if the previous cleaning
ports cannot be removed or if new cleaning ports cannot be created.
:raises: InvalidParameterValue if cleaning network UUID config option
has an invalid value.
:returns: states.CLEANWAIT to signify an asynchronous prepare
"""
return deploy_utils.prepare_inband_cleaning(
@ -391,8 +393,8 @@ class AgentDeploy(base.DeployInterface):
"""Clean up the PXE and DHCP files after cleaning.
:param task: a TaskManager object containing the node
:raises NodeCleaningFailure: if the cleaning ports cannot be
removed
:raises: NodeCleaningFailure, NetworkError if the cleaning ports cannot
be removed
"""
deploy_utils.tear_down_inband_cleaning(
task, manage_boot=CONF.agent.manage_agent_boot)

View File

@ -910,6 +910,8 @@ def get_boot_option(node):
return capabilities.get('boot_option', 'netboot').lower()
# TODO(vdrok): This method is left here for backwards compatibility with out of
# tree DHCP providers implementing cleaning methods. Remove it in Ocata
def prepare_cleaning_ports(task):
"""Prepare the Ironic ports of the node for cleaning.
@ -919,17 +921,39 @@ def prepare_cleaning_ports(task):
of each Ironic port, after creating the cleaning ports.
:param task: a TaskManager object containing the node
:raises NodeCleaningFailure: if the previous cleaning ports cannot
be removed or if new cleaning ports cannot be created
:raises: NodeCleaningFailure, NetworkError if the previous cleaning ports
cannot be removed or if new cleaning ports cannot be created.
:raises: InvalidParameterValue if cleaning network UUID config option has
an invalid value.
"""
provider = dhcp_factory.DHCPFactory()
provider_manages_delete_cleaning = hasattr(provider.provider,
'delete_cleaning_ports')
provider_manages_create_cleaning = hasattr(provider.provider,
'create_cleaning_ports')
# NOTE(vdrok): The neutron DHCP provider was changed to call network
# interface's add_cleaning_network anyway, so call it directly to avoid
# duplication of some actions
if (CONF.dhcp.dhcp_provider == 'neutron' or
(not provider_manages_delete_cleaning and
not provider_manages_create_cleaning)):
task.driver.network.add_cleaning_network(task)
return
LOG.warning(_LW("delete_cleaning_ports and create_cleaning_ports "
"functions in DHCP providers are deprecated, please move "
"this logic to the network interface's "
"remove_cleaning_network or add_cleaning_network methods "
"respectively and remove the old DHCP provider methods. "
"Possibility to do the cleaning via DHCP providers will "
"be removed in Ocata release."))
# If we have left over ports from a previous cleaning, remove them
if getattr(provider.provider, 'delete_cleaning_ports', None):
if provider_manages_delete_cleaning:
# Allow to raise if it fails, is caught and handled in conductor
provider.provider.delete_cleaning_ports(task)
# Create cleaning ports if necessary
if getattr(provider.provider, 'create_cleaning_ports', None):
if provider_manages_create_cleaning:
# Allow to raise if it fails, is caught and handled in conductor
ports = provider.provider.create_cleaning_ports(task)
@ -953,6 +977,8 @@ def prepare_cleaning_ports(task):
port.save()
# TODO(vdrok): This method is left here for backwards compatibility with out of
# tree DHCP providers implementing cleaning methods. Remove it in Ocata
def tear_down_cleaning_ports(task):
"""Deletes the cleaning ports created for each of the Ironic ports.
@ -960,22 +986,36 @@ def tear_down_cleaning_ports(task):
was started.
:param task: a TaskManager object containing the node
:raises NodeCleaningFailure: if the cleaning ports cannot be
:raises: NodeCleaningFailure, NetworkError if the cleaning ports cannot be
removed.
"""
# If we created cleaning ports, delete them
provider = dhcp_factory.DHCPFactory()
if getattr(provider.provider, 'delete_cleaning_ports', None):
provider_manages_delete_cleaning = hasattr(provider.provider,
'delete_cleaning_ports')
try:
# NOTE(vdrok): The neutron DHCP provider was changed to call network
# interface's remove_cleaning_network anyway, so call it directly to
# avoid duplication of some actions
if (CONF.dhcp.dhcp_provider == 'neutron' or
not provider_manages_delete_cleaning):
task.driver.network.remove_cleaning_network(task)
return
# NOTE(vdrok): No need for another deprecation warning here, if
# delete_cleaning_ports is in the DHCP provider the warning was
# printed in prepare_cleaning_ports
# Allow to raise if it fails, is caught and handled in conductor
provider.provider.delete_cleaning_ports(task)
for port in task.ports:
if 'cleaning_vif_port_id' in port.internal_info:
internal_info = port.internal_info
del internal_info['cleaning_vif_port_id']
port.internal_info = internal_info
port.save()
elif 'vif_port_id' in port.extra:
finally:
for port in task.ports:
if 'vif_port_id' in port.extra:
# TODO(vdrok): This piece is left for backwards compatibility,
# if ironic was upgraded during cleaning, vif_port_id
# containing cleaning neutron port UUID should be cleared,
@ -1028,8 +1068,10 @@ def prepare_inband_cleaning(task, manage_boot=True):
automatically boot agent ramdisk every time bare metal node is
rebooted.
:returns: states.CLEANWAIT to signify an asynchronous prepare.
:raises NodeCleaningFailure: if the previous cleaning ports cannot
be removed or if new cleaning ports cannot be created
:raises: NetworkError, NodeCleaningFailure if the previous cleaning ports
cannot be removed or if new cleaning ports cannot be created.
:raises: InvalidParameterValue if cleaning network UUID config option has
an invalid value.
"""
prepare_cleaning_ports(task)
@ -1062,7 +1104,7 @@ def tear_down_inband_cleaning(task, manage_boot=True):
:param manage_boot: If this is set to True, this method calls the
'clean_up_ramdisk' method of boot interface to boot the agent
ramdisk. If False, it skips this step.
:raises NodeCleaningFailure: if the cleaning ports cannot be
:raises: NetworkError, NodeCleaningFailure if the cleaning ports cannot be
removed.
"""
manager_utils.node_power_action(task, states.POWER_OFF)

View File

@ -274,8 +274,8 @@ class IloVirtualMediaAgentDeploy(agent.AgentDeploy):
:param task: a TaskManager object containing the node
:returns: states.CLEANWAIT to signify an asynchronous prepare.
:raises NodeCleaningFailure: if the previous cleaning ports cannot
be removed or if new cleaning ports cannot be created
:raises: NodeCleaningFailure, NetworkError if the previous cleaning
ports cannot be removed or if new cleaning ports cannot be created
:raises: IloOperationError, if some operation on iLO failed.
"""
# Powering off the Node before initiating boot for node cleaning.

View File

@ -0,0 +1,121 @@
# 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.
"""
Flat network interface. Useful for shared, flat networks.
"""
from oslo_config import cfg
from oslo_log import log
from oslo_utils import uuidutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LI
from ironic.common.i18n import _LW
from ironic.common import neutron
from ironic.drivers import base
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class FlatNetwork(base.NetworkInterface):
"""Flat network interface."""
def __init__(self):
cleaning_net = CONF.neutron.cleaning_network_uuid
# TODO(vdrok): Switch to DriverLoadError in Ocata
if not uuidutils.is_uuid_like(cleaning_net):
LOG.warning(_LW(
'Please specify a valid UUID for '
'[neutron]/cleaning_network_uuid configuration option so that '
'this interface is able to perform cleaning. It will be '
'required starting with the Ocata release, and if not '
'specified then, the conductor service will fail to start if '
'"flat" is in the list of values for '
'[DEFAULT]enabled_network_interfaces configuration option.'))
def add_provisioning_network(self, task):
"""Add the provisioning network to a node.
:param task: A TaskManager instance.
"""
pass
def remove_provisioning_network(self, task):
"""Remove the provisioning network from a node.
:param task: A TaskManager instance.
"""
pass
def configure_tenant_networks(self, task):
"""Configure tenant networks for a node.
:param task: A TaskManager instance.
"""
pass
def unconfigure_tenant_networks(self, task):
"""Unconfigure tenant networks for a node.
:param task: A TaskManager instance.
"""
for port in task.ports:
extra_dict = port.extra
extra_dict.pop('vif_port_id', None)
port.extra = extra_dict
port.save()
def add_cleaning_network(self, task):
"""Add the cleaning network to a node.
:param task: A TaskManager instance.
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
:raises: NetworkError, InvalidParameterValue
"""
if not uuidutils.is_uuid_like(CONF.neutron.cleaning_network_uuid):
raise exception.InvalidParameterValue(_(
'You must provide a valid cleaning network UUID in '
'[neutron]cleaning_network_uuid configuration option.'))
# If we have left over ports from a previous cleaning, remove them
neutron.rollback_ports(task, CONF.neutron.cleaning_network_uuid)
LOG.info(_LI('Adding cleaning network to node %s'), task.node.uuid)
vifs = neutron.add_ports_to_network(
task, CONF.neutron.cleaning_network_uuid, is_flat=True)
for port in task.ports:
if port.uuid in vifs:
internal_info = port.internal_info
internal_info['cleaning_vif_port_id'] = vifs[port.uuid]
port.internal_info = internal_info
port.save()
return vifs
def remove_cleaning_network(self, task):
"""Remove the cleaning network from a node.
:param task: A TaskManager instance.
:raises: NetworkError
"""
LOG.info(_LI('Removing ports from cleaning network for node %s'),
task.node.uuid)
neutron.remove_ports_from_network(
task, CONF.neutron.cleaning_network_uuid)
for port in task.ports:
if 'cleaning_vif_port_id' in port.internal_info:
internal_info = port.internal_info
del internal_info['cleaning_vif_port_id']
port.internal_info = internal_info
port.save()

View File

@ -0,0 +1,59 @@
# 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.
from ironic.drivers import base
class NoopNetwork(base.NetworkInterface):
"""Noop network interface."""
def add_provisioning_network(self, task):
"""Add the provisioning network to a node.
:param task: A TaskManager instance.
"""
pass
def remove_provisioning_network(self, task):
"""Remove the provisioning network from a node.
:param task: A TaskManager instance.
"""
pass
def configure_tenant_networks(self, task):
"""Configure tenant networks for a node.
:param task: A TaskManager instance.
"""
pass
def unconfigure_tenant_networks(self, task):
"""Unconfigure tenant networks for a node.
:param task: A TaskManager instance.
"""
pass
def add_cleaning_network(self, task):
"""Add the cleaning network to a node.
:param task: A TaskManager instance.
"""
pass
def remove_cleaning_network(self, task):
"""Remove the cleaning network from a node.
:param task: A TaskManager instance.
"""
pass

View File

@ -32,6 +32,7 @@ import fixtures
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_log import log as logging
from oslo_utils import uuidutils
import testtools
from ironic.common import config as ironic_config
@ -43,6 +44,7 @@ from ironic.tests.unit import policy_fixture
CONF = cfg.CONF
CONF.import_opt('host', 'ironic.common.service')
CONF.import_opt('cleaning_network_uuid', 'ironic.common.neutron', 'neutron')
logging.register_options(CONF)
logging.setup(CONF, 'ironic')
@ -115,6 +117,9 @@ class TestCase(testtools.TestCase):
self.config(use_stderr=False,
fatal_exception_format_errors=True,
tempdir=tempfile.tempdir)
self.config(cleaning_network_uuid=uuidutils.generate_uuid(),
group='neutron')
self.config(enabled_network_interfaces=['flat', 'noop'])
self.set_defaults(host='fake-mini',
debug=True)
self.set_defaults(connection="sqlite://",

View File

@ -17,8 +17,11 @@ from stevedore import dispatch
from ironic.common import driver_factory
from ironic.common import exception
from ironic.conductor import task_manager
from ironic.drivers import base as drivers_base
from ironic.tests import base
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
class FakeEp(object):
@ -86,3 +89,76 @@ class GetDriverTestCase(base.TestCase):
def test_get_driver_unknown(self):
self.assertRaises(exception.DriverNotFound,
driver_factory.get_driver, 'unknown_driver')
class NetworkInterfaceFactoryTestCase(db_base.DbTestCase):
def setUp(self):
super(NetworkInterfaceFactoryTestCase, self).setUp()
driver_factory.DriverFactory._extension_manager = None
driver_factory.NetworkInterfaceFactory._extension_manager = None
self.config(enabled_drivers=['fake'])
def test_build_driver_for_task(self):
# flat and noop network interfaces are enabled in base test case
factory = driver_factory.NetworkInterfaceFactory
node = obj_utils.create_test_node(self.context, driver='fake',
network_interface='flat')
with task_manager.acquire(self.context, node.id) as task:
extension_mgr = factory._extension_manager
self.assertIn('flat', extension_mgr)
self.assertIn('noop', extension_mgr)
self.assertEqual(extension_mgr['flat'].obj, task.driver.network)
self.assertEqual('ironic.hardware.interfaces.network',
factory._entrypoint_name)
self.assertEqual(['flat', 'noop'],
sorted(factory._enabled_driver_list))
def test_build_driver_for_task_default_is_none(self):
# flat and noop network interfaces are enabled in base test case
factory = driver_factory.NetworkInterfaceFactory
self.config(dhcp_provider='none', group='dhcp')
node = obj_utils.create_test_node(self.context, driver='fake')
with task_manager.acquire(self.context, node.id) as task:
extension_mgr = factory._extension_manager
self.assertIn('flat', extension_mgr)
self.assertIn('noop', extension_mgr)
self.assertEqual(extension_mgr['noop'].obj, task.driver.network)
def test_build_driver_for_task_default_network_interface_is_set(self):
# flat and noop network interfaces are enabled in base test case
factory = driver_factory.NetworkInterfaceFactory
self.config(dhcp_provider='none', group='dhcp')
self.config(default_network_interface='flat')
node = obj_utils.create_test_node(self.context, driver='fake')
with task_manager.acquire(self.context, node.id) as task:
extension_mgr = factory._extension_manager
self.assertIn('flat', extension_mgr)
self.assertIn('noop', extension_mgr)
self.assertEqual(extension_mgr['flat'].obj, task.driver.network)
def test_build_driver_for_task_default_is_flat(self):
# flat and noop network interfaces are enabled in base test case
factory = driver_factory.NetworkInterfaceFactory
node = obj_utils.create_test_node(self.context, driver='fake')
with task_manager.acquire(self.context, node.id) as task:
extension_mgr = factory._extension_manager
self.assertIn('flat', extension_mgr)
self.assertIn('noop', extension_mgr)
self.assertEqual(extension_mgr['flat'].obj, task.driver.network)
def test_build_driver_for_task_unknown_network_interface(self):
node = obj_utils.create_test_node(self.context, driver='fake',
network_interface='meow')
self.assertRaises(exception.DriverNotFoundInEntrypoint,
task_manager.acquire, self.context, node.id)
class NewDriverFactory(driver_factory.BaseDriverFactory):
_entrypoint_name = 'woof'
class NewFactoryTestCase(db_base.DbTestCase):
def test_new_driver_factory_unknown_entrypoint(self):
factory = NewDriverFactory()
self.assertEqual('woof', factory._entrypoint_name)
self.assertEqual([], factory._enabled_driver_list)

View File

@ -11,11 +11,18 @@
# under the License.
import mock
from neutronclient.common import exceptions as neutron_client_exc
from neutronclient.v2_0 import client
from oslo_config import cfg
from oslo_utils import uuidutils
from ironic.common import exception
from ironic.common import neutron
from ironic.conductor import task_manager
from ironic.tests import base
from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as object_utils
class TestNeutronClient(base.TestCase):
@ -107,3 +114,249 @@ class TestNeutronClient(base.TestCase):
self.assertRaises(ValueError, cfg.CONF.set_override,
'auth_strategy', 'fake', 'neutron',
enforce_type=True)
class TestNeutronNetworkActions(db_base.DbTestCase):
def setUp(self):
super(TestNeutronNetworkActions, self).setUp()
mgr_utils.mock_the_extension_manager(driver='fake')
self.config(enabled_drivers=['fake'])
self.node = object_utils.create_test_node(self.context)
self.ports = [object_utils.create_test_port(
self.context, node_id=self.node.id,
uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c782',
address='52:54:00:cf:2d:32',
extra={'vif_port_id': uuidutils.generate_uuid()}
)]
# Very simple neutron port representation
self.neutron_port = {'id': '132f871f-eaec-4fed-9475-0d54465e0f00',
'mac_address': '52:54:00:cf:2d:32'}
self.network_uuid = uuidutils.generate_uuid()
@mock.patch.object(client.Client, 'create_port')
def test_add_ports_to_vlan_network(self, create_mock):
# Ports will be created only if pxe_enabled is True
object_utils.create_test_port(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:22',
pxe_enabled=False
)
port = self.ports[0]
expected_body = {
'port': {
'network_id': self.network_uuid,
'admin_state_up': True,
'binding:vnic_type': 'baremetal',
'device_owner': 'baremetal:none',
'binding:host_id': self.node.uuid,
'device_id': self.node.uuid,
'mac_address': port.address,
'binding:profile': {
'local_link_information': [port.local_link_connection]
}
}
}
# Ensure we can create ports
create_mock.return_value = {'port': self.neutron_port}
expected = {port.uuid: self.neutron_port['id']}
with task_manager.acquire(self.context, self.node.uuid) as task:
ports = neutron.add_ports_to_network(task, self.network_uuid)
self.assertEqual(expected, ports)
create_mock.assert_called_once_with(expected_body)
@mock.patch.object(client.Client, 'create_port')
def test_add_ports_to_flat_network(self, create_mock):
port = self.ports[0]
expected_body = {
'port': {
'network_id': self.network_uuid,
'admin_state_up': True,
'binding:vnic_type': 'baremetal',
'device_owner': 'baremetal:none',
'device_id': self.node.uuid,
'mac_address': port.address,
'binding:profile': {
'local_link_information': [port.local_link_connection]
}
}
}
# Ensure we can create ports
create_mock.return_value = {'port': self.neutron_port}
expected = {port.uuid: self.neutron_port['id']}
with task_manager.acquire(self.context, self.node.uuid) as task:
ports = neutron.add_ports_to_network(task, self.network_uuid,
is_flat=True)
self.assertEqual(expected, ports)
create_mock.assert_called_once_with(expected_body)
@mock.patch.object(client.Client, 'create_port')
def test_add_ports_to_flat_network_no_neutron_port_id(self, create_mock):
port = self.ports[0]
expected_body = {
'port': {
'network_id': self.network_uuid,
'admin_state_up': True,
'binding:vnic_type': 'baremetal',
'device_owner': 'baremetal:none',
'device_id': self.node.uuid,
'mac_address': port.address,
'binding:profile': {
'local_link_information': [port.local_link_connection]
}
}
}
del self.neutron_port['id']
create_mock.return_value = {'port': self.neutron_port}
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.NetworkError,
neutron.add_ports_to_network,
task, self.network_uuid, is_flat=True)
create_mock.assert_called_once_with(expected_body)
@mock.patch.object(client.Client, 'create_port')
def test_add_ports_to_vlan_network_instance_uuid(self, create_mock):
self.node.instance_uuid = uuidutils.generate_uuid()
self.node.save()
port = self.ports[0]
expected_body = {
'port': {
'network_id': self.network_uuid,
'admin_state_up': True,
'binding:vnic_type': 'baremetal',
'device_owner': 'baremetal:none',
'binding:host_id': self.node.uuid,
'device_id': self.node.instance_uuid,
'mac_address': port.address,
'binding:profile': {
'local_link_information': [port.local_link_connection]
}
}
}
# Ensure we can create ports
create_mock.return_value = {'port': self.neutron_port}
expected = {port.uuid: self.neutron_port['id']}
with task_manager.acquire(self.context, self.node.uuid) as task:
ports = neutron.add_ports_to_network(task, self.network_uuid)
self.assertEqual(expected, ports)
create_mock.assert_called_once_with(expected_body)
@mock.patch.object(neutron, 'rollback_ports')
@mock.patch.object(client.Client, 'create_port')
def test_add_network_fail(self, create_mock, rollback_mock):
# Check that if creating a port fails, the ports are cleaned up
create_mock.side_effect = neutron_client_exc.ConnectionFailed
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(
exception.NetworkError, 'Could not create neutron port',
neutron.add_ports_to_network, task, self.network_uuid)
rollback_mock.assert_called_once_with(task, self.network_uuid)
@mock.patch.object(neutron, 'rollback_ports')
@mock.patch.object(client.Client, 'create_port', return_value={})
def test_add_network_fail_create_any_port_empty(self, create_mock,
rollback_mock):
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(
exception.NetworkError, 'any PXE enabled port',
neutron.add_ports_to_network, task, self.network_uuid)
self.assertFalse(rollback_mock.called)
@mock.patch.object(neutron, 'LOG')
@mock.patch.object(neutron, 'rollback_ports')
@mock.patch.object(client.Client, 'create_port')
def test_add_network_fail_create_some_ports_empty(self, create_mock,
rollback_mock, log_mock):
port2 = object_utils.create_test_port(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(),
address='52:54:55:cf:2d:32',
extra={'vif_port_id': uuidutils.generate_uuid()}
)
create_mock.side_effect = [{'port': self.neutron_port}, {}]
with task_manager.acquire(self.context, self.node.uuid) as task:
neutron.add_ports_to_network(task, self.network_uuid)
self.assertIn(str(port2.uuid),
# Call #0, argument #1
log_mock.warning.call_args[0][1]['ports'])
self.assertFalse(rollback_mock.called)
@mock.patch.object(neutron, 'remove_neutron_ports')
def test_remove_ports_from_network(self, remove_mock):
with task_manager.acquire(self.context, self.node.uuid) as task:
neutron.remove_ports_from_network(task, self.network_uuid)
remove_mock.assert_called_once_with(
task,
{'network_id': self.network_uuid,
'mac_address': [self.ports[0].address]}
)
@mock.patch.object(neutron, 'remove_neutron_ports')
def test_remove_ports_from_network_not_all_pxe_enabled(self, remove_mock):
object_utils.create_test_port(
self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(),
address='52:54:55:cf:2d:32',
pxe_enabled=False
)
with task_manager.acquire(self.context, self.node.uuid) as task:
neutron.remove_ports_from_network(task, self.network_uuid)
remove_mock.assert_called_once_with(
task,
{'network_id': self.network_uuid,
'mac_address': [self.ports[0].address]}
)
@mock.patch.object(client.Client, 'delete_port')
@mock.patch.object(client.Client, 'list_ports')
def test_remove_neutron_ports(self, list_mock, delete_mock):
with task_manager.acquire(self.context, self.node.uuid) as task:
list_mock.return_value = {'ports': [self.neutron_port]}
neutron.remove_neutron_ports(task, {'param': 'value'})
list_mock.assert_called_once_with(**{'param': 'value'})
delete_mock.assert_called_once_with(self.neutron_port['id'])
@mock.patch.object(client.Client, 'list_ports')
def test_remove_neutron_ports_list_fail(self, list_mock):
with task_manager.acquire(self.context, self.node.uuid) as task:
list_mock.side_effect = neutron_client_exc.ConnectionFailed
self.assertRaisesRegex(
exception.NetworkError, 'Could not get given network VIF',
neutron.remove_neutron_ports, task, {'param': 'value'})
list_mock.assert_called_once_with(**{'param': 'value'})
@mock.patch.object(client.Client, 'delete_port')
@mock.patch.object(client.Client, 'list_ports')
def test_remove_neutron_ports_delete_fail(self, list_mock, delete_mock):
with task_manager.acquire(self.context, self.node.uuid) as task:
delete_mock.side_effect = neutron_client_exc.ConnectionFailed
list_mock.return_value = {'ports': [self.neutron_port]}
self.assertRaisesRegex(
exception.NetworkError, 'Could not remove VIF',
neutron.remove_neutron_ports, task, {'param': 'value'})
list_mock.assert_called_once_with(**{'param': 'value'})
delete_mock.assert_called_once_with(self.neutron_port['id'])
def test_get_node_portmap(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
portmap = neutron.get_node_portmap(task)
self.assertEqual(
{self.ports[0].uuid: self.ports[0].local_link_connection},
portmap
)
@mock.patch.object(neutron, 'remove_ports_from_network')
def test_rollback_ports(self, remove_mock):
with task_manager.acquire(self.context, self.node.uuid) as task:
neutron.rollback_ports(task, self.network_uuid)
remove_mock.assert_called_once_with(task, self.network_uuid)
@mock.patch.object(neutron, 'LOG')
@mock.patch.object(neutron, 'remove_ports_from_network')
def test_rollback_ports_exception(self, remove_mock, log_mock):
remove_mock.side_effect = exception.NetworkError('boom')
with task_manager.acquire(self.context, self.node.uuid) as task:
neutron.rollback_ports(task, self.network_uuid)
self.assertTrue(log_mock.exception.called)

View File

@ -77,7 +77,8 @@ class StartStopTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase):
@mock.patch.object(driver_factory.DriverFactory, '__getitem__',
lambda *args: mock.MagicMock())
def test_start_registers_driver_names(self):
@mock.patch.object(driver_factory, 'NetworkInterfaceFactory')
def test_start_registers_driver_names(self, net_factory):
init_names = ['fake1', 'fake2']
restart_names = ['fake3', 'fake4']
@ -99,6 +100,7 @@ class StartStopTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase):
res = objects.Conductor.get_by_hostname(self.context,
self.hostname)
self.assertEqual(restart_names, res['drivers'])
self.assertEqual(2, net_factory.call_count)
@mock.patch.object(driver_factory.DriverFactory, '__getitem__')
def test_start_registers_driver_specific_tasks(self, get_mock):

View File

@ -2341,7 +2341,8 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
'management': {'result': True},
'boot': {'result': True},
'raid': {'result': True},
'deploy': {'result': True}}
'deploy': {'result': True},
'network': {'result': True}}
self.assertEqual(expected, ret)
mock_iwdi.assert_called_once_with(self.context, node.instance_info)

View File

@ -18,7 +18,6 @@ import mock
from neutronclient.common import exceptions as neutron_client_exc
from neutronclient.v2_0 import client
from oslo_config import cfg
from oslo_utils import uuidutils
from ironic.common import dhcp_factory
@ -474,127 +473,34 @@ class TestNeutron(db_base.DbTestCase):
[mock.call(task, task.ports[0], mock.ANY),
mock.call(task, task.portgroups[0], mock.ANY)])
@mock.patch.object(client.Client, 'create_port')
def test_create_cleaning_ports(self, create_mock):
# Ensure we can create cleaning ports for in band cleaning
create_mock.return_value = {'port': self.neutron_port}
expected = {self.ports[0].uuid: self.neutron_port['id']}
@mock.patch.object(neutron, 'create_cleaning_ports_deprecation', False)
@mock.patch.object(neutron, 'LOG', autospec=True)
def test_create_cleaning_ports(self, log_mock):
self.config(cleaning_network_uuid=uuidutils.generate_uuid(),
group='neutron')
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
ports = api.create_cleaning_ports(task)
self.assertEqual(expected, ports)
create_mock.assert_called_once_with({'port': {
'network_id': '00000000-0000-0000-0000-000000000000',
'admin_state_up': True, 'mac_address': self.ports[0].address}})
with mock.patch.object(
task.driver.network, 'add_cleaning_network',
autospec=True) as add_net_mock:
api.create_cleaning_ports(task)
add_net_mock.assert_called_once_with(task)
@mock.patch.object(neutron.NeutronDHCPApi, '_rollback_cleaning_ports')
@mock.patch.object(client.Client, 'create_port')
def test_create_cleaning_ports_fail(self, create_mock, rollback_mock):
# Check that if creating a port fails, the ports are cleaned up
create_mock.side_effect = neutron_client_exc.ConnectionFailed
api.create_cleaning_ports(task)
self.assertEqual(1, log_mock.warning.call_count)
@mock.patch.object(neutron, 'delete_cleaning_ports_deprecation', False)
@mock.patch.object(neutron, 'LOG', autospec=True)
def test_delete_cleaning_ports(self, log_mock):
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.NodeCleaningFailure,
api.create_cleaning_ports,
task)
create_mock.assert_called_once_with({'port': {
'network_id': '00000000-0000-0000-0000-000000000000',
'admin_state_up': True, 'mac_address': self.ports[0].address}})
rollback_mock.assert_called_once_with(task)
with mock.patch.object(
task.driver.network, 'remove_cleaning_network',
autospec=True) as rm_net_mock:
api.delete_cleaning_ports(task)
rm_net_mock.assert_called_once_with(task)
@mock.patch.object(neutron.NeutronDHCPApi, '_rollback_cleaning_ports')
@mock.patch.object(client.Client, 'create_port')
def test_create_cleaning_ports_fail_delayed(self, create_mock,
rollback_mock):
"""Check ports are cleaned up on failure to create them
This test checks that the port clean-up occurs
when the port create call was successful,
but the port in fact was not created.
"""
# NOTE(pas-ha) this is trying to emulate the complex port object
# with both methods and dictionary access with methods on elements
mockport = mock.MagicMock()
create_mock.return_value = mockport
# fail only on second 'or' branch to fool lazy eval
# and actually execute both expressions to assert on both mocks
mockport.get.return_value = True
mockitem = mock.Mock()
mockport.__getitem__.return_value = mockitem
mockitem.get.return_value = None
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.NodeCleaningFailure,
api.create_cleaning_ports,
task)
create_mock.assert_called_once_with({'port': {
'network_id': '00000000-0000-0000-0000-000000000000',
'admin_state_up': True, 'mac_address': self.ports[0].address}})
rollback_mock.assert_called_once_with(task)
mockport.get.assert_called_once_with('port')
mockitem.get.assert_called_once_with('id')
mockport.__getitem__.assert_called_once_with('port')
@mock.patch.object(client.Client, 'create_port')
def test_create_cleaning_ports_bad_config(self, create_mock):
# Check an error is raised if the cleaning network is not set
self.config(cleaning_network_uuid=None, group='neutron')
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
api.create_cleaning_ports, task)
@mock.patch.object(client.Client, 'delete_port')
@mock.patch.object(client.Client, 'list_ports')
def test_delete_cleaning_ports(self, list_mock, delete_mock):
# Ensure that we can delete cleaning ports, and that ports with
# different macs don't get deleted
other_port = {'id': '132f871f-eaec-4fed-9475-0d54465e0f01',
'mac_address': 'aa:bb:cc:dd:ee:ff'}
list_mock.return_value = {'ports': [self.neutron_port, other_port]}
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
api.delete_cleaning_ports(task)
list_mock.assert_called_once_with(
network_id='00000000-0000-0000-0000-000000000000')
delete_mock.assert_called_once_with(self.neutron_port['id'])
@mock.patch.object(client.Client, 'list_ports')
def test_delete_cleaning_ports_list_fail(self, list_mock):
# Check that if listing ports fails, the node goes to cleanfail
list_mock.side_effect = neutron_client_exc.ConnectionFailed
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.NodeCleaningFailure,
api.delete_cleaning_ports,
task)
list_mock.assert_called_once_with(
network_id='00000000-0000-0000-0000-000000000000')
@mock.patch.object(client.Client, 'delete_port')
@mock.patch.object(client.Client, 'list_ports')
def test_delete_cleaning_ports_delete_fail(self, list_mock, delete_mock):
# Check that if deleting ports fails, the node goes to cleanfail
list_mock.return_value = {'ports': [self.neutron_port]}
delete_mock.side_effect = neutron_client_exc.ConnectionFailed
api = dhcp_factory.DHCPFactory().provider
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.NodeCleaningFailure,
api.delete_cleaning_ports,
task)
list_mock.assert_called_once_with(
network_id='00000000-0000-0000-0000-000000000000')
delete_mock.assert_called_once_with(self.neutron_port['id'])
def test_out_range_auth_strategy(self):
self.assertRaises(ValueError, cfg.CONF.set_override,
'auth_strategy', 'fake', 'neutron',
enforce_type=True)
api.delete_cleaning_ports(task)
self.assertEqual(1, log_mock.warning.call_count)

View File

@ -0,0 +1,85 @@
# 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 oslo_config import cfg
from oslo_utils import uuidutils
from ironic.common import exception
from ironic.common import neutron
from ironic.conductor import task_manager
from ironic.drivers.modules.network import flat as flat_interface
from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils
CONF = cfg.CONF
class TestFlatInterface(db_base.DbTestCase):
def setUp(self):
super(TestFlatInterface, self).setUp()
self.config(enabled_drivers=['fake'])
mgr_utils.mock_the_extension_manager()
self.interface = flat_interface.FlatNetwork()
self.node = utils.create_test_node(self.context)
self.port = utils.create_test_port(
self.context, node_id=self.node.id,
internal_info={
'cleaning_vif_port_id': uuidutils.generate_uuid()})
@mock.patch.object(flat_interface, 'LOG')
def test_init_incorrect_cleaning_net(self, mock_log):
self.config(cleaning_network_uuid=None, group='neutron')
flat_interface.FlatNetwork()
self.assertTrue(mock_log.warning.called)
@mock.patch.object(neutron, 'add_ports_to_network')
@mock.patch.object(neutron, 'rollback_ports')
def test_add_cleaning_network(self, rollback_mock, add_mock):
add_mock.return_value = {self.port.uuid: 'vif-port-id'}
with task_manager.acquire(self.context, self.node.id) as task:
self.interface.add_cleaning_network(task)
rollback_mock.assert_called_once_with(
task, CONF.neutron.cleaning_network_uuid)
add_mock.assert_called_once_with(
task, CONF.neutron.cleaning_network_uuid, is_flat=True)
self.port.refresh()
self.assertEqual('vif-port-id',
self.port.internal_info['cleaning_vif_port_id'])
@mock.patch.object(neutron, 'add_ports_to_network')
@mock.patch.object(neutron, 'rollback_ports')
def test_add_cleaning_network_no_cleaning_net_uuid(self, rollback_mock,
add_mock):
self.config(cleaning_network_uuid='abc', group='neutron')
with task_manager.acquire(self.context, self.node.id) as task:
self.assertRaises(exception.InvalidParameterValue,
self.interface.add_cleaning_network, task)
self.assertFalse(rollback_mock.called)
self.assertFalse(add_mock.called)
@mock.patch.object(neutron, 'remove_ports_from_network')
def test_remove_cleaning_network(self, remove_mock):
with task_manager.acquire(self.context, self.node.id) as task:
self.interface.remove_cleaning_network(task)
remove_mock.assert_called_once_with(
task, CONF.neutron.cleaning_network_uuid)
self.port.refresh()
self.assertNotIn('cleaning_vif_port_id', self.port.internal_info)
def test_unconfigure_tenant_networks(self):
with task_manager.acquire(self.context, self.node.id) as task:
self.interface.unconfigure_tenant_networks(task)
self.port.refresh()
self.assertNotIn('vif_port_id', self.port.extra)

View File

@ -28,6 +28,7 @@ import testtools
from testtools import matchers
from ironic.common import boot_devices
from ironic.common import dhcp_factory
from ironic.common import exception
from ironic.common import image_service
from ironic.common import keystone
@ -1735,38 +1736,66 @@ class AgentMethodsTestCase(db_base.DbTestCase):
self.assertEqual(True, task.node.driver_internal_info[
'agent_continue_if_ata_erase_failed'])
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports',
autospec=True)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.create_cleaning_ports',
autospec=True)
def _test_prepare_inband_cleaning_ports(
self, create_mock, delete_mock, return_vif_port_id=True):
@mock.patch.object(utils.LOG, 'warning', autospec=True)
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)
def _test_prepare_inband_cleaning_ports_out_of_tree(
self, dhcp_factory_mock, log_mock, return_vif_port_id=True):
self.config(group='dhcp', dhcp_provider='my_shiny_dhcp_provider')
dhcp_provider = dhcp_factory_mock.return_value.provider
create = dhcp_provider.create_cleaning_ports
delete = dhcp_provider.delete_cleaning_ports
if return_vif_port_id:
create_mock.return_value = {self.ports[0].uuid: 'vif-port-id'}
create.return_value = {self.ports[0].uuid: 'vif-port-id'}
else:
create_mock.return_value = {}
create.return_value = {}
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.prepare_cleaning_ports(task)
create_mock.assert_called_once_with(mock.ANY, task)
delete_mock.assert_called_once_with(mock.ANY, task)
create.assert_called_once_with(task)
delete.assert_called_once_with(task)
self.assertTrue(log_mock.called)
self.ports[0].refresh()
self.assertEqual('vif-port-id',
self.ports[0].internal_info['cleaning_vif_port_id'])
def test_prepare_inband_cleaning_ports(self):
self._test_prepare_inband_cleaning_ports()
def test_prepare_inband_cleaning_ports_out_of_tree(self):
self._test_prepare_inband_cleaning_ports_out_of_tree()
def test_prepare_inband_cleaning_ports_no_vif_port_id(self):
def test_prepare_inband_cleaning_ports_out_of_tree_no_vif_port_id(self):
self.assertRaises(
exception.NodeCleaningFailure,
self._test_prepare_inband_cleaning_ports,
self._test_prepare_inband_cleaning_ports_out_of_tree,
return_vif_port_id=False)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports',
autospec=True)
def test_tear_down_inband_cleaning_ports(self, neutron_mock):
@mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.'
'add_cleaning_network')
def test_prepare_inband_cleaning_ports_neutron(self, add_clean_net_mock):
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.prepare_cleaning_ports(task)
add_clean_net_mock.assert_called_once_with(task)
@mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.'
'add_cleaning_network')
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)
def test_prepare_inband_cleaning_ports_provider_does_not_create(
self, dhcp_factory_mock, add_clean_net_mock):
self.config(group='dhcp', dhcp_provider='my_shiny_dhcp_provider')
dhcp_provider = dhcp_factory_mock.return_value.provider
del dhcp_provider.delete_cleaning_ports
del dhcp_provider.create_cleaning_ports
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.prepare_cleaning_ports(task)
add_clean_net_mock.assert_called_once_with(task)
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)
def test_tear_down_inband_cleaning_ports_out_of_tree(self,
dhcp_factory_mock):
self.config(group='dhcp', dhcp_provider='my_shiny_dhcp_provider')
dhcp_provider = dhcp_factory_mock.return_value.provider
delete = dhcp_provider.delete_cleaning_ports
internal_info = self.ports[0].internal_info
internal_info['cleaning_vif_port_id'] = 'vif-port-id-1'
self.ports[0].internal_info = internal_info
@ -1774,18 +1803,46 @@ class AgentMethodsTestCase(db_base.DbTestCase):
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.tear_down_cleaning_ports(task)
neutron_mock.assert_called_once_with(mock.ANY, task)
delete.assert_called_once_with(task)
self.ports[0].refresh()
self.assertNotIn('cleaning_vif_port_id', self.ports[0].internal_info)
self.assertNotIn('vif_port_id', self.ports[0].extra)
@mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.'
'remove_cleaning_network')
def test_tear_down_inband_cleaning_ports_neutron(self, rm_clean_net_mock):
extra_port = obj_utils.create_test_port(
self.context, node_id=self.node.id, address='10:00:00:00:00:01',
extra={'vif_port_id': 'vif-port'}, uuid=uuidutils.generate_uuid()
)
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.tear_down_cleaning_ports(task)
rm_clean_net_mock.assert_called_once_with(task)
extra_port.refresh()
self.assertNotIn('vif_port_id', extra_port.extra)
@mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.'
'remove_cleaning_network')
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)
def test_tear_down_inband_cleaning_ports_provider_does_not_delete(
self, dhcp_factory_mock, rm_clean_net_mock):
self.config(group='dhcp', dhcp_provider='my_shiny_dhcp_provider')
dhcp_provider = dhcp_factory_mock.return_value.provider
del dhcp_provider.delete_cleaning_ports
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.tear_down_cleaning_ports(task)
rm_clean_net_mock.assert_called_once_with(task)
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', autospec=True)
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
@mock.patch.object(utils, 'build_agent_options', autospec=True)
@mock.patch.object(utils, 'prepare_cleaning_ports', autospec=True)
@mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.'
'add_cleaning_network')
def _test_prepare_inband_cleaning(
self, prepare_cleaning_ports_mock,
self, add_cleaning_network_mock,
build_options_mock, power_mock, prepare_ramdisk_mock,
manage_boot=True):
build_options_mock.return_value = {'a': 'b'}
@ -1794,7 +1851,7 @@ class AgentMethodsTestCase(db_base.DbTestCase):
self.assertEqual(
states.CLEANWAIT,
utils.prepare_inband_cleaning(task, manage_boot=manage_boot))
prepare_cleaning_ports_mock.assert_called_once_with(task)
add_cleaning_network_mock.assert_called_once_with(task)
power_mock.assert_called_once_with(task, states.REBOOT)
self.assertEqual(1, task.node.driver_internal_info[
'agent_erase_devices_iterations'])
@ -1815,16 +1872,17 @@ class AgentMethodsTestCase(db_base.DbTestCase):
self._test_prepare_inband_cleaning(manage_boot=False)
@mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True)
@mock.patch.object(utils, 'tear_down_cleaning_ports', autospec=True)
@mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.'
'remove_cleaning_network')
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
def _test_tear_down_inband_cleaning(
self, power_mock, tear_down_ports_mock,
self, power_mock, remove_cleaning_network_mock,
clean_up_ramdisk_mock, manage_boot=True):
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
utils.tear_down_inband_cleaning(task, manage_boot=manage_boot)
power_mock.assert_called_once_with(task, states.POWER_OFF)
tear_down_ports_mock.assert_called_once_with(task)
remove_cleaning_network_mock.assert_called_once_with(task)
if manage_boot:
clean_up_ramdisk_mock.assert_called_once_with(
task.driver.boot, task)

View File

@ -0,0 +1,29 @@
---
features:
- |
Added network interface. Introduced two network interface implementations:
``flat``, which replicates the flat network behavior present previously and
``noop`` when neutron is not used, which is basically a noop interface.
The network interface is used to switch network for node during
provisioning/cleaning. Added ``enabled_network_interfaces`` option in
DEFAULT config section. This option defines a list of enabled network
interfaces on the conductor.
deprecations:
- |
``create_cleaning_ports`` and ``delete_cleaning_ports`` methods in DHCP
providers are deprecated and will be removed completely in the Ocata
release. The logic they are implementing should be moved to a custom
network interface's ``add_cleaning_network`` and
``remove_cleaning_network`` methods respectively. After that, the methods
themselves should be removed from DHCP provider so that network interface
is used instead. ``flat`` network interface does not require
``[neutron]cleaning_network_uuid`` for now so as not to break standalone
deployments, but it will be required in the Ocata release.
upgrade:
- |
``[DEFAULT]default_network_interface`` configuration option is introduced,
with empty default value. If set, the specified interface will be used as
the network interface for nodes that don't have ``network_interface`` field
set. If it is not set, the network interface is determined by looking at
the ``[dhcp]dhcp_provider`` value. If it is ``neutron`` - ``flat`` network
interface is the default, ``noop`` otherwise.

View File

@ -87,6 +87,10 @@ ironic.drivers =
pxe_iscsi_cimc = ironic.drivers.pxe:PXEAndCIMCDriver
pxe_agent_cimc = ironic.drivers.agent:AgentAndCIMCDriver
ironic.hardware.interfaces.network =
flat = ironic.drivers.modules.network.flat:FlatNetwork
noop = ironic.drivers.modules.network.noop:NoopNetwork
ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration