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': [], 'default_dns_nameservers': [],
'enable_auto_allocated_network': False,
'enable_distributed_router': False, 'enable_distributed_router': False,
'enable_fip_topology_check': True, 'enable_fip_topology_check': True,
'enable_ha_router': False, '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"]`` 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 enable_distributed_router
######################### #########################

View File

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

View File

@ -117,6 +117,36 @@ class Subnet(NeutronAPIDictWrapper):
super(Subnet, self).__init__(apidict) 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): class Trunk(NeutronAPIDictWrapper):
"""Wrapper for neutron trunks.""" """Wrapper for neutron trunks."""
@ -989,8 +1019,35 @@ def network_list(request, **params):
return [Network(n) for n in networks] 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 @profiler.trace
def network_list_for_tenant(request, tenant_id, include_external=False, def network_list_for_tenant(request, tenant_id, include_external=False,
include_pre_auto_allocate=False,
**params): **params):
"""Return a network list available for the tenant. """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 # In the current Neutron API, there is no way to retrieve
# both owner networks and public networks in a single API call. # both owner networks and public networks in a single API call.
networks += network_list(request, shared=True, **params) 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) params['router:external'] = params.get('router:external', True)
if params['router:external'] and include_external: if params['router:external'] and include_external:
if shared is not None: if shared is not None:
@ -1754,8 +1817,8 @@ def is_enabled_by_config(name, default=True):
@memoized @memoized
def is_service_enabled(request, config_name, ext_name): def is_service_enabled(request, config_name, ext_name, default=True):
return (is_enabled_by_config(config_name) and return (is_enabled_by_config(config_name, default) and
is_extension_supported(request, ext_name)) 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, availability_zone=None, instance_count=1, admin_pass=None,
disk_config=None, config_drive=None, meta=None, disk_config=None, config_drive=None, meta=None,
scheduler_hints=None, description=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 = {} kwargs = {}
if description is not None: if description is not None:
kwargs['description'] = description 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, name.strip(), image, flavor, userdata=user_data,
security_groups=security_groups, security_groups=security_groups,
key_name=key_name, block_device_mapping=block_device_mapping, key_name=key_name, block_device_mapping=block_device_mapping,

View File

@ -39,7 +39,13 @@ class Networks(generic.View):
a network. a network.
""" """
tenant_id = request.user.tenant_id 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]} return{'items': [n.to_dict() for n in result]}
@rest_utils.ajax(data_required=True) @rest_utils.ajax(data_required=True)

View File

@ -86,7 +86,8 @@ def server_group_list(request):
return [] 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. """Returns a list of tuples of all networks.
Generates a list of networks available to the user (request). And returns 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 tenant_id = request.user.tenant_id
networks = [] networks = []
if api.base.is_service_enabled(request, 'network'): if api.base.is_service_enabled(request, 'network'):
extra_params = {}
if for_launch:
extra_params['include_pre_auto_allocate'] = True
try: 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: except Exception as e:
msg = _('Failed to get network list {0}').format(six.text_type(e)) msg = _('Failed to get network list {0}').format(six.text_type(e))
exceptions.handle(request, msg) exceptions.handle(request, msg)

View File

@ -738,7 +738,7 @@ class SetNetworkAction(workflows.Action):
help_text = _("Select networks for your instance.") help_text = _("Select networks for your instance.")
def populate_network_choices(self, request, context): 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): class SetNetwork(workflows.Step):

View File

@ -173,7 +173,8 @@ class NetworkTopologyTests(test.TestCase):
self.mock_server_list.assert_called_once_with( self.mock_server_list.assert_called_once_with(
test.IsHttpRequest()) test.IsHttpRequest())
self.mock_network_list_for_tenant.assert_called_once_with( 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: if router_enable:
self.mock_router_list.assert_called_once_with( self.mock_router_list.assert_called_once_with(
test.IsHttpRequest(), tenant_id=self.tenant.id) 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 # specify tenant_id for subnet. The subnet which belongs to the public
# network is needed to draw subnet information on public network. # network is needed to draw subnet information on public network.
try: 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( neutron_networks = api.neutron.network_list_for_tenant(
request, request,
request.user.tenant_id) request.user.tenant_id,
include_pre_auto_allocate=False)
except Exception: except Exception:
neutron_networks = [] neutron_networks = []
networks = [] networks = []

View File

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

View File

@ -52,7 +52,10 @@ class IndexView(tables.DataTableView):
tenant_id = self.request.user.tenant_id tenant_id = self.request.user.tenant_id
search_opts = self.get_filters(filters_map=self.FILTERS_MAPPING) search_opts = self.get_filters(filters_map=self.FILTERS_MAPPING)
networks = api.neutron.network_list_for_tenant( 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: except Exception:
networks = [] networks = []
msg = _('Network list can not be retrieved.') 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()] exp_resp = [self._dictify_network(n) for n in self.networks.list()]
self.assertItemsCollectionEqual(response, exp_resp) self.assertItemsCollectionEqual(response, exp_resp)
mock_network_list_for_tenant.assert_called_once_with( 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): def test_create(self):
self._test_create( self._test_create(

View File

@ -42,11 +42,13 @@ class NeutronApiTests(test.APIMockTestCase):
neutronclient.list_networks.assert_called_once_with() neutronclient.list_networks.assert_called_once_with()
neutronclient.list_subnets.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', @test.create_mocks({api.neutron: ('network_list',
'subnet_list')}) 'subnet_list')})
def _test_network_list_for_tenant( def _test_network_list_for_tenant(
self, include_external, self, include_external,
filter_params, should_called): filter_params, should_called, **extra_kwargs):
"""Convenient method to test network_list_for_tenant. """Convenient method to test network_list_for_tenant.
:param include_external: Passed to 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 {} filter_params = filter_params or {}
all_networks = self.networks.list() all_networks = self.networks.list()
tenant_id = '1' 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 = [] return_values = []
expected_calls = [] expected_calls = []
if 'non_shared' in should_called: if 'non_shared' in should_called:
params = filter_params.copy() params = filter_params.copy()
params['shared'] = False params['shared'] = False
return_values.append([ return_values.append(tenant_networks)
network for network in all_networks
if network['tenant_id'] == tenant_id
])
expected_calls.append( expected_calls.append(
mock.call(test.IsHttpRequest(), tenant_id=tenant_id, **params), mock.call(test.IsHttpRequest(), tenant_id=tenant_id, **params),
) )
if 'shared' in should_called: if 'shared' in should_called:
params = filter_params.copy() params = filter_params.copy()
params['shared'] = True params['shared'] = True
return_values.append([ return_values.append(shared_networks)
network for network in all_networks
if network.get('shared')
])
expected_calls.append( expected_calls.append(
mock.call(test.IsHttpRequest(), **params), mock.call(test.IsHttpRequest(), **params),
) )
if 'external' in should_called: if 'external' in should_called:
params = filter_params.copy() params = filter_params.copy()
params['router:external'] = True params['router:external'] = True
return_values.append([ return_values.append(external_networks)
network for network in all_networks
if network.get('router:external')
])
expected_calls.append( expected_calls.append(
mock.call(test.IsHttpRequest(), **params), mock.call(test.IsHttpRequest(), **params),
) )
self.mock_network_list.side_effect = return_values self.mock_network_list.side_effect = return_values
extra_kwargs.update(filter_params)
ret_val = api.neutron.network_list_for_tenant( ret_val = api.neutron.network_list_for_tenant(
self.request, tenant_id, self.request, tenant_id,
include_external=include_external, include_external=include_external,
**filter_params) **extra_kwargs)
expected = [n for n in all_networks expected = []
if (('non_shared' in should_called and if 'non_shared' in should_called:
n['tenant_id'] == tenant_id) or expected += tenant_networks
('shared' in should_called and n['shared']) or if 'shared' in should_called:
('external' in should_called and expected += shared_networks
include_external and n['router:external']))] if 'external' in should_called and include_external:
expected += external_networks
self.assertEqual(set(n.id for n in expected), self.assertEqual(set(n.id for n in expected),
set(n.id for n in ret_val)) set(n.id for n in ret_val))
self.mock_network_list.assert_has_calls(expected_calls) 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): def test_network_list_for_tenant(self):
self._test_network_list_for_tenant( self._test_network_list_for_tenant(
include_external=False, filter_params=None, include_external=False, filter_params=None,
@ -164,6 +172,59 @@ class NeutronApiTests(test.APIMockTestCase):
'foo': 'bar'}, 'foo': 'bar'},
should_called=['non_shared', 'external']) 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') @mock.patch.object(api.neutron, 'neutronclient')
def test_network_get(self, mock_neutronclient): def test_network_get(self, mock_neutronclient):
network = {'network': self.api_networks.first()} network = {'network': self.api_networks.first()}

View File

@ -763,3 +763,70 @@ class ComputeApiTests(test.APIMockTestCase):
['bob', 'john', 'sam']) ['bob', 'john', 'sam'])
novaclient.availability_zones.list.assert_called_once_with( novaclient.availability_zones.list.assert_called_once_with(
detailed=detailed) 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``.