blob: 17b47b09927d467ee5db8899e5c8cb39ad87a673 [file] [log] [blame]
Sean McAllisterffce55f2021-02-22 20:08:18 -07001#!/usr/bin/env python3
2# Copyright 2021 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Run an equivalent to the backfill pipeline locally and generate diffs.
6
7Parse the actual current builder configurations from BuildBucket and run
8the join_config_payloads.py script locally. Generate a diff that shows any
9changes using the tip-of-tree code vs what's running in production.
10"""
11
12import argparse
13import collections
Sean McAllister9b5a33e2021-02-26 10:53:54 -070014import functools
Sean McAllisterffce55f2021-02-22 20:08:18 -070015import itertools
16import json
Sean McAllistere820fc02021-03-20 18:34:16 -060017import logging
Sean McAllisterffce55f2021-02-22 20:08:18 -070018import multiprocessing
19import multiprocessing.pool
20import os
21import pathlib
22import subprocess
23import sys
24import tempfile
25import time
26
Sean McAllister4b589082021-04-16 09:59:21 -060027from common import utilities
28
Sean McAllisterffce55f2021-02-22 20:08:18 -070029# resolve relative directories
30this_dir = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
31hwid_path = (this_dir / "../../platform/chromeos-hwid/v3").resolve()
32join_script = (this_dir / "../payload_utils/join_config_payloads.py").resolve()
Sean McAllistere820fc02021-03-20 18:34:16 -060033merge_script = (this_dir / "../payload_utils/aggregate_messages.py").resolve()
Sean McAllisterffce55f2021-02-22 20:08:18 -070034public_path = (this_dir / "../../overlays").resolve()
35private_path = (this_dir / "../../private-overlays").resolve()
36project_path = (this_dir / "../../project").resolve()
37
Sean McAllisterffce55f2021-02-22 20:08:18 -070038# record to store backfiller configuration in
39BackfillConfig = collections.namedtuple('BackfillConfig', [
40 'program',
41 'project',
42 'hwid_key',
43 'public_model',
44 'private_repo',
45 'private_model',
46])
47
48
Sean McAllisterffce55f2021-02-22 20:08:18 -070049def parse_build_property(build, name):
50 """Parse out a property value from a build and return its value.
51
52 Properties are always JSON values, so we decode them and return the
53 resulting object
54
55 Args:
56 build (dict): json object containing BuildBucket properties
57 name (str): name of the property to look up
58
59 Return:
60 decoded property value or None if not found
61 """
Sean McAllisterf9d0a6b2021-04-09 08:28:47 -060062 return json.loads(build["config"]["properties"]).get(name)
Sean McAllisterffce55f2021-02-22 20:08:18 -070063
64
Sean McAllistere820fc02021-03-20 18:34:16 -060065def run_backfill(config, logname=None, run_imported=True, run_joined=True):
Sean McAllister9b5a33e2021-02-26 10:53:54 -070066 """Run a single backfill job, return diff of current and new output.
67
68 Args:
69 config: BackfillConfig instance for the backfill operation.
70 logname: Filename to redirect stderr to from backfill
Sean McAllistere820fc02021-03-20 18:34:16 -060071 default is to suppress the output
72 run_imported: If True, generate a diff for the imported payload
73 run_joined: If True, generate a diff for the joined payload
Sean McAllister9b5a33e2021-02-26 10:53:54 -070074 """
75
Sean McAllistere820fc02021-03-20 18:34:16 -060076 def run_diff(cmd, current, output):
77 """Execute cmd and diff the current and output files"""
78 logfile.write("running: {}\n".format(" ".join(map(str, cmd))))
79
80 subprocess.run(cmd, stderr=logfile, check=True)
81
82 # if one or the other file doesn't exist, return the other as a diff
83 if current.exists() != output.exists():
84 if current.exists():
85 return open(current).read()
86 return open(output).read()
87
88 # otherwise run diff
Sean McAllister4b589082021-04-16 09:59:21 -060089 return utilities.jqdiff(current, output)
Sean McAllistere820fc02021-03-20 18:34:16 -060090
91 #### start of function body
92
Sean McAllisterf658fb22021-03-22 10:39:41 -060093 # path to project repo and config bundle
94 path_repo = project_path / config.program / config.project
95 path_config = path_repo / "generated/config.jsonproto"
96
Sean McAllister9b5a33e2021-02-26 10:53:54 -070097 logfile = subprocess.DEVNULL
98 if logname:
99 logfile = open(logname, "a")
Sean McAllisterffce55f2021-02-22 20:08:18 -0700100
101 # reef is currently broken because it _needs_ a real portage environment
102 # to pull in common code.
103 # TODO(https://crbug.com/1144956): fix when reef is corrected
Sean McAllisterffce55f2021-02-22 20:08:18 -0700104 if config.program == "reef":
105 return None
106
Sean McAllister9b5a33e2021-02-26 10:53:54 -0700107 cmd = [join_script, "--l", "DEBUG"]
Sean McAllisterffce55f2021-02-22 20:08:18 -0700108 cmd.extend(["--program-name", config.program])
109 cmd.extend(["--project-name", config.project])
110
Sean McAllisterf658fb22021-03-22 10:39:41 -0600111 if path_config.exists():
112 cmd.extend(["--config-bundle", path_config])
113
Sean McAllisterffce55f2021-02-22 20:08:18 -0700114 if config.hwid_key:
115 cmd.extend(["--hwid", hwid_path / config.hwid_key])
116
117 if config.public_model:
118 cmd.extend(["--public-model", public_path / config.public_model])
119
120 if config.private_model:
121 overlay = config.private_repo.split('/')[-1]
122 cmd.extend(
123 ["--private-model", private_path / overlay / config.private_model])
124
125 # create temporary directory for output
Sean McAllistere820fc02021-03-20 18:34:16 -0600126 diff_imported = ""
127 diff_joined = ""
128 with tempfile.TemporaryDirectory() as scratch:
129 scratch = pathlib.Path(scratch)
Sean McAllisterffce55f2021-02-22 20:08:18 -0700130
Sean McAllistere820fc02021-03-20 18:34:16 -0600131 # generate diff of imported payloads
132 path_imported_old = path_repo / "generated/imported.jsonproto"
133 path_imported_new = scratch / "imported.jsonproto"
Sean McAllisterffce55f2021-02-22 20:08:18 -0700134
Sean McAllistere820fc02021-03-20 18:34:16 -0600135 if run_imported:
136 diff_imported = run_diff(
Sean McAllisterf658fb22021-03-22 10:39:41 -0600137 cmd + ["--import-only", "--output", path_imported_new],
Sean McAllistere820fc02021-03-20 18:34:16 -0600138 path_imported_old,
139 path_imported_new,
140 )
141
142 # generate diff of joined payloads
143 if run_joined and path_config.exists():
144 path_joined_old = path_repo / "generated/joined.jsonproto"
145 path_joined_new = scratch / "joined.jsonproto"
146
Sean McAllisterf658fb22021-03-22 10:39:41 -0600147 diff_joined = run_diff(cmd + ["--output", path_joined_new],
148 path_joined_old, path_joined_new)
Sean McAllistere820fc02021-03-20 18:34:16 -0600149
150 return ("{}-{}".format(config.program,
151 config.project), diff_imported, diff_joined)
Sean McAllisterffce55f2021-02-22 20:08:18 -0700152
153
154def run_backfills(args, configs):
155 """Run backfill pipeline for each builder in configs.
156
157 Generate an über diff showing the changes that the current ToT
158 join_config_payloads code would generate vs what's currently committed.
159
160 Write the result to the output file specified on the command line.
161
162 Args:
163 args: command line arguments from argparse
164 configs: list of BackfillConfig instances to execute
165
166 Return:
167 nothing
168 """
169
Sean McAllister9b5a33e2021-02-26 10:53:54 -0700170 # create a logfile if requested
171 kwargs = {}
Sean McAllistere820fc02021-03-20 18:34:16 -0600172 kwargs["run_joined"] = args.joined_diff is not None
Sean McAllister9b5a33e2021-02-26 10:53:54 -0700173 if args.logfile:
174 # open and close the logfile to truncate it so backfills can append
175 # We can't pickle the file object and send it as an argument with
176 # multiprocessing, so this is a workaround for that limitation
177 with open(args.logfile, "w"):
178 kwargs["logname"] = args.logfile
179
Sean McAllisterffce55f2021-02-22 20:08:18 -0700180 nproc = 32
181 nconfig = len(configs)
Sean McAllistere820fc02021-03-20 18:34:16 -0600182 imported_diffs = {}
183 joined_diffs = {}
Sean McAllisterffce55f2021-02-22 20:08:18 -0700184 with multiprocessing.Pool(processes=nproc) as pool:
Sean McAllister9b5a33e2021-02-26 10:53:54 -0700185 results = pool.imap_unordered(
186 functools.partial(run_backfill, **kwargs), configs, chunksize=1)
Sean McAllisterffce55f2021-02-22 20:08:18 -0700187 for ii, result in enumerate(results, 1):
188 sys.stderr.write(
Sean McAllister4b589082021-04-16 09:59:21 -0600189 utilities.clear_line("[{}/{}] Processing backfills".format(
190 ii, nconfig)))
Sean McAllisterffce55f2021-02-22 20:08:18 -0700191
192 if result:
Sean McAllistere820fc02021-03-20 18:34:16 -0600193 key, imported, joined = result
194 imported_diffs[key] = imported
195 joined_diffs[key] = joined
Sean McAllisterffce55f2021-02-22 20:08:18 -0700196
Sean McAllister4b589082021-04-16 09:59:21 -0600197 sys.stderr.write(utilities.clear_line("Processing backfills"))
Sean McAllisterffce55f2021-02-22 20:08:18 -0700198
199 # generate final über diff showing all the changes
Sean McAllistere820fc02021-03-20 18:34:16 -0600200 with open(args.imported_diff, "w") as ofile:
201 for name, result in sorted(imported_diffs.items()):
Sean McAllisterffce55f2021-02-22 20:08:18 -0700202 ofile.write("## ---------------------\n")
203 ofile.write("## diff for {}\n".format(name))
204 ofile.write("\n")
205 ofile.write(result + "\n")
206
Sean McAllistere820fc02021-03-20 18:34:16 -0600207 if args.joined_diff:
208 with open(args.joined_diff, "w") as ofile:
209 for name, result in sorted(joined_diffs.items()):
210 ofile.write("## ---------------------\n")
211 ofile.write("## diff for {}\n".format(name))
212 ofile.write("\n")
213 ofile.write(result + "\n")
Sean McAllisterffce55f2021-02-22 20:08:18 -0700214
215
216def main():
217 parser = argparse.ArgumentParser(
218 description=__doc__,
219 formatter_class=argparse.RawTextHelpFormatter,
220 )
221
222 parser.add_argument(
Sean McAllistere820fc02021-03-20 18:34:16 -0600223 "--imported-diff",
Sean McAllisterffce55f2021-02-22 20:08:18 -0700224 type=str,
225 required=True,
Sean McAllistere820fc02021-03-20 18:34:16 -0600226 help="target file for diff on imported.jsonproto payload",
Sean McAllisterffce55f2021-02-22 20:08:18 -0700227 )
Sean McAllistere820fc02021-03-20 18:34:16 -0600228
229 parser.add_argument(
230 "--joined-diff",
231 type=str,
232 help="target file for diff on joined.jsonproto payload",
233 )
234
Sean McAllister9b5a33e2021-02-26 10:53:54 -0700235 parser.add_argument(
236 "-l",
237 "--logfile",
238 type=str,
239 help="target file to log output from backfills",
240 )
Sean McAllisterffce55f2021-02-22 20:08:18 -0700241 args = parser.parse_args()
242
243 # query BuildBucket for current builder configurations in the infra bucket
Sean McAllister4b589082021-04-16 09:59:21 -0600244 data, status = utilities.call_and_spin(
Sean McAllisterf9d0a6b2021-04-09 08:28:47 -0600245 "Listing backfill builder",
Sean McAllisterffce55f2021-02-22 20:08:18 -0700246 json.dumps({
Sean McAllister4b589082021-04-16 09:59:21 -0600247 "id": {
248 "project": "chromeos",
249 "bucket": "infra",
250 "builder": "backfiller"
251 }
Sean McAllisterffce55f2021-02-22 20:08:18 -0700252 }),
253 "prpc",
254 "call",
255 "cr-buildbucket.appspot.com",
Sean McAllisterf9d0a6b2021-04-09 08:28:47 -0600256 "buildbucket.v2.Builders.GetBuilder",
Sean McAllisterffce55f2021-02-22 20:08:18 -0700257 )
258
259 if status != 0:
260 print(
261 "Error executing prpc call to list builders. Try 'prpc login' first.",
262 file=sys.stderr,
263 )
264 sys.exit(status)
265
Sean McAllisterf9d0a6b2021-04-09 08:28:47 -0600266 builder = json.loads(data)
Sean McAllisterffce55f2021-02-22 20:08:18 -0700267
268 # construct backfill config from the configured builder properties
269 configs = []
Sean McAllisterf9d0a6b2021-04-09 08:28:47 -0600270 for builder_config in parse_build_property(builder, "configs"):
Sean McAllistere820fc02021-03-20 18:34:16 -0600271 config = BackfillConfig(
Sean McAllisterf9d0a6b2021-04-09 08:28:47 -0600272 program=builder_config["program_name"],
273 project=builder_config["project_name"],
274 hwid_key=builder_config.get("hwid_key"),
275 public_model=builder_config.get("public_yaml_path"),
276 private_repo=builder_config.get("private_yaml", {}).get("repo"),
277 private_model=builder_config.get("private_yaml", {}).get("path"),
Sean McAllistere820fc02021-03-20 18:34:16 -0600278 )
279
280 path_repo = project_path / config.program / config.project
281 if not path_repo.exists():
282 logging.warning("{}/{} does not exist locally, skipping".format(
283 config.program, config.project))
284 continue
285
286 configs.append(config)
Sean McAllisterffce55f2021-02-22 20:08:18 -0700287
288 run_backfills(args, configs)
289
290
291if __name__ == "__main__":
292 main()