Reserving image before uploading
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user