Merge "Add a description field to all standard resources"

This commit is contained in:
Jenkins 2016-03-06 04:56:54 +00:00 committed by Gerrit Code Review
commit 8a9b514c0a
27 changed files with 434 additions and 40 deletions

View File

@ -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:

View File

@ -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))

View File

@ -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']

View File

@ -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)

View File

@ -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,

View File

@ -1 +1 @@
5ffceebfada
4ffceebfcdc

View File

@ -1 +1 @@
3894bccad37f
0e66c5227a8a

View File

@ -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()

View File

@ -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))

View File

@ -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')

View File

@ -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

View File

@ -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)

View 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'])

View File

@ -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,

View 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 {}

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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 = {}

View File

@ -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):

View File

@ -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.