diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 306d704c22..993ffda089 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -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`` diff --git a/openstack_dashboard/dashboards/admin/networks/forms.py b/openstack_dashboard/dashboards/admin/networks/forms.py index 30ca4afb8c..b37233f832 100644 --- a/openstack_dashboard/dashboards/admin/networks/forms.py +++ b/openstack_dashboard/dashboards/admin/networks/forms.py @@ -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) diff --git a/openstack_dashboard/dashboards/admin/networks/tests.py b/openstack_dashboard/dashboards/admin/networks/tests.py index aa259ce3d8..603b16ff5c 100644 --- a/openstack_dashboard/dashboards/admin/networks/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/tests.py @@ -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, '', 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() diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 4e6e442803..595bef4622 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -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 diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index f7221998b5..3ec5f90f47 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -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",