diff --git a/nfs-utils-2.5.4-rpcctl.patch b/nfs-utils-2.5.4-rpcctl.patch new file mode 100644 index 0000000..ca32f63 --- /dev/null +++ b/nfs-utils-2.5.4-rpcctl.patch @@ -0,0 +1,377 @@ +diff --git a/configure.ac b/configure.ac +index 93520a80..d01ce6e4 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -712,6 +712,7 @@ AC_CONFIG_FILES([ + tools/rpcgen/Makefile + tools/mountstats/Makefile + tools/nfs-iostat/Makefile ++ tools/rpcctl/Makefile + tools/nfsdclnts/Makefile + tools/nfsconf/Makefile + tools/nfsdclddb/Makefile +diff --git a/tools/Makefile.am b/tools/Makefile.am +index 9b4b0803..c3feabbe 100644 +--- a/tools/Makefile.am ++++ b/tools/Makefile.am +@@ -12,6 +12,6 @@ if CONFIG_NFSDCLD + OPTDIRS += nfsdclddb + endif + +-SUBDIRS = locktest rpcdebug nlmtest mountstats nfs-iostat nfsdclnts $(OPTDIRS) ++SUBDIRS = locktest rpcdebug nlmtest mountstats nfs-iostat rpcctl nfsdclnts $(OPTDIRS) + + MAINTAINERCLEANFILES = Makefile.in +diff --git a/tools/rpcctl/Makefile.am b/tools/rpcctl/Makefile.am +new file mode 100644 +index 00000000..33fb431f +--- /dev/null ++++ b/tools/rpcctl/Makefile.am +@@ -0,0 +1,13 @@ ++## Process this file with automake to produce Makefile.in ++PYTHON_FILES = rpcctl.py ++ ++man8_MANS = rpcctl.man ++ ++EXTRA_DIST = $(man8_MANS) $(PYTHON_FILES) ++ ++all-local: $(PYTHON_FILES) ++ ++install-data-hook: ++ $(INSTALL) -m 755 rpcctl.py $(DESTDIR)$(sbindir)/rpcctl ++ ++MAINTAINERCLEANFILES=Makefile.in +diff --git a/tools/rpcctl/rpcctl.man b/tools/rpcctl/rpcctl.man +new file mode 100644 +index 00000000..b87ba0df +--- /dev/null ++++ b/tools/rpcctl/rpcctl.man +@@ -0,0 +1,67 @@ ++.\" ++.\" rpcctl(8) ++.\" ++.TH rpcctl 8 "15 Feb 2022" ++.SH NAME ++rpcctl \- Displays SunRPC connection information ++.SH SYNOPSIS ++.nf ++.BR rpcctl " [ \fB\-h \fR| \fB\-\-help \fR] { \fBclient \fR| \fBswitch \fR| \fBxprt \fR}" ++.P ++.BR "rpcctl client" " \fR[ \fB\-h \fR| \fB\-\-help \fR] { \fBshow \fR}" ++.BR "rpcctl client show " "\fR[ \fB\-h \f| \fB\-\-help \fR] [ \fIXPRT \fR]" ++.P ++.BR "rpcctl switch" " \fR[ \fB\-h \fR| \fB\-\-help \fR] { \fBset \fR| \fBshow \fR}" ++.BR "rpcctl switch set" " \fR[ \fB\-h \fR| \fB\-\-help \fR] \fISWITCH \fBdstaddr \fINEWADDR" ++.BR "rpcctl switch show" " \fR[ \fB\-h \fR| \fB\-\-help \fR] [ \fISWITCH \fR]" ++.P ++.BR "rpcctl xprt" " \fR[ \fB\-h \fR| \fB\-\-help \fR] { \fBremove \fR| \fBset \fR| \fBshow \fR}" ++.BR "rpcctl xprt remove" " \fR[ \fB\-h \fR| \fB\-\-help \fR] \fIXPRT" ++.BR "rpcctl xprt set" " \fR[ \fB\-h \fR| \fB\-\-help \fR] \fIXPRT \fR{ \fBdstaddr \fINEWADDR \fR| \fBoffline \fR| \fBonline \fR}" ++.BR "rpcctl xprt show" " \fR[ \fB\-h \fR| \fB\-\-help \fR] [ \fIXPRT \fR]" ++.fi ++.SH DESCRIPTION ++.RB "The " rpcctl " command displays information collected in the SunRPC sysfs files about the system's SunRPC objects. ++.P ++.SS rpcctl client \fR- \fBCommands operating on RPC clients ++.IP "\fBshow \fR[ \fICLIENT \fR] \fB(default)" ++Show detailed information about the RPC clients on this system. ++If \fICLIENT \fRwas provided, then only show information about a single RPC client. ++.P ++.SS rpcctl switch \fR- \fBCommands operating on groups of transports ++.IP "\fBset \fISWITCH \fBdstaddr \fINEWADDR" ++Change the destination address of all transports in the \fISWITCH \fRto \fINEWADDR\fR. ++\fINEWADDR \fRcan be an IP address, DNS name, or anything else resolvable by \fBgethostbyname\fR(3). ++.IP "\fBshow \fR[ \fISWITCH \fR] \fB(default)" ++Show detailed information about groups of transports on this system. ++If \fISWITCH \fRwas provided, then only show information about a single transport group. ++.P ++.SS rpcctl xprt \fR- \fBCommands operating on individual transports ++.IP "\fBremove \fIXPRT" ++Removes the specified \fIXPRT \fRfrom the system. ++Note that "main" transports cannot be removed. ++.P ++.IP "\fBset \fIXPRT \fBdstaddr \fINEWADDR" ++Change the destination address of the specified \fIXPRT \fR to \fINEWADDR\fR. ++\fINEWADDR \fRcan be an IP address, DNS name, or anything else resolvable by \fBgethostbyname\fR(3). ++.P ++.IP "\fBset \fIXPRT \fBoffline" ++Sets the specified \fIXPRT\fR's state to offline. ++.P ++.IP "\fBset \fIXPRT \fBonline" ++Sets the specified \fIXPRT\fR's state to online. ++.IP "\fBshow \fR[ \fIXPRT \fR] \fB(default)" ++Show detailed information about this system's transports. ++If \fIXPRT \fRwas provided, then only show information about a single transport. ++.SH EXAMPLES ++.IP "\fBrpcctl switch show switch-2" ++Show details about the RPC switch named "switch-2". ++.IP "\fBrpcctl xprt remove xprt-4" ++Remove the xprt named "xprt-4" from the system. ++.IP "\fBrpcctl xprt set xprt-3 dstaddr https://linux-nfs.org ++Change the dstaddr of the xprt named "xprt-3" to point to linux-nfs.org ++.SH DIRECTORY ++.TP ++.B /sys/kernel/sunrpc/ ++.SH AUTHOR ++Anna Schumaker +diff --git a/tools/rpcctl/rpcctl.py b/tools/rpcctl/rpcctl.py +new file mode 100755 +index 00000000..b8df556b +--- /dev/null ++++ b/tools/rpcctl/rpcctl.py +@@ -0,0 +1,255 @@ ++#!/usr/bin/python3 ++import argparse ++import collections ++import errno ++import os ++import pathlib ++import socket ++import sys ++ ++with open("/proc/mounts", 'r') as f: ++ mount = [ line.split()[1] for line in f if "sysfs" in line ] ++ if len(mount) == 0: ++ print("ERROR: sysfs is not mounted") ++ sys.exit(1) ++ ++sunrpc = pathlib.Path(mount[0]) / "kernel" / "sunrpc" ++if not sunrpc.is_dir(): ++ print("ERROR: sysfs does not have sunrpc directory") ++ sys.exit(1) ++ ++def read_addr_file(path): ++ try: ++ with open(path, 'r') as f: ++ return f.readline().strip() ++ except: ++ return "(enoent)" ++ ++def write_addr_file(path, newaddr): ++ with open(path, 'w') as f: ++ f.write(newaddr) ++ return read_addr_file(path) ++ ++def read_info_file(path): ++ res = collections.defaultdict(int) ++ try: ++ with open(path) as info: ++ lines = [ l.split("=", 1) for l in info if "=" in l ] ++ res.update({ key:int(val.strip()) for (key, val) in lines }) ++ finally: ++ return res ++ ++ ++class Xprt: ++ def __init__(self, path): ++ self.path = path ++ self.name = path.stem.rsplit("-", 1)[0] ++ self.type = path.stem.split("-")[2] ++ self.info = read_info_file(path / "xprt_info") ++ self.dstaddr = read_addr_file(path / "dstaddr") ++ self.srcaddr = read_addr_file(path / "srcaddr") ++ self.read_state() ++ ++ def __lt__(self, rhs): ++ return self.name < rhs.name ++ ++ def _xprt(self): ++ main = ", main" if self.info.get("main_xprt") else "" ++ return f"{self.name}: {self.type}, {self.dstaddr}, " \ ++ f"port {self.info['dst_port']}, state <{self.state}>{main}" ++ ++ def _src_reqs(self): ++ return f" Source: {self.srcaddr}, port {self.info['src_port']}, " \ ++ f"Requests: {self.info['num_reqs']}" ++ ++ def _cong_slots(self): ++ return f" Congestion: cur {self.info['cur_cong']}, win {self.info['cong_win']}, " \ ++ f"Slots: min {self.info['min_num_slots']}, max {self.info['max_num_slots']}" ++ ++ def _queues(self): ++ return f" Queues: binding {self.info['binding_q_len']}, " \ ++ f"sending {self.info['sending_q_len']}, pending {self.info['pending_q_len']}, " \ ++ f"backlog {self.info['backlog_q_len']}, tasks {self.info['tasks_queuelen']}" ++ ++ def __str__(self): ++ if not self.path.exists(): ++ return f"{self.name}: has been removed" ++ return "\n".join([self._xprt(), self._src_reqs(), ++ self._cong_slots(), self._queues() ]) ++ ++ def read_state(self): ++ if self.path.exists(): ++ with open(self.path / "xprt_state") as f: ++ self.state = ','.join(f.readline().split()[1:]) ++ ++ def small_str(self): ++ main = " [main]" if self.info.get("main_xprt") else "" ++ return f"{self.name}: {self.type}, {self.dstaddr}{main}" ++ ++ def set_dstaddr(self, newaddr): ++ self.dstaddr = write_addr_file(self.path / "dstaddr", newaddr) ++ ++ def set_state(self, state): ++ with open(self.path / "xprt_state", 'w') as f: ++ f.write(state) ++ self.read_state() ++ ++ def add_command(subparser): ++ parser = subparser.add_parser("xprt", help="Commands for individual xprts") ++ parser.set_defaults(func=Xprt.show, xprt=None) ++ subparser = parser.add_subparsers() ++ ++ remove = subparser.add_parser("remove", help="Remove an xprt") ++ remove.add_argument("xprt", metavar="XPRT", nargs=1, ++ help="Name of the xprt to remove") ++ remove.set_defaults(func=Xprt.set_property, property="remove") ++ ++ show = subparser.add_parser("show", help="Show xprts") ++ show.add_argument("xprt", metavar="XPRT", nargs='?', ++ help="Name of a specific xprt to show") ++ show.set_defaults(func=Xprt.show) ++ ++ set = subparser.add_parser("set", help="Change an xprt property") ++ set.add_argument("xprt", metavar="XPRT", nargs=1, ++ help="Name of a specific xprt to modify") ++ subparser = set.add_subparsers(required=True) ++ online = subparser.add_parser("online", help="Set an xprt online") ++ online.set_defaults(func=Xprt.set_property, property="online") ++ offline = subparser.add_parser("offline", help="Set an xprt offline") ++ offline.set_defaults(func=Xprt.set_property, property="offline") ++ dstaddr = subparser.add_parser("dstaddr", help="Change an xprt's dstaddr") ++ dstaddr.add_argument("newaddr", metavar="NEWADDR", nargs=1, ++ help="The new address for the xprt") ++ dstaddr.set_defaults(func=Xprt.set_property, property="dstaddr") ++ ++ def get_by_name(name): ++ glob = f"**/{name}-*" if name else "**/xprt-*" ++ res = [ Xprt(x) for x in (sunrpc / "xprt-switches").glob(glob) ] ++ if name and len(res) == 0: ++ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), ++ f"{sunrpc / 'xprt-switches' / glob}") ++ return sorted(res) ++ ++ def show(args): ++ for xprt in Xprt.get_by_name(args.xprt): ++ print(xprt) ++ ++ def set_property(args): ++ for xprt in Xprt.get_by_name(args.xprt[0]): ++ if args.property == "dstaddr": ++ xprt.set_dstaddr(socket.gethostbyname(args.newaddr[0])) ++ elif args.property == "remove": ++ xprt.set_state("offline") ++ xprt.set_state("remove") ++ else: ++ args.set_state(args.property) ++ print(xprt) ++ ++ ++class XprtSwitch: ++ def __init__(self, path, sep=":"): ++ self.path = path ++ self.name = path.stem ++ self.info = read_info_file(path / "xprt_switch_info") ++ self.xprts = sorted([ Xprt(p) for p in self.path.iterdir() if p.is_dir() ]) ++ self.sep = sep ++ ++ def __lt__(self, rhs): ++ return self.name < rhs.name ++ ++ def __str__(self): ++ switch = f"{self.name}{self.sep} " \ ++ f"xprts {self.info['num_xprts']}, " \ ++ f"active {self.info['num_active']}, " \ ++ f"queue {self.info['queue_len']}" ++ xprts = [ f" {x.small_str()}" for x in self.xprts ] ++ return "\n".join([ switch ] + xprts) ++ ++ def add_command(subparser): ++ parser = subparser.add_parser("switch", help="Commands for xprt switches") ++ parser.set_defaults(func=XprtSwitch.show, switch=None) ++ subparser = parser.add_subparsers() ++ ++ show = subparser.add_parser("show", help="Show xprt switches") ++ show.add_argument("switch", metavar="SWITCH", nargs='?', ++ help="Name of a specific switch to show") ++ show.set_defaults(func=XprtSwitch.show) ++ ++ set = subparser.add_parser("set", help="Change an xprt switch property") ++ set.add_argument("switch", metavar="SWITCH", nargs=1, ++ help="Name of a specific xprt switch to modify") ++ subparser = set.add_subparsers(required=True) ++ dstaddr = subparser.add_parser("dstaddr", help="Change an xprt switch's dstaddr") ++ dstaddr.add_argument("newaddr", metavar="NEWADDR", nargs=1, ++ help="The new address for the xprt switch") ++ dstaddr.set_defaults(func=XprtSwitch.set_property, property="dstaddr") ++ ++ def get_by_name(name): ++ xprt_switches = sunrpc / "xprt-switches" ++ if name: ++ return [ XprtSwitch(xprt_switches / name) ] ++ return [ XprtSwitch(f) for f in sorted(xprt_switches.iterdir()) ] ++ ++ def show(args): ++ for switch in XprtSwitch.get_by_name(args.switch): ++ print(switch) ++ ++ def set_property(args): ++ for switch in XprtSwitch.get_by_name(args.switch[0]): ++ resolved = socket.gethostbyname(args.newaddr[0]) ++ for xprt in switch.xprts: ++ xprt.set_dstaddr(resolved) ++ print(switch) ++ ++ ++class RpcClient: ++ def __init__(self, path): ++ self.path = path ++ self.name = path.stem ++ self.switch = XprtSwitch(path / (path / "switch").readlink(), sep=",") ++ ++ def __lt__(self, rhs): ++ return self.name < rhs.name ++ ++ def __str__(self): ++ return f"{self.name}: {self.switch}" ++ ++ def add_command(subparser): ++ parser = subparser.add_parser("client", help="Commands for rpc clients") ++ parser.set_defaults(func=RpcClient.show, client=None) ++ subparser = parser.add_subparsers() ++ ++ show = subparser.add_parser("show", help="Show rpc clients") ++ show.add_argument("client", metavar="CLIENT", nargs='?', ++ help="Name of a specific rpc client to show") ++ parser.set_defaults(func=RpcClient.show) ++ ++ def get_by_name(name): ++ rpc_clients = sunrpc / "rpc-clients" ++ if name: ++ return [ RpcClient(rpc_clients / name) ] ++ return [ RpcClient(f) for f in sorted(rpc_clients.iterdir()) ] ++ ++ def show(args): ++ for client in RpcClient.get_by_name(args.client): ++ print(client) ++ ++ ++parser = argparse.ArgumentParser() ++ ++def show_small_help(args): ++ parser.print_usage() ++ print("sunrpc dir:", sunrpc) ++parser.set_defaults(func=show_small_help) ++ ++subparser = parser.add_subparsers(title="commands") ++RpcClient.add_command(subparser) ++XprtSwitch.add_command(subparser) ++Xprt.add_command(subparser) ++ ++args = parser.parse_args() ++try: ++ args.func(args) ++except Exception as e: ++ print(str(e)) ++ sys.exit(1) diff --git a/nfs-utils.spec b/nfs-utils.spec index 0dc85a3..c55c633 100644 --- a/nfs-utils.spec +++ b/nfs-utils.spec @@ -2,7 +2,7 @@ Summary: NFS utilities and supporting clients and daemons for the kernel NFS ser Name: nfs-utils URL: http://linux-nfs.org/ Version: 2.5.4 -Release: 9%{?dist} +Release: 10%{?dist} Epoch: 1 # group all 32bit related archs @@ -22,6 +22,7 @@ Patch002: nfs-utils-2.5.4-nfsdcltrack-printf.patch Patch003: nfs-utils-2.5.4-general-memory-fixes.patch Patch004: nfs-utils-2.5.4-mount-nov2.patch Patch005: nfs-utils-2.5.4-gssd-debug-msg.patch +Patch006: nfs-utils-2.5.4-rpcctl.patch Patch100: nfs-utils-1.2.1-statdpath-man.patch Patch101: nfs-utils-1.2.1-exp-subtree-warn-off.patch @@ -35,6 +36,7 @@ Provides: exportfs = %{epoch}:%{version}-%{release} Provides: nfsstat = %{epoch}:%{version}-%{release} Provides: showmount = %{epoch}:%{version}-%{release} Provides: rpcdebug = %{epoch}:%{version}-%{release} +Provides: rpcclt = %{epoch}:%{version}-%{release} Provides: rpc.idmapd = %{epoch}:%{version}-%{release} Provides: rpc.mountd = %{epoch}:%{version}-%{release} Provides: rpc.nfsd = %{epoch}:%{version}-%{release} @@ -99,6 +101,7 @@ Show NFS client Statistics %package -n nfsv4-client-utils Summary: NFSv4 utilities for supporting client Provides: rpc.gssd = %{epoch}:%{version}-%{release} +Provides: rpcclt = %{epoch}:%{version}-%{release} Provides: mount.nfs = %{epoch}:%{version}-%{release} Provides: mount.nfs4 = %{epoch}:%{version}-%{release} Provides: umount.nfs = %{epoch}:%{version}-%{release} @@ -328,6 +331,7 @@ fi %{_sbindir}/exportfs %{_sbindir}/nfsstat %{_sbindir}/rpcdebug +%{_sbindir}/rpcctl %{_sbindir}/rpc.mountd %{_sbindir}/rpc.nfsd %{_sbindir}/showmount @@ -416,6 +420,7 @@ fi %attr(0600,root,root) %config(noreplace) %{_sysconfdir}/gssproxy/24-nfs-server.conf %attr(0600,root,root) %config(noreplace) %{_sysconfdir}/nfsmount.conf.d/10-nfsv4.conf %{_sbindir}/rpc.gssd +%{_sbindir}/rpcctl %{_sbindir}/nfsidmap %{_sbindir}/nfsstat %attr(4755,root,root) /sbin/mount.nfs @@ -434,6 +439,7 @@ fi %{_mandir}/*/umount.nfs.8.gz %{_mandir}/*/nfsidmap.8.gz %{_mandir}/*/nfsstat.8.gz +%{_mandir}/*/rpcctl.8.gz %{_pkgdir}/*/rpc-pipefs-generator %{_pkgdir}/*/auth-rpcgss-module.service %{_pkgdir}/*/nfs-client.target @@ -448,6 +454,9 @@ fi %{_mandir}/*/nfsiostat.8.gz %changelog +* Mon Feb 28 2022 Steve Dickson 2.5.4-10 +- Added the rpcctl command (bz 2059245) + * Sat Jan 22 2022 Steve Dickson 2.5.4-9 - manpage: remove the no longer supported value "vers2" (bz 1966643)