Permalink
Browse files

Split endpoint records in SQL by interface

This migrates the SQL backend such that v2 endpoints containing up to 3
URL's (public, internal and admin) stored in 'extra' are split into
unique endpoints.

Because legacy "endpoints" (each having publicUrl, internalUrl and
adminUrl) are no longer conceptually identical to v3's "endpoints" (each
having an interface and a url), new ID's are assigned to each entity and
each API continues to operate using with independent sets of endpoint
ID's.

Endpoints created on the v3 API are not exposed on the v2 API.

Change-Id: I2ba59d55907313ae65e908585fc49be0c4ce899a
  • Loading branch information...
1 parent 7db702c commit 2f851340ee8969193b9dcc1913401aa9b33c5d97 @dolph dolph committed with Adam Young Nov 30, 2012
@@ -36,12 +36,14 @@ class Service(sql.ModelBase, sql.DictBase):
class Endpoint(sql.ModelBase, sql.DictBase):
__tablename__ = 'endpoint'
- attributes = ['id', 'region', 'service_id']
+ attributes = ['id', 'interface', 'region', 'service_id', 'url']
id = sql.Column(sql.String(64), primary_key=True)
+ interface = sql.Column(sql.String(8), primary_key=True)
region = sql.Column('region', sql.String(255))
service_id = sql.Column(sql.String(64),
sql.ForeignKey('service.id'),
nullable=False)
+ url = sql.Column(sql.Text())
extra = sql.Column(sql.JsonBlob())
@@ -88,7 +90,9 @@ def update_service(self, service_id, service_ref):
old_dict = ref.to_dict()
old_dict.update(service_ref)
new_service = Service.from_dict(old_dict)
- ref.type = new_service.type
+ for attr in Service.attributes:
+ if attr != 'id':
+ setattr(ref, attr, getattr(new_service, attr))
ref.extra = new_service.extra
session.flush()
return ref.to_dict()
@@ -132,8 +136,9 @@ def update_endpoint(self, endpoint_id, endpoint_ref):
old_dict = ref.to_dict()
old_dict.update(endpoint_ref)
new_endpoint = Endpoint.from_dict(old_dict)
- ref.service_id = new_endpoint.service_id
- ref.region = new_endpoint.region
+ for attr in Endpoint.attributes:
+ if attr != 'id':
+ setattr(ref, attr, getattr(new_endpoint, attr))
ref.extra = new_endpoint.extra
session.flush()
return ref.to_dict()
@@ -142,25 +147,28 @@ def get_catalog(self, user_id, tenant_id, metadata=None):
d = dict(CONF.iteritems())
d.update({'tenant_id': tenant_id,
'user_id': user_id})
- catalog = {}
-
- endpoints = self.list_endpoints()
- for ep in endpoints:
- service = self.get_service(ep['service_id'])
- srv_type = service['type']
- srv_name = service['name']
- region = ep['region']
-
- if region not in catalog:
- catalog[region] = {}
- catalog[region][srv_type] = {}
-
- srv_type = catalog[region][srv_type]
- srv_type['id'] = ep['id']
- srv_type['name'] = srv_name
- srv_type['publicURL'] = core.format_url(ep.get('publicurl', ''), d)
- srv_type['internalURL'] = core.format_url(ep.get('internalurl'), d)
- srv_type['adminURL'] = core.format_url(ep.get('adminurl'), d)
+ catalog = {}
+ services = {}
+ for endpoint in self.list_endpoints():
+ # look up the service
+ services.setdefault(
+ endpoint['service_id'],
+ self.get_service(endpoint['service_id']))
+ service = services[endpoint['service_id']]
+
+ # add the endpoint to the catalog if it's not already there
+ catalog.setdefault(endpoint['region'], {})
+ catalog[endpoint['region']].setdefault(
+ service['type'], {
+ 'id': endpoint['id'],
+ 'name': service['name'],
+ 'publicURL': '', # this may be overridden, but must exist
+ })
+
+ # add the interface's url
+ url = core.format_url(endpoint.get('url'), d)
+ interface_url = '%sURL' % endpoint['interface']
+ catalog[endpoint['region']][service['type']][interface_url] = url
return catalog
@@ -20,6 +20,13 @@
from keystone.catalog import core
from keystone.common import controller
from keystone.common import wsgi
+from keystone import exception
+from keystone import identity
+from keystone import policy
+from keystone import token
+
+
+INTERFACES = ['public', 'internal', 'admin']
class Service(controller.V2Controller):
@@ -50,22 +57,62 @@ def create_service(self, context, OS_KSADM_service):
class Endpoint(controller.V2Controller):
def get_endpoints(self, context):
+ """Merge matching v3 endpoint refs into legacy refs."""
self.assert_admin(context)
- endpoint_list = self.catalog_api.list_endpoints(context)
- return {'endpoints': endpoint_list}
+ legacy_endpoints = {}
+ for endpoint in self.catalog_api.list_endpoints(context):
+ if not endpoint['legacy_endpoint_id']:
+ # endpoints created in v3 should not appear on the v2 API
+ continue
+
+ # is this is a legacy endpoint we haven't indexed yet?
+ if endpoint['legacy_endpoint_id'] not in legacy_endpoints:
+ legacy_ep = endpoint.copy()
+ legacy_ep['id'] = legacy_ep.pop('legacy_endpoint_id')
+ legacy_ep.pop('interface')
+ legacy_ep.pop('url')
+
+ legacy_endpoints[endpoint['legacy_endpoint_id']] = legacy_ep
+ else:
+ legacy_ep = legacy_endpoints[endpoint['legacy_endpoint_id']]
+
+ # add the legacy endpoint with an interface url
+ legacy_ep['%surl' % endpoint['interface']] = endpoint['url']
+ return {'endpoints': legacy_endpoints.values()}
def create_endpoint(self, context, endpoint):
+ """Create three v3 endpoint refs based on a legacy ref."""
self.assert_admin(context)
- endpoint_id = uuid.uuid4().hex
- endpoint_ref = endpoint.copy()
- endpoint_ref['id'] = endpoint_id
- new_endpoint_ref = self.catalog_api.create_endpoint(
- context, endpoint_id, endpoint_ref)
- return {'endpoint': new_endpoint_ref}
+
+ legacy_endpoint_ref = endpoint.copy()
+
+ urls = dict((i, endpoint.pop('%surl' % i)) for i in INTERFACES)
+ legacy_endpoint_id = uuid.uuid4().hex
+ for interface, url in urls.iteritems():
+ endpoint_ref = endpoint.copy()
+ endpoint_ref['id'] = uuid.uuid4().hex
+ endpoint_ref['legacy_endpoint_id'] = legacy_endpoint_id
+ endpoint_ref['interface'] = interface
+ endpoint_ref['url'] = url
+
+ self.catalog_api.create_endpoint(
+ context, endpoint_ref['id'], endpoint_ref)
+
+ legacy_endpoint_ref['id'] = legacy_endpoint_id
+ return {'endpoint': legacy_endpoint_ref}
def delete_endpoint(self, context, endpoint_id):
+ """Delete up to three v3 endpoint refs based on a legacy ref ID."""
self.assert_admin(context)
- self.catalog_api.delete_endpoint(context, endpoint_id)
+
+ deleted_at_least_one = False
+ for endpoint in self.catalog_api.list_endpoints(context):
+ if endpoint['legacy_endpoint_id'] == endpoint_id:
+ self.catalog_api.delete_endpoint(context, endpoint['id'])
+ deleted_at_least_one = True
+
+ if not deleted_at_least_one:
+ raise exception.EndpointNotFound(endpoint_id=endpoint_id)
class ServiceV3(controller.V3Controller):
@@ -0,0 +1,54 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import migrate
+import sqlalchemy as sql
+
+
+def upgrade(migrate_engine):
+ """Create API-version specific endpoint tables."""
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ legacy_table = sql.Table('endpoint', meta, autoload=True)
+ legacy_table.rename('endpoint_v2')
+
+ new_table = sql.Table(
+ 'endpoint_v3',
+ meta,
+ sql.Column('id', sql.String(64), primary_key=True),
+ sql.Column('legacy_endpoint_id', sql.String(64)),
+ sql.Column('interface', sql.String(8), nullable=False),
+ sql.Column('region', sql.String(255)),
+ sql.Column('service_id',
+ sql.String(64),
+ sql.ForeignKey('service.id'),
+ nullable=False),
+ sql.Column('url', sql.Text(), nullable=False),
+ sql.Column('extra', sql.Text()))
+ new_table.create(migrate_engine, checkfirst=True)
+
+
+def downgrade(migrate_engine):
+ """Replace API-version specific endpoint tables with one based on v2."""
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ new_table = sql.Table('endpoint_v3', meta, autoload=True)
+ new_table.drop()
+
+ legacy_table = sql.Table('endpoint_v2', meta, autoload=True)
+ legacy_table.rename('endpoint')
@@ -0,0 +1,96 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+import uuid
+
+import sqlalchemy as sql
+from sqlalchemy import orm
+
+
+ENDPOINT_TYPES = ['public', 'internal', 'admin']
+
+
+def upgrade(migrate_engine):
+ """Split each legacy endpoint into seperate records for each interface."""
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ legacy_table = sql.Table('endpoint_v2', meta, autoload=True)
+ new_table = sql.Table('endpoint_v3', meta, autoload=True)
+
+ session = orm.sessionmaker(bind=migrate_engine)()
+ for ref in session.query(legacy_table).all():
+ # pull urls out of extra
+ extra = json.loads(ref.extra)
+ urls = dict((i, extra.pop('%surl' % i)) for i in ENDPOINT_TYPES)
+
+ for interface in ENDPOINT_TYPES:
+ endpoint = {
+ 'id': uuid.uuid4().hex,
+ 'legacy_endpoint_id': ref.id,
+ 'interface': interface,
+ 'region': ref.region,
+ 'service_id': ref.service_id,
+ 'url': urls[interface],
+ 'extra': json.dumps(extra),
+ }
+ session.execute(
+ 'INSERT INTO `%s` (%s) VALUES (%s)' % (
+ new_table.name,
+ ', '.join('%s' % k for k in endpoint.keys()),
+ ', '.join("'%s'" % v for v in endpoint.values())))
+ session.commit()
+
+
+def downgrade(migrate_engine):
+ """Re-create the v2 endpoints table based on v3 endpoints."""
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ legacy_table = sql.Table('endpoint_v2', meta, autoload=True)
+ new_table = sql.Table('endpoint_v3', meta, autoload=True)
+
+ session = orm.sessionmaker(bind=migrate_engine)()
+ for ref in session.query(new_table).all():
+ extra = json.loads(ref.extra)
+ extra['%surl' % ref.interface] = ref.url
+ endpoint = {
+ 'id': ref.legacy_endpoint_id,
+ 'region': ref.region,
+ 'service_id': ref.service_id,
+ 'extra': json.dumps(extra),
+ }
+
+ try:
+ session.execute(
+ 'INSERT INTO `%s` (%s) VALUES (%s)' % (
+ legacy_table.name,
+ ', '.join('%s' % k for k in endpoint.keys()),
+ ', '.join("'%s'" % v for v in endpoint.values())))
+ except sql.exc.IntegrityError:
+ q = session.query(legacy_table)
+ q = q.filter_by(id=ref.legacy_endpoint_id)
+ legacy_ref = q.one()
+ extra = json.loads(legacy_ref.extra)
+ extra['%surl' % ref.interface] = ref.url
+
+ session.execute(
+ 'UPDATE `%s` SET extra=\'%s\' WHERE id="%s"' % (
+ legacy_table.name,
+ json.dumps(extra),
+ legacy_ref.id))
+ session.commit()
@@ -0,0 +1,51 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import migrate
+import sqlalchemy as sql
+
+
+def upgrade(migrate_engine):
+ """Replace API-version specific endpoint tables with one based on v3."""
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ legacy_table = sql.Table('endpoint_v2', meta, autoload=True)
+ legacy_table.drop()
+
+ new_table = sql.Table('endpoint_v3', meta, autoload=True)
+ new_table.rename('endpoint')
+
+
+def downgrade(migrate_engine):
+ """Create API-version specific endpoint tables."""
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ new_table = sql.Table('endpoint', meta, autoload=True)
+ new_table.rename('endpoint_v3')
+
+ legacy_table = sql.Table(
+ 'endpoint_v2',
+ meta,
+ sql.Column('id', sql.String(64), primary_key=True),
+ sql.Column('region', sql.String(255)),
+ sql.Column('service_id',
+ sql.String(64),
+ sql.ForeignKey('service.id'),
+ nullable=False),
+ sql.Column('extra', sql.Text()))
+ legacy_table.create(migrate_engine, checkfirst=True)
View
@@ -885,6 +885,9 @@ def test_delete_service_with_endpoint(self):
# create an endpoint attached to the service
endpoint = {
'id': uuid.uuid4().hex,
+ 'region': uuid.uuid4().hex,
+ 'interface': uuid.uuid4().hex,
+ 'url': uuid.uuid4().hex,
'service_id': service['id'],
}
self.catalog_api.create_endpoint(endpoint['id'], endpoint)
Oops, something went wrong.

0 comments on commit 2f85134

Please sign in to comment.