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:
Vasyl Saienko 2016-05-17 14:00:00 +03:00 committed by Vladyslav Drok
parent cde11611d9
commit ab97fa0f1f
14 changed files with 536 additions and 41 deletions

View File

@ -1518,12 +1518,16 @@
#auth_strategy = keystone
# 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 nodes. Required for "neutron" network interface. It
# is also required if cleaning nodes when using "flat" network
# interface or "neutron" DHCP provider. (string value)
#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]

View File

@ -32,15 +32,19 @@ def get_node_vif_ids(task):
portgroup_vifs = {}
port_vifs = {}
for portgroup in task.portgroups:
# NOTE(vdrok): This works because cleaning_vif_port_id doesn't exist
# when we're in deployment/tenant network
# 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
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'))
if vif:
portgroup_vifs[portgroup.uuid] = vif
vifs['portgroups'] = portgroup_vifs
for port in task.ports:
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'))
if vif:
port_vifs[port.uuid] = vif

View File

@ -51,10 +51,14 @@ neutron_opts = [
'should only be used for testing.')),
cfg.StrOpt('cleaning_network_uuid',
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.'))
'into for cleaning nodes. Required for "neutron" '
'network interface. It is also required if cleaning '
'nodes when using "flat" network interface or "neutron" '
'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')

View File

@ -208,9 +208,12 @@ class NeutronDHCPApi(base.BaseDHCP):
:raises: InvalidIPv4Address
"""
# NOTE(vdrok): This works because cleaning_vif_port_id doesn't exist
# when we're in deployment/tenant network
# 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
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'))
if not vif:
obj_name = 'portgroup'

View File

@ -518,8 +518,12 @@ def get_single_nic_with_vif_port_id(task):
:returns: MAC address of the port connected to deployment network.
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:
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')):
return port.address

View 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)

View File

@ -119,7 +119,9 @@ class TestCase(testtools.TestCase):
tempdir=tempfile.tempdir)
self.config(cleaning_network_uuid=uuidutils.generate_uuid(),
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',
debug=True)
self.set_defaults(connection="sqlite://",

View File

@ -110,7 +110,7 @@ class NetworkInterfaceFactoryTestCase(db_base.DbTestCase):
self.assertEqual(extension_mgr['flat'].obj, task.driver.network)
self.assertEqual('ironic.hardware.interfaces.network',
factory._entrypoint_name)
self.assertEqual(['flat', 'noop'],
self.assertEqual(['flat', 'neutron', 'noop'],
sorted(factory._enabled_driver_list))
def test_build_driver_for_task_default_is_none(self):

View File

@ -95,15 +95,21 @@ class TestNetwork(db_base.DbTestCase):
result = network.get_node_vif_ids(task)
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(
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(
node_id=self.node.id, address='dd:ee:ff:aa:bb:cc',
internal_info={'cleaning_vif_port_id': 'test-vif-B'})
expected = {'portgroups': {portgroup.uuid: 'test-vif-B'},
'ports': {port.uuid: 'test-vif-A'}}
internal_info={int_info_key: 'test-vif-B'})
expected = {'ports': {port.uuid: 'test-vif-A'},
'portgroups': {portgroup.uuid: 'test-vif-B'}}
with task_manager.acquire(self.context, self.node.uuid) as task:
result = network.get_node_vif_ids(task)
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')

View File

@ -322,31 +322,21 @@ class TestNeutron(db_base.DbTestCase):
fake_client.show_port.assert_called_once_with(port_id)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
def test__get_port_ip_address(self, mock_gfia):
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):
def _test__get_port_ip_address(self, mock_gfia, network):
expected = "192.168.1.3"
fake_vif = 'test-vif-%s' % network
port = object_utils.create_test_port(
self.context, node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
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
with task_manager.acquire(self.context,
self.node.uuid) as task:
@ -354,7 +344,16 @@ class TestNeutron(db_base.DbTestCase):
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_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')
def test__get_port_ip_address_for_portgroup(self, mock_gfia):

View 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)]
)

View File

@ -1406,6 +1406,17 @@ class VirtualMediaDeployUtilsTestCase(db_base.DbTestCase):
address = utils.get_single_nic_with_vif_port_id(task)
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):

View File

@ -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.

View File

@ -90,6 +90,7 @@ ironic.drivers =
ironic.hardware.interfaces.network =
flat = ironic.drivers.modules.network.flat:FlatNetwork
noop = ironic.drivers.modules.network.noop:NoopNetwork
neutron = ironic.drivers.modules.network.neutron:NeutronNetwork
ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration