Merge "Support new IPv6 subnet modes in Neutron"
This commit is contained in:
commit
e7b8b19620
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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}
|
||||
|
||||
|
||||
|
@ -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)]
|
@ -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']]
|
||||
|
@ -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.')
|
||||
|
@ -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 %}
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user