blob: 1159fcc477ea2f03837c32626d86361ffccf1f77 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2019 The ChromiumOS Authors
Alex Kleinc05f3d12019-05-29 14:16:21 -06002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Field handler classes.
6
7The field handlers are meant to parse information from or do some other generic
8action for a specific field type for the build_api script.
9"""
10
Alex Kleinc05f3d12019-05-29 14:16:21 -060011import contextlib
Alex Kleinbd6edf82019-07-18 10:30:49 -060012import functools
Chris McDonald1672ddb2021-07-21 11:48:23 -060013import logging
Alex Kleinc05f3d12019-05-29 14:16:21 -060014import os
15import shutil
Mike Frysinger40443592022-05-05 13:03:40 -040016from typing import Iterator, List, Optional, TYPE_CHECKING
Alex Kleinc05f3d12019-05-29 14:16:21 -060017
Mike Frysinger2c024062021-05-22 15:43:22 -040018from chromite.third_party.google.protobuf import message as protobuf_message
Mike Frysinger849d6402019-10-17 00:14:16 -040019
Alex Klein38c7d9e2019-05-08 09:31:19 -060020from chromite.api.controller import controller_util
Alex Kleinc05f3d12019-05-29 14:16:21 -060021from chromite.api.gen.chromiumos import common_pb2
Alex Kleinc05f3d12019-05-29 14:16:21 -060022from chromite.lib import osutils
23
24
Mike Frysinger40443592022-05-05 13:03:40 -040025if TYPE_CHECKING:
Alex Klein1699fab2022-09-08 08:46:06 -060026 from chromite.lib import chroot_lib
Mike Frysinger40443592022-05-05 13:03:40 -040027
28
Alex Kleinbd6edf82019-07-18 10:30:49 -060029class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060030 """Base error class for the module."""
Alex Kleinbd6edf82019-07-18 10:30:49 -060031
32
33class InvalidResultPathError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060034 """Result path is invalid."""
Alex Kleinbd6edf82019-07-18 10:30:49 -060035
36
Alex Kleinc05f3d12019-05-29 14:16:21 -060037class ChrootHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -060038 """Translate a Chroot message to chroot enter arguments and env."""
Alex Kleinc05f3d12019-05-29 14:16:21 -060039
Alex Klein1699fab2022-09-08 08:46:06 -060040 def __init__(self, clear_field):
41 self.clear_field = clear_field
Alex Kleinc05f3d12019-05-29 14:16:21 -060042
Alex Klein1699fab2022-09-08 08:46:06 -060043 def handle(self, message, recurse=True) -> Optional["chroot_lib.Chroot"]:
44 """Parse a message for a chroot field."""
Alex Klein54c891a2023-01-24 10:45:41 -070045 # Find the Chroot field. Search for the field by type to prevent it
46 # being tied to a naming convention.
Alex Klein1699fab2022-09-08 08:46:06 -060047 for descriptor in message.DESCRIPTOR.fields:
48 field = getattr(message, descriptor.name)
49 if isinstance(field, common_pb2.Chroot):
50 chroot = field
51 if self.clear_field:
52 message.ClearField(descriptor.name)
53 return self.parse_chroot(chroot)
Alex Kleinc05f3d12019-05-29 14:16:21 -060054
Alex Klein54c891a2023-01-24 10:45:41 -070055 # Recurse down one level. This is handy for meta-endpoints that use
56 # another endpoint's request to produce data for or about the second
57 # endpoint. e.g. PackageService/NeedsChromeSource.
Alex Klein1699fab2022-09-08 08:46:06 -060058 if recurse:
59 for descriptor in message.DESCRIPTOR.fields:
60 field = getattr(message, descriptor.name)
61 if isinstance(field, protobuf_message.Message):
62 chroot = self.handle(field, recurse=False)
63 if chroot:
64 return chroot
Alex Klein6becabc2020-09-11 14:03:05 -060065
Alex Klein1699fab2022-09-08 08:46:06 -060066 return None
Alex Kleinc05f3d12019-05-29 14:16:21 -060067
Alex Klein1699fab2022-09-08 08:46:06 -060068 def parse_chroot(
69 self, chroot_message: common_pb2.Chroot
70 ) -> "chroot_lib.Chroot":
71 """Parse a Chroot message instance."""
72 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060073
74
Alex Klein1699fab2022-09-08 08:46:06 -060075def handle_chroot(
76 message: protobuf_message.Message, clear_field: Optional[bool] = True
77) -> "chroot_lib.Chroot":
78 """Find and parse the chroot field, returning the Chroot instance."""
79 handler = ChrootHandler(clear_field)
80 chroot = handler.handle(message)
81 if chroot:
82 return chroot
Alex Kleinc05f3d12019-05-29 14:16:21 -060083
Alex Klein1699fab2022-09-08 08:46:06 -060084 logging.warning("No chroot message found, falling back to defaults.")
85 return handler.parse_chroot(common_pb2.Chroot())
Alex Kleinc05f3d12019-05-29 14:16:21 -060086
87
Alex Klein9b7331e2019-12-30 14:37:21 -070088def handle_goma(message, chroot_path):
Alex Klein1699fab2022-09-08 08:46:06 -060089 """Find and parse the GomaConfig field, returning the Goma instance."""
90 for descriptor in message.DESCRIPTOR.fields:
91 field = getattr(message, descriptor.name)
92 if isinstance(field, common_pb2.GomaConfig):
93 goma_config = field
94 return controller_util.ParseGomaConfig(goma_config, chroot_path)
Alex Klein9b7331e2019-12-30 14:37:21 -070095
Alex Klein1699fab2022-09-08 08:46:06 -060096 return None
Alex Klein9b7331e2019-12-30 14:37:21 -070097
98
Joanna Wang92cad812021-11-03 14:52:08 -070099def handle_remoteexec(message: protobuf_message.Message):
Alex Klein1699fab2022-09-08 08:46:06 -0600100 """Find the RemoteexecConfig field, returning the Remoteexec instance."""
101 for descriptor in message.DESCRIPTOR.fields:
102 field = getattr(message, descriptor.name)
103 if isinstance(field, common_pb2.RemoteexecConfig):
104 remoteexec_config = field
105 return controller_util.ParseRemoteexecConfig(remoteexec_config)
Joanna Wang92cad812021-11-03 14:52:08 -0700106
Alex Klein1699fab2022-09-08 08:46:06 -0600107 return None
Joanna Wang92cad812021-11-03 14:52:08 -0700108
109
Alex Kleinc05f3d12019-05-29 14:16:21 -0600110class PathHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600111 """Handles copying a file or directory into or out of the chroot."""
Alex Kleinc05f3d12019-05-29 14:16:21 -0600112
Alex Klein1699fab2022-09-08 08:46:06 -0600113 INSIDE = common_pb2.Path.INSIDE
114 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -0600115
Alex Klein1699fab2022-09-08 08:46:06 -0600116 def __init__(
117 self,
118 field: common_pb2.Path,
119 destination: str,
120 delete: bool,
121 prefix: Optional[str] = None,
122 reset: Optional[bool] = True,
123 ) -> None:
124 """Path handler initialization.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600125
Alex Klein1699fab2022-09-08 08:46:06 -0600126 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600127 field: The Path message.
128 destination: The destination base path.
129 delete: Whether the copied file(s) should be deleted on cleanup.
130 prefix: A path prefix to remove from the destination path when
131 moving files inside the chroot, or to add to the source paths
132 when moving files out of the chroot.
133 reset: Whether to reset the state on cleanup.
Alex Klein1699fab2022-09-08 08:46:06 -0600134 """
135 assert isinstance(field, common_pb2.Path)
136 assert field.path
137 assert field.location
Alex Kleinc05f3d12019-05-29 14:16:21 -0600138
Alex Klein1699fab2022-09-08 08:46:06 -0600139 self.field = field
140 self.destination = destination
141 self.prefix = "" if prefix is None else str(prefix)
142 self.delete = delete
143 self.tempdir = None
144 self.reset = reset
Alex Kleinbd6edf82019-07-18 10:30:49 -0600145
Alex Klein1699fab2022-09-08 08:46:06 -0600146 # For resetting the state.
147 self._transferred = False
148 self._original_message = common_pb2.Path()
149 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600150
Alex Klein1699fab2022-09-08 08:46:06 -0600151 def transfer(self, direction: int) -> None:
152 """Copy the file or directory to its destination.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600153
Alex Klein1699fab2022-09-08 08:46:06 -0600154 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600155 direction: The direction files are being copied (into or out of the
156 chroot). Specifying the direction allows avoiding performing
157 unnecessary copies.
Alex Klein1699fab2022-09-08 08:46:06 -0600158 """
159 if self._transferred:
160 return
Alex Kleinaa705412019-06-04 15:00:30 -0600161
Alex Klein1699fab2022-09-08 08:46:06 -0600162 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600163
Alex Klein1699fab2022-09-08 08:46:06 -0600164 if self.field.location == direction:
165 # Already in the correct location, nothing to do.
166 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600167
Alex Klein54c891a2023-01-24 10:45:41 -0700168 # Create a tempdir for the copied file if we're cleaning it up
169 # afterwords.
Alex Klein1699fab2022-09-08 08:46:06 -0600170 if self.delete:
171 self.tempdir = osutils.TempDir(base_dir=self.destination)
172 destination = self.tempdir.tempdir
173 else:
174 destination = self.destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600175
Alex Klein1699fab2022-09-08 08:46:06 -0600176 source = self.field.path
177 if direction == self.OUTSIDE and self.prefix:
178 # When we're extracting files, we need /tmp/result to be
179 # /path/to/chroot/tmp/result.
180 source = os.path.join(self.prefix, source.lstrip(os.sep))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600181
Alex Klein1699fab2022-09-08 08:46:06 -0600182 if os.path.isfile(source):
183 # File - use the old file name, just copy it into the destination.
184 dest_path = os.path.join(destination, os.path.basename(source))
185 copy_fn = shutil.copy
186 else:
187 # Directory - just copy everything into the new location.
188 dest_path = destination
189 copy_fn = functools.partial(
190 osutils.CopyDirContents, allow_nonempty=True
191 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600192
Alex Klein1699fab2022-09-08 08:46:06 -0600193 logging.debug("Copying %s to %s", source, dest_path)
194 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600195
Alex Klein1699fab2022-09-08 08:46:06 -0600196 # Clean up the destination path for returning, if applicable.
197 return_path = dest_path
198 if direction == self.INSIDE and return_path.startswith(self.prefix):
199 return_path = return_path[len(self.prefix) :]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600200
Alex Klein1699fab2022-09-08 08:46:06 -0600201 self.field.path = return_path
202 self.field.location = direction
203 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600204
Alex Klein1699fab2022-09-08 08:46:06 -0600205 def cleanup(self):
Alex Kleinb6d52022022-10-18 08:55:06 -0600206 """Post-execution cleanup."""
Alex Klein1699fab2022-09-08 08:46:06 -0600207 if self.tempdir:
208 self.tempdir.Cleanup()
209 self.tempdir = None
Alex Kleinc05f3d12019-05-29 14:16:21 -0600210
Alex Klein1699fab2022-09-08 08:46:06 -0600211 if self.reset:
212 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600213
Alex Kleinc05f3d12019-05-29 14:16:21 -0600214
Alex Kleinf0717a62019-12-06 09:45:00 -0700215class SyncedDirHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600216 """Handler for syncing directories across the chroot boundary."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 def __init__(self, field, destination, prefix):
219 self.field = field
220 self.prefix = prefix
Alex Kleinf0717a62019-12-06 09:45:00 -0700221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 self.source = self.field.dir
223 if not self.source.endswith(os.sep):
224 self.source += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700225
Alex Klein1699fab2022-09-08 08:46:06 -0600226 self.destination = destination
227 if not self.destination.endswith(os.sep):
228 self.destination += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 # For resetting the message later.
231 self._original_message = common_pb2.SyncedDir()
232 self._original_message.CopyFrom(self.field)
Alex Kleinf0717a62019-12-06 09:45:00 -0700233
Alex Klein1699fab2022-09-08 08:46:06 -0600234 def _sync(self, src, dest):
235 logging.info("Syncing %s to %s", src, dest)
236 # TODO: This would probably be more efficient with rsync.
237 osutils.EmptyDir(dest)
238 osutils.CopyDirContents(src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700239
Alex Klein1699fab2022-09-08 08:46:06 -0600240 def sync_in(self):
241 """Sync files from the source directory to the destination directory."""
242 self._sync(self.source, self.destination)
243 self.field.dir = "/%s" % os.path.relpath(self.destination, self.prefix)
Alex Kleinf0717a62019-12-06 09:45:00 -0700244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 def sync_out(self):
246 """Sync files from the destination directory to the source directory."""
247 self._sync(self.destination, self.source)
248 self.field.CopyFrom(self._original_message)
Alex Kleinf0717a62019-12-06 09:45:00 -0700249
250
Alex Kleinc05f3d12019-05-29 14:16:21 -0600251@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600252def copy_paths_in(
253 message: protobuf_message.Message,
254 destination: str,
255 delete: Optional[bool] = True,
256 prefix: Optional[str] = None,
257) -> Iterator[List[PathHandler]]:
258 """Context manager function to transfer and cleanup all Path messages.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600259
Alex Klein1699fab2022-09-08 08:46:06 -0600260 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600261 message: A message whose Path messages should be transferred.
262 destination: The base destination path.
263 delete: Whether the file(s) should be deleted.
264 prefix: A prefix path to remove from the final destination path in the
265 Path message (i.e. remove the chroot path).
Alex Kleinc05f3d12019-05-29 14:16:21 -0600266
Alex Klein1699fab2022-09-08 08:46:06 -0600267 Yields:
Alex Kleina0442682022-10-10 13:47:38 -0600268 list[PathHandler]: The path handlers.
Alex Klein1699fab2022-09-08 08:46:06 -0600269 """
270 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600271
Alex Klein1699fab2022-09-08 08:46:06 -0600272 handlers = _extract_handlers(
273 message, destination, prefix, delete=delete, reset=True
274 )
Alex Kleinaa705412019-06-04 15:00:30 -0600275
Alex Kleinaa705412019-06-04 15:00:30 -0600276 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600277 handler.transfer(PathHandler.INSIDE)
278
279 try:
280 yield handlers
281 finally:
282 for handler in handlers:
283 handler.cleanup()
Alex Kleinaa705412019-06-04 15:00:30 -0600284
285
Alex Kleinf0717a62019-12-06 09:45:00 -0700286@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600287def sync_dirs(
288 message: protobuf_message.Message, destination: str, prefix: str
289) -> Iterator[SyncedDirHandler]:
290 """Context manager function to handle SyncedDir messages.
Alex Kleinf0717a62019-12-06 09:45:00 -0700291
Alex Klein1699fab2022-09-08 08:46:06 -0600292 The sync semantics are effectively:
Alex Kleina0442682022-10-10 13:47:38 -0600293 rsync -r --del source/ destination/
294 * The endpoint runs. *
295 rsync -r --del destination/ source/
Alex Kleinf0717a62019-12-06 09:45:00 -0700296
Alex Klein1699fab2022-09-08 08:46:06 -0600297 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600298 message: A message whose SyncedPath messages should be synced.
299 destination: The destination path.
300 prefix: A prefix path to remove from the final destination path in the
301 Path message (i.e. remove the chroot path).
Alex Kleinf0717a62019-12-06 09:45:00 -0700302
Alex Klein1699fab2022-09-08 08:46:06 -0600303 Yields:
Alex Kleina0442682022-10-10 13:47:38 -0600304 The handlers.
Alex Klein1699fab2022-09-08 08:46:06 -0600305 """
306 assert destination
Alex Kleinf0717a62019-12-06 09:45:00 -0700307
Alex Klein1699fab2022-09-08 08:46:06 -0600308 handlers = _extract_handlers(
309 message,
310 destination,
311 prefix=prefix,
312 delete=False,
313 reset=True,
314 message_type=common_pb2.SyncedDir,
315 )
Alex Kleinf0717a62019-12-06 09:45:00 -0700316
Alex Kleinf0717a62019-12-06 09:45:00 -0700317 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600318 handler.sync_in()
319
320 try:
321 yield handlers
322 finally:
323 for handler in handlers:
324 handler.sync_out()
Alex Kleinf0717a62019-12-06 09:45:00 -0700325
326
Alex Klein1699fab2022-09-08 08:46:06 -0600327def extract_results(
328 request_message: protobuf_message.Message,
329 response_message: protobuf_message.Message,
330 chroot: "chroot_lib.Chroot",
331) -> None:
332 """Transfer all response Path messages to the request's ResultPath.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600333
Alex Klein1699fab2022-09-08 08:46:06 -0600334 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600335 request_message: The request message containing a ResultPath message.
336 response_message: The response message whose Path message(s) are to be
337 transferred.
338 chroot: The chroot the files are being copied out of.
Alex Klein1699fab2022-09-08 08:46:06 -0600339 """
340 # Find the ResultPath.
341 for descriptor in request_message.DESCRIPTOR.fields:
342 field = getattr(request_message, descriptor.name)
343 if isinstance(field, common_pb2.ResultPath):
344 result_path_message = field
345 break
Alex Kleinbd6edf82019-07-18 10:30:49 -0600346 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600347 # No ResultPath to handle.
348 return
Alex Kleinbd6edf82019-07-18 10:30:49 -0600349
Alex Klein1699fab2022-09-08 08:46:06 -0600350 destination = result_path_message.path.path
351 handlers = _extract_handlers(
352 response_message, destination, chroot.path, delete=False, reset=False
353 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600354
Alex Klein1699fab2022-09-08 08:46:06 -0600355 for handler in handlers:
356 handler.transfer(PathHandler.OUTSIDE)
357 handler.cleanup()
Alex Kleinc05f3d12019-05-29 14:16:21 -0600358
Alex Klein1699fab2022-09-08 08:46:06 -0600359
360def _extract_handlers(
361 message,
362 destination,
363 prefix,
364 delete=False,
365 reset=False,
366 field_name=None,
367 message_type=None,
368):
369 """Recursive helper for handle_paths to extract Path messages."""
370 message_type = message_type or common_pb2.Path
371 is_path_target = message_type is common_pb2.Path
372 is_synced_target = message_type is common_pb2.SyncedDir
373
374 is_message = isinstance(message, protobuf_message.Message)
375 is_result_path = isinstance(message, common_pb2.ResultPath)
376 if not is_message or is_result_path:
377 # Base case: Nothing to handle.
378 # There's nothing we can do with scalar values.
379 # Skip ResultPath instances to avoid unnecessary file copying.
380 return []
381 elif is_path_target and isinstance(message, common_pb2.Path):
382 # Base case: Create handler for this message.
383 if not message.path or not message.location:
384 logging.debug("Skipping %s; incomplete.", field_name or "message")
385 return []
386
387 handler = PathHandler(
388 message, destination, delete=delete, prefix=prefix, reset=reset
389 )
390 return [handler]
391 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
392 if not message.dir:
393 logging.debug(
394 "Skipping %s; no directory given.", field_name or "message"
395 )
396 return []
397
398 handler = SyncedDirHandler(message, destination, prefix)
399 return [handler]
400
401 # Iterate through each field and recurse.
402 handlers = []
403 for descriptor in message.DESCRIPTOR.fields:
404 field = getattr(message, descriptor.name)
405 if field_name:
406 new_field_name = "%s.%s" % (field_name, descriptor.name)
407 else:
408 new_field_name = descriptor.name
409
410 if isinstance(field, protobuf_message.Message):
411 # Recurse for nested Paths.
412 handlers.extend(
413 _extract_handlers(
414 field,
415 destination,
416 prefix,
417 delete,
418 reset,
419 field_name=new_field_name,
420 message_type=message_type,
421 )
422 )
423 else:
424 # If it's iterable it may be a repeated field, try each element.
425 try:
426 iterator = iter(field)
427 except TypeError:
428 # Definitely not a repeated field, just move on.
429 continue
430
431 for element in iterator:
432 handlers.extend(
433 _extract_handlers(
434 element,
435 destination,
436 prefix,
437 delete,
438 reset,
439 field_name=new_field_name,
440 message_type=message_type,
441 )
442 )
443
444 return handlers