blob: 753db0e42b0b20e12b5ac827be45fff3302ab3d0 [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
18
Mike Frysinger849d6402019-10-17 00:14:16 -040019from google.protobuf import message as protobuf_message
20
Alex Klein38c7d9e2019-05-08 09:31:19 -060021from chromite.api.controller import controller_util
Alex Kleinc05f3d12019-05-29 14:16:21 -060022from chromite.api.gen.chromiumos import common_pb2
Alex Kleinc05f3d12019-05-29 14:16:21 -060023from chromite.lib import cros_logging as logging
24from chromite.lib import osutils
25
26
Alex Kleinbd6edf82019-07-18 10:30:49 -060027class Error(Exception):
28 """Base error class for the module."""
29
30
31class InvalidResultPathError(Error):
32 """Result path is invalid."""
33
34
Alex Kleinc05f3d12019-05-29 14:16:21 -060035class ChrootHandler(object):
36 """Translate a Chroot message to chroot enter arguments and env."""
37
Alex Kleinc7d647f2020-01-06 12:00:48 -070038 def __init__(self, clear_field):
Alex Kleinc05f3d12019-05-29 14:16:21 -060039 self.clear_field = clear_field
40
41 def handle(self, message):
42 """Parse a message for a chroot field."""
43 # Find the Chroot field. Search for the field by type to prevent it being
44 # tied to a naming convention.
45 for descriptor in message.DESCRIPTOR.fields:
46 field = getattr(message, descriptor.name)
47 if isinstance(field, common_pb2.Chroot):
48 chroot = field
49 if self.clear_field:
50 message.ClearField(descriptor.name)
51 return self.parse_chroot(chroot)
52
53 return None
54
55 def parse_chroot(self, chroot_message):
56 """Parse a Chroot message instance."""
Alex Kleinc7d647f2020-01-06 12:00:48 -070057 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060058
59
Alex Kleinc7d647f2020-01-06 12:00:48 -070060def handle_chroot(message, clear_field=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060061 """Find and parse the chroot field, returning the Chroot instance.
62
63 Returns:
64 chroot_lib.Chroot
65 """
Alex Kleinc7d647f2020-01-06 12:00:48 -070066 handler = ChrootHandler(clear_field)
Alex Kleinc05f3d12019-05-29 14:16:21 -060067 chroot = handler.handle(message)
68 if chroot:
69 return chroot
70
71 logging.warning('No chroot message found, falling back to defaults.')
72 return handler.parse_chroot(common_pb2.Chroot())
73
74
Alex Klein9b7331e2019-12-30 14:37:21 -070075def handle_goma(message, chroot_path):
76 """Find and parse the GomaConfig field, returning the Goma instance."""
77 for descriptor in message.DESCRIPTOR.fields:
78 field = getattr(message, descriptor.name)
79 if isinstance(field, common_pb2.GomaConfig):
80 goma_config = field
81 return controller_util.ParseGomaConfig(goma_config, chroot_path)
82
83 return None
84
85
Alex Kleinc05f3d12019-05-29 14:16:21 -060086class PathHandler(object):
87 """Handles copying a file or directory into or out of the chroot."""
88
89 INSIDE = common_pb2.Path.INSIDE
90 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -060091
Alex Kleinbd6edf82019-07-18 10:30:49 -060092 def __init__(self, field, destination, delete, prefix=None, reset=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060093 """Path handler initialization.
94
95 Args:
96 field (common_pb2.Path): The Path message.
97 destination (str): The destination base path.
98 delete (bool): Whether the copied file(s) should be deleted on cleanup.
99 prefix (str|None): A path prefix to remove from the destination path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600100 when moving files inside the chroot, or to add to the source paths when
101 moving files out of the chroot.
102 reset (bool): Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600103 """
104 assert isinstance(field, common_pb2.Path)
105 assert field.path
106 assert field.location
107
108 self.field = field
109 self.destination = destination
110 self.prefix = prefix or ''
111 self.delete = delete
112 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600113 self.reset = reset
114
Alex Kleinaa705412019-06-04 15:00:30 -0600115 # For resetting the state.
116 self._transferred = False
117 self._original_message = common_pb2.Path()
118 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600119
Alex Kleinaae49772019-07-26 10:20:50 -0600120 def transfer(self, direction):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600121 """Copy the file or directory to its destination.
122
123 Args:
124 direction (int): The direction files are being copied (into or out of
125 the chroot). Specifying the direction allows avoiding performing
126 unnecessary copies.
127 """
Alex Kleinaa705412019-06-04 15:00:30 -0600128 if self._transferred:
129 return
130
Alex Kleinaae49772019-07-26 10:20:50 -0600131 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600132
133 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600134 # Already in the correct location, nothing to do.
135 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600136
Alex Kleinaae49772019-07-26 10:20:50 -0600137 # Create a tempdir for the copied file if we're cleaning it up afterwords.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600138 if self.delete:
139 self.tempdir = osutils.TempDir(base_dir=self.destination)
140 destination = self.tempdir.tempdir
141 else:
142 destination = self.destination
143
Alex Kleinbd6edf82019-07-18 10:30:49 -0600144 source = self.field.path
145 if direction == self.OUTSIDE and self.prefix:
Alex Kleinaae49772019-07-26 10:20:50 -0600146 # When we're extracting files, we need /tmp/result to be
147 # /path/to/chroot/tmp/result.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600148 source = os.path.join(self.prefix, source.lstrip(os.sep))
149
150 if os.path.isfile(source):
Alex Kleinaae49772019-07-26 10:20:50 -0600151 # File - use the old file name, just copy it into the destination.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600152 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600153 copy_fn = shutil.copy
154 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600155 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600156 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600157 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600158
Alex Kleinbd6edf82019-07-18 10:30:49 -0600159 logging.debug('Copying %s to %s', source, dest_path)
160 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600161
162 # Clean up the destination path for returning, if applicable.
163 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600164 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600165 return_path = return_path[len(self.prefix):]
166
Alex Kleinaa705412019-06-04 15:00:30 -0600167 self.field.path = return_path
168 self.field.location = direction
169 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600170
171 def cleanup(self):
172 if self.tempdir:
173 self.tempdir.Cleanup()
174 self.tempdir = None
175
Alex Kleinbd6edf82019-07-18 10:30:49 -0600176 if self.reset:
177 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600178
Alex Kleinc05f3d12019-05-29 14:16:21 -0600179
Alex Kleinf0717a62019-12-06 09:45:00 -0700180class SyncedDirHandler(object):
181 """Handler for syncing directories across the chroot boundary."""
182
183 def __init__(self, field, destination, prefix):
184 self.field = field
185 self.prefix = prefix
186
187 self.source = self.field.dir
188 if not self.source.endswith(os.sep):
189 self.source += os.sep
190
191 self.destination = destination
192 if not self.destination.endswith(os.sep):
193 self.destination += os.sep
194
195 # For resetting the message later.
196 self._original_message = common_pb2.SyncedDir()
197 self._original_message.CopyFrom(self.field)
198
199 def _sync(self, src, dest):
Alex Klein915cce92019-12-17 14:19:50 -0700200 logging.info('Syncing %s to %s', src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700201 # TODO: This would probably be more efficient with rsync.
202 osutils.EmptyDir(dest)
203 osutils.CopyDirContents(src, dest)
204
205 def sync_in(self):
206 """Sync files from the source directory to the destination directory."""
207 self._sync(self.source, self.destination)
208 self.field.dir = '/%s' % os.path.relpath(self.destination, self.prefix)
209
210 def sync_out(self):
211 """Sync files from the destination directory to the source directory."""
212 self._sync(self.destination, self.source)
213 self.field.CopyFrom(self._original_message)
214
215
Alex Kleinc05f3d12019-05-29 14:16:21 -0600216@contextlib.contextmanager
Alex Kleinaae49772019-07-26 10:20:50 -0600217def copy_paths_in(message, destination, delete=True, prefix=None):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600218 """Context manager function to transfer and cleanup all Path messages.
219
220 Args:
221 message (Message): A message whose Path messages should be transferred.
Alex Kleinf0717a62019-12-06 09:45:00 -0700222 destination (str): The base destination path.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600223 delete (bool): Whether the file(s) should be deleted.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600224 prefix (str|None): A prefix path to remove from the final destination path
225 in the Path message (i.e. remove the chroot path).
226
227 Returns:
228 list[PathHandler]: The path handlers.
229 """
230 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600231
Alex Kleinf0717a62019-12-06 09:45:00 -0700232 handlers = _extract_handlers(message, destination, prefix, delete=delete,
233 reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600234
235 for handler in handlers:
Alex Kleinaae49772019-07-26 10:20:50 -0600236 handler.transfer(PathHandler.INSIDE)
Alex Kleinaa705412019-06-04 15:00:30 -0600237
238 try:
239 yield handlers
240 finally:
241 for handler in handlers:
242 handler.cleanup()
243
244
Alex Kleinf0717a62019-12-06 09:45:00 -0700245@contextlib.contextmanager
246def sync_dirs(message, destination, prefix):
247 """Context manager function to handle SyncedDir messages.
248
249 The sync semantics are effectively:
250 rsync -r --del source/ destination/
251 * The endpoint runs. *
252 rsync -r --del destination/ source/
253
254 Args:
255 message (Message): A message whose SyncedPath messages should be synced.
256 destination (str): The destination path.
257 prefix (str): A prefix path to remove from the final destination path
258 in the Path message (i.e. remove the chroot path).
259
260 Returns:
261 list[SyncedDirHandler]: The handlers.
262 """
263 assert destination
264
265 handlers = _extract_handlers(message, destination, prefix=prefix,
266 delete=False, reset=True,
267 message_type=common_pb2.SyncedDir)
268
269 for handler in handlers:
270 handler.sync_in()
271
272 try:
273 yield handlers
274 finally:
275 for handler in handlers:
276 handler.sync_out()
277
278
Alex Kleinaae49772019-07-26 10:20:50 -0600279def extract_results(request_message, response_message, chroot):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600280 """Transfer all response Path messages to the request's ResultPath.
281
282 Args:
283 request_message (Message): The request message containing a ResultPath
284 message.
285 response_message (Message): The response message whose Path message(s)
286 are to be transferred.
287 chroot (chroot_lib.Chroot): The chroot the files are being copied out of.
288 """
289 # Find the ResultPath.
290 for descriptor in request_message.DESCRIPTOR.fields:
291 field = getattr(request_message, descriptor.name)
292 if isinstance(field, common_pb2.ResultPath):
293 result_path_message = field
294 break
295 else:
296 # No ResultPath to handle.
297 return
298
299 destination = result_path_message.path.path
Alex Kleinf0717a62019-12-06 09:45:00 -0700300 handlers = _extract_handlers(response_message, destination, chroot.path,
301 delete=False, reset=False)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600302
303 for handler in handlers:
304 handler.transfer(PathHandler.OUTSIDE)
305 handler.cleanup()
306
307
Alex Kleinf0717a62019-12-06 09:45:00 -0700308def _extract_handlers(message, destination, prefix, delete=False, reset=False,
309 field_name=None, message_type=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600310 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700311 message_type = message_type or common_pb2.Path
312 is_path_target = message_type is common_pb2.Path
313 is_synced_target = message_type is common_pb2.SyncedDir
314
Alex Kleinbd6edf82019-07-18 10:30:49 -0600315 is_message = isinstance(message, protobuf_message.Message)
316 is_result_path = isinstance(message, common_pb2.ResultPath)
317 if not is_message or is_result_path:
318 # Base case: Nothing to handle.
319 # There's nothing we can do with scalar values.
320 # Skip ResultPath instances to avoid unnecessary file copying.
321 return []
Alex Kleinf0717a62019-12-06 09:45:00 -0700322 elif is_path_target and isinstance(message, common_pb2.Path):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600323 # Base case: Create handler for this message.
324 if not message.path or not message.location:
325 logging.debug('Skipping %s; incomplete.', field_name or 'message')
326 return []
327
328 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
329 reset=reset)
330 return [handler]
Alex Kleinf0717a62019-12-06 09:45:00 -0700331 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
332 if not message.dir:
333 logging.debug('Skipping %s; no directory given.', field_name or 'message')
334 return []
335
336 handler = SyncedDirHandler(message, destination, prefix)
337 return [handler]
Alex Kleinbd6edf82019-07-18 10:30:49 -0600338
339 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600340 handlers = []
341 for descriptor in message.DESCRIPTOR.fields:
342 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600343 if field_name:
344 new_field_name = '%s.%s' % (field_name, descriptor.name)
345 else:
346 new_field_name = descriptor.name
347
348 if isinstance(field, protobuf_message.Message):
349 # Recurse for nested Paths.
350 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700351 _extract_handlers(field, destination, prefix, delete, reset,
352 field_name=new_field_name,
353 message_type=message_type))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600354 else:
355 # If it's iterable it may be a repeated field, try each element.
356 try:
357 iterator = iter(field)
358 except TypeError:
359 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600360 continue
361
Alex Kleinbd6edf82019-07-18 10:30:49 -0600362 for element in iterator:
363 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700364 _extract_handlers(element, destination, prefix, delete, reset,
365 field_name=new_field_name,
366 message_type=message_type))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600367
Alex Kleinaa705412019-06-04 15:00:30 -0600368 return handlers