Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | # Copyright (c) 2016 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Module to automate the process of deploying to production. |
| 8 | |
| 9 | Example usage of this script: |
| 10 | 1. Update both autotest and chromite to the lastest commit that has passed |
| 11 | the test instance. |
| 12 | $ ./site_utils/automated_deploy.py |
| 13 | 2. Skip updating a repo, e.g. autotest |
| 14 | $ ./site_utils/automated_deploy.py --skip_autotest |
| 15 | 3. Update a given repo to a specific commit |
| 16 | $ ./site_utils/automated_deploy.py --autotest_hash='1234' |
| 17 | """ |
| 18 | |
| 19 | import argparse |
| 20 | import os |
| 21 | import re |
| 22 | import sys |
| 23 | import subprocess |
| 24 | |
| 25 | import common |
| 26 | from autotest_lib.client.common_lib import revision_control |
| 27 | from autotest_lib.site_utils.lib import infra |
| 28 | |
| 29 | AUTOTEST_DIR = common.autotest_dir |
| 30 | GIT_URL = {'autotest': |
| 31 | 'https://chromium.googlesource.com/chromiumos/third_party/autotest', |
| 32 | 'chromite': |
| 33 | 'https://chromium.googlesource.com/chromiumos/chromite'} |
| 34 | PROD_BRANCH = 'prod' |
| 35 | MASTER_AFE = 'cautotest' |
Shuqian Zhao | a482c4a | 2016-11-21 18:49:41 -0800 | [diff] [blame] | 36 | NOTIFY_GROUP = 'chromeos-infra-discuss@google.com' |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 37 | |
Daniel Erat | 590a499 | 2018-01-29 15:57:58 -0800 | [diff] [blame] | 38 | # CIPD packages whose prod refs should be updated. |
| 39 | _CIPD_PACKAGES = ( |
| 40 | 'chromiumos/infra/lucifer', |
Daniel Erat | 590a499 | 2018-01-29 15:57:58 -0800 | [diff] [blame] | 41 | ) |
| 42 | |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 43 | |
| 44 | class AutoDeployException(Exception): |
| 45 | """Raised when any deploy step fails.""" |
| 46 | |
| 47 | |
| 48 | def parse_arguments(): |
| 49 | """Parse command line arguments. |
| 50 | |
| 51 | @returns An argparse.Namespace populated with argument values. |
| 52 | """ |
| 53 | parser = argparse.ArgumentParser( |
| 54 | description=('Command to update prod branch for autotest, chromite ' |
| 55 | 'repos. Then deploy new changes to all lab servers.')) |
| 56 | parser.add_argument('--skip_autotest', action='store_true', default=False, |
| 57 | help='Skip updating autotest prod branch. Default is False.') |
| 58 | parser.add_argument('--skip_chromite', action='store_true', default=False, |
| 59 | help='Skip updating chromite prod branch. Default is False.') |
Shuqian Zhao | 67ee3b4 | 2017-12-07 11:12:18 -0800 | [diff] [blame] | 60 | parser.add_argument('--force_update', action='store_true', default=False, |
| 61 | help=('Force a deployment without updating both autotest and ' |
| 62 | 'chromite prod branch')) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 63 | parser.add_argument('--autotest_hash', type=str, default=None, |
| 64 | help='Update autotest prod branch to the given hash. If it is not' |
| 65 | ' specified, autotest prod branch will be rebased to ' |
| 66 | 'prod-next branch, which is the latest commit that has ' |
| 67 | 'passed our test instance.') |
| 68 | parser.add_argument('--chromite_hash', type=str, default=None, |
| 69 | help='Same as autotest_hash option.') |
| 70 | |
| 71 | results = parser.parse_args(sys.argv[1:]) |
| 72 | |
| 73 | # Verify the validity of the options. |
| 74 | if ((results.skip_autotest and results.autotest_hash) or |
| 75 | (results.skip_chromite and results.chromite_hash)): |
| 76 | parser.print_help() |
| 77 | print 'Cannot specify skip_* and *_hash options at the same time.' |
| 78 | sys.exit(1) |
Shuqian Zhao | 67ee3b4 | 2017-12-07 11:12:18 -0800 | [diff] [blame] | 79 | if results.force_update: |
| 80 | results.skip_autotest = True |
| 81 | results.skip_chromite = True |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 82 | return results |
| 83 | |
| 84 | |
| 85 | def clone_prod_branch(repo): |
| 86 | """Method to clone the prod branch for a given repo under /tmp/ dir. |
| 87 | |
| 88 | @param repo: Name of the git repo to be cloned. |
| 89 | |
| 90 | @returns path to the cloned repo. |
| 91 | @raises subprocess.CalledProcessError on a command failure. |
| 92 | @raised revision_control.GitCloneError when git clone fails. |
| 93 | """ |
| 94 | repo_dir = '/tmp/%s' % repo |
| 95 | print 'Cloning %s prod branch under %s' % (repo, repo_dir) |
| 96 | if os.path.exists(repo_dir): |
| 97 | infra.local_runner('rm -rf %s' % repo_dir) |
| 98 | git_repo = revision_control.GitRepo(repo_dir, GIT_URL[repo]) |
| 99 | git_repo.clone(remote_branch=PROD_BRANCH) |
| 100 | print 'Successfully cloned %s prod branch' % repo |
| 101 | return repo_dir |
| 102 | |
| 103 | |
| 104 | def update_prod_branch(repo, repo_dir, hash_to_rebase): |
| 105 | """Method to update the prod branch of the given repo to the given hash. |
| 106 | |
| 107 | @param repo: Name of the git repo to be updated. |
| 108 | @param repo_dir: path to the cloned repo. |
| 109 | @param hash_to_rebase: Hash to rebase the prod branch to. If it is None, |
| 110 | prod branch will rebase to prod-next branch. |
| 111 | |
Shuqian Zhao | 673519b | 2017-05-05 15:13:25 -0700 | [diff] [blame] | 112 | @returns the range of the pushed commits as a string. E.g 123...345. If the |
| 113 | prod branch is already up-to-date, return None. |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 114 | @raises subprocess.CalledProcessError on a command failure. |
| 115 | """ |
| 116 | with infra.chdir(repo_dir): |
Shuqian Zhao | a482c4a | 2016-11-21 18:49:41 -0800 | [diff] [blame] | 117 | print 'Updating %s prod branch.' % repo |
| 118 | rebase_to = hash_to_rebase if hash_to_rebase else 'origin/prod-next' |
Shuqian Zhao | 673519b | 2017-05-05 15:13:25 -0700 | [diff] [blame] | 119 | # Check whether prod branch is already up-to-date, which means there is |
| 120 | # no changes since last push. |
| 121 | print 'Detecting new changes since last push...' |
| 122 | diff = infra.local_runner('git log prod..%s --oneline' % rebase_to, |
| 123 | stream_output=True) |
| 124 | if diff: |
| 125 | print 'Find new changes, will update prod branch...' |
| 126 | infra.local_runner('git rebase %s prod' % rebase_to, |
| 127 | stream_output=True) |
| 128 | result = infra.local_runner('git push origin prod', |
| 129 | stream_output=True) |
| 130 | print 'Successfully pushed %s prod branch!\n' % repo |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 131 | |
Shuqian Zhao | 673519b | 2017-05-05 15:13:25 -0700 | [diff] [blame] | 132 | # Get the pushed commit range, which is used to get pushed commits |
| 133 | # using git log E.g. 123..456, then run git log --oneline 123..456. |
| 134 | grep = re.search('(\w)*\.\.(\w)*', result) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 135 | |
Shuqian Zhao | 673519b | 2017-05-05 15:13:25 -0700 | [diff] [blame] | 136 | if not grep: |
| 137 | raise AutoDeployException( |
| 138 | 'Fail to get pushed commits for repo %s from git log: %s' % |
| 139 | (repo, result)) |
| 140 | return grep.group(0) |
| 141 | else: |
| 142 | print 'No new %s changes found since last push.' % repo |
| 143 | return None |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 144 | |
| 145 | |
| 146 | def get_pushed_commits(repo, repo_dir, pushed_commits_range): |
| 147 | """Method to get the pushed commits. |
| 148 | |
| 149 | @param repo: Name of the updated git repo. |
| 150 | @param repo_dir: path to the cloned repo. |
| 151 | @param pushed_commits_range: The range of the pushed commits. E.g 123...345 |
| 152 | @return: the commits that are pushed to prod branch. The format likes this: |
| 153 | "git log --oneline A...B | grep autotest |
| 154 | A xxxx |
| 155 | B xxxx" |
| 156 | @raises subprocess.CalledProcessError on a command failure. |
| 157 | """ |
Shuqian Zhao | a482c4a | 2016-11-21 18:49:41 -0800 | [diff] [blame] | 158 | print 'Getting pushed CLs for %s repo.' % repo |
Shuqian Zhao | 673519b | 2017-05-05 15:13:25 -0700 | [diff] [blame] | 159 | if not pushed_commits_range: |
| 160 | return '\n%s:\nNo new changes since last push.' % repo |
| 161 | |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 162 | with infra.chdir(repo_dir): |
| 163 | get_commits_cmd = 'git log --oneline %s' % pushed_commits_range |
xixuan | 6d782dc | 2017-06-21 18:08:48 -0700 | [diff] [blame] | 164 | |
| 165 | pushed_commits = infra.local_runner( |
| 166 | get_commits_cmd, stream_output=True) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 167 | if repo == 'autotest': |
xixuan | 6d782dc | 2017-06-21 18:08:48 -0700 | [diff] [blame] | 168 | autotest_commits = '' |
| 169 | for cl in pushed_commits.splitlines(): |
| 170 | if 'autotest' in cl: |
| 171 | autotest_commits += '%s\n' % cl |
| 172 | |
| 173 | pushed_commits = autotest_commits |
| 174 | |
Shuqian Zhao | a482c4a | 2016-11-21 18:49:41 -0800 | [diff] [blame] | 175 | print 'Successfully got pushed CLs for %s repo!\n' % repo |
Aviv Keshet | 6dd1c3d | 2017-09-26 17:46:49 -0700 | [diff] [blame] | 176 | displayed_cmd = get_commits_cmd |
| 177 | if repo == 'autotest': |
| 178 | displayed_cmd += ' | grep autotest' |
| 179 | return '\n%s:\n%s\n%s\n' % (repo, displayed_cmd, pushed_commits) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 180 | |
| 181 | |
| 182 | def kick_off_deploy(): |
| 183 | """Method to kick off deploy script to deploy changes to lab servers. |
| 184 | |
| 185 | @raises subprocess.CalledProcessError on a repo command failure. |
| 186 | """ |
| 187 | print 'Start deploying changes to all lab servers...' |
| 188 | with infra.chdir(AUTOTEST_DIR): |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 189 | # Then kick off the deploy script. |
Aviv Keshet | fc59c14 | 2017-10-31 09:27:57 -0700 | [diff] [blame] | 190 | deploy_cmd = ('runlocalssh ./site_utils/deploy_server.py -x --afe=%s' % |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 191 | MASTER_AFE) |
Shuqian Zhao | 6ad127a | 2017-05-22 22:57:19 +0000 | [diff] [blame] | 192 | infra.local_runner(deploy_cmd, stream_output=True) |
Aviv Keshet | fc59c14 | 2017-10-31 09:27:57 -0700 | [diff] [blame] | 193 | print 'Successfully deployed changes to all lab servers.' |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 194 | |
| 195 | |
| 196 | def main(args): |
| 197 | """Main entry""" |
| 198 | options = parse_arguments() |
| 199 | repos = dict() |
| 200 | if not options.skip_autotest: |
| 201 | repos.update({'autotest': options.autotest_hash}) |
| 202 | if not options.skip_chromite: |
| 203 | repos.update({'chromite': options.chromite_hash}) |
| 204 | |
Daniel Erat | 590a499 | 2018-01-29 15:57:58 -0800 | [diff] [blame] | 205 | print 'Moving CIPD prod refs to prod-next' |
| 206 | for pkg in _CIPD_PACKAGES: |
| 207 | subprocess.check_call(['cipd', 'set-ref', pkg, '-version', 'prod-next', |
| 208 | '-ref', 'prod']) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 209 | try: |
| 210 | # update_log saves the git log of the updated repo. |
| 211 | update_log = '' |
| 212 | for repo, hash_to_rebase in repos.iteritems(): |
| 213 | repo_dir = clone_prod_branch(repo) |
| 214 | push_commits_range = update_prod_branch( |
| 215 | repo, repo_dir, hash_to_rebase) |
| 216 | update_log += get_pushed_commits(repo, repo_dir, push_commits_range) |
| 217 | |
| 218 | kick_off_deploy() |
| 219 | except revision_control.GitCloneError as e: |
| 220 | print 'Fail to clone prod branch. Error:\n%s\n' % e |
| 221 | raise |
| 222 | except subprocess.CalledProcessError as e: |
Shuqian Zhao | 1c3f246 | 2017-02-02 17:21:42 -0800 | [diff] [blame] | 223 | print ('Deploy fails when running a subprocess cmd :\n%s\n' |
| 224 | 'Below is the push log:\n%s\n' % (e.output, update_log)) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 225 | raise |
| 226 | except Exception as e: |
Shuqian Zhao | 1c3f246 | 2017-02-02 17:21:42 -0800 | [diff] [blame] | 227 | print 'Deploy fails with error:\n%s\nPush log:\n%s\n' % (e, update_log) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 228 | raise |
| 229 | |
| 230 | # When deploy succeeds, print the update_log. |
| 231 | print ('Deploy succeeds!!! Below is the push log of the updated repo:\n%s' |
Shuqian Zhao | a482c4a | 2016-11-21 18:49:41 -0800 | [diff] [blame] | 232 | 'Please email this to %s.'% (update_log, NOTIFY_GROUP)) |
Shuqian Zhao | ae2d078 | 2016-11-15 16:58:47 -0800 | [diff] [blame] | 233 | |
| 234 | |
| 235 | if __name__ == '__main__': |
| 236 | sys.exit(main(sys.argv)) |