This exposes ZFS user and group quotas via the normal

quatactl(2) mechanism.  (Read-only at this point, however.)
In particular, this is to allow rpc.rquotad query quotas
for NFS mounts, allowing users to see their quotas on the
hosts using the datasets.

The changes specifically:

* Add new RPC entry points for querying quotas.
* Changes the library routines to allow non-UFS quotas.
* Changes rquotad to check for quotas on mounted filesystems,
rather than being limited to entries in /etc/fstab
* Lastly, adds a VFS entry-point for ZFS to query quotas.

Note that this makes one unavoidable behavioural change: if quotas
are enabled, then they can be queried, as opposed to the current
method of checking for quotas being specified in fstab.  (With
ZFS, if there are user or group quotas, they're used, always.)

Reviewed by:	delphij, mav
Approved by:	mav
Sponsored by:	iXsystems Inc
Differential Revision:	https://reviews.freebsd.org/D15886
This commit is contained in:
sef 2018-07-05 22:56:13 +00:00
parent 14ae1c4fd1
commit 107a344bbb
5 changed files with 416 additions and 74 deletions

View File

@ -1,22 +1,53 @@
/* @(#)rquota.x 2.1 88/08/01 4.0 RPCSRC */
/* @(#)rquota.x 1.2 87/09/20 Copyr 1987 Sun Micro */
/*
* Remote quota protocol
* Requires unix authentication
*/
#ifndef RPC_HDR
%#ifndef lint
%/*static char sccsid[] = "from: @(#)rquota.x 1.2 87/09/20 Copyr 1987 Sun Micro";*/
%/*static char sccsid[] = "from: @(#)rquota.x 2.1 88/08/01 4.0 RPCSRC";*/
%#endif /* not lint */
%#include <sys/cdefs.h>
%__FBSDID("$FreeBSD$");
#endif
const RQ_PATHLEN = 1024;
struct sq_dqblk {
unsigned int rq_bhardlimit; /* absolute limit on disk blks alloc */
unsigned int rq_bsoftlimit; /* preferred limit on disk blks */
unsigned int rq_curblocks; /* current block count */
unsigned int rq_fhardlimit; /* absolute limit on allocated files */
unsigned int rq_fsoftlimit; /* preferred file limit */
unsigned int rq_curfiles; /* current # allocated files */
unsigned int rq_btimeleft; /* time left for excessive disk use */
unsigned int rq_ftimeleft; /* time left for excessive files */
};
struct getquota_args {
string gqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
int gqa_uid; /* inquire about quota for uid */
int gqa_uid; /* Inquire about quota for uid */
};
struct setquota_args {
int sqa_qcmd;
string sqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
int sqa_id; /* Set quota for uid */
sq_dqblk sqa_dqblk;
};
struct ext_getquota_args {
string gqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
int gqa_type; /* Type of quota info is needed about */
int gqa_id; /* Inquire about quota for id */
};
struct ext_setquota_args {
int sqa_qcmd;
string sqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */
int sqa_id; /* Set quota for id */
int sqa_type; /* Type of quota to set */
sq_dqblk sqa_dqblk;
};
/*
@ -37,7 +68,7 @@ struct rquota {
enum gqr_status {
Q_OK = 1, /* quota returned */
Q_NOQUOTA = 2, /* noquota for uid */
Q_NOQUOTA = 2, /* noquota for uid */
Q_EPERM = 3 /* no permission to access quota */
};
@ -50,6 +81,15 @@ case Q_EPERM:
void;
};
union setquota_rslt switch (gqr_status status) {
case Q_OK:
rquota sqr_rquota; /* valid if status == Q_OK */
case Q_NOQUOTA:
void;
case Q_EPERM:
void;
};
program RQUOTAPROG {
version RQUOTAVERS {
/*
@ -63,5 +103,42 @@ program RQUOTAPROG {
*/
getquota_rslt
RQUOTAPROC_GETACTIVEQUOTA(getquota_args) = 2;
/*
* Set all quotas
*/
setquota_rslt
RQUOTAPROC_SETQUOTA(setquota_args) = 3;
/*
* Get active quotas only
*/
setquota_rslt
RQUOTAPROC_SETACTIVEQUOTA(setquota_args) = 4;
} = 1;
version EXT_RQUOTAVERS {
/*
* Get all quotas
*/
getquota_rslt
RQUOTAPROC_GETQUOTA(ext_getquota_args) = 1;
/*
* Get active quotas only
*/
getquota_rslt
RQUOTAPROC_GETACTIVEQUOTA(ext_getquota_args) = 2;
/*
* Set all quotas
*/
setquota_rslt
RQUOTAPROC_SETQUOTA(ext_setquota_args) = 3;
/*
* Set active quotas only
*/
setquota_rslt
RQUOTAPROC_SETACTIVEQUOTA(ext_setquota_args) = 4;
} = 2;
} = 100011;

View File

@ -120,8 +120,6 @@ quota_open(struct fstab *fs, int quotatype, int openflags)
struct stat st;
int qcmd, serrno;
if (strcmp(fs->fs_vfstype, "ufs"))
return (NULL);
if ((qf = calloc(1, sizeof(*qf))) == NULL)
return (NULL);
qf->fd = -1;
@ -130,10 +128,15 @@ quota_open(struct fstab *fs, int quotatype, int openflags)
if (stat(qf->fsname, &st) != 0)
goto error;
qf->dev = st.st_dev;
serrno = hasquota(fs, quotatype, qf->qfname, sizeof(qf->qfname));
qcmd = QCMD(Q_GETQUOTASIZE, quotatype);
if (quotactl(qf->fsname, qcmd, 0, &qf->wordsize) == 0)
return (qf);
/* We only check the quota file for ufs */
if (strcmp(fs->fs_vfstype, "ufs")) {
errno = 0;
goto error;
}
serrno = hasquota(fs, quotatype, qf->qfname, sizeof(qf->qfname));
if (serrno == 0) {
errno = EOPNOTSUPP;
goto error;

View File

@ -28,18 +28,19 @@ __FBSDID("$FreeBSD$");
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
static void rquota_service(struct svc_req *request, SVCXPRT *transp);
static void rquota_service_1(struct svc_req *request, SVCXPRT *transp);
static void rquota_service_2(struct svc_req *request, SVCXPRT *transp);
static void sendquota(struct svc_req *request, SVCXPRT *transp);
static void initfs(void);
static int getfsquota(long id, char *path, struct dqblk *dqblk);
static void sendquota_extended(struct svc_req *request, SVCXPRT *transp);
static int getfsquota(int type, long id, char *path, struct dqblk *dqblk);
static struct quotafile **qfa; /* array of qfs */
static int nqf, szqf; /* number of qfs and size of array */
static int from_inetd = 1;
static int debug = 0;
static void
cleanup(int sig)
@ -51,19 +52,32 @@ cleanup(int sig)
}
int
main(void)
main(int argc, char **argv)
{
SVCXPRT *transp;
int ok;
struct sockaddr_storage from;
socklen_t fromlen;
int vers;
int ch;
while ((ch = getopt(argc, argv, "d")) != -1) {
switch (ch) {
case 'd':
debug++;
break;
default:
break;
}
}
fromlen = sizeof(from);
if (getsockname(0, (struct sockaddr *)&from, &fromlen) < 0)
from_inetd = 0;
if (!from_inetd) {
daemon(0, 0);
if (!debug)
daemon(0, 0);
(void)rpcb_unset(RQUOTAPROG, RQUOTAVERS, NULL);
(void)signal(SIGINT, cleanup);
(void)signal(SIGTERM, cleanup);
@ -79,27 +93,60 @@ main(void)
syslog(LOG_ERR, "couldn't create udp service.");
exit(1);
}
vers = RQUOTAVERS;
ok = svc_reg(transp, RQUOTAPROG, RQUOTAVERS,
rquota_service, NULL);
rquota_service_1, NULL);
if (ok) {
vers = EXT_RQUOTAVERS;
ok = svc_reg(transp, RQUOTAPROG, EXT_RQUOTAVERS,
rquota_service_2, NULL);
}
} else {
ok = svc_create(rquota_service,
vers = RQUOTAVERS;
ok = svc_create(rquota_service_1,
RQUOTAPROG, RQUOTAVERS, "udp");
if (ok) {
vers = EXT_RQUOTAVERS;
ok = svc_create(rquota_service_2,
RQUOTAPROG, EXT_RQUOTAVERS, "udp");
}
}
if (!ok) {
syslog(LOG_ERR,
"unable to register (RQUOTAPROG, RQUOTAVERS, %s)",
from_inetd ? "(inetd)" : "udp");
"unable to register (RQUOTAPROG, %s, %s)",
vers == RQUOTAVERS ? "RQUOTAVERS" : "EXT_RQUOTAVERS",
from_inetd ? "(inetd)" : "udp");
exit(1);
}
initfs();
svc_run();
syslog(LOG_ERR, "svc_run returned");
exit(1);
}
static void
rquota_service(struct svc_req *request, SVCXPRT *transp)
rquota_service_2(struct svc_req *request, SVCXPRT *transp)
{
switch (request->rq_proc) {
case NULLPROC:
(void)svc_sendreply(transp, (xdrproc_t)xdr_void, (char *)NULL);
break;
case RQUOTAPROC_GETQUOTA:
case RQUOTAPROC_GETACTIVEQUOTA:
sendquota_extended(request, transp);
break;
default:
svcerr_noproc(transp);
break;
}
if (from_inetd)
exit(0);
}
static void
rquota_service_1(struct svc_req *request, SVCXPRT *transp)
{
switch (request->rq_proc) {
@ -136,7 +183,7 @@ sendquota(struct svc_req *request, SVCXPRT *transp)
if (request->rq_cred.oa_flavor != AUTH_UNIX) {
/* bad auth */
getq_rslt.status = Q_EPERM;
} else if (!getfsquota(getq_args.gqa_uid, getq_args.gqa_pathp, &dqblk)) {
} else if (!getfsquota(USRQUOTA, getq_args.gqa_uid, getq_args.gqa_pathp, &dqblk)) {
/* failed, return noquota */
getq_rslt.status = Q_NOQUOTA;
} else {
@ -172,38 +219,55 @@ sendquota(struct svc_req *request, SVCXPRT *transp)
}
static void
initfs(void)
sendquota_extended(struct svc_req *request, SVCXPRT *transp)
{
struct fstab *fs;
struct ext_getquota_args getq_args;
struct getquota_rslt getq_rslt;
struct dqblk dqblk;
struct timeval timev;
int scale;
setfsent();
szqf = 8;
if ((qfa = malloc(szqf * sizeof *qfa)) == NULL)
goto enomem;
while ((fs = getfsent())) {
if (strcmp(fs->fs_vfstype, "ufs"))
continue;
if (nqf >= szqf) {
szqf *= 2;
if ((qfa = reallocf(qfa, szqf * sizeof *qfa)) == NULL)
goto enomem;
}
if ((qfa[nqf] = quota_open(fs, USRQUOTA, O_RDONLY)) == NULL) {
if (errno != EOPNOTSUPP)
goto fserr;
continue;
}
++nqf;
/* XXX */
bzero(&getq_args, sizeof(getq_args));
if (!svc_getargs(transp, (xdrproc_t)xdr_ext_getquota_args, &getq_args)) {
svcerr_decode(transp);
return;
}
if (request->rq_cred.oa_flavor != AUTH_UNIX) {
/* bad auth */
getq_rslt.status = Q_EPERM;
} else if (!getfsquota(getq_args.gqa_type, getq_args.gqa_id, getq_args.gqa_pathp, &dqblk)) {
/* failed, return noquota */
getq_rslt.status = Q_NOQUOTA;
} else {
gettimeofday(&timev, NULL);
getq_rslt.status = Q_OK;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_active = TRUE;
scale = 1 << flsll(dqblk.dqb_bhardlimit >> 32);
getq_rslt.getquota_rslt_u.gqr_rquota.rq_bsize =
DEV_BSIZE * scale;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_bhardlimit =
dqblk.dqb_bhardlimit / scale;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_bsoftlimit =
dqblk.dqb_bsoftlimit / scale;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_curblocks =
dqblk.dqb_curblocks / scale;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_fhardlimit =
dqblk.dqb_ihardlimit;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_fsoftlimit =
dqblk.dqb_isoftlimit;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_curfiles =
dqblk.dqb_curinodes;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_btimeleft =
dqblk.dqb_btime - timev.tv_sec;
getq_rslt.getquota_rslt_u.gqr_rquota.rq_ftimeleft =
dqblk.dqb_itime - timev.tv_sec;
}
if (!svc_sendreply(transp, (xdrproc_t)xdr_getquota_rslt, &getq_rslt))
svcerr_systemerr(transp);
if (!svc_freeargs(transp, (xdrproc_t)xdr_getquota_args, &getq_args)) {
syslog(LOG_ERR, "unable to free arguments");
exit(1);
}
endfsent();
return;
enomem:
syslog(LOG_ERR, "out of memory");
exit(1);
fserr:
syslog(LOG_ERR, "%s: %s", fs->fs_file, strerror(errno));
exit(1);
}
/*
@ -211,12 +275,43 @@ initfs(void)
* Return 0 if fail, 1 otherwise
*/
static int
getfsquota(long id, char *path, struct dqblk *dqblk)
getfsquota(int type, long id, char *path, struct dqblk *dqblk)
{
int i;
struct quotafile *qf;
/*
* Remote quota checking is limited to mounted filesystems.
* Since UFS and ZFS support the quota system calls, we
* only need to make an fstab object that has the path, and
* a blank name for the filesystem type.
* This allows the quota_open() call to work the way we
* expect it to.
*
* The static char declaration is because compiler warnings
* don't allow passing a const char * to a char *.
*/
int rv;
static char blank[] = "";
struct fstab fst;
for (i = 0; i < nqf; ++i)
if (quota_check_path(qfa[i], path) == 1)
return (quota_read(qfa[i], dqblk, id) == 0);
return (0);
fst.fs_file = path;
fst.fs_mntops = blank;
fst.fs_vfstype = blank;
if (type != USRQUOTA && type != GRPQUOTA)
return (0);
qf = quota_open(&fst, type, O_RDONLY);
if (debug)
warnx("quota_open(<%s, %s>, %d) returned %p",
fst.fs_file, fst.fs_mntops, type,
qf);
if (qf == NULL)
return (0);
rv = quota_read(qf, dqblk, id) == 0;
quota_close(qf);
if (debug)
warnx("getfsquota(%d, %ld, %s, %p) -> %d",
type, id, path, dqblk, rv);
return (rv);
}

View File

@ -64,6 +64,8 @@
#include <sys/dmu_objset.h>
#include <sys/spa_boot.h>
#include <sys/jail.h>
#include <ufs/ufs/quota.h>
#include "zfs_comutil.h"
struct mtx zfs_debug_mtx;
@ -90,6 +92,7 @@ static int zfs_version_zpl = ZPL_VERSION;
SYSCTL_INT(_vfs_zfs_version, OID_AUTO, zpl, CTLFLAG_RD, &zfs_version_zpl, 0,
"ZPL_VERSION");
static int zfs_quotactl(vfs_t *vfsp, int cmds, uid_t id, void *arg);
static int zfs_mount(vfs_t *vfsp);
static int zfs_umount(vfs_t *vfsp, int fflag);
static int zfs_root(vfs_t *vfsp, int flags, vnode_t **vpp);
@ -111,6 +114,7 @@ struct vfsops zfs_vfsops = {
.vfs_sync = zfs_sync,
.vfs_checkexp = zfs_checkexp,
.vfs_fhtovp = zfs_fhtovp,
.vfs_quotactl = zfs_quotactl,
};
VFS_SET(zfs_vfsops, zfs, VFCF_JAIL | VFCF_DELEGADMIN);
@ -122,6 +126,159 @@ VFS_SET(zfs_vfsops, zfs, VFCF_JAIL | VFCF_DELEGADMIN);
*/
static uint32_t zfs_active_fs_count = 0;
static int
zfs_getquota(zfsvfs_t *zfsvfs, uid_t id, int isgroup, struct dqblk64 *dqp)
{
int error = 0;
char buf[32];
int err;
uint64_t usedobj, quotaobj;
uint64_t quota, used = 0;
timespec_t now;
usedobj = isgroup ? DMU_GROUPUSED_OBJECT : DMU_USERUSED_OBJECT;
quotaobj = isgroup ? zfsvfs->z_groupquota_obj : zfsvfs->z_userquota_obj;
if (quotaobj == 0 || zfsvfs->z_replay) {
error = ENOENT;
goto done;
}
(void)sprintf(buf, "%llx", (longlong_t)id);
if ((error = zap_lookup(zfsvfs->z_os, quotaobj,
buf, sizeof(quota), 1, &quota)) != 0) {
dprintf("%s(%d): quotaobj lookup failed\n", __FUNCTION__, __LINE__);
goto done;
}
/*
* quota(8) uses bsoftlimit as "quoota", and hardlimit as "limit".
* So we set them to be the same.
*/
dqp->dqb_bsoftlimit = dqp->dqb_bhardlimit = btodb(quota);
error = zap_lookup(zfsvfs->z_os, usedobj, buf, sizeof(used), 1, &used);
if (error && error != ENOENT) {
dprintf("%s(%d): usedobj failed; %d\n", __FUNCTION__, __LINE__, error);
goto done;
}
dqp->dqb_curblocks = btodb(used);
dqp->dqb_ihardlimit = dqp->dqb_isoftlimit = 0;
vfs_timestamp(&now);
/*
* Setting this to 0 causes FreeBSD quota(8) to print
* the number of days since the epoch, which isn't
* particularly useful.
*/
dqp->dqb_btime = dqp->dqb_itime = now.tv_sec;
done:
return (error);
}
static int
zfs_quotactl(vfs_t *vfsp, int cmds, uid_t id, void *arg)
{
zfsvfs_t *zfsvfs = vfsp->vfs_data;
struct thread *td;
int cmd, type, error = 0;
int bitsize;
uint64_t fuid;
zfs_userquota_prop_t quota_type;
struct dqblk64 dqblk = { 0 };
td = curthread;
cmd = cmds >> SUBCMDSHIFT;
type = cmds & SUBCMDMASK;
ZFS_ENTER(zfsvfs);
if (id == -1) {
switch (type) {
case USRQUOTA:
id = td->td_ucred->cr_ruid;
break;
case GRPQUOTA:
id = td->td_ucred->cr_rgid;
break;
default:
error = EINVAL;
goto done;
}
}
/*
* Map BSD type to:
* ZFS_PROP_USERUSED,
* ZFS_PROP_USERQUOTA,
* ZFS_PROP_GROUPUSED,
* ZFS_PROP_GROUPQUOTA
*/
switch (cmd) {
case Q_SETQUOTA:
case Q_SETQUOTA32:
if (type == USRQUOTA)
quota_type = ZFS_PROP_USERQUOTA;
else if (type == GRPQUOTA)
quota_type = ZFS_PROP_GROUPQUOTA;
else
error = EINVAL;
break;
case Q_GETQUOTA:
case Q_GETQUOTA32:
if (type == USRQUOTA)
quota_type = ZFS_PROP_USERUSED;
else if (type == GRPQUOTA)
quota_type = ZFS_PROP_GROUPUSED;
else
error = EINVAL;
break;
}
/*
* Depending on the cmd, we may need to get
* the ruid and domain (see fuidstr_to_sid?),
* the fuid (how?), or other information.
* Create fuid using zfs_fuid_create(zfsvfs, id,
* ZFS_OWNER or ZFS_GROUP, cr, &fuidp)?
* I think I can use just the id?
*
* Look at zfs_fuid_overquota() to look up a quota.
* zap_lookup(something, quotaobj, fuidstring, sizeof(long long), 1, &quota)
*
* See zfs_set_userquota() to set a quota.
*/
if ((u_int)type >= MAXQUOTAS) {
error = EINVAL;
goto done;
}
switch (cmd) {
case Q_GETQUOTASIZE:
bitsize = 64;
error = copyout(&bitsize, arg, sizeof(int));
break;
case Q_QUOTAON:
// As far as I can tell, you can't turn quotas on or off on zfs
error = 0;
break;
case Q_QUOTAOFF:
error = ENOTSUP;
break;
case Q_SETQUOTA:
error = copyin(&dqblk, arg, sizeof(dqblk));
if (error == 0)
error = zfs_set_userquota(zfsvfs, quota_type,
"", id, dbtob(dqblk.dqb_bhardlimit));
break;
case Q_GETQUOTA:
error = zfs_getquota(zfsvfs, id, type == GRPQUOTA, &dqblk);
if (error == 0)
error = copyout(&dqblk, arg, sizeof(dqblk));
break;
default:
error = EINVAL;
break;
}
done:
ZFS_EXIT(zfsvfs);
return (error);
}
/*ARGSUSED*/
static int
zfs_sync(vfs_t *vfsp, int waitfor)

View File

@ -98,7 +98,7 @@ static int getufsquota(struct fstab *fs, struct quotause *qup, long id,
int quotatype);
static int getnfsquota(struct statfs *fst, struct quotause *qup, long id,
int quotatype);
static int callaurpc(char *host, int prognum, int versnum, int procnum,
static enum clnt_stat callaurpc(char *host, int prognum, int versnum, int procnum,
xdrproc_t inproc, char *in, xdrproc_t outproc, char *out);
static int alldigits(char *s);
@ -568,21 +568,17 @@ getufsquota(struct fstab *fs, struct quotause *qup, long id, int quotatype)
static int
getnfsquota(struct statfs *fst, struct quotause *qup, long id, int quotatype)
{
struct getquota_args gq_args;
struct ext_getquota_args gq_args;
struct getquota_args old_gq_args;
struct getquota_rslt gq_rslt;
struct dqblk *dqp = &qup->dqblk;
struct timeval tv;
char *cp, host[NI_MAXHOST];
enum clnt_stat call_stat;
if (fst->f_flags & MNT_LOCAL)
return (0);
/*
* rpc.rquotad does not support group quotas
*/
if (quotatype != USRQUOTA)
return (0);
/*
* must be some form of "hostname:/path"
*/
@ -604,11 +600,26 @@ getnfsquota(struct statfs *fst, struct quotause *qup, long id, int quotatype)
return (0);
gq_args.gqa_pathp = cp + 1;
gq_args.gqa_uid = id;
if (callaurpc(host, RQUOTAPROG, RQUOTAVERS,
RQUOTAPROC_GETQUOTA, (xdrproc_t)xdr_getquota_args, (char *)&gq_args,
(xdrproc_t)xdr_getquota_rslt, (char *)&gq_rslt) != 0)
return (0);
gq_args.gqa_id = id;
gq_args.gqa_type = quotatype;
call_stat = callaurpc(host, RQUOTAPROG, EXT_RQUOTAVERS,
RQUOTAPROC_GETQUOTA, (xdrproc_t)xdr_ext_getquota_args, (char *)&gq_args,
(xdrproc_t)xdr_getquota_rslt, (char *)&gq_rslt);
if (call_stat == RPC_PROGVERSMISMATCH) {
if (quotatype == USRQUOTA) {
old_gq_args.gqa_pathp = cp + 1;
old_gq_args.gqa_uid = id;
call_stat = callaurpc(host, RQUOTAPROG, RQUOTAVERS,
RQUOTAPROC_GETQUOTA, (xdrproc_t)xdr_getquota_args, (char *)&old_gq_args,
(xdrproc_t)xdr_getquota_rslt, (char *)&gq_rslt);
} else {
/* Old rpc quota does not support group type */
return (0);
}
}
if (call_stat != 0)
return (call_stat);
switch (gq_rslt.status) {
case Q_NOQUOTA:
@ -650,7 +661,7 @@ getnfsquota(struct statfs *fst, struct quotause *qup, long id, int quotatype)
return (0);
}
static int
static enum clnt_stat
callaurpc(char *host, int prognum, int versnum, int procnum,
xdrproc_t inproc, char *in, xdrproc_t outproc, char *out)
{
@ -671,8 +682,7 @@ callaurpc(char *host, int prognum, int versnum, int procnum,
tottimeout.tv_usec = 0;
clnt_stat = clnt_call(client, procnum, inproc, in,
outproc, out, tottimeout);
return ((int) clnt_stat);
return (clnt_stat);
}
static int