| # 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 |
| |
| import os, shutil, subprocess, sys |
| |
| import portage |
| from portage.env.loaders import KeyValuePairFileLoader |
| from portage.localization import _ |
| from portage.util import shlex_split, varexpand |
| |
| 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'" |
| |
| 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 http://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 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 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 |
| |
| if os.path.isfile(curconf): |
| try: |
| shutil.copy2(curconf, archive) |
| except(IOError, os.error) as why: |
| print(_('dispatch-conf: Error copying %(curconf)s to %(archive)s: %(reason)s; fatal') % \ |
| {"curconf": curconf, "archive": archive, "reason": str(why)}, file=sys.stderr) |
| |
| if os.path.exists(archive + ',v'): |
| os.system(RCS_LOCK + ' ' + archive) |
| os.system(RCS_PUT + ' ' + archive) |
| |
| ret = 0 |
| if newconf != '': |
| os.system(RCS_GET + ' -r' + RCS_BRANCH + ' ' + archive) |
| has_branch = os.path.exists(archive) |
| if has_branch: |
| os.rename(archive, archive + '.dist') |
| |
| try: |
| shutil.copy2(newconf, archive) |
| except(IOError, os.error) as why: |
| print(_('dispatch-conf: Error copying %(newconf)s to %(archive)s: %(reason)s; fatal') % \ |
| {"newconf": newconf, "archive": archive, "reason": str(why)}, file=sys.stderr) |
| |
| if has_branch: |
| if mrgconf != '': |
| # This puts the results of the merge into mrgconf. |
| ret = os.system(RCS_MERGE % (archive, mrgconf)) |
| mystat = os.lstat(newconf) |
| 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(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).""" |
| |
| try: |
| os.makedirs(os.path.dirname(archive)) |
| except OSError: |
| pass |
| |
| # Archive the current config file if it isn't already saved |
| if (os.path.exists(archive) and |
| len(diffstatusoutput("diff -aq '%s' '%s'", curconf, archive)[1]) != 0): |
| suf = 1 |
| while suf < 9 and os.path.exists(archive + '.' + str(suf)): |
| suf += 1 |
| |
| while suf > 1: |
| os.rename(archive + '.' + str(suf-1), archive + '.' + str(suf)) |
| suf -= 1 |
| |
| os.rename(archive, archive + '.1') |
| |
| if os.path.isfile(curconf): |
| try: |
| shutil.copy2(curconf, archive) |
| except(IOError, os.error) as why: |
| print(_('dispatch-conf: Error copying %(curconf)s to %(archive)s: %(reason)s; fatal') % \ |
| {"curconf": curconf, "archive": archive, "reason": str(why)}, file=sys.stderr) |
| |
| if newconf != '': |
| # Save off new config file in the archive dir with .dist.new suffix |
| try: |
| shutil.copy2(newconf, archive + '.dist.new') |
| except(IOError, os.error) as why: |
| print(_('dispatch-conf: Error copying %(newconf)s to %(archive)s: %(reason)s; fatal') % \ |
| {"newconf": newconf, "archive": archive + '.dist.new', "reason": str(why)}, file=sys.stderr) |
| |
| ret = 0 |
| if mrgconf != '' and os.path.exists(archive + '.dist'): |
| # This puts the results of the merge into mrgconf. |
| ret = os.system(DIFF3_MERGE % (curconf, archive + '.dist', newconf, mrgconf)) |
| mystat = os.lstat(newconf) |
| 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.exists(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.exists(archive + '.dist.new'): |
| os.rename(archive + '.dist.new', archive + '.dist') |