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``.