Boot an instance with multiple vnics on same network

If the same L2 network is requested multiple times for the same
instance then creating ports on same network and attaching them
to the same instance raises a DuplicateNetworks exception.
Similarly, attaching multiple existent ports on same L2 network
to the same instance raises a DuplicateNetworks exception. This
is the default behavior that is defaulted by a newly introduced
nova flag "allow_duplicate_networks" which is set to
False by default.

Not raising a DuplicateNetwork exception and allowing an instance
to have multiple vnics on same network is useful for NfV service
instances and in that case this newly introduced nova flag should
be set to True.

DocImpact: New neutron.allow_duplicate_networks configuration option
Implements blueprint multiple-if-1-net
Change-Id: Id4d633162c785c9b56b9c8426c0445770bc1352e
Closes-Bug: #1187244
This commit is contained in:
Racha Ben Ali 2014-01-15 00:42:31 -08:00
parent 374418085d
commit 322cc9336f
3 changed files with 256 additions and 65 deletions

View File

@ -104,6 +104,10 @@ neutron_opts = [
'neutron client requests.',
deprecated_group='DEFAULT',
deprecated_name='neutron_ca_certificates_file'),
cfg.BoolOpt('allow_duplicate_networks',
default=False,
help='Allow an instance to have multiple vNICs attached to '
'the same Neutron network.'),
]
CONF = cfg.CONF
@ -258,8 +262,8 @@ class API(base_api.NetworkAPI):
requested_networks = kwargs.get('requested_networks')
dhcp_opts = kwargs.get('dhcp_options', None)
ports = {}
fixed_ips = {}
net_ids = []
ordered_networks = []
if requested_networks:
for network_id, fixed_ip, port_id in requested_networks:
if port_id:
@ -277,19 +281,30 @@ class API(base_api.NetworkAPI):
# discard rather than popping.
available_macs.discard(port['mac_address'])
network_id = port['network_id']
ports[network_id] = port
elif fixed_ip and network_id:
fixed_ips[network_id] = fixed_ip
ports[port_id] = port
if network_id:
net_ids.append(network_id)
ordered_networks.append((network_id, fixed_ip, port_id))
nets = self._get_available_networks(context, instance['project_id'],
net_ids)
if not nets:
LOG.warn(_("No network configured!"), instance=instance)
return network_model.NetworkInfo([])
# if this function is directly called without a requested_network param
# or if it is indirectly called through allocate_port_for_instance()
# with None params=(network_id=None, requested_ip=None, port_id=None):
if (not requested_networks
or requested_networks == [(None, None, None)]):
# bug/1267723 - if no network is requested and more
# than one is available then raise NetworkAmbiguous Exception
if len(nets) > 1:
msg = _("Multiple possible networks found, use a Network "
"ID to be more specific.")
raise exception.NetworkAmbiguous(msg)
ordered_networks.append((nets[0]['id'], None, None))
security_groups = kwargs.get('security_groups', [])
security_group_ids = []
@ -328,7 +343,20 @@ class API(base_api.NetworkAPI):
touched_port_ids = []
created_port_ids = []
ports_in_requested_order = []
for network in nets:
nets_in_requested_order = []
for network_id, fixed_ip, port_id in ordered_networks:
# Network lookup for available network_id
network = None
for net in nets:
if net['id'] == network_id:
network = net
break
# if network_id did not pass validate_networks() and not available
# here then skip it safely not continuing with a None Network
else:
continue
nets_in_requested_order.append(network)
# If security groups are requested on an instance then the
# network must has a subnet associated with it. Some plugins
# implement the port-security extension which requires
@ -345,21 +373,21 @@ class API(base_api.NetworkAPI):
port_req_body = {'port': {'device_id': instance['uuid'],
'device_owner': zone}}
try:
port = ports.get(network_id)
self._populate_neutron_extension_values(context, instance,
port_req_body)
# Requires admin creds to set port bindings
port_client = (neutron if not
self._has_port_binding_extension(context) else
neutronv2.get_client(context, admin=True))
if port:
if port_id:
port = ports[port_id]
port_client.update_port(port['id'], port_req_body)
touched_port_ids.append(port['id'])
ports_in_requested_order.append(port['id'])
else:
created_port = self._create_port(
port_client, instance, network_id,
port_req_body, fixed_ips.get(network_id),
port_req_body, fixed_ip,
security_group_ids, available_macs, dhcp_opts)
created_port_ids.append(created_port)
ports_in_requested_order.append(created_port)
@ -387,7 +415,8 @@ class API(base_api.NetworkAPI):
msg = _("Failed to delete port %s")
LOG.exception(msg, port_id)
nw_info = self.get_instance_nw_info(context, instance, networks=nets,
nw_info = self.get_instance_nw_info(context, instance,
networks=nets_in_requested_order,
port_ids=ports_in_requested_order)
# NOTE(danms): Only return info about ports we created in this run.
# In the initial allocation case, this will be everything we created,
@ -690,8 +719,9 @@ class API(base_api.NetworkAPI):
address=fixed_ip,
instance_uuid=i_uuid)
if net_id in instance_on_net_ids:
raise exception.NetworkDuplicated(network_id=net_id)
if (not CONF.neutron.allow_duplicate_networks and
net_id in instance_on_net_ids):
raise exception.NetworkDuplicated(network_id=net_id)
instance_on_net_ids.append(net_id)
# Now check to see if all requested networks exist
@ -709,10 +739,11 @@ class API(base_api.NetworkAPI):
requested_netid_set = set(net_ids_requested)
returned_netid_set = set([net['id'] for net in nets])
lostid_set = requested_netid_set - returned_netid_set
id_str = ''
for _id in lostid_set:
id_str = id_str and id_str + ', ' + _id or _id
raise exception.NetworkNotFound(network_id=id_str)
if lostid_set:
id_str = ''
for _id in lostid_set:
id_str = id_str and id_str + ', ' + _id or _id
raise exception.NetworkNotFound(network_id=id_str)
# Note(PhilD): Ideally Nova would create all required ports as part of
# network validation, but port creation requires some details

View File

@ -814,8 +814,14 @@ class MockClient(object):
return {'security_groups': ret}
def list_networks(self, **_params):
return {'networks':
[network for network in self._fake_networks.values()]}
# neutronv2/api.py _get_available_networks calls this assuming
# search_opts filter "shared" is implemented and not ignored
shared = _params.get("shared", None)
if shared:
return {'networks': []}
else:
return {'networks':
[network for network in self._fake_networks.values()]}
def list_ports(self, **_params):
ret = []

View File

@ -228,8 +228,19 @@ class TestNeutronv2Base(test.TestCase):
'name': 'out-of-this-world',
'router:external': True,
'tenant_id': 'should-be-an-admin'}]
# A network request with a duplicate
self.nets6 = []
self.nets6.append(self.nets1[0])
self.nets6.append(self.nets1[0])
# A network request with a combo
self.nets7 = []
self.nets7.append(self.nets2[1])
self.nets7.append(self.nets1[0])
self.nets7.append(self.nets2[1])
self.nets7.append(self.nets1[0])
self.nets = [self.nets1, self.nets2, self.nets3,
self.nets4, self.nets5]
self.nets4, self.nets5, self.nets6, self.nets7]
self.port_address = '10.0.1.2'
self.port_data1 = [{'network_id': 'my_netid1',
@ -357,30 +368,49 @@ class TestNeutronv2Base(test.TestCase):
if macs:
macs = set(macs)
req_net_ids = []
ordered_networks = []
port = {}
if 'requested_networks' in kwargs:
for id, fixed_ip, port_id in kwargs['requested_networks']:
for n_id, fixed_ip, port_id in kwargs['requested_networks']:
if port_id:
self.moxed_client.show_port(port_id).AndReturn(
{'port': {'id': 'my_portid1',
'network_id': 'my_netid1',
'mac_address': 'my_mac1',
'device_id': kwargs.get('_device') and
self.instance2['uuid'] or ''}})
ports['my_netid1'] = self.port_data1[0]
id = 'my_netid1'
if macs is not None:
macs.discard('my_mac1')
if port_id == 'my_portid3':
self.moxed_client.show_port(port_id).AndReturn(
{'port': {'id': 'my_portid3',
'network_id': 'my_netid1',
'mac_address': 'my_mac1',
'device_id': kwargs.get('_device') and
self.instance2['uuid'] or
''}})
ports['my_netid1'] = [self.port_data1[0],
self.port_data3[0]]
ports[port_id] = self.port_data3[0]
n_id = 'my_netid1'
if macs is not None:
macs.discard('my_mac1')
else:
self.moxed_client.show_port(port_id).AndReturn(
{'port': {'id': 'my_portid1',
'network_id': 'my_netid1',
'mac_address': 'my_mac1',
'device_id': kwargs.get('_device') and
self.instance2['uuid'] or
''}})
ports[port_id] = self.port_data1[0]
n_id = 'my_netid1'
if macs is not None:
macs.discard('my_mac1')
else:
fixed_ips[id] = fixed_ip
req_net_ids.append(id)
expected_network_order = req_net_ids
fixed_ips[n_id] = fixed_ip
req_net_ids.append(n_id)
ordered_networks.append((n_id, fixed_ip, port_id))
else:
expected_network_order = [n['id'] for n in nets]
for n in nets:
ordered_networks.append((n['id'], None, None))
if kwargs.get('_break') == 'pre_list_networks':
self.mox.ReplayAll()
return api
search_ids = [net['id'] for net in nets if net['id'] in req_net_ids]
# search all req_net_ids as in api.py
search_ids = req_net_ids
if search_ids:
mox_list_params = {'id': mox.SameElementsAs(search_ids)}
@ -395,18 +425,34 @@ class TestNeutronv2Base(test.TestCase):
self.moxed_client.list_networks(
**mox_list_params).AndReturn({'networks': []})
if (('requested_networks' not in kwargs
or kwargs['requested_networks'] == [(None, None, None)])
and len(nets) > 1):
self.mox.ReplayAll()
return api
ports_in_requested_net_order = []
for net_id in expected_network_order:
nets_in_requested_net_order = []
for net_id, fixed_ip, port_id in ordered_networks:
port_req_body = {
'port': {
'device_id': self.instance['uuid'],
'device_owner': 'compute:nova',
},
}
# Network lookup for available network_id
network = None
for net in nets:
if net['id'] == net_id:
network = net
break
# if net_id did not pass validate_networks() and not available
# here then skip it safely not continuing with a None Network
else:
continue
if has_portbinding:
port_req_body['port']['binding:host_id'] = (
self.instance.get('host'))
port = ports.get(net_id, None)
if not has_portbinding:
api._populate_neutron_extension_values(mox.IgnoreArg(),
self.instance, mox.IgnoreArg()).AndReturn(None)
@ -417,8 +463,8 @@ class TestNeutronv2Base(test.TestCase):
AndReturn(has_portbinding)
api._has_port_binding_extension(mox.IgnoreArg()).\
AndReturn(has_portbinding)
if port:
port_id = port['id']
if port_id:
port = ports[port_id]
self.moxed_client.update_port(port_id,
MyComparator(port_req_body)
).AndReturn(
@ -448,9 +494,11 @@ class TestNeutronv2Base(test.TestCase):
MyComparator(port_req_body)).AndReturn(res_port)
ports_in_requested_net_order.append(res_port['port']['id'])
nets_in_requested_net_order.append(network)
api.get_instance_nw_info(mox.IgnoreArg(),
self.instance,
networks=nets,
networks=nets_in_requested_net_order,
port_ids=ports_in_requested_net_order
).AndReturn(self._returned_nw_info)
self.mox.ReplayAll()
@ -783,7 +831,10 @@ class TestNeutronv2(TestNeutronv2Base):
def test_allocate_for_instance_2(self):
# Allocate one port in two networks env.
self._allocate_for_instance(2)
api = self._stub_allocate_for_instance(net_idx=2)
self.assertRaises(exception.NetworkAmbiguous,
api.allocate_for_instance,
self.context, self.instance)
def test_allocate_for_instance_accepts_macs_kwargs_None(self):
# The macs kwarg should be accepted as None.
@ -859,6 +910,23 @@ class TestNeutronv2(TestNeutronv2Base):
self.instance, requested_networks=requested_networks,
macs=set(['unknown:mac']))
def test_allocate_for_instance_without_requested_networks(self):
api = self._stub_allocate_for_instance(net_idx=3)
self.assertRaises(exception.NetworkAmbiguous,
api.allocate_for_instance,
self.context, self.instance)
def test_allocate_for_instance_with_requested_non_available_network(self):
"""verify that a non available network is ignored.
self.nets2 (net_idx=2) is composed of self.nets3[0] and self.nets3[1]
Do not create a port on a non available network self.nets3[2].
"""
requested_networks = [
(net['id'], None, None)
for net in (self.nets3[0], self.nets3[2], self.nets3[1])]
self._allocate_for_instance(net_idx=2,
requested_networks=requested_networks)
def test_allocate_for_instance_with_requested_networks(self):
# specify only first and last network
requested_networks = [
@ -874,7 +942,7 @@ class TestNeutronv2(TestNeutronv2Base):
requested_networks=requested_networks)
def test_allocate_for_instance_with_requested_networks_with_port(self):
requested_networks = [(None, None, 'myportid1')]
requested_networks = [(None, None, 'my_portid1')]
self._allocate_for_instance(net_idx=1,
requested_networks=requested_networks)
@ -903,12 +971,11 @@ class TestNeutronv2(TestNeutronv2Base):
self.mox.StubOutWithMock(api, '_has_port_binding_extension')
api._has_port_binding_extension(mox.IgnoreArg()).MultipleTimes().\
AndReturn(False)
requested_networks = [
(net['id'], None, None)
for net in (self.nets2[0], self.nets2[1])]
self.moxed_client.list_networks(
tenant_id=self.instance['project_id'],
shared=False).AndReturn(
{'networks': self.nets2})
self.moxed_client.list_networks(shared=True).AndReturn(
{'networks': []})
id=['my_netid1', 'my_netid2']).AndReturn({'networks': self.nets2})
index = 0
for network in self.nets2:
binding_port_req_body = {
@ -941,7 +1008,8 @@ class TestNeutronv2(TestNeutronv2Base):
self.mox.ReplayAll()
self.assertRaises(exception.PortLimitExceeded,
api.allocate_for_instance,
self.context, self.instance)
self.context, self.instance,
requested_networks=requested_networks)
def test_allocate_for_instance_ex2(self):
"""verify we have no port to delete
@ -955,12 +1023,11 @@ class TestNeutronv2(TestNeutronv2Base):
self.mox.StubOutWithMock(api, '_has_port_binding_extension')
api._has_port_binding_extension(mox.IgnoreArg()).MultipleTimes().\
AndReturn(False)
requested_networks = [
(net['id'], None, None)
for net in (self.nets2[0], self.nets2[1])]
self.moxed_client.list_networks(
tenant_id=self.instance['project_id'],
shared=False).AndReturn(
{'networks': self.nets2})
self.moxed_client.list_networks(shared=True).AndReturn(
{'networks': []})
id=['my_netid1', 'my_netid2']).AndReturn({'networks': self.nets2})
binding_port_req_body = {
'port': {
'device_id': self.instance['uuid'],
@ -982,7 +1049,8 @@ class TestNeutronv2(TestNeutronv2Base):
Exception("fail to create port"))
self.mox.ReplayAll()
self.assertRaises(NEUTRON_CLIENT_EXCEPTION, api.allocate_for_instance,
self.context, self.instance)
self.context, self.instance,
requested_networks=requested_networks)
def test_allocate_for_instance_no_port_or_network(self):
class BailOutEarly(Exception):
@ -1211,9 +1279,10 @@ class TestNeutronv2(TestNeutronv2Base):
except exception.NetworkNotFound as ex:
self.assertIn("my_netid2, my_netid3", str(ex))
def test_validate_networks_duplicate(self):
def test_validate_networks_duplicate_disable(self):
"""Verify that the correct exception is thrown when duplicate
network ids are passed to validate_networks.
network ids are passed to validate_networks, when nova config flag
allow_duplicate_networks is set to its default value: False
"""
requested_networks = [('my_netid1', None, None),
('my_netid1', None, None)]
@ -1222,8 +1291,59 @@ class TestNeutronv2(TestNeutronv2Base):
neutronv2.get_client(None)
api = neutronapi.API()
self.assertRaises(exception.NetworkDuplicated,
api.validate_networks,
self.context, requested_networks, 1)
api.validate_networks,
self.context, requested_networks, 1)
def test_validate_networks_duplicate_enable(self):
"""Verify that no duplicateNetworks exception is thrown when duplicate
network ids are passed to validate_networks, when nova config flag
allow_duplicate_networks is set to its non default value: True
"""
self.flags(allow_duplicate_networks=True, group='neutron')
requested_networks = [('my_netid1', None, None),
('my_netid1', None, None)]
ids = ['my_netid1', 'my_netid1']
self.moxed_client.list_networks(
id=mox.SameElementsAs(ids)).AndReturn(
{'networks': self.nets1})
self.moxed_client.list_ports(tenant_id='my_tenantid').AndReturn(
{'ports': []})
self.moxed_client.show_quota(
tenant_id='my_tenantid').AndReturn(
{'quota': {'port': 50}})
self.mox.ReplayAll()
api = neutronapi.API()
api.validate_networks(self.context, requested_networks, 1)
def test_allocate_for_instance_with_requested_networks_duplicates(self):
# specify a duplicate network to allocate to instance
self.flags(allow_duplicate_networks=True, group='neutron')
requested_networks = [
(net['id'], None, None)
for net in (self.nets6[0], self.nets6[1])]
self._allocate_for_instance(net_idx=6,
requested_networks=requested_networks)
def test_allocate_for_instance_requested_networks_duplicates_port(self):
# specify first port and last port that are in same network
self.flags(allow_duplicate_networks=True, group='neutron')
requested_networks = [
(None, None, port['id'])
for port in (self.port_data1[0], self.port_data3[0])]
self._allocate_for_instance(net_idx=6,
requested_networks=requested_networks)
def test_allocate_for_instance_requested_networks_duplicates_combo(self):
# specify a combo net_idx=7 : net2, port in net1, net2, port in net1
self.flags(allow_duplicate_networks=True, group='neutron')
requested_networks = [
('my_netid2', None, None),
(None, None, self.port_data1[0]['id']),
('my_netid2', None, None),
(None, None, self.port_data3[0]['id'])]
self._allocate_for_instance(net_idx=7,
requested_networks=requested_networks)
def test_validate_networks_not_specified(self):
requested_networks = []
@ -1315,7 +1435,41 @@ class TestNeutronv2(TestNeutronv2Base):
api.validate_networks,
self.context, requested_networks, 1)
def test_validate_networks_ports_in_same_network(self):
def test_validate_networks_ports_in_same_network_disable(self):
"""Verify that duplicateNetworks exception is thrown when ports on same
duplicate network are passed to validate_networks, when nova config
flag allow_duplicate_networks is set to its default False
"""
self.flags(allow_duplicate_networks=False, group='neutron')
port_a = self.port_data3[0]
port_a['fixed_ips'] = {'ip_address': '10.0.0.2',
'subnet_id': 'subnet_id'}
port_b = self.port_data1[0]
self.assertEqual(port_a['network_id'], port_b['network_id'])
for port in [port_a, port_b]:
port['device_id'] = None
port['device_owner'] = None
requested_networks = [(None, None, port_a['id']),
(None, None, port_b['id'])]
self.moxed_client.show_port(port_a['id']).AndReturn(
{'port': port_a})
self.moxed_client.show_port(port_b['id']).AndReturn(
{'port': port_b})
self.mox.ReplayAll()
api = neutronapi.API()
self.assertRaises(exception.NetworkDuplicated,
api.validate_networks,
self.context, requested_networks, 1)
def test_validate_networks_ports_in_same_network_enable(self):
"""Verify that duplicateNetworks exception is not thrown when ports
on same duplicate network are passed to validate_networks, when nova
config flag allow_duplicate_networks is set to its True
"""
self.flags(allow_duplicate_networks=True, group='neutron')
port_a = self.port_data3[0]
port_a['fixed_ips'] = {'ip_address': '10.0.0.2',
'subnet_id': 'subnet_id'}
@ -1327,15 +1481,15 @@ class TestNeutronv2(TestNeutronv2Base):
requested_networks = [(None, None, port_a['id']),
(None, None, port_b['id'])]
self.moxed_client.show_port(port_a['id']).AndReturn({'port': port_a})
self.moxed_client.show_port(port_b['id']).AndReturn({'port': port_b})
self.moxed_client.show_port(port_a['id']).AndReturn(
{'port': port_a})
self.moxed_client.show_port(port_b['id']).AndReturn(
{'port': port_b})
self.mox.ReplayAll()
api = neutronapi.API()
self.assertRaises(exception.NetworkDuplicated,
api.validate_networks,
self.context, requested_networks, 1)
api.validate_networks(self.context, requested_networks, 1)
def test_validate_networks_ports_not_in_same_network(self):
port_a = self.port_data3[0]