/*      $NetBSD: amfs_host.c,v 1.1.1.3 2015/01/17 16:34:15 christos Exp $       */

/*
* Copyright (c) 1997-2014 Erez Zadok
* Copyright (c) 1990 Jan-Simon Pendry
* Copyright (c) 1990 Imperial College of Science, Technology & Medicine
* Copyright (c) 1990 The Regents of the University of California.
* All rights reserved.
*
* This code is derived from software contributed to Berkeley by
* Jan-Simon Pendry at Imperial College, London.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
* 3. Neither the name of the University nor the names of its contributors
*    may be used to endorse or promote products derived from this software
*    without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*
* File: am-utils/amd/amfs_host.c
*
*/

/*
* NFS host file system.
* Mounts all exported filesystems from a given host.
* This has now degenerated into a mess but will not
* be rewritten.  Amd 6 will support the abstractions
* needed to make this work correctly.
*/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <am_defs.h>
#include <amd.h>

static char *amfs_host_match(am_opts *fo);
static int amfs_host_init(mntfs *mf);
static int amfs_host_mount(am_node *am, mntfs *mf);
static int amfs_host_umount(am_node *am, mntfs *mf);
static void amfs_host_umounted(mntfs *mf);

/*
* Ops structure
*/
am_ops amfs_host_ops =
{
 "host",
 amfs_host_match,
 amfs_host_init,
 amfs_host_mount,
 amfs_host_umount,
 amfs_error_lookup_child,
 amfs_error_mount_child,
 amfs_error_readdir,
 0,                            /* amfs_host_readlink */
 0,                            /* amfs_host_mounted */
 amfs_host_umounted,
 find_nfs_srvr,
 0,                            /* amfs_host_get_wchan */
 FS_MKMNT | FS_BACKGROUND | FS_AMQINFO,
#ifdef HAVE_FS_AUTOFS
 AUTOFS_HOST_FS_FLAGS,
#endif /* HAVE_FS_AUTOFS */
};


/*
* Determine the mount point:
*
* The next change we put in to better handle PCs.  This is a bit
* disgusting, so you'd better sit down.  We change the make_mntpt function
* to look for exported file systems without a leading '/'.  If they don't
* have a leading '/', we add one.  If the export is 'a:' through 'z:'
* (without a leading slash), we change it to 'a%' (or b% or z%).  This
* allows the entire PC disk to be mounted.
*/
static void
make_mntpt(char *mntpt, size_t l, const exports ex, const char *mf_mount)
{
 if (ex->ex_dir[0] == '/') {
   if (ex->ex_dir[1] == 0)
     xstrlcpy(mntpt, mf_mount, l);
   else
     xsnprintf(mntpt, l, "%s%s", mf_mount, ex->ex_dir);
 } else if (ex->ex_dir[0] >= 'a' &&
            ex->ex_dir[0] <= 'z' &&
            ex->ex_dir[1] == ':' &&
            ex->ex_dir[2] == '/' &&
            ex->ex_dir[3] == 0)
   xsnprintf(mntpt, l, "%s/%c%%", mf_mount, ex->ex_dir[0]);
 else
   xsnprintf(mntpt, l, "%s/%s", mf_mount, ex->ex_dir);
}


/*
* Execute needs the same as NFS plus a helper command
*/
static char *
amfs_host_match(am_opts *fo)
{
 extern am_ops nfs_ops;

 /*
  * Make sure rfs is specified to keep nfs_match happy...
  */
 if (!fo->opt_rfs)
   fo->opt_rfs = "/";

 return (*nfs_ops.fs_match) (fo);
}


static int
amfs_host_init(mntfs *mf)
{
 u_short mountd_port;

 if (strchr(mf->mf_info, ':') == 0)
   return ENOENT;

 /*
  * This is primarily to schedule a wakeup so that as soon
  * as our fileserver is ready, we can continue setting up
  * the host filesystem.  If we don't do this, the standard
  * amfs_auto code will set up a fileserver structure, but it will
  * have to wait for another nfs request from the client to come
  * in before finishing.  Our way is faster since we don't have
  * to wait for the client to resend its request (which could
  * take a second or two).
  */
 /*
  * First, we find the fileserver for this mntfs and then call
  * get_mountd_port with our mntfs passed as the wait channel.
  * get_mountd_port will check some things and then schedule
  * it so that when the fileserver is ready, a wakeup is done
  * on this mntfs.   amfs_cont() is already sleeping on this mntfs
  * so as soon as that wakeup happens amfs_cont() is called and
  * this mount is retried.
  */
 if (mf->mf_server)
   /*
    * We don't really care if there's an error returned.
    * Since this is just to help speed things along, the
    * error will get handled properly elsewhere.
    */
   get_mountd_port(mf->mf_server, &mountd_port, get_mntfs_wchan(mf));

 return 0;
}


static int
do_mount(am_nfs_handle_t *fhp, char *mntdir, char *fs_name, mntfs *mf)
{
 struct stat stb;

 dlog("amfs_host: mounting fs %s on %s\n", fs_name, mntdir);

 (void) mkdirs(mntdir, 0555);
 if (stat(mntdir, &stb) < 0 || (stb.st_mode & S_IFMT) != S_IFDIR) {
   plog(XLOG_ERROR, "No mount point for %s - skipping", mntdir);
   return ENOENT;
 }

 return mount_nfs_fh(fhp, mntdir, fs_name, mf);
}


static int
sortfun(const voidp x, const voidp y)
{
 exports *a = (exports *) x;
 exports *b = (exports *) y;

 return strcmp((*a)->ex_dir, (*b)->ex_dir);
}


/*
* Get filehandle
*/
static int
fetch_fhandle(CLIENT *client, char *dir, am_nfs_handle_t *fhp, u_long nfs_version)
{
 struct timeval tv;
 enum clnt_stat clnt_stat;
 struct fhstatus res;
#ifdef HAVE_FS_NFS3
 struct am_mountres3 res3;
#endif /* HAVE_FS_NFS3 */

 /*
  * Pick a number, any number...
  */
 tv.tv_sec = 20;
 tv.tv_usec = 0;

 dlog("Fetching fhandle for %s", dir);

 /*
  * Call the mount daemon on the remote host to
  * get the filehandle.  Use NFS version specific call.
  */

 plog(XLOG_INFO, "fetch_fhandle: NFS version %d", (int) nfs_version);
#ifdef HAVE_FS_NFS3
 if (nfs_version == NFS_VERSION3
#ifdef HAVE_FS_NFS4
#ifndef NO_FALLBACK
     || nfs_version == NFS_VERSION4
#endif /* NO_FALLBACK */
#endif /* HAVE_FS_NFS4 */
   ) {

   memset((char *) &res3, 0, sizeof(res3));
   clnt_stat = clnt_call(client,
                         MOUNTPROC_MNT,
                         (XDRPROC_T_TYPE) xdr_dirpath,
                         (SVC_IN_ARG_TYPE) &dir,
                         (XDRPROC_T_TYPE) xdr_am_mountres3,
                         (SVC_IN_ARG_TYPE) &res3,
                         tv);
   if (clnt_stat != RPC_SUCCESS) {
     plog(XLOG_ERROR, "mountd rpc failed: %s", clnt_sperrno(clnt_stat));
     return EIO;
   }
   /* Check the status of the filehandle */
   if ((errno = res3.fhs_status)) {
     dlog("fhandle fetch for mount version 3 failed: %m");
     return errno;
   }
   memset((voidp) &fhp->v3, 0, sizeof(am_nfs_fh3));
   fhp->v3.am_fh3_length = res3.mountres3_u.mountinfo.fhandle.fhandle3_len;
   memmove(fhp->v3.am_fh3_data,
           res3.mountres3_u.mountinfo.fhandle.fhandle3_val,
           fhp->v3.am_fh3_length);
 } else {                      /* not NFS_VERSION3 mount */
#endif /* HAVE_FS_NFS3 */
   clnt_stat = clnt_call(client,
                         MOUNTPROC_MNT,
                         (XDRPROC_T_TYPE) xdr_dirpath,
                         (SVC_IN_ARG_TYPE) &dir,
                         (XDRPROC_T_TYPE) xdr_fhstatus,
                         (SVC_IN_ARG_TYPE) &res,
                         tv);
   if (clnt_stat != RPC_SUCCESS) {
     plog(XLOG_ERROR, "mountd rpc failed: %s", clnt_sperrno(clnt_stat));
     return EIO;
   }
   /* Check status of filehandle */
   if (res.fhs_status) {
     errno = res.fhs_status;
     dlog("fhandle fetch for mount version 1 failed: %m");
     return errno;
   }
   memmove(&fhp->v2, &res.fhs_fh, NFS_FHSIZE);
#ifdef HAVE_FS_NFS3
 } /* end of "if (nfs_version == NFS_VERSION3)" statement */
#endif /* HAVE_FS_NFS3 */

 /* all is well */
 return 0;
}


/*
* Scan mount table to see if something already mounted
*/
static int
already_mounted(mntlist *mlist, char *dir)
{
 mntlist *ml;

 for (ml = mlist; ml; ml = ml->mnext)
   if (STREQ(ml->mnt->mnt_dir, dir))
     return 1;
 return 0;
}


static int
amfs_host_mount(am_node *am, mntfs *mf)
{
 struct timeval tv2;
 CLIENT *client;
 enum clnt_stat clnt_stat;
 int n_export;
 int j, k;
 exports exlist = 0, ex;
 exports *ep = NULL;
 am_nfs_handle_t *fp = NULL;
 char *host;
 int error = 0;
 struct sockaddr_in sin;
 int sock = RPC_ANYSOCK;
 int ok = FALSE;
 mntlist *mlist;
 char fs_name[MAXPATHLEN], *rfs_dir;
 char mntpt[MAXPATHLEN];
 struct timeval tv;
 u_long mnt_version;

 /*
  * WebNFS servers don't necessarily run mountd.
  */
 if (mf->mf_flags & MFF_WEBNFS) {
   plog(XLOG_ERROR, "amfs_host_mount: cannot support WebNFS");
   return EIO;
 }

 /*
  * Read the mount list
  */
 mlist = read_mtab(mf->mf_mount, mnttab_file_name);

#ifdef MOUNT_TABLE_ON_FILE
 /*
  * Unlock the mount list
  */
 unlock_mntlist();
#endif /* MOUNT_TABLE_ON_FILE */

 /*
  * Take a copy of the server hostname, address, and nfs version
  * to mount version conversion.
  */
 host = mf->mf_server->fs_host;
 sin = *mf->mf_server->fs_ip;
 plog(XLOG_INFO, "amfs_host_mount: NFS version %d", (int) mf->mf_server->fs_version);
#ifdef HAVE_FS_NFS3
 if (mf->mf_server->fs_version == NFS_VERSION3)
   mnt_version = AM_MOUNTVERS3;
 else
#endif /* HAVE_FS_NFS3 */
   mnt_version = MOUNTVERS;

 /*
  * The original 10 second per try timeout is WAY too large, especially
  * if we're only waiting 10 or 20 seconds max for the response.
  * That would mean we'd try only once in 10 seconds, and we could
  * lose the transmit or receive packet, and never try again.
  * A 2-second per try timeout here is much more reasonable.
  * 09/28/92 Mike Mitchell, [email protected]
  */
 tv.tv_sec = 2;
 tv.tv_usec = 0;

 /*
  * Create a client attached to mountd
  */
 client = get_mount_client(host, &sin, &tv, &sock, mnt_version);
 if (client == NULL) {
#ifdef HAVE_CLNT_SPCREATEERROR
   plog(XLOG_ERROR, "get_mount_client failed for %s: %s",
        host, clnt_spcreateerror(""));
#else /* not HAVE_CLNT_SPCREATEERROR */
   plog(XLOG_ERROR, "get_mount_client failed for %s", host);
#endif /* not HAVE_CLNT_SPCREATEERROR */
   error = EIO;
   goto out;
 }
 if (!nfs_auth) {
   error = make_nfs_auth();
   if (error)
     goto out;
 }
 client->cl_auth = nfs_auth;

 dlog("Fetching export list from %s", host);

 /*
  * Fetch the export list
  */
 tv2.tv_sec = 10;
 tv2.tv_usec = 0;
 clnt_stat = clnt_call(client,
                       MOUNTPROC_EXPORT,
                       (XDRPROC_T_TYPE) xdr_void,
                       0,
                       (XDRPROC_T_TYPE) xdr_exports,
                       (SVC_IN_ARG_TYPE) & exlist,
                       tv2);
 if (clnt_stat != RPC_SUCCESS) {
   const char *msg = clnt_sperrno(clnt_stat);
   plog(XLOG_ERROR, "host_mount rpc failed: %s", msg);
   /* clnt_perror(client, "rpc"); */
   error = EIO;
   goto out;
 }

 /*
  * Figure out how many exports were returned
  */
 for (n_export = 0, ex = exlist; ex; ex = ex->ex_next) {
   n_export++;
 }

 /*
  * Allocate an array of pointers into the list
  * so that they can be sorted.  If the filesystem
  * is already mounted then ignore it.
  */
 ep = (exports *) xmalloc(n_export * sizeof(exports));
 for (j = 0, ex = exlist; ex; ex = ex->ex_next) {
   make_mntpt(mntpt, sizeof(mntpt), ex, mf->mf_mount);
   if (already_mounted(mlist, mntpt))
     /* we have at least one mounted f/s, so don't fail the mount */
     ok = TRUE;
   else
     ep[j++] = ex;
 }
 n_export = j;

 /*
  * Sort into order.
  * This way the mounts are done in order down the tree,
  * instead of any random order returned by the mount
  * daemon (the protocol doesn't specify...).
  */
 qsort(ep, n_export, sizeof(exports), sortfun);

 /*
  * Allocate an array of filehandles
  */
 fp = (am_nfs_handle_t *) xmalloc(n_export * sizeof(am_nfs_handle_t));

 /*
  * Try to obtain filehandles for each directory.
  * If a fetch fails then just zero out the array
  * reference but discard the error.
  */
 for (j = k = 0; j < n_export; j++) {
   /* Check and avoid a duplicated export entry */
   if (j > k && ep[k] && STREQ(ep[j]->ex_dir, ep[k]->ex_dir)) {
     dlog("avoiding dup fhandle requested for %s", ep[j]->ex_dir);
     ep[j] = NULL;
   } else {
     k = j;
     error = fetch_fhandle(client, ep[j]->ex_dir, &fp[j],
                           mf->mf_server->fs_version);
     if (error)
       ep[j] = NULL;
   }
 }

 /*
  * Mount each filesystem for which we have a filehandle.
  * If any of the mounts succeed then mark "ok" and return
  * error code 0 at the end.  If they all fail then return
  * the last error code.
  */
 xstrlcpy(fs_name, mf->mf_info, sizeof(fs_name));
 if ((rfs_dir = strchr(fs_name, ':')) == (char *) NULL) {
   plog(XLOG_FATAL, "amfs_host_mount: mf_info has no colon");
   error = EINVAL;
   goto out;
 }
 ++rfs_dir;
 for (j = 0; j < n_export; j++) {
   ex = ep[j];
   if (ex) {
     /*
      * Note: the sizeof space left in rfs_dir is what's left in fs_name
      * after strchr() above returned a pointer _inside_ fs_name.  The
      * calculation below also takes into account that rfs_dir was
      * incremented by the ++ above.
      */
     xstrlcpy(rfs_dir, ex->ex_dir, sizeof(fs_name) - (rfs_dir - fs_name));
     make_mntpt(mntpt, sizeof(mntpt), ex, mf->mf_mount);
     if (do_mount(&fp[j], mntpt, fs_name, mf) == 0)
       ok = TRUE;
   }
 }

 /*
  * Clean up and exit
  */
out:
 discard_mntlist(mlist);
 XFREE(ep);
 XFREE(fp);
 if (sock != RPC_ANYSOCK)
   (void) amu_close(sock);
 if (client)
   clnt_destroy(client);
 if (exlist)
   xdr_pri_free((XDRPROC_T_TYPE) xdr_exports, (caddr_t) &exlist);
 if (ok)
   return 0;
 return error;
}


/*
* Return true if pref is a directory prefix of dir.
*
* XXX TODO:
* Does not work if pref is "/".
*/
static int
directory_prefix(char *pref, char *dir)
{
 int len = strlen(pref);

 if (!NSTREQ(pref, dir, len))
   return FALSE;
 if (dir[len] == '/' || dir[len] == '\0')
   return TRUE;
 return FALSE;
}


/*
* Unmount a mount tree
*/
static int
amfs_host_umount(am_node *am, mntfs *mf)
{
 mntlist *ml, *mprev;
 int unmount_flags = (mf->mf_flags & MFF_ON_AUTOFS) ? AMU_UMOUNT_AUTOFS : 0;
 int xerror = 0;

 /*
  * Read the mount list
  */
 mntlist *mlist = read_mtab(mf->mf_mount, mnttab_file_name);

#ifdef MOUNT_TABLE_ON_FILE
 /*
  * Unlock the mount list
  */
 unlock_mntlist();
#endif /* MOUNT_TABLE_ON_FILE */

 /*
  * Reverse list...
  */
 ml = mlist;
 mprev = NULL;
 while (ml) {
   mntlist *ml2 = ml->mnext;
   ml->mnext = mprev;
   mprev = ml;
   ml = ml2;
 }
 mlist = mprev;

 /*
  * Unmount all filesystems...
  */
 for (ml = mlist; ml && !xerror; ml = ml->mnext) {
   char *dir = ml->mnt->mnt_dir;
   if (directory_prefix(mf->mf_mount, dir)) {
     int error;
     dlog("amfs_host: unmounts %s", dir);
     /*
      * Unmount "dir"
      */
     error = UMOUNT_FS(dir, mnttab_file_name, unmount_flags);
     /*
      * Keep track of errors
      */
     if (error) {
       /*
        * If we have not already set xerror and error is not ENOENT,
        * then set xerror equal to error and log it.
        * 'xerror' is the return value for this function.
        *
        * We do not want to pass ENOENT as an error because if the
        * directory does not exists our work is done anyway.
        */
       if (!xerror && error != ENOENT)
         xerror = error;
       if (error != EBUSY) {
         errno = error;
         plog(XLOG_ERROR, "Tree unmount of %s failed: %m", ml->mnt->mnt_dir);
       }
     } else {
       (void) rmdirs(dir);
     }
   }
 }

 /*
  * Throw away mount list
  */
 discard_mntlist(mlist);

 /*
  * Try to remount, except when we are shutting down.
  */
 if (xerror && amd_state != Finishing) {
   xerror = amfs_host_mount(am, mf);
   if (!xerror) {
     /*
      * Don't log this - it's usually too verbose
      plog(XLOG_INFO, "Remounted host %s", mf->mf_info);
      */
     xerror = EBUSY;
   }
 }
 return xerror;
}


/*
* Tell mountd we're done.
* This is not quite right, because we may still
* have other filesystems mounted, but the existing
* mountd protocol is badly broken anyway.
*/
static void
amfs_host_umounted(mntfs *mf)
{
 char *host;
 CLIENT *client;
 enum clnt_stat clnt_stat;
 struct sockaddr_in sin;
 int sock = RPC_ANYSOCK;
 struct timeval tv;
 u_long mnt_version;

 if (mf->mf_error || mf->mf_refc > 1 || !mf->mf_server)
   return;

 /*
  * WebNFS servers shouldn't ever get here.
  */
 if (mf->mf_flags & MFF_WEBNFS) {
   plog(XLOG_ERROR, "amfs_host_umounted: cannot support WebNFS");
   return;
 }

 /*
  * Take a copy of the server hostname, address, and NFS version
  * to mount version conversion.
  */
 host = mf->mf_server->fs_host;
 sin = *mf->mf_server->fs_ip;
 plog(XLOG_INFO, "amfs_host_umounted: NFS version %d", (int) mf->mf_server->fs_version);
#ifdef HAVE_FS_NFS3
 if (mf->mf_server->fs_version == NFS_VERSION3)
   mnt_version = AM_MOUNTVERS3;
 else
#endif /* HAVE_FS_NFS3 */
   mnt_version = MOUNTVERS;

 /*
  * Create a client attached to mountd
  */
 tv.tv_sec = 10;
 tv.tv_usec = 0;
 client = get_mount_client(host, &sin, &tv, &sock, mnt_version);
 if (client == NULL) {
#ifdef HAVE_CLNT_SPCREATEERROR
   plog(XLOG_ERROR, "get_mount_client failed for %s: %s",
        host, clnt_spcreateerror(""));
#else /* not HAVE_CLNT_SPCREATEERROR */
   plog(XLOG_ERROR, "get_mount_client failed for %s", host);
#endif /* not HAVE_CLNT_SPCREATEERROR */
   goto out;
 }

 if (!nfs_auth) {
   if (make_nfs_auth())
     goto out;
 }
 client->cl_auth = nfs_auth;

 dlog("Unmounting all from %s", host);

 clnt_stat = clnt_call(client,
                       MOUNTPROC_UMNTALL,
                       (XDRPROC_T_TYPE) xdr_void,
                       0,
                       (XDRPROC_T_TYPE) xdr_void,
                       0,
                       tv);
 if (clnt_stat != RPC_SUCCESS && clnt_stat != RPC_SYSTEMERROR) {
   /* RPC_SYSTEMERROR seems to be returned for no good reason ... */
   const char *msg = clnt_sperrno(clnt_stat);
   plog(XLOG_ERROR, "unmount all from %s rpc failed: %s", host, msg);
   goto out;
 }

out:
 if (sock != RPC_ANYSOCK)
   (void) amu_close(sock);
 if (client)
   clnt_destroy(client);
}