diff mbox

[5/5] Add a tool to administer a simple OpenLDAP-based NSDB

Message ID 20131029194235.19294.29650.stgit@seurat.1015granger.net
State Accepted
Headers show

Commit Message

Chuck Lever Oct. 29, 2013, 7:42 p.m. UTC
NSDB set-up has been made simpler over the past releases, but it
remains onerously complex, even more so now that TLS is in the
picture.

Introduce a administration tool that can set up an NSDB from scratch
using a fresh OpenLDAP installation.

The tool depends on distribution packaging to pre-install an
appropriate OpenLDAP server package and copy in the FedFS schema
definition.

Several subcommands are available:

  o install -- set up an NSDB
  o backup  -- save a dated backup of the NSDB
  o restore -- restore a backup
  o status  -- report status of NSDB service

The install subcommand has a --security= option to specify whether
to set up a server certificate and enable TLS.

The tool maintains an activity log under /var/lib/fedfs.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 .gitignore                           |    1 
 configure.ac                         |    2 
 doc/man/Makefile.am                  |    2 
 doc/man/nsdb-jumpstart.8             |  404 ++++++++++++++++++++
 src/Makefile.am                      |    2 
 src/PyFedfs/Makefile.am              |    2 
 src/PyFedfs/jumpstart/Makefile.am    |   31 ++
 src/PyFedfs/jumpstart/__init__.py    |   23 +
 src/PyFedfs/jumpstart/backup.py      |  186 +++++++++
 src/PyFedfs/jumpstart/cert.py        |   91 +++++
 src/PyFedfs/jumpstart/firewall.py    |   97 +++++
 src/PyFedfs/jumpstart/install.py     |  341 +++++++++++++++++
 src/PyFedfs/jumpstart/slapd.py       |  671 ++++++++++++++++++++++++++++++++++
 src/PyFedfs/jumpstart/status.py      |   78 ++++
 src/PyFedfs/jumpstart/transaction.py |  237 ++++++++++++
 src/PyFedfs/run.py                   |   40 ++
 src/jumpstart/Makefile.am            |   40 ++
 src/jumpstart/nsdb-jumpstart.in      |  119 ++++++
 18 files changed, 2362 insertions(+), 5 deletions(-)
 create mode 100644 doc/man/nsdb-jumpstart.8
 create mode 100644 src/PyFedfs/jumpstart/Makefile.am
 create mode 100644 src/PyFedfs/jumpstart/__init__.py
 create mode 100644 src/PyFedfs/jumpstart/backup.py
 create mode 100644 src/PyFedfs/jumpstart/cert.py
 create mode 100644 src/PyFedfs/jumpstart/firewall.py
 create mode 100644 src/PyFedfs/jumpstart/install.py
 create mode 100644 src/PyFedfs/jumpstart/slapd.py
 create mode 100644 src/PyFedfs/jumpstart/status.py
 create mode 100644 src/PyFedfs/jumpstart/transaction.py
 create mode 100644 src/jumpstart/Makefile.am
 create mode 100644 src/jumpstart/nsdb-jumpstart.in
diff mbox

Patch

diff --git a/.gitignore b/.gitignore
index 407e013..00bafc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@  Doxygen/
 *.la
 src/domainroot/fedfs-domainroot
 src/fedfsd/fedfsd
+src/jumpstart/nsdb-jumpstart
 src/nsdbparams/nsdbparams
 src/nfsref/nfsref
 src/mount/mount.fedfs
diff --git a/configure.ac b/configure.ac
index 7b83a2c..f92f731 100644
--- a/configure.ac
+++ b/configure.ac
@@ -187,6 +187,7 @@  AC_CONFIG_FILES([Makefile
                  src/fedfsc/Makefile
                  src/fedfsd/Makefile
                  src/include/Makefile
+                 src/jumpstart/Makefile
                  src/libadmin/Makefile
                  src/libjunction/Makefile
                  src/libnsdb/Makefile
@@ -199,5 +200,6 @@  AC_CONFIG_FILES([Makefile
                  src/nsdbparams/Makefile
                  src/PyFedfs/Makefile
                  src/PyFedfs/domainroot/Makefile
+                 src/PyFedfs/jumpstart/Makefile
                  src/plug-ins/Makefile])
 AC_OUTPUT
diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am
index 65e9e9c..1123dfc 100644
--- a/doc/man/Makefile.am
+++ b/doc/man/Makefile.am
@@ -38,7 +38,7 @@  NSDB_CLIENT_CMDS	= nsdb-create-fsl.8 nsdb-create-fsn.8 \
 
 dist_man7_MANS		= fedfs.7 nsdb-parameters.7
 dist_man8_MANS		= rpc.fedfsd.8 mount.fedfs.8 fedfs-map-nfs4.8 nfsref.8 \
-			  nsdbparams.8 fedfs-domainroot.8 \
+			  nsdbparams.8 fedfs-domainroot.8 nsdb-jumpstart.8 \
 			  $(FEDFS_CLIENT_CMDS) $(NSDB_CLIENT_CMDS)
 
 CLEANFILES		= cscope.in.out cscope.out cscope.po.out *~
diff --git a/doc/man/nsdb-jumpstart.8 b/doc/man/nsdb-jumpstart.8
new file mode 100644
index 0000000..c6e443f
--- /dev/null
+++ b/doc/man/nsdb-jumpstart.8
@@ -0,0 +1,404 @@ 
+.\"@(#)nsdb-jumpstart.8
+.\"
+.\" @file doc/man/nsdb-jumpstart.8
+.\" @brief man page for nsdb-jumpstart tool
+.\"
+
+.\"
+.\" Copyright 2013 Oracle.  All rights reserved.
+.\"
+.\" This file is part of fedfs-utils.
+.\"
+.\" fedfs-utils is free software; you can redistribute it and/or modify
+.\" it under the terms of the GNU General Public License version 2.0 as
+.\" published by the Free Software Foundation.
+.\"
+.\" fedfs-utils 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 General Public License version 2.0 for more details.
+.\"
+.\" You should have received a copy of the GNU General Public License
+.\" version 2.0 along with fedfs-utils.  If not, see:
+.\"
+.\"	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+.\"
+.TH NSDB-JUMPSTART 8 "@publication-date@"
+.SH NAME
+nsdb-jumpstart \- Administer a basic FedFS NSDB using OpenLDAP
+.SH SYNOPSIS
+.B nsdb-jumpstart
+.RB [ \-h , \-\-help ]
+.RB [ \-\-version ]
+.P
+.B nsdb-jumpstart
+.RB [ \-\-statedir =
+.IR statedir ]
+.B install
+.RB [ \-\-security =
+.IR mode ]
+.P
+.B nsdb-jumpstart
+.RB [ \-\-statedir =
+.IR statedir ]
+.B status
+.P
+.B nsdb-jumpstart
+.RB [ \-\-statedir =
+.IR statedir ]
+.B backup
+.P
+.B nsdb-jumpstart
+.RB [ \-\-statedir =
+.IR statedir ]
+.B restore
+.RI [ backup-name ]
+.SH INTRODUCTION
+RFC 5716 introduces the Federated File System (FedFS, for short).
+FedFS is an extensible standardized mechanism
+by which system administrators construct
+a coherent namespace across multiple file servers using
+.IR "file system referrals" .
+For further details, see
+.BR fedfs (7).
+.P
+A FedFS domain's namespace is joined together via
+.IR junctions .
+When a file-access client encounters a junction on a file server,
+the file server provides a list of locations where that client
+can access the target file set to which the juntion refers.
+.P
+In a FedFS domain, these location lists are stored on one or more LDAP servers,
+known as
+.IR "namespace databases" ,
+or
+.IR NSDBs ,
+for short.
+.P
+FedFS-enabled file servers access the information stored
+on NSDBs via standard LDAP queries.
+Tools that administer a FedFS domain use ldapmodify queries
+to manage information stored on an NSDB.
+File-access clients have no need to access NSDBs directly.
+.P
+Further information about junctions and NSDBs is available in
+.BR fedfs (7).
+.SH DESCRIPTION
+The FedFS NSDB Proposed Standard allows flexible use
+of any LDAP server and its Directory Information Tree
+to store and manage NSDB information.
+.P
+The
+.BR nsdb-jumpstart (8)
+command provides a simplified but fully capable stand-alone
+NSDB based specifically on OpenLDAP.
+Using this command,
+you can install a fresh NSDB, or back up or restore your NSDB data.
+It can even construct a self-signed x.509 certificate to enable
+secure NSDB queries.
+.SS Operation
+The
+.B install
+subcommand sets up an empty NSDB, ready to be used in a FedFS domain.
+The new NSDB replaces any OpenLDAP configuration
+that may already exist on the system.
+OpenLDAP must already be installed on the system.
+.P
+Once the new NSDB is running,
+FedFS fileset location information is stored as records
+in a Directory Information Tree under the NCE.
+This information is managed with commands like
+.BR nsdb-create-fsn (8).
+.P
+A handful of parameters are needed to set up the new NSDB.
+These are gathered via a brief interview.
+The domain name and administrator credentials are provided during
+this interview.
+Passwords are not checked for strength,
+however blank passwords are not permitted.
+.P
+The baseline security requirements for the NSDB are specified
+at install time using the
+.B \-\-security=
+option.  See the
+.B SECURITY
+section for an in-depth discussion.
+.P
+Once set up with the
+.B install
+subcommand, OpenLDAP listens for LDAP queries on the standard LDAP port (389).
+The underlying LDAP server can be configured like any other OpenLDAP server
+using the new-style
+.I cn=config
+configuration interface.
+.P
+To display the current status of the NSDB service on the local host, use the
+.B status
+subcommand.
+Information about the local NSDB service is displayed, including whether
+the LDAP service is started, whether it actually is an NSDB, and
+whether TLS security is required to use it.
+.P
+The
+.BR nsdb-jumpstart (8)
+command also provides backup and restore facilities.
+The
+.B backup
+subcommand saves location information stored on the local NSDB
+to a dated LDIF file.
+LDIF files created by the
+.B backup
+command are stored in the
+.I @statedir@/nsdb-backup
+directory by default.
+.P
+The
+.B restore
+subcommand completely replaces the contents of the NSDB with a backup
+contained in of one of the previously saved LDIF files.
+The
+.B restore
+subcommand takes one positional argument, which is the name of
+the backup to restore.
+A list of backups is displayed by using the
+.B restore
+subcommand with no argument.
+.P
+The
+.BR nsdb-jumpstart (8)
+command must run as root.
+A audit log of each
+.BR nsdb-jumpstart (8)
+operation is stored in
+.IR @statedir@/nsdb-jumpstart.log .
+.SS Subcommands
+Valid
+.BR nsdb-jumpstart (8)
+subcommands are:
+.IP "\fBinstall\fP"
+Replace the OpenLDAP configuration on the local system with
+a ready-built NSDB.
+The user is asked to confirm before action is taken.
+.IP
+Specifying the
+.B \-\-security=
+option sets the transport security that the NSDB requires
+clients to use when communicating with it.
+.IP "\fBstatus\fP"
+Display the status of the NSDB on the local system.
+This subcommand takes no arguments.
+.IP "\fBbackup\fP"
+Generate an LDIF containing the NSDB information stored
+on the local LDAP server.
+The LDIF is stored in a dated file under
+.IR @statedir@/nsdb-backup .
+This subcommand takes no arguments.
+.IP "\fBrestore\fP"
+Replace the NSDB information on the local LDAP server
+with the contents of an LDIF.
+This subcommand takes a backup name as an argument.
+If no backup name is given,
+a list of backups that can be restored is displayed.
+The user is asked to confirm before action is taken.
+.SS Command line options
+The following options are specified before the subcommand on the command line.
+.IP "\fB\-\-help"
+Displays usage and copyright information, then exit.
+.IP "\fB\-\-version"
+Displays fedfs-utils version information, then exit.
+.IP "\fB\-\-stateidr=\fIpathname\fP"
+Specifies the pathname of the local directory
+under which NSDB data is maintained.
+By default, this directory is
+.IR @statedir .
+.SS Subcommand options
+.IP "\fB\-\-security=\fImode\fP"
+Selects the security mode of the NSDB.
+This option may be specified only on the
+.B install
+subcommand.
+Valid
+.I mode
+values are
+.B none
+and
+.BR tls .
+.P
+If
+.B none
+is specified, or the
+.B \-\-security=
+option is not specified, clients can connect to this NSDB in the clear.
+.P
+If
+.B tls
+is specified, the
+.B install
+subcommand creates a self-signed x.509 certificate,
+and configures the NSDB so that clients are required to use TLS
+when connecting to the NSDB.
+.SH EXIT CODES
+The
+.BR nsdb-jumpstart (8)
+command returns one of two values upon exit.
+.TP
+.B 0
+The subcommand succeeded.
+.TP
+.B 1
+The subcommand failed.
+.SH EXAMPLES
+Suppose you are the FedFS administrator of the
+.I example.net
+FedFS domain.
+After you have chosen a reliable server in the
+.I example.net
+domain to act as your NSDB, log in on that server as root,
+ensure that OpenLDAP is installed,
+and that any configuration can be discarded.
+.P
+To create a new NSDB with a self-signed certificate for the
+.I example.net
+domain, use:
+.RS
+.sp
+# ./nsdb-jumpstart install --security=tls
+.br
+This command is about to replace the OpenLDAP configuration on this system.
+.br
+Do you want to continue? [y/N] y
+.br
+Enter the name of the Fedfs domain this NSDB will server
+.br
+FedFS domain [ example.net ]:
+.br
+Enter the LDAP administrator DN for this NSDB
+.br
+Admin DN [ cn=admin,cn=config ]:
+.br
+Enter the LDAP administrator password for this DN
+.br
+New password:
+.br
+Re-enter new password:
+.br
+Enter the NSDB administrator password for this DN
+.br
+New password:
+.br
+Re-enter new password:
+.br
+Last chance: about to replace the OpenLDAP configuration on this system.
+.br
+Continue? [y/N] y
+.br
+Setting up a self-signed x.509 certificate.  Please answer the following questions:
+.br
+
+.br
+Country (C)? US
+.br
+State or province (ST)? Massachusetts
+.br
+City (L)? Boston
+.br
+Organization (O)? Red Sox
+.br
+Organizational unit (OU)? Fans
+.br
+
+.br
+NSDB configuration was successful.
+.br
+
+.br
+Slapd is enabled and running
+.br
+The LDAP administrator DN is: cn=admin,cn=config
+.br
+The NSDB administrator DN is: cn=NSDB Manager,dc=example,dc=net
+.br
+The NCE is: ou=fedfs,dc=example,dc=net
+.br
+
+.br
+Distribute the NSDB's certificate in /etc/openldap/nsdb-cert.pem
+.br
+#
+.RE
+.SH SECURITY
+The NSDB created by the
+.BR nsdb-jumpstart (8)
+command allows anonymous read access to the NCE and all entries under it.
+The LDAP server's rootDSE is also readable by anyone.
+An NSDB client must bind with administrator privileges
+to update NSDB records for a FedFS domain.
+ACLs may be adjusted after the NSDB is set up with
+.BR nsdb-jumpstart (8).
+.P
+Before binding, however, NSDB clients must connect to the NSDB to use it.
+The
+.B \-\-security=
+setting determines what type of transport layer security is required
+to connect to the NSDB.
+.P
+When the
+.B \-\-security=none
+option is specified during NSDB setup,
+or if no
+.B \-\-security=
+setting is specified,
+NSDB clients can connect to the NSDB using an unencrypted
+connection to the standard LDAP port (389).
+.P
+By specifying the
+.B \-\-security=tls
+option on the
+.BR nsdb-jumpstart (8)
+command, a self-signed x.509 certificate is created
+that NSDB clients must use to authenticate the NSDB and its contents.
+The underlying LDAP server requires the use of TLS
+and the use of AES or better encryption when a client access the NSDB.
+The NSDB never authenticates its clients.
+.P
+To use this NSDB, the new certificate material must be distributed
+to NSDB clients (fileservers and administrative systems)
+and installed using the
+.BR nsdbparams (8)
+command, or it can be transferred directly to NSDB clients that
+are running the
+.BR rpc.fedfsd (8)
+daemon.
+.P
+The use of a transport encryption mechanism such as TLS is
+strongly recommended to protect NSDB requests on untrusted networks.
+SASL is currently not supported for the NSDB protocol.
+.SH FILES
+.TP
+.I @statedir/nsdb-jumpstart.log
+Log file created during subcommand processing
+.TP
+.I /etc/openldap/nsdb-cert.pem
+File containing the server's x.509 certificate, in PEM format
+.TP
+.I /etc/openldap/nsdb-key.pem
+File containing the server's private key, in PEM format
+.TP
+.I @statedir/nsdb-db
+Directory containing back-end database for the LDAP server's
+domain controller root suffix
+.SH "SEE ALSO"
+.BR fedfs (7),
+.BR nfsref (8),
+.BR nsdb-create-fsn (8),
+.BR nsdbparams (8),
+.BR rpc.fedfsd (8)
+.sp
+RFC 5716 for FedFS requirements and overview
+.SH COLOPHON
+This page is part of the fedfs-utils package.
+A description of the project and information about reporting bugs
+can be found at
+.IR http://wiki.linux-nfs.org/wiki/index.php/FedFsUtilsProject .
+.SH "AUTHOR"
+Chuck Lever <chuck.lever@oracle.com>
diff --git a/src/Makefile.am b/src/Makefile.am
index 9f84ebc..51bc9d1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -26,7 +26,7 @@ 
 SUBDIRS			= include libxlog libadmin libnsdb libjunction \
 			  libparser libsi \
 			  fedfsc fedfsd mount nfsref nsdbc nsdbparams \
-			  plug-ins PyFedfs domainroot
+			  plug-ins PyFedfs domainroot jumpstart
 
 CLEANFILES		= cscope.in.out cscope.out cscope.po.out *~
 DISTCLEANFILES		= Makefile.in
diff --git a/src/PyFedfs/Makefile.am b/src/PyFedfs/Makefile.am
index bef9d11..fae2d1a 100644
--- a/src/PyFedfs/Makefile.am
+++ b/src/PyFedfs/Makefile.am
@@ -23,7 +23,7 @@ 
 ##	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 ##
 
-SUBDIRS			= domainroot
+SUBDIRS			= domainroot jumpstart
 pyfedfs_PYTHON		= __init__.py run.py userinput.py utilities.py
 pyfedfsdir		= $(pythondir)/PyFedfs
 
diff --git a/src/PyFedfs/jumpstart/Makefile.am b/src/PyFedfs/jumpstart/Makefile.am
new file mode 100644
index 0000000..13db46a
--- /dev/null
+++ b/src/PyFedfs/jumpstart/Makefile.am
@@ -0,0 +1,31 @@ 
+##
+## @file src/PyFedfs/jumpstart/Makefile.am
+## @brief Process this file with automake to produce src/PyFedfs/jumpstart/Makefile.in
+##
+
+##
+## Copyright 2013 Oracle.  All rights reserved.
+##
+## This file is part of fedfs-utils.
+##
+## fedfs-utils is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License version 2.0 as
+## published by the Free Software Foundation.
+##
+## fedfs-utils 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 General Public License version 2.0 for more details.
+##
+## You should have received a copy of the GNU General Public License
+## version 2.0 along with fedfs-utils.  If not, see:
+##
+##	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+##
+
+jumpstart_PYTHON	= __init__.py backup.py cert.py firewall.py \
+			  install.py slapd.py status.py transaction.py
+jumpstartdir		= $(pythondir)/PyFedfs/jumpstart
+
+CLEANFILES		= cscope.in.out cscope.out cscope.po.out *~
+DISTCLEANFILES		= Makefile.in
diff --git a/src/PyFedfs/jumpstart/__init__.py b/src/PyFedfs/jumpstart/__init__.py
new file mode 100644
index 0000000..4879aef
--- /dev/null
+++ b/src/PyFedfs/jumpstart/__init__.py
@@ -0,0 +1,23 @@ 
+"""
+PyFedfs
+
+This module contains components of the nsdb-jumpstart command.
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
diff --git a/src/PyFedfs/jumpstart/backup.py b/src/PyFedfs/jumpstart/backup.py
new file mode 100644
index 0000000..d2f1744
--- /dev/null
+++ b/src/PyFedfs/jumpstart/backup.py
@@ -0,0 +1,186 @@ 
+"""
+Back up an OpenLDAP-based FedFS NSDB
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+
+try:
+    import sys
+    import os
+    import logging as log
+    import datetime
+
+    from PyFedfs.jumpstart.slapd import backup_slapd_backend
+    from PyFedfs.jumpstart.slapd import make_ldap_directory
+    from PyFedfs.jumpstart.slapd import get_slapd_backend_dir
+    from PyFedfs.jumpstart.slapd import restore_slapd_backend
+    from PyFedfs.jumpstart.transaction import Transaction
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import start_service, stop_service
+    from PyFedfs.userinput import confirm
+    from PyFedfs.utilities import list_directory
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+BACKUP_DIRNAME = 'nsdb-backup'
+
+
+def do_backup(backup_dir, nocompress):
+    """
+    Backup the local NSDB
+
+    Returns a shell exit status
+    """
+    ret = stop_service('slapd')
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = backup_slapd_backend(backup_dir,
+                               datetime.datetime.now().strftime("%F-%T"),
+                               nocompress)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = start_service('slapd')
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return EXIT_SUCCESS
+
+
+def subcmd_backup(args):
+    """
+    Run the backup procedure
+
+    Returns a shell exit status
+    """
+    backup_dir = os.path.join(args.statedir, BACKUP_DIRNAME)
+
+    ret = make_ldap_directory(backup_dir, 0700)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    log.info('Running NSDB backup...')
+
+    os.umask(0277)
+    ret = do_backup(backup_dir, args.nocompress)
+    if ret != EXIT_SUCCESS:
+        log.info('Command aborted')
+        return EXIT_FAILURE
+    return EXIT_SUCCESS
+
+
+def list_backups(args):
+    """
+    List available backups
+
+    Returns a shell exit status
+    """
+    backup_dir = os.path.join(args.statedir, BACKUP_DIRNAME)
+
+    listing = list_directory(backup_dir)
+
+    output = []
+    for item in listing:
+        if item.endswith('.ldif'):
+            output.append(item[:-5])
+        if item.endswith('.ldif.gz'):
+            output.append(item[:-8])
+
+    if len(output) == 0:
+        log.info('No backups are available')
+        return EXIT_SUCCESS
+
+    log.info('Listing NSDB backups...')
+    for line in list(set(output)):
+        log.info(line)
+
+    return EXIT_SUCCESS
+
+
+def preserve_and_restore(args):
+    """
+    Restore the local NSDB
+
+    Returns a shell exit status
+    """
+    backend_dir = get_slapd_backend_dir()
+    if backend_dir == '':
+        log.error('Failed to find local NSDB\'s backend database')
+        return EXIT_FAILURE
+    log.info('NSDB backend database: %s', backend_dir)
+
+    xact = Transaction()
+    xact.add(backend_dir)
+    xact.checkpoint()
+
+    backup_dir = os.path.join(args.statedir, BACKUP_DIRNAME)
+    ret = restore_slapd_backend(backend_dir, backup_dir, args.backup)
+    if ret != EXIT_SUCCESS:
+        xact.revert()
+        return EXIT_FAILURE
+
+    xact.commit()
+    return EXIT_SUCCESS
+
+
+def do_restore(args):
+    """
+    Restore the local NSDB
+
+    Returns a shell exit status
+    """
+    ret = stop_service('slapd')
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = preserve_and_restore(args)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return start_service('slapd')
+
+
+def subcmd_restore(args):
+    """
+    Run the backup procedure
+
+    Returns a shell exit status
+    """
+    if args.backup == '':
+        return list_backups(args)
+
+    print('This command replaces all the NSDB information on this system.')
+    if not confirm('Do you want to continue?'):
+        log.error('Quitting...')
+        return EXIT_FAILURE
+
+    log.info('Restoring NSDB...')
+
+    if do_restore(args) != EXIT_SUCCESS:
+        log.info('Command aborted')
+        return EXIT_FAILURE
+    return EXIT_SUCCESS
+
+
+__all__ = ['subcmd_backup', 'subcmd_restore']
diff --git a/src/PyFedfs/jumpstart/cert.py b/src/PyFedfs/jumpstart/cert.py
new file mode 100644
index 0000000..8105a1b
--- /dev/null
+++ b/src/PyFedfs/jumpstart/cert.py
@@ -0,0 +1,91 @@ 
+"""
+Create a self-signed x.509 certificate for an LDAP server
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+
+try:
+    import sys
+    import os
+    import socket
+    from OpenSSL import crypto
+
+    from PyFedfs.run import EXIT_SUCCESS
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def create_self_signed_certificate(certfile, keyfile, owner_uid, owner_gid):
+    """
+    Create a self-signed server certificate
+    """
+    keypair = crypto.PKey()
+    keypair.generate_key(crypto.TYPE_RSA, 2048)
+
+    ss_cert = crypto.X509()
+
+    print('\nSetting up a self-signed x.509 certificate.  ' \
+        'Please answer the following questions:\n')
+    ss_cert.get_subject().C = raw_input('Country (C)? ')
+    ss_cert.get_subject().ST = raw_input('State or province (ST)? ')
+    ss_cert.get_subject().L = raw_input('City (L)? ')
+    ss_cert.get_subject().O = raw_input('Organization (O)? ')
+    ss_cert.get_subject().OU = raw_input('Organizational unit (OU)? ')
+    ss_cert.get_subject().CN = socket.getfqdn()
+    ss_cert.set_serial_number(1000)
+    ss_cert.gmtime_adj_notBefore(0)
+    ss_cert.gmtime_adj_notAfter(2 * 365 * 24 * 60 * 60)
+
+    ss_cert.set_issuer(ss_cert.get_subject())
+    ss_cert.set_pubkey(keypair)
+    ss_cert.sign(keypair, 'sha1')
+
+    cert_file = os.open(certfile, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0440)
+    os.fchown(cert_file, owner_uid, owner_gid)
+    os.write(cert_file, crypto.dump_certificate(crypto.FILETYPE_PEM, ss_cert))
+    os.close(cert_file)
+
+    key_file = os.open(keyfile, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0440)
+    os.fchown(key_file, owner_uid, owner_gid)
+    os.write(key_file, crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair))
+    os.close(key_file)
+
+    return EXIT_SUCCESS
+
+
+def __ut_create_certificate():
+    """
+    Unit tests for create_self_signed_certificate
+    """
+    ret = create_self_signed_certificate('/tmp/cert.pem', '/tmp/key.pem',
+                                         os.geteuid(), os.getegid())
+    if ret != EXIT_SUCCESS:
+        return
+
+    print('\nDone.  See /tmp/{cert,key}.pem\n')
+
+
+__all__ = ['create_self_signed_certificate']
+
+
+if __name__ == '__main__':
+    __ut_create_certificate()
diff --git a/src/PyFedfs/jumpstart/firewall.py b/src/PyFedfs/jumpstart/firewall.py
new file mode 100644
index 0000000..b1c6e52
--- /dev/null
+++ b/src/PyFedfs/jumpstart/firewall.py
@@ -0,0 +1,97 @@ 
+"""
+Manage local network firewall
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+
+try:
+    import sys
+    import os
+    import logging as log
+    from subprocess import Popen, PIPE
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import run_command
+    from PyFedfs.run import check_for_daemon
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def adjust_firewall():
+    """
+    Ensure LDAP (port 389) is allowed through the system firewall
+
+    Returns a shell exit status value
+    """
+    if not check_for_daemon('firewalld'):
+        log.info('firewalld is not running... skipping firewalld configuration')
+        return EXIT_SUCCESS
+
+    pathname = '/etc/firewalld/services/ldap.xml'
+    if os.path.isfile(pathname):
+        log.info('ldap.xml exists... skipping firewalld configuration')
+        return EXIT_SUCCESS
+
+    log.debug('Adjusting firewalld to permit LDAP service...')
+    try:
+        service = os.open(pathname, os.O_CREAT | os.O_WRONLY)
+    except OSError:
+        log.exception('Failed to create "%s"', pathname)
+        return EXIT_FAILURE
+
+    try:
+        os.fchmod(service, 0640)
+        os.write(service, '<?xml version="1.0" encoding="utf-8"?>\n')
+        os.write(service, '<?xml version="1.0" encoding="utf-8"?>\n')
+        os.write(service, '<service>\n')
+        os.write(service, '  <short>LDAP</short>\n')
+        os.write(service, '  <description>The Lightweight Directory '
+                 'Access Protocol is an application protocol for '
+                 'accessing and maintaining distributed directory '
+                 'information services over an Internet Protocol (IP) '
+                 'network.  Directory services may provide any '
+                 'organized set of records, often with a hierarchical '
+                 'structure, such as a corporate email directory.  '
+                 'Enable this option if you plan to provide an LDAP '
+                 'directory service (e.g. with slapd).</description>\n')
+        os.write(service, '  <port protocol="tcp" port="389"/>\n')
+        os.write(service, '  <port protocol="udp" port="389"/>\n')
+        os.write(service, '</service>\n')
+    except OSError:
+        log.exception('Failed to write "%s"', pathname)
+        os.close(service)
+        os.remove('/etc/firewalld/services/ldap.xml')
+        return EXIT_FAILURE
+    os.close(service)
+
+    ret = run_command(['firewall-cmd', '--reload'])
+    if ret != EXIT_SUCCESS:
+        os.remove('/etc/firewalld/services/ldap.xml')
+        return ret
+
+    # XXX: These are backwards: need the --reload after
+    # XXX: setting permanent configuration settings
+    return run_command(['firewall-cmd', '--permanent',
+                        '--zone=public', '--add-service=ldap'])
+
+
+__all__ = ['adjust_firewall']
diff --git a/src/PyFedfs/jumpstart/install.py b/src/PyFedfs/jumpstart/install.py
new file mode 100644
index 0000000..5dbf1ee
--- /dev/null
+++ b/src/PyFedfs/jumpstart/install.py
@@ -0,0 +1,341 @@ 
+"""
+Set up a simple FedFS NSDB using OpenLDAP
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+
+import sys
+import os
+import logging as log
+import socket
+import ldap
+
+try:
+    from PyFedfs.jumpstart.cert import create_self_signed_certificate
+    from PyFedfs.jumpstart.firewall import adjust_firewall
+    from PyFedfs.jumpstart.slapd import slapd_config, adjust_slapd_log
+    from PyFedfs.jumpstart.slapd import LDAP_UID, LDAP_GID
+    from PyFedfs.jumpstart.slapd import NSDB_CERTFILE, NSDB_KEYFILE
+    from PyFedfs.jumpstart.transaction import Transaction
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import stop_service, enable_and_start_service
+    from PyFedfs.userinput import confirm, get_password
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+PRESERVE_LIST = ['/etc/openldap/slapd.d',
+                 NSDB_CERTFILE,
+                 NSDB_KEYFILE]
+
+
+def get_domainname():
+    """
+    Get local system's domain name
+
+    Returns a string
+    """
+    hostname = socket.getfqdn().split('.')
+    if len(hostname) < 2:
+        return ''
+
+    domainname = '.'.join(hostname[1:])
+    return domainname.lower()
+
+
+def get_domaincontroller_dn(domainname):
+    """
+    Generate a domaincontroller DN
+
+    Returns a string
+    """
+    components = domainname.split('.')
+    if len(components) < 2:
+        return ''
+
+    distinguished_name = [[('dc', component, 1)] for component in components]
+    return ldap.dn.dn2str(distinguished_name)
+
+
+def get_nce_dn(domaincontroller):
+    """
+    Generate an NCE DN
+
+    Returns a string
+    """
+    try:
+        distinguished_name = ldap.dn.str2dn(domaincontroller)
+    except ldap.ENCODING_ERROR:
+        return ''
+
+    distinguished_name.insert(0, [('ou', 'fedfs', 1)])
+    return ldap.dn.dn2str(distinguished_name)
+
+
+def get_full_nsdb_admin(answers):
+    """
+    Generate NSDB administrator DN
+
+    Returns a string
+    """
+    try:
+        distinguished_name = ldap.dn.str2dn(answers['domaincontroller'])
+    except ldap.ENCODING_ERROR:
+        return ''
+
+    distinguished_name.insert(0, ldap.dn.str2dn(answers['nsdb_admin'])[0])
+    return ldap.dn.dn2str(distinguished_name)
+
+
+def ask_for_domainname(answers):
+    """
+    Ask for the system's domain name
+
+    Returns True if the interview data is good
+    """
+    answers['domainname'] = get_domainname()
+    print('Enter the name of the FedFS domain this NSDB will server')
+    if answers['domainname'] == []:
+        sys.stdout.write('FedFS domain: ')
+        choice = raw_input().lower()
+        if choice == '':
+            log.error('No domainname was provided')
+            return False
+        answers['domainname'] = choice
+    else:
+        sys.stdout.write('FedFS domain [ ' + answers['domainname'] + ' ]: ')
+        choice = raw_input().lower()
+        if choice != '':
+            answers['domainname'] = choice
+    return True
+
+
+def ask_for_domaincontroller(answers):
+    """
+    Ask for the domain controller DN
+
+    Returns True if the interview data is good
+    """
+    answers['domaincontroller'] = \
+        get_domaincontroller_dn(answers['domainname'])
+    if answers['domaincontroller'] == '':
+        log.error('An invalid domainname was provided')
+        return False
+    answers['nce'] = get_nce_dn(answers['domaincontroller'])
+    if answers['nce'] == '':
+        log.error('An invalid domainname was provided')
+        return False
+    log.info('Using "%s" as your FedFS domain name', answers['domainname'])
+    return True
+
+
+def ask_for_ldap_admin(answers):
+    """
+    Ask for the LDAP administrator DN and password
+
+    Returns True if the interview data is good
+    """
+    answers['ldap_admin'] = \
+        ldap.dn.dn2str([[('cn', 'admin', 1)], [('cn', 'config', 1)]])
+    print('Enter the LDAP administrator DN for this NSDB')
+    sys.stdout.write('Admin DN [ ' + answers['ldap_admin'] + ' ]: ')
+    choice = raw_input()
+    if choice != '':
+        answers['ldap_admin'] = choice
+    try:
+        ldap.dn.str2dn(choice)
+    except ldap.DECODING_ERROR:
+        log.error('An invalid administrator DN was provided')
+        return False
+    log.info('Using "%s" as your LDAP administrator', answers['ldap_admin'])
+
+    answers['ldap_password'] = \
+        get_password('Enter the LDAP administrator password for this DN')
+    if answers['ldap_password'] == '':
+        return False
+
+    return True
+
+
+def ask_for_nsdb_admin(answers):
+    """
+    Ask for the NSDB administrator DN and password
+
+    Returns True if the interview data is good
+    """
+    answers['nsdb_admin'] = 'cn=NSDB Manager'
+    answers['full_nsdb_admin'] = get_full_nsdb_admin(answers)
+    if answers['full_nsdb_admin'] == '':
+        return False
+    log.info('Using "%s" as your NSDB administrator',
+             answers['full_nsdb_admin'])
+
+    answers['nsdb_password'] = \
+        get_password('Enter the NSDB administrator password for this DN')
+    if answers['nsdb_password'] == '':
+        return False
+
+    return True
+
+
+def interview(answers):
+    """
+    Gather information for the configuration, perform some sanity checks
+
+    Returns True if the interview data is good
+    """
+    if not ask_for_domainname(answers):
+        return False
+    if not ask_for_domaincontroller(answers):
+        return False
+    if not ask_for_ldap_admin(answers):
+        return False
+    if not ask_for_nsdb_admin(answers):
+        return False
+    return True
+
+
+def setup_nsdb(answers):
+    """
+    Run the set-up procedure
+
+    Returns a shell exit status value
+    """
+
+    ret = slapd_config(answers)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = adjust_slapd_log()
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return adjust_firewall()
+
+
+def abort_command(xact):
+    """
+    Print a message and return failure
+
+    Returns a shell exit status value
+    """
+    xact.revert()
+
+    log.info('Command aborted')
+    return EXIT_FAILURE
+
+
+def do_setup(answers):
+    """
+    Interview user for configuration parameters, then run the setup
+
+    Returns a shell exit status value
+    """
+    print('Last chance: about to replace the OpenLDAP configuration '
+          'on this system.')
+    if not confirm('Continue?'):
+        log.error('Quitting...')
+        return EXIT_FAILURE
+
+    xact = Transaction()
+    for item in answers['preserve_list']:
+        xact.add(item)
+    xact.checkpoint()
+
+    ret = stop_service('slapd')
+    if ret != EXIT_SUCCESS:
+        return abort_command(xact)
+
+    ret = setup_nsdb(answers)
+    if ret != EXIT_SUCCESS:
+        return abort_command(xact)
+
+    if answers['security'] == 'tls':
+        ret = create_self_signed_certificate(NSDB_CERTFILE, NSDB_KEYFILE,
+                                             LDAP_UID, LDAP_GID)
+        if ret != EXIT_SUCCESS:
+            return abort_command(xact)
+
+    enable_and_start_service('slapd')
+
+    log.info('\nNSDB configuration was successful.\n')
+    log.info('Slapd is enabled and running')
+    log.info('The LDAP administrator DN is: ' + answers['ldap_admin'])
+    log.info('The NSDB administrator DN is: ' + answers['full_nsdb_admin'])
+    log.info('The NCE is: ' + answers['nce'])
+    if answers['security'] == 'tls':
+        log.info('Distribute the certificate in %s', NSDB_CERTFILE)
+
+    xact.commit()
+
+    return EXIT_SUCCESS
+
+
+def openldap_is_installed():
+    """
+    Predicate: Is an OpenLDAP server package installed?
+
+    Returns True if OpenLDAP server software is found;
+    otherwise returns False
+    """
+    if not os.path.isdir('/etc/openldap'):
+        log.error('No OpenLDAP configuration directory')
+        return False
+
+    if not os.path.isfile('/etc/openldap/schema/fedfs.schema'):
+        log.error('The FedFS schema is not installed')
+        return False
+
+    log.info('OpenLDAP server software found, proceeding')
+    return True
+
+
+def subcmd_install(args):
+    """
+    Set up local LDAP server as NSDB based on interview responses
+
+    Returns a shell exit status value
+    """
+    if not openldap_is_installed():
+        log.error('Quitting...')
+        return EXIT_FAILURE
+
+    print('This command replaces the OpenLDAP configuration on this system.')
+    if not confirm('Do you want to continue?'):
+        log.error('Quitting...')
+        return EXIT_FAILURE
+
+    backend_dir = os.path.join(args.statedir, 'nsdb-db')
+    answers = {'security': args.security, 'dc_backend': backend_dir}
+
+    if not interview(answers):
+        log.error('Quitting...')
+        return EXIT_FAILURE
+
+    answers['preserve_list'] = PRESERVE_LIST
+    answers['preserve_list'].append(backend_dir)
+
+    return do_setup(answers)
+
+
+__all__ = ['subcmd_install']
diff --git a/src/PyFedfs/jumpstart/slapd.py b/src/PyFedfs/jumpstart/slapd.py
new file mode 100644
index 0000000..58ea670
--- /dev/null
+++ b/src/PyFedfs/jumpstart/slapd.py
@@ -0,0 +1,671 @@ 
+"""
+Utility functions for interacting with slapd tools
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+import sys
+import os
+import logging as log
+import tempfile
+import pwd
+import grp
+import ldap
+import ldif
+import socket
+
+from subprocess import Popen, PIPE
+
+try:
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import run_as_user, restart_service
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+LDAP_USERNAME = 'ldap'
+LDAP_GROUPNAME = 'ldap'
+
+LDAP_UID = pwd.getpwnam(LDAP_USERNAME).pw_uid
+LDAP_GID = grp.getgrnam(LDAP_GROUPNAME).gr_gid
+
+NSDB_CERTFILE = '/etc/openldap/nsdb-cert.pem'
+NSDB_KEYFILE = '/etc/openldap/nsdb-key.pem'
+
+
+def adjust_slapd_log():
+    """
+    Set up syslog configuration for slapd
+    slapd must be restarted after this procedure.
+
+    Returns a shell exit status value
+    """
+    try:
+        logfile = os.open('/var/log/slapd', os.O_CREAT | os.O_EXCL)
+    except OSError as inst:
+        if inst.errno != 17:
+            log.error('Failed to create slapd log file')
+            return EXIT_FAILURE
+        log.info('/var/log/slapd exists... skipping rsyslog configuration')
+        return EXIT_SUCCESS
+    os.close(logfile)
+
+    log.debug('Enabling slapd logging with rsyslog...')
+    try:
+        os.chown('/var/log/slapd', LDAP_UID, LDAP_GID)
+    except OSError:
+        log.error('Failed to chown slapd log file')
+        os.remove('/var/log/slapd')
+        return EXIT_FAILURE
+
+    try:
+        config = os.open('/etc/rsyslog.d/slapd.conf',
+                         os.O_CREAT | os.O_EXCL | os.O_WRONLY)
+    except OSError:
+        log.error('Failed to create slapd log file')
+        os.remove('/var/log/slapd')
+        return EXIT_FAILURE
+
+    try:
+        os.write(config, 'local4.*\t/var/log/slapd\n')
+    except OSError:
+        log.error('Failed to write rsyslog config file')
+        os.close(config)
+        os.remove('/etc/rsyslog.d/slapd.conf')
+        os.remove('/var/log/slapd')
+        return EXIT_FAILURE
+    os.close(config)
+
+    ret = restart_service('rsyslog')
+    if ret != EXIT_SUCCESS:
+        os.remove('/etc/rsyslog.d/slapd.conf')
+        os.remove('/var/log/slapd')
+
+    return ret
+
+
+def search_ldif(blob, attribute):
+    """
+    Look for 'attribute' in an LDIF
+
+    Returns the first attribute value if found, or '' if not
+    """
+    for line in blob.splitlines():
+        record = line.split()
+        if len(record) and record[0] == attribute + ':':
+            return record[1]
+    return ''
+
+
+def local_nce_found():
+    """
+    Predicate: Does the local configuration contain an NCE record?
+
+    Returns True if an NCE record is found in a local LDAP database
+    """
+    log.debug('Checking local configuration for an NCE record...')
+
+    try:
+        process = Popen(['slapcat', '-n2'],
+                        stdout=PIPE, stderr=PIPE, shell=False)
+        output = process.communicate()[0]
+    except OSError:
+        log.error('slapcat command failed')
+        return False
+
+    return search_ldif(output, 'fedfsNceDN') != ''
+
+
+def __ut_local_nce_found():
+    """
+    Unit test for local_nce_found()
+    """
+    if local_nce_found():
+        print 'Local NCE found'
+    else:
+        print 'Local NCE not found'
+
+
+def local_tls_found():
+    """
+    Predicate: Is TLS enabled on the local configuration?
+
+    Returns True if TLS is enabled
+    """
+    log.debug('Checking local configuration for TLS support...')
+    try:
+        process = Popen(['slapcat', '-n0'],
+                        stdout=PIPE, stderr=PIPE, shell=False)
+        output = process.communicate()[0]
+    except OSError:
+        log.error('slapcat command failed')
+        return False
+
+    return search_ldif(output, 'olcTLSCACertificateFile') != ''
+
+
+def __ut_local_tls_found():
+    """
+    Unit test for local_tls_found()
+    """
+    if local_tls_found():
+        print 'TLS is configured on local slapd service'
+    else:
+        print 'TLS is not configured on local slapd service'
+
+
+def get_slapd_backend_dir():
+    """
+    Get the directory pathname of the configured backend database
+
+    Returns a string
+    """
+    log.debug('Retrieving pathname of configured backend database...')
+
+    try:
+        process = Popen(['slapcat', '-n0'],
+                        stdout=PIPE, stderr=PIPE, shell=False)
+        output = process.communicate()[0]
+    except OSError:
+        log.error('slapcat command failed')
+        return False
+
+    return search_ldif(output, 'olcDbDirectory')
+
+
+def __ut_get_slapd_backend_dir():
+    """
+    Unit test for get_slapd_backend_dir()
+    """
+    print get_slapd_backend_dir()
+
+
+def get_slapd_status():
+    """
+    Display status of local LDAP service on standard out
+
+    Returns a string
+    """
+    process = Popen(['systemctl', 'status', 'slapd.service'],
+                    stdout=PIPE, stderr=PIPE, shell=False)
+    return process.communicate()[0]
+
+
+def __ut_get_slapd_status():
+    """
+    Unit test for get_slapd_status()
+    """
+    print get_slapd_status()
+
+
+def check_ldap_connectivity():
+    """
+    Predicate: Can a basic LDAP query be performed on the local LDAP server?
+
+    Returns True if yes, otherwise False is returned
+    """
+    ldap_server = ldap.initialize('ldap://' + socket.getfqdn())
+    try:
+        ldap_server.search_s('', ldap.SCOPE_BASE, '(objectClass=*)')
+    except ldap.CONFIDENTIALITY_REQUIRED:
+        log.debug('Local LDAP server requires TLS confidentiality')
+        return True
+    except ldap.SERVER_DOWN:
+        log.debug('Local LDAP server is unreachable')
+        return False
+    except ldap.NO_SUCH_OBJECT:
+        log.debug('Local LDAP server contains no rootDSE')
+        return False
+    return True
+
+
+def __ut_check_ldap_connectivity():
+    """
+    Unit test for check_ldap_connectivity()
+    """
+    if check_ldap_connectivity():
+        print 'Able to contact local LDAP server'
+    else:
+        print 'Not able to contact local LDAP server'
+
+
+def temporary_ldap_file():
+    """
+    Create a temporary file owned by the ldap user
+
+    Returns a file-like object or None
+    """
+    try:
+        result = tempfile.NamedTemporaryFile(mode='w',
+                                             dir='/tmp',
+                                             delete=True)
+        os.chown(result.name, LDAP_UID, LDAP_GID)
+    except OSError:
+        return None
+    return result
+
+
+def make_ldap_directory(pathname, mode=0755):
+    """
+    Create a directory owned by the ldap user
+
+    Returns a shell exit status value
+    """
+    if os.path.isdir(pathname):
+        log.info('Directory "%s" already exists', pathname)
+        return EXIT_SUCCESS
+
+    try:
+        os.mkdir(pathname)
+        os.chmod(pathname, mode)
+        os.chown(pathname, LDAP_UID, LDAP_GID)
+    except OSError:
+        log.error('Failed to create "%s"', pathname)
+        return EXIT_FAILURE
+    return EXIT_SUCCESS
+
+
+def wipe_slapd_d():
+    """
+    Clean out the slapd.d directory
+    slapd must be stopped for this procedure.
+
+    Returns a shell exit status value
+    """
+    log.debug('Cleaning out "/etc/openldap/"...')
+
+    ret = make_ldap_directory('/etc/openldap/slapd.d')
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    try:
+        if os.path.isfile(NSDB_CERTFILE):
+            os.remove(NSDB_CERTFILE)
+        if os.path.isfile(NSDB_KEYFILE):
+            os.remove(NSDB_KEYFILE)
+    except OSError:
+        log.error('Failed to remove old certificates')
+        return EXIT_FAILURE
+
+    return EXIT_SUCCESS
+
+
+def replace_slapd_database(pathname):
+    """
+    Replace the contents of the back-end database
+    slapd must be stopped for this procedure.
+
+    Returns a shell exit status value
+    """
+    log.debug('Replacing "%s"...', pathname)
+
+    ret = make_ldap_directory(pathname, 0700)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    try:
+        dbconfig = os.open(os.path.join(pathname, 'DB_CONFIG'),
+                           os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0444)
+    except OSError:
+        log.error('Failed to create DB_CONFIG')
+        return EXIT_FAILURE
+
+    ret = EXIT_FAILURE
+    try:
+        os.fchown(dbconfig, LDAP_UID, LDAP_GID)
+        os.write(dbconfig, 'set_cachesize 0 268435456 1\n')
+        os.write(dbconfig, 'set_lg_regionmax 262144\n')
+        os.write(dbconfig, 'set_lg_bsize 2097152\n')
+        ret = EXIT_SUCCESS
+    except OSError:
+        log.error('Failed to write DB_CONFIG')
+
+    os.close(dbconfig)
+    return ret
+
+
+def generate_schema(config):
+    """
+    Generate set of include statements to construct server's schema
+    """
+    print >> config, 'include /etc/openldap/schema/corba.schema'
+    print >> config, 'include /etc/openldap/schema/core.schema'
+    print >> config, 'include /etc/openldap/schema/cosine.schema'
+    print >> config, 'include /etc/openldap/schema/duaconf.schema'
+    print >> config, 'include /etc/openldap/schema/dyngroup.schema'
+    print >> config, 'include /etc/openldap/schema/inetorgperson.schema'
+    print >> config, 'include /etc/openldap/schema/java.schema'
+    print >> config, 'include /etc/openldap/schema/misc.schema'
+    print >> config, 'include /etc/openldap/schema/nis.schema'
+    print >> config, 'include /etc/openldap/schema/openldap.schema'
+    print >> config, 'include /etc/openldap/schema/ppolicy.schema'
+    print >> config, 'include /etc/openldap/schema/collective.schema'
+    print >> config, 'include /etc/openldap/schema/fedfs.schema'
+    print >> config
+    print >> config, 'pidfile /var/run/openldap/slapd.pid'
+    print >> config, 'argsfile /var/run/openldap/slapd.args'
+    print >> config
+
+
+def generate_security_config(config, answers):
+    """
+    Generate server's security settings
+    """
+    if answers['security'] == 'tls':
+        print >> config, 'TLSCACertificateFile %s' % NSDB_CERTFILE
+        print >> config, 'TLSCertificateFile %s' % NSDB_CERTFILE
+        print >> config, 'TLSCertificateKeyFile %s' % NSDB_KEYFILE
+        print >> config, 'TLSVerifyClient never'
+        print >> config, 'security ssf=128 tls=1'
+    else:
+        print >> config, 'security tls=0'
+    print >> config
+
+
+def generate_config_database(config, answers):
+    """
+    Generate server's cn=config database
+    """
+    print >> config, 'database config'
+    print >> config, 'rootdn "%s"' % answers['ldap_admin']
+    print >> config, 'rootpw %s' % answers['ldap_password']
+    print >> config, 'access to *'
+    print >> config, '\tby dn.exact="gidNumber=0+uidNumber=0,' \
+        'cn=peercred,cn=external,cn=auth" manage'
+    print >> config, '\tby * none'
+    print >> config
+
+
+def generate_monitor_database(config, answers):
+    """
+    Generate server's cn=monitor database
+    """
+    print >> config, 'database monitor'
+    print >> config, 'access to *'
+    print >> config, '\tby dn.exact="gidNumber=0+uidNumber=0,' \
+        'cn=peercred,cn=external,cn=auth" read'
+    print >> config, '\tby dn.exact="%s" read' % answers['ldap_admin']
+    print >> config, '\tby * none'
+    print >> config
+
+
+def generate_dc_database(config, answers):
+    """
+    Generate database for domaincontroller root suffix
+    """
+    print >> config, 'database hdb'
+    print >> config, 'suffix "%s"' % answers['domaincontroller']
+    print >> config, 'checkpoint 1024 15'
+    print >> config, 'directory %s' % answers['dc_backend']
+    print >> config, 'rootdn "%s"' % answers['ldap_admin']
+    print >> config, 'access to filter=(objectClass=fedfsFsn)'
+    print >> config, '\tby dn="%s" manage' % answers['full_nsdb_admin']
+    print >> config, '\tby * read'
+    print >> config, 'access to filter=(objectClass=fedfsFsl)'
+    print >> config, '\tby dn="%s" manage' % answers['full_nsdb_admin']
+    print >> config, '\tby * read'
+    print >> config, 'access to filter=(objectClass=fedfsNsdbContainerEntry)'
+    print >> config, '\tby dn="%s" manage' % answers['full_nsdb_admin']
+    print >> config, '\tby * read'
+    print >> config, 'access to * by * read'
+    print >> config
+
+
+def generate_indices(config):
+    """
+    Generate index definitions for this server
+    """
+    print >> config, 'index objectClass eq,pres'
+    print >> config, 'index fedfsFsnUuid eq,pres'
+    print >> config, 'index fedFsFslUuid eq,pres'
+
+
+def generate_slapd_config(config, answers):
+    """
+    Build fresh old-style slapd configuration
+
+    Returns a shell exit status value
+    """
+    log.debug('Generating fresh slapd config in "%s"...', config.name)
+
+    ret = EXIT_FAILURE
+    try:
+        generate_schema(config)
+        generate_security_config(config, answers)
+        generate_config_database(config, answers)
+        generate_monitor_database(config, answers)
+        generate_dc_database(config, answers)
+        generate_indices(config)
+        config.flush()
+        ret = EXIT_SUCCESS
+    except OSError:
+        log.error('Failed to write new config file')
+
+    return ret
+
+
+def replace_slapd_config(answers):
+    """
+    Replace slapd configuration.
+    slapd must be stopped for this procedure.
+
+    Returns a shell exit status value
+    """
+    log.debug('Replacing slapd configuration...')
+
+    ret = wipe_slapd_d()
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    config = temporary_ldap_file()
+    if config is None:
+        log.error('Failed to create new config file')
+        return EXIT_FAILURE
+
+    ret = generate_slapd_config(config, answers)
+    if ret != EXIT_SUCCESS:
+        config.close()
+        return ret
+
+    ret = run_as_user(LDAP_USERNAME, ['slapadd', '-n2', '-l', '/dev/null',
+                                      '-f', config.name])
+    if ret != EXIT_SUCCESS:
+        config.close()
+        return ret
+
+    ret = run_as_user(LDAP_USERNAME, ['slaptest', '-f', config.name,
+                                      '-F', '/etc/openldap/slapd.d'])
+    config.close()
+    return ret
+
+
+def add_new_record(distinguished_name, new_entry):
+    """
+    Add a new entry to a slapd database
+
+    Returns a shell exit status value
+    """
+    tmp = temporary_ldap_file()
+    if tmp is None:
+        log.error('Failed to create temporary LDIF file')
+        return EXIT_FAILURE
+
+    writer = ldif.LDIFWriter(tmp)
+    writer.unparse(distinguished_name, new_entry)
+    tmp.flush()
+
+    ret = run_as_user(LDAP_USERNAME, ['slapadd', '-n2', '-l', tmp.name])
+
+    tmp.close()
+    return ret
+
+
+def add_domaincontroller(answers):
+    """
+    Add a domain controller root suffix.
+    slapd must be stopped for this procedure.
+
+    Returns a shell exit status value
+    """
+    log.debug('Adding root suffix for "%s"...', answers['domaincontroller'])
+
+    components = answers['domainname'].split('.')
+
+    entry = {}
+    entry['objectClass'] = ['top', 'organization', 'dcObject',
+                            'fedfsNsdbContainerInfo']
+    entry['dc'] = [components[0]]
+    entry['o'] = [answers['domainname']]
+    entry['fedfsNceDN'] = [answers['nce']]
+
+    return add_new_record(answers['domaincontroller'], entry)
+
+
+def add_nce(answers):
+    """
+    Add the NSDB Container Entry.
+    slapd must be stopped for this procedure.
+
+    Returns a shell exit status value
+    """
+    log.debug('Adding NSDB Container Entry "%s"...', answers['nce'])
+
+    entry = {}
+    entry['objectClass'] = ['top', 'organizationalUnit',
+                            'fedfsNsdbContainerEntry']
+    entry['ou'] = ['fedfs']
+
+    return add_new_record(answers['nce'], entry)
+
+
+def add_nsdb_manager(answers):
+    """
+    Add the NSDB Manager account
+
+    Returns a shell exit status value
+    """
+    log.debug('Adding NSDB Manager...')
+
+    ava = ldap.dn.str2dn(answers['nsdb_admin'])[0][0]
+
+    entry = {}
+    entry['objectClass'] = ['top', 'person']
+    entry['sn'] = ['Administrator']
+    entry['cn'] = [ava[1]]
+    entry['userPassword'] = [answers['nsdb_password']]
+
+    return add_new_record(answers['full_nsdb_admin'], entry)
+
+
+def slapd_config(answers):
+    """
+    Generate and customize slapd.conf
+    slapd must be stopped for this procedure.
+
+    Returns a shell exit status value
+    """
+    ret = replace_slapd_database(answers['dc_backend'])
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = replace_slapd_config(answers)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = add_domaincontroller(answers)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = add_nce(answers)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return add_nsdb_manager(answers)
+
+
+def backup_slapd_backend(backup_dir, backup, nocompress):
+    """
+    Backup the slapd NSDB backend
+
+    Returns a shell exit status
+    """
+    ldif_file = os.path.join(backup_dir, backup + '.ldif')
+
+    ret = run_as_user(LDAP_USERNAME, ['slapcat', '-n2', '-l', ldif_file])
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    if not nocompress:
+        if run_as_user(LDAP_USERNAME, ['gzip', ldif_file]) != EXIT_SUCCESS:
+            log.warning('Failed to compress the backup')
+
+    log.info('Backup "%s" created successfully', backup)
+    return EXIT_SUCCESS
+
+
+def restore_from_ldif(backup_dir, backup):
+    """
+    Restore the slapd NSDB backend
+
+    Returns a shell exit status
+    """
+    log.info('Restoring from backup "%s"...', backup)
+
+    ldif_gzip = os.path.join(backup_dir, backup + '.ldif.gz')
+    if os.path.isfile(ldif_gzip):
+        ret = run_as_user(LDAP_USERNAME, ['gunzip', ldif_gzip])
+        if ret != EXIT_SUCCESS:
+            log.error('Failed to uncompress "%s"', ldif_gzip)
+            return ret
+
+    ldif_file = os.path.join(backup_dir, backup + '.ldif')
+    if not os.path.isfile(ldif_file):
+        log.error('Backup "%s" not found', backup)
+        return EXIT_FAILURE
+
+    return run_as_user(LDAP_USERNAME, ['slapadd', '-n2', '-l', ldif_file])
+
+
+def restore_slapd_backend(backend_dir, backup_dir, backup):
+    """
+    Restore the slapd NSDB backend, assumes slapd is stopped
+
+    Returns a shell exit status
+    """
+    ret = replace_slapd_database(backend_dir)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return restore_from_ldif(backup_dir, backup)
+
+
+__all__ = ['LDAP_UID', 'LDAP_GID', 'NSDB_CERTFILE', 'NSDB_KEYFILE',
+           'local_nce_found', 'local_tls_found', 'get_slapd_status',
+           'check_ldap_connectivity', 'slapd_config',
+           'restore_slapd_backend', 'backup_slapd_backend',
+           'adjust_slapd_log', 'make_ldap_directory']
+
+if __name__ == '__main__':
+    __ut_local_nce_found()
+    __ut_local_tls_found()
+    __ut_get_slapd_backend_dir()
+    __ut_get_slapd_status()
+    __ut_check_ldap_connectivity()
diff --git a/src/PyFedfs/jumpstart/status.py b/src/PyFedfs/jumpstart/status.py
new file mode 100644
index 0000000..31c17cc
--- /dev/null
+++ b/src/PyFedfs/jumpstart/status.py
@@ -0,0 +1,78 @@ 
+"""
+Set up a simple FedFS NSDB using OpenLDAP
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+
+try:
+    import sys
+    import os
+    import logging as log
+
+    from PyFedfs.jumpstart.slapd import get_slapd_status
+    from PyFedfs.jumpstart.slapd import check_ldap_connectivity
+    from PyFedfs.jumpstart.slapd import local_nce_found, local_tls_found
+
+    from PyFedfs.run import EXIT_SUCCESS
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+# pylint: disable-msg=W0613
+def subcmd_status(args):
+    """
+    Display the status of the local LDAP/NSDB service
+
+    Returns a shell exit status value
+    """
+    if not os.path.isdir('/etc/openldap'):
+        log.info('OpenLDAP is not installed on this system')
+        return EXIT_SUCCESS
+
+    log.info(get_slapd_status())
+
+    if check_ldap_connectivity():
+        log.info('Local LDAP service is reachable')
+    else:
+        log.info('Unable to contact local LDAP service')
+
+    if not os.path.isfile('/var/log/slapd'):
+        log.info('Slapd logging is not configured')
+
+    if not os.path.isfile('/etc/openldap/schema/fedfs.schema'):
+        log.info('The FedFS schema file is not installed')
+        return EXIT_SUCCESS
+
+    if local_nce_found():
+        log.info('Local server is an NSDB')
+    else:
+        log.info('Local server is not an NSDB')
+
+    if local_tls_found():
+        log.info('TLS is enabled')
+    else:
+        log.info('TLS is not enabled')
+
+    return EXIT_SUCCESS
+
+
+__all__ = ['subcmd_status']
diff --git a/src/PyFedfs/jumpstart/transaction.py b/src/PyFedfs/jumpstart/transaction.py
new file mode 100644
index 0000000..22c76d7
--- /dev/null
+++ b/src/PyFedfs/jumpstart/transaction.py
@@ -0,0 +1,237 @@ 
+"""
+transaction - a simple way to commit or revert system changes
+
+Part of the PyFedfs module
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+import os
+import time
+import logging as log
+from shutil import rmtree, Error
+
+from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+
+
+class Transaction(object):
+    """
+    Represents a directory or file to be able to revert
+    """
+    def __init__(self):
+        self.items = []
+        self.unique = '%u' % int(time.time())
+        self.state = 'inited'
+
+        log.info('Created transaction %s', self.unique)
+
+    def __backup_name(self, pathname):
+        """
+        Generate name of backup object
+
+        Returns a string
+        """
+        return pathname + '.bak.' + self.unique
+
+    def __iterate(self, func):
+        """
+        Call a function on all items in the transaction list
+
+        Returns a shell exit status value
+        """
+        ret = EXIT_SUCCESS
+        for item in self.items:
+            if func(item) != EXIT_SUCCESS:
+                ret = EXIT_FAILURE
+        return ret
+
+    def __remove_item(self, pathname):
+        """
+        Remove a directory tree or file
+
+        Returns a shell exit status value
+        """
+        if not os.path.exists(pathname):
+            log.debug('No object "%s" to remove', pathname)
+            return EXIT_SUCCESS
+
+        if os.path.isfile(pathname):
+            try:
+                log.debug('Removing file "%s"...', pathname)
+                os.remove(pathname)
+            except OSError:
+                return EXIT_FAILURE
+        else:
+            try:
+                log.debug('Removing directory "%s"...', pathname)
+                rmtree(pathname)
+            except Error:
+                return EXIT_FAILURE
+        return EXIT_SUCCESS
+
+    def __checkpoint_item(self, pathname):
+        """
+        Move existing object out of the way
+
+        Returns a shell exit status value
+        """
+        if not os.path.exists(pathname):
+            log.warning('Checkpoint of non-existing object "%s"', pathname)
+            return EXIT_SUCCESS
+
+        if os.path.exists(self.__backup_name(pathname)):
+            log.error('Checkpoint of "%s" already exists', pathname)
+            return EXIT_FAILURE
+
+        log.debug('Checkpointing "%s"...', pathname)
+
+        try:
+            os.rename(pathname, self.__backup_name(pathname))
+        except OSError:
+            log.error('Failed to checkpoint "%s"', pathname)
+            return EXIT_FAILURE
+        return EXIT_SUCCESS
+
+    def __commit_item(self, pathname):
+        """
+        Remove an object's backup
+
+        Returns a shell exit status value
+        """
+        if not os.path.exists(self.__backup_name(pathname)):
+            log.warning('Backup of object "%s" is missing', pathname)
+            return EXIT_SUCCESS
+
+        log.debug('Committing "%s"...', pathname)
+
+        if self.__remove_item(self.__backup_name(pathname)) != EXIT_SUCCESS:
+            log.error('Failed to remove backup of "%s"', pathname)
+            return EXIT_FAILURE
+
+        return EXIT_SUCCESS
+
+    def __revert_item(self, pathname):
+        """
+        Restore an object from its backup
+
+        Returns a shell exit status value
+        """
+        if not os.path.exists(self.__backup_name(pathname)):
+            log.error('Backup of "%s" was not found', pathname)
+            return EXIT_FAILURE
+
+        if self.__remove_item(pathname) != EXIT_SUCCESS:
+            log.error('Failed to revert "%s"', pathname)
+            return EXIT_FAILURE
+
+        log.info('Reverting "%s"...', pathname)
+
+        try:
+            os.rename(self.__backup_name(pathname), pathname)
+        except OSError:
+            log.error('Failed to revert "%s"', pathname)
+            return EXIT_FAILURE
+        return EXIT_SUCCESS
+
+    def add(self, pathname):
+        """
+        Add filename of an object to be controlled by this transaction
+
+        Returns a shell exit status value
+        """
+        if self.state != 'inited':
+            log.error('"%s" not added to transaction %s: '
+                      'transaction already checkpointed',
+                      pathname, self.unique)
+            return EXIT_FAILURE
+
+        if type(pathname) != str:
+            log.error('Object not added to transaction %s: '
+                      'not a string', self.unique)
+            return EXIT_FAILURE
+
+        if not os.path.exists(pathname):
+            log.debug('"%s" not added to transacion %s: '
+                     'object does not exist', pathname, self.unique)
+            return EXIT_SUCCESS
+
+        self.items.append(pathname)
+        return EXIT_SUCCESS
+
+    def checkpoint(self):
+        """
+        Checkpoint all items in this transaction
+
+        Returns a shell exit status value
+        """
+        if self.state != 'inited':
+            log.warning('Transaction %s has already been checkpointed',
+                        self.unique)
+            return EXIT_SUCCESS
+
+        if len(self.items) == 0:
+            log.error('Transaction %s has no items to checkpoint',
+                      self.unique)
+            return EXIT_FAILURE
+
+        log.info('Checkpointing transaction %s...', self.unique)
+        self.state = 'checkpointed'
+        return self.__iterate(self.__checkpoint_item)
+
+    def commit(self):
+        """
+        Commit all items in this transaction
+
+        Returns a shell exit status value
+        """
+        if self.state == 'inited':
+            log.error('Nothing to commit: transaction %s has '
+                      'not been checkpointed', self.unique)
+            return EXIT_FAILURE
+
+        if self.state != 'checkpointed':
+            log.warning('Transaction %s has already been committed',
+                        self.unique)
+            return EXIT_SUCCESS
+
+        log.info('Committing transaction %s...', self.unique)
+        self.state = 'committed'
+        return self.__iterate(self.__commit_item)
+
+    def revert(self):
+        """
+        Revert all items in this transaction
+
+        Returns a shell exit status value
+        """
+        if self.state == 'inited':
+            log.error('Nothing to commit: transaction %s has '
+                      'not been checkpointed', self.unique)
+            return EXIT_FAILURE
+
+        if self.state != 'checkpointed':
+            log.warning('Transaction %s has already been committed',
+                        self.unique)
+            return EXIT_SUCCESS
+
+        log.info('Reverting transaction %s...', self.unique)
+        self.state = 'reverted'
+        return self.__iterate(self.__revert_item)
+
+__all__ = ['Transaction']
diff --git a/src/PyFedfs/run.py b/src/PyFedfs/run.py
index dc04b92..2ee3dc3 100644
--- a/src/PyFedfs/run.py
+++ b/src/PyFedfs/run.py
@@ -33,14 +33,14 @@  import logging as log
 from subprocess import Popen, PIPE
 
 
-def __run(command):
+def __run(command, shell=False):
     """
     Run a command, ignore all command output, but return exit status
 
     Returns a shell exit status value
     """
     try:
-        process = Popen(command, stdout=PIPE, stderr=PIPE, shell=False)
+        process = Popen(command, stdout=PIPE, stderr=PIPE, shell=shell)
     except OSError:
         log.error('"%s" command did not execute', ' '.join(command))
         return None
@@ -85,6 +85,41 @@  def __ut_run_command():
         print('run_command("ls -l"): %d' % result)
 
 
+def run_shell(line, force=False):
+    """
+    Run a shell command, ignore all command output, but return exit status
+
+    Returns a shell exit status value
+    """
+    log.debug('Running "%s"...', line)
+
+    process = __run(line, shell=True)
+    if process is None:
+        return EXIT_FAILURE
+
+    # pylint: disable-msg=E1101
+    process.wait()
+    # pylint: disable-msg=E1101
+    if process.returncode != 0:
+        if not force:
+            log.error('"%s" returned %d', line, process.returncode)
+            return EXIT_FAILURE
+    return EXIT_SUCCESS
+
+
+def __ut_run_shell():
+    """
+    Unit tests for run_shell
+    """
+    result = run_shell('ls -l')
+    if result == EXIT_SUCCESS:
+        print('run_shell("ls -l") succeeded')
+    elif result == EXIT_FAILURE:
+        print('run_shell("ls -l") failed')
+    else:
+        print('run_shell("ls -l"): %d' % result)
+
+
 def demote(user_uid, user_gid):
     """
     Returns a function that changes the UID and GID of a process
@@ -295,5 +330,6 @@  if __name__ == '__main__':
 
     __ut_check_for_daemon()
     __ut_run_command()
+    __ut_run_shell()
     __ut_run_as_user()
     __ut_systemctl()
diff --git a/src/jumpstart/Makefile.am b/src/jumpstart/Makefile.am
new file mode 100644
index 0000000..0c5723b
--- /dev/null
+++ b/src/jumpstart/Makefile.am
@@ -0,0 +1,40 @@ 
+##
+## @file src/jumpstart/Makefile.am
+## @brief Process this file with automake to produce src/jumpstart/Makefile.in
+##
+
+##
+## Copyright 2013 Oracle.  All rights reserved.
+##
+## This file is part of fedfs-utils.
+##
+## fedfs-utils is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License version 2.0 as
+## published by the Free Software Foundation.
+##
+## fedfs-utils 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 General Public License version 2.0 for more details.
+##
+## You should have received a copy of the GNU General Public License
+## version 2.0 along with fedfs-utils.  If not, see:
+##
+##	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+##
+
+
+bin_SCRIPTS = nsdb-jumpstart
+EXTRA_DIST = nsdb-jumpstart.in
+
+do_substitution = $(SED) -e 's,[@]pythondir[@],$(pythondir),g' \
+	-e 's,[@]PACKAGE[@],$(PACKAGE),g' \
+	-e 's,[@]VERSION[@],$(VERSION),g' \
+	-e 's,[@]STATEDIR[@],$(statedir),g'
+
+nsdb-jumpstart: nsdb-jumpstart.in Makefile
+	$(do_substitution) < $(srcdir)/nsdb-jumpstart.in > nsdb-jumpstart
+	chmod +x nsdb-jumpstart
+
+CLEANFILES		= $(bin_SCRIPTS) cscope.in.out cscope.out cscope.po.out *~
+DISTCLEANFILES		= Makefile.in
diff --git a/src/jumpstart/nsdb-jumpstart.in b/src/jumpstart/nsdb-jumpstart.in
new file mode 100644
index 0000000..fb5b058
--- /dev/null
+++ b/src/jumpstart/nsdb-jumpstart.in
@@ -0,0 +1,119 @@ 
+#!/usr/bin/env python
+
+"""
+Run the NSDB jump-start administration tool
+"""
+
+__copyright__ = """
+Copyright 2013 Oracle.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2.0
+as published by the Free Software Foundation.
+
+This program 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 General Public License version 2.0 for more details.
+
+A copy of the GNU General Public License version 2.0 is
+available here:
+
+    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+"""
+
+import sys
+import os
+import argparse
+import logging as log
+
+sys.path.insert(1, '@pythondir@')
+
+try:
+    from PyFedfs.run import EXIT_FAILURE
+
+    from PyFedfs.jumpstart.install import subcmd_install
+    from PyFedfs.jumpstart.backup import subcmd_backup, subcmd_restore
+    from PyFedfs.jumpstart.status import subcmd_status
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def main():
+    """
+    Domainroot helper main program
+
+    Returns a shell exit status value
+    """
+    if os.getegid() != 0:
+        print >> sys.stderr, 'You must be root to run nsdb-jumpstart.'
+        return EXIT_FAILURE
+
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        description='Jump-start a simple NSDB service',
+        epilog='''\
+Copyright 2013 Oracle.  All rights reserved.
+
+License GPLv2: <http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt>
+This is free software.  You are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.''')
+    parser.add_argument('--version',
+                        help='Display the version of this command',
+                        action='version',
+                        version='@PACKAGE@ @VERSION@')
+    parser.add_argument('--statedir',
+                        help='FedFS state dir (default @STATEDIR@)',
+                        default='@STATEDIR@')
+    subparsers = parser.add_subparsers(title='Sub-commands')
+
+    install_parser = subparsers.add_parser('install',
+                                           help='Install an NSDB')
+    install_parser.add_argument('--security',
+                                choices=['none', 'tls'],
+                                help='security strength')
+    install_parser.set_defaults(func=subcmd_install)
+
+    backup_parser = subparsers.add_parser('backup',
+                                          help='Backup the local NSDB')
+    backup_parser.add_argument('--nocompress',
+                               help='Do not compress the backup file',
+                               action='store_true')
+    backup_parser.set_defaults(func=subcmd_backup)
+
+    restore_parser = subparsers.add_parser('restore',
+                                           help='Restore NSDB from backup')
+    restore_parser.add_argument('backup',
+                                nargs='?', default='',
+                                help='Which backup to restore')
+    restore_parser.set_defaults(func=subcmd_restore)
+
+    status_parser = subparsers.add_parser('status',
+                                          help='Display status of NSDB '
+                                               'service')
+    status_parser.set_defaults(func=subcmd_status)
+
+    args = parser.parse_args()
+
+    log.basicConfig(level=log.DEBUG,
+                    format='%(asctime)s %(name)-12s '
+                           '%(levelname)-8s %(message)s',
+                    datefmt='%m-%d %H:%M',
+                    filename='/var/lib/fedfs/nsdb-jumpstart.log',
+                    filemode='a')
+    console = log.StreamHandler()
+    console.setLevel(log.INFO)
+    formatter = log.Formatter('%(levelname)-8s: %(message)s')
+    console.setFormatter(formatter)
+    log.getLogger('').addHandler(console)
+
+    return args.func(args)
+
+
+try:
+    if __name__ == '__main__':
+        sys.exit(main())
+except (SystemExit, KeyboardInterrupt, RuntimeError):
+    sys.exit(1)