Support "Get me a network" in launch instance

"Get-me-a-network" feature is supported in Nova API 2.37 or later.
To support this in horizon, a dummy "auto_allocated_network" is shown
in the launch instance form. I believe this approach fits the current
way of the launch instance form. The dummy network is a special network ID
and if specified the nova API wrapper converts 'nics' parameter properly.

In addition, a dummy "auto_allocated_network" is now shown in the network
table. This is because Neutron creates an actual auto allocated network
once a server is created specifying nics=="auto".
I believe this fake behavior brings consistency in the network table
to some extent, while this is just a compromise.

Note that this patch does not cover the network topology integration.
"auto_allocated_network" is not shown until the actual auto allocated
network is created. The network topology integration requires more work.
For example, the link for the fake auto allocated network should be
disabled in the flat view. The pop-up for the fake auto allocated network
should be disabled in the graph view.

Change-Id: I062fc1b7ed75dc771ddc7f13c8324ed4ffab6808
Closes-Bug: #1690433
This commit is contained in:
Akihiro Motoki 2017-11-18 17:36:39 +00:00
parent 2e0a3610bd
commit d16ed45e06
15 changed files with 317 additions and 33 deletions

View File

@ -1552,6 +1552,7 @@ Default:
{
'default_dns_nameservers': [],
'enable_auto_allocated_network': False,
'enable_distributed_router': False,
'enable_fip_topology_check': True,
'enable_ha_router': False,
@ -1581,6 +1582,22 @@ only a default. Users can still choose a different list of dns servers.
Example: ``["8.8.8.8", "8.8.4.4", "208.67.222.222"]``
enable_auto_allocated_network
#############################
.. versionadded:: 14.0.0(Rocky)
Default: ``False``
Enable or disable Nova and Neutron 'get-me-a-network' feature.
This sets up a neutron network topology for a project if there is no network
in the project. It simplifies the workflow when launching a server.
Horizon checks if both nova and neutron support the feature and enable it
only when supported. However, whether the feature works properly depends on
deployments, so this setting is disabled by default.
(The detail on the required preparation is described in `the Networking Guide
<https://docs.openstack.org/neutron/latest/admin/config-auto-allocation.html>`__.)
enable_distributed_router
#########################

View File

@ -33,7 +33,8 @@ MICROVERSION_FEATURES = {
"remote_console_mks": ["2.8", "2.53"],
"servergroup_soft_policies": ["2.15", "2.60"],
"servergroup_user_info": ["2.13", "2.60"],
"multiattach": ["2.60"]
"multiattach": ["2.60"],
"auto_allocated_network": ["2.37", "2.42"],
},
"cinder": {
"consistency_groups": ["2.0", "3.10"],

View File

@ -117,6 +117,36 @@ class Subnet(NeutronAPIDictWrapper):
super(Subnet, self).__init__(apidict)
AUTO_ALLOCATE_ID = '__auto_allocate__'
class PreAutoAllocateNetwork(Network):
def __init__(self, request):
tenant_id = request.user.tenant_id
auto_allocated_subnet = Subnet({
'name': 'auto_allocated_subnet',
'id': AUTO_ALLOCATE_ID,
'network_id': 'auto',
'tenant_id': tenant_id,
# The following two fields are fake so that Subnet class
# and the network topology view work without errors.
'ip_version': 4,
'cidr': '0.0.0.0/0',
})
auto_allocated_network = {
'name': 'auto_allocated_network',
'description': 'Network to be allocated automatically',
'id': AUTO_ALLOCATE_ID,
'status': 'ACTIVE',
'admin_state_up': True,
'shared': False,
'router:external': False,
'subnets': [auto_allocated_subnet],
'tenant_id': tenant_id,
}
super(PreAutoAllocateNetwork, self).__init__(auto_allocated_network)
class Trunk(NeutronAPIDictWrapper):
"""Wrapper for neutron trunks."""
@ -989,8 +1019,35 @@ def network_list(request, **params):
return [Network(n) for n in networks]
def _is_auto_allocated_network_supported(request):
try:
neutron_auto_supported = is_service_enabled(
request, 'enable_auto_allocated_network',
'auto-allocated-topology', default=False)
except Exception:
exceptions.handle(request, _('Failed to check if neutron supports '
'"auto_alloocated_network".'))
neutron_auto_supported = False
if not neutron_auto_supported:
return False
try:
# server_create needs to support both features,
# so we need to pass both features here.
nova_auto_supported = nova.is_feature_available(
request, ("instance_description",
"auto_allocated_network"))
except Exception:
exceptions.handle(request, _('Failed to check if nova supports '
'"auto_alloocated_network".'))
nova_auto_supported = False
return nova_auto_supported
@profiler.trace
def network_list_for_tenant(request, tenant_id, include_external=False,
include_pre_auto_allocate=False,
**params):
"""Return a network list available for the tenant.
@ -1016,6 +1073,12 @@ def network_list_for_tenant(request, tenant_id, include_external=False,
# In the current Neutron API, there is no way to retrieve
# both owner networks and public networks in a single API call.
networks += network_list(request, shared=True, **params)
# Hack for auto allocated network
if include_pre_auto_allocate and not networks:
if _is_auto_allocated_network_supported(request):
networks.append(PreAutoAllocateNetwork(request))
params['router:external'] = params.get('router:external', True)
if params['router:external'] and include_external:
if shared is not None:
@ -1754,8 +1817,8 @@ def is_enabled_by_config(name, default=True):
@memoized
def is_service_enabled(request, config_name, ext_name):
return (is_enabled_by_config(config_name) and
def is_service_enabled(request, config_name, ext_name, default=True):
return (is_enabled_by_config(config_name, default) and
is_extension_supported(request, ext_name))

View File

@ -512,10 +512,27 @@ def server_create(request, name, image, flavor, key_name, user_data,
availability_zone=None, instance_count=1, admin_pass=None,
disk_config=None, config_drive=None, meta=None,
scheduler_hints=None, description=None):
microversion = get_microversion(request, ("instance_description",
"auto_allocated_network"))
nova_client = novaclient(request, version=microversion)
# NOTE(amotoki): Handling auto allocated network
# Nova API 2.37 or later, it accepts a special string 'auto' for nics
# which means nova uses a network that is available for a current project
# if one exists and otherwise it creates a network automatically.
# This special handling is processed here as JS side assumes 'nics'
# is a list and it is easiest to handle it here.
if nics:
is_auto_allocate = any(nic.get('net-id') == '__auto_allocate__'
for nic in nics)
if is_auto_allocate:
nics = 'auto'
kwargs = {}
if description is not None:
kwargs['description'] = description
return Server(get_novaclient_with_instance_desc(request).servers.create(
return Server(nova_client.servers.create(
name.strip(), image, flavor, userdata=user_data,
security_groups=security_groups,
key_name=key_name, block_device_mapping=block_device_mapping,

View File

@ -39,7 +39,13 @@ class Networks(generic.View):
a network.
"""
tenant_id = request.user.tenant_id
result = api.neutron.network_list_for_tenant(request, tenant_id)
# NOTE(amotoki): At now, this method is only for server create,
# so it is no problem to pass include_pre_auto_allocate=True always.
# We need to revisit the logic if we use this method for
# other operations other than server create.
result = api.neutron.network_list_for_tenant(
request, tenant_id,
include_pre_auto_allocate=True)
return{'items': [n.to_dict() for n in result]}
@rest_utils.ajax(data_required=True)

View File

@ -86,7 +86,8 @@ def server_group_list(request):
return []
def network_field_data(request, include_empty_option=False, with_cidr=False):
def network_field_data(request, include_empty_option=False, with_cidr=False,
for_launch=False):
"""Returns a list of tuples of all networks.
Generates a list of networks available to the user (request). And returns
@ -101,8 +102,12 @@ def network_field_data(request, include_empty_option=False, with_cidr=False):
tenant_id = request.user.tenant_id
networks = []
if api.base.is_service_enabled(request, 'network'):
extra_params = {}
if for_launch:
extra_params['include_pre_auto_allocate'] = True
try:
networks = api.neutron.network_list_for_tenant(request, tenant_id)
networks = api.neutron.network_list_for_tenant(
request, tenant_id, **extra_params)
except Exception as e:
msg = _('Failed to get network list {0}').format(six.text_type(e))
exceptions.handle(request, msg)

View File

@ -738,7 +738,7 @@ class SetNetworkAction(workflows.Action):
help_text = _("Select networks for your instance.")
def populate_network_choices(self, request, context):
return instance_utils.network_field_data(request)
return instance_utils.network_field_data(request, for_launch=True)
class SetNetwork(workflows.Step):

View File

@ -173,7 +173,8 @@ class NetworkTopologyTests(test.TestCase):
self.mock_server_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_network_list_for_tenant.assert_called_once_with(
test.IsHttpRequest(), self.tenant.id)
test.IsHttpRequest(), self.tenant.id,
include_pre_auto_allocate=False)
if router_enable:
self.mock_router_list.assert_called_once_with(
test.IsHttpRequest(), tenant_id=self.tenant.id)

View File

@ -264,9 +264,17 @@ class JSONView(View):
# specify tenant_id for subnet. The subnet which belongs to the public
# network is needed to draw subnet information on public network.
try:
# NOTE(amotoki):
# To support auto allocated network in the network topology view,
# we need to handle the auto allocated network which haven't been
# created yet. The current network topology logic cannot not handle
# fake network ID properly, so we temporarily exclude
# pre-auto-allocated-network from the network topology view.
# It would be nice if someone is interested in supporting it.
neutron_networks = api.neutron.network_list_for_tenant(
request,
request.user.tenant_id)
request.user.tenant_id,
include_pre_auto_allocate=False)
except Exception:
neutron_networks = []
networks = []

View File

@ -15,6 +15,7 @@ import logging
from django import template
from django.template import defaultfilters as filters
from django.urls import reverse
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
@ -53,6 +54,11 @@ class DeleteNetwork(policy.PolicyTargetMixin, tables.DeleteAction):
policy_rules = (("network", "delete_network"),)
def allowed(self, request, datum=None):
if datum and datum.id == api.neutron.AUTO_ALLOCATE_ID:
return False
return True
@actions.handle_exception_with_detail_message(
# normal_log_message
'Failed to delete network %(id)s: %(exc)s',
@ -104,6 +110,11 @@ class EditNetwork(policy.PolicyTargetMixin, tables.LinkAction):
icon = "pencil"
policy_rules = (("network", "update_network"),)
def allowed(self, request, datum=None):
if datum and datum.id == api.neutron.AUTO_ALLOCATE_ID:
return False
return True
class CreateSubnet(subnet_tables.SubnetPolicyTargetMixin, tables.LinkAction):
name = "subnet"
@ -117,6 +128,8 @@ class CreateSubnet(subnet_tables.SubnetPolicyTargetMixin, tables.LinkAction):
("network:project_id", "tenant_id"),)
def allowed(self, request, datum=None):
if datum and datum.id == api.neutron.AUTO_ALLOCATE_ID:
return False
usages = quotas.tenant_quota_usages(request, targets=('subnet', ))
# when Settings.OPENSTACK_NEUTRON_NETWORK['enable_quotas'] = False
# usages["subnet'] is empty
@ -137,6 +150,12 @@ def get_subnets(network):
return template.loader.render_to_string(template_name, context)
def get_network_link(network):
if network.id == api.neutron.AUTO_ALLOCATE_ID:
return None
return reverse('horizon:project:networks:detail', args=[network.id])
DISPLAY_CHOICES = (
("up", pgettext_lazy("Admin state of a Network", u"UP")),
("down", pgettext_lazy("Admin state of a Network", u"DOWN")),
@ -172,7 +191,7 @@ class ProjectNetworksFilterAction(tables.FilterAction):
class NetworksTable(tables.DataTable):
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
link='horizon:project:networks:detail')
link=get_network_link)
subnets = tables.Column(get_subnets,
verbose_name=_("Subnets Associated"),)
shared = tables.Column("shared", verbose_name=_("Shared"),

View File

@ -52,7 +52,10 @@ class IndexView(tables.DataTableView):
tenant_id = self.request.user.tenant_id
search_opts = self.get_filters(filters_map=self.FILTERS_MAPPING)
networks = api.neutron.network_list_for_tenant(
self.request, tenant_id, include_external=True, **search_opts)
self.request, tenant_id,
include_external=True,
include_pre_auto_allocate=True,
**search_opts)
except Exception:
networks = []
msg = _('Network list can not be retrieved.')

View File

@ -39,7 +39,8 @@ class NeutronNetworksTestCase(test.TestCase):
exp_resp = [self._dictify_network(n) for n in self.networks.list()]
self.assertItemsCollectionEqual(response, exp_resp)
mock_network_list_for_tenant.assert_called_once_with(
request, request.user.tenant_id)
request, request.user.tenant_id,
include_pre_auto_allocate=True)
def test_create(self):
self._test_create(

View File

@ -42,11 +42,13 @@ class NeutronApiTests(test.APIMockTestCase):
neutronclient.list_networks.assert_called_once_with()
neutronclient.list_subnets.assert_called_once_with()
@override_settings(OPENSTACK_NEUTRON_NETWORK={
'enable_auto_allocated_network': True})
@test.create_mocks({api.neutron: ('network_list',
'subnet_list')})
def _test_network_list_for_tenant(
self, include_external,
filter_params, should_called):
filter_params, should_called, **extra_kwargs):
"""Convenient method to test network_list_for_tenant.
:param include_external: Passed to network_list_for_tenant.
@ -58,55 +60,61 @@ class NeutronApiTests(test.APIMockTestCase):
filter_params = filter_params or {}
all_networks = self.networks.list()
tenant_id = '1'
tenant_networks = [n for n in all_networks
if n['tenant_id'] == tenant_id]
shared_networks = [n for n in all_networks if n['shared']]
external_networks = [n for n in all_networks if n['router:external']]
return_values = []
expected_calls = []
if 'non_shared' in should_called:
params = filter_params.copy()
params['shared'] = False
return_values.append([
network for network in all_networks
if network['tenant_id'] == tenant_id
])
return_values.append(tenant_networks)
expected_calls.append(
mock.call(test.IsHttpRequest(), tenant_id=tenant_id, **params),
)
if 'shared' in should_called:
params = filter_params.copy()
params['shared'] = True
return_values.append([
network for network in all_networks
if network.get('shared')
])
return_values.append(shared_networks)
expected_calls.append(
mock.call(test.IsHttpRequest(), **params),
)
if 'external' in should_called:
params = filter_params.copy()
params['router:external'] = True
return_values.append([
network for network in all_networks
if network.get('router:external')
])
return_values.append(external_networks)
expected_calls.append(
mock.call(test.IsHttpRequest(), **params),
)
self.mock_network_list.side_effect = return_values
extra_kwargs.update(filter_params)
ret_val = api.neutron.network_list_for_tenant(
self.request, tenant_id,
include_external=include_external,
**filter_params)
**extra_kwargs)
expected = [n for n in all_networks
if (('non_shared' in should_called and
n['tenant_id'] == tenant_id) or
('shared' in should_called and n['shared']) or
('external' in should_called and
include_external and n['router:external']))]
expected = []
if 'non_shared' in should_called:
expected += tenant_networks
if 'shared' in should_called:
expected += shared_networks
if 'external' in should_called and include_external:
expected += external_networks
self.assertEqual(set(n.id for n in expected),
set(n.id for n in ret_val))
self.mock_network_list.assert_has_calls(expected_calls)
# Ensure all three types of networks are not empty. This is required
# to check 'pre_auto_allocate' network is not included.
self.assertTrue(tenant_networks)
self.assertTrue(shared_networks)
self.assertTrue(external_networks)
self.assertNotIn(api.neutron.AUTO_ALLOCATE_ID,
[n.id for n in ret_val])
def test_network_list_for_tenant(self):
self._test_network_list_for_tenant(
include_external=False, filter_params=None,
@ -164,6 +172,59 @@ class NeutronApiTests(test.APIMockTestCase):
'foo': 'bar'},
should_called=['non_shared', 'external'])
def test_network_list_for_tenant_no_pre_auto_allocate_if_net_exists(self):
self._test_network_list_for_tenant(
include_external=True, filter_params=None,
should_called=['non_shared', 'shared', 'external'],
include_pre_auto_allocate=True)
@override_settings(OPENSTACK_NEUTRON_NETWORK={
'enable_auto_allocated_network': True})
@test.create_mocks({api.neutron: ['network_list',
'is_extension_supported'],
api.nova: ['is_feature_available']})
def test_network_list_for_tenant_with_pre_auto_allocate(self):
tenant_id = '1'
self.mock_network_list.return_value = []
self.mock_is_extension_supported.return_value = True
self.mock_is_feature_available.return_value = True
ret_val = api.neutron.network_list_for_tenant(
self.request, tenant_id, include_pre_auto_allocate=True)
self.assertEqual(1, len(ret_val))
self.assertIsInstance(ret_val[0], api.neutron.PreAutoAllocateNetwork)
self.assertEqual(api.neutron.AUTO_ALLOCATE_ID, ret_val[0].id)
self.assertEqual(2, self.mock_network_list.call_count)
self.mock_network_list.assert_has_calls([
mock.call(test.IsHttpRequest(), tenant_id=tenant_id,
shared=False),
mock.call(test.IsHttpRequest(), shared=True),
])
self.mock_is_extension_supported.assert_called_once_with(
test.IsHttpRequest(), 'auto-allocated-topology')
self.mock_is_feature_available.assert_called_once_with(
test.IsHttpRequest(),
('instance_description', 'auto_allocated_network'))
@test.create_mocks({api.neutron: ['network_list']})
def test_network_list_for_tenant_no_pre_auto_allocate_if_disabled(self):
tenant_id = '1'
self.mock_network_list.return_value = []
ret_val = api.neutron.network_list_for_tenant(
self.request, tenant_id, include_pre_auto_allocate=True)
self.assertEqual(0, len(ret_val))
self.assertEqual(2, self.mock_network_list.call_count)
self.mock_network_list.assert_has_calls([
mock.call(test.IsHttpRequest(), tenant_id=tenant_id,
shared=False),
mock.call(test.IsHttpRequest(), shared=True),
])
@mock.patch.object(api.neutron, 'neutronclient')
def test_network_get(self, mock_neutronclient):
network = {'network': self.api_networks.first()}

View File

@ -763,3 +763,70 @@ class ComputeApiTests(test.APIMockTestCase):
['bob', 'john', 'sam'])
novaclient.availability_zones.list.assert_called_once_with(
detailed=detailed)
@test.create_mocks({api.nova: ['get_microversion',
'novaclient']})
def _test_server_create(self, extra_kwargs=None, expected_kwargs=None):
extra_kwargs = extra_kwargs or {}
expected_kwargs = expected_kwargs or {}
expected_kwargs.setdefault('nics', None)
self.mock_get_microversion.return_value = mock.sentinel.microversion
novaclient = mock.Mock()
self.mock_novaclient.return_value = novaclient
ret = api.nova.server_create(
mock.sentinel.request,
'vm1', 'image1', 'flavor1', 'key1', 'userdata1', ['sg1'],
**extra_kwargs)
self.assertIsInstance(ret, api.nova.Server)
self.mock_get_microversion.assert_called_once_with(
mock.sentinel.request, ('instance_description',
'auto_allocated_network'))
self.mock_novaclient.assert_called_once_with(
mock.sentinel.request, version=mock.sentinel.microversion)
novaclient.servers.create.assert_called_once_with(
'vm1', 'image1', 'flavor1', userdata='userdata1',
security_groups=['sg1'], key_name='key1',
block_device_mapping=None, block_device_mapping_v2=None,
availability_zone=None, min_count=1, admin_pass=None,
disk_config=None, config_drive=None, meta=None,
scheduler_hints=None, **expected_kwargs)
def test_server_create(self):
self._test_server_create()
def test_server_create_with_description(self):
kwargs = {'description': 'desc1'}
self._test_server_create(extra_kwargs=kwargs, expected_kwargs=kwargs)
def test_server_create_with_normal_nics(self):
kwargs = {
'nics': [
{'net-id': 'net1'},
{'port-id': 'port1'},
]
}
self._test_server_create(extra_kwargs=kwargs, expected_kwargs=kwargs)
def test_server_create_with_auto_nic(self):
kwargs = {
'nics': [
{'net-id': api.neutron.AUTO_ALLOCATE_ID},
]
}
self._test_server_create(extra_kwargs=kwargs,
expected_kwargs={'nics': 'auto'})
def test_server_create_with_auto_nic_with_others(self):
# This actually never happens. Just for checking the logic.
kwargs = {
'nics': [
{'net-id': 'net1'},
{'net-id': api.neutron.AUTO_ALLOCATE_ID},
{'port-id': 'port1'},
]
}
self._test_server_create(extra_kwargs=kwargs,
expected_kwargs={'nics': 'auto'})

View File

@ -0,0 +1,15 @@
---
features:
- |
[:bug:`1690433`] "Get me a network" feature provided by nova and neutron
is now exposed in the launch server form.
This feature will sets up a neutron network topology for a project
if there is no network in the project. It simplifies the workflow when
launching a server.
In the horizon support, when there is no network which can be used
for a server, a dummy network named 'auto_allocated_network' is shown
in the network choices.
The feature is disabled by default because it requires preparations
in your neutron deployment.
To enable it, set ``enable_auto_allocated_network`` in
``OPENSTACK_NEUTRON_NETWORK`` to ``True``.