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