blob: effed5aa27d59bb8a131b3bf2e787a939571c951 [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
Brian Norrisd0dfeae2023-03-09 13:06:47 -080088def handle_goma(message, chroot_path, out_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
Brian Norrisd0dfeae2023-03-09 13:06:47 -080094 return controller_util.ParseGomaConfig(
95 goma_config, chroot_path, out_path
96 )
Alex Klein9b7331e2019-12-30 14:37:21 -070097
Alex Klein1699fab2022-09-08 08:46:06 -060098 return None
Alex Klein9b7331e2019-12-30 14:37:21 -070099
100
Joanna Wang92cad812021-11-03 14:52:08 -0700101def handle_remoteexec(message: protobuf_message.Message):
Alex Klein1699fab2022-09-08 08:46:06 -0600102 """Find the RemoteexecConfig field, returning the Remoteexec instance."""
103 for descriptor in message.DESCRIPTOR.fields:
104 field = getattr(message, descriptor.name)
105 if isinstance(field, common_pb2.RemoteexecConfig):
106 remoteexec_config = field
107 return controller_util.ParseRemoteexecConfig(remoteexec_config)
Joanna Wang92cad812021-11-03 14:52:08 -0700108
Alex Klein1699fab2022-09-08 08:46:06 -0600109 return None
Joanna Wang92cad812021-11-03 14:52:08 -0700110
111
Alex Kleinc05f3d12019-05-29 14:16:21 -0600112class PathHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600113 """Handles copying a file or directory into or out of the chroot."""
Alex Kleinc05f3d12019-05-29 14:16:21 -0600114
Alex Klein1699fab2022-09-08 08:46:06 -0600115 INSIDE = common_pb2.Path.INSIDE
116 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -0600117
Alex Klein1699fab2022-09-08 08:46:06 -0600118 def __init__(
119 self,
120 field: common_pb2.Path,
121 destination: str,
122 delete: bool,
123 prefix: Optional[str] = None,
124 reset: Optional[bool] = True,
125 ) -> None:
126 """Path handler initialization.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600127
Alex Klein1699fab2022-09-08 08:46:06 -0600128 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600129 field: The Path message.
130 destination: The destination base path.
131 delete: Whether the copied file(s) should be deleted on cleanup.
132 prefix: A path prefix to remove from the destination path when
133 moving files inside the chroot, or to add to the source paths
134 when moving files out of the chroot.
135 reset: Whether to reset the state on cleanup.
Alex Klein1699fab2022-09-08 08:46:06 -0600136 """
137 assert isinstance(field, common_pb2.Path)
138 assert field.path
139 assert field.location
Alex Kleinc05f3d12019-05-29 14:16:21 -0600140
Alex Klein1699fab2022-09-08 08:46:06 -0600141 self.field = field
142 self.destination = destination
143 self.prefix = "" if prefix is None else str(prefix)
144 self.delete = delete
145 self.tempdir = None
146 self.reset = reset
Alex Kleinbd6edf82019-07-18 10:30:49 -0600147
Alex Klein1699fab2022-09-08 08:46:06 -0600148 # For resetting the state.
149 self._transferred = False
150 self._original_message = common_pb2.Path()
151 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600152
Alex Klein1699fab2022-09-08 08:46:06 -0600153 def transfer(self, direction: int) -> None:
154 """Copy the file or directory to its destination.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600155
Alex Klein1699fab2022-09-08 08:46:06 -0600156 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600157 direction: The direction files are being copied (into or out of the
158 chroot). Specifying the direction allows avoiding performing
159 unnecessary copies.
Alex Klein1699fab2022-09-08 08:46:06 -0600160 """
161 if self._transferred:
162 return
Alex Kleinaa705412019-06-04 15:00:30 -0600163
Alex Klein1699fab2022-09-08 08:46:06 -0600164 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 if self.field.location == direction:
167 # Already in the correct location, nothing to do.
168 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600169
Alex Klein54c891a2023-01-24 10:45:41 -0700170 # Create a tempdir for the copied file if we're cleaning it up
171 # afterwords.
Alex Klein1699fab2022-09-08 08:46:06 -0600172 if self.delete:
173 self.tempdir = osutils.TempDir(base_dir=self.destination)
174 destination = self.tempdir.tempdir
175 else:
176 destination = self.destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600177
Alex Klein1699fab2022-09-08 08:46:06 -0600178 source = self.field.path
179 if direction == self.OUTSIDE and self.prefix:
180 # When we're extracting files, we need /tmp/result to be
181 # /path/to/chroot/tmp/result.
182 source = os.path.join(self.prefix, source.lstrip(os.sep))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 if os.path.isfile(source):
185 # File - use the old file name, just copy it into the destination.
186 dest_path = os.path.join(destination, os.path.basename(source))
187 copy_fn = shutil.copy
188 else:
189 # Directory - just copy everything into the new location.
190 dest_path = destination
191 copy_fn = functools.partial(
192 osutils.CopyDirContents, allow_nonempty=True
193 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 logging.debug("Copying %s to %s", source, dest_path)
196 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600197
Alex Klein1699fab2022-09-08 08:46:06 -0600198 # Clean up the destination path for returning, if applicable.
199 return_path = dest_path
200 if direction == self.INSIDE and return_path.startswith(self.prefix):
201 return_path = return_path[len(self.prefix) :]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600202
Alex Klein1699fab2022-09-08 08:46:06 -0600203 self.field.path = return_path
204 self.field.location = direction
205 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 def cleanup(self):
Alex Kleinb6d52022022-10-18 08:55:06 -0600208 """Post-execution cleanup."""
Alex Klein1699fab2022-09-08 08:46:06 -0600209 if self.tempdir:
210 self.tempdir.Cleanup()
211 self.tempdir = None
Alex Kleinc05f3d12019-05-29 14:16:21 -0600212
Alex Klein1699fab2022-09-08 08:46:06 -0600213 if self.reset:
214 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600215
Alex Kleinc05f3d12019-05-29 14:16:21 -0600216
Alex Kleinf0717a62019-12-06 09:45:00 -0700217class SyncedDirHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600218 """Handler for syncing directories across the chroot boundary."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700219
Alex Klein1699fab2022-09-08 08:46:06 -0600220 def __init__(self, field, destination, prefix):
221 self.field = field
222 self.prefix = prefix
Alex Kleinf0717a62019-12-06 09:45:00 -0700223
Alex Klein1699fab2022-09-08 08:46:06 -0600224 self.source = self.field.dir
225 if not self.source.endswith(os.sep):
226 self.source += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 self.destination = destination
229 if not self.destination.endswith(os.sep):
230 self.destination += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700231
Alex Klein1699fab2022-09-08 08:46:06 -0600232 # For resetting the message later.
233 self._original_message = common_pb2.SyncedDir()
234 self._original_message.CopyFrom(self.field)
Alex Kleinf0717a62019-12-06 09:45:00 -0700235
Alex Klein1699fab2022-09-08 08:46:06 -0600236 def _sync(self, src, dest):
237 logging.info("Syncing %s to %s", src, dest)
238 # TODO: This would probably be more efficient with rsync.
239 osutils.EmptyDir(dest)
240 osutils.CopyDirContents(src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700241
Alex Klein1699fab2022-09-08 08:46:06 -0600242 def sync_in(self):
243 """Sync files from the source directory to the destination directory."""
244 self._sync(self.source, self.destination)
245 self.field.dir = "/%s" % os.path.relpath(self.destination, self.prefix)
Alex Kleinf0717a62019-12-06 09:45:00 -0700246
Alex Klein1699fab2022-09-08 08:46:06 -0600247 def sync_out(self):
248 """Sync files from the destination directory to the source directory."""
249 self._sync(self.destination, self.source)
250 self.field.CopyFrom(self._original_message)
Alex Kleinf0717a62019-12-06 09:45:00 -0700251
252
Alex Kleinc05f3d12019-05-29 14:16:21 -0600253@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600254def copy_paths_in(
255 message: protobuf_message.Message,
256 destination: str,
257 delete: Optional[bool] = True,
258 prefix: Optional[str] = None,
259) -> Iterator[List[PathHandler]]:
260 """Context manager function to transfer and cleanup all Path messages.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600263 message: A message whose Path messages should be transferred.
264 destination: The base destination path.
265 delete: Whether the file(s) should be deleted.
266 prefix: A prefix path to remove from the final destination path in the
267 Path message (i.e. remove the chroot path).
Alex Kleinc05f3d12019-05-29 14:16:21 -0600268
Alex Klein1699fab2022-09-08 08:46:06 -0600269 Yields:
Alex Kleina0442682022-10-10 13:47:38 -0600270 list[PathHandler]: The path handlers.
Alex Klein1699fab2022-09-08 08:46:06 -0600271 """
272 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600273
Alex Klein1699fab2022-09-08 08:46:06 -0600274 handlers = _extract_handlers(
275 message, destination, prefix, delete=delete, reset=True
276 )
Alex Kleinaa705412019-06-04 15:00:30 -0600277
Alex Kleinaa705412019-06-04 15:00:30 -0600278 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600279 handler.transfer(PathHandler.INSIDE)
280
281 try:
282 yield handlers
283 finally:
284 for handler in handlers:
285 handler.cleanup()
Alex Kleinaa705412019-06-04 15:00:30 -0600286
287
Alex Kleinf0717a62019-12-06 09:45:00 -0700288@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600289def sync_dirs(
290 message: protobuf_message.Message, destination: str, prefix: str
291) -> Iterator[SyncedDirHandler]:
292 """Context manager function to handle SyncedDir messages.
Alex Kleinf0717a62019-12-06 09:45:00 -0700293
Alex Klein1699fab2022-09-08 08:46:06 -0600294 The sync semantics are effectively:
Alex Kleina0442682022-10-10 13:47:38 -0600295 rsync -r --del source/ destination/
296 * The endpoint runs. *
297 rsync -r --del destination/ source/
Alex Kleinf0717a62019-12-06 09:45:00 -0700298
Alex Klein1699fab2022-09-08 08:46:06 -0600299 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600300 message: A message whose SyncedPath messages should be synced.
301 destination: The destination path.
302 prefix: A prefix path to remove from the final destination path in the
303 Path message (i.e. remove the chroot path).
Alex Kleinf0717a62019-12-06 09:45:00 -0700304
Alex Klein1699fab2022-09-08 08:46:06 -0600305 Yields:
Alex Kleina0442682022-10-10 13:47:38 -0600306 The handlers.
Alex Klein1699fab2022-09-08 08:46:06 -0600307 """
308 assert destination
Alex Kleinf0717a62019-12-06 09:45:00 -0700309
Alex Klein1699fab2022-09-08 08:46:06 -0600310 handlers = _extract_handlers(
311 message,
312 destination,
313 prefix=prefix,
314 delete=False,
315 reset=True,
316 message_type=common_pb2.SyncedDir,
317 )
Alex Kleinf0717a62019-12-06 09:45:00 -0700318
Alex Kleinf0717a62019-12-06 09:45:00 -0700319 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600320 handler.sync_in()
321
322 try:
323 yield handlers
324 finally:
325 for handler in handlers:
326 handler.sync_out()
Alex Kleinf0717a62019-12-06 09:45:00 -0700327
328
Alex Klein1699fab2022-09-08 08:46:06 -0600329def extract_results(
330 request_message: protobuf_message.Message,
331 response_message: protobuf_message.Message,
332 chroot: "chroot_lib.Chroot",
333) -> None:
334 """Transfer all response Path messages to the request's ResultPath.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600335
Alex Klein1699fab2022-09-08 08:46:06 -0600336 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600337 request_message: The request message containing a ResultPath message.
338 response_message: The response message whose Path message(s) are to be
339 transferred.
340 chroot: The chroot the files are being copied out of.
Alex Klein1699fab2022-09-08 08:46:06 -0600341 """
342 # Find the ResultPath.
343 for descriptor in request_message.DESCRIPTOR.fields:
344 field = getattr(request_message, descriptor.name)
345 if isinstance(field, common_pb2.ResultPath):
346 result_path_message = field
347 break
Alex Kleinbd6edf82019-07-18 10:30:49 -0600348 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600349 # No ResultPath to handle.
350 return
Alex Kleinbd6edf82019-07-18 10:30:49 -0600351
Alex Klein1699fab2022-09-08 08:46:06 -0600352 destination = result_path_message.path.path
353 handlers = _extract_handlers(
354 response_message, destination, chroot.path, delete=False, reset=False
355 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600356
Alex Klein1699fab2022-09-08 08:46:06 -0600357 for handler in handlers:
358 handler.transfer(PathHandler.OUTSIDE)
359 handler.cleanup()
Alex Kleinc05f3d12019-05-29 14:16:21 -0600360
Alex Klein1699fab2022-09-08 08:46:06 -0600361
362def _extract_handlers(
363 message,
364 destination,
365 prefix,
366 delete=False,
367 reset=False,
368 field_name=None,
369 message_type=None,
370):
371 """Recursive helper for handle_paths to extract Path messages."""
372 message_type = message_type or common_pb2.Path
373 is_path_target = message_type is common_pb2.Path
374 is_synced_target = message_type is common_pb2.SyncedDir
375
376 is_message = isinstance(message, protobuf_message.Message)
377 is_result_path = isinstance(message, common_pb2.ResultPath)
378 if not is_message or is_result_path:
379 # Base case: Nothing to handle.
380 # There's nothing we can do with scalar values.
381 # Skip ResultPath instances to avoid unnecessary file copying.
382 return []
383 elif is_path_target and isinstance(message, common_pb2.Path):
384 # Base case: Create handler for this message.
385 if not message.path or not message.location:
386 logging.debug("Skipping %s; incomplete.", field_name or "message")
387 return []
388
389 handler = PathHandler(
390 message, destination, delete=delete, prefix=prefix, reset=reset
391 )
392 return [handler]
393 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
394 if not message.dir:
395 logging.debug(
396 "Skipping %s; no directory given.", field_name or "message"
397 )
398 return []
399
400 handler = SyncedDirHandler(message, destination, prefix)
401 return [handler]
402
403 # Iterate through each field and recurse.
404 handlers = []
405 for descriptor in message.DESCRIPTOR.fields:
406 field = getattr(message, descriptor.name)
407 if field_name:
408 new_field_name = "%s.%s" % (field_name, descriptor.name)
409 else:
410 new_field_name = descriptor.name
411
412 if isinstance(field, protobuf_message.Message):
413 # Recurse for nested Paths.
414 handlers.extend(
415 _extract_handlers(
416 field,
417 destination,
418 prefix,
419 delete,
420 reset,
421 field_name=new_field_name,
422 message_type=message_type,
423 )
424 )
425 else:
426 # If it's iterable it may be a repeated field, try each element.
427 try:
428 iterator = iter(field)
429 except TypeError:
430 # Definitely not a repeated field, just move on.
431 continue
432
433 for element in iterator:
434 handlers.extend(
435 _extract_handlers(
436 element,
437 destination,
438 prefix,
439 delete,
440 reset,
441 field_name=new_field_name,
442 message_type=message_type,
443 )
444 )
445
446 return handlers