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:
parent
d24f00d24e
commit
b4fb18198c
@ -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``
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user