Source code for scap.train

# -*- coding: utf-8 -*-
import prettytable
import re

from scap import cli, utils, tasks, interaction

GROUPS = ["testwikis", "group0", "group1", "group2"]


class TrainInfo:
    def __init__(self, config):
        self.config = config
        self.train_version = self.get_train_version()
        self.groups = dict()
        self.train_is_at = None

        # FIXME: Verify that versions are ascending as we advance through groups.
        # Warn if there is an unusual arrangement.
        for group in GROUPS:
            versions = utils.get_group_versions(
                group, config["stage_dir"], config["wmf_realm"]
            )
            self.groups[group] = versions

            if versions == [self.train_version]:
                self.train_is_at = group

    def visualize(self, show_positions=False):
        # Copied from https://www.asciiart.eu/vehicles/trains
        train_image = """
____
|DD|_____T_
|_ |XXXXXX|<
  @-@-@-oo\
"""
        train_image = re.sub("XXXXXX", self.train_version[-6:], train_image)

        train_is_at = self.train_is_at

        stops = ["START"] + GROUPS

        # Dividers are not printed if border=False
        table = prettytable.PrettyTable(border=True, header=False)

        table.add_row(
            [
                train_image
                if (stop == train_is_at or (stop == "START" and train_is_at is None))
                else ""
                for stop in stops
            ],
            divider=True,
        )
        table.add_row(stops)
        table.add_row(
            ["/".join(self.groups[stop]) if stop != "START" else "" for stop in stops]
        )
        if show_positions:
            table.add_row([f"[{n}]" for n in range(0, len(stops))])

        # Must be set after adding the rows, otherwise it has no effect.
        table.align = "l"
        table.vrules = prettytable.NONE
        table.hrules = prettytable.NONE
        # ===== looks like a train track!
        table.horizontal_char = "="

        print(table)

    def get_train_version(self) -> str:
        """
        Returns the version of the current train.  Validation is performed
        first to ensure that a good version will be returned.
        """
        gerrit_latest_version = utils.get_current_train_version_from_gerrit(
            self.config["gerrit_url"]
        )
        train_info = utils.get_current_train_info(
            self.config["train_blockers_url"], self.config["web_proxy"]
        )
        task = train_info["task"]
        status = train_info["status"]
        version = train_info["version"]

        if status not in ["open", "progress"]:
            check_status = interaction.prompt_user_for_confirmation(
                f"Train task {task} has status '{status}'. Continue anyway?"
            )
            if not check_status:
                utils.abort(f"Train task {task} has status '{status}'.")

        if version != gerrit_latest_version:
            utils.abort(
                "Phabricator task {} says the train version is '{}', but '{}' is the latest available in Gerrit.".format(
                    task, version, gerrit_latest_version
                )
            )

        return version

    def get_prior_version(self):
        """
        Returns the latest on-disk train version that's not the current train version, or None
        if nothing found.
        """
        versions = tasks.get_wikiversions_ondisk(self.config["stage_dir"])
        if self.train_version in versions:
            versions.remove(self.train_version)

        if not versions:
            return None

        return versions[-1]


[docs]@cli.command( "train", help="Advance or rollback the train", primary_deploy_server_only=True, require_tty_multiplexer=True, ) class Train(cli.Application): """ Advance or rollback the train """
[docs] @cli.argument("--forward", action="store_true", help="Advance the train") @cli.argument( "--backward", "--rollback", action="store_true", help="Rollback the train" ) def main(self, *extra_args): if self.arguments.forward and self.arguments.backward: raise SystemExit("Please choose one of --forward or --backward") info = TrainInfo(self.config) train_version = info.train_version self.old_version = info.get_prior_version() train_is_at = info.train_is_at info.visualize() print() stops = ["START"] + GROUPS current_pos = 0 if train_is_at is None else stops.index(train_is_at) next_pos = current_pos + 1 previous_pos = current_pos - 1 if self.arguments.forward: target_pos = next_pos if target_pos >= len(stops): print(f"The train has already reached {stops[-1]}") return elif self.arguments.backward: target_pos = previous_pos if target_pos <= 0: print("The train is already rolled back all the way.") return if not self.old_version: raise SystemExit( "No other train branches are checked out. Nothing to roll back to" ) else: choices = {stop: str(index) for index, stop in enumerate(stops)} choices["Cancel"] = "c" default = str(next_pos) if next_pos < len(stops) else "c" target_pos = interaction.prompt_choices( f"What station do you want the {train_version} train to be at?", choices, default, ) if target_pos == "c": print("Cancelled") return try: target_pos = int(target_pos) if target_pos < 0 or target_pos >= len(stops): raise ValueError() except ValueError: raise SystemExit(f"'{target_pos}' is not a valid choice.") # At this point target_pos points to an entry in 'stops' indicating # the user's choice. if target_pos == 0: # START if not self.old_version: raise SystemExit( "No other train branches are checked out. Nothing to roll back to" ) self._deploy_promote("all", self.old_version) else: self._deploy_promote(stops[target_pos], train_version) # Re-read train info and show final visualization TrainInfo(self.config).visualize()
def _deploy_promote(self, group, version): args = [] if group != "all" and self.old_version: args = ["--train", "--old-version", self.old_version] self.scap_check_call(["deploy-promote"] + args + [group, version])