Support provider network extension when creating network

This adds the ability for admins to set the provider network
settings when creating a network. This includes the network type,
physical network, and segmentation ID. These options will only be
available if the provider network neutron extension is supported.

Change-Id: I6108504a92a32f067cf151ec103171a7bbb601df
Implements: blueprint quantum-network-provider-types
Closes-Bug: 1223360
This commit is contained in:
Justin Pomeroy 2014-02-17 08:49:42 -06:00
parent d24f00d24e
commit b4fb18198c
5 changed files with 336 additions and 12 deletions

View File

@ -493,7 +493,54 @@ by cinder. Currently only the backup service is available.
Default: ``{'enable_lb': False}``
A dictionary of settings which can be used to enable optional services provided
by neutron. Currently only the load balancer service is available.
by neutron and configure neutron specific features. The following options are
available.
.. enable_lb:
``enable_lb``
-------------
.. versionadded:: 2013.2(Havana)
Default: ``False``
Enable or disable the load balancer service.
.. supported_provider_types:
``supported_provider_types``
----------------------------
.. versionadded:: 2014.2(Juno)
Default: ``["*"]``
For use with the provider network extension. Use this to explicitly set which
provider network types are supported. Only the network types in this list will
be available to choose from when creating a network. Network types include
local, flat, vlan, gre, and vxlan. By default all provider network types will
be available to choose from.
Example: ``['local', 'flat', 'gre']``
.. segmentation_id_range:
``segmentation_id_range``
-------------------------
.. versionadded:: 2014.2(Juno)
Default: ``None``
For use with the provider network extension. This is a dictionary where each
key is a provider network type and each value is a list containing two numbers.
The first number is the minimum segmentation ID that is valid. The second
number is the maximum segmentation ID. Pertains only to the vlan, gre, and
vxlan network types. By default this option is not provided and each minimum
and maximum value will be the default for the provider network type.
Example: ``{'vlan': [1024, 2048], 'gre': [4094, 65536]}``
``OPENSTACK_SSL_CACERT``

View File

@ -14,6 +14,7 @@
import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
@ -25,6 +26,10 @@ from openstack_dashboard import api
LOG = logging.getLogger(__name__)
PROVIDER_TYPES = [('local', _('Local')), ('flat', _('Flat')),
('vlan', 'VLAN'), ('gre', 'GRE'), ('vxlan', 'VXLAN')]
SEGMENTATION_ID_RANGE = {'vlan': [1, 4094], 'gre': [0, (2 ** 32) - 1],
'vxlan': [0, (2 ** 24) - 1]}
class CreateNetwork(forms.SelfHandlingForm):
@ -39,6 +44,35 @@ class CreateNetwork(forms.SelfHandlingForm):
net_profile_id = forms.ChoiceField(label=_("Network Profile"),
required=False,
widget=widget)
network_type = forms.ChoiceField(
label=_("Provider Network Type"),
help_text=_("The physical mechanism by which the virtual "
"network is implemented."),
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'network_type'
}))
physical_network = forms.CharField(
max_length="255",
label=_("Physical Network"),
help_text=_("The name of the physical network over which the "
"virtual network is implemented."),
initial='default',
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'network_type',
'data-network_type-flat': _('Physical Network'),
'data-network_type-vlan': _('Physical Network')
}))
segmentation_id = forms.IntegerField(
label=_("Segmentation ID"),
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'network_type',
'data-network_type-vlan': _('Segmentation ID'),
'data-network_type-gre': _('Segmentation ID'),
'data-network_type-vxlan': _('Segmentation ID')
}))
admin_state = forms.BooleanField(label=_("Admin State"),
initial=True, required=False)
shared = forms.BooleanField(label=_("Shared"),
@ -63,6 +97,46 @@ class CreateNetwork(forms.SelfHandlingForm):
self.fields['net_profile_id'].choices = (
self.get_network_profile_choices(request))
if api.neutron.is_extension_supported(request, 'provider'):
neutron_settings = getattr(settings,
'OPENSTACK_NEUTRON_NETWORK', {})
seg_id_range = neutron_settings.get('segmentation_id_range', {})
self.seg_id_range = {
'vlan': seg_id_range.get('vlan',
SEGMENTATION_ID_RANGE.get('vlan')),
'gre': seg_id_range.get('gre',
SEGMENTATION_ID_RANGE.get('gre')),
'vxlan': seg_id_range.get('vxlan',
SEGMENTATION_ID_RANGE.get('vxlan'))
}
seg_id_help = (_("For VLAN networks, the VLAN VID on the physical "
"network that realizes the virtual network. Valid VLAN VIDs "
"are %(vlan_min)s through %(vlan_max)s. For GRE or VXLAN "
"networks, the tunnel ID. Valid tunnel IDs for GRE networks "
"are %(gre_min)s through %(gre_max)s. For VXLAN networks, "
"%(vxlan_min)s through %(vxlan_max)s.") % {
'vlan_min': self.seg_id_range['vlan'][0],
'vlan_max': self.seg_id_range['vlan'][1],
'gre_min': self.seg_id_range['gre'][0],
'gre_max': self.seg_id_range['gre'][1],
'vxlan_min': self.seg_id_range['vxlan'][0],
'vxlan_max': self.seg_id_range['vxlan'][1]})
self.fields['segmentation_id'].help_text = seg_id_help
supported_provider_types = neutron_settings.get(
'supported_provider_types', ['*'])
if supported_provider_types == ['*']:
network_type_choices = PROVIDER_TYPES
else:
network_type_choices = [net_type for net_type in
PROVIDER_TYPES if net_type[0] in supported_provider_types]
if len(network_type_choices) == 0:
self._hide_provider_network_type()
else:
self.fields['network_type'].choices = network_type_choices
else:
self._hide_provider_network_type()
def get_network_profile_choices(self, request):
profile_choices = [('', _("Select a profile"))]
for profile in self._get_profiles(request, 'network'):
@ -78,6 +152,14 @@ class CreateNetwork(forms.SelfHandlingForm):
exceptions.handle(request, msg)
return profiles
def _hide_provider_network_type(self):
self.fields['network_type'].widget = forms.HiddenInput()
self.fields['physical_network'].widget = forms.HiddenInput()
self.fields['segmentation_id'].widget = forms.HiddenInput()
self.fields['network_type'].required = False
self.fields['physical_network'].required = False
self.fields['segmentation_id'].required = False
def handle(self, request, data):
try:
params = {'name': data['name'],
@ -87,6 +169,15 @@ class CreateNetwork(forms.SelfHandlingForm):
'router:external': data['external']}
if api.neutron.is_port_profiles_supported():
params['net_profile_id'] = data['net_profile_id']
if api.neutron.is_extension_supported(request, 'provider'):
network_type = data['network_type']
params['provider:network_type'] = network_type
if network_type in ['flat', 'vlan']:
params['provider:physical_network'] = (
data['physical_network'])
if network_type in ['vlan', 'gre', 'vxlan']:
params['provider:segmentation_id'] = (
data['segmentation_id'])
network = api.neutron.network_create(request, **params)
msg = _('Network %s was successfully created.') % data['name']
LOG.debug(msg)
@ -97,6 +188,43 @@ class CreateNetwork(forms.SelfHandlingForm):
msg = _('Failed to create network %s') % data['name']
exceptions.handle(request, msg, redirect=redirect)
def clean(self):
cleaned_data = super(CreateNetwork, self).clean()
self._clean_physical_network(cleaned_data)
self._clean_segmentation_id(cleaned_data)
return cleaned_data
def _clean_physical_network(self, data):
network_type = data.get('network_type')
if 'physical_network' in self._errors and (
network_type in ['local', 'gre']):
# In this case the physical network is not required, so we can
# ignore any errors.
del self._errors['physical_network']
def _clean_segmentation_id(self, data):
network_type = data.get('network_type')
if 'segmentation_id' in self._errors:
if network_type in ['local', 'flat']:
# In this case the segmentation ID is not required, so we can
# ignore any errors.
del self._errors['segmentation_id']
elif network_type in ['vlan', 'gre', 'vxlan']:
seg_id = data.get('segmentation_id')
seg_id_range = {'min': self.seg_id_range[network_type][0],
'max': self.seg_id_range[network_type][1]}
if seg_id < seg_id_range['min'] or seg_id > seg_id_range['max']:
if network_type == 'vlan':
msg = _('For VLAN networks, valid VLAN IDs are %(min)s '
'through %(max)s.') % seg_id_range
elif network_type == 'gre':
msg = _('For GRE networks, valid tunnel IDs are %(min)s '
'through %(max)s.') % seg_id_range
elif network_type == 'vxlan':
msg = _('For VXLAN networks, valid tunnel IDs are %(min)s '
'through %(max)s.') % seg_id_range
self._errors['segmentation_id'] = self.error_class([msg])
class UpdateNetwork(forms.SelfHandlingForm):
name = forms.CharField(label=_("Name"), required=False)

View File

@ -241,17 +241,21 @@ class NetworkTests(test.BaseAdminViewTests):
self.assertItemsEqual(subnets, [self.subnets.first()])
self.assertEqual(len(ports), 0)
@test.create_stubs({api.neutron: ('profile_list',),
@test.create_stubs({api.neutron: ('profile_list',
'list_extensions',),
api.keystone: ('tenant_list',)})
def test_network_create_get(self,
test_with_profile=False):
tenants = self.tenants.list()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(
http.HttpRequest)).AndReturn([tenants, False])
if test_with_profile:
net_profiles = self.net_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
'network').AndReturn(net_profiles)
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
self.mox.ReplayAll()
url = reverse('horizon:admin:networks:create')
@ -264,26 +268,31 @@ class NetworkTests(test.BaseAdminViewTests):
self.test_network_create_get(test_with_profile=True)
@test.create_stubs({api.neutron: ('network_create',
'profile_list',),
'profile_list',
'list_extensions',),
api.keystone: ('tenant_list',)})
def test_network_create_post(self,
test_with_profile=False):
tenants = self.tenants.list()
tenant_id = self.tenants.first().id
network = self.networks.first()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest))\
.AndReturn([tenants, False])
params = {'name': network.name,
'tenant_id': tenant_id,
'admin_state_up': network.admin_state_up,
'router:external': True,
'shared': True}
'shared': True,
'provider:network_type': 'local'}
if test_with_profile:
net_profiles = self.net_profiles.list()
net_profile_id = self.net_profiles.first().id
api.neutron.profile_list(IsA(http.HttpRequest),
'network').AndReturn(net_profiles)
params['net_profile_id'] = net_profile_id
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
api.neutron.network_create(IsA(http.HttpRequest), **params)\
.AndReturn(network)
self.mox.ReplayAll()
@ -292,7 +301,8 @@ class NetworkTests(test.BaseAdminViewTests):
'name': network.name,
'admin_state': network.admin_state_up,
'external': True,
'shared': True}
'shared': True,
'network_type': 'local'}
if test_with_profile:
form_data['net_profile_id'] = net_profile_id
url = reverse('horizon:admin:networks:create')
@ -306,35 +316,41 @@ class NetworkTests(test.BaseAdminViewTests):
self.test_network_create_post(test_with_profile=True)
@test.create_stubs({api.neutron: ('network_create',
'profile_list',),
'profile_list',
'list_extensions',),
api.keystone: ('tenant_list',)})
def test_network_create_post_network_exception(self,
test_with_profile=False):
tenants = self.tenants.list()
tenant_id = self.tenants.first().id
network = self.networks.first()
api.keystone.tenant_list(IsA(http.HttpRequest))\
.AndReturn([tenants, False])
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([tenants,
False])
params = {'name': network.name,
'tenant_id': tenant_id,
'admin_state_up': network.admin_state_up,
'router:external': True,
'shared': False}
'shared': False,
'provider:network_type': 'local'}
if test_with_profile:
net_profiles = self.net_profiles.list()
net_profile_id = self.net_profiles.first().id
api.neutron.profile_list(IsA(http.HttpRequest),
'network').AndReturn(net_profiles)
params['net_profile_id'] = net_profile_id
api.neutron.network_create(IsA(http.HttpRequest), **params)\
.AndRaise(self.exceptions.neutron)
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
api.neutron.network_create(IsA(http.HttpRequest),
**params).AndRaise(self.exceptions.neutron)
self.mox.ReplayAll()
form_data = {'tenant_id': tenant_id,
'name': network.name,
'admin_state': network.admin_state_up,
'external': True,
'shared': False}
'shared': False,
'network_type': 'local'}
if test_with_profile:
form_data['net_profile_id'] = net_profile_id
url = reverse('horizon:admin:networks:create')
@ -348,6 +364,131 @@ class NetworkTests(test.BaseAdminViewTests):
self.test_network_create_post_network_exception(
test_with_profile=True)
@test.create_stubs({api.neutron: ('list_extensions',),
api.keystone: ('tenant_list',)})
def test_network_create_vlan_segmentation_id_invalid(self):
tenants = self.tenants.list()
tenant_id = self.tenants.first().id
network = self.networks.first()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([tenants,
False])
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
self.mox.ReplayAll()
form_data = {'tenant_id': tenant_id,
'name': network.name,
'admin_state': network.admin_state_up,
'external': True,
'shared': False,
'network_type': 'vlan',
'physical_network': 'default',
'segmentation_id': 4095}
url = reverse('horizon:admin:networks:create')
res = self.client.post(url, form_data)
self.assertFormErrors(res, 1)
self.assertContains(res, "1 through 4094")
@test.create_stubs({api.neutron: ('list_extensions',),
api.keystone: ('tenant_list',)})
def test_network_create_gre_segmentation_id_invalid(self):
tenants = self.tenants.list()
tenant_id = self.tenants.first().id
network = self.networks.first()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([tenants,
False])
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
self.mox.ReplayAll()
form_data = {'tenant_id': tenant_id,
'name': network.name,
'admin_state': network.admin_state_up,
'external': True,
'shared': False,
'network_type': 'gre',
'physical_network': 'default',
'segmentation_id': (2 ** 32) + 1}
url = reverse('horizon:admin:networks:create')
res = self.client.post(url, form_data)
self.assertFormErrors(res, 1)
self.assertContains(res, "0 through %s" % ((2 ** 32) - 1))
@test.create_stubs({api.neutron: ('list_extensions',),
api.keystone: ('tenant_list',)})
@override_settings(OPENSTACK_NEUTRON_NETWORK={
'segmentation_id_range': {'vxlan': [10, 20]}})
def test_network_create_vxlan_segmentation_id_custom(self):
tenants = self.tenants.list()
tenant_id = self.tenants.first().id
network = self.networks.first()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([tenants,
False])
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
self.mox.ReplayAll()
form_data = {'tenant_id': tenant_id,
'name': network.name,
'admin_state': network.admin_state_up,
'external': True,
'shared': False,
'network_type': 'vxlan',
'physical_network': 'default',
'segmentation_id': 9}
url = reverse('horizon:admin:networks:create')
res = self.client.post(url, form_data)
self.assertFormErrors(res, 1)
self.assertContains(res, "10 through 20")
@test.create_stubs({api.neutron: ('list_extensions',),
api.keystone: ('tenant_list',)})
@override_settings(OPENSTACK_NEUTRON_NETWORK={
'supported_provider_types': []})
def test_network_create_no_provider_types(self):
tenants = self.tenants.list()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([tenants,
False])
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
self.mox.ReplayAll()
url = reverse('horizon:admin:networks:create')
res = self.client.get(url)
self.assertTemplateUsed(res, 'admin/networks/create.html')
self.assertContains(res, '<input type="hidden" name="network_type" '
'id="id_network_type" />', html=True)
@test.create_stubs({api.neutron: ('list_extensions',),
api.keystone: ('tenant_list',)})
@override_settings(OPENSTACK_NEUTRON_NETWORK={
'supported_provider_types': ['local', 'flat', 'gre']})
def test_network_create_unsupported_provider_types(self):
tenants = self.tenants.list()
extensions = self.api_extensions.list()
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([tenants,
False])
api.neutron.list_extensions(
IsA(http.HttpRequest)).AndReturn(extensions)
self.mox.ReplayAll()
url = reverse('horizon:admin:networks:create')
res = self.client.get(url)
self.assertTemplateUsed(res, 'admin/networks/create.html')
network_type = res.context['form'].fields['network_type']
self.assertListEqual(list(network_type.choices), [('local', 'Local'),
('flat', 'Flat'),
('gre', 'GRE')])
@test.create_stubs({api.neutron: ('network_get',)})
def test_network_update_get(self):
network = self.networks.first()

View File

@ -187,6 +187,10 @@ OPENSTACK_NEUTRON_NETWORK = {
# profile_support can be turned on if needed.
'profile_support': None,
#'profile_support': 'cisco',
# Set which provider network types are supported. Only the network types
# in this list will be available to choose from when creating a network.
# Network types include local, flat, vlan, gre, and vxlan.
'supported_provider_types': ['*'],
}
# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features

View File

@ -563,8 +563,12 @@ def data(TEST):
extension_2 = {"name": "Quota management support",
"alias": "quotas",
"description": "Expose functions for quotas management"}
extension_3 = {"name": "Provider network",
"alias": "provider",
"description": "Provider network extension"}
TEST.api_extensions.add(extension_1)
TEST.api_extensions.add(extension_2)
TEST.api_extensions.add(extension_3)
# 1st agent.
agent_dict = {"binary": "neutron-openvswitch-agent",