Merge lp:~cerberus/nova/xs_resize into lp:~hudson-openstack/nova/trunk

Proposed by Matt Dietz
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
Reviewer Review Type Date Requested Status
Rick Harris (community) Approve
Sandy Walsh (community) Approve
Review via email: mp+53903@code.launchpad.net

Description of the change

Updates the previously merged xs_migration functionality to allow upsizing of the RAM and disk quotas for a XenServer instance.

To post a comment you must log in.
Revision history for this message
Rick Harris (rconradharris) wrote :

Good stuff, just some small nits:

Test failure for me: http://paste.openstack.org/show/928/

> 19 + try:
> 20 + migration = db.migration_get_by_instance_and_status(ctxt,
> 21 + inst['id'], 'finished')
> 22 + inst_dict['status'] = 'resize-confirm'
> 23 + except Exception, e:
> 24 + inst_dict['status'] = power_mapping[inst_dict['status']]

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')
  if len(migration) > 1:
    log.warn("More than one finished migration, something bad happened")
  elif migration:
    inst_dict['status'] = 'resize-confirm'
  else:
    inst_dict['status'] = power_mapping[inst_dict['status']]

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.

review: Needs Fixing
Revision history for this message
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.

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

Rick: Looks like jsb is adding periodic tasks handling in his ~justin-fathomdb/nova/refresh-instance-state branch. Presumably the auto-confirm functionality can be added some time in Diablo after this branch lands.

Revision history for this message
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?

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

sandy: thanks for the review dude. I think I've covered everything you mentioned.

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

lgtm

review: Approve
Revision history for this message
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_instance_type['memory_mb'] >= \
> 98 + new_instance_type['memory_mb']:
> 99 + raise exception.ApiError(_("Invalid flavor: cannot downsize"
> 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_instance_type['memory_mb']
    new_mem_mb = new_instance_type['memory_mb']
    if cur_mem_mb == new_mem_mb:
        raise exception.ApiError(_("Invalid flavor: cannot use same size"))
    elif cur_mem_mb < new_mem_mb:
        raise exception.ApiError(_("Invalid flavor: cannot downsize"))

> 409 + def test_resize_down_fails(self):
> 410 + """Ensure invalid flavors raise"""

Doc-string doesn't match.

Also, might be a good idea to include:

    def test_resize_same_size_fails(self):
        """Customers cannot resize to the same size. Admins however, can use
        resize to same size for purposes of migrations
        """

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

Thanks for the fixes, looks great!

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

No big deal, but there is an extra space here on #104:

103 + raise exception.ApiError(_("Invalid flavor: cannot use"
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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))