python-tripleoclient/tripleoclient/v1/overcloud_image.py
Dmitry Tantsur 9e717d67d7 Issue a message to update nodes after updating images
Currently it may be confusing for operators that the image upload
command does not update nodes when uploading new versions of images.
This change adds a reminder to do so.

Change-Id: I834043c559004bbf5eeaf0a611cfa70ff29b7b05
Closes-Bug: #1786970
2018-08-14 18:00:19 +02:00

503 lines
21 KiB
Python

# Copyright 2015 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.
#
from __future__ import print_function
import logging
import os
import re
import subprocess
import sys
from osc_lib import exceptions
from osc_lib.i18n import _
from osc_lib import utils
from prettytable import PrettyTable
import tripleo_common.arch
from tripleo_common.image import build
from tripleoclient import command
from tripleoclient import constants
from tripleoclient import utils as plugin_utils
class BuildOvercloudImage(command.Command):
"""Build images for the overcloud"""
auth_required = False
log = logging.getLogger(__name__ + ".BuildOvercloudImage")
IMAGE_YAML_PATH = "/usr/share/openstack-tripleo-common/image-yaml"
DEFAULT_YAML = ['overcloud-images.yaml', 'overcloud-images-centos7.yaml']
def get_parser(self, prog_name):
parser = super(BuildOvercloudImage, self).get_parser(prog_name)
parser.add_argument(
"--config-file",
dest="config_files",
metavar='<yaml config file>',
default=[],
action="append",
help=_("YAML config file specifying the image build. May be "
"specified multiple times. Order is preserved, and later "
"files will override some options in previous files. "
"Other options will append."),
)
parser.add_argument(
"--image-name",
dest="image_names",
metavar='<image name>',
default=None,
help=_("Name of image to build. May be specified multiple "
"times. If unspecified, will build all images in "
"given YAML files."),
)
parser.add_argument(
"--no-skip",
dest="skip",
action="store_false",
default=True,
help=_("Skip build if cached image exists."),
)
parser.add_argument(
"--output-directory",
dest="output_directory",
default=os.environ.get('TRIPLEO_ROOT', '.'),
help=_("Output directory for images. Defaults to $TRIPLEO_ROOT,"
"or current directory if unset."),
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
if not parsed_args.config_files:
parsed_args.config_files = [os.path.join(self.IMAGE_YAML_PATH, f)
for f in self.DEFAULT_YAML]
manager = build.ImageBuildManager(
parsed_args.config_files,
output_directory=parsed_args.output_directory,
skip=parsed_args.skip,
images=parsed_args.image_names)
manager.build()
class GlanceBaseClientAdapter(object):
def __init__(self, client):
self.client = client
def print_image_info(self, image):
table = PrettyTable(['ID', 'Name', 'Disk Format', 'Size', 'Status'])
table.add_row([image.id, image.name, image.disk_format, image.size,
image.status])
print(table, file=sys.stdout)
class GlanceV1ClientAdapter(GlanceBaseClientAdapter):
def upload_image(self, *args, **kwargs):
image = self.client.images.create(*args, **kwargs)
print('Image "%s" was uploaded.' % image.name, file=sys.stdout)
self.print_image_info(image)
return image
def get_image_property(self, image, prop):
return image.properties[prop]
class GlanceV2ClientAdapter(GlanceBaseClientAdapter):
def upload_image(self, *args, **kwargs):
is_public = kwargs.pop('is_public')
data = kwargs.pop('data')
properties = kwargs.pop('properties', None)
kwargs['visibility'] = 'public' if is_public else 'private'
kwargs.setdefault('container_format', 'bare')
image = self.client.images.create(*args, **kwargs)
self.client.images.upload(image.id, image_data=data)
if properties:
self.client.images.update(image.id, **properties)
# Refresh image info
image = self.client.images.get(image.id)
print('Image "%s" was uploaded.' % image.name, file=sys.stdout)
self.print_image_info(image)
return image
def get_image_property(self, image, prop):
return getattr(image, prop)
class UploadOvercloudImage(command.Command):
"""Create overcloud glance images from existing image files."""
log = logging.getLogger(__name__ + ".UploadOvercloudImage")
def _get_image(self, name):
try:
image = utils.find_resource(self.app.client_manager.image.images,
name)
except exceptions.CommandError as e:
# TODO(maufart): enhance error detection, when python-glanceclient
# starts provide it https://bugs.launchpad.net/glance/+bug/1480156
if 'More than one image exists' in e.args[0]:
raise exceptions.CommandError(
'Image "%s" already exists in glance more than once,'
' delete all copies except the first one.' % name
)
else:
self.log.debug('Image "%s" does not exists, no problem.'
% name)
return None
return image
def _image_changed(self, name, filename):
image = utils.find_resource(self.app.client_manager.image.images,
name)
return image.checksum != plugin_utils.file_checksum(filename)
def _check_file_exists(self, file_path):
if not os.path.isfile(file_path):
raise exceptions.CommandError(
'Required file "%s" does not exist.' % file_path
)
def _read_image_file_pointer(self, dirname, filename):
filepath = os.path.join(dirname, filename)
self._check_file_exists(filepath)
return open(filepath, 'rb')
def _copy_file(self, src, dest):
subprocess.check_call('sudo cp -f "{0}" "{1}"'.format(src, dest),
shell=True)
def _image_try_update(self, image_name, image_file, parsed_args):
image = self._get_image(image_name)
if image:
if self._image_changed(image_name, image_file):
if parsed_args.update_existing:
self.app.client_manager.image.images.update(
image.id,
name='%s_%s' % (image.name, re.sub(r'[\-:\.]|(0+$)',
'',
image.created_at))
)
self.updated = True
return None
else:
print('Image "%s" already exists and can be updated'
' with --update-existing.' % image_name)
return image
else:
print('Image "%s" is up-to-date, skipping.' % image_name)
return image
else:
return None
def _files_changed(self, filepath1, filepath2):
return (plugin_utils.file_checksum(filepath1) !=
plugin_utils.file_checksum(filepath2))
def _file_create_or_update(self, src_file, dest_file, update_existing):
if os.path.isfile(dest_file):
if self._files_changed(src_file, dest_file):
if update_existing:
self._copy_file(src_file, dest_file)
else:
print('Image file "%s" already exists and can be updated'
' with --update-existing.' % dest_file)
else:
print('Image file "%s" is up-to-date, skipping.' % dest_file)
else:
self._copy_file(src_file, dest_file)
def _get_glance_client_adaptor(self):
if self.app.client_manager.image.version >= 2.0:
return GlanceV2ClientAdapter(self.app.client_manager.image)
else:
return GlanceV1ClientAdapter(self.app.client_manager.image)
def _get_environment_var(self, envvar, default, deprecated=[]):
for env_key in deprecated:
if env_key in os.environ:
self.log.warn(('Found deprecated environment var \'%s\', '
'please use \'%s\' instead' % (env_key,
envvar)))
return os.environ.get(env_key)
return os.environ.get(envvar, default)
def get_parser(self, prog_name):
parser = super(UploadOvercloudImage, self).get_parser(prog_name)
parser.add_argument(
"--image-path",
default=self._get_environment_var('IMAGE_PATH', './'),
help=_("Path to directory containing image files"),
)
parser.add_argument(
"--os-image-name",
default=self._get_environment_var('OS_IMAGE_NAME',
'overcloud-full.qcow2'),
help=_("OpenStack disk image filename"),
)
parser.add_argument(
"--ironic-python-agent-name",
dest='ipa_name',
default=self._get_environment_var('IRONIC_PYTHON_AGENT_NAME',
'ironic-python-agent',
deprecated=['AGENT_NAME']),
help=_("OpenStack ironic-python-agent (agent) image filename"),
)
parser.add_argument(
"--http-boot",
default=self._get_environment_var(
'HTTP_BOOT',
constants.IRONIC_HTTP_BOOT_BIND_MOUNT),
help=_("Root directory for the introspection image")
)
parser.add_argument(
"--update-existing",
dest="update_existing",
action="store_true",
help=_("Update images if already exist"),
)
parser.add_argument(
"--whole-disk",
dest="whole_disk",
action="store_true",
default=False,
help=_("When set, the overcloud-full image to be uploaded "
"will be considered as a whole disk one"),
)
parser.add_argument(
"--architecture",
help=_("Architecture type for these images, "
"\'x86_64\', \'i386\' and \'ppc64le\' "
"are common options. This option should match at least "
"one \'arch\' value in instackenv.json"),
)
parser.add_argument(
"--platform",
help=_("Platform type for these images. Platform is a "
"sub-category of architecture. For example you may have "
"generic images for x86_64 but offer images specific to "
"SandyBridge (SNB)."),
)
parser.add_argument(
"--image-type",
dest="image_type",
choices=["os", "ironic-python-agent"],
help=_("If specified, allows to restrict the image type to upload "
"(os for the overcloud image or ironic-python-agent for "
"the ironic-python-agent one)"),
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
glance_client_adaptor = self._get_glance_client_adaptor()
self.updated = False
if parsed_args.platform and not parsed_args.architecture:
raise exceptions.CommandError('You supplied a platform (%s) '
'without specifying the '
'architecture')
self.log.debug("checking if image files exist")
image_files = []
if parsed_args.image_type is None or \
parsed_args.image_type == 'ironic-python-agent':
image_files.append('%s.initramfs' % parsed_args.ipa_name)
image_files.append('%s.kernel' % parsed_args.ipa_name)
if parsed_args.image_type is None or parsed_args.image_type == 'os':
image_files.append(parsed_args.os_image_name)
if parsed_args.whole_disk:
overcloud_image_type = 'whole disk'
else:
overcloud_image_type = 'partition'
for image in image_files:
self._check_file_exists(os.path.join(parsed_args.image_path,
image))
image_name = parsed_args.os_image_name.split('.')[0]
self.log.debug("uploading %s overcloud images to glance" %
overcloud_image_type)
properties = {}
arch = parsed_args.architecture
if arch:
properties['hw_architecture'] = arch
else:
properties['hw_architecture'] = tripleo_common.arch.kernel_arch()
platform = parsed_args.platform
if platform:
properties['tripleo_platform'] = platform
if parsed_args.image_type is None or parsed_args.image_type == 'os':
# vmlinuz and initrd only need to be uploaded for a partition image
if not parsed_args.whole_disk:
(oc_vmlinuz_name,
oc_vmlinuz_extension) = plugin_utils.overcloud_kernel(
image_name, arch=arch, platform=platform)
oc_vmlinuz_file = os.path.join(parsed_args.image_path,
image_name +
oc_vmlinuz_extension)
kernel = (self._image_try_update(oc_vmlinuz_name,
oc_vmlinuz_file,
parsed_args) or
glance_client_adaptor.upload_image(
name=oc_vmlinuz_name,
is_public=True,
disk_format='aki',
properties=properties,
data=self._read_image_file_pointer(
parsed_args.image_path, oc_vmlinuz_file)
))
(oc_initrd_name,
oc_initrd_extension) = plugin_utils.overcloud_ramdisk(
image_name, arch=arch, platform=platform)
oc_initrd_file = os.path.join(parsed_args.image_path,
image_name +
oc_initrd_extension)
ramdisk = (self._image_try_update(oc_initrd_name,
oc_initrd_file,
parsed_args) or
glance_client_adaptor.upload_image(
name=oc_initrd_name,
is_public=True,
disk_format='ari',
properties=properties,
data=self._read_image_file_pointer(
parsed_args.image_path, oc_initrd_file)
))
(oc_name,
oc_extension) = plugin_utils.overcloud_image(
image_name, arch=arch, platform=platform)
oc_file = os.path.join(parsed_args.image_path,
image_name +
oc_extension)
overcloud_image = (self._image_try_update(oc_name, oc_file,
parsed_args) or
glance_client_adaptor.upload_image(
name=oc_name,
is_public=True,
disk_format='qcow2',
container_format='bare',
properties=dict(
{'kernel_id': kernel.id,
'ramdisk_id': ramdisk.id},
**properties),
data=self._read_image_file_pointer(
parsed_args.image_path, oc_file)
))
img_kernel_id = glance_client_adaptor.get_image_property(
overcloud_image, 'kernel_id')
img_ramdisk_id = glance_client_adaptor.get_image_property(
overcloud_image, 'ramdisk_id')
# check overcloud image links
if (img_kernel_id != kernel.id or
img_ramdisk_id != ramdisk.id):
self.log.error('Link overcloud image to it\'s initrd and '
'kernel images is MISSING OR leads to OLD '
'image. You can keep it or fix it '
'manually.')
else:
(oc_name,
oc_extension) = plugin_utils.overcloud_image(
image_name, arch=arch, platform=platform)
oc_file = os.path.join(parsed_args.image_path,
image_name +
oc_extension)
overcloud_image = (self._image_try_update(oc_name, oc_file,
parsed_args) or
glance_client_adaptor.upload_image(
name=oc_name,
is_public=True,
disk_format='qcow2',
container_format='bare',
properties=properties,
data=self._read_image_file_pointer(
parsed_args.image_path, oc_file)
))
self.log.debug("uploading bm images to glance")
if parsed_args.image_type is None or \
parsed_args.image_type == 'ironic-python-agent':
(deploy_kernel_name,
deploy_kernel_extension) = plugin_utils.deploy_kernel(
arch=arch, platform=platform)
deploy_kernel_file = os.path.join(parsed_args.image_path,
parsed_args.ipa_name +
deploy_kernel_extension)
self._image_try_update(deploy_kernel_name, deploy_kernel_file,
parsed_args) or \
glance_client_adaptor.upload_image(
name=deploy_kernel_name,
is_public=True,
disk_format='aki',
properties=properties,
data=self._read_image_file_pointer(
parsed_args.image_path,
deploy_kernel_file))
(deploy_ramdisk_name,
deploy_ramdisk_extension) = plugin_utils.deploy_ramdisk(
arch=arch, platform=platform)
deploy_ramdisk_file = os.path.join(parsed_args.image_path,
parsed_args.ipa_name +
deploy_ramdisk_extension)
self._image_try_update(deploy_ramdisk_name, deploy_ramdisk_file,
parsed_args) or \
glance_client_adaptor.upload_image(
name=deploy_ramdisk_name,
is_public=True,
disk_format='ari',
properties=properties,
data=self._read_image_file_pointer(parsed_args.image_path,
deploy_ramdisk_file))
self.log.debug("copy agent images to HTTP BOOT dir")
# TODO(tonyb) Decide how to handle platform specific httpboot
# files/names
self._file_create_or_update(
os.path.join(parsed_args.image_path,
'%s.kernel' % parsed_args.ipa_name),
os.path.join(parsed_args.http_boot, 'agent.kernel'),
parsed_args.update_existing
)
self._file_create_or_update(
os.path.join(parsed_args.image_path,
'%s.initramfs' % parsed_args.ipa_name),
os.path.join(parsed_args.http_boot, 'agent.ramdisk'),
parsed_args.update_existing
)
if self.updated:
print('Some images have been updated in Glance, make sure to '
'rerun\n\topenstack overcloud node configure\nto reflect '
'the changes on the nodes')