From d16ed45e0615d54d983da43b1d0a2070c7edc814 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Sat, 18 Nov 2017 17:36:39 +0000 Subject: [PATCH] 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 --- doc/source/configuration/settings.rst | 17 +++ openstack_dashboard/api/microversions.py | 3 +- openstack_dashboard/api/neutron.py | 67 +++++++++++- openstack_dashboard/api/nova.py | 19 +++- openstack_dashboard/api/rest/neutron.py | 8 +- .../dashboards/project/instances/utils.py | 9 +- .../instances/workflows/create_instance.py | 2 +- .../project/network_topology/tests.py | 3 +- .../project/network_topology/views.py | 10 +- .../dashboards/project/networks/tables.py | 21 +++- .../dashboards/project/networks/views.py | 5 +- .../test/unit/api/rest/test_neutron.py | 3 +- .../test/unit/api/test_neutron.py | 101 ++++++++++++++---- .../test/unit/api/test_nova.py | 67 ++++++++++++ .../get-me-a-network-c979c244fa038258.yaml | 15 +++ 15 files changed, 317 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/get-me-a-network-c979c244fa038258.yaml diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst index c09e2e2576..8bab749aac 100644 --- a/doc/source/configuration/settings.rst +++ b/doc/source/configuration/settings.rst @@ -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 +`__.) + enable_distributed_router ######################### diff --git a/openstack_dashboard/api/microversions.py b/openstack_dashboard/api/microversions.py index be2da57eb9..a9c26f7bd7 100644 --- a/openstack_dashboard/api/microversions.py +++ b/openstack_dashboard/api/microversions.py @@ -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"], diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index fd04562441..748cdcbf57 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -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)) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 61d1c7719d..2e2e11d5ff 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -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, diff --git a/openstack_dashboard/api/rest/neutron.py b/openstack_dashboard/api/rest/neutron.py index 31945c2efc..25bcec9241 100644 --- a/openstack_dashboard/api/rest/neutron.py +++ b/openstack_dashboard/api/rest/neutron.py @@ -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) diff --git a/openstack_dashboard/dashboards/project/instances/utils.py b/openstack_dashboard/dashboards/project/instances/utils.py index fed49e8dcf..3c60091e70 100644 --- a/openstack_dashboard/dashboards/project/instances/utils.py +++ b/openstack_dashboard/dashboards/project/instances/utils.py @@ -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) diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index fd04ccd483..3787861899 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -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): diff --git a/openstack_dashboard/dashboards/project/network_topology/tests.py b/openstack_dashboard/dashboards/project/network_topology/tests.py index e3fbfcd9b7..5d5c50cac5 100644 --- a/openstack_dashboard/dashboards/project/network_topology/tests.py +++ b/openstack_dashboard/dashboards/project/network_topology/tests.py @@ -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) diff --git a/openstack_dashboard/dashboards/project/network_topology/views.py b/openstack_dashboard/dashboards/project/network_topology/views.py index a8878c0b10..74ead4623a 100644 --- a/openstack_dashboard/dashboards/project/network_topology/views.py +++ b/openstack_dashboard/dashboards/project/network_topology/views.py @@ -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 = [] diff --git a/openstack_dashboard/dashboards/project/networks/tables.py b/openstack_dashboard/dashboards/project/networks/tables.py index 1776e2c7a5..9a1230ec10 100644 --- a/openstack_dashboard/dashboards/project/networks/tables.py +++ b/openstack_dashboard/dashboards/project/networks/tables.py @@ -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"), diff --git a/openstack_dashboard/dashboards/project/networks/views.py b/openstack_dashboard/dashboards/project/networks/views.py index 5799a5c706..7a402c6ecf 100644 --- a/openstack_dashboard/dashboards/project/networks/views.py +++ b/openstack_dashboard/dashboards/project/networks/views.py @@ -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.') diff --git a/openstack_dashboard/test/unit/api/rest/test_neutron.py b/openstack_dashboard/test/unit/api/rest/test_neutron.py index 959245308c..09e37208ec 100644 --- a/openstack_dashboard/test/unit/api/rest/test_neutron.py +++ b/openstack_dashboard/test/unit/api/rest/test_neutron.py @@ -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( diff --git a/openstack_dashboard/test/unit/api/test_neutron.py b/openstack_dashboard/test/unit/api/test_neutron.py index 94b89224f1..c024706a4b 100644 --- a/openstack_dashboard/test/unit/api/test_neutron.py +++ b/openstack_dashboard/test/unit/api/test_neutron.py @@ -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()} diff --git a/openstack_dashboard/test/unit/api/test_nova.py b/openstack_dashboard/test/unit/api/test_nova.py index 2df8cc988a..59903bbf6c 100644 --- a/openstack_dashboard/test/unit/api/test_nova.py +++ b/openstack_dashboard/test/unit/api/test_nova.py @@ -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'}) diff --git a/releasenotes/notes/get-me-a-network-c979c244fa038258.yaml b/releasenotes/notes/get-me-a-network-c979c244fa038258.yaml new file mode 100644 index 0000000000..e83f66269c --- /dev/null +++ b/releasenotes/notes/get-me-a-network-c979c244fa038258.yaml @@ -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``.