forked from rpms/leapp-repository
e5599cfda4
- 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
1107 lines
38 KiB
Diff
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
|
|
|