Merge lp:~rackspace-titan/nova/rebuild-server-lp730233 into lp:~hudson-openstack/nova/trunk

Proposed by Brian Waldon
Status: Merged
Approved by: Jay Pipes
Approved revision: 959
Merged at revision: 1051
Proposed branch: lp:~rackspace-titan/nova/rebuild-server-lp730233
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 599 lines (+359/-60)
9 files modified
nova/api/openstack/servers.py (+79/-4)
nova/api/openstack/views/servers.py (+3/-1)
nova/compute/api.py (+28/-0)
nova/compute/manager.py (+60/-27)
nova/compute/power_state.py (+12/-9)
nova/exception.py (+5/-1)
nova/tests/api/openstack/test_servers.py (+169/-9)
nova/tests/test_exception.py (+2/-2)
nova/virt/xenapi/vmops.py (+1/-7)
To merge this branch: bzr merge lp:~rackspace-titan/nova/rebuild-server-lp730233
Reviewer Review Type Date Requested Status
Jay Pipes (community) Approve
Matt Dietz (community) Approve
Cory Wright (community) Approve
Review via email: mp+56243@code.launchpad.net

Description of the change

Adding support for server rebuild to v1.0 and v1.1 of the Openstack API

To post a comment you must log in.
Revision history for this message
Dan Prince (dan-prince) wrote :

I also re-factored the metadata properties quota branch to be a separate method in lp:~rackspace-titan/nova/server_metadata_quotas.

I named the method _check_metadata_properties_quota and it now takes a dict instead of an array.

Not a big deal we can merge this either way just wanted to make note of it.

Revision history for this message
Cory Wright (corywright) wrote :

FYI, I'm getting a conflict when I merge the latest trunk with this branch. May want to update that.

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

Should be good now. Thanks for the heads up

Revision history for this message
Cory Wright (corywright) wrote :

Nice work. lgtm.

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

Setting back to "review" because this was semi-dependent on a server metadata branch. So several merges and small tweaks later it should be good to go.

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

32 + msg = _("Instance %d is currently being rebuilt.") % instance_id

This message might be misleading, since the instance could also be building for the first time. Also, since you're already stating that in the compute API, why not catch and reraise, maintaining the original message?

52 + def _check_metadata(self, metadata):
53 + """Ensure that the metadata given is of the correct type."""
54 + try:
55 + metadata.iteritems()

This is confusing. Better written as "if isinstance(metadata, dict):" and remove the check_metadata method altogether

61 + def _check_personalities(self, personalities):

A more suitable method name might be in order. are_personalities_base64_encoded perhaps?

65 + path = personality["path"]
66 + contents = personality["contents"]

You can check for keys with the "in" keyword: 'path' in personality

03 + except exception.Error as ex:
104 + msg = _("Error encountered attempting to rebuild instance "
105 + "%(instance_id): %(ex)") % locals()

The inferred strings are lacking format parameters. I suspect you haven't had this exception raised in testing? You should still use the normal string formatting parameters you would in any other string inference. "%d(some_var) -> %s(the_other_one)" % locals(). Also, perhaps some unit tests to trigger these exception
s are raised?

155 + def _check_injected_file_format(self, injected_files):

You seem to be doing a lot of base64 decodability checking. Perhaps you should move such a thing somewhere common so you're not repeating yourself? Also, could you rename the method to be more explicit? check_injected_file_base64_encoded? Or if you move the method, "is_base64_encoded"

211 + def _update_state(self, context, instance_id, state=None):

Need some information on this one. Why would we call it to update a state to None?

621 - db.instance_set_state(context.get_admin_context(),
622 - instance['id'],
623 - power_state.NOSTATE,
624 - 'launching')

I would be very cautious about adding new power states. I've seen plenty of cases in the past where assumptions are made about what state something is *not*, which can cause all sorts of havoc with a new state it's not familiar with. I don't see anything particularly glaring about what you've done, but be aware of the potential for disaster, especially with the EC2 API possibly also relying on a "NOSTATE"

Nits:
97 + args = [context, instance_id, image_id, metadata, personalities]
98 + self.compute_api.rebuild(*args)

I suspect this is to avoid extending past the line-length limit. However, instantiating a new list for it seems a little pointless to me.
My personal preference is to do this instead:

self.compute_api.rebuild(context, instance_id, image_id, metadata,
                         personalities)

review: Needs Fixing
Revision history for this message
Mark Washenberger (markwash) wrote :

> 155 + def _check_injected_file_format(self, injected_files):
>
> You seem to be doing a lot of base64 decodability checking. Perhaps you should
> move such a thing somewhere common so you're not repeating yourself? Also,
> could you rename the method to be more explicit?
> check_injected_file_base64_encoded? Or if you move the method,
> "is_base64_encoded"
>

IMO you don't have to even do the check; just go ahead and decode from base64 in the openstack api, (catch any errors,) and pass the info as a plain python string to the compute layer. There is some prior art around this in the various create methods if I am not mistaken. Take a look at _get_injected_files in api.openstack.servers.

Cheers.

Revision history for this message
Brian Lamar (blamar) wrote :
Download full text (5.7 KiB)

Thanks for the through review Matt!

> 32 + msg = _("Instance %d is currently being rebuilt.") %
> instance_id
>
> This message might be misleading, since the instance could also be building
> for the first time. Also, since you're already stating that in the compute
> API, why not catch and reraise, maintaining the original message?
>

I see the need that the real API (read: User-facing) will need a more specific and user-friendly error message than that of the Compute API. Reusing the exception.BuildInProgress exception with different messages serves this purpose. The exception message is for programmers and the message generated at the OpenStack API layer is for users. Maybe removing the message from the exception in the compute API would be a good compromise?

> 52 + def _check_metadata(self, metadata):
> 53 + """Ensure that the metadata given is of the correct type."""
> 54 + try:
> 55 + metadata.iteritems()
>
> This is confusing. Better written as "if isinstance(metadata, dict):" and
> remove the check_metadata method altogether

I tend to shy away from isinstance because really all I want out of that check is to know that the next section of code will run when I call `iteritems` on the object, if that makes sense. While I wouldn't call `isinstance` harmful, I do agree a bit with this: http://www.canonical.org/~kragen/isinstance/

>
> 61 + def _check_personalities(self, personalities):
>
> A more suitable method name might be in order.
> are_personalities_base64_encoded perhaps?
>
> 65 + path = personality["path"]
> 66 + contents = personality["contents"]
>
> You can check for keys with the "in" keyword: 'path' in personality

Understood, however checking with "in" could result in either a True/False response or a TypeError exception (if the object isn't iterable) in which I'll need a try/except anyway. I attempted to cut to the chase here and just check for Key/Type errors in one go. Maybe I'm just thinking about it too much though, which happens.

>
> 03 + except exception.Error as ex:
> 104 + msg = _("Error encountered attempting to rebuild instance "
> 105 + "%(instance_id): %(ex)") % locals()
>
> The inferred strings are lacking format parameters. I suspect you haven't had
> this exception raised in testing? You should still use the normal string
> formatting parameters you would in any other string inference. "%d(some_var)
> -> %s(the_other_one)" % locals(). Also, perhaps some unit tests to trigger
> these exception
> s are raised?

Great catch. I really don't think those exception handling blocks are useful at all and as such I've removed them. There was an identical one in the V11 controller.

>
> 155 + def _check_injected_file_format(self, injected_files):
>
> You seem to be doing a lot of base64 decodability checking. Perhaps you should
> move such a thing somewhere common so you're not repeating yourself? Also,
> could you rename the method to be more explicit?
> check_injected_file_base64_encoded? Or if you move the method,
> "is_base64_encoded"
>

I've removed all base64 references in the co...

Read more...

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

Great feedback Brian. Sorry for the delay in getting back to you. I think on several of my points I must have been doing something else, because I go back now and don't really see the issue I was taking with them then. Good stuff, overall

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

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

Resolved remaining issues. I ended up resolving an issue with nova.exception.ApiError that caused nova.tests.test_exception.ApiErrorTestCase to fail for me.

Revision history for this message
Jay Pipes (jaypipes) wrote :

Hey guys,

Very nice work on this patch, and excellent review back and forth.

One thing I noticed:

82 + msg = _("Could not parse imageRef from request.")
83 + return faults.Fault(exc.HTTPBadRequest(explanation=msg))

Please put a LOG.debug(msg) in between those lines, please, to make the v10 and v11 _rebuild_server() methods match.

Excellent work on the test cases and removing the db calls from the driver layer.

-jay

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

Thanks, Jay! Most of this was Lamar, but I'll take the credit :) I added that debug line.

Revision history for this message
Jay Pipes (jaypipes) wrote :

coolio. lgtm.

review: Approve

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-05-02 07:55:54 +0000
3+++ nova/api/openstack/servers.py 2011-05-03 16:43:57 +0000
4@@ -317,10 +317,6 @@
5 return faults.Fault(exc.HTTPBadRequest())
6 return exc.HTTPAccepted()
7
8- def _action_rebuild(self, input_dict, req, id):
9- LOG.debug(_("Rebuild server action is not implemented"))
10- return faults.Fault(exc.HTTPNotImplemented())
11-
12 def _action_resize(self, input_dict, req, id):
13 """ Resizes a given instance to the flavor size requested """
14 try:
15@@ -603,6 +599,28 @@
16 except exception.TimeoutException:
17 return exc.HTTPRequestTimeout()
18
19+ def _action_rebuild(self, info, request, instance_id):
20+ context = request.environ['nova.context']
21+ instance_id = int(instance_id)
22+
23+ try:
24+ image_id = info["rebuild"]["imageId"]
25+ except (KeyError, TypeError):
26+ msg = _("Could not parse imageId from request.")
27+ LOG.debug(msg)
28+ return faults.Fault(exc.HTTPBadRequest(explanation=msg))
29+
30+ try:
31+ self.compute_api.rebuild(context, instance_id, image_id)
32+ except exception.BuildInProgress:
33+ msg = _("Instance %d is currently being rebuilt.") % instance_id
34+ LOG.debug(msg)
35+ return faults.Fault(exc.HTTPConflict(explanation=msg))
36+
37+ response = exc.HTTPAccepted()
38+ response.empty_body = True
39+ return response
40+
41
42 class ControllerV11(Controller):
43 def _image_id_from_req_data(self, data):
44@@ -639,6 +657,63 @@
45 def _limit_items(self, items, req):
46 return common.limited_by_marker(items, req)
47
48+ def _validate_metadata(self, metadata):
49+ """Ensure that we can work with the metadata given."""
50+ try:
51+ metadata.iteritems()
52+ except AttributeError as ex:
53+ msg = _("Unable to parse metadata key/value pairs.")
54+ LOG.debug(msg)
55+ raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
56+
57+ def _decode_personalities(self, personalities):
58+ """Decode the Base64-encoded personalities."""
59+ for personality in personalities:
60+ try:
61+ path = personality["path"]
62+ contents = personality["contents"]
63+ except (KeyError, TypeError):
64+ msg = _("Unable to parse personality path/contents.")
65+ LOG.info(msg)
66+ raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
67+
68+ try:
69+ personality["contents"] = base64.b64decode(contents)
70+ except TypeError:
71+ msg = _("Personality content could not be Base64 decoded.")
72+ LOG.info(msg)
73+ raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
74+
75+ def _action_rebuild(self, info, request, instance_id):
76+ context = request.environ['nova.context']
77+ instance_id = int(instance_id)
78+
79+ try:
80+ image_ref = info["rebuild"]["imageRef"]
81+ except (KeyError, TypeError):
82+ msg = _("Could not parse imageRef from request.")
83+ LOG.debug(msg)
84+ return faults.Fault(exc.HTTPBadRequest(explanation=msg))
85+
86+ image_id = common.get_id_from_href(image_ref)
87+ personalities = info["rebuild"].get("personality", [])
88+ metadata = info["rebuild"].get("metadata", {})
89+
90+ self._validate_metadata(metadata)
91+ self._decode_personalities(personalities)
92+
93+ try:
94+ self.compute_api.rebuild(context, instance_id, image_id, metadata,
95+ personalities)
96+ except exception.BuildInProgress:
97+ msg = _("Instance %d is currently being rebuilt.") % instance_id
98+ LOG.debug(msg)
99+ return faults.Fault(exc.HTTPConflict(explanation=msg))
100+
101+ response = exc.HTTPAccepted()
102+ response.empty_body = True
103+ return response
104+
105 def _get_server_admin_password(self, server):
106 """ Determine the admin password for a server on creation """
107 password = server.get('adminPass')
108
109=== modified file 'nova/api/openstack/views/servers.py'
110--- nova/api/openstack/views/servers.py 2011-04-26 13:45:53 +0000
111+++ nova/api/openstack/views/servers.py 2011-05-03 16:43:57 +0000
112@@ -66,7 +66,9 @@
113 power_state.SHUTDOWN: 'SHUTDOWN',
114 power_state.SHUTOFF: 'SHUTOFF',
115 power_state.CRASHED: 'ERROR',
116- power_state.FAILED: 'ERROR'}
117+ power_state.FAILED: 'ERROR',
118+ power_state.BUILDING: 'BUILD',
119+ }
120
121 inst_dict = {
122 'id': int(inst['id']),
123
124=== modified file 'nova/compute/api.py'
125--- nova/compute/api.py 2011-04-22 19:44:24 +0000
126+++ nova/compute/api.py 2011-05-03 16:43:57 +0000
127@@ -32,6 +32,7 @@
128 from nova import utils
129 from nova import volume
130 from nova.compute import instance_types
131+from nova.compute import power_state
132 from nova.scheduler import api as scheduler_api
133 from nova.db import base
134
135@@ -501,6 +502,33 @@
136 """Reboot the given instance."""
137 self._cast_compute_message('reboot_instance', context, instance_id)
138
139+ def rebuild(self, context, instance_id, image_id, metadata=None,
140+ files_to_inject=None):
141+ """Rebuild the given instance with the provided metadata."""
142+ instance = db.api.instance_get(context, instance_id)
143+
144+ if instance["state"] == power_state.BUILDING:
145+ msg = _("Instance already building")
146+ raise exception.BuildInProgress(msg)
147+
148+ metadata = metadata or {}
149+ self._check_metadata_properties_quota(context, metadata)
150+
151+ files_to_inject = files_to_inject or []
152+ self._check_injected_file_quota(context, files_to_inject)
153+
154+ self.db.instance_update(context, instance_id, {"metadata": metadata})
155+
156+ rebuild_params = {
157+ "image_id": image_id,
158+ "injected_files": files_to_inject,
159+ }
160+
161+ self._cast_compute_message('rebuild_instance',
162+ context,
163+ instance_id,
164+ params=rebuild_params)
165+
166 def revert_resize(self, context, instance_id):
167 """Reverts a resize, deleting the 'new' instance in the process."""
168 context = context.elevated()
169
170=== modified file 'nova/compute/manager.py'
171--- nova/compute/manager.py 2011-04-22 19:44:24 +0000
172+++ nova/compute/manager.py 2011-05-03 16:43:57 +0000
173@@ -137,17 +137,33 @@
174 """Initialization for a standalone compute service."""
175 self.driver.init_host(host=self.host)
176
177- def _update_state(self, context, instance_id):
178+ def _update_state(self, context, instance_id, state=None):
179 """Update the state of an instance from the driver info."""
180- # FIXME(ja): include other fields from state?
181 instance_ref = self.db.instance_get(context, instance_id)
182- try:
183- info = self.driver.get_info(instance_ref['name'])
184- state = info['state']
185- except exception.NotFound:
186- state = power_state.FAILED
187+
188+ if state is None:
189+ try:
190+ info = self.driver.get_info(instance_ref['name'])
191+ except exception.NotFound:
192+ info = None
193+
194+ if info is not None:
195+ state = info['state']
196+ else:
197+ state = power_state.FAILED
198+
199 self.db.instance_set_state(context, instance_id, state)
200
201+ def _update_launched_at(self, context, instance_id, launched_at=None):
202+ """Update the launched_at parameter of the given instance."""
203+ data = {'launched_at': launched_at or datetime.datetime.utcnow()}
204+ self.db.instance_update(context, instance_id, data)
205+
206+ def _update_image_id(self, context, instance_id, image_id):
207+ """Update the image_id for the given instance."""
208+ data = {'image_id': image_id}
209+ self.db.instance_update(context, instance_id, data)
210+
211 def get_console_topic(self, context, **kwargs):
212 """Retrieves the console host for a project on this host.
213
214@@ -231,24 +247,15 @@
215 instance_id)
216
217 # TODO(vish) check to make sure the availability zone matches
218- self.db.instance_set_state(context,
219- instance_id,
220- power_state.NOSTATE,
221- 'spawning')
222+ self._update_state(context, instance_id, power_state.BUILDING)
223
224 try:
225 self.driver.spawn(instance_ref)
226- now = datetime.datetime.utcnow()
227- self.db.instance_update(context,
228- instance_id,
229- {'launched_at': now})
230- except Exception: # pylint: disable=W0702
231- LOG.exception(_("Instance '%s' failed to spawn. Is virtualization"
232- " enabled in the BIOS?"), instance_id,
233- context=context)
234- self.db.instance_set_state(context,
235- instance_id,
236- power_state.SHUTDOWN)
237+ except Exception as ex: # pylint: disable=W0702
238+ msg = _("Instance '%(instance_id)s' failed to spawn. Is "
239+ "virtualization enabled in the BIOS? Details: "
240+ "%(ex)s") % locals()
241+ LOG.exception(msg)
242
243 if not FLAGS.stub_network and FLAGS.auto_assign_floating_ip:
244 public_ip = self.network_api.allocate_floating_ip(context)
245@@ -262,6 +269,8 @@
246 floating_ip,
247 fixed_ip,
248 affect_auto_assigned=True)
249+
250+ self._update_launched_at(context, instance_id)
251 self._update_state(context, instance_id)
252
253 @exception.wrap_exception
254@@ -318,6 +327,33 @@
255
256 @exception.wrap_exception
257 @checks_instance_lock
258+ def rebuild_instance(self, context, instance_id, image_id):
259+ """Destroy and re-make this instance.
260+
261+ A 'rebuild' effectively purges all existing data from the system and
262+ remakes the VM with given 'metadata' and 'personalities'.
263+
264+ :param context: `nova.RequestContext` object
265+ :param instance_id: Instance identifier (integer)
266+ :param image_id: Image identifier (integer)
267+ """
268+ context = context.elevated()
269+
270+ instance_ref = self.db.instance_get(context, instance_id)
271+ LOG.audit(_("Rebuilding instance %s"), instance_id, context=context)
272+
273+ self._update_state(context, instance_id, power_state.BUILDING)
274+
275+ self.driver.destroy(instance_ref)
276+ instance_ref.image_id = image_id
277+ self.driver.spawn(instance_ref)
278+
279+ self._update_image_id(context, instance_id, image_id)
280+ self._update_launched_at(context, instance_id)
281+ self._update_state(context, instance_id)
282+
283+ @exception.wrap_exception
284+ @checks_instance_lock
285 def reboot_instance(self, context, instance_id):
286 """Reboot an instance on this host."""
287 context = context.elevated()
288@@ -1072,8 +1108,7 @@
289 if vm_instance is None:
290 # NOTE(justinsb): We have to be very careful here, because a
291 # concurrent operation could be in progress (e.g. a spawn)
292- if db_state == power_state.NOSTATE:
293- # Assume that NOSTATE => spawning
294+ if db_state == power_state.BUILDING:
295 # TODO(justinsb): This does mean that if we crash during a
296 # spawn, the machine will never leave the spawning state,
297 # but this is just the way nova is; this function isn't
298@@ -1104,9 +1139,7 @@
299 if vm_state != db_state:
300 LOG.info(_("DB/VM state mismatch. Changing state from "
301 "'%(db_state)s' to '%(vm_state)s'") % locals())
302- self.db.instance_set_state(context,
303- db_instance['id'],
304- vm_state)
305+ self._update_state(context, db_instance['id'], vm_state)
306
307 # NOTE(justinsb): We no longer auto-remove SHUTOFF instances
308 # It's quite hard to get them back when we do.
309
310=== modified file 'nova/compute/power_state.py'
311--- nova/compute/power_state.py 2011-03-24 01:50:30 +0000
312+++ nova/compute/power_state.py 2011-05-03 16:43:57 +0000
313@@ -30,20 +30,23 @@
314 CRASHED = 0x06
315 SUSPENDED = 0x07
316 FAILED = 0x08
317+BUILDING = 0x09
318
319 # TODO(justinsb): Power state really needs to be a proper class,
320 # so that we're not locked into the libvirt status codes and can put mapping
321 # logic here rather than spread throughout the code
322 _STATE_MAP = {
323- NOSTATE: 'pending',
324- RUNNING: 'running',
325- BLOCKED: 'blocked',
326- PAUSED: 'paused',
327- SHUTDOWN: 'shutdown',
328- SHUTOFF: 'shutdown',
329- CRASHED: 'crashed',
330- SUSPENDED: 'suspended',
331- FAILED: 'failed to spawn'}
332+ NOSTATE: 'pending',
333+ RUNNING: 'running',
334+ BLOCKED: 'blocked',
335+ PAUSED: 'paused',
336+ SHUTDOWN: 'shutdown',
337+ SHUTOFF: 'shutdown',
338+ CRASHED: 'crashed',
339+ SUSPENDED: 'suspended',
340+ FAILED: 'failed to spawn',
341+ BUILDING: 'building',
342+}
343
344
345 def name(code):
346
347=== modified file 'nova/exception.py'
348--- nova/exception.py 2011-05-02 18:04:29 +0000
349+++ nova/exception.py 2011-05-03 16:43:57 +0000
350@@ -50,7 +50,7 @@
351
352 class ApiError(Error):
353 def __init__(self, message='Unknown', code=None):
354- self.message = message
355+ self.msg = message
356 self.code = code
357 if code:
358 outstr = '%s: %s' % (code, message)
359@@ -59,6 +59,10 @@
360 super(ApiError, self).__init__(outstr)
361
362
363+class BuildInProgress(Error):
364+ pass
365+
366+
367 class DBError(Error):
368 """Wraps an implementation specific exception."""
369 def __init__(self, inner_exception):
370
371=== modified file 'nova/tests/api/openstack/test_servers.py'
372--- nova/tests/api/openstack/test_servers.py 2011-05-02 21:37:59 +0000
373+++ nova/tests/api/openstack/test_servers.py 2011-05-03 16:43:57 +0000
374@@ -1058,15 +1058,175 @@
375 req.body = json.dumps(body)
376 res = req.get_response(fakes.wsgi_app())
377
378- def test_server_rebuild(self):
379- body = dict(server=dict(
380- name='server_test', imageId=2, flavorId=2, metadata={},
381- personality={}))
382- req = webob.Request.blank('/v1.0/servers/1/action')
383- req.method = 'POST'
384- req.content_type = 'application/json'
385- req.body = json.dumps(body)
386- res = req.get_response(fakes.wsgi_app())
387+ def test_server_rebuild_accepted(self):
388+ body = {
389+ "rebuild": {
390+ "imageId": 2,
391+ },
392+ }
393+
394+ req = webob.Request.blank('/v1.0/servers/1/action')
395+ req.method = 'POST'
396+ req.content_type = 'application/json'
397+ req.body = json.dumps(body)
398+
399+ res = req.get_response(fakes.wsgi_app())
400+ self.assertEqual(res.status_int, 202)
401+ self.assertEqual(res.body, "")
402+
403+ def test_server_rebuild_rejected_when_building(self):
404+ body = {
405+ "rebuild": {
406+ "imageId": 2,
407+ },
408+ }
409+
410+ state = power_state.BUILDING
411+ new_return_server = return_server_with_power_state(state)
412+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
413+
414+ req = webob.Request.blank('/v1.0/servers/1/action')
415+ req.method = 'POST'
416+ req.content_type = 'application/json'
417+ req.body = json.dumps(body)
418+
419+ res = req.get_response(fakes.wsgi_app())
420+ self.assertEqual(res.status_int, 409)
421+
422+ def test_server_rebuild_bad_entity(self):
423+ body = {
424+ "rebuild": {
425+ },
426+ }
427+
428+ req = webob.Request.blank('/v1.0/servers/1/action')
429+ req.method = 'POST'
430+ req.content_type = 'application/json'
431+ req.body = json.dumps(body)
432+
433+ res = req.get_response(fakes.wsgi_app())
434+ self.assertEqual(res.status_int, 400)
435+
436+ def test_server_rebuild_accepted_minimum_v11(self):
437+ body = {
438+ "rebuild": {
439+ "imageRef": "http://localhost/images/2",
440+ },
441+ }
442+
443+ req = webob.Request.blank('/v1.1/servers/1/action')
444+ req.method = 'POST'
445+ req.content_type = 'application/json'
446+ req.body = json.dumps(body)
447+
448+ res = req.get_response(fakes.wsgi_app())
449+ self.assertEqual(res.status_int, 202)
450+
451+ def test_server_rebuild_rejected_when_building_v11(self):
452+ body = {
453+ "rebuild": {
454+ "imageRef": "http://localhost/images/2",
455+ },
456+ }
457+
458+ state = power_state.BUILDING
459+ new_return_server = return_server_with_power_state(state)
460+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
461+
462+ req = webob.Request.blank('/v1.1/servers/1/action')
463+ req.method = 'POST'
464+ req.content_type = 'application/json'
465+ req.body = json.dumps(body)
466+
467+ res = req.get_response(fakes.wsgi_app())
468+ self.assertEqual(res.status_int, 409)
469+
470+ def test_server_rebuild_accepted_with_metadata_v11(self):
471+ body = {
472+ "rebuild": {
473+ "imageRef": "http://localhost/images/2",
474+ "metadata": {
475+ "new": "metadata",
476+ },
477+ },
478+ }
479+
480+ req = webob.Request.blank('/v1.1/servers/1/action')
481+ req.method = 'POST'
482+ req.content_type = 'application/json'
483+ req.body = json.dumps(body)
484+
485+ res = req.get_response(fakes.wsgi_app())
486+ self.assertEqual(res.status_int, 202)
487+
488+ def test_server_rebuild_accepted_with_bad_metadata_v11(self):
489+ body = {
490+ "rebuild": {
491+ "imageRef": "http://localhost/images/2",
492+ "metadata": "stack",
493+ },
494+ }
495+
496+ req = webob.Request.blank('/v1.1/servers/1/action')
497+ req.method = 'POST'
498+ req.content_type = 'application/json'
499+ req.body = json.dumps(body)
500+
501+ res = req.get_response(fakes.wsgi_app())
502+ self.assertEqual(res.status_int, 400)
503+
504+ def test_server_rebuild_bad_entity_v11(self):
505+ body = {
506+ "rebuild": {
507+ "imageId": 2,
508+ },
509+ }
510+
511+ req = webob.Request.blank('/v1.1/servers/1/action')
512+ req.method = 'POST'
513+ req.content_type = 'application/json'
514+ req.body = json.dumps(body)
515+
516+ res = req.get_response(fakes.wsgi_app())
517+ self.assertEqual(res.status_int, 400)
518+
519+ def test_server_rebuild_bad_personality_v11(self):
520+ body = {
521+ "rebuild": {
522+ "imageRef": "http://localhost/images/2",
523+ "personality": [{
524+ "path": "/path/to/file",
525+ "contents": "INVALID b64",
526+ }]
527+ },
528+ }
529+
530+ req = webob.Request.blank('/v1.1/servers/1/action')
531+ req.method = 'POST'
532+ req.content_type = 'application/json'
533+ req.body = json.dumps(body)
534+
535+ res = req.get_response(fakes.wsgi_app())
536+ self.assertEqual(res.status_int, 400)
537+
538+ def test_server_rebuild_personality_v11(self):
539+ body = {
540+ "rebuild": {
541+ "imageRef": "http://localhost/images/2",
542+ "personality": [{
543+ "path": "/path/to/file",
544+ "contents": base64.b64encode("Test String"),
545+ }]
546+ },
547+ }
548+
549+ req = webob.Request.blank('/v1.1/servers/1/action')
550+ req.method = 'POST'
551+ req.content_type = 'application/json'
552+ req.body = json.dumps(body)
553+
554+ res = req.get_response(fakes.wsgi_app())
555+ self.assertEqual(res.status_int, 202)
556
557 def test_delete_server_instance(self):
558 req = webob.Request.blank('/v1.0/servers/1')
559
560=== modified file 'nova/tests/test_exception.py'
561--- nova/tests/test_exception.py 2011-04-25 16:55:59 +0000
562+++ nova/tests/test_exception.py 2011-05-03 16:43:57 +0000
563@@ -26,9 +26,9 @@
564 err = exception.ApiError('fake error')
565 self.assertEqual(err.__str__(), 'fake error')
566 self.assertEqual(err.code, None)
567- self.assertEqual(err.message, 'fake error')
568+ self.assertEqual(err.msg, 'fake error')
569 # with 'code' arg
570 err = exception.ApiError('fake error', 'blah code')
571 self.assertEqual(err.__str__(), 'blah code: fake error')
572 self.assertEqual(err.code, 'blah code')
573- self.assertEqual(err.message, 'fake error')
574+ self.assertEqual(err.msg, 'fake error')
575
576=== modified file 'nova/virt/xenapi/vmops.py'
577--- nova/virt/xenapi/vmops.py 2011-04-26 22:48:28 +0000
578+++ nova/virt/xenapi/vmops.py 2011-05-03 16:43:57 +0000
579@@ -210,8 +210,6 @@
580 def _wait_for_boot():
581 try:
582 state = self.get_info(instance_name)['state']
583- db.instance_set_state(context.get_admin_context(),
584- instance['id'], state)
585 if state == power_state.RUNNING:
586 LOG.debug(_('Instance %s: booted'), instance_name)
587 timer.stop()
588@@ -219,11 +217,7 @@
589 return True
590 except Exception, exc:
591 LOG.warn(exc)
592- LOG.exception(_('instance %s: failed to boot'),
593- instance_name)
594- db.instance_set_state(context.get_admin_context(),
595- instance['id'],
596- power_state.SHUTDOWN)
597+ LOG.exception(_('Instance %s: failed to boot'), instance_name)
598 timer.stop()
599 return False
600