blob: 7e2eb854cfbe98452a392e0c2eaf1fff95255044 [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."""
45 # Find the Chroot field. Search for the field by type to prevent it being
46 # tied to a naming convention.
47 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 Klein1699fab2022-09-08 08:46:06 -060055 # Recurse down one level. This is handy for meta-endpoints that use another
56 # endpoint's request to produce data for or about the second endpoint.
57 # e.g. PackageService/NeedsChromeSource.
58 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 Klein1699fab2022-09-08 08:46:06 -0600168 # Create a tempdir for the copied file if we're cleaning it up afterwords.
169 if self.delete:
170 self.tempdir = osutils.TempDir(base_dir=self.destination)
171 destination = self.tempdir.tempdir
172 else:
173 destination = self.destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600174
Alex Klein1699fab2022-09-08 08:46:06 -0600175 source = self.field.path
176 if direction == self.OUTSIDE and self.prefix:
177 # When we're extracting files, we need /tmp/result to be
178 # /path/to/chroot/tmp/result.
179 source = os.path.join(self.prefix, source.lstrip(os.sep))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600180
Alex Klein1699fab2022-09-08 08:46:06 -0600181 if os.path.isfile(source):
182 # File - use the old file name, just copy it into the destination.
183 dest_path = os.path.join(destination, os.path.basename(source))
184 copy_fn = shutil.copy
185 else:
186 # Directory - just copy everything into the new location.
187 dest_path = destination
188 copy_fn = functools.partial(
189 osutils.CopyDirContents, allow_nonempty=True
190 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600191
Alex Klein1699fab2022-09-08 08:46:06 -0600192 logging.debug("Copying %s to %s", source, dest_path)
193 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 # Clean up the destination path for returning, if applicable.
196 return_path = dest_path
197 if direction == self.INSIDE and return_path.startswith(self.prefix):
198 return_path = return_path[len(self.prefix) :]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 self.field.path = return_path
201 self.field.location = direction
202 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 def cleanup(self):
Alex Kleinb6d52022022-10-18 08:55:06 -0600205 """Post-execution cleanup."""
Alex Klein1699fab2022-09-08 08:46:06 -0600206 if self.tempdir:
207 self.tempdir.Cleanup()
208 self.tempdir = None
Alex Kleinc05f3d12019-05-29 14:16:21 -0600209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 if self.reset:
211 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600212
Alex Kleinc05f3d12019-05-29 14:16:21 -0600213
Alex Kleinf0717a62019-12-06 09:45:00 -0700214class SyncedDirHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600215 """Handler for syncing directories across the chroot boundary."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700216
Alex Klein1699fab2022-09-08 08:46:06 -0600217 def __init__(self, field, destination, prefix):
218 self.field = field
219 self.prefix = prefix
Alex Kleinf0717a62019-12-06 09:45:00 -0700220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 self.source = self.field.dir
222 if not self.source.endswith(os.sep):
223 self.source += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 self.destination = destination
226 if not self.destination.endswith(os.sep):
227 self.destination += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700228
Alex Klein1699fab2022-09-08 08:46:06 -0600229 # For resetting the message later.
230 self._original_message = common_pb2.SyncedDir()
231 self._original_message.CopyFrom(self.field)
Alex Kleinf0717a62019-12-06 09:45:00 -0700232
Alex Klein1699fab2022-09-08 08:46:06 -0600233 def _sync(self, src, dest):
234 logging.info("Syncing %s to %s", src, dest)
235 # TODO: This would probably be more efficient with rsync.
236 osutils.EmptyDir(dest)
237 osutils.CopyDirContents(src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700238
Alex Klein1699fab2022-09-08 08:46:06 -0600239 def sync_in(self):
240 """Sync files from the source directory to the destination directory."""
241 self._sync(self.source, self.destination)
242 self.field.dir = "/%s" % os.path.relpath(self.destination, self.prefix)
Alex Kleinf0717a62019-12-06 09:45:00 -0700243
Alex Klein1699fab2022-09-08 08:46:06 -0600244 def sync_out(self):
245 """Sync files from the destination directory to the source directory."""
246 self._sync(self.destination, self.source)
247 self.field.CopyFrom(self._original_message)
Alex Kleinf0717a62019-12-06 09:45:00 -0700248
249
Alex Kleinc05f3d12019-05-29 14:16:21 -0600250@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600251def copy_paths_in(
252 message: protobuf_message.Message,
253 destination: str,
254 delete: Optional[bool] = True,
255 prefix: Optional[str] = None,
256) -> Iterator[List[PathHandler]]:
257 """Context manager function to transfer and cleanup all Path messages.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600260 message: A message whose Path messages should be transferred.
261 destination: The base destination path.
262 delete: Whether the file(s) should be deleted.
263 prefix: A prefix path to remove from the final destination path in the
264 Path message (i.e. remove the chroot path).
Alex Kleinc05f3d12019-05-29 14:16:21 -0600265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 Yields:
Alex Kleina0442682022-10-10 13:47:38 -0600267 list[PathHandler]: The path handlers.
Alex Klein1699fab2022-09-08 08:46:06 -0600268 """
269 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600270
Alex Klein1699fab2022-09-08 08:46:06 -0600271 handlers = _extract_handlers(
272 message, destination, prefix, delete=delete, reset=True
273 )
Alex Kleinaa705412019-06-04 15:00:30 -0600274
Alex Kleinaa705412019-06-04 15:00:30 -0600275 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600276 handler.transfer(PathHandler.INSIDE)
277
278 try:
279 yield handlers
280 finally:
281 for handler in handlers:
282 handler.cleanup()
Alex Kleinaa705412019-06-04 15:00:30 -0600283
284
Alex Kleinf0717a62019-12-06 09:45:00 -0700285@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600286def sync_dirs(
287 message: protobuf_message.Message, destination: str, prefix: str
288) -> Iterator[SyncedDirHandler]:
289 """Context manager function to handle SyncedDir messages.
Alex Kleinf0717a62019-12-06 09:45:00 -0700290
Alex Klein1699fab2022-09-08 08:46:06 -0600291 The sync semantics are effectively:
Alex Kleina0442682022-10-10 13:47:38 -0600292 rsync -r --del source/ destination/
293 * The endpoint runs. *
294 rsync -r --del destination/ source/
Alex Kleinf0717a62019-12-06 09:45:00 -0700295
Alex Klein1699fab2022-09-08 08:46:06 -0600296 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600297 message: A message whose SyncedPath messages should be synced.
298 destination: The destination path.
299 prefix: A prefix path to remove from the final destination path in the
300 Path message (i.e. remove the chroot path).
Alex Kleinf0717a62019-12-06 09:45:00 -0700301
Alex Klein1699fab2022-09-08 08:46:06 -0600302 Yields:
Alex Kleina0442682022-10-10 13:47:38 -0600303 The handlers.
Alex Klein1699fab2022-09-08 08:46:06 -0600304 """
305 assert destination
Alex Kleinf0717a62019-12-06 09:45:00 -0700306
Alex Klein1699fab2022-09-08 08:46:06 -0600307 handlers = _extract_handlers(
308 message,
309 destination,
310 prefix=prefix,
311 delete=False,
312 reset=True,
313 message_type=common_pb2.SyncedDir,
314 )
Alex Kleinf0717a62019-12-06 09:45:00 -0700315
Alex Kleinf0717a62019-12-06 09:45:00 -0700316 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600317 handler.sync_in()
318
319 try:
320 yield handlers
321 finally:
322 for handler in handlers:
323 handler.sync_out()
Alex Kleinf0717a62019-12-06 09:45:00 -0700324
325
Alex Klein1699fab2022-09-08 08:46:06 -0600326def extract_results(
327 request_message: protobuf_message.Message,
328 response_message: protobuf_message.Message,
329 chroot: "chroot_lib.Chroot",
330) -> None:
331 """Transfer all response Path messages to the request's ResultPath.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600332
Alex Klein1699fab2022-09-08 08:46:06 -0600333 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600334 request_message: The request message containing a ResultPath message.
335 response_message: The response message whose Path message(s) are to be
336 transferred.
337 chroot: The chroot the files are being copied out of.
Alex Klein1699fab2022-09-08 08:46:06 -0600338 """
339 # Find the ResultPath.
340 for descriptor in request_message.DESCRIPTOR.fields:
341 field = getattr(request_message, descriptor.name)
342 if isinstance(field, common_pb2.ResultPath):
343 result_path_message = field
344 break
Alex Kleinbd6edf82019-07-18 10:30:49 -0600345 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600346 # No ResultPath to handle.
347 return
Alex Kleinbd6edf82019-07-18 10:30:49 -0600348
Alex Klein1699fab2022-09-08 08:46:06 -0600349 destination = result_path_message.path.path
350 handlers = _extract_handlers(
351 response_message, destination, chroot.path, delete=False, reset=False
352 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600353
Alex Klein1699fab2022-09-08 08:46:06 -0600354 for handler in handlers:
355 handler.transfer(PathHandler.OUTSIDE)
356 handler.cleanup()
Alex Kleinc05f3d12019-05-29 14:16:21 -0600357
Alex Klein1699fab2022-09-08 08:46:06 -0600358
359def _extract_handlers(
360 message,
361 destination,
362 prefix,
363 delete=False,
364 reset=False,
365 field_name=None,
366 message_type=None,
367):
368 """Recursive helper for handle_paths to extract Path messages."""
369 message_type = message_type or common_pb2.Path
370 is_path_target = message_type is common_pb2.Path
371 is_synced_target = message_type is common_pb2.SyncedDir
372
373 is_message = isinstance(message, protobuf_message.Message)
374 is_result_path = isinstance(message, common_pb2.ResultPath)
375 if not is_message or is_result_path:
376 # Base case: Nothing to handle.
377 # There's nothing we can do with scalar values.
378 # Skip ResultPath instances to avoid unnecessary file copying.
379 return []
380 elif is_path_target and isinstance(message, common_pb2.Path):
381 # Base case: Create handler for this message.
382 if not message.path or not message.location:
383 logging.debug("Skipping %s; incomplete.", field_name or "message")
384 return []
385
386 handler = PathHandler(
387 message, destination, delete=delete, prefix=prefix, reset=reset
388 )
389 return [handler]
390 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
391 if not message.dir:
392 logging.debug(
393 "Skipping %s; no directory given.", field_name or "message"
394 )
395 return []
396
397 handler = SyncedDirHandler(message, destination, prefix)
398 return [handler]
399
400 # Iterate through each field and recurse.
401 handlers = []
402 for descriptor in message.DESCRIPTOR.fields:
403 field = getattr(message, descriptor.name)
404 if field_name:
405 new_field_name = "%s.%s" % (field_name, descriptor.name)
406 else:
407 new_field_name = descriptor.name
408
409 if isinstance(field, protobuf_message.Message):
410 # Recurse for nested Paths.
411 handlers.extend(
412 _extract_handlers(
413 field,
414 destination,
415 prefix,
416 delete,
417 reset,
418 field_name=new_field_name,
419 message_type=message_type,
420 )
421 )
422 else:
423 # If it's iterable it may be a repeated field, try each element.
424 try:
425 iterator = iter(field)
426 except TypeError:
427 # Definitely not a repeated field, just move on.
428 continue
429
430 for element in iterator:
431 handlers.extend(
432 _extract_handlers(
433 element,
434 destination,
435 prefix,
436 delete,
437 reset,
438 field_name=new_field_name,
439 message_type=message_type,
440 )
441 )
442
443 return handlers