# # 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