Files
Ansible-Build/task
T
Janik von Rotz fbfbc50bfa fix: improve hostname parsing logic to correctly identify and free available hostnames
- Replace `sed`-based hostname extraction with `grep -oE` to accurately isolate numeric suffixes
- Ensure consistent variable quoting in loops and `printf` commands to prevent word splitting
- Update the free hostname check to use anchored `grep` patterns for precise matching
2026-10-13 17:52:15 +02:00

915 lines
32 KiB
Bash
Executable File

#!/usr/bin/env bash
set -e
help-table() {
local -a rows
local max_cmd=7 max_opt=6 max_desc=11
rows+=("Command|Option|Description")
rows+=("===ENV===| | ")
rows+=("version||Show tool versions.")
rows+=("copy-dotenv|[env][env]|Copy .env file.")
rows+=("create-dotenv|[env][type]|Create .env file of type nextcloud, odoo, upgrade.")
rows+=("edit-dotenv|[env]|Open .env file in default editor.")
rows+=("list-dotenv||List .env files.")
rows+=("load-dotenv|[env]|Load and export .env file.")
rows+=("remove-dotenv|[env]|Remove environment config.")
rows+=("rename-dotenv|[env][env]|Rename .env file.")
rows+=("restore-dotenv-files|[path]|Extract and copy .env files from backup file.")
rows+=("show-dotenv|[env]|Output content of the .env file.")
rows+=("===VISUALIZATION===| | ")
rows+=("visualize-dependencies||Visualize Ansible role dependencies.")
rows+=("===LOGOS===| | ")
rows+=("resize-logos|[path]|Resize logos to 200x200.")
rows+=("convert-logos||Convert all .jpg logos to .png.")
rows+=("===GALAXY===| | ")
rows+=("build-galaxy||Create tarball of collection.")
rows+=("publish-galaxy||Publish tarball to Ansible Galaxy.")
rows+=("===DOCS===| | ")
rows+=("build-vuepress||Create vuepress build.")
rows+=("dev-vuepress||Start vuepress development server.")
rows+=("update-docs||Update docs.")
rows+=("install-vuepress||Install build dependencies.")
rows+=("serve-vuepress||Serve vuepress build.")
rows+=("format-role-readmes||Update readme files in roles folder.")
rows+=("===LLM===| | ")
rows+=("commit-with-llm||Commit with llm generated commit message.")
rows+=("update-with-llm|[glob][prompt]|Feed files matching and prompt to LLM and apply file changes.")
rows+=("===DOCKER===| | ")
rows+=("login-docker|[user][token]|Run Docker login and create docker config.")
rows+=("===VAULT===| | ")
rows+=("convert-vault-file|[vault_id] [path]|Re-encrypt vault file as single strings.")
rows+=("decrypt-file|[path]|Decrypt Ansible Vault file.")
rows+=("encrypt-file|[vault_id] [path]|Encrypt file with Ansible Vault.")
rows+=("encrypt-string|[vault_id] [key] [value] -q|Generate encrypted string with Ansible Vault.")
rows+=("generate-encrypted-string|[vault_id] [key] -q|Generate encrypted string for key with Ansible Vault.")
rows+=("generate-vault-password|[vault_id] [password]|Generate a password file for Ansible Vault.")
rows+=("show-vault-file|[vault_id] [path]|Show content of encrypted strings or encrypted vault file.")
rows+=("===SETUP===| | ")
rows+=("install||Install Ansible and dependencies with uv.")
rows+=("install-ansible-build-scripts|[role]|Install scripts of the specified Ansbile role.")
rows+=("install-exo||Install Exoscale CLI.")
rows+=("show-passwordless-sudo|[username]|Give instructions to setup passwordless sudo.")
rows+=("upgrade||Upgrade Ansible and dependencies with uv.")
rows+=("===DEVELOPMENT===| | ")
rows+=("lint||Lint Ansible roles and playbooks.")
rows+=("source||Source the Python virtual env.")
rows+=("===REPORT===| | ")
rows+=("get-last-hostname|[role]|Search inventory for \$ROLE_hostname entries.")
rows+=("list-hosts|[path] [grep]|Lists all hosts of an inventory.")
rows+=("list-roles||Lists all roles.")
rows+=("list-vault||Lists vault identities.")
rows+=("list-deployments-odoo|[path]|List Odoo deplyments.")
rows+=("===GIT===| | ")
rows+=("pull-repos||Pull git repos in inventories folder.")
rows+=("===RUN===| | ")
rows+=("play|[params]|Run ansible-playbook command.")
rows+=("list-tags|[path]|List tags of playbook.")
rows+=("run||Run ansible command.")
rows+=("connect-host|[host][username]|Connect to host with configs.")
rows+=("upgrade-odoo|[env][step]|Run the Odoo upgrade for a target environment.")
rows+=("===TEST===| | ")
rows+=("test-project|[cleanup]|Deploy to localhost. Pass option 'cleanup' to reset test env.")
rows+=("test-scripts|[cleanup]|Install and test scripts. Pass option 'cleanup' to reset env.")
for row in "${rows[@]}"; do
IFS='|' read -r cmd opt desc <<< "$row"
(( ${#cmd} > max_cmd )) && max_cmd=${#cmd}
(( ${#opt} > max_opt )) && max_opt=${#opt}
(( ${#desc} > max_desc )) && max_desc=${#desc}
done
printf '| %-*s | %-*s | %-*s |\n' \
"$max_cmd" "Command" \
"$max_opt" "Option" \
"$max_desc" "Description"
printf '|-%*s-|-%*s-|-%*s-|\n' \
"$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \
"$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \
"$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')"
local first_row=true
for row in "${rows[@]:1}"; do
IFS='|' read -r cmd opt desc <<< "$row"
if [[ "$cmd" =~ ^===.*===$ ]]; then
group_name="${cmd/===/}"
group_name="${group_name/===/}"
if [ "$first_row" = true ]; then
first_row=false
else
printf '|-%*s-|-%*s-|-%*s-|\n' \
"$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \
"$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \
"$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')"
fi
local left_pad=$(( (max_cmd - ${#group_name}) / 2 ))
local right_pad=$(( max_cmd - left_pad - ${#group_name} ))
printf '| %*s%s%*s | %*s | %*s |\n' \
"$left_pad" "" \
"$group_name" \
"$right_pad" "" \
"$max_opt" "" \
"$max_desc" ""
printf '|-%*s-|-%*s-|-%*s-|\n' \
"$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \
"$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \
"$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')"
else
first_row=false
printf '| %-*s | %-*s | %-*s |\n' \
"$max_cmd" "$cmd" \
"$max_opt" "$opt" \
"$max_desc" "$desc"
fi
done
}
help() {
echo
if [[ -n "$1" ]]; then
help-table | grep -i "$1" | column -t -s'|'
else
echo 'task <command> [options]'
echo
echo 'commands:'
echo
help-table
fi
echo
}
# Dynamic env vars
TASK_DOTENV_DIR="$PWD/inventories/dotenv"
GIT_BRANCH=$(git symbolic-ref --short -q HEAD)
LLM_MODEL=${LLM_MODEL:="llama"}
if [[ -a ".env" ]]; then
export $(cat .env | sed 's/^#.*//g' | xargs)
fi
# Import commands
clone-taskfile(){
if [ ! -d "$HOME/taskfile.build" ]; then
echo -e "\033[38;5;214mGit\033[0m: Clone taskfile repo"
git clone https://git.taskfile.build "$HOME/taskfile.build"
else
echo -e "\033[38;5;214mGit\033[0m: Pull taskfile repo"
git -C "$HOME/taskfile.build" pull
fi
}
if [ -d "$HOME/taskfile.build/bin" ]; then
for file in "$HOME/taskfile.build/bin/"*; do
if [ -f "$file" ]; then
source "$file"
fi
done
fi
# Vault
connect-host() {
if [[ -z "$1" ]]; then
echo 'Host name cannot be empty'
return 1
fi
local search_host="$1"
local override_user="$2"
# Find hosts.yaml file in inventories folder
local hosts_files
hosts_files=$(find ./inventories -type f -name 'hosts.yml')
# Search for host name match
local host_config=""
local hosts_file
for hosts_file in $hosts_files; do
local command="yq e '.all.children.*.hosts.$search_host' $hosts_file"
host_config=$(eval "$command" | sed '/null/d' | head -5)
if [[ -z "$host_config" ]]; then
command="yq e '.all.children.*.children.*.hosts.$search_host' $hosts_file"
host_config=$(eval "$command" | sed '/null/d' | head -5)
fi
if [[ -z "$host_config" ]]; then
command="yq e '.all.hosts.$search_host' $hosts_file"
host_config=$(eval "$command" | sed '/null/d' | head -5)
fi
if [[ -n "$host_config" ]]; then
break
fi
done
if [[ -z "$host_config" ]]; then
echo "Host '$search_host' not found in hosts.yml file"
return 1
fi
# Get Ansible vars from the host configuration
local ansible_host
local ansible_port
local ansible_user
local ansible_common_args
ansible_host=$(echo "$host_config" | grep -m1 '^ansible_host' | cut -d: -f2 | xargs)
ansible_port=$(echo "$host_config" | grep -m1 '^ansible_port' | cut -d: -f2 | xargs)
ansible_user=$(echo "$host_config" | grep -m1 '^ansible_user' | cut -d: -f2 | xargs)
ansible_common_args=$(echo "$host_config" | grep -m1 'ansible_ssh_common_args' | cut -d: -f2 | xargs)
# Build SSH command using extracted connection details
local ssh_command="ssh"
if [[ "$ansible_port" != "null" ]] && [[ -n "$ansible_port" ]]; then
ssh_command+=" -p $ansible_port"
fi
if [[ "$ansible_common_args" != "null" ]] && [[ -n "$ansible_common_args" ]]; then
ssh_command+=" $ansible_common_args"
fi
# Use override user if provided, else fall back to ansible_user
local effective_user="${override_user:-$ansible_user}"
if [[ "$effective_user" != "null" ]] && [[ -n "$effective_user" ]]; then
ssh_command+=" $effective_user@$ansible_host"
else
ssh_command+=" $ansible_host"
fi
echo "Run command: $ssh_command"
$ssh_command
}
decrypt-file() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
echo "Decrypt file $1."
ansible-vault decrypt --output="$1" "$1"
}
encrypt-file() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi
echo "Encrypt file $2 with vault id $1."
ansible-vault encrypt --encrypt-vault-id="$1" "$2"
}
list-vault() {
grep 'vault_identity_list' ansible.cfg | sed 's/^vault_identity_list = //' | tr ',' '\n' | sed '/^$/d'
}
generate-vault-password() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi
local vault_id="$1"
local vaultpassword="$2"
export VAULT_ID="$vault_id"
export VAULTPASSWORD="$vaultpassword"
echo "$vaultpassword" > ".vault_pass_$vault_id"
chmod 600 ".vault_pass_$vault_id"
echo "Password file .vault_pass_$vault_id created."
}
encrypt-string() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi
if [[ -z "$3" ]]; then echo '$3 is empty'; exit; fi
local string_key="$2"
local string_value="$3"
# string_key=$(echo "$2" | cut -d ':' -f 1)
# string_value=$(echo "$2" | cut -d ':' -f 2 | sed 's/^ *//')
if [[ "$4" != "-q" ]]; then
echo "Found key: $string_key"
echo "Encrypting value: $string_value"
fi
ansible-vault encrypt_string --encrypt-vault-id="$1" --name "$string_key" "$string_value"
}
generate-encrypted-string() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi
local string_key="$2"
local string_value="$(pwgen -c 20 1)"
encrypt-string "$1" "$string_key" "$string_value"
}
convert-vault-file() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi
local vault_id="$1"
local file_path="$2"
local temp_file
temp_file=$(mktemp)
# Decrypt the vault file
ansible-vault decrypt --output="$file_path" "$file_path"
# Process each key-value pair
yq eval '.' "$file_path" | while IFS= read -r line; do
local key
local value
key=$(echo "$line" | cut -d':' -f1)
value=$(echo "$line" | cut -d':' -f2-)
value=$(echo "$value" | sed 's/^[[:space:]"]*//;s/[[:space:]"]*$//')
# Re-encrypt the key-value pair
local encrypted
encrypted=$(ansible-vault encrypt_string --encrypt-vault-id="$vault_id" --name "$key" "$value" 2>/dev/null)
echo "$encrypted" >> "$temp_file"
done
# Move temp file to vault file
mv "$temp_file" "$file_path"
echo "Converted file saved as $file_path"
}
show-vault-file() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit 1; fi
if [[ -z "$2" ]]; then echo '$2 is empty'; exit 1; fi
local vault_id="$1"
local file_path="$2"
local temp_file
local temp_part_file
temp_file=$(mktemp)
temp_part_file=$(mktemp)
if head -n 1 "$file_path" | grep -q "^\$ANSIBLE_VAULT;1.2;AES256;$vault_id"; then
ansible-vault decrypt --output="$temp_file" "$file_path"
else
local write_finished=false
local current_key=""
while IFS= read -r line; do
if [[ "$line" =~ "!vault" ]]; then
# Process previous vault entry if it exists
if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then
ansible-vault decrypt "$temp_part_file"
local value
value=$(cat "$temp_part_file")
echo "$current_key: $value" >> "$temp_file"
fi
# Set up for new vault entry
current_key=$(echo "$line" | cut -d':' -f1)
: > "$temp_part_file"
write_finished=true
else
if [[ "$write_finished" = true ]]; then
echo "$line" | sed 's/^[[:space:]"]*//;s/[[:space:]"]*$//' >> "$temp_part_file"
fi
fi
done < "$file_path"
# Process the final vault entry
if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then
ansible-vault decrypt "$temp_part_file"
local value
value=$(cat "$temp_part_file")
echo "$current_key: $value" >> "$temp_file"
fi
fi
bat --paging=always "$temp_file"
rm -f "$temp_file"
rm -f "$temp_part_file"
}
# Env
activate-venv() {
if [[ -f ".venv/bin/activate" ]]; then
source ".venv/bin/activate"
else
echo "Could not find .venv/bin/active"
echo "Init venv with $(uv --version)"
uv venv
fi
}
install() {
echo "Ensure Python version $(cat .python-version) is installed."
uv python install
activate-venv
echo "Install Ansible and Python dependencies"
uv pip install -r requirements.txt
}
install-exo() {
curl -fsSL https://raw.githubusercontent.com/exoscale/cli/master/install-latest.sh | sh
}
visualize-dependencies() {
mkdir -p tmp
./bin/visualize-dependencies ./roles
open-with-os tmp/ansible-role-dependencies.html
}
version() {
uv --version
echo "python $(cat .python-version)"
ansible --version
}
show-passwordless-sudo() {
local user="${1:-$USERNAME}"
echo "echo \"$user ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers"
}
# Run
play() {
ansible-playbook "$@"
}
list-tags() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit 1; fi
ansible-playbook "$1" --list-tags
}
run() {
ansible "$@"
}
upgrade() {
echo "Update Ansible"
uv pip install --upgrade ansible
}
lint() {
ansible-lint --fix --offline roles/*
}
upgrade-odoo() {
source ./bin/upgrade-odoo
upgrade-odoo "$@"
}
# Report
list-hosts() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
local grep_filter="$2"
find "$1" -name 'hosts.yml' | sort | while read -r hosts_file; do
echo -e "\n<<< $hosts_file >>>\n"
local aliases
aliases=$(yq e '.. | select(has("ansible_host")) | (path | .[-1])' "$hosts_file")
local hosts
hosts=$(yq e '.. | select(has("ansible_host")) | .ansible_host' "$hosts_file")
local functions
functions=$(yq e '.. | select(has("ansible_host")) | .function // ""' "$hosts_file")
local customers
customers=$(yq e '.. | select(has("ansible_host")) | .customer // ""' "$hosts_file")
local providers
providers=$(yq e '.. | select(has("ansible_host")) | .hosting_provider // ""' "$hosts_file")
if [[ -z "$aliases" || -z "$hosts" ]]; then
echo "No hosts found in $hosts_file"
continue
fi
local alias_width=25
local host_width=30
local func_width=25
local cust_width=33
local prov_width=20
local column="| %-${alias_width}s | %-${host_width}s | %-${func_width}s | %-${cust_width}s | %-${prov_width}s |\n"
printf "$column" 'Alias' 'Host' 'Function' 'Customer' 'Provider'
echo "|$(printf '%*s' $((alias_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((host_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((func_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((cust_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((prov_width + 2)) '' | tr ' ' '-')|"
paste <(echo "$aliases") <(echo "$hosts") <(echo "$functions") <(echo "$customers") <(echo "$providers") | sort -t$'\t' -k1,1 -k4,4r | awk -F'\t' '!seen[$1]++' | while IFS=$'\t' read -r alias host function customer provider; do
local row
row=$(printf "$column" "$alias" "$host" "$function" "$customer" "$provider")
if [[ -z "$grep_filter" ]] || echo "$row" | grep -qi "$grep_filter"; then
echo "$row"
fi
done
done
}
list-roles() {
local role_width=25
local kind_width=15
local description_width=50
local column="| %-${role_width}s | %-${kind_width}s | %-${description_width}s |\n"
printf "$column" 'Role' 'Kind' 'Description'
echo "|$(printf '%*s' $((role_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((kind_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((description_width + 2)) '' | tr ' ' '-')|"
local md_files
md_files=$(find roles -name 'README.md' | sort)
local readme
for readme in $md_files; do
local role
role=$(basename "$(dirname "$readme")")
local kind
kind=$(grep "^kind:" "$readme" | sed 's/kind: //')
local description
description=$(sed -n '5,$p' "$readme" | grep -v '^$' | grep -v '<img' | grep -v '^#' | head -1)
printf "$column" "$role" "$kind" "$description"
done
}
get-last-hostname() {
if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi
local role="$1"
echo "Search inventories folder for '${role}_hostname: $role'"
local all_hostnames
all_hostnames=$(rg -I --no-ignore "^${role}_hostname: ${role}.+" inventories | cut -d' ' -f2 | sort -u)
local highest
highest=$(echo "$all_hostnames" | grep -oE "^${role}[0-9]+" | sed "s/${role}//" | sort -n | tail -1)
echo "Highest hostname: ${role}${highest}"
echo "Free hostnames:"
for i in $(seq 1 "$highest"); do
local num
num=$(printf "%02d" "$i")
local hostname="${role}${num}"
if ! echo "$all_hostnames" | grep -q "^${hostname}"; then
echo "$hostname"
fi
done
}
pull-repos() {
local git_folder
for git_folder in $(find inventories -name .git); do
local repo
repo=$(dirname "$git_folder")
echo "Pull repo $repo."
git -C "$repo" pull
done
}
list-deployments-odoo() {
INVENTORY_DIR="$1"
HOST_VARS_DIR="$INVENTORY_DIR/host_vars"
COL_WIDTH_ALIAS=15
COL_WIDTH_HOSTNAME=15
COL_WIDTH_IMAGE=80
printf "| %-${COL_WIDTH_ALIAS}s | %-${COL_WIDTH_HOSTNAME}s | %-${COL_WIDTH_IMAGE}s |\n" "alias" "odoo_hostname" "odoo_image"
printf "| %-${COL_WIDTH_ALIAS}s | %-${COL_WIDTH_HOSTNAME}s | %-${COL_WIDTH_IMAGE}s |\n" \
"$(printf '%.0s-' {1..$COL_WIDTH_ALIAS} | sed 's/-/---/')" \
"$(printf '%.0s-' {1..$COL_WIDTH_HOSTNAME} | sed 's/-/---/')" \
"$(printf '%.0s-' {1..$COL_WIDTH_IMAGE} | sed 's/-/---/')"
mapfile -t FILE_LIST < <(find "$HOST_VARS_DIR" -type f -name "vars.yml" -o -path "$HOST_VARS_DIR/*.yml" | sort)
for FILE_PATH in "${FILE_LIST[@]}"; do
if [[ "$FILE_PATH" == *vault* ]]; then
continue
fi
if [[ "$FILE_PATH" == */vars.yml ]]; then
ALIAS=$(basename "$(dirname "$FILE_PATH")")
VARS_FILE="$FILE_PATH"
elif [[ "$FILE_PATH" == *.yml ]]; then
ALIAS=$(basename "$FILE_PATH" .yml)
VARS_FILE="$FILE_PATH"
else
continue
fi
if [[ "$ALIAS" == "vars" ]] || [[ "$ALIAS" == "vault" ]] || [[ -z "$ALIAS" ]]; then
continue
fi
ODOO_HOSTNAME=$(yq eval '.odoo_hostname // "N/A"' "$VARS_FILE" | head -1)
ODOO_IMAGE=$(yq eval '.odoo_image // "N/A"' "$VARS_FILE" | head -1)
printf "| %-${COL_WIDTH_ALIAS}s | %-${COL_WIDTH_HOSTNAME}s | %-${COL_WIDTH_IMAGE}s |\n" \
"$ALIAS" "$ODOO_HOSTNAME" "$ODOO_IMAGE"
done
}
# Galaxy
build-galaxy() {
local version=$(yq eval '.version' galaxy.yml)
local namespace=$(yq eval '.namespace' galaxy.yml)
local name=$(yq eval '.name' galaxy.yml)
ansible-galaxy collection build
mv $namespace-$name-$version.tar.gz tmp/
}
publish-galaxy() {
local version=$(yq eval '.version' galaxy.yml)
local namespace=$(yq eval '.namespace' galaxy.yml)
local name=$(yq eval '.name' galaxy.yml)
ansible-galaxy collection publish tmp/$namespace-$name-$version.tar.gz --token "$GALAXY_API_TOKEN"
}
# Test commands
test-project() {
version
if [[ "$1" = "cleanup" ]]; then
echo "Cleanup localhost"
ansible-playbook -i test_inventory plays/cleanup.yml -t "odoo,odoo_data,odoo_volume,\
postgres,postgres_data,postgres_volume,\
meilisearch,meilisearch_data,meilisearch_volume,\
pgadmin,pgadmin_data,pgadmin_volume,\
n8n,n8n_data,n8n_volume,\
node_exporter,node_exporter_data,\
nginx,nginx_data,nginx_volume,\
metabase,metabase_data,metabase_volume,\
prometheus,prometheus_data,prometheus_volume,\
grafana,grafana_data,grafana_volume,\
loki,loki_data,loki_volume,\
promtail,promtail_data,promatil_volume,\
docker_compose,docker_compose_data"
else
echo "Setup localhost"
ansible-playbook -i test_inventory plays/localhost.yml --skip-tags check
echo "Curl the services and check HTTP code."
local fail=0
local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://n8n.local:8080")
[[ $code -eq 200 ]] || { echo "http://n8n.local:8080 -> $code"; fail=1; }
local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://odoo.local:8080")
[[ $code -eq 200 ]] || { echo "http://odoo.local:8080 -> $code"; fail=1; }
local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://odoo.local:8080/website/info")
[[ $code -eq 403 ]] || { echo "http://odoo.local:8080/website/info -> $code"; fail=1; }
#FIXME: Metabase container does not work GitHub Runner env.
# local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://metabase.local:8080")
# [[ $code -eq 200 ]] || { echo "http://metabase.local:8080 -> $code"; fail=1; }
local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://grafana.local:8080")
[[ $code -eq 200 ]] || { echo "http://grafana.local:8080 -> $code"; fail=1; }
echo "Exit code is: $fail"
if [[ $fail -eq 1 ]]; then
echo "Show Docker container status..."
docker ps
# echo "Show Docker container logs..."
# containers=$(docker ps -a --format "{{.Names}}:{{.Status}}")
# while IFS= read -r container; do
# name="${container%%:*}"
# echo "<<< $container >>>"
# if docker logs --tail 50 --timestamps "$name" 2>/dev/null | grep -q .; then
# docker logs --tail 50 --timestamps "$name" 2>/dev/null
# else
# echo "No logs available for $name"
# fi
# echo ""
# done <<< "$containers"
fi
exit $fail
fi
}
test-scripts() {
set -e
if [[ "$1" = "cleanup" ]]; then
echo -e "\033[38;5;214mTEST\033[0m: Cleanup test environment."
ansible localhost -m ansible.builtin.cron -a 'name="test" state="absent"'
ansible-playbook -i test_inventory plays/cleanup.yml -t "odoo,odoo_data,odoo_volume,\
postgres,postgres_data,postgres_volume"
else
echo -e "\033[38;5;214mTEST\033[0m: Test restic scripts"
sudo ./roles/cron/files/install
# Add cron job
ansible localhost -m ansible.builtin.cron -a 'name="test" minute="0" hour="*/6" job="echo \"test\""'
# List cron job and compare output
[[ "$(cron-job-list)" == "[1] 0 */6 * * * test" ]]
cron-job-run -i 1
echo -e "\033[38;5;214mTEST\033[0m: Test postgres scripts"
sudo ./roles/postgres/files/install
# Start postgres container
ansible-playbook -i test_inventory plays/localhost.yml -t postgres
docker-postgres-create -c postgres01 -d test
docker-postgres-list -c postgres01 | grep "test"
docker-postgres-shell -c postgres01 -d test -s 'CREATE TABLE example(id int);'
docker-postgres-duplicate -c postgres01 -s test -t example
docker-postgres-drop -c postgres01 -d test
docker-postgres-size -c postgres01 -d example | grep "0 bytes"
docker-postgres-backup -c postgres01 -d example
docker-postgres-restore -c postgres01 -f /var/tmp/postgres01/example.sql -d test -i
docker-postgres-size -c postgres01 -d test | grep "0 bytes"
mkdir -p tmp
echo 'DROP TABLE example;' > tmp/drop.sql
docker-postgres-patch -c postgres01 -d test -f ./tmp/drop.sql
docker-postgres-rename -c postgres01 -s test -t example -r
echo -e "\033[38;5;214mTEST\033[0m: Test odoo scripts"
sudo ./roles/odoo/files/install
# Start odoo container
ansible-playbook -i test_inventory plays/localhost.yml -t odoo --skip-tags depends
docker-odoo-init -c odoo01-prod -d odoo-prod -i contacts -w
docker-odoo-rename -c odoo01-prod -s odoo-prod -t odoo
docker-odoo-list -c odoo01-prod | grep "odoo$"
docker-odoo-user -c odoo01-prod -d odoo -a list | grep "admin"
docker-odoo-drop -c odoo01-int -d odoo-int
docker-odoo-duplicate -c odoo01-prod -s odoo -t odoo-int
docker-odoo-neutralize -c odoo01-int -d odoo-int
docker exec odoo01-int clone-git-addons
docker restart odoo01-int
docker-odoo-init -c odoo01-int -d odoo-int -i partner_firstname
docker-odoo-shell -c odoo01-int -d odoo-int -f -p 'env["res.partner"].browse(1).write({"name": "admint"})'
[[ "admint" == $(docker-odoo-shell -c odoo01-int -d odoo-int -p 'print(env["res.partner"].browse(1).name)') ]]
docker-odoo-upgrade -c postgres01 -d odoo-int -s "18.0" -n upgrade -t "19.0" -m test -l "$ODOO_ENTERPRISE_CODE"
echo -e "\033[38;5;214mTEST\033[0m: Test docker volume scripts"
docker stop postgres01
docker-volume-backup -c postgres01 -v postgres01
docker-volume-backup -c postgres01 -a
docker rm -f postgres01
docker volume rm postgres01
docker-volume-restore -v postgres01 -f /var/tmp/postgres01/postgres01.tar
ansible-playbook -i test_inventory plays/localhost.yml -t postgres --skip-tags depends
docker-postgres-list -c postgres01 | grep "example"
fi
}
# Docs
update-docs() {
echo "Update roles.md"
echo "# Roles" > "docs/roles.md"
echo "" >> "docs/roles.md"
echo "| Logo | Role | Kind | Description |" >> "docs/roles.md"
echo "| --- | --- | --- | --- |" >> "docs/roles.md"
local md_files=$(find roles -name 'README.md' | sort)
for readme in $md_files; do
local role
role=$(basename "$(dirname "$readme")")
local kind
kind=$(grep "^kind:" "$readme" | sed 's/kind: //')
local description
description=$(sed -n '5,$p' "$readme" | grep -v '^$' | grep -v '<img' | grep -v '^#' | head -1)
echo "| ![$role logo](/logos/$role.png) | [$role]($readme) | $kind | $description |" >> "docs/roles.md"
done
}
format-role-readmes() {
local md_files=$(find roles -name 'README.md' | sort)
for readme in $md_files; do
local role
role=$(basename "$(dirname "$readme")")
echo "Update README of $role role."
local kind
kind=$(grep "^kind:" "$readme" | sed 's/kind: //')
local logo_line="<img src=\"/logos/$role.png\" alt=\"$role logo\" width=\"100\" height=\"100\">"
local tmpfile
tmpfile=$(mktemp)
# Check if README already starts with frontmatter
if head -1 "$readme" | grep -q "^---$"; then
# Already has frontmatter at top, rebuild with single logo
{
echo "---"
echo "kind: $kind"
echo "---"
echo ""
echo "$logo_line"
echo ""
# Get content after frontmatter (skip first 5 lines), remove duplicate logo
tail -n +6 "$readme" | grep -v "^<img src"
} > "$tmpfile" && mv "$tmpfile" "$readme"
else
# No frontmatter at top, add it and remove duplicates
{
echo "---"
echo "kind: $kind"
echo "---"
echo ""
echo "$logo_line"
echo ""
# Skip first 8 lines (logo, blank, frontmatter 3 lines, blank, logo, blank)
tail -n +9 "$readme"
} > "$tmpfile" && mv "$tmpfile" "$readme"
fi
done
}
install-vuepress() {
pnpm install
}
dev-vuepress() {
echo "Copy logos folder to vuepress public folder."
cp -r logos .vuepress/public/
open-url http://localhost:8080
pnpm run dev
}
build-vuepress() {
echo "Copy logos folder to vuepress public folder."
cp -r logos .vuepress/public/
pnpm run build
echo "Create vuepress script folders."
mkdir -p .vuepress/dist/ansible_scripts
mkdir -p .vuepress/dist/cron
mkdir -p .vuepress/dist/certbot
mkdir -p .vuepress/dist/docker
mkdir -p .vuepress/dist/docker_volume
mkdir -p .vuepress/dist/grafana
mkdir -p .vuepress/dist/mysql
mkdir -p .vuepress/dist/nginx
mkdir -p .vuepress/dist/odoo
mkdir -p .vuepress/dist/postgres
echo "Copy script files to vuepress folder."
cp -r roles/ansible_scripts/files/* .vuepress/dist/ansible_scripts
cp -r roles/cron/files/* .vuepress/dist/cron
cp -r roles/certbot/files/* .vuepress/dist/certbot
cp -r roles/docker/files/* .vuepress/dist/docker
cp -r roles/docker_volume/files/* .vuepress/dist/docker_volume
cp -r roles/grafana/files/* .vuepress/dist/grafana
cp -r roles/mysql/files/* .vuepress/dist/mysql
cp -r roles/nginx/files/* .vuepress/dist/nginx
cp -r roles/odoo/files/* .vuepress/dist/odoo
cp -r roles/postgres/files/* .vuepress/dist/postgres
}
serve-vuepress() {
cd .vuepress/dist
pnpx serve
}
resize-logos() {
cd logos || return 1
if [[ -n "$1" ]] && [[ -f "$1" ]]; then
magick "$1" -resize 200x200\! "$1"
echo "Resized: $1"
else
local file
for file in *.png; do
[[ -e "$file" ]] && magick "$file" -resize 200x200\! "$file" && echo "Resized: $file"
done
fi
}
convert-logos() {
cd logos || return 1
local file
local base
for file in *.jpg; do
if [[ -e "$file" ]]; then
base="${file%.jpg}"
magick "$file" "${base}.png"
echo "Converted: $file -> ${base}.png"
fi
done
}
if [[ "$1" == *","* ]]; then
# Split comma-separated commands
IFS=',' read -ra COMMANDS <<< "$1"
for cmd in "${COMMANDS[@]}"; do
# Trim whitespace
cmd=$(echo "$cmd" | xargs)
if declare -f "$cmd" > /dev/null; then
"$cmd"
else
echo "Unknown command: $cmd"
help
exit 1
fi
done
elif declare -f "$1" > /dev/null; then
activate-venv || true
"$1" "${@:2}"
else
case "$1" in
*)
echo "Unknown command: $1"
help
exit 1
;;
esac
fi