Merge "Support create subnet w/Neutron subnet allocation"

This commit is contained in:
Jenkins 2015-09-02 06:37:10 +00:00 committed by Gerrit Code Review
commit 07d549c299
8 changed files with 658 additions and 105 deletions

View File

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

View File

@ -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 += "<option value='" + i + "'>" + i;
if (i == defaultPrefixLen) {
optionsAsString += " (" + gettext("pool default") + ")";
}
optionsAsString += "</option>";
}
$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');

View File

@ -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):

View File

@ -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="",

View File

@ -14,6 +14,12 @@
{% url 'horizon:project:networks:detail' subnet.network_id as network_url %}
<dt>{% trans "Network ID" %}</dt>
<dd><a href="{{ network_url }}">{{ subnet.network_id|default:_("None") }}</a></dd>
<dt>{% trans "Subnetpool" %}</dt>
{% if subnet.subnetpool_id %}
<dd>{{ subnet.subnetpool_name }} ({{ subnet.subnetpool_id }})</dd>
{% else %}
<dd>{% trans "None" %}</dd>
{% endif %}
<dt>{% trans "IP version" %}</dt>
<dd>{{ subnet.ipver_str|default:_("-") }}</dd>
<dt>{% trans "CIDR" %}</dt>

View File

@ -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),

View File

@ -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)

View File

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