摘自 shell脚本实战 第二版 第五章 用户管理
脚本35 分析磁盘用量
即便超大容量磁盘已经面世,价格也在持续下跌,但系统管理员似乎永远都得关注磁盘使用 情况,避免共享驱动器被占满。
最常见的监视技术是使用 du 命令查看目录/usr 或/home,以确定其下所有子目录的磁盘使用 情况,报告用量居于前 5 位或前 10 位的用户。但这种方法的问题在于无法统计磁盘其他位置的 使用情况。要是用户在其他磁盘中还有另外的存储空间,或是有人偷偷摸摸地把视频保存在/tmp 中的隐藏目录或者 ftp 中的未用目录,我们是无法检测到的。另外,如果主目录散布在多个磁盘, 搜索每个/home 目录未必是最佳做法。
更好的解决方案是直接从文件/etc/passwd 中获得所有的账户名,然后搜索整个文件系统,找 出每个账户所拥有的文件,如代码清单 5-1 所示。
代码 fquota
#!/bin/bash
# fquota -- 用于Unix的磁盘配额分析工具
# 假设所有用户UID都大于或等于100
MAXDISKUSAGE=20000 # 以MB为单位
for name in $(cut -d: -f1,3 /etc/passwd |awk -F: '$2 > 99 {print $1}');do
echo -n "User $name exceeds disk quota. Disk usage is :"
# 你需要根据个人的磁盘布局修改下面的目录列表
# 最可能做出的改动是将/User 改为/home
find / /usr /var /Users -xdev -user $name -type f -ls | awk '{sum+=$7}END{print sum / (1024*1024)" Mbtyes"}' # 1 -xdev 确保find 不会去遍历所有的文件系统。也就是说,它可以避免命令搜索系统区域、只读源目录、可移动设备、/proc运行进程目录(Linux 系统)等类似位置。这就是为什么要明确指定/usr、/var、/home 的原因。处于备份和管理目的,这些目录通常都会被挂载到单独的文件系统。如果它们位和根文件相同的文件系统中,那么find命令并不会因为其他命令行中被单独列出而重复搜索
done | awk "\$9 > $MAXDISKUSAGE {print \$0}" # 2 只允许磁盘用量大于预设MAXDISKUSAGE的用户发送信息
exit 0
运行结果
$ sudo ./fquota
User nobody exceeds disk quota. Disk usage is :0 Mbtyes
User systemd-timesync exceeds disk quota. Disk usage is :0 Mbtyes
User systemd-network exceeds disk quota. Disk usage is :0 Mbtyes
User systemd-resolve exceeds disk quota. Disk usage is :0 Mbtyes
User messagebus exceeds disk quota. Disk usage is :0 Mbtyes
User syslog exceeds disk quota. Disk usage is :48.267 Mbtyes
User _apt exceeds disk quota. Disk usage is :0 Mbtyes
User tss exceeds disk quota. Disk usage is :0 Mbtyes
User uuidd exceeds disk quota. Disk usage is :0 Mbtyes
User tcpdump exceeds disk quota. Disk usage is :0 Mbtyes
User avahi-autoipd exceeds disk quota. Disk usage is :0 Mbtyes
User usbmux exceeds disk quota. Disk usage is :0 Mbtyes
User rtkit exceeds disk quota. Disk usage is :0 Mbtyes
User dnsmasq exceeds disk quota. Disk usage is :0 Mbtyes
User avahi exceeds disk quota. Disk usage is :0 Mbtyes
User cups-pk-helper exceeds disk quota. Disk usage is :0 Mbtyes
User speech-dispatcher exceeds disk quota. Disk usage is :0 Mbtyes
User kernoops exceeds disk quota. Disk usage is :0 Mbtyes
User saned exceeds disk quota. Disk usage is :0 Mbtyes
User nm-openvpn exceeds disk quota. Disk usage is :0 Mbtyes
User whoopsie exceeds disk quota. Disk usage is :0 Mbtyes
User colord exceeds disk quota. Disk usage is :0 Mbtyes
User sssd exceeds disk quota. Disk usage is :0 Mbtyes
User geoclue exceeds disk quota. Disk usage is :0 Mbtyes
User pulse exceeds disk quota. Disk usage is :0 Mbtyes
User hplip exceeds disk quota. Disk usage is :0 Mbtyes
User gnome-initial-setup exceeds disk quota. Disk usage is :0 Mbtyes
User gdm exceeds disk quota. Disk usage is :19.0689 Mbtyes
User amlogic exceeds disk quota. Disk usage is :286233 Mbtyes
User systemd-coredump exceeds disk quota. Disk usage is :193.105 Mbtyes
User sshd exceeds disk quota. Disk usage is :0 Mbtyes
User minidlna exceeds disk quota. Disk usage is :0.0175667 Mbtyes
User mysql exceeds disk quota. Disk usage is :347.761 Mbtyes
User postfix exceeds disk quota. Disk usage is :6.29425e-05 Mbtyes
精益求精
这种脚本的完整版具备某种形式的自动化电子邮件功能,可以对那些攫取磁盘空间的用户作 出警告。在下一个脚本中就展示了这种改进。
脚本36 报告磁盘占用大户
大多数系统管理员都寻求以最简单的方法解决问题,而管理磁盘配额最简单的方法就是扩 展 fquota(脚本#35),让它直接向消耗过多磁盘空间的用户发送电子邮件,以示警告,如代码清 单 5-3 所示。
代码 diskhogs
#!/bin/bash
# diskhogs -- 用于Unix的磁盘配额分析工具
# 假设所有用户的UID都大于或等于100
# 向超出配额的用户发送电子邮件并在屏幕上报告汇总信息
MAXDISKUSAGE=500
violators="/tmp/diskhogs0.$$" # 1
trap "$(which rm) -f $violators" 0 # 2
for name in $(cut -d: -f1,3 /etc/passwd |awk -F: '$2 > 99 {print $1}');do # 3
echo -n "$name" # 4
# 你可能需要根据个人的磁盘布局修改下面的目录列表
# 最可能做出的改动是将/User 改为/Home
find /usr /var /home -xdev -user $name -type f -ls | awk '{sum+=$7}END{print sum / (1024*1024) }'
done | awk "\$2 > $MAXDISKUSAGE {print \$0}" > $violators
if [ ! -s $violators ];then # 5
echo "No users exceecd the disk quota of ${MAXDISKUSAGE}MB"
cat $violators
exit 0
fi
while read account usage;do
# 6 用于发送电子邮件的管道中加入了fmt命令
cat << EOF
"Warning: $account Exceeds Quota" $account Your disk usage is ${usage}MB, but you have been allocated only ${MAXDISKUSAGE}MB. This means that you need to delete some of your files, compress your files (see 'gzip' or 'bzip2' for powerful and easy-to-use compression programs), or talk with us about increasing your disk allocation.
Thanks for your cooperation in this matter.
Your friendly neighborhood sysadmin
EOF
echo "Account $account has $usage MB of disk space. User notified."
done < $violators
exit 0
运行结果
$ sudo ./diskhogs
No users exceecd the disk quota of 500MB
精益求精
该脚本中一处有用的改进是允许某些用户拥有比其他用户更高的配额。这很容易做到:创建 单独的一个文件,在其中定义每个用户的磁盘配额,由脚本为没有出现在该文件中的用户设置默 认配额。包含账户名及其配额的文件可以用 grep 扫描,然后调用 cut -f2 提取第二个字段。
脚本37 提高df输出的可读性
代码 newdf
#!/bin/bash
# newdf -- 一个更友好的df版本
awkscript="/tmp/newdf.$$"
trap "rm -f $awkscript" EXIT
cat << 'EOF' > $awkscript
function showunit(size)
{
mb = size / 1024;prettymb=(int(mb * 100)) / 100; # 1
gb = mb / 1024;prettygb=(int(gb * 100)) / 100; # 2
if (substr(size,1,1) !~ "[0-9]" ||
substr(size,2,1) !~ "[0-9]" ) {return size}
else if (mb < 1) { return size "K" }
else if (gb < 1) { return prettymb "M"}
else { return prettygb "G"}
}
BEGIN{
printf "%-37s %10s %7s %7s %8s %-s\n",
"Filesystem", "size","Used","Avail","Capacity","Mounted"
}
!/Filesystem/{
size=showunit($2);
used=showunit($3);
avail=showunit($4);
printf "%-37s %10s %7s %7s %8s %-s\n",
$1,size,used,avail,$5,$6
}
EOF
df -k|awk -f $awkscript # 3 -k 查看磁盘分区
exit 0
运行结果
$ ./newdf
Filesystem size Used Avail Capacity Mounted
df: /run/user/1000/doc: Operation not permitted
tmpfs 6.25G 2.46M 6.25G 1% /run
/dev/nvme0n1p2 1832.28G 297.64G 1441.49G 18% /
tmpfs 31.26G 39.2M 31.22G 1% /dev/shm
tmpfs 5M 4 4.99M 1% /run/lock
tmpfs 4M 0 4M 0% /sys/fs/cgroup
/dev/nvme0n1p1 510.98M 7.94M 503.04M 2% /boot/efi
overlay 1832.28G 297.64G 1441.49G 18% /var/lib/docker/overlay2/727cffd35dcdfd44dc5345ecfe5d5821433f37389ff6da52ac3cbaf307867543/merged
overlay 1832.28G 297.64G 1441.49G 18% /var/lib/docker/overlay2/6a1a0800215ebbe71cdcbb6c696b19802e6af9f0bf72e7a216d3deb206e09328/merged
overlay 1832.28G 297.64G 1441.49G 18% /var/lib/docker/overlay2/07ec654db7b8a78378e927587f41b42bfd0004a128069e3dd9a3a1a3056d6f55/merged
tmpfs 6.25G 148K 6.25G 1% /run/user/1000
精益求精
该脚本中还存在一些问题,其中最主要的是现在很多版本的 df 都包含 inode 的使用情况, 不少版本还会显示处理器内部信息,尽管这实在是没什么值得关心的(例如上面例子中的两个 map 项)。实际上,如果我们能把这些内容过滤掉,脚本会实用得多,所以,第一处改进就是给末 尾的 df 调用加上-P 选项,删除 inode 使用情况信息。(你也可以将其添加为新的一列,但这样 的话,输出会变得更宽,更难以格式化。)至于删掉那些 map 项,用 grep 就很容易搞定。只需在 后面加上|grep -v "^map"就行了。
脚本38 获取可用的磁盘空间
脚本#37 简化了 df 的输出,使其更容易阅读和理解。另一个更基本的问题,也就是系统究竟 有多少可用的磁盘空间,也可以通过 shell 脚本解决。df 命令以每个磁盘为基础报告磁盘使用情 况,但输出可能有点不太清晰:
Filesystem 512-blocks Used Available Capacity iused ifree %iused Mounted on
/dev/disk1s1s1 976490576 30767272 496699848 6% 577263 2483499240 0% /
devfs 380 380 0 100% 658 0 100% /dev
/dev/disk1s5 976490576 40 496699848 1% 2 2483499240 0% /System/Volumes/VM
/dev/disk1s3 976490576 1289928 496699848 1% 2132 2483499240 0% /System/Volumes/Preboot
/dev/disk1s6 976490576 12008 496699848 1% 19 2483499240 0% /System/Volumes/Update
/dev/disk1s2 976490576 445264312 496699848 48% 1091811 2483499240 0% /System/Volumes/Data
map auto_home 0 0 0 100% 0 0 100% /System/Volumes/Data/home
更实用的 df 版本可以汇总第 4 列“Available”的值,然后以更易读的形式给出汇总结果。 用 awk 命令很容易就可以做到,如代码清单 5-9 所示。
代码diskspace
#!/bin/bash
# diskspace -- 汇总可用磁盘空间,以更合乎逻辑且易读的形式输出
tempfile="/tmp/available.$$"
trap "rm -f $tempfile" EXIT
cat << 'EOF' > $tempfile
{ sum += $4 }
END { mb = sum / 1024
gb = mb / 1024
printf "%.0f MB(%.2fBG) of available disk space \n",mb ,gb
}
EOF
df -k | awk -f $tempfile # 1 df的输出管道传给awk
exit 0
运行结果
$ ./diskspace
5949669 MB(5810.22BG) of available disk space
精益求精
如果你的系统在多个磁盘上拥有大量的磁盘空间,你可以扩展该脚本,使其自动返回以 TB 为单位的值。如果只是空间不足,那么看到 0.03 GB 的可用空间无疑会让人沮丧,不过这倒是一 个不错的机会去试试用脚本#36 清理磁盘空间,你说是不是?
另一个要考虑的问题是,了解所有设备上的可用磁盘空间是否更有用(包括那些不会增长的 分区,例如/boot),或者是否只报告用户卷的空间就够了。如果选择后者,那么你可以在 df 调用 后立即调用 grep,通过 grep 输出那些需要汇总的设备,或者在 grep -v 后面跟上不感兴趣的 设备名称,将其从汇总过程中排除。
脚本39 实现安全的locate
脚本#19 的 locate 尽管实用,但存在一个安全问题:如果数据库的构建进程是以 root 身份运 行,那么数据库中将包含整个系统中所有的文件和目录,这使得用户可以查看到他们本无权访问 的目录和文件名。普通用户也可以构建数据库(OS X 就是这么做的,其中的 mklocatedb 以用户 nobody 身份运行),但是这样也不妥当,因为你想要的是能够在属于自己的目录树中找到任何文 件,不管用户 nobody 能不能访问这些特定的文件和目录。
这个两难问题的一种解决方法是增加保存在 locate 数据库中的数据,使得每条记录都包含 属主、属组、权限这些附加信息。但即便如此,mklocatedb 数据库本身也不够安全,除非 locate 脚本设置 setuid 或 setgid,但考虑到系统安全,这是我们要竭力避免的。
折衷的做法是让每个用户拥有独立的.locatedb 文件。这种选择并不算太糟糕,因为只有使用 locate 命令的用户才需要个人数据库。一旦调用该命令,系统会在用户的主目录中创建.locatedb 文件,同时 cron 作业可以每晚更新.locatedb,保持同步。用户首次运行安全版的 slocate 脚本时, 脚本会输出提示信息,告知用户也许只会看到可公开访问的文件。等到第二天(取决于 cron 的 安排),用户就能得到属于自己的数据库了。
代码mkslocatedb
#!/bin/bash
# mkslocatedb -- 以用户nobody的身份构建中央公共数据库,
# 同时遍历每个用户的主目录,在其中查找.slocatedb文件。
# 如果找到,就位该用户创建爱你另外一个私有版本的locate数据库。
locatedb="/var/locate.db"
slocatedb=".slocatedb"
if [ "$(id -nu)" != "root" ] ; then
echo "$0: Error: You must be root to run this command." >&2
exit 1
fi
if [ "$(grep '^nobody:' /etc/passwd)" = "" ] ; then
echo "$0: Error: you must have an account for user 'nobody'" >&2
echo "to create the default slocate database." >&2; exit 1
fi
cd / # 避开执行su之后的当前目录的权限问题。
# 首先,创建或更新公共数据库
# -c 变更为帐号为 USER 的使用者并执行指令(command)后再变回原来使用者
# -f 或 --fast 不必读启动档(如 csh.cshrc 等),仅用于 csh 或 tcsh
# -m -p 或 --preserve-environment 执行 su 时不改变环境变数
# 脚本中使用的su命令很有讲究,因为默认情况下,su不仅会修改有效用户ID,还会导入相应的用户环境。除非指定-m选项(该选项可以禁止带入用户环境),否则最终会在Unix系统中产生莫名其妙的错误信息。-f选项再多一层保险,绕过csh或tcsh用户的.cshrc文件
su -fm nobody -c "find / -print" > $locatedb 2>/dev/null
echo "building default slocate database (user = nobody)"
echo ... result is $(wc -l < $locatedb) lines long.
# 遍历所有用户的主目录,看看谁有.slocatedb 文件
for account in $(cut -d: -f1 /etc/passwd)
do
homedir="$(grep "^${account}:" /etc/passwd | cut -d: -f6)"
if [ "$homedir" = "/" ] ; then
continue # 如果是根目录,则不建立文件
elif [ -e $homedir/$slocatedb ] ; then
echo "building slocate database for user $account"
su -m $account -c "find / -print" > $homedir/$slocatedb \
2>/dev/null
chmod 600 $homedir/$slocatedb
chown $account $homedir/$slocatedb
echo ... result is $(wc -l < $homedir/$slocatedb) lines long.
fi
done
exit 0
代码 slocate
#!/bin/bash
# slocate -- 尝试在用户自己的安全slocatedb数据库中搜索指定的模式
# 如果找不到,则意味着数据库不安全,输出警告信息并创建数据库。
# 如果个人的.slocatedb 数据库为空,则使用系统数据库
locatedb="/var/locate.db"
slocatedb="$HOME/.slocatedb"
if [ ! -e $slocatedb -o "$1" = "--explain" ];then
cat << "EOF" >&2
Warning: Secure locate keeps a private database for each user, and your
database hasn't yet been created. Until it is (probably late tonight),
I'll just use the public locate database, which will show you all
publicly accessible matches rather than those explicitly available to
account ${USER:-$LOGNAME}.
EOF
if [ "$1" = "--explain" ];then
exit 0
fi
# 在继续往下进行之前,先创建.slocatedb,这样下次脚本
# mkslocatedb运行的时候,cron就可以向其中填入内容。
touch $slocatedb # mkslocatedb 会在下次建立该文件
chmod 600 $slocatedb # 设置好正确的权限
elif [ -s $slocatedb ];then # 判断文件大小是否为零
locatedb=#slocatedb
else
echo "Warning: using public database. Use \"$0 --explain\" for details." >&2
fi
if [ -z "$1" ];then
echo "Usage: $0 pattern" >&2; exit 1
fi
exec grep -i "$1" $locatedb
运行结果
# 以 root 身份运行脚本 mkslocatedb
# mkslocatedb
building default slocate database (user = nobody)
... result is 99809 lines long.
building slocate database for user taylor
... result is 99808 lines long.
# 先尝试以用户 tintin 的身份(该用户没有.slocatedb 文件)搜索匹配指定模式的文件:
tintin $ slocate Taylor-Self-Assess.doc
Warning: using public database. Use "slocate --explain" for details.
$
# 现在,再以用户 taylor 的身份输入相同的命令,taylor 是待查找文件的属主:
taylor $ slocate Taylor-Self-Assess.doc
/Users/taylor/Documents/Merrick/Taylor-Self-Assess.doc
精益求精
如果文件系统内的文件数量众多,那么这种方法消耗的磁盘空间可不是个小数量。一种解决 方法是确保单独的.slocatedb 数据库中不包含已经出现在中央数据库中的记录。这得提前做一些 处理(对两个数据库使用 sort,然后再使用 diff;或者在搜索用户文件的时候跳过/usr 和/bin), 但好处是节省了磁盘空间。
另一种方法是建立单独的.slocatedb 文件,仅引用自上次更新后访问过的文件。如果每周运 行一次 mkslocatedb 脚本的话,效果要比每天运行更好。否则,所有用户在每周一就都回到原点 了,因为他们不太可能在周末运行 slocate 命令。
最后,还有一种简单的方法是压缩.slocatedb 文件,在使用 slocate 进行搜索的时候再将其解 压缩。脚本#33 中的 zgrep 命令可以给你一些这方面的启发。