/*
 * Copyright (c) 1997-2011 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. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgment:
 *      This product includes software developed by the University of
 *      California, Berkeley and its contributors.
 * 4. 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) {
    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, mcm@unx.sas.com
   */
  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);
  if (ep)
    XFREE(ep);
  if (fp)
    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);
}
