Add 'neutron' network interface
This patch adds a 'neutron' network interface. This interface supports separate networks for provisioning and for cleaning of nodes. Partial-bug: #1526403 Co-Authored-By: Vladyslav Drok <vdrok@mirantis.com> Change-Id: Ia3442ab3536a1a8d8839b24dbfc640b818450350
This commit is contained in:
parent
cde11611d9
commit
ab97fa0f1f
@ -1518,12 +1518,16 @@
|
|||||||
#auth_strategy = keystone
|
#auth_strategy = keystone
|
||||||
|
|
||||||
# Neutron network UUID for the ramdisk to be booted into for
|
# Neutron network UUID for the ramdisk to be booted into for
|
||||||
# cleaning nodes. Required if cleaning (either automatic or
|
# cleaning nodes. Required for "neutron" network interface. It
|
||||||
# manual) is run for flat network interface, and, if DHCP
|
# is also required if cleaning nodes when using "flat" network
|
||||||
# providers are still being used, for neutron DHCP provider.
|
# interface or "neutron" DHCP provider. (string value)
|
||||||
# (string value)
|
|
||||||
#cleaning_network_uuid = <None>
|
#cleaning_network_uuid = <None>
|
||||||
|
|
||||||
|
# Neutron network UUID for the ramdisk to be booted into for
|
||||||
|
# provisioning nodes. Required for "neutron" network
|
||||||
|
# interface. (string value)
|
||||||
|
#provisioning_network_uuid = <None>
|
||||||
|
|
||||||
|
|
||||||
[oneview]
|
[oneview]
|
||||||
|
|
||||||
|
@ -32,15 +32,19 @@ def get_node_vif_ids(task):
|
|||||||
portgroup_vifs = {}
|
portgroup_vifs = {}
|
||||||
port_vifs = {}
|
port_vifs = {}
|
||||||
for portgroup in task.portgroups:
|
for portgroup in task.portgroups:
|
||||||
# NOTE(vdrok): This works because cleaning_vif_port_id doesn't exist
|
# NOTE(vdrok): We are booting the node only in one network at a time,
|
||||||
# when we're in deployment/tenant network
|
# and presence of cleaning_vif_port_id means we're doing cleaning, of
|
||||||
|
# provisioning_vif_port_id - provisioning. Otherwise it's a tenant
|
||||||
|
# network
|
||||||
vif = (portgroup.internal_info.get('cleaning_vif_port_id') or
|
vif = (portgroup.internal_info.get('cleaning_vif_port_id') or
|
||||||
|
portgroup.internal_info.get('provisioning_vif_port_id') or
|
||||||
portgroup.extra.get('vif_port_id'))
|
portgroup.extra.get('vif_port_id'))
|
||||||
if vif:
|
if vif:
|
||||||
portgroup_vifs[portgroup.uuid] = vif
|
portgroup_vifs[portgroup.uuid] = vif
|
||||||
vifs['portgroups'] = portgroup_vifs
|
vifs['portgroups'] = portgroup_vifs
|
||||||
for port in task.ports:
|
for port in task.ports:
|
||||||
vif = (port.internal_info.get('cleaning_vif_port_id') or
|
vif = (port.internal_info.get('cleaning_vif_port_id') or
|
||||||
|
port.internal_info.get('provisioning_vif_port_id') or
|
||||||
port.extra.get('vif_port_id'))
|
port.extra.get('vif_port_id'))
|
||||||
if vif:
|
if vif:
|
||||||
port_vifs[port.uuid] = vif
|
port_vifs[port.uuid] = vif
|
||||||
|
@ -51,10 +51,14 @@ neutron_opts = [
|
|||||||
'should only be used for testing.')),
|
'should only be used for testing.')),
|
||||||
cfg.StrOpt('cleaning_network_uuid',
|
cfg.StrOpt('cleaning_network_uuid',
|
||||||
help=_('Neutron network UUID for the ramdisk to be booted '
|
help=_('Neutron network UUID for the ramdisk to be booted '
|
||||||
'into for cleaning nodes. Required if cleaning (either '
|
'into for cleaning nodes. Required for "neutron" '
|
||||||
'automatic or manual) is run for flat network interface,'
|
'network interface. It is also required if cleaning '
|
||||||
' and, if DHCP providers are still being used, for '
|
'nodes when using "flat" network interface or "neutron" '
|
||||||
'neutron DHCP provider.'))
|
'DHCP provider.')),
|
||||||
|
cfg.StrOpt('provisioning_network_uuid',
|
||||||
|
help=_('Neutron network UUID for the ramdisk to be booted '
|
||||||
|
'into for provisioning nodes. Required for "neutron" '
|
||||||
|
'network interface.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF.register_opts(neutron_opts, group='neutron')
|
CONF.register_opts(neutron_opts, group='neutron')
|
||||||
|
@ -208,9 +208,12 @@ class NeutronDHCPApi(base.BaseDHCP):
|
|||||||
:raises: InvalidIPv4Address
|
:raises: InvalidIPv4Address
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE(vdrok): This works because cleaning_vif_port_id doesn't exist
|
# NOTE(vdrok): We are booting the node only in one network at a time,
|
||||||
# when we're in deployment/tenant network
|
# and presence of cleaning_vif_port_id means we're doing cleaning, of
|
||||||
|
# provisioning_vif_port_id - provisioning. Otherwise it's a tenant
|
||||||
|
# network
|
||||||
vif = (p_obj.internal_info.get('cleaning_vif_port_id') or
|
vif = (p_obj.internal_info.get('cleaning_vif_port_id') or
|
||||||
|
p_obj.internal_info.get('provisioning_vif_port_id') or
|
||||||
p_obj.extra.get('vif_port_id'))
|
p_obj.extra.get('vif_port_id'))
|
||||||
if not vif:
|
if not vif:
|
||||||
obj_name = 'portgroup'
|
obj_name = 'portgroup'
|
||||||
|
@ -518,8 +518,12 @@ def get_single_nic_with_vif_port_id(task):
|
|||||||
:returns: MAC address of the port connected to deployment network.
|
:returns: MAC address of the port connected to deployment network.
|
||||||
None if it cannot find any port with vif id.
|
None if it cannot find any port with vif id.
|
||||||
"""
|
"""
|
||||||
|
# NOTE(vdrok): We are booting the node only in one network at a time,
|
||||||
|
# and presence of cleaning_vif_port_id means we're doing cleaning, of
|
||||||
|
# provisioning_vif_port_id - provisioning. Otherwise it's a tenant network
|
||||||
for port in task.ports:
|
for port in task.ports:
|
||||||
if (port.internal_info.get('cleaning_vif_port_id') or
|
if (port.internal_info.get('cleaning_vif_port_id') or
|
||||||
|
port.internal_info.get('provisioning_vif_port_id') or
|
||||||
port.extra.get('vif_port_id')):
|
port.extra.get('vif_port_id')):
|
||||||
return port.address
|
return port.address
|
||||||
|
|
||||||
|
212
ironic/drivers/modules/network/neutron.py
Normal file
212
ironic/drivers/modules/network/neutron.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
# Copyright 2015 Rackspace, 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.
|
||||||
|
|
||||||
|
|
||||||
|
from neutronclient.common import exceptions as neutron_exceptions
|
||||||
|
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
|
||||||
|
from ironic import objects
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class NeutronNetwork(base.NetworkInterface):
|
||||||
|
"""Neutron v2 network interface"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
failures = []
|
||||||
|
cleaning_net = CONF.neutron.cleaning_network_uuid
|
||||||
|
if not uuidutils.is_uuid_like(cleaning_net):
|
||||||
|
failures.append('cleaning_network_uuid=%s' % cleaning_net)
|
||||||
|
|
||||||
|
provisioning_net = CONF.neutron.provisioning_network_uuid
|
||||||
|
if not uuidutils.is_uuid_like(provisioning_net):
|
||||||
|
failures.append('provisioning_network_uuid=%s' % provisioning_net)
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
raise exception.DriverLoadError(
|
||||||
|
driver=self.__class__.__name__,
|
||||||
|
reason=(_('The following [neutron] group configuration '
|
||||||
|
'options are incorrect, they must be valid UUIDs: '
|
||||||
|
'%s') % ', '.join(failures)))
|
||||||
|
|
||||||
|
def add_provisioning_network(self, task):
|
||||||
|
"""Add the provisioning network to a node.
|
||||||
|
|
||||||
|
:param task: A TaskManager instance.
|
||||||
|
:raises: NetworkError
|
||||||
|
"""
|
||||||
|
LOG.info(_LI('Adding provisioning network to node %s'),
|
||||||
|
task.node.uuid)
|
||||||
|
vifs = neutron.add_ports_to_network(
|
||||||
|
task, CONF.neutron.provisioning_network_uuid)
|
||||||
|
for port in task.ports:
|
||||||
|
if port.uuid in vifs:
|
||||||
|
internal_info = port.internal_info
|
||||||
|
internal_info['provisioning_vif_port_id'] = vifs[port.uuid]
|
||||||
|
port.internal_info = internal_info
|
||||||
|
port.save()
|
||||||
|
|
||||||
|
def remove_provisioning_network(self, task):
|
||||||
|
"""Remove the provisioning network from a node.
|
||||||
|
|
||||||
|
:param task: A TaskManager instance.
|
||||||
|
:raises: NetworkError
|
||||||
|
"""
|
||||||
|
LOG.info(_LI('Removing provisioning network from node %s'),
|
||||||
|
task.node.uuid)
|
||||||
|
neutron.remove_ports_from_network(
|
||||||
|
task, CONF.neutron.provisioning_network_uuid)
|
||||||
|
for port in task.ports:
|
||||||
|
if 'provisioning_vif_port_id' in port.internal_info:
|
||||||
|
internal_info = port.internal_info
|
||||||
|
del internal_info['provisioning_vif_port_id']
|
||||||
|
port.internal_info = internal_info
|
||||||
|
port.save()
|
||||||
|
|
||||||
|
def add_cleaning_network(self, task):
|
||||||
|
"""Create neutron ports for each port on task.node to boot the ramdisk.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance.
|
||||||
|
:raises: NetworkError
|
||||||
|
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
|
||||||
|
"""
|
||||||
|
# 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)
|
||||||
|
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):
|
||||||
|
"""Deletes the neutron port created for booting the ramdisk.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance.
|
||||||
|
:raises: NetworkError
|
||||||
|
"""
|
||||||
|
LOG.info(_LI('Removing cleaning network from 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()
|
||||||
|
|
||||||
|
def configure_tenant_networks(self, task):
|
||||||
|
"""Configure tenant networks for a node.
|
||||||
|
|
||||||
|
:param task: A TaskManager instance.
|
||||||
|
:raises: NetworkError
|
||||||
|
"""
|
||||||
|
node = task.node
|
||||||
|
ports = task.ports
|
||||||
|
LOG.info(_LI('Mapping instance ports to %s'), node.uuid)
|
||||||
|
|
||||||
|
# TODO(russell_h): this is based on the broken assumption that the
|
||||||
|
# number of Neutron ports will match the number of physical ports.
|
||||||
|
# Instead, we should probably list ports for this instance in
|
||||||
|
# Neutron and update all of those with the appropriate portmap.
|
||||||
|
if not ports:
|
||||||
|
msg = _("No ports are associated with node %s") % node.uuid
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.NetworkError(msg)
|
||||||
|
ports = [p for p in ports if not p.portgroup_id]
|
||||||
|
portgroups = task.portgroups
|
||||||
|
|
||||||
|
portmap = neutron.get_node_portmap(task)
|
||||||
|
|
||||||
|
client = neutron.get_client(task.context.auth_token)
|
||||||
|
for port_like_obj in ports + portgroups:
|
||||||
|
vif_port_id = port_like_obj.extra.get('vif_port_id')
|
||||||
|
|
||||||
|
if not vif_port_id:
|
||||||
|
LOG.warning(
|
||||||
|
_LW('%(port_like_object)s %(pobj_uuid)s in node %(node)s '
|
||||||
|
'has no vif_port_id value in extra field.'),
|
||||||
|
{'port_like_object': port_like_obj.__class__.__name__,
|
||||||
|
'pobj_uuid': port_like_obj.uuid, 'node': node.uuid})
|
||||||
|
continue
|
||||||
|
|
||||||
|
LOG.debug('Mapping tenant port %(vif_port_id)s to node '
|
||||||
|
'%(node_id)s',
|
||||||
|
{'vif_port_id': vif_port_id, 'node_id': node.uuid})
|
||||||
|
local_link_info = []
|
||||||
|
if isinstance(port_like_obj, objects.Portgroup):
|
||||||
|
pg_ports = [p for p in task.ports
|
||||||
|
if p.portgroup_id == port_like_obj.id]
|
||||||
|
for port in pg_ports:
|
||||||
|
local_link_info.append(portmap[port.uuid])
|
||||||
|
else:
|
||||||
|
# We iterate only on ports or portgroups, no need to check
|
||||||
|
# that it is a port
|
||||||
|
local_link_info.append(portmap[port_like_obj.uuid])
|
||||||
|
body = {
|
||||||
|
'port': {
|
||||||
|
'device_owner': 'baremetal:none',
|
||||||
|
'device_id': node.instance_uuid or node.uuid,
|
||||||
|
'admin_state_up': True,
|
||||||
|
'binding:vnic_type': 'baremetal',
|
||||||
|
'binding:host_id': node.uuid,
|
||||||
|
'binding:profile': {
|
||||||
|
'local_link_information': local_link_info,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.update_port(vif_port_id, body)
|
||||||
|
except neutron_exceptions.ConnectionFailed as e:
|
||||||
|
msg = (_('Could not add public network VIF %(vif)s '
|
||||||
|
'to node %(node)s, possible network issue. %(exc)s') %
|
||||||
|
{'vif': vif_port_id,
|
||||||
|
'node': node.uuid,
|
||||||
|
'exc': e})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.NetworkError(msg)
|
||||||
|
|
||||||
|
def unconfigure_tenant_networks(self, task):
|
||||||
|
"""Unconfigure tenant networks for a node.
|
||||||
|
|
||||||
|
Even though nova takes care of port removal from tenant network, we
|
||||||
|
remove it here/now to avoid the possibility of the ironic port being
|
||||||
|
bound to the tenant and cleaning networks at the same time.
|
||||||
|
|
||||||
|
:param task: A TaskManager instance.
|
||||||
|
:raises: NetworkError
|
||||||
|
"""
|
||||||
|
node = task.node
|
||||||
|
LOG.info(_LI('Unmapping instance ports from node %s'), node.uuid)
|
||||||
|
params = {'device_id': node.instance_uuid or node.uuid}
|
||||||
|
|
||||||
|
neutron.remove_neutron_ports(task, params)
|
@ -119,7 +119,9 @@ class TestCase(testtools.TestCase):
|
|||||||
tempdir=tempfile.tempdir)
|
tempdir=tempfile.tempdir)
|
||||||
self.config(cleaning_network_uuid=uuidutils.generate_uuid(),
|
self.config(cleaning_network_uuid=uuidutils.generate_uuid(),
|
||||||
group='neutron')
|
group='neutron')
|
||||||
self.config(enabled_network_interfaces=['flat', 'noop'])
|
self.config(provisioning_network_uuid=uuidutils.generate_uuid(),
|
||||||
|
group='neutron')
|
||||||
|
self.config(enabled_network_interfaces=['flat', 'noop', 'neutron'])
|
||||||
self.set_defaults(host='fake-mini',
|
self.set_defaults(host='fake-mini',
|
||||||
debug=True)
|
debug=True)
|
||||||
self.set_defaults(connection="sqlite://",
|
self.set_defaults(connection="sqlite://",
|
||||||
|
@ -110,7 +110,7 @@ class NetworkInterfaceFactoryTestCase(db_base.DbTestCase):
|
|||||||
self.assertEqual(extension_mgr['flat'].obj, task.driver.network)
|
self.assertEqual(extension_mgr['flat'].obj, task.driver.network)
|
||||||
self.assertEqual('ironic.hardware.interfaces.network',
|
self.assertEqual('ironic.hardware.interfaces.network',
|
||||||
factory._entrypoint_name)
|
factory._entrypoint_name)
|
||||||
self.assertEqual(['flat', 'noop'],
|
self.assertEqual(['flat', 'neutron', 'noop'],
|
||||||
sorted(factory._enabled_driver_list))
|
sorted(factory._enabled_driver_list))
|
||||||
|
|
||||||
def test_build_driver_for_task_default_is_none(self):
|
def test_build_driver_for_task_default_is_none(self):
|
||||||
|
@ -95,15 +95,21 @@ class TestNetwork(db_base.DbTestCase):
|
|||||||
result = network.get_node_vif_ids(task)
|
result = network.get_node_vif_ids(task)
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
def test_get_node_vif_ids_during_cleaning(self):
|
def _test_get_node_vif_ids_multitenancy(self, int_info_key):
|
||||||
port = db_utils.create_test_port(
|
port = db_utils.create_test_port(
|
||||||
node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
||||||
internal_info={'cleaning_vif_port_id': 'test-vif-A'})
|
internal_info={int_info_key: 'test-vif-A'})
|
||||||
portgroup = db_utils.create_test_portgroup(
|
portgroup = db_utils.create_test_portgroup(
|
||||||
node_id=self.node.id, address='dd:ee:ff:aa:bb:cc',
|
node_id=self.node.id, address='dd:ee:ff:aa:bb:cc',
|
||||||
internal_info={'cleaning_vif_port_id': 'test-vif-B'})
|
internal_info={int_info_key: 'test-vif-B'})
|
||||||
expected = {'portgroups': {portgroup.uuid: 'test-vif-B'},
|
expected = {'ports': {port.uuid: 'test-vif-A'},
|
||||||
'ports': {port.uuid: 'test-vif-A'}}
|
'portgroups': {portgroup.uuid: 'test-vif-B'}}
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
result = network.get_node_vif_ids(task)
|
result = network.get_node_vif_ids(task)
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_get_node_vif_ids_during_cleaning(self):
|
||||||
|
self._test_get_node_vif_ids_multitenancy('cleaning_vif_port_id')
|
||||||
|
|
||||||
|
def test_get_node_vif_ids_during_provisioning(self):
|
||||||
|
self._test_get_node_vif_ids_multitenancy('provisioning_vif_port_id')
|
||||||
|
@ -322,31 +322,21 @@ class TestNeutron(db_base.DbTestCase):
|
|||||||
fake_client.show_port.assert_called_once_with(port_id)
|
fake_client.show_port.assert_called_once_with(port_id)
|
||||||
|
|
||||||
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
|
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
|
||||||
def test__get_port_ip_address(self, mock_gfia):
|
def _test__get_port_ip_address(self, mock_gfia, network):
|
||||||
expected = "192.168.1.3"
|
|
||||||
port = object_utils.create_test_port(self.context,
|
|
||||||
node_id=self.node.id,
|
|
||||||
address='aa:bb:cc:dd:ee:ff',
|
|
||||||
uuid=uuidutils.generate_uuid(),
|
|
||||||
extra={'vif_port_id':
|
|
||||||
'test-vif-A'},
|
|
||||||
driver='fake')
|
|
||||||
mock_gfia.return_value = expected
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
api = dhcp_factory.DHCPFactory().provider
|
|
||||||
result = api._get_port_ip_address(task, port,
|
|
||||||
mock.sentinel.client)
|
|
||||||
self.assertEqual(expected, result)
|
|
||||||
mock_gfia.assert_called_once_with('test-vif-A', mock.sentinel.client)
|
|
||||||
|
|
||||||
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
|
|
||||||
def test__get_port_ip_address_cleaning(self, mock_gfia):
|
|
||||||
expected = "192.168.1.3"
|
expected = "192.168.1.3"
|
||||||
|
fake_vif = 'test-vif-%s' % network
|
||||||
port = object_utils.create_test_port(
|
port = object_utils.create_test_port(
|
||||||
self.context, node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
self.context, node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
internal_info={'cleaning_vif_port_id': 'test-vif-A'})
|
extra={'vif_port_id': fake_vif} if network == 'tenant' else {},
|
||||||
|
internal_info={
|
||||||
|
'cleaning_vif_port_id': (fake_vif if network == 'cleaning'
|
||||||
|
else None),
|
||||||
|
'provisioning_vif_port_id': (fake_vif
|
||||||
|
if network == 'provisioning'
|
||||||
|
else None),
|
||||||
|
}
|
||||||
|
)
|
||||||
mock_gfia.return_value = expected
|
mock_gfia.return_value = expected
|
||||||
with task_manager.acquire(self.context,
|
with task_manager.acquire(self.context,
|
||||||
self.node.uuid) as task:
|
self.node.uuid) as task:
|
||||||
@ -354,7 +344,16 @@ class TestNeutron(db_base.DbTestCase):
|
|||||||
result = api._get_port_ip_address(task, port,
|
result = api._get_port_ip_address(task, port,
|
||||||
mock.sentinel.client)
|
mock.sentinel.client)
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
mock_gfia.assert_called_once_with('test-vif-A', mock.sentinel.client)
|
mock_gfia.assert_called_once_with(fake_vif, mock.sentinel.client)
|
||||||
|
|
||||||
|
def test__get_port_ip_address_tenant(self):
|
||||||
|
self._test__get_port_ip_address(network='tenant')
|
||||||
|
|
||||||
|
def test__get_port_ip_address_cleaning(self):
|
||||||
|
self._test__get_port_ip_address(network='cleaning')
|
||||||
|
|
||||||
|
def test__get_port_ip_address_provisioning(self):
|
||||||
|
self._test__get_port_ip_address(network='provisioning')
|
||||||
|
|
||||||
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
|
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
|
||||||
def test__get_port_ip_address_for_portgroup(self, mock_gfia):
|
def test__get_port_ip_address_for_portgroup(self, mock_gfia):
|
||||||
|
231
ironic/tests/unit/drivers/modules/network/test_neutron.py
Normal file
231
ironic/tests/unit/drivers/modules/network/test_neutron.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# 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 copy
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from neutronclient.common import exceptions as neutron_exceptions
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common import neutron as neutron_common
|
||||||
|
from ironic.conductor import task_manager
|
||||||
|
from ironic.drivers.modules.network import neutron
|
||||||
|
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 NeutronInterfaceTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(NeutronInterfaceTestCase, self).setUp()
|
||||||
|
self.config(enabled_drivers=['fake'])
|
||||||
|
mgr_utils.mock_the_extension_manager()
|
||||||
|
self.interface = neutron.NeutronNetwork()
|
||||||
|
self.node = utils.create_test_node(self.context,
|
||||||
|
network_interface='neutron')
|
||||||
|
self.port = utils.create_test_port(
|
||||||
|
self.context, node_id=self.node.id,
|
||||||
|
address='52:54:00:cf:2d:32',
|
||||||
|
extra={'vif_port_id': uuidutils.generate_uuid()})
|
||||||
|
self.neutron_port = {'id': '132f871f-eaec-4fed-9475-0d54465e0f00',
|
||||||
|
'mac_address': '52:54:00:cf:2d:32'}
|
||||||
|
|
||||||
|
def test_init_incorrect_provisioning_net(self):
|
||||||
|
self.config(provisioning_network_uuid=None, group='neutron')
|
||||||
|
self.assertRaises(exception.DriverLoadError, neutron.NeutronNetwork)
|
||||||
|
self.config(provisioning_network_uuid=uuidutils.generate_uuid(),
|
||||||
|
group='neutron')
|
||||||
|
self.config(cleaning_network_uuid='asdf', group='neutron')
|
||||||
|
self.assertRaises(exception.DriverLoadError, neutron.NeutronNetwork)
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'add_ports_to_network')
|
||||||
|
def test_add_provisioning_network(self, add_ports_mock):
|
||||||
|
add_ports_mock.return_value = {self.port.uuid: self.neutron_port['id']}
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.add_provisioning_network(task)
|
||||||
|
add_ports_mock.assert_called_once_with(
|
||||||
|
task, CONF.neutron.provisioning_network_uuid)
|
||||||
|
self.port.refresh()
|
||||||
|
self.assertEqual(self.neutron_port['id'],
|
||||||
|
self.port.internal_info['provisioning_vif_port_id'])
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'remove_ports_from_network')
|
||||||
|
def test_remove_provisioning_network(self, remove_ports_mock):
|
||||||
|
self.port.internal_info = {'provisioning_vif_port_id': 'vif-port-id'}
|
||||||
|
self.port.save()
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.remove_provisioning_network(task)
|
||||||
|
remove_ports_mock.assert_called_once_with(
|
||||||
|
task, CONF.neutron.provisioning_network_uuid)
|
||||||
|
self.port.refresh()
|
||||||
|
self.assertNotIn('provisioning_vif_port_id', self.port.internal_info)
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'rollback_ports')
|
||||||
|
@mock.patch.object(neutron_common, 'add_ports_to_network')
|
||||||
|
def test_add_cleaning_network(self, add_ports_mock, rollback_mock):
|
||||||
|
add_ports_mock.return_value = {self.port.uuid: self.neutron_port['id']}
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
res = self.interface.add_cleaning_network(task)
|
||||||
|
rollback_mock.assert_called_once_with(
|
||||||
|
task, CONF.neutron.cleaning_network_uuid)
|
||||||
|
self.assertEqual(res, add_ports_mock.return_value)
|
||||||
|
self.port.refresh()
|
||||||
|
self.assertEqual(self.neutron_port['id'],
|
||||||
|
self.port.internal_info['cleaning_vif_port_id'])
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'remove_ports_from_network')
|
||||||
|
def test_remove_cleaning_network(self, remove_ports_mock):
|
||||||
|
self.port.internal_info = {'cleaning_vif_port_id': 'vif-port-id'}
|
||||||
|
self.port.save()
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.remove_cleaning_network(task)
|
||||||
|
remove_ports_mock.assert_called_once_with(
|
||||||
|
task, CONF.neutron.cleaning_network_uuid)
|
||||||
|
self.port.refresh()
|
||||||
|
self.assertNotIn('cleaning_vif_port_id', self.port.internal_info)
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'remove_neutron_ports')
|
||||||
|
def test_unconfigure_tenant_networks(self, remove_ports_mock):
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.unconfigure_tenant_networks(task)
|
||||||
|
remove_ports_mock.assert_called_once_with(
|
||||||
|
task, {'device_id': task.node.uuid})
|
||||||
|
|
||||||
|
def test_configure_tenant_networks_no_ports_for_node(self):
|
||||||
|
n = utils.create_test_node(self.context, network_interface='neutron',
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
with task_manager.acquire(self.context, n.id) as task:
|
||||||
|
self.assertRaisesRegexp(
|
||||||
|
exception.NetworkError, 'No ports are associated',
|
||||||
|
self.interface.configure_tenant_networks, task)
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'get_client')
|
||||||
|
@mock.patch.object(neutron, 'LOG')
|
||||||
|
def test_configure_tenant_networks_no_vif_id(self, log_mock, client_mock):
|
||||||
|
self.port.extra = {}
|
||||||
|
self.port.save()
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.configure_tenant_networks(task)
|
||||||
|
client_mock.assert_called_once_with(task.context.auth_token)
|
||||||
|
self.assertIn('no vif_port_id value in extra',
|
||||||
|
log_mock.warning.call_args[0][0])
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'get_client')
|
||||||
|
def test_configure_tenant_networks_update_fail(self, client_mock):
|
||||||
|
client = client_mock.return_value
|
||||||
|
client.update_port.side_effect = neutron_exceptions.ConnectionFailed(
|
||||||
|
reason='meow')
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.assertRaisesRegexp(
|
||||||
|
exception.NetworkError, 'Could not add',
|
||||||
|
self.interface.configure_tenant_networks, task)
|
||||||
|
client_mock.assert_called_once_with(task.context.auth_token)
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'get_client')
|
||||||
|
def _test_configure_tenant_networks(self, client_mock):
|
||||||
|
upd_mock = mock.Mock()
|
||||||
|
client_mock.return_value.update_port = upd_mock
|
||||||
|
second_port = utils.create_test_port(
|
||||||
|
self.context, node_id=self.node.id, address='52:54:00:cf:2d:33',
|
||||||
|
extra={'vif_port_id': uuidutils.generate_uuid()},
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
local_link_connection={'switch_id': '0a:1b:2c:3d:4e:ff',
|
||||||
|
'port_id': 'Ethernet1/1',
|
||||||
|
'switch_info': 'switch2'}
|
||||||
|
)
|
||||||
|
expected_body = {
|
||||||
|
'port': {
|
||||||
|
'device_owner': 'baremetal:none',
|
||||||
|
'device_id': self.node.instance_uuid or self.node.uuid,
|
||||||
|
'admin_state_up': True,
|
||||||
|
'binding:vnic_type': 'baremetal',
|
||||||
|
'binding:host_id': self.node.uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port1_body = copy.deepcopy(expected_body)
|
||||||
|
port1_body['port']['binding:profile'] = {
|
||||||
|
'local_link_information': [self.port.local_link_connection]
|
||||||
|
}
|
||||||
|
port2_body = copy.deepcopy(expected_body)
|
||||||
|
port2_body['port']['binding:profile'] = {
|
||||||
|
'local_link_information': [second_port.local_link_connection]
|
||||||
|
}
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.configure_tenant_networks(task)
|
||||||
|
client_mock.assert_called_once_with(task.context.auth_token)
|
||||||
|
upd_mock.assert_has_calls(
|
||||||
|
[mock.call(self.port.extra['vif_port_id'], port1_body),
|
||||||
|
mock.call(second_port.extra['vif_port_id'], port2_body)],
|
||||||
|
any_order=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_configure_tenant_networks(self):
|
||||||
|
self.node.instance_uuid = uuidutils.generate_uuid()
|
||||||
|
self.node.save()
|
||||||
|
self._test_configure_tenant_networks()
|
||||||
|
|
||||||
|
def test_configure_tenant_networks_no_instance_uuid(self):
|
||||||
|
self._test_configure_tenant_networks()
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'get_client')
|
||||||
|
def test_configure_tenant_networks_with_portgroups(self, client_mock):
|
||||||
|
pg = utils.create_test_portgroup(
|
||||||
|
self.context, node_id=self.node.id, address='ff:54:00:cf:2d:32',
|
||||||
|
extra={'vif_port_id': uuidutils.generate_uuid()})
|
||||||
|
port1 = utils.create_test_port(
|
||||||
|
self.context, node_id=self.node.id, address='ff:54:00:cf:2d:33',
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
portgroup_id=pg.id,
|
||||||
|
local_link_connection={'switch_id': '0a:1b:2c:3d:4e:ff',
|
||||||
|
'port_id': 'Ethernet1/1',
|
||||||
|
'switch_info': 'switch2'}
|
||||||
|
)
|
||||||
|
port2 = utils.create_test_port(
|
||||||
|
self.context, node_id=self.node.id, address='ff:54:00:cf:2d:34',
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
portgroup_id=pg.id,
|
||||||
|
local_link_connection={'switch_id': '0a:1b:2c:3d:4e:ff',
|
||||||
|
'port_id': 'Ethernet1/2',
|
||||||
|
'switch_info': 'switch2'}
|
||||||
|
)
|
||||||
|
upd_mock = mock.Mock()
|
||||||
|
client_mock.return_value.update_port = upd_mock
|
||||||
|
expected_body = {
|
||||||
|
'port': {
|
||||||
|
'device_owner': 'baremetal:none',
|
||||||
|
'device_id': self.node.uuid,
|
||||||
|
'admin_state_up': True,
|
||||||
|
'binding:vnic_type': 'baremetal',
|
||||||
|
'binding:host_id': self.node.uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call1_body = copy.deepcopy(expected_body)
|
||||||
|
call1_body['port']['binding:profile'] = {
|
||||||
|
'local_link_information': [self.port.local_link_connection]
|
||||||
|
}
|
||||||
|
call2_body = copy.deepcopy(expected_body)
|
||||||
|
call2_body['port']['binding:profile'] = {
|
||||||
|
'local_link_information': [port1.local_link_connection,
|
||||||
|
port2.local_link_connection]
|
||||||
|
}
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.configure_tenant_networks(task)
|
||||||
|
client_mock.assert_called_once_with(task.context.auth_token)
|
||||||
|
upd_mock.assert_has_calls(
|
||||||
|
[mock.call(self.port.extra['vif_port_id'], call1_body),
|
||||||
|
mock.call(pg.extra['vif_port_id'], call2_body)]
|
||||||
|
)
|
@ -1406,6 +1406,17 @@ class VirtualMediaDeployUtilsTestCase(db_base.DbTestCase):
|
|||||||
address = utils.get_single_nic_with_vif_port_id(task)
|
address = utils.get_single_nic_with_vif_port_id(task)
|
||||||
self.assertEqual('aa:bb:cc:dd:ee:ff', address)
|
self.assertEqual('aa:bb:cc:dd:ee:ff', address)
|
||||||
|
|
||||||
|
def test_get_single_nic_with_provisioning_vif_port_id(self):
|
||||||
|
obj_utils.create_test_port(
|
||||||
|
self.context, node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
internal_info={'provisioning_vif_port_id': 'test-vif-A'},
|
||||||
|
driver='iscsi_ilo')
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=False) as task:
|
||||||
|
address = utils.get_single_nic_with_vif_port_id(task)
|
||||||
|
self.assertEqual('aa:bb:cc:dd:ee:ff', address)
|
||||||
|
|
||||||
|
|
||||||
class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
|
class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
|
||||||
|
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added ``neutron`` network interface. This interface allows to provision
|
||||||
|
and/or clean node in separate networks. A new config option
|
||||||
|
``[neutron]provisioning_network_uuid`` has been added. This option
|
||||||
|
specifies provision network UUID.
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
If ``neutron`` network interface is specified in
|
||||||
|
``[DEFAULT]enabled_network_interfaces``,
|
||||||
|
``[neutron]provisioning_network_uuid`` and
|
||||||
|
``[neutron]cleaning_network_uuid`` configuration options are required. If
|
||||||
|
any of them is not specified, the ironic-conductor service will fail to
|
||||||
|
start.
|
@ -90,6 +90,7 @@ ironic.drivers =
|
|||||||
ironic.hardware.interfaces.network =
|
ironic.hardware.interfaces.network =
|
||||||
flat = ironic.drivers.modules.network.flat:FlatNetwork
|
flat = ironic.drivers.modules.network.flat:FlatNetwork
|
||||||
noop = ironic.drivers.modules.network.noop:NoopNetwork
|
noop = ironic.drivers.modules.network.noop:NoopNetwork
|
||||||
|
neutron = ironic.drivers.modules.network.neutron:NeutronNetwork
|
||||||
|
|
||||||
ironic.database.migration_backend =
|
ironic.database.migration_backend =
|
||||||
sqlalchemy = ironic.db.sqlalchemy.migration
|
sqlalchemy = ironic.db.sqlalchemy.migration
|
||||||
|
Loading…
Reference in New Issue
Block a user