88
def put_vdis(session, args):
65
def _download_tarball(sr_path, staging_path, image_id, glance_host,
67
"""Download the tarball image from Glance and extract it into the staging
70
conn = httplib.HTTPConnection(glance_host, glance_port)
71
conn.request('GET', '/images/%s' % image_id)
72
resp = conn.getresponse()
73
if resp.status == httplib.NOT_FOUND:
74
raise Exception("Image '%s' not found in Glance" % image_id)
75
elif resp.status != httplib.OK:
76
raise Exception("Unexpected response from Glance %i" % res.status)
78
tar_cmd = "tar -zx --directory=%(staging_path)s" % locals()
79
tar_proc = _make_subprocess(tar_cmd, stderr=True, stdin=True)
81
chunk = resp.read(CHUNK_SIZE)
83
tar_proc.stdin.write(chunk)
84
chunk = resp.read(CHUNK_SIZE)
86
_finish_subprocess(tar_proc, tar_cmd)
90
def _fixup_vhds(sr_path, staging_path, uuid_stack):
91
"""Fixup the downloaded VHDs before we move them into the SR.
93
We cannot extract VHDs directly into the SR since they don't yet have
94
UUIDs, aren't properly associated with each other, and would be subject to
95
a race-condition of one-file being present and the other not being
98
To avoid these we problems, we use a staging area to fixup the VHDs before
99
moving them into the SR. The steps involved are:
101
1. Extracting tarball into staging area
103
2. Renaming VHDs to use UUIDs ('snap.vhd' -> 'ffff-aaaa-...vhd')
105
3. Linking the two VHDs together
107
4. Pseudo-atomically moving the images into the SR. (It's not really
108
atomic because it takes place as two os.rename operations; however,
109
the chances of an SR.scan occuring between the two rename()
110
invocations is so small that we can safely ignore it)
112
def rename_with_uuid(orig_path):
113
"""Rename VHD using UUID so that it will be recognized by SR on a
116
Since Python2.4 doesn't have the `uuid` module, we pass a stack of
117
pre-computed UUIDs from the compute worker.
119
orig_dirname = os.path.dirname(orig_path)
120
uuid = uuid_stack.pop()
121
new_path = os.path.join(orig_dirname, "%s.vhd" % uuid)
122
os.rename(orig_path, new_path)
123
return new_path, uuid
125
def link_vhds(child_path, parent_path):
126
"""Use vhd-util to associate the snapshot VHD with its base_copy.
128
This needs to be done before we move both VHDs into the SR to prevent
129
the base_copy from being DOA (deleted-on-arrival).
131
modify_cmd = ("vhd-util modify -n %(child_path)s -p %(parent_path)s"
133
modify_proc = _make_subprocess(modify_cmd, stderr=True)
134
_finish_subprocess(modify_proc, modify_cmd)
136
def move_into_sr(orig_path):
137
"""Move a file into the SR"""
138
filename = os.path.basename(orig_path)
139
new_path = os.path.join(sr_path, filename)
140
os.rename(orig_path, new_path)
143
def assert_vhd_not_hidden(path):
145
This is a sanity check on the image; if a snap.vhd isn't
146
present, then the image.vhd better not be marked 'hidden' or it will
147
be deleted when moved into the SR.
149
query_cmd = "vhd-util query -n %(path)s -f" % locals()
150
query_proc = _make_subprocess(query_cmd, stdout=True, stderr=True)
151
out, err = _finish_subprocess(query_proc, query_cmd)
153
for line in out.splitlines():
154
if line.startswith('hidden'):
155
value = line.split(':')[1].strip()
158
"VHD %(path)s is marked as hidden without child" %
161
orig_base_copy_path = os.path.join(staging_path, 'image.vhd')
162
if not os.path.exists(orig_base_copy_path):
163
raise Exception("Invalid image: image.vhd not present")
165
base_copy_path, base_copy_uuid = rename_with_uuid(orig_base_copy_path)
167
vdi_uuid = base_copy_uuid
168
orig_snap_path = os.path.join(staging_path, 'snap.vhd')
169
if os.path.exists(orig_snap_path):
170
snap_path, snap_uuid = rename_with_uuid(orig_snap_path)
172
# NOTE(sirp): this step is necessary so that an SR scan won't
173
# delete the base_copy out from under us (since it would be
175
link_vhds(snap_path, base_copy_path)
176
move_into_sr(snap_path)
178
assert_vhd_not_hidden(base_copy_path)
180
move_into_sr(base_copy_path)
184
def _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids):
185
"""Hard-link VHDs into staging area with appropriate filename
186
('snap' or 'image.vhd')
188
for name, uuid in vdi_uuids.items():
189
source = os.path.join(sr_path, "%s.vhd" % uuid)
190
link_name = os.path.join(staging_path, "%s.vhd" % name)
191
os.link(source, link_name)
194
def _upload_tarball(staging_path, image_id, glance_host, glance_port):
196
Create a tarball of the image and then stream that into Glance
197
using chunked-transfer-encoded HTTP.
199
conn = httplib.HTTPConnection(glance_host, glance_port)
200
# NOTE(sirp): httplib under python2.4 won't accept a file-like object
202
conn.putrequest('PUT', '/images/%s' % image_id)
204
# TODO(sirp): make `store` configurable
206
'content-type': 'application/octet-stream',
207
'transfer-encoding': 'chunked',
208
'x-image-meta-is_public': 'True',
209
'x-image-meta-status': 'queued',
210
'x-image-meta-type': 'vhd'}
211
for header, value in headers.iteritems():
212
conn.putheader(header, value)
215
tar_cmd = "tar -zc --directory=%(staging_path)s ." % locals()
216
tar_proc = _make_subprocess(tar_cmd, stdout=True, stderr=True)
218
chunk = tar_proc.stdout.read(CHUNK_SIZE)
220
conn.send("%x\r\n%s\r\n" % (len(chunk), chunk))
221
chunk = tar_proc.stdout.read(CHUNK_SIZE)
222
conn.send("0\r\n\r\n")
224
_finish_subprocess(tar_proc, tar_cmd)
226
resp = conn.getresponse()
227
if resp.status != httplib.OK:
228
raise Exception("Unexpected response from Glance %i" % resp.status)
232
def _make_staging_area(sr_path):
234
The staging area is a place where we can temporarily store and
235
manipulate VHDs. The use of the staging area is different for upload and
241
When we download the tarball, the VHDs contained within will have names
242
like "snap.vhd" and "image.vhd". We need to assign UUIDs to them before
243
moving them into the SR. However, since 'image.vhd' may be a base_copy, we
244
need to link it to 'snap.vhd' (using vhd-util modify) before moving both
245
into the SR (otherwise the SR.scan will cause 'image.vhd' to be deleted).
246
The staging area gives us a place to perform these operations before they
247
are moved to the SR, scanned, and then registered with XenServer.
252
On upload, we want to rename the VHDs to reflect what they are, 'snap.vhd'
253
in the case of the snapshot VHD, and 'image.vhd' in the case of the
254
base_copy. The staging area provides a directory in which we can create
255
hard-links to rename the VHDs without affecting what's in the SR.
261
The staging area is created as a subdirectory within the SR in order to
262
guarantee that it resides within the same filesystem and therefore permit
263
hard-linking and cheap file moves.
265
staging_path = tempfile.mkdtemp(dir=sr_path)
269
def _cleanup_staging_area(staging_path):
270
"""Remove staging area directory
272
On upload, the staging area contains hard-links to the VHDs in the SR;
273
it's safe to remove the staging-area because the SR will keep the link
274
count > 0 (so the VHDs in the SR will not be deleted).
276
shutil.rmtree(staging_path)
279
def _make_subprocess(cmdline, stdout=False, stderr=False, stdin=False):
280
"""Make a subprocess according to the given command-line string
283
kwargs['stdout'] = stdout and subprocess.PIPE or None
284
kwargs['stderr'] = stderr and subprocess.PIPE or None
285
kwargs['stdin'] = stdin and subprocess.PIPE or None
286
args = shlex.split(cmdline)
287
proc = subprocess.Popen(args, **kwargs)
291
def _finish_subprocess(proc, cmdline):
292
"""Ensure that the process returned a zero exit code indicating success
294
out, err = proc.communicate()
295
ret = proc.returncode
297
raise Exception("'%(cmdline)s' returned non-zero exit code: "
298
"retcode=%(ret)i, stderr='%(err)s'" % locals())
302
def download_vhd(session, args):
303
"""Download an image from Glance, unbundle it, and then deposit the VHDs
304
into the storage repository
306
params = pickle.loads(exists(args, 'params'))
307
image_id = params["image_id"]
308
glance_host = params["glance_host"]
309
glance_port = params["glance_port"]
310
uuid_stack = params["uuid_stack"]
311
sr_path = params["sr_path"]
313
staging_path = _make_staging_area(sr_path)
315
_download_tarball(sr_path, staging_path, image_id, glance_host,
317
vdi_uuid = _fixup_vhds(sr_path, staging_path, uuid_stack)
320
_cleanup_staging_area(staging_path)
323
def upload_vhd(session, args):
324
"""Bundle the VHDs comprising an image and then stream them into Glance.
89
326
params = pickle.loads(exists(args, 'params'))
90
327
vdi_uuids = params["vdi_uuids"]
91
328
image_id = params["image_id"]
92
329
glance_host = params["glance_host"]
93
330
glance_port = params["glance_port"]
95
sr_path = get_sr_path(session)
96
#FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs
97
tmp_file = "%s.tar.gz" % os.path.join('/tmp', str(image_id))
98
tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path]
99
paths = ["%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids]
100
tar_cmd.extend(paths)
101
logging.debug("Bundling image with cmd: %s", tar_cmd)
102
subprocess.call(tar_cmd)
103
logging.debug("Writing to test file %s", tmp_file)
104
put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port)
105
# FIXME(sirp): return anything useful here?
109
def put_bundle_in_glance(tmp_file, image_id, glance_host, glance_port):
110
size = os.path.getsize(tmp_file)
111
basename = os.path.basename(tmp_file)
113
bundle = open(tmp_file, 'r')
331
sr_path = params["sr_path"]
333
staging_path = _make_staging_area(sr_path)
116
'x-image-meta-store': 'file',
117
'x-image-meta-is_public': 'True',
118
'x-image-meta-type': 'raw',
119
'x-image-meta-size': size,
120
'content-length': size,
121
'content-type': 'application/octet-stream',
123
conn = httplib.HTTPConnection(glance_host, glance_port)
124
#NOTE(sirp): httplib under python2.4 won't accept a file-like object
126
conn.putrequest('PUT', '/images/%s' % image_id)
128
for header, value in headers.iteritems():
129
conn.putheader(header, value)
132
chunk = bundle.read(CHUNK_SIZE)
135
chunk = bundle.read(CHUNK_SIZE)
137
res = conn.getresponse()
138
#FIXME(sirp): should this be 201 Created?
139
if res.status != httplib.OK:
140
raise Exception("Unexpected response from Glance %i" % res.status)
335
_prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids)
336
_upload_tarball(staging_path, image_id, glance_host, glance_port)
145
def get_sr_path(session):
146
sr_ref = find_sr(session)
149
raise Exception('Cannot find SR to read VDI from')
151
sr_rec = session.xenapi.SR.get_record(sr_ref)
152
sr_uuid = sr_rec["uuid"]
153
sr_path = os.path.join(FILE_SR_PATH, sr_uuid)
157
#TODO(sirp): both objectstore and glance need this, should this be refactored
159
def find_sr(session):
160
host = get_this_host(session)
161
srs = session.xenapi.SR.get_all()
163
sr_rec = session.xenapi.SR.get_record(sr)
164
if not ('i18n-key' in sr_rec['other_config'] and
165
sr_rec['other_config']['i18n-key'] == 'local-storage'):
167
for pbd in sr_rec['PBDs']:
168
pbd_rec = session.xenapi.PBD.get_record(pbd)
169
if pbd_rec['host'] == host:
338
_cleanup_staging_area(staging_path)
340
return "" # Nothing useful to return on an upload
343
def copy_kernel_vdi(session, args):
344
vdi = exists(args, 'vdi-ref')
345
size = exists(args, 'image-size')
346
#Use the uuid as a filename
347
vdi_uuid = session.xenapi.VDI.get_uuid(vdi)
348
copy_args = {'vdi_uuid': vdi_uuid, 'vdi_size': int(size)}
349
filename = with_vdi_in_dom0(session, vdi, False,
351
_copy_kernel_vdi('/dev/%s' % dev, copy_args))
355
def remove_kernel_ramdisk(session, args):
356
"""Removes kernel and/or ramdisk from dom0's file system"""
357
kernel_file = exists(args, 'kernel-file')
358
ramdisk_file = exists(args, 'ramdisk-file')
360
os.remove(kernel_file)
362
os.remove(ramdisk_file)
174
366
if __name__ == '__main__':
175
XenAPIPlugin.dispatch({'put_vdis': put_vdis,
367
XenAPIPlugin.dispatch({'upload_vhd': upload_vhd,
368
'download_vhd': download_vhd,
176
369
'copy_kernel_vdi': copy_kernel_vdi,
177
370
'remove_kernel_ramdisk': remove_kernel_ramdisk})