]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/dd-schroot-cmd
Allow a different naming scheme
[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         pid, fd = pty.fork()
110         if pid == pty.CHILD:
111             fd = os.open("/dev/null", os.O_RDWR)
112             os.dup2(fd, 0) # stdin
113             os.execlp(cmd[0], *cmd)
114         try:
115             while 1:
116                 b = os.read(fd, 1)
117                 if b == "": break
118                 sys.stdout.write(b)
119         except OSError, e:
120             if e[0] == EIO: pass
121             else: raise
122         os.close(fd)
123         p,v = os.waitpid(pid, 0)
124         s,r = WrappedRunner.get_ret(v)
125         return s,r
126
127 class AptSchroot:
128     APT_DRY = ['apt-get', '--dry-run']
129     APT_REAL = ['apt-get', '--assume-yes', '-o', 'Dpkg::Options::=--force-confnew']
130
131     def __init__(self, options, args):
132         self.session = options.chroot
133         self.assume_yes = options.assume_yes
134         if len(args) < 1:
135             die("No operation given for apt.")
136         op = args.pop(0)
137         self.args = args
138
139         if op == "update":
140             self.ensure_no_extra_args()
141             self.apt_update()
142         elif op == "upgrade":
143             self.ensure_no_extra_args()
144             self.apt_upgrade()
145         elif op == "dist-upgrade":
146             self.ensure_no_extra_args()
147             self.apt_dist_upgrade()
148         elif op == "install":
149             self.apt_install(args)
150         elif op == "build-dep":
151             self.apt_build_dep(args)
152         else:
153             die("Invalid operation %s"%(op,))
154
155     def ensure_no_extra_args(self):
156         if len(self.args) > 0:
157             die("superfluous arguments: %s"%(' '.join(self.args),))
158
159     def apt_update(self):
160         self.secure_run(AptSchroot.APT_REAL +['update'], unshare=False)
161
162     def apt_upgrade(self):
163         self.apt_simulate_and_ask(['upgrade'])
164
165     def apt_dist_upgrade(self):
166         self.apt_simulate_and_ask(['dist-upgrade'])
167
168     def apt_install(self, packages):
169         self.apt_simulate_and_ask(['install', '--'] + packages)
170
171     def apt_build_dep(self, packages):
172         self.apt_simulate_and_ask(['build-dep', '--'] + packages)
173
174     def apt_simulate_and_ask(self, cmd, split_download=True, run_clean=True):
175         if not self.assume_yes:
176             self.secure_run(AptSchroot.APT_DRY + cmd)
177             ans = raw_input("Do it for real [Y/n]: ")
178             if ans.lower() == 'n': sys.exit(0)
179         if split_download:
180             self.secure_run(AptSchroot.APT_REAL + ['--download-only'] +  cmd, unshare=False)
181         self.secure_run(AptSchroot.APT_REAL + cmd)
182         if run_clean:
183             self.secure_run(AptSchroot.APT_REAL + ['clean'])
184
185     def secure_run(self, args, unshare=True):
186         WrappedRunner(self.session, args, unshare)
187
188
189 parser = optparse.OptionParser()
190 parser.set_usage("""%prog [options] -c <session-chroot> [-y] -- <command>
191     Available commands:
192        apt-get update
193        apt-get upgrade
194        apt-get dist-upgrade
195        apt-get install <package> ...
196        apt-get build-dep <package> ...""")
197 parser.add_option("-c", "--chroot", metavar="chroot", dest="chroot",
198     help="Which chroot to act on")
199 parser.add_option("-y", "--assume-yes",  dest="assume_yes", default=False,
200     action="store_true", help="Assume yes on confirm questions.")
201
202 (options, args) = parser.parse_args()
203
204 if len(args) < 1 or options.chroot is None:
205     parser.print_help()
206     sys.exit(1)
207
208 if os.getuid() != SCHROOT_SUPER_UID:
209     os.execlp('sudo', 'sudo', '-u', SCHROOT_SUPER, '--', *sys.argv)
210
211 ensure_ok(options.chroot)
212
213 command = args.pop(0)
214 if command == "apt-get":
215     AptSchroot(options, args)
216 else:
217     die("Invalid command: %s."%(command,))