Scripts de shell no Ansible

Suponha que um cliente tenha solicitado que você ajude na migração de script para implementar um arquivo de sudoers centralizados nos servidores RHEL e AIX.



Bem, esse é um cenário muito comum e, com seu exemplo, você pode demonstrar o uso dos recursos avançados do Ansible, bem como a maneira como a abordagem muda - de um script que executa uma determinada tarefa, a uma descrição idempotente (sem fazer alterações) e a monitorar a conformidade com o estado da instância.

Veja o script:

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

Existem 212 linhas de código, sem controle de versão no arquivo sudoers. O cliente já possui um processo que é executado uma vez por semana e verifica a soma de verificação do arquivo para garantir a segurança. Embora o script contenha uma referência ao Solaris, para este cliente, não precisamos transferir esse requisito.

Vamos começar criando uma função e colocando o arquivo sudoers no Git para controle de versão. Entre outras coisas, isso nos permitirá livrar da necessidade de montar volumes NFS.

Com os parâmetros de validação e backup dos módulos de cópia e modelo , podemos eliminar a necessidade de escrever código para fazer backup e restaurar o arquivo. Nesse caso, a validação é realizada antes do arquivo ser colocado no ponto de destino e, se a validação falhar, o módulo emitirá um erro.

Para cada função, precisamos especificar tarefas, modelos e variáveis. Aqui está a estrutura do arquivo correspondente:



O arquivo de script de função (playbook), sudoers.yml , possui uma estrutura simples:

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

As variáveis ​​de função estão localizadas no arquivo vars / main.yml . Esse arquivo contém o arquivo de soma de verificação e inclui / exclui diretivas que serão usadas para criar lógica especial para ignorar os hosts Lawson e incluir o arquivo sudoers.d apenas nos hosts hd.

Aqui está o conteúdo do arquivo vars / main.yml :

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

Se usarmos os módulos copy e lineinfile , a função não será idempotente. O módulo de cópia instalará o arquivo base e o lineinfile será reinserido sempre que for iniciado. Como essa função será lançada na Ansible Tower , a idempotência é uma obrigação. Converteremos o arquivo em um modelo jinja2.

Na primeira linha, adicionamos o seguinte comando para controlar espaços e recuos :

#jinja2: lstrip_blocks: True, trim_blocks: True

Observe que as versões mais recentes do módulo de modelo incluem parâmetros para trim_blocks (adicionados no Ansible 2.4).

Aqui está o código que insere a linha de inclusão no final do arquivo:

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

Usamos a construção condicional ({% if%}, {% endif%}) para o comando shell que insere uma linha para hosts cujos nomes começam com os caracteres "hd". Usamos fatos Ansible e um filtro [0: 2] para analisar o nome do host.

Agora vamos às tarefas. Primeiro, você precisa estabelecer um fato para analisar o nome do host. Usaremos o fato "parhost" na construção condicional.

---
##
# Parse hostnames to grab 1st 2 characters
##
- name: "Parse hostname's 1st 2 characters"
set_fact: parhost={{ ansible_hostname[0:2] }}

Não há parâmetro csum no servidor de estoque RHEL. Se necessário, podemos usar outro fato para indicar condicionalmente o nome do arquivo binário com a soma de verificação. Observe que código adicional pode ser necessário se esses recursos forem diferentes no AIX, Solaris e Linux.

Além disso, temos que resolver o problema com as diferenças nos grupos raiz no AIX e 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' }}"

O uso de blocos nos permite definir a condição para toda a tarefa. Usaremos a condição no final do bloco para excluir hosts "la".

##
# Enclose in block so we can use parhost to exclude hosts
##
- block:

O módulo de modelo valida e instala o arquivo. Fixamos o resultado para que possamos determinar se a tarefa foi alterada. O uso do parâmetro validate neste módulo permite verificar a validade do novo arquivo sudoer antes de colocá-lo no host.

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

Se um novo modelo foi instalado, execute o script de shell para gerar o arquivo de soma de verificação. A construção condicional atualiza o arquivo de soma de verificação ao instalar o modelo sudoers ou se o arquivo de soma de verificação estiver ausente. Como o processo em execução também monitora outros arquivos, usamos o código do shell fornecido no script de origem:

- 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

O módulo de arquivo verifica a instalação das permissões necessárias:

- name: Ensure MD5FILE permissions
file:
path: "{{ MD5FILE }}"
owner: root
group: "{{ sysgroup }}"
mode: 0600
state: file

Como o parâmetro backup não fornece nenhuma opção para o processamento de backups anteriores, teremos que cuidar de criar o código apropriado. No exemplo abaixo, usamos o parâmetro "register" e o campo "stdout_lines" para isso.

##
# 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 != ""

Conclusão do bloco:

##
# This conditional restricts what hosts this block runs on
##
when: parhost != EXCLUDE
...

O caso de uso pretendido é desempenhar esse papel na Ansible Tower. Os alertas de torre da Ansible podem ser configurados para que, no caso de uma falha na execução da tarefa, os alertas sejam enviados para o e-mail, para o Slack ou de alguma outra maneira. Esta função é executada em Ansible, Ansible Engine ou Ansible Tower.

Como resultado, removemos tudo o que era supérfluo do script e criamos uma função completamente idempotente que pode fornecer o estado desejado do arquivo sudoers. O uso do SCM permite controle de versão, fornece gerenciamento de mudanças e transparência mais eficientes. CI / CDs com Jenkins ou outras ferramentas permitem testes automatizados de código Ansible para alterações futuras. A função de Auditor na Ansible Tower permite monitorar e reforçar os requisitos organizacionais.

O código para trabalhar com somas de verificação pode ser removido do script, mas, para isso, o cliente precisaria primeiro consultar seu serviço de segurança. Se necessário, o padrão sudoers pode ser protegido com o Ansible Vault. Por fim, o uso de grupos evita a escrita da lógica usando inclusões e exclusões.

→ Você pode baixar a função do GitHub neste link

Source: https://habr.com/ru/post/pt433792/


All Articles