nova-lxd/nova_lxd/nova/virt/lxd/container_image.py

240 lines
8.8 KiB
Python

# Copyright 2015 Canonical Ltd
# 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 hashlib
import io
import json
from nova import exception
from nova import i18n
from nova import image
from nova import utils
import os
from pylxd import api
from pylxd import exceptions as lxd_exceptions
import tarfile
import uuid
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import fileutils
from nova_lxd.nova.virt.lxd import utils as container_dir
_ = i18n._
_LE = i18n._LE
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
IMAGE_API = image.API()
class LXDContainerImage(object):
def __init__(self):
self.connection = api.API()
self.container_dir = container_dir.LXDContainerDirectories()
self.lock_path = str(os.path.join(CONF.instances_path, 'locks'))
def setup_image(self, context, instance, image_meta):
try:
LOG.debug('Fetching image info from glance')
with lockutils.lock(self.lock_path,
lock_file_prefix=('lxd-image-%s' %
instance.image_ref),
external=True):
if self._image_defined(instance):
return
base_dir = self.container_dir.get_base_dir()
if not os.path.exists(base_dir):
fileutils.ensure_tree(base_dir)
container_rootfs_img = (
self.container_dir.get_container_rootfs_image(
image_meta))
IMAGE_API.download(
context, instance.image_ref,
dest_path=container_rootfs_img)
container_manifest_img = self._get_lxd_manifest(instance,
image_meta)
utils.execute('xz', '-9', container_manifest_img)
self._image_upload(
(container_manifest_img + '.xz', container_rootfs_img),
container_manifest_img.split('/')[-1], False,
instance)
self._setup_alias((container_manifest_img + '.xz',
container_rootfs_img), instance)
os.unlink(container_manifest_img + '.xz')
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Failed to upload %(image)s to LXD: %(reason)s'),
{'image': instance.image_ref, 'reason': ex},
instance=instance)
self._cleanup_image(image_meta)
def _get_lxd_manifest(self, instance, image_meta):
LOG.debug('Creating LXD manifest')
try:
container_manifest = (
self.container_dir.get_container_manifest_image(
image_meta))
target_tarball = tarfile.open(container_manifest, "w:")
image_prop = image_meta.get('properties')
metadata = {
'architecture': image_prop.get('architecture',
os.uname()[4]),
'creation_date': int(os.stat(container_manifest).st_ctime),
'properties': {
'os': image_prop.get('os_distro', 'None'),
'architecture': image_prop.get('architecture',
os.uname()[4]),
'description': image_prop.get('description',
None),
'name': instance.image_ref
}
}
metadata_yaml = (json.dumps(metadata, sort_keys=True,
indent=4, separators=(',', ': '),
ensure_ascii=False).encode('utf-8')
+ b"\n")
metadata_file = tarfile.TarInfo()
metadata_file.size = len(metadata_yaml)
metadata_file.name = "metadata.yaml"
target_tarball.addfile(metadata_file,
io.BytesIO(metadata_yaml))
target_tarball.close()
return container_manifest
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Failed to upload %(image)s to LXD: %(reason)s'),
{'image': instance.image_ref, 'reason': ex},
instance=instance)
self._cleanup_image(image_meta)
def _image_upload(self, path, filename, split, instance):
LOG.debug('Uploading Image to LXD.')
headers = {}
if split:
headers['Content-Type'] = "application/octet-stream"
try:
status, data = (self.connection.image_upload(
data=open(path, 'rb'),
headers=headers))
except lxd_exceptions as ex:
raise exception.ImageUnacceptable(
image_id=instance.image_ref,
reason=_('Failed to upload image: %s') % ex)
else:
meta_path, rootfs_path = path
boundary = str(uuid.uuid1())
form = []
for name, path in [("metadata", meta_path),
("rootfs", rootfs_path)]:
filename = os.path.basename(path)
form.append("--%s" % boundary)
form.append("Content-Disposition: form-data; "
"name=%s; filename=%s" % (name, filename))
form.append("Content-Type: application/octet-stream")
form.append("")
with open(path, "rb") as fd:
form.append(fd.read())
form.append("--%s--" % boundary)
form.append("")
body = b""
for entry in form:
if isinstance(entry, bytes):
body += entry + b"\r\n"
else:
body += entry.encode() + b"\r\n"
headers['Content-Type'] = ("multipart/form-data; boundary=%s"
% boundary)
try:
status, data = self.connection.image_upload(data=body,
headers=headers)
except lxd_exceptions as ex:
raise exception.ImageUnacceptable(
image_id=instance.image_ref,
reason=_('Failed to upload image: %s') % ex)
return data
def _setup_alias(self, path, instance):
LOG.debug('Updating image and metadata')
try:
meta_path, rootfs_path = path
with open(meta_path, 'rb') as meta_fd:
with open(rootfs_path, "rb") as rootfs_fd:
fingerprint = hashlib.sha256(meta_fd.read() +
rootfs_fd.read()).hexdigest()
alias_config = {
'name': instance.image_ref,
'target': fingerprint
}
LOG.debug('Creating alias: %s' % alias_config)
self.connection.alias_create(alias_config)
except lxd_exceptions.APIError as ex:
raise exception.ImageUnacceptable(
image_id=instance.image_ref,
reason=_('Image already exists: %s') % ex)
def _image_defined(self, instance):
LOG.debug('Checking alias existance')
try:
return self.connection.alias_defined(instance.image_ref)
except lxd_exceptions.APIError as ex:
if ex.status_code == 404:
return False
else:
msg = _('Failed to determine image alias: %s') % ex
raise exception.NovaException(msg)
def _cleanup_image(self, image_meta):
container_rootfs_img = (
self.container_dir.get_container_rootfs_image(
image_meta))
container_manifest = (
self.container_dir.get_container_manifest_image(
image_meta))
if os.path.exists(container_rootfs_img):
os.unlink(container_rootfs_img)
if os.path.exists(container_manifest):
os.unlink(container_manifest)