1#
2# Copyright (C) 2015 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the 'License');
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an 'AS IS' BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16from __future__ import absolute_import
17
18import json
19import logging
20import os.path
21import re
22import requests
23
24import jenkinsapi
25
26import gerrit
27
28import config
29
30
31def is_untrusted_committer(change_id, patch_set):
32    # TODO(danalbert): Needs to be based on the account that made the comment.
33    commit = gerrit.get_commit(change_id, patch_set)
34    committer = commit['committer']['email']
35    return not committer.endswith('@google.com')
36
37
38def contains_cleanspec(change_id, patch_set):
39    files = gerrit.get_files_for_revision(change_id, patch_set)
40    return 'CleanSpec.mk' in [os.path.basename(f) for f in files]
41
42
43def contains_bionicbb(change_id, patch_set):
44    files = gerrit.get_files_for_revision(change_id, patch_set)
45    return any('tools/bionicbb' in f for f in files)
46
47
48def should_skip_build(info):
49    if info['MessageType'] not in ('newchange', 'newpatchset', 'comment'):
50        raise ValueError('should_skip_build() is only valid for new '
51                         'changes, patch sets, and commits.')
52
53    change_id = info['Change-Id']
54    patch_set = info['PatchSet']
55
56    checks = [
57        is_untrusted_committer,
58        contains_cleanspec,
59        contains_bionicbb,
60    ]
61    for check in checks:
62        if check(change_id, patch_set):
63            return True
64    return False
65
66
67def clean_project(dry_run):
68    username = config.jenkins_credentials['username']
69    password = config.jenkins_credentials['password']
70    jenkins_url = config.jenkins_url
71    jenkins = jenkinsapi.api.Jenkins(jenkins_url, username, password)
72
73    build = 'clean-bionic-presubmit'
74    if build in jenkins:
75        if not dry_run:
76            _ = jenkins[build].invoke()
77            # https://issues.jenkins-ci.org/browse/JENKINS-27256
78            # url = job.get_build().baseurl
79            url = 'URL UNAVAILABLE'
80        else:
81            url = 'DRY_RUN_URL'
82        logging.info('Cleaning: %s %s', build, url)
83    else:
84        logging.error('Failed to clean: could not find project %s', build)
85    return True
86
87
88def build_project(gerrit_info, dry_run, lunch_target=None):
89    project_to_jenkins_map = {
90        'platform/bionic': 'bionic-presubmit',
91        'platform/build': 'bionic-presubmit',
92        'platform/external/jemalloc': 'bionic-presubmit',
93        'platform/external/libcxx': 'bionic-presubmit',
94        'platform/external/libcxxabi': 'bionic-presubmit',
95        'platform/external/compiler-rt': 'bionic-presubmit',
96    }
97
98    username = config.jenkins_credentials['username']
99    password = config.jenkins_credentials['password']
100    jenkins_url = config.jenkins_url
101    jenkins = jenkinsapi.api.Jenkins(jenkins_url, username, password)
102
103    project = gerrit_info['Project']
104    change_id = gerrit_info['Change-Id']
105    if project in project_to_jenkins_map:
106        build = project_to_jenkins_map[project]
107    else:
108        build = 'bionic-presubmit'
109
110    if build in jenkins:
111        project_path = '/'.join(project.split('/')[1:])
112        if not project_path:
113            raise RuntimeError('bogus project: {}'.format(project))
114        if project_path.startswith('platform/'):
115            raise RuntimeError('Bad project mapping: {} => {}'.format(
116                project, project_path))
117        ref = gerrit.ref_for_change(change_id)
118        params = {
119            'REF': ref,
120            'CHANGE_ID': change_id,
121            'PROJECT': project_path
122        }
123        if lunch_target is not None:
124            params['LUNCH_TARGET'] = lunch_target
125        if not dry_run:
126            _ = jenkins[build].invoke(build_params=params)
127            # https://issues.jenkins-ci.org/browse/JENKINS-27256
128            # url = job.get_build().baseurl
129            url = 'URL UNAVAILABLE'
130        else:
131            url = 'DRY_RUN_URL'
132        logging.info('Building: %s => %s %s %s', project, build, url,
133                     change_id)
134    else:
135        logging.error('Unknown build: %s => %s %s', project, build, change_id)
136    return True
137
138
139def handle_change(gerrit_info, _, dry_run):
140    if should_skip_build(gerrit_info):
141        return True
142    return build_project(gerrit_info, dry_run)
143
144
145def drop_rejection(gerrit_info, dry_run):
146    request_data = {
147        'changeid': gerrit_info['Change-Id'],
148        'patchset': gerrit_info['PatchSet']
149    }
150    url = '{}/{}'.format(config.build_listener_url, 'drop-rejection')
151    headers = {'Content-Type': 'application/json;charset=UTF-8'}
152    if not dry_run:
153        try:
154            requests.post(url, headers=headers, data=json.dumps(request_data))
155        except requests.exceptions.ConnectionError as ex:
156            logging.error('Failed to drop rejection: %s', ex)
157            return False
158    logging.info('Dropped rejection: %s', gerrit_info['Change-Id'])
159    return True
160
161
162def handle_comment(gerrit_info, body, dry_run):
163    if 'Verified+1' in body:
164        drop_rejection(gerrit_info, dry_run)
165
166    if should_skip_build(gerrit_info):
167        return True
168
169    command_map = {
170        'clean': lambda: clean_project(dry_run),
171        'retry': lambda: build_project(gerrit_info, dry_run),
172
173        'arm': lambda: build_project(gerrit_info, dry_run,
174                                     lunch_target='aosp_arm-eng'),
175        'aarch64': lambda: build_project(gerrit_info, dry_run,
176                                         lunch_target='aosp_arm64-eng'),
177        'mips': lambda: build_project(gerrit_info, dry_run,
178                                      lunch_target='aosp_mips-eng'),
179        'mips64': lambda: build_project(gerrit_info, dry_run,
180                                        lunch_target='aosp_mips64-eng'),
181        'x86': lambda: build_project(gerrit_info, dry_run,
182                                     lunch_target='aosp_x86-eng'),
183        'x86_64': lambda: build_project(gerrit_info, dry_run,
184                                        lunch_target='aosp_x86_64-eng'),
185    }
186
187    def handle_unknown_command():
188        pass    # TODO(danalbert): should complain to the commenter.
189
190    commands = [match.group(1).strip() for match in
191                re.finditer(r'^bionicbb:\s*(.+)$', body, flags=re.MULTILINE)]
192
193    for command in commands:
194        if command in command_map:
195            command_map[command]()
196        else:
197            handle_unknown_command()
198
199    return True
200
201
202def skip_handler(gerrit_info, _, __):
203    logging.info('Skipping %s: %s', gerrit_info['MessageType'],
204                 gerrit_info['Change-Id'])
205    return True
206