Merge lp:~rconradharris/nova/uuid-redux into lp:~hudson-openstack/nova/trunk

Proposed by Rick Harris
Status: Merged
Approved by: Josh Kearney
Approved revision: 1205
Merged at revision: 1200
Proposed branch: lp:~rconradharris/nova/uuid-redux
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 671 lines (+317/-77)
13 files modified
nova/api/openstack/ips.py (+11/-12)
nova/api/openstack/views/servers.py (+2/-1)
nova/compute/api.py (+9/-2)
nova/compute/manager.py (+0/-1)
nova/db/api.py (+5/-0)
nova/db/sqlalchemy/api.py (+38/-27)
nova/db/sqlalchemy/migrate_repo/versions/025_add_uuid_to_instances.py (+43/-0)
nova/db/sqlalchemy/models.py (+1/-0)
nova/scheduler/api.py (+78/-25)
nova/tests/api/openstack/test_servers.py (+67/-7)
nova/tests/scheduler/test_scheduler.py (+30/-2)
nova/tests/test_utils.py (+18/-0)
nova/utils.py (+15/-0)
To merge this branch: bzr merge lp:~rconradharris/nova/uuid-redux
Reviewer Review Type Date Requested Status
Ed Leafe (community) Abstain
Matt Dietz (community) Approve
Brian Lamar (community) Needs Information
Brian Waldon (community) Approve
Review via email: mp+64875@code.launchpad.net

Description of the change

This patch adds support for working with instances by UUID in addition to integer IDs.

The Zone Scheduler routing mechanics were changed slightly so that when an UUID is passed in, it checks to see whether the item is available locally.

If it isn't it re-routes to a child zone.

If it is available locally, it substitutes the UUID with the integer ID and calls the wrapped function. This is the 'trick' allows us to not change any of the virt-layer code-- everything still uses integer IDs locally.

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Is there a bug/blueprint you can attach this to?

Revision history for this message
Josh Kearney (jk0) wrote :

> Is there a bug/blueprint you can attach this to?

nova-instance-referencing

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Cool stuff, Rick. Looking forward to uuids

8-37, 92: This is good refactoring, but it doesn't seem directly related to this branch. I'm not asking you to remove it, I just think we shouldn't touch things like this until we have a clear reason to change it.

178: This method name is a bit misleading. Can you call it _build_instance_get_query or something?

Is there a future plan to either remove instance.id or replace it with the uuid value? I can understand why it isn't in this MP.

review: Needs Fixing
Revision history for this message
Rick Harris (rconradharris) wrote :

> 8-37, 92: This is good refactoring, but it doesn't seem directly related to this branch. I'm not asking you to remove it, I just think we shouldn't touch things like this until we have a clear reason to change it.

The intent here wasn't to refactor, that was just a by-product. There was a nasty bug in the code that we uncovered. Notice, `server_id` and `id` in those methods. Since `id` wasn't passed in, it was actually using python's built-in function `id`.

The refactoring just made the cleanup a little easier.

(As to why the tests passed before, no idea. They shouldn't have. Maybe due to the shear amount of fakes and stubs.)

> 178: This method name is a bit misleading. Can you call it _build_instance_get_query or something?

Okay.

> Is there a future plan to either remove instance.id or replace it with the uuid value? I can understand why it isn't in this MP.

I think that's in the long-term plan; but, definitely couldn't get that in one-shot. This gets a step-closer and allows us to move to UUID in a careful, piece-wise fashion.

Revision history for this message
Josh Kearney (jk0) wrote :

> 8-37, 92: This is good refactoring, but it doesn't seem directly related to
> this branch. I'm not asking you to remove it, I just think we shouldn't touch
> things like this until we have a clear reason to change it.

Not sure I agree with this. Other than the fact that there was a very serious bug, is the duplication of all that code not a clear enough reason to make a small refactor? I could see this being a bad idea in certain cases, but something this trivial is perfectly acceptable IMHO.

Revision history for this message
Rick Harris (rconradharris) wrote :

> 178: This method name is a bit misleading. Can you call it _build_instance_get_query or something?

Fixed.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > 8-37, 92: This is good refactoring, but it doesn't seem directly related to
> > this branch. I'm not asking you to remove it, I just think we shouldn't
> touch
> > things like this until we have a clear reason to change it.
>
> Not sure I agree with this. Other than the fact that there was a very serious
> bug, is the duplication of all that code not a clear enough reason to make a
> small refactor? I could see this being a bad idea in certain cases, but
> something this trivial is perfectly acceptable IMHO.

First of all, I didn't realize this was actually fixing a bug. Given that, I have no problem in this specific case. I'm not a fan of attaching arbitrary refactoring to branches that have a very specific purpose. It increases the likelihood that new bugs will be introduced in code that was already working just fine. It's the "if it ain't broke, don't fix it" mentality.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > 178: This method name is a bit misleading. Can you call it
> _build_instance_get_query or something?
>
> Fixed.

Cool.

review: Approve
Revision history for this message
Brian Lamar (blamar) wrote :

This might have been covered, but why go through all of the migrations and headache changing SQL columns when we could just store the UUID as an integer in the existing column?

review: Needs Information
Revision history for this message
Rick Harris (rconradharris) wrote :

> This might have been covered, but why go through all of the migrations and headache changing SQL
> columns when we could just store the UUID as an integer in the existing column?

A UUID is 128-bits wide. The ID column is currently 32 bits wide (int); even if we bumped it up to a bigint, it would only be 64 bits wide.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

The migration script is incorrect; it populates all existing instances with the same UUID, since each pass through the rows updates every row; see http://paste.openstack.org/show/1674/ to see the results after running the migration.

review: Needs Fixing
Revision history for this message
Ed Leafe (ed-leafe) wrote :

Here's a patch that fixes the problem:

http://paste.openstack.org/show/1675/

Revision history for this message
Ed Leafe (ed-leafe) wrote :

The ec2 api reports the old ec2_id format (i.e., 'i-<hex of integer id>'), which will not work. It should be 'i-<first 8 of uuid>'.

See http://paste.openstack.org/show/1676/

review: Needs Fixing
Revision history for this message
Ed Leafe (ed-leafe) wrote :

Ah, I see none of the ec2 changes are included. So I guess that's by design.

Revision history for this message
Josh Kearney (jk0) wrote :

> The migration script is incorrect; it populates all existing instances with
> the same UUID, since each pass through the rows updates every row; see
> http://paste.openstack.org/show/1674/ to see the results after running the
> migration.

Very nice catch. This could have caused a lot of headaches.

Revision history for this message
Rick Harris (rconradharris) wrote :

> The ec2 api reports the old ec2_id format (i.e., 'i-<hex of integer id>'), which will not work. It
> should be 'i-<first 8 of uuid>'.

I'm not fully up-to-speed with the plan with respect to EC2; perhaps we should talk that part of the problem out.

That said, I'd vote to make that a separate patch, to keep this one, already fairly tricky, as simple as possible.

lp:~rconradharris/nova/uuid-redux updated
1200. By Rick Harris

Fixed migration per review feedback.

Revision history for this message
Rick Harris (rconradharris) wrote :

> The migration script is incorrect; it populates all existing instances with the same UUID, since
> each pass through the rows updates every row; see http://paste.openstack.org/show/1674/ to see the
> results after running the migration.

Nice catch. Fixed.

Revision history for this message
Matt Dietz (cerberus) wrote :

Looks awesome. Love the tests.

review: Approve
Revision history for this message
Ed Leafe (ed-leafe) wrote :

utils.py: is_uuid_like(val) is incorrect. A value of "1234567890123456789012" is a perfectly legitimate UUID hex string, but that method will return False for this. I would recommend using the version I wrote:

def is_uuid(val):
    """Returns True if the value passed is a valid UUID string."""
    if isinstance(val, basestring):
        # Remove any dashes
        val = val.replace("-", "")
        # UUIDs are 32 hex characters long
        if len(val) == 32:
            # Make sure that they are all valid hex characters.
            acceptable = "0123456789abcdefABCDEF"
            return not bool([ch for ch in val if ch not in acceptable])
    return False

review: Needs Fixing
Revision history for this message
Ed Leafe (ed-leafe) wrote :

The 'gen_uuid()' function in nova/utils.py returns a UUID object, not the hex string. As a result, functions that call it have to change it to a string. The nova/db/sqlalchemy/api.py method that populates the uuid column does this by wrapping it with str(), which generates a string with the dashes in it. The design was to not store the dashes in the database, as they serve no purpose there. We can always add them back when they are displayed to the user for readability if needed.

The simplest fix is to have gen_uuid() return uuid.uuid4().hex instead of a Python UUID object; this will not have the dashes, and will not have to be cast to a basestring for storage or comparison.

review: Needs Fixing
Revision history for this message
Ed Leafe (ed-leafe) wrote :

Line 405: the FAKE_UUID used for tests is not a valid UUID hex string. Any code that accepts UUID values and does anything requiring validation will fail. This should be changed to something akin to:

FAKE_UUID = "abcdabcd-abcd-abcd-abcd-abcdabcdabcd"

review: Needs Fixing
Revision history for this message
Ed Leafe (ed-leafe) wrote :

Just noticed that the above comment also applies to lines 554-5.

lp:~rconradharris/nova/uuid-redux updated
1201. By Rick Harris

Merging trunk

1202. By Rick Harris

Using proper UUID format for uuids

1203. By Rick Harris

Adding tests for is_uuid_like

Revision history for this message
Rick Harris (rconradharris) wrote :

> This should be changed to something akin to:
> FAKE_UUID = "abcdabcd-abcd-abcd-abcd-abcdabcdabcd"

Fixed.

Made is_uuid_like a bit stricter in what it accepts, added unit-tests for it.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

I still have some reservations, but there is nothing anymore that would cause errors, so I'm removing the 'Needs Fixing'.

review: Abstain
Revision history for this message
Chris Behrens (cbehrens) wrote :

> I still have some reservations, but there is nothing anymore that would cause
> errors, so I'm removing the 'Needs Fixing'.

The dashes or something more? If it's the dashes, and it was decided at the summit to not use dashes, I agree with Ed that we should not store them.

I don't remember the discussion (I may not have been there), though, so I can't really comment further on that part.

It makes me curious, however, why one would be worried about the dashes in the DB. I realize they don't serve any purpose and it'd be a waste of bytes as far as the DB is concerned. However, if the client is having to continuously rebuild 32/36 byte strings with and without dashes, it's really not a waste in the end. 32 bytes vs 36 bytes should have very minimal effect on the DB. If we're going to use a char column, it seems to make most sense to store them in the native string format. If I'm missing something, I wouldn't mind being set straight.

Why didn't we choose int(16) or binary(16) or what not? Portability?

Anyway, as I said at the top here and putting my personal thoughts aside, I feel we should implement whatever was discussed at the summit.

Revision history for this message
Rick Harris (rconradharris) wrote :

> I don't remember the discussion (I may not have been there), though, so I can't really comment further on that part.

Same here, don't recall hearing this discussion at the summit (might have been in a different talk).

> If we're going to use a char column, it seems to make most sense to store them in the native string format.

Agreed, if we're going to pay the penalty of storing the UUID as a VARCHAR, we might as well use the canonical representation. That would make the column a bit easier to work with: Ops wouldn't constantly have to convert from canonical-dashed-UUID-format to underlying-hex-format.

Granted we're trying to be database agnostic, but MySQL seems to have an opinion on the matter since their UUID function returns the UUID in canonical-dashed-format. So, it would be nice to be able to do something like:

UPDATE instances set uuid=UUID() WHERE id=3;

> Why didn't we choose int(16) or binary(16) or what not? Portability?

Portability was the main goal here. To my knowledge there is no good cross-db way of handling UUIDs. With Postgres you can use a native UUID type, however, that's not supported by SQLite and MySQL.

INT(16) is really just a 32-bit number with a display width of 16 digits.

The largest we could get in terms of integer datatypes would be a 64-bit BIGINT, too small for a 128-bit UUID.

The other route, using a 16-byte Binary field is a possibility. Looking around I see some people recommending that for performance reasons. However, I'm not sure in our particular use-case that this would be the right way to go.

For one, I don't think we're going to be storing enough data in our tables for the penalty of performing a scan on a VARCHAR column to be important.

Second, and most importantly, from an Ops perspective, it makes a lot of sense having the data-represented in the database match up with "what's expected". Forcing Ops to convert between a log entry showing "ABCD-ABCD-ABCD-ABCD" and some huge opaque number seemed error-prone and needlessly painful.

All that said, if either A) we'd previously committed to storing UUIDs differently or B) we find a compelling performance reason for using a binary representation, I'd be cool with changing it.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

The discussion at the summit was about storing UUIDs as integers or CHAR. VARCHAR makes no sense since there is no VAR involved: UUID strings are fixed length.

There was no discussion of storing the dashes. That is basic database design: you don't store presentation formatting in the database, no matter how canonical the representation might be. Dashes in social security numbers; spaces in credit card numbers;, etc. But since this is not fatal in any sense, and I am not concerned about that.

My main reservation was the rewriting of is_uuid() to is_uuid_like(), so that strings such as 'zzzzzzzz-zzzz-...' are accepted. I see no benefit to that, when either the version I wrote or the native uuid.UUID() function will work, while adding potential testing problems due to invalid UUID strings being accepted. But since this was a conscious design choice and not an oversight, and will not break the app in production, I withdrew my "Needs Fixing" review.

Revision history for this message
Rick Harris (rconradharris) wrote :

VARCHARs are used because that's what SQLAlchemy exposes via its `String` datatype.

http://www.sqlalchemy.org/docs/core/types.html#sqlalchemy.types.String

lp:~rconradharris/nova/uuid-redux updated
1204. By Rick Harris

Merging trunk, fixing conflicts

1205. By Rick Harris

Moving add_uuid migration to 025

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/openstack/ips.py'
2--- nova/api/openstack/ips.py 2011-05-20 18:42:19 +0000
3+++ nova/api/openstack/ips.py 2011-06-20 15:20:30 +0000
4@@ -32,25 +32,24 @@
5 self.compute_api = nova.compute.API()
6 self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
7
8+ def _get_instance(self, req, server_id):
9+ try:
10+ instance = self.compute_api.get(
11+ req.environ['nova.context'], server_id)
12+ except nova.exception.NotFound:
13+ return faults.Fault(exc.HTTPNotFound())
14+ return instance
15+
16 def index(self, req, server_id):
17- try:
18- instance = self.compute_api.get(req.environ['nova.context'], id)
19- except nova.exception.NotFound:
20- return faults.Fault(exc.HTTPNotFound())
21+ instance = self._get_instance(req, server_id)
22 return {'addresses': self.builder.build(instance)}
23
24 def public(self, req, server_id):
25- try:
26- instance = self.compute_api.get(req.environ['nova.context'], id)
27- except nova.exception.NotFound:
28- return faults.Fault(exc.HTTPNotFound())
29+ instance = self._get_instance(req, server_id)
30 return {'public': self.builder.build_public_parts(instance)}
31
32 def private(self, req, server_id):
33- try:
34- instance = self.compute_api.get(req.environ['nova.context'], id)
35- except nova.exception.NotFound:
36- return faults.Fault(exc.HTTPNotFound())
37+ instance = self._get_instance(req, server_id)
38 return {'private': self.builder.build_private_parts(instance)}
39
40 def show(self, req, server_id, id):
41
42=== modified file 'nova/api/openstack/views/servers.py'
43--- nova/api/openstack/views/servers.py 2011-06-09 18:52:53 +0000
44+++ nova/api/openstack/views/servers.py 2011-06-20 15:20:30 +0000
45@@ -75,7 +75,7 @@
46 }
47
48 inst_dict = {
49- 'id': int(inst['id']),
50+ 'id': inst['id'],
51 'name': inst['display_name'],
52 'addresses': self.addresses_builder.build(inst),
53 'status': power_mapping[inst.get('state')]}
54@@ -99,6 +99,7 @@
55 self._build_image(inst_dict, inst)
56 self._build_flavor(inst_dict, inst)
57
58+ inst_dict['uuid'] = inst['uuid']
59 return dict(server=inst_dict)
60
61 def _build_image(self, response, inst):
62
63=== modified file 'nova/compute/api.py'
64--- nova/compute/api.py 2011-06-17 23:52:22 +0000
65+++ nova/compute/api.py 2011-06-20 15:20:30 +0000
66@@ -579,8 +579,15 @@
67
68 def get(self, context, instance_id):
69 """Get a single instance with the given instance_id."""
70- rv = self.db.instance_get(context, instance_id)
71- return dict(rv.iteritems())
72+ # NOTE(sirp): id used to be exclusively integer IDs; now we're
73+ # accepting both UUIDs and integer IDs. The handling of this
74+ # is done in db/sqlalchemy/api/instance_get
75+ if utils.is_uuid_like(instance_id):
76+ uuid = instance_id
77+ instance = self.db.instance_get_by_uuid(context, uuid)
78+ else:
79+ instance = self.db.instance_get(context, instance_id)
80+ return dict(instance.iteritems())
81
82 @scheduler_api.reroute_compute("get")
83 def routing_get(self, context, instance_id):
84
85=== modified file 'nova/compute/manager.py'
86--- nova/compute/manager.py 2011-06-15 16:46:24 +0000
87+++ nova/compute/manager.py 2011-06-20 15:20:30 +0000
88@@ -783,7 +783,6 @@
89 def get_diagnostics(self, context, instance_id):
90 """Retrieve diagnostics for an instance on this host."""
91 instance_ref = self.db.instance_get(context, instance_id)
92-
93 if instance_ref["state"] == power_state.RUNNING:
94 LOG.audit(_("instance %s: retrieving diagnostics"), instance_id,
95 context=context)
96
97=== modified file 'nova/db/api.py'
98--- nova/db/api.py 2011-06-15 16:46:24 +0000
99+++ nova/db/api.py 2011-06-20 15:20:30 +0000
100@@ -419,6 +419,11 @@
101 return IMPL.instance_stop(context, instance_id)
102
103
104+def instance_get_by_uuid(context, uuid):
105+ """Get an instance or raise if it does not exist."""
106+ return IMPL.instance_get_by_uuid(context, uuid)
107+
108+
109 def instance_get(context, instance_id):
110 """Get an instance or raise if it does not exist."""
111 return IMPL.instance_get(context, instance_id)
112
113=== modified file 'nova/db/sqlalchemy/api.py'
114--- nova/db/sqlalchemy/api.py 2011-06-20 01:47:20 +0000
115+++ nova/db/sqlalchemy/api.py 2011-06-20 15:20:30 +0000
116@@ -797,6 +797,8 @@
117 values['metadata'] = _metadata_refs(values.get('metadata'))
118
119 instance_ref = models.Instance()
120+ instance_ref['uuid'] = str(utils.gen_uuid())
121+
122 instance_ref.update(values)
123
124 session = get_session()
125@@ -859,39 +861,48 @@
126
127
128 @require_context
129+def instance_get_by_uuid(context, uuid, session=None):
130+ partial = _build_instance_get(context, session=session)
131+ result = partial.filter_by(uuid=uuid)
132+ result = result.first()
133+ if not result:
134+ # FIXME(sirp): it would be nice if InstanceNotFound would accept a
135+ # uuid parameter as well
136+ raise exception.InstanceNotFound(instance_id=uuid)
137+ return result
138+
139+
140+@require_context
141 def instance_get(context, instance_id, session=None):
142- if not session:
143- session = get_session()
144- result = None
145-
146- if is_admin_context(context):
147- result = session.query(models.Instance).\
148- options(joinedload_all('fixed_ip.floating_ips')).\
149- options(joinedload_all('security_groups.rules')).\
150- options(joinedload('volumes')).\
151- options(joinedload_all('fixed_ip.network')).\
152- options(joinedload('metadata')).\
153- options(joinedload('instance_type')).\
154- filter_by(id=instance_id).\
155- filter_by(deleted=can_read_deleted(context)).\
156- first()
157- elif is_user_context(context):
158- result = session.query(models.Instance).\
159- options(joinedload_all('fixed_ip.floating_ips')).\
160- options(joinedload_all('security_groups.rules')).\
161- options(joinedload('volumes')).\
162- options(joinedload('metadata')).\
163- options(joinedload('instance_type')).\
164- filter_by(project_id=context.project_id).\
165- filter_by(id=instance_id).\
166- filter_by(deleted=False).\
167- first()
168+ partial = _build_instance_get(context, session=session)
169+ result = partial.filter_by(id=instance_id)
170+ result = result.first()
171 if not result:
172 raise exception.InstanceNotFound(instance_id=instance_id)
173-
174 return result
175
176
177+@require_context
178+def _build_instance_get(context, session=None):
179+ if not session:
180+ session = get_session()
181+
182+ partial = session.query(models.Instance).\
183+ options(joinedload_all('fixed_ip.floating_ips')).\
184+ options(joinedload_all('security_groups.rules')).\
185+ options(joinedload('volumes')).\
186+ options(joinedload_all('fixed_ip.network')).\
187+ options(joinedload('metadata')).\
188+ options(joinedload('instance_type'))
189+
190+ if is_admin_context(context):
191+ partial = partial.filter_by(deleted=can_read_deleted(context))
192+ elif is_user_context(context):
193+ partial = partial.filter_by(project_id=context.project_id).\
194+ filter_by(deleted=False)
195+ return partial
196+
197+
198 @require_admin_context
199 def instance_get_all(context):
200 session = get_session()
201
202=== added file 'nova/db/sqlalchemy/migrate_repo/versions/025_add_uuid_to_instances.py'
203--- nova/db/sqlalchemy/migrate_repo/versions/025_add_uuid_to_instances.py 1970-01-01 00:00:00 +0000
204+++ nova/db/sqlalchemy/migrate_repo/versions/025_add_uuid_to_instances.py 2011-06-20 15:20:30 +0000
205@@ -0,0 +1,43 @@
206+# vim: tabstop=4 shiftwidth=4 softtabstop=4
207+
208+# Copyright 2011 OpenStack LLC.
209+#
210+# Licensed under the Apache License, Version 2.0 (the "License"); you may
211+# not use this file except in compliance with the License. You may obtain
212+# a copy of the License at
213+#
214+# http://www.apache.org/licenses/LICENSE-2.0
215+#
216+# Unless required by applicable law or agreed to in writing, software
217+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
218+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
219+# License for the specific language governing permissions and limitations
220+# under the License.
221+
222+from sqlalchemy import Column, Integer, MetaData, String, Table
223+
224+from nova import utils
225+
226+
227+meta = MetaData()
228+
229+instances = Table("instances", meta,
230+ Column("id", Integer(), primary_key=True, nullable=False))
231+uuid_column = Column("uuid", String(36))
232+
233+
234+def upgrade(migrate_engine):
235+ meta.bind = migrate_engine
236+ instances.create_column(uuid_column)
237+
238+ rows = migrate_engine.execute(instances.select())
239+ for row in rows:
240+ instance_uuid = str(utils.gen_uuid())
241+ migrate_engine.execute(instances.update()\
242+ .where(instances.c.id == row[0])\
243+ .values(uuid=instance_uuid))
244+
245+
246+def downgrade(migrate_engine):
247+ meta.bind = migrate_engine
248+ instances.drop_column(uuid_column)
249
250=== modified file 'nova/db/sqlalchemy/models.py'
251--- nova/db/sqlalchemy/models.py 2011-06-15 16:46:24 +0000
252+++ nova/db/sqlalchemy/models.py 2011-06-20 15:20:30 +0000
253@@ -233,6 +233,7 @@
254
255 os_type = Column(String(255))
256 vm_mode = Column(String(255))
257+ uuid = Column(String(36))
258
259 # TODO(vish): see Ewan's email about state improvements, probably
260 # should be in a driver base class or some such
261
262=== modified file 'nova/scheduler/api.py'
263--- nova/scheduler/api.py 2011-06-15 13:40:42 +0000
264+++ nova/scheduler/api.py 2011-06-20 15:20:30 +0000
265@@ -24,6 +24,7 @@
266 from nova import flags
267 from nova import log as logging
268 from nova import rpc
269+from nova import utils
270
271 from eventlet import greenpool
272
273@@ -201,38 +202,78 @@
274
275
276 class reroute_compute(object):
277- """Decorator used to indicate that the method should
278- delegate the call the child zones if the db query
279- can't find anything."""
280+ """
281+ reroute_compute is responsible for trying to lookup a resource in the
282+ current zone and if it's not found there, delegating the call to the
283+ child zones.
284+
285+ Since reroute_compute will be making 'cross-zone' calls, the ID for the
286+ object must come in as a UUID-- if we receive an integer ID, we bail.
287+
288+ The steps involved are:
289+
290+ 1. Validate that item_id is UUID like
291+
292+ 2. Lookup item by UUID in the zone local database
293+
294+ 3. If the item was found, then extract integer ID, and pass that to
295+ the wrapped method. (This ensures that zone-local code can
296+ continue to use integer IDs).
297+
298+ 4. If the item was not found, we delgate the call to a child zone
299+ using the UUID.
300+ """
301 def __init__(self, method_name):
302 self.method_name = method_name
303
304+ def _route_to_child_zones(self, context, collection, item_uuid):
305+ if not FLAGS.enable_zone_routing:
306+ raise exception.InstanceNotFound(instance_id=item_uuid)
307+
308+ zones = db.zone_get_all(context)
309+ if not zones:
310+ raise exception.InstanceNotFound(instance_id=item_uuid)
311+
312+ # Ask the children to provide an answer ...
313+ LOG.debug(_("Asking child zones ..."))
314+ result = self._call_child_zones(zones,
315+ wrap_novaclient_function(_issue_novaclient_command,
316+ collection, self.method_name, item_uuid))
317+ # Scrub the results and raise another exception
318+ # so the API layers can bail out gracefully ...
319+ raise RedirectResult(self.unmarshall_result(result))
320+
321 def __call__(self, f):
322 def wrapped_f(*args, **kwargs):
323- collection, context, item_id = \
324+ collection, context, item_id_or_uuid = \
325 self.get_collection_context_and_id(args, kwargs)
326- try:
327- # Call the original function ...
328+
329+ attempt_reroute = False
330+ if utils.is_uuid_like(item_id_or_uuid):
331+ item_uuid = item_id_or_uuid
332+ try:
333+ instance = db.instance_get_by_uuid(context, item_uuid)
334+ except exception.InstanceNotFound, e:
335+ # NOTE(sirp): since a UUID was passed in, we can attempt
336+ # to reroute to a child zone
337+ attempt_reroute = True
338+ LOG.debug(_("Instance %(item_uuid)s not found "
339+ "locally: '%(e)s'" % locals()))
340+ else:
341+ # NOTE(sirp): since we're not re-routing in this case, and
342+ # we we were passed a UUID, we need to replace that UUID
343+ # with an integer ID in the argument list so that the
344+ # zone-local code can continue to use integer IDs.
345+ item_id = instance['id']
346+ args = list(args) # needs to be mutable to replace
347+ self.replace_uuid_with_id(args, kwargs, item_id)
348+
349+ if attempt_reroute:
350+ return self._route_to_child_zones(context, collection,
351+ item_uuid)
352+ else:
353 return f(*args, **kwargs)
354- except exception.InstanceNotFound, e:
355- LOG.debug(_("Instance %(item_id)s not found "
356- "locally: '%(e)s'" % locals()))
357-
358- if not FLAGS.enable_zone_routing:
359- raise
360-
361- zones = db.zone_get_all(context)
362- if not zones:
363- raise
364-
365- # Ask the children to provide an answer ...
366- LOG.debug(_("Asking child zones ..."))
367- result = self._call_child_zones(zones,
368- wrap_novaclient_function(_issue_novaclient_command,
369- collection, self.method_name, item_id))
370- # Scrub the results and raise another exception
371- # so the API layers can bail out gracefully ...
372- raise RedirectResult(self.unmarshall_result(result))
373+
374 return wrapped_f
375
376 def _call_child_zones(self, zones, function):
377@@ -251,6 +292,18 @@
378 instance_id = args[2]
379 return ("servers", context, instance_id)
380
381+ @staticmethod
382+ def replace_uuid_with_id(args, kwargs, replacement_id):
383+ """
384+ Extracts the UUID parameter from the arg or kwarg list and replaces
385+ it with an integer ID.
386+ """
387+ if 'instance_id' in kwargs:
388+ kwargs['instance_id'] = replacement_id
389+ elif len(args) > 1:
390+ args.pop(2)
391+ args.insert(2, replacement_id)
392+
393 def unmarshall_result(self, zone_responses):
394 """Result is a list of responses from each child zone.
395 Each decorator derivation is responsible to turning this
396
397=== modified file 'nova/tests/api/openstack/test_servers.py'
398--- nova/tests/api/openstack/test_servers.py 2011-06-14 18:40:15 +0000
399+++ nova/tests/api/openstack/test_servers.py 2011-06-20 15:20:30 +0000
400@@ -49,10 +49,22 @@
401 FLAGS.verbose = True
402
403
404-def return_server(context, id):
405+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
406+
407+
408+def fake_gen_uuid():
409+ return FAKE_UUID
410+
411+
412+def return_server_by_id(context, id):
413 return stub_instance(id)
414
415
416+def return_server_by_uuid(context, uuid):
417+ id = 1
418+ return stub_instance(id, uuid=uuid)
419+
420+
421 def return_server_with_addresses(private, public):
422 def _return_server(context, id):
423 return stub_instance(id, private_address=private,
424@@ -111,7 +123,8 @@
425
426
427 def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
428- host=None, power_state=0, reservation_id=""):
429+ host=None, power_state=0, reservation_id="",
430+ uuid=FAKE_UUID):
431 metadata = []
432 metadata.append(InstanceMetadata(key='seq', value=id))
433
434@@ -129,7 +142,7 @@
435 server_name = "reservation_%s" % (reservation_id, )
436
437 instance = {
438- "id": id,
439+ "id": int(id),
440 "admin_pass": "",
441 "user_id": user_id,
442 "project_id": "",
443@@ -157,7 +170,8 @@
444 "display_name": server_name,
445 "display_description": "",
446 "locked": False,
447- "metadata": metadata}
448+ "metadata": metadata,
449+ "uuid": uuid}
450
451 instance["fixed_ip"] = {
452 "address": private_address,
453@@ -196,8 +210,11 @@
454 fakes.stub_out_auth(self.stubs)
455 fakes.stub_out_key_pair_funcs(self.stubs)
456 fakes.stub_out_image_service(self.stubs)
457+ self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid)
458 self.stubs.Set(nova.db.api, 'instance_get_all', return_servers)
459- self.stubs.Set(nova.db.api, 'instance_get', return_server)
460+ self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
461+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
462+ return_server_by_uuid)
463 self.stubs.Set(nova.db.api, 'instance_get_all_by_user',
464 return_servers)
465 self.stubs.Set(nova.db.api, 'instance_add_security_group',
466@@ -229,6 +246,36 @@
467 self.assertEqual(res_dict['server']['id'], 1)
468 self.assertEqual(res_dict['server']['name'], 'server1')
469
470+ def test_get_server_by_uuid(self):
471+ """
472+ The steps involved with resolving a UUID are pretty complicated;
473+ here's what's happening in this scenario:
474+
475+ 1. Show is calling `routing_get`
476+
477+ 2. `routing_get` is wrapped by `reroute_compute` which does the work
478+ of resolving requests to child zones.
479+
480+ 3. `reroute_compute` looks up the UUID by hitting the stub
481+ (returns_server_by_uuid)
482+
483+ 4. Since the stub return that the record exists, `reroute_compute`
484+ considers the request to be 'zone local', so it replaces the UUID
485+ in the argument list with an integer ID and then calls the inner
486+ function ('get').
487+
488+ 5. The call to `get` hits the other stub 'returns_server_by_id` which
489+ has the UUID set to FAKE_UUID
490+
491+ So, counterintuitively, we call `get` twice on the `show` command.
492+ """
493+ req = webob.Request.blank('/v1.0/servers/%s' % FAKE_UUID)
494+ res = req.get_response(fakes.wsgi_app())
495+ res_dict = json.loads(res.body)
496+ self.assertEqual(res_dict['server']['id'], 1)
497+ self.assertEqual(res_dict['server']['uuid'], FAKE_UUID)
498+ self.assertEqual(res_dict['server']['name'], 'server1')
499+
500 def test_get_server_by_id_v1_1(self):
501 req = webob.Request.blank('/v1.1/servers/1')
502 res = req.get_response(fakes.wsgi_app())
503@@ -540,7 +587,8 @@
504 def _setup_for_create_instance(self):
505 """Shared implementation for tests below that create instance"""
506 def instance_create(context, inst):
507- return {'id': '1', 'display_name': 'server_test'}
508+ return {'id': 1, 'display_name': 'server_test',
509+ 'uuid': FAKE_UUID}
510
511 def server_update(context, id, params):
512 return instance_create(context, id)
513@@ -594,11 +642,22 @@
514 self.assertEqual(1, server['id'])
515 self.assertEqual(2, server['flavorId'])
516 self.assertEqual(3, server['imageId'])
517+ self.assertEqual(FAKE_UUID, server['uuid'])
518 self.assertEqual(res.status_int, 200)
519
520 def test_create_instance(self):
521 self._test_create_instance_helper()
522
523+ def test_create_instance_has_uuid(self):
524+ """Tests at the db-layer instead of API layer since that's where the
525+ UUID is generated
526+ """
527+ ctxt = context.RequestContext(1, 1)
528+ values = {}
529+ instance = nova.db.api.instance_create(ctxt, values)
530+ expected = FAKE_UUID
531+ self.assertEqual(instance['uuid'], expected)
532+
533 def test_create_instance_via_zones(self):
534 """Server generated ReservationID"""
535 self._setup_for_create_instance()
536@@ -1850,7 +1909,8 @@
537 self.injected_files = kwargs['injected_files']
538 else:
539 self.injected_files = None
540- return [{'id': '1234', 'display_name': 'fakeinstance'}]
541+ return [{'id': '1234', 'display_name': 'fakeinstance',
542+ 'uuid': FAKE_UUID}]
543
544 def set_admin_password(self, *args, **kwargs):
545 pass
546
547=== modified file 'nova/tests/scheduler/test_scheduler.py'
548--- nova/tests/scheduler/test_scheduler.py 2011-06-09 18:52:53 +0000
549+++ nova/tests/scheduler/test_scheduler.py 2011-06-20 15:20:30 +0000
550@@ -48,6 +48,10 @@
551 flags.DECLARE('instances_path', 'nova.compute.manager')
552
553
554+FAKE_UUID_NOT_FOUND = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
555+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
556+
557+
558 class TestDriver(driver.Scheduler):
559 """Scheduler Driver for Tests"""
560 def schedule(context, topic, *args, **kwargs):
561@@ -926,12 +930,23 @@
562 ]
563
564
565+def fake_instance_get_by_uuid(context, uuid):
566+ if FAKE_UUID_NOT_FOUND:
567+ raise exception.InstanceNotFound(instance_id=uuid)
568+ else:
569+ return {'id': 1}
570+
571+
572 class FakeRerouteCompute(api.reroute_compute):
573+ def __init__(self, method_name, id_to_return=1):
574+ super(FakeRerouteCompute, self).__init__(method_name)
575+ self.id_to_return = id_to_return
576+
577 def _call_child_zones(self, zones, function):
578 return []
579
580 def get_collection_context_and_id(self, args, kwargs):
581- return ("servers", None, 1)
582+ return ("servers", None, self.id_to_return)
583
584 def unmarshall_result(self, zone_responses):
585 return dict(magic="found me")
586@@ -960,6 +975,8 @@
587 self.stubs = stubout.StubOutForTesting()
588
589 self.stubs.Set(db, 'zone_get_all', zone_get_all)
590+ self.stubs.Set(db, 'instance_get_by_uuid',
591+ fake_instance_get_by_uuid)
592
593 self.enable_zone_routing = FLAGS.enable_zone_routing
594 FLAGS.enable_zone_routing = True
595@@ -976,8 +993,19 @@
596 except api.RedirectResult, e:
597 self.fail(_("Successful database hit should succeed"))
598
599- def test_trap_not_found_locally(self):
600+ def test_trap_not_found_locally_id_passed(self):
601+ """When an integer ID is not found locally, we cannot reroute to
602+ another zone, so just return InstanceNotFound exception
603+ """
604 decorator = FakeRerouteCompute("foo")
605+ self.assertRaises(exception.InstanceNotFound,
606+ decorator(go_boom), None, None, 1)
607+
608+ def test_trap_not_found_locally_uuid_passed(self):
609+ """When a UUID is found, if the item isn't found locally, we should
610+ try to reroute to a child zone to see if they have it
611+ """
612+ decorator = FakeRerouteCompute("foo", id_to_return=FAKE_UUID_NOT_FOUND)
613 try:
614 result = decorator(go_boom)(None, None, 1)
615 self.assertFail(_("Should have rerouted."))
616
617=== modified file 'nova/tests/test_utils.py'
618--- nova/tests/test_utils.py 2011-05-11 18:02:01 +0000
619+++ nova/tests/test_utils.py 2011-06-20 15:20:30 +0000
620@@ -275,3 +275,21 @@
621 # error case
622 result = utils.parse_server_string('www.exa:mple.com:8443')
623 self.assertEqual(('', ''), result)
624+
625+
626+class IsUUIDLikeTestCase(test.TestCase):
627+ def assertUUIDLike(self, val, expected):
628+ result = utils.is_uuid_like(val)
629+ self.assertEqual(result, expected)
630+
631+ def test_good_uuid(self):
632+ val = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
633+ self.assertUUIDLike(val, True)
634+
635+ def test_integer_passed(self):
636+ val = 1
637+ self.assertUUIDLike(val, False)
638+
639+ def test_non_uuid_string_passed(self):
640+ val = 'foo-fooo'
641+ self.assertUUIDLike(val, False)
642
643=== modified file 'nova/utils.py'
644--- nova/utils.py 2011-06-07 19:17:34 +0000
645+++ nova/utils.py 2011-06-20 15:20:30 +0000
646@@ -35,6 +35,7 @@
647 import sys
648 import time
649 import types
650+import uuid
651 from xml.sax import saxutils
652
653 from eventlet import event
654@@ -726,3 +727,17 @@
655 except:
656 LOG.debug(_('Invalid server_string: %s' % server_str))
657 return ('', '')
658+
659+
660+def gen_uuid():
661+ return uuid.uuid4()
662+
663+
664+def is_uuid_like(val):
665+ """For our purposes, a UUID is a string in canoical form:
666+
667+ aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
668+ """
669+ if not isinstance(val, basestring):
670+ return False
671+ return (len(val) == 36) and (val.count('-') == 4)