diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index e27770eb25..31bfa5c556 100755 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -797,6 +797,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 @@ -967,6 +969,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 0213edfc39..7bab6c3adb 100644 --- a/openstack_dashboard/dashboards/project/networks/tests.py +++ b/openstack_dashboard/dashboards/project/networks/tests.py @@ -463,6 +463,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() @@ -499,7 +500,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, @@ -514,6 +517,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), @@ -546,9 +554,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: @@ -556,6 +567,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, @@ -563,12 +579,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( @@ -577,10 +597,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() @@ -589,13 +616,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') @@ -610,10 +646,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() @@ -622,6 +665,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 @@ -631,6 +680,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') @@ -645,10 +698,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() @@ -657,6 +717,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 @@ -666,6 +732,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') @@ -679,6 +749,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() @@ -834,7 +908,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() @@ -883,7 +958,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), @@ -972,7 +1047,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), @@ -980,8 +1056,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) @@ -989,9 +1069,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), @@ -1007,8 +1092,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) @@ -1017,19 +1101,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) @@ -1038,37 +1137,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) @@ -1077,18 +1213,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) @@ -1097,18 +1252,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) @@ -1117,18 +1291,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) @@ -1137,14 +1330,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, @@ -1157,18 +1369,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) @@ -1177,19 +1409,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) @@ -1199,19 +1451,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) @@ -1221,19 +1492,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) @@ -1242,19 +1532,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) @@ -1263,9 +1572,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 5f4db63277..3a8f9558a0 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -225,6 +225,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.