Reserving image before uploading

This commit is contained in:
Rick Harris
2011-01-06 21:37:33 -06:00
parent dd1e36b969
commit 3bf9bc6f6c
10 changed files with 77 additions and 175 deletions

View File

@@ -115,7 +115,8 @@ class Controller(wsgi.Controller):
items = self._service.index(req.environ['nova.context'])
items = common.limited(items, req)
items = [_translate_keys(item) for item in items]
items = [_translate_status(item) for item in items]
#TODO(sirp): removing for glance
#items = [_translate_status(item) for item in items]
return dict(images=items)
def show(self, req, id):
@@ -131,7 +132,12 @@ class Controller(wsgi.Controller):
env = self._deserialize(req.body, req)
instance_id = env["image"]["serverId"]
name = env["image"]["name"]
return compute_api.ComputeAPI().snapshot(context, instance_id, name)
image_meta = compute_api.ComputeAPI().snapshot(
context, instance_id, name)
#TODO(sirp): need to map Glance attrs to OpenStackAPI attrs
return dict(image=image_meta)
def update(self, req, id):
# Users may not modify public images, and that's all that

View File

@@ -263,10 +263,18 @@ class ComputeAPI(base.Base):
"""Snapshot the given instance."""
instance = self.db.instance_get_by_internal_id(context, instance_id)
host = instance['host']
image_service = utils.import_object(FLAGS.image_service)
data = {'name': name, 'is_public': True}
image_meta = image_service.create(context, data)
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "snapshot_instance",
"args": {"instance_id": instance['id'], "name": name}})
"args": {"instance_id": instance['id'],
"image_id": image_meta['id']}})
return image_meta
def reboot(self, context, instance_id):
"""Reboot the given instance."""

View File

@@ -225,7 +225,7 @@ class ComputeManager(manager.Manager):
self._update_state(context, instance_id)
@exception.wrap_exception
def snapshot_instance(self, context, instance_id, name):
def snapshot_instance(self, context, instance_id, image_id):
"""Snapshot an instance on this server."""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
@@ -243,7 +243,7 @@ class ComputeManager(manager.Manager):
instance_ref['state'],
power_state.RUNNING)
self.driver.snapshot(instance_ref, name)
self.driver.snapshot(instance_ref, image_id)
@exception.wrap_exception
def rescue_instance(self, context, instance_id):

View File

@@ -14,9 +14,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Implementation of an image service that uses Glance as the backend"""
from __future__ import absolute_import
import httplib
import json
import logging
@@ -24,8 +24,6 @@ import urlparse
import webob.exc
from nova.compute import api as compute_api
from nova import utils
from nova import flags
from nova import exception
import nova.image.service
@@ -33,165 +31,30 @@ import nova.image.service
FLAGS = flags.FLAGS
flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1',
'IP address or URL where Glance\'s Teller service resides')
flags.DEFINE_string('glance_teller_port', '9191',
'Port for Glance\'s Teller service')
flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1',
'IP address or URL where Glance\'s Parallax service '
'resides')
flags.DEFINE_string('glance_parallax_port', '9292',
'Port for Glance\'s Parallax service')
class TellerClient(object):
def __init__(self):
self.address = FLAGS.glance_teller_address
self.port = FLAGS.glance_teller_port
url = urlparse.urlparse(self.address)
self.netloc = url.netloc
self.connection_type = {'http': httplib.HTTPConnection,
'https': httplib.HTTPSConnection}[url.scheme]
class ParallaxClient(object):
def __init__(self):
self.address = FLAGS.glance_parallax_address
self.port = FLAGS.glance_parallax_port
url = urlparse.urlparse(self.address)
self.netloc = url.netloc
self.connection_type = {'http': httplib.HTTPConnection,
'https': httplib.HTTPSConnection}[url.scheme]
def get_image_index(self):
"""
Returns a list of image id/name mappings from Parallax
"""
try:
c = self.connection_type(self.netloc, self.port)
c.request("GET", "images")
res = c.getresponse()
if res.status == 200:
# Parallax returns a JSONified dict(images=image_list)
data = json.loads(res.read())['images']
return data
else:
logging.warn(_("Parallax returned HTTP error %d from "
"request for /images"), res.status_int)
return []
finally:
c.close()
def get_image_details(self):
"""
Returns a list of detailed image data mappings from Parallax
"""
try:
c = self.connection_type(self.netloc, self.port)
c.request("GET", "images/detail")
res = c.getresponse()
if res.status == 200:
# Parallax returns a JSONified dict(images=image_list)
data = json.loads(res.read())['images']
return data
else:
logging.warn(_("Parallax returned HTTP error %d from "
"request for /images/detail"), res.status_int)
return []
finally:
c.close()
def get_image_metadata(self, image_id):
"""
Returns a mapping of image metadata from Parallax
"""
try:
c = self.connection_type(self.netloc, self.port)
c.request("GET", "images/%s" % image_id)
res = c.getresponse()
if res.status == 200:
# Parallax returns a JSONified dict(image=image_info)
data = json.loads(res.read())['image']
return data
else:
# TODO(jaypipes): log the error?
return None
finally:
c.close()
def add_image_metadata(self, image_metadata):
"""
Tells parallax about an image's metadata
"""
try:
c = self.connection_type(self.netloc, self.port)
body = json.dumps(image_metadata)
c.request("POST", "images", body)
res = c.getresponse()
if res.status == 200:
# Parallax returns a JSONified dict(image=image_info)
data = json.loads(res.read())['image']
return data['id']
else:
# TODO(jaypipes): log the error?
return None
finally:
c.close()
def update_image_metadata(self, image_id, image_metadata):
"""
Updates Parallax's information about an image
"""
try:
c = self.connection_type(self.netloc, self.port)
body = json.dumps(image_metadata)
c.request("PUT", "images/%s" % image_id, body)
res = c.getresponse()
return res.status == 200
finally:
c.close()
def delete_image_metadata(self, image_id):
"""
Deletes Parallax's information about an image
"""
try:
c = self.connection_type(self.netloc, self.port)
c.request("DELETE", "images/%s" % image_id)
res = c.getresponse()
return res.status == 200
finally:
c.close()
class GlanceImageService(nova.image.service.BaseImageService):
"""Provides storage and retrieval of disk image objects within Glance."""
def __init__(self):
self.teller = TellerClient()
self.parallax = ParallaxClient()
from glance.client import Client #TODO(sirp): lazy-import glance
self.client = Client(FLAGS.glance_host, FLAGS.glance_port)
def index(self, context):
"""
Calls out to Parallax for a list of images available
"""
images = self.parallax.get_image_index()
return images
return self.client.get_images()
def detail(self, context):
"""
Calls out to Parallax for a list of detailed image information
"""
images = self.parallax.get_image_details()
return images
return self.client.get_images_detailed()
def show(self, context, id):
"""
Returns a dict containing image data for the given opaque image id.
"""
image = self.parallax.get_image_metadata(id)
image = self.client.get_image_meta(id)
if image:
return image
raise exception.NotFound
@@ -203,7 +66,7 @@ class GlanceImageService(nova.image.service.BaseImageService):
:raises AlreadyExists if the image already exist.
"""
return self.parallax.add_image_metadata(data)
return self.client.add_image(image_meta=data)
def update(self, context, image_id, data):
"""Replace the contents of the given image with the new data.
@@ -211,7 +74,7 @@ class GlanceImageService(nova.image.service.BaseImageService):
:raises NotFound if the image does not exist.
"""
self.parallax.update_image_metadata(image_id, data)
return self.client.update_image(image_id, data)
def delete(self, context, image_id):
"""
@@ -220,10 +83,10 @@ class GlanceImageService(nova.image.service.BaseImageService):
:raises NotFound if the image does not exist.
"""
self.parallax.delete_image_metadata(image_id)
return self.client.delete_image(image_id)
def delete_all(self):
"""
Clears out all images
"""
pass
raise NotImplementedError

View File

@@ -304,6 +304,19 @@ class LazyPluggable(object):
return getattr(backend, key)
class LoopingCallDone(Exception):
"""The poll-function passed to LoopingCall can raise this exception to
break out of the loop normally. This is somewhat analogous to StopIteration.
An optional return-value can be included as the argument to the exception;
this return-value will be returned by LoopingCall.wait()
"""
def __init__(self, retvalue=True):
""":param retvalue: Value that LoopingCall.wait() should return"""
self.retvalue = retvalue
class LoopingCall(object):
def __init__(self, f=None, *args, **kw):
self.args = args
@@ -322,12 +335,15 @@ class LoopingCall(object):
while self._running:
self.f(*self.args, **self.kw)
greenthread.sleep(interval)
except LoopingCallDone, e:
self.stop()
done.send(e.retvalue)
except Exception:
logging.exception('in looping call')
done.send_exception(*sys.exc_info())
return
done.send(True)
else:
done.send(True)
self.done = done

View File

@@ -260,7 +260,7 @@ class LibvirtConnection(object):
virt_dom.detachDevice(xml)
@exception.wrap_exception
def snapshot(self, instance, name):
def snapshot(self, instance, image_id):
""" Create snapshot from a running VM instance """
raise NotImplementedError(
_("Instance snapshotting is not supported for libvirt"

View File

@@ -237,15 +237,15 @@ class VMHelper(HelperBase):
return template_vm_ref, [template_vdi_uuid, parent_uuid]
@classmethod
def upload_image(cls, session, instance_id, vdi_uuids, image_name):
def upload_image(cls, session, instance_id, vdi_uuids, image_id):
""" Requests that the Glance plugin bundle the specified VDIs and
push them into Glance using the specified human-friendly name.
"""
logging.debug(_("Asking xapi to upload %s as '%s'"),
vdi_uuids, image_name)
logging.debug(_("Asking xapi to upload %s as ID %s"),
vdi_uuids, image_id)
params = {'vdi_uuids': vdi_uuids,
'image_name': image_name,
'image_id': image_id,
'glance_host': FLAGS.glance_host,
'glance_port': FLAGS.glance_port}
@@ -427,9 +427,16 @@ def wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref,
* parent_vhd
snapshot
"""
#TODO(sirp): we need to timeout this req after a while
max_attempts = FLAGS.xenapi_vhd_coalesce_max_attempts
attempts = {'counter': 0}
def _poll_vhds():
attempts['counter'] += 1
if attempts['counter'] > max_attempts:
msg = (_("VHD coalesce attempts exceeded (%d > %d), giving up...")
% (attempts['counter'], max_attempts))
raise exception.Error(msg)
scan_sr(session, instance_id, sr_ref)
parent_uuid = get_vhd_parent_uuid(session, vdi_ref)
if original_parent_uuid and (parent_uuid != original_parent_uuid):
@@ -438,13 +445,12 @@ def wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref,
"waiting for coalesce..."),
parent_uuid, original_parent_uuid)
else:
done.send(parent_uuid)
# Breakout of the loop (normally) and return the parent_uuid
raise utils.LoopingCallDone(parent_uuid)
done = event.Event()
loop = utils.LoopingCall(_poll_vhds)
loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True)
parent_uuid = done.wait()
loop.stop()
parent_uuid = loop.wait()
return parent_uuid

View File

@@ -120,11 +120,11 @@ class VMOps(object):
timer.f = _wait_for_boot
return timer.start(interval=0.5, now=True)
def snapshot(self, instance, name):
def snapshot(self, instance, image_id):
""" Create snapshot from a running VM instance
:param instance: instance to be snapshotted
:param name: name/label to be given to the snapshot
:param image_id: id of image to upload to
Steps involved in a XenServer snapshot:
@@ -160,7 +160,7 @@ class VMOps(object):
try:
# call plugin to ship snapshot off to glance
VMHelper.upload_image(
self._session, instance.id, template_vdi_uuids, name)
self._session, instance.id, template_vdi_uuids, image_id)
finally:
self._destroy(instance, template_vm_ref, shutdown=False)

View File

@@ -87,6 +87,10 @@ flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval',
5.0,
'The interval used for polling of coalescing vhds.'
' Used only if connection_type=xenapi.')
flags.DEFINE_integer('xenapi_vhd_coalesce_max_attempts',
5,
'Max number of times to poll for VHD to coalesce.'
' Used only if connection_type=xenapi.')
flags.DEFINE_string('target_host',
None,
'iSCSI Target Host')
@@ -135,9 +139,9 @@ class XenAPIConnection(object):
"""Create VM instance"""
self._vmops.spawn(instance)
def snapshot(self, instance, name):
def snapshot(self, instance, image_id):
""" Create snapshot from a running VM instance """
self._vmops.snapshot(instance, name)
self._vmops.snapshot(instance, image_id)
def reboot(self, instance):
"""Reboot VM instance"""

View File

@@ -45,24 +45,24 @@ FILE_SR_PATH = '/var/run/sr-mount'
def put_vdis(session, args):
params = pickle.loads(exists(args, 'params'))
vdi_uuids = params["vdi_uuids"]
image_name = params["image_name"]
image_id = params["image_id"]
glance_host = params["glance_host"]
glance_port = params["glance_port"]
sr_path = get_sr_path(session)
#FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs
tmp_file = "%s.tar.gz" % os.path.join('/tmp', image_name)
tmp_file = "%s.tar.gz" % os.path.join('/tmp', str(image_id))
tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path]
paths = [ "%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids ]
tar_cmd.extend(paths)
logging.debug("Bundling image with cmd: %s", tar_cmd)
subprocess.call(tar_cmd)
logging.debug("Writing to test file %s", tmp_file)
put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port)
put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port)
return "" # FIXME(sirp): return anything useful here?
def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port):
def put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port):
size = os.path.getsize(tmp_file)
basename = os.path.basename(tmp_file)
@@ -72,7 +72,6 @@ def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port):
'x-image-meta-store': 'file',
'x-image-meta-is_public': 'True',
'x-image-meta-type': 'raw',
'x-image-meta-name': image_name,
'x-image-meta-size': size,
'content-length': size,
'content-type': 'application/octet-stream',
@@ -80,7 +79,7 @@ def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port):
conn = httplib.HTTPConnection(glance_host, glance_port)
#NOTE(sirp): httplib under python2.4 won't accept a file-like object
# to request
conn.putrequest('POST', '/images')
conn.putrequest('PUT', '/images/%s' % image_id)
for header, value in headers.iteritems():
conn.putheader(header, value)