将 Shell 脚本转换为 Ansible
在最近一次客户访问中,我们被要求帮助将以下脚本迁移到 RHEL 和 AIX 服务器上,以部署集中式 sudoers 文件。这是一个常见的场景,可以提供一些利用高级 Ansible 功能的良好示例。此外,我们可以考虑从执行任务的脚本转变为描述和强制执行项目的幂等状态。
以下是脚本。
#!/bin/sh
# Desc: Distribute unified copy of /etc/sudoers
#
# $Id: $
#set -x
export ODMDIR=/etc/repos
#
# perform any cleanup actions we need to do, and then exit with the
# passed status/return code
#
clean_exit()
{
cd /
test -f "$tmpfile" && rm $tmpfile
exit $1
}
#Set variables
PROG=`basename $0`
PLAT=`uname -s|awk '{print $1}'`
HOSTNAME=`uname -n | awk -F. '{print $1}'`
HOSTPFX=$(echo $HOSTNAME |cut -c 1-2)
NFSserver="nfs-server"
NFSdir="/NFS/AIXSOFT_NFS"
MOUNTPT="/mnt.$$"
MAILTO="[email protected]"
DSTRING=$(date +%Y%m%d%H%M)
LOGFILE="/tmp/${PROG}.dist_sudoers.${DSTRING}.log"
BKUPFILE=/etc/sudoers.${DSTRING}
SRCFILE=${MOUNTPT}/skel/sudoers-uni
MD5FILE="/.sudoers.md5"
echo "Starting ${PROG} on ${HOSTNAME}" >> ${LOGFILE} 2>&1
# Make sure we run as root
runas=`id | awk -F'(' '{print $1}' | awk -F'=' '{print $2}'`
if [ $runas -ne 0 ] ; then
echo "$PROG: you must be root to run this script." >> ${LOGFILE} 2>&1
exit 1
fi
case "$PLAT" in
SunOS)
export PINGP=" -t 7 $NFSserver "
export MOUNTP=" -F nfs -o vers=3,soft "
export PATH="/usr/sbin:/usr/bin"
echo "SunOS" >> ${LOGFILE} 2>&1
exit 0
;;
AIX)
export PINGP=" -T 7 $NFSserver 2 2"
export MOUNTP=" -o vers=3,bsy,soft "
export PATH="/usr/bin:/etc:/usr/sbin:/usr/ucb:/usr/bin/X11:/sbin:/usr/java5/jre/bin:/usr/java5/bin"
printf "Continuing on AIX...\n\n" >> ${LOGFILE} 2>&1
;;
Linux)
export PINGP=" -t 7 -c 2 $NFSserver"
export MOUNTP=" -o nfsvers=3,soft "
export PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin"
printf "Continuing on Linux...\n\n" >> ${LOGFILE} 2>&1
;;
*)
echo "Unsupported Platform." >> ${LOGFILE} 2>&1
exit 1
esac
##
## Exclude Lawson Hosts
##
if [ ${HOSTPFX} = "la" ]
then
echo "Exiting Lawson host ${HOSTNAME} with no changes." >> ${LOGFILE} 2>&1
exit 0
fi
##
## * NFS Mount Section *
##
## Check to make sure NFS host is up
printf "Current PATH is..." >> ${LOGFILE} 2>&1
echo $PATH >> $LOGFILE 2>&1
ping $PINGP >> $LOGFILE 2>&1
if [ $? -ne 0 ]; then
echo " NFS server is DOWN ... ABORTING SCRIPT ... Please check server..." >> $LOGFILE
echo "$PROG failed on $HOSTNAME ... NFS server is DOWN ... ABORTING SCRIPT ... Please check server ... " | mailx -s "$PROG Failed on $HOSTNAME" $MAILTO
exit 1
else
echo " NFS server is UP ... We will continue..." >> $LOGFILE
fi
##
## Mount NFS share to HOSTNAME. We do this using a soft mount in case it is lost during a backup
##
mkdir $MOUNTPT
mount $MOUNTP $NFSserver:${NFSdir} $MOUNTPT >> $LOGFILE 2>&1
##
## Check to make sure mount command returned 0. If it did not odds are something else is mounted on /mnt.$$
##
if [ $? -ne 0 ]; then
echo " Mount command did not work ... Please check server ... Odds are something is mounted on $MOUNTPT ..." >> $LOGFILE
echo " $PROG failed on $HOSTNAME ... Mount command did not work ... Please check server ... Odds are something is mounted on $MOUNTPT ..." | mailx -s "$PROG Failed on $HOSTNAME" $MAILTO
exit 1
else
echo " Mount command returned a good status which means $MOUNPT was free for us to use ... We will now continue ..." >> $LOGFILE
fi
##
## Now check to see if the mount worked
##
if [ ! -f ${SRCFILE} ]; then
echo " File ${SRCFILE} is missing... Maybe NFS mount did NOT WORK ... Please check server ..." >> $LOGFILE
echo " $PROG failed on $HOSTNAME ... File ${SRCFILE} is missing... Maybe NFS mount did NOT WORK ... Please check server ..." | mailx -s "$PROG Failed on $HOSTNAME" $MA
ILTO
umount -f $MOUNTPT >> $LOGFILE
rmdir $MOUNTPT >> $LOGFILE
exit 1
else
echo " NFS mount worked we are going to continue ..." >> $LOGFILE
fi
##
## * Main Section *
##
if [ ! -f ${BKUPFILE} ]
then
cp -p /etc/sudoers ${BKUPFILE}
else
echo "Backup file already exists$" >> ${LOGFILE} 2>&1
exit 1
fi
if [ -f "$SRCFILE" ]
then
echo "Copying in new sudoers file from $SRCFILE." >> ${LOGFILE} 2>&1
cp -p $SRCFILE /etc/sudoers
chmod 440 /etc/sudoers
else
echo "Source file not found" >> ${LOGFILE} 2>&1
exit 1
fi
echo >> ${LOGFILE} 2>&1
visudo -c |tee -a ${LOGFILE}
if [ $? -ne 0 ]
then
echo "sudoers syntax error on $HOSTNAME." >> ${LOGFILE} 2>&1
mailx -s "${PROG}: sudoers syntax error on $HOSTNAME" "$MAILTO" << EOF
Syntax error /etc/sudoers on $HOSTNAME.
Reverting changes
Please investigate.
EOF
echo "Reverting changes." >> ${LOGFILE} 2>&1
cp -p ${BKUPFILE} /etc/sudoers
else
#
# Update checksum file
#
grep -v '/etc/sudoers' ${MD5FILE} > ${MD5FILE}.tmp
csum /etc/sudoers >> ${MD5FILE}.tmp
mv ${MD5FILE}.tmp ${MD5FILE}
chmod 600 ${MD5FILE}
fi
echo >> ${LOGFILE} 2>&1
if [ "${HOSTPFX}" = "hd" ]
then
printf "\nAppending #includedir /etc/sudoers.d at end of file.\n" >> ${LOGFILE} 2>&1
echo "" >> /etc/sudoers
echo "## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment)" >> /etc/sudoers
echo "#includedir /etc/sudoers.d" >> /etc/sudoers
fi
##
## * NFS Un-mount Section *
##
##
## Unmount /mnt.$$ directory
##
umount ${MOUNTPT} >> $LOGFILE 2>&1
if [ -d ${MOUNTPT} ]; then
rmdir ${MOUNTPT} >> $LOGFILE 2>&1
fi
##
## Make sure that /mnt.$$ got unmounted
##
if [ -f ${SRCFILE} ]; then
echo " The umount command failed to unmount ${MOUNTPT} ... We will not force the unmount ..." >> $LOGFILE
umount -f ${MOUNTPT} >> $LOGFILE 2>&1
if [ -d ${MOUNTPT} ]; then
rmdir ${MOUNTPT} >> $LOGFILE 2>&1
fi
else
echo " $MOUNTPT was unmounted ... There is no need for user intervention on $HOSTNAME ..." >> $LOGFILE
fi
#
# as always, exit cleanly
#
clean_exit 0
这是 212 行代码;没有对 sudoers 文件进行版本控制。客户有一个现有的每周运行一次的过程,以验证文件的校验和以确保安全。虽然脚本引用了 Solaris,但对于此客户,我们不需要迁移 Solaris 需求。
我们从创建角色并使用 Git 对 sudoers 文件进行版本控制的想法开始。这也消除了对 NFS 挂载的需求。
使用copy
和template
模块的“validate”和“backup”参数,我们可以消除备份和恢复文件的代码需求。验证在将文件放置到目标位置之前运行,如果失败,模块将报错。
我们需要为角色创建任务、模板和变量。以下是文件布局。
├── README.md
├── roles
│ └── sudoers
│ ├── tasks
│ │ └── main.yml
│ ├── templates
│ │ └── sudoers.j2
│ └── vars
│ └── main.yml
└── sudoers.yml
角色剧本sudoers.yml
很简单。
---
##
# Role playbook
##
- hosts: all
roles:
- sudoers
...
角色变量位于vars/main.yml
文件中。我已经为校验和文件设置了变量,并包含/排除了变量,这些变量将用于创建逻辑,跳过“Lawson”宿主,并将 sudoers.d 包含项仅添加到“hd”宿主。
以下是vars/main.yml
文件的内容。
---
MD5FILE: /root/.sudoer.md5
EXCLUDE: la
INCLUDE: hd
...
如果我们使用copy
和lineinfile
模块,那么角色将不是幂等的。Copy 将部署基本文件,lineinfile 将不得不重新插入每次运行的包含项。由于此角色将在 Ansible Tower 中进行调度,因此幂等性是必需的。我们将文件转换为 jinja2 模板。
在第一行,我们添加以下内容,以管理空格和缩进
#jinja2: lstrip_blocks: True, trim_blocks: True
请注意,template
模块的较新版本包括trim_blocks
参数(在 Ansible 2.4 中添加)。
以下是将include
行插入文件末尾的代码。
{% if ansible_hostname[0:2] == INCLUDE %}
#includedir /etc/sudoers.d
{% endif %}
我们使用条件语句({% if %}
,{% endif %}
)来替换 shell,该 shell 为主机名前两位字符为 "hd" 的主机插入行。我们利用 Ansible 事实和过滤器 [0:2]
来解析主机名。
现在来处理任务。首先,设置一个事实来解析主机名。我们将在条件语句中使用 "parhost" 事实。
---
##
# Parse hostnames to grab 1st 2 characters
##
- name: "Parse hostname's 1st 2 characters"
set_fact: parhost={{ ansible_hostname[0:2] }}
接下来,我注意到 csum
在标准 RHEL 服务器上不存在。如果需要,我们可以使用另一个事实有条件地设置校验和二进制文件的名称。请注意,如果 AIX、Solaris 和 Linux 之间存在差异,可能需要进一步编码。由于客户不关心 Solaris 主机,因此我跳过了该开发工作。
我们还将处理 AIX 和 RHEL 之间 root 组的差异。
##
# Conditionally set name of checksum binary
##
- name: "set checksum binary"
set_fact:
csbin: "{{ 'cksum' if (ansible_distribution == 'RedHat') else 'csum' }}"
##
# Conditionally set name of root group
##
- name: "set system group"
set_fact:
sysgroup: "{{ 'root' if (ansible_distribution == 'RedHat') else 'sys' }}"
块将允许我们在任务周围提供一个条件。我们将在块的末尾使用一个条件来排除 "la" 主机。
##
# Enclose in block so we can use parhost to exclude hosts
##
- block:
模板模块验证并部署文件。我们注册结果以便我们可以确定此任务中是否存在更改。使用模块的 validate 参数确保新的 sudoers 文件在到位之前有效。
##
# Validate will prevent bad files, no need to revert
# Jinja2 template will add include line
##
- name: Ensure sudoers file
template:
src: sudoers.j2
dest: /etc/sudoers
owner: root
group: "{{ sysgroup }}"
mode: 0440
backup: yes
validate: /usr/sbin/visudo -cf %s
register: sudochg
如果部署了新的模板,我们将运行 shell 来生成校验和文件。条件语句在部署 sudoers 模板时或校验和文件丢失时更新校验和文件。由于现有流程还监控其他文件,因此我们使用原始脚本中提供的 shell 代码。
- name: sudoers checksum
shell: "grep -v '/etc/sudoers' {{ MD5FILE }} > {{ MD5FILE }}.tmp ; {{ csbin }} /etc/sudoers >> {{ MD5FILE }} ; mv {{ MD5FILE }}.tmp {{ MD5FILE }}"
when: sudochg.changed or MD5STAT.exists == false
file 模块强制执行权限。
- name: Ensure MD5FILE permissions
file:
path: "{{ MD5FILE }}"
owner: root
group: "{{ sysgroup }}"
mode: 0600
state: file
由于备份参数未提供任何有关清理旧备份的选项,因此我们将添加一些代码来为我们处理此问题。这也展示了如何利用 "register" 和 "stdout_lines" 功能。
##
# List and clean up backup files. Retain 3 copies.
##
- name: List /etc/sudoers.*~ files
shell: "ls -t /etc/sudoers*~ |tail -n +4"
register: LIST_SUDOERS
changed_when: false
- name: Cleanup /etc/sudoers.*~ files
file:
path: "{{ item }}"
state: absent
loop: "{{ LIST_SUDOERS.stdout_lines }}"
when: LIST_SUDOERS.stdout_lines != ""
关闭块。
##
# This conditional restricts what hosts this block runs on
##
when: parhost != EXCLUDE
...
这里预期的用途是在 Ansible Tower 中运行此角色。可以为 Ansible Tower 通知配置通过电子邮件、Slack 或其他方法进行作业失败通知。此角色在 Ansible、Ansible Engine 或 Ansible Tower 中运行。
我们已经压缩了脚本并创建了一个完全幂等的能够强制执行 sudoers 文件所需状态的角色。使用 SCM 提供版本控制、更好的变更管理和问责制。使用 Jenkins 或其他工具进行的 CI/CD 可以为将来的更改提供 Ansible 代码的自动化测试。Ansible Tower 中的审计员角色可以监督和维护组织的合规性要求。
我们可以删除围绕校验和的过程,但客户必须首先与其安全团队进行沟通。如果需要,可以使用 Ansible Vault 来保护 sudoers 模板。最后,可以使用组来替换围绕 includes 和 excludes 的逻辑。
你可以在 GitHub 上找到这个角色。