/
glance
executable file
·261 lines (213 loc) · 8.83 KB
/
glance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#!/usr/bin/env python
# Copyright (c) 2012 OpenStack, LLC
# Copyright (c) 2010 Citrix Systems, Inc.
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Handle the uploading and downloading of images via Glance."""
import cPickle as pickle
import httplib
try:
import json
except ImportError:
import simplejson as json
import md5
import os
import shutil
import urllib2
import XenAPIPlugin
import utils
#FIXME(sirp): should this use pluginlib from 5.6?
from pluginlib_nova import *
configure_logging('glance')
class RetryableError(Exception):
pass
def _download_tarball_and_verify(request, staging_path):
try:
response = urllib2.urlopen(request)
except urllib2.HTTPError, error:
raise RetryableError(error)
except urllib2.URLError, error:
raise RetryableError(error)
except httplib.HTTPException, error:
# httplib.HTTPException and derivatives (BadStatusLine in particular)
# don't have a useful __repr__ or __str__
raise RetryableError('%s: %s' % (error.__class__.__name__, error))
url = request.get_full_url()
logging.info("Reading image data from %s" % url)
callback_data = {'bytes_read': 0}
checksum = md5.new()
def update_md5(chunk):
callback_data['bytes_read'] += len(chunk)
checksum.update(chunk)
try:
try:
utils.extract_tarball(response, staging_path, callback=update_md5)
except Exception, error:
raise RetryableError(error)
finally:
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
etag = response.info().getheader('etag', None)
if etag is None:
etag = response.info().getheader('x-image-meta-checksum', None)
# Verify checksum using ETag
checksum = checksum.hexdigest()
if etag is None:
msg = "No ETag found for comparison to checksum %(checksum)s"
logging.info(msg % locals())
elif checksum != etag:
msg = 'ETag %(etag)s does not match computed md5sum %(checksum)s'
raise RetryableError(msg % locals())
else:
msg = "Verified image checksum %(checksum)s"
logging.info(msg % locals())
def _download_tarball(sr_path, staging_path, image_id, glance_host,
glance_port, glance_use_ssl, auth_token):
"""Download the tarball image from Glance and extract it into the staging
area. Retry if there is any failure.
"""
# Build request headers
headers = {}
if auth_token:
headers['x-auth-token'] = auth_token
if glance_use_ssl:
scheme = 'https'
else:
scheme = 'http'
url = ("%(scheme)s://%(glance_host)s:%(glance_port)d/v1/images/"
"%(image_id)s" % locals())
logging.info("Downloading %s" % url)
request = urllib2.Request(url, headers=headers)
try:
_download_tarball_and_verify(request, staging_path)
except Exception:
logging.exception('Failed to retrieve %(url)s' % locals())
raise
def _upload_tarball(staging_path, image_id, glance_host, glance_port,
glance_use_ssl, auth_token, properties):
"""
Create a tarball of the image and then stream that into Glance
using chunked-transfer-encoded HTTP.
"""
if glance_use_ssl:
scheme = 'https'
else:
scheme = 'http'
url = '%s://%s:%s/v1/images/%s' % (scheme, glance_host, glance_port,
image_id)
logging.info("Writing image data to %s" % url)
if glance_use_ssl:
conn = httplib.HTTPSConnection(glance_host, glance_port)
else:
conn = httplib.HTTPConnection(glance_host, glance_port)
# NOTE(sirp): httplib under python2.4 won't accept a file-like object
# to request
conn.putrequest('PUT', '/v1/images/%s' % image_id)
# 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.
# NOTE(dprince): In order to preserve existing Glance properties
# we set X-Glance-Registry-Purge-Props on this request.
headers = {
'content-type': 'application/octet-stream',
'transfer-encoding': 'chunked',
'x-image-meta-is-public': 'False',
'x-image-meta-status': 'queued',
'x-image-meta-disk-format': 'vhd',
'x-image-meta-container-format': 'ovf',
'x-glance-registry-purge-props': 'False'}
# If we have an auth_token, set an x-auth-token header
if auth_token:
headers['x-auth-token'] = auth_token
for key, value in properties.iteritems():
header_key = "x-image-meta-property-%s" % key.replace('_', '-')
headers[header_key] = str(value)
for header, value in headers.iteritems():
conn.putheader(header, value)
conn.endheaders()
callback_data = {'bytes_written': 0}
def send_chunked_transfer_encoded(chunk):
chunk_len = len(chunk)
callback_data['bytes_written'] += chunk_len
conn.send("%x\r\n%s\r\n" % (chunk_len, chunk))
utils.create_tarball(
None, staging_path, callback=send_chunked_transfer_encoded)
conn.send("0\r\n\r\n") # 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.OK:
logging.error("Unexpected response while writing image data to %s: "
"Response Status: %i, Response body: %s"
% (url, resp.status, resp.read()))
raise Exception("Unexpected response [%i] while uploading image [%s] "
"to glance host [%s:%s]"
% (resp.status, image_id, glance_host, glance_port))
conn.close()
def download_vhd(session, args):
"""Download an image from Glance, unbundle it, and then deposit the VHDs
into the storage repository
"""
params = pickle.loads(exists(args, 'params'))
image_id = params["image_id"]
glance_host = params["glance_host"]
glance_port = params["glance_port"]
glance_use_ssl = params["glance_use_ssl"]
uuid_stack = params["uuid_stack"]
sr_path = params["sr_path"]
auth_token = params["auth_token"]
staging_path = utils.make_staging_area(sr_path)
try:
# Download tarball into staging area and extract it
_download_tarball(
sr_path, staging_path, image_id, glance_host, glance_port,
glance_use_ssl, auth_token)
# Move the VHDs from the staging area into the storage repository
imported_vhds = utils.import_vhds(sr_path, staging_path, uuid_stack)
finally:
utils.cleanup_staging_area(staging_path)
# Right now, it's easier to return a single string via XenAPI,
# so we'll json encode the list of VHDs.
return json.dumps(imported_vhds)
def upload_vhd(session, args):
"""Bundle the VHDs comprising an image and then stream them into Glance.
"""
params = pickle.loads(exists(args, 'params'))
vdi_uuids = params["vdi_uuids"]
image_id = params["image_id"]
glance_host = params["glance_host"]
glance_port = params["glance_port"]
glance_use_ssl = params["glance_use_ssl"]
sr_path = params["sr_path"]
auth_token = params["auth_token"]
properties = params["properties"]
staging_path = utils.make_staging_area(sr_path)
try:
utils.prepare_staging_area(sr_path, staging_path, vdi_uuids)
_upload_tarball(staging_path, image_id, glance_host, glance_port,
glance_use_ssl, auth_token, properties)
finally:
utils.cleanup_staging_area(staging_path)
return "" # Nothing useful to return on an upload
if __name__ == '__main__':
XenAPIPlugin.dispatch({'upload_vhd': upload_vhd,
'download_vhd': download_vhd})