Shell 脚本到 Ansible
Shell 脚本到 Ansible
在最近的一次客户访问中,我们被要求帮助迁移以下脚本,该脚本用于将集中式 sudoers 文件部署到 RHEL 和 AIX 服务器。这是一种常见的场景,可以提供一些利用高级 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="unix@company.com" 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 需求。
我们从创建角色并将 sudoers 文件放入 Git 进行版本控制的想法开始。这也消除了对 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:
template 模块验证并部署文件。我们注册结果,以便我们可以确定此任务中是否存在更改。使用模块的 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
由于 backup 参数没有提供任何用于清理旧备份的选项,因此我们将添加一些代码来为我们处理此问题。这还演示了如何利用“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 模板。最后,可以使用组来替换围绕包含和排除的逻辑。
您可以在 GitHub 上找到该角色。