Use portage.subprocess_getstatusoutput() more.
[portage.git] / pym / portage / dispatch_conf.py
1 # archive_conf.py -- functionality common to archive-conf and dispatch-conf
2 # Copyright 2003-2011 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5
6 # Library by Wayne Davison <gentoo@blorf.net>, derived from code
7 # written by Jeremy Wohl (http://igmus.org)
8
9 from __future__ import print_function
10
11 import os, sys, shutil
12
13 import portage
14 from portage.env.loaders import KeyValuePairFileLoader
15 from portage.localization import _
16
17 RCS_BRANCH = '1.1.1'
18 RCS_LOCK = 'rcs -ko -M -l'
19 RCS_PUT = 'ci -t-"Archived config file." -m"dispatch-conf update."'
20 RCS_GET = 'co'
21 RCS_MERGE = "rcsmerge -p -r" + RCS_BRANCH + " '%s' > '%s'"
22
23 DIFF3_MERGE = "diff3 -mE '%s' '%s' '%s' > '%s'"
24
25 def diffstatusoutput_len(cmd):
26     """
27     Execute the string cmd in a shell with getstatusoutput() and return a
28     2-tuple (status, output_length). If getstatusoutput() raises
29     UnicodeDecodeError (known to happen with python3.1), return a
30     2-tuple (1, 1). This provides a simple way to check for non-zero
31     output length of diff commands, while providing simple handling of
32     UnicodeDecodeError when necessary.
33     """
34     try:
35         status, output = portage.subprocess_getstatusoutput(cmd)
36         return (status, len(output))
37     except UnicodeDecodeError:
38         return (1, 1)
39
40 def read_config(mandatory_opts):
41     loader = KeyValuePairFileLoader(
42         '/etc/dispatch-conf.conf', None)
43     opts, errors = loader.load()
44     if not opts:
45         print(_('dispatch-conf: Error reading /etc/dispatch-conf.conf; fatal'), file=sys.stderr)
46         sys.exit(1)
47
48         # Handle quote removal here, since KeyValuePairFileLoader doesn't do that.
49     quotes = "\"'"
50     for k, v in opts.items():
51         if v[:1] in quotes and v[:1] == v[-1:]:
52             opts[k] = v[1:-1]
53
54     for key in mandatory_opts:
55         if key not in opts:
56             if key == "merge":
57                 opts["merge"] = "sdiff --suppress-common-lines --output='%s' '%s' '%s'"
58             else:
59                 print(_('dispatch-conf: Missing option "%s" in /etc/dispatch-conf.conf; fatal') % (key,), file=sys.stderr)
60
61     if not os.path.exists(opts['archive-dir']):
62         os.mkdir(opts['archive-dir'])
63         # Use restrictive permissions by default, in order to protect
64         # against vulnerabilities (like bug #315603 involving rcs).
65         os.chmod(opts['archive-dir'], 0o700)
66     elif not os.path.isdir(opts['archive-dir']):
67         print(_('dispatch-conf: Config archive dir [%s] must exist; fatal') % (opts['archive-dir'],), file=sys.stderr)
68         sys.exit(1)
69
70     return opts
71
72
73 def rcs_archive(archive, curconf, newconf, mrgconf):
74     """Archive existing config in rcs (on trunk). Then, if mrgconf is
75     specified and an old branch version exists, merge the user's changes
76     and the distributed changes and put the result into mrgconf.  Lastly,
77     if newconf was specified, leave it in the archive dir with a .dist.new
78     suffix along with the last 1.1.1 branch version with a .dist suffix."""
79
80     try:
81         os.makedirs(os.path.dirname(archive))
82     except OSError:
83         pass
84
85     if os.path.isfile(curconf):
86         try:
87             shutil.copy2(curconf, archive)
88         except(IOError, os.error) as why:
89             print(_('dispatch-conf: Error copying %(curconf)s to %(archive)s: %(reason)s; fatal') % \
90                 {"curconf": curconf, "archive": archive, "reason": str(why)}, file=sys.stderr)
91
92     if os.path.exists(archive + ',v'):
93         os.system(RCS_LOCK + ' ' + archive)
94     os.system(RCS_PUT + ' ' + archive)
95
96     ret = 0
97     if newconf != '':
98         os.system(RCS_GET + ' -r' + RCS_BRANCH + ' ' + archive)
99         has_branch = os.path.exists(archive)
100         if has_branch:
101             os.rename(archive, archive + '.dist')
102
103         try:
104             shutil.copy2(newconf, archive)
105         except(IOError, os.error) as why:
106             print(_('dispatch-conf: Error copying %(newconf)s to %(archive)s: %(reason)s; fatal') % \
107                   {"newconf": newconf, "archive": archive, "reason": str(why)}, file=sys.stderr)
108
109         if has_branch:
110             if mrgconf != '':
111                 # This puts the results of the merge into mrgconf.
112                 ret = os.system(RCS_MERGE % (archive, mrgconf))
113                 mystat = os.lstat(newconf)
114                 os.chmod(mrgconf, mystat.st_mode)
115                 os.chown(mrgconf, mystat.st_uid, mystat.st_gid)
116         os.rename(archive, archive + '.dist.new')
117     return ret
118
119
120 def file_archive(archive, curconf, newconf, mrgconf):
121     """Archive existing config to the archive-dir, bumping old versions
122     out of the way into .# versions (log-rotate style). Then, if mrgconf
123     was specified and there is a .dist version, merge the user's changes
124     and the distributed changes and put the result into mrgconf.  Lastly,
125     if newconf was specified, archive it as a .dist.new version (which
126     gets moved to the .dist version at the end of the processing)."""
127
128     try:
129         os.makedirs(os.path.dirname(archive))
130     except OSError:
131         pass
132
133     # Archive the current config file if it isn't already saved
134     if os.path.exists(archive) \
135      and diffstatusoutput_len("diff -aq '%s' '%s'" % (curconf,archive))[1] != 0:
136         suf = 1
137         while suf < 9 and os.path.exists(archive + '.' + str(suf)):
138             suf += 1
139
140         while suf > 1:
141             os.rename(archive + '.' + str(suf-1), archive + '.' + str(suf))
142             suf -= 1
143
144         os.rename(archive, archive + '.1')
145
146     if os.path.isfile(curconf):
147         try:
148             shutil.copy2(curconf, archive)
149         except(IOError, os.error) as why:
150             print(_('dispatch-conf: Error copying %(curconf)s to %(archive)s: %(reason)s; fatal') % \
151                 {"curconf": curconf, "archive": archive, "reason": str(why)}, file=sys.stderr)
152
153     if newconf != '':
154         # Save off new config file in the archive dir with .dist.new suffix
155         try:
156             shutil.copy2(newconf, archive + '.dist.new')
157         except(IOError, os.error) as why:
158             print(_('dispatch-conf: Error copying %(newconf)s to %(archive)s: %(reason)s; fatal') % \
159                   {"newconf": newconf, "archive": archive + '.dist.new', "reason": str(why)}, file=sys.stderr)
160
161         ret = 0
162         if mrgconf != '' and os.path.exists(archive + '.dist'):
163             # This puts the results of the merge into mrgconf.
164             ret = os.system(DIFF3_MERGE % (curconf, archive + '.dist', newconf, mrgconf))
165             mystat = os.lstat(newconf)
166             os.chmod(mrgconf, mystat.st_mode)
167             os.chown(mrgconf, mystat.st_uid, mystat.st_gid)
168
169         return ret
170
171
172 def rcs_archive_post_process(archive):
173     """Check in the archive file with the .dist.new suffix on the branch
174     and remove the one with the .dist suffix."""
175     os.rename(archive + '.dist.new', archive)
176     if os.path.exists(archive + '.dist'):
177         # Commit the last-distributed version onto the branch.
178         os.system(RCS_LOCK + RCS_BRANCH + ' ' + archive)
179         os.system(RCS_PUT + ' -r' + RCS_BRANCH + ' ' + archive)
180         os.unlink(archive + '.dist')
181     else:
182         # Forcefully commit the last-distributed version onto the branch.
183         os.system(RCS_PUT + ' -f -r' + RCS_BRANCH + ' ' + archive)
184
185
186 def file_archive_post_process(archive):
187     """Rename the archive file with the .dist.new suffix to a .dist suffix"""
188     os.rename(archive + '.dist.new', archive + '.dist')