Support Address Group CRUD as extensions

Add support for basic address group CRUD. Subsequent patches will be added to
use address groups in security group rules.

Implements: blueprint address-groups-in-sg-rules
Change-Id: I4555c068ec6229b1d7ac1168d5687549370893b4
This commit is contained in:
Hang Yang 2020-06-26 15:16:37 -05:00
parent b425ca45dd
commit dd20cab371
14 changed files with 643 additions and 5 deletions

View File

@ -51,7 +51,7 @@ msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
neutron-lib==2.4.0
neutron-lib==2.5.0
openstacksdk==0.31.2
os-client-config==1.28.0
os-ken==0.3.0

View File

@ -50,6 +50,8 @@ def disable_security_group_extension_by_config(aliases):
_disable_extension(stateful_sg.ALIAS, aliases)
LOG.info('Disabled allowed-address-pairs extension.')
_disable_extension('allowed-address-pairs', aliases)
LOG.info('Disabled address-group extension.')
_disable_extension('address-group', aliases)
class SecurityGroupAgentRpc(object):

View File

@ -0,0 +1,124 @@
# 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.
import netaddr
from neutron_lib import constants
from neutron_lib.db import resource_extend
from neutron_lib.db import utils as db_utils
from neutron_lib.exceptions import address_group as ag_exc
from oslo_utils import uuidutils
from neutron.extensions import address_group as ag_ext
from neutron.objects import address_group as ag_obj
from neutron.objects import base as base_obj
@resource_extend.has_resource_extenders
class AddressGroupDbMixin(ag_ext.AddressGroupPluginBase):
"""Mixin class to add address group to db_base_plugin_v2."""
__native_bulk_support = True
@staticmethod
def _make_address_group_dict(address_group, fields=None):
res = address_group.to_dict()
res['addresses'] = [str(addr_assoc['address'])
for addr_assoc in address_group['addresses']]
return db_utils.resource_fields(res, fields)
def _get_address_group(self, context, id):
obj = ag_obj.AddressGroup.get_object(context, id=id)
if obj is None:
raise ag_exc.AddressGroupNotFound(address_group_id=id)
return obj
def _dedup_and_compare_addresses(self, ag_obj, req_addrs):
ag_addrs = set(self._make_address_group_dict(
ag_obj, fields=['addresses'])['addresses'])
req_addrs = set(str(netaddr.IPNetwork(addr)) for addr in req_addrs)
addrs_in_ag = []
addrs_not_in_ag = []
for req_addr in req_addrs:
if req_addr in ag_addrs:
addrs_in_ag.append(req_addr)
else:
addrs_not_in_ag.append(req_addr)
return addrs_in_ag, addrs_not_in_ag
def add_addresses(self, context, address_group_id, addresses):
ag = self._get_address_group(context, address_group_id)
addrs_in_ag, addrs_not_in_ag = self._dedup_and_compare_addresses(
ag, addresses['addresses'])
if addrs_in_ag:
raise ag_exc.AddressesAlreadyExist(
addresses=addrs_in_ag, address_group_id=address_group_id)
for addr in addrs_not_in_ag:
addr = netaddr.IPNetwork(addr)
args = {'address_group_id': address_group_id,
'address': addr}
addr_assoc = ag_obj.AddressAssociation(context, **args)
addr_assoc.create()
ag.update() # reload synthetic fields
return {'address_group': self._make_address_group_dict(ag)}
def remove_addresses(self, context, address_group_id, addresses):
ag = self._get_address_group(context, address_group_id)
addrs_in_ag, addrs_not_in_ag = self._dedup_and_compare_addresses(
ag, addresses['addresses'])
if addrs_not_in_ag:
raise ag_exc.AddressesNotFound(
addresses=addrs_not_in_ag, address_group_id=address_group_id)
for addr in addrs_in_ag:
ag_obj.AddressAssociation.delete_objects(
context, address_group_id=address_group_id, address=addr)
ag.update() # reload synthetic fields
return {'address_group': self._make_address_group_dict(ag)}
def create_address_group(self, context, address_group):
"""Create an address group."""
fields = address_group['address_group']
args = {'project_id': fields['tenant_id'],
'id': uuidutils.generate_uuid(),
'name': fields['name'],
'description': fields['description']}
ag = ag_obj.AddressGroup(context, **args)
ag.create()
if fields.get('addresses') is not constants.ATTR_NOT_SPECIFIED:
self.add_addresses(context, ag.id, fields)
ag.update() # reload synthetic fields
return self._make_address_group_dict(ag)
def update_address_group(self, context, id, address_group):
fields = address_group['address_group']
ag = self._get_address_group(context, id)
ag.update_fields(fields)
ag.update()
return self._make_address_group_dict(ag)
def get_address_group(self, context, id, fields=None):
ag = self._get_address_group(context, id)
return self._make_address_group_dict(ag, fields)
def get_address_groups(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
pager = base_obj.Pager(sorts, limit, page_reverse, marker)
address_groups = ag_obj.AddressGroup.get_objects(
context, _pager=pager, **filters)
return [
self._make_address_group_dict(addr_group, fields)
for addr_group in address_groups
]
def delete_address_group(self, context, id):
address_group = self._get_address_group(context, id)
address_group.delete()

View File

@ -1 +1 @@
fd6107509ccd
1ea5dab0897a

View File

@ -0,0 +1,53 @@
# 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 alembic import op
from neutron_lib.db import constants as db_const
import sqlalchemy as sa
"""add address group
Revision ID: 1ea5dab0897a
Revises: fd6107509ccd
Create Date: 2020-07-02 18:43:28.380941
"""
# revision identifiers, used by Alembic.
revision = '1ea5dab0897a'
down_revision = 'fd6107509ccd'
def upgrade():
op.create_table(
'address_groups',
sa.Column('project_id', sa.String(
length=db_const.PROJECT_ID_FIELD_SIZE), index=True),
sa.Column('id', sa.String(length=db_const.UUID_FIELD_SIZE),
primary_key=True),
sa.Column('name', sa.String(length=db_const.NAME_FIELD_SIZE),
nullable=True),
sa.Column('description', sa.String(
length=db_const.LONG_DESCRIPTION_FIELD_SIZE), nullable=True)
)
op.create_table(
'address_associations',
sa.Column('address', sa.String(length=db_const.IP_ADDR_FIELD_SIZE),
primary_key=True),
sa.Column('address_group_id', sa.String(
length=db_const.UUID_FIELD_SIZE), primary_key=True),
sa.ForeignKeyConstraint(['address_group_id'], ['address_groups.id'],
ondelete='CASCADE')
)

View File

@ -0,0 +1,41 @@
# 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 neutron_lib.db import constants as db_const
from neutron_lib.db import model_base
import sqlalchemy as sa
from sqlalchemy import orm
class AddressAssociation(model_base.BASEV2):
"""Represents a neutron address group's address association."""
__tablename__ = "address_associations"
address = sa.Column(sa.String(length=db_const.IP_ADDR_FIELD_SIZE),
nullable=False, primary_key=True)
address_group_id = sa.Column(sa.String(length=db_const.UUID_FIELD_SIZE),
sa.ForeignKey("address_groups.id",
ondelete="CASCADE"),
nullable=False, primary_key=True)
class AddressGroup(model_base.BASEV2, model_base.HasId, model_base.HasProject):
"""Represents a neutron address group."""
__tablename__ = "address_groups"
name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE))
description = sa.Column(sa.String(db_const.LONG_DESCRIPTION_FIELD_SIZE))
addresses = orm.relationship(AddressAssociation,
backref=orm.backref('address_groups',
load_on_pending=True),
lazy='subquery',
cascade='all, delete-orphan')

View File

@ -0,0 +1,80 @@
# 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.
import abc
from neutron_lib.api.definitions import address_group as apidef
from neutron_lib.api import extensions as api_extensions
from neutron_lib.plugins import directory
from neutron.api import extensions
from neutron.api.v2 import base
class Address_group(api_extensions.APIExtensionDescriptor):
"""Extension class supporting Address Groups."""
api_definition = apidef
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
plugin = directory.get_plugin()
collection_name = apidef.COLLECTION_NAME.replace('_', '-')
params = apidef.RESOURCE_ATTRIBUTE_MAP.get(
apidef.COLLECTION_NAME, dict())
controller = base.create_resource(collection_name,
apidef.RESOURCE_NAME,
plugin, params,
member_actions=apidef.ACTION_MAP[
apidef.RESOURCE_NAME],
allow_bulk=True,
allow_pagination=True,
allow_sorting=True)
ex = extensions.ResourceExtension(collection_name, controller,
member_actions=apidef.ACTION_MAP[
apidef.RESOURCE_NAME],
attr_map=params)
return [ex]
class AddressGroupPluginBase(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def create_address_group(self, context, address_group):
pass
@abc.abstractmethod
def update_address_group(self, context, id, address_group):
pass
@abc.abstractmethod
def get_address_group(self, context, id, fields=None):
pass
@abc.abstractmethod
def get_address_groups(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
pass
@abc.abstractmethod
def delete_address_group(self, context, id):
pass
@abc.abstractmethod
def add_addresses(self, context, address_group_id, addresses):
pass
@abc.abstractmethod
def remove_addresses(self, context, address_group_id, addresses):
pass

View File

@ -0,0 +1,65 @@
# 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.
import netaddr
from neutron_lib.objects import common_types
from oslo_versionedobjects import fields as obj_fields
from neutron.db.models import address_group as models
from neutron.objects import base
@base.NeutronObjectRegistry.register
class AddressGroup(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = models.AddressGroup
fields = {
'id': common_types.UUIDField(),
'name': obj_fields.StringField(nullable=True),
'description': obj_fields.StringField(nullable=True),
'project_id': obj_fields.StringField(),
'addresses': obj_fields.ListOfObjectsField('AddressAssociation',
nullable=True)
}
synthetic_fields = ['addresses']
@base.NeutronObjectRegistry.register
class AddressAssociation(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = models.AddressAssociation
fields = {
'address': common_types.IPNetworkField(nullable=False),
'address_group_id': common_types.UUIDField(nullable=False)
}
primary_keys = ['address', 'address_group_id']
foreign_keys = {'AddressGroup': {'address_group_id': 'id'}}
@classmethod
def modify_fields_to_db(cls, fields):
result = super(AddressAssociation, cls).modify_fields_to_db(fields)
if 'address' in result:
result['address'] = cls.filter_to_str(result['address'])
return result
@classmethod
def modify_fields_from_db(cls, db_obj):
fields = super(AddressAssociation, cls).modify_fields_from_db(db_obj)
if 'address' in fields:
fields['address'] = netaddr.IPNetwork(fields['address'])
return fields

View File

@ -18,6 +18,7 @@ import netaddr
from netaddr.strategy import eui48
from neutron_lib.agent import constants as agent_consts
from neutron_lib.agent import topics
from neutron_lib.api.definitions import address_group as addrgrp_def
from neutron_lib.api.definitions import address_scope
from neutron_lib.api.definitions import agent as agent_apidef
from neutron_lib.api.definitions import agent_resources_synced
@ -95,6 +96,7 @@ from neutron.api.rpc.handlers import metadata_rpc
from neutron.api.rpc.handlers import resources_rpc
from neutron.api.rpc.handlers import securitygroups_rpc
from neutron.common import utils
from neutron.db import address_group_db
from neutron.db import address_scope_db
from neutron.db import agents_db
from neutron.db import agentschedulers_db
@ -157,7 +159,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
vlantransparent_db.Vlantransparent_db_mixin,
extradhcpopt_db.ExtraDhcpOptMixin,
address_scope_db.AddressScopeDbMixin,
subnet_service_type_mixin.SubnetServiceTypeMixin):
subnet_service_type_mixin.SubnetServiceTypeMixin,
address_group_db.AddressGroupDbMixin):
"""Implement the Neutron L2 abstractions using modules.
@ -209,7 +212,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
agent_resources_synced.ALIAS,
subnet_onboard_def.ALIAS,
subnetpool_prefix_ops_def.ALIAS,
stateful_security_group.ALIAS]
stateful_security_group.ALIAS,
addrgrp_def.ALIAS]
# List of agent types for which all binding_failed ports should try to be
# rebound when agent revive

View File

@ -0,0 +1,205 @@
# 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 neutron_lib.api.definitions import address_group as apidef
from neutron_lib import context
import webob.exc
from neutron.db import address_group_db
from neutron.db import db_base_plugin_v2
from neutron.extensions import address_group as ag_ext
from neutron.tests.unit.db import test_db_base_plugin_v2
DB_PLUGIN_KLASS = ('neutron.tests.unit.extensions.test_address_group.'
'AddressGroupTestPlugin')
class AddressGroupTestExtensionManager(object):
def get_resources(self):
return ag_ext.Address_group.get_resources()
def get_actions(self):
return []
def get_request_extensions(self):
return []
class AddressGroupTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
def _create_address_group(self, **kwargs):
address_group = {'address_group': {}}
for k, v in kwargs.items():
if k != 'addresses':
v = str(v)
address_group['address_group'][k] = v
req = self.new_create_request('address-groups', address_group)
neutron_context = context.Context('', kwargs.get('tenant_id',
self._tenant_id))
req.environ['neutron.context'] = neutron_context
res = req.get_response(self.ext_api)
if res.status_int >= webob.exc.HTTPClientError.code:
raise webob.exc.HTTPClientError(code=res.status_int)
return res
def _test_create_address_group(self, expected=None, **kwargs):
keys = kwargs.copy()
keys.setdefault('tenant_id', self._tenant_id)
res = self._create_address_group(**keys)
ag = self.deserialize(self.fmt, res)
self._validate_resource(ag, keys, 'address_group')
if expected:
self._compare_resource(ag, expected, 'address_group')
return ag
def _test_update_address_group(self, addr_group_id, data,
expected=None, tenant_id=None):
update_req = self.new_update_request(
'address-groups', data, addr_group_id)
update_req.environ['neutron.context'] = context.Context(
'', tenant_id or self._tenant_id)
update_res = update_req.get_response(self.ext_api)
if expected:
addr_group = self.deserialize(self.fmt, update_res)
self._compare_resource(addr_group, expected, 'address_group')
return addr_group
return update_res
def _test_address_group_actions(self, addr_group_id, data, action,
expected=None, tenant_id=None):
act_req = self.new_action_request(
'address-groups', data, addr_group_id, action)
act_req.environ['neutron.context'] = context.Context(
'', tenant_id or self._tenant_id)
act_res = act_req.get_response(self.ext_api)
if expected:
addr_group = self.deserialize(self.fmt, act_res)
self._compare_resource(addr_group, expected, 'address_group')
return addr_group
return act_res
class AddressGroupTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
address_group_db.AddressGroupDbMixin):
__native_pagination_support = True
__native_sorting_support = True
# address-group requires security-group extension
supported_extension_aliases = [apidef.ALIAS, 'security-group']
class TestAddressGroup(AddressGroupTestCase):
def setUp(self):
plugin = DB_PLUGIN_KLASS
ext_mgr = AddressGroupTestExtensionManager()
super(TestAddressGroup, self).setUp(plugin=plugin, ext_mgr=ext_mgr)
def test_create_address_group_without_description_or_addresses(self):
expected_ag = {'name': 'foo',
'tenant_id': self._tenant_id,
'description': '',
'addresses': []}
self._test_create_address_group(name='foo',
expected=expected_ag)
def test_create_address_group_with_description_and_addresses(self):
expected_ag = {'name': 'foo',
'description': 'bar',
'tenant_id': self._tenant_id,
'addresses': ['10.0.1.255/28', '192.168.0.1/32']}
self._test_create_address_group(name='foo', description='bar',
addresses=['10.0.1.255/28',
'192.168.0.1/32'],
expected=expected_ag)
def test_create_address_group_empty_name(self):
expected_ag = {'name': ''}
self._test_create_address_group(name='', expected=expected_ag)
def test_update_address_group_name_and_description(self):
ag = self._test_create_address_group(name='foo')
data = {'address_group': {'name': 'bar', 'description': 'bar'}}
self._test_update_address_group(ag['address_group']['id'],
data, expected=data['address_group'])
def test_update_address_group_addresses(self):
ag = self._test_create_address_group(name='foo')
data = {'address_group': {'addresses': ['10.0.0.1/32']}}
res = self._test_update_address_group(ag['address_group']['id'], data)
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_get_address_group(self):
ag = self._test_create_address_group(name='foo')
req = self.new_show_request('address-groups',
ag['address_group']['id'])
res = self.deserialize(self.fmt, req.get_response(self.ext_api))
self.assertEqual(ag['address_group']['id'],
res['address_group']['id'])
def test_list_address_groups(self):
self._test_create_address_group(name='foo')
self._test_create_address_group(name='bar')
res = self._list('address-groups')
self.assertEqual(2, len(res['address_groups']))
def test_delete_address_group(self):
ag = self._test_create_address_group(name='foo')
self._delete('address-groups', ag['address_group']['id'])
self._show('address-groups', ag['address_group']['id'],
expected_code=webob.exc.HTTPNotFound.code)
def test_add_valid_addresses(self):
ag = self._test_create_address_group(name='foo')
data = {'addresses': ['10.0.0.1/32', '2001::/32']}
self._test_address_group_actions(ag['address_group']['id'], data,
'add_addresses', expected=data)
def test_add_invalid_addresses(self):
ag = self._test_create_address_group(name='foo')
data = {'addresses': ['123456']}
res = self._test_address_group_actions(ag['address_group']['id'],
data, 'add_addresses')
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_add_duplicated_addresses(self):
ag = self._test_create_address_group(name='foo',
addresses=['10.0.0.1/32'])
data = {'addresses': ['10.0.0.1/32']}
res = self._test_address_group_actions(ag['address_group']['id'],
data, 'add_addresses')
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_remove_valid_addresses(self):
ag = self._test_create_address_group(name='foo',
addresses=['10.0.0.1/32',
'2001::/32'])
data = {'addresses': ['10.0.0.1/32']}
self._test_address_group_actions(ag['address_group']['id'],
data, 'remove_addresses',
expected={
'addresses': ['2001::/32']
})
def test_remove_absent_addresses(self):
ag = self._test_create_address_group(name='foo',
addresses=['10.0.0.1/32'])
data = {'addresses': ['2001::/32']}
res = self._test_address_group_actions(ag['address_group']['id'],
data, 'remove_addresses')
self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int)

View File

@ -0,0 +1,50 @@
# 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 neutron.objects import address_group
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api
class AddressGroupIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):
_test_class = address_group.AddressGroup
class AddressGroupDbObjectTestCase(
obj_test_base.BaseDbObjectTestCase, testlib_api.SqlTestCase):
_test_class = address_group.AddressGroup
def setUp(self):
super(AddressGroupDbObjectTestCase, self).setUp()
class AddressAssociationIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):
_test_class = address_group.AddressAssociation
class AddressAssociationObjectTestCase(
obj_test_base.BaseDbObjectTestCase, testlib_api.SqlTestCase):
_test_class = address_group.AddressAssociation
def setUp(self):
super(AddressAssociationObjectTestCase, self).setUp()
self.update_obj_fields(
{
'address_group_id':
lambda: self._create_test_address_group_id()
})

View File

@ -38,6 +38,7 @@ from sqlalchemy import orm
import testtools
from neutron import objects
from neutron.objects import address_group
from neutron.objects import agent
from neutron.objects import base
from neutron.objects.db import api as obj_db_api
@ -1640,6 +1641,17 @@ class BaseDbObjectTestCase(_BaseObjectTestCase,
_securitygroup.create()
return _securitygroup.id
def _create_test_address_group_id(self, fields=None):
ag_fields = self.get_random_object_fields(address_group.AddressGroup)
fields = fields or {}
for field, value in ((f, v) for (f, v) in fields.items() if
f in ag_fields):
ag_fields[field] = value
_address_group = address_group.AddressGroup(
self.context, **ag_fields)
_address_group.create()
return _address_group.id
def _create_test_agent_id(self):
attrs = self.get_random_object_fields(obj_cls=agent.Agent)
_agent = agent.Agent(self.context, **attrs)

View File

@ -26,6 +26,8 @@ from neutron.tests import base as test_base
# corresponding version bump in the affected objects. Please keep the list in
# alphabetic order.
object_data = {
'AddressAssociation': '1.0-b92160a3dd2fb7b951adcd2e6ae1665a',
'AddressGroup': '1.0-a402a66e35d25e9381eab40e1e709907',
'AddressScope': '1.1-dd0dfdb67775892d3adc090e28e43bd8',
'AddressScopeRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d',
'Agent': '1.1-64b670752d57b3c7602cb136e0338507',

View File

@ -16,7 +16,7 @@ Jinja2>=2.10 # BSD License (3 clause)
keystonemiddleware>=4.17.0 # Apache-2.0
netaddr>=0.7.18 # BSD
netifaces>=0.10.4 # MIT
neutron-lib>=2.4.0 # Apache-2.0
neutron-lib>=2.5.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
tenacity>=4.4.0 # Apache-2.0
SQLAlchemy>=1.2.0 # MIT