From f6dbb3929d904b4d5a9ee01f8270051e29ac1ec3 Mon Sep 17 00:00:00 2001 From: Tim Rozet Date: Mon, 4 Dec 2017 11:20:23 -0500 Subject: Enables containerized overcloud deployments Changes Include: - For upstream deployments, Docker local registry will be updated with latest current RDO containers, regular deployments will use latest stable - Upstream container images will then be patched/modified and then re-uploaded into local docker registry with 'apex' tag - Deployment command modified to deploy with containers - Adds a --no-fetch deployment argument to disable pulling latest from upstream, and instead using what already exists in cache - Moves Undercloud NAT setup to just after undercloud is installed. This provides internet during overcloud install which is now required for upstream container deployments. - Creates loop device for Ceph deployment when no device is provided in deploy settings (for container deployment only) - Updates NIC J2 template to use the new format in OOO since the os-apply-config method is now deprecated in > Queens JIRA: APEX-566 JIRA: APEX-549 Change-Id: I0652c194c059b915a942ac7401936e8f5c69d1fa Signed-off-by: Tim Rozet --- apex/build_utils.py | 27 ++- apex/builders/common_builder.py | 90 +++++++- apex/builders/exceptions.py | 12 ++ apex/builders/overcloud_builder.py | 73 ++++++- apex/builders/undercloud_builder.py | 7 + apex/common/constants.py | 16 ++ apex/common/exceptions.py | 4 + apex/common/utils.py | 63 +++++- apex/deploy.py | 97 ++++++--- apex/overcloud/deploy.py | 172 ++++++++++++++-- apex/settings/deploy_settings.py | 3 +- apex/tests/config/98faaca.diff | 331 ++++++++++++++++++++++++++++++ apex/tests/test_apex_build_utils.py | 21 +- apex/tests/test_apex_common_builder.py | 69 +++++++ apex/tests/test_apex_common_utils.py | 48 +++++ apex/tests/test_apex_deploy.py | 70 ++++++- apex/tests/test_apex_overcloud_builder.py | 65 +++++- apex/tests/test_apex_overcloud_deploy.py | 159 ++++++++++++-- apex/tests/test_apex_undercloud.py | 29 ++- apex/undercloud/undercloud.py | 15 +- 20 files changed, 1264 insertions(+), 107 deletions(-) create mode 100644 apex/builders/exceptions.py create mode 100644 apex/tests/config/98faaca.diff (limited to 'apex') diff --git a/apex/build_utils.py b/apex/build_utils.py index c9d8472e..1c413dfd 100644 --- a/apex/build_utils.py +++ b/apex/build_utils.py @@ -90,6 +90,31 @@ def clone_fork(args): logging.info('Checked out commit:\n{}'.format(ws.head.commit.message)) +def strip_patch_sections(patch, sections=['releasenotes']): + """ + Removes patch sections from a diff which contain a file path + :param patch: patch to strip + :param sections: list of keywords to use to strip out of the patch file + :return: stripped patch + """ + + append_line = True + tmp_patch = [] + for line in patch.split("\n"): + if re.match('diff\s', line): + for section in sections: + if re.search(section, line): + logging.debug("Stripping {} from patch: {}".format( + section, line)) + append_line = False + break + else: + append_line = True + if append_line: + tmp_patch.append(line) + return '\n'.join(tmp_patch) + + def get_patch(change_id, repo, branch, url=con.OPENSTACK_GERRIT): logging.info("Fetching patch for change id {}".format(change_id)) change = get_change(url, repo, branch, change_id) @@ -100,7 +125,7 @@ def get_patch(change_id, repo, branch, url=con.OPENSTACK_GERRIT): change_id) patch_url = "changes/{}/revisions/{}/patch".format(change_path, current_revision) - return rest.get(patch_url) + return strip_patch_sections(rest.get(patch_url)) def get_parser(): diff --git a/apex/builders/common_builder.py b/apex/builders/common_builder.py index fd3bcc3d..05a81efe 100644 --- a/apex/builders/common_builder.py +++ b/apex/builders/common_builder.py @@ -10,11 +10,16 @@ # Common building utilities for undercloud and overcloud import git +import json import logging import os +import re +import apex.builders.overcloud_builder as oc_builder from apex import build_utils +from apex.builders import exceptions as exc from apex.common import constants as con +from apex.common import utils from apex.virtual import utils as virt_utils @@ -35,9 +40,38 @@ def project_to_path(project): return "/usr/lib/python2.7/site-packages/{}".format(project) +def project_to_docker_image(project): + """ + Translates OpenStack project to OOO services that are containerized + :param project: name of OpenStack project + :return: List of OOO docker service names + """ + # Fetch all docker containers in docker hub with tripleo and filter + # based on project + hub_output = utils.open_webpage(con.DOCKERHUB_OOO, timeout=10) + try: + results = json.loads(hub_output.decode())['results'] + except Exception as e: + logging.error("Unable to parse docker hub output for" + "tripleoupstream repository") + logging.debug("HTTP response from dockerhub:\n{}".format(hub_output)) + raise exc.ApexCommonBuilderException( + "Failed to parse docker image info from Docker Hub: {}".format(e)) + logging.debug("Docker Hub tripleoupstream entities found: {}".format( + results)) + docker_images = list() + for result in results: + if result['name'].startswith("centos-binary-{}".format(project)): + # add as docker image shortname (just service name) + docker_images.append(result['name'].replace('centos-binary-', '')) + + return docker_images + + def add_upstream_patches(patches, image, tmp_dir, default_branch=os.path.join('stable', - con.DEFAULT_OS_VERSION)): + con.DEFAULT_OS_VERSION), + uc_ip=None, docker_tag=None): """ Adds patches from upstream OpenStack gerrit to Undercloud for deployment :param patches: list of patches @@ -45,10 +79,13 @@ def add_upstream_patches(patches, image, tmp_dir, :param tmp_dir: to store temporary patch files :param default_branch: default branch to fetch commit (if not specified in patch) - :return: None + :param uc_ip: undercloud IP (required only for docker patches) + :param docker_tag: Docker Tag (required only for docker patches) + :return: Set of docker services patched (if applicable) """ virt_ops = [{con.VIRT_INSTALL: 'patch'}] logging.debug("Evaluating upstream patches:\n{}".format(patches)) + docker_services = set() for patch in patches: assert isinstance(patch, dict) assert all(i in patch.keys() for i in ['project', 'change-id']) @@ -60,21 +97,52 @@ def add_upstream_patches(patches, image, tmp_dir, patch['project'], branch) if patch_diff: patch_file = "{}.patch".format(patch['change-id']) - patch_file_path = os.path.join(tmp_dir, patch_file) + project_path = project_to_path(patch['project']) + # If docker tag and python we know this patch belongs on docker + # container for a docker service. Therefore we build the dockerfile + # and move the patch into the containers directory. We also assume + # this builder call is for overcloud, because we do not support + # undercloud containers + if docker_tag and 'python' in project_path: + # Projects map to multiple THT services, need to check which + # are supported + ooo_docker_services = project_to_docker_image(patch['project']) + else: + ooo_docker_services = [] + # If we found services, then we treat the patch like it applies to + # docker only + if ooo_docker_services: + os_version = default_branch.replace('stable/', '') + for service in ooo_docker_services: + docker_services = docker_services.union({service}) + docker_cmds = [ + "WORKDIR {}".format(project_path), + "ADD {} {}".format(patch_file, project_path), + "RUN patch -p1 < {}".format(patch_file) + ] + src_img_uri = "{}:8787/{}/centos-binary-{}:" \ + "{}".format(uc_ip, os_version, service, + docker_tag) + oc_builder.build_dockerfile(service, tmp_dir, docker_cmds, + src_img_uri) + patch_file_path = os.path.join(tmp_dir, 'containers', + patch_file) + else: + patch_file_path = os.path.join(tmp_dir, patch_file) + virt_ops.extend([ + {con.VIRT_UPLOAD: "{}:{}".format(patch_file_path, + project_path)}, + {con.VIRT_RUN_CMD: "cd {} && patch -p1 < {}".format( + project_path, patch_file)}]) + logging.info("Adding patch {} to {}".format(patch_file, + image)) with open(patch_file_path, 'w') as fh: fh.write(patch_diff) - project_path = project_to_path(patch['project']) - virt_ops.extend([ - {con.VIRT_UPLOAD: "{}:{}".format(patch_file_path, - project_path)}, - {con.VIRT_RUN_CMD: "cd {} && patch -p1 < {}".format( - project_path, patch_file)}]) - logging.info("Adding patch {} to {}".format(patch_file, - image)) else: logging.info("Ignoring patch:\n{}".format(patch)) if len(virt_ops) > 1: virt_utils.virt_customize(virt_ops, image) + return docker_services def add_repo(repo_url, repo_name, image, tmp_dir): diff --git a/apex/builders/exceptions.py b/apex/builders/exceptions.py new file mode 100644 index 00000000..b88f02bf --- /dev/null +++ b/apex/builders/exceptions.py @@ -0,0 +1,12 @@ +############################################################################## +# Copyright (c) 2018 Tim Rozet (trozet@redhat.com) and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +class ApexCommonBuilderException(Exception): + pass diff --git a/apex/builders/overcloud_builder.py b/apex/builders/overcloud_builder.py index e7b07963..a84d100b 100644 --- a/apex/builders/overcloud_builder.py +++ b/apex/builders/overcloud_builder.py @@ -10,13 +10,17 @@ # Used to modify overcloud qcow2 image import logging +import os +import tarfile -from apex.builders import common_builder as c_builder +import apex.builders.common_builder from apex.common import constants as con +from apex.common.exceptions import ApexBuildException from apex.virtual import utils as virt_utils -def inject_opendaylight(odl_version, image, tmp_dir): +def inject_opendaylight(odl_version, image, tmp_dir, uc_ip, + os_version, docker_tag=None): assert odl_version in con.VALID_ODL_VERSIONS # add repo if odl_version == 'master': @@ -28,18 +32,77 @@ def inject_opendaylight(odl_version, image, tmp_dir): odl_url = "https://nexus.opendaylight.org/content/repositories" \ "/opendaylight-{}-epel-7-x86_64-devel/".format(odl_pkg_version) repo_name = "opendaylight-{}".format(odl_pkg_version) - c_builder.add_repo(odl_url, repo_name, image, tmp_dir) + apex.builders.common_builder.add_repo(odl_url, repo_name, image, tmp_dir) # download puppet-opendaylight - archive = c_builder.create_git_archive( + archive = apex.builders.common_builder.create_git_archive( repo_url=con.PUPPET_ODL_URL, repo_name='puppet-opendaylight', tmp_dir=tmp_dir, branch=branch, prefix='opendaylight/') # install ODL, puppet-odl virt_ops = [ - {con.VIRT_INSTALL: 'opendaylight'}, {con.VIRT_UPLOAD: "{}:/etc/puppet/modules/".format(archive)}, {con.VIRT_RUN_CMD: 'rm -rf /etc/puppet/modules/opendaylight'}, {con.VIRT_RUN_CMD: "cd /etc/puppet/modules/ && tar xvf " "puppet-opendaylight.tar"} ] + if docker_tag: + docker_cmds = [ + "RUN yum remove opendaylight -y", + "RUN echo $'[opendaylight]\\n\\", + "baseurl={}\\n\\".format(odl_url), + "gpgcheck=0\\n\\", + "enabled=1' > /etc/yum.repos.d/opendaylight.repo", + "RUN yum -y install opendaylight" + ] + src_img_uri = "{}:8787/{}/centos-binary-{}:" \ + "{}".format(uc_ip, os_version, 'opendaylight', + docker_tag) + build_dockerfile('opendaylight', tmp_dir, docker_cmds, src_img_uri) + else: + virt_ops.append({con.VIRT_INSTALL: 'opendaylight'}) virt_utils.virt_customize(virt_ops, image) logging.info("OpenDaylight injected into {}".format(image)) + + +def build_dockerfile(service, tmp_dir, docker_cmds, src_image_uri): + """ + Builds docker file per service and stores it in a + tmp_dir/containers/ directory. If the Dockerfile already exists, + simply append the docker cmds to it. + :param service: name of sub-directory to store Dockerfile in + :param tmp_dir: Temporary directory to store the container's dockerfile in + :param docker_cmds: List of commands to insert into the dockerfile + :param src_image_uri: Docker URI format for where the source image exists + :return: None + """ + logging.debug("Building Dockerfile for {} with docker_cmds: {}".format( + service, docker_cmds)) + c_dir = os.path.join(tmp_dir, 'containers') + service_dir = os.path.join(c_dir, service) + if not os.path.isdir(service_dir): + os.makedirs(service_dir, exist_ok=True) + from_cmd = "FROM {}\n".format(src_image_uri) + service_file = os.path.join(service_dir, 'Dockerfile') + assert isinstance(docker_cmds, list) + if os.path.isfile(service_file): + append_cmds = True + else: + append_cmds = False + with open(service_file, "a+") as fh: + if not append_cmds: + fh.write(from_cmd) + fh.write('\n'.join(docker_cmds)) + + +def archive_docker_patches(tmp_dir): + """ + Archives Overcloud docker patches into a tar file for upload to Undercloud + :param tmp_dir: temporary directory where containers folder is stored + :return: None + """ + container_path = os.path.join(tmp_dir, 'containers') + if not os.path.isdir(container_path): + raise ApexBuildException("Docker directory for patches not found: " + "{}".format(container_path)) + archive_file = os.path.join(tmp_dir, 'docker_patches.tar.gz') + with tarfile.open(archive_file, "w:gz") as tar: + tar.add(container_path, arcname=os.path.basename(container_path)) diff --git a/apex/builders/undercloud_builder.py b/apex/builders/undercloud_builder.py index baba8a55..268bad7f 100644 --- a/apex/builders/undercloud_builder.py +++ b/apex/builders/undercloud_builder.py @@ -20,6 +20,11 @@ def add_upstream_packages(image): :return: None """ virt_ops = list() + # FIXME(trozet): we have to lock to this beta ceph ansible package because + # the current RPM versioning is wrong and an older package has a higher + # version than this package. We should change to just 'ceph-ansible' + # once the package/repo has been fixed. Note: luminous is fine here + # because Apex will only support container deployment for Queens and later pkgs = [ 'openstack-utils', 'ceph-common', @@ -29,6 +34,8 @@ def add_upstream_packages(image): 'docker-distribution', 'openstack-tripleo-validations', 'libguestfs-tools', + 'http://mirror.centos.org/centos/7/storage/x86_64/ceph-luminous' + + '/ceph-ansible-3.1.0-0.beta3.1.el7.noarch.rpm' ] for pkg in pkgs: diff --git a/apex/common/constants.py b/apex/common/constants.py index a2b9a634..0aa6a6ca 100644 --- a/apex/common/constants.py +++ b/apex/common/constants.py @@ -39,10 +39,14 @@ VIRT_PW = '--root-password' THT_DIR = '/usr/share/openstack-tripleo-heat-templates' THT_ENV_DIR = os.path.join(THT_DIR, 'environments') +THT_DOCKER_ENV_DIR = os.path.join(THT_ENV_DIR, 'services-docker') DEFAULT_OS_VERSION = 'pike' DEFAULT_ODL_VERSION = 'nitrogen' VALID_ODL_VERSIONS = ['carbon', 'nitrogen', 'oxygen', 'master'] +CEPH_VERSION_MAP = {'pike': 'jewel', + 'queens': 'luminous', + 'master': 'luminous'} PUPPET_ODL_URL = 'https://git.opendaylight.org/gerrit/integration/packaging' \ '/puppet-opendaylight' DEBUG_OVERCLOUD_PW = 'opnfvapex' @@ -50,3 +54,15 @@ NET_ENV_FILE = 'network-environment.yaml' DEPLOY_TIMEOUT = 90 UPSTREAM_RDO = 'https://images.rdoproject.org/pike/delorean/current-tripleo/' OPENSTACK_GERRIT = 'https://review.openstack.org' + +DOCKER_TAG = 'current-tripleo-rdo' +# Maps regular service files to docker versions +# None value means mapping is same as key +VALID_DOCKER_SERVICES = { + 'neutron-opendaylight.yaml': None, + 'neutron-opendaylight-dpdk.yaml': None, + 'neutron-opendaylight-sriov.yaml': None, + 'neutron-ml2-ovn.yaml': 'neutron-ovn.yaml' +} +DOCKERHUB_OOO = ('https://registry.hub.docker.com/v2/repositories' + '/tripleoupstream/?page_size=1024') diff --git a/apex/common/exceptions.py b/apex/common/exceptions.py index 54d99834..a4d390a4 100644 --- a/apex/common/exceptions.py +++ b/apex/common/exceptions.py @@ -18,3 +18,7 @@ class JumpHostNetworkException(Exception): class ApexCleanException(Exception): pass + + +class ApexBuildException(Exception): + pass diff --git a/apex/common/utils.py b/apex/common/utils.py index b727b11a..cb7cbe13 100644 --- a/apex/common/utils.py +++ b/apex/common/utils.py @@ -22,6 +22,8 @@ import urllib.request import urllib.parse import yaml +from apex.common import exceptions as exc + def str2bool(var): if isinstance(var, bool): @@ -139,30 +141,45 @@ def run_ansible(ansible_vars, playbook, host='localhost', user='root', raise Exception(e) -def fetch_upstream_and_unpack(dest, url, targets): +def fetch_upstream_and_unpack(dest, url, targets, fetch=True): """ Fetches targets from a url destination and downloads them if they are newer. Also unpacks tar files in dest dir. :param dest: Directory to download and unpack files to :param url: URL where target files are located :param targets: List of target files to download + :param fetch: Whether or not to fetch latest from internet (boolean) :return: None """ os.makedirs(dest, exist_ok=True) assert isinstance(targets, list) for target in targets: - download_target = True target_url = urllib.parse.urljoin(url, target) target_dest = os.path.join(dest, target) - logging.debug("Fetching and comparing upstream target: \n{}".format( - target_url)) - try: - u = urllib.request.urlopen(target_url) - except urllib.error.URLError as e: - logging.error("Failed to fetch target url. Error: {}".format( - e.reason)) - raise - if os.path.isfile(target_dest): + target_exists = os.path.isfile(target_dest) + if fetch: + download_target = True + elif not target_exists: + logging.warning("no-fetch requested but target: {} is not " + "cached, will download".format(target_dest)) + download_target = True + else: + logging.info("no-fetch requested and previous cache exists for " + "target: {}. Will skip download".format(target_dest)) + download_target = False + + if download_target: + logging.debug("Fetching and comparing upstream" + " target: \n{}".format(target_url)) + try: + u = urllib.request.urlopen(target_url) + except urllib.error.URLError as e: + logging.error("Failed to fetch target url. Error: {}".format( + e.reason)) + raise + # Check if previous file and fetch we need to compare files to + # determine if download is necessary + if target_exists and download_target: logging.debug("Previous file found: {}".format(target_dest)) metadata = u.info() headers = metadata.items() @@ -186,6 +203,7 @@ def fetch_upstream_and_unpack(dest, url, targets): download_target = False else: logging.debug('Unable to find last modified url date') + if download_target: urllib.request.urlretrieve(target_url, filename=target_dest) logging.info("Target downloaded: {}".format(target)) @@ -220,3 +238,26 @@ def internet_connectivity(): except (urllib.request.URLError, socket.timeout): logging.debug('No internet connectivity detected') return False + + +def open_webpage(url, timeout=5): + try: + response = urllib.request.urlopen(url, timeout=timeout) + return response.read() + except (urllib.request.URLError, socket.timeout): + logging.error("Unable to open URL: {}".format(url)) + raise + + +def edit_tht_env(env_file, section, settings): + assert isinstance(settings, dict) + with open(env_file) as fh: + data = yaml.safe_load(fh) + + if section not in data.keys(): + data[section] = {} + for setting, value in settings.items(): + data[section][setting] = value + with open(env_file, 'w') as fh: + yaml.safe_dump(data, fh, default_flow_style=False) + logging.debug("Data written to env file {}:\n{}".format(env_file, data)) diff --git a/apex/deploy.py b/apex/deploy.py index b9267a3c..7bc5568f 100644 --- a/apex/deploy.py +++ b/apex/deploy.py @@ -21,13 +21,13 @@ import tempfile import apex.virtual.configure_vm as vm_lib import apex.virtual.utils as virt_utils +import apex.builders.common_builder as c_builder +import apex.builders.overcloud_builder as oc_builder +import apex.builders.undercloud_builder as uc_builder from apex import DeploySettings from apex import Inventory from apex import NetworkEnvironment from apex import NetworkSettings -from apex.builders import common_builder as c_builder -from apex.builders import overcloud_builder as oc_builder -from apex.builders import undercloud_builder as uc_builder from apex.common import utils from apex.common import constants from apex.common import parsers @@ -181,6 +181,10 @@ def create_deploy_parser(): default=False, help='Force deployment to use upstream ' 'artifacts') + deploy_parser.add_argument('--no-fetch', action='store_true', + default=False, + help='Ignore fetching latest upstream and ' + 'use what is in cache') return deploy_parser @@ -352,15 +356,9 @@ def main(): constants.DEFAULT_OS_VERSION, os_version) upstream_targets = ['overcloud-full.tar', 'undercloud.qcow2'] utils.fetch_upstream_and_unpack(args.image_dir, upstream_url, - upstream_targets) + upstream_targets, + fetch=not args.no_fetch) sdn_image = os.path.join(args.image_dir, 'overcloud-full.qcow2') - if ds_opts['sdn_controller'] == 'opendaylight': - logging.info("Preparing upstream image with OpenDaylight") - oc_builder.inject_opendaylight( - odl_version=ds_opts['odl_version'], - image=sdn_image, - tmp_dir=APEX_TEMP_DIR - ) # copy undercloud so we don't taint upstream fetch uc_image = os.path.join(args.image_dir, 'undercloud_mod.qcow2') uc_fetch_img = os.path.join(args.image_dir, 'undercloud.qcow2') @@ -372,12 +370,12 @@ def main(): patches = deploy_settings['global_params']['patches'] c_builder.add_upstream_patches(patches['undercloud'], uc_image, APEX_TEMP_DIR, branch) - logging.info('Adding patches to overcloud') - c_builder.add_upstream_patches(patches['overcloud'], sdn_image, - APEX_TEMP_DIR, branch) else: sdn_image = os.path.join(args.image_dir, SDN_IMAGE) uc_image = 'undercloud.qcow2' + # patches are ignored in non-upstream deployments + patches = {'overcloud': [], 'undercloud': []} + # Create/Start Undercloud VM undercloud = uc_lib.Undercloud(args.image_dir, args.deploy_dir, root_pw=root_pw, @@ -385,6 +383,13 @@ def main(): image_name=os.path.basename(uc_image), os_version=os_version) undercloud.start() + undercloud_admin_ip = net_settings['networks'][ + constants.ADMIN_NETWORK]['installer_vm']['ip'] + + if upstream and ds_opts['containers']: + tag = constants.DOCKER_TAG + else: + tag = None # Generate nic templates for role in 'compute', 'controller': @@ -394,7 +399,7 @@ def main(): undercloud.configure(net_settings, deploy_settings, os.path.join(args.lib_dir, ANSIBLE_PATH, 'configure_undercloud.yml'), - APEX_TEMP_DIR) + APEX_TEMP_DIR, virtual_oc=args.virtual) # Prepare overcloud-full.qcow2 logging.info("Preparing Overcloud for deployment...") @@ -410,22 +415,57 @@ def main(): args.env_file = 'upstream-environment.yaml' opnfv_env = os.path.join(args.deploy_dir, args.env_file) if not upstream: + # TODO(trozet): Invoke with containers after Fraser migration oc_deploy.prep_env(deploy_settings, net_settings, inventory, opnfv_env, net_env_target, APEX_TEMP_DIR) - oc_deploy.prep_image(deploy_settings, net_settings, sdn_image, - APEX_TEMP_DIR, root_pw=root_pw) else: - shutil.copyfile(sdn_image, os.path.join(APEX_TEMP_DIR, - 'overcloud-full.qcow2')) shutil.copyfile( opnfv_env, os.path.join(APEX_TEMP_DIR, os.path.basename(opnfv_env)) ) + patched_containers = oc_deploy.prep_image( + deploy_settings, net_settings, sdn_image, APEX_TEMP_DIR, + root_pw=root_pw, docker_tag=tag, patches=patches['overcloud'], + upstream=upstream) oc_deploy.create_deploy_cmd(deploy_settings, net_settings, inventory, APEX_TEMP_DIR, args.virtual, os.path.basename(opnfv_env), net_data=net_data) + # Prepare undercloud with containers + docker_playbook = os.path.join(args.lib_dir, ANSIBLE_PATH, + 'prepare_overcloud_containers.yml') + if ds_opts['containers']: + ceph_version = constants.CEPH_VERSION_MAP[ds_opts['os_version']] + ceph_docker_image = "ceph/daemon:tag-build-master-" \ + "{}-centos-7".format(ceph_version) + logging.info("Preparing Undercloud with Docker containers") + if patched_containers: + oc_builder.archive_docker_patches(APEX_TEMP_DIR) + container_vars = dict() + container_vars['apex_temp_dir'] = APEX_TEMP_DIR + container_vars['patched_docker_services'] = list( + patched_containers) + container_vars['container_tag'] = constants.DOCKER_TAG + container_vars['stackrc'] = 'source /home/stack/stackrc' + container_vars['upstream'] = upstream + container_vars['sdn'] = ds_opts['sdn_controller'] + container_vars['undercloud_ip'] = undercloud_admin_ip + container_vars['os_version'] = os_version + container_vars['ceph_docker_image'] = ceph_docker_image + container_vars['sdn_env_file'] = \ + oc_deploy.get_docker_sdn_file(ds_opts) + try: + utils.run_ansible(container_vars, docker_playbook, + host=undercloud.ip, user='stack', + tmp_dir=APEX_TEMP_DIR) + logging.info("Container preparation complete") + except Exception: + logging.error("Unable to complete container prep on " + "Undercloud") + os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2')) + raise + deploy_playbook = os.path.join(args.lib_dir, ANSIBLE_PATH, 'deploy_overcloud.yml') virt_env = 'virtual-environment.yaml' @@ -494,19 +534,14 @@ def main(): else: deploy_vars['congress'] = False deploy_vars['calipso'] = ds_opts.get('calipso', False) - deploy_vars['calipso_ip'] = net_settings['networks']['admin'][ - 'installer_vm']['ip'] - # TODO(trozet): this is probably redundant with getting external - # network info from undercloud.py - if 'external' in net_settings.enabled_network_list: - ext_cidr = net_settings['networks']['external'][0]['cidr'] + deploy_vars['calipso_ip'] = undercloud_admin_ip + # overcloudrc.v3 removed and set as default in queens and later + if os_version == 'pike': + deploy_vars['overcloudrc_files'] = ['overcloudrc', + 'overcloudrc.v3'] else: - ext_cidr = net_settings['networks']['admin']['cidr'] - deploy_vars['external_cidr'] = str(ext_cidr) - if ext_cidr.version == 6: - deploy_vars['external_network_ipv6'] = True - else: - deploy_vars['external_network_ipv6'] = False + deploy_vars['overcloudrc_files'] = ['overcloudrc'] + post_undercloud = os.path.join(args.lib_dir, ANSIBLE_PATH, 'post_deploy_undercloud.yml') logging.info("Executing post deploy configuration undercloud playbook") diff --git a/apex/overcloud/deploy.py b/apex/overcloud/deploy.py index 33641ed5..0701c188 100644 --- a/apex/overcloud/deploy.py +++ b/apex/overcloud/deploy.py @@ -16,10 +16,13 @@ import shutil import uuid import struct import time +import apex.builders.overcloud_builder as oc_builder +import apex.builders.common_builder as c_builder from apex.common import constants as con from apex.common.exceptions import ApexDeployException from apex.common import parsers +from apex.common import utils from apex.virtual import utils as virt_utils from cryptography.hazmat.primitives import serialization as \ crypto_serialization @@ -72,6 +75,21 @@ OVS_NSH_RPM = "openvswitch-2.6.1-1.el7.centos.x86_64.rpm" ODL_NETVIRT_VPP_RPM = "/root/opendaylight-7.0.0-0.1.20170531snap665.el7" \ ".noarch.rpm" +LOSETUP_SERVICE = """[Unit] +Description=Setup loop devices +Before=network.target + +[Service] +Type=oneshot +ExecStart=/sbin/losetup /dev/loop3 /srv/data.img +ExecStop=/sbin/losetup -d /dev/loop3 +TimeoutSec=60 +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +""" + def build_sdn_env_list(ds, sdn_map, env_list=None): """ @@ -118,6 +136,25 @@ def build_sdn_env_list(ds, sdn_map, env_list=None): return env_list +def get_docker_sdn_file(ds_opts): + """ + Returns docker env file for detected SDN + :param ds_opts: deploy options + :return: docker THT env file for an SDN + """ + # FIXME(trozet): We assume right now there is only one docker SDN file + docker_services = con.VALID_DOCKER_SERVICES + sdn_env_list = build_sdn_env_list(ds_opts, SDN_FILE_MAP) + for sdn_file in sdn_env_list: + sdn_base = os.path.basename(sdn_file) + if sdn_base in docker_services: + if docker_services[sdn_base] is not None: + return os.path.join(con.THT_DOCKER_ENV_DIR, + docker_services[sdn_base]) + else: + return os.path.join(con.THT_DOCKER_ENV_DIR, sdn_base) + + def create_deploy_cmd(ds, ns, inv, tmp_dir, virtual, env_file='opnfv-environment.yaml', net_data=False): @@ -125,26 +162,47 @@ def create_deploy_cmd(ds, ns, inv, tmp_dir, logging.info("Creating deployment command") deploy_options = ['network-environment.yaml'] + ds_opts = ds['deploy_options'] + + if ds_opts['containers']: + deploy_options.append(os.path.join(con.THT_ENV_DIR, + 'docker.yaml')) + + if ds['global_params']['ha_enabled']: + if ds_opts['containers']: + deploy_options.append(os.path.join(con.THT_ENV_DIR, + 'docker-ha.yaml')) + else: + deploy_options.append(os.path.join(con.THT_ENV_DIR, + 'puppet-pacemaker.yaml')) + if env_file: deploy_options.append(env_file) - ds_opts = ds['deploy_options'] - deploy_options += build_sdn_env_list(ds_opts, SDN_FILE_MAP) + + if ds_opts['containers']: + deploy_options.append('docker-images.yaml') + sdn_docker_file = get_docker_sdn_file(ds_opts) + if sdn_docker_file: + deploy_options.append(sdn_docker_file) + deploy_options.append('sdn-images.yaml') + else: + deploy_options += build_sdn_env_list(ds_opts, SDN_FILE_MAP) for k, v in OTHER_FILE_MAP.items(): if k in ds_opts and ds_opts[k]: - deploy_options.append(os.path.join(con.THT_ENV_DIR, v)) + if ds_opts['containers']: + deploy_options.append(os.path.join(con.THT_DOCKER_ENV_DIR, + "{}.yaml".format(k))) + else: + deploy_options.append(os.path.join(con.THT_ENV_DIR, v)) if ds_opts['ceph']: - prep_storage_env(ds, tmp_dir) + prep_storage_env(ds, ns, virtual, tmp_dir) deploy_options.append(os.path.join(con.THT_ENV_DIR, 'storage-environment.yaml')) if ds_opts['sriov']: prep_sriov_env(ds, tmp_dir) - if ds['global_params']['ha_enabled']: - deploy_options.append(os.path.join(con.THT_ENV_DIR, - 'puppet-pacemaker.yaml')) - if virtual: deploy_options.append('virtual-environment.yaml') else: @@ -190,7 +248,8 @@ def create_deploy_cmd(ds, ns, inv, tmp_dir, return cmd -def prep_image(ds, ns, img, tmp_dir, root_pw=None): +def prep_image(ds, ns, img, tmp_dir, root_pw=None, docker_tag=None, + patches=None, upstream=False): """ Locates sdn image and preps for deployment. :param ds: deploy settings @@ -198,6 +257,9 @@ def prep_image(ds, ns, img, tmp_dir, root_pw=None): :param img: sdn image :param tmp_dir: dir to store modified sdn image :param root_pw: password to configure for overcloud image + :param docker_tag: Docker image tag for RDO version (default None) + :param patches: List of patches to apply to overcloud image + :param upstream: (boolean) Indicates if upstream deployment or not :return: None """ # TODO(trozet): Come up with a better way to organize this logic in this @@ -210,6 +272,7 @@ def prep_image(ds, ns, img, tmp_dir, root_pw=None): ds_opts = ds['deploy_options'] virt_cmds = list() sdn = ds_opts['sdn_controller'] + patched_containers = set() # we need this due to rhbz #1436021 # fixed in systemd-219-37.el7 if sdn is not False: @@ -298,7 +361,13 @@ def prep_image(ds, ns, img, tmp_dir, root_pw=None): "/root/nosdn_vpp_rpms/*.rpm"} ]) - if sdn == 'opendaylight': + tmp_oc_image = os.path.join(tmp_dir, 'overcloud-full.qcow2') + shutil.copyfile(img, tmp_oc_image) + logging.debug("Temporary overcloud image stored as: {}".format( + tmp_oc_image)) + + # TODO (trozet): remove this if block after Fraser + if sdn == 'opendaylight' and not upstream: if ds_opts['odl_version'] != con.DEFAULT_ODL_VERSION: virt_cmds.extend([ {con.VIRT_RUN_CMD: "yum -y remove opendaylight"}, @@ -325,6 +394,19 @@ def prep_image(ds, ns, img, tmp_dir, root_pw=None): {con.VIRT_RUN_CMD: "yum -y install /root/{}/*".format( ODL_NETVIRT_VPP_RPM)} ]) + elif sdn == 'opendaylight': + undercloud_admin_ip = ns['networks'][con.ADMIN_NETWORK][ + 'installer_vm']['ip'] + oc_builder.inject_opendaylight( + odl_version=ds_opts['odl_version'], + image=tmp_oc_image, + tmp_dir=tmp_dir, + uc_ip=undercloud_admin_ip, + os_version=ds_opts['os_version'], + docker_tag=docker_tag, + ) + if docker_tag: + patched_containers = patched_containers.union({'opendaylight'}) if sdn == 'ovn': virt_cmds.extend([ @@ -334,12 +416,35 @@ def prep_image(ds, ns, img, tmp_dir, root_pw=None): "*openvswitch*"} ]) - tmp_oc_image = os.path.join(tmp_dir, 'overcloud-full.qcow2') - shutil.copyfile(img, tmp_oc_image) - logging.debug("Temporary overcloud image stored as: {}".format( - tmp_oc_image)) + if patches: + if ds_opts['os_version'] == 'master': + branch = ds_opts['os_version'] + else: + branch = "stable/{}".format(ds_opts['os_version']) + logging.info('Adding patches to overcloud') + patched_containers = patched_containers.union( + c_builder.add_upstream_patches(patches, + tmp_oc_image, tmp_dir, + branch, + uc_ip=undercloud_admin_ip, + docker_tag=docker_tag)) + # if containers with ceph, and no ceph device we need to use a + # persistent loop device for Ceph OSDs + if docker_tag and not ds_opts.get('ceph_device', None): + tmp_losetup = os.path.join(tmp_dir, 'losetup.service') + with open(tmp_losetup, 'w') as fh: + fh.write(LOSETUP_SERVICE) + virt_cmds.extend([ + {con.VIRT_UPLOAD: "{}:/usr/lib/systemd/system/".format(tmp_losetup) + }, + {con.VIRT_RUN_CMD: 'truncate /srv/data.img --size 10G'}, + {con.VIRT_RUN_CMD: 'mkfs.ext4 -F /srv/data.img'}, + {con.VIRT_RUN_CMD: 'systemctl daemon-reload'}, + {con.VIRT_RUN_CMD: 'systemctl enable losetup.service'}, + ]) virt_utils.virt_customize(virt_cmds, tmp_oc_image) logging.info("Overcloud image customization complete") + return patched_containers def make_ssh_key(): @@ -541,11 +646,13 @@ def generate_ceph_key(): return base64.b64encode(header + key) -def prep_storage_env(ds, tmp_dir): +def prep_storage_env(ds, ns, virtual, tmp_dir): """ Creates storage environment file for deployment. Source file is copied by undercloud playbook to host. :param ds: + :param ns: + :param virtual: :param tmp_dir: :return: """ @@ -572,7 +679,40 @@ def prep_storage_env(ds, tmp_dir): 'utf-8'))) else: print(line) - if 'ceph_device' in ds_opts and ds_opts['ceph_device']: + + if ds_opts['containers']: + undercloud_admin_ip = ns['networks'][con.ADMIN_NETWORK][ + 'installer_vm']['ip'] + ceph_version = con.CEPH_VERSION_MAP[ds_opts['os_version']] + docker_image = "{}:8787/ceph/daemon:tag-build-master-" \ + "{}-centos-7".format(undercloud_admin_ip, + ceph_version) + ceph_params = { + 'DockerCephDaemonImage': docker_image, + } + if not ds['global_params']['ha_enabled']: + ceph_params['CephPoolDefaultSize'] = 1 + + if virtual: + ceph_params['CephAnsibleExtraConfig'] = { + 'centos_package_dependencies': [], + 'ceph_osd_docker_memory_limit': '1g', + 'ceph_mds_docker_memory_limit': '1g', + } + ceph_params['CephPoolDefaultPgNum'] = 32 + if 'ceph_device' in ds_opts and ds_opts['ceph_device']: + ceph_device = ds_opts['ceph_device'] + else: + # TODO(trozet): make this DS default after Fraser + ceph_device = '/dev/loop3' + + ceph_params['CephAnsibleDisksConfig'] = { + 'devices': [ceph_device], + 'journal_size': 512, + 'osd_scenario': 'collocated' + } + utils.edit_tht_env(storage_file, 'parameter_defaults', ceph_params) + elif 'ceph_device' in ds_opts and ds_opts['ceph_device']: with open(storage_file, 'a') as fh: fh.write(' ExtraConfig:\n') fh.write(" ceph::profile::params::osds:{{{}:{{}}}}\n".format( diff --git a/apex/settings/deploy_settings.py b/apex/settings/deploy_settings.py index c05922bf..4f887ed0 100644 --- a/apex/settings/deploy_settings.py +++ b/apex/settings/deploy_settings.py @@ -25,7 +25,8 @@ REQ_DEPLOY_SETTINGS = ['sdn_controller', 'rt_kvm', 'os_version', 'l2gw', - 'sriov'] + 'sriov', + 'containers'] OPT_DEPLOY_SETTINGS = ['performance', 'vsperf', diff --git a/apex/tests/config/98faaca.diff b/apex/tests/config/98faaca.diff new file mode 100644 index 00000000..68a66fbc --- /dev/null +++ b/apex/tests/config/98faaca.diff @@ -0,0 +1,331 @@ +From 98faacad44e39a456d9fe1a1d21f5a65e8de4fc1 Mon Sep 17 00:00:00 2001 +From: Janki Chhatbar +Date: Tue, 23 Jan 2018 22:43:49 +0530 +Subject: [PATCH] Minor update steps for ODL + +Updating OpenStack (within release) means updating ODL from v1 to v1.1. +This is done by "openstack overcloud update" which collects +update_tasks. ODL needs 2 different steps to achieve this +minor update. These are called Level1 and Level2. L1 is +simple - stop ODL, update, start. This is taken care by paunch +and no separate implementation is needed. L2 has extra steps +which are implemented in update_tasks and post_update_tasks. + +Updating ODL within the same major release (1->1.1) consists of either +L1 or L2 steps. These steps are decided from ODLUpdateLevel parameter +specified in environments/services-docker/update-odl.yaml. + +Upgrading ODL to the next major release (1.1->2) requires +only the L2 steps. These are implemented as upgrade_tasks and +post_upgrade_tasks in https://review.openstack.org/489201. + +Steps involved in level 2 update are + 1. Block OVS instances to connect to ODL + 2. Set ODL upgrade flag to True + 3. Start ODL + 4. Start Neutron re-sync and wait for it to finish + 5. Delete OVS groups and ports + 6. Stop OVS + 7. Unblock OVS ports + 8. Start OVS + 9. Unset ODL upgrade flag + +These steps are exactly same as upgrade_tasks. +The logic implemented is: +follow upgrade_tasks; when update_level == 2 + +Change-Id: Ie532800663dd24313a7350b5583a5080ddb796e7 +--- + +diff --git a/common/deploy-steps.j2 b/common/deploy-steps.j2 +index 595e16c..c4fb05f 100644 +--- a/common/deploy-steps.j2 ++++ b/common/deploy-steps.j2 +@@ -23,6 +23,7 @@ + {% set post_upgrade_steps_max = 4 -%} + {% set fast_forward_upgrade_steps_max = 9 -%} + {% set fast_forward_upgrade_prep_steps_max = 3 -%} ++{% set post_update_steps_max = 4 -%} + + heat_template_version: queens + +@@ -590,3 +591,15 @@ + - include_tasks: {{role.name}}/fast_forward_upgrade_tasks.yaml + when: role_name == '{{role.name}}' and ansible_hostname == {{role.name}}[0] + {%- endfor %} ++ post_update_steps_tasks: | ++{%- for role in roles %} ++ - include: {{role.name}}/post_update_tasks.yaml ++ when: role_name == '{{role.name}}' ++{%- endfor %} ++ post_update_steps_playbook: | ++ - hosts: overcloud ++ tasks: ++ - include: post_update_steps_tasks.yaml ++ with_sequence: start=0 end={{post_update_steps_max-1}} ++ loop_control: ++ loop_var: step +diff --git a/common/services.yaml b/common/services.yaml +index 2a62c1b..c197b05 100644 +--- a/common/services.yaml ++++ b/common/services.yaml +@@ -283,6 +283,16 @@ + expression: coalesce($.data, []).where($ != null).select($.get('update_tasks')).where($ != null).flatten().distinct() + data: {get_attr: [ServiceChain, role_data]} + ++ PostUpdateTasks: ++ type: OS::Heat::Value ++ properties: ++ type: comma_delimited_list ++ value: ++ yaql: ++ # Note we use distinct() here to filter any identical tasks, e.g yum update for all services ++ expression: coalesce($.data, []).where($ != null).select($.get('post_update_tasks')).where($ != null).flatten().distinct() ++ data: {get_attr: [ServiceChain, role_data]} ++ + UpgradeBatchTasks: + type: OS::Heat::Value + properties: +@@ -349,6 +359,7 @@ + upgrade_tasks: {get_attr: [UpgradeTasks, value]} + post_upgrade_tasks: {get_attr: [PostUpgradeTasks, value]} + update_tasks: {get_attr: [UpdateTasks, value]} ++ post_update_tasks: {get_attr: [PostUpdateTasks, value]} + upgrade_batch_tasks: {get_attr: [UpgradeBatchTasks, value]} + service_metadata_settings: {get_attr: [ServiceServerMetadataHook, metadata]} + +diff --git a/docker/services/opendaylight-api.yaml b/docker/services/opendaylight-api.yaml +index 6175db9..3cafe53 100644 +--- a/docker/services/opendaylight-api.yaml ++++ b/docker/services/opendaylight-api.yaml +@@ -44,6 +44,14 @@ + type: string + description: Specifies the default CA cert to use if TLS is used for + services in the internal network. ++ ODLUpdateLevel: ++ default: 1 ++ description: Specify the level of update ++ type: number ++ constraints: ++ - allowed_values: ++ - 1 ++ - 2 + + conditions: + +@@ -167,23 +175,25 @@ + - opendaylight_enabled.rc == 0 + service: name=opendaylight state=stopped enabled=no + # Containarised deployment upgrade steps +- - name: remove journal and snapshots +- when: step|int == 0 +- file: +- path: /var/lib/opendaylight/{{item}} +- state: absent +- with_items: +- - snapshots +- - journal +- - name: Set ODL upgrade flag to True +- copy: +- dest: /var/lib/opendaylight/etc/opendaylight/datastore/initial/config/genius-mdsalutil-config.xml +- content: | +- +- true +- +- when: step|int == 1 +- post_upgrade_tasks: ++ - name: ODL container L2 update and upgrade tasks ++ block: &odl_container_upgrade_tasks ++ - name: remove journal and snapshots ++ when: step|int == 0 ++ file: ++ path: /var/lib/opendaylight/{{item}} ++ state: absent ++ with_items: ++ - snapshots ++ - journal ++ - name: Set ODL upgrade flag to True ++ copy: ++ dest: /var/lib/opendaylight/etc/opendaylight/datastore/initial/config/genius-mdsalutil-config.xml ++ content: | ++ ++ true ++ ++ when: step|int == 1 ++ post_upgrade_tasks: &odl_container_post_upgrade_tasks + - name: Unset upgrade flag in ODL + shell: + str_replace: +@@ -192,7 +202,20 @@ + -H "Content-Type: application/json" \ + $ODL_URI/restconf/config/genius-mdsalutil:config' + params: +- $ODL_USERNAME: {get_param: [OpenDaylightBase, OpenDaylightUsername]} +- $ODL_PASSWORD: {get_param: [OpenDaylightBase, OpenDaylightPassword]} ++ $ODL_USERNAME: {get_attr: [OpenDaylightBase, role_data, config_settings, 'opendaylight::username']} ++ $ODL_PASSWORD: {get_attr: [OpenDaylightBase, role_data, config_settings, 'opendaylight::password']} + $ODL_URI: {get_param: [EndpointMap, OpenDaylightInternal, uri]} + when: step|int == 0 ++ update_tasks: ++ - name: Get ODL update level ++ block: &get_odl_update_level ++ - name: store update level to update_level variable ++ set_fact: ++ odl_update_level: {get_param: ODLUpdateLevel} ++ - name: Run L2 update tasks that are similar to upgrade_tasks when update level is 2 ++ block: *odl_container_upgrade_tasks ++ when: odl_update_level == 2 ++ post_update_tasks: ++ - block: *get_odl_update_level ++ - block: *odl_container_post_upgrade_tasks ++ when: odl_update_level == 2 +\ No newline at end of file +diff --git a/environments/services-docker/update-odl.yaml b/environments/services-docker/update-odl.yaml +new file mode 100644 +index 0000000..87d74ef +--- /dev/null ++++ b/environments/services-docker/update-odl.yaml +@@ -0,0 +1,11 @@ ++# This file describes parameters needed for ODL update. ++# This file is to be used along with other env files during ++# level 2 minor update. ++# Level 2 update involves yang changes in ODL within same ODL release and ++# hence needs DB wipe and resync. ++# Level 1 is simple update - stop ODL, pull new image, start ODL ++# This file is not be used during level1 update or major upgrade. ++# In case doubt, please reach out to ODL developers on #tripleo IRC channel ++ ++parameter_defaults: ++ ODLUpdateLevel: 2 +\ No newline at end of file +diff --git a/puppet/services/opendaylight-ovs.yaml b/puppet/services/opendaylight-ovs.yaml +index 3390645..958e1bb 100644 +--- a/puppet/services/opendaylight-ovs.yaml ++++ b/puppet/services/opendaylight-ovs.yaml +@@ -104,6 +104,14 @@ + type: string + description: Specifies the default CA cert to use if TLS is used for + services in the internal network. ++ ODLUpdateLevel: ++ default: 1 ++ description: Specify the level of update ++ type: number ++ constraints: ++ - allowed_values: ++ - 1 ++ - 2 + + parameter_groups: + - label: deprecated +@@ -230,14 +238,16 @@ + - openvswitch_enabled.rc == 0 + service: name=openvswitch state=stopped + # Container upgrade steps. +- - name: Block connections to ODL. #This rule will be inserted at the top. +- iptables: chain=OUTPUT action=insert protocol=tcp destination_port={{ item }} jump=DROP +- when: step|int == 0 +- with_items: +- - 6640 +- - 6653 +- - 6633 +- post_upgrade_tasks: ++ - name: ODL container L2 update and upgrade tasks ++ block: &odl_container_upgrade_tasks ++ - name: Block connections to ODL. #This rule will be inserted at the top. ++ iptables: chain=OUTPUT action=insert protocol=tcp destination_port={{ item }} jump=DROP ++ when: step|int == 0 ++ with_items: ++ - 6640 ++ - 6653 ++ - 6633 ++ post_upgrade_tasks: &odl_container_post_upgrade_tasks + - name: Check service openvswitch is running + command: systemctl is-active --quiet openvswitch + tags: common +@@ -260,6 +270,20 @@ + - name: start openvswitch service + when: step|int == 3 + service : name=openvswitch state=started ++ update_tasks: ++ - name: Get ODL update level ++ block: &get_odl_update_level ++ - name: store update level to update_level variable ++ set_fact: ++ odl_update_level: {get_param: ODLUpdateLevel} ++ - name: Run L2 update tasks that are similar to upgrade_tasks when update level is 2 ++ block: *odl_container_upgrade_tasks ++ when: odl_update_level == 2 ++ post_update_tasks: ++ - block: *get_odl_update_level ++ - block: *odl_container_post_upgrade_tasks ++ when: odl_update_level == 2 ++ + metadata_settings: + if: + - internal_tls_enabled +@@ -267,4 +291,4 @@ + - service: ovs + network: {get_param: [ServiceNetMap, OpendaylightApiNetwork]} + type: node +- - null ++ - null +\ No newline at end of file +diff --git a/releasenotes/notes/odl_upgrade-f5540d242b9a6b52.yaml b/releasenotes/notes/odl_upgrade-f5540d242b9a6b52.yaml +index 45703d0..e2943de 100644 +--- a/releasenotes/notes/odl_upgrade-f5540d242b9a6b52.yaml ++++ b/releasenotes/notes/odl_upgrade-f5540d242b9a6b52.yaml +@@ -1,6 +1,6 @@ + --- + +-features: ++upgrade: + - Add ODL upgradability + Steps of upgrade are as follows + 1. Block OVS instances to connect to ODL done in upgrade_tasks +diff --git a/releasenotes/notes/update_odl-cb997ce5c136ebb7.yaml b/releasenotes/notes/update_odl-cb997ce5c136ebb7.yaml +new file mode 100644 +index 0000000..1bcf8ed +--- /dev/null ++++ b/releasenotes/notes/update_odl-cb997ce5c136ebb7.yaml +@@ -0,0 +1,19 @@ ++--- ++features: ++ - Minor update ODL steps are added. ODL minor update (within same ODL ++ release) can have 2 different workflow. These are called level 1 and ++ level2. Level 1 is simple - stop, update and start ODL. Level 2 is ++ complex and involved yang model changes. This requires wiping of ++ DB and resync to repopulate the data. ++ Steps involved in level 2 update are ++ 1. Block OVS instances to connect to ODL ++ 2. Set ODL upgrade flag to True ++ 3. Start ODL ++ 4. Start Neutron re-sync and wait for it to finish ++ 5. Delete OVS groups and ports ++ 6. Stop OVS ++ 7. Unblock OVS ports ++ 8. Start OVS ++ 9. Unset ODL upgrade flag ++ To achieve L2 update, use "-e environments/services-docker/ ++ update-odl.yaml" along with other env files to the update command. +\ No newline at end of file +diff --git a/tools/yaml-validate.py b/tools/yaml-validate.py +index 59473f5..9ab6a87 100755 +--- a/tools/yaml-validate.py ++++ b/tools/yaml-validate.py +@@ -46,11 +46,11 @@ + OPTIONAL_DOCKER_SECTIONS = ['docker_puppet_tasks', 'upgrade_tasks', + 'fast_forward_upgrade_tasks', + 'post_upgrade_tasks', 'update_tasks', +- 'service_config_settings', 'host_prep_tasks', +- 'metadata_settings', 'kolla_config', +- 'global_config_settings', 'logging_source', +- 'logging_groups', 'external_deploy_tasks', +- 'external_post_deploy_tasks', ++ 'post_update_tasks', 'service_config_settings', ++ 'host_prep_tasks', 'metadata_settings', ++ 'kolla_config', 'global_config_settings', ++ 'logging_source', 'logging_groups', ++ 'external_deploy_tasks', 'external_post_deploy_tasks', + 'docker_config_scripts', 'step_config'] + REQUIRED_DOCKER_PUPPET_CONFIG_SECTIONS = ['config_volume', 'step_config', + 'config_image'] diff --git a/apex/tests/test_apex_build_utils.py b/apex/tests/test_apex_build_utils.py index d9d542d6..f18103c8 100644 --- a/apex/tests/test_apex_build_utils.py +++ b/apex/tests/test_apex_build_utils.py @@ -9,17 +9,20 @@ import argparse import git +import os +import unittest from mock import patch from apex import build_utils +from apex.tests import constants as con from nose.tools import ( assert_is_instance, assert_raises) -class TestBuildUtils(object): +class TestBuildUtils(unittest.TestCase): @classmethod def setup_class(cls): """This method is run once for each class before any tests are run""" @@ -165,3 +168,19 @@ class TestBuildUtils(object): def test_main_debug(self, mock_get_parser): with patch.object(build_utils.sys, 'argv', self.sys_argv_debug): build_utils.main() + + def test_strip_patch_sections(self): + with open(os.path.join(con.TEST_DUMMY_CONFIG, '98faaca.diff')) as fh: + dummy_patch = fh.read() + tmp_patch = build_utils.strip_patch_sections(dummy_patch) + self.assertNotRegex(tmp_patch, 'releasenotes') + self.assertNotRegex(tmp_patch, 'Minor update ODL steps') + self.assertNotRegex(tmp_patch, 'Steps of upgrade are as follows') + self.assertNotRegex(tmp_patch, 'Steps invlolved in level 2 update') + + def test_strip_no_patch_sections(self): + with open(os.path.join(con.TEST_DUMMY_CONFIG, '98faaca.diff')) as fh: + dummy_patch = fh.read() + tmp_patch = build_utils.strip_patch_sections(dummy_patch, + sections=[]) + self.assertEqual(dummy_patch, tmp_patch) diff --git a/apex/tests/test_apex_common_builder.py b/apex/tests/test_apex_common_builder.py index c32f72c9..d501746c 100644 --- a/apex/tests/test_apex_common_builder.py +++ b/apex/tests/test_apex_common_builder.py @@ -10,11 +10,20 @@ import unittest from apex.builders import common_builder as c_builder +from apex.builders import exceptions from apex.common import constants as con from mock import patch from mock import mock_open from mock import MagicMock +DOCKER_YAML = { + 'resource_registry': { + 'OS::TripleO::Services::NovaApi': '../docker/services/nova-api.yaml', + 'OS::TripleO::Services::NovaConductor': + '../docker/services/nova-conductor.yaml' + } +} + class TestCommonBuilder(unittest.TestCase): @classmethod @@ -67,6 +76,54 @@ class TestCommonBuilder(unittest.TestCase): c_builder.add_upstream_patches(patches, 'dummy.qcow2', '/dummytmp/') mock_customize.assert_called_once_with(test_virt_ops, 'dummy.qcow2') + @patch('builtins.open', mock_open()) + @patch('apex.build_utils.get_patch') + @patch('apex.virtual.utils.virt_customize') + def test_add_upstream_patches_docker_puppet( + self, mock_customize, mock_get_patch): + change_id = 'I301370fbf47a71291614dd60e4c64adc7b5ebb42' + patches = [{ + 'change-id': change_id, + 'project': 'openstack/puppet-tripleo' + }] + project_path = '/etc/puppet/modules/tripleo' + patch_file = "{}.patch".format(change_id) + patch_file_path = "/dummytmp/{}".format(patch_file) + test_virt_ops = [ + {con.VIRT_INSTALL: 'patch'}, + {con.VIRT_UPLOAD: "{}:{}".format(patch_file_path, + project_path)}, + {con.VIRT_RUN_CMD: "cd {} && patch -p1 < {}".format( + project_path, patch_file)}] + mock_get_patch.return_value = 'some random diff' + c_builder.add_upstream_patches(patches, 'dummy.qcow2', '/dummytmp/', + uc_ip='192.0.2.1', + docker_tag='latest') + mock_customize.assert_called_once_with(test_virt_ops, 'dummy.qcow2') + + @patch('builtins.open', mock_open()) + @patch('apex.builders.common_builder.project_to_docker_image') + @patch('apex.builders.overcloud_builder.build_dockerfile') + @patch('apex.build_utils.get_patch') + @patch('apex.virtual.utils.virt_customize') + def test_add_upstream_patches_docker_python( + self, mock_customize, mock_get_patch, mock_build_docker_file, + mock_project2docker): + mock_project2docker.return_value = ['NovaApi'] + change_id = 'I301370fbf47a71291614dd60e4c64adc7b5ebb42' + patches = [{ + 'change-id': change_id, + 'project': 'openstack/nova' + }] + mock_get_patch.return_value = 'some random diff' + services = c_builder.add_upstream_patches(patches, 'dummy.qcow2', + '/dummytmp/', + uc_ip='192.0.2.1', + docker_tag='latest') + assert mock_customize.not_called + assert mock_build_docker_file.called + self.assertSetEqual(services, {'NovaApi'}) + @patch('builtins.open', mock_open()) @patch('apex.virtual.utils.virt_customize') def test_add_repo(self, mock_customize): @@ -85,3 +142,15 @@ class TestCommonBuilder(unittest.TestCase): self.assertEqual(c_builder.create_git_archive('fake/url', 'dummyrepo', '/dummytmp/'), '/dummytmp/dummyrepo.tar') + + def test_project_to_docker_image(self): + found_services = c_builder.project_to_docker_image(project='nova') + assert 'nova-api' in found_services + + @patch('apex.common.utils.open_webpage') + def test_project_to_docker_image_bad_web_content( + self, mock_open_web): + mock_open_web.return_value = b'{"blah": "blah"}' + self.assertRaises(exceptions.ApexCommonBuilderException, + c_builder.project_to_docker_image, + 'nova') diff --git a/apex/tests/test_apex_common_utils.py b/apex/tests/test_apex_common_utils.py index 6f2a9476..0e4041ca 100644 --- a/apex/tests/test_apex_common_utils.py +++ b/apex/tests/test_apex_common_utils.py @@ -12,12 +12,14 @@ import os import shutil import urllib.error +from apex.common import exceptions from apex.common import utils from apex.settings.network_settings import NetworkSettings from apex.tests.constants import ( TEST_CONFIG_DIR, TEST_PLAYBOOK_DIR) +from mock import patch, mock_open from nose.tools import ( assert_equal, assert_is_instance, @@ -25,6 +27,7 @@ from nose.tools import ( assert_raises) NET_SETS = os.path.join(TEST_CONFIG_DIR, 'network', 'network_settings.yaml') +a_mock_open = mock_open(read_data=None) class TestCommonUtils: @@ -100,3 +103,48 @@ class TestCommonUtils: url, ['dummy_test.tar']) assert os.path.isfile('/tmp/fetch_test/test.txt') shutil.rmtree('/tmp/fetch_test') + + def test_nofetch_upstream_and_unpack(self): + test_file = 'overcloud-full.tar.md5' + url = 'https://images.rdoproject.org/master/delorean/' \ + 'current-tripleo/stable/' + os.makedirs('/tmp/fetch_test', exist_ok=True) + target = "/tmp/fetch_test/{}".format(test_file) + open(target, 'w').close() + target_mtime = os.path.getmtime(target) + utils.fetch_upstream_and_unpack('/tmp/fetch_test', + url, [test_file], fetch=False) + post_target_mtime = os.path.getmtime(target) + shutil.rmtree('/tmp/fetch_test') + assert_equal(target_mtime, post_target_mtime) + + def test_nofetch_upstream_and_unpack_no_target(self): + test_file = 'overcloud-full.tar.md5' + url = 'https://images.rdoproject.org/master/delorean/' \ + 'current-tripleo/stable/' + utils.fetch_upstream_and_unpack('/tmp/fetch_test', + url, [test_file]) + assert os.path.isfile("/tmp/fetch_test/{}".format(test_file)) + shutil.rmtree('/tmp/fetch_test') + + def test_open_webpage(self): + output = utils.open_webpage('http://opnfv.org') + assert output is not None + + def test_open_invalid_webpage(self): + assert_raises(urllib.request.URLError, utils.open_webpage, + 'http://inv4lIdweb-page.com') + + @patch('builtins.open', a_mock_open) + @patch('yaml.safe_dump') + @patch('yaml.safe_load') + def test_edit_tht_env(self, mock_yaml_load, mock_yaml_dump): + settings = {'SomeParameter': 'some_value'} + mock_yaml_load.return_value = { + 'parameter_defaults': {'SomeParameter': 'dummy'} + } + utils.edit_tht_env('/dummy-environment.yaml', 'parameter_defaults', + settings) + new_data = {'parameter_defaults': settings} + mock_yaml_dump.assert_called_once_with(new_data, a_mock_open(), + default_flow_style=False) diff --git a/apex/tests/test_apex_deploy.py b/apex/tests/test_apex_deploy.py index 403b7099..6c2a185d 100644 --- a/apex/tests/test_apex_deploy.py +++ b/apex/tests/test_apex_deploy.py @@ -143,7 +143,8 @@ class TestDeploy(unittest.TestCase): 'sfc': False, 'vpn': False, 'yardstick': 'test', - 'os_version': DEFAULT_OS_VERSION}} + 'os_version': DEFAULT_OS_VERSION, + 'containers': False}} args = mock_parser.return_value.parse_args.return_value args.virtual = False args.quickstart = False @@ -216,7 +217,8 @@ class TestDeploy(unittest.TestCase): 'sfc': False, 'vpn': False, 'yardstick': 'test', - 'os_version': DEFAULT_OS_VERSION}} + 'os_version': DEFAULT_OS_VERSION, + 'containers': False}} args = mock_parser.return_value.parse_args.return_value args.virtual = True args.quickstart = False @@ -236,3 +238,67 @@ class TestDeploy(unittest.TestCase): args.virt_compute_ram = 16 args.virt_default_ram = 10 main() + + @patch('apex.deploy.c_builder') + @patch('apex.deploy.uc_builder') + @patch('apex.deploy.oc_builder') + @patch('apex.deploy.network_data.create_network_data') + @patch('apex.deploy.shutil') + @patch('apex.deploy.oc_deploy') + @patch('apex.deploy.uc_lib') + @patch('apex.deploy.build_vms') + @patch('apex.deploy.Inventory') + @patch('apex.deploy.virt_utils') + @patch('apex.deploy.oc_cfg') + @patch('apex.deploy.parsers') + @patch('apex.deploy.utils') + @patch('apex.deploy.NetworkEnvironment') + @patch('apex.deploy.NetworkSettings') + @patch('apex.deploy.DeploySettings') + @patch('apex.deploy.os') + @patch('apex.deploy.json') + @patch('apex.deploy.jumphost') + @patch('apex.deploy.validate_cross_settings') + @patch('apex.deploy.validate_deploy_args') + @patch('apex.deploy.create_deploy_parser') + @patch('builtins.open', a_mock_open, create=True) + def test_main_virt_containers_upstream( + self, mock_parser, mock_val_args, mock_cross_sets, mock_jumphost, + mock_json, mock_os, mock_deploy_sets, mock_net_sets, mock_net_env, + mock_utils, mock_parsers, mock_oc_cfg, mock_virt_utils, + mock_inv, mock_build_vms, mock_uc_lib, mock_oc_deploy, + mock_shutil, mock_network_data, mock_oc_builder, + mock_uc_builder, mock_c_builder): + + ds_opts_dict = {'global_params': MagicMock(), + 'deploy_options': {'gluon': False, + 'congress': False, + 'sdn_controller': 'opendaylight', + 'dataplane': 'ovs', + 'sfc': False, + 'vpn': False, + 'yardstick': 'test', + 'os_version': DEFAULT_OS_VERSION, + 'containers': True}} + args = mock_parser.return_value.parse_args.return_value + args.virtual = True + args.quickstart = False + args.debug = True + args.virt_default_ram = 10 + args.ha_enabled = True + args.virt_compute_nodes = 1 + args.virt_compute_ram = None + args.virt_default_ram = 12 + args.upstream = True + net_sets = mock_net_sets.return_value + net_sets.enabled_network_list = ['admin'] + deploy_sets = mock_deploy_sets.return_value + deploy_sets.__getitem__.side_effect = ds_opts_dict.__getitem__ + deploy_sets.__contains__.side_effect = ds_opts_dict.__contains__ + main() + args.virt_compute_ram = 16 + args.virt_default_ram = 10 + main() + mock_oc_deploy.prep_image.assert_called + # TODO(trozet) add assertions here with arguments for functions in + # deploy main diff --git a/apex/tests/test_apex_overcloud_builder.py b/apex/tests/test_apex_overcloud_builder.py index e9a6e6cf..46b5f871 100644 --- a/apex/tests/test_apex_overcloud_builder.py +++ b/apex/tests/test_apex_overcloud_builder.py @@ -11,7 +11,9 @@ import unittest from apex.builders import overcloud_builder as oc_builder from apex.common import constants as con -from mock import patch +from mock import patch, mock_open + +a_mock_open = mock_open(read_data=None) class TestOvercloudBuilder(unittest.TestCase): @@ -37,14 +39,69 @@ class TestOvercloudBuilder(unittest.TestCase): mock_git_archive.return_value = '/dummytmp/puppet-opendaylight.tar' archive = '/dummytmp/puppet-opendaylight.tar' test_virt_ops = [ - {con.VIRT_INSTALL: 'opendaylight'}, {con.VIRT_UPLOAD: "{}:/etc/puppet/modules/".format(archive)}, {con.VIRT_RUN_CMD: 'rm -rf /etc/puppet/modules/opendaylight'}, {con.VIRT_RUN_CMD: "cd /etc/puppet/modules/ && tar xvf " - "puppet-opendaylight.tar"} + "puppet-opendaylight.tar"}, + {con.VIRT_INSTALL: 'opendaylight'} ] oc_builder.inject_opendaylight(con.DEFAULT_ODL_VERSION, 'dummy.qcow2', - '/dummytmp/') + '/dummytmp/', uc_ip='192.0.2.2', + os_version=con.DEFAULT_OS_VERSION) + assert mock_git_archive.called + assert mock_add_repo.called + mock_customize.assert_called_once_with(test_virt_ops, 'dummy.qcow2') + + @patch('apex.builders.overcloud_builder.build_dockerfile') + @patch('apex.builders.common_builder.create_git_archive') + @patch('apex.builders.common_builder.add_repo') + @patch('apex.virtual.utils.virt_customize') + def test_inject_opendaylight_docker(self, mock_customize, mock_add_repo, + mock_git_archive, mock_build_docker): + mock_git_archive.return_value = '/dummytmp/puppet-opendaylight.tar' + archive = '/dummytmp/puppet-opendaylight.tar' + test_virt_ops = [ + {con.VIRT_UPLOAD: "{}:/etc/puppet/modules/".format(archive)}, + {con.VIRT_RUN_CMD: 'rm -rf /etc/puppet/modules/opendaylight'}, + {con.VIRT_RUN_CMD: "cd /etc/puppet/modules/ && tar xvf " + "puppet-opendaylight.tar"}, + ] + oc_builder.inject_opendaylight('oxygen', 'dummy.qcow2', + '/dummytmp/', uc_ip='192.0.2.2', + os_version=con.DEFAULT_OS_VERSION, + docker_tag='latest') + odl_url = "https://nexus.opendaylight.org/content/repositories" \ + "/opendaylight-oxygen-epel-7-x86_64-devel/" + docker_cmds = [ + "RUN yum remove opendaylight -y", + "RUN echo $'[opendaylight]\\n\\", + "baseurl={}\\n\\".format(odl_url), + "gpgcheck=0\\n\\", + "enabled=1' > /etc/yum.repos.d/opendaylight.repo", + "RUN yum -y install opendaylight" + ] + src_img_uri = "192.0.2.1:8787/nova-api/centos-binary-master:latest" assert mock_git_archive.called assert mock_add_repo.called + assert mock_build_docker.called_once_with( + 'opendaylight', '/dummytmp', docker_cmds, src_img_uri + ) mock_customize.assert_called_once_with(test_virt_ops, 'dummy.qcow2') + + @patch('builtins.open', a_mock_open) + @patch('os.makedirs') + @patch('os.path.isfile') + @patch('os.path.isdir') + def test_build_dockerfile(self, mock_isdir, mock_isfile, mock_makedirs): + src_img_uri = "192.0.2.1:8787/nova-api/centos-binary-master:latest" + oc_builder.build_dockerfile('nova-api', '/tmpdummy/', ['RUN dummy'], + src_img_uri) + a_mock_open.assert_called_with( + '/tmpdummy/containers/nova-api/Dockerfile', 'a+') + a_mock_open().write.assert_called_once_with('RUN dummy') + + @patch('tarfile.open') + @patch('os.path.isdir') + def test_archive_docker_patches(self, mock_isdir, mock_tarfile): + oc_builder.archive_docker_patches('/tmpdummy/') + assert mock_tarfile.assert_called diff --git a/apex/tests/test_apex_overcloud_deploy.py b/apex/tests/test_apex_overcloud_deploy.py index b5b1b75e..54f95bb7 100644 --- a/apex/tests/test_apex_overcloud_deploy.py +++ b/apex/tests/test_apex_overcloud_deploy.py @@ -29,6 +29,7 @@ from apex.overcloud.deploy import prep_sriov_env from apex.overcloud.deploy import external_network_cmds from apex.overcloud.deploy import create_congress_cmds from apex.overcloud.deploy import SDN_FILE_MAP +from apex.overcloud.deploy import get_docker_sdn_file from nose.tools import ( assert_regexp_matches, @@ -88,14 +89,17 @@ class TestOvercloudDeploy(unittest.TestCase): def test_create_deploy_cmd(self, mock_sdn_list, mock_prep_storage, mock_prep_sriov): mock_sdn_list.return_value = [] - ds = {'deploy_options': MagicMock(), + ds = {'deploy_options': + {'ha_enabled': True, + 'congress': True, + 'tacker': True, + 'containers': False, + 'barometer': True, + 'ceph': False, + 'sriov': False + }, 'global_params': MagicMock()} - ds['global_params'].__getitem__.side_effect = \ - lambda i: True if i == 'ha_enabled' else MagicMock() - ds['deploy_options'].__getitem__.side_effect = \ - lambda i: True if i == 'congress' else MagicMock() - ds['deploy_options'].__contains__.side_effect = \ - lambda i: True if i == 'congress' else MagicMock() + ns = {'ntp': ['ntp']} inv = MagicMock() inv.get_node_counts.return_value = (3, 2) @@ -109,6 +113,44 @@ class TestOvercloudDeploy(unittest.TestCase): assert_in('--control-scale 3', result_cmd) assert_in('--compute-scale 2', result_cmd) + @patch('apex.overcloud.deploy.prep_sriov_env') + @patch('apex.overcloud.deploy.prep_storage_env') + @patch('builtins.open', mock_open()) + def test_create_deploy_cmd_containers_sdn(self, mock_prep_storage, + mock_prep_sriov): + ds = {'deploy_options': + {'ha_enabled': True, + 'congress': False, + 'tacker': False, + 'containers': True, + 'barometer': False, + 'ceph': True, + 'sdn_controller': 'opendaylight', + 'sriov': False + }, + 'global_params': MagicMock()} + + ns = {'ntp': ['ntp']} + inv = MagicMock() + inv.get_node_counts.return_value = (3, 2) + virt = True + result_cmd = create_deploy_cmd(ds, ns, inv, '/tmp', virt) + assert_in('--ntp-server ntp', result_cmd) + assert_not_in('enable_tacker.yaml', result_cmd) + assert_not_in('enable_congress.yaml', result_cmd) + assert_not_in('enable_barometer.yaml', result_cmd) + assert_in('virtual-environment.yaml', result_cmd) + assert_in('--control-scale 3', result_cmd) + assert_in('--compute-scale 2', result_cmd) + assert_in('docker-images.yaml', result_cmd) + assert_in('sdn-images.yaml', result_cmd) + assert_in('/usr/share/openstack-tripleo-heat-templates/environments' + '/docker.yaml', result_cmd) + assert_in('/usr/share/openstack-tripleo-heat-templates/environments/' + 'storage-environment.yaml', result_cmd) + assert_in('/usr/share/openstack-tripleo-heat-templates/environments' + '/services-docker/neutron-opendaylight.yaml', result_cmd) + @patch('apex.overcloud.deploy.prep_sriov_env') @patch('apex.overcloud.deploy.prep_storage_env') @patch('apex.overcloud.deploy.build_sdn_env_list') @@ -155,7 +197,8 @@ class TestOvercloudDeploy(unittest.TestCase): def test_prep_image(self, mock_os_path, mock_shutil, mock_virt_utils): ds_opts = {'dataplane': 'fdio', 'sdn_controller': 'opendaylight', - 'odl_version': 'master'} + 'odl_version': 'master', + 'sriov': False} ds = {'deploy_options': MagicMock(), 'global_params': MagicMock()} ds['deploy_options'].__getitem__.side_effect = \ @@ -200,6 +243,35 @@ class TestOvercloudDeploy(unittest.TestCase): prep_image(ds, ns, 'undercloud.qcow2', '/tmp', root_pw='test') mock_virt_utils.virt_customize.assert_called() + @patch('apex.overcloud.deploy.c_builder') + @patch('apex.overcloud.deploy.oc_builder') + @patch('apex.overcloud.deploy.virt_utils') + @patch('apex.overcloud.deploy.shutil') + @patch('apex.overcloud.deploy.os.path') + @patch('builtins.open', mock_open()) + def test_prep_image_sdn_odl_upstream_containers_patches( + self, mock_os_path, mock_shutil, mock_virt_utils, + mock_oc_builder, mock_c_builder): + ds_opts = {'dataplane': 'ovs', + 'sdn_controller': 'opendaylight', + 'odl_version': con.DEFAULT_ODL_VERSION, + 'odl_vpp_netvirt': True} + ds = {'deploy_options': MagicMock(), + 'global_params': MagicMock()} + ds['deploy_options'].__getitem__.side_effect = \ + lambda i: ds_opts.get(i, MagicMock()) + ds['deploy_options'].__contains__.side_effect = \ + lambda i: True if i in ds_opts else MagicMock() + ns = MagicMock() + mock_c_builder.add_upstream_patches.return_value = ['nova-api'] + patches = ['dummy_nova_patch'] + rv = prep_image(ds, ns, 'undercloud.qcow2', '/tmp', root_pw='test', + docker_tag='latest', patches=patches, upstream=True) + mock_oc_builder.inject_opendaylight.assert_called + mock_virt_utils.virt_customize.assert_called() + mock_c_builder.add_upstream_patches.assert_called + self.assertListEqual(sorted(rv), ['nova-api', 'opendaylight']) + @patch('apex.overcloud.deploy.virt_utils') @patch('apex.overcloud.deploy.shutil') @patch('apex.overcloud.deploy.os.path') @@ -380,19 +452,60 @@ class TestOvercloudDeploy(unittest.TestCase): mock_ceph_key): mock_fileinput.input.return_value = \ ['CephClusterFSID', 'CephMonKey', 'CephAdminKey', 'random_key'] - ds = {'deploy_options': MagicMock()} - ds['deploy_options'].__getitem__.side_effect = \ - lambda i: '/dev/sdx' if i == 'ceph_device' else MagicMock() - ds['deploy_options'].__contains__.side_effect = \ - lambda i: True if i == 'ceph_device' else MagicMock() - prep_storage_env(ds, '/tmp') + ds = {'deploy_options': { + 'ceph_device': '/dev/sdx', + 'containers': False + }} + ns = {} + prep_storage_env(ds, ns, virtual=False, tmp_dir='/tmp') + + @patch('apex.overcloud.deploy.utils.edit_tht_env') + @patch('apex.overcloud.deploy.generate_ceph_key') + @patch('apex.overcloud.deploy.fileinput') + @patch('apex.overcloud.deploy.os.path.isfile') + @patch('builtins.open', mock_open()) + def test_prep_storage_env_containers(self, mock_isfile, mock_fileinput, + mock_ceph_key, mock_edit_tht): + mock_fileinput.input.return_value = \ + ['CephClusterFSID', 'CephMonKey', 'CephAdminKey', 'random_key'] + ds = {'deploy_options': { + 'ceph_device': '/dev/sdx', + 'containers': True, + 'os_version': 'master' + }, 'global_params': {'ha_enabled': False}} + ns = {'networks': {con.ADMIN_NETWORK: {'installer_vm': + {'ip': '192.0.2.1'}}} + } + prep_storage_env(ds, ns, virtual=True, tmp_dir='/tmp') + ceph_params = { + 'DockerCephDaemonImage': + '192.0.2.1:8787/ceph/daemon:tag-build-master-luminous-centos' + '-7', + 'CephPoolDefaultSize': 1, + 'CephAnsibleExtraConfig': { + 'centos_package_dependencies': [], + 'ceph_osd_docker_memory_limit': '1g', + 'ceph_mds_docker_memory_limit': '1g' + }, + 'CephPoolDefaultPgNum': 32, + 'CephAnsibleDisksConfig': { + 'devices': ['/dev/sdx'], + 'journal_size': 512, + 'osd_scenario': 'collocated' + } + } + mock_edit_tht.assert_called_with('/tmp/storage-environment.yaml', + 'parameter_defaults', + ceph_params) @patch('apex.overcloud.deploy.os.path.isfile') @patch('builtins.open', mock_open()) def test_prep_storage_env_raises(self, mock_isfile): mock_isfile.return_value = False ds = {'deploy_options': MagicMock()} - assert_raises(ApexDeployException, prep_storage_env, ds, '/tmp') + ns = {} + assert_raises(ApexDeployException, prep_storage_env, ds, + ns, virtual=False, tmp_dir='/tmp') @patch('apex.overcloud.deploy.generate_ceph_key') @patch('apex.overcloud.deploy.fileinput') @@ -487,3 +600,19 @@ class TestOvercloudDeploy(unittest.TestCase): def test_create_congress_cmds_raises(self, mock_parsers): mock_parsers.return_value.__getitem__.side_effect = KeyError() assert_raises(KeyError, create_congress_cmds, 'overcloud_file') + + def test_get_docker_sdn_file(self): + ds_opts = {'ha_enabled': True, + 'congress': True, + 'tacker': True, + 'containers': False, + 'barometer': True, + 'ceph': False, + 'sdn_controller': 'opendaylight' + } + output = get_docker_sdn_file(ds_opts) + self.assertEqual(output, + ('/usr/share/openstack-tripleo-heat-templates' + '/environments/services-docker/neutron-opendaylight' + '.yaml') + ) diff --git a/apex/tests/test_apex_undercloud.py b/apex/tests/test_apex_undercloud.py index 0df785f9..9e2752ad 100644 --- a/apex/tests/test_apex_undercloud.py +++ b/apex/tests/test_apex_undercloud.py @@ -7,6 +7,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## +import ipaddress import libvirt import os import subprocess @@ -187,13 +188,27 @@ class TestUndercloud(unittest.TestCase): @patch.object(Undercloud, '_get_vm', return_value=None) @patch.object(Undercloud, 'create') def test_generate_config(self, mock_get_vm, mock_create): - ns_net = MagicMock() - ns_net.__getitem__.side_effect = \ - lambda i: '1234/24' if i is 'cidr' else MagicMock() - ns = {'apex': MagicMock(), - 'dns-domain': 'dns', - 'networks': {'admin': ns_net, - 'external': [ns_net]}} + ns = MagicMock() + ns.enabled_network_list = ['admin', 'external'] + ns_dict = { + 'apex': MagicMock(), + 'dns-domain': 'dns', + 'networks': {'admin': + {'cidr': ipaddress.ip_network('192.0.2.0/24'), + 'installer_vm': {'ip': '192.0.2.1', + 'vlan': 'native'}, + 'dhcp_range': ['192.0.2.15', '192.0.2.30'] + }, + 'external': + [{'enabled': True, + 'cidr': ipaddress.ip_network('192.168.0.0/24'), + 'installer_vm': {'ip': '192.168.0.1', + 'vlan': 'native'} + }] + } + } + ns.__getitem__.side_effect = ns_dict.__getitem__ + ns.__contains__.side_effect = ns_dict.__contains__ ds = {'global_params': {}} Undercloud('img_path', 'tplt_path').generate_config(ns, ds) diff --git a/apex/undercloud/undercloud.py b/apex/undercloud/undercloud.py index 915c85f3..e799d371 100644 --- a/apex/undercloud/undercloud.py +++ b/apex/undercloud/undercloud.py @@ -115,13 +115,14 @@ class Undercloud: "correctly") def configure(self, net_settings, deploy_settings, - playbook, apex_temp_dir): + playbook, apex_temp_dir, virtual_oc=False): """ Configures undercloud VM :param net_settings: Network settings for deployment :param deploy_settings: Deployment settings for deployment :param playbook: playbook to use to configure undercloud :param apex_temp_dir: temporary apex directory to hold configs/logs + :param virtual_oc: Boolean to determine if overcloud is virt :return: None """ @@ -130,6 +131,7 @@ class Undercloud: ansible_vars = Undercloud.generate_config(net_settings, deploy_settings) ansible_vars['apex_temp_dir'] = apex_temp_dir + ansible_vars['virtual_overcloud'] = virtual_oc try: utils.run_ansible(ansible_vars, playbook, host=self.ip, user='stack') @@ -239,7 +241,16 @@ class Undercloud: "prefix": str(ns_external['cidr']).split('/')[1], "enabled": ns_external['enabled'] } - + # TODO(trozet): clean this logic up and merge with above + if 'external' in ns.enabled_network_list: + nat_cidr = ns_external['cidr'] + else: + nat_cidr = ns['networks']['admin']['cidr'] + config['nat_cidr'] = str(nat_cidr) + if nat_cidr.version == 6: + config['nat_network_ipv6'] = True + else: + config['nat_network_ipv6'] = False config['http_proxy'] = ns.get('http_proxy', '') config['https_proxy'] = ns.get('https_proxy', '') -- cgit 1.2.3-korg