source: trunk/host/credit-card/host.py @ 2297

Last change on this file since 2297 was 2297, checked in by ezyang, 10 years ago
Refactor to support pushes to Wizard. This invalidates the old 'common' cache.
File size: 7.8 KB
Line 
1import os
2import optparse
3import socket
4import tempfile
5import shutil
6import errno
7import csv
8
9import shell
10
11HOST = socket.gethostname()
12
13PROD_GUESTS = frozenset([
14    'bees-knees',
15    'cats-whiskers',
16    'busy-beaver',
17    'pancake-bunny',
18    'whole-enchilada',
19    'real-mccoy',
20    'old-faithful',
21    'better-mousetrap',
22    'shining-armor',
23    'golden-egg',
24    'miracle-cure',
25    'lucky-star',
26    ])
27WIZARD_GUESTS = frozenset([
28    'not-backward',
29    ])
30
31COMMON_CREDS = {}
32
33# Format here assumes that we always chmod $USER:$USER,
34# but note the latter refers to group...
35#
36# Important: no leading slashes!
37COMMON_CREDS['all'] = [
38    ('root', 0o600, 'root/.bashrc'),
39    ('root', 0o600, 'root/.screenrc'),
40    ('root', 0o600, 'root/.ssh/authorized_keys'),
41    ('root', 0o600, 'root/.ssh/authorized_keys2'),
42    ('root', 0o600, 'root/.vimrc'),
43    ('root', 0o600, 'root/.k5login'),
44    ]
45
46COMMON_CREDS['prod'] = [
47    ('root', 0o600, 'root/.ldapvirc'),
48    ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'),
49    ('root', 0o600, 'etc/ssh/ssh_host_key'),
50    ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'),
51    ('root', 0o600, 'etc/pki/tls/private/scripts-1024.key'),
52    ('root', 0o600, 'etc/pki/tls/private/scripts.key'),
53    ('root', 0o600, 'etc/whoisd-password'),
54    ('afsagent', 0o600, 'etc/daemon.keytab'),
55
56    ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'),
57    ('root', 0o644, 'etc/ssh/ssh_host_key.pub'),
58    ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'),
59
60    ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'), # technically doesn't have to be secret anymore
61    ('sql', 0o600, 'etc/sql-password'),
62    ('signup', 0o600, 'etc/signup-ldap-pw'),
63    ('logview', 0o600, 'home/logview/.k5login'), # XXX user must be created in Kickstart
64    ]
65
66# note that these are duplicates with 'prod', but the difference
67# is that the files DIFFER between wizard and prod
68COMMON_CREDS['wizard'] = [
69    ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'),
70    ('root', 0o600, 'etc/ssh/ssh_host_key'),
71    ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'),
72    ('afsagent', 0o600, 'etc/daemon.keytab'),
73
74    ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'),
75    ('root', 0o644, 'etc/ssh/ssh_host_key.pub'),
76    ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'),
77    ]
78
79MACHINE_CREDS = {}
80
81MACHINE_CREDS['all'] = [
82    # XXX NEED TO CHECK THAT THE CONTENTS ARE SENSIBLE
83    ('root', 0o600, 'etc/krb5.keytab'),
84    ]
85
86MACHINE_CREDS['prod'] = [
87    ('fedora-ds', 0o600, 'etc/dirsrv/keytab'),
88    ]
89
90MACHINE_CREDS['wizard'] = []
91
92# Works for passwd and group, but be careful! They're different things!
93def lookup(filename):
94    # Super-safe to assume and volume IDs (expensive to check)
95    r = {
96        'root': 0,
97        'sql': 537704221,
98    }
99    with open(filename, 'rb') as f:
100        reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE)
101        for row in reader:
102            r[row[0]] = int(row[2])
103    return r
104
105def drop_caches():
106    with open("/proc/sys/vm/drop_caches", 'w') as f:
107        f.write("1")
108
109def mkdir_p(path): # it's like mkdir -p
110    try:
111        os.makedirs(path)
112    except OSError as e:
113        if e.errno == errno.EEXIST:
114            pass
115        else: raise
116
117# XXX This code is kind of dangerous, because we are directly using the
118# kernel modules to manipulate possibly untrusted disk images.  This
119# means that if an attacker can corrupt the disk, and exploit a problem
120# in the kernel vfs driver, he can escalate a guest root exploit
121# to a host root exploit.  Ultimately we should use libguestfs
122# which makes this attack harder to pull off, but at the time of writing
123# squeeze didn't package libguestfs.
124#
125# We try to minimize attack surface by explicitly specifying the
126# expected filesystem type.
127class WithMount(object):
128    """Context for running code with an extra mountpoint."""
129    guest = None
130    types = None # comma separated, like the mount argument -t
131    mount = None
132    dev = None
133    def __init__(self, guest, types):
134        self.guest = guest
135        self.types = types
136    def __enter__(self):
137        drop_caches()
138        self.dev = "/dev/%s/%s-root" % (HOST, self.guest)
139
140        mapper_name = shell.eval("kpartx", "-l", self.dev).split()[0]
141        shell.call("kpartx", "-a", self.dev)
142        mapper = "/dev/mapper/%s" % mapper_name
143
144        # this is why bracketing functions and hanging lambdas are a good idea
145        try:
146            self.mount = tempfile.mkdtemp("-%s" % self.guest, 'vm-', '/mnt') # no trailing slash
147            try:
148                shell.call("mount", "--types", self.types, mapper, self.mount)
149            except:
150                os.rmdir(self.mount)
151                raise
152        except:
153            shell.call("kpartx", "-d", self.dev)
154            raise
155
156        return self.mount
157    def __exit__(self, _type, _value, _traceback):
158        shell.call("umount", self.mount)
159        os.rmdir(self.mount)
160        shell.call("kpartx", "-d", self.dev)
161        drop_caches()
162
163def main():
164    usage = """usage: %prog [push|pull] [common|machine] GUEST"""
165
166    parser = optparse.OptionParser(usage)
167    # ext3 will probably supported for a while yet and a pretty
168    # reasonable thing to always try
169    parser.add_option('-t', '--types', dest="types", default="ext4,ext3",
170            help="filesystem type(s)") # same arg as 'mount'
171    parser.add_option('--creds-dir', dest="creds_dir", default="/root/creds",
172            help="directory to store/fetch credentials in")
173    options, args = parser.parse_args()
174
175    if not os.path.isdir(options.creds_dir):
176        raise Exception("%s does not exist" % options.creds_dir)
177    # XXX check owned by root and appropriately chmodded
178
179    os.umask(0o077) # overly restrictive
180
181    if len(args) != 3:
182        parser.print_help()
183        raise Exception("Wrong number of arguments")
184
185    command = args[0]
186    files   = args[1]
187    guest   = args[2]
188
189    if guest in PROD_GUESTS:
190        mode = 'prod'
191    elif guest in WIZARD_GUESTS:
192        mode = 'wizard'
193    else:
194        raise Exception("Unrecognized guest %s" % guest)
195
196    with WithMount(guest, options.types) as tmp_mount:
197        uid_lookup = lookup("%s/etc/passwd" % tmp_mount)
198        gid_lookup = lookup("%s/etc/group" % tmp_mount)
199        def push_files(files, type):
200            for (usergroup, perms, f) in files:
201                dest = "%s/%s" % (tmp_mount, f)
202                mkdir_p(os.path.dirname(dest)) # useful for .ssh
203                # assuming OK to overwrite
204                # XXX we could compare the files before doing anything...
205                shutil.copyfile("%s/%s/%s" % (options.creds_dir, type, f), dest)
206                try:
207                    os.chown(dest, uid_lookup[usergroup], gid_lookup[usergroup])
208                    os.chmod(dest, perms)
209                except:
210                    # never ever leave un-chowned files lying around
211                    os.unlink(dest)
212                    raise
213        def pull_files(files, type):
214            for (_, _, f) in files:
215                dest = "%s/%s/%s" % (options.creds_dir, type, f)
216                mkdir_p(os.path.dirname(dest))
217                # error if doesn't exist
218                shutil.copyfile("%s/%s" % (tmp_mount, f), dest)
219
220        # XXX ideally we should check these *before* we mount, but Python
221        # makes that pretty annoying to do
222        if command == "push":
223            run = push_files
224        elif command == "pull":
225            run = pull_files
226        else:
227            raise Exception("Unknown command %s, valid values are 'push' and 'pull'" % command)
228
229        if files == 'common':
230            run(COMMON_CREDS['all'], 'all')
231            run(COMMON_CREDS[mode], mode)
232        elif files == 'machine':
233            run(MACHINE_CREDS['all'], 'machine/%s' % guest)
234            run(MACHINE_CREDS[mode], 'machine/%s' % guest)
235        else:
236            raise Exception("Unknown file set %s, valid values are 'common' and 'machine'" % files)
237
238if __name__ == "__main__":
239    main()
Note: See TracBrowser for help on using the repository browser.