From ef20a53f9387645612ba65337dcd14a156d212a6 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Mon, 20 Jul 2015 12:18:40 +0200 Subject: [PATCH] Support create subnet w/Neutron subnet allocation The user is enabled to choose between entering a network address manually or to allocate a network address from a pool. With the advent of address scopes in Neutron this feature will be used for enabling routing of globally unique addresses (GUA) all the way in to the VM. This is a prerequisite for IPv6 to be routable, and will also at some point allow GUA-IPv4 without NAT. Workflow: If subnet allocation is not supported in backend, there are no subnet pools available, or the user chooses to enter network address manually the form will behave like before. If subnet allocation is supported in backend and there either are subnet pools available or the label for the default subnet pool(s) is configured, the user may choose to allocate a network address from a pool. The user will be presented with two new drop-down menus. The first drop-down menu will, in addition to the default pool (if configured), present the names and prefixes of available pools. The next drop-down menu will display available choices for prefix lengths (it will not be displayed if the "default pool" is selected). The available choices are defined by the subnet pool. Default prefix length defined by the pool will be selected. The correct IP version is used based on what IP version is defined by the selected subnet pool. The "Create Subnet" checkbox has been moved from the Subnet step to the first step of the workflow. This is because of how switchable fields work, and how you can not safely define more than one slug in a fields "switch-on" attribute. For the sake of User Experience, we need to reliably switch fields based on selected Address source on the Subnet tab. IMHO it also makes more sense to have this checkbox on the first step of the workflow, if you just want to create a network you can now uncheck the "Create Subnet" checkbox and click directly on "Create". Change-Id: Ie392ccd9feae4dc02c3f30e2475457e755700b6b Implements: blueprint neutron-subnet-allocation --- doc/source/topics/settings.rst | 24 + horizon/static/horizon/js/horizon.forms.js | 75 ++- .../project/networks/subnets/views.py | 8 + .../project/networks/subnets/workflows.py | 7 +- .../networks/subnets/_detail_overview.html | 6 + .../dashboards/project/networks/tests.py | 456 +++++++++++++++--- .../dashboards/project/networks/workflows.py | 175 +++++-- .../local/local_settings.py.example | 12 + 8 files changed, 658 insertions(+), 105 deletions(-) diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index c1b045a558..5ef8dc43bc 100755 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -750,6 +750,8 @@ Default:: 'supported_vnic_types': ["*"], 'segmentation_id_range': {}, 'enable_fip_topology_check': True, + 'default_ipv4_subnet_pool_label': None, + 'default_ipv6_subnet_pool_label': None, } A dictionary of settings which can be used to enable optional services provided @@ -918,6 +920,28 @@ subnet with no router if your Neutron backend allows it. .. versionadded:: 2015.2(Liberty) +``default_ipv4_subnet_pool_label``: + +.. versionadded:: 2015.2(Liberty) + +Default: ``None`` (Disabled) + +Neutron can be configured with a default Subnet Pool to be used for IPv4 +subnet-allocation. Specify the label you wish to display in the Address pool +selector on the create subnet step if you want to use this feature. + +``default_ipv6_subnet_pool_label``: + +.. versionadded:: 2015.2(Liberty) + +Default: ``None`` (Disabled) + +Neutron can be configured with a default Subnet Pool to be used for IPv6 +subnet-allocation. Specify the label you wish to display in the Address pool +selector on the create subnet step if you want to use this feature. + +You must set this to enable IPv6 Prefix Delegation in a PD-capable environment. + ``OPENSTACK_SSL_CACERT`` ------------------------ diff --git a/horizon/static/horizon/js/horizon.forms.js b/horizon/static/horizon/js/horizon.forms.js index 24c7ea1144..ce0cdda299 100644 --- a/horizon/static/horizon/js/horizon.forms.js +++ b/horizon/static/horizon/js/horizon.forms.js @@ -56,6 +56,68 @@ horizon.forms = { }); }, + handle_subnet_address_source: function() { + $("div.table_wrapper, #modal_wrapper").on("change", "select#id_address_source", function(evt) { + var $option = $(this).find("option:selected"); + var $form = $(this).closest("form"); + var $ipVersion = $form.find("select#id_ip_version"); + if ($option.val() == "subnetpool") { + $ipVersion.attr("disabled", "disabled"); + // disabled fields do not post, store the value in a hidden input + var el = document.createElement("input"); + el.type='hidden'; + el.id = "id_hidden_ip_version"; + el.name = $ipVersion.attr('name'); + el.value = $ipVersion.attr('value'); + $form.append(el); + } else { + var $hiddenIpVersion = $form.find("hidden#id_hidden_ip_version"); + $hiddenIpVersion.remove(); + $ipVersion.removeAttr("disabled"); + } + }); + }, + + handle_subnet_subnetpool: function() { + $("div.table_wrapper, #modal_wrapper").on("change", "select#id_subnetpool", function(evt) { + var $option = $(this).find("option:selected"); + var $form = $(this).closest("form"); + var $ipVersion = $form.find("select#id_ip_version"); + var $prefixLength = $form.find("select#id_prefixlen"); + var $ipv6Modes = $form.find("select#id_ipv6_modes"); + var subnetpoolIpVersion = parseInt($option.data("ip_version"), 10) || 4; + var minPrefixLen = parseInt($option.data("min_prefixlen"), 10) || 1; + var maxPrefixLen = parseInt($option.data("max_prefixlen"), 10); + var defaultPrefixLen = parseInt($option.data("default_prefixlen"), 10) || + -1; + var optionsAsString = ""; + + $ipVersion.val(subnetpoolIpVersion); + + if (!maxPrefixLen) { + if (subnetpoolIpVersion == 4) { + maxPrefixLen = 32; + } else { + maxPrefixLen = 128; + } + } + + for (i = minPrefixLen; i <= maxPrefixLen; i++) { + optionsAsString += ""; + } + $prefixLength.empty().append(optionsAsString); + if (defaultPrefixLen >= 0) { + $prefixLength.val(defaultPrefixLen); + } else { + $prefixLength.val(""); + } + }); + }, + /** * In the container's upload object form, copy the selected file name in the * object name field if the field is empty. The filename string is stored in @@ -196,6 +258,8 @@ horizon.addInitFunction(horizon.forms.init = function () { horizon.forms.handle_image_source(); horizon.forms.handle_object_upload_source(); horizon.forms.datepicker(); + horizon.forms.handle_subnet_address_source(); + horizon.forms.handle_subnet_subnetpool(); if (!horizon.conf.disable_password_reveal) { horizon.forms.add_password_fields_reveal_buttons($("body")); @@ -260,24 +324,25 @@ horizon.addInitFunction(horizon.forms.init = function () { visible = $switchable.is(':visible'), slug = $switchable.data('slug'), checked = $switchable.prop('checked'), - hide_tab = $switchable.data('hide-tab'), + hide_tab = String($switchable.data('hide-tab')).split(','), hide_on = $switchable.data('hideOnChecked'); // If checkbox is hidden then do not apply any further logic if (!visible) return; // If the checkbox has hide-tab attribute then hide/show the tab - if (hide_tab) { + var i, len; + for (i = 0, len = hide_tab.length; i < len; i++) { var $btnfinal = $('.button-final'); if(checked == hide_on) { // If the checkbox is not checked then hide the tab - $('*[data-target="#'+ hide_tab +'"]').parent().hide(); + $('*[data-target="#'+ hide_tab[i] +'"]').parent().hide(); $('.button-next').hide(); $btnfinal.show(); $btnfinal.data('show-on-tab', $fieldset.prop('id')); - } else if (!$('*[data-target="#'+ hide_tab +'"]').parent().is(':visible')) { + } else if (!$('*[data-target="#'+ hide_tab[i] +'"]').parent().is(':visible')) { // If the checkbox is checked and the tab is currently hidden then show the tab again - $('*[data-target="#'+ hide_tab +'"]').parent().show(); + $('*[data-target="#'+ hide_tab[i] +'"]').parent().show(); $btnfinal.hide(); $('.button-next').show(); $btnfinal.removeData('show-on-tab'); diff --git a/openstack_dashboard/dashboards/project/networks/subnets/views.py b/openstack_dashboard/dashboards/project/networks/subnets/views.py index 0cfafa9bbc..91d10d16dc 100644 --- a/openstack_dashboard/dashboards/project/networks/subnets/views.py +++ b/openstack_dashboard/dashboards/project/networks/subnets/views.py @@ -118,6 +118,14 @@ class DetailView(tabs.TabView): subnet.ipv6_ra_mode, subnet.ipv6_address_mode) subnet.ipv6_modes_desc = utils.IPV6_MODE_MAP.get(ipv6_modes) + if ('subnetpool_id' in subnet and + subnet.subnetpool_id and + api.neutron.is_extension_supported(self.request, + 'subnet_allocation')): + subnetpool = api.neutron.subnetpool_get(self.request, + subnet.subnetpool_id) + subnet.subnetpool_name = subnetpool.name + return subnet def get_context_data(self, **kwargs): diff --git a/openstack_dashboard/dashboards/project/networks/subnets/workflows.py b/openstack_dashboard/dashboards/project/networks/subnets/workflows.py index 5d4208573b..e72ae2a1e7 100644 --- a/openstack_dashboard/dashboards/project/networks/subnets/workflows.py +++ b/openstack_dashboard/dashboards/project/networks/subnets/workflows.py @@ -36,7 +36,6 @@ class CreateSubnetInfoAction(network_workflows.CreateSubnetInfoAction): def __init__(self, request, *args, **kwargs): super(CreateSubnetInfoAction, self).__init__(request, *args, **kwargs) - self.fields['cidr'].required = True class Meta(object): name = _("Subnet") @@ -82,6 +81,12 @@ class CreateSubnet(network_workflows.CreateNetwork): class UpdateSubnetInfoAction(CreateSubnetInfoAction): + address_source = forms.ChoiceField(widget=forms.HiddenInput(), + required=False) + subnetpool = forms.ChoiceField(widget=forms.HiddenInput(), + required=False) + prefixlen = forms.ChoiceField(widget=forms.HiddenInput(), + required=False) cidr = forms.IPField(label=_("Network Address"), required=False, initial="", diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html index d045b25ac0..e43a0fc71e 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html @@ -14,6 +14,12 @@ {% url 'horizon:project:networks:detail' subnet.network_id as network_url %}
{% trans "Network ID" %}
{{ subnet.network_id|default:_("None") }}
+
{% trans "Subnetpool" %}
+ {% if subnet.subnetpool_id %} +
{{ subnet.subnetpool_name }} ({{ subnet.subnetpool_id }})
+ {% else %} +
{% trans "None" %}
+ {% endif %}
{% trans "IP version" %}
{{ subnet.ipver_str|default:_("-") }}
{% trans "CIDR" %}
diff --git a/openstack_dashboard/dashboards/project/networks/tests.py b/openstack_dashboard/dashboards/project/networks/tests.py index 81c517242d..cbd61bc01a 100644 --- a/openstack_dashboard/dashboards/project/networks/tests.py +++ b/openstack_dashboard/dashboards/project/networks/tests.py @@ -461,6 +461,7 @@ class NetworkTests(test.TestCase): def test_network_create_post_with_subnet_network_exception( self, test_with_profile=False, + test_with_subnetpool=False, ): network = self.networks.first() subnet = self.subnets.first() @@ -497,7 +498,9 @@ class NetworkTests(test.TestCase): @test.create_stubs({api.neutron: ('network_create', 'network_delete', 'subnet_create', - 'profile_list')}) + 'profile_list', + 'is_extension_supported', + 'subnetpool_list',)}) def test_network_create_post_with_subnet_subnet_exception( self, test_with_profile=False, @@ -512,6 +515,11 @@ class NetworkTests(test.TestCase): api.neutron.profile_list(IsA(http.HttpRequest), 'network').AndReturn(net_profiles) params['net_profile_id'] = net_profile_id + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) api.neutron.network_create(IsA(http.HttpRequest), **params).AndReturn(network) api.neutron.subnet_create(IsA(http.HttpRequest), @@ -544,9 +552,12 @@ class NetworkTests(test.TestCase): self.test_network_create_post_with_subnet_subnet_exception( test_with_profile=True) - @test.create_stubs({api.neutron: ('profile_list',)}) + @test.create_stubs({api.neutron: ('profile_list', + 'is_extension_supported', + 'subnetpool_list',)}) def test_network_create_post_with_subnet_nocidr(self, - test_with_profile=False): + test_with_profile=False, + test_with_snpool=False): network = self.networks.first() subnet = self.subnets.first() if test_with_profile: @@ -554,6 +565,11 @@ class NetworkTests(test.TestCase): net_profile_id = self.net_profiles.first().id api.neutron.profile_list(IsA(http.HttpRequest), 'network').AndReturn(net_profiles) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) self.mox.ReplayAll() form_data = {'net_name': network.name, @@ -561,12 +577,16 @@ class NetworkTests(test.TestCase): 'with_subnet': True} if test_with_profile: form_data['net_profile_id'] = net_profile_id + if test_with_snpool: + form_data['subnetpool_id'] = '' + form_data['prefixlen'] = '' form_data.update(form_data_subnet(subnet, cidr='', allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) - self.assertContains(res, escape('Specify "Network Address" or ' + self.assertContains(res, escape('Specify "Network Address", ' + '"Address pool" or ' 'clear "Create Subnet" checkbox.')) @test.update_settings( @@ -575,10 +595,17 @@ class NetworkTests(test.TestCase): self.test_network_create_post_with_subnet_nocidr( test_with_profile=True) - @test.create_stubs({api.neutron: ('profile_list',)}) + def test_network_create_post_with_subnet_nocidr_nosubnetpool(self): + self.test_network_create_post_with_subnet_nocidr( + test_with_snpool=True) + + @test.create_stubs({api.neutron: ('profile_list', + 'is_extension_supported', + 'subnetpool_list',)}) def test_network_create_post_with_subnet_cidr_without_mask( self, test_with_profile=False, + test_with_subnetpool=False, ): network = self.networks.first() subnet = self.subnets.first() @@ -587,13 +614,22 @@ class NetworkTests(test.TestCase): net_profile_id = self.net_profiles.first().id api.neutron.profile_list(IsA(http.HttpRequest), 'network').AndReturn(net_profiles) - self.mox.ReplayAll() + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() form_data = {'net_name': network.name, 'admin_state': network.admin_state_up, 'with_subnet': True} if test_with_profile: form_data['net_profile_id'] = net_profile_id + if test_with_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + form_data['prefixlen'] = subnetpool.default_prefixlen form_data.update(form_data_subnet(subnet, cidr='10.0.0.0', allocation_pools=[])) url = reverse('horizon:project:networks:create') @@ -608,10 +644,17 @@ class NetworkTests(test.TestCase): self.test_network_create_post_with_subnet_cidr_without_mask( test_with_profile=True) - @test.create_stubs({api.neutron: ('profile_list',)}) + def test_network_create_post_with_subnet_cidr_without_mask_w_snpool(self): + self.test_network_create_post_with_subnet_cidr_without_mask( + test_with_subnetpool=True) + + @test.create_stubs({api.neutron: ('profile_list', + 'is_extension_supported', + 'subnetpool_list',)}) def test_network_create_post_with_subnet_cidr_inconsistent( self, test_with_profile=False, + test_with_subnetpool=False ): network = self.networks.first() subnet = self.subnets.first() @@ -620,6 +663,12 @@ class NetworkTests(test.TestCase): net_profile_id = self.net_profiles.first().id api.neutron.profile_list(IsA(http.HttpRequest), 'network').AndReturn(net_profiles) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) self.mox.ReplayAll() # dummy IPv6 address @@ -629,6 +678,10 @@ class NetworkTests(test.TestCase): 'with_subnet': True} if test_with_profile: form_data['net_profile_id'] = net_profile_id + if test_with_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + form_data['prefixlen'] = subnetpool.default_prefixlen form_data.update(form_data_subnet(subnet, cidr=cidr, allocation_pools=[])) url = reverse('horizon:project:networks:create') @@ -643,10 +696,17 @@ class NetworkTests(test.TestCase): self.test_network_create_post_with_subnet_cidr_inconsistent( test_with_profile=True) - @test.create_stubs({api.neutron: ('profile_list',)}) + def test_network_create_post_with_subnet_cidr_inconsistent_w_snpool(self): + self.test_network_create_post_with_subnet_cidr_inconsistent( + test_with_subnetpool=True) + + @test.create_stubs({api.neutron: ('profile_list', + 'is_extension_supported', + 'subnetpool_list',)}) def test_network_create_post_with_subnet_gw_inconsistent( self, test_with_profile=False, + test_with_subnetpool=False, ): network = self.networks.first() subnet = self.subnets.first() @@ -655,6 +715,12 @@ class NetworkTests(test.TestCase): net_profile_id = self.net_profiles.first().id api.neutron.profile_list(IsA(http.HttpRequest), 'network').AndReturn(net_profiles) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) self.mox.ReplayAll() # dummy IPv6 address @@ -664,6 +730,10 @@ class NetworkTests(test.TestCase): 'with_subnet': True} if test_with_profile: form_data['net_profile_id'] = net_profile_id + if test_with_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + form_data['prefixlen'] = subnetpool.default_prefixlen form_data.update(form_data_subnet(subnet, gateway_ip=gateway_ip, allocation_pools=[])) url = reverse('horizon:project:networks:create') @@ -677,6 +747,10 @@ class NetworkTests(test.TestCase): self.test_network_create_post_with_subnet_gw_inconsistent( test_with_profile=True) + def test_network_create_post_with_subnet_gw_inconsistent_w_snpool(self): + self.test_network_create_post_with_subnet_gw_inconsistent( + test_with_subnetpool=True) + @test.create_stubs({api.neutron: ('network_get',)}) def test_network_update_get(self): network = self.networks.first() @@ -832,7 +906,8 @@ class NetworkTests(test.TestCase): class NetworkSubnetTests(test.TestCase): - @test.create_stubs({api.neutron: ('network_get', 'subnet_get',)}) + @test.create_stubs({api.neutron: ('network_get', + 'subnet_get',)}) def test_subnet_detail(self): network = self.networks.first() subnet = self.subnets.first() @@ -881,7 +956,7 @@ class NetworkSubnetTests(test.TestCase): @test.create_stubs({api.neutron: ('network_get', 'subnet_create',)}) - def test_subnet_create_post(self): + def test_subnet_create_post(self, test_with_subnetpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), @@ -970,7 +1045,8 @@ class NetworkSubnetTests(test.TestCase): @test.create_stubs({api.neutron: ('network_get', 'subnet_create',)}) - def test_subnet_create_post_network_exception(self): + def test_subnet_create_post_network_exception(self, + test_with_subnetpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), @@ -978,8 +1054,12 @@ class NetworkSubnetTests(test.TestCase): .AndRaise(self.exceptions.neutron) self.mox.ReplayAll() - form_data = form_data_subnet(subnet, - allocation_pools=[]) + form_data = {} + if test_with_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + form_data.update(form_data_subnet(subnet, allocation_pools=[])) + url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -987,9 +1067,14 @@ class NetworkSubnetTests(test.TestCase): self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) + def test_subnet_create_post_network_exception_with_subnetpool(self): + self.test_subnet_create_post_network_exception( + test_with_subnetpool=True) + @test.create_stubs({api.neutron: ('network_get', 'subnet_create',)}) - def test_subnet_create_post_subnet_exception(self): + def test_subnet_create_post_subnet_exception(self, + test_with_subnetpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), @@ -1005,8 +1090,7 @@ class NetworkSubnetTests(test.TestCase): .AndRaise(self.exceptions.neutron) self.mox.ReplayAll() - form_data = form_data_subnet(subnet, - allocation_pools=[]) + form_data = form_data_subnet(subnet, allocation_pools=[]) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1015,19 +1099,34 @@ class NetworkSubnetTests(test.TestCase): args=[subnet.network_id]) self.assertRedirectsNoFollow(res, redir_url) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_cidr_inconsistent(self): + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_cidr_inconsistent(self, + test_with_subnetpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if test_with_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # dummy IPv6 address cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' - form_data = form_data_subnet(subnet, cidr=cidr, - allocation_pools=[]) + form_data.update(form_data_subnet(subnet, cidr=cidr, + allocation_pools=[])) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1036,37 +1135,74 @@ class NetworkSubnetTests(test.TestCase): self.assertFormErrors(res, 1, expected_msg) self.assertTemplateUsed(res, views.WorkflowView.template_name) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_gw_inconsistent(self): + def test_subnet_create_post_cidr_inconsistent_with_subnetpool(self): + self.test_subnet_create_post_cidr_inconsistent( + test_with_subnetpool=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_gw_inconsistent(self, + test_with_subnetpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if test_with_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # dummy IPv6 address gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' - form_data = form_data_subnet(subnet, gateway_ip=gateway_ip, - allocation_pools=[]) + form_data.update(form_data_subnet(subnet, gateway_ip=gateway_ip, + allocation_pools=[])) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) self.assertContains(res, 'Gateway IP and IP version are inconsistent.') - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_pools_start_only(self): + def test_subnet_create_post_gw_inconsistent_with_subnetpool(self): + self.test_subnet_create_post_gw_inconsistent(test_with_subnetpool=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_pools_start_only(self, + test_w_snpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if test_w_snpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # Start only allocation_pools allocation_pools = '10.0.0.2' - form_data = form_data_subnet(subnet, - allocation_pools=allocation_pools) + form_data.update(form_data_subnet(subnet, + allocation_pools=allocation_pools)) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1075,18 +1211,37 @@ class NetworkSubnetTests(test.TestCase): 'Start and end addresses must be specified ' '(value=%s)' % allocation_pools) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_pools_three_entries(self): + def test_subnet_create_post_invalid_pools_start_only_with_subnetpool(self): + self.test_subnet_create_post_invalid_pools_start_only( + test_w_snpool=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_pools_three_entries(self, + t_w_snpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if t_w_snpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # pool with three entries allocation_pools = '10.0.0.2,10.0.0.3,10.0.0.4' - form_data = form_data_subnet(subnet, - allocation_pools=allocation_pools) + form_data.update(form_data_subnet(subnet, + allocation_pools=allocation_pools)) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1095,18 +1250,37 @@ class NetworkSubnetTests(test.TestCase): 'Start and end addresses must be specified ' '(value=%s)' % allocation_pools) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_pools_invalid_address(self): + def test_subnet_create_post_invalid_pools_three_entries_w_subnetpool(self): + self.test_subnet_create_post_invalid_pools_three_entries( + t_w_snpool=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_pools_invalid_address(self, + t_w_snpl=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if t_w_snpl: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # end address is not a valid IP address allocation_pools = '10.0.0.2,invalid_address' - form_data = form_data_subnet(subnet, - allocation_pools=allocation_pools) + form_data.update(form_data_subnet(subnet, + allocation_pools=allocation_pools)) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1115,18 +1289,37 @@ class NetworkSubnetTests(test.TestCase): 'allocation_pools: Invalid IP address ' '(value=%s)' % allocation_pools.split(',')[1]) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_pools_ip_network(self): + def test_subnet_create_post_invalid_pools_invalid_address_w_snpool(self): + self.test_subnet_create_post_invalid_pools_invalid_address( + t_w_snpl=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_pools_ip_network(self, + test_w_snpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if test_w_snpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # start address is CIDR allocation_pools = '10.0.0.2/24,10.0.0.5' - form_data = form_data_subnet(subnet, - allocation_pools=allocation_pools) + form_data.update(form_data_subnet(subnet, + allocation_pools=allocation_pools)) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1135,14 +1328,33 @@ class NetworkSubnetTests(test.TestCase): 'allocation_pools: Invalid IP address ' '(value=%s)' % allocation_pools.split(',')[0]) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_pools_start_larger_than_end(self): + def test_subnet_create_post_invalid_pools_ip_network_with_subnetpool(self): + self.test_subnet_create_post_invalid_pools_ip_network( + test_w_snpool=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_pools_start_larger_than_end(self, + tsn=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if tsn: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # start address is larger than end address allocation_pools = '10.0.0.254,10.0.0.2' form_data = form_data_subnet(subnet, @@ -1155,18 +1367,38 @@ class NetworkSubnetTests(test.TestCase): 'Start address is larger than end address ' '(value=%s)' % allocation_pools) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_nameservers(self): + def test_subnet_create_post_invalid_pools_start_larger_than_end_tsn(self): + self.test_subnet_create_post_invalid_pools_start_larger_than_end( + tsn=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_nameservers(self, + test_w_subnetpool=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if test_w_subnetpool: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # invalid DNS server address dns_nameservers = ['192.168.0.2', 'invalid_address'] - form_data = form_data_subnet(subnet, dns_nameservers=dns_nameservers, - allocation_pools=[]) + form_data.update(form_data_subnet(subnet, + dns_nameservers=dns_nameservers, + allocation_pools=[])) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1175,19 +1407,39 @@ class NetworkSubnetTests(test.TestCase): 'dns_nameservers: Invalid IP address ' '(value=%s)' % dns_nameservers[1]) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_routes_destination_only(self): + def test_subnet_create_post_invalid_nameservers_with_subnetpool(self): + self.test_subnet_create_post_invalid_nameservers( + test_w_subnetpool=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_routes_destination_only(self, + tsn=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if tsn: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # Start only host_route host_routes = '192.168.0.0/24' - form_data = form_data_subnet(subnet, - allocation_pools=[], - host_routes=host_routes) + form_data.update(form_data_subnet(subnet, + allocation_pools=[], + host_routes=host_routes)) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1197,19 +1449,38 @@ class NetworkSubnetTests(test.TestCase): 'Destination CIDR and nexthop must be specified ' '(value=%s)' % host_routes) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_routes_three_entries(self): + def test_subnet_create_post_invalid_routes_destination_only_w_snpool(self): + self.test_subnet_create_post_invalid_routes_destination_only( + tsn=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_routes_three_entries(self, + tsn=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if tsn: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # host_route with three entries host_routes = 'aaaa,bbbb,cccc' - form_data = form_data_subnet(subnet, - allocation_pools=[], - host_routes=host_routes) + form_data.update(form_data_subnet(subnet, + allocation_pools=[], + host_routes=host_routes)) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1219,19 +1490,38 @@ class NetworkSubnetTests(test.TestCase): 'Destination CIDR and nexthop must be specified ' '(value=%s)' % host_routes) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_routes_invalid_destination(self): + def test_subnet_create_post_invalid_routes_three_entries_with_tsn(self): + self.test_subnet_create_post_invalid_routes_three_entries( + tsn=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_routes_invalid_destination(self, + tsn=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if tsn: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # invalid destination network host_routes = '172.16.0.0/64,10.0.0.253' - form_data = form_data_subnet(subnet, - host_routes=host_routes, - allocation_pools=[]) + form_data.update(form_data_subnet(subnet, + host_routes=host_routes, + allocation_pools=[])) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1240,19 +1530,38 @@ class NetworkSubnetTests(test.TestCase): 'host_routes: Invalid IP address ' '(value=%s)' % host_routes.split(',')[0]) - @test.create_stubs({api.neutron: ('network_get',)}) - def test_subnet_create_post_invalid_routes_nexthop_ip_network(self): + def test_subnet_create_post_invalid_routes_invalid_destination_tsn(self): + self.test_subnet_create_post_invalid_routes_invalid_destination( + tsn=True) + + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'subnetpool_list',)}) + def test_subnet_create_post_invalid_routes_nexthop_ip_network(self, + tsn=False): network = self.networks.first() subnet = self.subnets.first() api.neutron.network_get(IsA(http.HttpRequest), network.id).AndReturn(network) + + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) + self.mox.ReplayAll() + form_data = {} + if tsn: + subnetpool = self.subnetpools.first() + form_data['subnetpool'] = subnetpool.id + # nexthop is not an IP address host_routes = '172.16.0.0/24,10.0.0.253/24' - form_data = form_data_subnet(subnet, - host_routes=host_routes, - allocation_pools=[]) + form_data.update(form_data_subnet(subnet, + host_routes=host_routes, + allocation_pools=[])) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -1261,9 +1570,20 @@ class NetworkSubnetTests(test.TestCase): 'host_routes: Invalid IP address ' '(value=%s)' % host_routes.split(',')[1]) - @test.create_stubs({api.neutron: ('network_get', - 'subnet_create',)}) + def test_subnet_create_post_invalid_routes_nexthop_ip_network_tsn(self): + self.test_subnet_create_post_invalid_routes_nexthop_ip_network( + tsn=True) + + @test.create_stubs({api.neutron: ('is_extension_supported', + 'network_get', + 'subnet_create', + 'subnetpool_list',)}) def test_v6subnet_create_post(self): + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'subnet_allocation').\ + AndReturn(True) + api.neutron.subnetpool_list(IsA(http.HttpRequest)).\ + AndReturn(self.subnetpools.list()) network = self.networks.get(name="v6_net1") subnet = self.subnets.get(name="v6_subnet1") api.neutron.network_get(IsA(http.HttpRequest), diff --git a/openstack_dashboard/dashboards/project/networks/workflows.py b/openstack_dashboard/dashboards/project/networks/workflows.py index 76dea92c2b..7ffd346a28 100644 --- a/openstack_dashboard/dashboards/project/networks/workflows.py +++ b/openstack_dashboard/dashboards/project/networks/workflows.py @@ -50,6 +50,20 @@ class CreateNetworkInfoAction(workflows.Action): required=False, help_text=_("The state to start" " the network in.")) + with_subnet = forms.BooleanField(label=_("Create Subnet"), + widget=forms.CheckboxInput(attrs={ + 'class': 'switchable', + 'data-slug': 'with_subnet', + 'data-hide-tab': 'create_network__' + 'createsubnetinfo' + 'action,' + 'create_network__' + 'createsubnetdetail' + 'action,', + 'data-hide-on-checked': 'false' + }), + initial=True, + required=False) def __init__(self, request, *args, **kwargs): super(CreateNetworkInfoAction, self).__init__(request, @@ -84,35 +98,56 @@ class CreateNetworkInfoAction(workflows.Action): class CreateNetworkInfo(workflows.Step): action_class = CreateNetworkInfoAction - contributes = ("net_name", "admin_state", "net_profile_id") + contributes = ("net_name", "admin_state", "net_profile_id", "with_subnet") class CreateSubnetInfoAction(workflows.Action): - with_subnet = forms.BooleanField(label=_("Create Subnet"), - widget=forms.CheckboxInput(attrs={ - 'class': 'switchable', - 'data-slug': 'with_subnet', - 'data-hide-tab': 'create_network__' - 'createsubnetdetail' - 'action', - 'data-hide-on-checked': 'false' - }), - initial=True, - required=False) subnet_name = forms.CharField(max_length=255, widget=forms.TextInput(attrs={ - 'class': 'switched', - 'data-switch-on': 'with_subnet', }), label=_("Subnet Name"), required=False) + + address_source = forms.ChoiceField( + required=False, + label=_('Network Address Source'), + choices=[('manual', _('Enter Network Address manually')), + ('subnetpool', _('Allocate Network Address from a pool'))], + widget=forms.Select(attrs={ + 'class': 'switchable', + 'data-slug': 'source', + })) + + subnetpool = forms.ChoiceField( + label=_("Address pool"), + widget=forms.SelectWidget(attrs={ + 'class': 'switched switchable', + 'data-slug': 'subnetpool', + 'data-switch-on': 'source', + 'data-source-subnetpool': _('Address pool')}, + data_attrs=('name', 'prefixes', + 'ip_version', + 'min_prefixlen', + 'max_prefixlen', + 'default_prefixlen'), + transform=lambda x: "%s (%s)" % (x.name, ", ".join(x.prefixes)) + if 'prefixes' in x else "%s" % (x.name)), + required=False) + + prefixlen = forms.ChoiceField(widget=forms.Select(attrs={ + 'class': 'switched', + 'data-switch-on': 'subnetpool', + }), + label=_('Network Mask'), + required=False) + cidr = forms.IPField(label=_("Network Address"), required=False, initial="", widget=forms.TextInput(attrs={ 'class': 'switched', - 'data-switch-on': 'with_subnet', - 'data-is-required': 'true' + 'data-switch-on': 'source', + 'data-source-manual': _("Network Address"), }), help_text=_("Network address in CIDR format " "(e.g. 192.168.0.0/24, 2001:DB8::/48)"), @@ -120,16 +155,17 @@ class CreateSubnetInfoAction(workflows.Action): mask=True) ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')], widget=forms.Select(attrs={ - 'class': 'switchable switched', + 'class': 'switchable', 'data-slug': 'ipversion', - 'data-switch-on': 'with_subnet' }), - label=_("IP Version")) + label=_("IP Version"), + required=False) gateway_ip = forms.IPField( label=_("Gateway IP"), widget=forms.TextInput(attrs={ 'class': 'switched', - 'data-switch-on': 'with_subnet gateway_ip' + 'data-switch-on': 'source gateway_ip', + 'data-source-manual': _("Gateway IP") }), required=False, initial="", @@ -145,37 +181,109 @@ class CreateSubnetInfoAction(workflows.Action): mask=False) no_gateway = forms.BooleanField(label=_("Disable Gateway"), widget=forms.CheckboxInput(attrs={ - 'class': 'switched switchable', + 'class': 'switchable', 'data-slug': 'gateway_ip', - 'data-switch-on': 'with_subnet', 'data-hide-on-checked': 'true' }), initial=False, required=False) - msg = _('Specify "Network Address" or ' + msg = _('Specify "Network Address", "Address pool" or ' 'clear "Create Subnet" checkbox.') class Meta(object): name = _("Subnet") - help_text = _('Create a subnet associated with the new network, ' - 'in which case "Network Address" must be specified. ' - 'If you wish to create a network without a subnet, ' - 'uncheck the "Create Subnet" checkbox.') + help_text = _('Create a subnet associated with the network. ' + 'Advanced configuration is available by clicking on the ' + '"Subnet Details" tab.') def __init__(self, request, context, *args, **kwargs): super(CreateSubnetInfoAction, self).__init__(request, context, *args, **kwargs) + if 'with_subnet' in context: + self.fields['with_subnet'] = forms.BooleanField( + initial=context['with_subnet'], + required=False, + widget=forms.HiddenInput() + ) + if not getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}).get('enable_ipv6', True): self.fields['ip_version'].widget = forms.HiddenInput() self.fields['ip_version'].initial = 4 + try: + if api.neutron.is_extension_supported(request, + 'subnet_allocation'): + self.fields['subnetpool'].choices = \ + self.get_subnetpool_choices(request) + else: + self.hide_subnetpool_choices() + except Exception: + self.hide_subnetpool_choices() + msg = _('Unable to initialize subnetpools') + exceptions.handle(request, msg) + if len(self.fields['subnetpool'].choices): + # Pre-populate prefixlen choices to satisfy Django + # ChoiceField Validation. This is overridden w/data from + # subnetpool on select. + self.fields['prefixlen'].choices = \ + zip(list(range(0, 128 + 1)), + list(range(0, 128 + 1))) + # Populate data-fields for switching the prefixlen field + # when user selects a subnetpool other than + # "Provider default pool" + for (id, name) in self.fields['subnetpool'].choices: + if not len(id): + continue + key = 'data-subnetpool-' + id + self.fields['prefixlen'].widget.attrs[key] = \ + _('Network Mask') + else: + self.hide_subnetpool_choices() + + def get_subnetpool_choices(self, request): + subnetpool_choices = [('', _('Select a pool'))] + default_ipv6_subnet_pool_label = \ + getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}).get( + 'default_ipv6_subnet_pool_label', None) + default_ipv4_subnet_pool_label = \ + getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}).get( + 'default_ipv4_subnet_pool_label', None) + + if default_ipv6_subnet_pool_label: + subnetpool_dict = {'ip_version': 6, + 'name': default_ipv6_subnet_pool_label} + subnetpool = api.neutron.SubnetPool(subnetpool_dict) + subnetpool_choices.append(('', subnetpool)) + + if default_ipv4_subnet_pool_label: + subnetpool_dict = {'ip_version': 4, + 'name': default_ipv4_subnet_pool_label} + subnetpool = api.neutron.SubnetPool(subnetpool_dict) + subnetpool_choices.append(('', subnetpool)) + + for subnetpool in api.neutron.subnetpool_list(request): + subnetpool_choices.append((subnetpool.id, subnetpool)) + return subnetpool_choices + + def hide_subnetpool_choices(self): + self.fields['address_source'].widget = forms.HiddenInput() + self.fields['subnetpool'].choices = [] + self.fields['subnetpool'].widget = forms.HiddenInput() + self.fields['prefixlen'].widget = forms.HiddenInput() + def _check_subnet_data(self, cleaned_data, is_create=True): cidr = cleaned_data.get('cidr') ip_version = int(cleaned_data.get('ip_version')) gateway_ip = cleaned_data.get('gateway_ip') no_gateway = cleaned_data.get('no_gateway') - if not cidr: + address_source = cleaned_data.get('address_source') + + # When creating network from a pool it is allowed to supply empty + # subnetpool_id signalling that Neutron should choose the default + # pool configured by the operator. This is also part of the IPv6 + # Prefix Delegation Workflow. + if not cidr and address_source != 'subnetpool': raise forms.ValidationError(self.msg) if cidr: subnet = netaddr.IPNetwork(cidr) @@ -207,8 +315,9 @@ class CreateSubnetInfoAction(workflows.Action): class CreateSubnetInfo(workflows.Step): action_class = CreateSubnetInfoAction - contributes = ("with_subnet", "subnet_name", "cidr", - "ip_version", "gateway_ip", "no_gateway") + contributes = ("subnet_name", "cidr", "ip_version", + "gateway_ip", "no_gateway", "subnetpool", + "prefixlen", "address_source") class CreateSubnetDetailAction(workflows.Action): @@ -421,7 +530,7 @@ class CreateNetwork(workflows.Workflow): try: params = {'network_id': network_id, 'name': data['subnet_name'], - 'cidr': data['cidr'], + 'cidr': data['cidr'] if len(data['cidr']) else None, 'ip_version': int(data['ip_version'])} if tenant_id: params['tenant_id'] = tenant_id @@ -429,6 +538,10 @@ class CreateNetwork(workflows.Workflow): params['gateway_ip'] = None elif data['gateway_ip']: params['gateway_ip'] = data['gateway_ip'] + if 'subnetpool' in data and len(data['subnetpool']): + params['subnetpool_id'] = data['subnetpool'] + if 'prefixlen' in data and len(data['prefixlen']): + params['prefixlen'] = data['prefixlen'] self._setup_subnet_parameters(params, data) diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 7dc13520aa..d3fe85790f 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -238,6 +238,18 @@ OPENSTACK_NEUTRON_NETWORK = { 'enable_vpn': True, 'enable_fip_topology_check': True, + # Neutron can be configured with a default Subnet Pool to be used for IPv4 + # subnet-allocation. Specify the label you wish to display in the Address + # pool selector on the create subnet step if you want to use this feature. + 'default_ipv4_subnet_pool_label': None, + + # Neutron can be configured with a default Subnet Pool to be used for IPv6 + # subnet-allocation. Specify the label you wish to display in the Address + # pool selector on the create subnet step if you want to use this feature. + # You must set this to enable IPv6 Prefix Delegation in a PD-capable + # environment. + 'default_ipv6_subnet_pool_label': None, + # The profile_support option is used to detect if an external router can be # configured via the dashboard. When using specific plugins the # profile_support can be turned on if needed.