Beispiel #1
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
Beispiel #2
0
 def test_package_image_as_btrfs_loopback(self):
     with self._package_image(
         self._sibling_path('create_ops.layer'), 'btrfs',
     ) as out_path, \
             Unshare([Namespace.MOUNT, Namespace.PID]) as unshare, \
             tempfile.TemporaryDirectory() as mount_dir, \
             tempfile.NamedTemporaryFile() as temp_sendstream:
         # Future: use a LoopbackMount object here once that's checked in.
         subprocess.check_call(nsenter_as_root(
             unshare, 'mount', '-t', 'btrfs', '-o', 'loop,discard,nobarrier',
             out_path, mount_dir,
         ))
         try:
             # Future: Once I have FD, this should become:
             # Subvol(
             #     os.path.join(mount_dir.fd_path(), 'create_ops'),
             #     already_exists=True,
             # ).mark_readonly_and_write_sendstream_to_file(temp_sendstream)
             subprocess.check_call(nsenter_as_root(
                 unshare, 'btrfs', 'send', '-f', temp_sendstream.name,
                 os.path.join(mount_dir, 'create_ops'),
             ))
             self._assert_sendstream_files_equal(
                 self._sibling_path('create_ops-original.sendstream'),
                 temp_sendstream.name,
             )
         finally:
             nsenter_as_root(unshare, 'umount', mount_dir)
Beispiel #3
0
 def test_package_image_as_btrfs_loopback_writable(self):
     with self._package_image(
         self._sibling_path('create_ops.layer'),
         'btrfs',
         writable_subvolume=True,
     ) as out_path, \
             Unshare([Namespace.MOUNT, Namespace.PID]) as unshare, \
             tempfile.TemporaryDirectory() as mount_dir:
         os.chmod(
             out_path,
             stat.S_IMODE(os.stat(out_path).st_mode)
             | (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH),
         )
         subprocess.check_call(nsenter_as_root(
             unshare, 'mount', '-t', 'btrfs', '-o', 'loop,discard,nobarrier',
             out_path, mount_dir,
         ))
         try:
             subprocess.check_call(nsenter_as_root(
                 unshare, 'touch', os.path.join(mount_dir,
                                                'create_ops',
                                                'foo'),
             ))
         finally:
             nsenter_as_root(unshare, 'umount', mount_dir)
Beispiel #4
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
Beispiel #5
0
 def test_package_image_as_squashfs(self):
     with self._package_image(
         self._sibling_path('create_ops.layer'), 'squashfs',
     ) as out_path, TempSubvolumes(sys.argv[0]) as temp_subvolumes, \
             tempfile.NamedTemporaryFile() as temp_sendstream:
         subvol = temp_subvolumes.create('subvol')
         with Unshare([Namespace.MOUNT, Namespace.PID]) as unshare, \
                 tempfile.TemporaryDirectory() as mount_dir:
             subprocess.check_call(nsenter_as_root(
                 unshare, 'mount', '-t', 'squashfs', '-o', 'loop',
                 out_path, mount_dir,
             ))
             # `unsquashfs` would have been cleaner than `mount` +
             # `rsync`, and faster too, but unfortunately it corrupts
             # device nodes as of v4.3.
             subprocess.check_call(nsenter_as_root(
                 unshare, 'rsync', '--archive', '--hard-links',
                 '--sparse', '--xattrs', mount_dir + '/', subvol.path(),
             ))
         with subvol.mark_readonly_and_write_sendstream_to_file(
             temp_sendstream
         ):
             pass
         original_render = _render_sendstream_path(
             self._sibling_path('create_ops-original.sendstream'),
         )
         # SquashFS does not preserve the original's cloned extents of
         # zeros, nor the zero-hole-zero patter.  In all cases, it
         # (efficiently) transmutes the whole file into 1 sparse hole.
         self.assertEqual(original_render[1].pop('56KB_nuls'), [
             '(File d57344(create_ops@56KB_nuls_clone:0+49152@0/' +
             'create_ops@56KB_nuls_clone:49152+8192@49152))'
         ])
         original_render[1]['56KB_nuls'] = ['(File h57344)']
         self.assertEqual(original_render[1].pop('56KB_nuls_clone'), [
             '(File d57344(create_ops@56KB_nuls:0+49152@0/' +
             'create_ops@56KB_nuls:49152+8192@49152))'
         ])
         original_render[1]['56KB_nuls_clone'] = ['(File h57344)']
         self.assertEqual(original_render[1].pop('zeros_hole_zeros'), [
             '(File d16384h16384d16384)'
         ])
         original_render[1]['zeros_hole_zeros'] = ['(File h49152)']
         self.assertEqual(
             original_render, _render_sendstream_path(temp_sendstream.name),
         )
Beispiel #6
0
 def _check_ns_diff(self, unshare: Unshare, ns_diff: Iterable[str]):
     list_ns_cmd = [
         'readlink',
         *(f'/proc/self/ns/{name}' for name in _NS_FILES),
     ]
     in_ns, out_ns = [
         dict(
             ns_ino.split(':') for ns_ino in subprocess.check_output(
                 cmd).decode().strip().split('\n')) for cmd in [
                     list_ns_cmd,
                     nsenter_as_root(unshare, *list_ns_cmd),
                 ]
     ]
     for ns in ns_diff:
         self.assertNotEqual(in_ns.pop(ns), out_ns.pop(ns), ns)
     self.assertEqual(in_ns, out_ns)
Beispiel #7
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
Beispiel #8
0
 def unmount_if_mounted(self):
     if self._mount_dir:
         # Nothing might have been mounted, ignore exit code
         run_stdout_to_err(
             nsenter_as_root(self._unshare, 'umount', self._mount_dir), )
Beispiel #9
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'))
Beispiel #10
0
    def test_mount_namespace(self):
        try:
            sleep_pid = None
            with tempfile.TemporaryDirectory() as mnt_src, \
                    tempfile.TemporaryDirectory() as mnt_dest1, \
                    tempfile.TemporaryDirectory() as mnt_dest2:
                with open(os.path.join(mnt_src, 'cypa'), 'w') as outfile:
                    outfile.write('kvoh')

                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), ))

                with Unshare([Namespace.MOUNT]) as unshare:
                    # Without a PID namespace, this will outlive the
                    # __exit__ -- in fact, this process would leak but for
                    # our `finally`.
                    proc, sleep_pid = self._popen_sleep_forever(unshare)

                    subprocess.check_call(
                        nsenter_as_root(
                            unshare,
                            'mount',
                            mnt_src,
                            mnt_dest1,
                            '-o',
                            'bind',
                        ))
                    check_mnt_dest(mnt_dest1)

                    # Mount namespaces remain usable after the keepalive dies
                    self._kill_keepalive(unshare)

                    # We can make a second mount inside the namespace
                    subprocess.check_call(
                        nsenter_as_root(
                            unshare,
                            'mount',
                            mnt_src,
                            mnt_dest2,
                            '-o',
                            'bind',
                        ))
                    check_mnt_dest(mnt_dest2)
                    check_mnt_dest(mnt_dest1)  # The old mount is still good

                # Outside the context, nsenter cannot work. There's no way
                # to test the mounts are gone since we don't have any handle
                # by which to access them.  That's the point.
                with self.assertRaisesRegex(
                        RuntimeError,
                        'Must nsenter from inside an Unshare',
                ):
                    check_mnt_dest(mnt_dest1)

            time.sleep(2)  # Give some time for `sleep` to exit erroneously
            self.assertIs(None, proc.poll())  # Processes leak
        finally:
            # Ensure we don't leak the `sleep infinity` -- since it was
            # started via `sudo`, `subprocess` cannot kill it automatically.
            if sleep_pid:
                if proc.poll() is None:
                    os.kill(sleep_pid, signal.SIGTERM)
                proc.wait()