/*
 * 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/srvr_nfs.c
 *
 */

/*
 * NFS server modeling
 */

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

/*
 * Number of pings allowed to fail before host is declared down
 * - three-fifths of the allowed mount time...
 */
#define	MAX_ALLOWED_PINGS	(3 + /* for luck ... */ 1)

/*
 * How often to ping when starting a new server
 */
#define	FAST_NFS_PING		3

#if (FAST_NFS_PING * MAX_ALLOWED_PINGS) >= ALLOWED_MOUNT_TIME
# error: sanity check failed in srvr_nfs.c
/*
 * you cannot do things this way...
 * sufficient fast pings must be given the chance to fail
 * within the allowed mount time
 */
#endif /* (FAST_NFS_PING * MAX_ALLOWED_PINGS) >= ALLOWED_MOUNT_TIME */

/* structures and typedefs */
typedef struct nfs_private {
  u_short np_mountd;		/* Mount daemon port number */
  char np_mountd_inval;		/* Port *may* be invalid */
  int np_ping;			/* Number of failed ping attempts */
  time_t np_ttl;		/* Time when server is thought dead */
  int np_xid;			/* RPC transaction id for pings */
  int np_error;			/* Error during portmap request */
} nfs_private;

/* globals */
qelem nfs_srvr_list = {&nfs_srvr_list, &nfs_srvr_list};

/* statics */
static int global_xid;		/* For NFS pings */
#define	XID_ALLOC()		(++global_xid)

#ifdef HAVE_FS_NFS3
# define NUM_NFS_VERS 2
#else  /* not HAVE_FS_NFS3 */
# define NUM_NFS_VERS 1
#endif /* not HAVE_FS_NFS3 */
static int ping_len[NUM_NFS_VERS];
static char ping_buf[NUM_NFS_VERS][sizeof(struct rpc_msg) + 32];

#if defined(MNTTAB_OPT_PROTO) || defined(HAVE_FS_NFS3)
/*
 * Protocols we know about, in order of preference.
 *
 * Note that Solaris 8 and newer NetBSD systems are switching to UDP first,
 * so this order may have to be adjusted for Amd in the future once more
 * vendors make that change. -Erez 11/24/2000
 *
 * Or we might simply make this is a platform-specific order. -Ion 09/13/2003
 */
static char *protocols[] = { "tcp", "udp", NULL };
#endif /* defined(MNTTAB_OPT_PROTO) || defined(HAVE_FS_NFS3) */

/* forward definitions */
static void nfs_keepalive(voidp);


/*
 * Flush cached data for an fserver (or for all, if fs==NULL)
 */
void
flush_srvr_nfs_cache(fserver *fs)
{
  fserver *fs2 = NULL;

  ITER(fs2, fserver, &nfs_srvr_list) {
    if (fs == NULL || fs == fs2) {
      nfs_private *np = (nfs_private *) fs2->fs_private;
      if (np) {
	np->np_mountd_inval = TRUE;
	np->np_error = -1;
      }
    }
  }
}


/*
 * Startup the NFS ping for a particular version.
 */
static void
create_ping_payload(u_long nfs_version)
{
  XDR ping_xdr;
  struct rpc_msg ping_msg;

  /*
   * Non nfs mounts like /afs/glue.umd.edu have ended up here.
   */
  if (nfs_version == 0) {
    nfs_version = NFS_VERSION;
    plog(XLOG_WARNING, "create_ping_payload: nfs_version = 0, changed to 2");
  } else
    plog(XLOG_INFO, "create_ping_payload: nfs_version: %d", (int) nfs_version);

  rpc_msg_init(&ping_msg, NFS_PROGRAM, nfs_version, NFSPROC_NULL);

  /*
   * Create an XDR endpoint
   */
  xdrmem_create(&ping_xdr, ping_buf[nfs_version - NFS_VERSION], sizeof(ping_buf[0]), XDR_ENCODE);

  /*
   * Create the NFS ping message
   */
  if (!xdr_callmsg(&ping_xdr, &ping_msg)) {
    plog(XLOG_ERROR, "Couldn't create ping RPC message");
    going_down(3);
  }
  /*
   * Find out how long it is
   */
  ping_len[nfs_version - NFS_VERSION] = xdr_getpos(&ping_xdr);

  /*
   * Destroy the XDR endpoint - we don't need it anymore
   */
  xdr_destroy(&ping_xdr);
}


/*
 * Called when a portmap reply arrives
 */
static void
got_portmap(voidp pkt, int len, struct sockaddr_in *sa, struct sockaddr_in *ia, voidp idv, int done)
{
  fserver *fs2 = (fserver *) idv;
  fserver *fs = NULL;

  /*
   * Find which fileserver we are talking about
   */
  ITER(fs, fserver, &nfs_srvr_list)
    if (fs == fs2)
      break;

  if (fs == fs2) {
    u_long port = 0;	/* XXX - should be short but protocol is naff */
    int error = done ? pickup_rpc_reply(pkt, len, (voidp) &port, (XDRPROC_T_TYPE) xdr_u_long) : -1;
    nfs_private *np = (nfs_private *) fs->fs_private;

    if (!error && port) {
      dlog("got port (%d) for mountd on %s", (int) port, fs->fs_host);
      /*
       * Grab the port number.  Portmap sends back
       * an u_long in native ordering, so it
       * needs converting to a u_short in
       * network ordering.
       */
      np->np_mountd = htons((u_short) port);
      np->np_mountd_inval = FALSE;
      np->np_error = 0;
    } else {
      dlog("Error fetching port for mountd on %s", fs->fs_host);
      dlog("\t error=%d, port=%d", error, (int) port);
      /*
       * Almost certainly no mountd running on remote host
       */
      np->np_error = error ? error : ETIMEDOUT;
    }

    if (fs->fs_flags & FSF_WANT)
      wakeup_srvr(fs);
  } else if (done) {
    dlog("Got portmap for old port request");
  } else {
    dlog("portmap request timed out");
  }
}


/*
 * Obtain portmap information
 */
static int
call_portmap(fserver *fs, AUTH *auth, u_long prog, u_long vers, u_long prot)
{
  struct rpc_msg pmap_msg;
  int len;
  char iobuf[UDPMSGSIZE];
  int error;
  struct pmap pmap;

  rpc_msg_init(&pmap_msg, PMAPPROG, PMAPVERS, PMAPPROC_NULL);
  pmap.pm_prog = prog;
  pmap.pm_vers = vers;
  pmap.pm_prot = prot;
  pmap.pm_port = 0;
  len = make_rpc_packet(iobuf,
			sizeof(iobuf),
			PMAPPROC_GETPORT,
			&pmap_msg,
			(voidp) &pmap,
			(XDRPROC_T_TYPE) xdr_pmap,
			auth);
  if (len > 0) {
    struct sockaddr_in sin;
    memset((voidp) &sin, 0, sizeof(sin));
    sin = *fs->fs_ip;
    sin.sin_port = htons(PMAPPORT);
    error = fwd_packet(RPC_XID_PORTMAP, iobuf, len,
		       &sin, &sin, (voidp) fs, got_portmap);
  } else {
    error = -len;
  }

  return error;
}


static void
recompute_portmap(fserver *fs)
{
  int error;
  u_long mnt_version;

  /*
   * No portmap calls for pure WebNFS servers.
   */
  if (fs->fs_flags & FSF_WEBNFS)
    return;

  if (nfs_auth)
    error = 0;
  else
    error = make_nfs_auth();

  if (error) {
    nfs_private *np = (nfs_private *) fs->fs_private;
    np->np_error = error;
    return;
  }

  if (fs->fs_version == 0)
    plog(XLOG_WARNING, "recompute_portmap: nfs_version = 0 fixed");

  plog(XLOG_INFO, "recompute_portmap: NFS version %d on %s",
       (int) fs->fs_version, fs->fs_host);
#ifdef HAVE_FS_NFS3
  if (fs->fs_version == NFS_VERSION3)
    mnt_version = AM_MOUNTVERS3;
  else
#endif /* HAVE_FS_NFS3 */
    mnt_version = MOUNTVERS;

  plog(XLOG_INFO, "Using MOUNT version: %d", (int) mnt_version);
  call_portmap(fs, nfs_auth, MOUNTPROG, mnt_version, (u_long) IPPROTO_UDP);
}


int
get_mountd_port(fserver *fs, u_short *port, wchan_t wchan)
{
  int error = -1;
  if (FSRV_ISDOWN(fs))
    return EWOULDBLOCK;

  if (FSRV_ISUP(fs)) {
    nfs_private *np = (nfs_private *) fs->fs_private;
    if (np->np_error == 0) {
      *port = np->np_mountd;
      error = 0;
    } else {
      error = np->np_error;
    }
    /*
     * Now go get the port mapping again in case it changed.
     * Note that it is used even if (np_mountd_inval)
     * is True.  The flag is used simply as an
     * indication that the mountd may be invalid, not
     * that it is known to be invalid.
     */
    if (np->np_mountd_inval)
      recompute_portmap(fs);
    else
      np->np_mountd_inval = TRUE;
  }
  if (error < 0 && wchan && !(fs->fs_flags & FSF_WANT)) {
    /*
     * If a wait channel is supplied, and no
     * error has yet occurred, then arrange
     * that a wakeup is done on the wait channel,
     * whenever a wakeup is done on this fs node.
     * Wakeup's are done on the fs node whenever
     * it changes state - thus causing control to
     * come back here and new, better things to happen.
     */
    fs->fs_flags |= FSF_WANT;
    sched_task(wakeup_task, wchan, (wchan_t) fs);
  }
  return error;
}


/*
 * This is called when we get a reply to an RPC ping.
 * The value of id was taken from the nfs_private
 * structure when the ping was transmitted.
 */
static void
nfs_keepalive_callback(voidp pkt, int len, struct sockaddr_in *sp, struct sockaddr_in *tsp, voidp idv, int done)
{
  int xid = (long) idv;		/* cast needed for 64-bit archs */
  fserver *fs;
  int found_map = 0;

  if (!done)
    return;

  /*
   * For each node...
   */
  ITER(fs, fserver, &nfs_srvr_list) {
    nfs_private *np = (nfs_private *) fs->fs_private;
    if (np->np_xid == xid && (fs->fs_flags & FSF_PINGING)) {
      /*
       * Reset the ping counter.
       * Update the keepalive timer.
       * Log what happened.
       */
      if (fs->fs_flags & FSF_DOWN) {
	fs->fs_flags &= ~FSF_DOWN;
	if (fs->fs_flags & FSF_VALID) {
	  srvrlog(fs, "is up");
	} else {
	  if (np->np_ping > 1)
	    srvrlog(fs, "ok");
	  else
	    srvrlog(fs, "starts up");
	  fs->fs_flags |= FSF_VALID;
	}

	map_flush_srvr(fs);
      } else {
	if (fs->fs_flags & FSF_VALID) {
	  dlog("file server %s type nfs is still up", fs->fs_host);
	} else {
	  if (np->np_ping > 1)
	    srvrlog(fs, "ok");
	  fs->fs_flags |= FSF_VALID;
	}
      }

      /*
       * Adjust ping interval
       */
      untimeout(fs->fs_cid);
      fs->fs_cid = timeout(fs->fs_pinger, nfs_keepalive, (voidp) fs);

      /*
       * Update ttl for this server
       */
      np->np_ttl = clocktime(NULL) +
	(MAX_ALLOWED_PINGS - 1) * FAST_NFS_PING + fs->fs_pinger - 1;

      /*
       * New RPC xid...
       */
      np->np_xid = XID_ALLOC();

      /*
       * Failed pings is zero...
       */
      np->np_ping = 0;

      /*
       * Recompute portmap information if not known
       */
      if (np->np_mountd_inval)
	recompute_portmap(fs);

      found_map++;
      break;
    }
  }

  if (found_map == 0)
    dlog("Spurious ping packet");
}


static void
check_fs_addr_change(fserver *fs)
{
  struct hostent *hp = NULL;
  struct in_addr ia;
  char *old_ipaddr, *new_ipaddr;

  hp = gethostbyname(fs->fs_host);
  if (!hp ||
      hp->h_addrtype != AF_INET ||
      !STREQ((char *) hp->h_name, fs->fs_host) ||
      memcmp((voidp) &fs->fs_ip->sin_addr,
	     (voidp) hp->h_addr,
	     sizeof(fs->fs_ip->sin_addr)) == 0)
    return;
  /* if got here: downed server changed IP address */
  old_ipaddr = strdup(inet_ntoa(fs->fs_ip->sin_addr));
  memmove((voidp) &ia, (voidp) hp->h_addr, sizeof(struct in_addr));
  new_ipaddr = inet_ntoa(ia);	/* ntoa uses static buf */
  plog(XLOG_WARNING, "EZK: down fileserver %s changed ip: %s -> %s",
       fs->fs_host, old_ipaddr, new_ipaddr);
  XFREE(old_ipaddr);
  /* copy new IP addr */
  memmove((voidp) &fs->fs_ip->sin_addr,
	  (voidp) hp->h_addr,
	  sizeof(fs->fs_ip->sin_addr));
  /* XXX: do we need to un/set these flags? */
  fs->fs_flags &= ~FSF_DOWN;
  fs->fs_flags |= FSF_VALID | FSF_WANT;
  map_flush_srvr(fs);		/* XXX: a race with flush_srvr_nfs_cache? */
  flush_srvr_nfs_cache(fs);
  fs->fs_flags |= FSF_FORCE_UNMOUNT;

#if 0
  flush_nfs_fhandle_cache(fs);	/* done in caller: nfs_keepalive_timeout */
  /* XXX: need to purge nfs_private so that somehow it will get re-initialized? */
#endif /* 0 */
}


/*
 * Called when no ping-reply received
 */
static void
nfs_keepalive_timeout(voidp v)
{
  fserver *fs = v;
  nfs_private *np = (nfs_private *) fs->fs_private;

  /*
   * Another ping has failed
   */
  np->np_ping++;
  if (np->np_ping > 1)
    srvrlog(fs, "not responding");

  /*
   * Not known to be up any longer
   */
  if (FSRV_ISUP(fs))
    fs->fs_flags &= ~FSF_VALID;

  /*
   * If ttl has expired then guess that it is dead
   */
  if (np->np_ttl < clocktime(NULL)) {
    int oflags = fs->fs_flags;
    dlog("ttl has expired");
    if ((fs->fs_flags & FSF_DOWN) == 0) {
      /*
       * Server was up, but is now down.
       */
      srvrlog(fs, "is down");
      fs->fs_flags |= FSF_DOWN | FSF_VALID;
      /*
       * Since the server is down, the portmap
       * information may now be wrong, so it
       * must be flushed from the local cache
       */
      flush_nfs_fhandle_cache(fs);
      np->np_error = -1;
      check_fs_addr_change(fs); /* check if IP addr of fserver changed */
    } else {
      /*
       * Known to be down
       */
      if ((fs->fs_flags & FSF_VALID) == 0)
	srvrlog(fs, "starts down");
      fs->fs_flags |= FSF_VALID;
    }
    if (oflags != fs->fs_flags && (fs->fs_flags & FSF_WANT))
      wakeup_srvr(fs);
    /*
     * Reset failed ping count
     */
    np->np_ping = 0;
  } else {
    if (np->np_ping > 1)
      dlog("%d pings to %s failed - at most %d allowed", np->np_ping, fs->fs_host, MAX_ALLOWED_PINGS);
  }

  /*
   * New RPC xid, so any late responses to the previous ping
   * get ignored...
   */
  np->np_xid = XID_ALLOC();

  /*
   * Run keepalive again
   */
  nfs_keepalive(fs);
}


/*
 * Keep track of whether a server is alive
 */
static void
nfs_keepalive(voidp v)
{
  fserver *fs = v;
  int error;
  nfs_private *np = (nfs_private *) fs->fs_private;
  int fstimeo = -1;

  /*
   * Send an NFS ping to this node
   */

  if (ping_len[fs->fs_version - NFS_VERSION] == 0)
    create_ping_payload(fs->fs_version);

  /*
   * Queue the packet...
   */
  error = fwd_packet(MK_RPC_XID(RPC_XID_NFSPING, np->np_xid),
		     ping_buf[fs->fs_version - NFS_VERSION],
		     ping_len[fs->fs_version - NFS_VERSION],
		     fs->fs_ip,
		     (struct sockaddr_in *) NULL,
		     (voidp) ((long) np->np_xid), /* cast needed for 64-bit archs */
		     nfs_keepalive_callback);

  /*
   * See if a hard error occurred
   */
  switch (error) {
  case ENETDOWN:
  case ENETUNREACH:
  case EHOSTDOWN:
  case EHOSTUNREACH:
    np->np_ping = MAX_ALLOWED_PINGS;	/* immediately down */
    np->np_ttl = (time_t) 0;
    /*
     * This causes an immediate call to nfs_keepalive_timeout
     * whenever the server was thought to be up.
     * See +++ below.
     */
    fstimeo = 0;
    break;

  case 0:
    dlog("Sent NFS ping to %s", fs->fs_host);
    break;
  }

  /*
   * Back off the ping interval if we are not getting replies and
   * the remote system is known to be down.
   */
  switch (fs->fs_flags & (FSF_DOWN | FSF_VALID)) {
  case FSF_VALID:		/* Up */
    if (fstimeo < 0)		/* +++ see above */
      fstimeo = FAST_NFS_PING;
    break;

  case FSF_VALID | FSF_DOWN:	/* Down */
    fstimeo = fs->fs_pinger;
    break;

  default:			/* Unknown */
    fstimeo = FAST_NFS_PING;
    break;
  }

  dlog("NFS timeout in %d seconds", fstimeo);

  fs->fs_cid = timeout(fstimeo, nfs_keepalive_timeout, (voidp) fs);
}


static void
start_nfs_pings(fserver *fs, int pingval)
{
  if (pingval == 0)	    /* could be because ping mnt option not found */
    pingval = AM_PINGER;
  /* if pings haven't been initalized, then init them for first time */
  if (fs->fs_flags & FSF_PING_UNINIT) {
    fs->fs_flags &= ~FSF_PING_UNINIT;
    plog(XLOG_INFO, "initializing %s's pinger to %d sec", fs->fs_host, pingval);
    goto do_pings;
  }

  if ((fs->fs_flags & FSF_PINGING)  &&  fs->fs_pinger == pingval) {
    dlog("already running pings to %s", fs->fs_host);
    return;
  }

  /* if got here, then we need to update the ping value */
  plog(XLOG_INFO, "changing %s's ping value from %d%s to %d%s",
       fs->fs_host,
       fs->fs_pinger, (fs->fs_pinger < 0 ? " (off)" : ""),
       pingval, (pingval < 0 ? " (off)" : ""));
 do_pings:
  fs->fs_pinger = pingval;

  if (fs->fs_cid)
    untimeout(fs->fs_cid);
  if (pingval < 0) {
    srvrlog(fs, "wired up (pings disabled)");
    fs->fs_flags |= FSF_VALID;
    fs->fs_flags &= ~FSF_DOWN;
  } else {
    fs->fs_flags |= FSF_PINGING;
    nfs_keepalive(fs);
  }
}


/*
 * Find an nfs server for a host.
 */
fserver *
find_nfs_srvr(mntfs *mf)
{
  char *host = mf->mf_fo->opt_rhost;
  fserver *fs;
  int pingval;
  mntent_t mnt;
  nfs_private *np;
  struct hostent *hp = NULL;
  struct sockaddr_in *ip = NULL;
  u_long nfs_version = 0;	/* default is no version specified */
  u_long best_nfs_version = 0;
  char *nfs_proto = NULL;	/* no IP protocol either */
  int nfs_port = 0;
  int nfs_port_opt = 0;
  int fserver_is_down = 0;

  /*
   * Get ping interval from mount options.
   * Current only used to decide whether pings
   * are required or not.  < 0 = no pings.
   */
  mnt.mnt_opts = mf->mf_mopts;
  pingval = hasmntval(&mnt, "ping");

  if (mf->mf_flags & MFF_NFS_SCALEDOWN) {
    /*
     * the server granted us a filehandle, but we were unable to mount it.
     * therefore, scale down to NFSv2/UDP and try again.
     */
    nfs_version = NFS_VERSION;
    nfs_proto = "udp";
    plog(XLOG_WARNING, "find_nfs_srvr: NFS mount failed, trying again with NFSv2/UDP");
    mf->mf_flags &= ~MFF_NFS_SCALEDOWN;
  } else {
    /*
     * Get the NFS version from the mount options. This is used
     * to decide the highest NFS version to try.
     */
#ifdef MNTTAB_OPT_VERS
    nfs_version = hasmntval(&mnt, MNTTAB_OPT_VERS);
#endif /* MNTTAB_OPT_VERS */

#ifdef MNTTAB_OPT_PROTO
    {
      char *proto_opt = hasmnteq(&mnt, MNTTAB_OPT_PROTO);
      if (proto_opt) {
	char **p;
	for (p = protocols; *p; p++)
	  if (NSTREQ(proto_opt, *p, strlen(*p))) {
	    nfs_proto = *p;
	    break;
	  }
	if (*p == NULL)
	  plog(XLOG_WARNING, "ignoring unknown protocol option for %s:%s",
	       host, mf->mf_fo->opt_rfs);
      }
    }
#endif /* MNTTAB_OPT_PROTO */

#ifdef HAVE_NFS_NFSV2_H
    /* allow overriding if nfsv2 option is specified in mount options */
    if (amu_hasmntopt(&mnt, "nfsv2")) {
      nfs_version = NFS_VERSION;/* nullify any ``vers=X'' statements */
      nfs_proto = "udp";	/* nullify any ``proto=tcp'' statements */
      plog(XLOG_WARNING, "found compatibility option \"nfsv2\": set options vers=2,proto=udp for host %s", host);
    }
#endif /* HAVE_NFS_NFSV2_H */

    /* check if we've globally overridden the NFS version/protocol */
    if (gopt.nfs_vers) {
      nfs_version = gopt.nfs_vers;
      plog(XLOG_INFO, "find_nfs_srvr: force NFS version to %d",
	   (int) nfs_version);
    }
    if (gopt.nfs_proto) {
      nfs_proto = gopt.nfs_proto;
      plog(XLOG_INFO, "find_nfs_srvr: force NFS protocol transport to %s", nfs_proto);
    }
  }

  /*
   * lookup host address and canonical name
   */
  hp = gethostbyname(host);

  /*
   * New code from Bob Harris <harris@basil-rathbone.mit.edu>
   * Use canonical name to keep track of file server
   * information.  This way aliases do not generate
   * multiple NFS pingers.  (Except when we're normalizing
   * hosts.)
   */
  if (hp && !(gopt.flags & CFM_NORMALIZE_HOSTNAMES))
    host = (char *) hp->h_name;

  if (hp) {
    switch (hp->h_addrtype) {
    case AF_INET:
      ip = ALLOC(struct sockaddr_in);
      memset((voidp) ip, 0, sizeof(*ip));
      /* as per POSIX, sin_len need not be set (used internally by kernel) */
      ip->sin_family = AF_INET;
      memmove((voidp) &ip->sin_addr, (voidp) hp->h_addr, sizeof(ip->sin_addr));
      break;

    default:
      plog(XLOG_USER, "No IP address for host %s", host);
      goto no_dns;
    }
  } else {
    plog(XLOG_USER, "Unknown host: %s", host);
    goto no_dns;
  }

  /*
   * This may not be the best way to do things, but it really doesn't make
   * sense to query a file server which is marked as 'down' for any
   * version/proto combination.
   */
  ITER(fs, fserver, &nfs_srvr_list) {
    if (FSRV_ISDOWN(fs) &&
	STREQ(host, fs->fs_host)) {
      plog(XLOG_WARNING, "fileserver %s is already hung - not running NFS proto/version discovery", host);
      fs->fs_refc++;
      if (ip)
	XFREE(ip);
      return fs;
    }
  }

  /*
   * Get the NFS Version, and verify server is up.
   * If the client only supports NFSv2, hardcode it but still try to
   * contact the remote portmapper to see if the service is running.
   */
#ifndef HAVE_FS_NFS3
  nfs_version = NFS_VERSION;
  nfs_proto = "udp";
  plog(XLOG_INFO, "The client supports only NFS(2,udp)");
#endif /* not HAVE_FS_NFS3 */


  if (amu_hasmntopt(&mnt, MNTTAB_OPT_PUBLIC)) {
    /*
     * Use WebNFS to obtain file handles.
     */
    mf->mf_flags |= MFF_WEBNFS;
    plog(XLOG_INFO, "%s option used, NOT contacting the portmapper on %s",
	 MNTTAB_OPT_PUBLIC, host);
    /*
     * Prefer NFSv3/tcp if the client supports it (cf. RFC 2054, 7).
     */
    if (!nfs_version) {
#ifdef HAVE_FS_NFS3
      nfs_version = NFS_VERSION3;
#else /* not HAVE_FS_NFS3 */
      nfs_version = NFS_VERSION;
#endif /* not HAVE_FS_NFS3 */
      plog(XLOG_INFO, "No NFS version specified, will use NFSv%d",
	   (int) nfs_version);
    }
    if (!nfs_proto) {
#if defined(MNTTAB_OPT_PROTO) || defined(HAVE_FS_NFS3)
      nfs_proto = "tcp";
#else /* not defined(MNTTAB_OPT_PROTO) || defined(HAVE_FS_NFS3) */
      nfs_proto = "udp";
#endif /* not defined(MNTTAB_OPT_PROTO) || defined(HAVE_FS_NFS3) */
      plog(XLOG_INFO, "No NFS protocol transport specified, will use %s",
	   nfs_proto);
    }
  } else {
    /*
     * Find the best combination of NFS version and protocol.
     * When given a choice, use the highest available version,
     * and use TCP over UDP if available.
     */
    if (check_pmap_up(host, ip)) {
      if (nfs_proto) {
	best_nfs_version = get_nfs_version(host, ip, nfs_version, nfs_proto);
	nfs_port = ip->sin_port;
      }
#ifdef MNTTAB_OPT_PROTO
      else {
	u_int proto_nfs_version;
	char **p;

	for (p = protocols; *p; p++) {
	  proto_nfs_version = get_nfs_version(host, ip, nfs_version, *p);

	  if (proto_nfs_version > best_nfs_version) {
	    best_nfs_version = proto_nfs_version;
	    nfs_proto = *p;
	    nfs_port = ip->sin_port;
	  }
	}
      }
#endif /* MNTTAB_OPT_PROTO */
    } else {
      plog(XLOG_INFO, "portmapper service not running on %s", host);
    }

    /* use the portmapper results only nfs_version is not set yet */
    if (!best_nfs_version) {
      /*
       * If the NFS server is down or does not support the portmapper call
       * (such as certain Novell NFS servers) we mark it as version 2 and we
       * let the nfs code deal with the case when it is down.  If/when the
       * server comes back up and it can support NFSv3 and/or TCP, it will
       * use those.
       */
      if (nfs_version == 0) {
	nfs_version = NFS_VERSION;
	nfs_proto = "udp";
      }
      plog(XLOG_INFO, "NFS service not running on %s", host);
      fserver_is_down = 1;
    } else {
      if (nfs_version == 0)
	nfs_version = best_nfs_version;
      plog(XLOG_INFO, "Using NFS version %d, protocol %s on host %s",
	   (int) nfs_version, nfs_proto, host);
    }
  }

  /*
   * Determine the NFS port.
   *
   * A valid "port" mount option overrides anything else.
   * If the port has been determined from the portmapper, use that.
   * Default to NFS_PORT otherwise (cf. RFC 2054, 3).
   */
  nfs_port_opt = hasmntval(&mnt, MNTTAB_OPT_PORT);
  if (nfs_port_opt > 0)
    nfs_port = htons(nfs_port_opt);
  if (!nfs_port)
    nfs_port = htons(NFS_PORT);

  dlog("find_nfs_srvr: using port %d for nfs on %s",
       (int) ntohs(nfs_port), host);
  ip->sin_port = nfs_port;

no_dns:
  /*
   * Try to find an existing fs server structure for this host.
   * Note that differing versions or protocols have their own structures.
   * XXX: Need to fix the ping mechanism to actually use the NFS protocol
   * chosen here (right now it always uses datagram sockets).
   */
  ITER(fs, fserver, &nfs_srvr_list) {
    if (STREQ(host, fs->fs_host) &&
 	nfs_version == fs->fs_version &&
	STREQ(nfs_proto, fs->fs_proto)) {
      /*
       * fill in the IP address -- this is only needed
       * if there is a chance an IP address will change
       * between mounts.
       * Mike Mitchell, mcm@unx.sas.com, 09/08/93
       */
      if (hp && fs->fs_ip &&
	  memcmp((voidp) &fs->fs_ip->sin_addr,
		 (voidp) hp->h_addr,
		 sizeof(fs->fs_ip->sin_addr)) != 0) {
	struct in_addr ia;
	char *old_ipaddr, *new_ipaddr;
	old_ipaddr = strdup(inet_ntoa(fs->fs_ip->sin_addr));
	memmove((voidp) &ia, (voidp) hp->h_addr, sizeof(struct in_addr));
	new_ipaddr = inet_ntoa(ia);	/* ntoa uses static buf */
	plog(XLOG_WARNING, "fileserver %s changed ip: %s -> %s",
	     fs->fs_host, old_ipaddr, new_ipaddr);
	XFREE(old_ipaddr);
	flush_nfs_fhandle_cache(fs);
	memmove((voidp) &fs->fs_ip->sin_addr, (voidp) hp->h_addr, sizeof(fs->fs_ip->sin_addr));
      }

      /*
       * If the new file systems doesn't use WebNFS, the nfs pings may
       * try to contact the portmapper.
       */
      if (!(mf->mf_flags & MFF_WEBNFS))
	fs->fs_flags &= ~FSF_WEBNFS;

      /* check if pingval needs to be updated/set/reset */
      start_nfs_pings(fs, pingval);

      /*
       * Following if statement from Mike Mitchell <mcm@unx.sas.com>
       * Initialize the ping data if we aren't pinging now.  The np_ttl and
       * np_ping fields are especially important.
       */
      if (!(fs->fs_flags & FSF_PINGING)) {
	np = (nfs_private *) fs->fs_private;
	np->np_mountd_inval = TRUE;
	np->np_xid = XID_ALLOC();
	np->np_error = -1;
	np->np_ping = 0;
	/*
	 * Initially the server will be deemed dead
	 * after MAX_ALLOWED_PINGS of the fast variety
	 * have failed.
	 */
	np->np_ttl = MAX_ALLOWED_PINGS * FAST_NFS_PING + clocktime(NULL) - 1;
	start_nfs_pings(fs, pingval);
	if (fserver_is_down)
	  fs->fs_flags |= FSF_VALID | FSF_DOWN;
      }

      fs->fs_refc++;
      if (ip)
	XFREE(ip);
      return fs;
    }
  }

  /*
   * Get here if we can't find an entry
   */

  /*
   * Allocate a new server
   */
  fs = ALLOC(struct fserver);
  fs->fs_refc = 1;
  fs->fs_host = strdup(hp ? hp->h_name : "unknown_hostname");
  if (gopt.flags & CFM_NORMALIZE_HOSTNAMES)
    host_normalize(&fs->fs_host);
  fs->fs_ip = ip;
  fs->fs_cid = 0;
  if (ip) {
    fs->fs_flags = FSF_DOWN;	/* Starts off down */
  } else {
    fs->fs_flags = FSF_ERROR | FSF_VALID;
    mf->mf_flags |= MFF_ERROR;
    mf->mf_error = ENOENT;
  }
  if (mf->mf_flags & MFF_WEBNFS)
    fs->fs_flags |= FSF_WEBNFS;
  fs->fs_version = nfs_version;
  fs->fs_proto = nfs_proto;
  fs->fs_type = MNTTAB_TYPE_NFS;
  fs->fs_pinger = AM_PINGER;
  fs->fs_flags |= FSF_PING_UNINIT; /* pinger hasn't been initialized */
  np = ALLOC(struct nfs_private);
  memset((voidp) np, 0, sizeof(*np));
  np->np_mountd_inval = TRUE;
  np->np_xid = XID_ALLOC();
  np->np_error = -1;

  /*
   * Initially the server will be deemed dead after
   * MAX_ALLOWED_PINGS of the fast variety have failed.
   */
  np->np_ttl = clocktime(NULL) + MAX_ALLOWED_PINGS * FAST_NFS_PING - 1;
  fs->fs_private = (voidp) np;
  fs->fs_prfree = (void (*)(voidp)) free;

  if (!FSRV_ERROR(fs)) {
    /* start of keepalive timer, first updating pingval */
    start_nfs_pings(fs, pingval);
    if (fserver_is_down)
      fs->fs_flags |= FSF_VALID | FSF_DOWN;
  }

  /*
   * Add to list of servers
   */
  ins_que(&fs->fs_q, &nfs_srvr_list);

  return fs;
}
