Merge "Add a description field to all standard resources"
This commit is contained in:
commit
8a9b514c0a
@ -149,6 +149,17 @@ class ExtensionDescriptor(object):
|
||||
"""Returns a list of extensions to be processed before this one."""
|
||||
return []
|
||||
|
||||
def get_optional_extensions(self):
|
||||
"""Returns a list of extensions to be processed before this one.
|
||||
|
||||
Unlike get_required_extensions. This will not fail the loading of
|
||||
the extension if one of these extensions is not present. This is
|
||||
useful for an extension that extends multiple resources across
|
||||
other extensions that should still work for the remaining extensions
|
||||
when one is missing.
|
||||
"""
|
||||
return []
|
||||
|
||||
def update_attributes_map(self, extended_attributes,
|
||||
extension_attrs_map=None):
|
||||
"""Update attributes map for this extension.
|
||||
@ -432,6 +443,7 @@ class ExtensionManager(object):
|
||||
"""
|
||||
processed_exts = {}
|
||||
exts_to_process = self.extensions.copy()
|
||||
check_optionals = True
|
||||
# Iterate until there are unprocessed extensions or if no progress
|
||||
# is made in a whole iteration
|
||||
while exts_to_process:
|
||||
@ -442,12 +454,21 @@ class ExtensionManager(object):
|
||||
required_exts_set = set(ext.get_required_extensions())
|
||||
if required_exts_set - set(processed_exts):
|
||||
continue
|
||||
optional_exts_set = set(ext.get_optional_extensions())
|
||||
if check_optionals and optional_exts_set - set(processed_exts):
|
||||
continue
|
||||
extended_attrs = ext.get_extended_resources(version)
|
||||
for res, resource_attrs in six.iteritems(extended_attrs):
|
||||
attr_map.setdefault(res, {}).update(resource_attrs)
|
||||
processed_exts[ext_name] = ext
|
||||
del exts_to_process[ext_name]
|
||||
if len(processed_exts) == processed_ext_count:
|
||||
# if we hit here, it means there are unsatisfied
|
||||
# dependencies. try again without optionals since optionals
|
||||
# are only necessary to set order if they are present.
|
||||
if check_optionals:
|
||||
check_optionals = False
|
||||
continue
|
||||
# Exit loop as no progress was made
|
||||
break
|
||||
if exts_to_process:
|
||||
|
@ -20,6 +20,7 @@ from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
import six
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.ext import associationproxy
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import sql
|
||||
|
||||
@ -211,7 +212,13 @@ class CommonDbMixin(object):
|
||||
if not value:
|
||||
query = query.filter(sql.false())
|
||||
return query
|
||||
query = query.filter(column.in_(value))
|
||||
if isinstance(column, associationproxy.AssociationProxy):
|
||||
# association proxies don't support in_ so we have to
|
||||
# do multiple equals matches
|
||||
query = query.filter(
|
||||
or_(*[column == v for v in value]))
|
||||
else:
|
||||
query = query.filter(column.in_(value))
|
||||
elif key == 'shared' and hasattr(model, 'rbac_entries'):
|
||||
# translate a filter on shared into a query against the
|
||||
# object's rbac entries
|
||||
@ -301,9 +308,11 @@ class CommonDbMixin(object):
|
||||
return None
|
||||
|
||||
def _filter_non_model_columns(self, data, model):
|
||||
"""Remove all the attributes from data which are not columns of
|
||||
the model passed as second parameter.
|
||||
"""Remove all the attributes from data which are not columns or
|
||||
association proxies of the model passed as second parameter
|
||||
"""
|
||||
columns = [c.name for c in model.__table__.columns]
|
||||
return dict((k, v) for (k, v) in
|
||||
six.iteritems(data) if k in columns)
|
||||
six.iteritems(data) if k in columns or
|
||||
isinstance(getattr(model, k, None),
|
||||
associationproxy.AssociationProxy))
|
||||
|
@ -298,7 +298,8 @@ class DbBasePluginCommon(common_db_mixin.CommonDbMixin):
|
||||
'cidr': str(detail.subnet_cidr),
|
||||
'subnetpool_id': subnetpool_id,
|
||||
'enable_dhcp': subnet['enable_dhcp'],
|
||||
'gateway_ip': gateway_ip}
|
||||
'gateway_ip': gateway_ip,
|
||||
'description': subnet.get('description')}
|
||||
if subnet['ip_version'] == 6 and subnet['enable_dhcp']:
|
||||
if attributes.is_attr_set(subnet['ipv6_ra_mode']):
|
||||
args['ipv6_ra_mode'] = subnet['ipv6_ra_mode']
|
||||
|
@ -45,6 +45,7 @@ from neutron.db import models_v2
|
||||
from neutron.db import rbac_db_mixin as rbac_mixin
|
||||
from neutron.db import rbac_db_models as rbac_db
|
||||
from neutron.db import sqlalchemyutils
|
||||
from neutron.db import standardattrdescription_db as stattr_db
|
||||
from neutron.extensions import l3
|
||||
from neutron import ipam
|
||||
from neutron.ipam import subnet_alloc
|
||||
@ -80,7 +81,8 @@ def _check_subnet_not_used(context, subnet_id):
|
||||
|
||||
class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
neutron_plugin_base_v2.NeutronPluginBaseV2,
|
||||
rbac_mixin.RbacPluginMixin):
|
||||
rbac_mixin.RbacPluginMixin,
|
||||
stattr_db.StandardAttrDescriptionMixin):
|
||||
"""V2 Neutron plugin interface implementation using SQLAlchemy models.
|
||||
|
||||
Whenever a non-read call happens the plugin will call an event handler
|
||||
@ -319,7 +321,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
'name': n['name'],
|
||||
'admin_state_up': n['admin_state_up'],
|
||||
'mtu': n.get('mtu', constants.DEFAULT_NETWORK_MTU),
|
||||
'status': n.get('status', constants.NET_STATUS_ACTIVE)}
|
||||
'status': n.get('status', constants.NET_STATUS_ACTIVE),
|
||||
'description': n.get('description')}
|
||||
network = models_v2.Network(**args)
|
||||
if n['shared']:
|
||||
entry = rbac_db.NetworkRBAC(
|
||||
@ -988,7 +991,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
'is_default': sp_reader.is_default,
|
||||
'shared': sp_reader.shared,
|
||||
'default_quota': sp_reader.default_quota,
|
||||
'address_scope_id': sp_reader.address_scope_id}
|
||||
'address_scope_id': sp_reader.address_scope_id,
|
||||
'description': sp_reader.description}
|
||||
subnetpool = models_v2.SubnetPool(**pool_args)
|
||||
context.session.add(subnetpool)
|
||||
for prefix in sp_reader.prefixes:
|
||||
@ -1026,10 +1030,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
for key in ['id', 'name', 'ip_version', 'min_prefixlen',
|
||||
'max_prefixlen', 'default_prefixlen', 'is_default',
|
||||
'shared', 'default_quota', 'address_scope_id',
|
||||
'standard_attr']:
|
||||
'standard_attr', 'description']:
|
||||
self._write_key(key, updated, model, new_pool)
|
||||
self._apply_dict_extend_functions(attributes.SUBNETPOOLS,
|
||||
updated, model)
|
||||
return updated
|
||||
|
||||
def _write_key(self, key, update, orig, new_dict):
|
||||
@ -1078,7 +1080,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
|
||||
for key in ['min_prefixlen', 'max_prefixlen', 'default_prefixlen']:
|
||||
updated['key'] = str(updated[key])
|
||||
|
||||
self._apply_dict_extend_functions(attributes.SUBNETPOOLS,
|
||||
updated, orig_sp)
|
||||
return updated
|
||||
|
||||
def get_subnetpool(self, context, id, fields=None):
|
||||
@ -1211,7 +1214,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
admin_state_up=p['admin_state_up'],
|
||||
status=p.get('status', constants.PORT_STATUS_ACTIVE),
|
||||
device_id=p['device_id'],
|
||||
device_owner=p['device_owner'])
|
||||
device_owner=p['device_owner'],
|
||||
description=p.get('description'))
|
||||
if ('dns-integration' in self.supported_extension_aliases and
|
||||
'dns_name' in p):
|
||||
request_dns_name = self._get_request_dns_name(p)
|
||||
|
@ -38,6 +38,7 @@ from neutron.common import utils
|
||||
from neutron.db import l3_agentschedulers_db as l3_agt
|
||||
from neutron.db import model_base
|
||||
from neutron.db import models_v2
|
||||
from neutron.db import standardattrdescription_db as st_attr
|
||||
from neutron.extensions import external_net
|
||||
from neutron.extensions import l3
|
||||
from neutron import manager
|
||||
@ -131,7 +132,8 @@ class FloatingIP(model_base.HasStandardAttributes, model_base.BASEV2,
|
||||
router = orm.relationship(Router, backref='floating_ips')
|
||||
|
||||
|
||||
class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
st_attr.StandardAttrDescriptionMixin):
|
||||
"""Mixin class to add L3/NAT router methods to db_base_plugin_v2."""
|
||||
|
||||
router_device_owners = (
|
||||
@ -182,7 +184,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
tenant_id=tenant_id,
|
||||
name=router['name'],
|
||||
admin_state_up=router['admin_state_up'],
|
||||
status="ACTIVE")
|
||||
status="ACTIVE",
|
||||
description=router.get('description'))
|
||||
context.session.add(router_db)
|
||||
return router_db
|
||||
|
||||
@ -1008,10 +1011,13 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
previous_router_id = floatingip_db.router_id
|
||||
port_id, internal_ip_address, router_id = (
|
||||
self._check_and_get_fip_assoc(context, fip, floatingip_db))
|
||||
floatingip_db.update({'fixed_ip_address': internal_ip_address,
|
||||
'fixed_port_id': port_id,
|
||||
'router_id': router_id,
|
||||
'last_known_router_id': previous_router_id})
|
||||
update = {'fixed_ip_address': internal_ip_address,
|
||||
'fixed_port_id': port_id,
|
||||
'router_id': router_id,
|
||||
'last_known_router_id': previous_router_id}
|
||||
if 'description' in fip:
|
||||
update['description'] = fip['description']
|
||||
floatingip_db.update(update)
|
||||
next_hop = None
|
||||
if router_id:
|
||||
# NOTE(tidwellr) use admin context here
|
||||
@ -1094,7 +1100,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
status=initial_status,
|
||||
floating_network_id=fip['floating_network_id'],
|
||||
floating_ip_address=floating_ip_address,
|
||||
floating_port_id=external_port['id'])
|
||||
floating_port_id=external_port['id'],
|
||||
description=fip.get('description'))
|
||||
# Update association with internal port
|
||||
# and define external IP address
|
||||
self._update_fip_assoc(context, fip,
|
||||
@ -1110,6 +1117,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
self._process_dns_floatingip_create_postcommit(context,
|
||||
floatingip_dict,
|
||||
dns_data)
|
||||
self._apply_dict_extend_functions(l3.FLOATINGIPS, floatingip_dict,
|
||||
floatingip_db)
|
||||
return floatingip_dict
|
||||
|
||||
def create_floatingip(self, context, floatingip,
|
||||
|
@ -1 +1 @@
|
||||
5ffceebfada
|
||||
4ffceebfcdc
|
||||
|
@ -1 +1 @@
|
||||
3894bccad37f
|
||||
0e66c5227a8a
|
||||
|
@ -0,0 +1,64 @@
|
||||
# Copyright 2015 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""standard_desc
|
||||
|
||||
Revision ID: 4ffceebfcdc
|
||||
Revises: 5ffceebfada
|
||||
Create Date: 2016-02-10 23:12:04.012457
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4ffceebfcdc'
|
||||
down_revision = '5ffceebfada'
|
||||
depends_on = ('0e66c5227a8a',)
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# A simple model of the security groups table with only the fields needed for
|
||||
# the migration.
|
||||
securitygroups = sa.Table('securitygroups', sa.MetaData(),
|
||||
sa.Column('standard_attr_id', sa.BigInteger(),
|
||||
nullable=False),
|
||||
sa.Column('description', sa.String(length=255)))
|
||||
|
||||
standardattr = sa.Table(
|
||||
'standardattributes', sa.MetaData(),
|
||||
sa.Column('id', sa.BigInteger(), primary_key=True, autoincrement=True),
|
||||
sa.Column('description', sa.String(length=255)))
|
||||
|
||||
|
||||
def upgrade():
|
||||
migrate_values()
|
||||
op.drop_column('securitygroups', 'description')
|
||||
|
||||
|
||||
def migrate_values():
|
||||
session = sa.orm.Session(bind=op.get_bind())
|
||||
values = []
|
||||
for row in session.query(securitygroups):
|
||||
values.append({'id': row[0],
|
||||
'description': row[1]})
|
||||
with session.begin(subtransactions=True):
|
||||
for value in values:
|
||||
session.execute(
|
||||
standardattr.update().values(
|
||||
description=value['description']).where(
|
||||
standardattr.c.id == value['id']))
|
||||
# this commit appears to be necessary to allow further operations
|
||||
session.commit()
|
@ -0,0 +1,34 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Add desc to standard attr table
|
||||
|
||||
Revision ID: 0e66c5227a8a
|
||||
Revises: 3894bccad37f
|
||||
Create Date: 2016-02-02 10:50:34.238563
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0e66c5227a8a'
|
||||
down_revision = '3894bccad37f'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('standardattributes', sa.Column('description',
|
||||
sa.String(length=255), nullable=True))
|
@ -16,6 +16,7 @@
|
||||
from oslo_db.sqlalchemy import models
|
||||
from oslo_utils import uuidutils
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext import declarative
|
||||
from sqlalchemy import orm
|
||||
|
||||
@ -107,6 +108,7 @@ class StandardAttribute(BASEV2, models.TimestampMixin):
|
||||
# before a 2-byte prefix is required. We shouldn't get anywhere near this
|
||||
# limit with our table names...
|
||||
resource_type = sa.Column(sa.String(255), nullable=False)
|
||||
description = sa.Column(sa.String(attr.DESCRIPTION_MAX_LEN))
|
||||
|
||||
|
||||
class HasStandardAttributes(object):
|
||||
@ -130,8 +132,10 @@ class HasStandardAttributes(object):
|
||||
single_parent=True,
|
||||
uselist=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, description='', *args, **kwargs):
|
||||
super(HasStandardAttributes, self).__init__(*args, **kwargs)
|
||||
# here we automatically create the related standard attribute object
|
||||
self.standard_attr = StandardAttribute(
|
||||
resource_type=self.__tablename__)
|
||||
resource_type=self.__tablename__, description=description)
|
||||
|
||||
description = association_proxy('standard_attr', 'description')
|
||||
|
@ -142,8 +142,8 @@ class Port(model_base.HasStandardAttributes, model_base.BASEV2,
|
||||
def __init__(self, id=None, tenant_id=None, name=None, network_id=None,
|
||||
mac_address=None, admin_state_up=None, status=None,
|
||||
device_id=None, device_owner=None, fixed_ips=None,
|
||||
dns_name=None):
|
||||
super(Port, self).__init__()
|
||||
dns_name=None, **kwargs):
|
||||
super(Port, self).__init__(**kwargs)
|
||||
self.id = id
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
|
@ -44,7 +44,6 @@ class SecurityGroup(model_base.HasStandardAttributes, model_base.BASEV2,
|
||||
"""Represents a v2 neutron security group."""
|
||||
|
||||
name = sa.Column(sa.String(attributes.NAME_MAX_LEN))
|
||||
description = sa.Column(sa.String(attributes.DESCRIPTION_MAX_LEN))
|
||||
|
||||
|
||||
class DefaultSecurityGroup(model_base.BASEV2):
|
||||
@ -317,6 +316,8 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
|
||||
'description': security_group['description']}
|
||||
res['security_group_rules'] = [self._make_security_group_rule_dict(r)
|
||||
for r in security_group.rules]
|
||||
self._apply_dict_extend_functions(ext_sg.SECURITYGROUPS, res,
|
||||
security_group)
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _make_security_group_binding_dict(self, security_group, fields=None):
|
||||
@ -397,7 +398,9 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
|
||||
protocol=rule_dict['protocol'],
|
||||
port_range_min=rule_dict['port_range_min'],
|
||||
port_range_max=rule_dict['port_range_max'],
|
||||
remote_ip_prefix=rule_dict.get('remote_ip_prefix'))
|
||||
remote_ip_prefix=rule_dict.get('remote_ip_prefix'),
|
||||
description=rule_dict.get('description')
|
||||
)
|
||||
context.session.add(db)
|
||||
self._registry_notify(resources.SECURITY_GROUP_RULE,
|
||||
events.PRECOMMIT_CREATE,
|
||||
@ -515,6 +518,8 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
|
||||
'remote_ip_prefix': security_group_rule['remote_ip_prefix'],
|
||||
'remote_group_id': security_group_rule['remote_group_id']}
|
||||
|
||||
self._apply_dict_extend_functions(ext_sg.SECURITYGROUPRULES, res,
|
||||
security_group_rule)
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _make_security_group_rule_filter_dict(self, security_group_rule):
|
||||
@ -525,7 +530,7 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
|
||||
|
||||
include_if_present = ['protocol', 'port_range_max', 'port_range_min',
|
||||
'ethertype', 'remote_ip_prefix',
|
||||
'remote_group_id']
|
||||
'remote_group_id', 'description']
|
||||
for key in include_if_present:
|
||||
value = sgr.get(key)
|
||||
if value:
|
||||
@ -547,7 +552,9 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
|
||||
# Check in database if rule exists
|
||||
filters = self._make_security_group_rule_filter_dict(
|
||||
security_group_rule)
|
||||
db_rules = self.get_security_group_rules(context, filters)
|
||||
db_rules = self.get_security_group_rules(
|
||||
context, filters,
|
||||
fields=security_group_rule['security_group_rule'].keys())
|
||||
# Note(arosen): the call to get_security_group_rules wildcards
|
||||
# values in the filter that have a value of [None]. For
|
||||
# example, filters = {'remote_group_id': [None]} will return
|
||||
@ -559,7 +566,6 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
|
||||
# below to check for these corner cases.
|
||||
for db_rule in db_rules:
|
||||
# need to remove id from db_rule for matching
|
||||
id = db_rule.pop('id')
|
||||
if (security_group_rule['security_group_rule'] == db_rule):
|
||||
raise ext_sg.SecurityGroupRuleExists(id=id)
|
||||
|
||||
|
35
neutron/db/standardattrdescription_db.py
Normal file
35
neutron/db/standardattrdescription_db.py
Normal file
@ -0,0 +1,35 @@
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.api.v2 import attributes
|
||||
from neutron.db import common_db_mixin
|
||||
from neutron.extensions import l3
|
||||
from neutron.extensions import securitygroup
|
||||
|
||||
|
||||
class StandardAttrDescriptionMixin(object):
|
||||
supported_extension_aliases = ['standard-attr-description']
|
||||
|
||||
def _extend_standard_attr_description(self, res, db_object):
|
||||
if not hasattr(db_object, 'description'):
|
||||
return
|
||||
res['description'] = db_object.description
|
||||
|
||||
for resource in [attributes.NETWORKS, attributes.PORTS,
|
||||
attributes.SUBNETS, attributes.SUBNETPOOLS,
|
||||
securitygroup.SECURITYGROUPS,
|
||||
securitygroup.SECURITYGROUPRULES,
|
||||
l3.ROUTERS, l3.FLOATINGIPS]:
|
||||
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
|
||||
resource, ['_extend_standard_attr_description'])
|
@ -213,10 +213,12 @@ attr.validators['type:name_not_default'] = _validate_name_not_default
|
||||
|
||||
sg_supported_protocols = [None] + list(const.IP_PROTOCOL_MAP.keys())
|
||||
sg_supported_ethertypes = ['IPv4', 'IPv6']
|
||||
SECURITYGROUPS = 'security_groups'
|
||||
SECURITYGROUPRULES = 'security_group_rules'
|
||||
|
||||
# Attribute Map
|
||||
RESOURCE_ATTRIBUTE_MAP = {
|
||||
'security_groups': {
|
||||
SECURITYGROUPS: {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_visible': True,
|
||||
@ -231,10 +233,10 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'required_by_policy': True,
|
||||
'validate': {'type:string': attr.TENANT_ID_MAX_LEN},
|
||||
'is_visible': True},
|
||||
'security_group_rules': {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
SECURITYGROUPRULES: {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
},
|
||||
'security_group_rules': {
|
||||
SECURITYGROUPRULES: {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_visible': True,
|
||||
@ -270,7 +272,6 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
SECURITYGROUPS = 'security_groups'
|
||||
EXTENDED_ATTRIBUTES_2_0 = {
|
||||
'ports': {SECURITYGROUPS: {'allow_post': True,
|
||||
'allow_put': True,
|
||||
|
55
neutron/extensions/standardattrdescription.py
Normal file
55
neutron/extensions/standardattrdescription.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.api import extensions
|
||||
from neutron.api.v2 import attributes as attr
|
||||
|
||||
|
||||
EXTENDED_ATTRIBUTES_2_0 = {}
|
||||
|
||||
for resource in ('security_group_rules', 'security_groups', 'ports', 'subnets',
|
||||
'networks', 'routers', 'floatingips', 'subnetpools'):
|
||||
EXTENDED_ATTRIBUTES_2_0[resource] = {
|
||||
'description': {'allow_post': True, 'allow_put': True,
|
||||
'validate': {'type:string': attr.DESCRIPTION_MAX_LEN},
|
||||
'is_visible': True, 'default': ''},
|
||||
}
|
||||
|
||||
|
||||
class Standardattrdescription(extensions.ExtensionDescriptor):
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "standard-attr-description"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
return "standard-attr-description"
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return "Extension to add descriptions to standard attributes"
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
return "2016-02-10T10:00:00-00:00"
|
||||
|
||||
def get_optional_extensions(self):
|
||||
return ['security-group', 'router']
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
if version == "2.0":
|
||||
return dict(EXTENDED_ATTRIBUTES_2_0.items())
|
||||
return {}
|
@ -229,6 +229,7 @@ class SubnetPoolReader(object):
|
||||
self._read_prefix_bounds(subnetpool)
|
||||
self._read_attrs(subnetpool,
|
||||
['tenant_id', 'name', 'is_default', 'shared'])
|
||||
self.description = subnetpool.get('description')
|
||||
self._read_address_scope(subnetpool)
|
||||
self.subnetpool = {'id': self.id,
|
||||
'name': self.name,
|
||||
@ -243,7 +244,8 @@ class SubnetPoolReader(object):
|
||||
'default_quota': self.default_quota,
|
||||
'address_scope_id': self.address_scope_id,
|
||||
'is_default': self.is_default,
|
||||
'shared': self.shared}
|
||||
'shared': self.shared,
|
||||
'description': self.description}
|
||||
|
||||
def _read_attrs(self, subnetpool, keys):
|
||||
for key in keys:
|
||||
|
@ -24,10 +24,11 @@ class BaseSecGroupTest(base.BaseNetworkTest):
|
||||
def resource_setup(cls):
|
||||
super(BaseSecGroupTest, cls).resource_setup()
|
||||
|
||||
def _create_security_group(self):
|
||||
def _create_security_group(self, **kwargs):
|
||||
# Create a security group
|
||||
name = data_utils.rand_name('secgroup-')
|
||||
group_create_body = self.client.create_security_group(name=name)
|
||||
group_create_body = self.client.create_security_group(name=name,
|
||||
**kwargs)
|
||||
self.addCleanup(self._delete_security_group,
|
||||
group_create_body['security_group']['id'])
|
||||
self.assertEqual(group_create_body['security_group']['name'], name)
|
||||
|
@ -118,6 +118,26 @@ class FloatingIPTestJSON(base.BaseNetworkTest):
|
||||
self.assertIsNone(updated_floating_ip['fixed_ip_address'])
|
||||
self.assertIsNone(updated_floating_ip['router_id'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-eeee-b1442641ffff')
|
||||
def test_create_update_floatingip_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
body = self.client.create_floatingip(
|
||||
floating_network_id=self.ext_net_id,
|
||||
port_id=self.ports[0]['id'],
|
||||
description='d1'
|
||||
)['floatingip']
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.show_floatingip(body['id'])['floatingip']
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.update_floatingip(body['id'], description='d2')
|
||||
self.assertEqual('d2', body['floatingip']['description'])
|
||||
body = self.client.show_floatingip(body['floatingip']['id'])
|
||||
self.assertEqual('d2', body['floatingip']['description'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('e1f6bffd-442f-4668-b30e-df13f2705e77')
|
||||
def test_floating_ip_delete_port(self):
|
||||
|
@ -236,6 +236,24 @@ class NetworksTestJSON(base.BaseNetworkTest):
|
||||
if network['id'] == self.network['id']]
|
||||
self.assertNotEmpty(networks, "Created network not found in the list")
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-ccc4-b1442640bbbb')
|
||||
def test_create_update_network_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
body = self.create_network(description='d1')
|
||||
self.assertEqual('d1', body['description'])
|
||||
net_id = body['id']
|
||||
body = self.client.list_networks(id=net_id)['networks'][0]
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.update_network(body['id'],
|
||||
description='d2')
|
||||
self.assertEqual('d2', body['network']['description'])
|
||||
body = self.client.list_networks(id=net_id)['networks'][0]
|
||||
self.assertEqual('d2', body['description'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('6ae6d24f-9194-4869-9c85-c313cb20e080')
|
||||
def test_list_networks_fields(self):
|
||||
@ -272,6 +290,24 @@ class NetworksTestJSON(base.BaseNetworkTest):
|
||||
for field_name in fields:
|
||||
self.assertEqual(subnet[field_name], self.subnet[field_name])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-eeee-b1442640bbbb')
|
||||
def test_create_update_subnet_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
body = self.create_subnet(self.network, description='d1')
|
||||
self.assertEqual('d1', body['description'])
|
||||
sub_id = body['id']
|
||||
body = self.client.list_subnets(id=sub_id)['subnets'][0]
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.update_subnet(body['id'],
|
||||
description='d2')
|
||||
self.assertEqual('d2', body['subnet']['description'])
|
||||
body = self.client.list_subnets(id=sub_id)['subnets'][0]
|
||||
self.assertEqual('d2', body['description'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('db68ba48-f4ea-49e9-81d1-e367f6d0b20a')
|
||||
def test_list_subnets(self):
|
||||
|
@ -69,6 +69,24 @@ class PortsTestJSON(sec_base.BaseSecGroupTest):
|
||||
self.assertEqual(updated_port['name'], new_name)
|
||||
self.assertFalse(updated_port['admin_state_up'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-bbb4-b1442640bbbb')
|
||||
def test_create_update_port_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
body = self.create_port(self.network,
|
||||
description='d1')
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.list_ports(id=body['id'])['ports'][0]
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.update_port(body['id'],
|
||||
description='d2')
|
||||
self.assertEqual('d2', body['port']['description'])
|
||||
body = self.client.list_ports(id=body['port']['id'])['ports'][0]
|
||||
self.assertEqual('d2', body['description'])
|
||||
|
||||
@test.idempotent_id('67f1b811-f8db-43e2-86bd-72c074d4a42c')
|
||||
def test_create_bulk_port(self):
|
||||
network1 = self.network
|
||||
|
@ -79,6 +79,22 @@ class RoutersTest(base.BaseRouterTest):
|
||||
create_body['router']['id'])
|
||||
self.assertEqual(show_body['router']['name'], updated_name)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-eeee-b1442640eeee')
|
||||
def test_create_update_router_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
body = self.create_router(description='d1', router_name='test')
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.show_router(body['id'])['router']
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.update_router(body['id'], description='d2')
|
||||
self.assertEqual('d2', body['router']['description'])
|
||||
body = self.client.show_router(body['router']['id'])['router']
|
||||
self.assertEqual('d2', body['description'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('e54dd3a3-4352-4921-b09d-44369ae17397')
|
||||
def test_create_router_setting_tenant_id(self):
|
||||
|
@ -142,6 +142,23 @@ class SecGroupTest(base.BaseSecGroupTest):
|
||||
self.assertIn(rule_create_body['security_group_rule']['id'],
|
||||
rule_list)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-fff2-b1442640bbbb')
|
||||
def test_create_security_group_rule_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
sg = self._create_security_group()[0]['security_group']
|
||||
rule = self.client.create_security_group_rule(
|
||||
security_group_id=sg['id'], protocol='tcp',
|
||||
direction='ingress', ethertype=self.ethertype,
|
||||
description='d1'
|
||||
)['security_group_rule']
|
||||
self.assertEqual('d1', rule['description'])
|
||||
body = self.client.show_security_group_rule(rule['id'])
|
||||
self.assertEqual('d1', body['security_group_rule']['description'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('87dfbcf9-1849-43ea-b1e4-efa3eeae9f71')
|
||||
def test_create_security_group_rule_with_additional_args(self):
|
||||
|
@ -103,6 +103,25 @@ class SubnetPoolsTest(SubnetPoolsTestBase):
|
||||
[sp['name'] for sp in subnetpools],
|
||||
"Created subnetpool name should be in the list")
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('c72c1c0c-2193-4aca-ddd4-b1442640bbbb')
|
||||
def test_create_update_subnetpool_description(self):
|
||||
if not test.is_extension_enabled('standard-attr-description',
|
||||
'network'):
|
||||
msg = "standard-attr-description not enabled."
|
||||
raise self.skipException(msg)
|
||||
body = self._create_subnetpool(description='d1')
|
||||
self.assertEqual('d1', body['description'])
|
||||
sub_id = body['id']
|
||||
body = filter(lambda x: x['id'] == sub_id,
|
||||
self.client.list_subnetpools()['subnetpools'])[0]
|
||||
self.assertEqual('d1', body['description'])
|
||||
body = self.client.update_subnetpool(sub_id, description='d2')
|
||||
self.assertEqual('d2', body['subnetpool']['description'])
|
||||
body = filter(lambda x: x['id'] == sub_id,
|
||||
self.client.list_subnetpools()['subnetpools'])[0]
|
||||
self.assertEqual('d2', body['description'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
@test.idempotent_id('741d08c2-1e3f-42be-99c7-0ea93c5b728c')
|
||||
def test_get_subnetpool(self):
|
||||
|
@ -434,6 +434,8 @@ class NetworkClientJSON(service_client.ServiceClient):
|
||||
update_body['name'] = kwargs.get('name', body['router']['name'])
|
||||
update_body['admin_state_up'] = kwargs.get(
|
||||
'admin_state_up', body['router']['admin_state_up'])
|
||||
if 'description' in kwargs:
|
||||
update_body['description'] = kwargs['description']
|
||||
cur_gw_info = body['router']['external_gateway_info']
|
||||
if cur_gw_info:
|
||||
# TODO(kevinbenton): setting the external gateway info is not
|
||||
|
@ -579,6 +579,14 @@ class RequestExtensionTest(base.BaseTestCase):
|
||||
|
||||
class ExtensionManagerTest(base.BaseTestCase):
|
||||
|
||||
def test_optional_extensions_no_error(self):
|
||||
ext_mgr = extensions.ExtensionManager('')
|
||||
attr_map = {}
|
||||
ext_mgr.add_extension(ext_stubs.StubExtension('foo_alias',
|
||||
optional=['cats']))
|
||||
ext_mgr.extend_resources("2.0", attr_map)
|
||||
self.assertIn('foo_alias', ext_mgr.extensions)
|
||||
|
||||
def test_missing_required_extensions_raise_error(self):
|
||||
ext_mgr = extensions.ExtensionManager('')
|
||||
attr_map = {}
|
||||
|
@ -21,8 +21,9 @@ from neutron import wsgi
|
||||
|
||||
class StubExtension(extensions.ExtensionDescriptor):
|
||||
|
||||
def __init__(self, alias="stub_extension"):
|
||||
def __init__(self, alias="stub_extension", optional=None):
|
||||
self.alias = alias
|
||||
self.optional = optional or []
|
||||
|
||||
def get_name(self):
|
||||
return "Stub Extension"
|
||||
@ -36,6 +37,9 @@ class StubExtension(extensions.ExtensionDescriptor):
|
||||
def get_updated(self):
|
||||
return ""
|
||||
|
||||
def get_optional_extensions(self):
|
||||
return self.optional
|
||||
|
||||
|
||||
class StubExtensionWithReqs(StubExtension):
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
prelude: >
|
||||
Add description field to security group rules, networks, ports, routers,
|
||||
floating IPs, and subnet pools.
|
||||
features:
|
||||
- Security group rules, networks, ports, routers, floating IPs, and subnet
|
||||
pools may now contain an optional description which allows users to
|
||||
easily store details about entities.
|
Loading…
x
Reference in New Issue
Block a user