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