Use a regular pipe to transport apt's output
[mirror/dsa-puppet.git] / modules / porterbox / files / dd-schroot-cmd
1 #!/usr/bin/python
2
3 ##
4 ## THIS FILE IS UNDER PUPPET CONTROL. DON'T EDIT IT HERE.
5 ## USE: git clone git+ssh://$USER@puppet.debian.org/srv/puppet.debian.org/git/dsa-puppet.git
6 ##
7
8
9 # Copyright (c) 2013 Peter Palfrader <peter@palfrader.org>
10 #
11 # Permission is hereby granted, free of charge, to any person obtaining
12 # a copy of this software and associated documentation files (the
13 # "Software"), to deal in the Software without restriction, including
14 # without limitation the rights to use, copy, modify, merge, publish,
15 # distribute, sublicense, and/or sell copies of the Software, and to
16 # permit persons to whom the Software is furnished to do so, subject to
17 # the following conditions:
18 #
19 # The above copyright notice and this permission notice shall be
20 # included in all copies or substantial portions of the Software.
21 #
22 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29
30 # script to allow otherwise unprivileged users to do certain
31 # apt commands in schroot environments.
32
33 # bugs:
34 #  - ownership of the schroot session is only checked at the beginning.
35 #    This means that if the original user deleted it, and then somebody
36 #    else comes along and creates a session of the same name, they might
37 #    get some of our commands run in there.
38
39 import ConfigParser
40 import optparse
41 import os
42 import pipes
43 import platform
44 import pty
45 import re
46 import stat
47 import subprocess
48 import sys
49 from errno import EIO
50
51 SCHROOT_SUPER_UID = 0
52 SCHROOT_SUPER = 'root'
53
54 def die(s):
55     print >> sys.stderr, s
56     sys.exit(1)
57
58 def get_session_owner(session):
59     if re.search('[^0-9a-zA-Z_-]', session):
60         die("Invalid session name.")
61
62     path = os.path.join('/var/lib/schroot/session', session)
63     config = ConfigParser.RawConfigParser()
64     config.read(path)
65     owner = []
66     try:
67         owner.append(config.get(session, 'users'))
68         owner.append(config.get(session, 'root-users'))
69     except ConfigParser.NoSectionError:
70         die("Did not find session definition in session file.")
71     except ConfigParser.NoOptionError:
72         die("Did not find user information in session file.")
73     return owner
74
75
76 def ensure_ok(session):
77     if 'SUDO_USER' not in os.environ:
78         die("Cannot find SUDO_USER in environment.")
79     if not os.environ['SUDO_USER'] in get_session_owner(session):
80         die("Session owner mismatch.")
81
82 def os_supports_unshare():
83     if platform.uname()[0] == 'GNU/kFreeBSD':
84         return False
85     return True
86
87 class WrappedRunner():
88     def __init__(self, session, args, unshare=True):
89         self.unshare = unshare
90         if not os_supports_unshare(): self.unshare = False
91         s,r = self.run('schroot', '-c', session, '--run-session', '--', 'env', 'DEBIAN_FRONTEND=noninteractive', *args)
92         if s != 0:
93             die("Command %s exited due to signal %d."%(' '.join(args), s))
94         if r != 0:
95             die("Command %s exited with exit code %d."%(' '.join(args), r))
96
97     @staticmethod
98     def get_ret(status):
99         signal = status & 0xff
100         if signal == 0: retcode = status > 8
101         else:           retcode = 0
102         return signal, retcode
103
104     def run(self, *cmd):
105         if self.unshare:
106             cmdstr = ' '.join(pipes.quote(s) for s in cmd)
107             cmd = ['unshare', '--uts', '--ipc', '--net', '--']
108             cmd += ['sh', '-c', 'ip addr add 127.0.0.1/8 dev lo && ip link set dev lo up && %s'%(cmdstr)]
109         (r, w) = os.pipe()
110         pid, ptyfd = pty.fork()
111         if pid == pty.CHILD:
112             os.close(r)
113             fd = os.open("/dev/null", os.O_RDWR)
114             os.dup2(fd, 0) # stdin
115             os.dup2(w, 1) # stdout
116             os.dup2(w, 2) # stderr
117             os.execlp(cmd[0], *cmd)
118         os.close(w)
119         try:
120             while 1:
121                 b = os.read(r, 1)
122                 if b == "": break
123                 sys.stdout.write(b)
124         except OSError, e:
125             if e[0] == EIO: pass
126             else: raise
127         os.close(r)
128         os.close(ptyfd) # we don't care about that one
129         p,v = os.waitpid(pid, 0)
130         s,r = WrappedRunner.get_ret(v)
131         return s,r
132
133 class AptSchroot:
134     APT_DRY = ['apt-get', '--dry-run']
135     APT_REAL = ['apt-get', '--assume-yes', '-o', 'Dpkg::Options::=--force-confnew']
136
137     def __init__(self, options, args):
138         self.session = options.chroot
139         self.assume_yes = options.assume_yes
140         if len(args) < 1:
141             die("No operation given for apt.")
142         op = args.pop(0)
143         self.args = args
144
145         if op == "update":
146             self.ensure_no_extra_args()
147             self.apt_update()
148         elif op == "upgrade":
149             self.ensure_no_extra_args()
150             self.apt_upgrade()
151         elif op == "dist-upgrade":
152             self.ensure_no_extra_args()
153             self.apt_dist_upgrade()
154         elif op == "install":
155             self.apt_install(args)
156         elif op == "build-dep":
157             self.apt_build_dep(args)
158         else:
159             die("Invalid operation %s"%(op,))
160
161     def ensure_no_extra_args(self):
162         if len(self.args) > 0:
163             die("superfluous arguments: %s"%(' '.join(self.args),))
164
165     def apt_update(self):
166         self.secure_run(AptSchroot.APT_REAL +['update'], unshare=False)
167
168     def apt_upgrade(self):
169         self.apt_simulate_and_ask(['upgrade'])
170
171     def apt_dist_upgrade(self):
172         self.apt_simulate_and_ask(['dist-upgrade'])
173
174     def apt_install(self, packages):
175         self.apt_simulate_and_ask(['install', '--'] + packages)
176
177     def apt_build_dep(self, packages):
178         self.apt_simulate_and_ask(['build-dep', '--'] + packages)
179
180     def apt_simulate_and_ask(self, cmd, split_download=True, run_clean=True):
181         if not self.assume_yes:
182             self.secure_run(AptSchroot.APT_DRY + cmd)
183             ans = raw_input("Do it for real [Y/n]: ")
184             if ans.lower() == 'n': sys.exit(0)
185         if split_download:
186             self.secure_run(AptSchroot.APT_REAL + ['--download-only'] +  cmd, unshare=False)
187         self.secure_run(AptSchroot.APT_REAL + cmd)
188         if run_clean:
189             self.secure_run(AptSchroot.APT_REAL + ['clean'])
190
191     def secure_run(self, args, unshare=True):
192         WrappedRunner(self.session, args, unshare)
193
194
195 parser = optparse.OptionParser()
196 parser.set_usage("""%prog [options] -c <session-chroot> [-y] -- <command>
197     Available commands:
198        apt-get update
199        apt-get upgrade
200        apt-get dist-upgrade
201        apt-get install <package> ...
202        apt-get build-dep <package> ...""")
203 parser.add_option("-c", "--chroot", metavar="chroot", dest="chroot",
204     help="Which chroot to act on")
205 parser.add_option("-y", "--assume-yes",  dest="assume_yes", default=False,
206     action="store_true", help="Assume yes on confirm questions.")
207
208 (options, args) = parser.parse_args()
209
210 if len(args) < 1 or options.chroot is None:
211     parser.print_help()
212     sys.exit(1)
213
214 if os.getuid() != SCHROOT_SUPER_UID:
215     os.execlp('sudo', 'sudo', '-u', SCHROOT_SUPER, '--', *sys.argv)
216
217 ensure_ok(options.chroot)
218
219 command = args.pop(0)
220 if command == "apt-get":
221     AptSchroot(options, args)
222 else:
223     die("Invalid command: %s."%(command,))