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 += "" + i;
+ if (i == defaultPrefixLen) {
+ optionsAsString += " (" + gettext("pool default") + ")";
+ }
+ 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.