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:
parent
0a533df573
commit
78fbb4776e
@ -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',
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
_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,
|
||||
_upload_tarball_by_url_v1(staging_path, image_id, url,
|
||||
extra_headers, properties)
|
||||
|
||||
|
||||
def _upload_tarball_by_url(staging_path, image_id, glance_endpoint,
|
||||
def _upload_tarball_by_url_v1(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
|
||||
"""
|
||||
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,15 +538,69 @@ 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(
|
||||
# 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)
|
||||
|
||||
@ -397,14 +610,20 @@ 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,
|
||||
# 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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user