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:
@@ -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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user