leapp-repository/0060-Rework-_copy_decouple-to-follow-relative-symlinks-an.patch
Petr Stodulka e5599cfda4 RHEL 8.10: CTC2 candidate - 0
- Add detection of possible usage of OpenSSL IBMCA engine on IBM Z machines
- Add detection of modified /etc/pki/tls/openssl.cnf file
- Update the leapp upgrade data files
- Fix handling of symlinks under /etc/pki with relative paths specified
- Report custom actors and modifications of the upgrade tooling
- Requires xfsprogs and e2fsprogs to ensure that Ext4 and XFS tools are installed
- Bump leapp-repository-dependencies to 10
- Resolves: RHEL-1774, RHEL-16729
2024-01-12 20:45:10 +01:00

1107 lines
38 KiB
Diff

From af50cfc60e5c8a962ece7f89b424e7ea4a32ba2a Mon Sep 17 00:00:00 2001
From: Toshio Kuratomi <a.badger@gmail.com>
Date: Wed, 10 Jan 2024 18:38:25 -0800
Subject: [PATCH 60/60] Rework _copy_decouple to follow relative symlinks and
symlinks to directories.
* The previous code handled absolute symlinks fine but when there were relative symlinks it would
traceback. Additionally, it did not handle symlinks to directories that occurred outside of
/etc/pki. This should fix both of those cases.
In order to handle symlinks to the /etc/pki directory, we need to introduce the concept of the
canonical path. The canonical path is an absolute path that has had all symlinks dereferenced and
doesn't contain any parent directories ("..") or self directories ("."). We have to use the
canonical path for most file path comparisons since symlinks allow multiple paths that will point
to a file but there is only one canonical path. The logic is somewhat tricky since we need to use
the canonical path for comparisons but we have to use srcdir when constructing the paths that we
will put into links we create (since we want to use /etc/pki in the container context even if
/etc/pki is a symlink on the host system.)
* Add some unittests that test symlink handling of copy_decouple with relative symlinks.
* Enhance the temporary_directory fixture to handle creation of relative symlinks too.
* Add better error messages to asserts in assert_firectory_structure_matches
* Modify _copy_decouple() unittest to raise CalledProcessError() if run() encounters an error.
If the command line executable that run() executes has a non-zero exit code, the real code will
raise CalledProcessError() but the mock in the unittest would not. Change the unittest to match
the actual code's behaviour.
* Move explanation of the parametrize structure to traverse_structure's docstring.
* Use pytest.param() and id for the parametrize on test_copy_decouple. The ids
help to determine which tests have failed and allow us to select a specific test
to rerun (with PYLINT_ARGS="-k '<ID>'"
* If decouple_copy fails, then print out the entire directory structure that was
created. That will help to debug the failed assertions.
---
.../libraries/userspacegen.py | 213 ++++-
.../tests/unit_test_targetuserspacecreator.py | 733 ++++++++++++++----
2 files changed, 766 insertions(+), 180 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
index c1d34f18..8d804407 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
@@ -73,6 +73,10 @@ def _check_deprecated_rhsm_skip():
)
+class BrokenSymlinkError(Exception):
+ """Raised when we encounter a broken symlink where we weren't expecting it."""
+
+
class _InputData(object):
def __init__(self):
self._consume_data()
@@ -328,9 +332,131 @@ def _get_files_owned_by_rpms(context, dirpath, pkgs=None, recursive=False):
return files_owned_by_rpms
+def _mkdir_with_copied_mode(path, mode_from):
+ """
+ Create directories with a file to copy the mode from.
+
+ :param path: The directory path to create.
+ :param mode_from: A file or directory whose mode we will copy to the
+ newly created directory.
+ :raises subprocess.CalledProcessError: mkdir or chmod fails. For instance,
+ the directory already exists, the file to get permissions from does
+ not exist, a parent directory does not exist.
+ """
+ # Create with maximally restrictive permissions
+ run(['mkdir', '-m', '0', '-p', path])
+ run(['chmod', '--reference={}'.format(mode_from), path])
+
+
+def _choose_copy_or_link(symlink, srcdir):
+ """
+ Copy file contents or create a symlink depending on where the pointee resides.
+
+ :param symlink: The source symlink to follow. This must be an absolute path.
+ :param srcdir: The root directory that every piece of content must be present in.
+ :returns: A tuple of action and sourcefile. Action is one of 'copy' or 'link' and means that
+ the caller should either copy the sourcefile to the target location or create a symlink from
+ the sourcefile to the target location. sourcefile is the path to the file that should be
+ the source of the operation. It is either a real file outside of the srcdir hierarchy or
+ a file (real, directory, symlink or otherwise) inside of the srcdir hierarchy.
+ :raises ValueError: if the arguments are not correct
+ :raises BrokenSymlinkError: if the symlink is invalid
+
+ Determine whether the file pointed to by the symlink chain is within srcdir. If it is within,
+ then create a synlink that points from symlink to it.
+
+ If it is not within, then walk the symlink chain until we find something that is within srcdir
+ and return that. This means we will omit any symlinks that are outside of srcdir from
+ the symlink chain.
+
+ If we reach a real file and it is outside of srcdir, then copy the file instead.
+ """
+ if not symlink.startswith('/'):
+ raise ValueError('File{} must be an absolute path!'.format(symlink))
+
+ # os.path.exists follows symlinks
+ if not os.path.exists(symlink):
+ raise BrokenSymlinkError('File {} is a broken symlink!'.format(symlink))
+
+ # If srcdir is a symlink, then we need a name for it that we can compare
+ # with other paths.
+ canonical_srcdir = os.path.realpath(srcdir)
+
+ pointee_as_abspath = symlink
+ seen = set([pointee_as_abspath])
+
+ # The goal of this while loop is to find the next link in a possible
+ # symlink chain that either points to a symlink inside of srcdir or to
+ # a file or directory that we can copy.
+ while os.path.islink(pointee_as_abspath):
+ # Advance pointee to the target of the previous link
+ pointee = os.readlink(pointee_as_abspath)
+
+ # Note: os.path.join()'s behaviour if the pointee is an absolute path
+ # essentially ignores the first argument (which is what we want).
+ pointee_as_abspath = os.path.normpath(os.path.join(os.path.dirname(pointee_as_abspath), pointee))
+
+ # Make sure we aren't in a circular set of references.
+ # On Linux, this should not happen as the os.path.exists() call
+ # before the loop should catch it but we don't want to enter an
+ # infinite loop if that code changes later.
+ if pointee_as_abspath in seen:
+ if symlink == pointee_as_abspath:
+ error_msg = ('File {} is a broken symlink that references'
+ ' itself!'.format(pointee_as_abspath))
+ else:
+ error_msg = ('File {} references {} which is a broken symlink'
+ ' that references itself!'.format(symlink, pointee_as_abspath))
+
+ raise BrokenSymlinkError(error_msg)
+
+ seen.add(pointee_as_abspath)
+
+ # To make comparisons, we need to resolve all symlinks in the directory
+ # structure leading up to pointee. However, we can't include pointee
+ # itself otherwise it will resolve to the file that it points to in the
+ # end.
+ canonical_pointee_dir, pointee_filename = os.path.split(pointee_as_abspath)
+ canonical_pointee_dir = os.path.realpath(canonical_pointee_dir)
+
+ if canonical_pointee_dir.startswith(canonical_srcdir):
+ # Absolute path inside of the correct dir so we need to link to it
+ # But we need to determine what the link path should be before
+ # returning.
+
+ # Construct a relative path that points from the symlinks directory
+ # to the pointee.
+ link_to = os.readlink(symlink)
+ canonical_symlink_dir = os.path.realpath(os.path.dirname(symlink))
+ relative_path = os.path.relpath(canonical_pointee_dir, canonical_symlink_dir)
+
+ if link_to.startswith('/'):
+ # The original symlink was an absolute path so we will set this
+ # one to absolute too
+ # Note: Because absolute paths are constructed inside of
+ # srcdir, the relative path that we need to join here has to be
+ # relative to srcdir, not the directory that the symlink is
+ # being created in.
+ relative_to_srcdir = os.path.relpath(canonical_pointee_dir, canonical_srcdir)
+ corrected_path = os.path.normpath(os.path.join(srcdir, relative_to_srcdir, pointee_filename))
+
+ else:
+ # If the original link is a relative link, then we want the new
+ # link to be relative as well
+ corrected_path = os.path.normpath(os.path.join(relative_path, pointee_filename))
+
+ return ("link", corrected_path)
+
+ # pointee is a symlink that points outside of the srcdir so continue to
+ # the next symlink in the chain.
+
+ # The file is not a link so copy it
+ return ('copy', pointee_as_abspath)
+
+
def _copy_decouple(srcdir, dstdir):
"""
- Copy `srcdir` to `dstdir` while decoupling symlinks.
+ Copy files inside of `srcdir` to `dstdir` while decoupling symlinks.
What we mean by decoupling the `srcdir` is that any symlinks pointing
outside the directory will be copied as regular files. This means that the
@@ -338,58 +464,55 @@ def _copy_decouple(srcdir, dstdir):
symlinks. Any symlink (or symlink chains) within the directory will be
preserved.
+ .. warning::
+ `dstdir` must already exist.
"""
+ symlinks_to_process = []
+ for root, directories, files in os.walk(srcdir):
+ # relative path from srcdir because srcdir is replaced with dstdir for
+ # the copy.
+ relpath = os.path.relpath(root, srcdir)
+
+ # Create all directories with proper permissions for security
+ # reasons (Putting private data into directories that haven't had their
+ # permissions set appropriately may leak the private information.)
+ for directory in directories:
+ source_dirpath = os.path.join(root, directory)
+ target_dirpath = os.path.join(dstdir, relpath, directory)
+ _mkdir_with_copied_mode(target_dirpath, source_dirpath)
- for root, dummy_dirs, files in os.walk(srcdir):
for filename in files:
- relpath = os.path.relpath(root, srcdir)
source_filepath = os.path.join(root, filename)
target_filepath = os.path.join(dstdir, relpath, filename)
- # Skip and report broken symlinks
- if not os.path.exists(source_filepath):
- api.current_logger().warning(
- 'File {} is a broken symlink! Will not copy the file.'.format(source_filepath))
- continue
-
- # Copy symlinks to the target userspace
- source_is_symlink = os.path.islink(source_filepath)
- pointee = None
- if source_is_symlink:
- pointee = os.readlink(source_filepath)
-
- # If source file is a symlink within `srcdir` then preserve it,
- # otherwise resolve and copy it as a file it points to
- if pointee is not None and not pointee.startswith(srcdir):
- # Follow the path until we hit a file or get back to /etc/pki
- while not pointee.startswith(srcdir) and os.path.islink(pointee):
- pointee = os.readlink(pointee)
-
- # Pointee points to a _regular file_ outside /etc/pki so we
- # copy it instead
- if not pointee.startswith(srcdir) and not os.path.islink(pointee):
- source_is_symlink = False
- source_filepath = pointee
- else:
- # pointee points back to /etc/pki
- pass
-
- # Ensure parent directory exists
- parent_dir = os.path.dirname(target_filepath)
- # Note: This is secure because we know that parent_dir is located
- # inside of `$target_userspace/etc/pki` which is a directory that
- # is not writable by unprivileged users. If this function is used
- # elsewhere we may need to be more careful before running `mkdir -p`.
- run(['mkdir', '-p', parent_dir])
-
- if source_is_symlink:
- # Preserve the owner and permissions of the original symlink
- run(['ln', '-s', pointee, target_filepath])
- run(['chmod', '--reference={}'.format(source_filepath), target_filepath])
+ # Defer symlinks until later because we may end up having to copy
+ # the file contents and the directory may not exist yet.
+ if os.path.islink(source_filepath):
+ symlinks_to_process.append((source_filepath, target_filepath))
continue
+ # Not a symlink so we can copy it now too
run(['cp', '-a', source_filepath, target_filepath])
+ # Now process all symlinks
+ for source_linkpath, target_linkpath in symlinks_to_process:
+ try:
+ action, source_path = _choose_copy_or_link(source_linkpath, srcdir)
+ except BrokenSymlinkError as e:
+ # Skip and report broken symlinks
+ api.current_logger().warning('{} Will not copy the file!'.format(str(e)))
+ continue
+
+ if action == "copy":
+ # Note: source_path could be a directory, so '-a' or '-r' must be
+ # given to cp.
+ run(['cp', '-a', source_path, target_linkpath])
+ elif action == 'link':
+ run(["ln", "-s", source_path, target_linkpath])
+ else:
+ # This will not happen unless _copy_or_link() has a bug.
+ raise RuntimeError("Programming error: _copy_or_link() returned an unknown action:{}".format(action))
+
def _copy_certificates(context, target_userspace):
"""
@@ -414,6 +537,10 @@ def _copy_certificates(context, target_userspace):
# Backup container /etc/pki
run(['mv', target_pki, backup_pki])
+ # _copy_decouple() requires we create the target_pki directory here because we don't know
+ # the mode inside of _copy_decouple().
+ _mkdir_with_copied_mode(target_pki, backup_pki)
+
# Copy source /etc/pki to the container
_copy_decouple('/etc/pki', target_pki)
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py b/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
index 1a1ee56e..bd49f657 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
@@ -1,4 +1,4 @@
-from __future__ import division
+from __future__ import division, print_function
import os
import subprocess
@@ -59,6 +59,33 @@ class MockedMountingBase(object):
def traverse_structure(structure, root=Path('/')):
+ """
+ Given a description of a directory structure, return fullpaths to the
+ files and what they link to.
+
+ :param structure: A dict which defined the directory structure. See below
+ for what it looks like.
+ :param root: A path to prefix to the files. On an actual run in production.
+ this would be `/` but since we're doing this in a unittest, it needs to
+ be a temporary directory.
+ :returns: This is a generator, so pairs of (filepath, what it links to) will
+ be returned one at a time, each time through the iterable.
+
+ The semantics of `structure` are as follows:
+
+ 1. The outermost dictionary encodes the root of a directory structure
+
+ 2. Depending on the value for a key in a dict, each key in the dictionary
+ denotes the name of either a:
+ a) directory -- if value is dict
+ b) regular file -- if value is None
+ c) symlink -- if a value is str
+
+ 3. The value of a symlink entry is a absolute path to a file in the context of
+ the structure.
+
+ .. warning:: Empty directories are not returned.
+ """
for filename, links_to in structure.items():
filepath = root / filename
@@ -72,14 +99,30 @@ def traverse_structure(structure, root=Path('/')):
def assert_directory_structure_matches(root, initial, expected):
# Assert every file that is supposed to be present is present
for filepath, links_to in traverse_structure(expected, root=root / 'expected'):
- assert filepath.exists()
+ assert filepath.exists(), "{} was supposed to exist and does not".format(filepath)
if links_to is None:
- assert filepath.is_file()
+ assert filepath.is_file(), "{} was supposed to be a file but is not".format(filepath)
continue
- assert filepath.is_symlink()
- assert os.readlink(str(filepath)) == str(root / 'initial' / links_to.lstrip('/'))
+ assert filepath.is_symlink(), '{} was supposed to be a symlink but is not'.format(filepath)
+
+ # We need to rewrite absolute paths because:
+ # * links_to contains an absolute path to the resource where the root
+ # directory is `/`.
+ # * In our test case, the source resource is rooted in a temporary
+ # directory rather than '/'.
+ # * The temporary directory name is root / 'initial'.
+ # So we rewrite the initial `/` to be `root/{initial}` to account for
+ # that. In production, the root directory will be `/` so no rewriting
+ # will happen there.
+ #
+ if links_to.startswith('/'):
+ links_to = str(root / 'initial' / links_to.lstrip('/'))
+
+ actual_links_to = os.readlink(str(filepath))
+ assert actual_links_to == str(links_to), (
+ '{} linked to {} instead of {}'.format(filepath, actual_links_to, links_to))
# Assert there are no extra files
result_dir = str(root / 'expected')
@@ -95,21 +138,36 @@ def assert_directory_structure_matches(root, initial, expected):
filepath = os.path.join(fileroot, filename)
if os.path.islink(filepath):
- links_to = '/' + os.path.relpath(os.readlink(filepath), str(root / 'initial'))
+ links_to = os.readlink(filepath)
+ # We rewrite absolute paths because the root directory is in
+ # a temp dir instead of `/` in the unittest. See the comment
+ # where we rewrite `links_to` for the previous loop in this
+ # function for complete details.
+ if links_to.startswith('/'):
+ links_to = '/' + os.path.relpath(links_to, str(root / 'initial'))
assert cwd[filename] == links_to
@pytest.fixture
def temp_directory_layout(tmp_path, initial_structure):
for filepath, links_to in traverse_structure(initial_structure, root=tmp_path / 'initial'):
+ # Directories are inlined by traverse_structure so we need to create
+ # them here
file_path = tmp_path / filepath
file_path.parent.mkdir(parents=True, exist_ok=True)
+ # Real file
if links_to is None:
file_path.touch()
continue
- file_path.symlink_to(tmp_path / 'initial' / links_to.lstrip('/'))
+ # Symlinks
+ if links_to.startswith('/'):
+ # Absolute symlink
+ file_path.symlink_to(tmp_path / 'initial' / links_to.lstrip('/'))
+ else:
+ # Relative symlink
+ file_path.symlink_to(links_to)
(tmp_path / 'expected').mkdir()
assert (tmp_path / 'expected').exists()
@@ -117,159 +175,560 @@ def temp_directory_layout(tmp_path, initial_structure):
return tmp_path
-# The semantics of initial_structure and expected_structure are as follows:
-#
-# 1. The outermost dictionary encodes the root of a directory structure
-#
-# 2. Depending on the value for a key in a dict, each key in the dictionary
-# denotes the name of either a:
-# a) directory -- if value is dict
-# b) regular file -- if value is None
-# c) symlink -- if a value is str
-#
-# 3. The value of a symlink entry is a absolute path to a file in the context of
-# the structure.
-#
+# The semantics of initial_structure and expected_structure are defined in the
+# traverse_structure() docstring.
@pytest.mark.parametrize('initial_structure,expected_structure', [
- ({ # Copy a regular file
- 'dir': {
- 'fileA': None
- }
- }, {
- 'dir': {
- 'fileA': None
- }
- }),
- ({ # Do not copy a broken symlink
- 'dir': {
- 'fileA': 'nonexistent'
- }
- }, {
- 'dir': {}
- }),
- ({ # Copy a regular symlink
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': None
- }
- }, {
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': None
- }
- }),
- ({ # Do not copy a chain of broken symlinks
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': 'nonexistent'
- }
- }, {
- 'dir': {}
- }),
- ({ # Copy a chain of symlinks
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': None
- }
- }, {
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': None
- }
- }),
- ({ # Circular symlinks
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': '/dir/fileC',
- }
- }, {
- 'dir': {}
- }),
- ({ # Copy a link to a file outside the considered directory as file
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': '/outside/fileOut',
- 'fileE': None
- },
- 'outside': {
- 'fileOut': '/outside/fileD',
- 'fileD': '/dir/fileE'
- }
- }, {
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': '/dir/fileE',
- 'fileE': None,
- }
- }),
- ({ # Same test with a nested structure within the source dir
- 'dir': {
- 'nested': {
- 'fileA': '/dir/nested/fileB',
- 'fileB': '/dir/nested/fileC',
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': None
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': None
+ },
+ },
+ id="Copy_a_regular_file"
+ )),
+ # Absolute symlink tests
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/nonexistent'
+ }
+ },
+ {
+ 'dir': {},
+ },
+ id="Absolute_do_not_copy_a_broken_symlink"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/nonexistent'
+ }
+ },
+ {
+ 'dir': {}
+ },
+ id="Absolute_do_not_copy_a_chain_of_broken_symlinks"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/nonexistent-dir/nonexistent'
+ },
+ },
+ {
+ 'dir': {},
+ },
+ id="Absolute_do_not_copy_a_broken_symlink_to_a_nonexistent_directory"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
+ 'fileC': '/dir/fileA',
+ 'fileD': '/dir/fileD',
+ }
+ },
+ {
+ 'dir': {}
+ },
+ id="Absolute_do_not_copy_circular_symlinks"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': None
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': None
+ }
+ },
+ id="Absolute_copy_a_regular_symlink"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
+ 'fileC': None
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
+ 'fileC': None
+ }
+ },
+ id="Absolute_copy_a_chain_of_symlinks"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
'fileC': '/outside/fileOut',
'fileE': None
+ },
+ 'outside': {
+ 'fileOut': '/outside/fileD',
+ 'fileD': '/dir/fileE'
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
+ 'fileC': '/dir/fileE',
+ 'fileE': None,
+ }
+ },
+ id="Absolute_copy_a_link_to_a_file_outside_the_considered_directory_as_file"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'nested': {
+ 'fileA': '/dir/nested/fileB',
+ 'fileB': '/dir/nested/fileC',
+ 'fileC': '/outside/fileOut',
+ 'fileE': None
+ }
+ },
+ 'outside': {
+ 'fileOut': '/outside/fileD',
+ 'fileD': '/dir/nested/fileE'
}
},
- 'outside': {
- 'fileOut': '/outside/fileD',
- 'fileD': '/dir/nested/fileE'
- }
- }, {
- 'dir': {
- 'nested': {
- 'fileA': '/dir/nested/fileB',
- 'fileB': '/dir/nested/fileC',
- 'fileC': '/dir/nested/fileE',
+ {
+ 'dir': {
+ 'nested': {
+ 'fileA': '/dir/nested/fileB',
+ 'fileB': '/dir/nested/fileC',
+ 'fileC': '/dir/nested/fileE',
+ 'fileE': None
+ }
+ }
+ },
+ id="Absolute_copy_a_link_to_a_file_outside_with_a_nested_structure_within_the_source_dir"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
+ 'fileC': '/outside/nested/fileOut',
'fileE': None
+ },
+ 'outside': {
+ 'nested': {
+ 'fileOut': '/outside/nested/fileD',
+ 'fileD': '/dir/fileE'
+ }
}
- }
- }),
- ({ # Same test with a nested structure in the outside dir
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': '/outside/nested/fileOut',
- 'fileE': None
- },
- 'outside': {
- 'nested': {
- 'fileOut': '/outside/nested/fileD',
- 'fileD': '/dir/fileE'
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': '/dir/fileC',
+ 'fileC': '/dir/fileE',
+ 'fileE': None,
}
- }
- }, {
- 'dir': {
- 'fileA': '/dir/fileB',
- 'fileB': '/dir/fileC',
- 'fileC': '/dir/fileE',
- 'fileE': None,
- }
- }),
+ },
+ id="Absolute_copy_a_link_to_a_file_outside_with_a_nested_structure_in_the_outside_dir"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/outside/fileOut',
+ 'fileB': None,
+ },
+ 'outside': {
+ 'fileOut': '../dir/fileB',
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': None,
+ },
+ },
+ id="Absolute_symlink_that_leaves_the_directory_but_returns_with_relative_outside"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '/outside/fileB',
+ 'fileB': None,
+ },
+ 'outside': '/dir',
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': None,
+ },
+ },
+ id="Absolute_symlink_to_a_file_inside_via_a_symlink_to_the_rootdir"
+ )),
+ (pytest.param(
+ # This one is very tricky:
+ # * The user has made /etc/pki a symlink to some other directory that
+ # they keep certificates.
+ # * In the target system, we are going to make /etc/pki an actual
+ # directory with the contents that the actual directory on the host
+ # system had.
+ {
+ 'dir': '/funkydir',
+ 'funkydir': {
+ 'fileA': '/funkydir/fileB',
+ 'fileB': None,
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': '/dir/fileB',
+ 'fileB': None,
+ },
+ },
+ id="Absolute_symlink_where_srcdir_is_a_symlink_on_the_host_system"
+ )),
+ # Relative symlink tests
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'nonexistent'
+ },
+ },
+ {
+ 'dir': {},
+ },
+ id="Relative_do_not_copy_a_broken_symlink"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'nonexistent'
+ }
+ },
+ {
+ 'dir': {}
+ },
+ id="Relative_do_not_copy_a_chain_of_broken_symlinks"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'nonexistent-dir/nonexistent'
+ },
+ },
+ {
+ 'dir': {},
+ },
+ id="Relative_do_not_copy_a_broken_symlink_to_a_nonexistent_directory"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': 'fileA',
+ 'fileD': 'fileD',
+ }
+ },
+ {
+ 'dir': {}
+ },
+ id="Relative_do_not_copy_circular_symlinks"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ id="Relative_copy_a_regular_symlink_to_a_file_in_the_same_directory"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'dir2/../fileB',
+ 'fileB': None,
+ 'dir2': {
+ 'fileC': None
+ },
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ 'dir2': {
+ 'fileC': None
+ },
+ },
+ },
+ id="Relative_symlink_with_parent_dir_but_still_in_same_directory"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': None
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': None
+ }
+ },
+ id="Relative_copy_a_chain_of_symlinks"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': '../outside/fileOut',
+ 'fileE': None
+ },
+ 'outside': {
+ 'fileOut': 'fileD',
+ 'fileD': '../dir/fileE'
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': 'fileE',
+ 'fileE': None,
+ }
+ },
+ id="Relative_copy_a_link_to_a_file_outside_the_considered_directory_as_file"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '../outside/fileOut',
+ 'fileB': None,
+ },
+ 'outside': {
+ 'fileOut': None,
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': None,
+ 'fileB': None,
+ },
+ },
+ id="Relative_symlink_to_outside"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'nested/fileB',
+ 'nested': {
+ 'fileB': None,
+ },
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': 'nested/fileB',
+ 'nested': {
+ 'fileB': None,
+ },
+ },
+ },
+ id="Relative_copy_a_symlink_to_a_file_in_a_subdir"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileF': 'nested/fileC',
+ 'nested': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': '../../outside/fileOut',
+ 'fileE': None,
+ }
+ },
+ 'outside': {
+ 'fileOut': 'fileD',
+ 'fileD': '../dir/nested/fileE',
+ }
+ },
+ {
+ 'dir': {
+ 'fileF': 'nested/fileC',
+ 'nested': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': 'fileE',
+ 'fileE': None,
+ }
+ }
+ },
+ id="Relative_copy_a_link_to_a_file_outside_with_a_nested_structure_within_the_source_dir"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': '../outside/nested/fileOut',
+ 'fileE': None
+ },
+ 'outside': {
+ 'nested': {
+ 'fileOut': 'fileD',
+ 'fileD': '../../dir/fileE'
+ }
+ }
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': 'fileC',
+ 'fileC': 'fileE',
+ 'fileE': None,
+ }
+ },
+ id="Relative_copy_a_link_to_a_file_outside_with_a_nested_structure_in_the_outside_dir"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '../outside/fileOut',
+ 'fileB': None,
+ },
+ 'outside': {
+ 'fileOut': '../dir/fileB',
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ id="Relative_symlink_that_leaves_the_directory_but_returns"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '../outside/fileOut',
+ 'fileB': None,
+ },
+ 'outside': {
+ 'fileOut': '/dir/fileB',
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ id="Relative_symlink_that_leaves_the_directory_but_returns_with_absolute_outside"
+ )),
+ (pytest.param(
+ {
+ 'dir': {
+ 'fileA': '../outside/fileB',
+ 'fileB': None,
+ },
+ 'outside': '/dir',
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ id="Relative_symlink_to_a_file_inside_via_a_symlink_to_the_rootdir"
+ )),
+ (pytest.param(
+ # This one is very tricky:
+ # * The user has made /etc/pki a symlink to some other directory that
+ # they keep certificates.
+ # * In the target system, we are going to make /etc/pki an actual
+ # directory with the contents that the actual directory on the host
+ # system had.
+ {
+ 'dir': 'funkydir',
+ 'funkydir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ {
+ 'dir': {
+ 'fileA': 'fileB',
+ 'fileB': None,
+ },
+ },
+ id="Relative_symlink_where_srcdir_is_a_symlink_on_the_host_system"
+ )),
]
)
def test_copy_decouple(monkeypatch, temp_directory_layout, initial_structure, expected_structure):
def run_mocked(command):
- subprocess.Popen(
+ subprocess.check_call(
' '.join(command),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
- ).wait()
+ )
monkeypatch.setattr(userspacegen, 'run', run_mocked)
+ expected_dir = temp_directory_layout / 'expected' / 'dir'
+ expected_dir.mkdir()
userspacegen._copy_decouple(
str(temp_directory_layout / 'initial' / 'dir'),
- str(temp_directory_layout / 'expected' / 'dir'),
+ str(expected_dir),
)
- assert_directory_structure_matches(temp_directory_layout, initial_structure, expected_structure)
+ try:
+ assert_directory_structure_matches(temp_directory_layout, initial_structure, expected_structure)
+ except AssertionError:
+ # For debugging purposes, print out the entire directory structure if an
+ # assertion failed.
+ for rootdir, dirs, files in os.walk(temp_directory_layout):
+ for d in dirs:
+ print(os.path.join(rootdir, d))
+ for f in files:
+ filename = os.path.join(rootdir, f)
+ print(" {}".format(filename))
+ if os.path.islink(filename):
+ print(" => Links to: {}".format(os.readlink(filename)))
+
+ # Then re-raise the assertion
+ raise
@pytest.mark.parametrize('result,dst_ver,arch,prod_type', [
--
2.43.0