| # Copyright 2014-2015 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| from __future__ import unicode_literals |
| |
| import io |
| from functools import partial |
| import shutil |
| import stat |
| import subprocess |
| import sys |
| import time |
| |
| import portage |
| from portage import os |
| from portage import _encodings, _unicode_decode |
| from portage.const import BASH_BINARY, PORTAGE_PYM_PATH |
| from portage.process import find_binary |
| from portage.tests import TestCase |
| from portage.tests.resolver.ResolverPlayground import ResolverPlayground |
| from portage.util import (ensure_dirs, find_updated_config_files, |
| shlex_split) |
| |
| class ConfigProtectTestCase(TestCase): |
| |
| def testConfigProtect(self): |
| """ |
| Demonstrates many different scenarios. For example: |
| |
| * regular file replaces regular file |
| * regular file replaces symlink |
| * regular file replaces directory |
| * symlink replaces symlink |
| * symlink replaces regular file |
| * symlink replaces directory |
| * directory replaces regular file |
| * directory replaces symlink |
| """ |
| |
| debug = False |
| |
| content_A_1 = """ |
| S="${WORKDIR}" |
| |
| src_install() { |
| insinto /etc/A |
| keepdir /etc/A/dir_a |
| keepdir /etc/A/symlink_replaces_dir |
| keepdir /etc/A/regular_replaces_dir |
| echo regular_a_1 > "${T}"/regular_a |
| doins "${T}"/regular_a |
| echo regular_b_1 > "${T}"/regular_b |
| doins "${T}"/regular_b |
| dosym regular_a /etc/A/regular_replaces_symlink |
| dosym regular_b /etc/A/symlink_replaces_symlink |
| echo regular_replaces_regular_1 > \ |
| "${T}"/regular_replaces_regular |
| doins "${T}"/regular_replaces_regular |
| echo symlink_replaces_regular > \ |
| "${T}"/symlink_replaces_regular |
| doins "${T}"/symlink_replaces_regular |
| } |
| |
| """ |
| |
| content_A_2 = """ |
| S="${WORKDIR}" |
| |
| src_install() { |
| insinto /etc/A |
| keepdir /etc/A/dir_a |
| dosym dir_a /etc/A/symlink_replaces_dir |
| echo regular_replaces_dir > "${T}"/regular_replaces_dir |
| doins "${T}"/regular_replaces_dir |
| echo regular_a_2 > "${T}"/regular_a |
| doins "${T}"/regular_a |
| echo regular_b_2 > "${T}"/regular_b |
| doins "${T}"/regular_b |
| echo regular_replaces_symlink > \ |
| "${T}"/regular_replaces_symlink |
| doins "${T}"/regular_replaces_symlink |
| dosym regular_b /etc/A/symlink_replaces_symlink |
| echo regular_replaces_regular_2 > \ |
| "${T}"/regular_replaces_regular |
| doins "${T}"/regular_replaces_regular |
| dosym regular_a /etc/A/symlink_replaces_regular |
| } |
| |
| """ |
| |
| ebuilds = { |
| "dev-libs/A-1": { |
| "EAPI" : "5", |
| "IUSE" : "+flag", |
| "KEYWORDS": "x86", |
| "LICENSE": "GPL-2", |
| "MISC_CONTENT": content_A_1, |
| }, |
| "dev-libs/A-2": { |
| "EAPI" : "5", |
| "IUSE" : "+flag", |
| "KEYWORDS": "x86", |
| "LICENSE": "GPL-2", |
| "MISC_CONTENT": content_A_2, |
| }, |
| } |
| |
| playground = ResolverPlayground( |
| ebuilds=ebuilds, debug=debug) |
| settings = playground.settings |
| eprefix = settings["EPREFIX"] |
| eroot = settings["EROOT"] |
| var_cache_edb = os.path.join(eprefix, "var", "cache", "edb") |
| |
| portage_python = portage._python_interpreter |
| dispatch_conf_cmd = (portage_python, "-b", "-Wd", |
| os.path.join(self.sbindir, "dispatch-conf")) |
| emerge_cmd = (portage_python, "-b", "-Wd", |
| os.path.join(self.bindir, "emerge")) |
| etc_update_cmd = (BASH_BINARY, |
| os.path.join(self.sbindir, "etc-update")) |
| etc_update_auto = etc_update_cmd + ("--automode", "-5",) |
| |
| config_protect = "/etc" |
| |
| def modify_files(dir_path): |
| for name in os.listdir(dir_path): |
| path = os.path.join(dir_path, name) |
| st = os.lstat(path) |
| if stat.S_ISREG(st.st_mode): |
| with io.open(path, mode='a', |
| encoding=_encodings["stdio"]) as f: |
| f.write("modified at %d\n" % time.time()) |
| elif stat.S_ISLNK(st.st_mode): |
| old_dest = os.readlink(path) |
| os.unlink(path) |
| os.symlink(old_dest + |
| " modified at %d" % time.time(), path) |
| |
| def updated_config_files(count): |
| self.assertEqual(count, |
| sum(len(x[1]) for x in find_updated_config_files(eroot, |
| shlex_split(config_protect)))) |
| |
| test_commands = ( |
| etc_update_cmd, |
| dispatch_conf_cmd, |
| emerge_cmd + ("-1", "=dev-libs/A-1"), |
| partial(updated_config_files, 0), |
| emerge_cmd + ("-1", "=dev-libs/A-2"), |
| partial(updated_config_files, 2), |
| etc_update_auto, |
| partial(updated_config_files, 0), |
| emerge_cmd + ("-1", "=dev-libs/A-2"), |
| partial(updated_config_files, 0), |
| # Test bug #523684, where a file renamed or removed by the |
| # admin forces replacement files to be merged with config |
| # protection. |
| partial(shutil.rmtree, |
| os.path.join(eprefix, "etc", "A")), |
| emerge_cmd + ("-1", "=dev-libs/A-2"), |
| partial(updated_config_files, 8), |
| etc_update_auto, |
| partial(updated_config_files, 0), |
| # Modify some config files, and verify that it triggers |
| # config protection. |
| partial(modify_files, |
| os.path.join(eroot, "etc", "A")), |
| emerge_cmd + ("-1", "=dev-libs/A-2"), |
| partial(updated_config_files, 6), |
| etc_update_auto, |
| partial(updated_config_files, 0), |
| # Modify some config files, downgrade to A-1, and verify |
| # that config protection works properly when the file |
| # types are changing. |
| partial(modify_files, |
| os.path.join(eroot, "etc", "A")), |
| emerge_cmd + ("-1", "--noconfmem", "=dev-libs/A-1"), |
| partial(updated_config_files, 6), |
| etc_update_auto, |
| partial(updated_config_files, 0), |
| ) |
| |
| distdir = playground.distdir |
| fake_bin = os.path.join(eprefix, "bin") |
| portage_tmpdir = os.path.join(eprefix, "var", "tmp", "portage") |
| |
| path = os.environ.get("PATH") |
| if path is not None and not path.strip(): |
| path = None |
| if path is None: |
| path = "" |
| else: |
| path = ":" + path |
| path = fake_bin + path |
| |
| pythonpath = os.environ.get("PYTHONPATH") |
| if pythonpath is not None and not pythonpath.strip(): |
| pythonpath = None |
| if pythonpath is not None and \ |
| pythonpath.split(":")[0] == PORTAGE_PYM_PATH: |
| pass |
| else: |
| if pythonpath is None: |
| pythonpath = "" |
| else: |
| pythonpath = ":" + pythonpath |
| pythonpath = PORTAGE_PYM_PATH + pythonpath |
| |
| env = { |
| "PORTAGE_OVERRIDE_EPREFIX" : eprefix, |
| "CLEAN_DELAY" : "0", |
| "CONFIG_PROTECT": config_protect, |
| "DISTDIR" : distdir, |
| "EMERGE_DEFAULT_OPTS": "-v", |
| "EMERGE_WARNING_DELAY" : "0", |
| "INFODIR" : "", |
| "INFOPATH" : "", |
| "PATH" : path, |
| "PORTAGE_INST_GID" : str(portage.data.portage_gid), |
| "PORTAGE_INST_UID" : str(portage.data.portage_uid), |
| "PORTAGE_PYTHON" : portage_python, |
| "PORTAGE_REPOSITORIES" : settings.repositories.config_string(), |
| "PORTAGE_TMPDIR" : portage_tmpdir, |
| "PYTHONDONTWRITEBYTECODE" : os.environ.get("PYTHONDONTWRITEBYTECODE", ""), |
| "PYTHONPATH" : pythonpath, |
| "__PORTAGE_TEST_PATH_OVERRIDE" : fake_bin, |
| } |
| |
| if "__PORTAGE_TEST_HARDLINK_LOCKS" in os.environ: |
| env["__PORTAGE_TEST_HARDLINK_LOCKS"] = \ |
| os.environ["__PORTAGE_TEST_HARDLINK_LOCKS"] |
| |
| dirs = [distdir, fake_bin, portage_tmpdir, |
| var_cache_edb] |
| etc_symlinks = ("dispatch-conf.conf", "etc-update.conf") |
| # Override things that may be unavailable, or may have portability |
| # issues when running tests in exotic environments. |
| # prepstrip - bug #447810 (bash read builtin EINTR problem) |
| true_symlinks = ["prepstrip", "scanelf"] |
| true_binary = find_binary("true") |
| self.assertEqual(true_binary is None, False, |
| "true command not found") |
| try: |
| for d in dirs: |
| ensure_dirs(d) |
| for x in true_symlinks: |
| os.symlink(true_binary, os.path.join(fake_bin, x)) |
| for x in etc_symlinks: |
| os.symlink(os.path.join(self.cnf_etc_path, x), |
| os.path.join(eprefix, "etc", x)) |
| with open(os.path.join(var_cache_edb, "counter"), 'wb') as f: |
| f.write(b"100") |
| |
| if debug: |
| # The subprocess inherits both stdout and stderr, for |
| # debugging purposes. |
| stdout = None |
| else: |
| # The subprocess inherits stderr so that any warnings |
| # triggered by python -Wd will be visible. |
| stdout = subprocess.PIPE |
| |
| for args in test_commands: |
| |
| if hasattr(args, '__call__'): |
| args() |
| continue |
| |
| if isinstance(args[0], dict): |
| local_env = env.copy() |
| local_env.update(args[0]) |
| args = args[1:] |
| else: |
| local_env = env |
| |
| proc = subprocess.Popen(args, |
| env=local_env, stdout=stdout) |
| |
| if debug: |
| proc.wait() |
| else: |
| output = proc.stdout.readlines() |
| proc.wait() |
| proc.stdout.close() |
| if proc.returncode != os.EX_OK: |
| for line in output: |
| sys.stderr.write(_unicode_decode(line)) |
| |
| self.assertEqual(os.EX_OK, proc.returncode, |
| "emerge failed with args %s" % (args,)) |
| finally: |
| playground.cleanup() |