Merge lp:~rackspace-titan/nova/rebuild-server-lp730233 into lp:~hudson-openstack/nova/trunk
- rebuild-server-lp730233
- Merge into trunk
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 | ||||||||
Related bugs: |
|
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 |
Commit message
Description of the change
Adding support for server rebuild to v1.0 and v1.1 of the Openstack API
Dan Prince (dan-prince) wrote : | # |
Cory Wright (corywright) wrote : | # |
FYI, I'm getting a conflict when I merge the latest trunk with this branch. May want to update that.
Brian Lamar (blamar) wrote : | # |
Should be good now. Thanks for the heads up
Cory Wright (corywright) wrote : | # |
Nice work. lgtm.
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.
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_
53 + """Ensure that the metadata given is of the correct type."""
54 + try:
55 + metadata.
This is confusing. Better written as "if isinstance(
61 + def _check_
A more suitable method name might be in order. are_personaliti
65 + path = personality["path"]
66 + contents = personality[
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_
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_
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_
622 - instance['id'],
623 - power_state.
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_
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_
Mark Washenberger (markwash) wrote : | # |
> 155 + def _check_
>
> 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_
> "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.
Cheers.
Brian Lamar (blamar) wrote : | # |
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.
> 52 + def _check_
> 53 + """Ensure that the metadata given is of the correct type."""
> 54 + try:
> 55 + metadata.
>
> This is confusing. Better written as "if isinstance(
> 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://
>
> 61 + def _check_
>
> A more suitable method name might be in order.
> are_personaliti
>
> 65 + path = personality["path"]
> 66 + contents = personality[
>
> 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_
>
> 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_
> "is_base64_encoded"
>
I've removed all base64 references in the co...
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
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.
Brian Waldon (bcwaldon) wrote : | # |
Resolved remaining issues. I ended up resolving an issue with nova.exception.
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.
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
Brian Waldon (bcwaldon) wrote : | # |
Thanks, Jay! Most of this was Lamar, but I'll take the credit :) I added that debug line.
Preview Diff
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 |
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.