summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorMike Fedosin <mfedosin@mirantis.com>2016-06-02 16:41:58 +0300
committerSudipta Biswas <sbiswas7@in.ibm.com>2016-06-08 15:37:01 +0530
commit78fbb4776e545b0896b57b4292b4ef207de8edcb (patch)
tree15dc15b9846ff3e630659008da5835be92345ad7 /plugins
parent0a533df573552e7200786db8dee1ea8483c1a86c (diff)
downloadnova-78fbb4776e545b0896b57b4292b4ef207de8edcb.tar.gz
Make Xenplugin to work with glance v2 api
This code makes xenplugin version agnostic and allows to work with both apis, depending on what version is considered as 'current' Plugin version is bumped to 1.4. partially implements bp use-glance-v2-api Co-Authored-By: Sudipta Biswas <sbiswas7@in.ibm.com> Change-Id: I84f6971afaeaeed69a2d3e93a34af8df70d1fb00
Diffstat (limited to 'plugins')
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/glance299
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version3
2 files changed, 261 insertions, 41 deletions
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
index f2a8f90a7d..0149b16660 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
@@ -30,6 +30,11 @@ try:
except ImportError:
from six.moves import http_client as httplib
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
import md5 # noqa
import socket
import urllib2
@@ -50,6 +55,15 @@ class RetryableError(Exception):
pass
+def _create_connection(scheme, netloc):
+ if scheme == 'https':
+ conn = httplib.HTTPSConnection(netloc)
+ else:
+ conn = httplib.HTTPConnection(netloc)
+ conn.connect()
+ return conn
+
+
def _download_tarball_and_verify(request, staging_path):
# NOTE(johngarbutt) By default, there is no timeout.
# To ensure the script does not hang if we lose connection
@@ -88,9 +102,12 @@ def _download_tarball_and_verify(request, staging_path):
bytes_read = callback_data['bytes_read']
logging.info("Read %d bytes from %s", bytes_read, url)
- # Use ETag if available, otherwise X-Image-Meta-Checksum
+ # Use ETag if available, otherwise content-md5(v2) or
+ # X-Image-Meta-Checksum(v1)
etag = response.info().getheader('etag', None)
if etag is None:
+ etag = response.info().getheader('content-md5', None)
+ if etag is None:
etag = response.info().getheader('x-image-meta-checksum', None)
# Verify checksum using ETag
@@ -107,10 +124,10 @@ def _download_tarball_and_verify(request, staging_path):
logging.info(msg % {'checksum': checksum})
-def _download_tarball(sr_path, staging_path, image_id, glance_host,
+def _download_tarball_v1(sr_path, staging_path, image_id, glance_host,
glance_port, glance_use_ssl, extra_headers):
- """Download the tarball image from Glance and extract it into the staging
- area. Retry if there is any failure.
+ """Download the tarball image from Glance v1 and extract it into the
+ staging area. Retry if there is any failure.
"""
if glance_use_ssl:
scheme = 'https'
@@ -120,19 +137,20 @@ def _download_tarball(sr_path, staging_path, image_id, glance_host,
endpoint = "%(scheme)s://%(glance_host)s:%(glance_port)d" % {
'scheme': scheme, 'glance_host': glance_host,
'glance_port': glance_port}
- _download_tarball_by_url(sr_path, staging_path, image_id,
- endpoint, extra_headers)
+ _download_tarball_by_url_v1(sr_path, staging_path, image_id,
+ endpoint, extra_headers)
-def _download_tarball_by_url(sr_path, staging_path, image_id,
- glance_endpoint, extra_headers):
- """Download the tarball image from Glance and extract it into the staging
- area. Retry if there is any failure.
+def _download_tarball_by_url_v1(
+ sr_path, staging_path, image_id, glance_endpoint, extra_headers):
+ """Download the tarball image from Glance v1 and extract it into the
+ staging area. Retry if there is any failure.
"""
- url = ("%(glance_endpoint)s/v1/images/%(image_id)s" % {
+
+ url = "%(glance_endpoint)s/v1/images/%(image_id)s" % {
'glance_endpoint': glance_endpoint,
- 'image_id': image_id})
- logging.info("Downloading %s" % url)
+ 'image_id': image_id}
+ logging.info("Downloading %s with glance v1 api" % url)
request = urllib2.Request(url, headers=extra_headers)
try:
@@ -142,7 +160,26 @@ def _download_tarball_by_url(sr_path, staging_path, image_id,
raise
-def _upload_tarball(staging_path, image_id, glance_host, glance_port,
+def _download_tarball_by_url_v2(
+ sr_path, staging_path, image_id, glance_endpoint, extra_headers):
+ """Download the tarball image from Glance v2 and extract it into the
+ staging area. Retry if there is any failure.
+ """
+
+ url = "%(glance_endpoint)s/v2/images/%(image_id)s/file" % {
+ 'glance_endpoint': glance_endpoint,
+ 'image_id': image_id}
+ logging.debug("Downloading %s with glance v2 api" % url)
+
+ request = urllib2.Request(url, headers=extra_headers)
+ try:
+ _download_tarball_and_verify(request, staging_path)
+ except Exception:
+ logging.exception('Failed to retrieve %(url)s' % {'url': url})
+ raise
+
+
+def _upload_tarball_v1(staging_path, image_id, glance_host, glance_port,
glance_use_ssl, extra_headers, properties):
if glance_use_ssl:
scheme = 'https'
@@ -150,15 +187,14 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port,
scheme = 'http'
url = '%s://%s:%s' % (scheme, glance_host, glance_port)
- _upload_tarball_by_url(staging_path, image_id, url,
- extra_headers, properties)
-
+ _upload_tarball_by_url_v1(staging_path, image_id, url,
+ extra_headers, properties)
-def _upload_tarball_by_url(staging_path, image_id, glance_endpoint,
- extra_headers, properties):
- """Upload an image to Glance.
- Create a tarball of the image and then stream that into Glance
+def _upload_tarball_by_url_v1(staging_path, image_id, glance_endpoint,
+ extra_headers, properties):
+ """
+ Create a tarball of the image and then stream that into Glance v1
using chunked-transfer-encoded HTTP.
"""
# NOTE(johngarbutt) By default, there is no timeout.
@@ -167,8 +203,10 @@ def _upload_tarball_by_url(staging_path, image_id, glance_endpoint,
# This is here so there is no chance the timeout out has
# been adjusted by other library calls.
socket.setdefaulttimeout(SOCKET_TIMEOUT_SECONDS)
+ logging.debug("Uploading image %s with glance v1 api"
+ % image_id)
- url = '%(glance_endpoint)s/v1/images/%(image_id)s' % {
+ url = "%(glance_endpoint)s/v1/images/%(image_id)s" % {
'glance_endpoint': glance_endpoint,
'image_id': image_id}
logging.info("Writing image data to %s" % url)
@@ -181,17 +219,13 @@ def _upload_tarball_by_url(staging_path, image_id, glance_endpoint,
parts = urlparse(url)
try:
- if parts[0] == 'https':
- conn = httplib.HTTPSConnection(parts[1])
- else:
- conn = httplib.HTTPConnection(parts[1])
- conn.connect()
+ conn = _create_connection(parts[0], parts[1])
except Exception, error: # noqa
logging.exception('Failed to connect %(url)s' % {'url': url})
raise RetryableError(error)
try:
- validate_image_status_before_upload(conn, url, extra_headers)
+ validate_image_status_before_upload_v1(conn, url, extra_headers)
try:
# NOTE(sirp): httplib under python2.4 won't accept
@@ -268,6 +302,131 @@ def _upload_tarball_by_url(staging_path, image_id, glance_endpoint,
conn.close()
+def _update_image_meta_v2(conn, image_id, extra_headers, properties):
+ # NOTE(sirp): There is some confusion around OVF. Here's a summary
+ # of where we currently stand:
+ # 1. OVF as a container format is misnamed. We really should be
+ # using OVA since that is the name for the container format;
+ # OVF is the standard applied to the manifest file contained
+ # within.
+ # 2. We're currently uploading a vanilla tarball. In order to be
+ # OVF/OVA compliant, we'll need to embed a minimal OVF
+ # manifest as the first file.
+ body = [
+ {"path": "/container_format", "value": "ovf", "op": "add"},
+ {"path": "/disk_format", "value": "vhd", "op": "add"},
+ {"path": "/visibility", "value": "private", "op": "add"}]
+
+ headers = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
+ headers.update(**extra_headers)
+
+ for key, value in properties.iteritems():
+ prop = {"path": "/%s" % key.replace('_', '-'),
+ "value": key,
+ "op": "add"}
+ body.append(prop)
+ body = json.dumps(body)
+ conn.request('PATCH', '/v2/images/%s' % image_id, body=body, headers=headers)
+ resp = conn.getresponse()
+ resp.read()
+
+ if resp.status == httplib.OK:
+ return
+ logging.error("Image meta was not updated. Status: %s, Reason: %s" %
+ (resp.status, resp.reason))
+
+
+def _upload_tarball_by_url_v2(staging_path, image_id, glance_endpoint,
+ extra_headers, properties):
+ """
+ Create a tarball of the image and then stream that into Glance v2
+ using chunked-transfer-encoded HTTP.
+ """
+ # NOTE(johngarbutt) By default, there is no timeout.
+ # To ensure the script does not hang if we lose connection
+ # to glance, we add this socket timeout.
+ # This is here so there is no chance the timeout out has
+ # been adjusted by other library calls.
+ socket.setdefaulttimeout(SOCKET_TIMEOUT_SECONDS)
+ logging.debug("Uploading imaged %s with glance v2 api"
+ % image_id)
+
+ url = "%(glance_endpoint)s/v2/images/%(image_id)s/file" % {
+ 'glance_endpoint': glance_endpoint,
+ 'image_id': image_id}
+
+ # NOTE(sdague): this is python 2.4, which means urlparse returns a
+ # tuple, not a named tuple.
+ # 0 - scheme
+ # 1 - host:port (aka netloc)
+ # 2 - path
+ parts = urlparse(url)
+
+ try:
+ conn = _create_connection(parts[0], parts[1])
+ except Exception, error:
+ raise RetryableError(error)
+
+ try:
+ _update_image_meta_v2(conn, image_id, extra_headers, properties)
+
+ validate_image_status_before_upload_v2(conn, url, extra_headers)
+
+ try:
+ conn.connect()
+ # NOTE(sirp): httplib under python2.4 won't accept
+ # a file-like object to request
+ conn.putrequest('PUT', parts[2])
+
+ headers = {
+ 'content-type': 'application/octet-stream',
+ 'transfer-encoding': 'chunked'}
+
+ headers.update(**extra_headers)
+
+ for header, value in headers.items():
+ conn.putheader(header, value)
+ conn.endheaders()
+ except Exception, error: # noqa
+ logging.exception('Failed to upload %(url)s' % {'url': url})
+ raise RetryableError(error)
+
+ callback_data = {'bytes_written': 0}
+
+ def send_chunked_transfer_encoded(chunk):
+ chunk_len = len(chunk)
+ callback_data['bytes_written'] += chunk_len
+ try:
+ conn.send("%x\r\n%s\r\n" % (chunk_len, chunk))
+ except Exception, error:
+ logging.exception('Failed to upload when sending chunks')
+ raise RetryableError(error)
+
+ compression_level = properties.get('xenapi_image_compression_level')
+
+ utils.create_tarball(
+ None, staging_path, callback=send_chunked_transfer_encoded,
+ compression_level=compression_level)
+
+ send_chunked_transfer_encoded('') # Chunked-Transfer terminator
+
+ bytes_written = callback_data['bytes_written']
+ logging.info("Wrote %d bytes to %s" % (bytes_written, url))
+
+ resp = conn.getresponse()
+ if resp.status == httplib.NO_CONTENT:
+ return
+
+ logging.error("Unexpected response while writing image data to %s: "
+ "Response Status: %i, Response body: %s"
+ % (url, resp.status, resp.read()))
+
+ check_resp_status_and_retry(resp, image_id, url)
+
+ finally:
+ conn.close()
+
+
def check_resp_status_and_retry(resp, image_id, url):
# Note(Jesse): This branch sorts errors into those that are permanent,
# those that are ephemeral, and those that are unexpected.
@@ -320,7 +479,7 @@ def check_resp_status_and_retry(resp, image_id, url):
% (resp.status, image_id, url))
-def validate_image_status_before_upload(conn, url, extra_headers):
+def validate_image_status_before_upload_v1(conn, url, extra_headers):
try:
parts = urlparse(url)
path = parts[2]
@@ -379,17 +538,71 @@ def validate_image_status_before_upload(conn, url, extra_headers):
'image_status': image_status})
+def validate_image_status_before_upload_v2(conn, url, extra_headers):
+ try:
+ parts = urlparse(url)
+ path = parts[2]
+ image_id = path.split('/')[-2]
+ # NOTE(nikhil): Attempt to determine if the Image has a status
+ # of 'queued'. Because data will continued to be sent to Glance
+ # until it has a chance to check the Image state, discover that
+ # it is not 'active' and send back a 409. Hence, the data will be
+ # unnecessarily buffered by Glance. This wastes time and bandwidth.
+ # LP bug #1202785
+
+ conn.request('GET', '/v2/images/%s' % image_id, headers=extra_headers)
+ get_resp = conn.getresponse()
+ except Exception, error: # noqa
+ logging.exception('Failed to GET the image %(image_id)s while '
+ 'checking image status before attempting to '
+ 'upload %(url)s' % {'image_id': image_id,
+ 'url': url})
+ raise RetryableError(error)
+
+ if get_resp.status != httplib.OK:
+ logging.error("Unexpected response while doing a GET call "
+ "to image %s , url = %s , Response Status: "
+ "%i" % (image_id, url, get_resp.status))
+
+ check_resp_status_and_retry(get_resp, image_id, url)
+
+ else:
+ body = json.loads(get_resp.read())
+ image_status = body['status']
+ if image_status not in ('queued', ):
+ err_msg = ('Cannot upload data for image %(image_id)s as the '
+ 'image status is %(image_status)s' %
+ {'image_id': image_id, 'image_status': image_status})
+ logging.exception(err_msg)
+ raise PluginError("Got Permanent Error while uploading image "
+ "[%s] to glance [%s]. "
+ "Message: %s" % (image_id, url,
+ err_msg))
+ else:
+ logging.info('Found image %(image_id)s in status '
+ '%(image_status)s. Attempting to '
+ 'upload.' % {'image_id': image_id,
+ 'image_status': image_status})
+ get_resp.read()
+
+
def download_vhd2(session, image_id, endpoint,
- uuid_stack, sr_path, extra_headers):
- """Download an image from Glance, unbundle it, and then deposit the VHDs
- into the storage repository
+ uuid_stack, sr_path, extra_headers, api_version=1):
+ """Download an image from Glance v2, unbundle it, and then deposit the
+ VHDs into the storage repository.
"""
staging_path = utils.make_staging_area(sr_path)
try:
# Download tarball into staging area and extract it
- _download_tarball_by_url(
- sr_path, staging_path, image_id,
- endpoint, extra_headers)
+ # TODO(mfedosin): remove this check when v1 is deprecated.
+ if api_version == 1:
+ _download_tarball_by_url_v1(
+ sr_path, staging_path, image_id,
+ endpoint, extra_headers)
+ else:
+ _download_tarball_by_url_v2(
+ sr_path, staging_path, image_id,
+ endpoint, extra_headers)
# Move the VHDs from the staging area into the storage repository
return utils.import_vhds(sr_path, staging_path, uuid_stack)
@@ -397,15 +610,21 @@ def download_vhd2(session, image_id, endpoint,
utils.cleanup_staging_area(staging_path)
-def upload_vhd2(session, vdi_uuids, image_id,
- endpoint, sr_path, extra_headers, properties):
- """Bundle the VHDs comprising an image and then stream them into Glance.
+def upload_vhd2(session, vdi_uuids, image_id, endpoint, sr_path,
+ extra_headers, properties, api_version=1):
+ """Bundle the VHDs comprising an image and then stream them into
+ Glance.
"""
staging_path = utils.make_staging_area(sr_path)
try:
utils.prepare_staging_area(sr_path, staging_path, vdi_uuids)
- _upload_tarball_by_url(staging_path, image_id,
- endpoint, extra_headers, properties)
+ # TODO(mfedosin): remove this check when v1 is deprecated.
+ if api_version == 1:
+ _upload_tarball_by_url_v1(staging_path, image_id,
+ endpoint, extra_headers, properties)
+ else:
+ _upload_tarball_by_url_v2(staging_path, image_id,
+ endpoint, extra_headers, properties)
finally:
utils.cleanup_staging_area(staging_path)
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version b/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version
index 8ef21eaaa5..bec3a96f88 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version
@@ -29,7 +29,8 @@ import utils
# 1.1 - New call to check GC status
# 1.2 - Added support for pci passthrough devices
# 1.3 - Add vhd2 functions for doing glance operations by url
-PLUGIN_VERSION = "1.3"
+# 1.4 - Add support of Glance v2 api
+PLUGIN_VERSION = "1.4"
def get_version(session):