Merge lp:~rconradharris/nova/uuid-redux into lp:~hudson-openstack/nova/trunk
- uuid-redux
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
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 |
Commit message
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.
Brian Waldon (bcwaldon) wrote : | # |
Josh Kearney (jk0) wrote : | # |
> Is there a bug/blueprint you can attach this to?
nova-instance-
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_
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.
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_
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.
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.
Rick Harris (rconradharris) wrote : | # |
> 178: This method name is a bit misleading. Can you call it _build_
Fixed.
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.
Brian Waldon (bcwaldon) wrote : | # |
> > 178: This method name is a bit misleading. Can you call it
> _build_
>
> Fixed.
Cool.
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?
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.
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://
Ed Leafe (ed-leafe) wrote : | # |
Here's a patch that fixes the problem:
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>'.
Ed Leafe (ed-leafe) wrote : | # |
Ah, I see none of the ec2 changes are included. So I guess that's by design.
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://
> migration.
Very nice catch. This could have caused a lot of headaches.
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.
- 1200. By Rick Harris
-
Fixed migration per review feedback.
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://
> results after running the migration.
Nice catch. Fixed.
Matt Dietz (cerberus) wrote : | # |
Looks awesome. Love the tests.
Ed Leafe (ed-leafe) wrote : | # |
utils.py: is_uuid_like(val) is incorrect. A value of "12345678901234
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.
return not bool([ch for ch in val if ch not in acceptable])
return False
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/
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.
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-
Ed Leafe (ed-leafe) wrote : | # |
Just noticed that the above comment also applies to lines 554-5.
- 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
Rick Harris (rconradharris) wrote : | # |
> This should be changed to something akin to:
> FAKE_UUID = "abcdabcd-
Fixed.
Made is_uuid_like a bit stricter in what it accepts, added unit-tests for it.
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'.
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.
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-
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-
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-
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.
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.
Rick Harris (rconradharris) wrote : | # |
VARCHARs are used because that's what SQLAlchemy exposes via its `String` datatype.
http://
- 1204. By Rick Harris
-
Merging trunk, fixing conflicts
- 1205. By Rick Harris
-
Moving add_uuid migration to 025
Preview Diff
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) |
Is there a bug/blueprint you can attach this to?