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
This commit is contained in:
Mike Fedosin 2016-06-02 16:41:58 +03:00 committed by Sudipta Biswas
parent 0a533df573
commit 78fbb4776e
6 changed files with 266 additions and 43 deletions

View File

@ -36,6 +36,7 @@ class TestGlanceStore(stubs.XenAPITestBaseNoDB):
self.store = glance.GlanceStore()
self.flags(api_servers=['http://localhost:9292'], group='glance')
self.flags(use_glance_v1=True, group='glance')
self.flags(connection_url='test_url',
connection_password='test_pass',
group='xenserver')
@ -61,6 +62,7 @@ class TestGlanceStore(stubs.XenAPITestBaseNoDB):
return {'image_id': 'fake_image_uuid',
'endpoint': 'http://localhost:9292',
'sr_path': '/fake/sr/path',
'api_version': 1,
'extra_headers': {'X-Auth-Token': 'foobar',
'X-Roles': '',
'X-Tenant-Id': 'project',

View File

@ -69,7 +69,7 @@ class XenAPISession(object):
# changed in development environments.
# MAJOR VERSION: Incompatible changes with the plugins
# MINOR VERSION: Compatible changes, new plguins, etc
PLUGIN_REQUIRED_VERSION = '1.3'
PLUGIN_REQUIRED_VERSION = '1.4'
def __init__(self, url, user, pw):
version_string = version.version_string_with_package()

View File

@ -760,7 +760,7 @@ class SessionBase(object):
return base64.b64encode(zlib.compress("dom_id: %s" % dom_id))
def _plugin_nova_plugin_version_get_version(self, method, args):
return pickle.dumps("1.3")
return pickle.dumps("1.4")
def _plugin_xenhost_query_gc(self, method, args):
return pickle.dumps("False")

View File

@ -37,6 +37,7 @@ class GlanceStore(object):
def pick_glance(kwargs):
server = next(glance_api_servers)
kwargs['endpoint'] = server
kwargs['api_version'] = 1 if CONF.glance.use_glance_v1 else 2
# NOTE(sdague): is the return significant here at all?
return server

View File

@ -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,8 +102,11 @@ 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)
@ -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)

View File

@ -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):