# -*- coding: utf-8 -*- # vim: noai:ts=4:sw=4:expandtab import grp import os import os.path from contextlib import contextmanager from . import file_util from . import exception from . import util from .trace_decorator import traceLog class MountPoint(object): '''base class for mounts''' @traceLog() def __init__(self, mountsource, mountpath): self.mountpath = mountpath self.mountsource = mountsource self.mounted = None self._is_chroot = False def treat_as_chroot(self): """ If we use this directory as chroot, we might want to do special actions while mounting and unmounting. """ self._is_chroot = True return self @traceLog() # pylint: disable=unused-argument def umount(self, force=False, nowarn=False): """ Return None if not mounted, and True if successfully umounted """ if not self.mounted: return None if self._is_chroot: # Don't keep background processes running with unmounted # /proc/self/root directory. util.orphansKill(self.mountpath) if self._do_umount(): self.mounted = False return True return False def mount(self): """ Mount the mountpoint. Return True if successful, False if not successful and None if already mounted. """ raise NotImplementedError @contextmanager def having_mounted(self): """ Mount this mountpoint and then automatically umount """ umount = self.mount() try: yield finally: if umount: self.umount() def _do_umount(self): raise NotImplementedError @traceLog() def ismounted(self): with open('/proc/mounts') as f: if self.mountpath.rstrip('/') in [x.split()[1] for x in f]: return True return False def __repr__(self): return "".format( self.mountsource, self.mountpath) class FileSystemMountPoint(MountPoint): '''class for managing filesystem mounts in the chroot''' @traceLog() def __init__(self, path, filetype=None, device=None, options=None): if not path: raise RuntimeError("no path specified for mountpoint") if not filetype: raise RuntimeError("no filetype specified for mountpoint") if filetype in ('pts', 'proc', 'sys', 'sysfs', 'tmpfs', 'devpts'): device = filetype if not device: raise RuntimeError("no device file specified for mountpoint") MountPoint.__init__(self, mountsource=device, mountpath=path) self.device = device self.path = path self.filetype = filetype self.options = options self.mounted = self.ismounted() @traceLog() def mount(self): if self.mounted: return None file_util.mkdirIfAbsent(self.path) cmd = ['/bin/mount', '-n', '-t', self.filetype] if self.options: cmd += ['-o', self.options] cmd += [self.device, self.path] util.do(cmd) self.mounted = True return True def _do_umount(self): cmd = ['/bin/umount', '-n', '-l', self.path] try: util.do(cmd) except exception.Error: return False return True def __repr__(self): return ("".format( self.device, self.path, self.filetype, self.options, self.mounted)) class BindMountPoint(MountPoint): '''class for managing bind-mounts in the chroot''' @traceLog() def __init__(self, srcpath, bindpath, recursive=False, options=None): MountPoint.__init__(self, mountsource=srcpath, mountpath=bindpath) self.srcpath = srcpath self.bindpath = bindpath self.recursive = recursive self.options = options self.mounted = self.ismounted() @traceLog() def mount(self): if self.mounted: return None if os.path.isdir(self.srcpath): file_util.mkdirIfAbsent(self.bindpath) elif not os.path.exists(self.bindpath): normbindpath = os.path.normpath(self.bindpath) file_util.mkdirIfAbsent(os.path.dirname(normbindpath)) file_util.touch(self.bindpath) bind_option = 'rbind' if self.recursive else 'bind' util.do(['/bin/mount', '-n', '-o', bind_option, self.srcpath, self.bindpath]) self.mounted = True # Remount the new bind-mount to set specified options (rhbz#1584443). # Userspace must implement this as separate system calls anyway. if self.options: options = ','.join(['remount', self.options, bind_option]) util.do(['/bin/mount', '-n', '-o', options, "--target", self.bindpath]) return True @traceLog() def _do_umount(self): cmd = ['/bin/umount', '-n'] if self.recursive: # The mount is busy because of the submounts - a lazy unmount # implies a recursive unmount, so takes care of that. # (-R also works, but is implemented in userspace, and thus racy) cmd += ['-l'] cmd.append(self.bindpath) try: util.do(cmd) except exception.Error: return False return True def __repr__(self): return "".format( self.srcpath, self.bindpath, self.mounted) class Mounts(object): '''class to manage all mountpoints''' @traceLog() def __init__(self, rootObj): self.rootObj = rootObj self.essential_mounts = [] # /proc, /sys ... normally managed by systemd self.managed_mounts = [] # mounts owned by mock self.user_mounts = [] # mounts injected by user self.bootstrap_mounts = [] # Instead of mounting a fresh procfs and sysfs, we bind mount /proc # and /sys. This avoids problems with kernel restrictions if running # within a user namespace, and is pretty much identical otherwise. # The bind mounts additionally need to be recursive, because the # kernel forbids mounts that might reveal parts of the filesystem # that a container runtime overmounted to hide from the container # (rhbz#1745048). for mount in ['proc', 'sys']: mount_point = "/" + mount device = 'mock_hide_{}fs_from_host'.format(mount) host_path = rootObj.make_chroot_path(mount_point) self.essential_mounts += [ # The recursive mount point needs to be later lazy umounted and # it would affect hosts's counterpart sub-mounts as well. To # avoid this, we need to make the mount point and parent mount # point private in unshare()d namespace. But since the parent # mount point of /sys and /proc so far was plain '/' mount (and # we need to keep that one shared, to keep LVM/tmpfs features # working) we crate a new parent mount for the final mountpoint # on the same path. So the mount graph looks like: # / (shared) -> /sys (private) -> /sys (recursive, private) # # Acknowledgement, IOW: We mount on host_path twice and it is # expected. This is because when you umount 'rprivate' mount # then parent mount point is notified .. so first we mount tmpfs # stub which we actually never use -- but is private -- and only # then we mount above the actual mount point. This prevents # from umount events to propagate to host from chroot. FileSystemMountPoint(filetype='tmpfs', device=device, path=host_path, options="rprivate,mode=0755"), BindMountPoint(srcpath=mount_point, bindpath=host_path, recursive=True, options="nodev,noexec,nosuid,readonly,rprivate"), ] if rootObj.config['internal_dev_setup']: self.essential_mounts.append( FileSystemMountPoint( filetype='tmpfs', device='mock_chroot_shmfs', path=rootObj.make_chroot_path('/dev/shm') ) ) opts = 'gid=%d,mode=0620,ptmxmode=0666' % grp.getgrnam('tty').gr_gid if util.cmpKernelVer(os.uname()[2], '2.6.29') >= 0: opts += ',newinstance' self.essential_mounts.append( FileSystemMountPoint( filetype='devpts', device='mock_chroot_devpts', path=rootObj.make_chroot_path('/dev/pts'), options=opts ) ) self._essential_mounted = all(m.ismounted() for m in self.essential_mounts) @traceLog() def add(self, mount): self.managed_mounts.append(mount) @traceLog() def add_device_bindmount(self, path): mount = BindMountPoint(path, self.rootObj.make_chroot_path(path), options="noexec,nosuid,readonly") self.essential_mounts.append(mount) @traceLog() def add_user_mount(self, mount): self.user_mounts.append(mount) @traceLog() def mountall_essential(self): self._essential_mounted = True for m in self.essential_mounts: m.mount() @traceLog() def _mount_bootstrap(self): with self.rootObj.uid_manager.elevated_privileges(): for m in self.bootstrap_mounts: m.mount() @traceLog() def _umount_bootstrap(self): # Kill leftover processes in the bind-mountpoint, typically these # processes can be started by DNF/RPM via buggy scriptlets. util.orphansKill(self.rootObj.make_chroot_path(), manual_forced=True) with self.rootObj.uid_manager.elevated_privileges(): for m in reversed(self.bootstrap_mounts): m.umount() @contextmanager def essential_mounted(self, noop=False): """ Convenience wrapper around commands that need essential mountpoints mounted. """ do_umount = False try: if not noop and not self._essential_mounted: do_umount = True self.mountall_essential() yield finally: if do_umount: self.umountall_essential() @contextmanager def buildroot_in_bootstrap_mounted(self): """ Context manager! Mount the chroot into bootstrap recursively, execute the command within the context, and (lazy) umount it. """ self._mount_bootstrap() try: yield finally: self._umount_bootstrap() @traceLog() def mountall_managed(self): if not util.USE_NSPAWN: self.mountall_essential() for m in self.managed_mounts: m.mount() @traceLog() def mountall_user(self): for m in self.user_mounts: m.mount() @traceLog() # pylint: disable=unused-argument def umountall(self, force=False, nowarn=False): failed_old = 1 failed_new = 0 while (failed_new != failed_old): # there can be deps, we will try to umount everything several times # as long as in every loop at least one umount succeed. failed_old = failed_new failed_new = 0 for m in reversed(self.managed_mounts + self.user_mounts): if m.umount() is False: failed_new += 1 if self._essential_mounted: self.umountall_essential() @traceLog() def umountall_essential(self): for m in reversed(self.essential_mounts): m.umount() self._essential_mounted = False @traceLog() def get_mountpoints(self): # including essentials (no matter if we use nspawn) # this is used to exclude path in archiving etc. and we want to do that for essentials too return [m.mountpath for m in self.essential_mounts + self.managed_mounts + self.user_mounts] def __repr__(self): return "".format(repr(self.managed_mounts), repr(self.user_mounts))