blob: e18ef1dc0628e0cb1dd5a81997d24ccd93a1e9d0 [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 Klein915cce92019-12-17 14:19:50 -070038 def __init__(self, clear_field, parse_goma):
Alex Kleinc05f3d12019-05-29 14:16:21 -060039 self.clear_field = clear_field
Alex Klein915cce92019-12-17 14:19:50 -070040 self.parse_goma = parse_goma
Alex Kleinc05f3d12019-05-29 14:16:21 -060041
42 def handle(self, message):
43 """Parse a message for a chroot field."""
44 # Find the Chroot field. Search for the field by type to prevent it being
45 # tied to a naming convention.
46 for descriptor in message.DESCRIPTOR.fields:
47 field = getattr(message, descriptor.name)
48 if isinstance(field, common_pb2.Chroot):
49 chroot = field
50 if self.clear_field:
51 message.ClearField(descriptor.name)
52 return self.parse_chroot(chroot)
53
54 return None
55
56 def parse_chroot(self, chroot_message):
57 """Parse a Chroot message instance."""
Alex Klein915cce92019-12-17 14:19:50 -070058 return controller_util.ParseChroot(chroot_message,
59 parse_goma=self.parse_goma)
Alex Kleinc05f3d12019-05-29 14:16:21 -060060
61
Alex Klein915cce92019-12-17 14:19:50 -070062def handle_chroot(message, clear_field=True, parse_goma=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060063 """Find and parse the chroot field, returning the Chroot instance.
64
65 Returns:
66 chroot_lib.Chroot
67 """
Alex Klein915cce92019-12-17 14:19:50 -070068 handler = ChrootHandler(clear_field, parse_goma)
Alex Kleinc05f3d12019-05-29 14:16:21 -060069 chroot = handler.handle(message)
70 if chroot:
71 return chroot
72
73 logging.warning('No chroot message found, falling back to defaults.')
74 return handler.parse_chroot(common_pb2.Chroot())
75
76
Alex Klein9b7331e2019-12-30 14:37:21 -070077def handle_goma(message, chroot_path):
78 """Find and parse the GomaConfig field, returning the Goma instance."""
79 for descriptor in message.DESCRIPTOR.fields:
80 field = getattr(message, descriptor.name)
81 if isinstance(field, common_pb2.GomaConfig):
82 goma_config = field
83 return controller_util.ParseGomaConfig(goma_config, chroot_path)
84
85 return None
86
87
Alex Kleinc05f3d12019-05-29 14:16:21 -060088class PathHandler(object):
89 """Handles copying a file or directory into or out of the chroot."""
90
91 INSIDE = common_pb2.Path.INSIDE
92 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -060093
Alex Kleinbd6edf82019-07-18 10:30:49 -060094 def __init__(self, field, destination, delete, prefix=None, reset=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060095 """Path handler initialization.
96
97 Args:
98 field (common_pb2.Path): The Path message.
99 destination (str): The destination base path.
100 delete (bool): Whether the copied file(s) should be deleted on cleanup.
101 prefix (str|None): A path prefix to remove from the destination path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600102 when moving files inside the chroot, or to add to the source paths when
103 moving files out of the chroot.
104 reset (bool): Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600105 """
106 assert isinstance(field, common_pb2.Path)
107 assert field.path
108 assert field.location
109
110 self.field = field
111 self.destination = destination
112 self.prefix = prefix or ''
113 self.delete = delete
114 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600115 self.reset = reset
116
Alex Kleinaa705412019-06-04 15:00:30 -0600117 # For resetting the state.
118 self._transferred = False
119 self._original_message = common_pb2.Path()
120 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600121
Alex Kleinaae49772019-07-26 10:20:50 -0600122 def transfer(self, direction):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600123 """Copy the file or directory to its destination.
124
125 Args:
126 direction (int): The direction files are being copied (into or out of
127 the chroot). Specifying the direction allows avoiding performing
128 unnecessary copies.
129 """
Alex Kleinaa705412019-06-04 15:00:30 -0600130 if self._transferred:
131 return
132
Alex Kleinaae49772019-07-26 10:20:50 -0600133 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600134
135 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600136 # Already in the correct location, nothing to do.
137 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600138
Alex Kleinaae49772019-07-26 10:20:50 -0600139 # Create a tempdir for the copied file if we're cleaning it up afterwords.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600140 if self.delete:
141 self.tempdir = osutils.TempDir(base_dir=self.destination)
142 destination = self.tempdir.tempdir
143 else:
144 destination = self.destination
145
Alex Kleinbd6edf82019-07-18 10:30:49 -0600146 source = self.field.path
147 if direction == self.OUTSIDE and self.prefix:
Alex Kleinaae49772019-07-26 10:20:50 -0600148 # When we're extracting files, we need /tmp/result to be
149 # /path/to/chroot/tmp/result.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600150 source = os.path.join(self.prefix, source.lstrip(os.sep))
151
152 if os.path.isfile(source):
Alex Kleinaae49772019-07-26 10:20:50 -0600153 # File - use the old file name, just copy it into the destination.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600154 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600155 copy_fn = shutil.copy
156 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600157 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600158 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600159 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600160
Alex Kleinbd6edf82019-07-18 10:30:49 -0600161 logging.debug('Copying %s to %s', source, dest_path)
162 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600163
164 # Clean up the destination path for returning, if applicable.
165 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600166 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600167 return_path = return_path[len(self.prefix):]
168
Alex Kleinaa705412019-06-04 15:00:30 -0600169 self.field.path = return_path
170 self.field.location = direction
171 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600172
173 def cleanup(self):
174 if self.tempdir:
175 self.tempdir.Cleanup()
176 self.tempdir = None
177
Alex Kleinbd6edf82019-07-18 10:30:49 -0600178 if self.reset:
179 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600180
Alex Kleinc05f3d12019-05-29 14:16:21 -0600181
Alex Kleinf0717a62019-12-06 09:45:00 -0700182class SyncedDirHandler(object):
183 """Handler for syncing directories across the chroot boundary."""
184
185 def __init__(self, field, destination, prefix):
186 self.field = field
187 self.prefix = prefix
188
189 self.source = self.field.dir
190 if not self.source.endswith(os.sep):
191 self.source += os.sep
192
193 self.destination = destination
194 if not self.destination.endswith(os.sep):
195 self.destination += os.sep
196
197 # For resetting the message later.
198 self._original_message = common_pb2.SyncedDir()
199 self._original_message.CopyFrom(self.field)
200
201 def _sync(self, src, dest):
Alex Klein915cce92019-12-17 14:19:50 -0700202 logging.info('Syncing %s to %s', src, dest)
Alex Kleinf0717a62019-12-06 09:45:00 -0700203 # TODO: This would probably be more efficient with rsync.
204 osutils.EmptyDir(dest)
205 osutils.CopyDirContents(src, dest)
206
207 def sync_in(self):
208 """Sync files from the source directory to the destination directory."""
209 self._sync(self.source, self.destination)
210 self.field.dir = '/%s' % os.path.relpath(self.destination, self.prefix)
211
212 def sync_out(self):
213 """Sync files from the destination directory to the source directory."""
214 self._sync(self.destination, self.source)
215 self.field.CopyFrom(self._original_message)
216
217
Alex Kleinc05f3d12019-05-29 14:16:21 -0600218@contextlib.contextmanager
Alex Kleinaae49772019-07-26 10:20:50 -0600219def copy_paths_in(message, destination, delete=True, prefix=None):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600220 """Context manager function to transfer and cleanup all Path messages.
221
222 Args:
223 message (Message): A message whose Path messages should be transferred.
Alex Kleinf0717a62019-12-06 09:45:00 -0700224 destination (str): The base destination path.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600225 delete (bool): Whether the file(s) should be deleted.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600226 prefix (str|None): A prefix path to remove from the final destination path
227 in the Path message (i.e. remove the chroot path).
228
229 Returns:
230 list[PathHandler]: The path handlers.
231 """
232 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600233
Alex Kleinf0717a62019-12-06 09:45:00 -0700234 handlers = _extract_handlers(message, destination, prefix, delete=delete,
235 reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600236
237 for handler in handlers:
Alex Kleinaae49772019-07-26 10:20:50 -0600238 handler.transfer(PathHandler.INSIDE)
Alex Kleinaa705412019-06-04 15:00:30 -0600239
240 try:
241 yield handlers
242 finally:
243 for handler in handlers:
244 handler.cleanup()
245
246
Alex Kleinf0717a62019-12-06 09:45:00 -0700247@contextlib.contextmanager
248def sync_dirs(message, destination, prefix):
249 """Context manager function to handle SyncedDir messages.
250
251 The sync semantics are effectively:
252 rsync -r --del source/ destination/
253 * The endpoint runs. *
254 rsync -r --del destination/ source/
255
256 Args:
257 message (Message): A message whose SyncedPath messages should be synced.
258 destination (str): The destination path.
259 prefix (str): A prefix path to remove from the final destination path
260 in the Path message (i.e. remove the chroot path).
261
262 Returns:
263 list[SyncedDirHandler]: The handlers.
264 """
265 assert destination
266
267 handlers = _extract_handlers(message, destination, prefix=prefix,
268 delete=False, reset=True,
269 message_type=common_pb2.SyncedDir)
270
271 for handler in handlers:
272 handler.sync_in()
273
274 try:
275 yield handlers
276 finally:
277 for handler in handlers:
278 handler.sync_out()
279
280
Alex Kleinaae49772019-07-26 10:20:50 -0600281def extract_results(request_message, response_message, chroot):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600282 """Transfer all response Path messages to the request's ResultPath.
283
284 Args:
285 request_message (Message): The request message containing a ResultPath
286 message.
287 response_message (Message): The response message whose Path message(s)
288 are to be transferred.
289 chroot (chroot_lib.Chroot): The chroot the files are being copied out of.
290 """
291 # Find the ResultPath.
292 for descriptor in request_message.DESCRIPTOR.fields:
293 field = getattr(request_message, descriptor.name)
294 if isinstance(field, common_pb2.ResultPath):
295 result_path_message = field
296 break
297 else:
298 # No ResultPath to handle.
299 return
300
301 destination = result_path_message.path.path
Alex Kleinf0717a62019-12-06 09:45:00 -0700302 handlers = _extract_handlers(response_message, destination, chroot.path,
303 delete=False, reset=False)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600304
305 for handler in handlers:
306 handler.transfer(PathHandler.OUTSIDE)
307 handler.cleanup()
308
309
Alex Kleinf0717a62019-12-06 09:45:00 -0700310def _extract_handlers(message, destination, prefix, delete=False, reset=False,
311 field_name=None, message_type=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600312 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinf0717a62019-12-06 09:45:00 -0700313 message_type = message_type or common_pb2.Path
314 is_path_target = message_type is common_pb2.Path
315 is_synced_target = message_type is common_pb2.SyncedDir
316
Alex Kleinbd6edf82019-07-18 10:30:49 -0600317 is_message = isinstance(message, protobuf_message.Message)
318 is_result_path = isinstance(message, common_pb2.ResultPath)
319 if not is_message or is_result_path:
320 # Base case: Nothing to handle.
321 # There's nothing we can do with scalar values.
322 # Skip ResultPath instances to avoid unnecessary file copying.
323 return []
Alex Kleinf0717a62019-12-06 09:45:00 -0700324 elif is_path_target and isinstance(message, common_pb2.Path):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600325 # Base case: Create handler for this message.
326 if not message.path or not message.location:
327 logging.debug('Skipping %s; incomplete.', field_name or 'message')
328 return []
329
330 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
331 reset=reset)
332 return [handler]
Alex Kleinf0717a62019-12-06 09:45:00 -0700333 elif is_synced_target and isinstance(message, common_pb2.SyncedDir):
334 if not message.dir:
335 logging.debug('Skipping %s; no directory given.', field_name or 'message')
336 return []
337
338 handler = SyncedDirHandler(message, destination, prefix)
339 return [handler]
Alex Kleinbd6edf82019-07-18 10:30:49 -0600340
341 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600342 handlers = []
343 for descriptor in message.DESCRIPTOR.fields:
344 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600345 if field_name:
346 new_field_name = '%s.%s' % (field_name, descriptor.name)
347 else:
348 new_field_name = descriptor.name
349
350 if isinstance(field, protobuf_message.Message):
351 # Recurse for nested Paths.
352 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700353 _extract_handlers(field, destination, prefix, delete, reset,
354 field_name=new_field_name,
355 message_type=message_type))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600356 else:
357 # If it's iterable it may be a repeated field, try each element.
358 try:
359 iterator = iter(field)
360 except TypeError:
361 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600362 continue
363
Alex Kleinbd6edf82019-07-18 10:30:49 -0600364 for element in iterator:
365 handlers.extend(
Alex Kleinf0717a62019-12-06 09:45:00 -0700366 _extract_handlers(element, destination, prefix, delete, reset,
367 field_name=new_field_name,
368 message_type=message_type))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600369
Alex Kleinaa705412019-06-04 15:00:30 -0600370 return handlers