← RHCE in Ansible: automação e operação
09 / 15 · 45 MIN

Ensaio executável: âmbito, estado e limites

Constrói evidência reproduzível sem confundir aliases locais com sistemas independentes.

Contrato do laboratório

Uma equipa APS prepara a configuração de dois workers de reconciliação. O pedido de mudança exige saber quais os alvos e que ficheiros foram alterados. Antes do ensaio, escreve três critérios: cada worker tem a sua identidade no ficheiro; uma repetição sem alterações não volta a ativar a configuração; uma proposta inválida conserva o estado anterior. Estes critérios permitem observar efeitos concretos. Um código de saída zero sem inspeção do destino não responde aos três. O laboratório deste percurso utiliza dois nomes de inventário ligados por connection=local à mesma máquina. A diretoria de cada nome fica separada. Esta construção estuda a execução por host, mas não prova isolamento de máquinas, acesso SSH ou resiliência.

Preparar e executar

Na pasta content/labs/rhce-execution estão os playbooks, o template, o validador e run.py. Cria um ambiente Python isolado e instala ansible-core 2.16.14 para reproduzir a versão do ensaio registado. Depois executa run.py com --ansible-bin apontado para ansible-playbook e --output para o relatório pretendido. O runner cria um inventário temporário, fixa o interpretador e fornece lab_root por extra vars. O ensaio de seleção usa --list-hosts com --limit worker_a e confirma que worker_b não aparece. Só depois executa os dois alvos. O código apresentado abaixo é o runner que gera esse inventário; fornece o caminho real de ansible-playbook no teu ambiente.

Identidade e observação

Compara os bytes dos dois worker.conf: devem diferir no campo worker. Compara também os logs de ativação, não apenas o recap. Se ambos escrevessem no mesmo caminho, dois resultados por host poderiam esconder uma substituição do mesmo ficheiro. Num projeto real, regista ainda a correspondência entre alias, endereço e função. Uma tarefa delegada para o controlador pode continuar a ser chamada por vários hosts em paralelo. Para um registo partilhado, escolhe uma agregação única ou serialização explícita adequada ao requisito; mudar apenas delegate_to não evita concorrência. O laboratório não força uma corrida aleatória para declarar que foi corrigida.

Aceitação e transferência para RHEL

O relatório identifica a versão de Ansible, o sistema usado, os hashes dos ficheiros e as verificações executadas. Não o uses como evidência de reboot ou de serviços systemd: nenhum destes comportamentos é exercitado aqui. Para a próxima fase, aplica o mesmo contrato a máquinas RHEL descartáveis com acessos próprios da operação. Acrescenta verificação do serviço, da persistência e das permissões reais. Entrega à equipa RUN o comando, a versão e os resultados esperados, para que consiga repetir o ensaio sem o portátil ou a conta pessoal do autor.

# Save as run.py. Requires the other five lab files in the same directory.
# Usage: python3 run.py --ansible-bin /absolute/venv/bin/ansible-playbook --output evidence.json
"""Original local-only Ansible lab. No SSH, become, services or real data."""
import argparse,hashlib,json,os,pathlib,platform,re,subprocess,tempfile,zipfile
from datetime import datetime,timezone
p=argparse.ArgumentParser();p.add_argument('--ansible-bin',required=True);p.add_argument('--output',required=True);args=p.parse_args()
source=pathlib.Path(__file__).resolve().parent;binary=pathlib.Path(args.ansible_bin).resolve();checks=[];runs=[]
def require(name,condition):
 assert condition,name
 checks.append({'name':name,'passed':True})
with tempfile.TemporaryDirectory(prefix='dr-rhce-lab-') as temporary:
 root=pathlib.Path(temporary);(root/'home').mkdir();inventory=root/'inventory.ini';inventory.write_text('[workers]\nworker_a ansible_connection=local\nworker_b ansible_connection=local\n[workers:vars]\nansible_python_interpreter='+str(binary.parent/'python')+'\n')
 env={**os.environ,'ANSIBLE_HOME':str(root/'home'),'ANSIBLE_LOCAL_TEMP':str(root/'local'),'ANSIBLE_REMOTE_TEMP':str(root/'remote'),'ANSIBLE_NOCOLOR':'1','ANSIBLE_CONFIG':str(root/'ansible.cfg'),'OBJC_DISABLE_INITIALIZE_FORK_SAFETY':'YES'}
 (root/'ansible.cfg').write_text('[defaults]\nretry_files_enabled=False\nhost_key_checking=True\n')
 version=subprocess.check_output([str(binary),'--version'],env=env,text=True).splitlines()[0]
 def run(label,play,extra=None,flags=(),expected=0):
  variables={'lab_root':str(root/'nodes'),'service_port':8443,'use_marker':False,**(extra or {})}
  command=[str(binary),'-i',str(inventory),str(source/play),'-e',json.dumps(variables),*flags]
  r=subprocess.run(command,env=env,text=True,capture_output=True,timeout=60)
  assert r.returncode==expected,(label,r.returncode,r.stdout,r.stderr)
  recaps={host:{k:int(v) for k,v in re.findall(r'(ok|changed|unreachable|failed|skipped|rescued|ignored)=(\d+)',line)} for line in r.stdout.splitlines() for host in ['worker_a','worker_b'] if line.startswith(host+' ') and 'ok=' in line}
  runs.append({'label':label,'returnCode':r.returncode,'recaps':recaps,'stdoutSha256':hashlib.sha256(r.stdout.encode()).hexdigest()});return r,recaps
 run('syntax','converge.yml',flags=['--syntax-check'])
 listed,_=run('limited-selection','converge.yml',flags=['--list-hosts','--limit','worker_a'])
 require('limit resolves only worker_a','worker_a' in listed.stdout and 'worker_b' not in listed.stdout)
 run('first-convergence','converge.yml')
 def contents(name):return {h:(root/'nodes'/h/name).read_bytes() for h in ['worker_a','worker_b']}
 initial=contents('worker.conf');activation=contents('activation.log')
 require('separate identities have distinct configurations',initial['worker_a']!=initial['worker_b'])
 require('first activation once per worker',all(x==b'activated\n' for x in activation.values()))
 _,recap=run('repeat-convergence','converge.yml');require('second convergence reports no change',all(x['changed']==0 for x in recap.values()) and len(recap)==2)
 require('repeat leaves configuration and activation unchanged',contents('worker.conf')==initial and contents('activation.log')==activation)
 run('invalid-template','converge.yml',{'service_port':70000},expected=2)
 require('invalid candidate preserves destinations',contents('worker.conf')==initial)
 require('invalid candidate never activates',contents('activation.log')==activation)
 run('check-prediction','converge.yml',{'service_port':9443},flags=['--check'])
 require('check leaves actual state unchanged',contents('worker.conf')==initial and contents('activation.log')==activation)
 for h in ['worker_a','worker_b']:
  release=root/'nodes'/h/'release';release.mkdir();(release/'obsolete.txt').write_text('old helper\n')
 bundle=root/'release.zip'
 def pack(text):
  with zipfile.ZipFile(bundle,'w') as z:z.writestr('payload.txt',text)
 pack('release-one\n');run('archive-first','archive.yml',{'bundle_path':str(bundle)})
 require('archive contains first payload',all(x==b'release-one\n' for x in contents('release/payload.txt').values()))
 require('archive does not prune unlisted files',all(x==b'old helper\n' for x in contents('release/obsolete.txt').values()))
 _,recap=run('archive-repeat','archive.yml',{'bundle_path':str(bundle)})
 require('unchanged zip repeat reports no change',len(recap)==2 and all(x['changed']==0 for x in recap.values()))
 for h in ['worker_a','worker_b']:(root/'nodes'/h/'release/ready').write_text('old marker\n')
 pack('release-two\n');run('stale-marker','archive.yml',{'bundle_path':str(bundle),'use_marker':True})
 require('existing marker suppresses new archive',all(x==b'release-one\n' for x in contents('release/payload.txt').values()))
 run('archive-without-marker','archive.yml',{'bundle_path':str(bundle)})
 require('new archive changes intended payload',all(x==b'release-two\n' for x in contents('release/payload.txt').values()))
 require('new archive still preserves extra file',all(x==b'old helper\n' for x in contents('release/obsolete.txt').values()))
 run('failed-release-recovery','recovery.yml',expected=2)
 require('rescue restores exact previous bytes',contents('worker.conf')==initial)
 require('always evidence written after rescue failure',all(x==b'recovery-path-visited\n' for x in contents('recovery.log').values()))
 require('recovery did not claim service activation',contents('activation.log')==activation)
 report={'checkedAt':datetime.now(timezone.utc).isoformat(),'ansible':version,'platform':platform.platform(),'localAliases':2,'actualMachines':1,'runs':runs,'checks':checks,'sourceHashes':{f.name:hashlib.sha256(f.read_bytes()).hexdigest() for f in sorted(source.iterdir()) if f.suffix in ['.yml','.j2','.py']},'limitations':['Local filesystem execution on macOS; no RHEL managed node or AAP execution environment.','Activation log is a synthetic handler effect, not a service restart or health check.','No SSH, privilege escalation, reboot, storage devices or SELinux validation.'],'cleanup':'Owned temporary target tree removed by TemporaryDirectory.'}
pathlib.Path(args.output).write_text(json.dumps(report,indent=2)+'\n');print(json.dumps({'runs':len(runs),'checks':len(checks),'ansible':version}))
NA PRÁTICA

Dois aliases locais produzem dois ficheiros diferentes; isso demonstra seleção e separação de caminhos, não duas máquinas.

Armadilhas comuns

Confundir host de inventário com máquina física; usar recap como única evidência; delegar escrita partilhada sem controlar concorrência.

Tópicos relacionados: Configuração validada e ativação observável · Arquivos, ficheiros residuais e marcadores · Recuperação e resultado da mudança

Leva esta ideia contigo

Declara o contrato, mede os efeitos e limita a conclusão ao ambiente exercitado.

Criar conta

Referência: Delegation and local execution · EX294 current objectives inspected 2026-09-30; RHCE in Ansible framework effective 2026-05-11; booking product version not publicly pinned

Red Hat®, RHCE e Ansible são marcas comerciais ou marcas registadas de Red Hat, Inc. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Red Hat. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.