Merge "Support new IPv6 subnet modes in Neutron"

This commit is contained in:
Jenkins 2014-09-08 07:36:18 +00:00 committed by Gerrit Code Review
commit e7b8b19620
9 changed files with 246 additions and 10 deletions

View File

@ -232,7 +232,7 @@ horizon.addInitFunction(function () {
visible = $switchable.is(':visible'),
val = $switchable.val();
$fieldset.find('.switched[data-switch-on*="' + slug + '"]').each(function(index, input){
function handle_switched_field(index, input){
var $input = $(input),
data = $input.data(slug + "-" + val);
@ -242,7 +242,10 @@ horizon.addInitFunction(function () {
$('label[for=' + $input.attr('id') + ']').html(data);
$input.closest('.form-group').show();
}
});
}
$fieldset.find('.switched[data-switch-on*="' + slug + '"]').each(handle_switched_field);
$fieldset.siblings().find('.switched[data-switch-on*="' + slug + '"]').each(handle_switched_field);
});
});

View File

@ -19,6 +19,7 @@ from horizon import exceptions
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.networks.subnets import utils
class OverviewTab(tabs.Tab):
@ -34,6 +35,10 @@ class OverviewTab(tabs.Tab):
redirect = reverse('horizon:project:networks:index')
msg = _('Unable to retrieve subnet details.')
exceptions.handle(request, msg, redirect=redirect)
if subnet.ip_version == 6:
ipv6_modes = utils.get_ipv6_modes_menu_from_attrs(
subnet.ipv6_ra_mode, subnet.ipv6_address_mode)
subnet.ipv6_modes_desc = utils.IPV6_MODE_MAP.get(ipv6_modes)
return {'subnet': subnet}

View File

@ -0,0 +1,41 @@
# Copyright 2014 NEC Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
IPV6_DEFAULT_MODE = 'none/none'
IPV6_MODE_CHOICES = [
('none/none',
_('No options specified')),
('slaac/slaac',
_('SLAAC: Address discovered from Openstack Router')),
('dhcpv6-stateful/dhcpv6-stateful',
_('DHCPv6 stateful: Address discovered from Openstack DHCP')),
('dhcpv6-stateless/dhcpv6-stateless',
_('DHCPv6 stateless: Address discovered from Openstack Router '
'and info from Openstack DHCP')),
]
IPV6_MODE_MAP = dict(IPV6_MODE_CHOICES)
def get_ipv6_modes_menu_from_attrs(ipv6_ra_mode, ipv6_address_mode):
ipv6_modes = '%s/%s' % (str(ipv6_ra_mode).lower(),
str(ipv6_address_mode).lower())
if ipv6_modes in IPV6_MODE_MAP:
return ipv6_modes
def get_ipv6_modes_attrs_from_menu(ipv6_modes):
return [None if mode == 'none' else mode
for mode in ipv6_modes.split('/', 1)]

View File

@ -27,6 +27,7 @@ from openstack_dashboard import api
from openstack_dashboard.dashboards.project.networks.subnets \
import tabs as project_tabs
from openstack_dashboard.dashboards.project.networks.subnets import utils
from openstack_dashboard.dashboards.project.networks.subnets \
import workflows as project_workflows
@ -80,6 +81,10 @@ class UpdateView(workflows.WorkflowView):
initial['gateway_ip'] = subnet['gateway_ip'] or ''
initial['no_gateway'] = (subnet['gateway_ip'] is None)
if initial['ip_version'] == 6:
initial['ipv6_modes'] = utils.get_ipv6_modes_menu_from_attrs(
subnet['ipv6_ra_mode'], subnet['ipv6_address_mode'])
initial['dns_nameservers'] = '\n'.join(subnet['dns_nameservers'])
pools = ['%s,%s' % (p['start'], p['end'])
for p in subnet['allocation_pools']]

View File

@ -132,6 +132,19 @@ class UpdateSubnetDetailAction(network_workflows.CreateSubnetDetailAction):
allocation_pools = forms.CharField(widget=forms.HiddenInput(),
required=False)
def __init__(self, request, context, *args, **kwargs):
super(UpdateSubnetDetailAction, self).__init__(request, context,
*args, **kwargs)
# TODO(amotoki): Due to Neutron bug 1362966, we cannot pass "None"
# to Neutron. It means we cannot set IPv6 two modes to
# "No option selected".
# Until bug 1362966 is fixed, we disable this field.
# if context['ip_version'] != 6:
# self.fields['ipv6_modes'].widget = forms.HiddenInput()
# self.fields['ipv6_modes'].required = False
self.fields['ipv6_modes'].widget = forms.HiddenInput()
self.fields['ipv6_modes'].required = False
class Meta:
name = _("Subnet Detail")
help_text = _('Specify additional attributes for the subnet.')

View File

@ -4,7 +4,6 @@
<h3>{% trans "Subnet Overview" %}</h3>
<div class="info row detail">
<h4>{% trans "Subnet" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Name" %}</dt>
@ -25,10 +24,20 @@
{% trans " - End" %} {{ pool.end }}<br>
{% endfor %}
</dd>
<dt>{% trans "DHCP Enable" %}<dt>
<dd>{{ subnet.enable_dhcp|yesno|capfirst }}</dd>
<dt>{% trans "Gateway IP" %}</dt>
<dd>{{ subnet.gateway_ip|default:"-" }}</dd>
<dt>{% trans "DHCP Enable" %}<dt>
<dd>{{ subnet.enable_dhcp|yesno|capfirst }}</dd>
{% if subnet.ip_version == 6 %}
<dt>{% trans "IPv6 Address Configuration Mode" %}<dt>
{% if subnet.ipv6_modes_desc %}
<dd>{{ subnet.ipv6_modes_desc }}</dd>
{% else %}
<dd>{% blocktrans with ra_mode=subnet.ipv6_ra_mode addr_mode=subnet.ipv6_address_mode %}
Other IPv6 modes: ipv6_ra_mode={{ ra_mode }}, ipv6_address_mode={{ addr_mode }}
{% endblocktrans %}</dd>
{% endif %}
{% endif %}
<dt>{% trans "Additional routes" %}</dt>
<dd>
{% for route in subnet.host_routes %}

View File

@ -48,6 +48,8 @@ def form_data_subnet(subnet,
data['no_gateway'] = (gateway_ip is None)
data['enable_dhcp'] = get_value(enable_dhcp, subnet.enable_dhcp)
if data['ip_version'] == 6:
data['ipv6_modes'] = subnet.ipv6_modes
pools = get_value(allocation_pools, subnet.allocation_pools)
data['allocation_pools'] = _str_allocation_pools(pools)
@ -1211,6 +1213,66 @@ 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_v6subnet_create_post(self):
network = self.networks.get(name="v6_net1")
subnet = self.subnets.get(name="v6_subnet1")
api.neutron.network_get(IsA(http.HttpRequest),
network.id)\
.AndReturn(network)
api.neutron.subnet_create(IsA(http.HttpRequest),
network_id=network.id,
name=subnet.name,
cidr=subnet.cidr,
ip_version=subnet.ip_version,
gateway_ip=subnet.gateway_ip,
enable_dhcp=subnet.enable_dhcp,
allocation_pools=subnet.allocation_pools)\
.AndReturn(subnet)
self.mox.ReplayAll()
form_data = form_data_subnet(subnet)
url = reverse('horizon:project:networks:addsubnet',
args=[subnet.network_id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
redir_url = reverse('horizon:project:networks:detail',
args=[subnet.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('network_get',
'subnet_create',)})
def test_v6subnet_create_post_with_slaac_attributes(self):
network = self.networks.get(name="v6_net2")
subnet = self.subnets.get(name="v6_subnet2")
api.neutron.network_get(IsA(http.HttpRequest),
network.id)\
.AndReturn(network)
api.neutron.subnet_create(IsA(http.HttpRequest),
network_id=network.id,
name=subnet.name,
cidr=subnet.cidr,
ip_version=subnet.ip_version,
gateway_ip=subnet.gateway_ip,
enable_dhcp=subnet.enable_dhcp,
allocation_pools=subnet.allocation_pools,
ipv6_address_mode='slaac',
ipv6_ra_mode='slaac')\
.AndReturn(subnet)
self.mox.ReplayAll()
form_data = form_data_subnet(subnet)
url = reverse('horizon:project:networks:addsubnet',
args=[subnet.network_id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
redir_url = reverse('horizon:project:networks:detail',
args=[subnet.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('subnet_update',
'subnet_get',)})
def test_subnet_update_post(self):

View File

@ -26,6 +26,7 @@ from horizon import messages
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.networks.subnets import utils
LOG = logging.getLogger(__name__)
@ -91,10 +92,14 @@ class CreateSubnetInfoAction(workflows.Action):
required=False,
initial="",
help_text=_("Network address in CIDR format "
"(e.g. 192.168.0.0/24)"),
"(e.g. 192.168.0.0/24, 2001:DB8::/48)"),
version=forms.IPv4 | forms.IPv6,
mask=True)
ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')],
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'ipversion',
}),
label=_("IP Version"))
gateway_ip = forms.IPField(
label=_("Gateway IP"),
@ -102,8 +107,9 @@ class CreateSubnetInfoAction(workflows.Action):
initial="",
help_text=_("IP address of Gateway (e.g. 192.168.0.254) "
"The default value is the first IP of the "
"network address (e.g. 192.168.0.1 for "
"192.168.0.0/24). "
"network address "
"(e.g. 192.168.0.1 for 192.168.0.0/24, "
"2001:DB8::1 for 2001:DB8::/48). "
"If you use the default, leave blank. "
"If you do not want to use a gateway, "
"check 'Disable Gateway' below."),
@ -173,6 +179,21 @@ class CreateSubnetInfo(workflows.Step):
class CreateSubnetDetailAction(workflows.Action):
enable_dhcp = forms.BooleanField(label=_("Enable DHCP"),
initial=True, required=False)
ipv6_modes = forms.ChoiceField(
label=_("IPv6 Address Configuration Mode"),
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'ipversion',
'data-ipversion-6': _("IPv6 Address Configuration Mode"),
}),
initial=utils.IPV6_DEFAULT_MODE,
required=False,
help_text=_("It specifies how IPv6 address and additional information "
"are configured. We can specify SLAAC/DHCPv6 stateful/"
"DHCPv6 stateless provided by OpenStack, "
"or specify no option. "
"'No option selected' means addresses are configured "
"manually or configured by non-OpenStack system."))
allocation_pools = forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}),
label=_("Allocation Pools"),
@ -200,6 +221,12 @@ class CreateSubnetDetailAction(workflows.Action):
name = _("Subnet Detail")
help_text = _('Specify additional attributes for the subnet.')
def populate_ipv6_modes_choices(self, request, context):
return [(value, _("%s (Default)") % label)
if value == utils.IPV6_DEFAULT_MODE
else (value, label)
for value, label in utils.IPV6_MODE_CHOICES]
def _convert_ip_address(self, ip, field_name):
try:
return netaddr.IPAddress(ip)
@ -264,7 +291,7 @@ class CreateSubnetDetailAction(workflows.Action):
class CreateSubnetDetail(workflows.Step):
action_class = CreateSubnetDetailAction
contributes = ("enable_dhcp", "allocation_pools",
contributes = ("enable_dhcp", "ipv6_modes", "allocation_pools",
"dns_nameservers", "host_routes")
@ -317,6 +344,13 @@ class CreateNetwork(workflows.Workflow):
"""
is_update = not is_create
params['enable_dhcp'] = data['enable_dhcp']
if int(data['ip_version']) == 6:
ipv6_modes = utils.get_ipv6_modes_attrs_from_menu(
data['ipv6_modes'])
if ipv6_modes[0] or is_update:
params['ipv6_ra_mode'] = ipv6_modes[0]
if ipv6_modes[1] or is_update:
params['ipv6_address_mode'] = ipv6_modes[1]
if is_create and data['allocation_pools']:
pools = [dict(zip(['start', 'end'], pool.strip().split(',')))
for pool in data['allocation_pools'].split('\n')

View File

@ -258,6 +258,70 @@ def data(TEST):
TEST.networks.add(neutron.Network(network))
TEST.subnets.add(subnet)
# 1st v6 network.
network_dict = {'admin_state_up': True,
'id': '96688ea1-ffa5-78ec-22ca-33aaabfaf775',
'name': 'v6_net1',
'status': 'ACTIVE',
'subnets': ['88ddd443-4377-ab1f-87dd-4bc4a662dbb6'],
'tenant_id': '1',
'router:external': False,
'shared': False}
subnet_dict = {'allocation_pools': [{'end': 'ff09::ff',
'start': 'ff09::02'}],
'dns_nameservers': [],
'host_routes': [],
'cidr': 'ff09::/64',
'enable_dhcp': True,
'gateway_ip': 'ff09::1',
'id': network_dict['subnets'][0],
'ip_version': 6,
'name': 'v6_subnet1',
'network_id': network_dict['id'],
'tenant_id': network_dict['tenant_id'],
'ipv6_modes': 'none/none'}
TEST.api_networks.add(network_dict)
TEST.api_subnets.add(subnet_dict)
network = copy.deepcopy(network_dict)
subnet = neutron.Subnet(subnet_dict)
network['subnets'] = [subnet]
TEST.networks.add(neutron.Network(network))
TEST.subnets.add(subnet)
# 2nd v6 network - slaac.
network_dict = {'admin_state_up': True,
'id': 'c62e4bb3-296a-4cd1-8f6b-aaa7a0092326',
'name': 'v6_net2',
'status': 'ACTIVE',
'subnets': ['5d736a21-0036-4779-8f8b-eed5f98077ec'],
'tenant_id': '1',
'router:external': False,
'shared': False}
subnet_dict = {'allocation_pools': [{'end': 'ff09::ff',
'start': 'ff09::02'}],
'dns_nameservers': [],
'host_routes': [],
'cidr': 'ff09::/64',
'enable_dhcp': True,
'gateway_ip': 'ff09::1',
'id': network_dict['subnets'][0],
'ip_version': 6,
'name': 'v6_subnet2',
'network_id': network_dict['id'],
'tenant_id': network_dict['tenant_id'],
'ipv6_modes': 'slaac/slaac'}
TEST.api_networks.add(network_dict)
TEST.api_subnets.add(subnet_dict)
network = copy.deepcopy(network_dict)
subnet = neutron.Subnet(subnet_dict)
network['subnets'] = [subnet]
TEST.networks.add(neutron.Network(network))
TEST.subnets.add(subnet)
# Set up router data.
port_dict = {'admin_state_up': True,
'device_id': '7180cede-bcd8-4334-b19f-f7ef2f331f53',
@ -267,7 +331,7 @@ def data(TEST):
'id': '44ec6726-4bdc-48c5-94d4-df8d1fbf613b',
'mac_address': 'fa:16:3e:9c:d5:7e',
'name': '',
'network_id': network_dict['id'],
'network_id': TEST.networks.get(name="ext_net")['id'],
'status': 'ACTIVE',
'tenant_id': '1'}
TEST.api_ports.add(port_dict)