blob: 3078f5e754ac55fc3b93c3841933572ad663d0c9 [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
16
Mike Frysinger2c024062021-05-22 15:43:22 -040017from chromite.third_party.google.protobuf import message as protobuf_message
Mike Frysinger849d6402019-10-17 00:14:16 -040018
Alex Klein38c7d9e2019-05-08 09:31:19 -060019from chromite.api.controller import controller_util
Alex Kleinc05f3d12019-05-29 14:16:21 -060020from chromite.api.gen.chromiumos import common_pb2
Alex Kleinc05f3d12019-05-29 14:16:21 -060021from chromite.lib import osutils
22
23
Alex Kleinbd6edf82019-07-18 10:30:49 -060024class Error(Exception):
25 """Base error class for the module."""
26
27
28class InvalidResultPathError(Error):
29 """Result path is invalid."""
30
31
Alex Kleinc05f3d12019-05-29 14:16:21 -060032class ChrootHandler(object):
33 """Translate a Chroot message to chroot enter arguments and env."""
34
Alex Kleinc7d647f2020-01-06 12:00:48 -070035 def __init__(self, clear_field):
Alex Kleinc05f3d12019-05-29 14:16:21 -060036 self.clear_field = clear_field
37
Alex Klein6becabc2020-09-11 14:03:05 -060038 def handle(self, message, recurse=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060039 """Parse a message for a chroot field."""
40 # Find the Chroot field. Search for the field by type to prevent it being
41 # tied to a naming convention.
42 for descriptor in message.DESCRIPTOR.fields:
43 field = getattr(message, descriptor.name)
44 if isinstance(field, common_pb2.Chroot):
45 chroot = field
46 if self.clear_field:
47 message.ClearField(descriptor.name)
48 return self.parse_chroot(chroot)
49
Alex Klein6becabc2020-09-11 14:03:05 -060050 # Recurse down one level. This is handy for meta-endpoints that use another
51 # endpoint's request to produce data for or about the second endpoint.
52 # e.g. PackageService/NeedsChromeSource.
53 if recurse:
54 for descriptor in message.DESCRIPTOR.fields:
55 field = getattr(message, descriptor.name)
56 if isinstance(field, protobuf_message.Message):
57 chroot = self.handle(field, recurse=False)
58 if chroot:
59 return chroot
60
Alex Kleinc05f3d12019-05-29 14:16:21 -060061 return None
62
63 def parse_chroot(self, chroot_message):
64 """Parse a Chroot message instance."""
Alex Kleinc7d647f2020-01-06 12:00:48 -070065 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060066
67
Alex Kleinc7d647f2020-01-06 12:00:48 -070068def handle_chroot(message, clear_field=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060069 """Find and parse the chroot field, returning the Chroot instance.
70
71 Returns:
72 chroot_lib.Chroot
73 """
Alex Kleinc7d647f2020-01-06 12:00:48 -070074 handler = ChrootHandler(clear_field)
Alex Kleinc05f3d12019-05-29 14:16:21 -060075 chroot = handler.handle(message)
76 if chroot:
77 return chroot
78
79 logging.warning('No chroot message found, falling back to defaults.')
80 return handler.parse_chroot(common_pb2.Chroot())
81
82
Alex Klein9b7331e2019-12-30 14:37:21 -070083def handle_goma(message, chroot_path):
84 """Find and parse the GomaConfig field, returning the Goma instance."""
85 for descriptor in message.DESCRIPTOR.fields:
86 field = getattr(message, descriptor.name)
87 if isinstance(field, common_pb2.GomaConfig):
88 goma_config = field
89 return controller_util.ParseGomaConfig(goma_config, chroot_path)
90
91 return None
92
93
Joanna Wang92cad812021-11-03 14:52:08 -070094def handle_remoteexec(message: protobuf_message.Message):
95 """Find the RemoteexecConfig field, returning the Remoteexec instance."""
96 for descriptor in message.DESCRIPTOR.fields:
97 field = getattr(message, descriptor.name)
98 if isinstance(field, common_pb2.RemoteexecConfig):
99 remoteexec_config = field
100 return controller_util.ParseRemoteexecConfig(remoteexec_config)
101
102 return None
103
104
Alex Kleinc05f3d12019-05-29 14:16:21 -0600105class PathHandler(object):
106 """Handles copying a file or directory into or out of the chroot."""
107
108 INSIDE = common_pb2.Path.INSIDE
109 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -0600110
Alex Kleinbd6edf82019-07-18 10:30:49 -0600111 def __init__(self, field, destination, delete, prefix=None, reset=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600112 """Path handler initialization.
113
114 Args:
115 field (common_pb2.Path): The Path message.
116 destination (str): The destination base path.
117 delete (bool): Whether the copied file(s) should be deleted on cleanup.
118 prefix (str|None): A path prefix to remove from the destination path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600119 when moving files inside the chroot, or to add to the source paths when
120 moving files out of the chroot.
121 reset (bool): Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600122 """
123 assert isinstance(field, common_pb2.Path)
124 assert field.path
125 assert field.location
126
127 self.field = field
128 self.destination = destination
129 self.prefix = prefix or ''
130 self.delete = delete
131 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600132 self.reset = reset
133
Alex Kleinaa705412019-06-04 15:00:30 -0600134 # For resetting the state.
135 self._transferred = False
136 self._original_message = common_pb2.Path()
137 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600138
Alex Kleinaae49772019-07-26 10:20:50 -0600139 def transfer(self, direction):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600140 """Copy the file or directory to its destination.
141
142 Args:
143 direction (int): The direction files are being copied (into or out of
144 the chroot). Specifying the direction allows avoiding performing
145 unnecessary copies.
146 """
Alex Kleinaa705412019-06-04 15:00:30 -0600147 if self._transferred:
148 return
149
Alex Kleinaae49772019-07-26 10:20:50 -0600150 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600151
152 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600153 # Already in the correct location, nothing to do.
154 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600155
Alex Kleinaae49772019-07-26 10:20:50 -0600156 # Create a tempdir for the copied file if we're cleaning it up afterwords.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600157 if self.delete:
158 self.tempdir = osutils.TempDir(base_dir=self.destination)
159 destination = self.tempdir.tempdir
160 else:
161 destination = self.destination
162
Alex Kleinbd6edf82019-07-18 10:30:49 -0600163 source = self.field.path
164 if direction == self.OUTSIDE and self.prefix:
Alex Kleinaae49772019-07-26 10:20:50 -0600165 # When we're extracting files, we need /tmp/result to be
166 # /path/to/chroot/tmp/result.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600167 source = os.path.join(self.prefix, source.lstrip(os.sep))
168
169 if os.path.isfile(source):
Alex Kleinaae49772019-07-26 10:20:50 -0600170 # File - use the old file name, just copy it into the destination.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600171 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600172 copy_fn = shutil.copy
173 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600174 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600175 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600176 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600177
Alex Kleinbd6edf82019-07-18 10:30:49 -0600178 logging.debug('Copying %s to %s', source, dest_path)
179 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600180
181 # Clean up the destination path for returning, if applicable.
182 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600183 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600184 return_path = return_path[len(self.prefix):]
185
Alex Kleinaa705412019-06-04 15:00:30 -0600186 self.field.path = return_path
187 self.field.location = direction
188 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600189
190 def cleanup(self):
191 if self.tempdir:
192 self.tempdir.Cleanup()
193 self.tempdir = None
194
Alex Kleinbd6edf82019-07-18 10:30:49 -0600195 if self.reset:
196 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600197
Alex Kleinc05f3d12019-05-29 14:16:21 -0600198
Alex Kleinf0717a62019-12-06 09:45:00 -0700199class SyncedDirHandler(object):
200 """Handler for syncing directories across the chroot boundary."""
201
202 def __init__(self, field, destination, prefix):
203 self.field = field
204 self.prefix = prefix
205
206 self.source = self.field.dir
207 if not self.source.endswith(os.sep):
208 self.source += os.sep
209
210 self.destination = destination
211 if not self.destination.endswith(os.sep):
212 self.destination += os.sep
213
214 # For resetting the message later.
215 self._original_message = common_pb2.SyncedDir()
216 self._original_message.CopyFrom(self.field)
217
218 def _sync(self, src, dest):
Alex Klein915cce92019-12-17 14:19:50 -0700219 logging.info('Syncing %s to %s', src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700220 # TODO: This would probably be more efficient with rsync.
221 osutils.EmptyDir(dest)
222 osutils.CopyDirContents(src, dest)
223
224 def sync_in(self):
225 """Sync files from the source directory to the destination directory."""
226 self._sync(self.source, self.destination)
227 self.field.dir = '/%s' % os.path.relpath(self.destination, self.prefix)
228
229 def sync_out(self):
230 """Sync files from the destination directory to the source directory."""
231 self._sync(self.destination, self.source)
232 self.field.CopyFrom(self._original_message)
233
234
Alex Kleinc05f3d12019-05-29 14:16:21 -0600235@contextlib.contextmanager
Alex Kleinaae49772019-07-26 10:20:50 -0600236def copy_paths_in(message, destination, delete=True, prefix=None):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600237 """Context manager function to transfer and cleanup all Path messages.
238
239 Args:
240 message (Message): A message whose Path messages should be transferred.
Alex Kleinf0717a62019-12-06 09:45:00 -0700241 destination (str): The base destination path.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600242 delete (bool): Whether the file(s) should be deleted.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600243 prefix (str|None): A prefix path to remove from the final destination path
244 in the Path message (i.e. remove the chroot path).
245
246 Returns:
247 list[PathHandler]: The path handlers.
248 """
249 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600250
Alex Kleinf0717a62019-12-06 09:45:00 -0700251 handlers = _extract_handlers(message, destination, prefix, delete=delete,
252 reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600253
254 for handler in handlers:
Alex Kleinaae49772019-07-26 10:20:50 -0600255 handler.transfer(PathHandler.INSIDE)
Alex Kleinaa705412019-06-04 15:00:30 -0600256
257 try:
258 yield handlers
259 finally:
260 for handler in handlers:
261 handler.cleanup()
262
263
Alex Kleinf0717a62019-12-06 09:45:00 -0700264@contextlib.contextmanager
265def sync_dirs(message, destination, prefix):
266 """Context manager function to handle SyncedDir messages.
267
268 The sync semantics are effectively:
269 rsync -r --del source/ destination/
270 * The endpoint runs. *
271 rsync -r --del destination/ source/
272
273 Args:
274 message (Message): A message whose SyncedPath messages should be synced.
275 destination (str): The destination path.
276 prefix (str): A prefix path to remove from the final destination path
277 in the Path message (i.e. remove the chroot path).
278
279 Returns:
280 list[SyncedDirHandler]: The handlers.
281 """
282 assert destination
283
284 handlers = _extract_handlers(message, destination, prefix=prefix,
285 delete=False, reset=True,
286 message_type=common_pb2.SyncedDir)
287
288 for handler in handlers:
289 handler.sync_in()
290
291 try:
292 yield handlers
293 finally:
294 for handler in handlers:
295 handler.sync_out()
296
297
Alex Kleinaae49772019-07-26 10:20:50 -0600298def extract_results(request_message, response_message, chroot):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600299 """Transfer all response Path messages to the request's ResultPath.
300
301 Args:
302 request_message (Message): The request message containing a ResultPath
303 message.
304 response_message (Message): The response message whose Path message(s)
305 are to be transferred.
306 chroot (chroot_lib.Chroot): The chroot the files are being copied out of.
307 """
308 # Find the ResultPath.
309 for descriptor in request_message.DESCRIPTOR.fields:
310 field = getattr(request_message, descriptor.name)
311 if isinstance(field, common_pb2.ResultPath):
312 result_path_message = field
313 break
314 else:
315 # No ResultPath to handle.
316 return
317
318 destination = result_path_message.path.path
Alex Kleinf0717a62019-12-06 09:45:00 -0700319 handlers = _extract_handlers(response_message, destination, chroot.path,
320 delete=False, reset=False)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600321
322 for handler in handlers:
323 handler.transfer(PathHandler.OUTSIDE)
324 handler.cleanup()
325
326
Alex Kleinf0717a62019-12-06 09:45:00 -0700327def _extract_handlers(message, destination, prefix, delete=False, reset=False,
328 field_name=None, message_type=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600329 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700330 message_type = message_type or common_pb2.Path
331 is_path_target = message_type is common_pb2.Path
332 is_synced_target = message_type is common_pb2.SyncedDir
333
Alex Kleinbd6edf82019-07-18 10:30:49 -0600334 is_message = isinstance(message, protobuf_message.Message)
335 is_result_path = isinstance(message, common_pb2.ResultPath)
336 if not is_message or is_result_path:
337 # Base case: Nothing to handle.
338 # There's nothing we can do with scalar values.
339 # Skip ResultPath instances to avoid unnecessary file copying.
340 return []
Alex Kleinf0717a62019-12-06 09:45:00 -0700341 elif is_path_target and isinstance(message, common_pb2.Path):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600342 # Base case: Create handler for this message.
343 if not message.path or not message.location:
344 logging.debug('Skipping %s; incomplete.', field_name or 'message')
345 return []
346
347 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
348 reset=reset)
349 return [handler]
Alex Kleinf0717a62019-12-06 09:45:00 -0700350 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
351 if not message.dir:
352 logging.debug('Skipping %s; no directory given.', field_name or 'message')
353 return []
354
355 handler = SyncedDirHandler(message, destination, prefix)
356 return [handler]
Alex Kleinbd6edf82019-07-18 10:30:49 -0600357
358 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600359 handlers = []
360 for descriptor in message.DESCRIPTOR.fields:
361 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600362 if field_name:
363 new_field_name = '%s.%s' % (field_name, descriptor.name)
364 else:
365 new_field_name = descriptor.name
366
367 if isinstance(field, protobuf_message.Message):
368 # Recurse for nested Paths.
369 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700370 _extract_handlers(field, destination, prefix, delete, reset,
371 field_name=new_field_name,
372 message_type=message_type))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600373 else:
374 # If it's iterable it may be a repeated field, try each element.
375 try:
376 iterator = iter(field)
377 except TypeError:
378 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600379 continue
380
Alex Kleinbd6edf82019-07-18 10:30:49 -0600381 for element in iterator:
382 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700383 _extract_handlers(element, destination, prefix, delete, reset,
384 field_name=new_field_name,
385 message_type=message_type))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600386
Alex Kleinaa705412019-06-04 15:00:30 -0600387 return handlers