ba7d45a526
Change-Id: Ibc73ba24aba7644bdfc131a23f2f4060bb053f4f
357 lines
13 KiB
Python
357 lines
13 KiB
Python
# Copyright 2018 Red Hat, Inc.
|
|
#
|
|
# 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.
|
|
|
|
"""Image sources to use when provisioning nodes."""
|
|
|
|
import abc
|
|
import logging
|
|
import os
|
|
from urllib import parse as urlparse
|
|
import warnings
|
|
|
|
import openstack.exceptions
|
|
import requests
|
|
|
|
|
|
from metalsmith import _utils
|
|
from metalsmith import exceptions
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class _Source(object, metaclass=abc.ABCMeta):
|
|
|
|
def _validate(self, connection, root_size_gb):
|
|
"""Validate the source."""
|
|
|
|
@abc.abstractmethod
|
|
def _node_updates(self, connection):
|
|
"""Updates required for a node to use this source."""
|
|
|
|
|
|
class GlanceImage(_Source):
|
|
"""Image from the OpenStack Image service."""
|
|
|
|
def __init__(self, image):
|
|
"""Create a Glance source.
|
|
|
|
:param image: `Image` object, ID or name.
|
|
"""
|
|
self.image = image
|
|
self._image_obj = None
|
|
|
|
def _validate(self, connection, root_size_gb):
|
|
if self._image_obj is not None:
|
|
return
|
|
try:
|
|
self._image_obj = connection.image.find_image(self.image,
|
|
ignore_missing=False)
|
|
except openstack.exceptions.SDKException as exc:
|
|
raise exceptions.InvalidImage(
|
|
'Cannot find image %(image)s: %(error)s' %
|
|
{'image': self.image, 'error': exc})
|
|
|
|
if (root_size_gb is None
|
|
and any(getattr(self._image_obj, x, None) is not None
|
|
for x in ('kernel_id', 'ramdisk_id'))):
|
|
raise exceptions.UnknownRootDiskSize(
|
|
'Partition images require root partition size')
|
|
|
|
def _node_updates(self, connection):
|
|
LOG.debug('Image: %s', self._image_obj)
|
|
|
|
updates = {
|
|
'image_source': self._image_obj.id
|
|
}
|
|
for prop in ('kernel', 'ramdisk'):
|
|
value = getattr(self._image_obj, '%s_id' % prop, None)
|
|
if value:
|
|
updates[prop] = value
|
|
|
|
return updates
|
|
|
|
|
|
class HttpWholeDiskImage(_Source):
|
|
"""A whole-disk image from HTTP(s) location.
|
|
|
|
Some deployment methods require a checksum of the image. It has to be
|
|
provided via ``checksum`` or ``checksum_url``.
|
|
|
|
Only ``checksum_url`` (if provided) has to be accessible from the current
|
|
machine. Other URLs have to be accessible by the Bare Metal service (more
|
|
specifically, by **ironic-conductor** processes).
|
|
"""
|
|
|
|
def __init__(self, url, checksum=None, checksum_url=None,
|
|
disk_format=None):
|
|
"""Create an HTTP source.
|
|
|
|
:param url: URL of the image.
|
|
:param checksum: SHA256 checksum of the image. Mutually exclusive with
|
|
``checksum_url``.
|
|
:param checksum_url: URL of the checksum file for the image. Has to
|
|
be in the standard format of the ``sha256sum`` tool. Mutually
|
|
exclusive with ``checksum``.
|
|
:param disk_format: Optional value to set for ``instance_info``
|
|
``image_disk_format``.
|
|
"""
|
|
if (checksum and checksum_url) or (not checksum and not checksum_url):
|
|
raise TypeError('Exactly one of checksum and checksum_url has '
|
|
'to be specified')
|
|
|
|
self.url = url
|
|
self.checksum = checksum
|
|
self.checksum_url = checksum_url
|
|
self.disk_format = disk_format
|
|
|
|
def _validate(self, connection, root_size_gb):
|
|
# TODO(dtantsur): should we validate image URLs here? Ironic will do it
|
|
# as well, and images do not have to be accessible from where
|
|
# metalsmith is running.
|
|
if self.checksum:
|
|
return
|
|
|
|
try:
|
|
response = requests.get(self.checksum_url)
|
|
response.raise_for_status()
|
|
checksums = response.text
|
|
except requests.RequestException as exc:
|
|
raise exceptions.InvalidImage(
|
|
'Cannot download checksum file %(url)s: %(err)s' %
|
|
{'url': self.checksum_url, 'err': exc})
|
|
|
|
try:
|
|
checksums = _utils.parse_checksums(checksums)
|
|
except (ValueError, TypeError) as exc:
|
|
raise exceptions.InvalidImage(
|
|
'Invalid checksum file %(url)s: %(err)s' %
|
|
{'url': self.checksum_url, 'err': exc})
|
|
|
|
fname = os.path.basename(urlparse.urlparse(self.url).path)
|
|
try:
|
|
self.checksum = checksums[fname]
|
|
except KeyError:
|
|
raise exceptions.InvalidImage(
|
|
'There is no image checksum for %(fname)s in %(url)s' %
|
|
{'fname': fname, 'url': self.checksum_url})
|
|
|
|
def _node_updates(self, connection):
|
|
LOG.debug('Image: %(image)s, checksum %(checksum)s',
|
|
{'image': self.url, 'checksum': self.checksum})
|
|
updates = {
|
|
'image_source': self.url,
|
|
'image_checksum': self.checksum,
|
|
}
|
|
if self.disk_format:
|
|
updates['image_disk_format'] = self.disk_format
|
|
return updates
|
|
|
|
|
|
class HttpPartitionImage(HttpWholeDiskImage):
|
|
"""A partition image from an HTTP(s) location."""
|
|
|
|
def __init__(self, url, kernel_url, ramdisk_url, checksum=None,
|
|
checksum_url=None, disk_format=None):
|
|
"""Create an HTTP source.
|
|
|
|
:param url: URL of the root disk image.
|
|
:param kernel_url: URL of the kernel image.
|
|
:param ramdisk_url: URL of the initramfs image.
|
|
:param checksum: SHA256 checksum of the root disk image. Mutually
|
|
exclusive with ``checksum_url``.
|
|
:param checksum_url: URL of the checksum file for the root disk image.
|
|
Has to be in the standard format of the ``sha256sum`` tool.
|
|
Mutually exclusive with ``checksum``.
|
|
:param disk_format: Optional value to set for ``instance_info``
|
|
``image_disk_format``.
|
|
"""
|
|
super(HttpPartitionImage, self).__init__(url, checksum=checksum,
|
|
checksum_url=checksum_url,
|
|
disk_format=disk_format)
|
|
self.kernel_url = kernel_url
|
|
self.ramdisk_url = ramdisk_url
|
|
|
|
def _validate(self, connection, root_size_gb):
|
|
super(HttpPartitionImage, self)._validate(connection, root_size_gb)
|
|
if root_size_gb is None:
|
|
raise exceptions.UnknownRootDiskSize(
|
|
'Partition images require root partition size')
|
|
|
|
def _node_updates(self, connection):
|
|
updates = super(HttpPartitionImage, self)._node_updates(connection)
|
|
updates['kernel'] = self.kernel_url
|
|
updates['ramdisk'] = self.ramdisk_url
|
|
return updates
|
|
|
|
|
|
class FileWholeDiskImage(_Source):
|
|
"""A whole-disk image from a local file location.
|
|
|
|
.. warning::
|
|
The location must be local to the **ironic-conductor** process handling
|
|
the node, not to metalsmith itself! Since there is no easy way to
|
|
determine which conductor handles a node, the same file must be
|
|
available at the same location to all conductors in the same group.
|
|
"""
|
|
|
|
def __init__(self, location, checksum=None):
|
|
"""Create a local file source.
|
|
|
|
:param location: Location of the image, optionally starting with
|
|
``file://``.
|
|
:param checksum: SHA256 checksum of the image. DEPRECATED: checksums do
|
|
not actually work with file images.
|
|
"""
|
|
if not location.startswith('file://'):
|
|
location = 'file://' + location
|
|
self.location = location
|
|
self.checksum = checksum
|
|
if self.checksum:
|
|
warnings.warn("Checksums cannot be used with file images",
|
|
DeprecationWarning)
|
|
|
|
def _node_updates(self, connection):
|
|
LOG.debug('Image: %s', self.location)
|
|
return {
|
|
'image_source': self.location,
|
|
}
|
|
|
|
|
|
class FilePartitionImage(FileWholeDiskImage):
|
|
"""A partition image from a local file location.
|
|
|
|
.. warning::
|
|
The location must be local to the **ironic-conductor** process handling
|
|
the node, not to metalsmith itself! Since there is no easy way to
|
|
determine which conductor handles a node, the same file must be
|
|
available at the same location to all conductors in the same group.
|
|
"""
|
|
|
|
def __init__(self, location, kernel_location, ramdisk_location,
|
|
checksum=None):
|
|
"""Create a local file source.
|
|
|
|
:param location: Location of the image, optionally starting with
|
|
``file://``.
|
|
:param kernel_location: Location of the kernel of the image,
|
|
optionally starting with ``file://``.
|
|
:param ramdisk_location: Location of the ramdisk of the image,
|
|
optionally starting with ``file://``.
|
|
:param checksum: SHA256 checksum of the image. DEPRECATED: checksums do
|
|
not actually work with file images.
|
|
"""
|
|
super(FilePartitionImage, self).__init__(location, checksum)
|
|
if not kernel_location.startswith('file://'):
|
|
kernel_location = 'file://' + kernel_location
|
|
if not ramdisk_location.startswith('file://'):
|
|
ramdisk_location = 'file://' + ramdisk_location
|
|
self.kernel_location = kernel_location
|
|
self.ramdisk_location = ramdisk_location
|
|
|
|
def _validate(self, connection, root_size_gb):
|
|
super(FilePartitionImage, self)._validate(connection, root_size_gb)
|
|
if root_size_gb is None:
|
|
raise exceptions.UnknownRootDiskSize(
|
|
'Partition images require root partition size')
|
|
|
|
def _node_updates(self, connection):
|
|
updates = super(FilePartitionImage, self)._node_updates(connection)
|
|
updates['kernel'] = self.kernel_location
|
|
updates['ramdisk'] = self.ramdisk_location
|
|
return updates
|
|
|
|
|
|
def detect(image, kernel=None, ramdisk=None, checksum=None):
|
|
"""Try detecting the correct source type from the provided information.
|
|
|
|
.. note::
|
|
Images without a schema are assumed to be Glance images.
|
|
|
|
:param image: Location of the image: ``file://``, ``http://``, ``https://``
|
|
link or a Glance image name or UUID.
|
|
:param kernel: Location of the kernel (if present): ``file://``,
|
|
``http://``, ``https://`` link or a Glance image name or UUID.
|
|
:param ramdisk: Location of the ramdisk (if present): ``file://``,
|
|
``http://``, ``https://`` link or a Glance image name or UUID.
|
|
:param checksum: SHA256 checksum of the image: ``http://`` or ``https://``
|
|
link or a string.
|
|
:return: A valid source object.
|
|
:raises: ValueError if the given parameters do not correspond to any
|
|
valid source.
|
|
"""
|
|
image_type = _link_type(image)
|
|
checksum_type = _link_type(checksum)
|
|
|
|
if image_type == 'glance':
|
|
if kernel or ramdisk or checksum:
|
|
raise ValueError('kernel, image and checksum cannot be provided '
|
|
'for Glance images')
|
|
else:
|
|
return GlanceImage(image)
|
|
|
|
kernel_type = _link_type(kernel)
|
|
ramdisk_type = _link_type(ramdisk)
|
|
if image_type == 'http' and not checksum:
|
|
raise ValueError('checksum is required for HTTP images')
|
|
|
|
if image_type == 'file':
|
|
if (kernel_type not in (None, 'file')
|
|
or ramdisk_type not in (None, 'file')):
|
|
raise ValueError('kernel and ramdisk can only be files '
|
|
'for file images')
|
|
|
|
if kernel or ramdisk:
|
|
return FilePartitionImage(image,
|
|
kernel_location=kernel,
|
|
ramdisk_location=ramdisk,
|
|
checksum=checksum)
|
|
else:
|
|
return FileWholeDiskImage(image, checksum=checksum)
|
|
else:
|
|
if (kernel_type not in (None, 'http')
|
|
or ramdisk_type not in (None, 'http')
|
|
or checksum_type == 'file'):
|
|
raise ValueError('kernal, ramdisk and checksum can only be HTTP '
|
|
'links for HTTP images')
|
|
|
|
if checksum_type == 'http':
|
|
kwargs = {'checksum_url': checksum}
|
|
else:
|
|
kwargs = {'checksum': checksum}
|
|
|
|
# Assume raw image based on file extension
|
|
if image.endswith('.raw'):
|
|
kwargs['disk_format'] = 'raw'
|
|
|
|
if kernel or ramdisk:
|
|
return HttpPartitionImage(image,
|
|
kernel_url=kernel,
|
|
ramdisk_url=ramdisk,
|
|
**kwargs)
|
|
else:
|
|
return HttpWholeDiskImage(image, **kwargs)
|
|
|
|
|
|
def _link_type(link):
|
|
if link is None:
|
|
return None
|
|
elif link.startswith('http://') or link.startswith('https://'):
|
|
return 'http'
|
|
elif link.startswith('file://'):
|
|
return 'file'
|
|
else:
|
|
return 'glance'
|