blob: e2d5be8b5bd7543a8bf06e7fe56fbe4701cbaff0 [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
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 cros_logging as logging
22from chromite.lib import osutils
23
Alex Kleinaa705412019-06-04 15:00:30 -060024from google.protobuf import message as protobuf_message
25
Alex Kleinc05f3d12019-05-29 14:16:21 -060026
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
38 def __init__(self, clear_field):
39 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 Klein38c7d9e2019-05-08 09:31:19 -060057 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060058
59
60def handle_chroot(message, clear_field=True):
61 """Find and parse the chroot field, returning the Chroot instance.
62
63 Returns:
64 chroot_lib.Chroot
65 """
66 handler = ChrootHandler(clear_field)
67 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
75class PathHandler(object):
76 """Handles copying a file or directory into or out of the chroot."""
77
78 INSIDE = common_pb2.Path.INSIDE
79 OUTSIDE = common_pb2.Path.OUTSIDE
80 ALL = -1
81
Alex Kleinbd6edf82019-07-18 10:30:49 -060082 def __init__(self, field, destination, delete, prefix=None, reset=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060083 """Path handler initialization.
84
85 Args:
86 field (common_pb2.Path): The Path message.
87 destination (str): The destination base path.
88 delete (bool): Whether the copied file(s) should be deleted on cleanup.
89 prefix (str|None): A path prefix to remove from the destination path
Alex Kleinbd6edf82019-07-18 10:30:49 -060090 when moving files inside the chroot, or to add to the source paths when
91 moving files out of the chroot.
92 reset (bool): Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -060093 """
94 assert isinstance(field, common_pb2.Path)
95 assert field.path
96 assert field.location
97
98 self.field = field
99 self.destination = destination
100 self.prefix = prefix or ''
101 self.delete = delete
102 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600103 self.reset = reset
104
Alex Kleinaa705412019-06-04 15:00:30 -0600105 # For resetting the state.
106 self._transferred = False
107 self._original_message = common_pb2.Path()
108 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600109
Alex Kleinbd6edf82019-07-18 10:30:49 -0600110 def __enter__(self):
111 self.transfer()
112
113 def __exit__(self, exc_type, exc_val, exc_tb):
114 self.cleanup()
115
Alex Kleinc05f3d12019-05-29 14:16:21 -0600116 def transfer(self, direction=None):
117 """Copy the file or directory to its destination.
118
119 Args:
120 direction (int): The direction files are being copied (into or out of
121 the chroot). Specifying the direction allows avoiding performing
122 unnecessary copies.
123 """
Alex Kleinaa705412019-06-04 15:00:30 -0600124 if self._transferred:
125 return
126
Alex Kleinc05f3d12019-05-29 14:16:21 -0600127 if direction is None:
128 direction = self.ALL
129 assert direction in [self.INSIDE, self.OUTSIDE, self.ALL]
130
131 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600132 # Already in the correct location, nothing to do.
133 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600134
135 if self.delete:
136 self.tempdir = osutils.TempDir(base_dir=self.destination)
137 destination = self.tempdir.tempdir
138 else:
139 destination = self.destination
140
Alex Kleinbd6edf82019-07-18 10:30:49 -0600141 source = self.field.path
142 if direction == self.OUTSIDE and self.prefix:
143 source = os.path.join(self.prefix, source.lstrip(os.sep))
144
145 if os.path.isfile(source):
146 # File - use the old file name, just copy it into dest.
147 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600148 copy_fn = shutil.copy
149 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600150 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600151 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600152 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600153
Alex Kleinbd6edf82019-07-18 10:30:49 -0600154 logging.debug('Copying %s to %s', source, dest_path)
155 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600156
157 # Clean up the destination path for returning, if applicable.
158 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600159 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600160 return_path = return_path[len(self.prefix):]
161
Alex Kleinaa705412019-06-04 15:00:30 -0600162 self.field.path = return_path
163 self.field.location = direction
164 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600165
166 def cleanup(self):
167 if self.tempdir:
168 self.tempdir.Cleanup()
169 self.tempdir = None
170
Alex Kleinbd6edf82019-07-18 10:30:49 -0600171 if self.reset:
172 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600173
Alex Kleinc05f3d12019-05-29 14:16:21 -0600174
175@contextlib.contextmanager
176def handle_paths(message, destination, delete=True, direction=None,
177 prefix=None):
178 """Context manager function to transfer and cleanup all Path messages.
179
180 Args:
181 message (Message): A message whose Path messages should be transferred.
182 destination (str): A base destination path.
183 delete (bool): Whether the file(s) should be deleted.
184 direction (int): One of the PathHandler constants (INSIDE, OUTSIDE, ALL).
185 This allows avoiding unnecessarily copying files already in the right
186 place (e.g. copying a file into the chroot that's already in the chroot).
187 prefix (str|None): A prefix path to remove from the final destination path
188 in the Path message (i.e. remove the chroot path).
189
190 Returns:
191 list[PathHandler]: The path handlers.
192 """
193 assert destination
194 direction = direction or PathHandler.ALL
195
Alex Kleinbd6edf82019-07-18 10:30:49 -0600196 handlers = _extract_handlers(message, destination, delete, prefix, reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600197
198 for handler in handlers:
199 handler.transfer(direction)
200
201 try:
202 yield handlers
203 finally:
204 for handler in handlers:
205 handler.cleanup()
206
207
Alex Kleinbd6edf82019-07-18 10:30:49 -0600208def handle_result_paths(request_message, response_message, chroot):
209 """Transfer all response Path messages to the request's ResultPath.
210
211 Args:
212 request_message (Message): The request message containing a ResultPath
213 message.
214 response_message (Message): The response message whose Path message(s)
215 are to be transferred.
216 chroot (chroot_lib.Chroot): The chroot the files are being copied out of.
217 """
218 # Find the ResultPath.
219 for descriptor in request_message.DESCRIPTOR.fields:
220 field = getattr(request_message, descriptor.name)
221 if isinstance(field, common_pb2.ResultPath):
222 result_path_message = field
223 break
224 else:
225 # No ResultPath to handle.
226 return
227
228 destination = result_path_message.path.path
229 handlers = _extract_handlers(response_message, destination, delete=False,
230 prefix=chroot.path, reset=False)
231
232 for handler in handlers:
233 handler.transfer(PathHandler.OUTSIDE)
234 handler.cleanup()
235
236
237def _extract_handlers(message, destination, delete, prefix, reset,
238 field_name=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600239 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinbd6edf82019-07-18 10:30:49 -0600240 is_message = isinstance(message, protobuf_message.Message)
241 is_result_path = isinstance(message, common_pb2.ResultPath)
242 if not is_message or is_result_path:
243 # Base case: Nothing to handle.
244 # There's nothing we can do with scalar values.
245 # Skip ResultPath instances to avoid unnecessary file copying.
246 return []
247 elif isinstance(message, common_pb2.Path):
248 # Base case: Create handler for this message.
249 if not message.path or not message.location:
250 logging.debug('Skipping %s; incomplete.', field_name or 'message')
251 return []
252
253 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
254 reset=reset)
255 return [handler]
256
257 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600258 handlers = []
259 for descriptor in message.DESCRIPTOR.fields:
260 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600261 if field_name:
262 new_field_name = '%s.%s' % (field_name, descriptor.name)
263 else:
264 new_field_name = descriptor.name
265
266 if isinstance(field, protobuf_message.Message):
267 # Recurse for nested Paths.
268 handlers.extend(
269 _extract_handlers(field, destination, delete, prefix, reset,
270 field_name=new_field_name))
271 else:
272 # If it's iterable it may be a repeated field, try each element.
273 try:
274 iterator = iter(field)
275 except TypeError:
276 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600277 continue
278
Alex Kleinbd6edf82019-07-18 10:30:49 -0600279 for element in iterator:
280 handlers.extend(
281 _extract_handlers(element, destination, delete, prefix, reset,
282 field_name=new_field_name))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600283
Alex Kleinaa705412019-06-04 15:00:30 -0600284 return handlers