]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/dd-schroot-cmd
gabrielli: decomission
[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] in ('GNU/kFreeBSD', 'GNU'):
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, '--directory=/', '--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             try:
158                 args.remove("--arch-only")
159                 archonly = True
160             except ValueError:
161                 archonly = False
162             self.apt_build_dep(args, archonly)
163         else:
164             die("Invalid operation %s"%(op,))
165
166     def ensure_no_extra_args(self):
167         if len(self.args) > 0:
168             die("superfluous arguments: %s"%(' '.join(self.args),))
169
170     def apt_update(self):
171         self.secure_run(AptSchroot.APT_REAL +['update'], unshare=False)
172
173     def apt_upgrade(self):
174         self.apt_simulate_and_ask(['upgrade'])
175
176     def apt_dist_upgrade(self):
177         self.apt_simulate_and_ask(['dist-upgrade'])
178
179     def apt_install(self, packages):
180         self.apt_simulate_and_ask(['install', '--'] + packages)
181
182     def apt_build_dep(self, packages, archonly=False):
183         cmd = (['--arch-only'] if archonly else []) + ['build-dep', '--']
184         self.apt_simulate_and_ask(cmd + packages)
185
186     def apt_simulate_and_ask(self, cmd, split_download=True, run_clean=True):
187         if not self.assume_yes:
188             self.secure_run(AptSchroot.APT_DRY + cmd)
189             ans = raw_input("Do it for real [Y/n]: ")
190             if ans.lower() == 'n': sys.exit(0)
191         if split_download:
192             self.secure_run(AptSchroot.APT_REAL + ['--download-only'] +  cmd, unshare=False)
193         self.secure_run(AptSchroot.APT_REAL + cmd)
194         if run_clean:
195             self.secure_run(AptSchroot.APT_REAL + ['clean'])
196
197     def secure_run(self, args, unshare=True):
198         WrappedRunner(self.session, args, unshare)
199
200
201 parser = optparse.OptionParser()
202 parser.set_usage("""%prog [options] -c <session-chroot> [-y] -- <command>
203     Available commands:
204        apt-get update
205        apt-get upgrade
206        apt-get dist-upgrade
207        apt-get install <package> ...
208        apt-get build-dep <package> ...""")
209 parser.add_option("-c", "--chroot", metavar="chroot", dest="chroot",
210     help="Which chroot to act on")
211 parser.add_option("-y", "--assume-yes",  dest="assume_yes", default=False,
212     action="store_true", help="Assume yes on confirm questions.")
213
214 (options, args) = parser.parse_args()
215
216 if len(args) < 1 or options.chroot is None:
217     parser.print_help()
218     sys.exit(1)
219
220 if os.getuid() != SCHROOT_SUPER_UID:
221     os.execlp('sudo', 'sudo', '-u', SCHROOT_SUPER, '--', *sys.argv)
222
223 ensure_ok(options.chroot)
224
225 command = args.pop(0)
226 if command == "apt-get":
227     AptSchroot(options, args)
228 else:
229     die("Invalid command: %s."%(command,))