• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
39class AutoDeployException(Exception):
40    """Raised when any deploy step fails."""
41
42
43def 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
74def 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
93def 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
101    @returns the range of the pushed commits as a string. E.g 123...345
102    @raises subprocess.CalledProcessError on a command failure.
103    """
104    with infra.chdir(repo_dir):
105        print 'Updating %s prod branch.' % repo
106        rebase_to = hash_to_rebase if hash_to_rebase else 'origin/prod-next'
107        infra.local_runner('git rebase %s prod' % rebase_to, stream_output=True)
108        result = infra.local_runner('git push origin prod', stream_output=True)
109        print 'Successfully pushed %s prod branch!\n' % repo
110
111        # Get the pushed commit range, which is used to get the pushed commits
112        # using git log E.g. 123..456, then run git log --oneline 123..456.
113        grep = re.search('(\w)*\.\.(\w)*', result)
114
115    if not grep:
116        raise AutoDeployException(
117            'Fail to get pushed commits for repo %s from git push log: %s' %
118            (repo, result))
119    return grep.group(0)
120
121
122def get_pushed_commits(repo, repo_dir, pushed_commits_range):
123    """Method to get the pushed commits.
124
125    @param repo: Name of the updated git repo.
126    @param repo_dir: path to the cloned repo.
127    @param pushed_commits_range: The range of the pushed commits. E.g 123...345
128    @return: the commits that are pushed to prod branch. The format likes this:
129             "git log --oneline A...B | grep autotest
130              A xxxx
131              B xxxx"
132    @raises subprocess.CalledProcessError on a command failure.
133    """
134    print 'Getting pushed CLs for %s repo.' % repo
135    with infra.chdir(repo_dir):
136        get_commits_cmd = 'git log --oneline %s' % pushed_commits_range
137        if repo == 'autotest':
138            get_commits_cmd += '|grep autotest'
139        pushed_commits = infra.local_runner(get_commits_cmd, stream_output=True)
140        print 'Successfully got pushed CLs for %s repo!\n' % repo
141        return '\n%s:\n%s\n%s\n' % (repo, get_commits_cmd, pushed_commits)
142
143
144def kick_off_deploy():
145    """Method to kick off deploy script to deploy changes to lab servers.
146
147    @raises subprocess.CalledProcessError on a repo command failure.
148    """
149    print 'Start deploying changes to all lab servers...'
150    with infra.chdir(AUTOTEST_DIR):
151        # Then kick off the deploy script.
152        deploy_cmd = ('runlocalssh ./site_utils/deploy_server.py --afe=%s' %
153                      MASTER_AFE)
154        infra.local_runner(deploy_cmd, stream_output=True)
155        print 'Successfully deploy changes to all lab servers.!'
156
157
158def main(args):
159    """Main entry"""
160    options = parse_arguments()
161    repos = dict()
162    if not options.skip_autotest:
163        repos.update({'autotest': options.autotest_hash})
164    if not options.skip_chromite:
165        repos.update({'chromite': options.chromite_hash})
166
167    try:
168        # update_log saves the git log of the updated repo.
169        update_log = ''
170        for repo, hash_to_rebase in repos.iteritems():
171            repo_dir = clone_prod_branch(repo)
172            push_commits_range = update_prod_branch(
173                repo, repo_dir, hash_to_rebase)
174            update_log += get_pushed_commits(repo, repo_dir, push_commits_range)
175
176        kick_off_deploy()
177    except revision_control.GitCloneError as e:
178        print 'Fail to clone prod branch. Error:\n%s\n' % e
179        raise
180    except subprocess.CalledProcessError as e:
181        print ('Deploy fails when running a subprocess cmd :\n%s\n'
182               'Below is the push log:\n%s\n' % (e.output, update_log))
183        raise
184    except Exception as e:
185        print 'Deploy fails with error:\n%s\nPush log:\n%s\n' % (e, update_log)
186        raise
187
188    # When deploy succeeds, print the update_log.
189    print ('Deploy succeeds!!! Below is the push log of the updated repo:\n%s'
190           'Please email this to %s.'% (update_log, NOTIFY_GROUP))
191
192
193if __name__ == '__main__':
194    sys.exit(main(sys.argv))
195