google-nomulus/release/rollback/rollback_tool.py
Weimin Yu 6f3b382a2d An automated rollback tool for Nomulus (#847)
* An automated rollback tool for Nomulus

A tool that directs traffic between deployed versions. It handles the
conversion between Nomulus tags and AppEngine versions, executes schema
compatibility tests, ensures that steps are executed in the correct order,
and updates deployment records appropriately.
2020-10-29 10:37:20 -04:00

178 lines
6.6 KiB
Python

# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Script to rollback the Nomulus server on AppEngine."""
import argparse
import dataclasses
import sys
import textwrap
from typing import Any, Optional, Tuple
import appengine
import gcs
import plan
MAIN_HELP = 'Script to roll back the Nomulus server on AppEngine.'
ROLLBACK_HELP = 'Rolls back Nomulus to the target release.'
GET_SERVING_RELEASE_HELP = 'Shows the release tag(s) of the serving versions.'
GET_RECENT_DEPLOYMENTS_HELP = ('Shows recently deployed versions and their '
'release tags.')
ROLLBACK_MODE_HELP = textwrap.dedent("""\
The execution mode.
- dryrun: Prints descriptions of all steps.
- interactive: Prompts for confirmation before executing
each step.
- auto: Executes all steps in one go.
""")
@dataclasses.dataclass(frozen=True)
class Argument:
"""Describes a command line argument.
This class is for use with argparse.ArgumentParser. Except for the
'arg_names' attribute which specifies the argument name and/or flags, all
other attributes must match an accepted parameter in the parser's
add_argument() method.
"""
arg_names: Tuple[str, ...]
help: str
default: Optional[Any] = None
required: bool = True
choices: Optional[Tuple[str, ...]] = None
def get_arg_attrs(self):
return dict((k, v) for k, v in vars(self).items() if k != 'arg_names')
ARGUMENTS = (Argument(('--dev_project', '-d'),
'The GCP project with Nomulus deployment records.'),
Argument(('--project', '-p'),
'The GCP project where the Nomulus server is deployed.'),
Argument(('--env', '-e'),
'The name of the Nomulus server environment.',
choices=('production', 'sandbox', 'crash', 'alpha')))
ROLLBACK_ARGUMENTS = (Argument(('--target_release', '-t'),
'The release to be deployed.'),
Argument(('--run_mode', '-m'),
ROLLBACK_MODE_HELP,
required=False,
default='dryrun',
choices=('dryrun', 'interactive', 'auto')))
def rollback(dev_project: str, project: str, env: str, target_release: str,
run_mode: str) -> None:
"""Rolls back a Nomulus server to the target release.
Args:
dev_project: The GCP project with deployment records.
project: The GCP project of the Nomulus server.
env: The environment name of the Nomulus server.
target_release: The tag of the release to be brought up.
run_mode: How to handle the rollback steps: print-only (dryrun)
one step at a time with user confirmation (interactive),
or all steps in one shot (automatic).
"""
steps = plan.get_rollback_plan(gcs.GcsClient(dev_project),
appengine.AppEngineAdmin(project), env,
target_release)
print('Rollback steps:\n\n')
for step in steps:
print(f'{step.info()}\n')
if run_mode == 'dryrun':
continue
if run_mode == 'interactive':
confirmation = input(
'Do you wish to (c)ontinue, (s)kip, or (a)bort? ')
if confirmation == 'a':
return
if confirmation == 's':
continue
step.execute()
def show_serving_release(dev_project: str, project: str, env: str) -> None:
"""Shows the release tag(s) of the currently serving versions."""
serving_versions = appengine.AppEngineAdmin(project).get_serving_versions()
versions_to_tags = gcs.GcsClient(dev_project).get_releases_by_versions(
env, serving_versions)
print(f'{project}:')
for version, tag in versions_to_tags.items():
print(f'{version.service_id}\t{version.version_id}\t{tag}')
def show_recent_deployments(dev_project: str, project: str, env: str) -> None:
"""Show release and version of recent deployments."""
num_services = len(appengine.SERVICES)
num_records = 3 * num_services
print(f'{project}:')
for version, tag in gcs.GcsClient(dev_project).get_recent_deployments(
env, num_records).items():
print(f'{version.service_id}\t{version.version_id}\t{tag}')
def main() -> int:
parser = argparse.ArgumentParser(prog='nom_rollback',
description=MAIN_HELP)
subparsers = parser.add_subparsers(dest='command',
help='Supported commands')
rollback_parser = subparsers.add_parser(
'rollback',
help=ROLLBACK_HELP,
formatter_class=argparse.RawTextHelpFormatter)
for flag in ARGUMENTS:
rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs())
for flag in ROLLBACK_ARGUMENTS:
rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs())
show_serving_release_parser = subparsers.add_parser(
'show_serving_release', help=GET_SERVING_RELEASE_HELP)
for flag in ARGUMENTS:
show_serving_release_parser.add_argument(*flag.arg_names,
**flag.get_arg_attrs())
show_recent_deployments_parser = subparsers.add_parser(
'show_recent_deployments', help=GET_RECENT_DEPLOYMENTS_HELP)
for flag in ARGUMENTS:
show_recent_deployments_parser.add_argument(*flag.arg_names,
**flag.get_arg_attrs())
args = parser.parse_args()
command = args.command
args = {k: v for k, v in vars(args).items() if k != 'command'}
{
'rollback': rollback,
'show_recent_deployments': show_recent_deployments,
'show_serving_release': show_serving_release
}[command](**args)
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except Exception as ex: # pylint: disable=broad-except
print(ex)
sys.exit(1)