Merge lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot into lp:~hudson-openstack/nova/trunk

Proposed by Rick Harris
Status: Merged
Approved by: Jay Pipes
Approved revision: 532
Merged at revision: 570
Proposed branch: lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 684 lines (+148/-213)
11 files modified
nova/api/openstack/images.py (+18/-5)
nova/compute/api.py (+36/-8)
nova/compute/manager.py (+2/-2)
nova/image/glance.py (+13/-144)
nova/tests/api/openstack/fakes.py (+30/-32)
nova/utils.py (+19/-2)
nova/virt/libvirt_conn.py (+1/-1)
nova/virt/xenapi/vm_utils.py (+15/-8)
nova/virt/xenapi/vmops.py (+3/-3)
nova/virt/xenapi_conn.py (+6/-2)
plugins/xenserver/xenapi/etc/xapi.d/plugins/glance (+5/-6)
To merge this branch: bzr merge lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot
Reviewer Review Type Date Requested Status
Jay Pipes (community) Approve
Thierry Carrez (community) ffe Approve
Soren Hansen (community) Approve
Eric Day (community) Approve
Review via email: mp+45494@code.launchpad.net

Description of the change

The Openstack API requires image metadata to be returned immediately after an image-create call.

This is accomplished by having the ImageService create a 'queued' image in Glance.

When the image is subsequently uploaded, the image will go from 'queued' -> 'saving' -> 'queued'.

Related Future Work:

The ImageService needs to be cleaned up so that there is a canonical set of attributes (id, status, etc), and a canonical set of values ('queued', 'saving', etc). Right now, EC2 is fairly coupled to LocalImageService and S3ImageService while OpenStackAPI is coupled to GlanceImageService; ideally, we should be able mix-and-match from any of these.

To post a comment you must log in.
Revision history for this message
Eric Day (eday) wrote :

26: you want compute.API() here, compute_api.ComputeAPI() was the old name

42: Not needed, use self.image_service that was made in constructor

review: Needs Fixing
Revision history for this message
Soren Hansen (soren) wrote :

[2011-01-12 10:22:41] soren@lenny:~/src/openstack/nova/nova$ bzr merge lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot
 M nova/api/openstack/images.py
 M nova/compute/api.py
 M nova/compute/manager.py
 M nova/image/glance.py
 M nova/utils.py
 M nova/virt/libvirt_conn.py
 M nova/virt/xenapi/vm_utils.py
 M nova/virt/xenapi/vmops.py
 M nova/virt/xenapi_conn.py
 M plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
Text conflict in nova/image/glance.py
Text conflict in nova/virt/xenapi/vm_utils.py
2 conflicts encountered.

review: Needs Fixing
Revision history for this message
Soren Hansen (soren) wrote :

compute.api.snapshot sets is_public to True by default. This seems like a poor default to me. I'd rather they were private by default and then you can go and make them public after the fact... Unless of course I'm misunderstanding what is_public actually denotes?

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

> compute.api.snapshot sets is_public to True by default. This seems like a poor default to me.

Agreed. I'm doing it this way right now because Glance (as of yet) doesn't really support images-tied-to-servers. So, in order for an image to be visible, it has to be public.

Adding the image-instance relationships should definitely be part of Cactus (I don't think we should cram it into Bexar since there still needs to be some discussion on how best to do it).

For now, I've added a TODO so we don't forget. Soren, does that work for you for now?

Revision history for this message
Soren Hansen (soren) wrote :

I'm not sure I understand. _is_public means everyone can see it, right? So if I have sensitive data on my instance, everyone will be able to see them by looking at these snapshots?

Revision history for this message
Eric Day (eday) wrote :

26: Still broken, s/ComputeAPI/API/

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

Eday: Oops, fixed now, thanks.

Soren: Right, is_public does mean everyone can see it, for now, until we build the ownership/sharing modeling into Glance (in Cactus). The idea with is_public=True was something like:

Nobody should be using this right now since we haven't addressed security (at all), let's just make it a little easier on the devs since the newly created snapshot will appear in the image-listing as they'd expect.

For now, I've changed it to is_public=False, so there shouldn't be anymore confusion.

Revision history for this message
Eric Day (eday) wrote :

lgtm

review: Approve
Revision history for this message
Soren Hansen (soren) :
review: Approve
Revision history for this message
Soren Hansen (soren) wrote :

Rocking

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

The attempt to merge lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot into lp:nova failed. Below is the output from the failed tests.

nova/image/glance.py:95:13: E261 at least two spaces before inline comment
        pass #raise NotImplementedError
            ^
    Separate inline comments by at least two spaces.

    An inline comment is a comment on the same line as a statement. Inline
    comments should be separated by at least two spaces from the statement.
    They should start with a # and a single space.

    Okay: x = x + 1 # Increment x
    Okay: x = x + 1 # Increment x
    E261: x = x + 1 # Increment x
    E262: x = x + 1 #Increment x
    E262: x = x + 1 # Increment x

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

Oops, I should have caught that. Pep-8 issue has been fixed. Full Pep-8 suite passes.

Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (21.7 KiB)

The attempt to merge lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot into lp:nova failed. Below is the output from the failed tests.

TrialTestCase
    runTest ok
Failure
    runTest ERROR
APITest
    test_exceptions_are_converted_to_faults ok
Failure
    runTest ERROR
TestFaults
    test_fault_parts ok
    test_raise ok
    test_retry_header ok
Failure
    runTest ERROR
    runTest ERROR
LimiterTest
    test_minute ok
    test_one_per_period ok
    test_second ok
    test_users_get_separate_buckets ok
    test_we_can_go_indefinitely_if_we_spread_out_requests ok
WSGIAppProxyTest
    test_200 ok
    test_403 ok
    test_failure ok
WSGIAppTest
    test_escaping ok
    test_good_urls ok
    test_invalid_methods ok
    test_invalid_urls ok
    test_response_to_delays ok
Failure
    runTest ERROR
SharedIpGroupsTest
    test_create_shared_ip_group ok
    test_delete_shared_ip_group ok
    test_get_shared_ip_groups ok
Test
    test_ec2 ok
    test_ec2_root ok
    test_metadata ok
    test_not_found ok
    test_openstack ok
    test_query_api_versions ok
SerializerTest
    test_basic ok
    test_defaults_to_json ok
    test_deserialize ok
    test_deserialize_empty_xml ok
    test_suffix_takes_precedence_over_accept_header ok
Test
    test_controller ok
    test_debug **************************************** REQUEST ENVIRON
wsgi.multithread = False
SCRIPT_NAME =
webob.adhoc_attrs = {'response': <Response at 0x49343d0 200 OK>}
wsgi.input = <cStringIO.StringI object at 0x4920e40>
REQUEST_METHOD = GET
HTTP_HOST = localhost:80
PATH_INFO = /
S...

Revision history for this message
Thierry Carrez (ttx) wrote :

This needs some work to fix the test failures, but is in good shape. Let's see if we can get it in before Monday.

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

It's really just waiting on packaging for glance..

Revision history for this message
Ewan Mellor (ewanmellor) wrote :

> It's really just waiting on packaging for glance..

Glance is on PyPI now, so I think you just need to add glance to pip-requires, and this branch will merge.

Revision history for this message
Soren Hansen (soren) wrote :

Packaged glance (based on work from Monty). Installed it on Hudson. Retrying.

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

Attempt to merge into lp:nova failed due to conflicts:

text conflict in nova/compute/api.py

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

Alrighty, let's try this again :)

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

The attempt to merge lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot into lp:nova failed. Below is the output from the failed tests.

nova/compute/api.py:378:1: W293 blank line contains whitespace

^
    JCR: Trailing whitespace is superfluous.
    FBM: Except when it occurs as part of a blank line (i.e. the line is
         nothing but whitespace). According to Python docs[1] a line with only
         whitespace is considered a blank line, and is to be ignored. However,
         matching a blank line to its indentation level avoids mistakenly
         terminating a multi-line statement (e.g. class declaration) when
         pasting code into the standard Python interpreter.

         [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines

    The warning returned varies on whether the line itself is blank, for easier
    filtering for those who want to indent their blank lines.

    Okay: spam(1)
    W291: spam(1)\s
    W293: class Foo(object):\n \n bang = 12
nova/compute/api.py:379:50: W291 trailing whitespace
        :retval: A dict containing image metadata
                                                 ^
    JCR: Trailing whitespace is superfluous.
    FBM: Except when it occurs as part of a blank line (i.e. the line is
         nothing but whitespace). According to Python docs[1] a line with only
         whitespace is considered a blank line, and is to be ignored. However,
         matching a blank line to its indentation level avoids mistakenly
         terminating a multi-line statement (e.g. class declaration) when
         pasting code into the standard Python interpreter.

         [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines

    The warning returned varies on whether the line itself is blank, for easier
    filtering for those who want to indent their blank lines.

    Okay: spam(1)
    W291: spam(1)\s
    W293: class Foo(object):\n \n bang = 12

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

pep8 fixes approved.

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
Jay Pipes (jaypipes) :
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/images.py'
2--- nova/api/openstack/images.py 2011-01-06 02:23:23 +0000
3+++ nova/api/openstack/images.py 2011-01-17 17:34:42 +0000
4@@ -78,7 +78,14 @@
5 'decrypting': 'preparing',
6 'untarring': 'saving',
7 'available': 'active'}
8- item['status'] = status_mapping[item['status']]
9+ try:
10+ item['status'] = status_mapping[item['status']]
11+ except KeyError:
12+ # TODO(sirp): Performing translation of status (if necessary) here for
13+ # now. Perhaps this should really be done in EC2 API and
14+ # S3ImageService
15+ pass
16+
17 return item
18
19
20@@ -92,9 +99,11 @@
21
22
23 def _convert_image_id_to_hash(image):
24- image_id = abs(hash(image['imageId']))
25- image['imageId'] = image_id
26- image['id'] = image_id
27+ if 'imageId' in image:
28+ # Convert EC2-style ID (i-blah) to Rackspace-style (int)
29+ image_id = abs(hash(image['imageId']))
30+ image['imageId'] = image_id
31+ image['id'] = image_id
32
33
34 class Controller(wsgi.Controller):
35@@ -147,7 +156,11 @@
36 env = self._deserialize(req.body, req)
37 instance_id = env["image"]["serverId"]
38 name = env["image"]["name"]
39- return compute.API().snapshot(context, instance_id, name)
40+
41+ image_meta = compute.API().snapshot(
42+ context, instance_id, name)
43+
44+ return dict(image=image_meta)
45
46 def update(self, req, id):
47 # Users may not modify public images, and that's all that
48
49=== modified file 'nova/compute/api.py'
50--- nova/compute/api.py 2011-01-15 01:48:48 +0000
51+++ nova/compute/api.py 2011-01-17 17:34:42 +0000
52@@ -335,27 +335,55 @@
53 project_id)
54 return self.db.instance_get_all(context)
55
56- def _cast_compute_message(self, method, context, instance_id, host=None):
57- """Generic handler for RPC casts to compute."""
58+ def _cast_compute_message(self, method, context, instance_id, host=None,
59+ params=None):
60+ """Generic handler for RPC casts to compute.
61+
62+ :param params: Optional dictionary of arguments to be passed to the
63+ compute worker
64+
65+ :retval None
66+ """
67+ if not params:
68+ params = {}
69 if not host:
70 instance = self.get(context, instance_id)
71 host = instance['host']
72 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
73- kwargs = {'method': method, 'args': {'instance_id': instance_id}}
74+ params['instance_id'] = instance_id
75+ kwargs = {'method': method, 'args': params}
76 rpc.cast(context, queue, kwargs)
77
78- def _call_compute_message(self, method, context, instance_id, host=None):
79- """Generic handler for RPC calls to compute."""
80+ def _call_compute_message(self, method, context, instance_id, host=None,
81+ params=None):
82+ """Generic handler for RPC calls to compute.
83+
84+ :param params: Optional dictionary of arguments to be passed to the
85+ compute worker
86+
87+ :retval: Result returned by compute worker
88+ """
89+ if not params:
90+ params = {}
91 if not host:
92 instance = self.get(context, instance_id)
93 host = instance["host"]
94 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
95- kwargs = {"method": method, "args": {"instance_id": instance_id}}
96+ params['instance_id'] = instance_id
97+ kwargs = {'method': method, 'args': params}
98 return rpc.call(context, queue, kwargs)
99
100 def snapshot(self, context, instance_id, name):
101- """Snapshot the given instance."""
102- self._cast_compute_message('snapshot_instance', context, instance_id)
103+ """Snapshot the given instance.
104+
105+ :retval: A dict containing image metadata
106+ """
107+ data = {'name': name, 'is_public': False}
108+ image_meta = self.image_service.create(context, data)
109+ params = {'image_id': image_meta['id']}
110+ self._cast_compute_message('snapshot_instance', context, instance_id,
111+ params=params)
112+ return image_meta
113
114 def reboot(self, context, instance_id):
115 """Reboot the given instance."""
116
117=== modified file 'nova/compute/manager.py'
118--- nova/compute/manager.py 2011-01-13 16:51:31 +0000
119+++ nova/compute/manager.py 2011-01-17 17:34:42 +0000
120@@ -294,7 +294,7 @@
121 self._update_state(context, instance_id)
122
123 @exception.wrap_exception
124- def snapshot_instance(self, context, instance_id, name):
125+ def snapshot_instance(self, context, instance_id, image_id):
126 """Snapshot an instance on this server."""
127 context = context.elevated()
128 instance_ref = self.db.instance_get(context, instance_id)
129@@ -311,7 +311,7 @@
130 'instance: %s (state: %s excepted: %s)'),
131 instance_id, instance_ref['state'], power_state.RUNNING)
132
133- self.driver.snapshot(instance_ref, name)
134+ self.driver.snapshot(instance_ref, image_id)
135
136 @exception.wrap_exception
137 @checks_instance_lock
138
139=== modified file 'nova/image/glance.py'
140--- nova/image/glance.py 2011-01-07 14:46:17 +0000
141+++ nova/image/glance.py 2011-01-17 17:34:42 +0000
142@@ -14,9 +14,9 @@
143 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
144 # License for the specific language governing permissions and limitations
145 # under the License.
146-
147 """Implementation of an image service that uses Glance as the backend"""
148
149+from __future__ import absolute_import
150 import httplib
151 import json
152 import urlparse
153@@ -24,171 +24,40 @@
154 from nova import exception
155 from nova import flags
156 from nova import log as logging
157+from nova import utils
158 from nova.image import service
159
160
161 LOG = logging.getLogger('nova.image.glance')
162
163 FLAGS = flags.FLAGS
164-flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1',
165- 'IP address or URL where Glance\'s Teller service resides')
166-flags.DEFINE_string('glance_teller_port', '9191',
167- 'Port for Glance\'s Teller service')
168-flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1',
169- 'IP address or URL where Glance\'s Parallax service '
170- 'resides')
171-flags.DEFINE_string('glance_parallax_port', '9292',
172- 'Port for Glance\'s Parallax service')
173-
174-
175-class TellerClient(object):
176-
177- def __init__(self):
178- self.address = FLAGS.glance_teller_address
179- self.port = FLAGS.glance_teller_port
180- url = urlparse.urlparse(self.address)
181- self.netloc = url.netloc
182- self.connection_type = {'http': httplib.HTTPConnection,
183- 'https': httplib.HTTPSConnection}[url.scheme]
184-
185-
186-class ParallaxClient(object):
187-
188- def __init__(self):
189- self.address = FLAGS.glance_parallax_address
190- self.port = FLAGS.glance_parallax_port
191- url = urlparse.urlparse(self.address)
192- self.netloc = url.netloc
193- self.connection_type = {'http': httplib.HTTPConnection,
194- 'https': httplib.HTTPSConnection}[url.scheme]
195-
196- def get_image_index(self):
197- """
198- Returns a list of image id/name mappings from Parallax
199- """
200- try:
201- c = self.connection_type(self.netloc, self.port)
202- c.request("GET", "images")
203- res = c.getresponse()
204- if res.status == 200:
205- # Parallax returns a JSONified dict(images=image_list)
206- data = json.loads(res.read())['images']
207- return data
208- else:
209- LOG.warn(_("Parallax returned HTTP error %d from "
210- "request for /images"), res.status_int)
211- return []
212- finally:
213- c.close()
214-
215- def get_image_details(self):
216- """
217- Returns a list of detailed image data mappings from Parallax
218- """
219- try:
220- c = self.connection_type(self.netloc, self.port)
221- c.request("GET", "images/detail")
222- res = c.getresponse()
223- if res.status == 200:
224- # Parallax returns a JSONified dict(images=image_list)
225- data = json.loads(res.read())['images']
226- return data
227- else:
228- LOG.warn(_("Parallax returned HTTP error %d from "
229- "request for /images/detail"), res.status_int)
230- return []
231- finally:
232- c.close()
233-
234- def get_image_metadata(self, image_id):
235- """
236- Returns a mapping of image metadata from Parallax
237- """
238- try:
239- c = self.connection_type(self.netloc, self.port)
240- c.request("GET", "images/%s" % image_id)
241- res = c.getresponse()
242- if res.status == 200:
243- # Parallax returns a JSONified dict(image=image_info)
244- data = json.loads(res.read())['image']
245- return data
246- else:
247- # TODO(jaypipes): log the error?
248- return None
249- finally:
250- c.close()
251-
252- def add_image_metadata(self, image_metadata):
253- """
254- Tells parallax about an image's metadata
255- """
256- try:
257- c = self.connection_type(self.netloc, self.port)
258- body = json.dumps(image_metadata)
259- c.request("POST", "images", body)
260- res = c.getresponse()
261- if res.status == 200:
262- # Parallax returns a JSONified dict(image=image_info)
263- data = json.loads(res.read())['image']
264- return data['id']
265- else:
266- # TODO(jaypipes): log the error?
267- return None
268- finally:
269- c.close()
270-
271- def update_image_metadata(self, image_id, image_metadata):
272- """
273- Updates Parallax's information about an image
274- """
275- try:
276- c = self.connection_type(self.netloc, self.port)
277- body = json.dumps(image_metadata)
278- c.request("PUT", "images/%s" % image_id, body)
279- res = c.getresponse()
280- return res.status == 200
281- finally:
282- c.close()
283-
284- def delete_image_metadata(self, image_id):
285- """
286- Deletes Parallax's information about an image
287- """
288- try:
289- c = self.connection_type(self.netloc, self.port)
290- c.request("DELETE", "images/%s" % image_id)
291- res = c.getresponse()
292- return res.status == 200
293- finally:
294- c.close()
295+
296+GlanceClient = utils.import_class('glance.client.Client')
297
298
299 class GlanceImageService(service.BaseImageService):
300 """Provides storage and retrieval of disk image objects within Glance."""
301
302 def __init__(self):
303- self.teller = TellerClient()
304- self.parallax = ParallaxClient()
305+ self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port)
306
307 def index(self, context):
308 """
309- Calls out to Parallax for a list of images available
310+ Calls out to Glance for a list of images available
311 """
312- images = self.parallax.get_image_index()
313- return images
314+ return self.client.get_images()
315
316 def detail(self, context):
317 """
318- Calls out to Parallax for a list of detailed image information
319+ Calls out to Glance for a list of detailed image information
320 """
321- images = self.parallax.get_image_details()
322- return images
323+ return self.client.get_images_detailed()
324
325 def show(self, context, id):
326 """
327 Returns a dict containing image data for the given opaque image id.
328 """
329- image = self.parallax.get_image_metadata(id)
330+ image = self.client.get_image_meta(id)
331 if image:
332 return image
333 raise exception.NotFound
334@@ -200,7 +69,7 @@
335 :raises AlreadyExists if the image already exist.
336
337 """
338- return self.parallax.add_image_metadata(data)
339+ return self.client.add_image(image_meta=data)
340
341 def update(self, context, image_id, data):
342 """Replace the contents of the given image with the new data.
343@@ -208,7 +77,7 @@
344 :raises NotFound if the image does not exist.
345
346 """
347- self.parallax.update_image_metadata(image_id, data)
348+ return self.client.update_image(image_id, data)
349
350 def delete(self, context, image_id):
351 """
352@@ -217,7 +86,7 @@
353 :raises NotFound if the image does not exist.
354
355 """
356- self.parallax.delete_image_metadata(image_id)
357+ return self.client.delete_image(image_id)
358
359 def delete_all(self):
360 """
361
362=== modified file 'nova/tests/api/openstack/fakes.py'
363--- nova/tests/api/openstack/fakes.py 2011-01-10 02:08:54 +0000
364+++ nova/tests/api/openstack/fakes.py 2011-01-17 17:34:42 +0000
365@@ -23,6 +23,8 @@
366 import webob
367 import webob.dec
368
369+from glance import client as glance_client
370+
371 from nova import auth
372 from nova import context
373 from nova import exception as exc
374@@ -116,64 +118,60 @@
375 stubs.Set(nova.compute.API, 'snapshot', snapshot)
376
377
378-def stub_out_glance(stubs, initial_fixtures=[]):
379+def stub_out_glance(stubs, initial_fixtures=None):
380
381- class FakeParallaxClient:
382+ class FakeGlanceClient:
383
384 def __init__(self, initial_fixtures):
385- self.fixtures = initial_fixtures
386+ self.fixtures = initial_fixtures or []
387
388- def fake_get_image_index(self):
389+ def fake_get_images(self):
390 return [dict(id=f['id'], name=f['name'])
391 for f in self.fixtures]
392
393- def fake_get_image_details(self):
394+ def fake_get_images_detailed(self):
395 return self.fixtures
396
397- def fake_get_image_metadata(self, image_id):
398+ def fake_get_image_meta(self, image_id):
399 for f in self.fixtures:
400 if f['id'] == image_id:
401 return f
402 return None
403
404- def fake_add_image_metadata(self, image_data):
405+ def fake_add_image(self, image_meta):
406 id = ''.join(random.choice(string.letters) for _ in range(20))
407- image_data['id'] = id
408- self.fixtures.append(image_data)
409+ image_meta['id'] = id
410+ self.fixtures.append(image_meta)
411 return id
412
413- def fake_update_image_metadata(self, image_id, image_data):
414- f = self.fake_get_image_metadata(image_id)
415+ def fake_update_image(self, image_id, image_meta):
416+ f = self.fake_get_image_meta(image_id)
417 if not f:
418 raise exc.NotFound
419
420- f.update(image_data)
421+ f.update(image_meta)
422
423- def fake_delete_image_metadata(self, image_id):
424- f = self.fake_get_image_metadata(image_id)
425+ def fake_delete_image(self, image_id):
426+ f = self.fake_get_image_meta(image_id)
427 if not f:
428 raise exc.NotFound
429
430 self.fixtures.remove(f)
431
432- def fake_delete_all(self):
433- self.fixtures = []
434-
435- fake_parallax_client = FakeParallaxClient(initial_fixtures)
436- stubs.Set(nova.image.glance.ParallaxClient, 'get_image_index',
437- fake_parallax_client.fake_get_image_index)
438- stubs.Set(nova.image.glance.ParallaxClient, 'get_image_details',
439- fake_parallax_client.fake_get_image_details)
440- stubs.Set(nova.image.glance.ParallaxClient, 'get_image_metadata',
441- fake_parallax_client.fake_get_image_metadata)
442- stubs.Set(nova.image.glance.ParallaxClient, 'add_image_metadata',
443- fake_parallax_client.fake_add_image_metadata)
444- stubs.Set(nova.image.glance.ParallaxClient, 'update_image_metadata',
445- fake_parallax_client.fake_update_image_metadata)
446- stubs.Set(nova.image.glance.ParallaxClient, 'delete_image_metadata',
447- fake_parallax_client.fake_delete_image_metadata)
448- stubs.Set(nova.image.glance.GlanceImageService, 'delete_all',
449- fake_parallax_client.fake_delete_all)
450+ ##def fake_delete_all(self):
451+ ## self.fixtures = []
452+
453+ GlanceClient = glance_client.Client
454+ fake = FakeGlanceClient(initial_fixtures)
455+
456+ stubs.Set(GlanceClient, 'get_images', fake.fake_get_images)
457+ stubs.Set(GlanceClient, 'get_images_detailed',
458+ fake.fake_get_images_detailed)
459+ stubs.Set(GlanceClient, 'get_image_meta', fake.fake_get_image_meta)
460+ stubs.Set(GlanceClient, 'add_image', fake.fake_add_image)
461+ stubs.Set(GlanceClient, 'update_image', fake.fake_update_image)
462+ stubs.Set(GlanceClient, 'delete_image', fake.fake_delete_image)
463+ #stubs.Set(GlanceClient, 'delete_all', fake.fake_delete_all)
464
465
466 class FakeToken(object):
467
468=== modified file 'nova/utils.py'
469--- nova/utils.py 2011-01-15 01:48:48 +0000
470+++ nova/utils.py 2011-01-17 17:34:42 +0000
471@@ -334,6 +334,20 @@
472 return getattr(backend, key)
473
474
475+class LoopingCallDone(Exception):
476+ """The poll-function passed to LoopingCall can raise this exception to
477+ break out of the loop normally. This is somewhat analogous to
478+ StopIteration.
479+
480+ An optional return-value can be included as the argument to the exception;
481+ this return-value will be returned by LoopingCall.wait()
482+ """
483+
484+ def __init__(self, retvalue=True):
485+ """:param retvalue: Value that LoopingCall.wait() should return"""
486+ self.retvalue = retvalue
487+
488+
489 class LoopingCall(object):
490 def __init__(self, f=None, *args, **kw):
491 self.args = args
492@@ -352,12 +366,15 @@
493 while self._running:
494 self.f(*self.args, **self.kw)
495 greenthread.sleep(interval)
496+ except LoopingCallDone, e:
497+ self.stop()
498+ done.send(e.retvalue)
499 except Exception:
500 logging.exception('in looping call')
501 done.send_exception(*sys.exc_info())
502 return
503-
504- done.send(True)
505+ else:
506+ done.send(True)
507
508 self.done = done
509
510
511=== modified file 'nova/virt/libvirt_conn.py'
512--- nova/virt/libvirt_conn.py 2011-01-15 01:54:36 +0000
513+++ nova/virt/libvirt_conn.py 2011-01-17 17:34:42 +0000
514@@ -295,7 +295,7 @@
515 virt_dom.detachDevice(xml)
516
517 @exception.wrap_exception
518- def snapshot(self, instance, name):
519+ def snapshot(self, instance, image_id):
520 """ Create snapshot from a running VM instance """
521 raise NotImplementedError(
522 _("Instance snapshotting is not supported for libvirt"
523
524=== modified file 'nova/virt/xenapi/vm_utils.py'
525--- nova/virt/xenapi/vm_utils.py 2011-01-11 06:47:35 +0000
526+++ nova/virt/xenapi/vm_utils.py 2011-01-17 17:34:42 +0000
527@@ -236,14 +236,15 @@
528 return template_vm_ref, [template_vdi_uuid, parent_uuid]
529
530 @classmethod
531- def upload_image(cls, session, instance_id, vdi_uuids, image_name):
532+ def upload_image(cls, session, instance_id, vdi_uuids, image_id):
533 """ Requests that the Glance plugin bundle the specified VDIs and
534 push them into Glance using the specified human-friendly name.
535 """
536- LOG.debug(_("Asking xapi to upload %s as '%s'"), vdi_uuids, image_name)
537+ logging.debug(_("Asking xapi to upload %s as ID %s"),
538+ vdi_uuids, image_id)
539
540 params = {'vdi_uuids': vdi_uuids,
541- 'image_name': image_name,
542+ 'image_id': image_id,
543 'glance_host': FLAGS.glance_host,
544 'glance_port': FLAGS.glance_port}
545
546@@ -424,9 +425,16 @@
547 * parent_vhd
548 snapshot
549 """
550- #TODO(sirp): we need to timeout this req after a while
551+ max_attempts = FLAGS.xenapi_vhd_coalesce_max_attempts
552+ attempts = {'counter': 0}
553
554 def _poll_vhds():
555+ attempts['counter'] += 1
556+ if attempts['counter'] > max_attempts:
557+ msg = (_("VHD coalesce attempts exceeded (%d > %d), giving up...")
558+ % (attempts['counter'], max_attempts))
559+ raise exception.Error(msg)
560+
561 scan_sr(session, instance_id, sr_ref)
562 parent_uuid = get_vhd_parent_uuid(session, vdi_ref)
563 if original_parent_uuid and (parent_uuid != original_parent_uuid):
564@@ -434,13 +442,12 @@
565 "waiting for coalesce..."), parent_uuid,
566 original_parent_uuid)
567 else:
568- done.send(parent_uuid)
569+ # Breakout of the loop (normally) and return the parent_uuid
570+ raise utils.LoopingCallDone(parent_uuid)
571
572- done = event.Event()
573 loop = utils.LoopingCall(_poll_vhds)
574 loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True)
575- parent_uuid = done.wait()
576- loop.stop()
577+ parent_uuid = loop.wait()
578 return parent_uuid
579
580
581
582=== modified file 'nova/virt/xenapi/vmops.py'
583--- nova/virt/xenapi/vmops.py 2011-01-13 16:53:13 +0000
584+++ nova/virt/xenapi/vmops.py 2011-01-17 17:34:42 +0000
585@@ -161,11 +161,11 @@
586 raise Exception(_('Instance not present %s') % instance_name)
587 return vm
588
589- def snapshot(self, instance, name):
590+ def snapshot(self, instance, image_id):
591 """ Create snapshot from a running VM instance
592
593 :param instance: instance to be snapshotted
594- :param name: name/label to be given to the snapshot
595+ :param image_id: id of image to upload to
596
597 Steps involved in a XenServer snapshot:
598
599@@ -201,7 +201,7 @@
600 try:
601 # call plugin to ship snapshot off to glance
602 VMHelper.upload_image(
603- self._session, instance.id, template_vdi_uuids, name)
604+ self._session, instance.id, template_vdi_uuids, image_id)
605 finally:
606 self._destroy(instance, template_vm_ref, shutdown=False)
607
608
609=== modified file 'nova/virt/xenapi_conn.py'
610--- nova/virt/xenapi_conn.py 2011-01-12 19:22:01 +0000
611+++ nova/virt/xenapi_conn.py 2011-01-17 17:34:42 +0000
612@@ -93,6 +93,10 @@
613 5.0,
614 'The interval used for polling of coalescing vhds.'
615 ' Used only if connection_type=xenapi.')
616+flags.DEFINE_integer('xenapi_vhd_coalesce_max_attempts',
617+ 5,
618+ 'Max number of times to poll for VHD to coalesce.'
619+ ' Used only if connection_type=xenapi.')
620 flags.DEFINE_string('target_host',
621 None,
622 'iSCSI Target Host')
623@@ -141,9 +145,9 @@
624 """Create VM instance"""
625 self._vmops.spawn(instance)
626
627- def snapshot(self, instance, name):
628+ def snapshot(self, instance, image_id):
629 """ Create snapshot from a running VM instance """
630- self._vmops.snapshot(instance, name)
631+ self._vmops.snapshot(instance, image_id)
632
633 def reboot(self, instance):
634 """Reboot VM instance"""
635
636=== modified file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/glance'
637--- plugins/xenserver/xenapi/etc/xapi.d/plugins/glance 2010-12-22 19:01:33 +0000
638+++ plugins/xenserver/xenapi/etc/xapi.d/plugins/glance 2011-01-17 17:34:42 +0000
639@@ -45,24 +45,24 @@
640 def put_vdis(session, args):
641 params = pickle.loads(exists(args, 'params'))
642 vdi_uuids = params["vdi_uuids"]
643- image_name = params["image_name"]
644+ image_id = params["image_id"]
645 glance_host = params["glance_host"]
646 glance_port = params["glance_port"]
647
648 sr_path = get_sr_path(session)
649 #FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs
650- tmp_file = "%s.tar.gz" % os.path.join('/tmp', image_name)
651+ tmp_file = "%s.tar.gz" % os.path.join('/tmp', str(image_id))
652 tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path]
653 paths = [ "%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids ]
654 tar_cmd.extend(paths)
655 logging.debug("Bundling image with cmd: %s", tar_cmd)
656 subprocess.call(tar_cmd)
657 logging.debug("Writing to test file %s", tmp_file)
658- put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port)
659+ put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port)
660 return "" # FIXME(sirp): return anything useful here?
661
662
663-def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port):
664+def put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port):
665 size = os.path.getsize(tmp_file)
666 basename = os.path.basename(tmp_file)
667
668@@ -72,7 +72,6 @@
669 'x-image-meta-store': 'file',
670 'x-image-meta-is_public': 'True',
671 'x-image-meta-type': 'raw',
672- 'x-image-meta-name': image_name,
673 'x-image-meta-size': size,
674 'content-length': size,
675 'content-type': 'application/octet-stream',
676@@ -80,7 +79,7 @@
677 conn = httplib.HTTPConnection(glance_host, glance_port)
678 #NOTE(sirp): httplib under python2.4 won't accept a file-like object
679 # to request
680- conn.putrequest('POST', '/images')
681+ conn.putrequest('PUT', '/images/%s' % image_id)
682
683 for header, value in headers.iteritems():
684 conn.putheader(header, value)