diff options
27 files changed, 434 insertions, 40 deletions
diff --git a/neutron/api/extensions.py b/neutron/api/extensions.py index 9c2dc03fbe..f59ead8332 100644 --- a/neutron/api/extensions.py +++ b/neutron/api/extensions.py @@ -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: diff --git a/neutron/db/common_db_mixin.py b/neutron/db/common_db_mixin.py index 82ffeee0ca..3ee2cec538 100644 --- a/neutron/db/common_db_mixin.py +++ b/neutron/db/common_db_mixin.py @@ -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)) diff --git a/neutron/db/db_base_plugin_common.py b/neutron/db/db_base_plugin_common.py index 12775b0e77..3eebde0c1f 100644 --- a/neutron/db/db_base_plugin_common.py +++ b/neutron/db/db_base_plugin_common.py @@ -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'] diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 7ad8809764..d78f0d4834 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -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( @@ -989,7 +992,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: @@ -1027,10 +1031,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): @@ -1079,7 +1081,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): @@ -1212,7 +1215,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) diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 9d4e465db2..add1330260 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -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, diff --git a/neutron/db/migration/alembic_migrations/versions/CONTRACT_HEAD b/neutron/db/migration/alembic_migrations/versions/CONTRACT_HEAD index d1b0015881..03a635981f 100644 --- a/neutron/db/migration/alembic_migrations/versions/CONTRACT_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/CONTRACT_HEAD @@ -1 +1 @@ -5ffceebfada +4ffceebfcdc diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 542e85d00a..2b79b8e15c 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -3894bccad37f +0e66c5227a8a diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/contract/4ffceebfcdc_standard_desc.py b/neutron/db/migration/alembic_migrations/versions/mitaka/contract/4ffceebfcdc_standard_desc.py new file mode 100644 index 0000000000..c3c724d586 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/contract/4ffceebfcdc_standard_desc.py @@ -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() diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/0e66c5227a8a_add_desc_to_standard_attr.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/0e66c5227a8a_add_desc_to_standard_attr.py new file mode 100644 index 0000000000..733ce2f336 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/0e66c5227a8a_add_desc_to_standard_attr.py @@ -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)) diff --git a/neutron/db/model_base.py b/neutron/db/model_base.py index 0db04fb7a9..a0dcab6a1f 100644 --- a/neutron/db/model_base.py +++ b/neutron/db/model_base.py @@ -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') diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index 3451902bbe..3c6bf0d7e5 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -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 diff --git a/neutron/db/securitygroups_db.py b/neutron/db/securitygroups_db.py index 518e01aec9..d17cfb9ac4 100644 --- a/neutron/db/securitygroups_db.py +++ b/neutron/db/securitygroups_db.py @@ -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) diff --git a/neutron/db/standardattrdescription_db.py b/neutron/db/standardattrdescription_db.py new file mode 100644 index 0000000000..6ad8cf4526 --- /dev/null +++ b/neutron/db/standardattrdescription_db.py @@ -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']) diff --git a/neutron/extensions/securitygroup.py b/neutron/extensions/securitygroup.py index 434172758d..c5d8f212c5 100644 --- a/neutron/extensions/securitygroup.py +++ b/neutron/extensions/securitygroup.py @@ -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, diff --git a/neutron/extensions/standardattrdescription.py b/neutron/extensions/standardattrdescription.py new file mode 100644 index 0000000000..71ea725e91 --- /dev/null +++ b/neutron/extensions/standardattrdescription.py @@ -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 {} diff --git a/neutron/ipam/subnet_alloc.py b/neutron/ipam/subnet_alloc.py index 7e90ceee07..46716b823d 100644 --- a/neutron/ipam/subnet_alloc.py +++ b/neutron/ipam/subnet_alloc.py @@ -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: diff --git a/neutron/tests/api/base_security_groups.py b/neutron/tests/api/base_security_groups.py index c11e353b0a..e3c165113c 100644 --- a/neutron/tests/api/base_security_groups.py +++ b/neutron/tests/api/base_security_groups.py @@ -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) diff --git a/neutron/tests/api/test_floating_ips.py b/neutron/tests/api/test_floating_ips.py index 8ee8b31df3..472b3e0550 100644 --- a/neutron/tests/api/test_floating_ips.py +++ b/neutron/tests/api/test_floating_ips.py @@ -119,6 +119,26 @@ class FloatingIPTestJSON(base.BaseNetworkTest): 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): # Create a floating IP diff --git a/neutron/tests/api/test_networks.py b/neutron/tests/api/test_networks.py index 7836606598..d28f028f93 100644 --- a/neutron/tests/api/test_networks.py +++ b/neutron/tests/api/test_networks.py @@ -237,6 +237,24 @@ class NetworksTestJSON(base.BaseNetworkTest): 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): # Verify specific fields of the networks @@ -273,6 +291,24 @@ class NetworksTestJSON(base.BaseNetworkTest): 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): # Verify the subnet exists in the list of all subnets diff --git a/neutron/tests/api/test_ports.py b/neutron/tests/api/test_ports.py index 2de3a74495..d584147014 100644 --- a/neutron/tests/api/test_ports.py +++ b/neutron/tests/api/test_ports.py @@ -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 diff --git a/neutron/tests/api/test_routers.py b/neutron/tests/api/test_routers.py index 514b5b0293..c10dcd6871 100644 --- a/neutron/tests/api/test_routers.py +++ b/neutron/tests/api/test_routers.py @@ -80,6 +80,22 @@ class RoutersTest(base.BaseRouterTest): 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): # Test creating router from admin user setting tenant_id. diff --git a/neutron/tests/api/test_security_groups.py b/neutron/tests/api/test_security_groups.py index 73c92432aa..bf6092e291 100644 --- a/neutron/tests/api/test_security_groups.py +++ b/neutron/tests/api/test_security_groups.py @@ -143,6 +143,23 @@ class SecGroupTest(base.BaseSecGroupTest): 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): """Verify security group rule with additional arguments works. diff --git a/neutron/tests/api/test_subnetpools.py b/neutron/tests/api/test_subnetpools.py index d2aeadb68b..5c50b3c1ea 100644 --- a/neutron/tests/api/test_subnetpools.py +++ b/neutron/tests/api/test_subnetpools.py @@ -104,6 +104,25 @@ class SubnetPoolsTest(SubnetPoolsTestBase): "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): created_subnetpool = self._create_subnetpool() diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 3cadcbb471..545c2e91dd 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -439,6 +439,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 diff --git a/neutron/tests/unit/api/test_extensions.py b/neutron/tests/unit/api/test_extensions.py index c93bddc85c..c0ee404950 100644 --- a/neutron/tests/unit/api/test_extensions.py +++ b/neutron/tests/unit/api/test_extensions.py @@ -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 = {} diff --git a/neutron/tests/unit/extension_stubs.py b/neutron/tests/unit/extension_stubs.py index abfde11ee1..99b2ec1de5 100644 --- a/neutron/tests/unit/extension_stubs.py +++ b/neutron/tests/unit/extension_stubs.py @@ -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): diff --git a/releasenotes/notes/add-standard-attr-descriptions-1ba0d7a454c3fd8f.yaml b/releasenotes/notes/add-standard-attr-descriptions-1ba0d7a454c3fd8f.yaml new file mode 100644 index 0000000000..5da879c5d4 --- /dev/null +++ b/releasenotes/notes/add-standard-attr-descriptions-1ba0d7a454c3fd8f.yaml @@ -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. |