neutron: handle 'auto' network request in allocate_for_instance

This handles auto-allocating a network with Neutron when no
specific networks are requested, 'auto' is specified on the
request and there are no existing networks available to the
project.

Related-Bug: #1591766

Part of blueprint get-me-a-network

Change-Id: Ibeccfd83c5703671b7fe08090e24c23cc3509371
This commit is contained in:
Matt Riedemann
2016-06-10 14:24:34 -04:00
parent f3700e20d9
commit d7320be2e2
2 changed files with 196 additions and 11 deletions

View File

@@ -136,7 +136,8 @@ class API(base_api.NetworkAPI):
"""Setup or teardown the network structures."""
def _get_available_networks(self, context, project_id,
net_ids=None, neutron=None):
net_ids=None, neutron=None,
auto_allocate=False):
"""Return a network list available for the tenant.
The list contains networks owned by the tenant and public networks.
If net_ids specified, it searches networks with requested IDs only.
@@ -153,6 +154,14 @@ class API(base_api.NetworkAPI):
else:
# (1) Retrieve non-public network list owned by the tenant.
search_opts = {'tenant_id': project_id, 'shared': False}
if auto_allocate:
# The auto-allocated-topology extension may create complex
# network topologies and it does so in a non-transactional
# fashion. Therefore API users may be exposed to resources that
# are transient or partially built. A client should use
# resources that are meant to be ready and this can be done by
# checking their admin_state_up flag.
search_opts['admin_state_up'] = True
nets = neutron.list_networks(**search_opts).get('networks', [])
# (2) Retrieve public network list.
search_opts = {'shared': True}
@@ -362,7 +371,10 @@ class API(base_api.NetworkAPI):
ports = {}
net_ids = []
ordered_networks = []
if requested_networks:
# If we're asked to auto-allocate the network then there won't be any
# ports or real neutron networks to lookup, so just return empty
# results.
if requested_networks and not requested_networks.auto_allocate:
for request in requested_networks:
# Process a request to use a pre-existing neutron port.
@@ -545,16 +557,28 @@ class API(base_api.NetworkAPI):
self._process_requested_networks(context,
instance, neutron, requested_networks, hypervisor_macs))
auto_allocate = requested_networks and requested_networks.auto_allocate
nets = self._get_available_networks(context, instance.project_id,
net_ids, neutron=neutron)
net_ids, neutron=neutron,
auto_allocate=auto_allocate)
if not nets:
# NOTE(chaochin): If user specifies a network id and the network
# can not be found, raise NetworkNotFound error.
if requested_networks:
for request in requested_networks:
if not request.port_id and request.network_id:
raise exception.NetworkNotFound(
network_id=request.network_id)
# There are no networks available for the project to use and
# none specifically requested, so check to see if we're asked
# to auto-allocate the network.
if auto_allocate:
# During validate_networks we checked to see if
# auto-allocation is available so we don't need to do that
# again here.
nets = [self._auto_allocate_network(instance, neutron)]
else:
# NOTE(chaochin): If user specifies a network id and the
# network can not be found, raise NetworkNotFound error.
for request in requested_networks:
if not request.port_id and request.network_id:
raise exception.NetworkNotFound(
network_id=request.network_id)
else:
LOG.debug("No network configured", instance=instance)
return network_model.NetworkInfo([])
@@ -564,7 +588,8 @@ class API(base_api.NetworkAPI):
# with None params=(network_id=None, requested_ip=None, port_id=None,
# pci_request_id=None):
if (not requested_networks
or requested_networks.is_single_unspecified):
or requested_networks.is_single_unspecified
or requested_networks.auto_allocate):
# If no networks were requested and none are available, consider
# it a bad request.
if not nets:
@@ -1140,6 +1165,39 @@ class API(base_api.NetworkAPI):
'auto-allocated-topology extension is not available.')
return False
def _auto_allocate_network(self, instance, neutron):
"""Automatically allocates a network for the given project.
:param instance: create the network for the project that owns this
instance
:param neutron: neutron client
:returns: Details of the network that was created.
:raises: nova.exception.UnableToAutoAllocateNetwork
:raises: nova.exception.NetworkNotFound
"""
project_id = instance.project_id
LOG.debug('Automatically allocating a network for project %s.',
project_id, instance=instance)
try:
topology = neutron.get_auto_allocated_topology(
project_id)['auto_allocated_topology']
except neutron_client_exc.Conflict:
raise exception.UnableToAutoAllocateNetwork(project_id=project_id)
try:
network = neutron.show_network(topology['id'])['network']
except neutron_client_exc.NetworkNotFoundClient:
# This shouldn't happen since we just created the network, but
# handle it anyway.
LOG.error(_LE('Automatically allocated network %(network_id)s '
'was not found.'), {'network_id': topology['id']},
instance=instance)
raise exception.UnableToAutoAllocateNetwork(project_id=project_id)
LOG.debug('Automatically allocated network: %s', network,
instance=instance)
return network
def _ports_needed_per_instance(self, context, neutron, requested_networks):
# TODO(danms): Remove me when all callers pass an object

View File

@@ -1280,7 +1280,8 @@ class TestNeutronv2(TestNeutronv2Base):
# of the function
api._get_available_networks(self.context, self.instance.project_id,
[],
neutron=self.moxed_client).\
neutron=self.moxed_client,
auto_allocate=False).\
AndRaise(BailOutEarly)
self.mox.ReplayAll()
requested_networks = objects.NetworkRequestList(
@@ -4795,3 +4796,129 @@ class TestNeutronv2AutoAllocateNetwork(test.NoDBTestCase):
requested_networks))
can_alloc.assert_called_once_with(
self.context, mock.sentinel.neutron)
def test__process_requested_networks_auto_allocate(self):
# Tests that _process_requested_networks doesn't really do anything
# if there is an auto-allocate network request.
net_req = objects.NetworkRequest(
network_id=net_req_obj.NETWORK_ID_AUTO)
requested_networks = objects.NetworkRequestList(objects=[net_req])
self.assertEqual(({}, [], [], None),
self.api._process_requested_networks(
self.context, mock.sentinel.instance,
mock.sentinel.neutron_client, requested_networks))
def test__auto_allocate_network_conflict(self):
# Tests that we handle a 409 from Neutron when auto-allocating topology
instance = mock.Mock(project_id=self.context.project_id)
ntrn = mock.Mock()
ntrn.get_auto_allocated_topology = mock.Mock(
side_effect=exceptions.Conflict)
self.assertRaises(exception.UnableToAutoAllocateNetwork,
self.api._auto_allocate_network, instance, ntrn)
ntrn.get_auto_allocated_topology.assert_called_once_with(
instance.project_id)
def test__auto_allocate_network_network_not_found(self):
# Tests that we handle a 404 from Neutron when auto-allocating topology
instance = mock.Mock(project_id=self.context.project_id)
ntrn = mock.Mock()
ntrn.get_auto_allocated_topology.return_value = {
'auto_allocated_topology': {
'id': uuids.network_id
}
}
ntrn.show_network = mock.Mock(
side_effect=exceptions.NetworkNotFoundClient)
self.assertRaises(exception.UnableToAutoAllocateNetwork,
self.api._auto_allocate_network, instance, ntrn)
ntrn.show_network.assert_called_once_with(uuids.network_id)
def test__auto_allocate_network(self):
# Tests the happy path.
instance = mock.Mock(project_id=self.context.project_id)
ntrn = mock.Mock()
ntrn.get_auto_allocated_topology.return_value = {
'auto_allocated_topology': {
'id': uuids.network_id
}
}
ntrn.show_network.return_value = {'network': mock.sentinel.network}
self.assertEqual(mock.sentinel.network,
self.api._auto_allocate_network(instance, ntrn))
def test_allocate_for_instance_auto_allocate(self):
# Tests the happy path.
ntrn = mock.Mock()
# mock neutron.list_networks which is called from
# _get_available_networks when net_ids is empty, which it will be
# because _process_requested_networks will return an empty list since
# we requested 'auto' allocation.
ntrn.list_networks.return_value = {}
fake_network = {
'id': uuids.network_id,
'subnets': [
uuids.subnet_id,
]
}
def fake_get_instance_nw_info(context, instance, **kwargs):
# assert the network and port are what was used in the test
self.assertIn('networks', kwargs)
self.assertEqual(1, len(kwargs['networks']))
self.assertEqual(uuids.network_id,
kwargs['networks'][0]['id'])
self.assertIn('port_ids', kwargs)
self.assertEqual(1, len(kwargs['port_ids']))
self.assertEqual(uuids.port_id, kwargs['port_ids'][0])
# return a fake vif
return [model.VIF(id=uuids.port_id)]
@mock.patch('nova.network.neutronv2.api.get_client', return_value=ntrn)
@mock.patch.object(self.api, '_has_port_binding_extension',
return_value=True)
@mock.patch.object(self.api, '_auto_allocate_network',
return_value=fake_network)
@mock.patch.object(self.api, '_check_external_network_attach')
@mock.patch.object(self.api, '_populate_neutron_extension_values')
@mock.patch.object(self.api, '_populate_mac_address')
@mock.patch.object(self.api, '_create_port', spec=True,
return_value=uuids.port_id)
@mock.patch.object(self.api, '_update_port_dns_name')
@mock.patch.object(self.api, 'get_instance_nw_info',
fake_get_instance_nw_info)
def do_test(self,
update_port_dsn_name_mock,
create_port_mock,
populate_mac_addr_mock,
populate_ext_values_mock,
check_external_net_attach_mock,
auto_allocate_mock,
has_port_binding_mock,
get_client_mock):
instance = fake_instance.fake_instance_obj(self.context)
net_req = objects.NetworkRequest(
network_id=net_req_obj.NETWORK_ID_AUTO)
requested_networks = objects.NetworkRequestList(objects=[net_req])
nw_info = self.api.allocate_for_instance(
self.context, instance, requested_networks=requested_networks)
self.assertEqual(1, len(nw_info))
self.assertEqual(uuids.port_id, nw_info[0]['id'])
# assert that we filtered available networks on admin_state_up=True
ntrn.list_networks.assert_has_calls([
mock.call(tenant_id=instance.project_id, shared=False,
admin_state_up=True),
mock.call(shared=True)])
# assert the calls to create the port are using the network that
# was auto-allocated
port_req_body = mock.ANY
create_port_mock.assert_called_once_with(
ntrn, instance, uuids.network_id, port_req_body,
None, # request.address (fixed IP)
[], # security_group_ids - we didn't request any
)
do_test(self)