Example #1
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)
Example #2
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)
Example #3
0
 def test_no_namespaces(self):
     '''
     A silly test that shows that unsharing nothing still works -- which
     is useful to distinguish self._namespace_to_file {} vs None.  That
     said, people should just use nsenter_as_*(None, ...) instead.
     '''
     with Unshare([]) as unshare:
         self._check_ns_diff(unshare, {})
Example #4
0
 def test_context_enter_error(self):
     'Exercise triggering __exit__ when __enter__ raises'
     unshare = Unshare([Namespace.MOUNT])  # This does not fail
     # Give bad arguments to the inner `sudo` to make the keepalive fail
     # quickly without outputting the inner PID.
     with mock.patch('os.geteuid', side_effect='NOT-A-REAL-USER-ID'), \
             self.assertRaises(IndexError):  # nspid_out[0] fails
         with unshare:
             raise AssertionError  # Guarantees __enter__ was what failed
     # The Unshare was left in a clean-ish state, which strongly suggests
     # that __exit__ ran, given that __enter__ immediately assigns to
     # `self._keepalive_proc`, and that did run (CalledProcessError).
     self.assertEqual(None, unshare._keepalive_proc)
     self.assertEqual(None, unshare._namespace_to_file)
Example #5
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
Example #6
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),
         )
Example #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
Example #8
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)
Example #9
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()
Example #10
0
 def test_multiple_namespaces(self):
     'Just a smoke test for multiple namespaces being entered at once'
     with Unshare([Namespace.PID, Namespace.MOUNT]) as unshare:
         self._check_ns_diff(unshare, {'mnt', 'pid'})