blob: 018a86152f9775edcd4f5767440aba1f4bfd174a [file] [log] [blame]
Alex Kleinc05f3d12019-05-29 14:16:21 -06001# Copyright 2019 The Chromium OS Authors. All rights reserved.
2# 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:
26 from chromite.lib import chroot_lib
27
28
Alex Kleinbd6edf82019-07-18 10:30:49 -060029class Error(Exception):
30 """Base error class for the module."""
31
32
33class InvalidResultPathError(Error):
34 """Result path is invalid."""
35
36
Alex Kleinc05f3d12019-05-29 14:16:21 -060037class ChrootHandler(object):
38 """Translate a Chroot message to chroot enter arguments and env."""
39
Alex Kleinc7d647f2020-01-06 12:00:48 -070040 def __init__(self, clear_field):
Alex Kleinc05f3d12019-05-29 14:16:21 -060041 self.clear_field = clear_field
42
Mike Frysinger40443592022-05-05 13:03:40 -040043 def handle(self, message, recurse=True) -> Optional['chroot_lib.Chroot']:
Alex Kleinc05f3d12019-05-29 14:16:21 -060044 """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)
54
Alex Klein6becabc2020-09-11 14:03:05 -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
65
Alex Kleinc05f3d12019-05-29 14:16:21 -060066 return None
67
Mike Frysinger40443592022-05-05 13:03:40 -040068 def parse_chroot(self,
69 chroot_message: common_pb2.Chroot) -> 'chroot_lib.Chroot':
Alex Kleinc05f3d12019-05-29 14:16:21 -060070 """Parse a Chroot message instance."""
Alex Kleinc7d647f2020-01-06 12:00:48 -070071 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060072
73
Mike Frysinger40443592022-05-05 13:03:40 -040074def handle_chroot(message: protobuf_message.Message,
75 clear_field: Optional[bool] = True) -> 'chroot_lib.Chroot':
76 """Find and parse the chroot field, returning the Chroot instance."""
Alex Kleinc7d647f2020-01-06 12:00:48 -070077 handler = ChrootHandler(clear_field)
Alex Kleinc05f3d12019-05-29 14:16:21 -060078 chroot = handler.handle(message)
79 if chroot:
80 return chroot
81
82 logging.warning('No chroot message found, falling back to defaults.')
83 return handler.parse_chroot(common_pb2.Chroot())
84
85
Alex Klein9b7331e2019-12-30 14:37:21 -070086def handle_goma(message, chroot_path):
87 """Find and parse the GomaConfig field, returning the Goma instance."""
88 for descriptor in message.DESCRIPTOR.fields:
89 field = getattr(message, descriptor.name)
90 if isinstance(field, common_pb2.GomaConfig):
91 goma_config = field
92 return controller_util.ParseGomaConfig(goma_config, chroot_path)
93
94 return None
95
96
Joanna Wang92cad812021-11-03 14:52:08 -070097def handle_remoteexec(message: protobuf_message.Message):
98 """Find the RemoteexecConfig field, returning the Remoteexec instance."""
99 for descriptor in message.DESCRIPTOR.fields:
100 field = getattr(message, descriptor.name)
101 if isinstance(field, common_pb2.RemoteexecConfig):
102 remoteexec_config = field
103 return controller_util.ParseRemoteexecConfig(remoteexec_config)
104
105 return None
106
107
Alex Kleinc05f3d12019-05-29 14:16:21 -0600108class PathHandler(object):
109 """Handles copying a file or directory into or out of the chroot."""
110
111 INSIDE = common_pb2.Path.INSIDE
112 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -0600113
Mike Frysinger40443592022-05-05 13:03:40 -0400114 def __init__(self,
115 field: common_pb2.Path,
116 destination: str,
117 delete: bool,
118 prefix: Optional[str] = None,
119 reset: Optional[bool] = True) -> None:
Alex Kleinc05f3d12019-05-29 14:16:21 -0600120 """Path handler initialization.
121
122 Args:
Mike Frysinger40443592022-05-05 13:03:40 -0400123 field: The Path message.
124 destination: The destination base path.
125 delete: Whether the copied file(s) should be deleted on cleanup.
126 prefix: A path prefix to remove from the destination path when moving
127 files inside the chroot, or to add to the source paths when moving files
128 out of the chroot.
129 reset: Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600130 """
131 assert isinstance(field, common_pb2.Path)
132 assert field.path
133 assert field.location
134
135 self.field = field
136 self.destination = destination
Mike Frysinger3bb61cb2022-04-14 16:07:44 -0400137 self.prefix = '' if prefix is None else str(prefix)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600138 self.delete = delete
139 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600140 self.reset = reset
141
Alex Kleinaa705412019-06-04 15:00:30 -0600142 # For resetting the state.
143 self._transferred = False
144 self._original_message = common_pb2.Path()
145 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600146
Mike Frysinger40443592022-05-05 13:03:40 -0400147 def transfer(self, direction: int) -> None:
Alex Kleinc05f3d12019-05-29 14:16:21 -0600148 """Copy the file or directory to its destination.
149
150 Args:
Mike Frysinger40443592022-05-05 13:03:40 -0400151 direction: The direction files are being copied (into or out of the
152 chroot). Specifying the direction allows avoiding performing unnecessary
153 copies.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600154 """
Alex Kleinaa705412019-06-04 15:00:30 -0600155 if self._transferred:
156 return
157
Alex Kleinaae49772019-07-26 10:20:50 -0600158 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600159
160 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600161 # Already in the correct location, nothing to do.
162 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600163
Alex Kleinaae49772019-07-26 10:20:50 -0600164 # Create a tempdir for the copied file if we're cleaning it up afterwords.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600165 if self.delete:
166 self.tempdir = osutils.TempDir(base_dir=self.destination)
167 destination = self.tempdir.tempdir
168 else:
169 destination = self.destination
170
Alex Kleinbd6edf82019-07-18 10:30:49 -0600171 source = self.field.path
172 if direction == self.OUTSIDE and self.prefix:
Alex Kleinaae49772019-07-26 10:20:50 -0600173 # When we're extracting files, we need /tmp/result to be
174 # /path/to/chroot/tmp/result.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600175 source = os.path.join(self.prefix, source.lstrip(os.sep))
176
177 if os.path.isfile(source):
Alex Kleinaae49772019-07-26 10:20:50 -0600178 # File - use the old file name, just copy it into the destination.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600179 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600180 copy_fn = shutil.copy
181 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600182 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600183 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600184 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600185
Alex Kleinbd6edf82019-07-18 10:30:49 -0600186 logging.debug('Copying %s to %s', source, dest_path)
187 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600188
189 # Clean up the destination path for returning, if applicable.
190 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600191 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600192 return_path = return_path[len(self.prefix):]
193
Alex Kleinaa705412019-06-04 15:00:30 -0600194 self.field.path = return_path
195 self.field.location = direction
196 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600197
198 def cleanup(self):
199 if self.tempdir:
200 self.tempdir.Cleanup()
201 self.tempdir = None
202
Alex Kleinbd6edf82019-07-18 10:30:49 -0600203 if self.reset:
204 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600205
Alex Kleinc05f3d12019-05-29 14:16:21 -0600206
Alex Kleinf0717a62019-12-06 09:45:00 -0700207class SyncedDirHandler(object):
208 """Handler for syncing directories across the chroot boundary."""
209
210 def __init__(self, field, destination, prefix):
211 self.field = field
212 self.prefix = prefix
213
214 self.source = self.field.dir
215 if not self.source.endswith(os.sep):
216 self.source += os.sep
217
218 self.destination = destination
219 if not self.destination.endswith(os.sep):
220 self.destination += os.sep
221
222 # For resetting the message later.
223 self._original_message = common_pb2.SyncedDir()
224 self._original_message.CopyFrom(self.field)
225
226 def _sync(self, src, dest):
Alex Klein915cce92019-12-17 14:19:50 -0700227 logging.info('Syncing %s to %s', src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700228 # TODO: This would probably be more efficient with rsync.
229 osutils.EmptyDir(dest)
230 osutils.CopyDirContents(src, dest)
231
232 def sync_in(self):
233 """Sync files from the source directory to the destination directory."""
234 self._sync(self.source, self.destination)
235 self.field.dir = '/%s' % os.path.relpath(self.destination, self.prefix)
236
237 def sync_out(self):
238 """Sync files from the destination directory to the source directory."""
239 self._sync(self.destination, self.source)
240 self.field.CopyFrom(self._original_message)
241
242
Alex Kleinc05f3d12019-05-29 14:16:21 -0600243@contextlib.contextmanager
Mike Frysinger40443592022-05-05 13:03:40 -0400244def copy_paths_in(message: protobuf_message.Message,
245 destination: str,
246 delete: Optional[bool] = True,
247 prefix: Optional[str] = None) -> Iterator[List[PathHandler]]:
Alex Kleinc05f3d12019-05-29 14:16:21 -0600248 """Context manager function to transfer and cleanup all Path messages.
249
250 Args:
Mike Frysinger40443592022-05-05 13:03:40 -0400251 message: A message whose Path messages should be transferred.
252 destination: The base destination path.
253 delete: Whether the file(s) should be deleted.
254 prefix: A prefix path to remove from the final destination path in the Path
255 message (i.e. remove the chroot path).
Alex Kleinc05f3d12019-05-29 14:16:21 -0600256
Mike Frysinger40443592022-05-05 13:03:40 -0400257 Yields:
Alex Kleinc05f3d12019-05-29 14:16:21 -0600258 list[PathHandler]: The path handlers.
259 """
260 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600261
Alex Kleinf0717a62019-12-06 09:45:00 -0700262 handlers = _extract_handlers(message, destination, prefix, delete=delete,
263 reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600264
265 for handler in handlers:
Alex Kleinaae49772019-07-26 10:20:50 -0600266 handler.transfer(PathHandler.INSIDE)
Alex Kleinaa705412019-06-04 15:00:30 -0600267
268 try:
269 yield handlers
270 finally:
271 for handler in handlers:
272 handler.cleanup()
273
274
Alex Kleinf0717a62019-12-06 09:45:00 -0700275@contextlib.contextmanager
Mike Frysinger40443592022-05-05 13:03:40 -0400276def sync_dirs(message: protobuf_message.Message,
277 destination: str,
278 prefix: str) -> Iterator[SyncedDirHandler]:
Alex Kleinf0717a62019-12-06 09:45:00 -0700279 """Context manager function to handle SyncedDir messages.
280
281 The sync semantics are effectively:
282 rsync -r --del source/ destination/
283 * The endpoint runs. *
284 rsync -r --del destination/ source/
285
286 Args:
Mike Frysinger40443592022-05-05 13:03:40 -0400287 message: A message whose SyncedPath messages should be synced.
288 destination: The destination path.
289 prefix: A prefix path to remove from the final destination path in the Path
290 message (i.e. remove the chroot path).
Alex Kleinf0717a62019-12-06 09:45:00 -0700291
Mike Frysinger40443592022-05-05 13:03:40 -0400292 Yields:
293 The handlers.
Alex Kleinf0717a62019-12-06 09:45:00 -0700294 """
295 assert destination
296
297 handlers = _extract_handlers(message, destination, prefix=prefix,
298 delete=False, reset=True,
299 message_type=common_pb2.SyncedDir)
300
301 for handler in handlers:
302 handler.sync_in()
303
304 try:
305 yield handlers
306 finally:
307 for handler in handlers:
308 handler.sync_out()
309
310
Mike Frysinger40443592022-05-05 13:03:40 -0400311def extract_results(request_message: protobuf_message.Message,
312 response_message: protobuf_message.Message,
313 chroot: 'chroot_lib.Chroot') -> None:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600314 """Transfer all response Path messages to the request's ResultPath.
315
316 Args:
Mike Frysinger40443592022-05-05 13:03:40 -0400317 request_message: The request message containing a ResultPath message.
318 response_message: The response message whose Path message(s) are to be
319 transferred.
320 chroot: The chroot the files are being copied out of.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600321 """
322 # Find the ResultPath.
323 for descriptor in request_message.DESCRIPTOR.fields:
324 field = getattr(request_message, descriptor.name)
325 if isinstance(field, common_pb2.ResultPath):
326 result_path_message = field
327 break
328 else:
329 # No ResultPath to handle.
330 return
331
332 destination = result_path_message.path.path
Alex Kleinf0717a62019-12-06 09:45:00 -0700333 handlers = _extract_handlers(response_message, destination, chroot.path,
334 delete=False, reset=False)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600335
336 for handler in handlers:
337 handler.transfer(PathHandler.OUTSIDE)
338 handler.cleanup()
339
340
Alex Kleinf0717a62019-12-06 09:45:00 -0700341def _extract_handlers(message, destination, prefix, delete=False, reset=False,
342 field_name=None, message_type=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600343 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700344 message_type = message_type or common_pb2.Path
345 is_path_target = message_type is common_pb2.Path
346 is_synced_target = message_type is common_pb2.SyncedDir
347
Alex Kleinbd6edf82019-07-18 10:30:49 -0600348 is_message = isinstance(message, protobuf_message.Message)
349 is_result_path = isinstance(message, common_pb2.ResultPath)
350 if not is_message or is_result_path:
351 # Base case: Nothing to handle.
352 # There's nothing we can do with scalar values.
353 # Skip ResultPath instances to avoid unnecessary file copying.
354 return []
Alex Kleinf0717a62019-12-06 09:45:00 -0700355 elif is_path_target and isinstance(message, common_pb2.Path):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600356 # Base case: Create handler for this message.
357 if not message.path or not message.location:
358 logging.debug('Skipping %s; incomplete.', field_name or 'message')
359 return []
360
361 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
362 reset=reset)
363 return [handler]
Alex Kleinf0717a62019-12-06 09:45:00 -0700364 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
365 if not message.dir:
366 logging.debug('Skipping %s; no directory given.', field_name or 'message')
367 return []
368
369 handler = SyncedDirHandler(message, destination, prefix)
370 return [handler]
Alex Kleinbd6edf82019-07-18 10:30:49 -0600371
372 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600373 handlers = []
374 for descriptor in message.DESCRIPTOR.fields:
375 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600376 if field_name:
377 new_field_name = '%s.%s' % (field_name, descriptor.name)
378 else:
379 new_field_name = descriptor.name
380
381 if isinstance(field, protobuf_message.Message):
382 # Recurse for nested Paths.
383 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700384 _extract_handlers(field, destination, prefix, delete, reset,
385 field_name=new_field_name,
386 message_type=message_type))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600387 else:
388 # If it's iterable it may be a repeated field, try each element.
389 try:
390 iterator = iter(field)
391 except TypeError:
392 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600393 continue
394
Alex Kleinbd6edf82019-07-18 10:30:49 -0600395 for element in iterator:
396 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700397 _extract_handlers(element, destination, prefix, delete, reset,
398 field_name=new_field_name,
399 message_type=message_type))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600400
Alex Kleinaa705412019-06-04 15:00:30 -0600401 return handlers