summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--neutron/api/extensions.py21
-rw-r--r--neutron/db/common_db_mixin.py17
-rw-r--r--neutron/db/db_base_plugin_common.py3
-rw-r--r--neutron/db/db_base_plugin_v2.py20
-rw-r--r--neutron/db/l3_db.py23
-rw-r--r--neutron/db/migration/alembic_migrations/versions/CONTRACT_HEAD2
-rw-r--r--neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD2
-rw-r--r--neutron/db/migration/alembic_migrations/versions/mitaka/contract/4ffceebfcdc_standard_desc.py64
-rw-r--r--neutron/db/migration/alembic_migrations/versions/mitaka/expand/0e66c5227a8a_add_desc_to_standard_attr.py34
-rw-r--r--neutron/db/model_base.py8
-rw-r--r--neutron/db/models_v2.py4
-rw-r--r--neutron/db/securitygroups_db.py16
-rw-r--r--neutron/db/standardattrdescription_db.py35
-rw-r--r--neutron/extensions/securitygroup.py11
-rw-r--r--neutron/extensions/standardattrdescription.py55
-rw-r--r--neutron/ipam/subnet_alloc.py4
-rw-r--r--neutron/tests/api/base_security_groups.py5
-rw-r--r--neutron/tests/api/test_floating_ips.py20
-rw-r--r--neutron/tests/api/test_networks.py36
-rw-r--r--neutron/tests/api/test_ports.py18
-rw-r--r--neutron/tests/api/test_routers.py16
-rw-r--r--neutron/tests/api/test_security_groups.py17
-rw-r--r--neutron/tests/api/test_subnetpools.py19
-rw-r--r--neutron/tests/tempest/services/network/json/network_client.py2
-rw-r--r--neutron/tests/unit/api/test_extensions.py8
-rw-r--r--neutron/tests/unit/extension_stubs.py6
-rw-r--r--releasenotes/notes/add-standard-attr-descriptions-1ba0d7a454c3fd8f.yaml8
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.