diff mbox

[4/5] Add a script to configure domain roots

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

Commit Message

Chuck Lever Oct. 29, 2013, 7:42 p.m. UTC
Simplify the process of setting up FedFS domain roots by introducing
a tool called "fedfs-domainroot" that can handle the details.

Sub-commands:
  {clean,status,add,remove}
    clean               Remove FedFS domain root infrastructure
    status              Display current FedFS domain root configuration
    add                 Add a new domain root directory
    remove              Remove a domain root directory

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 .gitignore                           |    1 
 configure.ac                         |    2 
 doc/man/Makefile.am                  |    3 
 doc/man/fedfs-domainroot.8           |  305 ++++++++++++++++++++++++++++++++++
 src/Makefile.am                      |    2 
 src/PyFedfs/Makefile.am              |    1 
 src/PyFedfs/domainroot/Makefile.am   |   31 +++
 src/PyFedfs/domainroot/__init__.py   |   23 +++
 src/PyFedfs/domainroot/addremove.py  |  175 ++++++++++++++++++++
 src/PyFedfs/domainroot/clean.py      |   86 ++++++++++
 src/PyFedfs/domainroot/exports.py    |  173 +++++++++++++++++++
 src/PyFedfs/domainroot/mounts.py     |  168 +++++++++++++++++++
 src/PyFedfs/domainroot/parse_file.py |   54 ++++++
 src/PyFedfs/domainroot/paths.py      |   24 +++
 src/PyFedfs/domainroot/status.py     |   61 +++++++
 src/domainroot/Makefile.am           |   40 ++++
 src/domainroot/fedfs-domainroot.in   |  117 +++++++++++++
 17 files changed, 1264 insertions(+), 2 deletions(-)
 create mode 100644 doc/man/fedfs-domainroot.8
 create mode 100644 src/PyFedfs/domainroot/Makefile.am
 create mode 100644 src/PyFedfs/domainroot/__init__.py
 create mode 100644 src/PyFedfs/domainroot/addremove.py
 create mode 100644 src/PyFedfs/domainroot/clean.py
 create mode 100644 src/PyFedfs/domainroot/exports.py
 create mode 100644 src/PyFedfs/domainroot/mounts.py
 create mode 100644 src/PyFedfs/domainroot/parse_file.py
 create mode 100644 src/PyFedfs/domainroot/paths.py
 create mode 100644 src/PyFedfs/domainroot/status.py
 create mode 100644 src/domainroot/Makefile.am
 create mode 100644 src/domainroot/fedfs-domainroot.in
diff mbox

Patch

diff --git a/.gitignore b/.gitignore
index ab759f1..407e013 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@  Doxygen/
 *.o
 *.lo
 *.la
+src/domainroot/fedfs-domainroot
 src/fedfsd/fedfsd
 src/nsdbparams/nsdbparams
 src/nfsref/nfsref
diff --git a/configure.ac b/configure.ac
index fd70d92..7b83a2c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -183,6 +183,7 @@  AC_CONFIG_FILES([Makefile
                  doc/man/Makefile
                  doc/rpcl/Makefile
                  src/Makefile
+                 src/domainroot/Makefile
                  src/fedfsc/Makefile
                  src/fedfsd/Makefile
                  src/include/Makefile
@@ -197,5 +198,6 @@  AC_CONFIG_FILES([Makefile
                  src/nsdbc/Makefile
                  src/nsdbparams/Makefile
                  src/PyFedfs/Makefile
+                 src/PyFedfs/domainroot/Makefile
                  src/plug-ins/Makefile])
 AC_OUTPUT
diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am
index 19230b1..65e9e9c 100644
--- a/doc/man/Makefile.am
+++ b/doc/man/Makefile.am
@@ -38,7 +38,8 @@  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_CLIENT_CMDS) $(NSDB_CLIENT_CMDS)
+			  nsdbparams.8 fedfs-domainroot.8 \
+			  $(FEDFS_CLIENT_CMDS) $(NSDB_CLIENT_CMDS)
 
 CLEANFILES		= cscope.in.out cscope.out cscope.po.out *~
 DISTCLEANFILES		= Makefile.in
diff --git a/doc/man/fedfs-domainroot.8 b/doc/man/fedfs-domainroot.8
new file mode 100644
index 0000000..359a416
--- /dev/null
+++ b/doc/man/fedfs-domainroot.8
@@ -0,0 +1,305 @@ 
+.\"@(#)fedfs-domainroot.8"
+.\"
+.\" @file doc/man/fedfs-domainroot.8
+.\" @brief man page for fedfs-domainroot 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 FEDFS-DOMAINROOT 8 "@publication-date@"
+.SH NAME
+fedfs-domainroot \- set up FedFS domain root infrastructure
+.SH SYNOPSIS
+.B fedfs-domainroot
+.RB [ \-h ", " \-\-help ]
+.RB [ \-\-version ]
+.P
+.B fedfs-domainroot
+.RB [ \-\-silent ]
+.RB [ \-\-statedir =
+.IR statedir ]
+.B add
+.I domainname
+.P
+.B fedfs-domainroot
+.RB [ \-\-silent ]
+.RB [ \-\-statedir =
+.IR statedir ]
+.B remove
+.I domainname
+.RB [ \-\-force ]
+.P
+.B fedfs-domainroot
+.RB [ \-\-silent ]
+.RB [ \-\-statedir =
+.IR statedir ]
+.B status
+.P
+.B fedfs-domainroot
+.RB [ \-\-silent ]
+.RB [ \-\-statedir =
+.IR statedir ]
+.B clean
+.RB [ \-\-force ]
+.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
+The top directory of a FedFS domain namespace is known as a
+.I domain root
+directory.
+FedFS-enabled clients discover the fileserver that exports
+a FedFS domain's root directory using a DNS SRV query.
+Using a well-known export path,
+clients then mount the domain root directory
+on that fileserver in the normal fashion.
+.P
+After a filesystem client mounts a domain's root directory,
+applications on that client descend into the domain's name space
+starting in that directory,
+and are directed transparently to exports on other fileservers.
+.P
+Further information about domain roots is available in
+.BR fedfs (7).
+.P
+.SH DESCRIPTION
+A single fileserver may host domain root directories
+for one or more FedFS domains.
+The
+.BR fedfs-domainroot (8)
+command is a convenient way to securely manage domain root exports
+on a Linux NFS fileserver.
+FedFS itself is agnostic about the underlying file-access protocol,
+but the
+.BR fedfs-domainroot (8)
+command supports only NFS at this time.
+.P
+FedFS domain root directories are exported using a standard well-known
+pathname to make it simple for clients to find them.
+The first component of the domain root's export pathname is always
+.IR /.domainroot .
+The second component is a FedFS domain name.
+.P
+For instance, the export pathname of the domain root of the
+.I example.net
+FedFS domain is
+.IR /.domainroot/example.net .
+.SS Operation
+The
+.B add
+subcommand creates a directory under
+.I @statedir@/domainroots
+where the contents of the domain root directory reside.
+A directory is also set up under
+.I /.domainroot
+for each doman root directory.
+.BR fedfs-domainroot (8)
+bind-mounts the domain root directory under
+.I @statedir@/domainroots,
+then exports the directory under
+.IR /.domainroot .
+.P
+In this way, each domain root directory is exported via a well-known pathname,
+and can have its own export settings separate from other domain root directories,
+including security settings and client and network designations.
+These can be modified by editing
+.I /etc/exports
+after the
+domain root export is created.
+.P
+The final step of setting up a FedFS domain is adding a set of DNS SRV
+records that direct FedFS-enabled clients to the fileserver
+where the domain's root directory resides.
+Adding DNS SRV records is outside the scope of the
+.BR fedfs-domainroot (8)
+command.
+.P
+The
+.BR fedfs-domainroot (8)
+command must run as root in order to create and remove NFS exports
+and entries in
+.IR /etc/fstab .
+.SS Subcommands
+Valid
+.BR fedfs-domainroot (8)
+subcommands are:
+.IP "\fBclean\fP"
+Remove the
+.I /.domainroot
+directory and other infrastructure (as long as it is empty).
+The user is asked to confirm before action is taken.
+.IP
+By default, this process stops when a step encounters an error.
+Adding the
+.B \-\-force
+option forces the process to try each step even if an error occurs,
+and bypasses the confirmation request.
+.IP "\fBstatus\fP"
+Display the status of the domain root infrastructure on the local system.
+This includes whether NFSD is running, and what domain root directories
+are currently configured and exported.
+This subcommand takes no arguments.
+.IP "\fBadd\fP"
+Create a new FedFS domain root directory under
+.I /.domainroot
+and export it.
+This subcommand takes a FedFS domain name as an argument.
+.IP "\fBremove\fP"
+Remove an existing FedFS domain root directory from
+.IR /.domainroot .
+This subcommand takes a FedFS domain name as an argument.
+The user is asked to confirm before action is taken.
+.IP
+By default, this process stops when a step encounters an error.
+Adding the
+.B \-\-force
+option forces the process to try each step even if an error occurs,
+and bypasses the confirmation request.
+.SS Command line options
+The following options are specified before the subcommand on the command line.
+.IP "\fB\-h, \-\-help"
+Display usage and copyright information, then exit.
+.IP "\fB\-\-version"
+Display fedfs-utils version information, then exit.
+.IP "\fB\-\-silent"
+Process quietly.
+.IP "\fB\-\-statedir=\fIstate-directory\fP"
+Find FedFS domain root directories on the local system in the
+.I domainroots
+subdirectory of the specified directory.
+By default, the state directory is
+.IR @statedir .
+.SH EXIT CODES
+The
+.BR fedfs-domainroot (8)
+command returns one of two values upon exit.
+.TP
+.B 0
+The requested subcommand succeeded.
+.TP
+.B 1
+The requested subcommand failed.
+.SH EXAMPLES
+Suppose you are the FedFS administrator of the
+.I example.net
+FedFS domain.
+After you have chosen a reliable NFS fileserver to serve your
+FedFS domain root directory, log in on that fileserver as root
+and ensure that NFSD is running.
+.P
+To create a new FedFS domain root for the
+.I example.net
+domain, use:
+.RS
+.sp
+# fedfs-domainroot --silent add example.net
+.br
+Added domain root for FedFS domain "example.net"
+.br
+#
+.sp
+.RE
+To populate the new domain root, change your current directory to
+.IR /.domainroot/example.net ,
+then add junctions with the
+.BR nfsref (8)
+command on the fileserver.
+.P
+You can list the domain roots that are currently exported
+by your fileserver with:
+.RS
+.sp
+# fedfs-domainroot --silent status
+.br
+FedFS domain roots:
+.br
+        example.net is exported with options
+              *(ro,subtree_check,mp,insecure,sec=sys:none)
+.br
+#
+.sp
+.RE
+When you want to remove this domain root (say, because you have
+moved it to another fileserver), remove it's contents, then use:
+.RS
+.sp
+# fedfs-domainroot remove example.net
+.br
+Removed domain root for FedFS domain "example.net"
+.br
+#
+.RE
+.SH SECURITY
+FedFS domain root exports created by
+.BR fedfs-domainroot (8)
+are exported with
+.BR *(ro,insecure,subtree_check,sec=sys:none) .
+FedFS standards recommend that
+FedFS domain root directories should be globally readable.
+Specific access restrictions typically occur lower in a domain's name space.
+.P
+However, fileserver administrators can alter
+a domain root export's security settings
+by editing a domain root export's entry in
+.IR /etc/exports ,
+and then refreshing the kernel's export cache with
+.BR "exportfs -r" .
+.P
+For example, if the domain root fileserver has Kerberos configured,
+an administrator might change a domain root export's
+.B sec=
+option to
+.BR sec=krb5p:krb5i:krb5:sys:none .
+Or, to restrict the range of clients that can access the
+domain root, an administrator might replace the leading
+.B *
+with a specific netgroup or IP network designation.
+.P
+It is recommended to keep the
+.B subtree_check
+export option.
+Refer to
+.BR exports (5)
+for details.
+.SH FILES
+.TP
+.I @statedir@/domainroots
+directory containing domain root directories
+.TP
+.I /.domainroot
+directory containing domain root exports
+.SH "SEE ALSO"
+.BR fedfs (7),
+.BR nfsref (8),
+.BR rpc.fedfsd (8),
+.BR exportfs (8),
+.BR exports (5)
+.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 c7dff86..9f84ebc 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
+			  plug-ins PyFedfs domainroot
 
 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 be67d2a..bef9d11 100644
--- a/src/PyFedfs/Makefile.am
+++ b/src/PyFedfs/Makefile.am
@@ -23,6 +23,7 @@ 
 ##	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 ##
 
+SUBDIRS			= domainroot
 pyfedfs_PYTHON		= __init__.py run.py userinput.py utilities.py
 pyfedfsdir		= $(pythondir)/PyFedfs
 
diff --git a/src/PyFedfs/domainroot/Makefile.am b/src/PyFedfs/domainroot/Makefile.am
new file mode 100644
index 0000000..a36a4c0
--- /dev/null
+++ b/src/PyFedfs/domainroot/Makefile.am
@@ -0,0 +1,31 @@ 
+##
+## @file src/PyFedfs/domainroot/Makefile.am
+## @brief Process this file with automake to produce src/domainroot/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
+##
+
+pyfedfs_PYTHON		= __init__.py addremove.py clean.py status.py \
+			  exports.py mounts.py paths.py parse_file.py
+pyfedfsdir		= $(pythondir)/PyFedfs/domainroot
+
+CLEANFILES		= cscope.in.out cscope.out cscope.po.out *~
+DISTCLEANFILES		= Makefile.in
diff --git a/src/PyFedfs/domainroot/__init__.py b/src/PyFedfs/domainroot/__init__.py
new file mode 100644
index 0000000..5513e49
--- /dev/null
+++ b/src/PyFedfs/domainroot/__init__.py
@@ -0,0 +1,23 @@ 
+"""
+PyFedfs
+
+This module contains components of the fedfs-domainroot 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/domainroot/addremove.py b/src/PyFedfs/domainroot/addremove.py
new file mode 100644
index 0000000..2363350
--- /dev/null
+++ b/src/PyFedfs/domainroot/addremove.py
@@ -0,0 +1,175 @@ 
+"""
+Set up FedFS domain root infrastructure on an NFS 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 logging as log
+
+    from PyFedfs.domainroot.paths import DOMAINROOT_PATH
+    from PyFedfs.domainroot.paths import DOMAINROOTS_DIR_PATH
+    from PyFedfs.domainroot.mounts import add_mount, remove_mount
+    from PyFedfs.domainroot.exports import add_export, remove_export
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import check_for_daemon
+    from PyFedfs.userinput import confirm
+    from PyFedfs.utilities import make_directory, list_directory
+    from PyFedfs.utilities import remove_directory
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def prepare_domainroot(domainroots_dir_path):
+    """
+    Ensure domainroot infrastructure is set up
+
+    Returns a shell exit status value
+    """
+
+    try:
+        os.mkdir(DOMAINROOT_PATH)
+    except OSError as inst:
+        if inst.errno != 17:
+            log.exception('Failed to make directory "%s"',
+                          DOMAINROOT_PATH)
+            return EXIT_FAILURE
+
+    parent = os.path.dirname(domainroots_dir_path)
+    if not os.path.isdir(parent):
+        log.error('"%s" does not exist', parent)
+        return EXIT_FAILURE
+
+    try:
+        os.mkdir(domainroots_dir_path)
+    except OSError as inst:
+        if inst.errno != 17:
+            log.exception('Failed to make directory "%s"',
+                          domainroots_dir_path)
+            return EXIT_FAILURE
+
+    return EXIT_SUCCESS
+
+
+def do_add(export_pathname, bind_pathname):
+    """
+    Add a domain root directory
+
+    Returns a shell exit status value
+    """
+    ret = make_directory(bind_pathname, 0755)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = add_mount(bind_pathname, export_pathname, 'bind')
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return add_export(export_pathname)
+
+
+def do_remove(export_pathname, bind_pathname, force):
+    """
+    Remove a domain root directory
+
+    Returns a shell exit status value
+    """
+    ret = remove_export(export_pathname, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = remove_mount(export_pathname, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = remove_directory(bind_pathname, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return EXIT_SUCCESS
+
+
+# No verification of the domain argument is done.  A DNS SRV lookup
+# would be surest, but that would require the SRV record exist a
+# priori... Otherwise we'd need to regular expression of some kind
+# that can match both ASCII and IDNA hostnames but not IP addresses.
+def subcmd_add(args):
+    """
+    The 'add domain' subcommand
+
+    Returns a shell exit status value
+    """
+    domainroots_dir_path = os.path.join(args.statedir, DOMAINROOTS_DIR_PATH)
+
+    ret = prepare_domainroot(domainroots_dir_path)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    if not check_for_daemon('nfsd'):
+        log.warn('NFSD is not running')
+
+    export_pathname = os.path.join(DOMAINROOT_PATH, args.domainname)
+    bind_pathname = os.path.join(domainroots_dir_path, args.domainname)
+
+    ret = do_add(export_pathname, bind_pathname)
+    if ret != EXIT_SUCCESS:
+        do_remove(export_pathname, bind_pathname, True)
+        return ret
+
+    print('Added domain root for FedFS domain "%s"' % args.domainname)
+    return EXIT_SUCCESS
+
+
+def subcmd_remove(args):
+    """
+    The 'remove domain' subcommand
+
+    Returns a shell exit status value
+    """
+    domainroots_dir_path = os.path.join(args.statedir, DOMAINROOTS_DIR_PATH)
+
+    export_pathname = os.path.join(DOMAINROOT_PATH, args.domainname)
+    bind_pathname = os.path.join(domainroots_dir_path, args.domainname)
+
+    if not args.force:
+        items = list_directory(bind_pathname)
+        if len(items):
+            log.error('The domain root directory is not empty')
+            return EXIT_FAILURE
+        print('This tool will remove the domain root directory ' \
+              'for the "%s" domain' % args.domainname)
+        if not confirm('Do you want to continue? [y/N] '):
+            print('Command aborted')
+            return EXIT_FAILURE
+
+    ret = do_remove(export_pathname, bind_pathname, args.force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    if not args.force:
+        print('Removed domain root for FedFS domain "%s"' % args.domainname)
+    return EXIT_SUCCESS
+
+
+__all__ = ['subcmd_add', 'subcmd_remove']
diff --git a/src/PyFedfs/domainroot/clean.py b/src/PyFedfs/domainroot/clean.py
new file mode 100644
index 0000000..eb08db6
--- /dev/null
+++ b/src/PyFedfs/domainroot/clean.py
@@ -0,0 +1,86 @@ 
+"""
+Set up FedFS domain root infrastructure on an NFS 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 logging as log
+
+    from PyFedfs.domainroot.paths import DOMAINROOT_PATH
+    from PyFedfs.domainroot.paths import DOMAINROOTS_DIR_PATH
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.userinput import confirm
+    from PyFedfs.utilities import list_directory, remove_directory
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def do_clean(domainroots_dir_path, force):
+    """
+    Remove FedFS domainroot infrastructure
+
+    Returns a shell exit status value
+    """
+    ret = remove_directory(DOMAINROOT_PATH, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = remove_directory(domainroots_dir_path, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return EXIT_SUCCESS
+
+
+def subcmd_clean(args):
+    """
+    The 'clean infrastructure' subcommand
+
+    Returns a shell exit status value
+    """
+
+    if not args.force:
+        items = list_directory(DOMAINROOT_PATH)
+        if len(items):
+            log.error('There still exist some domain root directories')
+            return EXIT_FAILURE
+        print('This tool will remove all FedFS domain root infrastructure')
+        if not confirm('Do you want to continue? [y/N] '):
+            print('Command aborted')
+            return EXIT_FAILURE
+
+    domainroots_dir_path = os.path.join(args.statedir, DOMAINROOTS_DIR_PATH)
+
+    ret = do_clean(domainroots_dir_path, args.force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    if not args.force:
+        print('Successfully cleaned FedFS domain root infrastructure')
+    return EXIT_SUCCESS
+
+
+__all__ = ['subcmd_clean']
diff --git a/src/PyFedfs/domainroot/exports.py b/src/PyFedfs/domainroot/exports.py
new file mode 100644
index 0000000..20597dc
--- /dev/null
+++ b/src/PyFedfs/domainroot/exports.py
@@ -0,0 +1,173 @@ 
+"""
+Manage NFS exports
+"""
+
+__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 logging as log
+    import augeas
+
+    from PyFedfs.domainroot.parse_file import parse_file
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import run_command
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def filesystem_is_exported(pathname):
+    """
+    Predicate: is filesystem exported?
+
+    Returns True if "pathname" is exported, otherwise returns False
+    """
+    exports = parse_file('/var/lib/nfs/etab')
+    for export in exports:
+        if export[0] == pathname:
+            return True
+    return False
+
+
+def add_exports_entry(pathname):
+    """
+    Add entry to /etc/exports
+
+    Returns a shell exit status value
+    """
+    log.debug('Adding entry for "%s" to /etc/exports...', pathname)
+
+    config = augeas.augeas()
+
+    config.set('/files/etc/exports/dir[last()+1]', pathname)
+    config.set('/files/etc/exports/dir[last()]/client[1]', '*')
+    config.set('/files/etc/exports/dir[last()]/client[1]/option[1]',
+               'ro')
+    config.set('/files/etc/exports/dir[last()]/client[1]/option[2]',
+               'mp')
+    config.set('/files/etc/exports/dir[last()]/client[1]/option[3]',
+               'subtree_check')
+    config.set('/files/etc/exports/dir[last()]/client[1]/option[4]',
+               'insecure')
+    config.set('/files/etc/exports/dir[last()]/client[1]/option[5]',
+               'sec=sys:none')
+
+    ret = EXIT_SUCCESS
+    try:
+        config.save()
+    except IOError:
+        log.exception('Failed to save /etc/exports')
+        ret = EXIT_FAILURE
+
+    config.close()
+
+    return ret
+
+
+def exports_contains_entry(pathname):
+    """
+    Predicate: does /etc/exports contain an entry for "pathname" ?
+
+    Returns True if /etc/exports contains an entry for "pathname"
+    otherwise returns False
+    """
+    config = augeas.augeas()
+    path = '/files/etc/exports/dir[.="' + pathname + '"]'
+    exports = config.match(path)
+    config.close()
+    if len(exports):
+        return True
+    return False
+
+
+def remove_exports_entry(pathname, force):
+    """
+    Remove an entry from /etc/exports
+
+    Returns a shell exit status value
+    """
+    log.debug('Removing entry for "%s" from /etc/exports...', pathname)
+
+    ret = EXIT_FAILURE
+    config = augeas.augeas()
+
+    path = '/files/etc/exports/dir[.="' + pathname + '"]'
+    matches = config.match(path)
+    if len(matches) == 1:
+        config.remove(matches[0])
+        config.save()
+        ret = EXIT_SUCCESS
+    else:
+        if not force:
+            log.error('No entry for "%s" in /etc/exports', pathname)
+
+    config.close()
+
+    if force:
+        return EXIT_SUCCESS
+    return ret
+
+
+def add_export(pathname):
+    """
+    Add an export
+
+    Returns a shell exit status value
+    """
+    ret = add_exports_entry(pathname)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return run_command(['exportfs', '*:' + pathname], False)
+
+
+def display_export(pathname):
+    """
+    Display the status of a domain root export
+
+    Returns no value
+    """
+    exports = parse_file('/var/lib/nfs/etab')
+    for export in exports:
+        if export[0] == pathname:
+            print('\t"%s" is exported with options' % export[0])
+            print('\t\t%s' % export[1])
+            break
+    else:
+        print('\t"%s" is not exported' % pathname)
+
+
+def remove_export(pathname, force):
+    """
+    Remove an export
+
+    Returns a shell exit status value
+    """
+    ret = run_command(['exportfs', '-u', '*:' + pathname], force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = remove_exports_entry(pathname, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return EXIT_SUCCESS
diff --git a/src/PyFedfs/domainroot/mounts.py b/src/PyFedfs/domainroot/mounts.py
new file mode 100644
index 0000000..a17f2e1
--- /dev/null
+++ b/src/PyFedfs/domainroot/mounts.py
@@ -0,0 +1,168 @@ 
+"""
+Manage local mounts
+"""
+
+__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 logging as log
+    import augeas
+
+    from PyFedfs.domainroot.parse_file import parse_file
+
+    from PyFedfs.run import EXIT_SUCCESS, EXIT_FAILURE
+    from PyFedfs.run import run_command
+    from PyFedfs.utilities import change_mode
+    from PyFedfs.utilities import make_directory, remove_directory
+except ImportError:
+    print >> sys.stderr, \
+        'Could not import a required Python module:', sys.exc_value
+    sys.exit(1)
+
+
+def filesystem_is_mounted(pathname):
+    """
+    Predicate: is filesystem mounted?
+
+    Returns True if "pathname" is mounted, otherwise returns False
+    """
+    mounts = parse_file('/proc/mounts')
+    for mount in mounts:
+        if mount[1] == pathname:
+            return True
+    return False
+
+
+def add_fstab_entry(devicepath, mounted_on, fs_type):
+    """
+    Add entry to /etc/fstab
+
+    Returns a shell exit status value
+    """
+    log.debug('Adding entry for "%s" to /etc/fstab...', devicepath)
+
+    config = augeas.augeas()
+
+    config.set('/files/etc/fstab/01/spec', devicepath)
+    config.set('/files/etc/fstab/01/file', mounted_on)
+    config.set('/files/etc/fstab/01/vfstype', fs_type)
+    if fs_type == 'bind':
+        config.set('/files/etc/fstab/01/opt', 'bind')
+    else:
+        config.set('/files/etc/fstab/01/opt', 'defaults')
+    config.set('/files/etc/fstab/01/dump', '1')
+    config.set('/files/etc/fstab/01/passno', '2')
+
+    ret = EXIT_SUCCESS
+    try:
+        config.save()
+    except IOError:
+        log.exception('Failed to save /etc/fstab')
+        ret = EXIT_FAILURE
+
+    config.close()
+
+    return ret
+
+
+def fstab_contains_entry(pathname):
+    """
+    Predicate: does /etc/fstab contain an entry for "pathname" ?
+
+    Returns True if /etc/fstab contains an entry for "pathname"
+    otherwise returns False
+    """
+    config = augeas.augeas()
+    path = '/files/etc/fstab/*[file="' + pathname + '"]'
+    fstab = config.match(path)
+    config.close()
+    if len(fstab):
+        return True
+    return False
+
+
+def remove_fstab_entry(pathname, force):
+    """
+    Remove an entry from /etc/fstab
+
+    Returns a shell exit status value
+    """
+    log.debug('Removing entry for "%s" from /etc/fstab...', pathname)
+
+    ret = EXIT_FAILURE
+    config = augeas.augeas()
+
+    path = '/files/etc/fstab/*[file="' + pathname + '"]'
+    matches = config.match(path)
+    if len(matches) == 1:
+        config.remove(matches[0])
+        config.save()
+        ret = EXIT_SUCCESS
+    else:
+        if not force:
+            log.error('No entry for "%s" in /etc/fstab', pathname)
+
+    config.close()
+
+    if force:
+        return EXIT_SUCCESS
+    return ret
+
+
+def add_mount(pathname, mounted_on, fs_type):
+    """
+    Add a mount point
+
+    Returns a shell exit status value
+    """
+    ret = add_fstab_entry(pathname, mounted_on, fs_type)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = make_directory(mounted_on, 0755)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = run_command(['mount', mounted_on], False)
+    if ret != EXIT_SUCCESS:
+        return EXIT_FAILURE
+
+    return change_mode(mounted_on, 0755)
+
+
+def remove_mount(pathname, force):
+    """
+    Remove a mount
+
+    Returns a shell exit status value
+    """
+    ret = run_command(['umount', pathname], force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = remove_directory(pathname, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    ret = remove_fstab_entry(pathname, force)
+    if ret != EXIT_SUCCESS:
+        return ret
+
+    return EXIT_SUCCESS
diff --git a/src/PyFedfs/domainroot/parse_file.py b/src/PyFedfs/domainroot/parse_file.py
new file mode 100644
index 0000000..b92d9e6
--- /dev/null
+++ b/src/PyFedfs/domainroot/parse_file.py
@@ -0,0 +1,54 @@ 
+"""
+Parse a text file into a list of lists
+"""
+
+__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 logging as log
+
+
+def parse_file(pathname):
+    """
+    Read a configuration file into a list of lists
+
+    I could use the csv module for this, but some files may contain
+    interspersed blanks and tabs, which csv does not handle.
+    Can't use augeas, as some files aren't under /etc.
+
+    Returns a list containing each line in "pathname".
+    Each line is parsed into a list of the line's fields.
+    """
+    try:
+        file_object = open(pathname, 'r')
+    except OSError as inst:
+        log.debug('Failed to open "%s": %s', pathname, inst)
+        return []
+
+    ret = []
+    try:
+        for line in file_object:
+            stripped = line.strip()
+            if len(stripped):
+                items = stripped.split()
+                if items[0][0] != '#':
+                    ret.append(items)
+    finally:
+        file_object.close()
+
+    return ret
diff --git a/src/PyFedfs/domainroot/paths.py b/src/PyFedfs/domainroot/paths.py
new file mode 100644
index 0000000..f53bff8
--- /dev/null
+++ b/src/PyFedfs/domainroot/paths.py
@@ -0,0 +1,24 @@ 
+"""
+Common directory pathnames for fedfs-domainroot 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
+"""
+
+DOMAINROOT_PATH = '/.domainroot'
+DOMAINROOTS_DIR_PATH = 'domainroots'
diff --git a/src/PyFedfs/domainroot/status.py b/src/PyFedfs/domainroot/status.py
new file mode 100644
index 0000000..1dd9a45
--- /dev/null
+++ b/src/PyFedfs/domainroot/status.py
@@ -0,0 +1,61 @@ 
+"""
+Set up FedFS domain root infrastructure on an NFS 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
+
+    from PyFedfs.domainroot.paths import DOMAINROOT_PATH
+    from PyFedfs.domainroot.exports import display_export
+
+    from PyFedfs.run import EXIT_SUCCESS
+    from PyFedfs.run import check_for_daemon
+    from PyFedfs.utilities import list_directory
+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):
+    """
+    The 'display status' subcommand
+
+    Returns a shell exit status value
+    """
+    if not check_for_daemon('nfsd'):
+        print('NFSD is not running')
+        return EXIT_SUCCESS
+
+    output = list_directory(DOMAINROOT_PATH)
+    if len(output):
+        print('FedFS domain roots:')
+        for item in output:
+            display_export(os.path.join(DOMAINROOT_PATH, item))
+    else:
+        print('FedFS domain roots:  None')
+
+    return EXIT_SUCCESS
+
+
+__all__ = ['subcmd_status']
diff --git a/src/domainroot/Makefile.am b/src/domainroot/Makefile.am
new file mode 100644
index 0000000..26a3c2e
--- /dev/null
+++ b/src/domainroot/Makefile.am
@@ -0,0 +1,40 @@ 
+##
+## @file src/domainroot/Makefile.am
+## @brief Process this file with automake to produce src/domainroot/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 = fedfs-domainroot
+EXTRA_DIST = fedfs-domainroot.in
+
+do_substitution = $(SED) -e 's,[@]pythondir[@],$(pythondir),g' \
+	-e 's,[@]PACKAGE[@],$(PACKAGE),g' \
+	-e 's,[@]VERSION[@],$(VERSION),g' \
+	-e 's,[@]STATEDIR[@],$(statedir),g'
+
+fedfs-domainroot: fedfs-domainroot.in Makefile
+	$(do_substitution) < $(srcdir)/fedfs-domainroot.in > fedfs-domainroot
+	chmod +x fedfs-domainroot
+
+CLEANFILES		= $(bin_SCRIPTS) cscope.in.out cscope.out cscope.po.out *~
+DISTCLEANFILES		= Makefile.in
diff --git a/src/domainroot/fedfs-domainroot.in b/src/domainroot/fedfs-domainroot.in
new file mode 100644
index 0000000..e8878de
--- /dev/null
+++ b/src/domainroot/fedfs-domainroot.in
@@ -0,0 +1,117 @@ 
+#!/usr/bin/env python
+
+"""
+Run the domainroot 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.domainroot.addremove import subcmd_add, subcmd_remove
+    from PyFedfs.domainroot.clean import subcmd_clean
+    from PyFedfs.domainroot.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 fedfs-domainroot.'
+        return EXIT_FAILURE
+
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        description='Set up FedFS domain root infrastructure',
+        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('--silent',
+                        help='Display less output',
+                        action='store_true')
+    parser.add_argument('--statedir',
+                        help='FedFS state dir (default @STATEDIR@)',
+                        default='@STATEDIR@')
+    subparsers = parser.add_subparsers(title='Sub-commands')
+
+    add_parser = subparsers.add_parser('add',
+                                       help='Add a new domain root directory')
+    add_parser.add_argument('domainname',
+                            help='Name of FedFS domain to add')
+    add_parser.set_defaults(func=subcmd_add)
+
+    remove_parser = subparsers.add_parser('remove',
+                                          help='Remove a domain root '
+                                               'directory')
+    remove_parser.add_argument('--force',
+                               help='Ignore errors', action='store_true')
+    remove_parser.add_argument('domainname',
+                               help='Name of FedFS domain to remove')
+    remove_parser.set_defaults(func=subcmd_remove)
+
+    status_parser = subparsers.add_parser('status',
+                                          help='Display current FedFS domain '
+                                               'root configuration')
+    status_parser.set_defaults(func=subcmd_status)
+
+    clean_parser = subparsers.add_parser('clean',
+                                         help='Remove domain root '
+                                              'infrastructure')
+    clean_parser.add_argument('--force',
+                              help='Ignore errors', action='store_true')
+    clean_parser.set_defaults(func=subcmd_clean)
+
+    args = parser.parse_args()
+
+    if args.silent:
+        log.basicConfig(format='%(levelname)s: %(message)s')
+    else:
+        log.basicConfig(format='%(levelname)s: %(message)s',
+                        level=log.DEBUG)
+
+    return args.func(args)
+
+
+try:
+    if __name__ == '__main__':
+        sys.exit(main())
+except (SystemExit, KeyboardInterrupt, RuntimeError):
+    sys.exit(1)