[ostree/wip/bootloader: 4/4] [INCOMPATIBLE CHANGE] Implement new deployment model



commit 385784aa3ffe930ffbb40cf3cfe5a86fedb8f698
Author: Colin Walters <walters verbum org>
Date:   Sat Jun 29 11:45:53 2013 -0400

    [INCOMPATIBLE CHANGE] Implement new deployment model
    
    See https://wiki.gnome.org/OSTree/DeploymentModel2

 Makefile-ostree.am                          |   15 +-
 Makefile-tests.am                           |    1 +
 src/ostree/DEPLOY2.0                        |  239 ++++++
 src/ostree/ot-admin-builtin-deploy.c        |  709 ++--------------
 src/ostree/ot-admin-builtin-diff.c          |   57 +-
 src/ostree/ot-admin-builtin-init-fs.c       |    3 +-
 src/ostree/ot-admin-builtin-install.c       |   26 +-
 src/ostree/ot-admin-builtin-os-init.c       |    5 +-
 src/ostree/ot-admin-builtin-prune.c         |   42 +-
 src/ostree/ot-admin-builtin-pull-deploy.c   |  178 ----
 src/ostree/ot-admin-builtin-status.c        |  100 +++
 src/ostree/ot-admin-builtin-update-kernel.c |  296 -------
 src/ostree/ot-admin-builtin-upgrade.c       |  128 ++--
 src/ostree/ot-admin-builtins.h              |    6 +-
 src/ostree/ot-admin-deploy.c                | 1219 +++++++++++++++++++++++++++
 src/ostree/ot-admin-deploy.h                |   51 ++
 src/ostree/ot-admin-functions.c             | 1178 +++++++++++++++++++-------
 src/ostree/ot-admin-functions.h             |  100 ++-
 src/ostree/ot-bootloader-syslinux.c         |  312 +++++++
 src/ostree/ot-bootloader-syslinux.h         |   40 +
 src/ostree/ot-bootloader.c                  |   49 ++
 src/ostree/ot-bootloader.h                  |   59 ++
 src/ostree/ot-builtin-admin.c               |   25 +-
 src/ostree/ot-config-parser.c               |  230 +++++
 src/ostree/ot-config-parser.h               |   58 ++
 src/ostree/ot-deployment.c                  |  209 +++++
 src/ostree/ot-deployment.h                  |   66 ++
 src/ostree/ot-ordered-hash.c                |   82 ++
 src/ostree/ot-ordered-hash.h                |   46 +
 src/switchroot/ostree-prepare-root.c        |  100 +--
 tests/libtest.sh                            |   97 +++
 tests/t0015-admin-deploy.sh                 |  124 +++
 32 files changed, 4181 insertions(+), 1669 deletions(-)
---
diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index 7825db9..04d740b 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -52,15 +52,26 @@ ostree_SOURCES += \
        src/ostree/ot-admin-builtin-diff.c \
        src/ostree/ot-admin-builtin-deploy.c \
        src/ostree/ot-admin-builtin-prune.c \
-       src/ostree/ot-admin-builtin-pull-deploy.c \
        src/ostree/ot-admin-builtin-os-init.c \
        src/ostree/ot-admin-builtin-install.c \
+       src/ostree/ot-admin-builtin-status.c \
        src/ostree/ot-admin-builtin-run-triggers.c \
        src/ostree/ot-admin-builtin-upgrade.c \
-       src/ostree/ot-admin-builtin-update-kernel.c \
        src/ostree/ot-admin-builtins.h \
        src/ostree/ot-admin-functions.h \
        src/ostree/ot-admin-functions.c \
+       src/ostree/ot-admin-deploy.h \
+       src/ostree/ot-admin-deploy.c \
+       src/ostree/ot-bootloader.h \
+       src/ostree/ot-bootloader.c \
+       src/ostree/ot-bootloader-syslinux.h \
+       src/ostree/ot-bootloader-syslinux.c \
+       src/ostree/ot-config-parser.h \
+       src/ostree/ot-config-parser.c \
+       src/ostree/ot-deployment.h \
+       src/ostree/ot-deployment.c \
+       src/ostree/ot-ordered-hash.h \
+       src/ostree/ot-ordered-hash.c \
        $(NULL)
 
 ostree_bin_shared_cflags = $(AM_CFLAGS) -I$(srcdir)/src/libgsystem -I$(srcdir)/src/libotutil 
-I$(srcdir)/src/libostree -I$(srcdir)/src/ostree  -DLOCALEDIR=\"$(datadir)/locale\"
diff --git a/Makefile-tests.am b/Makefile-tests.am
index cd22ab2..b81866b 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -28,6 +28,7 @@ testfiles = t0000-basic \
        t0005-corruption \
        t0006-libarchive \
        t0011-pull-archive-z \
+       t0015-admin-deploy \
        $(NULL)
 insttest_SCRIPTS = $(addprefix tests/,$(testfiles:=.sh))
 
diff --git a/src/ostree/DEPLOY2.0 b/src/ostree/DEPLOY2.0
new file mode 100644
index 0000000..ab94fa6
--- /dev/null
+++ b/src/ostree/DEPLOY2.0
@@ -0,0 +1,239 @@
+This should be migrated to https://live.gnome.org/OSTree/DeploymentModel2
+
+/** TODO:
+ *
+ * Drop the bind mount for /etc; instead check out the writable copy
+ * inside the deployment, and keep the lookaside copy inside the tree
+ * as etc.default ?  But that would need its own read-only bind mount,
+ * so maybe /usr/etc.default or even just /usr/etc ?
+ *
+ * For /var, change it to be a systemd generator; then it could
+ * be overriden by an admin.
+ *
+ */
+
+/* ATOMIC DIRECTORY:
+ *
+ * name -> name.l0
+ * name.d:
+ *   foo/
+ *   bar
+ *   baz
+ *   ... and more files
+ * name.l0:
+ *   ../name.d/foo foo
+ *   ../name.d/bar bar
+ *   ../name.d/baz baz
+ *
+ * To update:
+ *  0) Perform cleanup
+ *  1) Add new contents to name.d.  If necessary, rename.
+ *  2) Create new name.l1, with symlinks to contents in name.d
+ *  3) Atomic swap name -> name.l1
+ * Cleanup:
+ *  1) Read value of name, e.g. name.l0
+ *  2) for each (linkname, target) i name.l0 not also in name.l1:
+ *    rm -rf ${linkname} ${target}
+ *  3) rm -rf name.l0
+ */ 
+
+/* 
+ * Terms
+ * -----
+ * We have an ordered list of deployments:
+ * "new": The new tree
+ * "current/deploy0": The currently deployed tree, set as the bootloader default
+ * "previous/deploy1": (optional) The previous tree relative to "current"
+ * "deploy2"...:  Any number of previous deployments
+ *
+ * Also in the mix is the special "running" deployment, the one we're
+ * currently running.  We want to support generating an arbitrary set
+ * of new deployments, atomically.  But the normal case is to make
+ * "new" be "current", shift everything down by one, and "previous"
+ * would drop off.
+ *
+ * Because we want to support deploying the same tree twice,
+ * e.g. going from foo -> bar -> foo, the deployments have a serial
+ * number.  A deployment list looks like:
+ *
+ * deploy0:   da39a538021.1  (tag:current, ref:x86_64-runtime, running)
+ * deploy1:   1ca9d348d67.0  (tag:previous, ref:x86_64-devel)
+ * deploy2:   da39a538021.0  (ref:x86_64-runtime)
+ * deploy3:   587e2144109.0  (ref:x86_64-runtime)
+ *
+ * The very common case of course is for the deployment serial to be
+ * 0, so we should omit it from the list.
+ *
+ * Also, exactly one deployment can be "running".  Let's say I just ran:
+ * $ ostree admin pull-deploy gnome-ostree/buildmaster/x86_64-runtime
+ * but I haven't rebooted yet.  Then:
+ *
+ * deploy0:   da39a538021.1  (tag:current, ref:x86_64-runtime)
+ * deploy1:   1ca9d348d67.0  (tag:previous, ref:x86_64-devel, running)
+ * deploy2:   da39a538021.0  (ref:x86_64-runtime)
+ * deploy3:   587e2144109.0  (ref:x86_64-runtime)
+ *
+ * In this scenario, we expect doing:
+ * $ ostree admin upgrade -r
+ * would delete the deploy0, and configuration changes are merged based on deploy1.
+ *
+ * It's also possible for deploy2/3 to be running, if the user
+ * explicitly rebooted back into them.  We should have a command:
+ * $ ostree admin reset-current
+ * to make deploy2 current, and delete the old current/previous.
+ *
+ * Each deployment has a few variables:
+ *
+ * struct deployment {
+ *  int index;  // Global offset
+ *  char *ref;  // originating OSTree refname (e.g. gnome-ostree/buildmaster/x86_64-runtime)
+ *  char *csum;     // OSTree checksum of tree
+ *  int bootserial; // An integer assigned to this tree per its ${bootcsum}
+ *  char *bootcsum;   //Checksum of kernel+initramfs
+ * }
+ *
+ * Global variables:
+ *
+ * bootversion: Either 0 or 1, as expressed by /boot/loader.${bootversion}.
+ *
+ * Constraints
+ * -----------
+ *
+ * We want fully atomic upgrades.  Some constraints though:
+ *
+ *   1) We don't want to update /boot unless the kernel actually
+ *   changed; it might be on flash storage for example, and constant
+ *   wear on that would be bad.
+ *
+ *   2) /boot may be on a separate partition, so we can't just swap
+ *      one symbolic link and have it take effect for everything.
+ *
+ * Implementation
+ * --------------
+ *
+ * We have two deployment lists: OLD and NEW.  We need to preserve any
+ * entries from OLD in NEW.  Let's take the case above:  
+ * 
+ * deploy0:   da39a538021.1  (tag:current, ref:x86_64-runtime, bootcsum:c90d227a60, running)
+ * deploy1:   1ca9d348d67.0  (tag:previous, ref:x86_64-devel, bootcsum:648a9123b4)
+ * deploy2:   da39a538021.0  (ref:x86_64-runtime, bootcsum:c90d227a60)
+ * deploy3:   587e2144109.0  (ref:x86_64-runtime, bootcsum:c90d227a60)
+ *
+ * And we want to generate the new deployment list:
+ *
+ * deploy0:   afd96ee01b1.0  (tag:current, ref:x86_64-runtime, bootcsum:c90d227a60)
+ * deploy1:   da39a538021.1  (tag:current, ref:x86_64-runtime, bootcsum:c90d227a60, running)
+ * deploy2:   1ca9d348d67.0  (tag:previous, ref:x86_64-devel, bootcsum:648a9123b4)
+ * deploy3:   da39a538021.0  (ref:x86_64-runtime, bootcsum:c90d227a60)
+ * deploy4:   587e2144109.0  (ref:x86_64-runtime, bootcsum:c90d227a60)
+ *
+ * Filesystem layout
+ * -----------------
+ *
+ * If we need to change kernel configuration, we do so atomically via
+ * the "swapped directory pattern".  In this case, that's
+ * /boot/loader.{0,1}, and /boot/loader is a symbolic link to one or
+ * the other.
+ *
+ * A tree deployment is represented by these directories:
+ *  /boot/loader/entries/${osname}-${bootcsum}
+ *    Contains kernel+initramfs files and configuration
+ *  /ostree/boot.${bootversion}/${osname}/${bootcsum}/${treebootserial}
+ *    Where ${treebootserial} is a symbolic link 0..N, pointing to 
../../../deploy/${osname}/${treecsum}.${treeserial}
+ *
+ * And these directories:
+ *  /ostree/deploy/${osname}/${treecsum}.${serial}
+ *    Contains "OS/" - this is the chroot target
+ * And the following files:
+ *  /ostree/deploy/${osname}/deploy/${treecsum}.${serial}/usr/share/ostree/ref
+ *    containing ${treeref}
+ *  /ostree/deploy/${osname}/deploy/${treecsum}.${serial}/usr/share/ostree/rev
+ *    containing the full ${treecsum} (it's shortened elsewhere to a minimum unique length, at least 8)
+ *
+ * In order to implement upgrades without touching /boot,
+ * /ostree/boot.${bootversion}/${osname}/${bootcsum} is also a
+ * swapped directory.  So the real target is e.g. ${bootcsum}.[01]
+ *
+ * Booting
+ * -------
+ *
+ * The kernel commandline contains:
+ *   ostree=/ostree/boot.${bootversion}/${osname}/${bootcsum}/${treeserial}
+ * Inside the initramfs (dracut), the ostree-prepare-root tool parses this, and
+ * sets up /sysroot.
+ *
+ * Redeploying
+ * -----------
+ *
+ * We have two deployment lists, call them CURRENT and NEW.  First,
+ * determine if we need to generate a new bootloader configuration;
+ * this will be true if NEW adds or removes "bootcsum" entries.
+ *
+ * New bootloader configuration
+ * ----------------------------
+ *
+ * Let's assume the current bootversion is 0; the new bootversion will
+ * therefore be 1.
+ *
+ * 0) Compute KEEPSET, which is the set of deployments in both OLD and NEW.
+ *    For each deployment (osname, treecsum, bootcsum, serial=0) in KEEPSET, check out
+ *      - Copy symbolic links in /ostree/boot.0/${osname}/${bootcsum} to the
+ *        corresponding .1
+ *      - Clone /boot/loader.0/${osname}-${bootcsum} to /boot/loader.1/${osname}-${bootcsum}
+ * 1) Compute NEWSET, which is the set of deployments that are only in NEW.
+ *    For each deployment (osname, treecsum, bootcsum, serial=0) in NEWSET:
+ *      - Check out /ostree/deploy/${osname}/${treecsum}.${serial}
+ *      - Do the merge into /etc in that directory, and create usr/share/ostree/{ref,rev}.
+ *      - Look in its boot/ directory, and copy the kernel/initramfs to
+ *        /boot/loader.1/entries/${osname}-${bootcsum} (unless the directory already exists).
+ *      -  Allocate an unused treebootserial in /ostree/boot.1/${osname}/${bootcsum}
+ * 2) Generate new bootloader configuration in /boot/loader.1; for example, if we're
+ *    using syslinux, /boot/loader.1/syslinux/syslinux.cfg
+ * 3) fsync() all the above, all files and directories
+ * 4) Atomically swap /boot/loader to point to /boot/loader.1
+ *
+ * Cleanup
+ * -------
+ *
+ * Read value of /boot/loader.  If it's 1, then the cleanup value is
+ * 0, and vice versa.  Assume here that it's 1.
+ *
+ * 0) rm -rf /boot/loader.0 /ostree/boot.0
+ * 1) Generate set of deployments from symbolic links in /ostree/boot.1; this is CURRENTSET
+ * 2) Generate set of all deployments from /ostree/deploy; this is DISKSET
+ * 3) Delete all deployments in DISKSET - CURRENTSET
+ *    
+ * Reusing existing bootloader configuration
+ * -----------------------------------------
+ *
+ * This only applies when we changing the deployment list only for a
+ * given ${bootcsum}, AND the new and old number of entries is the
+ * same.  However it's worth optimizing for this case because the
+ * default OSTree configuration is to keep only "current" and
+ * "previous", and most updates are not going to change the kernel.
+ *
+ * 0) Read the value of /boot/loader; this is ${bootversion}
+ * 1) Read the value of /ostree/deploy/${osname}/deploy.${bootversion}; this is ${osdeployversion};
+ *    Here we assume ${osdeployversion} is 0, the new version will be 1, and vice versa.
+ * 2) Create the directory /ostree/deploy/${osname}/deploy.${bootversion}.1
+ * 3) Compute KEEPSET, which is the set of deployments in both OLD and NEW.
+ *      Each of these will have a symbolic link number, referenced by the bootloader entry,
+ *      that needs to be preserved.
+ *      for each (treecsum, bootsum, serial, boottag) in KEEPSET:
+ *        ln -s ../deploy.${bootversion}.0/${treecsum} 
/ostree/deploy/${osname}/deploy.${bootversion}.1/${boottag}
+ * 4) Compute NEWSET, which is the set of deployments in only in NEW.
+ *    Assign each entry in NEWSET a boottag that has not yet been used.  The normal case is
+ *    removing previous, so that would be boottag 1.
+ *    for each deployment (treecsum, bootsum, serial) in NEWSET:
+ *      Check out /ostree/deploy/${osname}/deploy.${boottag}.1/${bootcsum}/${treecsum}.0
+ *    then create a corresponding -etc directory, merging from the running tree.
+ *      cp -al /ostree/deploy/${osname}/deploy.0/${bootcsum}/${treecsum}.${serial}-etc to corresponding .1
+ *      if !exists(/boot/loader.1/${osname}-${bootcsum}) then
+ *        cp -al /boot/loader.0/${osname}-${bootcsum} to corresponding .1
+ *    Note we hardlink even the writable copy of -etc; only one of these can be active
+ *    at a time, so it's fine if mutating one affects another.  We also don't make
+ *    any guarantees about whether modifications to /etc during or after a deployment
+ *    will be visible on reboot.
+ * 
+ */
+
diff --git a/src/ostree/ot-admin-builtin-deploy.c b/src/ostree/ot-admin-builtin-deploy.c
index 4569761..93e3837 100644
--- a/src/ostree/ot-admin-builtin-deploy.c
+++ b/src/ostree/ot-admin-builtin-deploy.c
@@ -1,6 +1,6 @@
 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
  *
- * Copyright (C) 2012 Colin Walters <walters verbum org>
+ * Copyright (C) 2012,2013 Colin Walters <walters verbum org>
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -24,692 +24,109 @@
 
 #include "ot-admin-builtins.h"
 #include "ot-admin-functions.h"
+#include "ot-admin-deploy.h"
+#include "ot-ordered-hash.h"
 #include "ostree.h"
 
 #include <glib/gi18n.h>
 
-typedef struct {
-  OstreeRepo  *repo;
-  OtAdminBuiltinOpts *admin_opts;
-  GFile *ostree_dir;
-  char  *osname;
-  GFile *osname_dir;
-
-  char        *current_deployment_ref;
-  char        *previous_deployment_ref;
-  char        *resolved_commit;
-  char        *resolved_previous_commit;
-  
-  char        *previous_deployment_revision;
-  GFile       *deploy_target_path;
-  GFile       *previous_deployment;
-} OtAdminDeploy;
-
-static gboolean opt_no_kernel;
-static gboolean opt_force;
+static gboolean opt_no_bootloader;
+static gboolean opt_retain;
+static char **opt_kernel_argv;
+static char *opt_osname;
+static char *opt_origin_path;
 
 static GOptionEntry options[] = {
-  { "no-kernel", 0, 0, G_OPTION_ARG_NONE, &opt_no_kernel, "Don't update kernel related config (initramfs, 
bootloader)", NULL },
-  { "force", 0, 0, G_OPTION_ARG_NONE, &opt_force, "Overwrite any existing deployment", NULL },
+  { "os", 0, 0, G_OPTION_ARG_STRING, &opt_osname, "Specify operating system root to use", NULL },
+  { "origin-file", 0, 0, G_OPTION_ARG_FILENAME, &opt_origin_path, "Specify origin file", NULL },
+  { "no-bootloader", 0, 0, G_OPTION_ARG_NONE, &opt_no_bootloader, "Don't update bootloader", NULL },
+  { "retain", 0, 0, G_OPTION_ARG_NONE, &opt_retain, "Do not delete previous deployment", NULL },
+  { "karg", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_kernel_argv, "Set kernel argument, like 
--karg=root=/dev/sda1", NULL },
   { NULL }
 };
 
-/**
- * update_current:
- *
- * Atomically swap the /ostree/current symbolic link to point to a new
- * path.  If successful, the old current will be saved as
- * /ostree/previous, and /ostree/current-etc will be a link to the
- * current /etc subdirectory.
- *
- * Unless the new-current equals current, in which case, do nothing.
- */
-static gboolean
-update_current (OtAdminDeploy      *self,
-                GFile              *current_deployment,
-                GFile              *deploy_target,
-                GCancellable       *cancellable,
-                GError            **error)
-{
-  gboolean ret = FALSE;
-  ot_lobj GFile *current_path = NULL;
-  ot_lobj GFile *current_etc_path = NULL;
-  ot_lobj GFile *previous_path = NULL;
-  ot_lobj GFile *tmp_current_path = NULL;
-  ot_lobj GFile *tmp_current_etc_path = NULL;
-  ot_lobj GFile *tmp_previous_path = NULL;
-  ot_lobj GFileInfo *previous_info = NULL;
-  ot_lfree char *relative_current = NULL;
-  ot_lfree char *relative_current_etc = NULL;
-  ot_lfree char *relative_previous = NULL;
-
-  current_path = g_file_get_child (self->osname_dir, "current");
-  current_etc_path = g_file_get_child (self->osname_dir, "current-etc");
-  previous_path = g_file_get_child (self->osname_dir, "previous");
-
-  relative_current = g_file_get_relative_path (self->osname_dir, deploy_target);
-  g_assert (relative_current);
-  relative_current_etc = g_strconcat (relative_current, "-etc", NULL);
-
-  if (current_deployment)
-    {
-      ot_lfree char *relative_previous = NULL;
-
-      if (g_file_equal (current_deployment, deploy_target))
-        {
-          g_print ("ostadmin: %s already points to %s\n", gs_file_get_path_cached (current_path),
-                   relative_current);
-          return TRUE;
-        }
-
-      tmp_previous_path = g_file_get_child (self->osname_dir, "tmp-previous");
-      (void) gs_file_unlink (tmp_previous_path, NULL, NULL);
-
-      relative_previous = g_file_get_relative_path (self->osname_dir, current_deployment);
-      g_assert (relative_previous);
-      if (symlink (relative_previous, gs_file_get_path_cached (tmp_previous_path)) < 0)
-        {
-          ot_util_set_error_from_errno (error, errno);
-          goto out;
-        }
-    }
-
-  tmp_current_path = g_file_get_child (self->osname_dir, "tmp-current");
-  (void) gs_file_unlink (tmp_current_path, NULL, NULL);
-
-  if (symlink (relative_current, gs_file_get_path_cached (tmp_current_path)) < 0)
-    {
-      ot_util_set_error_from_errno (error, errno);
-      goto out;
-    }
-
-  tmp_current_etc_path = g_file_get_child (self->osname_dir, "tmp-current-etc");
-  (void) gs_file_unlink (tmp_current_etc_path, NULL, NULL);
-  if (symlink (relative_current_etc, gs_file_get_path_cached (tmp_current_etc_path)) < 0)
-    {
-      ot_util_set_error_from_errno (error, errno);
-      goto out;
-    }
-
-  if (!gs_file_rename (tmp_current_path, current_path,
-                       cancellable, error))
-    goto out;
-  if (!gs_file_rename (tmp_current_etc_path, current_etc_path,
-                       cancellable, error))
-    goto out;
-
-  if (tmp_previous_path)
-    {
-      if (!gs_file_rename (tmp_previous_path, previous_path,
-                           cancellable, error))
-        goto out;
-    }
-
-  g_print ("ostadmin: %s set to %s\n", gs_file_get_path_cached (current_path),
-           relative_current);
-
-  ret = TRUE;
- out:
-  return ret;
-}
-
-typedef struct {
-  GError **error;
-  gboolean caught_error;
-
-  GMainLoop *loop;
-} ProcessOneCheckoutData;
-
-static void
-on_checkout_complete (GObject         *object,
-                      GAsyncResult    *result,
-                      gpointer         user_data)
-{
-  ProcessOneCheckoutData *data = user_data;
-  GError *local_error = NULL;
-
-  if (!ostree_repo_checkout_tree_finish ((OstreeRepo*)object, result,
-                                         &local_error))
-    goto out;
-
- out:
-  if (local_error)
-    {
-      data->caught_error = TRUE;
-      g_propagate_error (data->error, local_error);
-    }
-  g_main_loop_quit (data->loop);
-}
-
-
-/**
- * ensure_unlinked:
- *
- * Like gs_file_unlink(), but return successfully if the file doesn't
- * exist.
- */
-static gboolean
-ensure_unlinked (GFile         *path,
-                 GCancellable  *cancellable,
-                 GError       **error)
+gboolean
+ot_admin_builtin_deploy (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error)
 {
   gboolean ret = FALSE;
-  GError *temp_error = NULL;
-
-  if (!gs_file_unlink (path, cancellable, &temp_error))
-    {
-      if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
-        {
-          g_clear_error (&temp_error);
-        }
-      else
-        {
-          g_propagate_error (error, temp_error);
-          goto out;
-        }
-    }
-  
-  ret = TRUE;
- out:
-  return ret;
-}
+  __attribute__((unused)) GCancellable *cancellable = NULL;
+  const char *refspec;
+  GOptionContext *context;
+  GFile *sysroot = admin_opts->sysroot;
+  GKeyFile *origin = NULL;
+  int current_bootversion;
+  int new_bootversion;
+  gs_unref_object OstreeRepo *repo = NULL;
+  gs_unref_ptrarray GPtrArray *current_deployments = NULL;
+  gs_unref_ptrarray GPtrArray *new_deployments = NULL;
+  gs_unref_object OtDeployment *new_deployment = NULL;
+  gs_unref_object OtDeployment *booted_deployment = NULL;
+  gs_free char *revision = NULL;
+
+  context = g_option_context_new ("REFSPEC - Checkout revision REFSPEC as the new default deployment");
 
-/**
- * copy_one_config_file:
- *
- * Copy @file from @modified_etc to @new_etc, overwriting any existing
- * file there.
- */
-static gboolean
-copy_one_config_file (OtAdminDeploy      *self,
-                      GFile              *orig_etc,
-                      GFile              *modified_etc,
-                      GFile              *new_etc,
-                      GFile              *src,
-                      GCancellable       *cancellable,
-                      GError            **error)
-{
-  gboolean ret = FALSE;
-  ot_lobj GFileInfo *src_info = NULL;
-  ot_lobj GFile *dest = NULL;
-  ot_lobj GFile *parent = NULL;
-  ot_lfree char *relative_path = NULL;
-  
-  relative_path = g_file_get_relative_path (modified_etc, src);
-  g_assert (relative_path);
-  dest = g_file_resolve_relative_path (new_etc, relative_path);
+  g_option_context_add_main_entries (context, options, NULL);
 
-  src_info = g_file_query_info (src, OSTREE_GIO_FAST_QUERYINFO, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                cancellable, error);
-  if (!src_info)
+  if (!g_option_context_parse (context, &argc, &argv, error))
     goto out;
 
-  if (g_file_info_get_file_type (src_info) == G_FILE_TYPE_DIRECTORY)
-    {
-      ot_lobj GFileEnumerator *src_enum = NULL;
-      ot_lobj GFileInfo *child_info = NULL;
-      GError *temp_error = NULL;
-
-      /* FIXME actually we need to copy permissions and xattrs */
-      if (!gs_file_ensure_directory (dest, TRUE, cancellable, error))
-        goto out;
-
-      src_enum = g_file_enumerate_children (src, OSTREE_GIO_FAST_QUERYINFO,
-                                            G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                            cancellable, error);
-
-      while ((child_info = g_file_enumerator_next_file (src_enum, cancellable, error)) != NULL)
-        {
-          ot_lobj GFile *child = g_file_get_child (src, g_file_info_get_name (child_info));
-
-          if (!copy_one_config_file (self, orig_etc, modified_etc, new_etc, child,
-                                     cancellable, error))
-            goto out;
-        }
-      g_clear_object (&child_info);
-      if (temp_error != NULL)
-        {
-          g_propagate_error (error, temp_error);
-          goto out;
-        }
-    }
-  else
-    {
-      parent = g_file_get_parent (dest);
-
-      /* FIXME actually we need to copy permissions and xattrs */
-      if (!gs_file_ensure_directory (parent, TRUE, cancellable, error))
-        goto out;
-      
-      if (!g_file_copy (src, dest, G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS | 
G_FILE_COPY_ALL_METADATA,
-                        cancellable, NULL, NULL, error))
-        goto out;
-    }
-
-  ret = TRUE;
- out:
-  return ret;
-}
-
-/**
- * merge_etc_changes:
- *
- * Compute the difference between @orig_etc and @modified_etc,
- * and apply that to @new_etc.
- *
- * The algorithm for computing the difference is pretty simple; it's
- * approximately equivalent to "diff -unR orig_etc modified_etc",
- * except that rather than attempting a 3-way merge if a file is also
- * changed in @new_etc, the modified version always wins.
- */
-static gboolean
-merge_etc_changes (OtAdminDeploy  *self,
-                   GFile          *orig_etc,
-                   GFile          *modified_etc,
-                   GFile          *new_etc,
-                   GCancellable   *cancellable,
-                   GError        **error)
-{
-  gboolean ret = FALSE;
-  ot_lobj GFile *ostree_etc = NULL;
-  ot_lobj GFile *tmp_etc = NULL;
-  ot_lptrarray GPtrArray *modified = NULL;
-  ot_lptrarray GPtrArray *removed = NULL;
-  ot_lptrarray GPtrArray *added = NULL;
-  guint i;
-
-  modified = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_diff_item_unref);
-  removed = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
-  added = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
-
-  if (!ostree_diff_dirs (orig_etc, modified_etc, modified, removed, added,
-                         cancellable, error))
+  if (argc < 2)
     {
-      g_prefix_error (error, "While computing configuration diff: ");
+      ot_util_usage_error (context, "REF/REV must be specified", error);
       goto out;
     }
 
-  if (modified->len > 0 || removed->len > 0 || added->len > 0)
-    g_print ("ostadmin: Processing config: %u modified, %u removed, %u added\n", 
-             modified->len,
-             removed->len,
-             added->len);
-  else
-    g_print ("ostadmin: No modified configuration\n");
-
-  for (i = 0; i < removed->len; i++)
-    {
-      GFile *file = removed->pdata[i];
-      ot_lobj GFile *target_file = NULL;
-      ot_lfree char *path = NULL;
-
-      path = g_file_get_relative_path (orig_etc, file);
-      g_assert (path);
-      target_file = g_file_resolve_relative_path (new_etc, path);
-
-      if (!ensure_unlinked (target_file, cancellable, error))
-        goto out;
-    }
-
-  for (i = 0; i < modified->len; i++)
-    {
-      OstreeDiffItem *diff = modified->pdata[i];
-
-      if (!copy_one_config_file (self, orig_etc, modified_etc, new_etc, diff->target,
-                                 cancellable, error))
-        goto out;
-    }
-  for (i = 0; i < added->len; i++)
-    {
-      GFile *file = added->pdata[i];
-
-      if (!copy_one_config_file (self, orig_etc, modified_etc, new_etc, file,
-                                 cancellable, error))
-        goto out;
-    }
+  refspec = argv[1];
 
-  ret = TRUE;
- out:
-  return ret;
-}
-
-/**
- * deploy_tree:
- *
- * Look up @revision in the repository, and check it out in
- * OSTREE_DIR/deploy/OS/DEPLOY_TARGET.
- *
- * Merge configuration changes from the old deployment, if any.
- *
- * Update the OSTREE_DIR/current{,-etc} and OSTREE_DIR/previous symbolic
- * links.
- */
-static gboolean
-deploy_tree (OtAdminDeploy     *self,
-             const char        *deploy_target,
-             const char        *revision,
-             GCancellable      *cancellable,
-             GError           **error)
-{
-  gboolean ret = FALSE;
-  ot_lfree char *deploy_target_fullname = NULL;
-  ot_lfree char *deploy_target_fullname_tmp = NULL;
-  ot_lobj GFile *deploy_target_path_tmp = NULL;
-  ot_lfree char *deploy_target_etc_name = NULL;
-  ot_lobj GFile *deploy_target_etc_path = NULL;
-  ot_lobj GFile *deploy_target_default_etc_path = NULL;
-  ot_lobj GFile *deploy_parent = NULL;
-  ot_lobj GFile *previous_deployment_etc = NULL;
-  ot_lobj GFile *previous_deployment_etc_default = NULL;
-  ot_lobj OstreeRepoFile *root = NULL;
-  ot_lobj GFileInfo *file_info = NULL;
-  ot_lobj GFileInfo *existing_checkout_info = NULL;
-  ot_lfree char *checkout_target_name = NULL;
-  ot_lfree char *checkout_target_tmp_name = NULL;
-  GError *temp_error = NULL;
-  gboolean skip_checkout;
-
-  if (!revision)
-    revision = deploy_target;
-
-  if (!g_file_query_exists (self->osname_dir, cancellable))
-    {
-      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                   "No OS \"%s\" found in \"%s\"", self->osname,
-                   gs_file_get_path_cached (self->osname_dir));
-      goto out;
-    }
-
-  if (!ostree_repo_resolve_rev (self->repo, revision, FALSE, &self->resolved_commit, error))
-    goto out;
-  if (!ostree_repo_resolve_rev (self->repo, revision, TRUE, &self->resolved_previous_commit, error))
-    goto out;
-
-  root = (OstreeRepoFile*)ostree_repo_file_new_root (self->repo, self->resolved_commit);
-  if (!ostree_repo_file_ensure_resolved (root, error))
-    goto out;
-
-  file_info = g_file_query_info ((GFile*)root, OSTREE_GIO_FAST_QUERYINFO,
-                                 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                 cancellable, error);
-  if (!file_info)
-    goto out;
-
-  deploy_target_fullname = g_strconcat (deploy_target, "-", self->resolved_commit, NULL);
-  self->deploy_target_path = g_file_resolve_relative_path (self->osname_dir, deploy_target_fullname);
-
-  deploy_target_fullname_tmp = g_strconcat (deploy_target_fullname, ".tmp", NULL);
-  deploy_target_path_tmp = g_file_resolve_relative_path (self->osname_dir, deploy_target_fullname_tmp);
-
-  deploy_parent = g_file_get_parent (self->deploy_target_path);
-  if (!gs_file_ensure_directory (deploy_parent, TRUE, cancellable, error))
-    goto out;
-
-  deploy_target_etc_name = g_strconcat (deploy_target, "-", self->resolved_commit, "-etc", NULL);
-  deploy_target_etc_path = g_file_resolve_relative_path (self->osname_dir, deploy_target_etc_name);
-
-  /* Delete any previous temporary data */
-  if (!gs_shutil_rm_rf (deploy_target_path_tmp, cancellable, error))
+  if (!ot_admin_get_repo (sysroot, &repo, cancellable, error))
     goto out;
 
-  existing_checkout_info = g_file_query_info (self->deploy_target_path, OSTREE_GIO_FAST_QUERYINFO,
-                                              G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                              cancellable, &temp_error);
-  if (existing_checkout_info)
-    {
-      if (opt_force)
-        {
-          if (!gs_shutil_rm_rf (self->deploy_target_path, cancellable, error))
-            goto out;
-          if (!gs_shutil_rm_rf (deploy_target_etc_path, cancellable, error))
-            goto out;
-          
-          skip_checkout = FALSE;
-        }
-      else
-        skip_checkout = TRUE;
-    }
-  else if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
-    {
-      g_clear_error (&temp_error);
-      skip_checkout = FALSE;
-    }
-  else
+  if (!ot_admin_list_deployments (sysroot, &current_bootversion, &current_deployments,
+                                  cancellable, error))
     {
-      g_propagate_error (error, temp_error);
+      g_prefix_error (error, "While listing deployments: ");
       goto out;
     }
 
-  if (!ot_admin_get_current_deployment (self->ostree_dir, self->osname, &self->previous_deployment,
-                                        cancellable, error))
-    goto out;
-  if (self->previous_deployment)
+  /* Find the currently booted deployment, if any; we will ensure it
+   * is present in the new deployment list.
+   */
+  if (!ot_admin_require_deployment_or_osname (sysroot, current_deployments,
+                                              opt_osname,
+                                              &booted_deployment,
+                                              cancellable, error))
     {
-      ot_lfree char *etc_name;
-      ot_lobj GFile *parent;
-
-      etc_name = g_strconcat (gs_file_get_basename_cached (self->previous_deployment), "-etc", NULL);
-      parent = g_file_get_parent (self->previous_deployment);
-
-      previous_deployment_etc = g_file_get_child (parent, etc_name);
-
-      if (!g_file_query_exists (previous_deployment_etc, cancellable)
-          || g_file_equal (self->previous_deployment, self->deploy_target_path))
-        g_clear_object (&previous_deployment_etc);
-      else
-        previous_deployment_etc_default = g_file_get_child (self->previous_deployment, "etc");
-
-      if (!ostree_repo_resolve_rev (self->repo, self->current_deployment_ref, TRUE,
-                                    &self->previous_deployment_revision, error))
-        goto out;
+      g_prefix_error (error, "Looking for booted deployment: ");
+      goto out;
     }
 
-
-  if (!skip_checkout)
+  if (opt_origin_path)
     {
-      ProcessOneCheckoutData checkout_data;
-      ot_lobj GFile *triggers_run_path = NULL;
-
-      g_print ("ostadmin: Creating deployment %s\n",
-               gs_file_get_path_cached (self->deploy_target_path));
-
-      memset (&checkout_data, 0, sizeof (checkout_data));
-      checkout_data.loop = g_main_loop_new (NULL, TRUE);
-      checkout_data.error = error;
-
-      ostree_repo_checkout_tree_async (self->repo, 0, 0, deploy_target_path_tmp, root,
-                                       file_info, cancellable,
-                                       on_checkout_complete, &checkout_data);
-
-      g_main_loop_run (checkout_data.loop);
-
-      g_main_loop_unref (checkout_data.loop);
-
-      if (checkout_data.caught_error)
-        goto out;
-
-      triggers_run_path = g_file_resolve_relative_path (deploy_target_path_tmp, 
"usr/share/ostree/triggers-run");
-
-      if (!g_file_query_exists (triggers_run_path, NULL))
-        {
-          if (!ostree_run_triggers_in_root (deploy_target_path_tmp, cancellable, error))
-            goto out;
-        }
-
-      deploy_target_default_etc_path = ot_gfile_get_child_strconcat (deploy_target_path_tmp, "etc", NULL);
-
-      if (!gs_shutil_rm_rf (deploy_target_etc_path, cancellable, error))
-        goto out;
-
-      if (!gs_shutil_cp_a (deploy_target_default_etc_path, deploy_target_etc_path,
-                           cancellable, error))
-        goto out;
-
-      g_print ("ostadmin: Created %s\n", gs_file_get_path_cached (deploy_target_etc_path));
-
-      if (previous_deployment_etc)
-        {
-          if (!merge_etc_changes (self, previous_deployment_etc_default,
-                                  previous_deployment_etc, deploy_target_etc_path, 
-                                  cancellable, error))
-            goto out;
-        }
-      else
-        g_print ("ostadmin: No previous deployment; therefore, no configuration changes to merge\n");
-
-      if (!gs_file_rename (deploy_target_path_tmp, self->deploy_target_path,
-                           cancellable, error))
-        goto out;
-    }
-
-  ret = TRUE;
- out:
-  return ret;
-}
-
-/**
- * do_update_kernel:
- *
- * Ensure we have a GRUB entry, initramfs set up, etc.
- */
-static gboolean
-do_update_kernel (OtAdminDeploy     *self,
-                  GCancellable      *cancellable,
-                  GError           **error)
-{
-  gboolean ret = FALSE;
-  gs_unref_object GSSubprocess *proc = NULL;
-  gs_unref_ptrarray GPtrArray *args = NULL;
-
-  args = g_ptr_array_new ();
-  ot_ptrarray_add_many (args, "ostree", "admin",
-                        "--ostree-dir", gs_file_get_path_cached (self->ostree_dir),
-                        "--boot-dir", gs_file_get_path_cached (self->admin_opts->boot_dir),
-                        "update-kernel",
-                        self->osname,
-                        gs_file_get_path_cached (self->deploy_target_path), NULL);
-  g_ptr_array_add (args, NULL);
-
-  proc = gs_subprocess_new_simple_argv ((char**)args->pdata,
-                                        GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
-                                        GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
-                                        cancellable, error);
-  if (!proc)
-    goto out;
-  if (!gs_subprocess_wait_sync_check (proc, cancellable, error))
-    goto out;
-
-  ret = TRUE;
- out:
-  return ret;
-}
-
-static gboolean
-complete_deployment (OtAdminDeploy     *self,
-                     GCancellable      *cancellable,
-                     GError           **error)
-{
-  gboolean ret = FALSE;
-
-  /* Write out a ref so that any "ostree prune" on the raw repo
-   * doesn't GC the currently deployed tree.
-   */
-  if (!ostree_repo_write_ref (self->repo, NULL, self->current_deployment_ref,
-                              self->resolved_commit, error))
-    goto out;
-  /* Only overwrite previous if it's different from what we're deploying now.
-   */
-  if (self->resolved_previous_commit != NULL
-      && strcmp (self->resolved_previous_commit, self->resolved_commit) != 0)
-    {
-      if (!ostree_repo_write_ref (self->repo, NULL, self->previous_deployment_ref,
-                                  self->previous_deployment_revision, error))
+      origin = g_key_file_new ();
+      
+      if (!g_key_file_load_from_file (origin, opt_origin_path, 0, error))
         goto out;
     }
-
-  if (!update_current (self, self->previous_deployment, self->deploy_target_path,
-                       cancellable, error))
-    goto out;
-
-  ret = TRUE;
- out:
-  return ret;
-}
-
-gboolean
-ot_admin_builtin_deploy (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error)
-{
-  GOptionContext *context;
-  OtAdminDeploy self_data;
-  OtAdminDeploy *self = &self_data;
-  gboolean ret = FALSE;
-  ot_lobj GFile *repo_path = NULL;
-  ot_lobj GFile *deploy_path = NULL;
-  const char *osname = NULL;
-  const char *deploy_target = NULL;
-  const char *revision = NULL;
-  __attribute__((unused)) GCancellable *cancellable = NULL;
-
-  memset (self, 0, sizeof (*self));
-
-  context = g_option_context_new ("OSNAME TREENAME [REVISION] - In operating system OS, check out revision 
TREENAME (or REVISION as TREENAME)");
-
-  g_option_context_add_main_entries (context, options, NULL);
-
-  if (!g_option_context_parse (context, &argc, &argv, error))
-    goto out;
-
-  if (argc < 3)
+  else
     {
-      ot_util_usage_error (context, "OSNAME and TREENAME must be specified", error);
-      goto out;
+      origin = ot_origin_new_from_refspec (refspec);
     }
 
-  self->admin_opts = admin_opts;
-  self->ostree_dir = g_object_ref (admin_opts->ostree_dir);
-
-  if (!ot_admin_ensure_initialized (self->ostree_dir, cancellable, error))
-    goto out;
-
-  repo_path = g_file_get_child (self->ostree_dir, "repo");
-  self->repo = ostree_repo_new (repo_path);
-  if (!ostree_repo_check (self->repo, error))
-    goto out;
-
-  osname = argv[1];
-  deploy_target = argv[2];
-  if (argc > 3)
-    revision = argv[3];
-
-  self->osname = g_strdup (osname);
-  self->osname_dir = ot_gfile_get_child_build_path (self->ostree_dir, "deploy", osname, NULL);
-  self->current_deployment_ref = g_strdup_printf ("deployment/%s/current", self->osname);
-  self->previous_deployment_ref = g_strdup_printf ("deployment/%s/previous", self->osname);
-
-  if (!deploy_tree (self, deploy_target, revision, cancellable, error))
+  if (!ostree_repo_resolve_rev (repo, refspec, FALSE, &revision, error))
     goto out;
 
-  if (!opt_no_kernel)
-    {
-      if (!do_update_kernel (self, cancellable, error))
-        goto out;
-    }
-
-  if (!complete_deployment (self, cancellable, error))
+  if (!ot_admin_deploy (sysroot, current_bootversion, current_deployments,
+                        opt_osname, revision, origin,
+                        opt_kernel_argv, opt_retain,
+                        booted_deployment, NULL,
+                        &new_deployment, &new_bootversion, &new_deployments,
+                        cancellable, error))
     goto out;
 
   ret = TRUE;
  out:
-  g_clear_object (&self->repo);
-  g_free (self->osname);
-  g_free (self->current_deployment_ref);
-  g_free (self->previous_deployment_ref);
-  g_free (self->resolved_commit);
-  g_free (self->resolved_previous_commit);
-  g_free (self->previous_deployment_revision);
-  g_clear_object (&self->previous_deployment);
-  g_clear_object (&self->ostree_dir);
-  g_clear_object (&self->osname_dir);
+  if (origin)
+    g_key_file_unref (origin);
   if (context)
     g_option_context_free (context);
   return ret;
diff --git a/src/ostree/ot-admin-builtin-diff.c b/src/ostree/ot-admin-builtin-diff.c
index 7792db3..97fb16a 100644
--- a/src/ostree/ot-admin-builtin-diff.c
+++ b/src/ostree/ot-admin-builtin-diff.c
@@ -28,7 +28,10 @@
 
 #include <glib/gi18n.h>
 
+static char *opt_osname;
+
 static GOptionEntry options[] = {
+  { "os", 0, 0, G_OPTION_ARG_STRING, &opt_osname, "Specify operating system root to use", NULL },
   { NULL }
 };
 
@@ -37,57 +40,55 @@ ot_admin_builtin_diff (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GE
 {
   GOptionContext *context;
   gboolean ret = FALSE;
-  const char *osname;
-  GFile *ostree_dir = admin_opts->ostree_dir;
+  gs_free char *booted_osname = NULL;
   ot_lobj GFile *repo_path = NULL;
-  ot_lobj GFile *deployment = NULL;
+  gs_unref_object OtDeployment *deployment = NULL;
+  gs_unref_object GFile *deployment_dir = NULL;
   ot_lobj GFile *deploy_parent = NULL;
   ot_lptrarray GPtrArray *modified = NULL;
   ot_lptrarray GPtrArray *removed = NULL;
   ot_lptrarray GPtrArray *added = NULL;
+  gs_unref_ptrarray GPtrArray *deployments = NULL;
   ot_lobj GFile *orig_etc_path = NULL;
   ot_lobj GFile *new_etc_path = NULL;
   __attribute__((unused)) GCancellable *cancellable = NULL;
+  int bootversion;
 
-  context = g_option_context_new ("OSNAME [REVISION] - Diff configuration for OSNAME");
+  context = g_option_context_new ("Diff current /etc configuration versus default");
 
   g_option_context_add_main_entries (context, options, NULL);
 
   if (!g_option_context_parse (context, &argc, &argv, error))
     goto out;
   
-  repo_path = g_file_get_child (ostree_dir, "repo");
+  repo_path = g_file_resolve_relative_path (admin_opts->sysroot, "ostree/repo");
 
-  if (argc < 2)
+  if (!ot_admin_list_deployments (admin_opts->sysroot, &bootversion, &deployments,
+                                  cancellable, error))
     {
-      ot_util_usage_error (context, "OSNAME must be specified", error);
+      g_prefix_error (error, "While listing deployments: ");
       goto out;
     }
 
-  osname = argv[1];
-
-  if (argc > 2)
-    {
-      deployment = ot_gfile_get_child_build_path (ostree_dir, "deploy", osname, argv[2], NULL);
-      if (!g_file_query_exists (deployment, NULL))
-        {
-          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                       "Deployment %s doesn't exist", gs_file_get_path_cached (deployment));
-          goto out;
-        }
-    }
-  else
+  if (!ot_admin_require_deployment_or_osname (admin_opts->sysroot, deployments,
+                                              opt_osname, &deployment,
+                                              cancellable, error))
+    goto out;
+  if (deployment != NULL)
+    opt_osname = (char*)ot_deployment_get_osname (deployment);
+  if (deployment == NULL)
+    deployment = ot_admin_get_merge_deployment (deployments, opt_osname, deployment, NULL);
+  if (deployment == NULL)
     {
-      if (!ot_admin_get_current_deployment (ostree_dir, osname, &deployment,
-                                            cancellable, error))
-        goto out;
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                   "No deployment for OS '%s'", opt_osname);
+      goto out;
     }
 
-  orig_etc_path = g_file_resolve_relative_path (deployment, "etc");
-  deploy_parent = g_file_get_parent (deployment);
-  new_etc_path = ot_gfile_get_child_strconcat (deploy_parent,
-                                               gs_file_get_basename_cached (deployment),
-                                               "-etc", NULL);
+  deployment_dir = ot_admin_get_deployment_directory (admin_opts->sysroot, deployment);
+
+  orig_etc_path = g_file_resolve_relative_path (deployment_dir, "usr/etc");
+  new_etc_path = g_file_resolve_relative_path (deployment_dir, "etc");
   
   modified = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_diff_item_unref);
   removed = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
diff --git a/src/ostree/ot-admin-builtin-init-fs.c b/src/ostree/ot-admin-builtin-init-fs.c
index d8a5355..dc1a45a 100644
--- a/src/ostree/ot-admin-builtin-init-fs.c
+++ b/src/ostree/ot-admin-builtin-init-fs.c
@@ -75,8 +75,7 @@ ot_admin_builtin_init_fs (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
     goto out;
   g_clear_object (&child);
 
-  child = g_file_get_child (dir, "ostree");
-  if (!ot_admin_ensure_initialized (child, cancellable, error))
+  if (!ot_admin_ensure_initialized (dir, cancellable, error))
     goto out;
 
   ret = TRUE;
diff --git a/src/ostree/ot-admin-builtin-install.c b/src/ostree/ot-admin-builtin-install.c
index 5f5fb3d..675f52c 100644
--- a/src/ostree/ot-admin-builtin-install.c
+++ b/src/ostree/ot-admin-builtin-install.c
@@ -70,7 +70,6 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
   gboolean ret = FALSE;
   const char *keyfile_arg = NULL;
   const char *treename_arg = NULL;
-  GFile *ostree_dir = admin_opts->ostree_dir;
   ot_lobj GFile *deploy_dir = NULL;
   ot_lobj GFile *osdir = NULL;
   ot_lobj GFile *dest_osconfig_path = NULL;
@@ -96,14 +95,7 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
       goto out;
     }
 
-  if (admin_opts->ostree_dir == NULL)
-    {
-      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                   "No existing /ostree found; use --ostree-dir");
-      goto out;
-    }
-
-  if (!ot_admin_ensure_initialized (admin_opts->ostree_dir, cancellable, error))
+  if (!ot_admin_ensure_initialized (admin_opts->sysroot, cancellable, error))
     goto out;
 
   self->loop = g_main_loop_new (NULL, TRUE);
@@ -136,11 +128,11 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
 
   osname = g_key_file_get_string (keyfile, "os", "Name", error);
 
-  ostree_dir_arg = g_strconcat ("--ostree-dir=",
-                                gs_file_get_path_cached (ostree_dir),
+  ostree_dir_arg = g_strconcat ("--sysroot=",
+                                gs_file_get_path_cached (admin_opts->sysroot),
                                 NULL);
 
-  if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
+  if (!gs_subprocess_simple_run_sync (NULL,
                                       GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                       cancellable, error,
                                       "ostree", "admin", ostree_dir_arg, "os-init", osname, NULL))
@@ -157,7 +149,7 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
         goto out;
     }
 
-  osdir = ot_gfile_get_child_build_path (ostree_dir, "deploy", osname, NULL);
+  osdir = ot_gfile_get_child_build_path (admin_opts->sysroot, "ostree", "deploy", osname, NULL);
   dest_osconfig_path = ot_gfile_get_child_strconcat (osdir, osname, ".cfg", NULL);
 
   if (!g_file_copy (self->osconfig_path, dest_osconfig_path, G_FILE_COPY_OVERWRITE | 
G_FILE_COPY_TARGET_DEFAULT_PERMS,
@@ -168,7 +160,7 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
     goto out;
 
   repoarg = g_strconcat ("--repo=",
-                         gs_file_get_path_cached (ostree_dir), "/repo",
+                         gs_file_get_path_cached (admin_opts->sysroot), "/ostree/repo",
                          NULL);
 
   {
@@ -178,7 +170,7 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
     if (!repourl)
       goto out;
     
-    if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
+    if (!gs_subprocess_simple_run_sync (NULL,
                                         GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                         cancellable, error,
                                         "ostree", repoarg, "remote", "add",
@@ -186,13 +178,13 @@ ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
       goto out;
   }
 
-  if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
+  if (!gs_subprocess_simple_run_sync (NULL,
                                       GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                       cancellable, error,
                                         "ostree", "pull", repoarg, osname, NULL))
     goto out;
 
-  if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
+  if (!gs_subprocess_simple_run_sync (NULL,
                                       GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                       cancellable, error,
                                       "ostree", "admin", ostree_dir_arg, "deploy", osname,
diff --git a/src/ostree/ot-admin-builtin-os-init.c b/src/ostree/ot-admin-builtin-os-init.c
index 0536167..8d725fd 100644
--- a/src/ostree/ot-admin-builtin-os-init.c
+++ b/src/ostree/ot-admin-builtin-os-init.c
@@ -38,7 +38,6 @@ ot_admin_builtin_os_init (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
   GOptionContext *context;
   gboolean ret = FALSE;
   const char *osname = NULL;
-  GFile *ostree_dir = admin_opts->ostree_dir;
   ot_lobj GFile *deploy_dir = NULL;
   ot_lobj GFile *dir = NULL;
   __attribute__((unused)) GCancellable *cancellable = NULL;
@@ -49,7 +48,7 @@ ot_admin_builtin_os_init (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
   if (!g_option_context_parse (context, &argc, &argv, error))
     goto out;
 
-  if (!ot_admin_ensure_initialized (ostree_dir, cancellable, error))
+  if (!ot_admin_ensure_initialized (admin_opts->sysroot, cancellable, error))
     goto out;
 
   if (argc < 2)
@@ -60,7 +59,7 @@ ot_admin_builtin_os_init (int argc, char **argv, OtAdminBuiltinOpts *admin_opts,
 
   osname = argv[1];
 
-  deploy_dir = ot_gfile_get_child_build_path (ostree_dir, "deploy", osname, NULL);
+  deploy_dir = ot_gfile_get_child_build_path (admin_opts->sysroot, "ostree", "deploy", osname, NULL);
 
   /* Ensure core subdirectories of /var exist, since we need them for
    * dracut generation, and the host will want them too.  Note that at
diff --git a/src/ostree/ot-admin-builtin-prune.c b/src/ostree/ot-admin-builtin-prune.c
index 4d60099..024abd2 100644
--- a/src/ostree/ot-admin-builtin-prune.c
+++ b/src/ostree/ot-admin-builtin-prune.c
@@ -41,15 +41,12 @@ ot_admin_builtin_prune (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, G
 {
   GOptionContext *context;
   gboolean ret = FALSE;
-  guint i;
   const char *osname;
-  GFile *ostree_dir = admin_opts->ostree_dir;
   ot_lobj GFile *repo_path = NULL;
   ot_lobj GFile *deploy_dir = NULL;
   ot_lobj GFile *current_deployment = NULL;
   ot_lobj GFile *previous_deployment = NULL;
   ot_lobj GFile *active_deployment = NULL;
-  ot_lptrarray GPtrArray *deployments = NULL;
   gs_free char *active_osname = NULL;
   __attribute__((unused)) GCancellable *cancellable = NULL;
 
@@ -68,43 +65,10 @@ ot_admin_builtin_prune (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, G
 
   osname = argv[1];
 
-  if (!ot_admin_list_deployments (ostree_dir, osname, &deployments,
-                                  cancellable, error))
+  if (!ot_admin_cleanup (admin_opts->sysroot, cancellable, error))
     goto out;
 
-  if (!ot_admin_get_current_deployment (ostree_dir, osname, &current_deployment,
-                                        cancellable, error));
-  if (!ot_admin_get_previous_deployment (ostree_dir, osname, &previous_deployment,
-                                         cancellable, error));
-  if (!ot_admin_get_active_deployment (ostree_dir, &active_osname, &active_deployment,
-                                       cancellable, error));
-
-  for (i = 0; i < deployments->len; i++)
-    {
-      GFile *deployment = deployments->pdata[i];
-      ot_lobj GFile *deployment_etc = NULL;
-      ot_lobj GFile *parent = NULL;
-
-      if ((current_deployment && g_file_equal (deployment, current_deployment))
-          || (previous_deployment && g_file_equal (deployment, previous_deployment))
-          || (active_deployment && g_file_equal (deployment, active_deployment)))
-        continue;
-
-      parent = g_file_get_parent (deployment);
-      deployment_etc = ot_gfile_get_child_strconcat (parent, gs_file_get_basename_cached (deployment),
-                                                     "-etc", NULL);
-      
-      g_print ("Deleting deployment %s\n", gs_file_get_path_cached (deployment));
-      if (!gs_shutil_rm_rf (deployment, cancellable, error))
-        goto out;
-      /* Note - not atomic; we may be leaving the -etc directory around
-       * if this fails in the middle =/
-       */
-      if (!gs_shutil_rm_rf (deployment_etc, cancellable, error))
-        goto out;
-    }
-  
-  repo_path = g_file_get_child (ostree_dir, "repo");
+  repo_path = g_file_resolve_relative_path (admin_opts->sysroot, "ostree/repo");
 
   if (!opt_no_repo_prune)
     {
@@ -112,7 +76,7 @@ ot_admin_builtin_prune (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, G
 
       repo_arg = g_strconcat ("--repo=", gs_file_get_path_cached (repo_path), NULL);
       
-      if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
+      if (!gs_subprocess_simple_run_sync (NULL,
                                           GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                           cancellable, error,
                                           "ostree", repo_arg, "prune", "--refs-only",
diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c
new file mode 100644
index 0000000..3e26d9c
--- /dev/null
+++ b/src/ostree/ot-admin-builtin-status.c
@@ -0,0 +1,100 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2012,2013 Colin Walters <walters verbum org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Colin Walters <walters verbum org>
+ */
+
+#include "config.h"
+
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "ostree.h"
+
+#include <glib/gi18n.h>
+
+static GOptionEntry options[] = {
+  { NULL }
+};
+
+gboolean
+ot_admin_builtin_status (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error)
+{
+  GOptionContext *context;
+  gboolean ret = FALSE;
+  int bootversion;
+  gs_unref_object OtDeployment *booted_deployment = NULL;
+  gs_unref_ptrarray GPtrArray *deployments = NULL;
+  __attribute__((unused)) GCancellable *cancellable = NULL;
+  guint i;
+
+  context = g_option_context_new ("List deployments");
+
+  g_option_context_add_main_entries (context, options, NULL);
+
+  if (!g_option_context_parse (context, &argc, &argv, error))
+    goto out;
+
+  if (!ot_admin_list_deployments (admin_opts->sysroot, &bootversion, &deployments,
+                                  cancellable, error))
+    {
+      g_prefix_error (error, "While listing deployments: ");
+      goto out;
+    }
+
+  /* Find the currently booted deployment, if any; we will
+   * ensure it is present in the new deployment list.
+   */
+  if (!ot_admin_find_booted_deployment (admin_opts->sysroot, deployments,
+                                        &booted_deployment,
+                                        cancellable, error))
+    goto out;
+
+  if (deployments->len == 0)
+    {
+      g_print ("No deployments.\n");
+    }
+  else
+    {
+      int subbootversion;
+
+      if (!ot_admin_read_current_subbootversion (admin_opts->sysroot, bootversion,
+                                                 &subbootversion,
+                                                 cancellable, error))
+        goto out;
+
+      g_print ("bootversion: %d.%d\n", bootversion, subbootversion);
+
+      for (i = 0; i < deployments->len; i++)
+        {
+          OtDeployment *deployment = deployments->pdata[i];
+          g_print ("%u: %c %s %s.%d\n",
+                   i,
+                   deployment == booted_deployment ? '*' : ' ',
+                   ot_deployment_get_osname (deployment),
+                   ot_deployment_get_csum (deployment),
+                   ot_deployment_get_deployserial (deployment));
+        }
+    }
+
+  ret = TRUE;
+ out:
+  if (context)
+    g_option_context_free (context);
+  return ret;
+}
diff --git a/src/ostree/ot-admin-builtin-upgrade.c b/src/ostree/ot-admin-builtin-upgrade.c
index f13ce21..35a2e12 100644
--- a/src/ostree/ot-admin-builtin-upgrade.c
+++ b/src/ostree/ot-admin-builtin-upgrade.c
@@ -24,6 +24,7 @@
 
 #include "ot-admin-builtins.h"
 #include "ot-admin-functions.h"
+#include "ot-admin-deploy.h"
 #include "ostree.h"
 #include "otutil.h"
 
@@ -32,8 +33,10 @@
 #include <glib/gi18n.h>
 
 static gboolean opt_reboot;
+static char *opt_osname;
 
 static GOptionEntry options[] = {
+  { "os", 0, 0, G_OPTION_ARG_STRING, &opt_osname, "Specify operating system root to use", NULL },
   { "reboot", 'r', 0, G_OPTION_ARG_NONE, &opt_reboot, "Reboot after a successful upgrade", NULL },
   { NULL }
 };
@@ -41,79 +44,106 @@ static GOptionEntry options[] = {
 gboolean
 ot_admin_builtin_upgrade (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error)
 {
-  GOptionContext *context;
   gboolean ret = FALSE;
-  GFile *ostree_dir = admin_opts->ostree_dir;
+  __attribute__((unused)) GCancellable *cancellable = NULL;
+  GOptionContext *context;
+  GFile *sysroot = admin_opts->sysroot;
   gs_free char *booted_osname = NULL;
-  const char *osname = NULL;
-  gs_unref_object GFile *deployment = NULL;
-  gs_unref_object GFile *repo_path = NULL;
   gs_unref_object OstreeRepo *repo = NULL;
-  gs_free char *deploy_name = NULL;
-  gs_free char *current_rev = NULL;
-  gs_free char *new_rev = NULL;
+  gs_unref_object GFile *repo_path = NULL;
+  gs_free char *origin_refspec = NULL;
+  gs_free char *origin_remote = NULL;
+  gs_free char *origin_ref = NULL;
+  gs_free char *new_revision = NULL;
+  gs_unref_object GFile *deployment_path = NULL;
+  gs_unref_object GFile *deployment_origin_path = NULL;
+  gs_unref_object OtDeployment *booted_deployment = NULL;
+  gs_unref_object OtDeployment *merge_deployment = NULL;
+  gs_unref_ptrarray GPtrArray *current_deployments = NULL;
+  gs_unref_ptrarray GPtrArray *new_deployments = NULL;
+  gs_unref_object OtDeployment *new_deployment = NULL;
   gs_free char *ostree_dir_arg = NULL;
-  __attribute__((unused)) GCancellable *cancellable = NULL;
+  int current_bootversion;
+  int new_bootversion;
+  GKeyFile *origin;
 
-  context = g_option_context_new ("[OSNAME] - pull, deploy, and prune");
+  context = g_option_context_new ("Construct new tree from current origin and deploy it, if it changed");
   g_option_context_add_main_entries (context, options, NULL);
 
   if (!g_option_context_parse (context, &argc, &argv, error))
     goto out;
 
-  if (argc > 1)
+  if (!ot_admin_list_deployments (admin_opts->sysroot, &current_bootversion,
+                                  &current_deployments,
+                                  cancellable, error))
     {
-      osname = argv[1];
+      g_prefix_error (error, "While listing deployments: ");
+      goto out;
     }
-  else
-    {
-      if (!ot_admin_get_booted_os (&booted_osname, NULL,
-                                   cancellable, error))
-        goto out;
-      if (booted_osname == NULL)
-        {
-          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                       "Not in an active OSTree system; must specify OSNAME");
-          goto out;
-        }
-      osname = booted_osname;
-    }
-  
-  if (!ot_admin_get_current_deployment (ostree_dir, osname, &deployment,
-                                        cancellable, error))
+
+  if (!ot_admin_require_deployment_or_osname (admin_opts->sysroot, current_deployments,
+                                              opt_osname,
+                                              &booted_deployment,
+                                              cancellable, error))
     goto out;
+  merge_deployment = ot_admin_get_merge_deployment (current_deployments, opt_osname,
+                                                    booted_deployment,
+                                                    NULL);
 
-  ot_admin_parse_deploy_name (ostree_dir, osname, deployment, &deploy_name, &current_rev);
+  deployment_path = ot_admin_get_deployment_directory (admin_opts->sysroot, merge_deployment);
+  deployment_origin_path = ot_admin_get_deployment_origin_path (deployment_path);
 
-  ostree_dir_arg = g_strconcat ("--ostree-dir=",
-                                gs_file_get_path_cached (ostree_dir),
-                                NULL);
-  
-  if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
-                                      GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
-                                      cancellable, error,
-                                      "ostree", "admin", ostree_dir_arg, "pull-deploy", osname, NULL))
+  repo_path = g_file_resolve_relative_path (admin_opts->sysroot, "ostree/repo");
+  repo = ostree_repo_new (repo_path);
+  if (!ostree_repo_check (repo, error))
     goto out;
 
-  if (!gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
-                                      GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
-                                      cancellable, error,
-                                      "ostree", "admin", ostree_dir_arg, "prune", osname, NULL))
+  origin = ot_deployment_get_origin (merge_deployment);
+  if (!origin)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No origin known for current deployment");
+      goto out;
+    }
+  origin_refspec = g_key_file_get_string (origin, "origin", "refspec", NULL);
+  if (!origin_refspec)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No origin/refspec in current deployment origin; cannot upgrade via ostree");
+      goto out;
+    }
+  if (!ostree_parse_refspec (origin_refspec, &origin_remote, &origin_ref, error))
     goto out;
 
-  if (opt_reboot)
+  if (origin_remote)
     {
-      repo_path = g_file_get_child (ostree_dir, "repo");
-
-      repo = ostree_repo_new (repo_path);
-      if (!ostree_repo_check (repo, error))
+      g_print ("Fetching remote %s ref %s\n", origin_remote, origin_ref);
+      if (!ot_admin_pull (admin_opts->sysroot, origin_remote, origin_ref,
+                          cancellable, error))
         goto out;
+    }
 
-      if (!ostree_repo_resolve_rev (repo, deploy_name, TRUE, &new_rev,
-                                    error))
+  if (!ostree_repo_resolve_rev (repo, origin_ref, FALSE, &new_revision,
+                                error))
+    goto out;
+
+  if (strcmp (ot_deployment_get_csum (merge_deployment), new_revision) == 0)
+    {
+      g_print ("Refspec %s is unchanged\n", origin_refspec);
+    }
+  else
+    {
+      gs_unref_object GFile *real_sysroot = g_file_new_for_path ("/");
+      if (!ot_admin_deploy (admin_opts->sysroot,
+                            current_bootversion, current_deployments,
+                            opt_osname, new_revision, origin,
+                            NULL, FALSE,
+                            booted_deployment, merge_deployment,
+                            &new_deployment, &new_bootversion, &new_deployments,
+                            cancellable, error))
         goto out;
 
-      if (strcmp (current_rev, new_rev) != 0 && opt_reboot)
+      if (opt_reboot && g_file_equal (sysroot, real_sysroot))
         {
           gs_subprocess_simple_run_sync (NULL, GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                          cancellable, error,
diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h
index 5fbdacd..9924086 100644
--- a/src/ostree/ot-admin-builtins.h
+++ b/src/ostree/ot-admin-builtins.h
@@ -28,8 +28,7 @@
 G_BEGIN_DECLS
 
 typedef struct {
-  GFile *ostree_dir;
-  GFile *boot_dir;
+  GFile *sysroot;
 } OtAdminBuiltinOpts;
 
 gboolean ot_admin_builtin_os_init (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
@@ -37,10 +36,9 @@ gboolean ot_admin_builtin_install (int argc, char **argv, OtAdminBuiltinOpts *ad
 gboolean ot_admin_builtin_init_fs (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
 gboolean ot_admin_builtin_deploy (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
 gboolean ot_admin_builtin_prune (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
-gboolean ot_admin_builtin_pull_deploy (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError 
**error);
+gboolean ot_admin_builtin_status (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
 gboolean ot_admin_builtin_diff (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
 gboolean ot_admin_builtin_run_triggers (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError 
**error);
-gboolean ot_admin_builtin_update_kernel (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError 
**error);
 gboolean ot_admin_builtin_upgrade (int argc, char **argv, OtAdminBuiltinOpts *admin_opts, GError **error);
 
 G_END_DECLS
diff --git a/src/ostree/ot-admin-deploy.c b/src/ostree/ot-admin-deploy.c
new file mode 100644
index 0000000..8f0caa9
--- /dev/null
+++ b/src/ostree/ot-admin-deploy.c
@@ -0,0 +1,1219 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2012 Colin Walters <walters verbum org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Colin Walters <walters verbum org>
+ */
+
+#include "config.h"
+
+#include "ot-admin-functions.h"
+#include "ot-admin-deploy.h"
+#include "ot-deployment.h"
+#include "ot-config-parser.h"
+#include "ot-bootloader-syslinux.h"
+#include "otutil.h"
+#include "ostree-core.h"
+#include "libgsystem.h"
+
+typedef struct {
+  GError **error;
+  gboolean caught_error;
+
+  GMainLoop *loop;
+} ProcessOneCheckoutData;
+
+static void
+on_checkout_complete (GObject         *object,
+                      GAsyncResult    *result,
+                      gpointer         user_data)
+{
+  ProcessOneCheckoutData *data = user_data;
+  GError *local_error = NULL;
+
+  if (!ostree_repo_checkout_tree_finish ((OstreeRepo*)object, result,
+                                         &local_error))
+    goto out;
+
+ out:
+  if (local_error)
+    {
+      data->caught_error = TRUE;
+      g_propagate_error (data->error, local_error);
+    }
+  g_main_loop_quit (data->loop);
+}
+
+
+/**
+ * copy_one_config_file:
+ *
+ * Copy @file from @modified_etc to @new_etc, overwriting any existing
+ * file there.
+ */
+static gboolean
+copy_one_config_file (GFile              *orig_etc,
+                      GFile              *modified_etc,
+                      GFile              *new_etc,
+                      GFile              *src,
+                      GCancellable       *cancellable,
+                      GError            **error)
+{
+  gboolean ret = FALSE;
+  ot_lobj GFileInfo *src_info = NULL;
+  ot_lobj GFile *dest = NULL;
+  ot_lobj GFile *parent = NULL;
+  ot_lfree char *relative_path = NULL;
+  
+  relative_path = g_file_get_relative_path (modified_etc, src);
+  g_assert (relative_path);
+  dest = g_file_resolve_relative_path (new_etc, relative_path);
+
+  src_info = g_file_query_info (src, OSTREE_GIO_FAST_QUERYINFO, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                cancellable, error);
+  if (!src_info)
+    goto out;
+
+  if (g_file_info_get_file_type (src_info) == G_FILE_TYPE_DIRECTORY)
+    {
+      ot_lobj GFileEnumerator *src_enum = NULL;
+      ot_lobj GFileInfo *child_info = NULL;
+      GError *temp_error = NULL;
+
+      /* FIXME actually we need to copy permissions and xattrs */
+      if (!gs_file_ensure_directory (dest, TRUE, cancellable, error))
+        goto out;
+
+      src_enum = g_file_enumerate_children (src, OSTREE_GIO_FAST_QUERYINFO,
+                                            G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                            cancellable, error);
+
+      while ((child_info = g_file_enumerator_next_file (src_enum, cancellable, error)) != NULL)
+        {
+          ot_lobj GFile *child = g_file_get_child (src, g_file_info_get_name (child_info));
+
+          if (!copy_one_config_file (orig_etc, modified_etc, new_etc, child,
+                                     cancellable, error))
+            goto out;
+        }
+      g_clear_object (&child_info);
+      if (temp_error != NULL)
+        {
+          g_propagate_error (error, temp_error);
+          goto out;
+        }
+    }
+  else
+    {
+      parent = g_file_get_parent (dest);
+
+      /* FIXME actually we need to copy permissions and xattrs */
+      if (!gs_file_ensure_directory (parent, TRUE, cancellable, error))
+        goto out;
+      
+      /* We unlink here because otherwise gio throws an error on
+       * dangling symlinks.
+       */
+      if (!ot_gfile_ensure_unlinked (dest, cancellable, error))
+        goto out;
+
+      if (!g_file_copy (src, dest, G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS | 
G_FILE_COPY_ALL_METADATA,
+                        cancellable, NULL, NULL, error))
+        goto out;
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+/**
+ * merge_etc_changes:
+ *
+ * Compute the difference between @orig_etc and @modified_etc,
+ * and apply that to @new_etc.
+ *
+ * The algorithm for computing the difference is pretty simple; it's
+ * approximately equivalent to "diff -unR orig_etc modified_etc",
+ * except that rather than attempting a 3-way merge if a file is also
+ * changed in @new_etc, the modified version always wins.
+ */
+static gboolean
+merge_etc_changes (GFile          *orig_etc,
+                   GFile          *modified_etc,
+                   GFile          *new_etc,
+                   GCancellable   *cancellable,
+                   GError        **error)
+{
+  gboolean ret = FALSE;
+  ot_lobj GFile *ostree_etc = NULL;
+  ot_lobj GFile *tmp_etc = NULL;
+  ot_lptrarray GPtrArray *modified = NULL;
+  ot_lptrarray GPtrArray *removed = NULL;
+  ot_lptrarray GPtrArray *added = NULL;
+  guint i;
+
+  modified = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_diff_item_unref);
+  removed = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+  added = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+
+  if (!ostree_diff_dirs (orig_etc, modified_etc, modified, removed, added,
+                         cancellable, error))
+    {
+      g_prefix_error (error, "While computing configuration diff: ");
+      goto out;
+    }
+
+  if (modified->len > 0 || removed->len > 0 || added->len > 0)
+    g_print ("ostadmin: Processing config: %u modified, %u removed, %u added\n", 
+             modified->len,
+             removed->len,
+             added->len);
+  else
+    g_print ("ostadmin: No modified configuration\n");
+
+  for (i = 0; i < removed->len; i++)
+    {
+      GFile *file = removed->pdata[i];
+      ot_lobj GFile *target_file = NULL;
+      ot_lfree char *path = NULL;
+
+      path = g_file_get_relative_path (orig_etc, file);
+      g_assert (path);
+      target_file = g_file_resolve_relative_path (new_etc, path);
+
+      if (!ot_gfile_ensure_unlinked (target_file, cancellable, error))
+        goto out;
+    }
+
+  for (i = 0; i < modified->len; i++)
+    {
+      OstreeDiffItem *diff = modified->pdata[i];
+
+      if (!copy_one_config_file (orig_etc, modified_etc, new_etc, diff->target,
+                                 cancellable, error))
+        goto out;
+    }
+  for (i = 0; i < added->len; i++)
+    {
+      GFile *file = added->pdata[i];
+
+      if (!copy_one_config_file (orig_etc, modified_etc, new_etc, file,
+                                 cancellable, error))
+        goto out;
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+/**
+ * checkout_deployment_tree:
+ *
+ * Look up @revision in the repository, and check it out in
+ * /ostree/deploy/OS/deploy/${treecsum}.${deployserial}.
+ */
+static gboolean
+checkout_deployment_tree (GFile             *sysroot,
+                          OstreeRepo        *repo,
+                          OtDeployment      *deployment,
+                          GFile            **out_deployment_path,
+                          GCancellable      *cancellable,
+                          GError           **error)
+{
+  gboolean ret = FALSE;
+  const char *csum = ot_deployment_get_csum (deployment);
+  gs_unref_object OstreeRepoFile *root = NULL;
+  gs_unref_object GFileInfo *file_info = NULL;
+  gs_unref_object GFileInfo *existing_checkout_info = NULL;
+  gs_free char *checkout_target_name = NULL;
+  gs_free char *checkout_target_tmp_name = NULL;
+  gs_unref_object GFile *osdeploy_path = NULL;
+  gs_unref_object GFile *deploy_target_path = NULL;
+  gs_unref_object GFile *deploy_parent = NULL;
+  ProcessOneCheckoutData checkout_data;
+
+  root = (OstreeRepoFile*)ostree_repo_file_new_root (repo, csum);
+  if (!ostree_repo_file_ensure_resolved (root, error))
+    goto out;
+
+  file_info = g_file_query_info ((GFile*)root, OSTREE_GIO_FAST_QUERYINFO,
+                                 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                 cancellable, error);
+  if (!file_info)
+    goto out;
+
+  osdeploy_path = ot_gfile_get_child_build_path (sysroot, "ostree", "deploy",
+                                                 ot_deployment_get_osname (deployment),
+                                                 "deploy", NULL);
+  checkout_target_name = g_strdup_printf ("%s.%d", csum, ot_deployment_get_deployserial (deployment));
+  deploy_target_path = g_file_get_child (osdeploy_path, checkout_target_name);
+
+  deploy_parent = g_file_get_parent (deploy_target_path);
+  if (!gs_file_ensure_directory (deploy_parent, TRUE, cancellable, error))
+    goto out;
+  
+  g_print ("ostadmin: Creating deployment %s\n",
+           gs_file_get_path_cached (deploy_target_path));
+
+  memset (&checkout_data, 0, sizeof (checkout_data));
+  checkout_data.loop = g_main_loop_new (NULL, TRUE);
+  checkout_data.error = error;
+  
+  ostree_repo_checkout_tree_async (repo, 0, 0, deploy_target_path, root,
+                                   file_info, cancellable,
+                                   on_checkout_complete, &checkout_data);
+  
+  g_main_loop_run (checkout_data.loop);
+  
+  g_main_loop_unref (checkout_data.loop);
+  
+  if (checkout_data.caught_error)
+    goto out;
+
+  ret = TRUE;
+  ot_transfer_out_value (out_deployment_path, &deploy_target_path);
+ out:
+  return ret;
+}
+
+static gboolean
+merge_configuration (GFile             *sysroot,
+                     OtDeployment      *previous_deployment,
+                     OtDeployment      *deployment,
+                     GFile             *deployment_path,
+                     GCancellable      *cancellable,
+                     GError           **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *source_etc_path = NULL;
+  gs_unref_object GFile *source_etc_pristine_path = NULL;
+  gs_unref_object GFile *deployment_usretc_path = NULL;
+  gs_unref_object GFile *deployment_etc_path = NULL;
+  gboolean etc_exists;
+  gboolean usretc_exists;
+
+  if (previous_deployment)
+    {
+      gs_unref_object GFile *previous_path = NULL;
+      OtConfigParser *previous_bootconfig;
+
+      previous_path = ot_admin_get_deployment_directory (sysroot, previous_deployment);
+      source_etc_path = g_file_resolve_relative_path (previous_path, "etc");
+      source_etc_pristine_path = g_file_resolve_relative_path (previous_path, "usr/etc");
+
+      previous_bootconfig = ot_deployment_get_bootconfig (previous_deployment);
+      if (previous_bootconfig)
+        {
+          const char *previous_options = ot_config_parser_get (previous_bootconfig, "options");
+          /* Completely overwrite the previous options here; we will extend
+           * them later.
+           */
+          ot_config_parser_set (ot_deployment_get_bootconfig (deployment), "options",
+                                previous_options);
+        }
+    }
+
+  deployment_etc_path = g_file_get_child (deployment_path, "etc");
+  deployment_usretc_path = g_file_resolve_relative_path (deployment_path, "usr/etc");
+  
+  etc_exists = g_file_query_exists (deployment_etc_path, NULL);
+  usretc_exists = g_file_query_exists (deployment_usretc_path, NULL);
+
+  if (etc_exists && usretc_exists)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                           "Tree contains both /etc and /usr/etc");
+      goto out;
+    }
+  else if (etc_exists)
+    {
+      /* Compatibility hack */
+      if (!gs_file_rename (deployment_etc_path, deployment_usretc_path,
+                           cancellable, error))
+        goto out;
+      usretc_exists = TRUE;
+      etc_exists = FALSE;
+    }
+  
+  if (usretc_exists)
+    {
+      g_assert (!etc_exists);
+      if (!gs_shutil_cp_a (deployment_usretc_path, deployment_etc_path,
+                           cancellable, error))
+        goto out;
+      g_print ("ostadmin: Created %s\n", gs_file_get_path_cached (deployment_etc_path));
+    }
+
+  if (source_etc_path)
+    {
+      if (!merge_etc_changes (source_etc_pristine_path, source_etc_path, deployment_etc_path, 
+                              cancellable, error))
+        goto out;
+    }
+  else
+    {
+      g_print ("ostadmin: No previous configuration changes to merge\n");
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static gboolean
+write_origin_file (GFile             *sysroot,
+                   OtDeployment      *deployment,
+                   GCancellable      *cancellable,
+                   GError           **error)
+{
+  gboolean ret = FALSE;
+  GKeyFile *origin = ot_deployment_get_origin (deployment);
+
+  if (origin)
+    {
+      gs_unref_object GFile *deployment_path = ot_admin_get_deployment_directory (sysroot, deployment);
+      gs_unref_object GFile *origin_path = ot_admin_get_deployment_origin_path (deployment_path);
+      gs_free char *contents = NULL;
+      gsize len;
+
+      contents = g_key_file_to_data (origin, &len, error);
+      if (!contents)
+        goto out;
+
+      if (!g_file_replace_contents (origin_path, contents, len, NULL, FALSE,
+                                    G_FILE_CREATE_REPLACE_DESTINATION, NULL,
+                                    cancellable, error))
+        goto out;
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static gboolean
+get_kernel_from_tree (GFile         *deployroot,
+                      GFile        **out_kernel,
+                      GFile        **out_initramfs,
+                      GCancellable  *cancellable,
+                      GError       **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *bootdir = g_file_get_child (deployroot, "boot");
+  gs_unref_object GFileEnumerator *dir_enum = NULL;
+  gs_unref_object GFileInfo *file_info = NULL;
+  gs_unref_object GFile *ret_kernel = NULL;
+  gs_unref_object GFile *ret_initramfs = NULL;
+
+  dir_enum = g_file_enumerate_children (bootdir, OSTREE_GIO_FAST_QUERYINFO,
+                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                        NULL, error);
+  if (!dir_enum)
+    goto out;
+
+  while (TRUE)
+    {
+      GFileInfo *file_info = NULL;
+      const char *name;
+
+      if (!gs_file_enumerator_iterate (dir_enum, &file_info, NULL,
+                                       cancellable, error))
+        goto out;
+      if (file_info == NULL)
+        break;
+
+      name = g_file_info_get_name (file_info);
+
+      if (ret_kernel == NULL && g_str_has_prefix (name, "vmlinuz-"))
+        ret_kernel = g_file_get_child (bootdir, name);
+      else if (ret_initramfs == NULL && g_str_has_prefix (name, "initramfs-"))
+        ret_initramfs = g_file_get_child (bootdir, name);
+      
+      if (ret_kernel && ret_initramfs)
+        break;
+    }
+
+  if (ret_kernel == NULL)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                   "Failed to find boot/vmlinuz-CHECKSUM in %s",
+                   gs_file_get_path_cached (deployroot));
+      goto out;
+    }
+
+  ot_transfer_out_value (out_kernel, &ret_kernel);
+  ot_transfer_out_value (out_initramfs, &ret_initramfs);
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static gboolean
+checksum_from_kernel_src (GFile        *src,
+                          char        **out_checksum,
+                          GError     **error)
+{
+  const char *last_dash = strrchr (gs_file_get_path_cached (src), '-');
+  if (!last_dash)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Malformed initramfs name '%s', missing '-'", gs_file_get_basename_cached (src));
+      return FALSE;
+    }
+  *out_checksum = g_strdup (last_dash + 1);
+  return TRUE;
+}
+
+static int
+sort_by_bootserial (gconstpointer ap, gconstpointer bp)
+{
+  OtDeployment **a_loc = (OtDeployment**)ap;
+  OtDeployment *a = *a_loc;
+  OtDeployment **b_loc = (OtDeployment**)bp;
+  OtDeployment *b = *b_loc;
+
+  if (ot_deployment_get_bootserial (a) == ot_deployment_get_bootserial (b))
+    return 0;
+  else if (ot_deployment_get_bootserial (a) < ot_deployment_get_bootserial (b))
+    return -1;
+  return 1;
+}
+
+static GPtrArray *
+filter_deployments_by_bootcsum (GPtrArray    *deployments,
+                                const char   *osname,
+                                const char   *bootcsum)
+{
+  GPtrArray *ret = g_ptr_array_new ();
+  guint i;
+
+  for (i = 0; i < deployments->len; i++)
+    {
+      OtDeployment *deployment = deployments->pdata[i];
+      
+      if (strcmp (ot_deployment_get_osname (deployment), osname) != 0)
+        continue;
+      if (strcmp (ot_deployment_get_bootcsum (deployment), bootcsum) != 0)
+        continue;
+      
+      g_ptr_array_add (ret, deployment);
+    }
+  g_ptr_array_sort (ret, sort_by_bootserial);
+
+  return ret;
+}
+
+static void
+compute_new_deployment_list (int           current_bootversion,
+                             GPtrArray    *current_deployments,
+                             const char   *osname,
+                             OtDeployment *booted_deployment,
+                             OtDeployment *merge_deployment,
+                             gboolean      retain,
+                             const char   *revision,
+                             const char   *bootcsum,
+                             GPtrArray   **out_new_deployments,
+                             int          *out_new_bootversion)
+{
+  guint i;
+  int new_index;
+  guint new_deployserial = 0;
+  int new_bootserial = 0;
+  gs_unref_object OtDeployment *new_deployment = NULL;
+  gs_unref_ptrarray GPtrArray *matching_deployments_by_bootserial = NULL;
+  OtDeployment *last_deployment_for_this_os = NULL;
+  gs_unref_ptrarray GPtrArray *ret_new_deployments = NULL;
+  gboolean requires_new_bootversion;
+
+  if (osname == NULL)
+    osname = ot_deployment_get_osname (booted_deployment);
+
+  for (i = 0; i < current_deployments->len; i++)
+    {
+      OtDeployment *deployment = current_deployments->pdata[i];
+      
+      if (strcmp (ot_deployment_get_osname (deployment), osname) != 0)
+        continue;
+      if (strcmp (ot_deployment_get_csum (deployment), revision) != 0)
+        continue;
+
+      // Keep both the booted and merge deployments
+      if (!retain && 
+          (!ot_deployment_equal (deployment, booted_deployment) || 
+           !ot_deployment_equal (deployment, merge_deployment)))
+        last_deployment_for_this_os = deployment;
+      new_deployserial = MAX(new_deployserial, ot_deployment_get_deployserial (deployment)+1);
+    }
+
+  /* We need to update the bootloader only if the deployment we're
+   * removing uses a different kernel.
+   */
+  requires_new_bootversion =
+    (last_deployment_for_this_os == NULL) ||
+    (strcmp (ot_deployment_get_bootcsum (last_deployment_for_this_os), bootcsum) != 0);
+
+  ret_new_deployments = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
+
+  new_deployment = ot_deployment_new (0, osname, revision, new_deployserial,
+                                      bootcsum, new_bootserial);
+  g_ptr_array_add (ret_new_deployments, g_object_ref (new_deployment));
+  new_index = 1;
+  for (i = 0; i < current_deployments->len; i++)
+    {
+      OtDeployment *orig_deployment = current_deployments->pdata[i];
+      gs_unref_object OtDeployment *deployment_clone = NULL;
+
+      if (orig_deployment == last_deployment_for_this_os)
+        continue;
+
+      deployment_clone = ot_deployment_clone (orig_deployment);
+      ot_deployment_set_index (deployment_clone, new_index);
+      new_index++;
+      g_ptr_array_add (ret_new_deployments, g_object_ref (deployment_clone));
+    }
+
+  /* Just renumber the deployments for the OS we're adding; we don't
+   * handle anything else at the moment.
+   */
+  matching_deployments_by_bootserial = filter_deployments_by_bootcsum (ret_new_deployments,
+                                                                       osname, bootcsum);
+  for (i = 0; i < matching_deployments_by_bootserial->len; i++)
+    {
+      OtDeployment *deployment = matching_deployments_by_bootserial->pdata[i];
+      ot_deployment_set_bootserial (deployment, i);
+    }
+
+  *out_new_deployments = ret_new_deployments;
+  ret_new_deployments = NULL;
+  g_assert (current_bootversion == 0 || current_bootversion == 1);
+  if (requires_new_bootversion)
+    *out_new_bootversion = (current_bootversion == 0) ? 1 : 0;
+  else
+    *out_new_bootversion = current_bootversion;
+}
+
+static GHashTable *
+object_array_to_set (GPtrArray   *objlist,
+                     GHashFunc    hashfunc,
+                     GEqualFunc   equalfunc)
+{
+  GHashTable *ret = g_hash_table_new_full (hashfunc, equalfunc, g_object_unref, NULL);
+  guint i;
+
+  for (i = 0; i < objlist->len; i++)
+    {
+      GObject *obj = g_object_ref (objlist->pdata[i]);
+      g_hash_table_insert (ret, obj, obj);
+    }
+  
+  return ret;
+}
+
+static GHashTable *
+object_set_subtract (GHashTable *a, GHashTable *b)
+{
+  GHashTable *ret = g_hash_table_new_full (NULL, NULL, g_object_unref, NULL);
+  GHashTableIter hashiter;
+  gpointer hashkey, hashvalue;
+  
+  g_hash_table_iter_init (&hashiter, a);
+  while (g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue))
+    {
+      if (!g_hash_table_contains (b, hashkey))
+        {
+          GObject *o = g_object_ref (hashkey);
+          g_hash_table_insert (ret, o, o);
+        }
+    }
+
+  return ret;
+}
+
+static void
+print_deployment_set (gboolean    for_removal,
+                      GHashTable *set)
+{
+  GHashTableIter hashiter;
+  gpointer hashkey, hashvalue;
+
+  if (g_hash_table_size (set) == 0)
+    return;
+
+  g_print ("%s\n", for_removal ? "removed:" : "added: ");
+
+  g_hash_table_iter_init (&hashiter, set);
+  while (g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue))
+    {
+      OtDeployment *deployment = hashkey;
+
+      g_print ("  %c %s %s.%d",
+               for_removal ? '-' : '+', ot_deployment_get_osname (deployment),
+               ot_deployment_get_csum (deployment),
+               ot_deployment_get_deployserial (deployment));
+
+      if (!for_removal)
+        g_print (" index=%d", ot_deployment_get_index (deployment));
+      g_print ("\n");
+    }
+}
+
+static void
+print_deployment_diff (GPtrArray   *current_deployments,
+                       GPtrArray   *new_deployments)
+{
+  gs_unref_hashtable GHashTable *curset = object_array_to_set (current_deployments, ot_deployment_hash, 
ot_deployment_equal);
+  gs_unref_hashtable GHashTable *newset = object_array_to_set (new_deployments, ot_deployment_hash, 
ot_deployment_equal);
+  gs_unref_hashtable GHashTable *removed = NULL;
+  gs_unref_hashtable GHashTable *added = NULL;
+
+  removed = object_set_subtract (curset, newset);
+  added = object_set_subtract (newset, curset);
+
+  print_deployment_set (TRUE, removed);
+  print_deployment_set (FALSE, removed);
+}
+
+/* FIXME: We should really do individual fdatasync() on files/dirs,
+ * since this causes us to block on unrelated I/O.  However, it's just
+ * safer for now.
+ */
+static gboolean
+full_system_sync (GCancellable      *cancellable,
+                  GError           **error)
+{
+  sync ();
+  return TRUE;
+}
+
+static gboolean
+swap_bootlinks (GFile        *sysroot,
+                int           current_bootversion,
+                GPtrArray    *new_deployments,
+                GCancellable *cancellable,
+                GError      **error)
+{
+  gboolean ret = FALSE;
+  guint i;
+  int old_subbootversion, new_subbootversion;
+  gs_unref_object GFile *ostree_dir = g_file_get_child (sysroot, "ostree");
+  gs_free char *ostree_bootdir_name = g_strdup_printf ("boot.%d", current_bootversion);
+  gs_unref_object GFile *ostree_bootdir = g_file_resolve_relative_path (ostree_dir, ostree_bootdir_name);
+  gs_free char *ostree_subbootdir_name = NULL;
+  gs_unref_object GFile *ostree_subbootdir = NULL;
+  gs_unref_ptrarray GPtrArray *deployments_to_swap = NULL;
+
+  if (!ot_admin_read_current_subbootversion (sysroot, current_bootversion,
+                                             &old_subbootversion,
+                                             cancellable, error))
+    goto out;
+
+  new_subbootversion = old_subbootversion == 0 ? 1 : 0;
+
+  ostree_subbootdir_name = g_strdup_printf ("boot.%d.%d", current_bootversion, new_subbootversion);
+  ostree_subbootdir = g_file_resolve_relative_path (ostree_dir, ostree_subbootdir_name);
+
+  if (!gs_file_ensure_directory (ostree_subbootdir, TRUE, cancellable, error))
+    goto out;
+
+  for (i = 0; i < new_deployments->len; i++)
+    {
+      OtDeployment *deployment = new_deployments->pdata[i];
+      gs_free char *bootlink_pathname = g_strdup_printf ("%s/%s/%d",
+                                                         ot_deployment_get_osname (deployment),
+                                                         ot_deployment_get_bootcsum (deployment),
+                                                         ot_deployment_get_bootserial (deployment));
+      gs_free char *bootlink_target = g_strdup_printf ("../../../deploy/%s/deploy/%s.%d",
+                                                       ot_deployment_get_osname (deployment),
+                                                       ot_deployment_get_csum (deployment),
+                                                       ot_deployment_get_deployserial (deployment));
+      gs_unref_object GFile *linkname = g_file_get_child (ostree_subbootdir, bootlink_pathname);
+      gs_unref_object GFile *linkname_parent = g_file_get_parent (linkname);
+
+      if (!gs_file_ensure_directory (linkname_parent, TRUE, cancellable, error))
+        goto out;
+
+      if (!g_file_make_symbolic_link (linkname, bootlink_target, cancellable, error))
+        goto out;
+    }
+
+  if (!ot_gfile_atomic_symlink_swap (ostree_bootdir, ostree_subbootdir_name,
+                                     cancellable, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static char *
+remove_checksum_from_kernel_name (const char *name,
+                                  const char *csum)
+{
+  const char *p = strrchr (name, '-');
+  g_assert_cmpstr (p+1, ==, csum);
+  return g_strndup (name, p-name);
+}
+
+static GHashTable *
+parse_os_release (const char *contents,
+                  const char *split)
+{
+  char **lines = g_strsplit (contents, split, -1);
+  char **iter;
+  GHashTable *ret = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+  for (iter = lines; *iter; iter++)
+    {
+      char *line = *iter;
+      char *eq;
+      const char *quotedval;
+      char *val;
+
+      if (g_str_has_prefix (line, "#"))
+        continue;
+      
+      eq = strchr (line, '=');
+      if (!eq)
+        continue;
+      
+      *eq = '\0';
+      quotedval = eq + 1;
+      val = g_shell_unquote (quotedval, NULL);
+      if (!val)
+        continue;
+      
+      g_hash_table_insert (ret, line, val);
+    }
+
+  return ret;
+}
+
+static gboolean
+install_deployment_kernel (GFile          *sysroot,
+                           int             new_bootversion,
+                           OtDeployment   *deployment,
+                           GCancellable   *cancellable,
+                           GError        **error)
+
+{
+  gboolean ret = FALSE;
+  const char *osname = ot_deployment_get_osname (deployment);
+  const char *bootcsum = ot_deployment_get_bootcsum (deployment);
+  gs_unref_object GFile *bootdir = NULL;
+  gs_unref_object GFile *bootcsumdir = NULL;
+  gs_unref_object GFile *bootconfpath = NULL;
+  gs_unref_object GFile *bootconfpath_parent = NULL;
+  gs_free char *dest_kernel_name = NULL;
+  gs_unref_object GFile *dest_kernel_path = NULL;
+  gs_unref_object GFile *dest_initramfs_path = NULL;
+  gs_unref_object GFile *tree_kernel_path = NULL;
+  gs_unref_object GFile *tree_initramfs_path = NULL;
+  gs_unref_object GFile *etc_os_release = NULL;
+  gs_unref_object GFile *deployment_dir = NULL;
+  gs_free char *contents = NULL;
+  gs_unref_hashtable GHashTable *osrelease_values = NULL;
+  gs_free char *linux_relpath = NULL;
+  gs_free char *linux_key = NULL;
+  gs_free char *initramfs_relpath = NULL;
+  gs_free char *title_key = NULL;
+  gs_free char *initrd_key = NULL;
+  gs_free char *version_key = NULL;
+  gs_free char *ostree_kernel_arg = NULL;
+  gs_free char *options_key = NULL;
+  __attribute__((cleanup(ot_ordered_hash_cleanup))) OtOrderedHash *ohash = NULL;
+  const char *val;
+  OtConfigParser *bootconfig;
+  gsize len;
+
+  bootconfig = ot_deployment_get_bootconfig (deployment);
+  deployment_dir = ot_admin_get_deployment_directory (sysroot, deployment);
+
+  if (!get_kernel_from_tree (deployment_dir, &tree_kernel_path, &tree_initramfs_path,
+                             cancellable, error))
+    goto out;
+
+  bootdir = g_file_get_child (sysroot, "boot");
+  bootcsumdir = ot_gfile_resolve_path_printf (bootdir, "ostree/%s-%s",
+                                              osname,
+                                              bootcsum);
+  bootconfpath = ot_gfile_resolve_path_printf (bootdir, "loader.%d/entries/ostree-%s-%s-%d.conf",
+                                               new_bootversion, osname, 
+                                               ot_deployment_get_csum (deployment),
+                                               ot_deployment_get_bootserial (deployment));
+
+  if (!gs_file_ensure_directory (bootcsumdir, TRUE, cancellable, error))
+    goto out;
+  bootconfpath_parent = g_file_get_parent (bootconfpath);
+  if (!gs_file_ensure_directory (bootconfpath_parent, TRUE, cancellable, error))
+    goto out;
+
+  dest_kernel_name = remove_checksum_from_kernel_name (gs_file_get_basename_cached (tree_kernel_path),
+                                                       bootcsum);
+  dest_kernel_path = g_file_get_child (bootcsumdir, dest_kernel_name);
+  if (!g_file_query_exists (dest_kernel_path, NULL))
+    {
+      if (!gs_file_linkcopy_sync_data (tree_kernel_path, dest_kernel_path, G_FILE_COPY_OVERWRITE | 
G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_ALL_METADATA,
+                                       cancellable, error))
+        goto out;
+    }
+
+  if (tree_initramfs_path)
+    {
+      gs_free char *dest_initramfs_name = remove_checksum_from_kernel_name (gs_file_get_basename_cached 
(tree_initramfs_path),
+                                                                       bootcsum);
+      dest_initramfs_path = g_file_get_child (bootcsumdir, dest_initramfs_name);
+
+      if (!g_file_query_exists (dest_initramfs_path, NULL))
+        {
+          if (!gs_file_linkcopy_sync_data (tree_initramfs_path, dest_initramfs_path, G_FILE_COPY_OVERWRITE | 
G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_ALL_METADATA,
+                                           cancellable, error))
+            goto out;
+        }
+    }
+
+  etc_os_release = g_file_resolve_relative_path (deployment_dir, "etc/os-release");
+
+  if (!g_file_load_contents (etc_os_release, cancellable,
+                             &contents, &len, NULL, error))
+    {
+      g_prefix_error (error, "Reading /etc/os-release: ");
+      goto out;
+    }
+
+  osrelease_values = parse_os_release (contents, "\n");
+
+  /* title */
+  val = g_hash_table_lookup (osrelease_values, "PRETTY_NAME");
+  if (val == NULL)
+      val = g_hash_table_lookup (osrelease_values, "ID");
+  if (val == NULL)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "No PRETTY_NAME or ID in /etc/os-release");
+      goto out;
+    }
+  
+  title_key = g_strdup_printf ("ostree:%s:%d %s", ot_deployment_get_osname (deployment),
+                               ot_deployment_get_index (deployment),
+                               val);
+  ot_config_parser_set (bootconfig, "title", title_key);
+
+  version_key = g_strdup_printf ("%d", ot_deployment_get_bootserial (deployment));
+  ot_config_parser_set (bootconfig, "version", version_key);
+
+  linux_relpath = g_file_get_relative_path (bootdir, dest_kernel_path);
+  linux_key = g_strconcat ("/", linux_relpath, NULL);
+  ot_config_parser_set (bootconfig, "linux", linux_key);
+
+  if (dest_initramfs_path)
+    {
+      initramfs_relpath = g_file_get_relative_path (bootdir, dest_initramfs_path);
+      initrd_key = g_strconcat ("/", initramfs_relpath, NULL);
+      ot_config_parser_set (bootconfig, "initrd", initrd_key);
+    }
+
+  val = ot_config_parser_get (bootconfig, "options");
+  ostree_kernel_arg = g_strdup_printf ("/ostree/boot.%d/%s/%s/%d",
+                                       new_bootversion, osname, bootcsum,
+                                       ot_deployment_get_bootserial (deployment));
+  ohash = ot_admin_parse_kernel_args (val);
+  ot_ordered_hash_replace_key (ohash, "ostree", ostree_kernel_arg);
+  options_key = ot_admin_kernel_arg_string_serialize (ohash);
+  ot_config_parser_set (bootconfig, "options", options_key);
+  
+  if (!ot_config_parser_write (ot_deployment_get_bootconfig (deployment), bootconfpath,
+                               cancellable, error))
+      goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static gboolean
+swap_bootloader (GFile          *sysroot,
+                 int             current_bootversion,
+                 int             new_bootversion,
+                 GCancellable   *cancellable,
+                 GError        **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *boot_loader_link = NULL;
+  gs_free char *new_target = NULL;
+
+  g_assert ((current_bootversion == 0 && new_bootversion == 1) ||
+            (current_bootversion == 1 && new_bootversion == 0));
+
+  boot_loader_link = g_file_resolve_relative_path (sysroot, "boot/loader");
+  new_target = g_strdup_printf ("loader.%d", new_bootversion);
+
+  if (!ot_gfile_atomic_symlink_swap (boot_loader_link, new_target,
+                                     cancellable, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+gboolean
+ot_admin_deploy (GFile             *sysroot,
+                 int                current_bootversion,
+                 GPtrArray         *current_deployments,
+                 const char        *osname,
+                 const char        *revision,
+                 GKeyFile          *origin,
+                 char             **add_kernel_argv,
+                 gboolean           retain,
+                 OtDeployment      *booted_deployment,
+                 OtDeployment      *provided_merge_deployment,
+                 OtDeployment     **out_new_deployment,
+                 int               *out_new_bootversion,
+                 GPtrArray        **out_new_deployments,
+                 GCancellable      *cancellable,
+                 GError           **error)
+{
+  gboolean ret = FALSE;
+  OtDeployment *new_deployment;
+  gs_unref_object OtDeployment *merge_deployment = NULL;
+  gs_unref_object OtBootloader *bootloader = NULL;
+  gs_unref_object GFile *rootfs = NULL;
+  gs_unref_object OstreeRepo *repo = NULL;
+  gs_unref_object GFile *commit_root = NULL;
+  gs_unref_object GFile *tree_kernel_path = NULL;
+  gs_unref_object GFile *tree_initramfs_path = NULL;
+  gs_unref_object GFile *new_deployment_path = NULL;
+  gs_unref_object GFile *deploy_path = NULL;
+  gs_unref_object GFile *osdir = NULL;
+  gs_free char *new_bootcsum = NULL;
+  gs_unref_object GFile *source_etc_path = NULL;
+  gs_unref_object GFile *source_etc_pristine_path = NULL;
+  gs_unref_object OtConfigParser *bootconfig = NULL;
+  gs_free char *source_etc_kernel_args = NULL;
+  gs_unref_ptrarray GPtrArray *new_deployments = NULL;
+  int new_bootversion;
+  int i;
+
+  if (!ot_admin_get_repo (sysroot, &repo, cancellable, error))
+    goto out;
+
+  /* Here we perform cleanup of any leftover data from previous
+   * partial failures.  This avoids having to call gs_shutil_rm_rf()
+   * at random points throughout the process.
+   *
+   * TODO: Add /ostree/transaction file, and only do this cleanup if
+   * we find it.
+   */
+  if (!ot_admin_cleanup (sysroot, cancellable, error))
+    {
+      g_prefix_error (error, "Performing initial cleanup: ");
+      goto out;
+    }
+
+  if (!ostree_repo_read_commit (repo, revision, &commit_root, cancellable, error))
+    goto out;
+
+  if (!get_kernel_from_tree (commit_root, &tree_kernel_path, &tree_initramfs_path,
+                             cancellable, error))
+    goto out;
+  
+  if (tree_initramfs_path != NULL)
+    {
+      if (!checksum_from_kernel_src (tree_initramfs_path, &new_bootcsum, error))
+        goto out;
+    }
+  else
+    {
+      if (!checksum_from_kernel_src (tree_kernel_path, &new_bootcsum, error))
+        goto out;
+    }
+
+  bootloader = ot_admin_query_bootloader (sysroot);
+  if (!bootloader)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No known bootloader configuration detected");
+      goto out;
+    }
+
+  /* If we're booted into the OS into which we're deploying, then
+   * merge the currently *booted* configuration, rather than the most
+   * recently deployed.
+   */
+  if (provided_merge_deployment != NULL)
+    merge_deployment = g_object_ref (provided_merge_deployment);
+  else
+    merge_deployment = ot_admin_get_merge_deployment (current_deployments, osname,
+                                                      booted_deployment,
+                                                      new_deployment);
+
+  compute_new_deployment_list (current_bootversion,
+                               current_deployments, osname,
+                               booted_deployment, merge_deployment,
+                               retain,
+                               revision, new_bootcsum,
+                               &new_deployments,
+                               &new_bootversion);
+  new_deployment = g_object_ref (new_deployments->pdata[0]);
+  ot_deployment_set_origin (new_deployment, origin);
+
+  print_deployment_diff (current_deployments, new_deployments);
+
+  /* Check out the userspace tree onto the filesystem */
+  if (!checkout_deployment_tree (sysroot, repo, new_deployment, &new_deployment_path,
+                                 cancellable, error))
+    {
+      g_prefix_error (error, "Checking out tree: ");
+      goto out;
+    }
+
+  if (!write_origin_file (sysroot, new_deployment, cancellable, error))
+    {
+      g_prefix_error (error, "Writing out origin file: ");
+      goto out;
+    }
+
+  /* Create an empty boot configuration; we will merge things into
+   * it as we go.
+   */
+  bootconfig = ot_config_parser_new (" ");
+  ot_deployment_set_bootconfig (new_deployment, bootconfig);
+
+  if (!merge_configuration (sysroot, merge_deployment, new_deployment,
+                            new_deployment_path,
+                            cancellable, error))
+    {
+      g_prefix_error (error, "During /etc merge: ");
+      goto out;
+    }
+
+  /* We have inherited kernel arguments from the previous deployment;
+   * now, override/extend that with arguments provided by the command
+   * line.
+   * 
+   * After this, install_deployment_kernel() will set the other boot
+   * options and write it out to disk.
+   */
+  if (add_kernel_argv)
+    {
+      char **strviter;
+      __attribute__((cleanup(ot_ordered_hash_cleanup))) OtOrderedHash *ohash = NULL;
+      gs_free char *new_options = NULL;
+
+      ohash = ot_admin_parse_kernel_args (ot_config_parser_get (bootconfig, "options"));
+
+      for (strviter = add_kernel_argv; *strviter; strviter++)
+        {
+          char *karg = g_strdup (*strviter);
+          const char *val = ot_admin_split_keyeq (karg);
+        
+          ot_ordered_hash_replace_key_take (ohash, karg, val);
+        }
+
+      new_options = ot_admin_kernel_arg_string_serialize (ohash);
+      ot_config_parser_set (bootconfig, "options", new_options);
+    }
+
+  if (current_bootversion == new_bootversion)
+    {
+      if (!full_system_sync (cancellable, error))
+        {
+          g_prefix_error (error, "Full sync: ");
+          goto out;
+        }
+
+      if (!swap_bootlinks (sysroot, current_bootversion,
+                           new_deployments,
+                           cancellable, error))
+        {
+          g_prefix_error (error, "Swapping current bootlinks: ");
+          goto out;
+        }
+    }
+  else
+    {
+      for (i = 0; i < new_deployments->len; i++)
+        {
+          OtDeployment *deployment = new_deployments->pdata[i];
+          if (!install_deployment_kernel (sysroot, new_bootversion, deployment,
+                                          cancellable, error))
+            {
+              g_prefix_error (error, "Installing kernel: ");
+              goto out;
+            }
+        }
+
+      /* Swap bootlinks for *new* version */
+      if (!swap_bootlinks (sysroot, new_bootversion, new_deployments,
+                           cancellable, error))
+        {
+          g_prefix_error (error, "Generating new bootlinks: ");
+          goto out;
+        }
+
+      if (!full_system_sync (cancellable, error))
+        {
+          g_prefix_error (error, "Full sync: ");
+          goto out;
+        }
+
+      if (!ot_bootloader_write_config (bootloader, new_bootversion,
+                                       cancellable, error))
+        goto out;
+
+      if (!swap_bootloader (sysroot, current_bootversion, new_bootversion,
+                            cancellable, error))
+        {
+          g_prefix_error (error, "Final bootloader swap: ");
+          goto out;
+        }
+    }
+
+  /* TEMPORARY HACK: Add a "current" symbolic link that's easy to
+   * follow inside the gnome-ostree build scripts.  This isn't atomic,
+   * but that doesn't matter because it's only used by deployments
+   * done from the host.
+   */
+  {
+    gs_unref_object GFile *osdir = ot_gfile_resolve_path_printf (sysroot, "ostree/deploy/%s", 
ot_deployment_get_osname (new_deployment));
+    gs_unref_object GFile *os_current_path = g_file_get_child (osdir, "current");
+    gs_free char *target = g_file_get_relative_path (osdir, new_deployment_path);
+    g_assert (target != NULL);
+    if (!ot_gfile_atomic_symlink_swap (os_current_path, target,
+                                       cancellable, error))
+      goto out;
+  }
+
+  /* And finally, cleanup of any leftover data.
+   */
+  if (!ot_admin_cleanup (sysroot, cancellable, error))
+    {
+      g_prefix_error (error, "Performing final cleanup: ");
+      goto out;
+    }
+
+  ret = TRUE;
+  ot_transfer_out_value (out_new_deployment, &new_deployment);
+  *out_new_bootversion = new_bootversion;
+  ot_transfer_out_value (out_new_deployments, &new_deployments)
+ out:
+  return ret;
+}
+
diff --git a/src/ostree/ot-admin-deploy.h b/src/ostree/ot-admin-deploy.h
new file mode 100644
index 0000000..1eab703
--- /dev/null
+++ b/src/ostree/ot-admin-deploy.h
@@ -0,0 +1,51 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2012 Colin Walters <walters verbum org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Colin Walters <walters verbum org>
+ */
+
+#ifndef __OT_ADMIN_DEPLOY__
+#define __OT_ADMIN_DEPLOY_
+
+#include <gio/gio.h>
+#include "ot-deployment.h"
+#include "ot-bootloader.h"
+#include "ot-ordered-hash.h"
+
+G_BEGIN_DECLS
+
+gboolean ot_admin_deploy (GFile             *sysroot,
+                          int                current_bootversion,
+                          GPtrArray         *current_deployments,
+                          const char        *osname,
+                          const char        *revision,
+                          GKeyFile          *origin,
+                          char             **add_kernel_argv,
+                          gboolean           retain,
+                          OtDeployment      *booted_deployment,
+                          OtDeployment      *merge_deployment,
+                          OtDeployment     **out_new_deployment,
+                          int               *out_new_bootversion,
+                          GPtrArray        **out_new_deployments,
+                          GCancellable      *cancellable,
+                          GError           **error);
+
+G_END_DECLS
+
+#endif
diff --git a/src/ostree/ot-admin-functions.c b/src/ostree/ot-admin-functions.c
index 28eb077..d6410d3 100644
--- a/src/ostree/ot-admin-functions.c
+++ b/src/ostree/ot-admin-functions.c
@@ -23,16 +23,113 @@
 #include "config.h"
 
 #include "ot-admin-functions.h"
+#include "ot-deployment.h"
+#include "ot-config-parser.h"
+#include "ot-bootloader-syslinux.h"
 #include "otutil.h"
 #include "ostree-core.h"
+#include "libgsystem.h"
+
+/*
+ * Modify @arg which should be of the form key=value to make @arg just
+ * contain key.  Return a pointer to the start of value.
+ */
+char *
+ot_admin_split_keyeq (char *arg)
+{
+  char *eq;
+      
+  eq = strchr (arg, '=');
+  if (eq)
+    {
+      /* Note key/val are in one malloc block,
+       * so we don't free val...
+       */
+      *eq = '\0';
+      return eq+1;
+    }
+  else
+    {
+      /* ...and this allows us to insert a constant
+       * string.
+       */
+      return "";
+    }
+}
+
+OtOrderedHash *
+ot_admin_parse_kernel_args (const char *options)
+{
+  OtOrderedHash *ret;
+  char **args;
+  char **iter;
+
+  ret = ot_ordered_hash_new ();
+
+  if (!options)
+    return ret;
+  
+  args = g_strsplit (options, " ", -1);
+  for (iter = args; *iter; iter++)
+    {
+      char *arg = *iter;
+      char *val;
+      
+      val = ot_admin_split_keyeq (arg);
+
+      g_ptr_array_add (ret->order, arg);
+      g_hash_table_insert (ret->table, arg, val);
+    }
+
+  return ret;
+}
+
+char *
+ot_admin_kernel_arg_string_serialize (OtOrderedHash *ohash)
+{
+  guint i;
+  GString *buf = g_string_new ("");
+  gboolean first = TRUE;
+
+  for (i = 0; i < ohash->order->len; i++)
+    {
+      const char *key = ohash->order->pdata[i];
+      const char *val = g_hash_table_lookup (ohash->table, key);
+
+      g_assert (val != NULL);
+
+      if (first)
+        first = FALSE;
+      else
+        g_string_append_c (buf, ' ');
+
+      if (*val)
+        g_string_append_printf (buf, "%s=%s", key, val);
+      else
+        g_string_append (buf, key);
+    }
+
+  return g_string_free (buf, FALSE);
+}
+
+
+static void
+match_info_cleanup (void *loc)
+{
+  GMatchInfo **match = (GMatchInfo**)loc;
+  if (*match) g_match_info_unref (*match);
+}
 
 gboolean
-ot_admin_ensure_initialized (GFile         *ostree_dir,
+ot_admin_ensure_initialized (GFile         *sysroot,
                              GCancellable  *cancellable,
                              GError       **error)
 {
   gboolean ret = FALSE;
-  ot_lobj GFile *dir = NULL;
+  gs_unref_object GFile *dir = NULL;
+  gs_unref_object GFile *ostree_dir = NULL;
+
+  ostree_dir = g_file_get_child (sysroot, "ostree");
 
   g_clear_object (&dir);
   dir = g_file_get_child (ostree_dir, "repo");
@@ -65,445 +162,940 @@ ot_admin_ensure_initialized (GFile         *ostree_dir,
   return ret;
 }
 
+gboolean
+ot_admin_check_os (GFile         *sysroot, 
+                   const char    *osname,
+                   GCancellable  *cancellable,
+                   GError       **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *osdir = NULL;
+
+  osdir = ot_gfile_resolve_path_printf (sysroot, "ostree/deploy/%s/var", osname);
+  if (!g_file_query_exists (osdir, NULL))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No such OS '%s', use os-init to create it", osname);
+      goto out;
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
 static gboolean
-query_file_info_allow_noent (GFile         *path,
-                             GFileInfo    **out_info,
-                             GCancellable  *cancellable,
-                             GError       **error)
+parse_bootlink (const char    *bootlink,
+                int           *out_entry_bootversion,
+                char         **out_osname,
+                char         **out_bootcsum,
+                int           *out_treebootserial,
+                GError       **error)
 {
   gboolean ret = FALSE;
-  ot_lobj GFileInfo *ret_file_info = NULL;
-  GError *temp_error = NULL;
+  __attribute__((cleanup(match_info_cleanup))) GMatchInfo *match = NULL;
+  gs_free char *bootversion_str = NULL;
+  gs_free char *treebootserial_str = NULL;
 
-  ret_file_info = g_file_query_info (path, OSTREE_GIO_FAST_QUERYINFO,
-                                     G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                     cancellable, &temp_error);
-  if (!ret_file_info)
+  static gsize regex_initialized;
+  static GRegex *regex;
+
+  if (g_once_init_enter (&regex_initialized))
     {
-      if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
-        {
-          g_clear_error (&temp_error);
-        }
-      else
-        {
-          g_propagate_error (error, temp_error);
-          goto out;
-        }
+      regex = g_regex_new ("^/ostree/boot.([01])/([^/]+)/([^/]+)/([0-9]+)$", 0, 0, NULL);
+      g_assert (regex);
+      g_once_init_leave (&regex_initialized, 1);
     }
 
+  if (!g_regex_match (regex, bootlink, 0, &match))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Invalid ostree= argument '%s', expected 
ostree=/ostree/boot.BOOTVERSION/OSNAME/BOOTCSUM/TREESERIAL", bootlink);
+      goto out;
+    }
+    
+  bootversion_str = g_match_info_fetch (match, 1);
+  *out_entry_bootversion = (int)g_ascii_strtoll (bootversion_str, NULL, 10);
+  *out_osname = g_match_info_fetch (match, 2);
+  *out_bootcsum = g_match_info_fetch (match, 3);
+  treebootserial_str = g_match_info_fetch (match, 4);
+  *out_treebootserial = (int)g_ascii_strtoll (treebootserial_str, NULL, 10);
+  
   ret = TRUE;
-  ot_transfer_out_value (out_info, &ret_file_info);
  out:
   return ret;
 }
 
 static gboolean
-query_symlink_target_allow_noent (GFile         *path,
-                                  GFile        **out_target,
-                                  GCancellable  *cancellable,
-                                  GError       **error)
+parse_deploy_path_name (const char *name,
+                        char      **out_csum,
+                        int        *out_serial,
+                        GError    **error)
 {
   gboolean ret = FALSE;
-  ot_lobj GFileInfo *file_info = NULL;
-  ot_lobj GFile *ret_target = NULL;
-  ot_lobj GFile *path_parent = NULL;
+  __attribute__((cleanup(match_info_cleanup))) GMatchInfo *match = NULL;
+  gs_free char *serial_str = NULL;
 
-  if (!query_file_info_allow_noent (path, &file_info,
-                                    cancellable, error))
-    goto out;
+  static gsize regex_initialized;
+  static GRegex *regex;
 
-  path_parent = g_file_get_parent (path);
+  if (g_once_init_enter (&regex_initialized))
+    {
+      regex = g_regex_new ("^([0-9a-f]+)\\.([0-9]+)$", 0, 0, NULL);
+      g_assert (regex);
+      g_once_init_leave (&regex_initialized, 1);
+    }
 
-  if (file_info != NULL)
+  if (!g_regex_match (regex, name, 0, &match))
     {
-      const char *target;
-      
-      if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_SYMBOLIC_LINK)
-        {
-          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                       "Not a symbolic link");
-          goto out;
-        }
-      target = g_file_info_get_symlink_target (file_info);
-      g_assert (target);
-      ret_target = g_file_resolve_relative_path (path_parent, target);
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Invalid deploy name '%s', expected CHECKSUM.TREESERIAL", name);
+      goto out;
     }
 
+  *out_csum = g_match_info_fetch (match, 1);
+  serial_str = g_match_info_fetch (match, 2);
+  *out_serial = (int)g_ascii_strtoll (serial_str, NULL, 10);
+
   ret = TRUE;
-  ot_transfer_out_value (out_target, &ret_target);
  out:
   return ret;
 }
 
-/**
- * ot_admin_get_current_deployment:
- * 
- * Returns in @out_deployment the full file path of the current
- * deployment that the /ostree/current symbolic link points to, or
- * %NULL if none.
- */
-gboolean
-ot_admin_get_current_deployment (GFile           *ostree_dir,
-                                 const char      *osname,
-                                 GFile          **out_deployment,
-                                 GCancellable    *cancellable,
-                                 GError         **error)
+GFile *
+ot_admin_get_deployment_origin_path (GFile   *deployment_path)
 {
-  ot_lobj GFile *current_path = NULL;
+  gs_unref_object GFile *deployment_parent = g_file_get_parent (deployment_path);
+  return ot_gfile_resolve_path_printf (deployment_parent,
+                                       "%s.origin",
+                                       gs_file_get_path_cached (deployment_path));
+}
 
-  current_path = ot_gfile_get_child_build_path (ostree_dir, "deploy", osname,
-                                                "current", NULL);
+static gboolean
+parse_origin (GFile           *sysroot,
+              GFile           *deployment_path,
+              GKeyFile       **out_origin,
+              GCancellable    *cancellable,
+              GError         **error)
+{
+  gboolean ret = FALSE;
+  GKeyFile *ret_origin = NULL;
+  gs_unref_object GFile *origin_path = ot_admin_get_deployment_origin_path (deployment_path);
+  gs_free char *origin_contents = NULL;
+  
+  if (!ot_gfile_load_contents_utf8_allow_noent (origin_path, &origin_contents,
+                                                cancellable, error))
+    goto out;
 
-  return query_symlink_target_allow_noent (current_path, out_deployment,
-                                           cancellable, error);
+  if (origin_contents)
+    {
+      ret_origin = g_key_file_new ();
+      if (!g_key_file_load_from_data (ret_origin, origin_contents, -1, 0, error))
+        goto out;
+    }
+
+  ret = TRUE;
+  ot_transfer_out_value (out_origin, &ret_origin);
+ out:
+  if (error)
+    g_prefix_error (error, "Parsing %s: ", gs_file_get_path_cached (origin_path));
+  if (ret_origin)
+    g_key_file_unref (ret_origin);
+  return ret;
 }
 
-/**
- * ot_admin_get_previous_deployment:
- * 
- * Returns in @out_deployment the full file path of the current
- * deployment that the /ostree/previous symbolic link points to, or
- * %NULL if none.
- */
-gboolean
-ot_admin_get_previous_deployment (GFile           *ostree_dir,
-                                  const char      *osname,
-                                  GFile          **out_deployment,
-                                  GCancellable    *cancellable,
-                                  GError         **error)
+static gboolean
+parse_deployment (GFile           *sysroot,
+                  const char      *boot_link,
+                  OtDeployment   **out_deployment,
+                  GCancellable    *cancellable,
+                  GError         **error)
 {
-  ot_lobj GFile *previous_path = NULL;
-
-  previous_path = ot_gfile_get_child_build_path (ostree_dir, "deploy", osname,
-                                                 "previous", NULL);
+  gboolean ret = FALSE;
+  const char *relative_boot_link;
+  gs_unref_object OtDeployment *ret_deployment = NULL;
+  int entry_boot_version;
+  int treebootserial;
+  int deployserial;
+  gs_free char *osname = NULL;
+  gs_free char *bootcsum = NULL;
+  gs_free char *treecsum = NULL;
+  gs_unref_object GFile *treebootserial_link = NULL;
+  gs_unref_object GFileInfo *treebootserial_info = NULL;
+  gs_unref_object GFile *treebootserial_target = NULL;
+  GKeyFile *origin = NULL;
+      
+  if (!parse_bootlink (boot_link, &entry_boot_version,
+                       &osname, &bootcsum, &treebootserial,
+                       error))
+    goto out;
 
-  return query_symlink_target_allow_noent (previous_path, out_deployment,
+  relative_boot_link = boot_link;
+  if (*relative_boot_link == '/')
+    relative_boot_link++;
+  treebootserial_link = g_file_resolve_relative_path (sysroot, relative_boot_link);
+  treebootserial_info = g_file_query_info (treebootserial_link, OSTREE_GIO_FAST_QUERYINFO,
+                                           G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                            cancellable, error);
+  if (!treebootserial_info)
+    goto out;
+
+  if (!ot_gfile_get_symlink_target_from_info (treebootserial_link, treebootserial_info,
+                                              &treebootserial_target, cancellable, error))
+    goto out;
+
+  if (!parse_deploy_path_name (gs_file_get_basename_cached (treebootserial_target),
+                               &treecsum, &deployserial, error))
+    goto out;
+
+  if (!parse_origin (sysroot, treebootserial_target, &origin,
+                     cancellable, error))
+    goto out;
+
+  ret_deployment = ot_deployment_new (-1, osname, treecsum, deployserial,
+                                      bootcsum, treebootserial);
+  if (origin)
+    ot_deployment_set_origin (ret_deployment, origin);
+
+  ret = TRUE;
+  ot_transfer_out_value (out_deployment, &ret_deployment);
+ out:
+  if (origin)
+    g_key_file_unref (origin);
+  return ret;
 }
 
-/*
 static gboolean
-ot_admin_list_osnames (GFile               *ostree_dir,
-                       GPtrArray          **out_osnames,
-                       GCancellable        *cancellable,
-                       GError             **error)
+parse_kernel_commandline (OtOrderedHash  **out_args,
+                          GCancellable    *cancellable,
+                          GError         **error)
 {
   gboolean ret = FALSE;
-  ot_lobj GFileEnumerator *dir_enum = NULL;
-  ot_lobj GFileInfo *file_info = NULL;
-  ot_lobj GFile *deploy_dir = NULL;
-  ot_lptrarray GPtrArray *ret_osnames = NULL;
-  GError *temp_error = NULL;
+  gs_unref_object GFile *proc_cmdline = g_file_new_for_path ("/proc/cmdline");
+  gs_free char *contents = NULL;
+  gsize len;
 
-  deploy_dir = g_file_get_child (ostree_dir, "deploy");
+  if (!g_file_load_contents (proc_cmdline, cancellable, &contents, &len, NULL,
+                             error))
+    goto out;
 
-  dir_enum = g_file_enumerate_children (deploy_dir, OSTREE_GIO_FAST_QUERYINFO,
-                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                        NULL, error);
-  if (!dir_enum)
+  ret = TRUE;
+  *out_args = ot_admin_parse_kernel_args (contents);;
+ out:
+  return ret;
+}
+
+static gboolean
+get_devino (GFile         *path,
+            guint32       *out_device,
+            guint64       *out_inode,
+            GCancellable  *cancellable,
+            GError       **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFileInfo *finfo = g_file_query_info (path, "unix::device,unix::inode",
+                                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                                        cancellable, error);
+
+  if (!finfo)
     goto out;
 
-  while ((file_info = g_file_enumerator_next_file (dir_enum, NULL, error)) != NULL)
-    {
-      if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY)
+  ret = TRUE;
+  *out_device = g_file_info_get_attribute_uint32 (finfo, "unix::device");
+  *out_inode = g_file_info_get_attribute_uint64 (finfo, "unix::inode");
+ out:
+  return ret;
+}
+
+/**
+ * ot_admin_find_booted_deployment:
+ * 
+ * Returns in @out_deployment the currently booted deployment using
+ * the list in @deployments.  Will always return %NULL if
+ * @target_sysroot is not equal to "/".
+ */
+gboolean
+ot_admin_find_booted_deployment (GFile               *target_sysroot,
+                                 GPtrArray           *deployments,
+                                 OtDeployment       **out_deployment,
+                                 GCancellable        *cancellable,
+                                 GError             **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *active_root = g_file_new_for_path ("/");
+  gs_unref_object OtDeployment *ret_deployment = NULL;
+
+  if (g_file_equal (active_root, target_sysroot))
+    { 
+      guint i;
+      const char *bootlink_arg;
+      __attribute__((cleanup(ot_ordered_hash_cleanup))) OtOrderedHash *kernel_args = NULL;
+      guint32 root_device;
+      guint64 root_inode;
+      
+      if (!get_devino (active_root, &root_device, &root_inode,
+                       cancellable, error))
+        goto out;
+
+      if (!parse_kernel_commandline (&kernel_args, cancellable, error))
+        goto out;
+      
+      bootlink_arg = g_hash_table_lookup (kernel_args->table, "ostree");
+      if (bootlink_arg)
         {
-          char *name = g_strdup (g_file_info_get_name (file_info));
-          g_ptr_array_add (ret_osnames, name);
+          for (i = 0; i < deployments->len; i++)
+            {
+              OtDeployment *deployment = deployments->pdata[i];
+              gs_unref_object GFile *deployment_path = ot_admin_get_deployment_directory (active_root, 
deployment);
+              guint32 device;
+              guint64 inode;
+
+              if (!get_devino (deployment_path, &device, &inode,
+                               cancellable, error))
+                goto out;
+
+              if (device == root_device && inode == root_inode)
+                {
+                  ret_deployment = g_object_ref (deployment);
+                  break;
+                }
+            }
+          if (ret_deployment == NULL)
+            {
+              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Unexpected state: ostree= kernel argument found, but / is not a deployment 
root");
+              goto out;
+            }
+        }
+      else
+        {
+          /* Not an ostree system */
         }
-      g_clear_object (&file_info);
     }
 
-  if (temp_error != NULL)
+  ret = TRUE;
+  ot_transfer_out_value (out_deployment, &ret_deployment);
+ out:
+  return ret;
+}
+
+gboolean
+ot_admin_require_deployment_or_osname (GFile               *sysroot,
+                                       GPtrArray           *deployments,
+                                       const char          *osname,
+                                       OtDeployment       **out_deployment,
+                                       GCancellable        *cancellable,
+                                       GError             **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object OtDeployment *ret_deployment = NULL;
+
+  if (!ot_admin_find_booted_deployment (sysroot, deployments, &ret_deployment,
+                                        cancellable, error))
+    goto out;
+
+  if (ret_deployment == NULL && osname == NULL)
     {
-      g_propagate_error (error, temp_error);
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Not currently booted into an OSTree system and no --os= argument given");
       goto out;
     }
   
   ret = TRUE;
-  ot_transfer_out_value (out_osnames, &ret_osnames);
+  ot_transfer_out_value (out_deployment, &ret_deployment);
  out:
   return ret;
 }
-*/
+
+OtDeployment *
+ot_admin_get_merge_deployment (GPtrArray         *deployments,
+                               const char        *osname,
+                               OtDeployment      *booted_deployment,
+                               OtDeployment      *new_deployment)
+{
+  if (booted_deployment &&
+      new_deployment &&
+      g_strcmp0 (ot_deployment_get_osname (booted_deployment),
+                 ot_deployment_get_osname (new_deployment)) == 0)
+    {
+      return g_object_ref (booted_deployment);
+    }
+  else
+    {
+      guint i;
+      for (i = 0; i < deployments->len; i++)
+        {
+          OtDeployment *deployment = deployments->pdata[i];
+
+          if (strcmp (ot_deployment_get_osname (deployment), osname) != 0)
+            continue;
+          if (deployment == new_deployment)
+            continue;
+          
+          return g_object_ref (deployment);
+        }
+    }
+  return NULL;
+}
 
 static gboolean
-list_deployments_internal (GFile               *from_dir,
-                           GPtrArray           *inout_deployments,
-                           GCancellable        *cancellable,
-                           GError             **error)
+read_current_bootversion (GFile         *sysroot,
+                          int           *out_bootversion,
+                          GCancellable  *cancellable,
+                          GError       **error)
 {
   gboolean ret = FALSE;
-  GError *temp_error = NULL;
-  ot_lobj GFileEnumerator *dir_enum = NULL;
-  ot_lobj GFileInfo *file_info = NULL;
+  gs_unref_object GFile *boot_loader_path = g_file_resolve_relative_path (sysroot, "boot/loader");
+  gs_unref_object GFileInfo *info = NULL;
+  const char *target;
+  int ret_bootversion;
 
-  dir_enum = g_file_enumerate_children (from_dir, OSTREE_GIO_FAST_QUERYINFO,
+  if (!ot_gfile_query_info_allow_noent (boot_loader_path, OSTREE_GIO_FAST_QUERYINFO,
                                         G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                        NULL, error);
-  if (!dir_enum)
+                                        &info,
+                                        cancellable, error))
     goto out;
 
-  while ((file_info = g_file_enumerator_next_file (dir_enum, cancellable, error)) != NULL)
+  if (info == NULL)
+    ret_bootversion = 0;
+  else
     {
-      const char *name;
-      ot_lobj GFile *child = NULL;
-      ot_lobj GFile *possible_etc = NULL;
-      ot_lobj GFile *possible_usr = NULL;
-
-      name = g_file_info_get_name (file_info);
-
-      if (g_str_has_suffix (name, "-etc"))
-        goto next;
-      if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_DIRECTORY)
-        goto next;
-
-      child = g_file_get_child (from_dir, name);
-
-      possible_etc = ot_gfile_get_child_strconcat (from_dir, name, "-etc", NULL);
-      /* Bit of a hack... */
-      possible_usr = g_file_get_child (child, "usr");
+      if (g_file_info_get_file_type (info) != G_FILE_TYPE_SYMBOLIC_LINK)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Not a symbolic link: %s", gs_file_get_path_cached (boot_loader_path));
+          goto out;
+        }
 
-      if (g_file_query_exists (possible_etc, cancellable))
-        g_ptr_array_add (inout_deployments, g_file_get_child (from_dir, name));
-      else if (g_file_query_exists (possible_usr, cancellable))
-        goto next;
+      target = g_file_info_get_symlink_target (info);
+      if (g_strcmp0 (target, "loader.0") == 0)
+        ret_bootversion = 0;
+      else if (g_strcmp0 (target, "loader.1") == 0)
+        ret_bootversion = 1;
       else
         {
-          if (!list_deployments_internal (child, inout_deployments,
-                                          cancellable, error))
-            goto out;
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Invalid target '%s' in %s", target, gs_file_get_path_cached (boot_loader_path));
+          goto out;
         }
-
-    next:
-      g_clear_object (&file_info);
-    }
-  if (temp_error != NULL)
-    {
-      g_propagate_error (error, temp_error);
-      goto out;
     }
 
   ret = TRUE;
+  *out_bootversion = ret_bootversion;
  out:
   return ret;
 }
 
 gboolean
-ot_admin_list_deployments (GFile               *ostree_dir,
-                           const char          *osname,
-                           GPtrArray          **out_deployments,
-                           GCancellable        *cancellable,
-                           GError             **error)
+ot_admin_read_current_subbootversion (GFile         *sysroot,
+                                      int            bootversion,
+                                      int           *out_subbootversion,
+                                      GCancellable  *cancellable,
+                                      GError       **error)
 {
   gboolean ret = FALSE;
-  ot_lobj GFileEnumerator *dir_enum = NULL;
-  ot_lobj GFileInfo *file_info = NULL;
-  ot_lobj GFile *osdir = NULL;
-  ot_lptrarray GPtrArray *ret_deployments = NULL;
-
-  osdir = ot_gfile_get_child_build_path (ostree_dir, "deploy", osname, NULL);
-  ret_deployments = g_ptr_array_new_with_free_func (g_object_unref);
+  gs_unref_object GFile *ostree_dir = g_file_get_child (sysroot, "ostree");
+  gs_free char *ostree_bootdir_name = g_strdup_printf ("boot.%d", bootversion);
+  gs_unref_object GFile *ostree_bootdir = g_file_resolve_relative_path (ostree_dir, ostree_bootdir_name);
+  gs_free char *ostree_subbootdir_name = NULL;
+  gs_unref_object GFile *ostree_subbootdir = NULL;
+  gs_unref_ptrarray GPtrArray *deployments_to_swap = NULL;
 
-  if (!list_deployments_internal (osdir, ret_deployments, cancellable, error))
+  if (!ot_gfile_query_symlink_target_allow_noent (ostree_bootdir, &ostree_subbootdir,
+                                                  cancellable, error))
     goto out;
-  
+
+  if (ostree_subbootdir == NULL)
+    {
+      *out_subbootversion = 0;
+    }
+  else
+    {
+      const char *current_subbootdir_name = gs_file_get_basename_cached (ostree_subbootdir);
+      if (g_str_has_suffix (current_subbootdir_name, ".0"))
+        *out_subbootversion = 0;
+      else if (g_str_has_suffix (current_subbootdir_name, ".1"))
+        *out_subbootversion = 1;
+      else
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Invalid target '%s' in %s",
+                       gs_file_get_path_cached (ostree_subbootdir),
+                       gs_file_get_path_cached (ostree_bootdir));
+          goto out;
+        }
+    }
+
   ret = TRUE;
-  ot_transfer_out_value (out_deployments, &ret_deployments);
  out:
   return ret;
 }
 
 gboolean
-ot_admin_get_booted_os (char        **out_osname,
-                        char        **out_tree,
-                        GCancellable *cancellable,
-                        GError      **error)
+ot_admin_read_boot_loader_configs (GFile         *sysroot,
+                                   int            bootversion,
+                                   GPtrArray    **out_loader_configs,
+                                   GCancellable  *cancellable,
+                                   GError       **error)
 {
   gboolean ret = FALSE;
-  gs_free char *ret_osname = NULL;
-  gs_free char *ret_tree = NULL;
-  gs_free char *cmdline_contents = NULL;
-  const char *iter;
-  gsize len;
+  gs_unref_object GFileEnumerator *dir_enum = NULL;
+  gs_unref_object GFile *loader_entries_dir = NULL;
+  gs_unref_ptrarray GPtrArray *ret_loader_configs = NULL;
+  GError *temp_error = NULL;
 
-  if (!g_file_get_contents ("/proc/cmdline", &cmdline_contents, &len,
-                            error))
-    goto out;
+  loader_entries_dir = ot_gfile_resolve_path_printf (sysroot, "boot/loader.%d/entries",
+                                                     bootversion);
+  ret_loader_configs = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
 
-  iter = cmdline_contents;
-  do
+  dir_enum = g_file_enumerate_children (loader_entries_dir, OSTREE_GIO_FAST_QUERYINFO,
+                                        0, NULL, &temp_error);
+  if (!dir_enum)
     {
-      const char *next = strchr (iter, ' ');
-      if (next)
-       next += 1;
-      if (g_str_has_prefix (iter, "ostree="))
+      if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
         {
-          const char *slash = strchr (iter, '/');
-          if (slash)
+          g_clear_error (&temp_error);
+          goto done;
+        } 
+      else
+        {
+          g_propagate_error (error, temp_error);
+          goto out;
+        }
+    }
+
+  while (TRUE)
+    {
+      GFileInfo *file_info;
+      GFile *child;
+      const char *name;
+
+      if (!gs_file_enumerator_iterate (dir_enum, &file_info, &child,
+                                       cancellable, error))
+        goto out;
+      if (file_info == NULL)
+        break;
+
+      name = g_file_info_get_name (file_info);
+
+      if (g_str_has_prefix (name, "ostree-") &&
+          g_str_has_suffix (name, ".conf") &&
+          g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
+        {
+          gs_unref_object OtConfigParser *config = ot_config_parser_new (" \t");
+  
+          if (!ot_config_parser_parse (config, child, cancellable, error))
             {
-              const char *start = iter + strlen ("ostree=");
-              ret_osname = g_strndup (start, slash - start);
-              if (next)
-                ret_tree = g_strndup (slash + 1, next - slash - 1);
-              else
-                ret_tree = g_strdup (slash + 1);
-              break;
+              g_prefix_error (error, "Parsing %s: ", gs_file_get_path_cached (child));
+              goto out;
             }
+
+          g_ptr_array_add (ret_loader_configs, g_object_ref (config));
         }
-      iter = next;
     }
-  while (iter != NULL);
 
+ done:
+  ot_transfer_out_value (out_loader_configs, &ret_loader_configs);
   ret = TRUE;
  out:
-  ot_transfer_out_value (out_osname, &ret_osname);
-  ot_transfer_out_value (out_tree, &ret_tree);
   return ret;
 }
 
-gboolean
-ot_admin_get_active_deployment (GFile           *ostree_dir,
-                                char           **out_osname,
-                                GFile          **out_deployment,
-                                GCancellable    *cancellable,
-                                GError         **error)
+static gboolean
+list_deployment_dirs_for_os (GFile               *osdir,
+                             GPtrArray           *inout_deployments,
+                             GCancellable        *cancellable,
+                             GError             **error)
 {
   gboolean ret = FALSE;
-  ot_lptrarray GPtrArray *osnames = NULL;
-  ot_lptrarray GPtrArray *deployments = NULL;
-  gs_free char *ret_osname = NULL;
-  gs_unref_object GFile *ret_deployment = NULL;
+  const char *osname = gs_file_get_basename_cached (osdir);
+  gs_unref_object GFileEnumerator *dir_enum = NULL;
+  gs_unref_object GFile *osdeploy_dir = NULL;
+  GError *temp_error = NULL;
 
-  if (!ot_admin_get_booted_os (&ret_osname, NULL, cancellable, error))
-    goto out;
+  osdeploy_dir = g_file_get_child (osdir, "deploy");
 
-  if (ret_osname != NULL && out_deployment != NULL)
+  dir_enum = g_file_enumerate_children (osdeploy_dir, OSTREE_GIO_FAST_QUERYINFO,
+                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                        NULL, &temp_error);
+  if (!dir_enum)
     {
-      gs_unref_object GFile *rootfs_path = NULL;
-      gs_unref_object GFileInfo *rootfs_info = NULL;
-      guint32 root_dev;
-      guint64 root_inode;
-      guint i;
+      if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          g_clear_error (&temp_error);
+          goto done;
+        } 
+      else
+        {
+          g_propagate_error (error, temp_error);
+          goto out;
+        }
+    }
+
+  while (TRUE)
+    {
+      const char *name;
+      GFileInfo *file_info = NULL;
+      GFile *child = NULL;
+      gs_unref_object OtDeployment *deployment = NULL;
+      gs_free char *csum = NULL;
+      gint deployserial;
 
-      rootfs_path = g_file_new_for_path ("/");
-      rootfs_info = g_file_query_info (rootfs_path, OSTREE_GIO_FAST_QUERYINFO,
-                                       G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                       cancellable, error);
-      if (!rootfs_info)
+      if (!gs_file_enumerator_iterate (dir_enum, &file_info, &child,
+                                       cancellable, error))
         goto out;
+      if (file_info == NULL)
+        break;
+
+      name = g_file_info_get_name (file_info);
 
-      root_dev = g_file_info_get_attribute_uint32 (rootfs_info, "unix::device");
-      root_inode = g_file_info_get_attribute_uint64 (rootfs_info, "unix::inode");
+      if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_DIRECTORY)
+        continue;
 
-      if (!ot_admin_list_deployments (ostree_dir, ret_osname, &deployments,
-                                      cancellable, error))
+      if (!parse_deploy_path_name (name, &csum, &deployserial, error))
         goto out;
       
-      for (i = 0; i < deployments->len; i++)
-        {
-          GFile *deployment = deployments->pdata[i];
-          gs_unref_object GFileInfo *deployment_info = NULL;
-          guint32 deploy_dev;
-          guint64 deploy_inode;
-          
-          deployment_info = g_file_query_info (deployment, OSTREE_GIO_FAST_QUERYINFO,
-                                               G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                               cancellable, error);
-          if (!deployment_info)
-            goto out;
+      deployment = ot_deployment_new (-1, osname, csum, deployserial, NULL, -1);
+      g_ptr_array_add (inout_deployments, g_object_ref (deployment));
+    }
 
-          deploy_dev = g_file_info_get_attribute_uint32 (deployment_info, "unix::device");
-          deploy_inode = g_file_info_get_attribute_uint64 (deployment_info, "unix::inode");
+ done:
+  ret = TRUE;
+ out:
+  return ret;
+}
 
-          if (root_dev == deploy_dev && root_inode == deploy_inode)
-            {
-              ret_deployment = g_object_ref (deployment);
-              break;
-            }
+static gboolean
+list_all_deployment_directories (GFile               *sysroot,
+                                 GPtrArray          **out_deployments,
+                                 GCancellable        *cancellable,
+                                 GError             **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFileEnumerator *dir_enum = NULL;
+  gs_unref_object GFile *deploydir = NULL;
+  gs_unref_object GFile *osdir = NULL;
+  gs_unref_ptrarray GPtrArray *ret_deployments = NULL;
+  GError *temp_error = NULL;
+
+  deploydir = g_file_resolve_relative_path (sysroot, "ostree/deploy");
+
+  ret_deployments = g_ptr_array_new_with_free_func (g_object_unref);
+
+  dir_enum = g_file_enumerate_children (deploydir, OSTREE_GIO_FAST_QUERYINFO,
+                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                        cancellable, &temp_error);
+  if (!dir_enum)
+    {
+      if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          g_clear_error (&temp_error);
+          goto done;
+        } 
+      else
+        {
+          g_propagate_error (error, temp_error);
+          goto out;
         }
+    }
+
+  while (TRUE)
+    {
+      GFileInfo *file_info = NULL;
+      GFile *child = NULL;
+
+      if (!gs_file_enumerator_iterate (dir_enum, &file_info, &child,
+                                       NULL, error))
+        goto out;
+      if (file_info == NULL)
+        break;
+
+      if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_DIRECTORY)
+        continue;
       
-      g_assert (ret_deployment != NULL);
+      if (!list_deployment_dirs_for_os (child, ret_deployments, cancellable, error))
+        goto out;
+    }
+  
+ done:
+  ret = TRUE;
+  ot_transfer_out_value (out_deployments, &ret_deployments);
+ out:
+  return ret;
+}
+
+static char *
+get_ostree_kernel_arg_from_config (OtConfigParser  *config)
+{
+  const char *options;
+  char *ret;
+  char **opts, **iter;
+
+  options = ot_config_parser_get (config, "options");
+  if (!options)
+    return NULL;
+
+  opts = g_strsplit (options, " ", -1);
+  for (iter = opts; *iter; iter++)
+    {
+      const char *opt = *iter;
+      if (g_str_has_prefix (opt, "ostree="))
+        {
+          ret = g_strdup (opt + strlen ("ostree="));
+          break;
+        }
     }
+  g_strfreev (opts);
+
+  return ret;
+}
+
+static gboolean
+list_deployments_process_one_boot_entry (GFile               *sysroot,
+                                         guint                index,
+                                         OtConfigParser      *config,
+                                         GPtrArray           *inout_deployments,
+                                         GCancellable        *cancellable,
+                                         GError             **error)
+{
+  gboolean ret = FALSE;
+  gs_free char *ostree_arg = NULL;
+  gs_unref_object OtDeployment *deployment = NULL;
+
+  ostree_arg = get_ostree_kernel_arg_from_config (config);
+  if (ostree_arg == NULL)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No ostree= kernel argument found");
+      goto out;
+    }
+  
+  if (!parse_deployment (sysroot, ostree_arg, &deployment,
+                         cancellable, error))
+    goto out;
+  
+  ot_deployment_set_index (deployment, index);
+  ot_deployment_set_bootconfig (deployment, config);
 
+  g_ptr_array_add (inout_deployments, g_object_ref (deployment));
+  
   ret = TRUE;
-  ot_transfer_out_value (out_osname, &ret_osname);
-  ot_transfer_out_value (out_deployment, &ret_deployment);
  out:
   return ret;
 }
 
 gboolean
-ot_admin_get_default_ostree_dir (GFile        **out_ostree_dir,
-                                 GCancellable  *cancellable,
-                                 GError       **error)
+ot_admin_list_deployments (GFile               *sysroot,
+                           int                 *out_current_bootversion,
+                           GPtrArray          **out_deployments,
+                           GCancellable        *cancellable,
+                           GError             **error)
 {
   gboolean ret = FALSE;
-  gs_unref_object GFile *possible_ostree_dir = NULL;
-  gs_unref_object GFile *ret_ostree_dir = NULL;
-  gs_unref_object GFile *host_usr = NULL;
+  gs_unref_ptrarray GPtrArray *boot_loader_configs = NULL;
+  gs_unref_ptrarray GPtrArray *ret_deployments = NULL;
+  guint i;
+  int bootversion;
 
-  host_usr = g_file_new_for_path ("/usr");
+  if (!read_current_bootversion (sysroot, &bootversion, cancellable, error))
+    goto out;
 
-  if (ret_ostree_dir == NULL)
-    {
-      g_clear_object (&possible_ostree_dir);
-      possible_ostree_dir = g_file_new_for_path ("/sysroot/ostree");
-      if (g_file_query_exists (possible_ostree_dir, NULL))
-        ret_ostree_dir = g_object_ref (possible_ostree_dir);
-    }
-  if (ret_ostree_dir == NULL)
+  if (!ot_admin_read_boot_loader_configs (sysroot, bootversion, &boot_loader_configs,
+                                          cancellable, error))
+    goto out;
+
+  ret_deployments = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
+
+  for (i = 0; i < boot_loader_configs->len; i++)
     {
-      g_clear_object (&possible_ostree_dir);
-      possible_ostree_dir = g_file_new_for_path ("/ostree");
-      /* If there's also /usr, we assume we're outside an ostree root
-       * and thus should use /ostree.
-       */
-      if (g_file_query_exists (possible_ostree_dir, NULL) ||
-          g_file_query_exists (host_usr, NULL))
-        ret_ostree_dir = g_object_ref (possible_ostree_dir);
+      OtConfigParser *config = boot_loader_configs->pdata[i];
+
+      if (!list_deployments_process_one_boot_entry (sysroot, (guint)i,
+                                                    config, ret_deployments,
+                                                    cancellable, error))
+        goto out;
     }
 
   ret = TRUE;
-  ot_transfer_out_value (out_ostree_dir, &ret_ostree_dir);
+  *out_current_bootversion = bootversion;
+  ot_transfer_out_value (out_deployments, &ret_deployments);
+ out:
   return ret;
 }
 
 gboolean
-ot_admin_pull (GFile         *ostree_dir,
-               const char    *osname,
+ot_admin_pull (GFile         *sysroot,
+               const char    *remote,
+               const char    *ref,
                GCancellable  *cancellable,
                GError       **error)
 {
-  gs_unref_object GFile *repo_path = g_file_get_child (ostree_dir, "repo");
+  gs_unref_object GFile *repo_path = g_file_resolve_relative_path (sysroot, "ostree/repo");
   gs_free char *repo_arg = g_strconcat ("--repo=",
                                         gs_file_get_path_cached (repo_path),
                                         NULL);
 
-  return gs_subprocess_simple_run_sync (gs_file_get_path_cached (ostree_dir),
+  return gs_subprocess_simple_run_sync (NULL,
                                         GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
                                         cancellable, error,
-                                        "ostree", repo_arg, "pull", osname, NULL);
+                                        "ostree", repo_arg, "pull", remote, ref, NULL);
 }
 
-void
-ot_admin_parse_deploy_name (GFile       *ostree_dir,
-                            const char  *osname,
-                            GFile       *deployment,
-                            char       **out_name,
-                            char       **out_rev)
+GFile *
+ot_admin_get_deployment_directory (GFile        *sysroot,
+                                   OtDeployment *deployment)
 {
-  gs_unref_object GFile *deploy_dir = g_file_get_child (ostree_dir, "deploy");
-  gs_unref_object GFile *os_dir = g_file_get_child (deploy_dir, osname);
-  gs_free char *relpath = g_file_get_relative_path (os_dir, deployment);
-  const char *last_dash;
-
-  g_assert (relpath);
-  last_dash = strrchr (relpath, '-');
-  if (!last_dash)
-    g_error ("Failed to parse deployment name %s", relpath);
+  gs_free char *path = g_strdup_printf ("ostree/deploy/%s/deploy/%s.%d",
+                                        ot_deployment_get_osname (deployment),
+                                        ot_deployment_get_csum (deployment),
+                                        ot_deployment_get_deployserial (deployment));
+  return g_file_resolve_relative_path (sysroot, path);
+}
+
+gboolean
+ot_admin_cleanup (GFile               *sysroot,
+                  GCancellable        *cancellable,
+                  GError             **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *active_root = g_file_new_for_path ("/");
+  gs_unref_hashtable GHashTable *active_deployment_dirs = NULL;
+  gs_unref_ptrarray GPtrArray *deployments = NULL;
+  gs_unref_ptrarray GPtrArray *all_deployment_dirs = NULL;
+  gs_free char *cleanup_boot_name = NULL;
+  gs_unref_object GFile *cleanup_boot_dir = NULL;
+  int bootversion;
+  int subbootversion;
+  int cleanup_bootversion;
+  int cleanup_subbootversion;
+  guint i;
+  guint32 root_device;
+  guint64 root_inode;
+
+  if (!get_devino (active_root, &root_device, &root_inode,
+                   cancellable, error))
+    goto out;
+
+  active_deployment_dirs = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, NULL, 
g_object_unref);
+
+  if (!ot_admin_list_deployments (sysroot, &bootversion, &deployments,
+                                  cancellable, error))
+    goto out;
+
+  if (!ot_admin_read_current_subbootversion (sysroot, bootversion, &subbootversion,
+                                             cancellable, error))
+    goto out;
+
+  cleanup_bootversion = bootversion == 0 ? 1 : 0;
+  cleanup_subbootversion = subbootversion == 0 ? 1 : 0;
+
+  cleanup_boot_dir = ot_gfile_resolve_path_printf (sysroot, "boot/loader.%d", cleanup_bootversion);
+  if (!gs_shutil_rm_rf (cleanup_boot_dir, cancellable, error))
+    goto out;
+  g_clear_object (&cleanup_boot_dir);
+
+  cleanup_boot_dir = ot_gfile_resolve_path_printf (sysroot, "ostree/boot.%d", cleanup_bootversion);
+  if (!gs_shutil_rm_rf (cleanup_boot_dir, cancellable, error))
+    goto out;
+  g_clear_object (&cleanup_boot_dir);
+
+  cleanup_boot_dir = ot_gfile_resolve_path_printf (sysroot, "ostree/boot.%d.0", cleanup_bootversion);
+  if (!gs_shutil_rm_rf (cleanup_boot_dir, cancellable, error))
+    goto out;
+  g_clear_object (&cleanup_boot_dir);
+
+  cleanup_boot_dir = ot_gfile_resolve_path_printf (sysroot, "ostree/boot.%d.1", cleanup_bootversion);
+  if (!gs_shutil_rm_rf (cleanup_boot_dir, cancellable, error))
+    goto out;
+  g_clear_object (&cleanup_boot_dir);
+
+  cleanup_boot_dir = ot_gfile_resolve_path_printf (sysroot, "ostree/boot.%d.%d", bootversion,
+                                                   cleanup_subbootversion);
+  if (!gs_shutil_rm_rf (cleanup_boot_dir, cancellable, error))
+    goto out;
+  g_clear_object (&cleanup_boot_dir);
+
+  for (i = 0; i < deployments->len; i++)
+    {
+      OtDeployment *deployment = deployments->pdata[i];
+      GFile *deployment_path = ot_admin_get_deployment_directory (sysroot, deployment);
+      /* Transfer ownership */
+      g_hash_table_insert (active_deployment_dirs, deployment_path, deployment_path);
+    }
+
+  if (!list_all_deployment_directories (sysroot, &all_deployment_dirs,
+                                        cancellable, error))
+    goto out;
   
-  if (out_name)
-    *out_name = g_strndup (relpath, last_dash - relpath);
-  if (out_rev)
-    *out_rev = g_strdup (last_dash + 1);
+  for (i = 0; i < all_deployment_dirs->len; i++)
+    {
+      OtDeployment *deployment = all_deployment_dirs->pdata[i];
+      gs_unref_object GFile *deployment_path = ot_admin_get_deployment_directory (sysroot, deployment);
+      gs_unref_object GFile *origin_path = ot_admin_get_deployment_origin_path (deployment_path);
+      if (!g_hash_table_lookup (active_deployment_dirs, deployment_path))
+        {
+          guint32 device;
+          guint64 inode;
+
+          if (!get_devino (deployment_path, &device, &inode,
+                           cancellable, error))
+            goto out;
+
+          /* This shouldn't happen, because higher levels should
+           * disallow having the booted deployment not in the active
+           * deployment list, but let's be extra safe. */
+          if (device == root_device && inode == root_inode)
+            continue;
+
+          g_print ("ostadmin: Deleting deployment %s\n", gs_file_get_path_cached (deployment_path));
+          if (!gs_shutil_rm_rf (deployment_path, cancellable, error))
+            goto out;
+          if (!gs_shutil_rm_rf (origin_path, cancellable, error))
+            goto out;
+        }
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+OtBootloader *
+ot_admin_query_bootloader (GFile         *sysroot)
+{
+  OtBootloaderSyslinux *syslinux;
+
+  syslinux = ot_bootloader_syslinux_new (sysroot);
+  if (ot_bootloader_query ((OtBootloader*)syslinux))
+    return (OtBootloader*) (syslinux);
+
+  return NULL;
+}
+
+GKeyFile *
+ot_origin_new_from_refspec (const char *refspec)
+{
+  GKeyFile *ret = g_key_file_new ();
+  g_key_file_set_string (ret, "origin", "refspec", refspec);
+  return ret;
+}
+
+gboolean
+ot_admin_get_repo (GFile         *sysroot,
+                   OstreeRepo   **out_repo,
+                   GCancellable  *cancellable,
+                   GError       **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object OstreeRepo *ret_repo = NULL;
+  gs_unref_object GFile *repo_path = g_file_resolve_relative_path (sysroot, "ostree/repo");
+
+  ret_repo = ostree_repo_new (repo_path);
+  if (!ostree_repo_check (ret_repo, error))
+    goto out;
+    
+  ret = TRUE;
+  ot_transfer_out_value (out_repo, &ret_repo);
+ out:
+  return ret;
 }
diff --git a/src/ostree/ot-admin-functions.h b/src/ostree/ot-admin-functions.h
index a03ef59..5a3c076 100644
--- a/src/ostree/ot-admin-functions.h
+++ b/src/ostree/ot-admin-functions.h
@@ -24,6 +24,10 @@
 #define __OT_ADMIN_FUNCTIONS__
 
 #include <gio/gio.h>
+#include "ostree.h"
+#include "ot-deployment.h"
+#include "ot-bootloader.h"
+#include "ot-ordered-hash.h"
 
 G_BEGIN_DECLS
 
@@ -31,50 +35,84 @@ gboolean ot_admin_ensure_initialized (GFile         *ostree_dir,
                                      GCancellable  *cancellable,
                                      GError       **error);
 
-gboolean ot_admin_get_booted_os (char        **out_osname,
-                                 char        **out_tree,
-                                 GCancellable *cancellable,
-                                 GError      **error);
-
-gboolean ot_admin_get_current_deployment (GFile           *ostree_dir,
-                                          const char      *osname,
-                                          GFile          **out_deployment,
-                                          GCancellable    *cancellable,
-                                          GError         **error);
-gboolean ot_admin_get_previous_deployment (GFile           *ostree_dir,
-                                           const char      *osname,
-                                           GFile          **out_deployment,
-                                           GCancellable    *cancellable,
-                                           GError         **error);
-
-gboolean ot_admin_list_deployments (GFile               *ostree_dir,
-                                    const char          *osname,
+gboolean ot_admin_check_os (GFile         *sysroot, 
+                            const char    *osname,
+                            GCancellable  *cancellable,
+                            GError       **error);
+
+char *ot_admin_split_keyeq (char *str);
+OtOrderedHash *ot_admin_parse_kernel_args (const char *options);
+char * ot_admin_kernel_arg_string_serialize (OtOrderedHash *ohash);
+
+OtBootloader *ot_admin_query_bootloader (GFile         *sysroot);
+
+gboolean ot_admin_read_current_subbootversion (GFile         *sysroot,
+                                               int            bootversion,
+                                               int           *out_subbootversion,
+                                               GCancellable  *cancellable,
+                                               GError       **error);
+
+gboolean ot_admin_read_boot_loader_configs (GFile         *boot_dir,
+                                            int            bootversion,
+                                            GPtrArray    **out_loader_configs,
+                                            GCancellable  *cancellable,
+                                            GError       **error);
+
+gboolean ot_admin_list_deployments (GFile               *sysroot,
+                                    int                 *out_bootversion,
                                     GPtrArray          **out_deployments,
                                     GCancellable        *cancellable,
                                     GError             **error);
 
-gboolean ot_admin_get_active_deployment (GFile           *ostree_dir,
-                                         char           **out_osname,
-                                         GFile          **out_deployment,
-                                         GCancellable    *cancellable,
-                                         GError         **error);
+gboolean ot_admin_find_booted_deployment (GFile               *sysroot,
+                                          GPtrArray           *deployments,
+                                          OtDeployment       **out_deployment,
+                                          GCancellable        *cancellable,
+                                          GError             **error);
+
+gboolean ot_admin_require_booted_deployment (GFile               *sysroot,
+                                             OtDeployment       **out_deployment,
+                                             GCancellable        *cancellable,
+                                             GError             **error);
+
+gboolean ot_admin_require_deployment_or_osname (GFile               *sysroot,
+                                                GPtrArray           *deployment_list,
+                                                const char          *osname,
+                                                OtDeployment       **out_deployment,
+                                                GCancellable        *cancellable,
+                                                GError             **error);
+
+OtDeployment *ot_admin_get_merge_deployment (GPtrArray         *deployment_list,
+                                             const char        *osname,
+                                             OtDeployment      *booted_deployment,
+                                             OtDeployment      *new_deployment);
+
+GFile *ot_admin_get_deployment_origin_path (GFile   *deployment_path);
+
+GFile *ot_admin_get_deployment_directory (GFile        *sysroot,
+                                          OtDeployment *deployment);
+
+gboolean ot_admin_get_repo (GFile         *sysroot,
+                            OstreeRepo   **out_repo,
+                            GCancellable  *cancellable,
+                            GError       **error);
+
+gboolean ot_admin_cleanup (GFile               *sysroot,
+                           GCancellable        *cancellable,
+                           GError             **error);
 
 gboolean ot_admin_get_default_ostree_dir (GFile        **out_ostree_dir,
                                           GCancellable  *cancellable,
                                           GError       **error);
 
+GKeyFile *ot_origin_new_from_refspec (const char *refspec);
+
 gboolean ot_admin_pull (GFile         *ostree_dir,
-                        const char    *osname,
+                        const char    *remote,
+                        const char    *ref,
                         GCancellable  *cancellable,
                         GError       **error);
 
-void
-ot_admin_parse_deploy_name (GFile   *ostree_dir,
-                            const char *osname,
-                            GFile   *deployment,
-                            char   **out_name,
-                            char   **out_rev);
-
 G_END_DECLS
 
 #endif
diff --git a/src/ostree/ot-bootloader-syslinux.c b/src/ostree/ot-bootloader-syslinux.c
new file mode 100644
index 0000000..9fe6ae1
--- /dev/null
+++ b/src/ostree/ot-bootloader-syslinux.c
@@ -0,0 +1,312 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "ot-bootloader-syslinux.h"
+#include "libgsystem.h"
+#include "otutil.h"
+#include "ot-admin-functions.h"
+
+#include <string.h>
+
+struct _OtBootloaderSyslinux
+{
+  GObject       parent_instance;
+
+  GFile        *sysroot;
+  GFile        *config_path;
+};
+
+typedef GObjectClass OtBootloaderSyslinuxClass;
+
+static void ot_bootloader_syslinux_bootloader_iface_init (OtBootloaderInterface *iface);
+G_DEFINE_TYPE_WITH_CODE (OtBootloaderSyslinux, ot_bootloader_syslinux, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (OT_TYPE_BOOTLOADER, 
ot_bootloader_syslinux_bootloader_iface_init));
+
+static gboolean
+ot_bootloader_syslinux_query (OtBootloader *bootloader)
+{
+  OtBootloaderSyslinux *self = OT_BOOTLOADER_SYSLINUX (bootloader);
+
+  return g_file_query_file_type (self->config_path, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == 
G_FILE_TYPE_SYMBOLIC_LINK;
+}
+
+static gboolean
+append_config_from_boot_loader_entries (OtBootloaderSyslinux  *self,
+                                        gboolean               regenerate_default,
+                                        int                    bootversion,
+                                        GPtrArray             *new_lines,
+                                        GCancellable          *cancellable,
+                                        GError               **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_ptrarray GPtrArray *boot_loader_configs = NULL;
+  guint i;
+
+  if (!ot_admin_read_boot_loader_configs (self->sysroot, bootversion, &boot_loader_configs,
+                                          cancellable, error))
+    goto out;
+
+  for (i = 0; i < boot_loader_configs->len; i++)
+    {
+      OtConfigParser *config = boot_loader_configs->pdata[i];
+      const char *val;
+
+      val = ot_config_parser_get (config, "title");
+      if (!val)
+        val = "(Untitled)";
+
+      if (regenerate_default && i == 0)
+        {
+          g_ptr_array_add (new_lines, g_strdup_printf ("DEFAULT %s", val));
+        }
+
+      g_ptr_array_add (new_lines, g_strdup_printf ("LABEL %s", val));
+      
+      val = ot_config_parser_get (config, "linux");
+      if (!val)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "No \"linux\" key in bootloader config");
+          goto out;
+        }
+      g_ptr_array_add (new_lines, g_strdup_printf ("\tKERNEL %s", val));
+
+      val = ot_config_parser_get (config, "initrd");
+      if (val)
+        g_ptr_array_add (new_lines, g_strdup_printf ("\tINITRD %s", val));
+
+      val = ot_config_parser_get (config, "options");
+      if (val)
+        g_ptr_array_add (new_lines, g_strdup_printf ("\tAPPEND %s", val));
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static char *
+join_lines (GPtrArray  *lines)
+{
+  GString *buf = g_string_new ("");
+  guint i;
+  gboolean prev_was_empty = FALSE;
+
+  for (i = 0; i < lines->len; i++)
+    {
+      const char *line = lines->pdata[i];
+      /* Special bit to remove extraneous empty lines */
+      if (*line == '\0')
+        {
+          if (prev_was_empty || i == 0)
+            continue;
+          else
+            prev_was_empty = TRUE;
+        }
+      g_string_append (buf, line);
+      g_string_append_c (buf, '\n');
+    }
+  return g_string_free (buf, FALSE);
+}
+
+static gboolean
+ot_bootloader_syslinux_write_config (OtBootloader          *bootloader,
+                                     int                    bootversion,
+                                     GCancellable          *cancellable,
+                                     GError               **error)
+{
+  gboolean ret = FALSE;
+  OtBootloaderSyslinux *self = OT_BOOTLOADER_SYSLINUX (bootloader);
+  gs_unref_object GFile *new_config_path = NULL;
+  gs_free char *config_contents = NULL;
+  gs_free char *new_config_contents = NULL;
+  gs_unref_ptrarray GPtrArray *new_lines = NULL;
+  gs_unref_ptrarray GPtrArray *tmp_lines = NULL;
+  gs_free char *kernel_arg = NULL;
+  gboolean saw_default = FALSE;
+  gboolean regenerate_default = FALSE;
+  gboolean parsing_label = FALSE;
+  char **lines = NULL;
+  char **iter;
+  guint i;
+
+  new_config_path = ot_gfile_resolve_path_printf (self->sysroot, "boot/loader.%d/syslinux.cfg",
+                                                  bootversion);
+
+  /* This should follow the symbolic link to the current bootversion. */
+  config_contents = gs_file_load_contents_utf8 (self->config_path, cancellable, error);
+  if (!config_contents)
+    goto out;
+
+  lines = g_strsplit (config_contents, "\n", -1);
+  new_lines = g_ptr_array_new_with_free_func (g_free);
+  tmp_lines = g_ptr_array_new_with_free_func (g_free);
+  
+  /* Note special iteration condition here; we want to also loop one
+   * more time at the end where line = NULL to ensure we finish off
+   * processing the last LABEL.
+   */
+  iter = lines;
+  while (TRUE)
+    {
+      char *line = *iter;
+      gboolean skip = FALSE;
+
+      if (parsing_label && 
+          (line == NULL || !g_str_has_prefix (line, "\t")))
+        {
+          parsing_label = FALSE;
+          if (kernel_arg == NULL)
+            {
+              g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                                   "No KERNEL argument found after LABEL");
+              goto out;
+            }
+
+          /* If this is a non-ostree kernel, just emit the lines
+           * we saw.
+           */
+          if (!g_str_has_prefix (kernel_arg, "/ostree/"))
+            {
+              for (i = 0; i < tmp_lines->len; i++)
+                {
+                  g_ptr_array_add (new_lines, tmp_lines->pdata[i]);
+                  tmp_lines->pdata[i] = NULL; /* Transfer ownership */
+                }
+            }
+          else
+            {
+              /* Otherwise, we drop the config on the floor - it
+               * will be regenerated.
+               */
+              g_ptr_array_set_size (tmp_lines, 0);
+            }
+        }
+
+      if (line == NULL)
+        break;
+
+      if (!parsing_label &&
+          (g_str_has_prefix (line, "LABEL ")))
+        {
+          parsing_label = TRUE;
+          g_ptr_array_set_size (tmp_lines, 0);
+        }
+      else if (parsing_label && g_str_has_prefix (line, "\tKERNEL "))
+        {
+          g_free (kernel_arg);
+          kernel_arg = g_strdup (line + strlen ("\tKERNEL "));
+        }
+      else if (!parsing_label &&
+               (g_str_has_prefix (line, "DEFAULT ")))
+        {
+          saw_default = TRUE;
+          if (g_str_has_prefix (line, "DEFAULT ostree:"))
+            regenerate_default = TRUE;
+          skip = TRUE;
+        }
+      
+      if (skip)
+        {
+          g_free (line);
+        }
+      else
+        {
+          if (parsing_label)
+            {
+              g_ptr_array_add (tmp_lines, line);
+            }
+          else
+            {
+              g_ptr_array_add (new_lines, line);
+            }
+        }
+      /* Transfer ownership */
+      *iter = NULL;
+      iter++;
+    }
+
+  if (!saw_default)
+    regenerate_default = TRUE;
+
+  if (!append_config_from_boot_loader_entries (self, regenerate_default,
+                                               bootversion, new_lines,
+                                               cancellable, error))
+    goto out;
+
+  new_config_contents = join_lines (new_lines);
+
+  if (strcmp (new_config_contents, config_contents) != 0)
+    {
+      if (!g_file_replace_contents (new_config_path, new_config_contents,
+                                    strlen (new_config_contents), 
+                                    NULL, FALSE, G_FILE_CREATE_NONE,
+                                    NULL, cancellable, error))
+        goto out;
+      g_print ("Saved new version of %s\n", gs_file_get_path_cached (self->config_path));
+    }
+  
+  ret = TRUE;
+ out:
+  g_free (lines); /* Note we freed elements individually */
+  return ret;
+}
+
+static void
+ot_bootloader_syslinux_finalize (GObject *object)
+{
+  OtBootloaderSyslinux *self = OT_BOOTLOADER_SYSLINUX (object);
+
+  g_clear_object (&self->sysroot);
+  g_clear_object (&self->config_path);
+
+  G_OBJECT_CLASS (ot_bootloader_syslinux_parent_class)->finalize (object);
+}
+
+void
+ot_bootloader_syslinux_init (OtBootloaderSyslinux *self)
+{
+}
+
+static void
+ot_bootloader_syslinux_bootloader_iface_init (OtBootloaderInterface *iface)
+{
+  iface->query = ot_bootloader_syslinux_query;
+  iface->write_config = ot_bootloader_syslinux_write_config;
+}
+
+void
+ot_bootloader_syslinux_class_init (OtBootloaderSyslinuxClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->finalize = ot_bootloader_syslinux_finalize;
+}
+
+OtBootloaderSyslinux *
+ot_bootloader_syslinux_new (GFile *sysroot)
+{
+  OtBootloaderSyslinux *self = g_object_new (OT_TYPE_BOOTLOADER_SYSLINUX, NULL);
+  self->sysroot = g_object_ref (sysroot);
+  self->config_path = g_file_resolve_relative_path (self->sysroot, "boot/syslinux/syslinux.cfg");
+  return self;
+}
diff --git a/src/ostree/ot-bootloader-syslinux.h b/src/ostree/ot-bootloader-syslinux.h
new file mode 100644
index 0000000..8a81089
--- /dev/null
+++ b/src/ostree/ot-bootloader-syslinux.h
@@ -0,0 +1,40 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __OT_BOOTLOADER_SYSLINUX_H__
+#define __OT_BOOTLOADER_SYSLINUX_H__
+
+#include "ot-bootloader.h"
+
+G_BEGIN_DECLS
+
+#define OT_TYPE_BOOTLOADER_SYSLINUX (ot_bootloader_syslinux_get_type ())
+#define OT_BOOTLOADER_SYSLINUX(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), OT_TYPE_BOOTLOADER_SYSLINUX, 
OtBootloaderSyslinux))
+#define OT_IS_BOOTLOADER_SYSLINUX(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), OT_TYPE_BOOTLOADER_SYSLINUX))
+
+typedef struct _OtBootloaderSyslinux OtBootloaderSyslinux;
+
+GType ot_bootloader_syslinux_get_type (void) G_GNUC_CONST;
+
+OtBootloaderSyslinux * ot_bootloader_syslinux_new (GFile *sysroot);
+
+G_END_DECLS
+
+#endif /* __OT_BOOTLOADER_SYSLINUX_H__ */
diff --git a/src/ostree/ot-bootloader.c b/src/ostree/ot-bootloader.c
new file mode 100644
index 0000000..68ac223
--- /dev/null
+++ b/src/ostree/ot-bootloader.c
@@ -0,0 +1,49 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+#include "ot-bootloader.h"
+
+G_DEFINE_INTERFACE (OtBootloader, ot_bootloader, G_TYPE_OBJECT)
+
+static void
+ot_bootloader_default_init (OtBootloaderInterface *iface)
+{
+}
+
+gboolean
+ot_bootloader_query (OtBootloader  *self)
+{
+  g_return_val_if_fail (OT_IS_BOOTLOADER (self), FALSE);
+
+  return OT_BOOTLOADER_GET_IFACE (self)->query (self);
+}
+
+gboolean
+ot_bootloader_write_config (OtBootloader  *self,
+                            int            bootversion,
+                            GCancellable  *cancellable,
+                            GError       **error)
+{
+  g_return_val_if_fail (OT_IS_BOOTLOADER (self), FALSE);
+
+  return OT_BOOTLOADER_GET_IFACE (self)->write_config (self, bootversion, 
+                                                       cancellable, error);
+}
diff --git a/src/ostree/ot-bootloader.h b/src/ostree/ot-bootloader.h
new file mode 100644
index 0000000..6043b83
--- /dev/null
+++ b/src/ostree/ot-bootloader.h
@@ -0,0 +1,59 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __OT_BOOTLOADER_H__
+#define __OT_BOOTLOADER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define OT_TYPE_BOOTLOADER (ot_bootloader_get_type ())
+#define OT_BOOTLOADER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), OT_TYPE_BOOTLOADER, OtBootloader))
+#define OT_IS_BOOTLOADER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), OT_TYPE_BOOTLOADER))
+#define OT_BOOTLOADER_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), OT_TYPE_BOOTLOADER, 
OtBootloaderInterface))
+
+typedef struct _OtBootloader OtBootloader;
+typedef struct _OtBootloaderInterface                            OtBootloaderInterface;
+
+struct _OtBootloaderInterface
+{
+  GTypeInterface g_iface;
+
+  /* virtual functions */
+  gboolean             (* query)                  (OtBootloader  *self);
+  gboolean             (* write_config)           (OtBootloader  *self,
+                                                   int            bootversion,
+                                                   GCancellable  *cancellable,
+                                                   GError       **error);
+};
+
+GType ot_bootloader_get_type (void) G_GNUC_CONST;
+
+gboolean ot_bootloader_query (OtBootloader *self);
+
+gboolean ot_bootloader_write_config (OtBootloader  *self,
+                                     int            bootversion,
+                                     GCancellable  *cancellable,
+                                     GError       **error);
+
+G_END_DECLS
+
+#endif /* __OT_BOOTLOADER_H__ */
diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c
index c8f60fb..b1dfac7 100644
--- a/src/ostree/ot-builtin-admin.c
+++ b/src/ostree/ot-builtin-admin.c
@@ -31,12 +31,10 @@
 
 #include <glib/gi18n.h>
 
-static char *opt_ostree_dir = NULL;
-static char *opt_boot_dir = "/boot";
+static char *opt_sysroot = "/";
 
 static GOptionEntry options[] = {
-  { "ostree-dir", 0, 0, G_OPTION_ARG_STRING, &opt_ostree_dir, "Path to OSTree root directory (default: 
/ostree)", NULL },
-  { "boot-dir", 0, 0, G_OPTION_ARG_STRING, &opt_boot_dir, "Path to system boot directory (default: /boot)", 
NULL },
+  { "sysroot", 0, 0, G_OPTION_ARG_STRING, &opt_sysroot, "Path to root directory (default: /)", NULL },
   { NULL }
 };
 
@@ -51,9 +49,8 @@ static OstreeAdminCommand admin_subcommands[] = {
   { "deploy", ot_admin_builtin_deploy },
   { "install", ot_admin_builtin_install },
   { "upgrade", ot_admin_builtin_upgrade },
-  { "pull-deploy", ot_admin_builtin_pull_deploy },
   { "prune", ot_admin_builtin_prune },
-  { "update-kernel", ot_admin_builtin_update_kernel },
+  { "status", ot_admin_builtin_status },
   { "config-diff", ot_admin_builtin_diff },
   { "run-triggers", ot_admin_builtin_run_triggers },
   { NULL, NULL }
@@ -70,8 +67,6 @@ ostree_builtin_admin (int argc, char **argv, GFile *repo_path, GError **error)
   int subcmd_argc;
   OtAdminBuiltinOpts admin_opts;
   char **subcmd_argv = NULL;
-  ot_lobj GFile *ostree_dir = NULL;
-  ot_lobj GFile *boot_dir = NULL;
 
   context = g_option_context_new ("[OPTIONS] SUBCOMMAND - Run an administrative subcommand");
 
@@ -117,21 +112,9 @@ ostree_builtin_admin (int argc, char **argv, GFile *repo_path, GError **error)
       goto out;
     }
 
-  if (opt_ostree_dir != NULL)
-    {
-      ostree_dir = g_file_new_for_path (opt_ostree_dir);
-    }
-  else
-    {
-      if (!ot_admin_get_default_ostree_dir (&ostree_dir, cancellable, error))
-        goto out;
-    }
-  boot_dir = g_file_new_for_path (opt_boot_dir);
-
   ostree_prep_builtin_argv (subcommand_name, argc-2, argv+2, &subcmd_argc, &subcmd_argv);
 
-  admin_opts.ostree_dir = ostree_dir;
-  admin_opts.boot_dir = boot_dir;
+  admin_opts.sysroot = g_file_new_for_path (opt_sysroot);
   if (!subcommand->fn (subcmd_argc, subcmd_argv, &admin_opts, error))
     goto out;
  
diff --git a/src/ostree/ot-config-parser.c b/src/ostree/ot-config-parser.c
new file mode 100644
index 0000000..e87d5b9
--- /dev/null
+++ b/src/ostree/ot-config-parser.c
@@ -0,0 +1,230 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "ot-config-parser.h"
+#include "libgsystem.h"
+
+struct _OtConfigParser
+{
+  GObject       parent_instance;
+
+  gboolean      parsed;
+  char         *separators;
+
+  GHashTable   *options;
+  GPtrArray    *lines;
+};
+
+typedef GObjectClass OtConfigParserClass;
+
+G_DEFINE_TYPE (OtConfigParser, ot_config_parser, G_TYPE_OBJECT)
+
+gboolean
+ot_config_parser_parse (OtConfigParser  *self,
+                        GFile           *path,
+                        GCancellable    *cancellable,
+                        GError         **error)
+{
+  gboolean ret = FALSE;
+  gs_free char *contents = NULL;
+  char **lines = NULL;
+  char **iter = NULL;
+
+  g_return_val_if_fail (!self->parsed, FALSE);
+
+  contents = gs_file_load_contents_utf8 (path, cancellable, error);
+  if (!contents)
+    goto out;
+
+  lines = g_strsplit (contents, "\n", -1);
+  for (iter = lines; *iter; iter++)
+    {
+      const char *line = *iter;
+      char *keyname = "";
+      
+      if (g_ascii_isalpha (*line))
+        {
+          char **items = NULL;
+          items = g_strsplit_set (line, self->separators, 2);
+          if (g_strv_length (items) == 2 && items[0][0] != '\0')
+            {
+              keyname = items[0];
+              g_hash_table_insert (self->options, items[0], items[1]);
+              g_free (items); /* Transfer ownership */
+            }
+          else
+            {
+              g_strfreev (items);
+            }
+        }
+      g_ptr_array_add (self->lines, g_variant_new ("(ss)", keyname, line));
+    }
+
+  self->parsed = TRUE;
+  
+  ret = TRUE;
+ out:
+  g_strfreev (lines);
+  return ret;
+}
+
+void
+ot_config_parser_set (OtConfigParser  *self,
+                      const char      *key,
+                      const char      *value)
+{
+  g_hash_table_replace (self->options, g_strdup (key), g_strdup (value));
+}
+
+const char *
+ot_config_parser_get (OtConfigParser  *self,
+                      const char      *key)
+{
+  return g_hash_table_lookup (self->options, key);
+}
+
+static gboolean
+write_key (OtConfigParser         *self,
+           GDataOutputStream      *out,
+           const char             *key,
+           const char             *value,
+           GCancellable           *cancellable,
+           GError                **error)
+{
+  gboolean ret = FALSE;
+
+  if (!g_data_output_stream_put_string (out, key, cancellable, error))
+    goto out;
+  if (!g_data_output_stream_put_byte (out, self->separators[0], cancellable, error))
+    goto out;
+  if (!g_data_output_stream_put_string (out, value, cancellable, error))
+    goto out;
+  if (!g_data_output_stream_put_byte (out, '\n', cancellable, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+           
+gboolean
+ot_config_parser_write (OtConfigParser   *self,
+                        GFile            *output,
+                        GCancellable     *cancellable,
+                        GError          **error)
+{
+  gboolean ret = FALSE;
+  GHashTableIter hashiter;
+  gpointer hashkey, hashvalue;
+  gs_unref_object GOutputStream *out = NULL;
+  gs_unref_object GDataOutputStream *dataout = NULL;
+  guint i;
+  gs_unref_hashtable GHashTable *written_overrides = NULL;
+
+  written_overrides = g_hash_table_new (g_str_hash, g_str_equal);
+
+  out = (GOutputStream*)g_file_replace (output, NULL, FALSE, 0, cancellable, error);
+  if (!out)
+    goto out;
+
+  dataout = g_data_output_stream_new (out);
+
+  for (i = 0; i < self->lines->len; i++)
+    {
+      GVariant *linedata = self->lines->pdata[i];
+      const char *key;
+      const char *value;
+      const char *line;
+
+      g_variant_get (linedata, "(&s&s)", &key, &line);
+
+      value = g_hash_table_lookup (self->options, key);
+      if (value == NULL)
+        {
+          if (!g_data_output_stream_put_string (dataout, line, cancellable, error))
+            goto out;
+          if (!g_data_output_stream_put_byte (dataout, '\n', cancellable, error))
+            goto out;
+        }
+      else
+        {
+          if (!write_key (self, dataout, key, value, cancellable, error))
+            goto out;
+          g_hash_table_insert (written_overrides, (gpointer)key, (gpointer)key);
+        }
+    }
+
+  g_hash_table_iter_init (&hashiter, self->options);
+  while (g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue))
+    {
+      if (g_hash_table_lookup (written_overrides, hashkey))
+        continue;
+      if (!write_key (self, dataout, hashkey, hashvalue, cancellable, error))
+        goto out;
+    }
+
+  if (!g_output_stream_close ((GOutputStream*)dataout, cancellable, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static void
+ot_config_parser_finalize (GObject *object)
+{
+  OtConfigParser *self = OT_CONFIG_PARSER (object);
+
+  g_hash_table_unref (self->options);
+  g_ptr_array_unref (self->lines);
+  g_free (self->separators);
+
+  G_OBJECT_CLASS (ot_config_parser_parent_class)->finalize (object);
+}
+
+static void
+ot_config_parser_init (OtConfigParser *self)
+{
+  self->options = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+  self->lines = g_ptr_array_new ();
+}
+
+void
+ot_config_parser_class_init (OtConfigParserClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->finalize = ot_config_parser_finalize;
+}
+
+OtConfigParser *
+ot_config_parser_new (const char *separators)
+{
+  OtConfigParser *self = NULL;
+
+  g_return_val_if_fail (separators != NULL && separators[0], NULL);
+
+  self = g_object_new (OT_TYPE_CONFIG_PARSER, NULL);
+  self->separators = g_strdup (separators);
+  return self;
+}
diff --git a/src/ostree/ot-config-parser.h b/src/ostree/ot-config-parser.h
new file mode 100644
index 0000000..8b7b47a
--- /dev/null
+++ b/src/ostree/ot-config-parser.h
@@ -0,0 +1,58 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __OT_CONFIG_PARSER_H__
+#define __OT_CONFIG_PARSER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define OT_TYPE_CONFIG_PARSER (ot_config_parser_get_type ())
+#define OT_CONFIG_PARSER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), OT_TYPE_CONFIG_PARSER, OtConfigParser))
+#define OT_IS_CONFIG_PARSER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), OT_TYPE_CONFIG_PARSER))
+
+typedef struct _OtConfigParser OtConfigParser;
+
+GType ot_config_parser_get_type (void) G_GNUC_CONST;
+
+OtConfigParser * ot_config_parser_new (const char *separator);
+
+gboolean ot_config_parser_parse (OtConfigParser  *self,
+                                 GFile           *path,
+                                 GCancellable    *cancellable,
+                                 GError         **error);
+
+gboolean ot_config_parser_write (OtConfigParser   *self,
+                                 GFile            *output,
+                                 GCancellable     *cancellable,
+                                 GError          **error);
+
+void ot_config_parser_set (OtConfigParser  *self,
+                           const char      *key,
+                           const char      *value);
+
+const char *ot_config_parser_get (OtConfigParser  *self,
+                                  const char      *key);
+
+
+G_END_DECLS
+
+#endif /* __OT_CONFIG_PARSER_H__ */
diff --git a/src/ostree/ot-deployment.c b/src/ostree/ot-deployment.c
new file mode 100644
index 0000000..4f51b95
--- /dev/null
+++ b/src/ostree/ot-deployment.c
@@ -0,0 +1,209 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "ot-deployment.h"
+
+struct _OtDeployment
+{
+  GObject       parent_instance;
+
+  int index;  /* Global offset */
+  char *osname;  /* osname */
+  char *csum;  /* OSTree checksum of tree */
+  int deployserial;  /* How many times this particular csum appears in deployment list */
+  char *bootcsum;  /* Checksum of kernel+initramfs */
+  int bootserial; /* An integer assigned to this tree per its ${bootcsum} */
+  OtConfigParser *bootconfig; /* Bootloader configuration */
+  GKeyFile *origin; /* How to construct an upgraded version of this tree */
+};
+
+typedef GObjectClass OtDeploymentClass;
+
+G_DEFINE_TYPE (OtDeployment, ot_deployment, G_TYPE_OBJECT)
+
+const char *
+ot_deployment_get_csum (OtDeployment *self)
+{
+  return self->csum;
+}
+
+const char *
+ot_deployment_get_bootcsum (OtDeployment *self)
+{
+  return self->bootcsum;
+}
+
+const char *
+ot_deployment_get_osname (OtDeployment *self)
+{
+  return self->osname;
+}
+
+int
+ot_deployment_get_deployserial (OtDeployment *self)
+{
+  return self->deployserial;
+}
+
+int
+ot_deployment_get_bootserial (OtDeployment *self)
+{
+  return self->bootserial;
+}
+
+OtConfigParser *
+ot_deployment_get_bootconfig (OtDeployment *self)
+{
+  return self->bootconfig;
+}
+
+GKeyFile *
+ot_deployment_get_origin (OtDeployment *self)
+{
+  return self->origin;
+}
+
+int
+ot_deployment_get_index (OtDeployment *self)
+{
+  return self->index;
+}
+
+void
+ot_deployment_set_index (OtDeployment *self, int index)
+{
+  self->index = index;
+}
+
+void
+ot_deployment_set_bootserial (OtDeployment *self, int index)
+{
+  self->bootserial = index;
+}
+
+void
+ot_deployment_set_bootconfig (OtDeployment *self, OtConfigParser *bootconfig)
+{
+  g_clear_object (&self->bootconfig);
+  if (bootconfig)
+    self->bootconfig = g_object_ref (bootconfig);
+}
+
+void
+ot_deployment_set_origin (OtDeployment *self, GKeyFile *origin)
+{
+  g_clear_pointer (&self->origin, g_key_file_unref);
+  if (origin)
+    self->origin = g_key_file_ref (origin);
+}
+
+OtDeployment *
+ot_deployment_clone (OtDeployment *self)
+{
+  OtDeployment *ret = ot_deployment_new (self->index, self->osname, self->csum,
+                                         self->deployserial,
+                                         self->bootcsum, self->bootserial);
+  ot_deployment_set_bootconfig (ret, self->bootconfig);
+  ot_deployment_set_origin (ret, self->origin);
+  return ret;
+}
+
+guint
+ot_deployment_hash (gconstpointer v)
+{
+  OtDeployment *d = (OtDeployment*)v;
+  return g_str_hash (ot_deployment_get_osname (d)) +
+    g_str_hash (ot_deployment_get_csum (d)) +
+    ot_deployment_get_deployserial (d);
+}
+
+gboolean
+ot_deployment_equal (gconstpointer ap, gconstpointer bp)
+{
+  OtDeployment *a = (OtDeployment*)ap;
+  OtDeployment *b = (OtDeployment*)bp;
+  
+  if (a == NULL && b == NULL)
+    return TRUE;
+  else if (a != NULL && b != NULL)
+    return g_str_equal (ot_deployment_get_osname (a),
+                        ot_deployment_get_osname (b)) &&
+      g_str_equal (ot_deployment_get_csum (a),
+                   ot_deployment_get_csum (b)) &&
+      ot_deployment_get_deployserial (a) == ot_deployment_get_deployserial (b);
+  else 
+    return FALSE;
+}
+
+static void
+ot_deployment_finalize (GObject *object)
+{
+  OtDeployment *self = OT_DEPLOYMENT (object);
+
+  g_free (self->osname);
+  g_free (self->csum);
+  g_free (self->bootcsum);
+  g_clear_object (&self->bootconfig);
+  g_clear_pointer (&self->origin, g_key_file_unref);
+
+  G_OBJECT_CLASS (ot_deployment_parent_class)->finalize (object);
+}
+
+void
+ot_deployment_init (OtDeployment *self)
+{
+}
+
+void
+ot_deployment_class_init (OtDeploymentClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->finalize = ot_deployment_finalize;
+}
+
+OtDeployment *
+ot_deployment_new (int    index,
+                   const char  *osname,
+                   const char  *csum,
+                   int    deployserial,
+                   const char  *bootcsum,
+                   int    bootserial)
+{
+  OtDeployment *self;
+  
+  /* index may be -1 */
+  g_return_val_if_fail (osname != NULL, NULL);
+  g_return_val_if_fail (csum != NULL, NULL);
+  g_return_val_if_fail (deployserial >= 0, NULL);
+  /* We can have "disconnected" deployments that don't have a
+     bootcsum/serial */
+
+  self = g_object_new (OT_TYPE_DEPLOYMENT, NULL);
+  self->index = index;
+  self->osname = g_strdup (osname);
+  self->csum = g_strdup (csum);
+  self->deployserial = deployserial;
+  self->bootcsum = g_strdup (bootcsum);
+  self->bootserial = bootserial;
+  return self;
+}
diff --git a/src/ostree/ot-deployment.h b/src/ostree/ot-deployment.h
new file mode 100644
index 0000000..66470d4
--- /dev/null
+++ b/src/ostree/ot-deployment.h
@@ -0,0 +1,66 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __OT_DEPLOYMENT_H__
+#define __OT_DEPLOYMENT_H__
+
+#include <gio/gio.h>
+#include "ot-config-parser.h"
+
+G_BEGIN_DECLS
+
+#define OT_TYPE_DEPLOYMENT (ot_deployment_get_type ())
+#define OT_DEPLOYMENT(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), OT_TYPE_DEPLOYMENT, OtDeployment))
+#define OT_IS_DEPLOYMENT(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), OT_TYPE_DEPLOYMENT))
+
+typedef struct _OtDeployment OtDeployment;
+
+GType ot_deployment_get_type (void) G_GNUC_CONST;
+
+guint ot_deployment_hash (gconstpointer v);
+gboolean ot_deployment_equal (gconstpointer a, gconstpointer b);
+
+OtDeployment * ot_deployment_new (int    index,
+                                  const char  *osname,
+                                  const char  *csum,
+                                  int    deployserial,
+                                  const char  *bootcsum,
+                                  int    bootserial);
+
+int ot_deployment_get_index (OtDeployment *self);
+const char *ot_deployment_get_osname (OtDeployment *self);
+int ot_deployment_get_deployserial (OtDeployment *self);
+const char *ot_deployment_get_csum (OtDeployment *self);
+const char *ot_deployment_get_bootcsum (OtDeployment *self);
+int ot_deployment_get_bootserial (OtDeployment *self);
+OtConfigParser *ot_deployment_get_bootconfig (OtDeployment *self);
+GKeyFile *ot_deployment_get_origin (OtDeployment *self);
+
+void ot_deployment_set_index (OtDeployment *self, int index);
+void ot_deployment_set_bootserial (OtDeployment *self, int index);
+void ot_deployment_set_bootconfig (OtDeployment *self, OtConfigParser *bootconfig);
+void ot_deployment_set_origin (OtDeployment *self, GKeyFile *origin);
+
+OtDeployment *ot_deployment_clone (OtDeployment *self);
+
+
+G_END_DECLS
+
+#endif /* __OT_DEPLOYMENT_H__ */
diff --git a/src/ostree/ot-ordered-hash.c b/src/ostree/ot-ordered-hash.c
new file mode 100644
index 0000000..a7709e4
--- /dev/null
+++ b/src/ostree/ot-ordered-hash.c
@@ -0,0 +1,82 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "ot-ordered-hash.h"
+
+OtOrderedHash *
+ot_ordered_hash_new (void)
+{
+  OtOrderedHash *ret;
+  ret = g_new0 (OtOrderedHash, 1);
+  ret->order = g_ptr_array_new_with_free_func (g_free);
+  ret->table = g_hash_table_new (g_str_hash, g_str_equal);
+  return ret;
+}
+
+void
+ot_ordered_hash_free (OtOrderedHash *ohash)
+{
+  if (!ohash)
+    return;
+  g_ptr_array_unref (ohash->order);
+  g_hash_table_unref (ohash->table);
+  g_free (ohash);
+}
+
+void
+ot_ordered_hash_cleanup (void *loc)
+{
+  ot_ordered_hash_free (*((OtOrderedHash**)loc));
+}
+
+void
+ot_ordered_hash_replace_key_take (OtOrderedHash   *ohash,
+                                  char            *key,
+                                  const char      *value)
+{
+  gboolean existed;
+
+  existed = g_hash_table_remove (ohash->table, key);
+  if (!existed)
+    g_ptr_array_add (ohash->order, key);
+  g_hash_table_insert (ohash->table, key, (char*)value);
+}
+
+void
+ot_ordered_hash_replace_key (OtOrderedHash  *ohash,
+                             const char     *key,
+                             const char     *val)
+{
+  GString *buf;
+  gsize keylen;
+  char *valp;
+  char *valblock;
+  
+  buf = g_string_new (key);
+  keylen = buf->len;
+  g_string_append_c (buf, '\0');
+  g_string_append (buf, val);
+  valblock = g_string_free (buf, FALSE);
+  valp = valblock + keylen + 1;
+
+  ot_ordered_hash_replace_key_take (ohash, valblock, valp);
+}
diff --git a/src/ostree/ot-ordered-hash.h b/src/ostree/ot-ordered-hash.h
new file mode 100644
index 0000000..a3a4ef8
--- /dev/null
+++ b/src/ostree/ot-ordered-hash.h
@@ -0,0 +1,46 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters verbum org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __OT_ORDERED_HASH_H__
+#define __OT_ORDERED_HASH_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+typedef struct {
+  GPtrArray  *order;
+  GHashTable *table;
+} OtOrderedHash;
+
+OtOrderedHash *ot_ordered_hash_new (void);
+void ot_ordered_hash_free (OtOrderedHash *ohash);
+void ot_ordered_hash_cleanup (void *loc);
+void ot_ordered_hash_replace_key_take (OtOrderedHash  *ohash,
+                                       char         *key,
+                                       const char   *value);
+void ot_ordered_hash_replace_key (OtOrderedHash  *ohash,
+                                  const char   *key,
+                                  const char   *val);
+
+
+G_END_DECLS
+
+#endif /* __OT_ORDERED_HASH_H__ */
diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c
index cfd7481..d6e68b1 100644
--- a/src/switchroot/ostree-prepare-root.c
+++ b/src/switchroot/ostree-prepare-root.c
@@ -41,18 +41,19 @@
 
 #include "ostree-mount-util.h"
 
-static void
-parse_ostree_cmdline (char **out_osname,
-                     char **out_tree)
+static char *
+parse_ostree_cmdline (void)
 {
   FILE *f = fopen("/proc/cmdline", "r");
   char *cmdline = NULL;
   const char *iter;
+  char *ret = NULL;
   size_t len;
+
   if (!f)
-    return;
+    return NULL;
   if (getline (&cmdline, &len, f) < 0)
-    return;
+    return NULL;
 
   if (cmdline[len-1] == '\n')
     cmdline[len-1] = '\0';
@@ -66,39 +67,31 @@ parse_ostree_cmdline (char **out_osname,
        next_nonspc += 1;
       if (strncmp (iter, "ostree=", strlen ("ostree=")) == 0)
         {
-          const char *slash = strchr (iter, '/');
-          if (slash)
-            {
-              const char *start = iter + strlen ("ostree=");
-              *out_osname = strndup (start, slash - start);
-              if (next)
-                *out_tree = strndup (slash + 1, next - slash - 1);
-              else
-                *out_tree = strdup (slash + 1);
-              break;
-            }
+         const char *start = iter + strlen ("ostree=");
+         if (next)
+           ret = strndup (start, next - start);
+         else
+           ret = strdup (start);
+         break;
         }
       iter = next_nonspc;
     }
 
   free (cmdline);
+  return ret;
 }
 
 int
 main(int argc, char *argv[])
 {
   const char *toproot_bind_mounts[] = { "/home", "/root", "/tmp", NULL };
-  const char *ostree_bind_mounts[] = { "/var", NULL };
   const char *readonly_bind_mounts[] = { "/usr", NULL };
   const char *root_mountpoint = NULL;
-  char *ostree_osname = NULL;
   char *ostree_target = NULL;
-  char ostree_target_path[PATH_MAX];
   char *deploy_path = NULL;
   char srcpath[PATH_MAX];
   char destpath[PATH_MAX];
   struct stat stbuf;
-  size_t len;
   int i;
 
   if (argc < 2)
@@ -108,29 +101,20 @@ main(int argc, char *argv[])
     }
 
   root_mountpoint = argv[1];
-
-  parse_ostree_cmdline (&ostree_osname, &ostree_target);
-
-  if (!ostree_osname)
+  if (strcmp (root_mountpoint, "/sysroot") != 0)
     {
-      fprintf (stderr, "No OSTree target; expected ostree=OSNAME/TREENAME\n");
+      fprintf (stderr, "ostree-prepare-root: Expected /sysroot\n");
       exit (1);
     }
 
-  /* Work-around for a kernel bug: for some reason the kernel
-   * refuses switching root if any file systems are mounted
-   * MS_SHARED. Hence remount them MS_PRIVATE here as a
-   * work-around.
-   *
-   * https://bugzilla.redhat.com/show_bug.cgi?id=847418 */
-  if (mount (NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL) < 0)
+  ostree_target = parse_ostree_cmdline ();
+  if (!ostree_target)
     {
-      perrorv ("Failed to make \"/\" private mount: %m");
+      fprintf (stderr, "No OSTree target; expected ostree=/ostree/boot.N/...\n");
       exit (1);
     }
 
-  snprintf (destpath, sizeof(destpath), "%s/ostree/deploy/%s/%s",
-           root_mountpoint, ostree_osname, ostree_target);
+  snprintf (destpath, sizeof(destpath), "%s/%s", root_mountpoint, ostree_target);
   fprintf (stderr, "Examining %s\n", destpath);
   if (lstat (destpath, &stbuf) < 0)
     {
@@ -142,18 +126,26 @@ main(int argc, char *argv[])
       fprintf (stderr, "OSTree target is not a symbolic link: %s\n", destpath);
       exit (1);
     }
-  if (readlink (destpath, ostree_target_path, PATH_MAX) < 0)
+  deploy_path = realpath (destpath, NULL);
+  if (deploy_path == NULL)
     {
-      perrorv ("readlink(%s) failed: ", destpath);
+      perrorv ("realpath(%s) failed: ", destpath);
       exit (1);
     }
-  len = strlen (ostree_target_path);
-  if (ostree_target_path[len-1] == '/')
-    ostree_target_path[len-1] = '\0';
-  fprintf (stderr, "Resolved OSTree target to: %s\n", ostree_target_path);
-  (void) asprintf (&deploy_path, "%s/ostree/deploy/%s/%s", root_mountpoint,
-                  ostree_osname, ostree_target_path);
+  fprintf (stderr, "Resolved OSTree target to: %s\n", deploy_path);
   
+  /* Work-around for a kernel bug: for some reason the kernel
+   * refuses switching root if any file systems are mounted
+   * MS_SHARED. Hence remount them MS_PRIVATE here as a
+   * work-around.
+   *
+   * https://bugzilla.redhat.com/show_bug.cgi?id=847418 */
+  if (mount (NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL) < 0)
+    {
+      perrorv ("Failed to make \"/\" private mount: %m");
+      exit (1);
+    }
+
   /* Make deploy_path a bind mount, so we can move it later */
   if (mount (deploy_path, deploy_path, NULL, MS_BIND, NULL) < 0)
     {
@@ -168,14 +160,6 @@ main(int argc, char *argv[])
       exit (1);
     }
 
-  snprintf (srcpath, sizeof(srcpath), "%s-etc", deploy_path);
-  snprintf (destpath, sizeof(destpath), "%s/etc", deploy_path);
-  if (mount (srcpath, destpath, NULL, MS_BIND, NULL) < 0)
-    {
-      perrorv ("Failed to bind mount '%s' to '%s'", srcpath, destpath);
-      exit (1);
-    }
-
   for (i = 0; toproot_bind_mounts[i] != NULL; i++)
     {
       snprintf (srcpath, sizeof(srcpath), "%s%s", root_mountpoint, toproot_bind_mounts[i]);
@@ -193,16 +177,12 @@ main(int argc, char *argv[])
        }
     }
 
-  for (i = 0; ostree_bind_mounts[i] != NULL; i++)
+  snprintf (srcpath, sizeof(srcpath), "%s/../../var", deploy_path);
+  snprintf (destpath, sizeof(destpath), "%s/var", deploy_path);
+  if (mount (srcpath, destpath, NULL, MS_MGC_VAL|MS_BIND, NULL) < 0)
     {
-      snprintf (srcpath, sizeof(srcpath), "%s/ostree/deploy/%s%s", root_mountpoint,
-               ostree_osname, ostree_bind_mounts[i]);
-      snprintf (destpath, sizeof(destpath), "%s%s", deploy_path, ostree_bind_mounts[i]);
-      if (mount (srcpath, destpath, NULL, MS_MGC_VAL|MS_BIND, NULL) < 0)
-       {
-         perrorv ("failed to bind mount (class:bind) %s to %s", srcpath, destpath);
-         exit (1);
-       }
+      perrorv ("failed to bind mount %s to %s", srcpath, destpath);
+      exit (1);
     }
 
   for (i = 0; readonly_bind_mounts[i] != NULL; i++)
diff --git a/tests/libtest.sh b/tests/libtest.sh
index e4518f1..c3ad9a4 100644
--- a/tests/libtest.sh
+++ b/tests/libtest.sh
@@ -34,16 +34,30 @@ assert_streq () {
     test "$1" = "$2" || (echo 1>&2 "$1 != $2"; exit 1)
 }
 
+assert_not_streq () {
+    (! test "$1" = "$2") || (echo 1>&2 "$1 == $2"; exit 1)
+}
+
 assert_has_file () {
     test -f "$1" || (echo 1>&2 "Couldn't find '$1'"; exit 1)
 }
 
+assert_has_dir () {
+    test -d "$1" || (echo 1>&2 "Couldn't find '$1'"; exit 1)
+}
+
 assert_not_has_file () {
     if test -f "$1"; then
        echo 1>&2 "File '$1' exists"; exit 1
     fi
 }
 
+assert_not_has_dir () {
+    if test -d "$1"; then
+       echo 1>&2 "Directory '$1' exists"; exit 1
+    fi
+}
+
 assert_file_has_content () {
     if ! grep -q -e "$2" "$1"; then
        echo 1>&2 "File '$1' doesn't match regexp '$2'"; exit 1
@@ -159,3 +173,86 @@ EOF
 
     export OSTREE="ostree --repo=repo"
 }
+
+setup_os_repository () {
+    mode=$1
+    shift
+
+    oldpwd=`pwd`
+
+    cd ${test_tmpdir}
+    mkdir testos-repo
+    if test -n "$mode"; then
+       ostree --repo=testos-repo init --mode=${mode}
+    else
+       ostree --repo=testos-repo init
+    fi
+
+    cd ${test_tmpdir}
+    mkdir osdata
+    cd osdata
+    mkdir -p boot usr/bin usr/lib/modules/3.6.0 usr/share usr/etc
+    echo "a kernel" > boot/vmlinuz-3.6.0
+    echo "an initramfs" > boot/initramfs-3.6.0
+    echo "a kernel module" > usr/lib/modules/3.6.0/foofs.ko
+    bootcsum=$(cat boot/vmlinuz-3-6.0 boot/initramfs-3.6.0 usr/lib/modules/3.6.0/foofs.ko | sha256sum | cut 
-f 1 -d ' ')
+    export bootcsum
+    mv boot/vmlinuz-3.6.0 boot/vmlinuz-3.6.0-${bootcsum}
+    mv boot/initramfs-3.6.0 boot/initramfs-3.6.0-${bootcsum}
+    
+    echo "an executable" > usr/bin/sh
+    echo "some shared data" > usr/share/langs.txt
+    echo "a library" > usr/lib/libfoo.so.0
+    ln -s usr/bin bin
+cat > usr/etc/os-release <<EOF
+NAME=TestOS
+VERSION=42
+ID=testos
+VERSION_ID=42
+PRETTY_NAME="TestOS 42"
+EOF
+    echo "a config file" > usr/etc/aconfigfile
+    mkdir -p usr/etc/NetworkManager
+    echo "a default daemon file" > usr/etc/NetworkManager/nm.conf
+
+    ostree --repo=${test_tmpdir}/testos-repo commit -b testos/buildmaster/x86_64-runtime -s "Build"
+    
+    echo "a new executable" > usr/bin/sh
+    ostree --repo=${test_tmpdir}/testos-repo commit -b testos/buildmaster/x86_64-runtime -s "Build"
+
+    ostree --repo=${test_tmpdir}/testos-repo fsck -q
+
+    cd ${test_tmpdir}
+    mkdir sysroot
+    ostree admin --sysroot=sysroot init-fs sysroot
+    ostree admin --sysroot=sysroot os-init testos
+
+    # Stub syslinux configuration
+    mkdir -p sysroot/boot/loader.0
+    ln -s loader.0 sysroot/boot/loader
+    touch sysroot/boot/loader/syslinux.cfg
+    # And a compatibility symlink
+    mkdir -p sysroot/boot/syslinux
+    ln -s ../loader/syslinux.cfg sysroot/boot/syslinux/syslinux.cfg
+}
+
+os_repository_new_commit ()
+{
+    cd ${test_tmpdir}/osdata
+    rm boot/*
+    echo "new: a kernel" > boot/vmlinuz-3.6.0
+    echo "new: an initramfs" > boot/initramfs-3.6.0
+    echo "new: a kernel module" > usr/lib/modules/3.6.0/foofs.ko
+    echo "new: another kernel module" > usr/lib/modules/3.6.0/othermod.ko
+    bootcsum=$(cat boot/vmlinuz-3.6.0 boot/initramfs-3.6.0 usr/lib/modules/3.6.0/foofs.ko 
usr/lib/modules/3.6.0/othermod.ko | sha256sum | cut -f 1 -d ' ')
+    export bootcsum
+    mv boot/vmlinuz-3.6.0 boot/vmlinuz-3.6.0-${bootcsum}
+    mv boot/initramfs-3.6.0 boot/initramfs-3.6.0-${bootcsum}
+
+    echo "a new default config file" > usr/etc/a-new-default-config-file
+    mkdir -p usr/etc/new-default-dir
+    echo "a new default dir and file" > usr/etc/new-default-dir/moo
+
+    ostree --repo=${test_tmpdir}/testos-repo commit -b testos/buildmaster/x86_64-runtime -s "Build"
+    cd ${test_tmpdir}
+}
diff --git a/tests/t0015-admin-deploy.sh b/tests/t0015-admin-deploy.sh
new file mode 100755
index 0000000..a5527bf
--- /dev/null
+++ b/tests/t0015-admin-deploy.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+#
+# Copyright (C) 2011 Colin Walters <walters verbum org>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -e
+
+. $(dirname $0)/libtest.sh
+
+echo "1..1"
+
+setup_os_repository "archive-z2"
+
+echo "ok setup"
+
+echo "1..7"
+
+ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmaster/x86_64-runtime
+rev=$(ostree --repo=sysroot/ostree/repo rev-parse testos/buildmaster/x86_64-runtime)
+export rev
+# This initial deployment gets kicked off with some kernel arguments 
+ostree admin --sysroot=sysroot deploy --karg=root=LABEL=MOO --karg=quiet --os=testos 
testos:testos/buildmaster/x86_64-runtime
+
+echo "ok deploy command"
+
+assert_not_has_dir sysroot/boot/loader.0
+assert_has_dir sysroot/boot/loader.1
+assert_has_dir sysroot/ostree/boot.1.1
+assert_has_file sysroot/boot/loader/entries/ostree-testos-${rev}-0.conf
+assert_file_has_content sysroot/boot/loader/entries/ostree-testos-${rev}-0.conf 'options.* root=LABEL=MOO'
+assert_file_has_content sysroot/boot/loader/entries/ostree-testos-${rev}-0.conf 'options.* quiet'
+assert_file_has_content sysroot/boot/ostree/testos-${bootcsum}/vmlinuz-3.6.0 'a kernel'
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.0/etc/os-release 'NAME=TestOS'
+assert_file_has_content sysroot/ostree/boot.1/testos/${bootcsum}/0/etc/os-release 'NAME=TestOS'
+
+echo "ok layout"
+
+ostree admin --sysroot=sysroot deploy --os=testos testos/buildmaster/x86_64-runtime
+# Keep the same bootversion
+assert_not_has_dir sysroot/boot/loader.0
+assert_has_dir sysroot/boot/loader.1
+# But swap subbootversion
+assert_not_has_dir sysroot/ostree/boot.1.1
+assert_has_dir sysroot/ostree/boot.1.0
+# Ensure we propagated kernel arguments from previous deployment
+assert_file_has_content sysroot/boot/loader/entries/ostree-testos-${rev}-0.conf 'options.* root=LABEL=MOO'
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.1/etc/os-release 'NAME=TestOS'
+assert_file_has_content sysroot/ostree/boot.1/testos/${bootcsum}/0/etc/os-release 'NAME=TestOS'
+
+echo "ok second deploy (swap)"
+
+ostree admin --sysroot=sysroot deploy --os=otheros testos/buildmaster/x86_64-runtime
+assert_has_dir sysroot/boot/loader.0
+assert_not_has_dir sysroot/boot/loader.1
+assert_has_file sysroot/boot/loader/entries/ostree-testos-${rev}-0.conf
+assert_has_file sysroot/boot/loader/entries/ostree-otheros-${rev}-0.conf
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.1/etc/os-release 'NAME=TestOS'
+assert_file_has_content sysroot/ostree/deploy/otheros/deploy/${rev}.0/etc/os-release 'NAME=TestOS'
+
+echo "ok independent deploy"
+
+ostree admin --sysroot=sysroot deploy --retain --os=testos testos/buildmaster/x86_64-runtime
+assert_not_has_dir sysroot/boot/loader.0
+assert_has_dir sysroot/boot/loader.1
+assert_has_file sysroot/boot/loader/entries/ostree-testos-${rev}-0.conf
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.1/etc/os-release 'NAME=TestOS'
+assert_has_file sysroot/boot/loader/entries/ostree-testos-${rev}-1.conf
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.2/etc/os-release 'NAME=TestOS'
+
+echo "ok second deploy (retain)"
+
+echo "a new local config file" > sysroot/ostree/deploy/testos/deploy/${rev}.2/etc/a-new-config-file
+rm sysroot/ostree/deploy/testos/deploy/${rev}.2/etc/aconfigfile
+ln -s /ENOENT sysroot/ostree/deploy/testos/deploy/${rev}.2/etc/a-new-broken-symlink
+ostree admin --sysroot=sysroot deploy --retain --os=testos testos/buildmaster/x86_64-runtime
+linktarget=$(readlink sysroot/ostree/deploy/testos/deploy/${rev}.3/etc/a-new-broken-symlink)
+test "${linktarget}" = /ENOENT
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.2/etc/os-release 'NAME=TestOS'
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.3/etc/os-release 'NAME=TestOS'
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${rev}.3/etc/a-new-config-file 'a new local 
config file'
+assert_not_has_file sysroot/ostree/deploy/testos/deploy/${rev}.3/etc/aconfigfile
+
+echo "ok deploy with modified /etc"
+
+os_repository_new_commit
+ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmaster/x86_64-runtime
+newrev=$(ostree --repo=sysroot/ostree/repo rev-parse testos/buildmaster/x86_64-runtime)
+export newrev
+assert_not_streq ${rev} ${newrev}
+
+ostree admin --sysroot=sysroot deploy --os=testos testos/buildmaster/x86_64-runtime
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${newrev}.0/etc/os-release 'NAME=TestOS'
+# New files in /usr/etc
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${newrev}.0/etc/a-new-default-config-file "a new 
default config file"
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${newrev}.0/etc/new-default-dir/moo "a new 
default dir and file"
+# And persist /etc changes from before
+assert_not_has_file sysroot/ostree/deploy/testos/deploy/${rev}.3/etc/aconfigfile
+
+echo "ok upgrade bare"
+
+os_repository_new_commit
+ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmaster/x86_64-runtime
+rev=${newrev}
+newrev=$(ostree --repo=sysroot/ostree/repo rev-parse testos/buildmaster/x86_64-runtime)
+assert_not_streq ${rev} ${newrev}
+
+ostree admin --sysroot=sysroot upgrade --os=testos
+assert_file_has_content sysroot/ostree/deploy/testos/deploy/${newrev}.0/etc/os-release 'NAME=TestOS'
+
+echo "ok upgrade"


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]