
Allow caller to provide "images" branch of deployement config structure. Also provide called API to make "correct"/"default" images branch. Till now setup method of environment object silentlry override "images" branch. Change-Id: I1bdb0f91c7bf83e3c888650b2f8917dde80dea42
379 lines
13 KiB
Python
379 lines
13 KiB
Python
#
|
|
# Copyright 2016 Cray 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 collections
|
|
import errno
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import json
|
|
import logging
|
|
import time
|
|
import sh
|
|
|
|
import jinja2
|
|
import pkg_resources
|
|
from oslo_config import cfg
|
|
|
|
import ramdisk_func_test
|
|
from ramdisk_func_test import conf
|
|
from ramdisk_func_test import exception
|
|
from ramdisk_func_test import network
|
|
from ramdisk_func_test import node
|
|
from ramdisk_func_test import utils
|
|
|
|
|
|
CONF = conf.CONF
|
|
CONF.register_opts([
|
|
cfg.StrOpt('image_build_dir',
|
|
default="/tmp/rft_image_build",
|
|
help='A path where images from DIB will be build. Expected '
|
|
'build artifacts are: kernel, ramdisk, ramdisk_key'),
|
|
cfg.StrOpt('tenant_images_dir',
|
|
default="/tmp/rft_golden_images",
|
|
help='A path where mock web-server will take tenant images '),
|
|
cfg.StrOpt('kernel',
|
|
default='vmlinuz',
|
|
help='Name of kernel image'),
|
|
cfg.StrOpt('ramdisk',
|
|
default='initramfs',
|
|
help='Name of ramdisk image'),
|
|
cfg.StrOpt('ramdisk_key',
|
|
default='bareon_key',
|
|
help='Name of private ssh key to access ramdisk'),
|
|
cfg.IntOpt('stub_webserver_port',
|
|
default=8011,
|
|
help='The port used by stub webserver')
|
|
])
|
|
CONF.import_opt('ramdisk_func_test_workdir', 'ramdisk_func_test.utils')
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
PXELINUX_PATH = (
|
|
('/usr/lib/PXELINUX/pxelinux.0',
|
|
'/usr/lib/syslinux/modules/bios/ldlinux.c32',),
|
|
('/usr/share/syslinux/pxelinux.0', '/usr/share/syslinux/ldlinux.c32'),
|
|
('/usr/share/syslinux/pxelinux.0',),
|
|
('/usr/lib/syslinux/pxelinux.0',),
|
|
)
|
|
|
|
|
|
class Environment(object):
|
|
_loaded_config = object() # to fail comparison with None
|
|
|
|
deploy_driver = 'swift'
|
|
node = None
|
|
network = None
|
|
webserver = None
|
|
rsync_dir = None
|
|
image_mount_point = None
|
|
|
|
def __init__(self, template_path, config=None):
|
|
super(Environment, self).__init__()
|
|
self._load_config(config)
|
|
self.jinja_env = self._init_jinja2(template_path)
|
|
|
|
@staticmethod
|
|
def _init_jinja2(path):
|
|
path = path[:]
|
|
path.append(pkg_resources.resource_filename(
|
|
ramdisk_func_test.__name__, 'templates'))
|
|
loader = jinja2.FileSystemLoader(path)
|
|
jinja_env = jinja2.Environment(loader=loader)
|
|
|
|
# Custom template callbacks
|
|
jinja_env.globals['empty_disk'] = utils.create_empty_disk
|
|
jinja_env.globals['disk_from_base'] = utils.create_disk_from_base
|
|
jinja_env.globals['get_rand_mac'] = utils.get_random_mac
|
|
|
|
return jinja_env
|
|
|
|
def setupclass(self):
|
|
"""Global setup - single for all tests"""
|
|
self.network = network.Network(self.jinja_env)
|
|
self.network.start()
|
|
|
|
self._setup_webserver()
|
|
self._check_rsync()
|
|
self._setup_pxe()
|
|
|
|
def setup(self, node_template, deploy_config):
|
|
"""Per-test setup"""
|
|
|
|
ssh_key_path = os.path.join(CONF.image_build_dir, CONF.ramdisk_key)
|
|
self.node = node.Node(
|
|
self.jinja_env, node_template, self.network.name, ssh_key_path)
|
|
|
|
public_key = '.'.join([ssh_key_path, 'pub'])
|
|
self._generate_cloud_config(public_key)
|
|
self.add_pxe_config_for_current_node()
|
|
self.network.add_node(self.node)
|
|
|
|
path = self._save_provision_json_for_node(deploy_config)
|
|
|
|
self.node.start()
|
|
self.node.wait_for_callback()
|
|
self.node.put_file(path, '/tmp/provision.json')
|
|
|
|
def init_unlabelled_disk(self, path='/tmp/vdc'):
|
|
"""Create/format an unlabelled disk"""
|
|
subprocess.check_call(
|
|
'sudo /usr/bin/qemu-img create -f qcow2 -o '
|
|
'preallocation=metadata %s 1M' % path, shell=True)
|
|
|
|
def teardown(self):
|
|
"""Per-test teardown"""
|
|
self.network.remove_node(self.node)
|
|
self.node.kill()
|
|
self._delete_node_workdir(self.node)
|
|
self._teardown_rsync()
|
|
|
|
def teardownclass(self):
|
|
"""Global tear down - single for all tests"""
|
|
LOG.info("Tearing down Environment class...")
|
|
self._teardown_webserver()
|
|
|
|
self.network.kill()
|
|
self._delete_workdir()
|
|
|
|
def update_deploy_config(self, deploy_config):
|
|
path = self._save_provision_json_for_node(deploy_config)
|
|
self.node.put_file(path, '/tmp/provision.json')
|
|
|
|
def _setup_pxe(self):
|
|
LOG.info("Setting up PXE configuration/images")
|
|
tftp_root = self.network.tftp_root
|
|
img_build = CONF.image_build_dir
|
|
|
|
for paths in PXELINUX_PATH:
|
|
paths_flat = "', '".join(paths)
|
|
LOG.info("Looking for boot program files in '%s'", paths_flat)
|
|
for path in paths:
|
|
if not os.path.exists(path):
|
|
LOG.info("File %s not found. Trying next paths option...",
|
|
path)
|
|
break
|
|
else:
|
|
LOG.info("Boot program files found in: '%s'", paths_flat)
|
|
for path in paths:
|
|
utils.copy_file(path, tftp_root)
|
|
break
|
|
else:
|
|
raise exception.PXELinuxNotFound()
|
|
|
|
for img in (CONF.kernel, CONF.ramdisk):
|
|
utils.copy_file(os.path.join(img_build, img), tftp_root)
|
|
|
|
def add_pxe_config_for_current_node(self):
|
|
LOG.info("Setting up PXE configuration file for node {0}".format(
|
|
self.node.name))
|
|
|
|
tftp_root = self.network.tftp_root
|
|
|
|
template = self.jinja_env.get_template('bareon_config.template')
|
|
pxe_config = template.render(
|
|
kernel=CONF.kernel,
|
|
ramdisk=CONF.ramdisk,
|
|
deployment_id=self.node.name,
|
|
network=self.network,
|
|
stub_server_port=CONF.stub_webserver_port)
|
|
|
|
pxe_path = os.path.join(tftp_root, "pxelinux.cfg")
|
|
utils.ensure_tree(pxe_path)
|
|
|
|
conf_path = os.path.join(pxe_path, '01-{0}'.format(
|
|
self.node.mac.replace(':', '-')))
|
|
with open(conf_path, 'w') as f:
|
|
f.write(pxe_config)
|
|
|
|
def _generate_cloud_config(self, public_key):
|
|
""""Used to support logging into the tenant image."""
|
|
with open(public_key, 'r') as f:
|
|
key = f.readline()
|
|
|
|
template = self.jinja_env.get_template('cloud.cfg.template')
|
|
path = os.path.join(self.node.workdir, 'cloud.cfg')
|
|
with open(path, 'w') as f:
|
|
f.write(template.render(bareon_public_key=key))
|
|
|
|
def _setup_webserver(self):
|
|
port = CONF.stub_webserver_port
|
|
LOG.info("Starting stub webserver (at IP {0} port {1}, path to tenant "
|
|
"images folder is '{2}')".format(self.network.address, port,
|
|
CONF.tenant_images_dir))
|
|
|
|
# TODO(max_lobur) make webserver singletone
|
|
cmd = ['ramdisk-stub-webserver', self.network.address, str(port)]
|
|
self.webserver = subprocess.Popen(cmd, shell=False)
|
|
|
|
def patch_config_images(self, deploy_config, payload=None):
|
|
if isinstance(payload, basestring):
|
|
images = [self._make_image_config_record(payload)]
|
|
elif isinstance(payload, collections.Mapping):
|
|
images = self._set_multiple_tenant_image(payload)
|
|
elif payload is None:
|
|
record = self._make_image_config_record('FAKE')
|
|
record['image_pull_url'] = 'http://{0}:{1}/fake'.format(
|
|
self.network.address, CONF.stub_webserver_port),
|
|
images = [record]
|
|
else:
|
|
raise TypeError('Invalid "payload" value: {!r}'.format(payload))
|
|
|
|
deploy_config['images'] = images
|
|
return deploy_config
|
|
|
|
def _make_image_config_record(self, name, boot=True):
|
|
return {
|
|
"name": name,
|
|
"boot": boot,
|
|
"target": '/',
|
|
"image_pull_url": self.get_url_for_image(
|
|
name, self.deploy_driver),
|
|
}
|
|
|
|
def _set_multiple_tenant_image(self, image_names):
|
|
images = []
|
|
for index, element in enumerate(image_names.items()):
|
|
os_id, name = element
|
|
if not name:
|
|
name = os_id
|
|
record = self._make_image_config_record(name, boot=index == 0)
|
|
images.append(record)
|
|
return images
|
|
|
|
def get_url_for_image(self, image_name, source_type):
|
|
if source_type == 'swift':
|
|
return self._get_swift_tenant_image_url(image_name)
|
|
elif source_type == 'rsync':
|
|
return self._get_rsync_tenant_image_url(image_name)
|
|
else:
|
|
raise exception.UnknownDeployDriver()
|
|
|
|
def _get_swift_tenant_image_url(self, image_name):
|
|
return (
|
|
'http://{0}:{1}/tenant_images/{2}'.format(
|
|
self.network.address, CONF.stub_webserver_port, image_name))
|
|
|
|
def _get_rsync_tenant_image_url(self, image_name):
|
|
url = "{0}::ironic_rsync/{1}/".format(self.network.address,
|
|
image_name)
|
|
if self.image_mount_point:
|
|
# Image already mounted.
|
|
if not os.path.exists(
|
|
os.path.join(self.image_mount_point, 'etc/passwd')):
|
|
raise exception.ImageMountError(
|
|
'Previously mounted image no longer present.')
|
|
return url
|
|
|
|
image_path = os.path.join(CONF.tenant_images_dir, image_name)
|
|
if os.path.exists(image_path):
|
|
image_mount_point = os.path.join(self.rsync_dir, image_name)
|
|
self.image_mount_point = image_mount_point
|
|
utils.ensure_tree(image_mount_point)
|
|
sh.sudo.mount('-o', 'loop,ro', image_path, image_mount_point)
|
|
if not os.path.exists('{0}/etc/passwd'.format(
|
|
image_mount_point)):
|
|
raise exception.ImageMountError()
|
|
else:
|
|
raise exception.ImageNotFound(image_name=image_name,
|
|
directory=CONF.tenant_images_dir)
|
|
return url
|
|
|
|
def _save_provision_json_for_node(self, deploy_config):
|
|
prov_json = json.dumps(deploy_config)
|
|
path = os.path.join(self.node.workdir, "provision.json")
|
|
with open(path, "w") as f:
|
|
f.write(prov_json)
|
|
return path
|
|
|
|
def _teardown_webserver(self):
|
|
LOG.info("Stopping stub web server ...")
|
|
try:
|
|
self.webserver.terminate()
|
|
for i in range(0, 15):
|
|
if self.webserver.poll() is not None:
|
|
LOG.info("Stub web server has stopped.")
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
LOG.warning(
|
|
'15 seconds have passed since sending SIGTERM to the stub '
|
|
'web server. It is still alive. Send SIGKILL.')
|
|
self.webserver.kill()
|
|
self.webserver.wait() # collect zombie
|
|
except OSError as e:
|
|
if e.errno == errno.ESRCH:
|
|
return
|
|
raise
|
|
|
|
def _delete_workdir(self):
|
|
LOG.info("Deleting workdir {0}".format(CONF.ramdisk_func_test_workdir))
|
|
try:
|
|
shutil.rmtree(CONF.ramdisk_func_test_workdir)
|
|
except OSError as e:
|
|
if e.errno != errno.ENOENT:
|
|
raise
|
|
|
|
def _delete_node_workdir(self, node):
|
|
wdir = node.workdir
|
|
LOG.info("Deleting node workdir {0}".format(wdir))
|
|
shutil.rmtree(wdir)
|
|
|
|
def _check_rsync(self):
|
|
|
|
rsync_config_path = "/etc/rsyncd.conf"
|
|
rsync_ironic_section_name = 'ironic_rsync'
|
|
|
|
if not utils._pid_of('rsync'):
|
|
raise exception.RsyncProcessNotFound()
|
|
|
|
if os.path.exists(rsync_config_path):
|
|
cfg = utils.read_config(rsync_config_path)
|
|
else:
|
|
raise exception.RsyncConfigNotFound(path=rsync_config_path)
|
|
|
|
if rsync_ironic_section_name in cfg.sections():
|
|
self.rsync_dir = cfg.get(rsync_ironic_section_name, 'path')
|
|
else:
|
|
raise exception.RsyncIronicSectionNotFound(
|
|
section=rsync_ironic_section_name
|
|
)
|
|
|
|
def _teardown_rsync(self):
|
|
if self.image_mount_point:
|
|
sh.sudo.umount(self.image_mount_point)
|
|
sh.rmdir(self.image_mount_point)
|
|
self.image_mount_point = None
|
|
|
|
@classmethod
|
|
def _load_config(cls, path):
|
|
if cls._loaded_config == path:
|
|
return
|
|
|
|
LOG.debug('Load ramdisk-func-test configuration')
|
|
args = {}
|
|
if path:
|
|
args['default_config_files'] = [path]
|
|
conf.CONF([], project=conf.PROJECT_NAME, **args)
|
|
|
|
# configure log level for libs we are using
|
|
for channel, level in [
|
|
('paramiko', logging.WARN),
|
|
('ironic.openstack.common', logging.WARN)]:
|
|
logger = logging.getLogger(channel)
|
|
logger.setLevel(level)
|
|
|
|
cls._loaded_config = path
|