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
=== modified file 'nova/api/openstack/images.py'
--- nova/api/openstack/images.py 2011-01-06 02:23:23 +0000
+++ nova/api/openstack/images.py 2011-01-17 17:34:42 +0000
@@ -78,7 +78,14 @@
78 'decrypting': 'preparing',78 'decrypting': 'preparing',
79 'untarring': 'saving',79 'untarring': 'saving',
80 'available': 'active'}80 'available': 'active'}
81 item['status'] = status_mapping[item['status']]81 try:
82 item['status'] = status_mapping[item['status']]
83 except KeyError:
84 # TODO(sirp): Performing translation of status (if necessary) here for
85 # now. Perhaps this should really be done in EC2 API and
86 # S3ImageService
87 pass
88
82 return item89 return item
8390
8491
@@ -92,9 +99,11 @@
9299
93100
94def _convert_image_id_to_hash(image):101def _convert_image_id_to_hash(image):
95 image_id = abs(hash(image['imageId']))102 if 'imageId' in image:
96 image['imageId'] = image_id103 # Convert EC2-style ID (i-blah) to Rackspace-style (int)
97 image['id'] = image_id104 image_id = abs(hash(image['imageId']))
105 image['imageId'] = image_id
106 image['id'] = image_id
98107
99108
100class Controller(wsgi.Controller):109class Controller(wsgi.Controller):
@@ -147,7 +156,11 @@
147 env = self._deserialize(req.body, req)156 env = self._deserialize(req.body, req)
148 instance_id = env["image"]["serverId"]157 instance_id = env["image"]["serverId"]
149 name = env["image"]["name"]158 name = env["image"]["name"]
150 return compute.API().snapshot(context, instance_id, name)159
160 image_meta = compute.API().snapshot(
161 context, instance_id, name)
162
163 return dict(image=image_meta)
151164
152 def update(self, req, id):165 def update(self, req, id):
153 # Users may not modify public images, and that's all that166 # Users may not modify public images, and that's all that
154167
=== modified file 'nova/compute/api.py'
--- nova/compute/api.py 2011-01-15 01:48:48 +0000
+++ nova/compute/api.py 2011-01-17 17:34:42 +0000
@@ -335,27 +335,55 @@
335 project_id)335 project_id)
336 return self.db.instance_get_all(context)336 return self.db.instance_get_all(context)
337337
338 def _cast_compute_message(self, method, context, instance_id, host=None):338 def _cast_compute_message(self, method, context, instance_id, host=None,
339 """Generic handler for RPC casts to compute."""339 params=None):
340 """Generic handler for RPC casts to compute.
341
342 :param params: Optional dictionary of arguments to be passed to the
343 compute worker
344
345 :retval None
346 """
347 if not params:
348 params = {}
340 if not host:349 if not host:
341 instance = self.get(context, instance_id)350 instance = self.get(context, instance_id)
342 host = instance['host']351 host = instance['host']
343 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)352 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
344 kwargs = {'method': method, 'args': {'instance_id': instance_id}}353 params['instance_id'] = instance_id
354 kwargs = {'method': method, 'args': params}
345 rpc.cast(context, queue, kwargs)355 rpc.cast(context, queue, kwargs)
346356
347 def _call_compute_message(self, method, context, instance_id, host=None):357 def _call_compute_message(self, method, context, instance_id, host=None,
348 """Generic handler for RPC calls to compute."""358 params=None):
359 """Generic handler for RPC calls to compute.
360
361 :param params: Optional dictionary of arguments to be passed to the
362 compute worker
363
364 :retval: Result returned by compute worker
365 """
366 if not params:
367 params = {}
349 if not host:368 if not host:
350 instance = self.get(context, instance_id)369 instance = self.get(context, instance_id)
351 host = instance["host"]370 host = instance["host"]
352 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)371 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
353 kwargs = {"method": method, "args": {"instance_id": instance_id}}372 params['instance_id'] = instance_id
373 kwargs = {'method': method, 'args': params}
354 return rpc.call(context, queue, kwargs)374 return rpc.call(context, queue, kwargs)
355375
356 def snapshot(self, context, instance_id, name):376 def snapshot(self, context, instance_id, name):
357 """Snapshot the given instance."""377 """Snapshot the given instance.
358 self._cast_compute_message('snapshot_instance', context, instance_id)378
379 :retval: A dict containing image metadata
380 """
381 data = {'name': name, 'is_public': False}
382 image_meta = self.image_service.create(context, data)
383 params = {'image_id': image_meta['id']}
384 self._cast_compute_message('snapshot_instance', context, instance_id,
385 params=params)
386 return image_meta
359387
360 def reboot(self, context, instance_id):388 def reboot(self, context, instance_id):
361 """Reboot the given instance."""389 """Reboot the given instance."""
362390
=== modified file 'nova/compute/manager.py'
--- nova/compute/manager.py 2011-01-13 16:51:31 +0000
+++ nova/compute/manager.py 2011-01-17 17:34:42 +0000
@@ -294,7 +294,7 @@
294 self._update_state(context, instance_id)294 self._update_state(context, instance_id)
295295
296 @exception.wrap_exception296 @exception.wrap_exception
297 def snapshot_instance(self, context, instance_id, name):297 def snapshot_instance(self, context, instance_id, image_id):
298 """Snapshot an instance on this server."""298 """Snapshot an instance on this server."""
299 context = context.elevated()299 context = context.elevated()
300 instance_ref = self.db.instance_get(context, instance_id)300 instance_ref = self.db.instance_get(context, instance_id)
@@ -311,7 +311,7 @@
311 'instance: %s (state: %s excepted: %s)'),311 'instance: %s (state: %s excepted: %s)'),
312 instance_id, instance_ref['state'], power_state.RUNNING)312 instance_id, instance_ref['state'], power_state.RUNNING)
313313
314 self.driver.snapshot(instance_ref, name)314 self.driver.snapshot(instance_ref, image_id)
315315
316 @exception.wrap_exception316 @exception.wrap_exception
317 @checks_instance_lock317 @checks_instance_lock
318318
=== modified file 'nova/image/glance.py'
--- nova/image/glance.py 2011-01-07 14:46:17 +0000
+++ nova/image/glance.py 2011-01-17 17:34:42 +0000
@@ -14,9 +14,9 @@
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations15# License for the specific language governing permissions and limitations
16# under the License.16# under the License.
17
18"""Implementation of an image service that uses Glance as the backend"""17"""Implementation of an image service that uses Glance as the backend"""
1918
19from __future__ import absolute_import
20import httplib20import httplib
21import json21import json
22import urlparse22import urlparse
@@ -24,171 +24,40 @@
24from nova import exception24from nova import exception
25from nova import flags25from nova import flags
26from nova import log as logging26from nova import log as logging
27from nova import utils
27from nova.image import service28from nova.image import service
2829
2930
30LOG = logging.getLogger('nova.image.glance')31LOG = logging.getLogger('nova.image.glance')
3132
32FLAGS = flags.FLAGS33FLAGS = flags.FLAGS
33flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1',34
34 'IP address or URL where Glance\'s Teller service resides')35GlanceClient = utils.import_class('glance.client.Client')
35flags.DEFINE_string('glance_teller_port', '9191',
36 'Port for Glance\'s Teller service')
37flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1',
38 'IP address or URL where Glance\'s Parallax service '
39 'resides')
40flags.DEFINE_string('glance_parallax_port', '9292',
41 'Port for Glance\'s Parallax service')
42
43
44class TellerClient(object):
45
46 def __init__(self):
47 self.address = FLAGS.glance_teller_address
48 self.port = FLAGS.glance_teller_port
49 url = urlparse.urlparse(self.address)
50 self.netloc = url.netloc
51 self.connection_type = {'http': httplib.HTTPConnection,
52 'https': httplib.HTTPSConnection}[url.scheme]
53
54
55class ParallaxClient(object):
56
57 def __init__(self):
58 self.address = FLAGS.glance_parallax_address
59 self.port = FLAGS.glance_parallax_port
60 url = urlparse.urlparse(self.address)
61 self.netloc = url.netloc
62 self.connection_type = {'http': httplib.HTTPConnection,
63 'https': httplib.HTTPSConnection}[url.scheme]
64
65 def get_image_index(self):
66 """
67 Returns a list of image id/name mappings from Parallax
68 """
69 try:
70 c = self.connection_type(self.netloc, self.port)
71 c.request("GET", "images")
72 res = c.getresponse()
73 if res.status == 200:
74 # Parallax returns a JSONified dict(images=image_list)
75 data = json.loads(res.read())['images']
76 return data
77 else:
78 LOG.warn(_("Parallax returned HTTP error %d from "
79 "request for /images"), res.status_int)
80 return []
81 finally:
82 c.close()
83
84 def get_image_details(self):
85 """
86 Returns a list of detailed image data mappings from Parallax
87 """
88 try:
89 c = self.connection_type(self.netloc, self.port)
90 c.request("GET", "images/detail")
91 res = c.getresponse()
92 if res.status == 200:
93 # Parallax returns a JSONified dict(images=image_list)
94 data = json.loads(res.read())['images']
95 return data
96 else:
97 LOG.warn(_("Parallax returned HTTP error %d from "
98 "request for /images/detail"), res.status_int)
99 return []
100 finally:
101 c.close()
102
103 def get_image_metadata(self, image_id):
104 """
105 Returns a mapping of image metadata from Parallax
106 """
107 try:
108 c = self.connection_type(self.netloc, self.port)
109 c.request("GET", "images/%s" % image_id)
110 res = c.getresponse()
111 if res.status == 200:
112 # Parallax returns a JSONified dict(image=image_info)
113 data = json.loads(res.read())['image']
114 return data
115 else:
116 # TODO(jaypipes): log the error?
117 return None
118 finally:
119 c.close()
120
121 def add_image_metadata(self, image_metadata):
122 """
123 Tells parallax about an image's metadata
124 """
125 try:
126 c = self.connection_type(self.netloc, self.port)
127 body = json.dumps(image_metadata)
128 c.request("POST", "images", body)
129 res = c.getresponse()
130 if res.status == 200:
131 # Parallax returns a JSONified dict(image=image_info)
132 data = json.loads(res.read())['image']
133 return data['id']
134 else:
135 # TODO(jaypipes): log the error?
136 return None
137 finally:
138 c.close()
139
140 def update_image_metadata(self, image_id, image_metadata):
141 """
142 Updates Parallax's information about an image
143 """
144 try:
145 c = self.connection_type(self.netloc, self.port)
146 body = json.dumps(image_metadata)
147 c.request("PUT", "images/%s" % image_id, body)
148 res = c.getresponse()
149 return res.status == 200
150 finally:
151 c.close()
152
153 def delete_image_metadata(self, image_id):
154 """
155 Deletes Parallax's information about an image
156 """
157 try:
158 c = self.connection_type(self.netloc, self.port)
159 c.request("DELETE", "images/%s" % image_id)
160 res = c.getresponse()
161 return res.status == 200
162 finally:
163 c.close()
16436
16537
166class GlanceImageService(service.BaseImageService):38class GlanceImageService(service.BaseImageService):
167 """Provides storage and retrieval of disk image objects within Glance."""39 """Provides storage and retrieval of disk image objects within Glance."""
16840
169 def __init__(self):41 def __init__(self):
170 self.teller = TellerClient()42 self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port)
171 self.parallax = ParallaxClient()
17243
173 def index(self, context):44 def index(self, context):
174 """45 """
175 Calls out to Parallax for a list of images available46 Calls out to Glance for a list of images available
176 """47 """
177 images = self.parallax.get_image_index()48 return self.client.get_images()
178 return images
17949
180 def detail(self, context):50 def detail(self, context):
181 """51 """
182 Calls out to Parallax for a list of detailed image information52 Calls out to Glance for a list of detailed image information
183 """53 """
184 images = self.parallax.get_image_details()54 return self.client.get_images_detailed()
185 return images
18655
187 def show(self, context, id):56 def show(self, context, id):
188 """57 """
189 Returns a dict containing image data for the given opaque image id.58 Returns a dict containing image data for the given opaque image id.
190 """59 """
191 image = self.parallax.get_image_metadata(id)60 image = self.client.get_image_meta(id)
192 if image:61 if image:
193 return image62 return image
194 raise exception.NotFound63 raise exception.NotFound
@@ -200,7 +69,7 @@
200 :raises AlreadyExists if the image already exist.69 :raises AlreadyExists if the image already exist.
20170
202 """71 """
203 return self.parallax.add_image_metadata(data)72 return self.client.add_image(image_meta=data)
20473
205 def update(self, context, image_id, data):74 def update(self, context, image_id, data):
206 """Replace the contents of the given image with the new data.75 """Replace the contents of the given image with the new data.
@@ -208,7 +77,7 @@
208 :raises NotFound if the image does not exist.77 :raises NotFound if the image does not exist.
20978
210 """79 """
211 self.parallax.update_image_metadata(image_id, data)80 return self.client.update_image(image_id, data)
21281
213 def delete(self, context, image_id):82 def delete(self, context, image_id):
214 """83 """
@@ -217,7 +86,7 @@
217 :raises NotFound if the image does not exist.86 :raises NotFound if the image does not exist.
21887
219 """88 """
220 self.parallax.delete_image_metadata(image_id)89 return self.client.delete_image(image_id)
22190
222 def delete_all(self):91 def delete_all(self):
223 """92 """
22493
=== modified file 'nova/tests/api/openstack/fakes.py'
--- nova/tests/api/openstack/fakes.py 2011-01-10 02:08:54 +0000
+++ nova/tests/api/openstack/fakes.py 2011-01-17 17:34:42 +0000
@@ -23,6 +23,8 @@
23import webob23import webob
24import webob.dec24import webob.dec
2525
26from glance import client as glance_client
27
26from nova import auth28from nova import auth
27from nova import context29from nova import context
28from nova import exception as exc30from nova import exception as exc
@@ -116,64 +118,60 @@
116 stubs.Set(nova.compute.API, 'snapshot', snapshot)118 stubs.Set(nova.compute.API, 'snapshot', snapshot)
117119
118120
119def stub_out_glance(stubs, initial_fixtures=[]):121def stub_out_glance(stubs, initial_fixtures=None):
120122
121 class FakeParallaxClient:123 class FakeGlanceClient:
122124
123 def __init__(self, initial_fixtures):125 def __init__(self, initial_fixtures):
124 self.fixtures = initial_fixtures126 self.fixtures = initial_fixtures or []
125127
126 def fake_get_image_index(self):128 def fake_get_images(self):
127 return [dict(id=f['id'], name=f['name'])129 return [dict(id=f['id'], name=f['name'])
128 for f in self.fixtures]130 for f in self.fixtures]
129131
130 def fake_get_image_details(self):132 def fake_get_images_detailed(self):
131 return self.fixtures133 return self.fixtures
132134
133 def fake_get_image_metadata(self, image_id):135 def fake_get_image_meta(self, image_id):
134 for f in self.fixtures:136 for f in self.fixtures:
135 if f['id'] == image_id:137 if f['id'] == image_id:
136 return f138 return f
137 return None139 return None
138140
139 def fake_add_image_metadata(self, image_data):141 def fake_add_image(self, image_meta):
140 id = ''.join(random.choice(string.letters) for _ in range(20))142 id = ''.join(random.choice(string.letters) for _ in range(20))
141 image_data['id'] = id143 image_meta['id'] = id
142 self.fixtures.append(image_data)144 self.fixtures.append(image_meta)
143 return id145 return id
144146
145 def fake_update_image_metadata(self, image_id, image_data):147 def fake_update_image(self, image_id, image_meta):
146 f = self.fake_get_image_metadata(image_id)148 f = self.fake_get_image_meta(image_id)
147 if not f:149 if not f:
148 raise exc.NotFound150 raise exc.NotFound
149151
150 f.update(image_data)152 f.update(image_meta)
151153
152 def fake_delete_image_metadata(self, image_id):154 def fake_delete_image(self, image_id):
153 f = self.fake_get_image_metadata(image_id)155 f = self.fake_get_image_meta(image_id)
154 if not f:156 if not f:
155 raise exc.NotFound157 raise exc.NotFound
156158
157 self.fixtures.remove(f)159 self.fixtures.remove(f)
158160
159 def fake_delete_all(self):161 ##def fake_delete_all(self):
160 self.fixtures = []162 ## self.fixtures = []
161163
162 fake_parallax_client = FakeParallaxClient(initial_fixtures)164 GlanceClient = glance_client.Client
163 stubs.Set(nova.image.glance.ParallaxClient, 'get_image_index',165 fake = FakeGlanceClient(initial_fixtures)
164 fake_parallax_client.fake_get_image_index)166
165 stubs.Set(nova.image.glance.ParallaxClient, 'get_image_details',167 stubs.Set(GlanceClient, 'get_images', fake.fake_get_images)
166 fake_parallax_client.fake_get_image_details)168 stubs.Set(GlanceClient, 'get_images_detailed',
167 stubs.Set(nova.image.glance.ParallaxClient, 'get_image_metadata',169 fake.fake_get_images_detailed)
168 fake_parallax_client.fake_get_image_metadata)170 stubs.Set(GlanceClient, 'get_image_meta', fake.fake_get_image_meta)
169 stubs.Set(nova.image.glance.ParallaxClient, 'add_image_metadata',171 stubs.Set(GlanceClient, 'add_image', fake.fake_add_image)
170 fake_parallax_client.fake_add_image_metadata)172 stubs.Set(GlanceClient, 'update_image', fake.fake_update_image)
171 stubs.Set(nova.image.glance.ParallaxClient, 'update_image_metadata',173 stubs.Set(GlanceClient, 'delete_image', fake.fake_delete_image)
172 fake_parallax_client.fake_update_image_metadata)174 #stubs.Set(GlanceClient, 'delete_all', fake.fake_delete_all)
173 stubs.Set(nova.image.glance.ParallaxClient, 'delete_image_metadata',
174 fake_parallax_client.fake_delete_image_metadata)
175 stubs.Set(nova.image.glance.GlanceImageService, 'delete_all',
176 fake_parallax_client.fake_delete_all)
177175
178176
179class FakeToken(object):177class FakeToken(object):
180178
=== modified file 'nova/utils.py'
--- nova/utils.py 2011-01-15 01:48:48 +0000
+++ nova/utils.py 2011-01-17 17:34:42 +0000
@@ -334,6 +334,20 @@
334 return getattr(backend, key)334 return getattr(backend, key)
335335
336336
337class LoopingCallDone(Exception):
338 """The poll-function passed to LoopingCall can raise this exception to
339 break out of the loop normally. This is somewhat analogous to
340 StopIteration.
341
342 An optional return-value can be included as the argument to the exception;
343 this return-value will be returned by LoopingCall.wait()
344 """
345
346 def __init__(self, retvalue=True):
347 """:param retvalue: Value that LoopingCall.wait() should return"""
348 self.retvalue = retvalue
349
350
337class LoopingCall(object):351class LoopingCall(object):
338 def __init__(self, f=None, *args, **kw):352 def __init__(self, f=None, *args, **kw):
339 self.args = args353 self.args = args
@@ -352,12 +366,15 @@
352 while self._running:366 while self._running:
353 self.f(*self.args, **self.kw)367 self.f(*self.args, **self.kw)
354 greenthread.sleep(interval)368 greenthread.sleep(interval)
369 except LoopingCallDone, e:
370 self.stop()
371 done.send(e.retvalue)
355 except Exception:372 except Exception:
356 logging.exception('in looping call')373 logging.exception('in looping call')
357 done.send_exception(*sys.exc_info())374 done.send_exception(*sys.exc_info())
358 return375 return
359376 else:
360 done.send(True)377 done.send(True)
361378
362 self.done = done379 self.done = done
363380
364381
=== modified file 'nova/virt/libvirt_conn.py'
--- nova/virt/libvirt_conn.py 2011-01-15 01:54:36 +0000
+++ nova/virt/libvirt_conn.py 2011-01-17 17:34:42 +0000
@@ -295,7 +295,7 @@
295 virt_dom.detachDevice(xml)295 virt_dom.detachDevice(xml)
296296
297 @exception.wrap_exception297 @exception.wrap_exception
298 def snapshot(self, instance, name):298 def snapshot(self, instance, image_id):
299 """ Create snapshot from a running VM instance """299 """ Create snapshot from a running VM instance """
300 raise NotImplementedError(300 raise NotImplementedError(
301 _("Instance snapshotting is not supported for libvirt"301 _("Instance snapshotting is not supported for libvirt"
302302
=== modified file 'nova/virt/xenapi/vm_utils.py'
--- nova/virt/xenapi/vm_utils.py 2011-01-11 06:47:35 +0000
+++ nova/virt/xenapi/vm_utils.py 2011-01-17 17:34:42 +0000
@@ -236,14 +236,15 @@
236 return template_vm_ref, [template_vdi_uuid, parent_uuid]236 return template_vm_ref, [template_vdi_uuid, parent_uuid]
237237
238 @classmethod238 @classmethod
239 def upload_image(cls, session, instance_id, vdi_uuids, image_name):239 def upload_image(cls, session, instance_id, vdi_uuids, image_id):
240 """ Requests that the Glance plugin bundle the specified VDIs and240 """ Requests that the Glance plugin bundle the specified VDIs and
241 push them into Glance using the specified human-friendly name.241 push them into Glance using the specified human-friendly name.
242 """242 """
243 LOG.debug(_("Asking xapi to upload %s as '%s'"), vdi_uuids, image_name)243 logging.debug(_("Asking xapi to upload %s as ID %s"),
244 vdi_uuids, image_id)
244245
245 params = {'vdi_uuids': vdi_uuids,246 params = {'vdi_uuids': vdi_uuids,
246 'image_name': image_name,247 'image_id': image_id,
247 'glance_host': FLAGS.glance_host,248 'glance_host': FLAGS.glance_host,
248 'glance_port': FLAGS.glance_port}249 'glance_port': FLAGS.glance_port}
249250
@@ -424,9 +425,16 @@
424 * parent_vhd425 * parent_vhd
425 snapshot426 snapshot
426 """427 """
427 #TODO(sirp): we need to timeout this req after a while428 max_attempts = FLAGS.xenapi_vhd_coalesce_max_attempts
429 attempts = {'counter': 0}
428430
429 def _poll_vhds():431 def _poll_vhds():
432 attempts['counter'] += 1
433 if attempts['counter'] > max_attempts:
434 msg = (_("VHD coalesce attempts exceeded (%d > %d), giving up...")
435 % (attempts['counter'], max_attempts))
436 raise exception.Error(msg)
437
430 scan_sr(session, instance_id, sr_ref)438 scan_sr(session, instance_id, sr_ref)
431 parent_uuid = get_vhd_parent_uuid(session, vdi_ref)439 parent_uuid = get_vhd_parent_uuid(session, vdi_ref)
432 if original_parent_uuid and (parent_uuid != original_parent_uuid):440 if original_parent_uuid and (parent_uuid != original_parent_uuid):
@@ -434,13 +442,12 @@
434 "waiting for coalesce..."), parent_uuid,442 "waiting for coalesce..."), parent_uuid,
435 original_parent_uuid)443 original_parent_uuid)
436 else:444 else:
437 done.send(parent_uuid)445 # Breakout of the loop (normally) and return the parent_uuid
446 raise utils.LoopingCallDone(parent_uuid)
438447
439 done = event.Event()
440 loop = utils.LoopingCall(_poll_vhds)448 loop = utils.LoopingCall(_poll_vhds)
441 loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True)449 loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True)
442 parent_uuid = done.wait()450 parent_uuid = loop.wait()
443 loop.stop()
444 return parent_uuid451 return parent_uuid
445452
446453
447454
=== modified file 'nova/virt/xenapi/vmops.py'
--- nova/virt/xenapi/vmops.py 2011-01-13 16:53:13 +0000
+++ nova/virt/xenapi/vmops.py 2011-01-17 17:34:42 +0000
@@ -161,11 +161,11 @@
161 raise Exception(_('Instance not present %s') % instance_name)161 raise Exception(_('Instance not present %s') % instance_name)
162 return vm162 return vm
163163
164 def snapshot(self, instance, name):164 def snapshot(self, instance, image_id):
165 """ Create snapshot from a running VM instance165 """ Create snapshot from a running VM instance
166166
167 :param instance: instance to be snapshotted167 :param instance: instance to be snapshotted
168 :param name: name/label to be given to the snapshot168 :param image_id: id of image to upload to
169169
170 Steps involved in a XenServer snapshot:170 Steps involved in a XenServer snapshot:
171171
@@ -201,7 +201,7 @@
201 try:201 try:
202 # call plugin to ship snapshot off to glance202 # call plugin to ship snapshot off to glance
203 VMHelper.upload_image(203 VMHelper.upload_image(
204 self._session, instance.id, template_vdi_uuids, name)204 self._session, instance.id, template_vdi_uuids, image_id)
205 finally:205 finally:
206 self._destroy(instance, template_vm_ref, shutdown=False)206 self._destroy(instance, template_vm_ref, shutdown=False)
207207
208208
=== modified file 'nova/virt/xenapi_conn.py'
--- nova/virt/xenapi_conn.py 2011-01-12 19:22:01 +0000
+++ nova/virt/xenapi_conn.py 2011-01-17 17:34:42 +0000
@@ -93,6 +93,10 @@
93 5.0,93 5.0,
94 'The interval used for polling of coalescing vhds.'94 'The interval used for polling of coalescing vhds.'
95 ' Used only if connection_type=xenapi.')95 ' Used only if connection_type=xenapi.')
96flags.DEFINE_integer('xenapi_vhd_coalesce_max_attempts',
97 5,
98 'Max number of times to poll for VHD to coalesce.'
99 ' Used only if connection_type=xenapi.')
96flags.DEFINE_string('target_host',100flags.DEFINE_string('target_host',
97 None,101 None,
98 'iSCSI Target Host')102 'iSCSI Target Host')
@@ -141,9 +145,9 @@
141 """Create VM instance"""145 """Create VM instance"""
142 self._vmops.spawn(instance)146 self._vmops.spawn(instance)
143147
144 def snapshot(self, instance, name):148 def snapshot(self, instance, image_id):
145 """ Create snapshot from a running VM instance """149 """ Create snapshot from a running VM instance """
146 self._vmops.snapshot(instance, name)150 self._vmops.snapshot(instance, image_id)
147151
148 def reboot(self, instance):152 def reboot(self, instance):
149 """Reboot VM instance"""153 """Reboot VM instance"""
150154
=== modified file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/glance'
--- plugins/xenserver/xenapi/etc/xapi.d/plugins/glance 2010-12-22 19:01:33 +0000
+++ plugins/xenserver/xenapi/etc/xapi.d/plugins/glance 2011-01-17 17:34:42 +0000
@@ -45,24 +45,24 @@
45def put_vdis(session, args):45def put_vdis(session, args):
46 params = pickle.loads(exists(args, 'params'))46 params = pickle.loads(exists(args, 'params'))
47 vdi_uuids = params["vdi_uuids"]47 vdi_uuids = params["vdi_uuids"]
48 image_name = params["image_name"]48 image_id = params["image_id"]
49 glance_host = params["glance_host"]49 glance_host = params["glance_host"]
50 glance_port = params["glance_port"]50 glance_port = params["glance_port"]
51 51
52 sr_path = get_sr_path(session)52 sr_path = get_sr_path(session)
53 #FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs53 #FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs
54 tmp_file = "%s.tar.gz" % os.path.join('/tmp', image_name) 54 tmp_file = "%s.tar.gz" % os.path.join('/tmp', str(image_id))
55 tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path]55 tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path]
56 paths = [ "%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids ]56 paths = [ "%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids ]
57 tar_cmd.extend(paths)57 tar_cmd.extend(paths)
58 logging.debug("Bundling image with cmd: %s", tar_cmd)58 logging.debug("Bundling image with cmd: %s", tar_cmd)
59 subprocess.call(tar_cmd)59 subprocess.call(tar_cmd)
60 logging.debug("Writing to test file %s", tmp_file) 60 logging.debug("Writing to test file %s", tmp_file)
61 put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port)61 put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port)
62 return "" # FIXME(sirp): return anything useful here?62 return "" # FIXME(sirp): return anything useful here?
6363
6464
65def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port):65def put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port):
66 size = os.path.getsize(tmp_file) 66 size = os.path.getsize(tmp_file)
67 basename = os.path.basename(tmp_file)67 basename = os.path.basename(tmp_file)
6868
@@ -72,7 +72,6 @@
72 'x-image-meta-store': 'file',72 'x-image-meta-store': 'file',
73 'x-image-meta-is_public': 'True',73 'x-image-meta-is_public': 'True',
74 'x-image-meta-type': 'raw',74 'x-image-meta-type': 'raw',
75 'x-image-meta-name': image_name,
76 'x-image-meta-size': size,75 'x-image-meta-size': size,
77 'content-length': size,76 'content-length': size,
78 'content-type': 'application/octet-stream',77 'content-type': 'application/octet-stream',
@@ -80,7 +79,7 @@
80 conn = httplib.HTTPConnection(glance_host, glance_port)79 conn = httplib.HTTPConnection(glance_host, glance_port)
81 #NOTE(sirp): httplib under python2.4 won't accept a file-like object80 #NOTE(sirp): httplib under python2.4 won't accept a file-like object
82 # to request81 # to request
83 conn.putrequest('POST', '/images')82 conn.putrequest('PUT', '/images/%s' % image_id)
8483
85 for header, value in headers.iteritems():84 for header, value in headers.iteritems():
86 conn.putheader(header, value)85 conn.putheader(header, value)