Anyone managing multiple Linux servers knows that manual user onboarding, offboarding, and privilege delegation (sudo/wheel) frequently lead to operational drift and security inconsistencies.
In hybrid server fleets (where some machines run control panels like WHM/cPanel and others run standard Linux distributions) the challenge is ensuring that account provisioning executes consistently and auditably across dozens of nodes.
Here is the practical evolution of an access automation workflow: from building a dual-mode Bash script with automated logrotate provisioning to scaling it with Ansible without rewriting established logic.
1. The starting point: a dual-mode Bash script#
The initial goal was creating a CLI tool (jvx_mngr) to automate creating (-a) and removing (-d) administrative users while adapting to the target host environment.
Key script architecture requirements:#
- Execution and Parameter Validation: Ensure
rootexecution and parse arguments viagetopts. - Dual Execution Engine (WHM API vs. Native Linux):
- If
whmapi1is available, interact with the WHM API (createacctwithreseller_without_domain=1orremoveacct) and apply specific access control lists (ACLs). - If running on vanilla Linux, fall back to native OS utilities (
useradd,chpasswd,chmod 711,userdel -r, andwheel/sudoassignment).
- Human Error Safeguards: A protected accounts array (
USUARIO_PROTEGIDO=(admin_master backup_user root_corp)) to prevent accidental deletion of core accounts. - Audit Logging: Record timestamped entries including the operator (
whoami) and hostname to/var/log/jvx_mngr.log.
Idempotent self-provisioned logrotate#
To prevent log files from growing indefinitely without requiring manual setup on each node, the script includes an internal logrotate configuration function:
# --- Logrotate Configuration ---
function configurar_logrotate() {
local LOGROTATE_CONFIG="/etc/logrotate.d/jvx_mngr"
if [[ -f "${LOGROTATE_CONFIG}" ]]; then
return 0
fi
print_info "Creating logrotate configuration in ${LOGROTATE_CONFIG}..."
cat <<'EOF' > "${LOGROTATE_CONFIG}"
# Logrotate configuration for jvx_mngr script
/var/log/jvx_mngr.log {
monthly
rotate 6
compress
delaycompress
missingok
notifempty
create 640 root root
}
EOF
if [[ $? -eq 0 ]]; then
chmod 644 "${LOGROTATE_CONFIG}"
print_success "Logrotate configuration created successfully."
log "Created logrotate rule in ${LOGROTATE_CONFIG}."
else
print_error "Failed to create logrotate configuration."
log "ERROR: Failed to create logrotate rule in ${LOGROTATE_CONFIG}."
fi
}
Invoking this during the main execution routine provides idempotency: if the configuration exists in /etc/logrotate.d/, execution proceeds silently; otherwise, it sets up monthly rotation with compression and 640 permissions.
2. Scaling out: orchestrating without rewriting#
Once verified locally, the next requirement was running the routine across server fleets using Ansible.
Rewriting proven procedural Bash scripts into native Ansible modules often introduces subtle bugs in complex API edge cases (such as WHM reseller creation). The optimal architecture is using Ansible as the orchestrator while keeping Bash as the domain tool.
Two-playbook architecture#
To maintain a clean separation of concerns:
- Installation Playbook (
instalar_jvx_mngr.yml): Deploys the script to/usr/local/binand sets0755permissions. Runs only during node provisioning or version updates. - Daily Operations Playbook (
usar_jvx_mngr.yml): Prompts the operator for input and invokes the deployed binary across target hosts.
3. Solving Ansible terminal UX challenges#
Building the operational playbook presented two visual and prompt sequencing challenges:
Problem 1: Broken ASCII banners in debug#
Attempting to output an ASCII banner using the debug: msg: module resulted in escaped newline characters (\n) inside a JSON block:
TASK [Displaying banner] **************************************
ok: [localhost] => {
"msg": "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢴⣃\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢴⣫⢗⡯⡖..."
}
Solution: The debug module is intended for variable inspection. The fix was running a localhost task using the shell module with a Here Document (cat <<'EOF'), printing cleanly to standard output:
- name: Display banner using shell module
shell: |
cat <<'EOF'
[ASCII ART HERE]
EOF
args:
warn: false
Problem 2: Displaying instructions before vars_prompt#
Because vars_prompt blocks immediately before any tasks in a play execute, displaying instructions required chaining two distinct plays in a single YAML file:
- Play 1 (
hosts: localhost): Runs locally to print the ASCII banner and display operational instructions. - Play 2 (
hosts: target_servers): Runs immediately afterward, prompting for inputs with full context before executing the remote script.
4. The complete operational playbook#
# usar_jvx_mngr.yml
# --- Play 1: UI & Instructions on Localhost ---
- name: Display Banner and Instructions
hosts: localhost
gather_facts: no
tasks:
- name: Print ASCII Banner
shell: |
cat <<'EOF'
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢴⣃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢴⣫⢗⡯⡖⠀⠀⠀⠀⠀⢠⡄⠀⠀⠀⠀⠀⠀⠀⠀⢠⡤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⢷⡹⢮⣳⠇⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠠⠐⣀⠂⡀⢸⢧⡻⣝⢮⡏⠀⠀⠀⠀⠀⢸⡇⢀⣾⠟⠿⠻⠿⣷⠀⢸⣏⠀⣿⡟⠿⠻⠿⣶⡀⢸⣿⠀⠀⠀⢀⣿⠃⢸⡏⠀⠙⢿⣦⣀⣼⡟⠁⠀⠀
⠀⠀⠤⠑⡈⢡⠀⠆⡁⢢⣉⠓⢯⡳⡇⠀⠀⠀⠀⠀⢸⡇⠨⣿⠀⠀⠀⠀⣿⡁⢸⡧⠀⣿⡂⠀⠀⠀⢿⡃⢸⡷⠀⠀⢠⣾⠋⠀⢸⡗⠀⠀⠀⣹⣿⣇⠀⠀⠀⠀
⠀⠀⠀⠀⠁⠂⡌⠐⡄⢡⠢⡙⢤⠉⠇⠀⠀⠀⠀⠀⢸⡇⠈⣿⣤⣤⣤⣴⡿⠁⢸⡧⠀⣿⡅⠀⠀⠀⢿⡇⢘⣿⣤⡾⠟⠁⠀⠀⢸⡧⠀⢠⣾⠟⠈⢻⣦⡀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠡⠐⢨⡑⠘⠀⠀⠀⠀⠀⠀⠀⠀⣸⡇⠀⠀⠉⠉⠉⠈⠁⠀⠈⠁⠀⠉⠀⠀⠀⠀⠉⠁⠀⠉⠁⠀⠀⠀⠀⠀⠈⠁⠀⠉⠁⠀⠀⠀⠉⠉⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠿⠻⠟⠟⠁⠀User Access Management via Ansible
EOF
args:
warn: false
- name: Print Usage Instructions
shell: |
cat <<'EOF'
------------------------------------------------------------------------------------
INSTRUCTIONS:
- Enter the required parameters for remote server execution.
- When deleting an account (-d), press [Enter] to leave email and level blank.
------------------------------------------------------------------------------------
EOF
args:
warn: false
# --- Play 2: Input Collection & Remote Execution ---
- name: Execute jvx_mngr to manage user access
hosts: target_servers
become: yes
vars_prompt:
- name: "acao"
prompt: "Action to perform? ('-a' to add, '-d' to delete)"
private: no
- name: "usuario"
prompt: "Username"
private: no
- name: "email"
prompt: "User email (press Enter if deleting)"
private: no
default: ""
- name: "nivel"
prompt: "ACL level (n1, n2, n3) (press Enter if deleting)"
private: no
default: ""
tasks:
- name: Execute jvx_mngr on remote targets
command:
cmd: "/usr/local/bin/jvx_mngr {{ acao }} {{ usuario }} {{ email }} {{ nivel }}"
register: command_output
- name: Display remote execution output
debug:
var: command_output.stdout_lines
Practical automation takeaways#
- Avoid Unnecessary Refactoring: Field-tested Bash scripts with specific API bindings (
whmapi1) pair effectively with Ansible's parallel execution model. - Decouple Deployment from Operations: Separating deployment tasks from operational playbooks reduces execution overhead.
- Prioritize Operator Ergonomics: Clear terminal banners, formatted instructions, and structured inputs minimize operational errors during bulk operations.
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments