~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to plugins/xenserver/xenapi/etc/xapi.d/plugins/glance

  • Committer: Justin Santa Barbara
  • Date: 2011-03-07 22:37:27 UTC
  • mfrom: (700.14.10 servicify-nova-api)
  • Revision ID: justin@fathomdb.com-20110307223727-lwm6854hqe9jzvsz
Merged with pre-req branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
# XenAPI plugin for managing glance images
22
22
#
23
23
 
24
 
import base64
25
 
import errno
26
 
import hmac
27
24
import httplib
28
25
import os
29
26
import os.path
30
27
import pickle
31
 
import sha
 
28
import shlex
 
29
import shutil
32
30
import subprocess
33
 
import time
34
 
import urlparse
 
31
import tempfile
35
32
 
36
33
import XenAPIPlugin
37
34
 
41
38
 
42
39
CHUNK_SIZE = 8192
43
40
KERNEL_DIR = '/boot/guest'
44
 
FILE_SR_PATH = '/var/run/sr-mount'
45
 
 
46
 
 
47
 
def remove_kernel_ramdisk(session, args):
48
 
    """Removes kernel and/or ramdisk from dom0's file system"""
49
 
    kernel_file = exists(args, 'kernel-file')
50
 
    ramdisk_file = exists(args, 'ramdisk-file')
51
 
    if kernel_file:
52
 
        os.remove(kernel_file)
53
 
    if ramdisk_file:
54
 
        os.remove(ramdisk_file)
55
 
    return "ok"
56
 
 
57
 
 
58
 
def copy_kernel_vdi(session, args):
59
 
    vdi = exists(args, 'vdi-ref')
60
 
    size = exists(args, 'image-size')
61
 
    #Use the uuid as a filename
62
 
    vdi_uuid = session.xenapi.VDI.get_uuid(vdi)
63
 
    copy_args = {'vdi_uuid': vdi_uuid, 'vdi_size': int(size)}
64
 
    filename = with_vdi_in_dom0(session, vdi, False,
65
 
                              lambda dev:
66
 
                              _copy_kernel_vdi('/dev/%s' % dev, copy_args))
67
 
    return filename
68
41
 
69
42
 
70
43
def _copy_kernel_vdi(dest, copy_args):
73
46
    logging.debug("copying kernel/ramdisk file from %s to /boot/guest/%s",
74
47
                  dest, vdi_uuid)
75
48
    filename = KERNEL_DIR + '/' + vdi_uuid
 
49
    #make sure KERNEL_DIR exists, otherwise create it
 
50
    if not os.path.isdir(KERNEL_DIR):
 
51
        logging.debug("Creating directory %s", KERNEL_DIR)
 
52
        os.makedirs(KERNEL_DIR)
76
53
    #read data from /dev/ and write into a file on /boot/guest
77
54
    of = open(filename, 'wb')
78
55
    f = open(dest, 'rb')
85
62
    return filename
86
63
 
87
64
 
88
 
def put_vdis(session, args):
 
65
def _download_tarball(sr_path, staging_path, image_id, glance_host,
 
66
                      glance_port):
 
67
    """Download the tarball image from Glance and extract it into the staging
 
68
    area.
 
69
    """
 
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)
 
77
 
 
78
    tar_cmd = "tar -zx --directory=%(staging_path)s" % locals()
 
79
    tar_proc = _make_subprocess(tar_cmd, stderr=True, stdin=True)
 
80
 
 
81
    chunk = resp.read(CHUNK_SIZE)
 
82
    while chunk:
 
83
        tar_proc.stdin.write(chunk)
 
84
        chunk = resp.read(CHUNK_SIZE)
 
85
 
 
86
    _finish_subprocess(tar_proc, tar_cmd)
 
87
    conn.close()
 
88
 
 
89
 
 
90
def _fixup_vhds(sr_path, staging_path, uuid_stack):
 
91
    """Fixup the downloaded VHDs before we move them into the SR.
 
92
 
 
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
 
96
    downloaded yet.
 
97
 
 
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:
 
100
 
 
101
        1. Extracting tarball into staging area
 
102
 
 
103
        2. Renaming VHDs to use UUIDs ('snap.vhd' -> 'ffff-aaaa-...vhd')
 
104
 
 
105
        3. Linking the two VHDs together
 
106
 
 
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)
 
111
    """
 
112
    def rename_with_uuid(orig_path):
 
113
        """Rename VHD using UUID so that it will be recognized by SR on a
 
114
        subsequent scan.
 
115
 
 
116
        Since Python2.4 doesn't have the `uuid` module, we pass a stack of
 
117
        pre-computed UUIDs from the compute worker.
 
118
        """
 
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
 
124
 
 
125
    def link_vhds(child_path, parent_path):
 
126
        """Use vhd-util to associate the snapshot VHD with its base_copy.
 
127
 
 
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).
 
130
        """
 
131
        modify_cmd = ("vhd-util modify -n %(child_path)s -p %(parent_path)s"
 
132
                      % locals())
 
133
        modify_proc = _make_subprocess(modify_cmd, stderr=True)
 
134
        _finish_subprocess(modify_proc, modify_cmd)
 
135
 
 
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)
 
141
        return new_path
 
142
 
 
143
    def assert_vhd_not_hidden(path):
 
144
        """
 
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.
 
148
        """
 
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)
 
152
 
 
153
        for line in out.splitlines():
 
154
            if line.startswith('hidden'):
 
155
                value = line.split(':')[1].strip()
 
156
                if value == "1":
 
157
                    raise Exception(
 
158
                        "VHD %(path)s is marked as hidden without child" %
 
159
                        locals())
 
160
 
 
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")
 
164
 
 
165
    base_copy_path, base_copy_uuid = rename_with_uuid(orig_base_copy_path)
 
166
 
 
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)
 
171
        vdi_uuid = snap_uuid
 
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
 
174
        # orphaned)
 
175
        link_vhds(snap_path, base_copy_path)
 
176
        move_into_sr(snap_path)
 
177
    else:
 
178
        assert_vhd_not_hidden(base_copy_path)
 
179
 
 
180
    move_into_sr(base_copy_path)
 
181
    return vdi_uuid
 
182
 
 
183
 
 
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')
 
187
    """
 
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)
 
192
 
 
193
 
 
194
def _upload_tarball(staging_path, image_id, glance_host, glance_port):
 
195
    """
 
196
    Create a tarball of the image and then stream that into Glance
 
197
    using chunked-transfer-encoded HTTP.
 
198
    """
 
199
    conn = httplib.HTTPConnection(glance_host, glance_port)
 
200
    # NOTE(sirp): httplib under python2.4 won't accept a file-like object
 
201
    # to request
 
202
    conn.putrequest('PUT', '/images/%s' % image_id)
 
203
 
 
204
    # TODO(sirp): make `store` configurable
 
205
    headers = {
 
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)
 
213
    conn.endheaders()
 
214
 
 
215
    tar_cmd = "tar -zc --directory=%(staging_path)s ." % locals()
 
216
    tar_proc = _make_subprocess(tar_cmd, stdout=True, stderr=True)
 
217
 
 
218
    chunk = tar_proc.stdout.read(CHUNK_SIZE)
 
219
    while chunk:
 
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")
 
223
 
 
224
    _finish_subprocess(tar_proc, tar_cmd)
 
225
 
 
226
    resp = conn.getresponse()
 
227
    if resp.status != httplib.OK:
 
228
        raise Exception("Unexpected response from Glance %i" % resp.status)
 
229
    conn.close()
 
230
 
 
231
 
 
232
def _make_staging_area(sr_path):
 
233
    """
 
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
 
236
    download:
 
237
 
 
238
    Download
 
239
    ========
 
240
 
 
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.
 
248
 
 
249
    Upload
 
250
    ======
 
251
 
 
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.
 
256
 
 
257
 
 
258
    NOTE
 
259
    ====
 
260
 
 
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.
 
264
    """
 
265
    staging_path = tempfile.mkdtemp(dir=sr_path)
 
266
    return staging_path
 
267
 
 
268
 
 
269
def _cleanup_staging_area(staging_path):
 
270
    """Remove staging area directory
 
271
 
 
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).
 
275
    """
 
276
    shutil.rmtree(staging_path)
 
277
 
 
278
 
 
279
def _make_subprocess(cmdline, stdout=False, stderr=False, stdin=False):
 
280
    """Make a subprocess according to the given command-line string
 
281
    """
 
282
    kwargs = {}
 
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)
 
288
    return proc
 
289
 
 
290
 
 
291
def _finish_subprocess(proc, cmdline):
 
292
    """Ensure that the process returned a zero exit code indicating success
 
293
    """
 
294
    out, err = proc.communicate()
 
295
    ret = proc.returncode
 
296
    if ret != 0:
 
297
        raise Exception("'%(cmdline)s' returned non-zero exit code: "
 
298
                        "retcode=%(ret)i,  stderr='%(err)s'" % locals())
 
299
    return out, err
 
300
 
 
301
 
 
302
def download_vhd(session, args):
 
303
    """Download an image from Glance, unbundle it, and then deposit the VHDs
 
304
    into the storage repository
 
305
    """
 
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"]
 
312
 
 
313
    staging_path = _make_staging_area(sr_path)
 
314
    try:
 
315
        _download_tarball(sr_path, staging_path, image_id, glance_host,
 
316
                          glance_port)
 
317
        vdi_uuid = _fixup_vhds(sr_path, staging_path, uuid_stack)
 
318
        return vdi_uuid
 
319
    finally:
 
320
        _cleanup_staging_area(staging_path)
 
321
 
 
322
 
 
323
def upload_vhd(session, args):
 
324
    """Bundle the VHDs comprising an image and then stream them into Glance.
 
325
    """
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"]
94
 
 
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?
106
 
    return ""
107
 
 
108
 
 
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)
112
 
 
113
 
    bundle = open(tmp_file, 'r')
 
331
    sr_path = params["sr_path"]
 
332
 
 
333
    staging_path = _make_staging_area(sr_path)
114
334
    try:
115
 
        headers = {
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',
122
 
         }
123
 
        conn = httplib.HTTPConnection(glance_host, glance_port)
124
 
        #NOTE(sirp): httplib under python2.4 won't accept a file-like object
125
 
        # to request
126
 
        conn.putrequest('PUT', '/images/%s' % image_id)
127
 
 
128
 
        for header, value in headers.iteritems():
129
 
            conn.putheader(header, value)
130
 
        conn.endheaders()
131
 
 
132
 
        chunk = bundle.read(CHUNK_SIZE)
133
 
        while chunk:
134
 
            conn.send(chunk)
135
 
            chunk = bundle.read(CHUNK_SIZE)
136
 
 
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)
141
337
    finally:
142
 
        bundle.close()
143
 
 
144
 
 
145
 
def get_sr_path(session):
146
 
    sr_ref = find_sr(session)
147
 
 
148
 
    if sr_ref is None:
149
 
        raise Exception('Cannot find SR to read VDI from')
150
 
 
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)
154
 
    return sr_path
155
 
 
156
 
 
157
 
#TODO(sirp): both objectstore and glance need this, should this be refactored
158
 
#into common lib
159
 
def find_sr(session):
160
 
    host = get_this_host(session)
161
 
    srs = session.xenapi.SR.get_all()
162
 
    for sr in srs:
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'):
166
 
            continue
167
 
        for pbd in sr_rec['PBDs']:
168
 
            pbd_rec = session.xenapi.PBD.get_record(pbd)
169
 
            if pbd_rec['host'] == host:
170
 
                return sr
171
 
    return None
 
338
        _cleanup_staging_area(staging_path)
 
339
 
 
340
    return ""  # Nothing useful to return on an upload
 
341
 
 
342
 
 
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,
 
350
                                lambda dev:
 
351
                               _copy_kernel_vdi('/dev/%s' % dev, copy_args))
 
352
    return filename
 
353
 
 
354
 
 
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')
 
359
    if kernel_file:
 
360
        os.remove(kernel_file)
 
361
    if ramdisk_file:
 
362
        os.remove(ramdisk_file)
 
363
    return "ok"
172
364
 
173
365
 
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})