Attempting to fix NFS issues
[apex.git] / apex / builders / common_builder.py
index 05a81ef..59af94c 100644 (file)
@@ -9,11 +9,16 @@
 
 # Common building utilities for undercloud and overcloud
 
+import datetime
 import git
 import json
 import logging
 import os
+import platform
+import pprint
 import re
+import urllib.parse
+import yaml
 
 import apex.builders.overcloud_builder as oc_builder
 from apex import build_utils
@@ -23,10 +28,11 @@ from apex.common import utils
 from apex.virtual import utils as virt_utils
 
 
-def project_to_path(project):
+def project_to_path(project, patch=None):
     """
-    Translates project to absolute file path
+    Translates project to absolute file path to use in patching
     :param project: name of project
+    :param patch: the patch to applied to the project
     :return: File path
     """
     if project.startswith('openstack/'):
@@ -35,20 +41,37 @@ def project_to_path(project):
         return "/etc/puppet/modules/{}".format(project.replace('puppet-', ''))
     elif 'tripleo-heat-templates' in project:
         return "/usr/share/openstack-tripleo-heat-templates"
+    elif ('tripleo-common' in project and
+          build_utils.is_path_in_patch(patch, 'container-images/')):
+        # tripleo-common has python and another component to it
+        # here we detect if there is a change to the yaml component and if so
+        # treat it like it is not python. This has the caveat of if there
+        # is a patch to both python and yaml this will not work
+        # FIXME(trozet): add ability to split tripleo-common patches that
+        # modify both python and yaml
+        return "/usr/share/openstack-tripleo-common-containers/"
     else:
-        # assume python
-        return "/usr/lib/python2.7/site-packages/{}".format(project)
+        # assume python.  python patches will apply to a project name subdir.
+        # For example, python-tripleoclient patch will apply to the
+        # tripleoclient directory, which is the directory extracted during
+        # python install into the PYTHONPATH.  Therefore we need to just be
+        # in the PYTHONPATH directory to apply a patch
+        return "/usr/lib/python2.7/site-packages/"
 
 
-def project_to_docker_image(project):
+def project_to_docker_image(project, docker_url):
     """
     Translates OpenStack project to OOO services that are containerized
-    :param project: name of OpenStack project
+    :param project: short 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)
+    logging.info("Checking for docker images matching project: {}".format(
+        project))
+    hub_output = utils.open_webpage(
+        urllib.parse.urljoin(docker_url,
+                             '?page_size=1024'), timeout=10)
     try:
         results = json.loads(hub_output.decode())['results']
     except Exception as e:
@@ -63,11 +86,67 @@ def project_to_docker_image(project):
     for result in results:
         if result['name'].startswith("centos-binary-{}".format(project)):
             # add as docker image shortname (just service name)
+            logging.debug("Adding docker image {} for project {} for "
+                          "patching".format(result['name'], project))
             docker_images.append(result['name'].replace('centos-binary-', ''))
 
     return docker_images
 
 
+def is_patch_promoted(change, branch, docker_url, docker_image=None):
+    """
+    Checks to see if a patch that is in merged exists in either the docker
+    container or the promoted tripleo images
+    :param change: gerrit change json output
+    :param branch: branch to use when polling artifacts (does not include
+    stable prefix)
+    :param docker_image: container this applies to if (defaults to None)
+    :return: True if the patch exists in a promoted artifact upstream
+    """
+    assert isinstance(change, dict)
+    assert 'status' in change
+
+    # if not merged we already know this is not closed/abandoned, so we know
+    # this is not promoted
+    if change['status'] != 'MERGED':
+        return False
+    assert 'submitted' in change
+    # drop microseconds cause who cares
+    stime = re.sub('\..*$', '', change['submitted'])
+    submitted_date = datetime.datetime.strptime(stime, "%Y-%m-%d %H:%M:%S")
+    # Patch applies to overcloud/undercloud
+    if docker_image is None:
+        oc_url = urllib.parse.urljoin(
+            con.UPSTREAM_RDO.replace('master', branch), 'overcloud-full.tar')
+        oc_mtime = utils.get_url_modified_date(oc_url)
+        if oc_mtime > submitted_date:
+            logging.debug("oc image was last modified at {}, which is"
+                          "newer than merge date: {}".format(oc_mtime,
+                                                             submitted_date))
+            return True
+    else:
+        # must be a docker patch, check docker tag modified time
+        docker_url = docker_url.replace('tripleomaster',
+                                        "tripleo{}".format(branch))
+        url_path = "{}/tags/{}".format(docker_image, con.DOCKER_TAG)
+        docker_url = urllib.parse.urljoin(docker_url, url_path)
+        logging.debug("docker url is: {}".format(docker_url))
+        docker_output = utils.open_webpage(docker_url, 10)
+        logging.debug('Docker web output: {}'.format(docker_output))
+        hub_mtime = json.loads(docker_output.decode())['last_updated']
+        hub_mtime = re.sub('\..*$', '', hub_mtime)
+        # docker modified time is in this format '2018-06-11T15:23:55.135744Z'
+        # and we drop microseconds
+        hub_dtime = datetime.datetime.strptime(hub_mtime, "%Y-%m-%dT%H:%M:%S")
+        if hub_dtime > submitted_date:
+            logging.debug("docker image: {} was last modified at {}, which is"
+                          "newer than merge date: {}".format(docker_image,
+                                                             hub_dtime,
+                                                             submitted_date))
+            return True
+    return False
+
+
 def add_upstream_patches(patches, image, tmp_dir,
                          default_branch=os.path.join('stable',
                                                      con.DEFAULT_OS_VERSION),
@@ -95,38 +174,75 @@ def add_upstream_patches(patches, image, tmp_dir,
             branch = default_branch
         patch_diff = build_utils.get_patch(patch['change-id'],
                                            patch['project'], branch)
-        if patch_diff:
+        project_path = project_to_path(patch['project'], patch_diff)
+        # 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 platform.machine() == 'aarch64':
+            docker_url = con.DOCKERHUB_AARCH64
+        else:
+            docker_url = con.DOCKERHUB_OOO
+        if docker_tag and 'python' in project_path:
+            # Projects map to multiple THT services, need to check which
+            # are supported
+            project_short_name = os.path.basename(patch['project'])
+            ooo_docker_services = project_to_docker_image(project_short_name,
+                                                          docker_url)
+            if not ooo_docker_services:
+                logging.error("Did not find any matching docker containers "
+                              "for project: {}".format(project_short_name))
+                raise exc.ApexCommonBuilderException(
+                    'Unable to find docker services for python project in '
+                    'patch')
+            # Just use the first image to see if patch was promoted into it
+            docker_img = ooo_docker_services[0]
+        else:
+            ooo_docker_services = []
+            docker_img = None
+        change = build_utils.get_change(con.OPENSTACK_GERRIT,
+                                        patch['project'], branch,
+                                        patch['change-id'])
+        patch_promoted = is_patch_promoted(change,
+                                           branch.replace('stable/', ''),
+                                           docker_url,
+                                           docker_img)
+
+        if patch_diff and not patch_promoted:
             patch_file = "{}.patch".format(patch['change-id'])
-            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 = []
+            patch_file_paths = []
             # 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})
+                    # We need to go root to be able to install patch and then
+                    # switch back to previous user. Some containers that
+                    # have the same name as the project do not necessarily
+                    # contain the project code. For example
+                    # novajoin-notifier does not contain nova package code.
+                    # Therefore we must try to patch and unfortunately
+                    # ignore failures until we have a better way of checking
+                    # this
                     docker_cmds = [
                         "WORKDIR {}".format(project_path),
+                        "USER root",
+                        "ARG REAL_USER",
+                        "RUN yum -y install patch",
                         "ADD {} {}".format(patch_file, project_path),
-                        "RUN patch -p1 < {}".format(patch_file)
+                        "RUN patch -p1 < {} || echo "
+                        "'Patching failed'".format(patch_file),
+                        "USER $REAL_USER"
                     ]
-                    src_img_uri = "{}:8787/{}/centos-binary-{}:" \
+                    src_img_uri = "{}:8787/tripleo{}/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)
+                    patch_file_paths.append(os.path.join(
+                        tmp_dir, "containers/{}".format(service), patch_file))
             else:
                 patch_file_path = os.path.join(tmp_dir, patch_file)
                 virt_ops.extend([
@@ -136,8 +252,10 @@ def add_upstream_patches(patches, image, tmp_dir,
                         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)
+                patch_file_paths.append(patch_file_path)
+            for patch_fp in patch_file_paths:
+                with open(patch_fp, 'w') as fh:
+                    fh.write(patch_diff)
         else:
             logging.info("Ignoring patch:\n{}".format(patch))
     if len(virt_ops) > 1:
@@ -177,3 +295,41 @@ def create_git_archive(repo_url, repo_name, tmp_dir,
         repo.archive(fh, prefix=prefix)
     logging.debug("Wrote archive file: {}".format(archive_path))
     return archive_path
+
+
+def get_neutron_driver(ds_opts):
+    sdn = ds_opts.get('sdn_controller', None)
+
+    if sdn == 'opendaylight':
+        return 'odl'
+    elif sdn == 'ovn':
+        return sdn
+    elif ds_opts.get('vpp', False):
+        return 'vpp'
+    else:
+        return None
+
+
+def prepare_container_images(prep_file, branch='master', neutron_driver=None):
+    if not os.path.isfile(prep_file):
+        raise exc.ApexCommonBuilderException("Prep file does not exist: "
+                                             "{}".format(prep_file))
+    with open(prep_file) as fh:
+        data = yaml.safe_load(fh)
+    try:
+        p_set = data['parameter_defaults']['ContainerImagePrepare'][0]['set']
+        if neutron_driver:
+            p_set['neutron_driver'] = neutron_driver
+        p_set['namespace'] = "docker.io/tripleo{}".format(branch)
+        if platform.machine() == 'aarch64':
+            p_set['namespace'] = "docker.io/armbandapex"
+            p_set['ceph_tag'] = 'v3.1.0-stable-3.1-luminous-centos-7-aarch64'
+
+    except KeyError:
+        logging.error("Invalid prep file format: {}".format(prep_file))
+        raise exc.ApexCommonBuilderException("Invalid format for prep file")
+
+    logging.debug("Writing new container prep file:\n{}".format(
+        pprint.pformat(data)))
+    with open(prep_file, 'w') as fh:
+        yaml.safe_dump(data, fh, default_flow_style=False)