| [1999] | 1 | import os | 
|---|
 | 2 | import optparse | 
|---|
 | 3 | import socket | 
|---|
 | 4 | import tempfile | 
|---|
 | 5 | import shutil | 
|---|
 | 6 | import errno | 
|---|
| [2044] | 7 | import csv | 
|---|
| [1999] | 8 |  | 
|---|
 | 9 | import shell | 
|---|
 | 10 |  | 
|---|
 | 11 | HOST = socket.gethostname() | 
|---|
 | 12 |  | 
|---|
| [2297] | 13 | PROD_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 |     ]) | 
|---|
 | 27 | WIZARD_GUESTS = frozenset([ | 
|---|
 | 28 |     'not-backward', | 
|---|
 | 29 |     ]) | 
|---|
| [1999] | 30 |  | 
|---|
| [2297] | 31 | COMMON_CREDS = {} | 
|---|
| [1999] | 32 |  | 
|---|
| [2297] | 33 | # Format here assumes that we always chmod $USER:$USER, | 
|---|
| [2044] | 34 | # but note the latter refers to group... | 
|---|
| [2297] | 35 | # | 
|---|
 | 36 | # Important: no leading slashes! | 
|---|
 | 37 | COMMON_CREDS['all'] = [ | 
|---|
| [2044] | 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'), | 
|---|
| [1999] | 44 |     ] | 
|---|
 | 45 |  | 
|---|
| [2297] | 46 | COMMON_CREDS['prod'] = [ | 
|---|
| [2044] | 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'), | 
|---|
| [2045] | 51 |     ('root', 0o600, 'etc/pki/tls/private/scripts-1024.key'), | 
|---|
| [2044] | 52 |     ('root', 0o600, 'etc/pki/tls/private/scripts.key'), | 
|---|
 | 53 |     ('root', 0o600, 'etc/whoisd-password'), | 
|---|
| [2049] | 54 |     ('afsagent', 0o600, 'etc/daemon.keytab'), | 
|---|
| [1999] | 55 |  | 
|---|
| [2044] | 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'), | 
|---|
| [1999] | 59 |  | 
|---|
| [2269] | 60 |     ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'), # technically doesn't have to be secret anymore | 
|---|
 | 61 |     ('sql', 0o600, 'etc/sql-password'), | 
|---|
| [2044] | 62 |     ('signup', 0o600, 'etc/signup-ldap-pw'), | 
|---|
| [2297] | 63 |     ('logview', 0o600, 'home/logview/.k5login'), # XXX user must be created in Kickstart | 
|---|
| [1999] | 64 |     ] | 
|---|
 | 65 |  | 
|---|
| [2297] | 66 | # note that these are duplicates with 'prod', but the difference | 
|---|
 | 67 | # is that the files DIFFER between wizard and prod | 
|---|
 | 68 | COMMON_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 |  | 
|---|
 | 79 | MACHINE_CREDS = {} | 
|---|
 | 80 |  | 
|---|
 | 81 | MACHINE_CREDS['all'] = [ | 
|---|
 | 82 |     # XXX NEED TO CHECK THAT THE CONTENTS ARE SENSIBLE | 
|---|
| [2044] | 83 |     ('root', 0o600, 'etc/krb5.keytab'), | 
|---|
| [1999] | 84 |     ] | 
|---|
 | 85 |  | 
|---|
| [2297] | 86 | MACHINE_CREDS['prod'] = [ | 
|---|
 | 87 |     ('fedora-ds', 0o600, 'etc/dirsrv/keytab'), | 
|---|
 | 88 |     ] | 
|---|
 | 89 |  | 
|---|
 | 90 | MACHINE_CREDS['wizard'] = [] | 
|---|
 | 91 |  | 
|---|
 | 92 | # Works for passwd and group, but be careful! They're different things! | 
|---|
 | 93 | def 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 |  | 
|---|
| [2246] | 105 | def drop_caches(): | 
|---|
 | 106 |     with open("/proc/sys/vm/drop_caches", 'w') as f: | 
|---|
 | 107 |         f.write("1") | 
|---|
 | 108 |  | 
|---|
| [2044] | 109 | def mkdir_p(path): # it's like mkdir -p | 
|---|
| [1999] | 110 |     try: | 
|---|
 | 111 |         os.makedirs(path) | 
|---|
| [2044] | 112 |     except OSError as e: | 
|---|
 | 113 |         if e.errno == errno.EEXIST: | 
|---|
| [1999] | 114 |             pass | 
|---|
 | 115 |         else: raise | 
|---|
 | 116 |  | 
|---|
| [2044] | 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. | 
|---|
| [1999] | 127 | class WithMount(object): | 
|---|
 | 128 |     """Context for running code with an extra mountpoint.""" | 
|---|
 | 129 |     guest = None | 
|---|
| [2044] | 130 |     types = None # comma separated, like the mount argument -t | 
|---|
| [1999] | 131 |     mount = None | 
|---|
 | 132 |     dev = None | 
|---|
| [2044] | 133 |     def __init__(self, guest, types): | 
|---|
| [1999] | 134 |         self.guest = guest | 
|---|
| [2044] | 135 |         self.types = types | 
|---|
| [1999] | 136 |     def __enter__(self): | 
|---|
| [2246] | 137 |         drop_caches() | 
|---|
| [1999] | 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: | 
|---|
| [2044] | 148 |                 shell.call("mount", "--types", self.types, mapper, self.mount) | 
|---|
| [1999] | 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 | 
|---|
| [2044] | 157 |     def __exit__(self, _type, _value, _traceback): | 
|---|
| [1999] | 158 |         shell.call("umount", self.mount) | 
|---|
 | 159 |         os.rmdir(self.mount) | 
|---|
 | 160 |         shell.call("kpartx", "-d", self.dev) | 
|---|
| [2246] | 161 |         drop_caches() | 
|---|
| [1999] | 162 |  | 
|---|
 | 163 | def main(): | 
|---|
| [2297] | 164 |     usage = """usage: %prog [push|pull] [common|machine] GUEST""" | 
|---|
| [1999] | 165 |  | 
|---|
 | 166 |     parser = optparse.OptionParser(usage) | 
|---|
| [2044] | 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", | 
|---|
| [2297] | 170 |             help="filesystem type(s)") # same arg as 'mount' | 
|---|
| [2044] | 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() | 
|---|
| [1999] | 174 |  | 
|---|
| [2044] | 175 |     if not os.path.isdir(options.creds_dir): | 
|---|
| [2297] | 176 |         raise Exception("%s does not exist" % options.creds_dir) | 
|---|
| [2044] | 177 |     # XXX check owned by root and appropriately chmodded | 
|---|
| [1999] | 178 |  | 
|---|
 | 179 |     os.umask(0o077) # overly restrictive | 
|---|
 | 180 |  | 
|---|
| [2297] | 181 |     if len(args) != 3: | 
|---|
| [2044] | 182 |         parser.print_help() | 
|---|
| [1999] | 183 |         raise Exception("Wrong number of arguments") | 
|---|
 | 184 |  | 
|---|
 | 185 |     command = args[0] | 
|---|
| [2297] | 186 |     files   = args[1] | 
|---|
 | 187 |     guest   = args[2] | 
|---|
| [1999] | 188 |  | 
|---|
| [2297] | 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 |  | 
|---|
| [2044] | 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) | 
|---|
| [1999] | 199 |         def push_files(files, type): | 
|---|
| [2044] | 200 |             for (usergroup, perms, f) in files: | 
|---|
| [1999] | 201 |                 dest = "%s/%s" % (tmp_mount, f) | 
|---|
| [2044] | 202 |                 mkdir_p(os.path.dirname(dest)) # useful for .ssh | 
|---|
| [1999] | 203 |                 # assuming OK to overwrite | 
|---|
| [2044] | 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 | 
|---|
| [1999] | 213 |         def pull_files(files, type): | 
|---|
 | 214 |             for (_, _, f) in files: | 
|---|
| [2044] | 215 |                 dest = "%s/%s/%s" % (options.creds_dir, type, f) | 
|---|
| [1999] | 216 |                 mkdir_p(os.path.dirname(dest)) | 
|---|
 | 217 |                 # error if doesn't exist | 
|---|
 | 218 |                 shutil.copyfile("%s/%s" % (tmp_mount, f), dest) | 
|---|
 | 219 |  | 
|---|
| [2297] | 220 |         # XXX ideally we should check these *before* we mount, but Python | 
|---|
 | 221 |         # makes that pretty annoying to do | 
|---|
| [1999] | 222 |         if command == "push": | 
|---|
| [2297] | 223 |             run = push_files | 
|---|
| [1999] | 224 |         elif command == "pull": | 
|---|
| [2297] | 225 |             run = pull_files | 
|---|
 | 226 |         else: | 
|---|
 | 227 |             raise Exception("Unknown command %s, valid values are 'push' and 'pull'" % command) | 
|---|
| [1999] | 228 |  | 
|---|
| [2297] | 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 |  | 
|---|
| [1999] | 238 | if __name__ == "__main__": | 
|---|
 | 239 |     main() | 
|---|