| # archive_conf.py -- functionality common to archive-conf and dispatch-conf |
| # Copyright 2003-2014 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| |
| # Library by Wayne Davison <gentoo@blorf.net>, derived from code |
| # written by Jeremy Wohl (http://igmus.org) |
| |
| from __future__ import print_function, unicode_literals |
| |
| import errno |
| import io |
| import functools |
| import stat |
| import subprocess |
| import sys |
| import tempfile |
| |
| import portage |
| from portage import _encodings, os, shutil |
| from portage.env.loaders import KeyValuePairFileLoader |
| from portage.localization import _ |
| from portage.util import shlex_split, varexpand |
| from portage.util.path import iter_parents |
| |
| RCS_BRANCH = '1.1.1' |
| RCS_LOCK = 'rcs -ko -M -l' |
| RCS_PUT = 'ci -t-"Archived config file." -m"dispatch-conf update."' |
| RCS_GET = 'co' |
| RCS_MERGE = "rcsmerge -p -r" + RCS_BRANCH + " '%s' > '%s'" |
| |
| DIFF3_MERGE = "diff3 -mE '%s' '%s' '%s' > '%s'" |
| _ARCHIVE_ROTATE_MAX = 9 |
| |
| def diffstatusoutput(cmd, file1, file2): |
| """ |
| Execute the string cmd in a shell with getstatusoutput() and return a |
| 2-tuple (status, output). |
| """ |
| # Use Popen to emulate getstatusoutput(), since getstatusoutput() may |
| # raise a UnicodeDecodeError which makes the output inaccessible. |
| args = shlex_split(cmd % (file1, file2)) |
| |
| if sys.hexversion < 0x3020000 and sys.hexversion >= 0x3000000 and \ |
| not os.path.isabs(args[0]): |
| # Python 3.1 _execvp throws TypeError for non-absolute executable |
| # path passed as bytes (see https://bugs.python.org/issue8513). |
| fullname = portage.process.find_binary(args[0]) |
| if fullname is None: |
| raise portage.exception.CommandNotFound(args[0]) |
| args[0] = fullname |
| |
| args = [portage._unicode_encode(x, errors='strict') for x in args] |
| proc = subprocess.Popen(args, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| output = portage._unicode_decode(proc.communicate()[0]) |
| if output and output[-1] == "\n": |
| # getstatusoutput strips one newline |
| output = output[:-1] |
| return (proc.wait(), output) |
| |
| def diff_mixed(func, file1, file2): |
| tempdir = None |
| try: |
| if os.path.islink(file1) and \ |
| not os.path.islink(file2) and \ |
| os.path.isfile(file1) and \ |
| os.path.isfile(file2): |
| # If a regular file replaces a symlink to a regular |
| # file, then show the diff between the regular files |
| # (bug #330221). |
| diff_files = (file2, file2) |
| else: |
| files = [file1, file2] |
| diff_files = [file1, file2] |
| for i in range(len(diff_files)): |
| try: |
| st = os.lstat(diff_files[i]) |
| except OSError: |
| st = None |
| if st is not None and stat.S_ISREG(st.st_mode): |
| continue |
| |
| if tempdir is None: |
| tempdir = tempfile.mkdtemp() |
| diff_files[i] = os.path.join(tempdir, "%d" % i) |
| if st is None: |
| content = "/dev/null\n" |
| elif stat.S_ISLNK(st.st_mode): |
| link_dest = os.readlink(files[i]) |
| content = "SYM: %s -> %s\n" % \ |
| (file1, link_dest) |
| elif stat.S_ISDIR(st.st_mode): |
| content = "DIR: %s\n" % (file1,) |
| elif stat.S_ISFIFO(st.st_mode): |
| content = "FIF: %s\n" % (file1,) |
| else: |
| content = "DEV: %s\n" % (file1,) |
| with io.open(diff_files[i], mode='w', |
| encoding=_encodings['stdio']) as f: |
| f.write(content) |
| |
| return func(diff_files[0], diff_files[1]) |
| |
| finally: |
| if tempdir is not None: |
| shutil.rmtree(tempdir) |
| |
| class diff_mixed_wrapper(object): |
| |
| def __init__(self, f, *args): |
| self._func = f |
| self._args = args |
| |
| def __call__(self, *args): |
| return diff_mixed( |
| functools.partial(self._func, *(self._args + args[:-2])), |
| *args[-2:]) |
| |
| diffstatusoutput_mixed = diff_mixed_wrapper(diffstatusoutput) |
| |
| def read_config(mandatory_opts): |
| eprefix = portage.settings["EPREFIX"] |
| if portage._not_installed: |
| config_path = os.path.join(portage.PORTAGE_BASE_PATH, "cnf", "dispatch-conf.conf") |
| else: |
| config_path = os.path.join(eprefix or os.sep, "etc/dispatch-conf.conf") |
| loader = KeyValuePairFileLoader(config_path, None) |
| opts, _errors = loader.load() |
| if not opts: |
| print(_('dispatch-conf: Error reading /etc/dispatch-conf.conf; fatal'), file=sys.stderr) |
| sys.exit(1) |
| |
| # Handle quote removal here, since KeyValuePairFileLoader doesn't do that. |
| quotes = "\"'" |
| for k, v in opts.items(): |
| if v[:1] in quotes and v[:1] == v[-1:]: |
| opts[k] = v[1:-1] |
| |
| for key in mandatory_opts: |
| if key not in opts: |
| if key == "merge": |
| opts["merge"] = "sdiff --suppress-common-lines --output='%s' '%s' '%s'" |
| else: |
| print(_('dispatch-conf: Missing option "%s" in /etc/dispatch-conf.conf; fatal') % (key,), file=sys.stderr) |
| |
| # archive-dir supports ${EPREFIX} expansion, in order to avoid hardcoding |
| variables = {"EPREFIX": eprefix} |
| opts['archive-dir'] = varexpand(opts['archive-dir'], mydict=variables) |
| |
| if not os.path.exists(opts['archive-dir']): |
| os.mkdir(opts['archive-dir']) |
| # Use restrictive permissions by default, in order to protect |
| # against vulnerabilities (like bug #315603 involving rcs). |
| os.chmod(opts['archive-dir'], 0o700) |
| elif not os.path.isdir(opts['archive-dir']): |
| print(_('dispatch-conf: Config archive dir [%s] must exist; fatal') % (opts['archive-dir'],), file=sys.stderr) |
| sys.exit(1) |
| |
| return opts |
| |
| def _archive_copy(src_st, src_path, dest_path): |
| """ |
| Copy file from src_path to dest_path. Regular files and symlinks |
| are supported. If an EnvironmentError occurs, then it is logged |
| to stderr. |
| |
| @param src_st: source file lstat result |
| @type src_st: posix.stat_result |
| @param src_path: source file path |
| @type src_path: str |
| @param dest_path: destination file path |
| @type dest_path: str |
| """ |
| # Remove destination file in order to ensure that the following |
| # symlink or copy2 call won't fail (see bug #535850). |
| try: |
| os.unlink(dest_path) |
| except OSError: |
| pass |
| try: |
| if stat.S_ISLNK(src_st.st_mode): |
| os.symlink(os.readlink(src_path), dest_path) |
| else: |
| shutil.copy2(src_path, dest_path) |
| except EnvironmentError as e: |
| portage.util.writemsg( |
| _('dispatch-conf: Error copying %(src_path)s to ' |
| '%(dest_path)s: %(reason)s\n') % { |
| "src_path": src_path, |
| "dest_path": dest_path, |
| "reason": e |
| }, noiselevel=-1) |
| |
| def rcs_archive(archive, curconf, newconf, mrgconf): |
| """Archive existing config in rcs (on trunk). Then, if mrgconf is |
| specified and an old branch version exists, merge the user's changes |
| and the distributed changes and put the result into mrgconf. Lastly, |
| if newconf was specified, leave it in the archive dir with a .dist.new |
| suffix along with the last 1.1.1 branch version with a .dist suffix.""" |
| |
| try: |
| os.makedirs(os.path.dirname(archive)) |
| except OSError: |
| pass |
| |
| try: |
| curconf_st = os.lstat(curconf) |
| except OSError: |
| curconf_st = None |
| |
| if curconf_st is not None and \ |
| (stat.S_ISREG(curconf_st.st_mode) or |
| stat.S_ISLNK(curconf_st.st_mode)): |
| _archive_copy(curconf_st, curconf, archive) |
| |
| if os.path.lexists(archive + ',v'): |
| os.system(RCS_LOCK + ' ' + archive) |
| os.system(RCS_PUT + ' ' + archive) |
| |
| ret = 0 |
| mystat = None |
| if newconf: |
| try: |
| mystat = os.lstat(newconf) |
| except OSError: |
| pass |
| |
| if mystat is not None and \ |
| (stat.S_ISREG(mystat.st_mode) or |
| stat.S_ISLNK(mystat.st_mode)): |
| os.system(RCS_GET + ' -r' + RCS_BRANCH + ' ' + archive) |
| has_branch = os.path.lexists(archive) |
| if has_branch: |
| os.rename(archive, archive + '.dist') |
| |
| _archive_copy(mystat, newconf, archive) |
| |
| if has_branch: |
| if mrgconf and os.path.isfile(archive) and \ |
| os.path.isfile(mrgconf): |
| # This puts the results of the merge into mrgconf. |
| ret = os.system(RCS_MERGE % (archive, mrgconf)) |
| os.chmod(mrgconf, mystat.st_mode) |
| os.chown(mrgconf, mystat.st_uid, mystat.st_gid) |
| os.rename(archive, archive + '.dist.new') |
| |
| return ret |
| |
| def _file_archive_rotate(archive): |
| """ |
| Rename archive to archive + '.1', and perform similar rotation |
| for files up to archive + '.9'. |
| |
| @param archive: file path to archive |
| @type archive: str |
| """ |
| |
| max_suf = 0 |
| try: |
| for max_suf, max_st, max_path in ( |
| (suf, os.lstat(path), path) for suf, path in ( |
| (suf, "%s.%s" % (archive, suf)) for suf in range( |
| 1, _ARCHIVE_ROTATE_MAX + 1))): |
| pass |
| except OSError as e: |
| if e.errno not in (errno.ENOENT, errno.ESTALE): |
| raise |
| # There's already an unused suffix. |
| else: |
| # Free the max suffix in order to avoid possible problems |
| # when we rename another file or directory to the same |
| # location (see bug 256376). |
| if stat.S_ISDIR(max_st.st_mode): |
| # Removing a directory might destroy something important, |
| # so rename it instead. |
| head, tail = os.path.split(archive) |
| placeholder = tempfile.NamedTemporaryFile( |
| prefix="%s." % tail, |
| dir=head) |
| placeholder.close() |
| os.rename(max_path, placeholder.name) |
| else: |
| os.unlink(max_path) |
| |
| # The max suffix is now unused. |
| max_suf -= 1 |
| |
| for suf in range(max_suf + 1, 1, -1): |
| os.rename("%s.%s" % (archive, suf - 1), "%s.%s" % (archive, suf)) |
| |
| os.rename(archive, "%s.1" % (archive,)) |
| |
| def _file_archive_ensure_dir(parent_dir): |
| """ |
| Ensure that the parent directory for an archive exists. |
| If a file exists where a directory is needed, then rename |
| it (see bug 256376). |
| |
| @param parent_dir: path of parent directory |
| @type parent_dir: str |
| """ |
| |
| for parent in iter_parents(parent_dir): |
| # Use lstat because a symlink to a directory might point |
| # to a directory outside of the config archive, making |
| # it an unsuitable parent. |
| try: |
| parent_st = os.lstat(parent) |
| except OSError: |
| pass |
| else: |
| if not stat.S_ISDIR(parent_st.st_mode): |
| _file_archive_rotate(parent) |
| break |
| |
| try: |
| os.makedirs(parent_dir) |
| except OSError: |
| pass |
| |
| def file_archive(archive, curconf, newconf, mrgconf): |
| """Archive existing config to the archive-dir, bumping old versions |
| out of the way into .# versions (log-rotate style). Then, if mrgconf |
| was specified and there is a .dist version, merge the user's changes |
| and the distributed changes and put the result into mrgconf. Lastly, |
| if newconf was specified, archive it as a .dist.new version (which |
| gets moved to the .dist version at the end of the processing).""" |
| |
| _file_archive_ensure_dir(os.path.dirname(archive)) |
| |
| # Archive the current config file if it isn't already saved |
| if (os.path.lexists(archive) and |
| len(diffstatusoutput_mixed( |
| "diff -aq '%s' '%s'", curconf, archive)[1]) != 0): |
| _file_archive_rotate(archive) |
| |
| try: |
| curconf_st = os.lstat(curconf) |
| except OSError: |
| curconf_st = None |
| |
| if curconf_st is not None and \ |
| (stat.S_ISREG(curconf_st.st_mode) or |
| stat.S_ISLNK(curconf_st.st_mode)): |
| _archive_copy(curconf_st, curconf, archive) |
| |
| mystat = None |
| if newconf: |
| try: |
| mystat = os.lstat(newconf) |
| except OSError: |
| pass |
| |
| if mystat is not None and \ |
| (stat.S_ISREG(mystat.st_mode) or |
| stat.S_ISLNK(mystat.st_mode)): |
| # Save off new config file in the archive dir with .dist.new suffix |
| newconf_archive = archive + '.dist.new' |
| if os.path.isdir(newconf_archive |
| ) and not os.path.islink(newconf_archive): |
| _file_archive_rotate(newconf_archive) |
| _archive_copy(mystat, newconf, newconf_archive) |
| |
| ret = 0 |
| if mrgconf and os.path.isfile(curconf) and \ |
| os.path.isfile(newconf) and \ |
| os.path.isfile(archive + '.dist'): |
| # This puts the results of the merge into mrgconf. |
| ret = os.system(DIFF3_MERGE % (curconf, archive + '.dist', newconf, mrgconf)) |
| os.chmod(mrgconf, mystat.st_mode) |
| os.chown(mrgconf, mystat.st_uid, mystat.st_gid) |
| |
| return ret |
| |
| |
| def rcs_archive_post_process(archive): |
| """Check in the archive file with the .dist.new suffix on the branch |
| and remove the one with the .dist suffix.""" |
| os.rename(archive + '.dist.new', archive) |
| if os.path.lexists(archive + '.dist'): |
| # Commit the last-distributed version onto the branch. |
| os.system(RCS_LOCK + RCS_BRANCH + ' ' + archive) |
| os.system(RCS_PUT + ' -r' + RCS_BRANCH + ' ' + archive) |
| os.unlink(archive + '.dist') |
| else: |
| # Forcefully commit the last-distributed version onto the branch. |
| os.system(RCS_PUT + ' -f -r' + RCS_BRANCH + ' ' + archive) |
| |
| |
| def file_archive_post_process(archive): |
| """Rename the archive file with the .dist.new suffix to a .dist suffix""" |
| if os.path.lexists(archive + '.dist.new'): |
| dest = "%s.dist" % archive |
| if os.path.isdir(dest) and not os.path.islink(dest): |
| _file_archive_rotate(dest) |
| os.rename(archive + '.dist.new', dest) |