blob: 332f421e33b1e90139a416dfbeedfd17f91ad222 [file] [log] [blame]
Xinan Lin3ba18a02019-08-13 15:44:55 -07001# Copyright 2019 The Chromium OS Authors. All rights reserved.
Xinan Linc61196b2019-08-13 10:37:30 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module for interacting with Buildbucket."""
Xinan Linc61196b2019-08-13 10:37:30 -07006
Xinan Lin3ba18a02019-08-13 15:44:55 -07007import datetime
linxinane5eb4552019-08-26 05:44:45 +00008import logging
Xinan Lin3ba18a02019-08-13 15:44:55 -07009import re
10import string
11
12import build_lib
Xinan Linc61196b2019-08-13 10:37:30 -070013import constants
14import file_getter
15
16from chromite.api.gen.test_platform import request_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070017from components import auth
Xinan Lin3ba18a02019-08-13 15:44:55 -070018from components.prpc import client as prpc_client
19from infra_libs.buildbucket.proto import rpc_pb2, build_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070020from infra_libs.buildbucket.proto.rpc_prpc_pb2 import BuildsServiceDescription
21
22from oauth2client import service_account
Xinan Lin3ba18a02019-08-13 15:44:55 -070023
24from google.protobuf import json_format, struct_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070025
26
Xinan Lin3ba18a02019-08-13 15:44:55 -070027_enum = request_pb2.Request.Params.Scheduling
Xinan Linc61196b2019-08-13 10:37:30 -070028NONSTANDARD_POOL_NAMES = {
Xinan Lin3ba18a02019-08-13 15:44:55 -070029 'cq': _enum.MANAGED_POOL_CQ,
30 'bvt': _enum.MANAGED_POOL_BVT,
31 'suites': _enum.MANAGED_POOL_SUITES,
32 'cts': _enum.MANAGED_POOL_CTS,
33 'cts-perbuild': _enum.MANAGED_POOL_CTS_PERBUILD,
34 'continuous': _enum.MANAGED_POOL_CONTINUOUS,
35 'arc-presubmit': _enum.MANAGED_POOL_ARC_PRESUBMIT,
36 'quota': _enum.MANAGED_POOL_QUOTA,
Xinan Linc61196b2019-08-13 10:37:30 -070037}
38
Xinan Lin3ba18a02019-08-13 15:44:55 -070039
Xinan Linc61196b2019-08-13 10:37:30 -070040def _get_client(address):
41 """Create a prpc client instance for given address."""
Xinan Lin3ba18a02019-08-13 15:44:55 -070042
Xinan Linc61196b2019-08-13 10:37:30 -070043 return prpc_client.Client(address, BuildsServiceDescription)
44
45
Xinan Lin3ba18a02019-08-13 15:44:55 -070046class BuildbucketRunError(Exception):
47 """Raised when interactions with buildbucket server fail."""
48
49
Xinan Linc61196b2019-08-13 10:37:30 -070050class TestPlatformClient(object):
51 """prpc client for cros_test_platform, aka frontdoor."""
52
53 def __init__(self, address, project, bucket, builder):
54 self.client = _get_client(address)
55 self.builder = build_pb2.BuilderID(project=project,
56 bucket=bucket,
57 builder=builder)
58 self.scope = 'https://www.googleapis.com/auth/userinfo.email'
59 self.running_env = constants.environment()
60
Xinan Lin3ba18a02019-08-13 15:44:55 -070061 # This is cloned from swarming_lib.py.
62 def _get_final_build(self, **kwargs):
63 """Get the final build to kick off on a lab DUT.
64
65 Args:
66 **kwargs: The parameters of a task loaded from suite queue.
67
68 Returns:
69 A string representing the build to run.
70 """
71 cros_build = kwargs[build_lib.BuildVersionKey.CROS_VERSION]
72 android_build = kwargs[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
73 testbed_build = kwargs[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
74 return cros_build or android_build or testbed_build
75
76 def _get_scheduling(self, pool, quota):
77 """Assign appropriate quota or pool name to scheduling instance.
78
79 Args:
80 pool: string pool name (e.g. 'bvt', 'quota').
81 quota: string quota account, or None.
82
83 Returns:
84 scheduling: A request_pb2.Request.Params.Scheduling instance.
85 """
86 if quota:
87 return request_pb2.Request.Params.Scheduling(quota_account=quota)
88
89 mp = request_pb2.Request.Params.Scheduling.ManagedPool
90 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
91 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
92
93 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
94 match = re.match(DUT_POOL_PREFIX, pool)
95 if match:
Alex Zamorzaevc1935602019-08-28 14:37:35 -070096 pool = string.lower(match.group('munged_pool'))
97
98 if NONSTANDARD_POOL_NAMES.get(pool):
Xinan Lin3ba18a02019-08-13 15:44:55 -070099 return request_pb2.Request.Params.Scheduling(
Alex Zamorzaevc1935602019-08-28 14:37:35 -0700100 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
Xinan Lin3ba18a02019-08-13 15:44:55 -0700101
102 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
103
104 def _form_test_platform_request(self, final_build, **task_params):
105 """Generate ScheduleBuildRequest for calling buildbucket.
106
107 Args:
108 final_build: string, the build to run with the suite (e.g.
109 'nyan_blaze-release/R69-10895.33.0').
110 **task_params: dict, containing the parameters of a task got from suite
111 queue.
112
113 Returns:
114 A request_pb2 instance.
115 """
116
117 pool = task_params.get('override_pool') or task_params['pool']
118 quota = task_params.get('override_qs_account')
119 scheduling = self._get_scheduling(pool, quota)
120
121 software_dep = request_pb2.Request.Params.SoftwareDependency()
122 software_dep.chromeos_build = final_build
123
124 software_attributes = request_pb2.Request.Params.SoftwareAttributes()
125 software_attributes.build_target.name = task_params['board']
126
127 metadata = request_pb2.Request.Params.Metadata()
128 metadata.test_metadata_url = task_params['test_source_build']
129
130 timeout_mins = int(task_params['timeout_mins'])
131 if task_params.get('timeout'):
132 timeout_mins = int(task_params['timeout'])
133 if timeout_mins > constants.Buildbucket.MAX_SWARMING_TIMEOUT_MINS:
134 timeout_mins = constants.Buildbucket.MAX_SWARMING_TIMEOUT_MINS
135 td = datetime.timedelta(minutes=timeout_mins)
136 params_time = request_pb2.Request.Params.Time()
137 params_time.maximum_duration.FromTimedelta(td)
138
139 params = request_pb2.Request.Params(scheduling=scheduling,
140 software_dependencies=[software_dep],
141 software_attributes=software_attributes,
142 metadata=metadata,
143 time=params_time)
144
145 if task_params['model'] != 'None':
146 params.hardware_attributes.model = task_params['model']
147
148 if task_params['job_retry'] == 'True':
149 params.retry.allow = True
150 params.retry.max = constants.Buildbucket.MAX_RETRY
151
152 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
153 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
154 if fw_ro_build:
155 build = params.software_dependencies.add()
156 build.ro_firmware_build = fw_ro_build
157 if fw_rw_build:
158 build = params.software_dependencies.add()
159 build.rw_firmware_build = fw_rw_build
160
161 test_plan = request_pb2.Request.TestPlan()
162 suite = test_plan.suite.add()
163 suite.name = task_params['suite']
164
165 return request_pb2.Request(params=params,
166 test_plan=test_plan)
167
168 def run(self, **task_params):
169 """Call TestPlatform Builder to schedule a test.
170
171 Args:
172 **task_params: The suite parameters to run.
173
174 Raises:
175 BuildbucketRunError: if failed to get build info from task parameters.
176 """
177
178 final_build = self._get_final_build(**task_params)
179 if final_build == 'None':
180 raise BuildbucketRunError('No proper build in task params: %r' %
181 task_params)
linxinane5eb4552019-08-26 05:44:45 +0000182
Xinan Lin3ba18a02019-08-13 15:44:55 -0700183 req = self._form_test_platform_request(final_build, **task_params)
184 req_struct = self._request_pb2_to_struct_pb2(req)
linxinane5eb4552019-08-26 05:44:45 +0000185 req_build = self._build_request(req_struct)
186 logging.debug('Raw request to buildbucket: %s' % req_build)
187
Xinan Lin3ba18a02019-08-13 15:44:55 -0700188 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
189 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
190 # If running locally, use the staging service account.
191 sa_key = self._gen_service_account_key(
192 file_getter.STAGING_CLIENT_SECRETS_FILE)
193 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
linxinan2da19332019-08-26 06:27:40 +0000194 else:
195 cred = prpc_client.service_account_credentials()
linxinane5eb4552019-08-26 05:44:45 +0000196
197 resp = self.client.ScheduleBuild(req_build, credentials=cred)
198 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700199
Xinan Linc61196b2019-08-13 10:37:30 -0700200 def dummy_run(self):
201 """Perform a dummy run of prpc call to cros_test_platform-dev."""
202
203 req = request_pb2.Request()
204 req_struct = self._request_pb2_to_struct_pb2(req)
205
206 # Use the staging service account to authorize the request.
207 sa_key = self._gen_service_account_key(
208 file_getter.STAGING_CLIENT_SECRETS_FILE)
209 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
210 return self.client.ScheduleBuild(
211 self._build_request(req_struct), credentials=cred)
212
213 # TODO(linxinan): Handle exceptions when fail to transform proto.
214 def _request_pb2_to_struct_pb2(self, req):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700215 """Transform test_platform_request to google struct proto.
Xinan Linc61196b2019-08-13 10:37:30 -0700216
Xinan Lin3ba18a02019-08-13 15:44:55 -0700217 Args:
218 req: A request_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700219
Xinan Lin3ba18a02019-08-13 15:44:55 -0700220 Returns:
221 A struct_pb2 instance.
222 """
Xinan Linc61196b2019-08-13 10:37:30 -0700223 json = json_format.MessageToJson(req)
224 structpb = struct_pb2.Struct()
225 return json_format.Parse(json, structpb, ignore_unknown_fields=True)
226
227 def _build_request(self, req_struct):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700228 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700229
Xinan Lin3ba18a02019-08-13 15:44:55 -0700230 Args:
231 req_struct: A struct_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700232
Xinan Lin3ba18a02019-08-13 15:44:55 -0700233 Returns:
234 A ScheduleBuildRequest instance.
235 """
Xinan Linc61196b2019-08-13 10:37:30 -0700236 recipe_struct = struct_pb2.Struct(fields={
237 'request': struct_pb2.Value(struct_value=req_struct)})
238 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
239 properties=recipe_struct)
240
241 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700242 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700243
Xinan Lin3ba18a02019-08-13 15:44:55 -0700244 Args:
245 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700246
Xinan Lin3ba18a02019-08-13 15:44:55 -0700247 Returns:
248 A service account key.
249 """
Xinan Linc61196b2019-08-13 10:37:30 -0700250 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700251 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700252 return auth.ServiceAccountKey(
253 client_email=key.service_account_email,
254 private_key=key._private_key_pkcs8_pem,
255 private_key_id=key._private_key_id)