[ostree] admin: Add an `unlock` command, and libostree API



commit 09238da065e8a442dcffdd1f8d906f4330a6e032
Author: Colin Walters <walters verbum org>
Date:   Fri Mar 18 15:32:58 2016 -0400

    admin: Add an `unlock` command, and libostree API
    
    I'm trying to improve the developer experience on OSTree-managed
    systems, and I had an epiphany the other day - there's no reason we
    have to be absolutely against mutating the current rootfs live.  The
    key should be making it easy to rollback/reset to a known good state.
    
    I see this command as useful for two related but distinct workflows:
    
     - `ostree admin unlock` will assume you're doing "development".  The
       semantics hare are that we mount an overlayfs on `/usr`, but the
       overlay data is in `/var/tmp`, and is thus discarded on reboot.
     - `ostree admin unlock --hotfix` first clones your current deployment,
       then creates an overlayfs over `/usr` persistent
       to this deployment.  Persistent in that now the initramfs switchroot
       tool knows how to mount it as well.  In this model, if you want
       to discard the hotfix, at the moment you roll back/reboot into
       the clone.
    
    Note originally, I tried using `rofiles-fuse` over `/usr` for this,
    but then everything immediately explodes because the default (at least
    CentOS 7) SELinux policy denies tons of things (including `sshd_t`
    access to `fusefs_t`).  Sigh.
    
    So the switch to `overlayfs` came after experimentation.  It still
    seems to have some issues...specifically `unix_chkpwd` is broken,
    possibly because it's setuid?  Basically I can't ssh in anymore.
    
    But I *can* `rpm -Uvh strace.rpm` which is handy.
    
    NOTE: I haven't tested the hotfix path fully yet, specifically
    the initramfs bits.

 Makefile-man.am                           |   12 +-
 Makefile-ostree.am                        |    1 +
 buildutil/tap-test                        |    2 +-
 man/ostree-admin-unlock.xml               |   88 ++++++++
 src/libostree/libostree.sym               |    3 +
 src/libostree/ostree-deployment-private.h |   15 ++
 src/libostree/ostree-deployment.c         |   36 ++--
 src/libostree/ostree-deployment.h         |   12 +
 src/libostree/ostree-sysroot-private.h    |    3 +
 src/libostree/ostree-sysroot.c            |  328 ++++++++++++++++++++++++++++-
 src/libostree/ostree-sysroot.h            |   10 +-
 src/ostree/ot-admin-builtin-status.c      |   16 ++-
 src/ostree/ot-admin-builtin-unlock.c      |  104 +++++++++
 src/ostree/ot-admin-builtin-upgrade.c     |    3 +
 src/ostree/ot-admin-builtins.h            |    1 +
 src/ostree/ot-builtin-admin.c             |    1 +
 src/switchroot/ostree-mount-util.c        |   19 ++
 src/switchroot/ostree-mount-util.h        |    2 +
 src/switchroot/ostree-prepare-root.c      |   66 +++++-
 src/switchroot/ostree-remount.c           |   14 --
 20 files changed, 690 insertions(+), 46 deletions(-)
---
diff --git a/Makefile-man.am b/Makefile-man.am
index 615bf0f..ce7e93c 100644
--- a/Makefile-man.am
+++ b/Makefile-man.am
@@ -19,7 +19,17 @@
 
 if ENABLE_MAN
 
-man1_files = ostree.1 ostree-admin-cleanup.1 ostree-admin-config-diff.1 ostree-admin-deploy.1 
ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1 ostree-admin-status.1 
ostree-admin-set-origin.1 ostree-admin-switch.1 ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin.1 
ostree-cat.1 ostree-checkout.1 ostree-checksum.1 ostree-commit.1 ostree-export.1 ostree-gpg-sign.1 
ostree-config.1 ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1 ostree-prune.1 
ostree-pull-local.1 ostree-pull.1 ostree-refs.1 ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 
ostree-show.1 ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1
+man1_files = ostree.1 ostree-admin-cleanup.1                           \
+ostree-admin-config-diff.1 ostree-admin-deploy.1                       \
+ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1  \
+ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1  \
+ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1   \
+ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1                \
+ostree-commit.1 ostree-export.1 ostree-gpg-sign.1 ostree-config.1      \
+ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1     \
+ostree-prune.1 ostree-pull-local.1 ostree-pull.1 ostree-refs.1         \
+ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 ostree-show.1                \
+ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1
 
 if BUILDOPT_FUSE
 man1_files += rofiles-fuse.1
diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index ff7e372..0ef5c4e 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -66,6 +66,7 @@ ostree_SOURCES += \
        src/ostree/ot-admin-builtin-status.c \
        src/ostree/ot-admin-builtin-switch.c \
        src/ostree/ot-admin-builtin-upgrade.c \
+       src/ostree/ot-admin-builtin-unlock.c \
        src/ostree/ot-admin-builtins.h \
        src/ostree/ot-admin-instutil-builtin-selinux-ensure-labeled.c \
        src/ostree/ot-admin-instutil-builtin-set-kargs.c \
diff --git a/buildutil/tap-test b/buildutil/tap-test
index 38080bb..e791454 100755
--- a/buildutil/tap-test
+++ b/buildutil/tap-test
@@ -12,7 +12,7 @@ tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX)
 touch ${tempdir}/.testtmp
 function cleanup () {
     if test -n "${TEST_SKIP_CLEANUP:-}"; then
-       echo "Skipping cleanup of ${test_tmpdir}"
+       echo "Skipping cleanup of ${tempdir}"
     else if test -f ${tempdir}/.test; then
        rm "${tempdir}" -rf
     fi
diff --git a/man/ostree-admin-unlock.xml b/man/ostree-admin-unlock.xml
new file mode 100644
index 0000000..ca02bbd
--- /dev/null
+++ b/man/ostree-admin-unlock.xml
@@ -0,0 +1,88 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+    "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd";>
+
+<!--
+Copyright 2016 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.
+-->
+
+<refentry id="ostree">
+
+    <refentryinfo>
+        <title>ostree admin unlock</title>
+        <productname>OSTree</productname>
+
+        <authorgroup>
+            <author>
+                <contrib>Developer</contrib>
+                <firstname>Colin</firstname>
+                <surname>Walters</surname>
+                <email>walters verbum org</email>
+            </author>
+        </authorgroup>
+    </refentryinfo>
+
+    <refmeta>
+        <refentrytitle>ostree admin unlock</refentrytitle>
+        <manvolnum>1</manvolnum>
+    </refmeta>
+
+    <refnamediv>
+        <refname>ostree-admin-unlock</refname>
+        <refpurpose>Prepare the current deployment for hotfix or development</refpurpose>
+    </refnamediv>
+
+    <refsynopsisdiv>
+            <cmdsynopsis>
+                <command>ostree admin unlock</command> <arg choice="opt" rep="repeat">OPTIONS</arg>
+            </cmdsynopsis>
+    </refsynopsisdiv>
+
+    <refsect1>
+        <title>Description</title>
+
+        <para>
+         Remove the read-only bind mount on <literal>/usr</literal>
+         and replace it with a writable overlay filesystem.  This
+         default invocation of "unlock" is intended for
+         development/testing purposes.  All changes in the overlay
+         are lost on reboot.  However, this command also supports
+         "hotfixes", see below.
+        </para>
+    </refsect1>
+
+    <refsect1>
+        <title>Options</title>
+
+        <variablelist>
+            <varlistentry>
+                <term><option>--hotfix</option></term>
+
+                <listitem><para>If this option is provided, the
+                current deployment will be cloned as a rollback
+                target.  This option is intended for things like
+                emergency security updates to userspace components
+                such as <literal>sshd</literal>.  The semantics here
+               differ from the default "development" unlock mode
+               in that reboots will retain any changes (which is what
+               you likely want for security hotfixes).
+                </para></listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect1>
+</refentry>
diff --git a/src/libostree/libostree.sym b/src/libostree/libostree.sym
index a85f0db..c78986e 100644
--- a/src/libostree/libostree.sym
+++ b/src/libostree/libostree.sym
@@ -318,4 +318,7 @@ global:
         ostree_repo_list_refs_ext;
         ostree_sysroot_init_osname;
         ostree_sysroot_load_if_changed;
+        ostree_sysroot_deployment_unlock;
+        ostree_deployment_get_unlocked;
+        ostree_deployment_unlocked_state_to_string;
 } LIBOSTREE_2016.3;
diff --git a/src/libostree/ostree-deployment-private.h b/src/libostree/ostree-deployment-private.h
index b5ebb95..856a398 100644
--- a/src/libostree/ostree-deployment-private.h
+++ b/src/libostree/ostree-deployment-private.h
@@ -24,6 +24,21 @@
 
 G_BEGIN_DECLS
 
+struct _OstreeDeployment
+{
+  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} */
+  OstreeBootconfigParser *bootconfig; /* Bootloader configuration */
+  GKeyFile *origin; /* How to construct an upgraded version of this tree */
+  OstreeDeploymentUnlockedState unlocked;  /* The unlocked state */
+};
+
 void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum);
 
 G_END_DECLS
diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c
index 3a80474..7b93e6c 100644
--- a/src/libostree/ostree-deployment.c
+++ b/src/libostree/ostree-deployment.c
@@ -23,20 +23,6 @@
 #include "ostree-deployment-private.h"
 #include "libglnx.h"
 
-struct _OstreeDeployment
-{
-  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} */
-  OstreeBootconfigParser *bootconfig; /* Bootloader configuration */
-  GKeyFile *origin; /* How to construct an upgraded version of this tree */
-};
-
 typedef GObjectClass OstreeDeploymentClass;
 
 G_DEFINE_TYPE (OstreeDeployment, ostree_deployment, G_TYPE_OBJECT)
@@ -258,6 +244,7 @@ ostree_deployment_new (int    index,
   self->deployserial = deployserial;
   self->bootcsum = g_strdup (bootcsum);
   self->bootserial = bootserial;
+  self->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE;
   return self;
 }
 
@@ -279,3 +266,24 @@ ostree_deployment_get_origin_relpath (OstreeDeployment *self)
                           ostree_deployment_get_csum (self),
                           ostree_deployment_get_deployserial (self));
 }
+
+const char *
+ostree_deployment_unlocked_state_to_string (OstreeDeploymentUnlockedState state)
+{
+  switch (state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      return "none";
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      return "hotfix";
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      return "development";
+    }
+  g_assert_not_reached ();
+}
+
+OstreeDeploymentUnlockedState
+ostree_deployment_get_unlocked (OstreeDeployment *self)
+{
+  return self->unlocked;
+}
diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h
index a474b35..bde0cf3 100644
--- a/src/libostree/ostree-deployment.h
+++ b/src/libostree/ostree-deployment.h
@@ -78,4 +78,16 @@ OstreeDeployment *ostree_deployment_clone (OstreeDeployment *self);
 _OSTREE_PUBLIC
 char *ostree_deployment_get_origin_relpath (OstreeDeployment *self);
 
+typedef enum {
+  OSTREE_DEPLOYMENT_UNLOCKED_NONE,
+  OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT,
+  OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX
+} OstreeDeploymentUnlockedState;
+
+_OSTREE_PUBLIC
+const char *ostree_deployment_unlocked_state_to_string (OstreeDeploymentUnlockedState state);
+
+_OSTREE_PUBLIC
+OstreeDeploymentUnlockedState ostree_deployment_get_unlocked (OstreeDeployment *self);
+
 G_END_DECLS
diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h
index 229893d..d210a36 100644
--- a/src/libostree/ostree-sysroot-private.h
+++ b/src/libostree/ostree-sysroot-private.h
@@ -58,6 +58,9 @@ struct OstreeSysroot {
 };
 
 #define OSTREE_SYSROOT_LOCKFILE "ostree/lock"
+/* We keep some transient state in /run */
+#define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/"
+#define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development"
 
 gboolean
 _ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self,
diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c
index aa08838..6ee3dff 100644
--- a/src/libostree/ostree-sysroot.c
+++ b/src/libostree/ostree-sysroot.c
@@ -24,6 +24,7 @@
 
 #include "ostree-core-private.h"
 #include "ostree-sysroot-private.h"
+#include "ostree-deployment-private.h"
 #include "ostree-bootloader-uboot.h"
 #include "ostree-bootloader-syslinux.h"
 #include "ostree-bootloader-grub2.h"
@@ -646,6 +647,16 @@ parse_bootlink (const char    *bootlink,
   return ret;
 }
 
+static char *
+get_unlocked_development_path (OstreeDeployment *deployment)
+{
+  return g_strdup_printf ("%s%s.%d/%s",
+                          _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR,
+                          ostree_deployment_get_csum (deployment),
+                          ostree_deployment_get_deployserial (deployment),
+                          _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
+}
+
 static gboolean
 parse_deployment (OstreeSysroot       *self,
                   const char          *boot_link,
@@ -667,6 +678,8 @@ parse_deployment (OstreeSysroot       *self,
   g_autofree char *treebootserial_target = NULL;
   g_autofree char *deploy_dir = NULL;
   GKeyFile *origin = NULL;
+  g_autofree char *unlocked_development_path = NULL;
+  struct stat stbuf;
 
   if (!ensure_sysroot_fd (self, error))
     goto out;
@@ -704,6 +717,24 @@ parse_deployment (OstreeSysroot       *self,
   if (origin)
     ostree_deployment_set_origin (ret_deployment, origin);
 
+  ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE;
+  unlocked_development_path = get_unlocked_development_path (ret_deployment);
+  if (lstat (unlocked_development_path, &stbuf) == 0)
+    ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
+  else
+    {
+      g_autofree char *existing_unlocked_state =
+        g_key_file_get_string (origin, "origin", "unlocked", NULL);
+
+      if (g_strcmp0 (existing_unlocked_state, "hotfix") == 0)
+        {
+          ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX;
+        }
+      /* TODO: warn on unknown unlock types? */
+    }
+
+  g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked);
+
   ret = TRUE;
   if (out_deployment)
     *out_deployment = g_steal_pointer (&ret_deployment);
@@ -1481,6 +1512,10 @@ ostree_sysroot_init_osname (OstreeSysroot       *self,
  *
  * If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN is
  * specified, then all current deployments will be kept.
+ *
+ * If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT is
+ * specified, then instead of prepending, the new deployment will be
+ * added right after the booted or merge deployment, instead of first.
  */
 gboolean
 ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
@@ -1497,6 +1532,8 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
   g_autoptr(GPtrArray) deployments = NULL;
   g_autoptr(GPtrArray) new_deployments = g_ptr_array_new_with_free_func (g_object_unref);
   gboolean retain = (flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN) > 0;
+  const gboolean make_default = !((flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT) > 0);
+  gboolean added_new = FALSE;
 
   deployments = ostree_sysroot_get_deployments (sysroot);
   booted_deployment = ostree_sysroot_get_booted_deployment (sysroot);
@@ -1504,23 +1541,44 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
   if (osname == NULL && booted_deployment)
     osname = ostree_deployment_get_osname (booted_deployment);
 
-  g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+  if (make_default)
+    {
+      g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+      added_new = TRUE;
+    }
 
   for (i = 0; i < deployments->len; i++)
     {
       OstreeDeployment *deployment = deployments->pdata[i];
+      const gboolean is_merge_or_booted = 
+        ostree_deployment_equal (deployment, booted_deployment) ||
+        ostree_deployment_equal (deployment, merge_deployment);
       
       /* Keep deployments with different osnames, as well as the
        * booted and merge deployments
        */
       if (retain ||
-          (osname != NULL &&
-           strcmp (ostree_deployment_get_osname (deployment), osname) != 0) ||
-          ostree_deployment_equal (deployment, booted_deployment) ||
-          ostree_deployment_equal (deployment, merge_deployment))
+          (osname != NULL && strcmp (ostree_deployment_get_osname (deployment), osname) != 0) ||
+          is_merge_or_booted)
         {
           g_ptr_array_add (new_deployments, g_object_ref (deployment));
         }
+
+      if (!added_new)
+        {
+          g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+          added_new = TRUE;
+        }
+    }
+
+  /* In this non-default case , an improvement in the future would be
+   * to put the new deployment right after the current default in the
+   * order.
+   */
+  if (!added_new)
+    {
+      g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+      added_new = TRUE;
     }
 
   if (!ostree_sysroot_write_deployments (sysroot, new_deployments, cancellable, error))
@@ -1533,3 +1591,263 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
  out:
   return ret;
 }
+
+static gboolean
+clone_deployment (OstreeSysroot  *sysroot,
+                  OstreeDeployment *target_deployment,
+                  OstreeDeployment *merge_deployment,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+  gboolean ret = FALSE;
+  __attribute__((cleanup(_ostree_kernel_args_cleanup))) OstreeKernelArgs *kargs = NULL;
+  glnx_unref_object OstreeDeployment *new_deployment = NULL;
+
+  /* Ensure we have a clean slate */
+  if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error))
+    {
+      g_prefix_error (error, "Performing initial cleanup: ");
+      goto out;
+    }
+
+  kargs = _ostree_kernel_args_new ();
+
+  { OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (merge_deployment);
+    g_auto(GStrv) previous_args = g_strsplit (ostree_bootconfig_parser_get (bootconfig, "options"), " ", -1);
+    
+    _ostree_kernel_args_append_argv (kargs, previous_args);
+  }
+
+  {
+    g_auto(GStrv) kargs_strv = _ostree_kernel_args_to_strv (kargs);
+
+    if (!ostree_sysroot_deploy_tree (sysroot,
+                                     ostree_deployment_get_osname (target_deployment),
+                                     ostree_deployment_get_csum (target_deployment),
+                                     ostree_deployment_get_origin (target_deployment),
+                                     merge_deployment,
+                                     kargs_strv,
+                                     &new_deployment,
+                                     cancellable, error))
+      goto out;
+  }
+
+  /* Hotfixes push the deployment as rollback target, so it shouldn't
+   * be the default.
+   */
+  if (!ostree_sysroot_simple_write_deployment (sysroot, ostree_deployment_get_osname (target_deployment),
+                                               new_deployment, merge_deployment,
+                                               OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT,
+                                               cancellable, error))
+    goto out;
+  
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+/**
+ * ostree_sysroot_deployment_unlock:
+ * @self: Sysroot
+ * @deployment: Deployment
+ * @unlocked_state: Transition to this unlocked state
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Configure the target deployment @deployment such that it
+ * is writable.  There are multiple modes, essentially differing
+ * in whether or not any changes persist across reboot.
+ *
+ * The `OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX` state is persistent
+ * across reboots.
+ */
+gboolean
+ostree_sysroot_deployment_unlock (OstreeSysroot     *self,
+                                  OstreeDeployment  *deployment,
+                                  OstreeDeploymentUnlockedState unlocked_state,
+                                  GCancellable      *cancellable,
+                                  GError           **error)
+{
+  gboolean ret = FALSE;
+  OstreeDeploymentUnlockedState current_unlocked =
+    ostree_deployment_get_unlocked (deployment); 
+  glnx_unref_object OstreeDeployment *deployment_clone =
+    ostree_deployment_clone (deployment);
+  glnx_unref_object OstreeDeployment *merge_deployment = NULL;
+  GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone);
+  const char hotfix_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work";
+  const char *ovl_options = NULL;
+  g_autofree char *deployment_path = NULL;
+  glnx_fd_close int deployment_dfd = -1;
+  pid_t mount_child;
+
+  /* This function cannot re-lock */
+  g_return_val_if_fail (unlocked_state != OSTREE_DEPLOYMENT_UNLOCKED_NONE, FALSE);
+
+  if (current_unlocked != OSTREE_DEPLOYMENT_UNLOCKED_NONE)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Deployment is already in unlocked state: %s",
+                   ostree_deployment_unlocked_state_to_string (current_unlocked));
+      goto out;
+    }
+
+  merge_deployment = ostree_sysroot_get_merge_deployment (self, ostree_deployment_get_osname (deployment));
+  if (!merge_deployment)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "No previous deployment to duplicate");
+      goto out;
+    }
+
+  /* For hotfixes, we push a rollback target */
+  if (unlocked_state == OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX)
+    {
+      if (!clone_deployment (self, deployment, merge_deployment, cancellable, error))
+        goto out;
+    }
+
+  /* Crack it open */
+  if (!ostree_sysroot_deployment_set_mutable (self, deployment, TRUE,
+                                              cancellable, error))
+    goto out;
+
+  deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment);
+
+  if (!glnx_opendirat (self->sysroot_fd, deployment_path, TRUE, &deployment_dfd, error))
+    goto out;
+
+  switch (unlocked_state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      g_assert_not_reached ();
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      {
+        /* Create the overlayfs directories in the deployment root
+         * directly for hotfixes.  The ostree-prepare-root.c helper
+         * is also set up to detect and mount these.
+         */
+        if (!glnx_shutil_mkdir_p_at (deployment_dfd, ".usr-ovl-upper", 0755, cancellable, error))
+          goto out;
+        if (!glnx_shutil_mkdir_p_at (deployment_dfd, ".usr-ovl-work", 0755, cancellable, error))
+          goto out;
+        ovl_options = hotfix_ovl_options;
+      }
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      {
+        /* We're just doing transient development/hacking?  Okay,
+         * stick the overlayfs bits in /var/tmp.
+         */
+        char *development_ovldir = strdupa ("/var/tmp/ostree-unlock-ovl.XXXXXX");
+        const char *development_ovl_upper;
+        const char *development_ovl_work;
+
+        if (!glnx_mkdtempat (AT_FDCWD, development_ovldir, 0700, error))
+          goto out;
+
+        development_ovl_upper = glnx_strjoina (development_ovldir, "/upper");
+        if (!glnx_shutil_mkdir_p_at (AT_FDCWD, development_ovl_upper, 0755, cancellable, error))
+          goto out;
+        development_ovl_work = glnx_strjoina (development_ovldir, "/work");
+        if (!glnx_shutil_mkdir_p_at (AT_FDCWD, development_ovl_work, 0755, cancellable, error))
+          goto out;
+        ovl_options = glnx_strjoina ("lowerdir=usr,upperdir=", development_ovl_upper,
+                                     ",workdir=", development_ovl_work);
+      }
+    }
+
+  g_assert (ovl_options != NULL);
+
+  /* Here we run `mount()` in a fork()ed child because we need to use
+   * `chdir()` in order to have the mount path options to overlayfs not
+   * look ugly.
+   *
+   * We can't `chdir()` inside a shared library since there may be
+   * threads, etc.
+   */
+  {
+    /* Make a copy of the fd that's *not* FD_CLOEXEC so that we pass
+     * it to the child.
+     */
+    glnx_fd_close int child_deployment_dfd = dup (deployment_dfd);
+
+    if (child_deployment_dfd < 0)
+      {
+        glnx_set_error_from_errno (error);
+        goto out;
+      }
+
+    mount_child = fork ();
+    if (mount_child < 0)
+      {
+        glnx_set_prefix_error_from_errno (error, "%s", "fork");
+        goto out;
+      }
+    else if (mount_child == 0)
+      {
+        /* Child process.  Do NOT use any GLib API here. */
+        if (fchdir (child_deployment_dfd) < 0)
+          exit (EXIT_FAILURE);
+        (void) close (child_deployment_dfd);
+        if (mount ("overlay", "/usr", "overlay", 0, ovl_options) < 0)
+          exit (EXIT_FAILURE);
+        exit (EXIT_SUCCESS);
+      }
+    else
+      {
+        /* Parent */
+        int estatus;
+
+        if (TEMP_FAILURE_RETRY (waitpid (mount_child, &estatus, 0)) < 0)
+          {
+            glnx_set_prefix_error_from_errno (error, "%s", "waitpid() on mount helper");
+            goto out;
+          }
+        if (!g_spawn_check_exit_status (estatus, error))
+          {
+            g_prefix_error (error, "overlayfs mount helper: "); 
+            goto out;
+          }
+      }
+  }
+
+  /* Now, write out the flag saying what we did */
+  switch (unlocked_state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      g_assert_not_reached ();
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      g_key_file_set_string (origin_clone, "origin", "unlocked",
+                             ostree_deployment_unlocked_state_to_string (unlocked_state));
+      if (!ostree_sysroot_write_origin_file (self, deployment, origin_clone,
+                                             cancellable, error))
+        goto out;
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      {
+        g_autofree char *devpath = get_unlocked_development_path (deployment);
+        g_autofree char *devpath_parent = dirname (g_strdup (devpath));
+
+        if (!glnx_shutil_mkdir_p_at (AT_FDCWD, devpath_parent, 0755, cancellable, error))
+          goto out;
+        
+        if (!g_file_set_contents (devpath, "", 0, error))
+          goto out;
+      }
+    }
+
+  /* For hotfixes we already pushed a rollback which will bump the
+   * mtime, but we need to bump it again so that clients get the state
+   * change for this deployment.  For development we need to do this
+   * regardless.
+   */
+  if (!_ostree_sysroot_bump_mtime (self, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h
index 077862a..1e60ddb 100644
--- a/src/libostree/ostree-sysroot.h
+++ b/src/libostree/ostree-sysroot.h
@@ -164,6 +164,13 @@ gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot     *self,
                                                 GError           **error);
 
 _OSTREE_PUBLIC
+gboolean ostree_sysroot_deployment_unlock (OstreeSysroot     *self,
+                                           OstreeDeployment  *deployment,
+                                           OstreeDeploymentUnlockedState unlocked_state,
+                                           GCancellable      *cancellable,
+                                           GError           **error);
+
+_OSTREE_PUBLIC
 OstreeDeployment *ostree_sysroot_get_merge_deployment (OstreeSysroot     *self,
                                                        const char        *osname);
 
@@ -174,7 +181,8 @@ GKeyFile *ostree_sysroot_origin_new_from_refspec (OstreeSysroot      *self,
 
 typedef enum {
   OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NONE = 0,
-  OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN = (1 << 0)
+  OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN = (1 << 0),
+  OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT = (1 << 1)
 } OstreeSysrootSimpleWriteDeploymentFlags;
 
 _OSTREE_PUBLIC
diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c
index cd275cc..df4d074 100644
--- a/src/ostree/ot-admin-builtin-status.c
+++ b/src/ostree/ot-admin-builtin-status.c
@@ -89,6 +89,9 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro
   glnx_unref_object OstreeRepo *repo = NULL;
   OstreeDeployment *booted_deployment = NULL;
   g_autoptr(GPtrArray) deployments = NULL;
+  const int is_tty = isatty (1);
+  const char *red_bold_prefix = is_tty ? "\x1b[31m\x1b[1m" : "";
+  const char *red_bold_suffix = is_tty ? "\x1b[22m\x1b[0m" : "";
   guint i;
 
   context = g_option_context_new ("List deployments");
@@ -118,12 +121,15 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro
           OstreeDeployment *deployment = deployments->pdata[i];
           GKeyFile *origin;
           const char *ref = ostree_deployment_get_csum (deployment);
+          OstreeDeploymentUnlockedState unlocked = ostree_deployment_get_unlocked (deployment);
           g_autofree char *version = version_of_commit (repo, ref);
           glnx_unref_object OstreeGpgVerifyResult *result = NULL;
           GString *output_buffer;
           guint jj, n_signatures;
           GError *local_error = NULL;
 
+          origin = ostree_deployment_get_origin (deployment);
+
           g_print ("%c %s %s.%d\n",
                    deployment == booted_deployment ? '*' : ' ',
                    ostree_deployment_get_osname (deployment),
@@ -131,7 +137,15 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro
                    ostree_deployment_get_deployserial (deployment));
           if (version)
             g_print ("    Version: %s\n", version);
-          origin = ostree_deployment_get_origin (deployment);
+          switch (unlocked)
+            {
+            case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+              break;
+            default:
+              g_print ("    %sUnlocked: %s%s\n", red_bold_prefix,
+                       ostree_deployment_unlocked_state_to_string (unlocked),
+                       red_bold_suffix);
+            }
           if (!origin)
             g_print ("    origin: none\n");
           else
diff --git a/src/ostree/ot-admin-builtin-unlock.c b/src/ostree/ot-admin-builtin-unlock.c
new file mode 100644
index 0000000..9d26532
--- /dev/null
+++ b/src/ostree/ot-admin-builtin-unlock.c
@@ -0,0 +1,104 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2016 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.
+ */
+
+#include "config.h"
+
+#include "ot-main.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "ostree.h"
+#include "otutil.h"
+
+#include "../libostree/ostree-kernel-args.h"
+
+#include <glib/gi18n.h>
+#include <err.h>
+
+static gboolean opt_hotfix;
+
+static GOptionEntry options[] = {
+  { "hotfix", 0, 0, G_OPTION_ARG_NONE, &opt_hotfix, "Keep the current deployment as default", NULL },
+  { NULL }
+};
+
+gboolean
+ot_admin_builtin_unlock (int argc, char **argv, GCancellable *cancellable, GError **error)
+{
+  gboolean ret = FALSE;
+  GOptionContext *context;
+  glnx_unref_object OstreeSysroot *sysroot = NULL;
+  glnx_unref_object OstreeRepo *repo = NULL;
+  g_autoptr(GPtrArray) new_deployments = NULL;
+  glnx_unref_object OstreeDeployment *merge_deployment = NULL;
+  OstreeDeployment *booted_deployment = NULL;
+  OstreeDeploymentUnlockedState target_state;
+
+  context = g_option_context_new ("Make the current deployment mutable (as a hotfix or development)");
+
+  if (!ostree_admin_option_context_parse (context, options, &argc, &argv,
+                                          OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER,
+                                          &sysroot, cancellable, error))
+    goto out;
+  
+  if (argc > 1)
+    {
+      ot_util_usage_error (context, "This command takes no extra arguments", error);
+      goto out;
+    }
+
+  if (!ostree_sysroot_load (sysroot, cancellable, error))
+    goto out;
+
+  booted_deployment = ostree_sysroot_get_booted_deployment (sysroot);
+  if (!booted_deployment)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Not currently booted into an OSTree system");
+      goto out;
+    }
+
+  target_state = opt_hotfix ? OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX : OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
+
+  if (!ostree_sysroot_deployment_unlock (sysroot, booted_deployment,
+                                         target_state, cancellable, error))
+    goto out;
+  
+  switch (target_state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      g_assert_not_reached ();
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      g_print ("Hotfix mode enabled.  A writable overlayfs is now mounted on /usr\n"
+               "for this booted deployment.  A non-hotfixed clone has been created\n"
+               "as the non-default rollback target.\n");
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      g_print ("Development mode enabled.  A writable overlayfs is now mounted on /usr.\n"
+               "All changes there will be discarded on reboot.\n");
+      break;
+    }
+
+  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 5d4796a..b3b531c 100644
--- a/src/ostree/ot-admin-builtin-upgrade.c
+++ b/src/ostree/ot-admin-builtin-upgrade.c
@@ -97,6 +97,9 @@ ot_admin_builtin_upgrade (int argc, char **argv, GCancellable *cancellable, GErr
                                                   "override-commit", NULL);
         }
 
+      /* Should we consider requiring --discard-hotfix here? */
+      origin_changed |= g_key_file_remove_key (origin, "origin", "unlocked", NULL);
+
       if (origin_changed)
         {
           /* XXX GCancellable parameter is not used. */
diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h
index 1a3c126..f942511 100644
--- a/src/ostree/ot-admin-builtins.h
+++ b/src/ostree/ot-admin-builtins.h
@@ -34,6 +34,7 @@ gboolean ot_admin_builtin_init_fs (int argc, char **argv, GCancellable *cancella
 gboolean ot_admin_builtin_undeploy (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_deploy (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_cleanup (int argc, char **argv, GCancellable *cancellable, GError **error);
+gboolean ot_admin_builtin_unlock (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_set_origin (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_diff (int argc, char **argv, GCancellable *cancellable, GError **error);
diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c
index a4cb0dd..8b86617 100644
--- a/src/ostree/ot-builtin-admin.c
+++ b/src/ostree/ot-builtin-admin.c
@@ -47,6 +47,7 @@ static OstreeAdminCommand admin_subcommands[] = {
   { "status", ot_admin_builtin_status },
   { "switch", ot_admin_builtin_switch },
   { "undeploy", ot_admin_builtin_undeploy },
+  { "unlock", ot_admin_builtin_unlock }, 
   { "upgrade", ot_admin_builtin_upgrade },
   { NULL, NULL }
 };
diff --git a/src/switchroot/ostree-mount-util.c b/src/switchroot/ostree-mount-util.c
index c6df559..daec66c 100644
--- a/src/switchroot/ostree-mount-util.c
+++ b/src/switchroot/ostree-mount-util.c
@@ -26,6 +26,10 @@
 #include <errno.h>
 #include <string.h>
 #include <stdio.h>
+#include <sys/mount.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/statvfs.h>
 
 #include "ostree-mount-util.h"
 
@@ -48,3 +52,18 @@ perrorv (const char *format, ...)
 
   return 0;
 }
+
+int
+path_is_on_readonly_fs (char *path)
+{
+  struct statvfs stvfsbuf;
+
+  if (statvfs (path, &stvfsbuf) == -1)
+    {
+      perrorv ("statvfs(%s): ", path);
+      exit (EXIT_FAILURE);
+    }
+
+  return (stvfsbuf.f_flag & ST_RDONLY) != 0;
+}
+
diff --git a/src/switchroot/ostree-mount-util.h b/src/switchroot/ostree-mount-util.h
index 63e90c6..475b2ca 100644
--- a/src/switchroot/ostree-mount-util.h
+++ b/src/switchroot/ostree-mount-util.h
@@ -22,3 +22,5 @@
 #pragma once
 
 int perrorv (const char *format, ...) __attribute__ ((format (printf, 1, 2)));
+
+int path_is_on_readonly_fs (char *path);
diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c
index 3de137b..375867b 100644
--- a/src/switchroot/ostree-prepare-root.c
+++ b/src/switchroot/ostree-prepare-root.c
@@ -111,7 +111,6 @@ touch_run_ostree (void)
 int
 main(int argc, char *argv[])
 {
-  const char *readonly_bind_mounts[] = { "/usr", NULL };
   const char *root_mountpoint = NULL;
   char *ostree_target = NULL;
   char *deploy_path = NULL;
@@ -119,7 +118,7 @@ main(int argc, char *argv[])
   char destpath[PATH_MAX];
   char newroot[PATH_MAX];
   struct stat stbuf;
-  int i;
+  int orig_cwd_dfd;
 
   if (argc < 2)
     {
@@ -211,22 +210,71 @@ main(int argc, char *argv[])
         }
     }
 
-  /* Set up any read-only bind mounts (notably /usr) */
-  for (i = 0; readonly_bind_mounts[i] != NULL; i++)
+  /* Here we do a dance to chdir to the newroot so that we can have
+   * the potential overlayfs mount points not look ugly.  However...I
+   * think we could do this a lot earlier and make all of the mounts
+   * here just be relative.
+   */
+  orig_cwd_dfd = openat (AT_FDCWD, ".", O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+  if (orig_cwd_dfd < 0)
+    {
+      perrorv ("failed to open .");
+      exit (EXIT_FAILURE);
+    }
+
+  if (chdir (newroot) < 0)
     {
-      snprintf (destpath, sizeof(destpath), "%s%s", newroot, readonly_bind_mounts[i]);
-      if (mount (destpath, destpath, NULL, MS_BIND, NULL) < 0)
+      perrorv ("failed to chdir to newroot");
+      exit (EXIT_FAILURE);
+    }
+
+  /* Do we have a persistent overlayfs for /usr?  If so, mount it now. */
+  if (lstat (".usr-ovl-work", &stbuf) == 0)
+    {
+      const char usr_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work";
+
+      /* Except overlayfs barfs if we try to mount it on a read-only
+       * filesystem.  For this use case I think admins are going to be
+       * okay if we remount the rootfs here, rather than waiting until
+       * later boot and `systemd-remount-fs.service`.
+       */
+      if (path_is_on_readonly_fs ("."))
        {
-         perrorv ("failed to bind mount (class:readonly) %s", destpath);
+         if (mount (".", ".", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0)
+           {
+             perrorv ("Failed to remount rootfs writable (for overlayfs)");
+             exit (EXIT_FAILURE);
+           }
+       }
+      
+      if (mount ("overlay", "usr", "overlay", 0, usr_ovl_options) < 0)
+       {
+         perrorv ("failed to mount /usr overlayfs");
+         exit (EXIT_FAILURE);
+       }
+    }
+  else
+    {
+      /* Otherwise, a read-only bind mount for /usr */
+      if (mount ("usr", "usr", NULL, MS_BIND, NULL) < 0)
+       {
+         perrorv ("failed to bind mount (class:readonly) /usr");
          exit (EXIT_FAILURE);
        }
-      if (mount (destpath, destpath, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) < 0)
+      if (mount ("usr", "usr", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) < 0)
        {
-         perrorv ("failed to bind mount (class:readonly) %s", destpath);
+         perrorv ("failed to bind mount (class:readonly) /usr");
          exit (EXIT_FAILURE);
        }
     }
 
+  if (fchdir (orig_cwd_dfd) < 0)
+    {
+      perrorv ("failed to chdir to orig root");
+      exit (EXIT_FAILURE);
+    }
+  (void) close (orig_cwd_dfd);
+
   touch_run_ostree ();
 
   /* Move physical root to $deployment/sysroot */
diff --git a/src/switchroot/ostree-remount.c b/src/switchroot/ostree-remount.c
index b8d3a96..aecaf9a 100644
--- a/src/switchroot/ostree-remount.c
+++ b/src/switchroot/ostree-remount.c
@@ -37,20 +37,6 @@
 
 #include "ostree-mount-util.h"
 
-static int
-path_is_on_readonly_fs (char *path)
-{
-  struct statvfs stvfsbuf;
-
-  if (statvfs (path, &stvfsbuf) == -1)
-    {
-      perrorv ("statvfs(%s): ", path);
-      exit (EXIT_FAILURE);
-    }
-
-  return (stvfsbuf.f_flag & ST_RDONLY) != 0;
-}
-
 /* Having a writeable /var is necessary for full system functioning.
  * If /var isn't writeable, we mount tmpfs over it. While this is
  * somewhat outside of ostree's scope, having all /var twiddling



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