def UnmountPath(path: str): """Unmount the specified path. Args: path: The path being unmounted. """ logging.info('Unmounting path %s', path) try: osutils.UmountTree(path) except cros_build_lib.RunCommandError as e: fs_debug = cros_sdk_lib.GetFileSystemDebug(path, run_ps=True) raise UnmountError(path, e, fs_debug)
def testUnmountTree(self): with osutils.TempDir(prefix='chromite.test.osutils') as tempdir: # Mount the dir and verify it worked. st_before = os.stat(tempdir) osutils.MountTmpfsDir(tempdir) st_after = os.stat(tempdir) self.assertNotEqual(st_before.st_dev, st_after.st_dev) # Mount an inner dir the same way. tempdir2 = os.path.join(tempdir, 'inner') osutils.SafeMakedirsNonRoot(tempdir2) st_before2 = os.stat(tempdir2) osutils.MountTmpfsDir(tempdir2) st_after2 = os.stat(tempdir2) self.assertNotEqual(st_before2.st_dev, st_after2.st_dev) # Unmount the whole tree and verify it worked. osutils.UmountTree(tempdir) st_umount = os.stat(tempdir) self.assertNotExists(tempdir2) self.assertEqual(st_before.st_dev, st_umount.st_dev)
def CleanupChrootMount(chroot=None, buildroot=None, delete_image=False, proc_mounts='/proc/mounts'): """Unmounts a chroot and cleans up attached devices. This function attempts to perform all of the cleanup steps even if the chroot directory and/or image isn't present. This ensures that a partially destroyed chroot can still be cleaned up. This function does not remove the actual chroot directory (or its content for non-loopback chroots). Args: chroot: Full path to the chroot to examine, or None to find it relative to |buildroot|. buildroot: Ignored if |chroot| is set. If |chroot| is None, find the chroot relative to |buildroot|. delete_image: Also delete the .img file after cleaning up. If |delete_image| is False, the chroot contents will still be present and can be immediately re-mounted without recreating a fresh chroot. proc_mounts: The path to a list of mounts to read (intended for testing). """ if chroot is None and buildroot is None: raise ValueError('need either |chroot| or |buildroot| to search') if chroot is None: chroot = os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR) chroot_img = chroot + '.img' # Try to find the VG that might already be mounted on the chroot before we # unmount it. vg_name, _ = FindChrootMountSource(chroot, proc_mounts=proc_mounts) osutils.UmountTree(chroot) # Find the loopback device by either matching the VG or the image. chroot_dev = None if vg_name: cmd = [ 'vgs', '-q', '--noheadings', '-o', 'pv_name', '--unbuffered', vg_name ] result = cros_build_lib.SudoRunCommand(cmd, capture_output=True, error_code_ok=True, print_cmd=False) if result.returncode == 0: chroot_dev = result.output.strip() else: vg_name = None if not chroot_dev: chroot_dev = _DeviceFromFile(chroot_img) # If we didn't find a mounted VG before but we did find a loopback device, # re-check for a VG attached to the loopback. if not vg_name: vg_name = FindVolumeGroupForDevice(chroot, chroot_dev) if vg_name: cmd = ['vgs', vg_name] result = cros_build_lib.SudoRunCommand(cmd, capture_output=True, error_code_ok=True, print_cmd=False) if result.returncode != 0: vg_name = None # Clean up all the pieces we found above. if vg_name: cmd = ['vgchange', '-an', vg_name] cros_build_lib.SudoRunCommand(cmd, capture_output=True, print_cmd=False) if chroot_dev: cmd = ['losetup', '-d', chroot_dev] cros_build_lib.SudoRunCommand(cmd, capture_output=True, print_cmd=False) if delete_image: osutils.SafeUnlink(chroot_img) if chroot_dev: # Force a rescan after everything is gone to make sure lvmetad is updated. _RescanDeviceLvmMetadata(chroot_dev)
def PerformStage(self): if (not (self._run.options.buildbot or self._run.options.remote_trybot) and self._run.options.clobber): if not commands.ValidateClobber(self._build_root): cros_build_lib.Die('--clobber in local mode must be approved.') # If we can't get a manifest out of it, then it's not usable and must be # clobbered. manifest = None delete_chroot = False if not self._run.options.clobber: try: manifest = git.ManifestCheckout.Cached(self._build_root, search=False) except (KeyboardInterrupt, MemoryError, SystemExit): raise except Exception as e: # Either there is no repo there, or the manifest isn't usable. If the # directory exists, log the exception for debugging reasons. Either # way, the checkout needs to be wiped since it's in an unknown # state. if os.path.exists(self._build_root): logging.warning('ManifestCheckout at %s is unusable: %s', self._build_root, e) delete_chroot = True # Clean mount points first to be safe about deleting. chroot_path = os.path.join(self._build_root, constants.DEFAULT_CHROOT_DIR) cros_sdk_lib.CleanupChrootMount(chroot=chroot_path) osutils.UmountTree(self._build_root) if not delete_chroot: delete_chroot = not self.CanReuseChroot(chroot_path) # If we're going to delete the chroot and we can use a snapshot instead, # try to revert. If the revert succeeds, we don't need to delete after all. if delete_chroot and self.CanUseChrootSnapshotToDelete(chroot_path): delete_chroot = not self._RevertChrootToCleanSnapshot() # Re-mount chroot image if it exists so that subsequent steps can clean up # inside. if not delete_chroot and self._run.config.chroot_use_image: try: cros_sdk_lib.MountChroot(chroot=chroot_path, create=False) except cros_build_lib.RunCommandError as e: logging.error( 'Unable to mount chroot under %s. Deleting chroot. ' 'Error: %s', self._build_root, e) delete_chroot = True if manifest is None: self._DeleteChroot() repository.ClearBuildRoot(self._build_root, self._run.options.preserve_paths) else: tasks = [ self._BuildRootGitCleanup, self._WipeOldOutput, self._DeleteArchivedTrybotImages, self._DeleteArchivedPerfResults, self._DeleteAutotestSitePackages ] if self._run.options.chrome_root: tasks.append(self._DeleteChromeBuildOutput) if delete_chroot: tasks.append(self._DeleteChroot) else: tasks.append(self._CleanChroot) if self._run.options.workspace: tasks.append(self._CleanWorkspace) # CancelObsoleteSlaveBuilds, if there are slave builds to cancel. if self._run.config.slave_configs: tasks.append(self.CancelObsoleteSlaveBuilds) parallel.RunParallelSteps(tasks) # If chroot.img still exists after everything is cleaned up, it means we're # planning to reuse it. This chroot was created by the previous run, so its # creation isn't affected by any potential changes in the current run. # Therefore, if this run fails, having the subsequent run revert to this # snapshot will still produce a clean chroot. If this run succeeds, the # next run will reuse the chroot without needing to revert it. Thus, taking # a snapshot now should be correct regardless of whether this run will # ultimately succeed or not. if os.path.exists(chroot_path + '.img'): self._CreateCleanSnapshot()
def CleanupChrootMount(chroot=None, buildroot=None, delete=False, proc_mounts='/proc/mounts'): """Unmounts a chroot and cleans up attached devices. This function attempts to perform all of the cleanup steps even if the chroot directory and/or image isn't present. This ensures that a partially destroyed chroot can still be cleaned up. This function does not remove the actual chroot directory (or its content for non-loopback chroots). Args: chroot: Full path to the chroot to examine, or None to find it relative to |buildroot|. buildroot: Ignored if |chroot| is set. If |chroot| is None, find the chroot relative to |buildroot|. delete: Delete chroot contents and the .img file after cleaning up. If |delete| is False, the chroot contents will still be present and can be immediately re-mounted without recreating a fresh chroot. proc_mounts: The path to a list of mounts to read (intended for testing). """ if chroot is None and buildroot is None: raise ValueError('need either |chroot| or |buildroot| to search') if chroot is None: chroot = os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR) chroot_img = chroot + '.img' # Try to find the VG that might already be mounted on the chroot before we # unmount it. vg_name, _ = FindChrootMountSource(chroot, proc_mounts=proc_mounts) try: osutils.UmountTree(chroot) except cros_build_lib.RunCommandError as e: # TODO(lamontjones): Dump some information to help find the process still # inside the chroot, causing crbug.com/923432. In the end, this is likely # to become fuser -k. fs_debug = GetFileSystemDebug(chroot, run_ps=True) raise Error( 'Umount failed: %s.\nfuser output=%s\nlsof output=%s\nps output=%s\n' % (e.result.error, fs_debug.fuser, fs_debug.lsof, fs_debug.ps)) # Find the loopback device by either matching the VG or the image. chroot_dev = None if vg_name: cmd = [ 'vgs', '-q', '--noheadings', '-o', 'pv_name', '--unbuffered', vg_name ] result = cros_build_lib.sudo_run(cmd, capture_output=True, check=False, print_cmd=False, encoding='utf-8') if result.returncode == 0: chroot_dev = result.output.strip() else: vg_name = None if not chroot_dev: chroot_dev = _DeviceFromFile(chroot_img) # If we didn't find a mounted VG before but we did find a loopback device, # re-check for a VG attached to the loopback. if not vg_name: vg_name = FindVolumeGroupForDevice(chroot, chroot_dev) if vg_name: cmd = ['vgs', vg_name] result = cros_build_lib.sudo_run(cmd, capture_output=True, check=False, print_cmd=False, encoding='utf-8') if result.returncode != 0: vg_name = None # Clean up all the pieces we found above. if vg_name: cmd = ['vgchange', '-an', vg_name] cros_build_lib.sudo_run(cmd, capture_output=True, print_cmd=False) if chroot_dev: cmd = ['losetup', '-d', chroot_dev] cros_build_lib.sudo_run(cmd, capture_output=True, print_cmd=False) if delete: osutils.SafeUnlink(chroot_img) osutils.RmDir(chroot, ignore_missing=True, sudo=True) if chroot_dev: # Force a rescan after everything is gone to make sure lvmetad is updated. _RescanDeviceLvmMetadata(chroot_dev)
def main(argv): # Turn on strict sudo checks. cros_build_lib.STRICT_SUDO = True conf = key_value_store.LoadFile(os.path.join(constants.SOURCE_ROOT, constants.SDK_VERSION_FILE), ignore_missing=True) sdk_latest_version = conf.get('SDK_LATEST_VERSION', '<unknown>') bootstrap_frozen_version = conf.get('BOOTSTRAP_FROZEN_VERSION', '<unknown>') # Use latest SDK for bootstrapping if requested. Use a frozen version of SDK # for bootstrapping if BOOTSTRAP_FROZEN_VERSION is set. bootstrap_latest_version = (sdk_latest_version if bootstrap_frozen_version == '<unknown>' else bootstrap_frozen_version) parser, commands = _CreateParser(sdk_latest_version, bootstrap_latest_version) options = parser.parse_args(argv) chroot_command = options.commands # Some sanity checks first, before we ask for sudo credentials. cros_build_lib.AssertOutsideChroot() host = os.uname()[4] if host != 'x86_64': cros_build_lib.Die( "cros_sdk is currently only supported on x86_64; you're running" ' %s. Please find a x86_64 machine.' % (host, )) # Merge the outside PATH setting if we re-execed ourselves. if 'CHROMEOS_SUDO_PATH' in os.environ: os.environ['PATH'] = '%s:%s' % (os.environ.pop('CHROMEOS_SUDO_PATH'), os.environ['PATH']) _ReportMissing(osutils.FindMissingBinaries(NEEDED_TOOLS)) if options.proxy_sim: _ReportMissing(osutils.FindMissingBinaries(PROXY_NEEDED_TOOLS)) missing_image_tools = osutils.FindMissingBinaries(IMAGE_NEEDED_TOOLS) if (sdk_latest_version == '<unknown>' or bootstrap_latest_version == '<unknown>'): cros_build_lib.Die( 'No SDK version was found. ' 'Are you in a Chromium source tree instead of Chromium OS?\n\n' 'Please change to a directory inside your Chromium OS source tree\n' 'and retry. If you need to setup a Chromium OS source tree, see\n' ' https://dev.chromium.org/chromium-os/developer-guide') any_snapshot_operation = (options.snapshot_create or options.snapshot_restore or options.snapshot_delete or options.snapshot_list) if any_snapshot_operation and not options.use_image: cros_build_lib.Die('Snapshot operations are not compatible with ' '--nouse-image.') if (options.snapshot_delete and options.snapshot_delete == options.snapshot_restore): parser.error('Cannot --snapshot_delete the same snapshot you are ' 'restoring with --snapshot_restore.') _ReExecuteIfNeeded([sys.argv[0]] + argv) lock_path = os.path.dirname(options.chroot) lock_path = os.path.join( lock_path, '.%s_lock' % os.path.basename(options.chroot).lstrip('.')) # Expand out the aliases... if options.replace: options.delete = options.create = True if options.bootstrap: options.create = True # If a command is not given, default to enter. # pylint: disable=protected-access # This _group_actions access sucks, but upstream decided to not include an # alternative to optparse's option_list, and this is what they recommend. options.enter |= not any( getattr(options, x.dest) for x in commands._group_actions) # pylint: enable=protected-access options.enter |= bool(chroot_command) if (options.delete and not options.create and (options.enter or any_snapshot_operation)): parser.error('Trying to enter or snapshot the chroot when --delete ' 'was specified makes no sense.') if (options.unmount and (options.create or options.enter or any_snapshot_operation)): parser.error( '--unmount cannot be specified with other chroot actions.') if options.working_dir is not None and not os.path.isabs( options.working_dir): options.working_dir = path_util.ToChrootPath(options.working_dir) # Discern if we need to create the chroot. chroot_exists = cros_sdk_lib.IsChrootReady(options.chroot) if (options.use_image and not chroot_exists and not options.delete and not options.unmount and not missing_image_tools and os.path.exists(_ImageFileForChroot(options.chroot))): # Try to re-mount an existing image in case the user has rebooted. with cgroups.SimpleContainChildren('cros_sdk'): with locking.FileLock(lock_path, 'chroot lock') as lock: logging.debug( 'Checking if existing chroot image can be mounted.') lock.write_lock() cros_sdk_lib.MountChroot(options.chroot, create=False) chroot_exists = cros_sdk_lib.IsChrootReady(options.chroot) if chroot_exists: logging.notice('Mounted existing image %s on chroot', _ImageFileForChroot(options.chroot)) # Finally, flip create if necessary. if options.enter or options.snapshot_create: options.create |= not chroot_exists # Make sure we will download if we plan to create. options.download |= options.create # Anything that needs to manipulate the main chroot mount or communicate with # LVM needs to be done here before we enter the new namespaces. # If deleting, do it regardless of the use_image flag so that a # previously-created loopback chroot can also be cleaned up. # TODO(bmgordon): See if the DeleteChroot call below can be removed in # favor of this block. chroot_deleted = False if options.delete: with cgroups.SimpleContainChildren('cros_sdk'): # Set a timeout of 300 seconds when getting the lock. with locking.FileLock(lock_path, 'chroot lock', blocking_timeout=300) as lock: try: lock.write_lock() except timeout_util.TimeoutError as e: logging.error('Acquiring write_lock on %s failed: %s', lock_path, e) if not options.force: cros_build_lib.Die( 'Exiting; use --force to continue w/o lock.') else: logging.warning( 'cros_sdk was invoked with force option, continuing.' ) if missing_image_tools: logging.notice('Unmounting chroot.') osutils.UmountTree(options.chroot) else: logging.notice('Deleting chroot.') cros_sdk_lib.CleanupChrootMount(options.chroot, delete=True) chroot_deleted = True # If cleanup was requested, we have to do it while we're still in the original # namespace. Since cleaning up the mount will interfere with any other # commands, we exit here. The check above should have made sure that no other # action was requested, anyway. if options.unmount: # Set a timeout of 300 seconds when getting the lock. with locking.FileLock(lock_path, 'chroot lock', blocking_timeout=300) as lock: try: lock.write_lock() except timeout_util.TimeoutError as e: logging.error('Acquiring write_lock on %s failed: %s', lock_path, e) logging.warning( 'Continuing with CleanupChroot(%s), which will umount the tree.', options.chroot) # We can call CleanupChroot (which calls cros_sdk_lib.CleanupChrootMount) # even if we don't get the lock because it will attempt to unmount the # tree and will print diagnostic information from 'fuser', 'lsof', and # 'ps'. CleanupChroot(options.chroot) sys.exit(0) # Make sure the main chroot mount is visible. Contents will be filled in # below if needed. if options.create and options.use_image: if missing_image_tools: raise SystemExit("""The tool(s) %s were not found. Please make sure the lvm2 and thin-provisioning-tools packages are installed on your host. Example(ubuntu): sudo apt-get install lvm2 thin-provisioning-tools If you want to run without lvm2, pass --nouse-image (chroot snapshots will be unavailable).""" % ', '.join(missing_image_tools)) logging.debug('Making sure chroot image is mounted.') with cgroups.SimpleContainChildren('cros_sdk'): with locking.FileLock(lock_path, 'chroot lock') as lock: lock.write_lock() if not cros_sdk_lib.MountChroot(options.chroot, create=True): cros_build_lib.Die('Unable to mount %s on chroot', _ImageFileForChroot(options.chroot)) logging.notice('Mounted %s on chroot', _ImageFileForChroot(options.chroot)) # Snapshot operations will always need the VG/LV, but other actions won't. if any_snapshot_operation: with cgroups.SimpleContainChildren('cros_sdk'): with locking.FileLock(lock_path, 'chroot lock') as lock: chroot_vg, chroot_lv = cros_sdk_lib.FindChrootMountSource( options.chroot) if not chroot_vg or not chroot_lv: cros_build_lib.Die('Unable to find VG/LV for chroot %s', options.chroot) # Delete snapshot before creating a new one. This allows the user to # throw out old state, create a new snapshot, and enter the chroot in a # single call to cros_sdk. Since restore involves deleting, also do it # before creating. if options.snapshot_restore: lock.write_lock() valid_snapshots = ListChrootSnapshots(chroot_vg, chroot_lv) if options.snapshot_restore not in valid_snapshots: cros_build_lib.Die( '%s is not a valid snapshot to restore to. ' 'Valid snapshots: %s', options.snapshot_restore, ', '.join(valid_snapshots)) osutils.UmountTree(options.chroot) if not RestoreChrootSnapshot(options.snapshot_restore, chroot_vg, chroot_lv): cros_build_lib.Die( 'Unable to restore chroot to snapshot.') if not cros_sdk_lib.MountChroot(options.chroot, create=False): cros_build_lib.Die( 'Unable to mount restored snapshot onto chroot.') # Use a read lock for snapshot delete and create even though they modify # the filesystem, because they don't modify the mounted chroot itself. # The underlying LVM commands take their own locks, so conflicting # concurrent operations here may crash cros_sdk, but won't corrupt the # chroot image. This tradeoff seems worth it to allow snapshot # operations on chroots that have a process inside. if options.snapshot_delete: lock.read_lock() DeleteChrootSnapshot(options.snapshot_delete, chroot_vg, chroot_lv) if options.snapshot_create: lock.read_lock() if not CreateChrootSnapshot(options.snapshot_create, chroot_vg, chroot_lv): cros_build_lib.Die('Unable to create snapshot.') img_path = _ImageFileForChroot(options.chroot) if (options.use_image and os.path.exists(options.chroot) and os.path.exists(img_path)): img_stat = os.stat(img_path) img_used_bytes = img_stat.st_blocks * 512 mount_stat = os.statvfs(options.chroot) mount_used_bytes = mount_stat.f_frsize * (mount_stat.f_blocks - mount_stat.f_bfree) extra_gbs = (img_used_bytes - mount_used_bytes) // 2**30 if extra_gbs > MAX_UNUSED_IMAGE_GBS: logging.notice( '%s is using %s GiB more than needed. Running ' 'fstrim.', img_path, extra_gbs) cmd = ['fstrim', options.chroot] try: cros_build_lib.dbg_run(cmd) except cros_build_lib.RunCommandError as e: logging.warning( 'Running fstrim failed. Consider running fstrim on ' 'your chroot manually.\n%s', e) # Enter a new set of namespaces. Everything after here cannot directly affect # the hosts's mounts or alter LVM volumes. namespaces.SimpleUnshare() if options.ns_pid: first_pid = namespaces.CreatePidNs() else: first_pid = None if options.snapshot_list: for snap in ListChrootSnapshots(chroot_vg, chroot_lv): print(snap) sys.exit(0) if not options.sdk_version: sdk_version = (bootstrap_latest_version if options.bootstrap else sdk_latest_version) else: sdk_version = options.sdk_version if options.buildbot_log_version: logging.PrintBuildbotStepText(sdk_version) # Based on selections, determine the tarball to fetch. if options.download: if options.sdk_url: urls = [options.sdk_url] else: urls = GetArchStageTarballs(sdk_version) with cgroups.SimpleContainChildren('cros_sdk', pid=first_pid): with locking.FileLock(lock_path, 'chroot lock') as lock: if options.proxy_sim: _ProxySimSetup(options) if (options.delete and not chroot_deleted and (os.path.exists(options.chroot) or os.path.exists(_ImageFileForChroot(options.chroot)))): lock.write_lock() DeleteChroot(options.chroot) sdk_cache = os.path.join(options.cache_dir, 'sdks') distfiles_cache = os.path.join(options.cache_dir, 'distfiles') osutils.SafeMakedirsNonRoot(options.cache_dir) for target in (sdk_cache, distfiles_cache): src = os.path.join(constants.SOURCE_ROOT, os.path.basename(target)) if not os.path.exists(src): osutils.SafeMakedirsNonRoot(target) continue lock.write_lock( 'Upgrade to %r needed but chroot is locked; please exit ' 'all instances so this upgrade can finish.' % src) if not os.path.exists(src): # Note that while waiting for the write lock, src may've vanished; # it's a rare race during the upgrade process that's a byproduct # of us avoiding taking a write lock to do the src check. If we # took a write lock for that check, it would effectively limit # all cros_sdk for a chroot to a single instance. osutils.SafeMakedirsNonRoot(target) elif not os.path.exists(target): # Upgrade occurred, but a reversion, or something whacky # occurred writing to the old location. Wipe and continue. os.rename(src, target) else: # Upgrade occurred once already, but either a reversion or # some before/after separate cros_sdk usage is at play. # Wipe and continue. osutils.RmDir(src) if options.download: lock.write_lock() sdk_tarball = FetchRemoteTarballs( sdk_cache, urls, 'stage3' if options.bootstrap else 'SDK') if options.create: lock.write_lock() # Recheck if the chroot is set up here before creating to make sure we # account for whatever the various delete/unmount/remount steps above # have done. if cros_sdk_lib.IsChrootReady(options.chroot): logging.debug('Chroot already exists. Skipping creation.') else: CreateChroot(options.chroot, sdk_tarball, options.cache_dir, nousepkg=(options.bootstrap or options.nousepkg)) if options.enter: lock.read_lock() EnterChroot(options.chroot, options.cache_dir, options.chrome_root, options.chrome_root_mount, options.goma_dir, options.goma_client_json, options.working_dir, chroot_command)