blob: ae3571f17e9629a42cdcde87b894ceffa594bb0b [file] [log] [blame]
Alex Kleinc05f3d12019-05-29 14:16:21 -06001# -*- coding: utf-8 -*-
2# Copyright 2019 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Field handler classes.
7
8The field handlers are meant to parse information from or do some other generic
9action for a specific field type for the build_api script.
10"""
11
12from __future__ import print_function
13
14import contextlib
Alex Kleinbd6edf82019-07-18 10:30:49 -060015import functools
Alex Kleinc05f3d12019-05-29 14:16:21 -060016import os
17import shutil
Mike Frysingeref94e4c2020-02-10 23:59:54 -050018import sys
Alex Kleinc05f3d12019-05-29 14:16:21 -060019
Mike Frysinger849d6402019-10-17 00:14:16 -040020from google.protobuf import message as protobuf_message
21
Alex Klein38c7d9e2019-05-08 09:31:19 -060022from chromite.api.controller import controller_util
Alex Kleinc05f3d12019-05-29 14:16:21 -060023from chromite.api.gen.chromiumos import common_pb2
Alex Kleinc05f3d12019-05-29 14:16:21 -060024from chromite.lib import cros_logging as logging
25from chromite.lib import osutils
26
27
Mike Frysingeref94e4c2020-02-10 23:59:54 -050028assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
29
30
Alex Kleinbd6edf82019-07-18 10:30:49 -060031class Error(Exception):
32 """Base error class for the module."""
33
34
35class InvalidResultPathError(Error):
36 """Result path is invalid."""
37
38
Alex Kleinc05f3d12019-05-29 14:16:21 -060039class ChrootHandler(object):
40 """Translate a Chroot message to chroot enter arguments and env."""
41
Alex Kleinc7d647f2020-01-06 12:00:48 -070042 def __init__(self, clear_field):
Alex Kleinc05f3d12019-05-29 14:16:21 -060043 self.clear_field = clear_field
44
Alex Klein6becabc2020-09-11 14:03:05 -060045 def handle(self, message, recurse=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060046 """Parse a message for a chroot field."""
47 # Find the Chroot field. Search for the field by type to prevent it being
48 # tied to a naming convention.
49 for descriptor in message.DESCRIPTOR.fields:
50 field = getattr(message, descriptor.name)
51 if isinstance(field, common_pb2.Chroot):
52 chroot = field
53 if self.clear_field:
54 message.ClearField(descriptor.name)
55 return self.parse_chroot(chroot)
56
Alex Klein6becabc2020-09-11 14:03:05 -060057 # Recurse down one level. This is handy for meta-endpoints that use another
58 # endpoint's request to produce data for or about the second endpoint.
59 # e.g. PackageService/NeedsChromeSource.
60 if recurse:
61 for descriptor in message.DESCRIPTOR.fields:
62 field = getattr(message, descriptor.name)
63 if isinstance(field, protobuf_message.Message):
64 chroot = self.handle(field, recurse=False)
65 if chroot:
66 return chroot
67
Alex Kleinc05f3d12019-05-29 14:16:21 -060068 return None
69
70 def parse_chroot(self, chroot_message):
71 """Parse a Chroot message instance."""
Alex Kleinc7d647f2020-01-06 12:00:48 -070072 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060073
74
Alex Kleinc7d647f2020-01-06 12:00:48 -070075def handle_chroot(message, clear_field=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060076 """Find and parse the chroot field, returning the Chroot instance.
77
78 Returns:
79 chroot_lib.Chroot
80 """
Alex Kleinc7d647f2020-01-06 12:00:48 -070081 handler = ChrootHandler(clear_field)
Alex Kleinc05f3d12019-05-29 14:16:21 -060082 chroot = handler.handle(message)
83 if chroot:
84 return chroot
85
86 logging.warning('No chroot message found, falling back to defaults.')
87 return handler.parse_chroot(common_pb2.Chroot())
88
89
Alex Klein9b7331e2019-12-30 14:37:21 -070090def handle_goma(message, chroot_path):
91 """Find and parse the GomaConfig field, returning the Goma instance."""
92 for descriptor in message.DESCRIPTOR.fields:
93 field = getattr(message, descriptor.name)
94 if isinstance(field, common_pb2.GomaConfig):
95 goma_config = field
96 return controller_util.ParseGomaConfig(goma_config, chroot_path)
97
98 return None
99
100
Alex Kleinc05f3d12019-05-29 14:16:21 -0600101class PathHandler(object):
102 """Handles copying a file or directory into or out of the chroot."""
103
104 INSIDE = common_pb2.Path.INSIDE
105 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -0600106
Alex Kleinbd6edf82019-07-18 10:30:49 -0600107 def __init__(self, field, destination, delete, prefix=None, reset=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600108 """Path handler initialization.
109
110 Args:
111 field (common_pb2.Path): The Path message.
112 destination (str): The destination base path.
113 delete (bool): Whether the copied file(s) should be deleted on cleanup.
114 prefix (str|None): A path prefix to remove from the destination path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600115 when moving files inside the chroot, or to add to the source paths when
116 moving files out of the chroot.
117 reset (bool): Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600118 """
119 assert isinstance(field, common_pb2.Path)
120 assert field.path
121 assert field.location
122
123 self.field = field
124 self.destination = destination
125 self.prefix = prefix or ''
126 self.delete = delete
127 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600128 self.reset = reset
129
Alex Kleinaa705412019-06-04 15:00:30 -0600130 # For resetting the state.
131 self._transferred = False
132 self._original_message = common_pb2.Path()
133 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600134
Alex Kleinaae49772019-07-26 10:20:50 -0600135 def transfer(self, direction):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600136 """Copy the file or directory to its destination.
137
138 Args:
139 direction (int): The direction files are being copied (into or out of
140 the chroot). Specifying the direction allows avoiding performing
141 unnecessary copies.
142 """
Alex Kleinaa705412019-06-04 15:00:30 -0600143 if self._transferred:
144 return
145
Alex Kleinaae49772019-07-26 10:20:50 -0600146 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600147
148 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600149 # Already in the correct location, nothing to do.
150 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600151
Alex Kleinaae49772019-07-26 10:20:50 -0600152 # Create a tempdir for the copied file if we're cleaning it up afterwords.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600153 if self.delete:
154 self.tempdir = osutils.TempDir(base_dir=self.destination)
155 destination = self.tempdir.tempdir
156 else:
157 destination = self.destination
158
Alex Kleinbd6edf82019-07-18 10:30:49 -0600159 source = self.field.path
160 if direction == self.OUTSIDE and self.prefix:
Alex Kleinaae49772019-07-26 10:20:50 -0600161 # When we're extracting files, we need /tmp/result to be
162 # /path/to/chroot/tmp/result.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600163 source = os.path.join(self.prefix, source.lstrip(os.sep))
164
165 if os.path.isfile(source):
Alex Kleinaae49772019-07-26 10:20:50 -0600166 # File - use the old file name, just copy it into the destination.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600167 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600168 copy_fn = shutil.copy
169 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600170 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600171 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600172 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600173
Alex Kleinbd6edf82019-07-18 10:30:49 -0600174 logging.debug('Copying %s to %s', source, dest_path)
175 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600176
177 # Clean up the destination path for returning, if applicable.
178 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600179 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600180 return_path = return_path[len(self.prefix):]
181
Alex Kleinaa705412019-06-04 15:00:30 -0600182 self.field.path = return_path
183 self.field.location = direction
184 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600185
186 def cleanup(self):
187 if self.tempdir:
188 self.tempdir.Cleanup()
189 self.tempdir = None
190
Alex Kleinbd6edf82019-07-18 10:30:49 -0600191 if self.reset:
192 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600193
Alex Kleinc05f3d12019-05-29 14:16:21 -0600194
Alex Kleinf0717a62019-12-06 09:45:00 -0700195class SyncedDirHandler(object):
196 """Handler for syncing directories across the chroot boundary."""
197
198 def __init__(self, field, destination, prefix):
199 self.field = field
200 self.prefix = prefix
201
202 self.source = self.field.dir
203 if not self.source.endswith(os.sep):
204 self.source += os.sep
205
206 self.destination = destination
207 if not self.destination.endswith(os.sep):
208 self.destination += os.sep
209
210 # For resetting the message later.
211 self._original_message = common_pb2.SyncedDir()
212 self._original_message.CopyFrom(self.field)
213
214 def _sync(self, src, dest):
Alex Klein915cce92019-12-17 14:19:50 -0700215 logging.info('Syncing %s to %s', src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700216 # TODO: This would probably be more efficient with rsync.
217 osutils.EmptyDir(dest)
218 osutils.CopyDirContents(src, dest)
219
220 def sync_in(self):
221 """Sync files from the source directory to the destination directory."""
222 self._sync(self.source, self.destination)
223 self.field.dir = '/%s' % os.path.relpath(self.destination, self.prefix)
224
225 def sync_out(self):
226 """Sync files from the destination directory to the source directory."""
227 self._sync(self.destination, self.source)
228 self.field.CopyFrom(self._original_message)
229
230
Alex Kleinc05f3d12019-05-29 14:16:21 -0600231@contextlib.contextmanager
Alex Kleinaae49772019-07-26 10:20:50 -0600232def copy_paths_in(message, destination, delete=True, prefix=None):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600233 """Context manager function to transfer and cleanup all Path messages.
234
235 Args:
236 message (Message): A message whose Path messages should be transferred.
Alex Kleinf0717a62019-12-06 09:45:00 -0700237 destination (str): The base destination path.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600238 delete (bool): Whether the file(s) should be deleted.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600239 prefix (str|None): A prefix path to remove from the final destination path
240 in the Path message (i.e. remove the chroot path).
241
242 Returns:
243 list[PathHandler]: The path handlers.
244 """
245 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600246
Alex Kleinf0717a62019-12-06 09:45:00 -0700247 handlers = _extract_handlers(message, destination, prefix, delete=delete,
248 reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600249
250 for handler in handlers:
Alex Kleinaae49772019-07-26 10:20:50 -0600251 handler.transfer(PathHandler.INSIDE)
Alex Kleinaa705412019-06-04 15:00:30 -0600252
253 try:
254 yield handlers
255 finally:
256 for handler in handlers:
257 handler.cleanup()
258
259
Alex Kleinf0717a62019-12-06 09:45:00 -0700260@contextlib.contextmanager
261def sync_dirs(message, destination, prefix):
262 """Context manager function to handle SyncedDir messages.
263
264 The sync semantics are effectively:
265 rsync -r --del source/ destination/
266 * The endpoint runs. *
267 rsync -r --del destination/ source/
268
269 Args:
270 message (Message): A message whose SyncedPath messages should be synced.
271 destination (str): The destination path.
272 prefix (str): A prefix path to remove from the final destination path
273 in the Path message (i.e. remove the chroot path).
274
275 Returns:
276 list[SyncedDirHandler]: The handlers.
277 """
278 assert destination
279
280 handlers = _extract_handlers(message, destination, prefix=prefix,
281 delete=False, reset=True,
282 message_type=common_pb2.SyncedDir)
283
284 for handler in handlers:
285 handler.sync_in()
286
287 try:
288 yield handlers
289 finally:
290 for handler in handlers:
291 handler.sync_out()
292
293
Alex Kleinaae49772019-07-26 10:20:50 -0600294def extract_results(request_message, response_message, chroot):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600295 """Transfer all response Path messages to the request's ResultPath.
296
297 Args:
298 request_message (Message): The request message containing a ResultPath
299 message.
300 response_message (Message): The response message whose Path message(s)
301 are to be transferred.
302 chroot (chroot_lib.Chroot): The chroot the files are being copied out of.
303 """
304 # Find the ResultPath.
305 for descriptor in request_message.DESCRIPTOR.fields:
306 field = getattr(request_message, descriptor.name)
307 if isinstance(field, common_pb2.ResultPath):
308 result_path_message = field
309 break
310 else:
311 # No ResultPath to handle.
312 return
313
314 destination = result_path_message.path.path
Alex Kleinf0717a62019-12-06 09:45:00 -0700315 handlers = _extract_handlers(response_message, destination, chroot.path,
316 delete=False, reset=False)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600317
318 for handler in handlers:
319 handler.transfer(PathHandler.OUTSIDE)
320 handler.cleanup()
321
322
Alex Kleinf0717a62019-12-06 09:45:00 -0700323def _extract_handlers(message, destination, prefix, delete=False, reset=False,
324 field_name=None, message_type=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600325 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700326 message_type = message_type or common_pb2.Path
327 is_path_target = message_type is common_pb2.Path
328 is_synced_target = message_type is common_pb2.SyncedDir
329
Alex Kleinbd6edf82019-07-18 10:30:49 -0600330 is_message = isinstance(message, protobuf_message.Message)
331 is_result_path = isinstance(message, common_pb2.ResultPath)
332 if not is_message or is_result_path:
333 # Base case: Nothing to handle.
334 # There's nothing we can do with scalar values.
335 # Skip ResultPath instances to avoid unnecessary file copying.
336 return []
Alex Kleinf0717a62019-12-06 09:45:00 -0700337 elif is_path_target and isinstance(message, common_pb2.Path):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600338 # Base case: Create handler for this message.
339 if not message.path or not message.location:
340 logging.debug('Skipping %s; incomplete.', field_name or 'message')
341 return []
342
343 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
344 reset=reset)
345 return [handler]
Alex Kleinf0717a62019-12-06 09:45:00 -0700346 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
347 if not message.dir:
348 logging.debug('Skipping %s; no directory given.', field_name or 'message')
349 return []
350
351 handler = SyncedDirHandler(message, destination, prefix)
352 return [handler]
Alex Kleinbd6edf82019-07-18 10:30:49 -0600353
354 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600355 handlers = []
356 for descriptor in message.DESCRIPTOR.fields:
357 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600358 if field_name:
359 new_field_name = '%s.%s' % (field_name, descriptor.name)
360 else:
361 new_field_name = descriptor.name
362
363 if isinstance(field, protobuf_message.Message):
364 # Recurse for nested Paths.
365 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700366 _extract_handlers(field, destination, prefix, delete, reset,
367 field_name=new_field_name,
368 message_type=message_type))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600369 else:
370 # If it's iterable it may be a repeated field, try each element.
371 try:
372 iterator = iter(field)
373 except TypeError:
374 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600375 continue
376
Alex Kleinbd6edf82019-07-18 10:30:49 -0600377 for element in iterator:
378 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700379 _extract_handlers(element, destination, prefix, delete, reset,
380 field_name=new_field_name,
381 message_type=message_type))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600382
Alex Kleinaa705412019-06-04 15:00:30 -0600383 return handlers