blob: a9caff1e0e10bca612b91ac3803e3763ed14205b [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
Mike Frysinger17844a02019-08-24 18:21:02 -040024# TODO(vapier): Re-enable check once we upgrade to pylint-1.8+.
25# pylint: disable=no-name-in-module
Alex Kleinaa705412019-06-04 15:00:30 -060026from google.protobuf import message as protobuf_message
Mike Frysinger17844a02019-08-24 18:21:02 -040027# pylint: enable=no-name-in-module
Alex Kleinaa705412019-06-04 15:00:30 -060028
Alex Kleinc05f3d12019-05-29 14:16:21 -060029
Alex Kleinbd6edf82019-07-18 10:30:49 -060030class Error(Exception):
31 """Base error class for the module."""
32
33
34class InvalidResultPathError(Error):
35 """Result path is invalid."""
36
37
Alex Kleinc05f3d12019-05-29 14:16:21 -060038class ChrootHandler(object):
39 """Translate a Chroot message to chroot enter arguments and env."""
40
41 def __init__(self, clear_field):
42 self.clear_field = clear_field
43
44 def handle(self, message):
45 """Parse a message for a chroot field."""
46 # Find the Chroot field. Search for the field by type to prevent it being
47 # tied to a naming convention.
48 for descriptor in message.DESCRIPTOR.fields:
49 field = getattr(message, descriptor.name)
50 if isinstance(field, common_pb2.Chroot):
51 chroot = field
52 if self.clear_field:
53 message.ClearField(descriptor.name)
54 return self.parse_chroot(chroot)
55
56 return None
57
58 def parse_chroot(self, chroot_message):
59 """Parse a Chroot message instance."""
Alex Klein38c7d9e2019-05-08 09:31:19 -060060 return controller_util.ParseChroot(chroot_message)
Alex Kleinc05f3d12019-05-29 14:16:21 -060061
62
63def handle_chroot(message, clear_field=True):
64 """Find and parse the chroot field, returning the Chroot instance.
65
66 Returns:
67 chroot_lib.Chroot
68 """
69 handler = ChrootHandler(clear_field)
70 chroot = handler.handle(message)
71 if chroot:
72 return chroot
73
74 logging.warning('No chroot message found, falling back to defaults.')
75 return handler.parse_chroot(common_pb2.Chroot())
76
77
78class PathHandler(object):
79 """Handles copying a file or directory into or out of the chroot."""
80
81 INSIDE = common_pb2.Path.INSIDE
82 OUTSIDE = common_pb2.Path.OUTSIDE
Alex Kleinc05f3d12019-05-29 14:16:21 -060083
Alex Kleinbd6edf82019-07-18 10:30:49 -060084 def __init__(self, field, destination, delete, prefix=None, reset=True):
Alex Kleinc05f3d12019-05-29 14:16:21 -060085 """Path handler initialization.
86
87 Args:
88 field (common_pb2.Path): The Path message.
89 destination (str): The destination base path.
90 delete (bool): Whether the copied file(s) should be deleted on cleanup.
91 prefix (str|None): A path prefix to remove from the destination path
Alex Kleinbd6edf82019-07-18 10:30:49 -060092 when moving files inside the chroot, or to add to the source paths when
93 moving files out of the chroot.
94 reset (bool): Whether to reset the state on cleanup.
Alex Kleinc05f3d12019-05-29 14:16:21 -060095 """
96 assert isinstance(field, common_pb2.Path)
97 assert field.path
98 assert field.location
99
100 self.field = field
101 self.destination = destination
102 self.prefix = prefix or ''
103 self.delete = delete
104 self.tempdir = None
Alex Kleinbd6edf82019-07-18 10:30:49 -0600105 self.reset = reset
106
Alex Kleinaa705412019-06-04 15:00:30 -0600107 # For resetting the state.
108 self._transferred = False
109 self._original_message = common_pb2.Path()
110 self._original_message.CopyFrom(self.field)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600111
Alex Kleinaae49772019-07-26 10:20:50 -0600112 def transfer(self, direction):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600113 """Copy the file or directory to its destination.
114
115 Args:
116 direction (int): The direction files are being copied (into or out of
117 the chroot). Specifying the direction allows avoiding performing
118 unnecessary copies.
119 """
Alex Kleinaa705412019-06-04 15:00:30 -0600120 if self._transferred:
121 return
122
Alex Kleinaae49772019-07-26 10:20:50 -0600123 assert direction in [self.INSIDE, self.OUTSIDE]
Alex Kleinc05f3d12019-05-29 14:16:21 -0600124
125 if self.field.location == direction:
Alex Kleinaa705412019-06-04 15:00:30 -0600126 # Already in the correct location, nothing to do.
127 return
Alex Kleinc05f3d12019-05-29 14:16:21 -0600128
Alex Kleinaae49772019-07-26 10:20:50 -0600129 # Create a tempdir for the copied file if we're cleaning it up afterwords.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600130 if self.delete:
131 self.tempdir = osutils.TempDir(base_dir=self.destination)
132 destination = self.tempdir.tempdir
133 else:
134 destination = self.destination
135
Alex Kleinbd6edf82019-07-18 10:30:49 -0600136 source = self.field.path
137 if direction == self.OUTSIDE and self.prefix:
Alex Kleinaae49772019-07-26 10:20:50 -0600138 # When we're extracting files, we need /tmp/result to be
139 # /path/to/chroot/tmp/result.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600140 source = os.path.join(self.prefix, source.lstrip(os.sep))
141
142 if os.path.isfile(source):
Alex Kleinaae49772019-07-26 10:20:50 -0600143 # File - use the old file name, just copy it into the destination.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600144 dest_path = os.path.join(destination, os.path.basename(source))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600145 copy_fn = shutil.copy
146 else:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600147 # Directory - just copy everything into the new location.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600148 dest_path = destination
Alex Kleinbd6edf82019-07-18 10:30:49 -0600149 copy_fn = functools.partial(osutils.CopyDirContents, allow_nonempty=True)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600150
Alex Kleinbd6edf82019-07-18 10:30:49 -0600151 logging.debug('Copying %s to %s', source, dest_path)
152 copy_fn(source, dest_path)
Alex Kleinc05f3d12019-05-29 14:16:21 -0600153
154 # Clean up the destination path for returning, if applicable.
155 return_path = dest_path
Alex Kleinbd6edf82019-07-18 10:30:49 -0600156 if direction == self.INSIDE and return_path.startswith(self.prefix):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600157 return_path = return_path[len(self.prefix):]
158
Alex Kleinaa705412019-06-04 15:00:30 -0600159 self.field.path = return_path
160 self.field.location = direction
161 self._transferred = True
Alex Kleinc05f3d12019-05-29 14:16:21 -0600162
163 def cleanup(self):
164 if self.tempdir:
165 self.tempdir.Cleanup()
166 self.tempdir = None
167
Alex Kleinbd6edf82019-07-18 10:30:49 -0600168 if self.reset:
169 self.field.CopyFrom(self._original_message)
Alex Kleinaa705412019-06-04 15:00:30 -0600170
Alex Kleinc05f3d12019-05-29 14:16:21 -0600171
172@contextlib.contextmanager
Alex Kleinaae49772019-07-26 10:20:50 -0600173def copy_paths_in(message, destination, delete=True, prefix=None):
Alex Kleinc05f3d12019-05-29 14:16:21 -0600174 """Context manager function to transfer and cleanup all Path messages.
175
176 Args:
177 message (Message): A message whose Path messages should be transferred.
178 destination (str): A base destination path.
179 delete (bool): Whether the file(s) should be deleted.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600180 prefix (str|None): A prefix path to remove from the final destination path
181 in the Path message (i.e. remove the chroot path).
182
183 Returns:
184 list[PathHandler]: The path handlers.
185 """
186 assert destination
Alex Kleinc05f3d12019-05-29 14:16:21 -0600187
Alex Kleinbd6edf82019-07-18 10:30:49 -0600188 handlers = _extract_handlers(message, destination, delete, prefix, reset=True)
Alex Kleinaa705412019-06-04 15:00:30 -0600189
190 for handler in handlers:
Alex Kleinaae49772019-07-26 10:20:50 -0600191 handler.transfer(PathHandler.INSIDE)
Alex Kleinaa705412019-06-04 15:00:30 -0600192
193 try:
194 yield handlers
195 finally:
196 for handler in handlers:
197 handler.cleanup()
198
199
Alex Kleinaae49772019-07-26 10:20:50 -0600200def extract_results(request_message, response_message, chroot):
Alex Kleinbd6edf82019-07-18 10:30:49 -0600201 """Transfer all response Path messages to the request's ResultPath.
202
203 Args:
204 request_message (Message): The request message containing a ResultPath
205 message.
206 response_message (Message): The response message whose Path message(s)
207 are to be transferred.
208 chroot (chroot_lib.Chroot): The chroot the files are being copied out of.
209 """
210 # Find the ResultPath.
211 for descriptor in request_message.DESCRIPTOR.fields:
212 field = getattr(request_message, descriptor.name)
213 if isinstance(field, common_pb2.ResultPath):
214 result_path_message = field
215 break
216 else:
217 # No ResultPath to handle.
218 return
219
220 destination = result_path_message.path.path
221 handlers = _extract_handlers(response_message, destination, delete=False,
222 prefix=chroot.path, reset=False)
223
224 for handler in handlers:
225 handler.transfer(PathHandler.OUTSIDE)
226 handler.cleanup()
227
228
229def _extract_handlers(message, destination, delete, prefix, reset,
230 field_name=None):
Alex Kleinaa705412019-06-04 15:00:30 -0600231 """Recursive helper for handle_paths to extract Path messages."""
Alex Kleinbd6edf82019-07-18 10:30:49 -0600232 is_message = isinstance(message, protobuf_message.Message)
233 is_result_path = isinstance(message, common_pb2.ResultPath)
234 if not is_message or is_result_path:
235 # Base case: Nothing to handle.
236 # There's nothing we can do with scalar values.
237 # Skip ResultPath instances to avoid unnecessary file copying.
238 return []
239 elif isinstance(message, common_pb2.Path):
240 # Base case: Create handler for this message.
241 if not message.path or not message.location:
242 logging.debug('Skipping %s; incomplete.', field_name or 'message')
243 return []
244
245 handler = PathHandler(message, destination, delete=delete, prefix=prefix,
246 reset=reset)
247 return [handler]
248
249 # Iterate through each field and recurse.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600250 handlers = []
251 for descriptor in message.DESCRIPTOR.fields:
252 field = getattr(message, descriptor.name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600253 if field_name:
254 new_field_name = '%s.%s' % (field_name, descriptor.name)
255 else:
256 new_field_name = descriptor.name
257
258 if isinstance(field, protobuf_message.Message):
259 # Recurse for nested Paths.
260 handlers.extend(
261 _extract_handlers(field, destination, delete, prefix, reset,
262 field_name=new_field_name))
263 else:
264 # If it's iterable it may be a repeated field, try each element.
265 try:
266 iterator = iter(field)
267 except TypeError:
268 # Definitely not a repeated field, just move on.
Alex Kleinc05f3d12019-05-29 14:16:21 -0600269 continue
270
Alex Kleinbd6edf82019-07-18 10:30:49 -0600271 for element in iterator:
272 handlers.extend(
273 _extract_handlers(element, destination, delete, prefix, reset,
274 field_name=new_field_name))
Alex Kleinc05f3d12019-05-29 14:16:21 -0600275
Alex Kleinaa705412019-06-04 15:00:30 -0600276 return handlers