Merge lp:~cerberus/nova/xs_resize into lp:~hudson-openstack/nova/trunk
- xs_resize
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Rick Harris |
Approved revision: | 826 |
Merged at revision: | 855 |
Proposed branch: | lp:~cerberus/nova/xs_resize |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
682 lines (+277/-67) 14 files modified
nova/api/openstack/servers.py (+1/-0) nova/api/openstack/views/servers.py (+8/-0) nova/compute/api.py (+39/-3) nova/compute/manager.py (+52/-21) nova/db/sqlalchemy/api.py (+4/-4) nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py (+50/-0) nova/db/sqlalchemy/models.py (+2/-0) nova/tests/api/openstack/test_servers.py (+12/-10) nova/tests/test_compute.py (+55/-4) nova/tests/test_localization.py (+4/-3) nova/tests/xenapi/stubs.py (+5/-2) nova/virt/xenapi/vmops.py (+38/-12) nova/virt/xenapi_conn.py (+5/-7) plugins/xenserver/xenapi/etc/xapi.d/plugins/migration (+2/-1) |
To merge this branch: | bzr merge lp:~cerberus/nova/xs_resize |
Related bugs: | |
Related blueprints: |
XenServer Resize
(High)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Rick Harris (community) | Approve | ||
Sandy Walsh (community) | Approve | ||
Review via email: mp+53903@code.launchpad.net |
Commit message
Description of the change
Updates the previously merged xs_migration functionality to allow upsizing of the RAM and disk quotas for a XenServer instance.
Matt Dietz (cerberus) wrote : | # |
Thanks for the review Rick.
I'll take a look at the test failure
In that block, your addendum makes sense. As for the catch, the migration_get DB call raises if none are found, which seems to be the convention in the DB API
Good point on the same flavor resize. I forgot about that one.
I hadn't previously seen any downgrade functionality. I'm assuming that backported to most of the existing db migrations after I wrote that originally?
I'll address the rest.
And yes, auto-confirm will have to be addressed later. AFAIK, we haven't discussed any sort of auto-maintenance routines via cron or something else. Please correct me if I'm wrong.
Matt Dietz (cerberus) wrote : | # |
Rick: Looks like jsb is adding periodic tasks handling in his ~justin-
Sandy Walsh (sandy-walsh) wrote : | # |
Looks good. No major complaints.
Minor niggly things:
Shouldn't be db operations in api.openstack ... they should be moved into <service>/api.py in the event another API wanted to offer resize.
37 - except: not allowed (see HACKING). Should be except Exception: at the least
80 - perhaps a hint about the flavor not found?
144-145,192-193,etc - space after #? Dunno the pep8 rule there?
Matt Dietz (cerberus) wrote : | # |
sandy: thanks for the review dude. I think I've covered everything you mentioned.
Rick Harris (rconradharris) wrote : | # |
Thanks for the fixes, Matt. Just a few more minor nits:
> 55 + except Exception, e:
> 56 + return False
Should probably be changed to:
except exception.NotFound:
return False
Wouldn't, for example, want MySQL-Gone-Away errors getting caught here.
> 97 + if current_
> 98 + new_instance_
> 99 + raise exception.
> 100 + "instances"))
I think this could be a little clearer, to distinguish between the resize
down/same-size cases. Something like:
cur_mem_mb = current_
new_mem_mb = new_instance_
if cur_mem_mb == new_mem_mb:
raise exception.
elif cur_mem_mb < new_mem_mb:
raise exception.
> 409 + def test_resize_
> 410 + """Ensure invalid flavors raise"""
Doc-string doesn't match.
Also, might be a good idea to include:
def test_resize_
resize to same size for purposes of migrations
"""
Rick Harris (rconradharris) wrote : | # |
Thanks for the fixes, looks great!
Josh Kearney (jk0) wrote : | # |
No big deal, but there is an extra space here on #104:
103 + raise exception.
104 + "the same flavor. "))
Also not a big deal, but will this pass pep8.py v0.5.x (current version included in pip-requires)?
149 + 'instance_id': instance_id, },
150 + })
Preview Diff
1 | === modified file 'nova/api/openstack/servers.py' |
2 | --- nova/api/openstack/servers.py 2011-03-17 22:24:08 +0000 |
3 | +++ nova/api/openstack/servers.py 2011-03-23 21:12:17 +0000 |
4 | @@ -22,6 +22,7 @@ |
5 | from webob import exc |
6 | |
7 | from nova import compute |
8 | +from nova import context |
9 | from nova import exception |
10 | from nova import flags |
11 | from nova import log as logging |
12 | |
13 | === modified file 'nova/api/openstack/views/servers.py' |
14 | --- nova/api/openstack/views/servers.py 2011-03-15 23:55:13 +0000 |
15 | +++ nova/api/openstack/views/servers.py 2011-03-23 21:12:17 +0000 |
16 | @@ -16,7 +16,10 @@ |
17 | # under the License. |
18 | |
19 | import hashlib |
20 | + |
21 | from nova.compute import power_state |
22 | +import nova.compute |
23 | +import nova.context |
24 | from nova.api.openstack import common |
25 | from nova.api.openstack.views import addresses as addresses_view |
26 | from nova.api.openstack.views import flavors as flavors_view |
27 | @@ -84,7 +87,12 @@ |
28 | for k, v in mapped_keys.iteritems(): |
29 | inst_dict[k] = inst[v] |
30 | |
31 | + ctxt = nova.context.get_admin_context() |
32 | inst_dict['status'] = power_mapping[inst_dict['status']] |
33 | + compute_api = nova.compute.API() |
34 | + if compute_api.has_finished_migration(ctxt, inst['id']): |
35 | + inst_dict['status'] = 'resize-confirm' |
36 | + |
37 | inst_dict['addresses'] = self.addresses_builder.build(inst) |
38 | |
39 | # Return the metadata as a dictionary |
40 | |
41 | === modified file 'nova/compute/api.py' |
42 | --- nova/compute/api.py 2011-03-15 21:56:00 +0000 |
43 | +++ nova/compute/api.py 2011-03-23 21:12:17 +0000 |
44 | @@ -253,6 +253,16 @@ |
45 | |
46 | return [dict(x.iteritems()) for x in instances] |
47 | |
48 | + def has_finished_migration(self, context, instance_id): |
49 | + """Retrieves whether or not a finished migration exists for |
50 | + an instance""" |
51 | + try: |
52 | + db.migration_get_by_instance_and_status(context, instance_id, |
53 | + 'finished') |
54 | + return True |
55 | + except exception.NotFound: |
56 | + return False |
57 | + |
58 | def ensure_default_security_group(self, context): |
59 | """ Create security group for the security context if it |
60 | does not already exist |
61 | @@ -464,6 +474,8 @@ |
62 | params = {'migration_id': migration_ref['id']} |
63 | self._cast_compute_message('revert_resize', context, instance_id, |
64 | migration_ref['dest_compute'], params=params) |
65 | + self.db.migration_update(context, migration_ref['id'], |
66 | + {'status': 'reverted'}) |
67 | |
68 | def confirm_resize(self, context, instance_id): |
69 | """Confirms a migration/resize, deleting the 'old' instance in the |
70 | @@ -479,17 +491,41 @@ |
71 | self._cast_compute_message('confirm_resize', context, instance_id, |
72 | migration_ref['source_compute'], params=params) |
73 | |
74 | - self.db.migration_update(context, migration_id, |
75 | + self.db.migration_update(context, migration_ref['id'], |
76 | {'status': 'confirmed'}) |
77 | self.db.instance_update(context, instance_id, |
78 | {'host': migration_ref['dest_compute'], }) |
79 | |
80 | - def resize(self, context, instance_id, flavor): |
81 | + def resize(self, context, instance_id, flavor_id): |
82 | """Resize a running instance.""" |
83 | + instance = self.db.instance_get(context, instance_id) |
84 | + current_instance_type = self.db.instance_type_get_by_name( |
85 | + context, instance['instance_type']) |
86 | + |
87 | + new_instance_type = self.db.instance_type_get_by_flavor_id( |
88 | + context, flavor_id) |
89 | + current_instance_type_name = current_instance_type['name'] |
90 | + new_instance_type_name = new_instance_type['name'] |
91 | + LOG.debug(_("Old instance type %(current_instance_type_name)s, " |
92 | + " new instance type %(new_instance_type_name)s") % locals()) |
93 | + if not new_instance_type: |
94 | + raise exception.ApiError(_("Requested flavor %(flavor_id)d " |
95 | + "does not exist") % locals()) |
96 | + |
97 | + current_memory_mb = current_instance_type['memory_mb'] |
98 | + new_memory_mb = new_instance_type['memory_mb'] |
99 | + if current_memory_mb > new_memory_mb: |
100 | + raise exception.ApiError(_("Invalid flavor: cannot downsize" |
101 | + "instances")) |
102 | + if current_memory_mb == new_memory_mb: |
103 | + raise exception.ApiError(_("Invalid flavor: cannot use" |
104 | + "the same flavor. ")) |
105 | + |
106 | self._cast_scheduler_message(context, |
107 | {"method": "prep_resize", |
108 | "args": {"topic": FLAGS.compute_topic, |
109 | - "instance_id": instance_id, }},) |
110 | + "instance_id": instance_id, |
111 | + "flavor_id": flavor_id}}) |
112 | |
113 | def pause(self, context, instance_id): |
114 | """Pause the given instance.""" |
115 | |
116 | === modified file 'nova/compute/manager.py' |
117 | --- nova/compute/manager.py 2011-03-21 15:41:03 +0000 |
118 | +++ nova/compute/manager.py 2011-03-23 21:12:17 +0000 |
119 | @@ -437,25 +437,41 @@ |
120 | instance_ref = self.db.instance_get(context, instance_id) |
121 | migration_ref = self.db.migration_get(context, migration_id) |
122 | |
123 | - #TODO(mdietz): we may want to split these into separate methods. |
124 | - if migration_ref['source_compute'] == FLAGS.host: |
125 | - self.driver._start(instance_ref) |
126 | - self.db.migration_update(context, migration_id, |
127 | - {'status': 'reverted'}) |
128 | - else: |
129 | - self.driver.destroy(instance_ref) |
130 | - topic = self.db.queue_get_for(context, FLAGS.compute_topic, |
131 | - instance_ref['host']) |
132 | - rpc.cast(context, topic, |
133 | - {'method': 'revert_resize', |
134 | - 'args': { |
135 | - 'migration_id': migration_ref['id'], |
136 | - 'instance_id': instance_id, }, |
137 | - }) |
138 | - |
139 | - @exception.wrap_exception |
140 | - @checks_instance_lock |
141 | - def prep_resize(self, context, instance_id): |
142 | + self.driver.destroy(instance_ref) |
143 | + topic = self.db.queue_get_for(context, FLAGS.compute_topic, |
144 | + instance_ref['host']) |
145 | + rpc.cast(context, topic, |
146 | + {'method': 'finish_revert_resize', |
147 | + 'args': { |
148 | + 'migration_id': migration_ref['id'], |
149 | + 'instance_id': instance_id, }, |
150 | + }) |
151 | + |
152 | + @exception.wrap_exception |
153 | + @checks_instance_lock |
154 | + def finish_revert_resize(self, context, instance_id, migration_id): |
155 | + """Finishes the second half of reverting a resize, powering back on |
156 | + the source instance and reverting the resized attributes in the |
157 | + database""" |
158 | + instance_ref = self.db.instance_get(context, instance_id) |
159 | + migration_ref = self.db.migration_get(context, migration_id) |
160 | + instance_type = self.db.instance_type_get_by_flavor_id(context, |
161 | + migration_ref['old_flavor_id']) |
162 | + |
163 | + # Just roll back the record. There's no need to resize down since |
164 | + # the 'old' VM already has the preferred attributes |
165 | + self.db.instance_update(context, instance_id, |
166 | + dict(memory_mb=instance_type['memory_mb'], |
167 | + vcpus=instance_type['vcpus'], |
168 | + local_gb=instance_type['local_gb'])) |
169 | + |
170 | + self.driver.revert_resize(instance_ref) |
171 | + self.db.migration_update(context, migration_id, |
172 | + {'status': 'reverted'}) |
173 | + |
174 | + @exception.wrap_exception |
175 | + @checks_instance_lock |
176 | + def prep_resize(self, context, instance_id, flavor_id): |
177 | """Initiates the process of moving a running instance to another |
178 | host, possibly changing the RAM and disk size in the process""" |
179 | context = context.elevated() |
180 | @@ -464,12 +480,17 @@ |
181 | raise exception.Error(_( |
182 | 'Migration error: destination same as source!')) |
183 | |
184 | + instance_type = self.db.instance_type_get_by_flavor_id(context, |
185 | + flavor_id) |
186 | migration_ref = self.db.migration_create(context, |
187 | {'instance_id': instance_id, |
188 | 'source_compute': instance_ref['host'], |
189 | 'dest_compute': FLAGS.host, |
190 | 'dest_host': self.driver.get_host_ip_addr(), |
191 | + 'old_flavor_id': instance_type['flavorid'], |
192 | + 'new_flavor_id': flavor_id, |
193 | 'status': 'pre-migrating'}) |
194 | + |
195 | LOG.audit(_('instance %s: migrating to '), instance_id, |
196 | context=context) |
197 | topic = self.db.queue_get_for(context, FLAGS.compute_topic, |
198 | @@ -495,8 +516,6 @@ |
199 | self.db.migration_update(context, migration_id, |
200 | {'status': 'post-migrating', }) |
201 | |
202 | - #TODO(mdietz): This is where we would update the VM record |
203 | - #after resizing |
204 | service = self.db.service_get_by_host_and_topic(context, |
205 | migration_ref['dest_compute'], FLAGS.compute_topic) |
206 | topic = self.db.queue_get_for(context, FLAGS.compute_topic, |
207 | @@ -517,7 +536,19 @@ |
208 | migration_ref = self.db.migration_get(context, migration_id) |
209 | instance_ref = self.db.instance_get(context, |
210 | migration_ref['instance_id']) |
211 | + # TODO(mdietz): apply the rest of the instance_type attributes going |
212 | + # after they're supported |
213 | + instance_type = self.db.instance_type_get_by_flavor_id(context, |
214 | + migration_ref['new_flavor_id']) |
215 | + self.db.instance_update(context, instance_id, |
216 | + dict(instance_type=instance_type['name'], |
217 | + memory_mb=instance_type['memory_mb'], |
218 | + vcpus=instance_type['vcpus'], |
219 | + local_gb=instance_type['local_gb'])) |
220 | |
221 | + # reload the updated instance ref |
222 | + # FIXME(mdietz): is there reload functionality? |
223 | + instance_ref = self.db.instance_get(context, instance_id) |
224 | self.driver.finish_resize(instance_ref, disk_info) |
225 | |
226 | self.db.migration_update(context, migration_id, |
227 | |
228 | === modified file 'nova/db/sqlalchemy/api.py' |
229 | --- nova/db/sqlalchemy/api.py 2011-03-23 05:29:32 +0000 |
230 | +++ nova/db/sqlalchemy/api.py 2011-03-23 21:12:17 +0000 |
231 | @@ -2220,8 +2220,8 @@ |
232 | filter_by(instance_id=instance_id).\ |
233 | filter_by(status=status).first() |
234 | if not result: |
235 | - raise exception.NotFound(_("No migration found with instance id %s") |
236 | - % migration_id) |
237 | + raise exception.NotFound(_("No migration found for instance " |
238 | + "%(instance_id)s with status %(status)s") % locals()) |
239 | return result |
240 | |
241 | |
242 | @@ -2336,8 +2336,8 @@ |
243 | instance_type_ref = models.InstanceTypes() |
244 | instance_type_ref.update(values) |
245 | instance_type_ref.save() |
246 | - except: |
247 | - raise exception.DBError |
248 | + except Exception, e: |
249 | + raise exception.DBError(e) |
250 | return instance_type_ref |
251 | |
252 | |
253 | |
254 | === added file 'nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py' |
255 | --- nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py 1970-01-01 00:00:00 +0000 |
256 | +++ nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py 2011-03-23 21:12:17 +0000 |
257 | @@ -0,0 +1,50 @@ |
258 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
259 | + |
260 | +# Copyright 2011 OpenStack LLC. |
261 | +# All Rights Reserved. |
262 | +# |
263 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
264 | +# not use this file except in compliance with the License. You may obtain |
265 | +# a copy of the License at |
266 | +# |
267 | +# http://www.apache.org/licenses/LICENSE-2.0 |
268 | +# |
269 | +# Unless required by applicable law or agreed to in writing, software |
270 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
271 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
272 | +# License for the specific language governing permissions and limitations |
273 | +# under the License.from sqlalchemy import * |
274 | + |
275 | +from sqlalchemy import * |
276 | +from migrate import * |
277 | + |
278 | +from nova import log as logging |
279 | + |
280 | + |
281 | +meta = MetaData() |
282 | + |
283 | +migrations = Table('migrations', meta, |
284 | + Column('id', Integer(), primary_key=True, nullable=False), |
285 | + ) |
286 | + |
287 | +# |
288 | +# Tables to alter |
289 | +# |
290 | +# |
291 | + |
292 | +old_flavor_id = Column('old_flavor_id', Integer()) |
293 | +new_flavor_id = Column('new_flavor_id', Integer()) |
294 | + |
295 | + |
296 | +def upgrade(migrate_engine): |
297 | + # Upgrade operations go here. Don't create your own engine; |
298 | + # bind migrate_engine to your metadata |
299 | + meta.bind = migrate_engine |
300 | + migrations.create_column(old_flavor_id) |
301 | + migrations.create_column(new_flavor_id) |
302 | + |
303 | + |
304 | +def downgrade(migrate_engine): |
305 | + meta.bind = migrate_engine |
306 | + migrations.drop_column(old_flavor_id) |
307 | + migrations.drop_column(new_flavor_id) |
308 | |
309 | === modified file 'nova/db/sqlalchemy/models.py' |
310 | --- nova/db/sqlalchemy/models.py 2011-03-23 05:29:32 +0000 |
311 | +++ nova/db/sqlalchemy/models.py 2011-03-23 21:12:17 +0000 |
312 | @@ -436,6 +436,8 @@ |
313 | source_compute = Column(String(255)) |
314 | dest_compute = Column(String(255)) |
315 | dest_host = Column(String(255)) |
316 | + old_flavor_id = Column(Integer()) |
317 | + new_flavor_id = Column(Integer()) |
318 | instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True) |
319 | #TODO(_cerberus_): enum |
320 | status = Column(String(255)) |
321 | |
322 | === modified file 'nova/tests/api/openstack/test_servers.py' |
323 | --- nova/tests/api/openstack/test_servers.py 2011-03-20 19:06:22 +0000 |
324 | +++ nova/tests/api/openstack/test_servers.py 2011-03-23 21:12:17 +0000 |
325 | @@ -524,16 +524,6 @@ |
326 | req.body = json.dumps(body) |
327 | res = req.get_response(fakes.wsgi_app()) |
328 | |
329 | - def test_server_resize(self): |
330 | - body = dict(server=dict( |
331 | - name='server_test', imageId=2, flavorId=2, metadata={}, |
332 | - personality={})) |
333 | - req = webob.Request.blank('/v1.0/servers/1/action') |
334 | - req.method = 'POST' |
335 | - req.content_type = 'application/json' |
336 | - req.body = json.dumps(body) |
337 | - res = req.get_response(fakes.wsgi_app()) |
338 | - |
339 | def test_delete_server_instance(self): |
340 | req = webob.Request.blank('/v1.0/servers/1') |
341 | req.method = 'DELETE' |
342 | @@ -589,6 +579,18 @@ |
343 | res = req.get_response(fakes.wsgi_app()) |
344 | self.assertEqual(res.status_int, 400) |
345 | |
346 | + def test_resized_server_has_correct_status(self): |
347 | + req = self.webreq('/1', 'GET', dict(resize=dict(flavorId=3))) |
348 | + |
349 | + def fake_migration_get(*args): |
350 | + return {} |
351 | + |
352 | + self.stubs.Set(nova.db, 'migration_get_by_instance_and_status', |
353 | + fake_migration_get) |
354 | + res = req.get_response(fakes.wsgi_app()) |
355 | + body = json.loads(res.body) |
356 | + self.assertEqual(body['server']['status'], 'resize-confirm') |
357 | + |
358 | def test_confirm_resize_server(self): |
359 | req = self.webreq('/1/action', 'POST', dict(confirmResize=None)) |
360 | |
361 | |
362 | === modified file 'nova/tests/test_compute.py' |
363 | --- nova/tests/test_compute.py 2011-03-14 22:17:00 +0000 |
364 | +++ nova/tests/test_compute.py 2011-03-23 21:12:17 +0000 |
365 | @@ -82,6 +82,21 @@ |
366 | inst.update(params) |
367 | return db.instance_create(self.context, inst)['id'] |
368 | |
369 | + def _create_instance_type(self, params={}): |
370 | + """Create a test instance""" |
371 | + context = self.context.elevated() |
372 | + inst = {} |
373 | + inst['name'] = 'm1.small' |
374 | + inst['memory_mb'] = '1024' |
375 | + inst['vcpus'] = '1' |
376 | + inst['local_gb'] = '20' |
377 | + inst['flavorid'] = '1' |
378 | + inst['swap'] = '2048' |
379 | + inst['rxtx_quota'] = 100 |
380 | + inst['rxtx_cap'] = 200 |
381 | + inst.update(params) |
382 | + return db.instance_type_create(context, inst)['id'] |
383 | + |
384 | def _create_group(self): |
385 | values = {'name': 'testgroup', |
386 | 'description': 'testgroup', |
387 | @@ -299,15 +314,53 @@ |
388 | """Ensure instance can be migrated/resized""" |
389 | instance_id = self._create_instance() |
390 | context = self.context.elevated() |
391 | + |
392 | self.compute.run_instance(self.context, instance_id) |
393 | db.instance_update(self.context, instance_id, {'host': 'foo'}) |
394 | - self.compute.prep_resize(context, instance_id) |
395 | + self.compute.prep_resize(context, instance_id, 1) |
396 | migration_ref = db.migration_get_by_instance_and_status(context, |
397 | instance_id, 'pre-migrating') |
398 | self.compute.resize_instance(context, instance_id, |
399 | migration_ref['id']) |
400 | self.compute.terminate_instance(context, instance_id) |
401 | |
402 | + def test_resize_invalid_flavor_fails(self): |
403 | + """Ensure invalid flavors raise""" |
404 | + instance_id = self._create_instance() |
405 | + context = self.context.elevated() |
406 | + self.compute.run_instance(self.context, instance_id) |
407 | + |
408 | + self.assertRaises(exception.NotFound, self.compute_api.resize, |
409 | + context, instance_id, 200) |
410 | + |
411 | + self.compute.terminate_instance(context, instance_id) |
412 | + |
413 | + def test_resize_down_fails(self): |
414 | + """Ensure resizing down raises and fails""" |
415 | + context = self.context.elevated() |
416 | + instance_id = self._create_instance() |
417 | + |
418 | + self.compute.run_instance(self.context, instance_id) |
419 | + db.instance_update(self.context, instance_id, |
420 | + {'instance_type': 'm1.xlarge'}) |
421 | + |
422 | + self.assertRaises(exception.ApiError, self.compute_api.resize, |
423 | + context, instance_id, 1) |
424 | + |
425 | + self.compute.terminate_instance(context, instance_id) |
426 | + |
427 | + def test_resize_same_size_fails(self): |
428 | + """Ensure invalid flavors raise""" |
429 | + context = self.context.elevated() |
430 | + instance_id = self._create_instance() |
431 | + |
432 | + self.compute.run_instance(self.context, instance_id) |
433 | + |
434 | + self.assertRaises(exception.ApiError, self.compute_api.resize, |
435 | + context, instance_id, 1) |
436 | + |
437 | + self.compute.terminate_instance(context, instance_id) |
438 | + |
439 | def test_get_by_flavor_id(self): |
440 | type = instance_types.get_by_flavor_id(1) |
441 | self.assertEqual(type, 'm1.tiny') |
442 | @@ -318,10 +371,8 @@ |
443 | instance_id = self._create_instance() |
444 | self.compute.run_instance(self.context, instance_id) |
445 | self.assertRaises(exception.Error, self.compute.prep_resize, |
446 | - self.context, instance_id) |
447 | + self.context, instance_id, 1) |
448 | self.compute.terminate_instance(self.context, instance_id) |
449 | - type = instance_types.get_by_flavor_id("1") |
450 | - self.assertEqual(type, 'm1.tiny') |
451 | |
452 | def _setup_other_managers(self): |
453 | self.volume_manager = utils.import_object(FLAGS.volume_manager) |
454 | |
455 | === modified file 'nova/tests/test_localization.py' |
456 | --- nova/tests/test_localization.py 2011-02-21 07:16:10 +0000 |
457 | +++ nova/tests/test_localization.py 2011-03-23 21:12:17 +0000 |
458 | @@ -21,9 +21,10 @@ |
459 | import unittest |
460 | |
461 | import nova |
462 | - |
463 | - |
464 | -class LocalizationTestCase(unittest.TestCase): |
465 | +from nova import test |
466 | + |
467 | + |
468 | +class LocalizationTestCase(test.TestCase): |
469 | def test_multiple_positional_format_placeholders(self): |
470 | pat = re.compile("\W_\(") |
471 | single_pat = re.compile("\W%\W") |
472 | |
473 | === modified file 'nova/tests/xenapi/stubs.py' |
474 | --- nova/tests/xenapi/stubs.py 2011-03-08 23:24:19 +0000 |
475 | +++ nova/tests/xenapi/stubs.py 2011-03-23 21:12:17 +0000 |
476 | @@ -228,6 +228,9 @@ |
477 | def VDI_get_by_uuid(*args): |
478 | return 'hurr' |
479 | |
480 | + def VDI_resize_online(*args): |
481 | + pass |
482 | + |
483 | def VM_start(self, _1, ref, _2, _3): |
484 | vm = fake.get_record('VM', ref) |
485 | if vm['power_state'] != 'Halted': |
486 | @@ -240,7 +243,7 @@ |
487 | |
488 | def stub_out_migration_methods(stubs): |
489 | def fake_get_snapshot(self, instance): |
490 | - return 'foo', 'bar' |
491 | + return 'vm_ref', dict(image='foo', snap='bar') |
492 | |
493 | @classmethod |
494 | def fake_get_vdi(cls, session, vm_ref): |
495 | @@ -249,7 +252,7 @@ |
496 | vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref) |
497 | return vdi_ref, {'uuid': vdi_rec['uuid'], } |
498 | |
499 | - def fake_shutdown(self, inst, vm, method='clean'): |
500 | + def fake_shutdown(self, inst, vm, hard=True): |
501 | pass |
502 | |
503 | @classmethod |
504 | |
505 | === modified file 'nova/virt/xenapi/vmops.py' |
506 | --- nova/virt/xenapi/vmops.py 2011-03-23 12:53:10 +0000 |
507 | +++ nova/virt/xenapi/vmops.py 2011-03-23 21:12:17 +0000 |
508 | @@ -63,6 +63,17 @@ |
509 | vm_refs.append(vm_rec["name_label"]) |
510 | return vm_refs |
511 | |
512 | + def revert_resize(self, instance): |
513 | + vm_ref = VMHelper.lookup(self._session, instance.name) |
514 | + self._start(instance, vm_ref) |
515 | + |
516 | + def finish_resize(self, instance, disk_info): |
517 | + vdi_uuid = self.link_disks(instance, disk_info['base_copy'], |
518 | + disk_info['cow']) |
519 | + vm_ref = self._create_vm(instance, vdi_uuid) |
520 | + self.resize_instance(instance, vdi_uuid) |
521 | + self._spawn(instance, vm_ref) |
522 | + |
523 | def _start(self, instance, vm_ref=None): |
524 | """Power on a VM instance""" |
525 | if not vm_ref: |
526 | @@ -73,7 +84,7 @@ |
527 | LOG.debug(_("Starting instance %s"), instance.name) |
528 | self._session.call_xenapi('VM.start', vm_ref, False, False) |
529 | |
530 | - def create_disk(self, instance): |
531 | + def _create_disk(self, instance): |
532 | user = AuthManager().get_user(instance.user_id) |
533 | project = AuthManager().get_project(instance.project_id) |
534 | disk_image_type = VMHelper.determine_disk_image_type(instance) |
535 | @@ -82,10 +93,11 @@ |
536 | return vdi_uuid |
537 | |
538 | def spawn(self, instance, network_info=None): |
539 | - vdi_uuid = self.create_disk(instance) |
540 | - self._spawn_with_disk(instance, vdi_uuid, network_info) |
541 | + vdi_uuid = self._create_disk(instance) |
542 | + vm_ref = self._create_vm(instance, vdi_uuid, network_info) |
543 | + self._spawn(instance, vm_ref) |
544 | |
545 | - def _spawn_with_disk(self, instance, vdi_uuid, network_info=None): |
546 | + def _create_vm(self, instance, vdi_uuid, network_info=None): |
547 | """Create VM instance""" |
548 | instance_name = instance.name |
549 | vm_ref = VMHelper.lookup(self._session, instance_name) |
550 | @@ -128,16 +140,19 @@ |
551 | VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, |
552 | vdi_ref=vdi_ref, userdevice=0, bootable=True) |
553 | |
554 | - # inject_network_info and create vifs |
555 | # TODO(tr3buchet) - check to make sure we have network info, otherwise |
556 | # create it now. This goes away once nova-multi-nic hits. |
557 | if network_info is None: |
558 | network_info = self._get_network_info(instance) |
559 | self.create_vifs(vm_ref, network_info) |
560 | self.inject_network_info(instance, vm_ref, network_info) |
561 | + return vm_ref |
562 | |
563 | + def _spawn(self, instance, vm_ref): |
564 | + """Spawn a new instance""" |
565 | LOG.debug(_('Starting VM %s...'), vm_ref) |
566 | self._start(instance, vm_ref) |
567 | + instance_name = instance.name |
568 | LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.') |
569 | % locals()) |
570 | |
571 | @@ -310,7 +325,7 @@ |
572 | try: |
573 | # transfer the base copy |
574 | template_vm_ref, template_vdi_uuids = self._get_snapshot(instance) |
575 | - base_copy_uuid = template_vdi_uuids[1] |
576 | + base_copy_uuid = template_vdi_uuids['image'] |
577 | vdi_ref, vm_vdi_rec = \ |
578 | VMHelper.get_vdi_for_vm_safely(self._session, vm_ref) |
579 | cow_uuid = vm_vdi_rec['uuid'] |
580 | @@ -325,7 +340,7 @@ |
581 | self._session.wait_for_task(task, instance.id) |
582 | |
583 | # Now power down the instance and transfer the COW VHD |
584 | - self._shutdown(instance, vm_ref, method='clean') |
585 | + self._shutdown(instance, vm_ref, hard=False) |
586 | |
587 | params = {'host': dest, |
588 | 'vdi_uuid': cow_uuid, |
589 | @@ -345,7 +360,7 @@ |
590 | # sensible so we don't need to blindly pass around dictionaries |
591 | return {'base_copy': base_copy_uuid, 'cow': cow_uuid} |
592 | |
593 | - def attach_disk(self, instance, base_copy_uuid, cow_uuid): |
594 | + def link_disks(self, instance, base_copy_uuid, cow_uuid): |
595 | """Links the base copy VHD to the COW via the XAPI plugin""" |
596 | vm_ref = VMHelper.lookup(self._session, instance.name) |
597 | new_base_copy_uuid = str(uuid.uuid4()) |
598 | @@ -366,9 +381,19 @@ |
599 | |
600 | return new_cow_uuid |
601 | |
602 | - def resize(self, instance, flavor): |
603 | + def resize_instance(self, instance, vdi_uuid): |
604 | """Resize a running instance by changing it's RAM and disk size """ |
605 | - raise NotImplementedError() |
606 | + #TODO(mdietz): this will need to be adjusted for swap later |
607 | + #The new disk size must be in bytes |
608 | + |
609 | + new_disk_size = str(instance.local_gb * 1024 * 1024 * 1024) |
610 | + instance_name = instance.name |
611 | + instance_local_gb = instance.local_gb |
612 | + LOG.debug(_("Resizing VDI %(vdi_uuid)s for instance %(instance_name)s." |
613 | + " Expanding to %(instance_local_gb)d GB") % locals()) |
614 | + vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid) |
615 | + self._session.call_xenapi('VDI.resize_online', vdi_ref, new_disk_size) |
616 | + LOG.debug(_("Resize instance %s complete") % (instance.name)) |
617 | |
618 | def reboot(self, instance): |
619 | """Reboot VM instance""" |
620 | @@ -443,8 +468,9 @@ |
621 | """Shutdown an instance""" |
622 | state = self.get_info(instance['name'])['state'] |
623 | if state == power_state.SHUTDOWN: |
624 | - LOG.warn(_("VM %(vm)s already halted, skipping shutdown...") % |
625 | - locals()) |
626 | + instance_name = instance.name |
627 | + LOG.warn(_("VM %(instance_name)s already halted," |
628 | + "skipping shutdown...") % locals()) |
629 | return |
630 | |
631 | instance_id = instance.id |
632 | |
633 | === modified file 'nova/virt/xenapi_conn.py' |
634 | --- nova/virt/xenapi_conn.py 2011-03-14 17:59:41 +0000 |
635 | +++ nova/virt/xenapi_conn.py 2011-03-23 21:12:17 +0000 |
636 | @@ -164,20 +164,18 @@ |
637 | """Create VM instance""" |
638 | self._vmops.spawn(instance) |
639 | |
640 | + def revert_resize(self, instance): |
641 | + """Reverts a resize, powering back on the instance""" |
642 | + self._vmops.revert_resize(instance) |
643 | + |
644 | def finish_resize(self, instance, disk_info): |
645 | """Completes a resize, turning on the migrated instance""" |
646 | - vdi_uuid = self._vmops.attach_disk(instance, disk_info['base_copy'], |
647 | - disk_info['cow']) |
648 | - self._vmops._spawn_with_disk(instance, vdi_uuid) |
649 | + self._vmops.finish_resize(instance, disk_info) |
650 | |
651 | def snapshot(self, instance, image_id): |
652 | """ Create snapshot from a running VM instance """ |
653 | self._vmops.snapshot(instance, image_id) |
654 | |
655 | - def resize(self, instance, flavor): |
656 | - """Resize a VM instance""" |
657 | - raise NotImplementedError() |
658 | - |
659 | def reboot(self, instance): |
660 | """Reboot VM instance""" |
661 | self._vmops.reboot(instance) |
662 | |
663 | === modified file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/migration' |
664 | --- plugins/xenserver/xenapi/etc/xapi.d/plugins/migration 2011-02-28 18:30:02 +0000 |
665 | +++ plugins/xenserver/xenapi/etc/xapi.d/plugins/migration 2011-03-23 21:12:17 +0000 |
666 | @@ -22,6 +22,7 @@ |
667 | import os |
668 | import os.path |
669 | import pickle |
670 | +import shlex |
671 | import shutil |
672 | import subprocess |
673 | |
674 | @@ -97,7 +98,7 @@ |
675 | logging.debug("Preparing to transmit %s to %s" % (source_path, |
676 | dest_path)) |
677 | |
678 | - ssh_cmd = 'ssh -o StrictHostKeyChecking=no' |
679 | + ssh_cmd = '\"ssh -o StrictHostKeyChecking=no\"' |
680 | |
681 | rsync_args = shlex.split('nohup /usr/bin/rsync -av --progress -e %s %s %s' |
682 | % (ssh_cmd, source_path, dest_path)) |
Good stuff, just some small nits:
Test failure for me: http:// paste.openstack .org/show/ 928/
> 19 + try: get_by_ instance_ and_status( ctxt, inst_dict[ 'status' ]]
> 20 + migration = db.migration_
> 21 + inst['id'], 'finished')
> 22 + inst_dict['status'] = 'resize-confirm'
> 23 + except Exception, e:
> 24 + inst_dict['status'] = power_mapping[
It's a little unclear what's going on here: what exception is being raised when we ask for a 'finished' migration? It *seems* like it would be clearer as something like:
migration = db.migration_ get_by_ instance_ and_status( ctxt, inst['id'], 'finished') dict['status' ] = 'resize-confirm' dict['status' ] = power_mapping[ inst_dict[ 'status' ]]
if len(migration) > 1:
log.warn("More than one finished migration, something bad happened")
elif migration:
inst_
else:
inst_
Do we let customers migrate servers by resizing to the same flavor. If that
isn't currently supported, perhaps we need a check at the API layer to assert
that the new flavor isn't the same as the old flavor.
> 187 + # FIXME: is there reload functionality?
FIXME(mdietz)
> 225 +# Copyright 2010 OpenStack LLC.
2011
> 261 +def upgrade( migrate_ engine) :
No downgrade()? Are not supporting rolling back migrations at this point?
I'm assuming auto-confirm will be tackled in a future bp.