blob: 613fbf2df589bc31da9a319384ad614790dc2d80 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
David Pursell9476bf42015-03-30 13:34:27 -07002# Copyright 2015 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
Alex Kleinaaddc932020-01-30 15:02:24 -07006"""Deploy packages onto a target device.
7
8Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
9See that file for more information.
10"""
David Pursell9476bf42015-03-30 13:34:27 -070011
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040012from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070013from __future__ import print_function
14
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070015import bz2
David Pursell9476bf42015-03-30 13:34:27 -070016import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070017import functools
David Pursell9476bf42015-03-30 13:34:27 -070018import json
19import os
Mike Frysinger3f087aa2020-03-20 06:03:16 -040020import sys
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070021import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070022
Ralph Nathane01ccf12015-04-16 10:40:32 -070023from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070024from chromite.lib import cros_build_lib
25from chromite.lib import cros_logging as logging
Ralph Nathane01ccf12015-04-16 10:40:32 -070026from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070027from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070029from chromite.lib import remote_access
Andrewc7e1c6b2020-02-27 16:03:53 -080030from chromite.scripts import build_dlc
David Pursell9476bf42015-03-30 13:34:27 -070031try:
32 import portage
33except ImportError:
34 if cros_build_lib.IsInsideChroot():
35 raise
36
37
Mike Frysinger3f087aa2020-03-20 06:03:16 -040038assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
39
40
David Pursell9476bf42015-03-30 13:34:27 -070041_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
42# This is defined in src/platform/dev/builder.py
43_STRIPPED_PACKAGES_DIR = 'stripped-packages'
44
45_MAX_UPDATES_NUM = 10
46_MAX_UPDATES_WARNING = (
47 'You are about to update a large number of installed packages, which '
48 'might take a long time, fail midway, or leave the target in an '
49 'inconsistent state. It is highly recommended that you flash a new image '
50 'instead.')
51
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070052_DLC_ID = 'DLC_ID'
53_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080054_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070055_ENVIRONMENT_FILENAME = 'environment.bz2'
56_DLC_INSTALL_ROOT = '/var/cache/dlc'
57
David Pursell9476bf42015-03-30 13:34:27 -070058
59class DeployError(Exception):
60 """Thrown when an unrecoverable error is encountered during deploy."""
61
62
Ralph Nathane01ccf12015-04-16 10:40:32 -070063class BrilloDeployOperation(operation.ProgressBarOperation):
64 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070065 # These two variables are used to validate the output in the VM integration
66 # tests. Changes to the output must be reflected here.
67 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing', 'WARNING: Ignoring',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070068 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070069 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070070
71 def __init__(self, pkg_count, emerge):
72 """Construct BrilloDeployOperation object.
73
74 Args:
75 pkg_count: number of packages being built.
76 emerge: True if emerge, False is unmerge.
77 """
78 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070079 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070080 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070081 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070082 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070083 self._total = pkg_count * len(self._events)
84 self._completed = 0
85
Ralph Nathandc14ed92015-04-22 11:17:40 -070086 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070087 """Parse the output of brillo deploy to update a progress bar."""
88 stdout = self._stdout.read()
89 stderr = self._stderr.read()
90 output = stdout + stderr
91 for event in self._events:
92 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040093 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070094
95
David Pursell9476bf42015-03-30 13:34:27 -070096class _InstallPackageScanner(object):
97 """Finds packages that need to be installed on a target device.
98
99 Scans the sysroot bintree, beginning with a user-provided list of packages,
100 to find all packages that need to be installed. If so instructed,
101 transitively scans forward (mandatory) and backward (optional) dependencies
102 as well. A package will be installed if missing on the target (mandatory
103 packages only), or it will be updated if its sysroot version and build time
104 are different from the target. Common usage:
105
106 pkg_scanner = _InstallPackageScanner(sysroot)
107 pkgs = pkg_scanner.Run(...)
108 """
109
110 class VartreeError(Exception):
111 """An error in the processing of the installed packages tree."""
112
113 class BintreeError(Exception):
114 """An error in the processing of the source binpkgs tree."""
115
116 class PkgInfo(object):
117 """A record containing package information."""
118
119 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
120
121 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
122 self.cpv = cpv
123 self.build_time = build_time
124 self.rdeps_raw = rdeps_raw
125 self.rdeps = set() if rdeps is None else rdeps
126 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
127
128 # Python snippet for dumping vartree info on the target. Instantiate using
129 # _GetVartreeSnippet().
130 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700131import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700132import os
133import portage
134
135# Normalize the path to match what portage will index.
136target_root = os.path.normpath('%(root)s')
137if not target_root.endswith('/'):
138 target_root += '/'
139trees = portage.create_trees(target_root=target_root, config_root='/')
140vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700141pkg_info = []
142for cpv in vartree.dbapi.cpv_all():
143 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
144 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
145 pkg_info.append((cpv, slot, rdep_raw, build_time))
146
147print(json.dumps(pkg_info))
148"""
149
150 def __init__(self, sysroot):
151 self.sysroot = sysroot
152 # Members containing the sysroot (binpkg) and target (installed) package DB.
153 self.target_db = None
154 self.binpkgs_db = None
155 # Members for managing the dependency resolution work queue.
156 self.queue = None
157 self.seen = None
158 self.listed = None
159
160 @staticmethod
161 def _GetCP(cpv):
162 """Returns the CP value for a given CPV string."""
163 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600164 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700165 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600166 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700167
168 @staticmethod
169 def _InDB(cp, slot, db):
170 """Returns whether CP and slot are found in a database (if provided)."""
171 cp_slots = db.get(cp) if db else None
172 return cp_slots is not None and (not slot or slot in cp_slots)
173
174 @staticmethod
175 def _AtomStr(cp, slot):
176 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
177 return '%s:%s' % (cp, slot) if slot else cp
178
179 @classmethod
180 def _GetVartreeSnippet(cls, root='/'):
181 """Returns a code snippet for dumping the vartree on the target.
182
183 Args:
184 root: The installation root.
185
186 Returns:
187 The said code snippet (string) with parameters filled in.
188 """
189 return cls._GET_VARTREE % {'root': root}
190
191 @classmethod
192 def _StripDepAtom(cls, dep_atom, installed_db=None):
193 """Strips a dependency atom and returns a (CP, slot) pair."""
194 # TODO(garnold) This is a gross simplification of ebuild dependency
195 # semantics, stripping and ignoring various qualifiers (versions, slots,
196 # USE flag, negation) and will likely need to be fixed. chromium:447366.
197
198 # Ignore unversioned blockers, leaving them for the user to resolve.
199 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
200 return None, None
201
202 cp = dep_atom
203 slot = None
204 require_installed = False
205
206 # Versioned blockers should be updated, but only if already installed.
207 # These are often used for forcing cascaded updates of multiple packages,
208 # so we're treating them as ordinary constraints with hopes that it'll lead
209 # to the desired result.
210 if cp.startswith('!'):
211 cp = cp.lstrip('!')
212 require_installed = True
213
214 # Remove USE flags.
215 if '[' in cp:
216 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
217
218 # Separate the slot qualifier and strip off subslots.
219 if ':' in cp:
220 cp, slot = cp.split(':')
221 for delim in ('/', '='):
222 slot = slot.split(delim, 1)[0]
223
224 # Strip version wildcards (right), comparators (left).
225 cp = cp.rstrip('*')
226 cp = cp.lstrip('<=>~')
227
228 # Turn into CP form.
229 cp = cls._GetCP(cp)
230
231 if require_installed and not cls._InDB(cp, None, installed_db):
232 return None, None
233
234 return cp, slot
235
236 @classmethod
237 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
238 """Resolves and returns a list of dependencies from a dependency string.
239
240 This parses a dependency string and returns a list of package names and
241 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
242 resolving disjunctive deps, we include all choices that are fully present
243 in |installed_db|. If none is present, we choose an arbitrary one that is
244 available.
245
246 Args:
247 dep_str: A raw dependency string.
248 installed_db: A database of installed packages.
249 avail_db: A database of packages available for installation.
250
251 Returns:
252 A list of pairs (CP, slot).
253
254 Raises:
255 ValueError: the dependencies string is malformed.
256 """
257 def ProcessSubDeps(dep_exp, disjunct):
258 """Parses and processes a dependency (sub)expression."""
259 deps = set()
260 default_deps = set()
261 sub_disjunct = False
262 for dep_sub_exp in dep_exp:
263 sub_deps = set()
264
265 if isinstance(dep_sub_exp, (list, tuple)):
266 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
267 sub_disjunct = False
268 elif sub_disjunct:
269 raise ValueError('Malformed disjunctive operation in deps')
270 elif dep_sub_exp == '||':
271 sub_disjunct = True
272 elif dep_sub_exp.endswith('?'):
273 raise ValueError('Dependencies contain a conditional')
274 else:
275 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
276 if cp:
277 sub_deps = set([(cp, slot)])
278 elif disjunct:
279 raise ValueError('Atom in disjunct ignored')
280
281 # Handle sub-deps of a disjunctive expression.
282 if disjunct:
283 # Make the first available choice the default, for use in case that
284 # no option is installed.
285 if (not default_deps and avail_db is not None and
286 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
287 default_deps = sub_deps
288
289 # If not all sub-deps are installed, then don't consider them.
290 if not all([cls._InDB(cp, slot, installed_db)
291 for cp, slot in sub_deps]):
292 sub_deps = set()
293
294 deps.update(sub_deps)
295
296 return deps or default_deps
297
298 try:
299 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
300 except portage.exception.InvalidDependString as e:
301 raise ValueError('Invalid dep string: %s' % e)
302 except ValueError as e:
303 raise ValueError('%s: %s' % (e, dep_str))
304
305 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
306 installed_db=None):
307 """Returns a database of packages given a list of CPV info.
308
309 Args:
310 cpv_info: A list of tuples containing package CPV and attributes.
311 process_rdeps: Whether to populate forward dependencies.
312 process_rev_rdeps: Whether to populate reverse dependencies.
313 installed_db: A database of installed packages for filtering disjunctive
314 choices against; if None, using own built database.
315
316 Returns:
317 A map from CP values to another dictionary that maps slots to package
318 attribute tuples. Tuples contain a CPV value (string), build time
319 (string), runtime dependencies (set), and reverse dependencies (set,
320 empty if not populated).
321
322 Raises:
323 ValueError: If more than one CPV occupies a single slot.
324 """
325 db = {}
326 logging.debug('Populating package DB...')
327 for cpv, slot, rdeps_raw, build_time in cpv_info:
328 cp = self._GetCP(cpv)
329 cp_slots = db.setdefault(cp, dict())
330 if slot in cp_slots:
331 raise ValueError('More than one package found for %s' %
332 self._AtomStr(cp, slot))
333 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
334 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
335 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
336
337 avail_db = db
338 if installed_db is None:
339 installed_db = db
340 avail_db = None
341
342 # Add approximate forward dependencies.
343 if process_rdeps:
344 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400345 for cp, cp_slots in db.items():
346 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700347 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
348 installed_db, avail_db))
349 logging.debug(' %s (%s) processed rdeps: %s',
350 self._AtomStr(cp, slot), pkg_info.cpv,
351 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
352 for rdep_cp, rdep_slot in pkg_info.rdeps]))
353
354 # Add approximate reverse dependencies (optional).
355 if process_rev_rdeps:
356 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400357 for cp, cp_slots in db.items():
358 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700359 for rdep_cp, rdep_slot in pkg_info.rdeps:
360 to_slots = db.get(rdep_cp)
361 if not to_slots:
362 continue
363
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400364 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700365 if rdep_slot and to_slot != rdep_slot:
366 continue
367 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
368 self._AtomStr(cp, slot), pkg_info.cpv,
369 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
370 to_pkg_info.rev_rdeps.add((cp, slot))
371
372 return db
373
374 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
375 """Initializes a dictionary of packages installed on |device|."""
376 get_vartree_script = self._GetVartreeSnippet(root)
377 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400378 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700379 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700380 except cros_build_lib.RunCommandError as e:
381 logging.error('Cannot get target vartree:\n%s', e.result.error)
382 raise
383
384 try:
385 self.target_db = self._BuildDB(json.loads(result.output),
386 process_rdeps, process_rev_rdeps)
387 except ValueError as e:
388 raise self.VartreeError(str(e))
389
390 def _InitBinpkgDB(self, process_rdeps):
391 """Initializes a dictionary of binary packages for updating the target."""
392 # Get build root trees; portage indexes require a trailing '/'.
393 build_root = os.path.join(self.sysroot, '')
394 trees = portage.create_trees(target_root=build_root, config_root=build_root)
395 bintree = trees[build_root]['bintree']
396 binpkgs_info = []
397 for cpv in bintree.dbapi.cpv_all():
398 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
399 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
400 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
401
402 try:
403 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
404 installed_db=self.target_db)
405 except ValueError as e:
406 raise self.BintreeError(str(e))
407
408 def _InitDepQueue(self):
409 """Initializes the dependency work queue."""
410 self.queue = set()
411 self.seen = {}
412 self.listed = set()
413
414 def _EnqDep(self, dep, listed, optional):
415 """Enqueues a dependency if not seen before or if turned non-optional."""
416 if dep in self.seen and (optional or not self.seen[dep]):
417 return False
418
419 self.queue.add(dep)
420 self.seen[dep] = optional
421 if listed:
422 self.listed.add(dep)
423 return True
424
425 def _DeqDep(self):
426 """Dequeues and returns a dependency, its listed and optional flags.
427
428 This returns listed packages first, if any are present, to ensure that we
429 correctly mark them as such when they are first being processed.
430 """
431 if self.listed:
432 dep = self.listed.pop()
433 self.queue.remove(dep)
434 listed = True
435 else:
436 dep = self.queue.pop()
437 listed = False
438
439 return dep, listed, self.seen[dep]
440
441 def _FindPackageMatches(self, cpv_pattern):
442 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
443
444 This is breaking |cpv_pattern| into its C, P and V components, each of
445 which may or may not be present or contain wildcards. It then scans the
446 binpkgs database to find all atoms that match these components, returning a
447 list of CP and slot qualifier. When the pattern does not specify a version,
448 or when a CP has only one slot in the binpkgs database, we omit the slot
449 qualifier in the result.
450
451 Args:
452 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
453
454 Returns:
455 A list of (CPV, slot) pairs of packages in the binpkgs database that
456 match the pattern.
457 """
458 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
459 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
460 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400461 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700462 if not fnmatch.fnmatchcase(cp, cp_pattern):
463 continue
464
465 # If no version attribute was given or there's only one slot, omit the
466 # slot qualifier.
467 if not attrs.version or len(cp_slots) == 1:
468 matches.append((cp, None))
469 else:
470 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400471 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700472 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
473 matches.append((cp, slot))
474
475 return matches
476
477 def _FindPackage(self, pkg):
478 """Returns the (CP, slot) pair for a package matching |pkg|.
479
480 Args:
481 pkg: Path to a binary package or a (partial) package CPV specifier.
482
483 Returns:
484 A (CP, slot) pair for the given package; slot may be None (unspecified).
485
486 Raises:
487 ValueError: if |pkg| is not a binpkg file nor does it match something
488 that's in the bintree.
489 """
490 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
491 package = os.path.basename(os.path.splitext(pkg)[0])
492 category = os.path.basename(os.path.dirname(pkg))
493 return self._GetCP(os.path.join(category, package)), None
494
495 matches = self._FindPackageMatches(pkg)
496 if not matches:
497 raise ValueError('No package found for %s' % pkg)
498
499 idx = 0
500 if len(matches) > 1:
501 # Ask user to pick among multiple matches.
502 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
503 ['%s:%s' % (cp, slot) if slot else cp
504 for cp, slot in matches])
505
506 return matches[idx]
507
508 def _NeedsInstall(self, cpv, slot, build_time, optional):
509 """Returns whether a package needs to be installed on the target.
510
511 Args:
512 cpv: Fully qualified CPV (string) of the package.
513 slot: Slot identifier (string).
514 build_time: The BUILT_TIME value (string) of the binpkg.
515 optional: Whether package is optional on the target.
516
517 Returns:
518 A tuple (install, update) indicating whether to |install| the package and
519 whether it is an |update| to an existing package.
520
521 Raises:
522 ValueError: if slot is not provided.
523 """
524 # If not checking installed packages, always install.
525 if not self.target_db:
526 return True, False
527
528 cp = self._GetCP(cpv)
529 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
530 if target_pkg_info is not None:
531 if cpv != target_pkg_info.cpv:
532 attrs = portage_util.SplitCPV(cpv)
533 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
534 logging.debug('Updating %s: version (%s) different on target (%s)',
535 cp, attrs.version, target_attrs.version)
536 return True, True
537
538 if build_time != target_pkg_info.build_time:
539 logging.debug('Updating %s: build time (%s) different on target (%s)',
540 cpv, build_time, target_pkg_info.build_time)
541 return True, True
542
543 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
544 cp, target_pkg_info.cpv, target_pkg_info.build_time)
545 return False, False
546
547 if optional:
548 logging.debug('Not installing %s: missing on target but optional', cp)
549 return False, False
550
551 logging.debug('Installing %s: missing on target and non-optional (%s)',
552 cp, cpv)
553 return True, False
554
555 def _ProcessDeps(self, deps, reverse):
556 """Enqueues dependencies for processing.
557
558 Args:
559 deps: List of dependencies to enqueue.
560 reverse: Whether these are reverse dependencies.
561 """
562 if not deps:
563 return
564
565 logging.debug('Processing %d %s dep(s)...', len(deps),
566 'reverse' if reverse else 'forward')
567 num_already_seen = 0
568 for dep in deps:
569 if self._EnqDep(dep, False, reverse):
570 logging.debug(' Queued dep %s', dep)
571 else:
572 num_already_seen += 1
573
574 if num_already_seen:
575 logging.debug('%d dep(s) already seen', num_already_seen)
576
577 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
578 """Returns a dictionary of packages that need to be installed on the target.
579
580 Args:
581 process_rdeps: Whether to trace forward dependencies.
582 process_rev_rdeps: Whether to trace backward dependencies as well.
583
584 Returns:
585 A dictionary mapping CP values (string) to tuples containing a CPV
586 (string), a slot (string), a boolean indicating whether the package
587 was initially listed in the queue, and a boolean indicating whether this
588 is an update to an existing package.
589 """
590 installs = {}
591 while self.queue:
592 dep, listed, optional = self._DeqDep()
593 cp, required_slot = dep
594 if cp in installs:
595 logging.debug('Already updating %s', cp)
596 continue
597
598 cp_slots = self.binpkgs_db.get(cp, dict())
599 logging.debug('Checking packages matching %s%s%s...', cp,
600 ' (slot: %s)' % required_slot if required_slot else '',
601 ' (optional)' if optional else '')
602 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400603 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700604 if required_slot and slot != required_slot:
605 continue
606
607 num_processed += 1
608 logging.debug(' Checking %s...', pkg_info.cpv)
609
610 install, update = self._NeedsInstall(pkg_info.cpv, slot,
611 pkg_info.build_time, optional)
612 if not install:
613 continue
614
615 installs[cp] = (pkg_info.cpv, slot, listed, update)
616
617 # Add forward and backward runtime dependencies to queue.
618 if process_rdeps:
619 self._ProcessDeps(pkg_info.rdeps, False)
620 if process_rev_rdeps:
621 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
622 if target_pkg_info:
623 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
624
625 if num_processed == 0:
626 logging.warning('No qualified bintree package corresponding to %s', cp)
627
628 return installs
629
630 def _SortInstalls(self, installs):
631 """Returns a sorted list of packages to install.
632
633 Performs a topological sort based on dependencies found in the binary
634 package database.
635
636 Args:
637 installs: Dictionary of packages to install indexed by CP.
638
639 Returns:
640 A list of package CPVs (string).
641
642 Raises:
643 ValueError: If dependency graph contains a cycle.
644 """
645 not_visited = set(installs.keys())
646 curr_path = []
647 sorted_installs = []
648
649 def SortFrom(cp):
650 """Traverses dependencies recursively, emitting nodes in reverse order."""
651 cpv, slot, _, _ = installs[cp]
652 if cpv in curr_path:
653 raise ValueError('Dependencies contain a cycle: %s -> %s' %
654 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
655 curr_path.append(cpv)
656 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
657 if rdep_cp in not_visited:
658 not_visited.remove(rdep_cp)
659 SortFrom(rdep_cp)
660
661 sorted_installs.append(cpv)
662 curr_path.pop()
663
664 # So long as there's more packages, keep expanding dependency paths.
665 while not_visited:
666 SortFrom(not_visited.pop())
667
668 return sorted_installs
669
670 def _EnqListedPkg(self, pkg):
671 """Finds and enqueues a listed package."""
672 cp, slot = self._FindPackage(pkg)
673 if cp not in self.binpkgs_db:
674 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
675 self._EnqDep((cp, slot), True, False)
676
677 def _EnqInstalledPkgs(self):
678 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400679 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700680 target_cp_slots = self.target_db.get(cp)
681 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400682 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700683 if slot in target_cp_slots:
684 self._EnqDep((cp, slot), True, False)
685
686 def Run(self, device, root, listed_pkgs, update, process_rdeps,
687 process_rev_rdeps):
688 """Computes the list of packages that need to be installed on a target.
689
690 Args:
691 device: Target handler object.
692 root: Package installation root.
693 listed_pkgs: Package names/files listed by the user.
694 update: Whether to read the target's installed package database.
695 process_rdeps: Whether to trace forward dependencies.
696 process_rev_rdeps: Whether to trace backward dependencies as well.
697
698 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700699 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
700 list of package CPVs (string) to install on the target in an order that
701 satisfies their inter-dependencies, |listed| the subset that was
702 requested by the user, and |num_updates| the number of packages being
703 installed over preexisting versions. Note that installation order should
704 be reversed for removal, |install_attrs| is a dictionary mapping a package
705 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700706 """
707 if process_rev_rdeps and not process_rdeps:
708 raise ValueError('Must processing forward deps when processing rev deps')
709 if process_rdeps and not update:
710 raise ValueError('Must check installed packages when processing deps')
711
712 if update:
713 logging.info('Initializing target intalled packages database...')
714 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
715
716 logging.info('Initializing binary packages database...')
717 self._InitBinpkgDB(process_rdeps)
718
719 logging.info('Finding listed package(s)...')
720 self._InitDepQueue()
721 for pkg in listed_pkgs:
722 if pkg == '@installed':
723 if not update:
724 raise ValueError(
725 'Must check installed packages when updating all of them.')
726 self._EnqInstalledPkgs()
727 else:
728 self._EnqListedPkg(pkg)
729
730 logging.info('Computing set of packages to install...')
731 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
732
733 num_updates = 0
734 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400735 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700736 if listed:
737 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400738 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700739 num_updates += 1
740
741 logging.info('Processed %d package(s), %d will be installed, %d are '
742 'updating existing packages',
743 len(self.seen), len(installs), num_updates)
744
745 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700746
747 install_attrs = {}
748 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400749 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700750 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
751 install_attrs[pkg] = {}
752 if dlc_id and dlc_package:
753 install_attrs[pkg][_DLC_ID] = dlc_id
754
755 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700756
757
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700758def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700759 """Copies |pkg| to |device| and emerges it.
760
761 Args:
762 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700763 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700764 root: Package installation root path.
765 extra_args: Extra arguments to pass to emerge.
766
767 Raises:
768 DeployError: Unrecoverable error during emerge.
769 """
David Pursell9476bf42015-03-30 13:34:27 -0700770 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700771 pkg_name = os.path.basename(pkg_path)
772 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700773 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400774 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
775 # Clean out the dirs first if we had a previous emerge on the device so as to
776 # free up space for this emerge. The last emerge gets implicitly cleaned up
777 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400778 device.run(
Mike Frysinger15a4e012015-05-21 22:18:45 -0400779 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
780 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700781
Ralph Nathane01ccf12015-04-16 10:40:32 -0700782 # This message is read by BrilloDeployOperation.
783 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700784 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700785
David Pursell9476bf42015-03-30 13:34:27 -0700786 logging.info('Use portage temp dir %s', portage_tmpdir)
787
Ralph Nathane01ccf12015-04-16 10:40:32 -0700788 # This message is read by BrilloDeployOperation.
789 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700790 pkg_path = os.path.join(pkg_dir, pkg_name)
791
792 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
793 # chromeos-base packages will be skipped due to the configuration
794 # in /etc/protage/make.profile/package.provided. However, there is
795 # a known bug that /usr/local/etc/portage is not setup properly
796 # (crbug.com/312041). This does not affect `cros deploy` because
797 # we do not use the preset PKGDIR.
798 extra_env = {
799 'FEATURES': '-sandbox',
800 'PKGDIR': pkgroot,
801 'PORTAGE_CONFIGROOT': '/usr/local',
802 'PORTAGE_TMPDIR': portage_tmpdir,
803 'PORTDIR': device.work_dir,
804 'CONFIG_PROTECT': '-*',
805 }
Alex Kleinaaddc932020-01-30 15:02:24 -0700806 # --ignore-built-slot-operator-deps because we don't rebuild everything.
807 # It can cause errors, but that's expected with cros deploy since it's just a
808 # best effort to prevent developers avoid rebuilding an image every time.
809 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', pkg_path,
810 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700811 if extra_args:
812 cmd.append(extra_args)
813
Alex Kleinaaddc932020-01-30 15:02:24 -0700814 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
815 'packages built against the old version may not be able to '
816 'load the new .so. This is expected, and you will just need '
817 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700818 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400819 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
820 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800821
822 pattern = ('A requested package will not be merged because '
823 'it is listed in package.provided')
824 output = result.error.replace('\n', ' ').replace('\r', '')
825 if pattern in output:
826 error = ('Package failed to emerge: %s\n'
827 'Remove %s from /etc/portage/make.profile/'
828 'package.provided/chromeos-base.packages\n'
829 '(also see crbug.com/920140 for more context)\n'
830 % (pattern, pkg_name))
831 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700832 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700833 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700834 raise
835 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700836 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700837
838
Qijiang Fand5958192019-07-26 12:32:36 +0900839def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800840 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900841
842 This reads the tarball from pkgpath, and calls restorecon on device to
843 restore SELinux context for files listed in the tarball, assuming those files
844 are installed to /
845
846 Args:
847 device: a ChromiumOSDevice object
848 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900849 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900850 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700851 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900852 if enforced:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400853 device.run(['setenforce', '0'])
Qijiang Fan8a945032019-04-25 20:53:29 +0900854 pkgroot = os.path.join(device.work_dir, 'packages')
855 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
856 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
857 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400858 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900859 ['cd', root, '&&',
860 'tar', 'tf', pkgpath_device, '|',
861 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900862 remote_sudo=True)
863 if enforced:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400864 device.run(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900865
866
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700867def _GetPackagesByCPV(cpvs, strip, sysroot):
868 """Returns paths to binary packages corresponding to |cpvs|.
869
870 Args:
871 cpvs: List of CPV components given by portage_util.SplitCPV().
872 strip: True to run strip_package.
873 sysroot: Sysroot path.
874
875 Returns:
876 List of paths corresponding to |cpvs|.
877
878 Raises:
879 DeployError: If a package is missing.
880 """
881 packages_dir = None
882 if strip:
883 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400884 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700885 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600886 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700887 packages_dir = _STRIPPED_PACKAGES_DIR
888 except cros_build_lib.RunCommandError:
889 logging.error('Cannot strip packages %s',
890 ' '.join([str(cpv) for cpv in cpvs]))
891 raise
892
893 paths = []
894 for cpv in cpvs:
895 path = portage_util.GetBinaryPackagePath(
896 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
897 packages_dir=packages_dir)
898 if not path:
899 raise DeployError('Missing package %s.' % cpv)
900 paths.append(path)
901
902 return paths
903
904
905def _GetPackagesPaths(pkgs, strip, sysroot):
906 """Returns paths to binary |pkgs|.
907
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700908 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700909 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700910 strip: Whether or not to run strip_package for CPV packages.
911 sysroot: The sysroot path.
912
913 Returns:
914 List of paths corresponding to |pkgs|.
915 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700916 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
917 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700918
919
David Pursell9476bf42015-03-30 13:34:27 -0700920def _Unmerge(device, pkg, root):
921 """Unmerges |pkg| on |device|.
922
923 Args:
924 device: A RemoteDevice object.
925 pkg: A package name.
926 root: Package installation root path.
927 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700928 pkg_name = os.path.basename(pkg)
929 # This message is read by BrilloDeployOperation.
930 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700931 cmd = ['qmerge', '--yes']
932 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400933 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700934 cmd = ['emerge']
935
936 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
937 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700938 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400939 device.run(cmd, capture_output=False, remote_sudo=True,
940 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700941 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700942 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700943 raise
944 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700945 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700946
947
948def _ConfirmDeploy(num_updates):
949 """Returns whether we can continue deployment."""
950 if num_updates > _MAX_UPDATES_NUM:
951 logging.warning(_MAX_UPDATES_WARNING)
952 return cros_build_lib.BooleanPrompt(default=False)
953
954 return True
955
956
Andrew06a5f812020-01-23 08:08:32 -0800957def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800958 """Call _Emerge for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700959 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700960 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
961 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700962 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900963 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800964
965 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
966 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -0800967 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700968 dlc_deployed = True
Andrew67b5fa72020-02-05 14:14:48 -0800969 # Clean up empty directories created by emerging DLCs.
970 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
971 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
972 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700973
974 # Restart dlcservice so it picks up the newly installed DLC modules (in case
975 # we installed new DLC images).
976 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400977 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700978
979
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700980def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700981 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700982 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700983 for pkg in pkgs:
984 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700985 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
986 dlc_uninstalled = True
987
988 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
989 # uninstalled DLC images).
990 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400991 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700992
993
994def _UninstallDLCImage(device, pkg_attrs):
995 """Uninstall a DLC image."""
996 if _DLC_ID in pkg_attrs:
997 dlc_id = pkg_attrs[_DLC_ID]
998 logging.notice('Uninstalling DLC image for %s', dlc_id)
999
Mike Frysinger3459bf52020-03-31 00:52:11 -04001000 device.run(['sudo', '-u', 'chronos', 'dlcservice_util', '--uninstall',
1001 '--dlc_ids=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001002 return True
1003 else:
1004 logging.debug('DLC_ID not found in package')
1005 return False
1006
1007
Andrew06a5f812020-01-23 08:08:32 -08001008def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001009 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001010 # Build the DLC image if the image is outdated or doesn't exist.
Andrew06a5f812020-01-23 08:08:32 -08001011 build_dlc.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001012
Andrewc7e1c6b2020-02-27 16:03:53 -08001013 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1014 try:
1015 device.run(['dlcservice_util', '--dlc_ids=%s' % dlc_id, '--uninstall'])
1016 except cros_build_lib.RunCommandError as e:
1017 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1018 e.result.error)
1019 except Exception:
1020 logging.error('Failed to uninstall DLC.')
1021 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001022
Andrewc7e1c6b2020-02-27 16:03:53 -08001023 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1024 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1025 # location.
1026 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew67b5fa72020-02-05 14:14:48 -08001027 dlc_img_path_src = os.path.join(sysroot, build_dlc.DLC_BUILD_DIR, dlc_id,
Andrewc7e1c6b2020-02-27 16:03:53 -08001028 dlc_package, build_dlc.DLC_IMAGE)
1029 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1030 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1031 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1032 # Create directories for DLC images.
1033 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1034 # Copy images to the destination directories.
1035 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
1036 build_dlc.DLC_IMAGE),
1037 mode='rsync')
1038 device.run(['cp', os.path.join(dlc_img_path_a, build_dlc.DLC_IMAGE),
1039 os.path.join(dlc_img_path_b, build_dlc.DLC_IMAGE)])
1040
1041 # Set the proper perms and ownership so dlcservice can access the image.
1042 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1043 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001044
Andrew67b5fa72020-02-05 14:14:48 -08001045 # Copy metadata to device.
1046 dest_mata_dir = os.path.join('/', build_dlc.DLC_META_DIR, dlc_id, dlc_package)
1047 device.run(['mkdir', '-p', dest_mata_dir])
1048 src_meta_dir = os.path.join(sysroot, build_dlc.DLC_BUILD_DIR, dlc_id,
1049 dlc_package, build_dlc.DLC_TMP_META_DIR)
1050 device.CopyToDevice(src_meta_dir + '/',
1051 dest_mata_dir,
1052 mode='rsync',
1053 recursive=True,
1054 remote_sudo=True)
1055
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001056
1057def _GetDLCInfo(device, pkg_path, from_dut):
1058 """Returns information of a DLC given its package path.
1059
1060 Args:
1061 device: commandline.Device object; None to use the default device.
1062 pkg_path: path to the package.
1063 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1064 info from host.
1065
1066 Returns:
1067 A tuple (dlc_id, dlc_package).
1068 """
1069 environment_content = ''
1070 if from_dut:
1071 # On DUT, |pkg_path| is the directory which contains environment file.
1072 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysinger3459bf52020-03-31 00:52:11 -04001073 result = device.run(['test', '-f', environment_path],
1074 check=False, encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001075 if result.returncode == 1:
1076 # The package is not installed on DUT yet. Skip extracting info.
1077 return None, None
Mike Frysinger3459bf52020-03-31 00:52:11 -04001078 result = device.run(['bzip2', '-d', '-c', environment_path], encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001079 environment_content = result.output
1080 else:
1081 # On host, pkg_path is tbz2 file which contains environment file.
1082 # Extract the metadata of the package file.
1083 data = portage.xpak.tbz2(pkg_path).get_data()
1084 # Extract the environment metadata.
Woody Chowde57a322020-01-07 16:18:52 +09001085 environment_content = bz2.decompress(
Alex Klein9a1b3722020-01-28 09:59:35 -07001086 data[_ENVIRONMENT_FILENAME.encode('utf-8')])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001087
1088 with tempfile.NamedTemporaryFile() as f:
1089 # Dumps content into a file so we can use osutils.SourceEnvironment.
1090 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001091 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001092 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1093 _DLC_ENABLED))
1094
1095 dlc_enabled = content.get(_DLC_ENABLED)
1096 if dlc_enabled is not None and (dlc_enabled is False or
1097 str(dlc_enabled) == 'false'):
1098 logging.info('Installing DLC in rootfs.')
1099 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001100 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001101
1102
Gilad Arnolda0a98062015-07-07 08:34:27 -07001103def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1104 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1105 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1106 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001107 """Deploys packages to a device.
1108
1109 Args:
David Pursell2e773382015-04-03 14:30:47 -07001110 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001111 packages: List of packages (strings) to deploy to device.
1112 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001113 emerge: True to emerge package, False to unmerge.
1114 update: Check installed version on device.
1115 deep: Install dependencies also. Implies |update|.
1116 deep_rev: Install reverse dependencies. Implies |deep|.
1117 clean_binpkg: Clean outdated binary packages.
1118 root: Package installation root path.
1119 strip: Run strip_package to filter out preset paths in the package.
1120 emerge_args: Extra arguments to pass to emerge.
1121 ssh_private_key: Path to an SSH private key file; None to use test keys.
1122 ping: True to ping the device before trying to connect.
1123 force: Ignore sanity checks and prompts.
1124 dry_run: Print deployment plan but do not deploy anything.
1125
1126 Raises:
1127 ValueError: Invalid parameter or parameter combination.
1128 DeployError: Unrecoverable failure during deploy.
1129 """
1130 if deep_rev:
1131 deep = True
1132 if deep:
1133 update = True
1134
Gilad Arnolda0a98062015-07-07 08:34:27 -07001135 if not packages:
1136 raise DeployError('No packages provided, nothing to deploy.')
1137
David Pursell9476bf42015-03-30 13:34:27 -07001138 if update and not emerge:
1139 raise ValueError('Cannot update and unmerge.')
1140
David Pursell2e773382015-04-03 14:30:47 -07001141 if device:
1142 hostname, username, port = device.hostname, device.username, device.port
1143 else:
1144 hostname, username, port = None, None, None
1145
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001146 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001147 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001148 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001149 # Somewhat confusing to clobber, but here we are.
1150 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001151 with remote_access.ChromiumOSDeviceHandler(
1152 hostname, port=port, username=username, private_key=ssh_private_key,
1153 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001154 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001155
Gilad Arnolda0a98062015-07-07 08:34:27 -07001156 board = cros_build_lib.GetBoard(device_board=device.board,
1157 override_board=board)
1158 if not force and board != device.board:
1159 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001160 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001161
Gilad Arnolda0a98062015-07-07 08:34:27 -07001162 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001163
1164 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001165 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001166 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001167
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001168 # Remount rootfs as writable if necessary.
1169 if not device.MountRootfsReadWrite():
1170 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001171
1172 # Obtain list of packages to upgrade/remove.
1173 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001174 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001175 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001176 if emerge:
1177 action_str = 'emerge'
1178 else:
1179 pkgs.reverse()
1180 action_str = 'unmerge'
1181
1182 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001183 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001184 return
1185
Ralph Nathane01ccf12015-04-16 10:40:32 -07001186 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001187 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001188 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001189
1190 if dry_run or not _ConfirmDeploy(num_updates):
1191 return
1192
Ralph Nathane01ccf12015-04-16 10:40:32 -07001193 # Select function (emerge or unmerge) and bind args.
1194 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001195 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001196 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001197 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001198 func = functools.partial(_UnmergePackages, pkgs, device, root,
1199 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001200
1201 # Call the function with the progress bar or with normal output.
1202 if command.UseProgressBar():
1203 op = BrilloDeployOperation(len(pkgs), emerge)
1204 op.Run(func, log_level=logging.DEBUG)
1205 else:
1206 func()
David Pursell9476bf42015-03-30 13:34:27 -07001207
Ben Pastene5f03b052019-08-12 18:03:24 -07001208 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001209 if sum(x.count('selinux-policy') for x in pkgs):
1210 logging.warning(
1211 'Deploying SELinux policy will not take effect until reboot. '
1212 'SELinux policy is loaded by init. Also, security contexts '
1213 '(labels) in files will require manual relabeling by the user '
1214 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001215
David Pursell9476bf42015-03-30 13:34:27 -07001216 logging.warning('Please restart any updated services on the device, '
1217 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001218 except Exception:
1219 if lsb_release:
1220 lsb_entries = sorted(lsb_release.items())
1221 logging.info('Following are the LSB version details of the device:\n%s',
1222 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1223 raise