예제 #1
0
def _mount_image_file(
    unshare: Optional[Unshare], file_path: bytes, mount_path: bytes,
) -> bytes:
    log.info(f'Mounting btrfs {file_path} at {mount_path}')
    # Explicitly set filesystem type to detect shenanigans.
    run_stdout_to_err(nsenter_as_root(
        unshare, 'mount', '-t', 'btrfs', '-o', 'loop,discard,nobarrier',
        file_path, mount_path,
    ), check=True)
    loop_dev = subprocess.check_output(nsenter_as_user(
        unshare, 'findmnt', '--noheadings', '--output', 'SOURCE',
        mount_path,
    )).rstrip(b'\n')
    # This increases the chances that --direct-io=on will succeed, since one
    # of the common failure modes is that the loopback's sector size is NOT
    # a multiple of the sector size of the underlying device (the devices
    # we've seen in production have sector sizes of 512, 1024, or 4096).
    if run_stdout_to_err([
        'sudo', 'losetup', '--sector-size=4096', loop_dev,
    ]).returncode != 0:
        log.error(
            f'Failed to set --sector-size=4096 for {loop_dev}, setting '
            'direct IO is more likely to fail.'
        )
    # This helps perf and avoids doubling our usage of buffer cache.
    # Also, when the image is on tmpfs, setting direct IO fails.
    if run_stdout_to_err([
        'sudo', 'losetup', '--direct-io=on', loop_dev,
    ]).returncode != 0:
        log.error(
            f'Could not enable --direct-io for {loop_dev}, expect worse '
            'performance.'
        )
    return loop_dev
예제 #2
0
def _minimize_image_size(
    *, unshare: Optional[Unshare], cur_size: int, image_path: bytes,
    mount_path: bytes, loop_dev: bytes,
) -> int:
    'Returns the new filesystem size.'
    min_size_out = subprocess.check_output(nsenter_as_root(
        unshare, 'btrfs', 'inspect-internal', 'min-dev-size', mount_path,
    )).split(b' ')
    assert min_size_out[1] == b'bytes'
    min_size = _fix_up_fs_size(int(min_size_out[0]), MIN_SHRINK_BYTES)
    if min_size >= cur_size:
        log.info(
            f'Nothing to do: the minimum resize limit {min_size} is no less '
            f'than the current filesystem size of {cur_size} bytes.'
        )
        return
    log.info(f'Shrinking {image_path} to the btrfs minimum, {min_size} bytes')
    run_stdout_to_err(nsenter_as_root(
        unshare, 'btrfs', 'filesystem', 'resize', str(min_size),
        mount_path,
    ), check=True)
    fs_bytes = int(subprocess.check_output(nsenter_as_user(
        unshare, 'findmnt', '--bytes', '--noheadings', '--output', 'SIZE',
        mount_path,
    )))
    # Log an error on size rounding since this is not expected to need it.
    _create_or_resize_image_file(image_path, fs_bytes, log_level=logging.ERROR)
    run_stdout_to_err([
        'sudo', 'losetup', '--set-capacity', loop_dev,
    ], check=True)
    return min_size
예제 #3
0
 def check_mnt_dest(mnt_dest: str):
     cypa = os.path.join(mnt_dest, 'cypa')
     # The outer NS cannot see the mount
     self.assertFalse(os.path.exists(cypa))
     # But we can read it from inside the namespace
     self.assertEqual(
         b'kvoh',
         subprocess.check_output(
             nsenter_as_user(unshare, 'cat', cypa), ))
예제 #4
0
    def test_pid_namespace(self):
        with Unshare([Namespace.PID]) as unshare:
            proc, _ = self._popen_sleep_forever(unshare)
            # Check that "as user" works.
            for arg, expected in (('-u', os.geteuid()), ('-g', os.getegid())):
                actual = int(
                    subprocess.check_output(nsenter_as_user(
                        unshare, 'id', arg)))
                self.assertEqual(expected, actual)
            time.sleep(2)  # Leave some time for `sleep` to exit erroneously
            self.assertEqual(None, proc.poll())  # Sleeps forever

            self._check_ns_diff(unshare, {'pid'})

        self.assertEqual(-signal.SIGKILL, proc.poll())  # Reaped by PID NS
예제 #5
0
 def _send_to_loopback_if_fits(self, output_path, fs_size_bytes) -> bool:
     '''
     Creates a loopback of the specified size, and sends the current
     subvolume to it.  Returns True if the subvolume fits in that space.
     '''
     open(output_path, 'wb').close()
     with pipe() as (r_send, w_send), \
             Unshare([Namespace.MOUNT, Namespace.PID]) as ns, \
             LoopbackVolume(ns, output_path, fs_size_bytes) as loop_vol, \
             self.mark_readonly_and_write_sendstream_to_file(w_send):
         w_send.close()  # This end is now fully owned by `btrfs send`.
         with r_send:
             recv_ret = run_stdout_to_err(nsenter_as_root(
                 ns,
                 'btrfs',
                 'receive',
                 loop_vol.dir(),
             ),
                                          stdin=r_send,
                                          stderr=subprocess.PIPE)
             if recv_ret.returncode != 0:
                 if recv_ret.stderr.endswith(self._OUT_OF_SPACE_SUFFIX):
                     return False
                 # It's pretty lame to rely on `btrfs receive` continuing
                 # to be unlocalized, and emitting that particular error
                 # message, so we fall back to checking available bytes.
                 size_ret = subprocess.run(nsenter_as_user(
                     ns,
                     'findmnt',
                     '--noheadings',
                     '--bytes',
                     '--output',
                     'AVAIL',
                     loop_vol.dir(),
                 ),
                                           stdout=subprocess.PIPE)
                 # If the `findmnt` fails, don't mask the original error.
                 if size_ret.returncode == 0 and int(size_ret.stdout) == 0:
                     return False
                 # Covering this is hard, so the test plan is "inspection".
                 log.error(  # pragma: no cover
                     'Unhandled receive stderr:\n\n' +
                     recv_ret.stderr.decode(errors='surrogateescape'), )
             recv_ret.check_returncode()
         loop_vol.minimize_size()
         return True
예제 #6
0
    def test_pid_namespace_dead_keepalive(self):
        with Unshare([Namespace.PID]) as unshare:
            self._check_ns_diff(unshare, {'pid'})

            good_echo = nsenter_as_user(unshare, 'echo')
            subprocess.check_call(good_echo)  # Will fail once the NS is dead

            proc, _ = self._popen_sleep_forever(unshare)
            time.sleep(2)  # Leave some time for `sleep` to exit erroneously
            self.assertEqual(None, proc.poll())  # Sleeps forever

            self._kill_keepalive(unshare)

            self.assertEqual(-signal.SIGKILL, proc.wait())  # The NS is dead

            # The `echo` command that worked above no longer works.
            with self.assertRaises(subprocess.CalledProcessError):
                subprocess.check_call(good_echo)
예제 #7
0
    def _popen_sleep_forever(self, unshare: Unshare):
        # We need the ready signal to know when we've actually executed the
        # payload -- otherwise, we might try to interact with it while we're
        # still at `nsenter`.
        proc = subprocess.Popen(nsenter_as_user(
            unshare,
            'bash',
            '-uec',
            'echo ready $$ ; exec sleep infinity',
        ),
                                stdout=subprocess.PIPE)

        # Wait for the child to start
        ready_and_pid = proc.stdout.readline().split(b' ')
        self.assertEqual(b'ready', ready_and_pid[0])

        proc.stdout.close()  # `sudo` keeps stdout open, but will not write.
        # Returning the PID lets us clean up the `sleep infinity` when it is
        # not inside a PID namespace.
        return proc, int(ready_and_pid[1])
예제 #8
0
 def test_nsenter_wrappers(self):
     self.assertEqual(('a', 'b'), nsenter_as_user(None, 'a', 'b'))
     self.assertEqual(('sudo', 'c', 'd'), nsenter_as_root(None, 'c', 'd'))