blob: faedcfb2313c9141303289fac5b8486099670118 [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:
127 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 moving
131 files inside the chroot, or to add to the source paths when moving files
132 out of the chroot.
133 reset: Whether to reset the state on cleanup.
134 """
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:
155 direction: The direction files are being copied (into or out of the
156 chroot). Specifying the direction allows avoiding performing unnecessary
157 copies.
158 """
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):
205 if self.tempdir:
206 self.tempdir.Cleanup()
207 self.tempdir = None
Alex Kleinc05f3d12019-05-29 14:16:21 -0600208
Alex Klein1699fab2022-09-08 08:46:06 -0600209 if self.reset:
210 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600211
Alex Kleinc05f3d12019-05-29 14:16:21 -0600212
Alex Kleinf0717a62019-12-06 09:45:00 -0700213class SyncedDirHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600214 """Handler for syncing directories across the chroot boundary."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700215
Alex Klein1699fab2022-09-08 08:46:06 -0600216 def __init__(self, field, destination, prefix):
217 self.field = field
218 self.prefix = prefix
Alex Kleinf0717a62019-12-06 09:45:00 -0700219
Alex Klein1699fab2022-09-08 08:46:06 -0600220 self.source = self.field.dir
221 if not self.source.endswith(os.sep):
222 self.source += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700223
Alex Klein1699fab2022-09-08 08:46:06 -0600224 self.destination = destination
225 if not self.destination.endswith(os.sep):
226 self.destination += os.sep
Alex Kleinf0717a62019-12-06 09:45:00 -0700227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 # For resetting the message later.
229 self._original_message = common_pb2.SyncedDir()
230 self._original_message.CopyFrom(self.field)
Alex Kleinf0717a62019-12-06 09:45:00 -0700231
Alex Klein1699fab2022-09-08 08:46:06 -0600232 def _sync(self, src, dest):
233 logging.info("Syncing %s to %s", src, dest)
234 # TODO: This would probably be more efficient with rsync.
235 osutils.EmptyDir(dest)
236 osutils.CopyDirContents(src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700237
Alex Klein1699fab2022-09-08 08:46:06 -0600238 def sync_in(self):
239 """Sync files from the source directory to the destination directory."""
240 self._sync(self.source, self.destination)
241 self.field.dir = "/%s" % os.path.relpath(self.destination, self.prefix)
Alex Kleinf0717a62019-12-06 09:45:00 -0700242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 def sync_out(self):
244 """Sync files from the destination directory to the source directory."""
245 self._sync(self.destination, self.source)
246 self.field.CopyFrom(self._original_message)
Alex Kleinf0717a62019-12-06 09:45:00 -0700247
248
Alex Kleinc05f3d12019-05-29 14:16:21 -0600249@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600250def copy_paths_in(
251 message: protobuf_message.Message,
252 destination: str,
253 delete: Optional[bool] = True,
254 prefix: Optional[str] = None,
255) -> Iterator[List[PathHandler]]:
256 """Context manager function to transfer and cleanup all Path messages.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600257
Alex Klein1699fab2022-09-08 08:46:06 -0600258 Args:
259 message: A message whose Path messages should be transferred.
260 destination: The base destination path.
261 delete: Whether the file(s) should be deleted.
262 prefix: A prefix path to remove from the final destination path in the Path
263 message (i.e. remove the chroot path).
Alex Kleinc05f3d12019-05-29 14:16:21 -0600264
Alex Klein1699fab2022-09-08 08:46:06 -0600265 Yields:
266 list[PathHandler]: The path handlers.
267 """
268 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 handlers = _extract_handlers(
271 message, destination, prefix, delete=delete, reset=True
272 )
Alex Kleinaa705412019-06-04 15:00:30 -0600273
Alex Kleinaa705412019-06-04 15:00:30 -0600274 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600275 handler.transfer(PathHandler.INSIDE)
276
277 try:
278 yield handlers
279 finally:
280 for handler in handlers:
281 handler.cleanup()
Alex Kleinaa705412019-06-04 15:00:30 -0600282
283
Alex Kleinf0717a62019-12-06 09:45:00 -0700284@contextlib.contextmanager
Alex Klein1699fab2022-09-08 08:46:06 -0600285def sync_dirs(
286 message: protobuf_message.Message, destination: str, prefix: str
287) -> Iterator[SyncedDirHandler]:
288 """Context manager function to handle SyncedDir messages.
Alex Kleinf0717a62019-12-06 09:45:00 -0700289
Alex Klein1699fab2022-09-08 08:46:06 -0600290 The sync semantics are effectively:
291 rsync -r --del source/ destination/
292 * The endpoint runs. *
293 rsync -r --del destination/ source/
Alex Kleinf0717a62019-12-06 09:45:00 -0700294
Alex Klein1699fab2022-09-08 08:46:06 -0600295 Args:
296 message: A message whose SyncedPath messages should be synced.
297 destination: The destination path.
298 prefix: A prefix path to remove from the final destination path in the Path
299 message (i.e. remove the chroot path).
Alex Kleinf0717a62019-12-06 09:45:00 -0700300
Alex Klein1699fab2022-09-08 08:46:06 -0600301 Yields:
302 The handlers.
303 """
304 assert destination
Alex Kleinf0717a62019-12-06 09:45:00 -0700305
Alex Klein1699fab2022-09-08 08:46:06 -0600306 handlers = _extract_handlers(
307 message,
308 destination,
309 prefix=prefix,
310 delete=False,
311 reset=True,
312 message_type=common_pb2.SyncedDir,
313 )
Alex Kleinf0717a62019-12-06 09:45:00 -0700314
Alex Kleinf0717a62019-12-06 09:45:00 -0700315 for handler in handlers:
Alex Klein1699fab2022-09-08 08:46:06 -0600316 handler.sync_in()
317
318 try:
319 yield handlers
320 finally:
321 for handler in handlers:
322 handler.sync_out()
Alex Kleinf0717a62019-12-06 09:45:00 -0700323
324
Alex Klein1699fab2022-09-08 08:46:06 -0600325def extract_results(
326 request_message: protobuf_message.Message,
327 response_message: protobuf_message.Message,
328 chroot: "chroot_lib.Chroot",
329) -> None:
330 """Transfer all response Path messages to the request's ResultPath.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600331
Alex Klein1699fab2022-09-08 08:46:06 -0600332 Args:
333 request_message: The request message containing a ResultPath message.
334 response_message: The response message whose Path message(s) are to be
335 transferred.
336 chroot: The chroot the files are being copied out of.
337 """
338 # Find the ResultPath.
339 for descriptor in request_message.DESCRIPTOR.fields:
340 field = getattr(request_message, descriptor.name)
341 if isinstance(field, common_pb2.ResultPath):
342 result_path_message = field
343 break
Alex Kleinbd6edf82019-07-18 10:30:49 -0600344 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600345 # No ResultPath to handle.
346 return
Alex Kleinbd6edf82019-07-18 10:30:49 -0600347
Alex Klein1699fab2022-09-08 08:46:06 -0600348 destination = result_path_message.path.path
349 handlers = _extract_handlers(
350 response_message, destination, chroot.path, delete=False, reset=False
351 )
Alex Kleinc05f3d12019-05-29 14:16:21 -0600352
Alex Klein1699fab2022-09-08 08:46:06 -0600353 for handler in handlers:
354 handler.transfer(PathHandler.OUTSIDE)
355 handler.cleanup()
Alex Kleinc05f3d12019-05-29 14:16:21 -0600356
Alex Klein1699fab2022-09-08 08:46:06 -0600357
358def _extract_handlers(
359 message,
360 destination,
361 prefix,
362 delete=False,
363 reset=False,
364 field_name=None,
365 message_type=None,
366):
367 """Recursive helper for handle_paths to extract Path messages."""
368 message_type = message_type or common_pb2.Path
369 is_path_target = message_type is common_pb2.Path
370 is_synced_target = message_type is common_pb2.SyncedDir
371
372 is_message = isinstance(message, protobuf_message.Message)
373 is_result_path = isinstance(message, common_pb2.ResultPath)
374 if not is_message or is_result_path:
375 # Base case: Nothing to handle.
376 # There's nothing we can do with scalar values.
377 # Skip ResultPath instances to avoid unnecessary file copying.
378 return []
379 elif is_path_target and isinstance(message, common_pb2.Path):
380 # Base case: Create handler for this message.
381 if not message.path or not message.location:
382 logging.debug("Skipping %s; incomplete.", field_name or "message")
383 return []
384
385 handler = PathHandler(
386 message, destination, delete=delete, prefix=prefix, reset=reset
387 )
388 return [handler]
389 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
390 if not message.dir:
391 logging.debug(
392 "Skipping %s; no directory given.", field_name or "message"
393 )
394 return []
395
396 handler = SyncedDirHandler(message, destination, prefix)
397 return [handler]
398
399 # Iterate through each field and recurse.
400 handlers = []
401 for descriptor in message.DESCRIPTOR.fields:
402 field = getattr(message, descriptor.name)
403 if field_name:
404 new_field_name = "%s.%s" % (field_name, descriptor.name)
405 else:
406 new_field_name = descriptor.name
407
408 if isinstance(field, protobuf_message.Message):
409 # Recurse for nested Paths.
410 handlers.extend(
411 _extract_handlers(
412 field,
413 destination,
414 prefix,
415 delete,
416 reset,
417 field_name=new_field_name,
418 message_type=message_type,
419 )
420 )
421 else:
422 # If it's iterable it may be a repeated field, try each element.
423 try:
424 iterator = iter(field)
425 except TypeError:
426 # Definitely not a repeated field, just move on.
427 continue
428
429 for element in iterator:
430 handlers.extend(
431 _extract_handlers(
432 element,
433 destination,
434 prefix,
435 delete,
436 reset,
437 field_name=new_field_name,
438 message_type=message_type,
439 )
440 )
441
442 return handlers