Ansible中的Shell脚本

假设某个客户要求您帮助脚本迁移,以在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="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的引用,但对于该客户,我们不必转移此要求。

让我们从创建角色开始,然后将sudoers文件放入Git中进行版本控制。 除其他外,这将使我们摆脱挂载NFS卷的需要。

使用复制模板模块的验证和备份选项,我们可以无需编写代码来备份和还原文件。 在这种情况下,将在将文件放置到目标点之前执行验证,如果验证失败,则模块将引发错误。

对于每个角色,我们需要指定任务,模板和变量。 这是相应文件的结构:



角色脚本文件(剧本) sudoers.yml具有简单的结构:

---
##
# Role playbook
##
- hosts: all
roles:
- sudoers
...

角色变量位于vars / main.yml文件中 。 该文件包含校验和文件以及include / exclude指令,这些指令将用于创建特殊逻辑以跳过Lawson主机,仅在hd主机中包括sudoers.d文件。

这是vars / main.yml文件的内容:

---
MD5FILE: /root/.sudoer.md5
EXCLUDE: la
INCLUDE: hd
...

如果我们使用copylineinfile模块 ,那么该角色将不是幂等的。 复制模块将安装基本文件,并且lineinfile将在每次启动时重新插入include。 由于此角色将在Ansible Tower启动 ,因此必须具备幂等性。 我们将文件转换为jinja2模板。

在第一行中,我们添加以下命令来控制空格和缩进

#jinja2: lstrip_blocks: True, trim_blocks: True

请注意,较新版本的模板模块包括trim_blocks的参数(在Ansible 2.4中添加)。

以下是在文件末尾插入include行的代码:

{% if ansible_hostname[0:2] == INCLUDE %}
#includedir /etc/sudoers.d
{% endif %}

我们为shell命令使用条件构造({%if%},{%endif%}),该命令为名称以字符“ 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] }}

RHEL库存服务器上没有csum参数。 如有必要,我们可以使用另一个事实来用校验和有条件地指示二进制文件的名称。 请注意,如果这些功能在AIX,Solaris和Linux上不同,则可能需要附加代码。

此外,我们还必须解决AIX和RHEL上根组差异的问题。

##
# 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参数可以使您在将新的sudoer文件放入主机之前验证其有效性。

##
# 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

文件模块验证必要权限的安装:

- 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塔式警报,以便在作业执行失败的情况下,警报将发送到电子邮件,Slack或其他方式。 该角色在Ansible,Ansible引擎或Ansible塔中运行。

结果,我们从脚本中删除了所有多余的内容,并创建了一个完全等幂的角色,可以提供sudoers文件的所需状态。 使用SCM可以进行版本控制,提供更有效的变更管理和透明度。 带有Jenkins或其他工具的CI / CD允许您设置Ansible代码的自动化测试以用于将来的更改。 Ansible Tower中的Auditor角色使您可以监视和执行组织要求。

可以从脚本中删除用于处理校验和的代码,但是为此,客户需要首先咨询其安全服务。 如有必要,可以使用Ansible Vault保护sudoers模式。 最后,使用组避免了使用包含和排除来编写逻辑。

→您可以在此链接从GitHub下载角色

Source: https://habr.com/ru/post/zh-CN433792/


All Articles