Adam Miller 77c87c8
#!/usr/bin/env python
Adam Miller 77c87c8
# -*- coding: utf-8 -*-
Adam Miller 77c87c8
#
Adam Miller 77c87c8
# push-two-week-atomic.py - An utility to sync two-week Atomic releases
Adam Miller 77c87c8
#
Adam Miller 77c87c8
# For more information about two-week Atomic releases please visit:
Adam Miller 77c87c8
#   https://fedoraproject.org/wiki/Changes/Two_Week_Atomic
Adam Miller 77c87c8
#
Adam Miller 77c87c8
# Copyright (C) 2015 Red Hat, Inc.
Adam Miller 77c87c8
# SPDX-License-Identifier:      GPL-2.0+
Adam Miller 77c87c8
#
Adam Miller 77c87c8
# Authors:
Adam Miller 77c87c8
#     Adam Miller <maxamillion@fedoraproject.org>
9a90d4e
#     Patrick Uiterwijk <puiterwijk@redhat.com>
Adam Miller 77c87c8
#
Adam Miller 77c87c8
# Exit codes:
Adam Miller 77c87c8
#   0 - Success
Adam Miller 77c87c8
#   1 - required arg missing
Adam Miller 77c87c8
#   2 - no successful AutoCloud builds found
Adam Miller 77c87c8
#   3 - subcommand failed, error message will be logged.
f1bbd15
#   4 - execution canceled by user
47a9c74
#   5 - masher lock file found
Adam Miller 77c87c8
#
Adam Miller 77c87c8
#
44c5f0d
# NOTE: This is bad and I feel bad for having written it, here there be dragons
9a90d4e
# NOTE2: The atomic tree ref code is also ugly. Blame to Patrick, credits to Adam.
Adam Miller 77c87c8
Adam Miller 77c87c8
import os
Adam Miller 77c87c8
import sys
Adam Miller 77c87c8
import json
Adam Miller 77c87c8
import glob
Adam Miller 77c87c8
import shutil
Adam Miller 77c87c8
import fnmatch
Adam Miller 77c87c8
import smtplib
Adam Miller 77c87c8
import argparse
Adam Miller 77c87c8
import logging
Adam Miller 77c87c8
import subprocess
Adam Miller 77c87c8
Adam Miller 77c87c8
import requests
Adam Miller 77c87c8
Adam Miller 77c87c8
from email.mime.multipart import MIMEMultipart
Adam Miller 77c87c8
from email.mime.text import MIMEText
Adam Miller 77c87c8
Adam Miller 77c87c8
# Set log level to logging.INFO
Adam Miller 77c87c8
logging.basicConfig(level=logging.INFO)
Adam Miller 77c87c8
log = logging.getLogger(os.path.basename(sys.argv[0]))
Adam Miller 77c87c8
Adam Miller 77c87c8
# Define "constants"
b771e5e
ATOMIC_DIR = "/mnt/koji/mash/atomic/%s"
fc574dd
PREVIOUS_MAJOR_RELEASE_FINAL_COMMIT = 'ce555fa89da934e6eef23764fb40e8333234b8b60b6f688222247c958e5ebd5b'
898848f
TARGET_REF = "fedora/%s/x86_64/atomic-host"
a2b4b50
COMPOSE_BASEDIR = "/mnt/koji/compose/twoweek/"
47a9c74
MASHER_LOCKFILE_GLOB = "/mnt/koji/mash/updates/MASHING*"
Adam Miller 77c87c8
Adam Miller 77c87c8
# FIXME ???? Do we need a real STMP server here?
Adam Miller 77c87c8
ATOMIC_EMAIL_SMTP = "localhost"
Adam Miller 77c87c8
ATOMIC_EMAIL_SENDER = "noreply@fedoraproject.org"
Adam Miller 77c87c8
Adam Miller 77c87c8
ATOMIC_EMAIL_RECIPIENTS = [
Adam Miller 6e91fe4
    "cloud@lists.fedoraproject.org",
Adam Miller 6e91fe4
    "rel-eng@lists.fedoraproject.org",
Adam Miller 6e91fe4
    "atomic-devel@projectatomic.io",
Adam Miller 6e91fe4
    "atomic-announce@projectatomic.io",
Adam Miller 77c87c8
]
Adam Miller 77c87c8
Adam Miller 77c87c8
# Full path will be:
Adam Miller 77c87c8
#   /pub/alt/stage/$VERSION-$DATE/$IMAGE_TYPE/x86_64/[Images|os]/
Adam Miller 77c87c8
# http://dl.fedoraproject.org/pub/alt/atomic/stable/
9a49e6c
ATOMIC_STABLE_BASEDIR = "/pub/alt/atomic/stable/"
Adam Miller 77c87c8
Adam Miller 77c87c8
# the modname gets used to construct the fully qualified topic, like
Adam Miller 77c87c8
# 'org.fedoraproject.prod.releng.blahblahblah'
Adam Miller 77c87c8
ATOMIC_FEDMSG_MODNAME = "releng"
Adam Miller 77c87c8
ATOMIC_FEDMSG_CERT_PREFIX = "releng"
Adam Miller 77c87c8
a2b4b50
MARK_ATOMIC_BAD_COMPOSES = None
a2b4b50
MARK_ATOMIC_GOOD_COMPOSES = None
Adam Miller ccedbed
BLOCK_ATOMIC_RELEASE = None
Adam Miller ccedbed
Adam Miller ccedbed
try:
Adam Miller ccedbed
    MARK_ATOMIC_BAD_JSON_URL = \
a2b4b50
        'https://pagure.io/mark-atomic-bad/raw/master/f/bad-composes.json'
Adam Miller ccedbed
    MARK_ATOMIC_BAD_JSON = requests.get(MARK_ATOMIC_BAD_JSON_URL).text
a2b4b50
    MARK_ATOMIC_BAD_COMPOSES = json.loads(MARK_ATOMIC_BAD_JSON)[u'bad-composes']
Adam Miller ccedbed
Adam Miller ccedbed
    BLOCK_ATOMIC_RELEASE_JSON_URL = \
Adam Miller ccedbed
        'https://pagure.io/mark-atomic-bad/raw/master/f/block-release.json'
Adam Miller ccedbed
    BLOCK_ATOMIC_RELEASE_JSON = \
Adam Miller ccedbed
        requests.get(BLOCK_ATOMIC_RELEASE_JSON_URL).text
Adam Miller ccedbed
    BLOCK_ATOMIC_RELEASE = \
Adam Miller ccedbed
        json.loads(BLOCK_ATOMIC_RELEASE_JSON)[u'block-release']
Adam Miller ccedbed
Adam Miller ccedbed
    MARK_ATOMIC_GOOD_URL = \
a2b4b50
        'https://pagure.io/mark-atomic-bad/raw/master/f/good-composes.json'
Adam Miller ccedbed
    MARK_ATOMIC_GOOD_JSON = \
Adam Miller ccedbed
        requests.get(MARK_ATOMIC_GOOD_URL).text
a2b4b50
    MARK_ATOMIC_GOOD_COMPOSES = \
a2b4b50
        json.loads(MARK_ATOMIC_GOOD_JSON)[u'good-composes']
Adam Miller ccedbed
except Exception, e:
Adam Miller ccedbed
    log.exception(
Adam Miller ccedbed
        "!!!!{0}!!!!\n{0}".format("Failed to fetch or parse json", e)
Adam Miller ccedbed
    )
Adam Miller ccedbed
    sys.exit(1)
Adam Miller 77c87c8
Adam Miller 77c87c8
Adam Miller 77c87c8
DATAGREPPER_URL = "https://apps.fedoraproject.org/datagrepper/raw"
Adam Miller 77c87c8
# delta = 2 weeks in seconds
Adam Miller 77c87c8
DATAGREPPER_DELTA = 1209600
Adam Miller 77c87c8
# category to filter on from datagrepper
c5c09ee
DATAGREPPER_TOPIC = "org.fedoraproject.prod.autocloud.compose.complete"
Adam Miller 77c87c8
Adam Miller 77c87c8
Adam Miller 77c87c8
SIGUL_SIGNED_TXT_PATH = "/tmp/signed"
Adam Miller 77c87c8
Adam Miller 74ce9e4
# Number of atomic testing composes to keep around
Adam Miller 74ce9e4
ATOMIC_COMPOSE_PERSIST_LIMIT = 20
Adam Miller 74ce9e4
Adam Miller 77c87c8
19ff11e
def construct_url(msg):
19ff11e
    """ Construct the final URL from koji URL.
19ff11e
19ff11e
    Takes an autocloud fedmsg message and returns the image name and final url.
19ff11e
    """
c5c09ee
    iul = msg[u'image_url'].split('/')
c5c09ee
c5c09ee
    # This isn't used in the path for the destination dir, it's in there twice
c5c09ee
    iul.remove('compose')
c5c09ee
    iul.remove('compose')
19ff11e
c5c09ee
    image_name = iul[-1]
c5c09ee
    image_url = os.path.join(ATOMIC_STABLE_BASEDIR, '/'.join(iul[4:]))
c5c09ee
    return image_name, image_url
19ff11e
Adam Miller 77c87c8
def get_latest_successful_autocloud_test_info(
Adam Miller 2387a90
        release,
Adam Miller 77c87c8
        datagrepper_url=DATAGREPPER_URL,
Adam Miller 77c87c8
        delta=DATAGREPPER_DELTA,
a2b4b50
        topic=DATAGREPPER_TOPIC):
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
    get_latest_successful_autocloud_test_info
Adam Miller 77c87c8
Adam Miller 77c87c8
        Query datagrepper[0] to find the latest successful Atomic images via
Adam Miller 77c87c8
        the autocloud[1] tests.
Adam Miller 77c87c8
Adam Miller 77c87c8
    return -> dict
Adam Miller 77c87c8
        Will return the build information of the latest successful build
Adam Miller 77c87c8
Adam Miller 77c87c8
    [0] - https://apps.fedoraproject.org/datagrepper/
Adam Miller 77c87c8
    [1] - https://github.com/kushaldas/autocloud/
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
Adam Miller 77c87c8
    # rows_per_page is maximum 100 from Fedora's datagrepper
Adam Miller 77c87c8
    request_params = {
Adam Miller 77c87c8
        "delta": delta,
a2b4b50
        "topic": topic,
Adam Miller 77c87c8
        "rows_per_page": 100,
Adam Miller 77c87c8
    }
Adam Miller 77c87c8
    r = requests.get(datagrepper_url, params=request_params)
Adam Miller 77c87c8
Adam Miller 77c87c8
    # Start with page 1 response from datagrepper, grab the raw messages
Adam Miller 77c87c8
    # and then continue to populate the list with the rest of the pages of data
Adam Miller 77c87c8
    autocloud_data = r.json()[u'raw_messages']
Adam Miller 77c87c8
    for rpage in range(2, r.json()[u'pages']+1):
Adam Miller 77c87c8
        autocloud_data += requests.get(
Adam Miller 77c87c8
            datagrepper_url,
Adam Miller 77c87c8
            params=dict(page=rpage, **request_params)
Adam Miller 77c87c8
        ).json()[u'raw_messages']
Adam Miller 77c87c8
Adam Miller 77c87c8
c5c09ee
    # List comprehension that will return a list of compose information from
c5c09ee
    # AutoCloud (the [u'msg'] payload of autocloud.compose.complete fedmsg)
c5c09ee
    # such that the following criteria are true:
c5c09ee
    #
c5c09ee
    #   - Is an Atomic compose (i.e. 'Atomic' is in the compose id)
c5c09ee
    #   - No compose artifacts failed the tests
c5c09ee
    #   - This is the current Fedora release we want
c5c09ee
    #
c5c09ee
    #   OR:
c5c09ee
    #       - This compose was manually marked good
c5c09ee
    candidate_composes = [
c5c09ee
        compose[u'msg'] for compose in autocloud_data
c5c09ee
            if u'Atomic' in compose[u'msg'][u'id']
c5c09ee
                and compose[u'msg'][u'results'][u'failed'] == 0
c5c09ee
                and compose[u'msg'][u'release'] == str(release)
c5c09ee
                or compose[u'msg'][u'id'] in MARK_ATOMIC_GOOD_COMPOSES
Adam Miller 77c87c8
    ]
Adam Miller 77c87c8
c5c09ee
    filtered_composes = list(candidate_composes)
c5c09ee
    for compose in candidate_composes:
c5c09ee
        if compose_manually_marked_bad(compose[u'id']):
c5c09ee
            filtered_composes.remove(compose)
Adam Miller 1441e42
c5c09ee
    # sc = successful compose
c5c09ee
    sc = filtered_composes[0]
Adam Miller ccedbed
c5c09ee
    autocloud_info = {}
Adam Miller ccedbed
c5c09ee
    # qcow2 image
c5c09ee
    qcow_msg = [
c5c09ee
        sc[u'results'][u'artifacts'][img] for img in sc[u'results'][u'artifacts']
c5c09ee
            if sc[u'results'][u'artifacts'][img][u'family'] == u'Atomic'
c5c09ee
            and sc[u'results'][u'artifacts'][img][u'type'] == u'qcow2'
c5c09ee
    ][0]
c5c09ee
    image_name, image_url = construct_url(qcow_msg)
c5c09ee
    autocloud_info["atomic_qcow2"] = {
c5c09ee
        "compose_id": sc[u'id'],
c5c09ee
        "name": qcow_msg[u'name'],
c5c09ee
        "release": sc[u'release'],
c5c09ee
        "image_name": image_name,
c5c09ee
        "image_url": image_url,
c5c09ee
    }
Adam Miller ccedbed
c5c09ee
    # raw image
c5c09ee
    #
c5c09ee
    # FIXME - This is a bit of a hack right now, but the raw image is what
c5c09ee
    #         the qcow2 is made of so only qcow2 is tested and infers the
c5c09ee
    #         success of both qcow2 and raw.xz
c5c09ee
    autocloud_info["atomic_raw"] = {
c5c09ee
        "compose_id": sc[u'id'],
c5c09ee
        "name": qcow_msg[u'name'],
c5c09ee
        "release": sc[u'release'],
c5c09ee
        "image_name": image_name.replace('qcow2', 'raw.xz'),    # HACK
c5c09ee
        "image_url": image_url.replace('qcow2', 'raw.xz'),      # HACK
c5c09ee
    }
Adam Miller ccedbed
c5c09ee
    # vagrant libvirt image
c5c09ee
    vlibvirt_msg = [
c5c09ee
        sc[u'results'][u'artifacts'][img] for img in sc[u'results'][u'artifacts']
c5c09ee
            if sc[u'results'][u'artifacts'][img][u'family'] == u'Atomic'
c5c09ee
            and sc[u'results'][u'artifacts'][img][u'type'] == u'vagrant-libvirt'
c5c09ee
    ][0]
c5c09ee
    image_name, image_url = construct_url(vlibvirt_msg)
c5c09ee
    autocloud_info["atomic_vagrant_libvirt"] = {
c5c09ee
        "compose_id": sc[u'id'],
c5c09ee
        "name": vlibvirt_msg[u'name'],
c5c09ee
        "release": sc[u'release'],
c5c09ee
        "image_name": image_name,
c5c09ee
        "image_url": image_url,
c5c09ee
    }
Adam Miller 1441e42
c5c09ee
    # vagrant vbox image
c5c09ee
    vvbox_msg = [
c5c09ee
        sc[u'results'][u'artifacts'][img] for img in sc[u'results'][u'artifacts']
c5c09ee
            if sc[u'results'][u'artifacts'][img][u'family'] == u'Atomic'
c5c09ee
            and sc[u'results'][u'artifacts'][img][u'type'] == u'vagrant-virtualbox'
c5c09ee
    ][0]
c5c09ee
    image_name, image_url = construct_url(vvbox_msg)
c5c09ee
    autocloud_info["atomic_vagrant_virtualbox"] = {
c5c09ee
        "compose_id": sc[u'id'],
c5c09ee
        "name": vvbox_msg[u'name'],
c5c09ee
        "release": sc[u'release'],
c5c09ee
        "image_name": image_name,
c5c09ee
        "image_url": image_url,
c5c09ee
    }
Adam Miller 77c87c8
Adam Miller 1441e42
    return autocloud_info
Adam Miller 77c87c8
Adam Miller 77c87c8
a2b4b50
def compose_manually_marked_bad(compose_id, bad_composes=MARK_ATOMIC_BAD_COMPOSES):
Adam Miller 77c87c8
    """
a2b4b50
    compose_manually_marked_bad
Adam Miller 77c87c8
a2b4b50
        Check for a compose that has been marked bad manually
Adam Miller 77c87c8
a2b4b50
        compose_id
a2b4b50
            Compose id of most recently found auto-tested good compose build
Adam Miller 77c87c8
Adam Miller 77c87c8
    return -> bool
Adam Miller 77c87c8
        True if the build was marked bad, else False
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
a2b4b50
    bad = [c for c in bad_composes if c == compose_id]
Adam Miller 77c87c8
Adam Miller 77c87c8
    return len(bad) > 0
Adam Miller 77c87c8
Adam Miller 77c87c8
def send_atomic_announce_email(
Adam Miller 77c87c8
        email_filelist,
Adam Miller 77c87c8
        mail_receivers=ATOMIC_EMAIL_RECIPIENTS,
Adam Miller 77c87c8
        sender_email=ATOMIC_EMAIL_SENDER,
Colin Walters bf45b6b
        sender_smtp=ATOMIC_EMAIL_SMTP,
Colin Walters bf45b6b
        tree_commit=None,
Colin Walters bf45b6b
        tree_version=None):
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
    send_atomic_announce_email
Adam Miller 77c87c8
Adam Miller 77c87c8
        Send the atomic announce email to the desired recipients
Adam Miller 77c87c8
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
Adam Miller 77c87c8
    released_artifacts = []
Adam Miller 77c87c8
    released_checksums = []
Adam Miller 77c87c8
    for e_file in email_filelist:
Adam Miller 77c87c8
        if "CHECKSUM" in e_file:
Adam Miller 77c87c8
            released_checksums.append(
34eaf97
                "https://alt.fedoraproject.org{}".format(e_file)
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
        else:
Adam Miller 77c87c8
            released_artifacts.append(
34eaf97
                "https://alt.fedoraproject.org{}".format(e_file)
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
Adam Miller 77c87c8
    msg = MIMEMultipart()
Adam Miller 77c87c8
    msg['To'] = "; ".join(mail_receivers)
Adam Miller 77c87c8
    msg['From'] = "noreply@fedoraproject.org"
Adam Miller 77c87c8
    msg['Subject'] = "Fedora Atomic Host Two Week Release Announcement"
Adam Miller 77c87c8
    msg.attach(
Adam Miller 77c87c8
        MIMEText(
Adam Miller 77c87c8
            """
Colin Walters b5bd2ca
A new Fedora Atomic Host update is available via an OSTree commit:
Adam Miller 77c87c8
Colin Walters bf45b6b
Commit: {}
Colin Walters bf45b6b
Version: {}
Colin Walters bf45b6b
Colin Walters bf45b6b
Existing systems can be upgraded in place via e.g. `atomic host upgrade` or
Colin Walters bf45b6b
`atomic host deploy`.
Colin Walters bf45b6b
Colin Walters bf45b6b
Corresponding image media for new installations can be downloaded from:
Adam Miller d2bf3e1
17f2a8b
    https://getfedora.org/en/atomic/download/
Adam Miller 77c87c8
Adam Miller 77c87c8
Respective signed CHECKSUM files can be found here:
34eaf97
{}
Adam Miller 77c87c8
9ec4fc8
For direct download, the "latest" targets are always available here:
5ef606f
    https://getfedora.org/atomic_iso_latest
9ec4fc8
    https://getfedora.org/atomic_qcow2_latest
9ec4fc8
    https://getfedora.org/atomic_raw_latest
9ec4fc8
    https://getfedora.org/atomic_vagrant_libvirt_latest
9ec4fc8
    https://getfedora.org/atomic_vagrant_virtualbox_latest
9ec4fc8
9ec4fc8
Filename fetching URLs are available here:
5ef606f
    https://getfedora.org/atomic_iso_latest_filename
9ec4fc8
    https://getfedora.org/atomic_qcow2_latest_filename
9ec4fc8
    https://getfedora.org/atomic_raw_latest_filename
9ec4fc8
    https://getfedora.org/atomic_vagrant_libvirt_latest_filename
9ec4fc8
    https://getfedora.org/atomic_vagrant_virtualbox_latest_filename
9ec4fc8
9ec4fc8
For more information about the latest targets, please reference the Fedora
9ec4fc8
Cloud Wiki space.
9ec4fc8
9ec4fc8
    https://fedoraproject.org/wiki/Cloud#Quick_Links
9ec4fc8
9ec4fc8
Do note that it can take some of the mirrors up to 12 hours to "check-in" at
9ec4fc8
their own discretion.
9a49e6c
Adam Miller 77c87c8
Thank you,
Adam Miller 77c87c8
Fedora Release Engineering
Adam Miller 77c87c8
            """.format(
a724183
                tree_commit,
a724183
                tree_version,
a724183
                '\n'.join(released_checksums)
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
        )
Adam Miller 77c87c8
    )
Adam Miller 77c87c8
Adam Miller 77c87c8
    # FIXME
Adam Miller 77c87c8
    # Need to add package information to fill in the template email
Adam Miller 77c87c8
    #
Adam Miller 77c87c8
    #   The following changes are included in this update:
Adam Miller 77c87c8
Adam Miller 77c87c8
    try:
Adam Miller 77c87c8
        s = smtplib.SMTP(sender_smtp)
Adam Miller 77c87c8
        s.sendmail(sender_email, mail_receivers, msg.as_string())
Adam Miller 77c87c8
    except smtplib.SMTPException, e:
34eaf97
        print "ERROR: Unable to send email:\n{}\n".format(e)
Adam Miller 77c87c8
Adam Miller 77c87c8
def stage_atomic_release(
Adam Miller 77c87c8
        compose_id,
Adam Miller 77c87c8
        compose_basedir=COMPOSE_BASEDIR,
c5c09ee
        dest_base_dir=ATOMIC_STABLE_BASEDIR):
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
    stage_atomic_release
Adam Miller 77c87c8
Adam Miller 77c87c8
        stage the release somewhere, this will remove the old and rsync up the
Adam Miller 77c87c8
        new twoweek release
Adam Miller 77c87c8
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
a2b4b50
    source_loc = os.path.join(compose_basedir, compose_id, "compose")
9a49e6c
    dest_dir = os.path.join(dest_base_dir, compose_id)
Adam Miller 77c87c8
34eaf97
    # FIXME - need sudo until pungi perms are fixed
Adam Miller 77c87c8
    rsync_cmd = [
34eaf97
        'sudo',
Adam Miller 77c87c8
        'rsync -avhHP --delete-after',
c5c09ee
        '--exclude Cloud/',
34eaf97
        "{}/".format(source_loc),
Adam Miller 77c87c8
        dest_dir
Adam Miller 77c87c8
    ]
a2b4b50
    # This looks silly but it gets everything properly split for
a2b4b50
    # subprocess.call but keeps it from looking messy above.
a2b4b50
    rsync_cmd = ' '.join(rsync_cmd).split()
a2b4b50
    if subprocess.call(rsync_cmd):
Adam Miller 77c87c8
        log.error(
34eaf97
            "stage_atomic_release: rsync command failed: {}".format(rsync_cmd)
Adam Miller 77c87c8
        )
Adam Miller 77c87c8
        exit(3)
Adam Miller 77c87c8
Adam Miller 77c87c8
def sign_checksum_files(
Adam Miller 77c87c8
        key,
Adam Miller 77c87c8
        artifact_path,
Adam Miller 77c87c8
        signed_txt_path=SIGUL_SIGNED_TXT_PATH):
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
    sign_checksum_files
Adam Miller 77c87c8
Adam Miller 77c87c8
        Use sigul to sign checksum files onces we know the successfully tested
Adam Miller 77c87c8
        builds.
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
Adam Miller 77c87c8
    # Grab all the checksum_files
Adam Miller 77c87c8
    checksum_files = []
Adam Miller 77c87c8
    for full_dir_path, _, short_names in os.walk(artifact_path):
Adam Miller 77c87c8
        for sname in fnmatch.filter(short_names, '*CHECKSUM'):
Adam Miller 77c87c8
            checksum_files.append(
Adam Miller 77c87c8
                os.path.join(
Adam Miller 77c87c8
                    full_dir_path,
Adam Miller 77c87c8
                    sname,
Adam Miller 77c87c8
                )
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
Adam Miller 77c87c8
    for cfile in checksum_files:
Adam Miller 77c87c8
Adam Miller 77c87c8
        # Check to make sure this file isn't already signed, if it is then
Adam Miller 77c87c8
        # don't sign it again
Adam Miller 77c87c8
        already_signed = False
Adam Miller 77c87c8
        with open(cfile, 'r') as f:
Adam Miller 77c87c8
            for line in f.readlines():
Adam Miller 77c87c8
                if "-----BEGIN PGP SIGNED MESSAGE-----" in line:
Adam Miller 77c87c8
                    already_signed = True
Adam Miller 77c87c8
                    break
Adam Miller 77c87c8
        if already_signed:
Adam Miller 77c87c8
            log.info(
34eaf97
                "sign_checksum_files: {} is already signed".format(cfile)
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
            continue
Adam Miller 77c87c8
Adam Miller 77c87c8
        shutil.copy(cfile, signed_txt_path)
Adam Miller 77c87c8
Adam Miller 77c87c8
        # Basically all of this is ugly and I feel bad about it.
Adam Miller 77c87c8
        sigulsign_cmd = [
34eaf97
            "sigul sign-text -o {} {} {}".format(
Adam Miller 77c87c8
                signed_txt_path,
Adam Miller 77c87c8
                key,
Adam Miller 77c87c8
                cfile
Adam Miller 77c87c8
            ),
Adam Miller 77c87c8
        ]
Adam Miller 77c87c8
34eaf97
        log.info("sign_checksum_files: Signing {}".format(cfile))
a2b4b50
        # This looks silly but it gets everything properly split for
a2b4b50
        # subprocess.call but keeps it from looking messy above.
a2b4b50
        sigulsign_cmd = ' '.join(sigulsign_cmd).split()
a2b4b50
        while subprocess.call(sigulsign_cmd):
Adam Miller 77c87c8
            log.warn(
34eaf97
                "sigul command for {} failed, retrying".format(cfile)
34eaf97
            )
34eaf97
34eaf97
        if subprocess.call(
34eaf97
            "chgrp releng-team {}".format(signed_txt_path).split()
34eaf97
        ):
34eaf97
            log.error(
34eaf97
                "sign_checksum_files: chgrp releng-team {}".format(
34eaf97
                    signed_txt_path
34eaf97
                )
Adam Miller 77c87c8
            )
34eaf97
            sys.exit(3)
34eaf97
34eaf97
        if subprocess.call(
34eaf97
            "chmod 664 {}".format(signed_txt_path).split()
34eaf97
        ):
34eaf97
            log.error(
34eaf97
                "sign_checksum_files: chmod 644 {}".format(
34eaf97
                    signed_txt_path
34eaf97
                )
34eaf97
            )
34eaf97
            sys.exit(3)
Adam Miller 77c87c8
34eaf97
        # FIXME - need sudo until new pungi perms are sorted out
Adam Miller 77c87c8
        if subprocess.call(
34eaf97
            #["sg", "releng-team", "'mv {} {}'".format(signed_txt_path, cfile)]
34eaf97
            "sudo mv {} {}".format(signed_txt_path, cfile).split()
Adam Miller 77c87c8
        ):
Adam Miller 77c87c8
            log.error(
34eaf97
                "sign_checksum_files: sudo sg releng-team 'mv {} {}' FAILED".format(
Adam Miller 77c87c8
                    signed_txt_path,
Adam Miller 77c87c8
                    cfile,
Adam Miller 77c87c8
                )
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
            sys.exit(3)
Adam Miller 77c87c8
Adam Miller 77c87c8
Adam Miller 77c87c8
def fedmsg_publish(topic, msg):
Adam Miller 77c87c8
    """ Try to publish a message on the fedmsg bus.
Adam Miller 77c87c8
Adam Miller 77c87c8
    But proceed happily if we weren't able to publish anything.
Adam Miller 77c87c8
    """
Adam Miller 77c87c8
Adam Miller 77c87c8
    try:
Adam Miller 77c87c8
        import fedmsg
Adam Miller 77c87c8
        import fedmsg.config
Adam Miller 77c87c8
Adam Miller 77c87c8
        # Load config from disk with all the environment goodies.
Adam Miller 77c87c8
        config = fedmsg.config.load_config()
Adam Miller 77c87c8
Adam Miller 77c87c8
        # And overwrite some values
Adam Miller 77c87c8
        config['modname'] = ATOMIC_FEDMSG_MODNAME
Adam Miller 77c87c8
        config['cert_prefix'] = ATOMIC_FEDMSG_CERT_PREFIX
Adam Miller 77c87c8
        config['active'] = True
Adam Miller 77c87c8
Adam Miller 77c87c8
        # Send it.
Adam Miller 77c87c8
        fedmsg.publish(topic=topic, msg=msg, **config)
Adam Miller 77c87c8
    except Exception:
Adam Miller 77c87c8
        # If you didn't know, log.exception automatically logs the traceback.
Adam Miller 77c87c8
        log.exception("Failed to publish to fedmsg.")
Adam Miller 77c87c8
        # But by passing, we don't let the exception bubble up and kill us.
Adam Miller 77c87c8
        pass
Adam Miller 77c87c8
Adam Miller 77c87c8
9a49e6c
def prune_old_composes(prune_base_dir, prune_limit):
Adam Miller 74ce9e4
    """
9a49e6c
    prune_old_composes
Adam Miller 74ce9e4
Adam Miller 74ce9e4
        Clean up old testing composes from /pub/alt/
9a49e6c
9a49e6c
    :param prune_base_dir: str, path to base diretory needing pruning
9a49e6c
    :param prune_limit: int, the number of composes that should be kept,
9a49e6c
                        pruning all others.
Adam Miller 74ce9e4
    """
Adam Miller 74ce9e4
Adam Miller 74ce9e4
    prune_candidate_dirs = os.listdir(prune_base_dir)
Adam Miller 74ce9e4
47d879f
    if len(prune_candidate_dirs) > 2:
47d879f
        # Sort then reverse so we can slice the list from [0:prune_limit]
47d879f
        prune_candidate_dirs.sort()
47d879f
        prune_candidate_dirs.reverse()
47d879f
47d879f
        for candidate_dir in prune_candidate_dirs[0:prune_limit]:
47d879f
            #try:
47d879f
            #    shutil.rmtree(
47d879f
            #        os.path.join(prune_base_dir, candidate_dir)
47d879f
            #    )
47d879f
            #except OSError, e:
47d879f
            #    log.error(
47d879f
            #        "Error trying to remove directory: {}\n{}".format(
47d879f
            #            candidate_dir,
47d879f
            #            e
47d879f
            #        )
47d879f
            #    )
47d879f
47d879f
            #FIXME - need to do this with sudo until pungi perms are fixed
47d879f
            prune_cmd = "sudo rm -fr {}".format(
47d879f
                os.path.join(
47d879f
                    prune_base_dir,
47d879f
                    candidate_dir
47d879f
                )
Adam Miller 74ce9e4
            )
47d879f
            if subprocess.call(prune_cmd.split()):
47d879f
                log.error(
47d879f
                    "prune_old_composes: command failed: {}".format(prune_cmd)
47d879f
                )
Adam Miller 74ce9e4
9bf9f51
def generate_static_delta(release, old_commit, new_commit):
9bf9f51
    """
9bf9f51
    generate_static_delta
9a90d4e
9bf9f51
        Generate a static delta betwee two commits
9bf9f51
9bf9f51
    :param release - the Fedora release to target (25,26,etc)
9bf9f51
    :param old_commit - starting point for delta
9bf9f51
    :param new_commit - ending point for delta
9bf9f51
    """
6fda058
    # Run as apache user because the files we are editing/creating
6fda058
    # need to be owned by the apache user
6fda058
    diff_cmd = ["/usr/bin/sudo", "-u", "apache",
6fda058
                "ostree", "static-delta", "generate", "--repo",
9a90d4e
                ATOMIC_DIR % release, "--if-not-exists", "--from", old_commit,
9a90d4e
                "--to", new_commit]
9bf9f51
    log.info("Creating Static Delta from %s to %s" % (old_commit, new_commit))
9a90d4e
    if subprocess.call(diff_cmd):
9bf9f51
        log.error("generate_static_delta: diff generation failed: %s", diff_cmd)
9a90d4e
        exit(3)
9a90d4e
1e9c81a
def update_ostree_summary_file(release):
1e9c81a
    """
1e9c81a
    update_ostree_summary_file
1e9c81a
1e9c81a
        Update the summary file for the ostree repo
1e9c81a
1e9c81a
    :param release - the Fedora release to target (25,26,etc)
1e9c81a
    """
1e9c81a
    # Run as apache user because the files we are editing/creating
1e9c81a
    # need to be owned by the apache user
1e9c81a
    summary_cmd = ["/usr/bin/sudo", "-u", "apache",
1e9c81a
                   "ostree", "summary", "-u", "--repo",
1e9c81a
                   ATOMIC_DIR % release]
1e9c81a
    log.info("Updating Summary file")
1e9c81a
    if subprocess.call(summary_cmd):
1e9c81a
        log.error("update_ostree_summary_file: update failed: %s", summary_cmd)
1e9c81a
        exit(3)
1e9c81a
9bf9f51
def move_tree_commit(release, old_commit, new_commit):
9bf9f51
    generate_static_delta(release=release,
9bf9f51
                          old_commit=old_commit,
9bf9f51
                          new_commit=new_commit)
9bf9f51
9bf9f51
    log.info("Moving ref %s to commit %s" %(TARGET_REF, new_commit))
6fda058
    reset_cmd = ['/usr/bin/sudo', '-u', 'apache',
6fda058
                 'ostree', 'reset', TARGET_REF % release,
0dae9d2
                 new_commit, '--repo', ATOMIC_DIR % release]
0dae9d2
    if subprocess.call(reset_cmd):
0dae9d2
        log.error("move_tree_commit: resetting ref to new commit failed: %s", reset_cmd)
0dae9d2
        exit(3)
9a90d4e
1e9c81a
    update_ostree_summary_file(release)
1e9c81a
9a90d4e
9a90d4e
Adam Miller 77c87c8
if __name__ == '__main__':
Adam Miller 77c87c8
Adam Miller 77c87c8
    # get args from command line
Adam Miller 77c87c8
    parser = argparse.ArgumentParser()
Adam Miller 77c87c8
    parser.add_argument(
Adam Miller 77c87c8
        "-k",
Adam Miller 77c87c8
        "--key",
Adam Miller 77c87c8
        help="signing key to use with sigul",
Adam Miller 77c87c8
    )
Adam Miller 2387a90
    parser.add_argument(
Adam Miller 2387a90
        "-r",
Adam Miller 2387a90
        "--release",
47a9c74
        help="Fedora Release to target for release (Ex: 24, 25, rawhide)",
47a9c74
    )
47a9c74
    parser.add_argument(
47a9c74
        "-f",
47a9c74
        "--force",
47a9c74
        type=bool,
47a9c74
        default=False,
47a9c74
        help="Force the release even if masher lock files are found (check with RelEng first)",
Adam Miller 2387a90
    )
Adam Miller 77c87c8
    pargs = parser.parse_args()
Adam Miller 77c87c8
Adam Miller 77c87c8
    if not pargs.key:
Adam Miller 77c87c8
        log.error("No key passed, see -h for help")
Adam Miller 77c87c8
        sys.exit(1)
Adam Miller 2387a90
    if not pargs.release:
Adam Miller 2387a90
        log.error("No release arg passed, see -h for help")
Adam Miller 2387a90
        sys.exit(1)
Adam Miller 77c87c8
47a9c74
    log.info("Checking for masher lock files")
47a9c74
    if glob.glob(MASHER_LOCKFILE_GLOB) and not pargs.force:
47a9c74
        errmsg = """
47a9c74
        Masher file found, must --force to proceed.
47a9c74
47a9c74
        MAKE SURE YOU KNOW WHAT YOU ARE DOING, WHEN IN DOUBT CHECK WITH
47a9c74
        #fedora-releng on irc.freenode.net TO VERIFY WE ARE SAFE TO NOT
47a9c74
        BREAK MASHER
47a9c74
        """
47a9c74
        log.error(errmsg)
47a9c74
        sys.exit(5)
47a9c74
47a9c74
47a9c74
Adam Miller a8e752a
    log.info("Checking to make sure release is not currently blocked")
Adam Miller a8e752a
    if BLOCK_ATOMIC_RELEASE:
Adam Miller a8e752a
        log.info("Release Blocked: Exiting.")
Adam Miller a8e752a
        sys.exit(0)
Adam Miller a8e752a
Adam Miller 77c87c8
    log.info("Querying datagrepper for latest AutoCloud successful tests")
Adam Miller 77c87c8
    # Acquire the latest successful builds from datagrepper
Adam Miller 2387a90
    tested_autocloud_info = get_latest_successful_autocloud_test_info(
Adam Miller 2387a90
        pargs.release
Adam Miller 2387a90
    )
c5c09ee
c5c09ee
    # FIXME - DEBUGGING
c5c09ee
    log.info("{}\n{}".format("TESTED_AUTOCLOUD_INFO", json.dumps(tested_autocloud_info, indent=2)))
c5c09ee
Adam Miller 77c87c8
    log.info("Query to datagrepper complete")
Adam Miller 77c87c8
    # If the dict is empty, there were no successful builds in the last two
Adam Miller 77c87c8
    # weeks, error accordingly
Adam Miller 77c87c8
    if not tested_autocloud_info:
Adam Miller 77c87c8
        log.error("No successful builds found")
Adam Miller 77c87c8
        sys.exit(2)
Adam Miller 77c87c8
9a90d4e
    log.info("Extracting compose_id from tested autocloud data")
9a90d4e
    compose_id = tested_autocloud_info['atomic_qcow2']['compose_id']
9a90d4e
9a90d4e
    # TODO: https://github.com/kushaldas/tunirtests/pull/59 will allow us to
9a90d4e
    # extract this from the autocloud test results.
9a90d4e
    print('Releasing compose %s' % compose_id)
9a90d4e
    tree_commit = None
Colin Walters bf45b6b
    tree_version = None
9a90d4e
    while not tree_commit:
9a90d4e
        tree_commit = raw_input('Tree commit: ').strip()
Colin Walters bf45b6b
        try:
Colin Walters bf45b6b
            print("Validating and finding version of {}".format(tree_commit))
abf16c3
            tree_version = subprocess.check_output(['/usr/bin/ostree', '--repo=' + ATOMIC_DIR % pargs.release, 'show', '--print-metadata-key=version', tree_commit])
Colin Walters bf45b6b
        except subprocess.CalledProcessError as e:
114ffb9
            print('Error when validating commit: %s. Try again.' % tree_commit)
9a90d4e
            tree_commit = None
Colin Walters bf45b6b
            continue
Colin Walters bf45b6b
        # It's in GVariant print format by default, we can make this less hacky when
Colin Walters bf45b6b
        # porting to use libostree.
5c287ae
        tree_version = tree_version.replace("'", "")
9a90d4e
2ad8a41
    rev_parse_cmd = ['/usr/bin/ostree', 'rev-parse', '--repo',
2ad8a41
                     ATOMIC_DIR % pargs.release, TARGET_REF % pargs.release]
0dae9d2
    previous_commit = subprocess.check_output(rev_parse_cmd).strip()
9a90d4e
47a9c74
    # This could happen if there was a failure in this script sending the email
f1bbd15
    # or anything after the commit has already been moved.
f1bbd15
    if previous_commit == tree_commit:
f1bbd15
        answer = raw_input('ref is already at that commit, are you sure?: (y/n)').strip()
f1bbd15
        if answer.lower() != 'y':
f1bbd15
            sys.exit(4)
f1bbd15
Adam Miller 77c87c8
    log.info("Sending fedmsg releng.atomic.twoweek.begin")
Adam Miller 77c87c8
    fedmsg_publish(
Adam Miller 77c87c8
        topic="atomic.twoweek.begin",
Adam Miller 77c87c8
        msg=dict(**tested_autocloud_info)
Adam Miller 77c87c8
    )
Adam Miller 77c87c8
9a49e6c
    log.info("Signing image metadata - compose")
Adam Miller 77c87c8
    sign_checksum_files(
Adam Miller 77c87c8
        pargs.key,
Adam Miller 2387a90
        os.path.join(COMPOSE_BASEDIR, compose_id),
Adam Miller 77c87c8
    )
Adam Miller 77c87c8
f1bbd15
    # If we are already at the new commit then there is nothing to do
f1bbd15
    if previous_commit == tree_commit:
f1bbd15
        log.info("Tree commit is already at %s. Skipping move", tree_commit)
f1bbd15
    else:
f1bbd15
        log.info("Moving tree commit %s => %s (%s)", previous_commit, tree_commit, tree_version)
f1bbd15
        move_tree_commit(pargs.release, previous_commit, tree_commit)
9a90d4e
2e550c0
    # Also, if existing previous release commit is defined, then
2e550c0
    # generate a static delta from it
2e550c0
    if PREVIOUS_MAJOR_RELEASE_FINAL_COMMIT is not None:
2e550c0
        generate_static_delta(release=pargs.release,
2e550c0
                              old_commit=PREVIOUS_MAJOR_RELEASE_FINAL_COMMIT,
2e550c0
                              new_commit=tree_commit)
2e550c0
        update_ostree_summary_file(pargs.release)
2e550c0
Adam Miller 77c87c8
    log.info("Staging release content in /pub/alt/atomic/stable/")
Adam Miller 77c87c8
    stage_atomic_release(compose_id)
Adam Miller 77c87c8
Adam Miller 77c87c8
    log.info("Sending fedmsg releng.atomic.twoweek.complete")
Adam Miller 77c87c8
    fedmsg_publish(
Adam Miller 77c87c8
        topic="atomic.twoweek.complete",
Adam Miller 77c87c8
        msg=dict(**tested_autocloud_info)
Adam Miller 77c87c8
    )
Adam Miller 77c87c8
Adam Miller 77c87c8
    log.info("Sending Two Week Atomic announcement email")
Adam Miller 77c87c8
    # Find all the Atomic images and CHECKSUM files to include in the email
Adam Miller 77c87c8
    email_filelist = []
9a49e6c
    for full_dir_path, _, short_names in \
47d879f
            os.walk(os.path.join(ATOMIC_STABLE_BASEDIR, compose_id)):
Adam Miller 77c87c8
        for sname in fnmatch.filter(short_names, '*Atomic*'):
Adam Miller 77c87c8
            email_filelist.append(
Adam Miller 77c87c8
                os.path.join(
Adam Miller 77c87c8
                    full_dir_path,
Adam Miller 77c87c8
                    sname,
Adam Miller 77c87c8
                )
Adam Miller 77c87c8
            )
Adam Miller 77c87c8
            for c_file in glob.glob(os.path.join(full_dir_path, "*CHECKSUM")):
Adam Miller 77c87c8
                email_filelist.append(c_file)
a2b4b50
Colin Walters bf45b6b
    send_atomic_announce_email(set(email_filelist), tree_commit=tree_commit,
Colin Walters bf45b6b
                               tree_version=tree_version)
Adam Miller 77c87c8
c5c09ee
    # FIXME - The logic in this functioni is broken, leave it disabled for now
c5c09ee
    #log.info("Pruning old Atomic test composes")
c5c09ee
    #prune_old_composes(ATOMIC_STABLE_BASEDIR, 2)
Adam Miller 74ce9e4
Adam Miller 77c87c8
    log.info("Two Week Atomic Release Complete!")
Adam Miller 77c87c8
Adam Miller 7cf81a4
    print("############REMINDER##########\n#\n#\n")
34eaf97
    print("Reset the block-release value to false in {}".format(
Adam Miller 7cf81a4
        "https://pagure.io/mark-atomic-bad"
Adam Miller 7cf81a4
    ))
Adam Miller 7cf81a4
Adam Miller 77c87c8
# vim: set expandtab sw=4 sts=4 ts=4