Index: /branches/fc15-dev/host/credit-card/host.py
===================================================================
--- /branches/fc15-dev/host/credit-card/host.py	(revision 2043)
+++ /branches/fc15-dev/host/credit-card/host.py	(revision 2044)
@@ -1,9 +1,9 @@
 import os
 import optparse
-import logging
 import socket
 import tempfile
 import shutil
 import errno
+import csv
 
 import shell
@@ -13,61 +13,87 @@
 # XXX test server and wizard server
 
-ROOT_UID = 0
-SIGNUP_UID = 102
-SQL_UID = 537704221
-FEDORA_DS_UID = 103 # XXX ACTUALLY CONFIGURE SERVERS TO USE THIS
-LOGVIEW_UID = 501 # XXX Autogenerated, I don't like this...
+# UIDs (sketchy):
+#   signup 102
+#   fedora-ds 103 (sketchy, not true for b-b)
+#   logview 501 (really sketchy, since it's in the dynamic range)
 
+# Works for passwd and group, but be careful! They're different things!
+def lookup(filename):
+    # Super-safe to assume and volume IDs (expensive to check)
+    r = {
+        'root': 0,
+        'sql': 537704221,
+    }
+    with open(filename, 'rb') as f:
+        reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE)
+        for row in reader:
+            r[row[0]] = int(row[2])
+    return r
+
+# Format here assumes that we always chmod $USER:$USER ...
+# but note the latter refers to group...
 COMMON_CREDS = [
-    (ROOT_UID, 0o600, 'root/.bashrc'),
-    (ROOT_UID, 0o600, 'root/.screenrc'),
-    (ROOT_UID, 0o600, 'root/.ssh/authorized_keys'),
-    (ROOT_UID, 0o600, 'root/.ssh/authorized_keys2'),
-    (ROOT_UID, 0o600, 'root/.vimrc'),
-    (ROOT_UID, 0o600, 'root/.k5login'),
+    ('root', 0o600, 'root/.bashrc'),
+    ('root', 0o600, 'root/.screenrc'),
+    ('root', 0o600, 'root/.ssh/authorized_keys'),
+    ('root', 0o600, 'root/.ssh/authorized_keys2'),
+    ('root', 0o600, 'root/.vimrc'),
+    ('root', 0o600, 'root/.k5login'),
     # punted /root/.ssh/known_hosts
 
-    # XXX must be created in Kickstart
-    (LOGVIEW_UID, 0o600, 'home/logview/.k5login'),
+    # XXX user must be created in Kickstart
+    ('logview', 0o600, 'home/logview/.k5login'),
     ]
 
 COMMON_PROD_CREDS = [ # important: no leading slashes!
-    (ROOT_UID, 0o600, 'root/.ldapvirc'),
-    (ROOT_UID, 0o600, 'etc/ssh/ssh_host_dsa_key'),
-    (ROOT_UID, 0o600, 'etc/ssh/ssh_host_key'),
-    (ROOT_UID, 0o600, 'etc/ssh/ssh_host_rsa_key'),
-    (ROOT_UID, 0o600, 'etc/pki/tls/private/scripts.key'),
-    (ROOT_UID, 0o600, 'etc/whoisd-password'),
-    (ROOT_UID, 0o600, 'etc/daemon.keytab'),
+    ('root', 0o600, 'root/.ldapvirc'),
+    ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'),
+    ('root', 0o600, 'etc/ssh/ssh_host_key'),
+    ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'),
+    ('root', 0o600, 'etc/pki/tls/private/scripts.key'),
+    ('root', 0o600, 'etc/whoisd-password'),
+    ('root', 0o600, 'etc/daemon.keytab'),
 
-    (ROOT_UID, 0o644, 'etc/ssh/ssh_host_dsa_key.pub'),
-    (ROOT_UID, 0o644, 'etc/ssh/ssh_host_key.pub'),
-    (ROOT_UID, 0o644, 'etc/ssh/ssh_host_rsa_key.pub'),
+    ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'),
+    ('root', 0o644, 'etc/ssh/ssh_host_key.pub'),
+    ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'),
 
-    (SQL_UID, 0o600, 'etc/sql-mit-edu.cfg.php'),
-    (SIGNUP_UID, 0o600, 'etc/signup-ldap-pw'),
+    ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'),
+    ('signup', 0o600, 'etc/signup-ldap-pw'),
     ]
 
 MACHINE_PROD_CREDS = [
     # XXX NEED TO CHECK THAT THESE ARE SENSIBLE
-    (ROOT_UID, 0o600, 'etc/krb5.keytab'),
-    (FEDORA_DS_UID, 0o600, 'etc/dirsrv/keytab')
+    ('root', 0o600, 'etc/krb5.keytab'),
+    ('fedora-ds', 0o600, 'etc/dirsrv/keytab')
     ]
 
-def mkdir_p(path):
+def mkdir_p(path): # it's like mkdir -p
     try:
         os.makedirs(path)
-    except OSError as exc: # Python >2.5
-        if exc.errno == errno.EEXIST:
+    except OSError as e:
+        if e.errno == errno.EEXIST:
             pass
         else: raise
 
+# XXX This code is kind of dangerous, because we are directly using the
+# kernel modules to manipulate possibly untrusted disk images.  This
+# means that if an attacker can corrupt the disk, and exploit a problem
+# in the kernel vfs driver, he can escalate a guest root exploit
+# to a host root exploit.  Ultimately we should use libguestfs
+# which makes this attack harder to pull off, but at the time of writing
+# squeeze didn't package libguestfs.
+#
+# We try to minimize attack surface by explicitly specifying the
+# expected filesystem type.
 class WithMount(object):
     """Context for running code with an extra mountpoint."""
     guest = None
+    types = None # comma separated, like the mount argument -t
     mount = None
     dev = None
-    def __init__(self, guest):
+    def __init__(self, guest, types):
         self.guest = guest
+        self.types = types
     def __enter__(self):
         self.dev = "/dev/%s/%s-root" % (HOST, self.guest)
@@ -81,5 +107,5 @@
             self.mount = tempfile.mkdtemp("-%s" % self.guest, 'vm-', '/mnt') # no trailing slash
             try:
-                shell.call("mount", mapper, self.mount)
+                shell.call("mount", "--types", self.types, mapper, self.mount)
             except:
                 os.rmdir(self.mount)
@@ -90,5 +116,5 @@
 
         return self.mount
-    def __exit__(self, *args):
+    def __exit__(self, _type, _value, _traceback):
         shell.call("umount", self.mount)
         os.rmdir(self.mount)
@@ -96,19 +122,23 @@
 
 def main():
-    usage = """usage: %prog [ARGS]"""
+    usage = """usage: %prog [push|pull|pull-common] GUEST"""
 
     parser = optparse.OptionParser(usage)
-    _, args = parser.parse_args()
+    # ext3 will probably supported for a while yet and a pretty
+    # reasonable thing to always try
+    parser.add_option('-t', '--types', dest="types", default="ext4,ext3",
+            help="filesystem type(s)")
+    parser.add_option('--creds-dir', dest="creds_dir", default="/root/creds",
+            help="directory to store/fetch credentials in")
+    options, args = parser.parse_args()
 
-    creds = "/root/creds" # XXX check exists, check owned by root
-    # make an option
-    if not os.path.isdir(creds):
-        raise Exception("/root/creds does not exist")
+    if not os.path.isdir(options.creds_dir):
+        raise Exception("/root/creds does not exist") # XXX STRING
+    # XXX check owned by root and appropriately chmodded
 
     os.umask(0o077) # overly restrictive
 
-    # XXX error handling
-
     if len(args) != 2:
+        parser.print_help()
         raise Exception("Wrong number of arguments")
 
@@ -116,21 +146,28 @@
     guest   = args[1]
 
-    with WithMount(guest) as tmp_mount:
+    with WithMount(guest, options.types) as tmp_mount:
+        uid_lookup = lookup("%s/etc/passwd" % tmp_mount)
+        gid_lookup = lookup("%s/etc/group" % tmp_mount)
         def push_files(files, type):
-            for (ugid, perms, f) in files:
-                # assumes directories exist
+            for (usergroup, perms, f) in files:
                 dest = "%s/%s" % (tmp_mount, f)
+                mkdir_p(os.path.dirname(dest)) # useful for .ssh
                 # assuming OK to overwrite
-                shutil.copyfile("%s/%s/%s" % (creds, type, f), dest)
-                os.chown(dest, ugid, ugid)
-                os.chmod(dest, perms)
+                # XXX we could compare the files before doing anything...
+                shutil.copyfile("%s/%s/%s" % (options.creds_dir, type, f), dest)
+                try:
+                    os.chown(dest, uid_lookup[usergroup], gid_lookup[usergroup])
+                    os.chmod(dest, perms)
+                except:
+                    # never ever leave un-chowned files lying around
+                    os.unlink(dest)
+                    raise
         def pull_files(files, type):
             for (_, _, f) in files:
-                dest = "%s/%s/%s" % (creds, type, f)
+                dest = "%s/%s/%s" % (options.creds_dir, type, f)
                 mkdir_p(os.path.dirname(dest))
                 # error if doesn't exist
                 shutil.copyfile("%s/%s" % (tmp_mount, f), dest)
 
-        # push case
         if command == "push":
             push_files(COMMON_CREDS, 'common')
@@ -138,5 +175,4 @@
             push_files(MACHINE_PROD_CREDS, 'machine/%s' % guest)
         elif command == "pull":
-            # check if /root/creds exists
             pull_files(MACHINE_PROD_CREDS, 'machine/%s' % guest)
         elif command == "pull-common":
