source: branches/fc19-dev/host/credit-card/host.py

Last change on this file 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.