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)
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)
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, {})
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)
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
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), )
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
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)
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()
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'})