373 lines
13 KiB
Python
373 lines
13 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (C) 2012 Yahoo! Inc. 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.
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import re
|
|
import tarfile
|
|
import urllib2
|
|
import urlparse
|
|
|
|
from devstack import downloader as down
|
|
from devstack import log
|
|
from devstack import shell as sh
|
|
from devstack import utils
|
|
|
|
from devstack.components import keystone
|
|
|
|
LOG = log.getLogger("devstack.image.uploader")
|
|
|
|
# These are used when looking inside archives
|
|
KERNEL_FN_MATCH = re.compile(r"(.*)-vmlinuz$", re.I)
|
|
RAMDISK_FN_MATCH = re.compile(r"(.*)-initrd$", re.I)
|
|
IMAGE_FN_MATCH = re.compile(r"(.*)img$", re.I)
|
|
|
|
# Glance commands
|
|
IMAGE_ADD = ['glance', 'add', '-A', '%TOKEN%',
|
|
'--silent-upload',
|
|
'name="%NAME%"',
|
|
'is_public=true',
|
|
'container_format=%CONTAINER_FORMAT%',
|
|
'disk_format=%DISK_FORMAT%']
|
|
DETAILS_SHOW = ['glance', '-A', '%TOKEN%', 'details']
|
|
|
|
# Extensions that tarfile knows how to work with
|
|
TAR_EXTS = ['.tgz', '.gzip', '.gz', '.bz2', '.tar']
|
|
|
|
# Used to attempt to produce a name for images (to see if we already have it)
|
|
# And to use as the final name...
|
|
# Reverse sorted so that .tar.gz replaces before .tar (and so on)
|
|
NAME_CLEANUPS = [
|
|
'.tar.gz',
|
|
'.img.gz',
|
|
'.qcow2',
|
|
'.img',
|
|
] + TAR_EXTS
|
|
NAME_CLEANUPS.sort()
|
|
NAME_CLEANUPS.reverse()
|
|
|
|
|
|
class Unpacker(object):
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def _unpack_tar(self, file_name, file_location, tmp_dir):
|
|
(root_name, _) = os.path.splitext(file_name)
|
|
kernel_fn = None
|
|
ramdisk_fn = None
|
|
root_img_fn = None
|
|
with contextlib.closing(tarfile.open(file_location, 'r')) as tfh:
|
|
for tmemb in tfh.getmembers():
|
|
fn = tmemb.name
|
|
if KERNEL_FN_MATCH.match(fn):
|
|
kernel_fn = fn
|
|
LOG.debug("Found kernel: %r" % (fn))
|
|
elif RAMDISK_FN_MATCH.match(fn):
|
|
ramdisk_fn = fn
|
|
LOG.debug("Found ram disk: %r" % (fn))
|
|
elif IMAGE_FN_MATCH.match(fn):
|
|
root_img_fn = fn
|
|
LOG.debug("Found root image: %r" % (fn))
|
|
else:
|
|
LOG.debug("Unknown member %r - skipping" % (fn))
|
|
if not root_img_fn:
|
|
msg = "Image %r has no root image member" % (file_name)
|
|
raise RuntimeError(msg)
|
|
extract_dir = sh.joinpths(tmp_dir, root_name)
|
|
sh.mkdir(extract_dir)
|
|
LOG.info("Extracting %r to %r", file_location, extract_dir)
|
|
with contextlib.closing(tarfile.open(file_location, 'r')) as tfh:
|
|
tfh.extractall(extract_dir)
|
|
info = dict()
|
|
if kernel_fn:
|
|
info['kernel'] = {
|
|
'FILE_NAME': sh.joinpths(extract_dir, kernel_fn),
|
|
'DISK_FORMAT': 'aki',
|
|
'CONTAINER_FORMAT': 'aki',
|
|
}
|
|
if ramdisk_fn:
|
|
info['ramdisk'] = {
|
|
'FILE_NAME': sh.joinpths(extract_dir, ramdisk_fn),
|
|
'DISK_FORMAT': 'ari',
|
|
'CONTAINER_FORMAT': 'ari',
|
|
}
|
|
info['FILE_NAME'] = sh.joinpths(extract_dir, root_img_fn)
|
|
info['DISK_FORMAT'] = 'ami'
|
|
info['CONTAINER_FORMAT'] = 'ami'
|
|
return info
|
|
|
|
def unpack(self, file_name, file_location, tmp_dir):
|
|
(_, fn_ext) = os.path.splitext(file_name)
|
|
fn_ext = fn_ext.lower()
|
|
if fn_ext in TAR_EXTS:
|
|
return self._unpack_tar(file_name, file_location, tmp_dir)
|
|
elif fn_ext in ['.img', '.qcow2']:
|
|
info = dict()
|
|
info['FILE_NAME'] = file_location,
|
|
if fn_ext == '.img':
|
|
info['DISK_FORMAT'] = 'raw'
|
|
else:
|
|
info['DISK_FORMAT'] = 'qcow2'
|
|
info['CONTAINER_FORMAT'] = 'bare'
|
|
return info
|
|
else:
|
|
msg = "Currently we do not know how to unpack %r" % (file_name)
|
|
raise NotImplementedError(msg)
|
|
|
|
|
|
class Image(object):
|
|
|
|
def __init__(self, url, token):
|
|
self.url = url
|
|
self.token = token
|
|
self._registry = Registry(token)
|
|
|
|
def _register(self, image_name, location):
|
|
|
|
# Upload the kernel, if we have one
|
|
kernel = location.pop('kernel', None)
|
|
kernel_id = ''
|
|
if kernel:
|
|
LOG.info('Adding kernel %s to glance.', kernel)
|
|
params = dict(kernel)
|
|
params['TOKEN'] = self.token
|
|
params['NAME'] = "%s-vmlinuz" % (image_name)
|
|
cmd = {'cmd': IMAGE_ADD}
|
|
with open(params['FILE_NAME'], 'r') as fh:
|
|
res = utils.execute_template(cmd,
|
|
params=params, stdin_fh=fh,
|
|
close_stdin=True)
|
|
if res:
|
|
(stdout, _) = res[0]
|
|
kernel_id = stdout.split(':')[1].strip()
|
|
|
|
# Upload the ramdisk, if we have one
|
|
initrd = location.pop('ramdisk', None)
|
|
initrd_id = ''
|
|
if initrd:
|
|
LOG.info('Adding ramdisk %s to glance.', initrd)
|
|
params = dict(initrd)
|
|
params['TOKEN'] = self.token
|
|
params['NAME'] = "%s-initrd" % (image_name)
|
|
cmd = {'cmd': IMAGE_ADD}
|
|
with open(params['FILE_NAME'], 'r') as fh:
|
|
res = utils.execute_template(cmd,
|
|
params=params, stdin_fh=fh,
|
|
close_stdin=True)
|
|
if res:
|
|
(stdout, _) = res[0]
|
|
initrd_id = stdout.split(':')[1].strip()
|
|
|
|
# Upload the root, we must have one...
|
|
root_image = dict(location)
|
|
LOG.info('Adding image %s to glance.', root_image)
|
|
add_cmd = list(IMAGE_ADD)
|
|
params = dict(root_image)
|
|
params['TOKEN'] = self.token
|
|
params['NAME'] = image_name
|
|
if kernel_id:
|
|
add_cmd += ['kernel_id=%KERNEL_ID%']
|
|
params['KERNEL_ID'] = kernel_id
|
|
if initrd_id:
|
|
add_cmd += ['ramdisk_id=%INITRD_ID%']
|
|
params['INITRD_ID'] = initrd_id
|
|
cmd = {'cmd': add_cmd}
|
|
with open(params['FILE_NAME'], 'r') as fh:
|
|
res = utils.execute_template(cmd,
|
|
params=params, stdin_fh=fh,
|
|
close_stdin=True)
|
|
img_id = ''
|
|
if res:
|
|
(stdout, _) = res[0]
|
|
img_id = stdout.split(':')[1].strip()
|
|
|
|
return img_id
|
|
|
|
def _generate_img_name(self, url_fn):
|
|
name = url_fn
|
|
for look_for in NAME_CLEANUPS:
|
|
name = name.replace(look_for, '')
|
|
return name
|
|
|
|
def _generate_check_names(self, url_fn):
|
|
name_checks = list()
|
|
name_checks.append(url_fn)
|
|
name_checks.append("%s.img" % (url_fn))
|
|
name_checks.append("%s-img" % (url_fn))
|
|
name = url_fn
|
|
for look_for in NAME_CLEANUPS:
|
|
name = name.replace(look_for, '')
|
|
name_checks.append(name)
|
|
name_checks.append("%s.img" % (name))
|
|
name_checks.append("%s-img" % (name))
|
|
name = self._generate_img_name(url_fn)
|
|
name_checks.append(name)
|
|
name_checks.append("%s.img" % (name))
|
|
name_checks.append("%s-img" % (name))
|
|
return set(name_checks)
|
|
|
|
def _extract_url_fn(self):
|
|
pieces = urlparse.urlparse(self.url)
|
|
return sh.basename(pieces.path)
|
|
|
|
def install(self):
|
|
url_fn = self._extract_url_fn()
|
|
if not url_fn:
|
|
msg = "Can not determine file name from url: %r" % (self.url)
|
|
raise RuntimeError(msg)
|
|
check_names = self._generate_check_names(url_fn)
|
|
found_name = False
|
|
for name in check_names:
|
|
if not name:
|
|
continue
|
|
LOG.debug("Checking if you already have an image named %r" % (name))
|
|
if self._registry.has_image(name):
|
|
LOG.warn("You already 'seem' to have image named %r, skipping its install..." % (name))
|
|
found_name = True
|
|
break
|
|
if not found_name:
|
|
with utils.tempdir() as tdir:
|
|
fetch_fn = sh.joinpths(tdir, url_fn)
|
|
down.UrlLibDownloader(self.url, fetch_fn).download()
|
|
unpack_info = Unpacker().unpack(url_fn, fetch_fn, tdir)
|
|
tgt_image_name = self._generate_img_name(url_fn)
|
|
self._register(tgt_image_name, unpack_info)
|
|
return tgt_image_name
|
|
else:
|
|
return None
|
|
|
|
|
|
class Registry:
|
|
|
|
def __init__(self, token):
|
|
self.token = token
|
|
self._info = {}
|
|
self._loaded = False
|
|
|
|
def _parse(self, text):
|
|
current = {}
|
|
for line in text.splitlines():
|
|
if not line:
|
|
continue
|
|
if line.startswith("==="):
|
|
if 'id' in current:
|
|
id_ = current['id']
|
|
del(current['id'])
|
|
self._info[id_] = current
|
|
current = {}
|
|
else:
|
|
l = line.split(':', 1)
|
|
current[l[0].strip().lower()] = l[1].strip().replace('"', '')
|
|
|
|
def _load(self):
|
|
if self._loaded:
|
|
return
|
|
LOG.info('Loading current glance image information.')
|
|
params = {'TOKEN': self.token}
|
|
cmd = {'cmd': DETAILS_SHOW}
|
|
res = utils.execute_template(cmd, params=params)
|
|
if res:
|
|
(stdout, _) = res[0]
|
|
self._parse(stdout)
|
|
self._loaded = True
|
|
|
|
def has_image(self, image):
|
|
return image in self.get_image_names()
|
|
|
|
def get_image_names(self):
|
|
self._load()
|
|
return [self._info[k]['name'] for k in self._info.keys()]
|
|
|
|
|
|
class Service:
|
|
def __init__(self, cfg, pw_gen):
|
|
self.cfg = cfg
|
|
self.pw_gen = pw_gen
|
|
|
|
def _get_token(self):
|
|
LOG.info("Fetching your keystone admin token so that we can perform image uploads/detail calls.")
|
|
|
|
key_params = keystone.get_shared_params(self.cfg, self.pw_gen)
|
|
keystone_service_url = key_params['SERVICE_ENDPOINT']
|
|
keystone_token_url = "%s/tokens" % (keystone_service_url)
|
|
|
|
# form the post json data
|
|
data = json.dumps(
|
|
{
|
|
"auth":
|
|
{
|
|
"passwordCredentials":
|
|
{
|
|
"username": key_params['ADMIN_USER_NAME'],
|
|
"password": key_params['ADMIN_PASSWORD'],
|
|
},
|
|
"tenantName": key_params['ADMIN_TENANT_NAME'],
|
|
}
|
|
})
|
|
|
|
# Prepare the request
|
|
headers = {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
request = urllib2.Request(keystone_token_url, data=data, headers=headers)
|
|
|
|
# Make the request
|
|
LOG.info("Getting your token from url %r, please wait..." % (keystone_token_url))
|
|
LOG.debug("With post data %s" % (data))
|
|
LOG.debug("With headers %s" % (headers))
|
|
|
|
response = urllib2.urlopen(request)
|
|
|
|
token = json.loads(response.read())
|
|
|
|
# TODO is there a better way to validate???
|
|
if (not token or not type(token) is dict or
|
|
not token.get('access') or not type(token.get('access')) is dict or
|
|
not token.get('access').get('token') or not type(token.get('access').get('token')) is dict or
|
|
not token.get('access').get('token').get('id')):
|
|
msg = "Response from url %r did not match expected json format." % (keystone_token_url)
|
|
raise IOError(msg)
|
|
|
|
# Basic checks passed, extract it!
|
|
tok = token['access']['token']['id']
|
|
LOG.debug("Got token %r" % (tok))
|
|
return tok
|
|
|
|
def install(self):
|
|
LOG.info("Setting up any specified images in glance.")
|
|
|
|
# Extract the urls from the config
|
|
flat_locations = self.cfg.getdefaulted('img', 'image_urls', '')
|
|
locations = [loc.strip() for loc in flat_locations.split(',') if len(loc.strip())]
|
|
|
|
# Install them in glance
|
|
am_installed = 0
|
|
if locations:
|
|
utils.log_iterable(locations, logger=LOG,
|
|
header="Attempting to download+extract+upload %s images." % len(locations))
|
|
token = self._get_token()
|
|
for uri in locations:
|
|
try:
|
|
name = Image(uri, token).install()
|
|
if name:
|
|
LOG.info("Installed image named %r" % (name))
|
|
am_installed += 1
|
|
except (IOError, tarfile.TarError) as e:
|
|
LOG.exception('Installing %r failed due to: %s', uri, e)
|
|
return am_installed
|