blob: 3db909100fd781d2be58862a205e097bbc4e5209 [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:
96 munged_pool = string.lower(match.group('munged_pool'))
97 if NONSTANDARD_POOL_NAMES.get(munged_pool):
98 return request_pb2.Request.Params.Scheduling(
99 managed_pool=NONSTANDARD_POOL_NAMES.get(munged_pool))
100
101 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
102
103 def _form_test_platform_request(self, final_build, **task_params):
104 """Generate ScheduleBuildRequest for calling buildbucket.
105
106 Args:
107 final_build: string, the build to run with the suite (e.g.
108 'nyan_blaze-release/R69-10895.33.0').
109 **task_params: dict, containing the parameters of a task got from suite
110 queue.
111
112 Returns:
113 A request_pb2 instance.
114 """
115
116 pool = task_params.get('override_pool') or task_params['pool']
117 quota = task_params.get('override_qs_account')
118 scheduling = self._get_scheduling(pool, quota)
119
120 software_dep = request_pb2.Request.Params.SoftwareDependency()
121 software_dep.chromeos_build = final_build
122
123 software_attributes = request_pb2.Request.Params.SoftwareAttributes()
124 software_attributes.build_target.name = task_params['board']
125
126 metadata = request_pb2.Request.Params.Metadata()
127 metadata.test_metadata_url = task_params['test_source_build']
128
129 timeout_mins = int(task_params['timeout_mins'])
130 if task_params.get('timeout'):
131 timeout_mins = int(task_params['timeout'])
132 if timeout_mins > constants.Buildbucket.MAX_SWARMING_TIMEOUT_MINS:
133 timeout_mins = constants.Buildbucket.MAX_SWARMING_TIMEOUT_MINS
134 td = datetime.timedelta(minutes=timeout_mins)
135 params_time = request_pb2.Request.Params.Time()
136 params_time.maximum_duration.FromTimedelta(td)
137
138 params = request_pb2.Request.Params(scheduling=scheduling,
139 software_dependencies=[software_dep],
140 software_attributes=software_attributes,
141 metadata=metadata,
142 time=params_time)
143
144 if task_params['model'] != 'None':
145 params.hardware_attributes.model = task_params['model']
146
147 if task_params['job_retry'] == 'True':
148 params.retry.allow = True
149 params.retry.max = constants.Buildbucket.MAX_RETRY
150
151 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
152 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
153 if fw_ro_build:
154 build = params.software_dependencies.add()
155 build.ro_firmware_build = fw_ro_build
156 if fw_rw_build:
157 build = params.software_dependencies.add()
158 build.rw_firmware_build = fw_rw_build
159
160 test_plan = request_pb2.Request.TestPlan()
161 suite = test_plan.suite.add()
162 suite.name = task_params['suite']
163
164 return request_pb2.Request(params=params,
165 test_plan=test_plan)
166
167 def run(self, **task_params):
168 """Call TestPlatform Builder to schedule a test.
169
170 Args:
171 **task_params: The suite parameters to run.
172
173 Raises:
174 BuildbucketRunError: if failed to get build info from task parameters.
175 """
176
177 final_build = self._get_final_build(**task_params)
178 if final_build == 'None':
179 raise BuildbucketRunError('No proper build in task params: %r' %
180 task_params)
linxinane5eb4552019-08-26 05:44:45 +0000181
Xinan Lin3ba18a02019-08-13 15:44:55 -0700182 req = self._form_test_platform_request(final_build, **task_params)
183 req_struct = self._request_pb2_to_struct_pb2(req)
linxinane5eb4552019-08-26 05:44:45 +0000184 req_build = self._build_request(req_struct)
185 logging.debug('Raw request to buildbucket: %s' % req_build)
186
Xinan Lin3ba18a02019-08-13 15:44:55 -0700187 cred = None
188 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)
linxinane5eb4552019-08-26 05:44:45 +0000194
195 resp = self.client.ScheduleBuild(req_build, credentials=cred)
196 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700197
Xinan Linc61196b2019-08-13 10:37:30 -0700198 def dummy_run(self):
199 """Perform a dummy run of prpc call to cros_test_platform-dev."""
200
201 req = request_pb2.Request()
202 req_struct = self._request_pb2_to_struct_pb2(req)
203
204 # Use the staging service account to authorize the request.
205 sa_key = self._gen_service_account_key(
206 file_getter.STAGING_CLIENT_SECRETS_FILE)
207 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
208 return self.client.ScheduleBuild(
209 self._build_request(req_struct), credentials=cred)
210
211 # TODO(linxinan): Handle exceptions when fail to transform proto.
212 def _request_pb2_to_struct_pb2(self, req):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700213 """Transform test_platform_request to google struct proto.
Xinan Linc61196b2019-08-13 10:37:30 -0700214
Xinan Lin3ba18a02019-08-13 15:44:55 -0700215 Args:
216 req: A request_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700217
Xinan Lin3ba18a02019-08-13 15:44:55 -0700218 Returns:
219 A struct_pb2 instance.
220 """
Xinan Linc61196b2019-08-13 10:37:30 -0700221 json = json_format.MessageToJson(req)
222 structpb = struct_pb2.Struct()
223 return json_format.Parse(json, structpb, ignore_unknown_fields=True)
224
225 def _build_request(self, req_struct):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700226 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700227
Xinan Lin3ba18a02019-08-13 15:44:55 -0700228 Args:
229 req_struct: A struct_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700230
Xinan Lin3ba18a02019-08-13 15:44:55 -0700231 Returns:
232 A ScheduleBuildRequest instance.
233 """
Xinan Linc61196b2019-08-13 10:37:30 -0700234 recipe_struct = struct_pb2.Struct(fields={
235 'request': struct_pb2.Value(struct_value=req_struct)})
236 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
237 properties=recipe_struct)
238
239 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700240 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700241
Xinan Lin3ba18a02019-08-13 15:44:55 -0700242 Args:
243 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700244
Xinan Lin3ba18a02019-08-13 15:44:55 -0700245 Returns:
246 A service account key.
247 """
Xinan Linc61196b2019-08-13 10:37:30 -0700248 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700249 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700250 return auth.ServiceAccountKey(
251 client_email=key.service_account_email,
252 private_key=key._private_key_pkcs8_pem,
253 private_key_id=key._private_key_id)