diff options
Diffstat (limited to 'core')
115 files changed, 3732 insertions, 0 deletions
diff --git a/core/inventory/groups.yaml b/core/inventory/groups.yaml new file mode 100644 index 0000000..52dae9b --- /dev/null +++ b/core/inventory/groups.yaml @@ -0,0 +1,9 @@ +# groups.yaml — Definición de grupos en ShFlow +# Este archivo permite describir grupos, asignar etiquetas y metadatos. +# Sintaxis: +# groups: +# nombre_grupo: +# description: <texto descriptivo> +# tags: [tag1, tag2, ...] + +groups: {} diff --git a/core/inventory/hosts.yaml b/core/inventory/hosts.yaml new file mode 100644 index 0000000..faafa09 --- /dev/null +++ b/core/inventory/hosts.yaml @@ -0,0 +1,17 @@ +# hosts.yaml — Inventario principal de ShFlow +# Este archivo define los hosts y su pertenencia a grupos. +# Sintaxis: +# all: +# hosts: +# nombre_host: +# ansible_host: <IP o FQDN> +# become: <true|false> +# <clave>: <valor> +# children: +# nombre_grupo: +# hosts: +# nombre_host: + +all: + hosts: {} + children: {} diff --git a/core/inventory/vars/all.yaml b/core/inventory/vars/all.yaml new file mode 100644 index 0000000..e0c5b8f --- /dev/null +++ b/core/inventory/vars/all.yaml @@ -0,0 +1,20 @@ +# all.yaml — Variables globales para todos los hosts en ShFlow +# Este archivo define valores comunes que se aplican a todos los hosts del inventario. +# Sintaxis: +# clave: valor +# Las variables aquí definidas pueden ser sobrescritas por vars de grupo o de host. + +language: es +timezone: Europe/Madrid +ntp_servers: + - 0.europe.pool.ntp.org + - 1.europe.pool.ntp.org +default_packages: + - curl + - vim + - bash-completion +ssh_port: 22 +become: true +env: production +vault_enabled: true +vault_rotation_interval: 30d diff --git a/core/lib/translate_msg.sh b/core/lib/translate_msg.sh new file mode 100644 index 0000000..3a1e4a9 --- /dev/null +++ b/core/lib/translate_msg.sh @@ -0,0 +1,9 @@ +render_msg() { + local template="$1"; shift + for pair in "$@"; do + local key="${pair%%=*}" + local val="${pair#*=}" + template="${template//\{$key\}/$val}" + done + echo "$template" +} diff --git a/core/modules/api.sh b/core/modules/api.sh new file mode 100644 index 0000000..a58f81a --- /dev/null +++ b/core/modules/api.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Module: api +# Description: Cliente declarativo para APIs REST y SOAP (GET, POST, PUT, DELETE, SOAP) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: curl, jq, xmllint + +api_task() { + local host="$1"; shift + declare -A args + local headers=() + local method="" body="" url="" output="" parse="" + + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}" + case "$key" in + headers) IFS=',' read -r -a headers <<< "$value" ;; + body) body="$value" ;; + url) url="$value" ;; + method) method="${value,,}" ;; + output) output="$value" ;; + parse) parse="${value,,}" ;; + esac + done + + [[ -z "$method" ]] && method="get" + [[ "$method" == "get" ]] && method="GET" + [[ "$method" == "post" ]] && method="POST" + [[ "$method" == "soap" ]] && method="POST" + + local header_args="" + for h in "${headers[@]}"; do header_args+=" -H \"$h\""; done + + local curl_cmd="curl -sSL -X $method \"$url\"$header_args" + [[ -n "$body" ]] && curl_cmd+=" --data-raw '$body'" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/api.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + echo "$(render_msg "${tr[start]}" "method=$method" "url=$url")" + [[ "$DEBUG" == "true" ]] && echo "$(render_msg "${tr[debug_cmd]}" "cmd=$curl_cmd")" + [[ "$DEBUG" == "true" && -n "$body" ]] && echo -e "$(render_msg "${tr[debug_body]}" "body=$body")" + + local response + if [[ "$host" == "localhost" ]]; then + response=$(eval "$curl_cmd") + else + response=$(ssh "$host" "$curl_cmd") + fi + + if [[ -n "$output" ]]; then + echo "$response" > "$output" + echo "$(render_msg "${tr[saved]}" "output=$output")" + fi + + case "$parse" in + json) + echo "$response" | jq '.' 2>/dev/null || echo "${tr[json_fail]:-⚠️ [api] No se pudo parsear como JSON}" + ;; + xml) + echo "$response" | xmllint --format - 2>/dev/null || { + echo "${tr[xml_fail]:-⚠️ [api] No se pudo parsear como XML}" + echo "$response" + } + ;; + *) echo "$response" ;; + esac +} + +check_dependencies_api() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/api.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in curl jq xmllint; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")" + else + echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")" + fi + done + return 0 +} diff --git a/core/modules/api.tr.en b/core/modules/api.tr.en new file mode 100644 index 0000000..fce15f6 --- /dev/null +++ b/core/modules/api.tr.en @@ -0,0 +1,8 @@ +start=🌐 [api] Executing {method} → {url} +debug_cmd=🔍 Actual command: {cmd} +debug_body=📦 [api] Body sent:\n{body} +saved=💾 [api] Response saved to: {output} +json_fail=⚠️ [api] Failed to parse as JSON +xml_fail=⚠️ [api] Failed to parse as XML +missing_cmd=⚠️ [api] '{cmd}' not available locally. Assuming it exists on the remote host. +cmd_ok=✅ [api] '{cmd}' available locally. diff --git a/core/modules/api.tr.es b/core/modules/api.tr.es new file mode 100644 index 0000000..6e75c04 --- /dev/null +++ b/core/modules/api.tr.es @@ -0,0 +1,8 @@ +start=🌐 [api] Ejecutando {method} → {url} +debug_cmd=🔍 Comando real: {cmd} +debug_body=📦 [api] Cuerpo enviado:\n{body} +saved=💾 [api] Respuesta guardada en: {output} +json_fail=⚠️ [api] No se pudo parsear como JSON +xml_fail=⚠️ [api] No se pudo parsear como XML +missing_cmd=⚠️ [api] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto. +cmd_ok=✅ [api] '{cmd}' disponible localmente. diff --git a/core/modules/archive.sh b/core/modules/archive.sh new file mode 100644 index 0000000..4459bfd --- /dev/null +++ b/core/modules/archive.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Module: archive +# Description: Comprime, descomprime y extrae archivos en remoto (tar, zip, gzip, bzip2) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.6.0 +# Dependencies: ssh, tar, gzip, bzip2, zip, unzip + +archive_task() { + local host="$1"; shift + declare -A args + local files=() + + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}" + [[ "$key" == "files" ]] && IFS=',' read -r -a files <<< "$value" || args["$key"]="$value" + done + + local action="${args[action]}" + local format="${args[format]:-tar}" + local become="${args[become]:-false}" + local prefix="" + [[ "$become" == "true" ]] && prefix="sudo" + + local output="" archive="" dest="" + case "$action" in + compress) output="${args[output]}" ;; + decompress|extract) archive="${args[archive]}"; dest="${args[dest]:-$(dirname "$archive")}" ;; + esac + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/archive.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ "$action" == "extract" || "$action" == "decompress" ]]; then + ssh "$host" "[ -d '$dest' ] || $prefix mkdir -p '$dest'" || { + echo "$(render_msg "${tr[mkdir_fail]}" "dest=$dest")" + return 1 + } + fi + + case "$action" in + compress) + case "$format" in + tar) + ssh "$host" "$prefix tar -czf '$output' ${files[*]}" && echo "$(render_msg "${tr[compressed_tar]}" "output=$output")" + ;; + zip) + ssh "$host" "$prefix zip -r '$output' ${files[*]}" && echo "$(render_msg "${tr[compressed_zip]}" "output=$output")" + ;; + gzip) + for file in "${files[@]}"; do + ssh "$host" "$prefix gzip -f '$file'" && echo "$(render_msg "${tr[compressed_gzip]}" "file=$file")" + done + ;; + bzip2) + for file in "${files[@]}"; do + ssh "$host" "$prefix bzip2 -f '$file'" && echo "$(render_msg "${tr[compressed_bzip2]}" "file=$file")" + done + ;; + *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;; + esac + ;; + decompress) + case "$format" in + gzip) + ssh "$host" "$prefix gunzip -f '$archive'" && echo "$(render_msg "${tr[decompressed_gzip]}" "archive=$archive")" + ;; + bzip2) + ssh "$host" "$prefix bunzip2 -f '$archive'" && echo "$(render_msg "${tr[decompressed_bzip2]}" "archive=$archive")" + ;; + zip) + ssh "$host" "$prefix unzip -o '$archive' -d '$dest'" && echo "$(render_msg "${tr[decompressed_zip]}" "dest=$dest")" + ;; + *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;; + esac + ;; + extract) + case "$format" in + tar) + if [[ ${#files[@]} -gt 0 ]]; then + ssh "$host" "$prefix tar -xzf '$archive' -C '$dest' ${files[*]}" && echo "$(render_msg "${tr[extracted_tar]}" "dest=$dest")" + else + ssh "$host" "$prefix tar -xzf '$archive' -C '$dest'" && echo "$(render_msg "${tr[extracted_tar]}" "dest=$dest")" + fi + ;; + zip) + ssh "$host" "$prefix unzip -o '$archive' -d '$dest'" && echo "$(render_msg "${tr[extracted_zip]}" "dest=$dest")" + ;; + *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;; + esac + ;; + *) echo "$(render_msg "${tr[unsupported_action]}" "action=$action")"; return 1 ;; + esac +} + +check_dependencies_archive() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/archive.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in ssh tar gzip bzip2 zip unzip; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")" + else + echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")" + fi + done + return 0 +} diff --git a/core/modules/archive.tr.en b/core/modules/archive.tr.en new file mode 100644 index 0000000..5528413 --- /dev/null +++ b/core/modules/archive.tr.en @@ -0,0 +1,14 @@ +mkdir_fail=❌ [archive] Failed to create destination directory: {dest} +compressed_tar=📦 [archive] Compressed as TAR: {output} +compressed_zip=📦 [archive] Compressed as ZIP: {output} +compressed_gzip=📦 [archive] GZIP: {file}.gz +compressed_bzip2=📦 [archive] BZIP2: {file}.bz2 +decompressed_gzip=📂 [archive] Decompressed GZIP: {archive} +decompressed_bzip2=📂 [archive] Decompressed BZIP2: {archive} +decompressed_zip=📂 [archive] Decompressed ZIP into: {dest} +extracted_tar=📂 [archive] Extracted TAR into: {dest} +extracted_zip=📂 [archive] Extracted ZIP into: {dest} +unsupported_format=❌ [archive] Unsupported format '{format}' +unsupported_action=❌ [archive] Unsupported action '{action}' +missing_cmd=⚠️ [archive] '{cmd}' not available locally. Assuming it exists on the remote host. +cmd_ok=✅ [archive] '{cmd}' available locally. diff --git a/core/modules/archive.tr.es b/core/modules/archive.tr.es new file mode 100644 index 0000000..86764fd --- /dev/null +++ b/core/modules/archive.tr.es @@ -0,0 +1,14 @@ +mkdir_fail=❌ [archive] No se pudo crear el directorio destino: {dest} +compressed_tar=📦 [archive] Comprimido en TAR: {output} +compressed_zip=📦 [archive] Comprimido en ZIP: {output} +compressed_gzip=📦 [archive] GZIP: {file}.gz +compressed_bzip2=📦 [archive] BZIP2: {file}.bz2 +decompressed_gzip=📂 [archive] Descomprimido GZIP: {archive} +decompressed_bzip2=📂 [archive] Descomprimido BZIP2: {archive} +decompressed_zip=📂 [archive] Descomprimido ZIP en: {dest} +extracted_tar=📂 [archive] Extraído TAR en: {dest} +extracted_zip=📂 [archive] Extraído ZIP en: {dest} +unsupported_format=❌ [archive] Formato '{format}' no soportado +unsupported_action=❌ [archive] Acción '{action}' no soportada +missing_cmd=⚠️ [archive] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto. +cmd_ok=✅ [archive] '{cmd}' disponible localmente. diff --git a/core/modules/blockinfile.sh b/core/modules/blockinfile.sh new file mode 100644 index 0000000..e42f5d8 --- /dev/null +++ b/core/modules/blockinfile.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Module: blockinfile +# Description: Inserta o actualiza bloques de texto delimitados en archivos +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: grep, sed, tee, awk + +blockinfile_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value" + done + + local path="${args[path]}" + local block="${args[block]}" + local marker="${args[marker]:-SHFLOW}" + local create="${args[create]:-true}" + local backup="${args[backup]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + local start="# BEGIN $marker" + local end="# END $marker" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/blockinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ ! -f "$path" ]]; then + if [[ "$create" == "true" ]]; then + echo "$(render_msg "${tr[creating]}" "path=$path")" + touch "$path" + else + echo "$(render_msg "${tr[missing_file]}" "path=$path")" + return 1 + fi + fi + + if [[ "$backup" == "true" ]]; then + cp "$path" "$path.bak" + echo "$(render_msg "${tr[backup]}" "path=$path")" + fi + + if grep -q "$start" "$path"; then + echo "$(render_msg "${tr[replacing]}" "marker=$marker")" + $prefix sed -i "/$start/,/$end/d" "$path" + fi + + echo "$(render_msg "${tr[inserting]}" "marker=$marker")" + { + echo "$start" + echo "$block" + echo "$end" + } | $prefix tee -a "$path" > /dev/null +} + +check_dependencies_blockinfile() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/blockinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + local missing=() + for cmd in grep sed tee awk; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [blockinfile] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/blockinfile.tr.en b/core/modules/blockinfile.tr.en new file mode 100644 index 0000000..70a7711 --- /dev/null +++ b/core/modules/blockinfile.tr.en @@ -0,0 +1,7 @@ +creating=📄 [blockinfile] Creating file: {path} +missing_file=❌ [blockinfile] File does not exist and create=false +backup=📦 Backup created: {path}.bak +replacing=🔁 [blockinfile] Replacing existing block with marker '{marker}' +inserting=➕ [blockinfile] Inserting delimited block with marker '{marker}' +missing_deps=❌ [blockinfile] Missing dependencies: {cmds} +deps_ok=✅ [blockinfile] All dependencies are available diff --git a/core/modules/blockinfile.tr.es b/core/modules/blockinfile.tr.es new file mode 100644 index 0000000..98df5c7 --- /dev/null +++ b/core/modules/blockinfile.tr.es @@ -0,0 +1,7 @@ +creating=📄 [blockinfile] Creando archivo: {path} +missing_file=❌ [blockinfile] El archivo no existe y create=false +backup=📦 Copia de seguridad creada: {path}.bak +replacing=🔁 [blockinfile] Reemplazando bloque existente con marcador '{marker}' +inserting=➕ [blockinfile] Insertando bloque delimitado con marcador '{marker}' +missing_deps=❌ [blockinfile] Dependencias faltantes: {cmds} +deps_ok=✅ [blockinfile] Todas las dependencias están disponibles diff --git a/core/modules/copy.sh b/core/modules/copy.sh new file mode 100644 index 0000000..b46d9e5 --- /dev/null +++ b/core/modules/copy.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Module: copy +# Description: Copia archivos locales al host remoto usando scp +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: scp, ssh + +copy_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local src="${args[src]}" + local dest="${args[dest]}" + local mode="${args[mode]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/copy.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$src" || -z "$dest" ]]; then + echo "${tr[missing_args]:-❌ [copy] Faltan parámetros: src y dest son obligatorios}" + return 1 + fi + + echo "$(render_msg "${tr[copying]}" "src=$src" "host=$host")" + scp "$src" "$host:/tmp/shflow_tmpfile" || { + echo "$(render_msg "${tr[scp_fail]}" "src=$src" "host=$host")" + return 1 + } + + echo "$(render_msg "${tr[moving]}" "dest=$dest")" + ssh "$host" "$prefix mv /tmp/shflow_tmpfile '$dest' && $prefix chmod $mode '$dest'" && \ + echo "$(render_msg "${tr[done]}" "dest=$dest")" +} + +check_dependencies_copy() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/copy.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + local missing=() + for cmd in scp ssh; do + command -v "$cmd" &> /dev/null || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [copy] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/copy.tr.en b/core/modules/copy.tr.en new file mode 100644 index 0000000..dcace81 --- /dev/null +++ b/core/modules/copy.tr.en @@ -0,0 +1,7 @@ +missing_args=❌ [copy] Missing parameters: src and dest are required +copying=📤 [copy] Copying '{src}' to host '{host}'... +scp_fail=❌ [copy] Failed to copy '{src}' to host '{host}' +moving=📦 [copy] Moving file to final destination: '{dest}' +done=✅ [copy] File successfully installed at '{dest}' +missing_deps=❌ [copy] Missing dependencies: {cmds} +deps_ok=✅ [copy] All dependencies are available diff --git a/core/modules/copy.tr.es b/core/modules/copy.tr.es new file mode 100644 index 0000000..9b5f934 --- /dev/null +++ b/core/modules/copy.tr.es @@ -0,0 +1,7 @@ +missing_args=❌ [copy] Faltan parámetros: src y dest son obligatorios +copying=📤 [copy] Copiando '{src}' al host '{host}'... +scp_fail=❌ [copy] Falló la copia de '{src}' al host '{host}' +moving=📦 [copy] Moviendo archivo a destino final: '{dest}' +done=✅ [copy] Archivo instalado correctamente en '{dest}' +missing_deps=❌ [copy] Dependencias faltantes: {cmds} +deps_ok=✅ [copy] Todas las dependencias están disponibles diff --git a/core/modules/cron.sh b/core/modules/cron.sh new file mode 100644 index 0000000..c1c4533 --- /dev/null +++ b/core/modules/cron.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Module: cron +# Description: Gestiona entradas de cron para usuarios del sistema (crear, modificar, eliminar, listar) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: bash, crontab, grep, id, sudo + +cron_task() { + local host="$1"; shift + local alias="" user="" state="" schedule="" command="" + for arg in "$@"; do + case "$arg" in + alias=*) alias="${arg#alias=}" ;; + user=*) user="${arg#user=}" ;; + state=*) state="${arg#state=}" ;; + schedule=*) schedule="${arg#schedule=}" ;; + command=*) command="${arg#command=}" ;; + esac + done + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/cron.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$user" || -z "$state" ]]; then + echo "${tr[missing_args]:-❌ [cron] Faltan argumentos obligatorios: 'user' y 'state'}" + return 1 + fi + + if ! id "$user" &>/dev/null; then + echo "$(render_msg "${tr[user_not_found]}" "user=$user")" + return 1 + fi + + local tag="# shflow:$alias" + local tmpfile + tmpfile=$(mktemp) + + echo "$(render_msg "${tr[checking]}" "user=$user")" + sudo crontab -u "$user" -l 2>/dev/null > "$tmpfile" || true + + case "$state" in + list) + echo "$(render_msg "${tr[list]}" "user=$user")" + grep -E "^.*$tag|^[^#]" "$tmpfile" || echo "${tr[no_entries]:-⚠️ [cron] No hay entradas visibles}" + rm -f "$tmpfile" + return 0 + ;; + absent) + if grep -q "$tag" "$tmpfile"; then + grep -v "$tag" "$tmpfile" > "${tmpfile}.new" + sudo crontab -u "$user" "${tmpfile}.new" + echo "$(render_msg "${tr[removed]}" "alias=$alias")" + rm -f "${tmpfile}.new" + else + echo "$(render_msg "${tr[not_found]}" "alias=$alias")" + fi + rm -f "$tmpfile" + return 0 + ;; + present) + if [[ -z "$alias" || -z "$schedule" || -z "$command" ]]; then + echo "${tr[missing_present]:-❌ [cron] Para 'present' se requieren: alias, schedule y command}" + rm -f "$tmpfile" + return 1 + fi + grep -v "$tag" "$tmpfile" > "${tmpfile}.new" + echo "$schedule $command $tag" >> "${tmpfile}.new" + sudo crontab -u "$user" "${tmpfile}.new" + echo "$(render_msg "${tr[added]}" "alias=$alias")" + rm -f "$tmpfile" "${tmpfile}.new" + return 0 + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "state=$state")" + rm -f "$tmpfile" + return 1 + ;; + esac +} + +check_dependencies_cron() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/cron.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v sudo &>/dev/null; then + echo "${tr[missing_sudo]:-❌ [cron] El comando 'sudo' no está disponible}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [cron] Dependencias OK}" + return 0 +} diff --git a/core/modules/cron.tr.en b/core/modules/cron.tr.en new file mode 100644 index 0000000..404c5f0 --- /dev/null +++ b/core/modules/cron.tr.en @@ -0,0 +1,12 @@ +missing_args=❌ [cron] Missing required arguments: 'user' and 'state' +user_not_found=❌ [cron] User '{user}' does not exist on the system +checking=🕒 [cron] Checking cron entries for user '{user}'... +list=📋 [cron] Current entries for '{user}': +no_entries=⚠️ [cron] No visible entries +removed=➖ [cron] Entry '{alias}' successfully removed +not_found=⚠️ [cron] Entry '{alias}' not found, nothing removed +missing_present=❌ [cron] For 'present', alias, schedule and command are required +added=➕ [cron] Entry '{alias}' successfully created/modified +unsupported=❌ [cron] Unknown state: '{state}'. Use 'present', 'absent' or 'list' +missing_sudo=❌ [cron] 'sudo' command is not available +deps_ok=✅ [cron] Dependencies OK diff --git a/core/modules/cron.tr.es b/core/modules/cron.tr.es new file mode 100644 index 0000000..683e2f8 --- /dev/null +++ b/core/modules/cron.tr.es @@ -0,0 +1,12 @@ +missing_args=❌ [cron] Faltan argumentos obligatorios: 'user' y 'state' +user_not_found=❌ [cron] El usuario '{user}' no existe en el sistema +checking=🕒 [cron] Revisando entradas de cron para usuario '{user}'... +list=📋 [cron] Entradas actuales para '{user}': +no_entries=⚠️ [cron] No hay entradas visibles +removed=➖ [cron] Entrada '{alias}' eliminada correctamente +not_found=⚠️ [cron] Entrada '{alias}' no encontrada, no se eliminó nada +missing_present=❌ [cron] Para 'present' se requieren: alias, schedule y command +added=➕ [cron] Entrada '{alias}' creada/modificada correctamente +unsupported=❌ [cron] Estado desconocido: '{state}'. Usa 'present', 'absent' o 'list' +missing_sudo=❌ [cron] El comando 'sudo' no está disponible +deps_ok=✅ [cron] Dependencias OK diff --git a/core/modules/docker.sh b/core/modules/docker.sh new file mode 100644 index 0000000..9d3350e --- /dev/null +++ b/core/modules/docker.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# License: GPLv3 +# Module: docker +# Description: Gestiona contenedores Docker (run, stop, remove, build, exec) +# Author: Luis GuLo +# Version: 1.7.0 +# Dependencies: ssh, docker + +docker_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local action="${args[action]}" + local become="${args[become]:-false}" + local detach="${args[detach]:-true}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + local detached="-d" + [ "$detach" = "false" ] && detached="" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/docker.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + local name="" image="" path="" command="" + case "$action" in present|stopped|absent|exec) name="${args[name]}" ;; esac + case "$action" in present|build) image="${args[image]}" ;; esac + [[ "$action" == "build" ]] && path="${args[path]}" + [[ "$action" == "exec" ]] && command="${args[command]}" + + case "$action" in + present) + local extra="${args[run_args]:-${args[extra_args]:-}}" + echo "$(render_msg "${tr[run]}" "name=$name" "image=$image")" + ssh "$host" "$prefix docker ps -a --format '{{.Names}}' | grep -q '^$name$' || $prefix docker run $detached --name '$name' $extra '$image'" + ;; + stopped) + echo "$(render_msg "${tr[stop]}" "name=$name")" + ssh "$host" "$prefix docker ps --format '{{.Names}}' | grep -q '^$name$' && $prefix docker stop '$name'" + ;; + absent) + echo "$(render_msg "${tr[remove]}" "name=$name")" + ssh "$host" "$prefix docker ps -a --format '{{.Names}}' | grep -q '^$name$' && $prefix docker rm -f '$name'" + ;; + build) + echo "$(render_msg "${tr[build]}" "image=$image" "path=$path")" + ssh "$host" "cd '$path' && $prefix docker build -t '$image' ." + ;; + exec) + echo "$(render_msg "${tr[exec]}" "name=$name" "command=$command")" + ssh "$host" "$prefix docker exec '$name' $command" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +check_dependencies_docker() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/docker.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_ssh]:-❌ [docker] ssh no está disponible.}" + return 1 + fi + echo "${tr[ssh_ok]:-✅ [docker] ssh disponible.}" + + if ! command -v docker &> /dev/null; then + echo "${tr[missing_docker]:-⚠️ [docker] docker no disponible localmente. Se asumirá que existe en el host remoto.}" + else + echo "${tr[docker_ok]:-✅ [docker] docker disponible localmente.}" + fi + return 0 +} diff --git a/core/modules/docker.tr.en b/core/modules/docker.tr.en new file mode 100644 index 0000000..59844f2 --- /dev/null +++ b/core/modules/docker.tr.en @@ -0,0 +1,10 @@ +run=🧪 [docker] Running container '{name}' with image '{image}' +stop=🛑 [docker] Stopping container '{name}' +remove=🧹 [docker] Removing container '{name}' +build=🏗️ [docker] Building image '{image}' from '{path}' +exec=🚀 [docker] Executing command in '{name}': {command} +unsupported=❌ [docker] Unsupported action '{action}' +missing_ssh=❌ [docker] ssh is not available. +ssh_ok=✅ [docker] ssh is available. +missing_docker=⚠️ [docker] docker not available locally. Assuming it exists on the remote host. +docker_ok=✅ [docker] docker available locally. diff --git a/core/modules/docker.tr.es b/core/modules/docker.tr.es new file mode 100644 index 0000000..4abe373 --- /dev/null +++ b/core/modules/docker.tr.es @@ -0,0 +1,10 @@ +run=🧪 [docker] Ejecutando contenedor '{name}' con imagen '{image}' +stop=🛑 [docker] Deteniendo contenedor '{name}' +remove=🧹 [docker] Eliminando contenedor '{name}' +build=🏗️ [docker] Construyendo imagen '{image}' desde '{path}' +exec=🚀 [docker] Ejecutando comando en '{name}': {command} +unsupported=❌ [docker] Acción '{action}' no soportada +missing_ssh=❌ [docker] ssh no está disponible. +ssh_ok=✅ [docker] ssh disponible. +missing_docker=⚠️ [docker] docker no disponible localmente. Se asumirá que existe en el host remoto. +docker_ok=✅ [docker] docker disponible localmente. diff --git a/core/modules/download.sh b/core/modules/download.sh new file mode 100644 index 0000000..a71fcc4 --- /dev/null +++ b/core/modules/download.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Module: download +# Description: Descarga ficheros remotos con soporte para reintentos, proxy y reanudación +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: wget o curl, sudo (si become=true) + +download_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local url="${args[url]}" + local dest="${args[dest]:-$(basename "$url")}" + local proxy="${args[proxy]:-}" + local continue="${args[continue]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/download.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$url" ]]; then + echo "${tr[missing_url]:-❌ [download] Falta el parámetro obligatorio 'url'}" + return 1 + fi + + local cmd="" + if command -v wget &>/dev/null; then + echo "${tr[using_wget]:-📦 [download] Usando wget}" + cmd="$prefix wget \"$url\" -O \"$dest\"" + [[ "$continue" == "true" ]] && cmd="$cmd -c" + [[ -n "$proxy" ]] && cmd="$cmd -e use_proxy=yes -e http_proxy=\"$proxy\"" + elif command -v curl &>/dev/null; then + echo "${tr[using_curl]:-📦 [download] Usando curl}" + cmd="$prefix curl -L \"$url\" -o \"$dest\"" + [[ "$continue" == "true" ]] && cmd="$cmd -C -" + [[ -n "$proxy" ]] && cmd="$cmd --proxy \"$proxy\"" + else + echo "${tr[missing_tool]:-❌ [download] Ni wget ni curl están disponibles}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "url=$url" "dest=$dest")" + eval "$cmd" && echo "$(render_msg "${tr[done]}" "dest=$dest")" +} + +check_dependencies_download() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/download.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v wget &>/dev/null && ! command -v curl &>/dev/null; then + echo "${tr[missing_tool]:-❌ [download] Se requiere 'wget' o 'curl'}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [download] Herramienta de descarga disponible}" + return 0 +} diff --git a/core/modules/download.tr.en b/core/modules/download.tr.en new file mode 100644 index 0000000..04526c4 --- /dev/null +++ b/core/modules/download.tr.en @@ -0,0 +1,7 @@ +missing_url=❌ [download] Missing required parameter 'url' +using_wget=📦 [download] Using wget +using_curl=📦 [download] Using curl +missing_tool=❌ [download] Neither wget nor curl are available +start=🔧 [download] Downloading '{url}' → '{dest}' +done=✅ [download] Download completed: {dest} +deps_ok=✅ [download] Download tool available diff --git a/core/modules/download.tr.es b/core/modules/download.tr.es new file mode 100644 index 0000000..d4f54d3 --- /dev/null +++ b/core/modules/download.tr.es @@ -0,0 +1,7 @@ +missing_url=❌ [download] Falta el parámetro obligatorio 'url' +using_wget=📦 [download] Usando wget +using_curl=📦 [download] Usando curl +missing_tool=❌ [download] Ni wget ni curl están disponibles +start=🔧 [download] Descargando '{url}' → '{dest}' +done=✅ [download] Descarga completada: {dest} +deps_ok=✅ [download] Herramienta de descarga disponible diff --git a/core/modules/echo.sh b/core/modules/echo.sh new file mode 100644 index 0000000..8b562a5 --- /dev/null +++ b/core/modules/echo.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Module: echo +# Description: Muestra un mensaje en consola con soporte para variables ShFlow +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: - + +echo_task() { + local host="$1"; shift + declare -A args + + while [[ "$#" -gt 0 ]]; do + case "$1" in + *=*) + key="${1%%=*}" + value="${1#*=}" + args["$key"]="$value" + ;; + esac + shift + done + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/echo.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # 🔁 Interpolar usando argumentos explícitos + for key in "${!args[@]}"; do + for var in "${!args[@]}"; do + args["$key"]="${args[$key]//\{\{ $var \}\}/${args[$var]}}" + done + done + + local message="${args[message]}" + echo "$(render_msg "${tr[output]}" "message=$message")" +} + +check_dependencies_echo() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/echo.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + echo "${tr[deps_ok]:-✅ [echo] No requiere dependencias.}" + return 0 +} diff --git a/core/modules/echo.tr.en b/core/modules/echo.tr.en new file mode 100644 index 0000000..ab2154a --- /dev/null +++ b/core/modules/echo.tr.en @@ -0,0 +1,2 @@ +output=📣 [echo] {message} +deps_ok=✅ [echo] No dependencies required. diff --git a/core/modules/echo.tr.es b/core/modules/echo.tr.es new file mode 100644 index 0000000..ae12ef8 --- /dev/null +++ b/core/modules/echo.tr.es @@ -0,0 +1,2 @@ +output=📣 [echo] {message} +deps_ok=✅ [echo] No requiere dependencias. diff --git a/core/modules/facts.sh b/core/modules/facts.sh new file mode 100644 index 0000000..498ccd0 --- /dev/null +++ b/core/modules/facts.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Module: facts +# Description: Extrae información del sistema con opciones de formato, filtrado y salida +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.4.0 +# Dependencies: lscpu, ip, free, lsblk, uname, hostnamectl + +facts_task() { + local host="$1"; shift + declare -A args + local field="" format="plain" output="" append="false" host_label="" + + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}" + case "$key" in + field) field="$value" ;; + format) format="${value,,}" ;; + output) output="$value" ;; + append) append="$value" ;; + host_label) host_label="$value" ;; + esac + done + + [[ -z "$host_label" ]] && host_label="$host" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/facts.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local prefix="" + [[ "$host" != "localhost" ]] && prefix="ssh $host" + + [[ "$DEBUG" == "true" ]] && echo "$(render_msg "${tr[debug_prefix]}" "prefix=$prefix")" + + local raw + raw=$($prefix bash --noprofile --norc <<'EOF' + cd /tmp || cd ~ + echo "hostname=$(hostname)" + lscpu | awk '/^CPU\(s\):/ {print "cpu_count="$2}' + free -m | awk '/Mem:/ {print "ram_total_mb="$2}' + if command -v hostnamectl &> /dev/null; then + hostnamectl | awk -F: '/Operating System/ {print "os_name=" $2}' | sed 's/^ *//' + hostnamectl | awk -F: '/Kernel/ {print "os_version=" $2}' | sed 's/^ *//' + else + echo "os_name=$(uname -s)" + echo "os_version=$(uname -r)" + fi + ip link show | awk -F: '/^[0-9]+: / {print $2}' | grep -Ev 'docker|virbr|lo|veth|br-' | while read -r dev; do + ip=$(ip -4 addr show "$dev" | awk '/inet / {print $2}' | cut -d/ -f1) + mac=$(ip link show "$dev" | awk '/ether/ {print $2}') + [[ -n "$ip" || -n "$mac" ]] && echo "net_$dev=IP:$ip MAC:$mac" + done + ip -4 addr show | awk '/inet / {print $2}' | cut -d/ -f1 | paste -sd ' ' - | awk '{print "ip_addresses="$0}' + lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT | grep -Ev 'loop|tmpfs|overlay|docker' | awk 'NR>1 && NF>0 {print "partition_list=" $1 " " $2 " " $3 " " $4}' +EOF +) + + [[ -n "$field" ]] && raw=$(echo "$raw" | grep "^$field=") + + local partitions=() facts=() + while IFS= read -r line; do + [[ "$line" == partition_list=* ]] && partitions+=("${line#*=}") || facts+=("$line") + done <<< "$raw" + + local formatted="" + case "$format" in + plain) + formatted+="Host: $host_label\n" + for f in "${facts[@]}"; do formatted+="${f%%=*}: ${f#*=}\n"; done + [[ ${#partitions[@]} -gt 0 ]] && formatted+="partitions:\n" && for p in "${partitions[@]}"; do formatted+=" - $p\n"; done + ;; + md) + formatted+="### $host_label\n" + for f in "${facts[@]}"; do formatted+="- **${f%%=*}:** ${f#*=}\n"; done + [[ ${#partitions[@]} -gt 0 ]] && formatted+="- **partitions:**\n" && for p in "${partitions[@]}"; do formatted+=" - $p\n"; done + ;; + kv) + for f in "${facts[@]}"; do formatted+="$f\n"; done + [[ ${#partitions[@]} -gt 0 ]] && formatted+="partitions=$(IFS=';'; echo "${partitions[*]}")\n" + ;; + json) + local json="{" + for f in "${facts[@]}"; do json+="\"${f%%=*}\":\"${f#*=}\","; done + [[ ${#partitions[@]} -gt 0 ]] && json+="\"partitions\":[" && for p in "${partitions[@]}"; do json+="\"$p\","; done && json="${json%,}]" || json="${json%,}" + json+="}" + formatted="$json" + ;; + *) + echo "$(render_msg "${tr[unsupported_format]}" "format=$format")" + return 1 + ;; + esac + + if [[ -n "$output" ]]; then + [[ "$append" == "true" ]] && echo -e "$formatted" >> "$output" || echo -e "$formatted" > "$output" + echo "$(render_msg "${tr[saved]}" "output=$output")" + else + echo -e "$formatted" + fi +} + +check_dependencies_facts() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/facts.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in lscpu ip free lsblk uname hostnamectl; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")" + else + echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")" + fi + done + return 0 +} diff --git a/core/modules/facts.tr.en b/core/modules/facts.tr.en new file mode 100644 index 0000000..015ef5b --- /dev/null +++ b/core/modules/facts.tr.en @@ -0,0 +1,5 @@ +debug_prefix=🔍 SSH line: {prefix} +unsupported_format=❌ [facts] Format '{format}' not supported. +saved=💾 [facts] Report saved to: {output} +missing_cmd=⚠️ [facts] '{cmd}' not available locally. Assuming it exists on remote host. +cmd_ok=✅ [facts] '{cmd}' available locally. diff --git a/core/modules/facts.tr.es b/core/modules/facts.tr.es new file mode 100644 index 0000000..7d2b08b --- /dev/null +++ b/core/modules/facts.tr.es @@ -0,0 +1,5 @@ +debug_prefix=🔍 Línea SSH: {prefix} +unsupported_format=❌ [facts] Formato '{format}' no soportado. +saved=💾 [facts] Informe guardado en: {output} +missing_cmd=⚠️ [facts] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto. +cmd_ok=✅ [facts] '{cmd}' disponible localmente. diff --git a/core/modules/file.sh b/core/modules/file.sh new file mode 100644 index 0000000..44804af --- /dev/null +++ b/core/modules/file.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Module: file +# Description: Gestiona archivos y directorios remotos (crear, eliminar, permisos) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh + +file_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local path="${args[path]}" + local state="${args[state]}" + local type="${args[type]}" + local mode="${args[mode]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$state" in + present) + if [[ "$type" == "directory" ]]; then + echo "$(render_msg "${tr[creating_dir]}" "path=$path")" + ssh "$host" "[ -d '$path' ] || $prefix mkdir -p '$path'" + elif [[ "$type" == "file" ]]; then + echo "$(render_msg "${tr[creating_file]}" "path=$path")" + ssh "$host" "[ -f '$path' ] || $prefix touch '$path'" + fi + if [[ -n "$mode" ]]; then + echo "$(render_msg "${tr[setting_mode]}" "mode=$mode" "path=$path")" + ssh "$host" "$prefix chmod $mode '$path'" + fi + ;; + absent) + if [[ "$type" == "directory" ]]; then + echo "$(render_msg "${tr[removing_dir]}" "path=$path")" + ssh "$host" "[ -d '$path' ] && $prefix rm -rf '$path'" + elif [[ "$type" == "file" ]]; then + echo "$(render_msg "${tr[removing_file]}" "path=$path")" + ssh "$host" "[ -f '$path' ] && $prefix rm -f '$path'" + fi + ;; + *) + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_file() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [file] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [file] ssh disponible.}" + return 0 +} diff --git a/core/modules/file.tr.en b/core/modules/file.tr.en new file mode 100644 index 0000000..2d11c91 --- /dev/null +++ b/core/modules/file.tr.en @@ -0,0 +1,8 @@ +creating_dir=📁 [file] Creating directory: {path} +creating_file=📄 [file] Creating file: {path} +setting_mode=🔐 [file] Setting permissions {mode} on {path} +removing_dir=🧹 [file] Removing directory: {path} +removing_file=🧹 [file] Removing file: {path} +unsupported_state=❌ [file] Unsupported state '{state}'. Use present or absent. +missing_deps=❌ [file] ssh is not available. +deps_ok=✅ [file] ssh is available. diff --git a/core/modules/file.tr.es b/core/modules/file.tr.es new file mode 100644 index 0000000..4123cad --- /dev/null +++ b/core/modules/file.tr.es @@ -0,0 +1,8 @@ +creating_dir=📁 [file] Creando directorio: {path} +creating_file=📄 [file] Creando archivo: {path} +setting_mode=🔐 [file] Estableciendo permisos {mode} en {path} +removing_dir=🧹 [file] Eliminando directorio: {path} +removing_file=🧹 [file] Eliminando archivo: {path} +unsupported_state=❌ [file] Estado '{state}' no soportado. Usa present o absent. +missing_deps=❌ [file] ssh no está disponible. +deps_ok=✅ [file] ssh disponible. diff --git a/core/modules/file_read.sh b/core/modules/file_read.sh new file mode 100644 index 0000000..e21423f --- /dev/null +++ b/core/modules/file_read.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Module: file_read +# Description: Lee el contenido de un archivo remoto, con opción de filtrado por patrón +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: ssh, cat, grep + +file_read_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local path="${args[path]}" + local grep="${args[grep]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file_read.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ -z "$path" ]]; then + echo "${tr[missing_path]:-❌ [file_read] Parámetro 'path' obligatorio}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "path=$path" "host=$host")" + + if [[ -n "$grep" ]]; then + ssh "$host" "$prefix grep -E '$grep' '$path'" + else + ssh "$host" "$prefix cat '$path'" + fi +} + +check_dependencies_file_read() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file_read.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null || ! command -v grep &> /dev/null; then + echo "${tr[missing_deps]:-❌ [file_read] ssh o grep no están disponibles.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [file_read] ssh y grep disponibles.}" + return 0 +} diff --git a/core/modules/file_read.tr.en b/core/modules/file_read.tr.en new file mode 100644 index 0000000..117764e --- /dev/null +++ b/core/modules/file_read.tr.en @@ -0,0 +1,4 @@ +missing_path=❌ [file_read] Parameter 'path' is required +start=📄 [file_read] Reading file '{path}' on {host}... +missing_deps=❌ [file_read] ssh or grep are not available. +deps_ok=✅ [file_read] ssh and grep are available. diff --git a/core/modules/file_read.tr.es b/core/modules/file_read.tr.es new file mode 100644 index 0000000..0331aa7 --- /dev/null +++ b/core/modules/file_read.tr.es @@ -0,0 +1,4 @@ +missing_path=❌ [file_read] Parámetro 'path' obligatorio +start=📄 [file_read] Leyendo archivo '{path}' en {host}... +missing_deps=❌ [file_read] ssh o grep no están disponibles. +deps_ok=✅ [file_read] ssh y grep disponibles. diff --git a/core/modules/fs.sh b/core/modules/fs.sh new file mode 100644 index 0000000..1b16bad --- /dev/null +++ b/core/modules/fs.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Module: fs +# Description: Operaciones remotas sobre ficheros (mover, renombrar, copiar, borrar, truncar) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.3.0 +# Dependencies: ssh + +fs_task() { + local host="$1"; shift + declare -A args + local files=() + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/fs.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # Parseo de argumentos + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + if [[ "$key" == "files" ]]; then + if [[ "$value" == *'*'* || "$value" == *'?'* || "$value" == *'['* ]]; then + mapfile -t files < <(ssh "$host" "ls -1 $value 2>/dev/null") + else + IFS=',' read -r -a files <<< "$value" + fi + else + args["$key"]="$value" + fi + done + + local action="${args[action]}" + local src="${args[src]}" + local dest="${args[dest]}" + local path="${args[path]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + case "$action" in + move|rename|copy) + if [[ ${#files[@]} -gt 0 ]]; then + for file in "${files[@]}"; do + base="$(basename "$file")" + target="$dest/$base" + cmd="$prefix mv"; [[ "$action" == "copy" ]] && cmd="$prefix cp" + ssh "$host" "$cmd '$file' '$target'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$file" "dest=$target")" + done + else + cmd="$prefix mv"; [[ "$action" == "copy" ]] && cmd="$prefix cp" + ssh "$host" "$cmd '$src' '$dest'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$src" "dest=$dest")" + fi + ;; + delete|truncate) + if [[ ${#files[@]} -gt 0 ]]; then + for file in "${files[@]}"; do + cmd="$prefix rm -f"; [[ "$action" == "truncate" ]] && cmd="$prefix truncate -s 0" + ssh "$host" "$cmd '$file'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$file")" + done + else + cmd="$prefix rm -f"; [[ "$action" == "truncate" ]] && cmd="$prefix truncate -s 0" + ssh "$host" "$cmd '$path'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$path")" + fi + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +check_dependencies_fs() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/fs.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [fs] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [fs] ssh disponible.}" + return 0 +} diff --git a/core/modules/fs.tr.en b/core/modules/fs.tr.en new file mode 100644 index 0000000..4e0b2ef --- /dev/null +++ b/core/modules/fs.tr.en @@ -0,0 +1,4 @@ +action_ok=📁 [{action}] {src} → {dest} +unsupported=❌ [fs] Unsupported action '{action}'. +missing_deps=❌ [fs] ssh is not available. +deps_ok=✅ [fs] ssh is available. diff --git a/core/modules/fs.tr.es b/core/modules/fs.tr.es new file mode 100644 index 0000000..35f3e0a --- /dev/null +++ b/core/modules/fs.tr.es @@ -0,0 +1,4 @@ +action_ok=📁 [{action}] {src} → {dest} +unsupported=❌ [fs] Acción '{action}' no soportada. +missing_deps=❌ [fs] ssh no está disponible. +deps_ok=✅ [fs] ssh disponible. diff --git a/core/modules/git.sh b/core/modules/git.sh new file mode 100644 index 0000000..0fa22f1 --- /dev/null +++ b/core/modules/git.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Module: git +# Description: Gestiona repositorios Git en hosts remotos (clone, pull, checkout, fetch-file) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh, git, curl, tar + +git_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local action="${args[action]}" + local repo="${args[repo]}" + local dest="${args[dest]}" + local branch="${args[branch]}" + local file_path="${args[file_path]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/git.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$action" in + clone) + echo "$(render_msg "${tr[cloning]}" "repo=$repo" "dest=$dest")" + ssh "$host" "[ -d '$dest/.git' ] || $prefix git clone '$repo' '$dest'" + ;; + pull) + echo "$(render_msg "${tr[pulling]}" "dest=$dest")" + ssh "$host" "[ -d '$dest/.git' ] && cd '$dest' && $prefix git pull" + ;; + checkout) + echo "$(render_msg "${tr[checkout]}" "branch=$branch" "dest=$dest")" + ssh "$host" "[ -d '$dest/.git' ] && cd '$dest' && $prefix git checkout '$branch'" + ;; + fetch-file) + echo "$(render_msg "${tr[fetching]}" "file=$file_path" "repo=$repo" "branch=$branch")" + fetch_file_from_repo "$host" "$repo" "$branch" "$file_path" "$dest" "$become" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +fetch_file_from_repo() { + local host="$1" + local repo="$2" + local branch="$3" + local file_path="$4" + local dest="$5" + local become="$6" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + ssh "$host" "$prefix git archive --remote='$repo' '$branch' '$file_path' | $prefix tar -xO > '$dest'" +} + +check_dependencies_git() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/git.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in ssh git curl tar; do + command -v "$cmd" &> /dev/null || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [git] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/git.tr.en b/core/modules/git.tr.en new file mode 100644 index 0000000..ca27acd --- /dev/null +++ b/core/modules/git.tr.en @@ -0,0 +1,7 @@ +cloning=📥 [git] Cloning repository '{repo}' into '{dest}' +pulling=🔄 [git] Running git pull in '{dest}' +checkout=📦 [git] Switching to branch '{branch}' in '{dest}' +fetching=📄 [git] Fetching file '{file}' from '{repo}' branch '{branch}' +unsupported=❌ [git] Unsupported action '{action}' +missing_deps=❌ [git] Missing dependencies: {cmds} +deps_ok=✅ [git] All dependencies are available diff --git a/core/modules/git.tr.es b/core/modules/git.tr.es new file mode 100644 index 0000000..60a68f7 --- /dev/null +++ b/core/modules/git.tr.es @@ -0,0 +1,7 @@ +cloning=📥 [git] Clonando repositorio '{repo}' en '{dest}' +pulling=🔄 [git] Ejecutando git pull en '{dest}' +checkout=📦 [git] Cambiando a rama '{branch}' en '{dest}' +fetching=📄 [git] Extrayendo archivo '{file}' desde '{repo}' rama '{branch}' +unsupported=❌ [git] Acción '{action}' no soportada +missing_deps=❌ [git] Dependencias faltantes: {cmds} +deps_ok=✅ [git] Todas las dependencias están disponibles diff --git a/core/modules/groups.sh b/core/modules/groups.sh new file mode 100644 index 0000000..e446b12 --- /dev/null +++ b/core/modules/groups.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Module: groups +# Description: Gestiona grupos del sistema (crear, modificar, eliminar) +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: getent, groupadd, groupmod, groupdel, sudo + +groups_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local groupname="${args[groupname]}" + local gid="${args[gid]:-}" + local state="${args[state]:-create}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/groups.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # 🛡️ Validación + if [[ "$become" != "true" && "$EUID" -ne 0 ]]; then + echo "${tr[priv_required]:-❌ [groups] Se requieren privilegios para gestionar grupos. Usa 'become: true'.}" + return 1 + fi + + if [[ -z "$groupname" ]]; then + echo "${tr[missing_groupname]:-❌ [groups] Falta el parámetro obligatorio 'groupname'}" + return 1 + fi + + case "$state" in + create) + echo "${tr[enter_create]:-🔧 [groups] Entrando en create}" + if getent group "$groupname" &>/dev/null; then + echo "$(render_msg "${tr[exists]}" "groupname=$groupname")" + return 0 + fi + local cmd="$prefix groupadd \"$groupname\"" + [[ -n "$gid" ]] && cmd="$cmd -g \"$gid\"" + eval "$cmd" && echo "$(render_msg "${tr[created]}" "groupname=$groupname")" + ;; + modify) + echo "${tr[enter_modify]:-🔧 [groups] Entrando en modify}" + if ! getent group "$groupname" &>/dev/null; then + echo "$(render_msg "${tr[not_exists]}" "groupname=$groupname")" + return 1 + fi + [[ -z "$gid" ]] && echo "${tr[nothing_to_modify]:-⚠️ [groups] Nada que modificar: falta 'gid'}" && return 0 + eval "$prefix groupmod -g \"$gid\" \"$groupname\"" && echo "$(render_msg "${tr[modified]}" "groupname=$groupname")" + ;; + absent) + echo "${tr[enter_absent]:-🔧 [groups] Entrando en absent}" + if ! getent group "$groupname" &>/dev/null; then + echo "$(render_msg "${tr[already_deleted]}" "groupname=$groupname")" + return 0 + fi + eval "$prefix groupdel \"$groupname\"" && echo "$(render_msg "${tr[deleted]}" "groupname=$groupname")" + ;; + *) + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_groups() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/groups.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in getent sudo; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [groups] Todas las dependencias están presentes}" + return 0 +} diff --git a/core/modules/groups.tr.en b/core/modules/groups.tr.en new file mode 100644 index 0000000..6df77e1 --- /dev/null +++ b/core/modules/groups.tr.en @@ -0,0 +1,15 @@ +priv_required=❌ [groups] Privileges required to manage groups. Use 'become: true'. +missing_groupname=❌ [groups] Missing required parameter 'groupname' +enter_create=🔧 [groups] Entering create +enter_modify=🔧 [groups] Entering modify +enter_absent=🔧 [groups] Entering absent +exists=✅ [groups] Group '{groupname}' already exists +not_exists=⚠️ [groups] Group '{groupname}' does not exist +nothing_to_modify=⚠️ [groups] Nothing to modify: missing 'gid' +created=✅ [groups] Group '{groupname}' created +modified=✅ [groups] Group '{groupname}' modified +deleted=✅ [groups] Group '{groupname}' deleted +already_deleted=✅ [groups] Group '{groupname}' already deleted +unsupported_state=❌ [groups] Unsupported state '{state}'. Use create, modify or absent. +missing_deps=❌ [groups] Missing dependencies: {cmds} +deps_ok=✅ [groups] All dependencies are present diff --git a/core/modules/groups.tr.es b/core/modules/groups.tr.es new file mode 100644 index 0000000..70e1492 --- /dev/null +++ b/core/modules/groups.tr.es @@ -0,0 +1,15 @@ +priv_required=❌ [groups] Se requieren privilegios para gestionar grupos. Usa 'become: true'. +missing_groupname=❌ [groups] Falta el parámetro obligatorio 'groupname' +enter_create=🔧 [groups] Entrando en create +enter_modify=🔧 [groups] Entrando en modify +enter_absent=🔧 [groups] Entrando en absent +exists=✅ [groups] Grupo '{groupname}' ya existe +not_exists=⚠️ [groups] Grupo '{groupname}' no existe +nothing_to_modify=⚠️ [groups] Nada que modificar: falta 'gid' +created=✅ [groups] Grupo '{groupname}' creado +modified=✅ [groups] Grupo '{groupname}' modificado +deleted=✅ [groups] Grupo '{groupname}' eliminado +already_deleted=✅ [groups] Grupo '{groupname}' ya eliminado +unsupported_state=❌ [groups] Estado '{state}' no soportado. Usa create, modify o absent. +missing_deps=❌ [groups] Dependencias faltantes: {cmds} +deps_ok=✅ [groups] Todas las dependencias están presentes diff --git a/core/modules/lineinfile.sh b/core/modules/lineinfile.sh new file mode 100644 index 0000000..aded42c --- /dev/null +++ b/core/modules/lineinfile.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Module: lineinfile +# Description: Asegura la presencia o reemplazo de una línea en un archivo +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: grep, sed, tee, awk + +lineinfile_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local path="${args[path]}" + local line="${args[line]}" + local regexp="${args[regexp]}" + local insert_after="${args[insert_after]}" + local create="${args[create]:-true}" + local backup="${args[backup]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lineinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ ! -f "$path" ]]; then + if [[ "$create" == "true" ]]; then + echo "$(render_msg "${tr[creating]}" "path=$path")" + touch "$path" + else + echo "$(render_msg "${tr[missing_file]}" "path=$path")" + return 1 + fi + fi + + if [[ "$backup" == "true" ]]; then + cp "$path" "$path.bak" + echo "$(render_msg "${tr[backup]}" "path=$path")" + fi + + if [[ -n "$regexp" && $(grep -Eq "$regexp" "$path" && echo "yes") == "yes" ]]; then + echo "$(render_msg "${tr[replacing]}" "regexp=$regexp")" + $prefix sed -i "s|$regexp|$line|" "$path" + return 0 + fi + + if [[ -n "$insert_after" && $(grep -q "$insert_after" "$path" && echo "yes") == "yes" ]]; then + echo "$(render_msg "${tr[inserting]}" "after=$insert_after")" + $prefix sed -i "/$insert_after/a $line" "$path" + return 0 + fi + + if grep -Fxq "$line" "$path"; then + echo "$(render_msg "${tr[exists]}" "line=$line")" + return 0 + fi + + echo "${tr[appending]:-➕ [lineinfile] Añadiendo línea al final}" + echo "$line" | $prefix tee -a "$path" > /dev/null +} + +check_dependencies_lineinfile() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lineinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in grep sed tee awk; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [lineinfile] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/lineinfile.tr.en b/core/modules/lineinfile.tr.en new file mode 100644 index 0000000..15054eb --- /dev/null +++ b/core/modules/lineinfile.tr.en @@ -0,0 +1,9 @@ +creating=📄 [lineinfile] Creating file: {path} +missing_file=❌ [lineinfile] File does not exist and create=false +backup=📦 Backup created: {path}.bak +replacing=🔁 [lineinfile] Replacing line matching: {regexp} +inserting=➕ [lineinfile] Inserting after: {after} +exists=✅ [lineinfile] Line already present: "{line}" +appending=➕ [lineinfile] Appending line at the end +missing_deps=❌ [lineinfile] Missing dependencies: {cmds} +deps_ok=✅ [lineinfile] All dependencies are available diff --git a/core/modules/lineinfile.tr.es b/core/modules/lineinfile.tr.es new file mode 100644 index 0000000..c28d6ca --- /dev/null +++ b/core/modules/lineinfile.tr.es @@ -0,0 +1,9 @@ +creating=📄 [lineinfile] Creando archivo: {path} +missing_file=❌ [lineinfile] El archivo no existe y create=false +backup=📦 Copia de seguridad creada: {path}.bak +replacing=🔁 [lineinfile] Reemplazando línea que coincide con: {regexp} +inserting=➕ [lineinfile] Insertando después de: {after} +exists=✅ [lineinfile] Línea ya presente: "{line}" +appending=➕ [lineinfile] Añadiendo línea al final +missing_deps=❌ [lineinfile] Dependencias faltantes: {cmds} +deps_ok=✅ [lineinfile] Todas las dependencias están disponibles diff --git a/core/modules/lookup.sh b/core/modules/lookup.sh new file mode 100644 index 0000000..d3c93bb --- /dev/null +++ b/core/modules/lookup.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Module: lookup +# Description: Recupera secretos cifrados del vault local +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: gpg + +lookup_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local vault_key="${args[key]}" + local vault_dir="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/core/vault" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lookup.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ -f "$vault_dir/$vault_key.gpg" ]]; then + gpg --quiet --batch --yes --passphrase-file "$HOME/.shflow.key" -d "$vault_dir/$vault_key.gpg" 2>/dev/null || \ + gpg --quiet --batch --yes -d "$vault_dir/$vault_key.gpg" + else + echo "$(render_msg "${tr[not_found]}" "key=$vault_key" "path=$vault_dir")" + return 1 + fi +} + +check_dependencies_lookup() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lookup.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v gpg &> /dev/null; then + echo "${tr[missing_deps]:-❌ [lookup] gpg no disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [lookup] gpg disponible.}" + return 0 +} diff --git a/core/modules/lookup.tr.en b/core/modules/lookup.tr.en new file mode 100644 index 0000000..c20d4b0 --- /dev/null +++ b/core/modules/lookup.tr.en @@ -0,0 +1,3 @@ +not_found=❌ [lookup] Secret '{key}' not found in {path} +missing_deps=❌ [lookup] gpg is not available. +deps_ok=✅ [lookup] gpg is available. diff --git a/core/modules/lookup.tr.es b/core/modules/lookup.tr.es new file mode 100644 index 0000000..e1a89b7 --- /dev/null +++ b/core/modules/lookup.tr.es @@ -0,0 +1,3 @@ +not_found=❌ [lookup] Secreto '{key}' no encontrado en {path} +missing_deps=❌ [lookup] gpg no disponible. +deps_ok=✅ [lookup] gpg disponible. diff --git a/core/modules/loop.sh b/core/modules/loop.sh new file mode 100644 index 0000000..3f7cb0d --- /dev/null +++ b/core/modules/loop.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Module: loop +# Description: Ejecuta un módulo sobre una lista o matriz de valores +# Author: Luis GuLo +# Version: 0.3.0 +# Dependencies: echo, tee + +loop_task() { + local host="$1"; shift + declare -A args + local items_raw="" secondary_raw="" target_module="" + local fail_fast="true" + declare -A module_args + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/loop.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # Parsear argumentos + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + case "$key" in + items) items_raw="$value" ;; + secondary) secondary_raw="$value" ;; + module) target_module="$value" ;; + fail_fast) fail_fast="$value" ;; + *) module_args["$key"]="$value" ;; + esac + done + + if [[ -z "$items_raw" || -z "$target_module" ]]; then + echo "${tr[missing_args]:-❌ [loop] Faltan argumentos obligatorios: items=... module=...}" + return 1 + fi + + IFS=',' read -r -a items <<< "$items_raw" + IFS=',' read -r -a secondary <<< "$secondary_raw" + + for item in "${items[@]}"; do + if [[ "$item" == *:* ]]; then + item_key="${item%%:*}" + item_value="${item#*:}" + else + item_key="$item" + item_value="" + fi + + if [[ -n "$secondary_raw" ]]; then + for sec in "${secondary[@]}"; do + run_module "$host" "$target_module" "$item" "$item_key" "$item_value" "$sec" module_args || { + echo "$(render_msg "${tr[fail_secondary]}" "item=$item" "secondary=$sec")" + [[ "$fail_fast" == "true" ]] && return 1 + } + done + else + run_module "$host" "$target_module" "$item" "$item_key" "$item_value" "" module_args || { + echo "$(render_msg "${tr[fail_item]}" "item=$item")" + [[ "$fail_fast" == "true" ]] && return 1 + } + fi + done +} + +run_module() { + local host="$1" + local module="$2" + local item="$3" + local item_key="$4" + local item_value="$5" + local secondary_item="$6" + declare -n args_ref="$7" + + local call_args=() + for key in "${!args_ref[@]}"; do + value="${args_ref[$key]}" + value="${value//\{\{item\}\}/$item}" + value="${value//\{\{item_key\}\}/$item_key}" + value="${value//\{\{item_value\}\}/$item_value}" + value="${value//\{\{secondary_item\}\}/$secondary_item}" + call_args+=("$key=$value") + done + + echo "🔁 [loop] → $module con item='$item' secondary='$secondary_item'" + + local PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + local MODULE_PATH="" + local SEARCH_PATHS=("$PROJECT_ROOT/core/modules" "$PROJECT_ROOT/user_modules" "$PROJECT_ROOT/community_modules") + for search_dir in "${SEARCH_PATHS[@]}"; do + while IFS= read -r -d '' candidate; do + [[ "$(basename "$candidate")" == "${module}.sh" ]] && MODULE_PATH="$candidate" && break 2 + done < <(find "$search_dir" -type f -name "${module}.sh" -print0) + done + + if [[ -z "$MODULE_PATH" ]]; then + echo "$(render_msg "${tr[module_not_found]}" "module=$module")" + return 1 + fi + + source "$MODULE_PATH" + ! declare -f "${module}_task" > /dev/null && echo "$(render_msg "${tr[task_not_found]}" "module=$module")" && return 1 + + "${module}_task" "$host" "${call_args[@]}" +} + +check_dependencies_loop() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/loop.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in echo tee; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [loop] Dependencias disponibles.}" + return 0 +} diff --git a/core/modules/loop.tr.en b/core/modules/loop.tr.en new file mode 100644 index 0000000..d0de2dd --- /dev/null +++ b/core/modules/loop.tr.en @@ -0,0 +1,7 @@ +missing_args=❌ [loop] Missing required arguments: items=... module=... +fail_item=⚠️ [loop] Iteration failed with '{item}' +fail_secondary=⚠️ [loop] Iteration failed with '{item}' and '{secondary}' +module_not_found=❌ [loop] Module '{module}' not found +task_not_found=❌ [loop] Function '{module}_task' not found +missing_deps=❌ [loop] Missing dependencies: {cmds} +deps_ok=✅ [loop] Dependencies available. diff --git a/core/modules/loop.tr.es b/core/modules/loop.tr.es new file mode 100644 index 0000000..aaf85ec --- /dev/null +++ b/core/modules/loop.tr.es @@ -0,0 +1,7 @@ +missing_args=❌ [loop] Faltan argumentos obligatorios: items=... module=... +fail_item=⚠️ [loop] Falló la iteración con '{item}' +fail_secondary=⚠️ [loop] Falló la iteración con '{item}' y '{secondary}' +module_not_found=❌ [loop] Módulo '{module}' no encontrado +task_not_found=❌ [loop] Función '{module}_task' no encontrada +missing_deps=❌ [loop] Dependencias faltantes: {cmds} +deps_ok=✅ [loop] Dependencias disponibles. diff --git a/core/modules/openssl.sh b/core/modules/openssl.sh new file mode 100644 index 0000000..d872dca --- /dev/null +++ b/core/modules/openssl.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# Module: openssl +# Description: Gestiona certificados y claves con OpenSSL (convertir, inspeccionar, instalar como CA) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: openssl, sudo, bash + +openssl_task() { + local host="$1"; shift + check_dependencies_openssl || return 1 + + local state="" src="" dest="" format="" password="" alias="" trust_path="" become="false" + for arg in "$@"; do + case "$arg" in + state=*) state="${arg#state=}" ;; + src=*) src="${arg#src=}" ;; + dest=*) dest="${arg#dest=}" ;; + format=*) format="${arg#format=}" ;; + password=*) password="${arg#password=}" ;; + alias=*) alias="${arg#alias=}" ;; + trust_path=*) trust_path="${arg#trust_path=}" ;; + become=*) become="${arg#become=}" ;; + esac + done + + local sudo_cmd="" + [[ "$become" == "true" ]] && sudo_cmd="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/openssl.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$state" in + convert) + if [[ -z "$src" || -z "$dest" || -z "$format" ]]; then + echo "${tr[missing_convert]:-❌ [openssl] Faltan argumentos para conversión: src, dest, format}" + return 1 + fi + if [[ ! -f "$src" ]]; then + echo "$(render_msg "${tr[src_not_found]}" "src=$src")" + return 1 + fi + echo "$(render_msg "${tr[converting]}" "src=$src" "format=$format")" + + case "$format" in + pem) + $sudo_cmd openssl pkcs12 -in "$src" -out "$dest" -nodes -password pass:"$password" && \ + echo "$(render_msg "${tr[converted]}" "dest=$dest")" + ;; + pfx) + $sudo_cmd openssl pkcs12 -export -out "$dest" -inkey "$src" -in "$src" -password pass:"$password" && \ + echo "$(render_msg "${tr[converted]}" "dest=$dest")" + ;; + key) + $sudo_cmd openssl pkey -in "$src" -out "$dest" && \ + echo "$(render_msg "${tr[key_extracted]}" "dest=$dest")" + ;; + cer) + $sudo_cmd openssl x509 -in "$src" -out "$dest" -outform DER && \ + echo "$(render_msg "${tr[cer_converted]}" "dest=$dest")" + ;; + *) + echo "$(render_msg "${tr[unsupported_format]}" "format=$format")" + return 1 + ;; + esac + ;; + + inspect) + if [[ -z "$src" || ! -f "$src" ]]; then + echo "$(render_msg "${tr[missing_inspect]}" "src=$src")" + return 1 + fi + echo "$(render_msg "${tr[inspecting]}" "src=$src")" + $sudo_cmd openssl x509 -in "$src" -noout -text | grep -E 'Subject:|Issuer:|Not Before:|Not After :|Fingerprint' || echo "${tr[inspect_fail]:-⚠️ [openssl] No se pudo extraer información}" + ;; + + trust) + if [[ -z "$src" || -z "$alias" || -z "$trust_path" ]]; then + echo "${tr[missing_trust]:-❌ [openssl] Faltan argumentos para instalación como CA: src, alias, trust_path}" + return 1 + fi + if [[ ! -f "$src" ]]; then + echo "$(render_msg "${tr[src_not_found]}" "src=$src")" + return 1 + fi + echo "$(render_msg "${tr[trusting]}" "alias=$alias")" + $sudo_cmd cp "$src" "$trust_path/$alias.crt" && \ + $sudo_cmd update-ca-certificates && \ + echo "${tr[trusted]:-✅ [openssl] Certificado instalado y CA actualizada}" + ;; + + untrust) + if [[ -z "$alias" || -z "$trust_path" ]]; then + echo "${tr[missing_untrust]:-❌ [openssl] Faltan argumentos para eliminación: alias, trust_path}" + return 1 + fi + local cert_path="$trust_path/$alias.crt" + if [[ ! -f "$cert_path" ]]; then + echo "$(render_msg "${tr[untrust_not_found]}" "alias=$alias" "trust_path=$trust_path")" + return 0 + fi + echo "$(render_msg "${tr[untrusting]}" "alias=$alias")" + $sudo_cmd rm -f "$cert_path" && \ + $sudo_cmd update-ca-certificates && \ + echo "${tr[untrusted]:-✅ [openssl] Certificado eliminado y CA actualizada}" + ;; + + *) + echo "$(render_msg "${tr[unknown_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_openssl() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/openssl.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in openssl sudo; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [openssl] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/openssl.tr.en b/core/modules/openssl.tr.en new file mode 100644 index 0000000..a0caffb --- /dev/null +++ b/core/modules/openssl.tr.en @@ -0,0 +1,20 @@ +missing_convert=❌ [openssl] Missing arguments for conversion: src, dest, format +missing_inspect=❌ [openssl] File not found for inspection: {src} +missing_trust=❌ [openssl] Missing arguments for CA installation: src, alias, trust_path +missing_untrust=❌ [openssl] Missing arguments for removal: alias, trust_path +src_not_found=❌ [openssl] Input file not found: {src} +converting=🔐 [openssl] Converting {src} to format {format}... +converted=✅ [openssl] Conversion completed: {dest} +key_extracted=✅ [openssl] Key extracted: {dest} +cer_converted=✅ [openssl] Certificate converted to .cer: {dest} +unsupported_format=❌ [openssl] Unsupported destination format: {format} +inspecting=🔍 [openssl] Inspecting certificate: {src} +inspect_fail=⚠️ [openssl] Failed to extract certificate information +trusting=🏛️ [openssl] Installing certificate '{alias}' as trusted CA... +trusted=✅ [openssl] Certificate installed and CA updated +untrust_not_found=⚠️ [openssl] Certificate '{alias}' not found in {trust_path} +untrusting=🧹 [openssl] Removing certificate '{alias}' from trusted CAs... +untrusted=✅ [openssl] Certificate removed and CA updated +unknown_state=❌ [openssl] Unknown state: '{state}'. Use 'convert', 'inspect', 'trust' or 'untrust' +missing_deps=❌ [openssl] Missing dependencies: {cmds} +deps_ok=✅ [openssl] All dependencies are available diff --git a/core/modules/openssl.tr.es b/core/modules/openssl.tr.es new file mode 100644 index 0000000..0b86cc0 --- /dev/null +++ b/core/modules/openssl.tr.es @@ -0,0 +1,20 @@ +missing_convert=❌ [openssl] Faltan argumentos para conversión: src, dest, format +missing_inspect=❌ [openssl] Archivo no encontrado para inspección: {src} +missing_trust=❌ [openssl] Faltan argumentos para instalación como CA: src, alias, trust_path +missing_untrust=❌ [openssl] Faltan argumentos para eliminación: alias, trust_path +src_not_found=❌ [openssl] Archivo no encontrado: {src} +converting=🔐 [openssl] Convirtiendo {src} a formato {format}... +converted=✅ [openssl] Conversión completada: {dest} +key_extracted=✅ [openssl] Clave extraída: {dest} +cer_converted=✅ [openssl] Certificado convertido a .cer: {dest} +unsupported_format=❌ [openssl] Formato de destino no soportado: {format} +inspecting=🔍 [openssl] Inspeccionando certificado: {src} +inspect_fail=⚠️ [openssl] No se pudo extraer información +trusting=🏛️ [openssl] Instalando certificado '{alias}' como CA... +trusted=✅ [openssl] Certificado instalado y CA actualizada +untrust_not_found=⚠️ [openssl] Certificado '{alias}' no encontrado en {trust_path} +untrusting=🧹 [openssl] Eliminando certificado '{alias}' de CA confiables... +untrusted=✅ [openssl] Certificado eliminado y CA actualizada +unknown_state=❌ [openssl] Estado desconocido: '{state}'. Usa 'convert', 'inspect', 'trust' o 'untrust' +missing_deps=❌ [openssl] Dependencias faltantes: {cmds} +deps_ok=✅ [openssl] Todas las dependencias están disponibles diff --git a/core/modules/package.sh b/core/modules/package.sh new file mode 100644 index 0000000..7e41161 --- /dev/null +++ b/core/modules/package.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Module: package +# Description: Instala, actualiza o elimina paquetes .deb/.rpm y permite actualizar el sistema +# License: GPLv3 +# Author: Luis GuLo +# Version: 2.2.0 +# Dependencies: ssh + +package_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local name="${args[name]:-}" + local state="${args[state]:-present}" + local become="${args[become]:-false}" + local update_type="${args[update_type]:-full}" # full | security + + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/package.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + fi + + echo "$(render_msg "${tr[start]}" "state=$state" "name=${name:-<sistema>}")" + + local pkg_mgr + pkg_mgr=$(ssh "$host" "command -v apt-get || command -v apt || command -v dnf || command -v yum") + + if [ -z "$pkg_mgr" ]; then + echo "${tr[no_pkg_mgr]:-❌ [package] No se detectó gestor de paquetes compatible en el host.}" + return 1 + fi + + case "$pkg_mgr" in + *apt*) + if [ "$state" = "system-update" ]; then + system_update_apt "$host" "$prefix" + else + package_apt "$host" "$name" "$state" "$prefix" + fi + ;; + *yum*|*dnf*) + if [ "$state" = "system-update" ]; then + system_update_rpm "$host" "$prefix" "$update_type" + else + package_rpm "$host" "$name" "$state" "$prefix" + fi + ;; + *) + echo "$(render_msg "${tr[unsupported_mgr]}" "mgr=$pkg_mgr")" + return 1 + ;; + esac +} + +package_apt() { + local host="$1" + local name="$2" + local state="$3" + local prefix="$4" + + local check_cmd="dpkg -s '$name' &> /dev/null" + local install_cmd="$prefix apt-get update && $prefix apt-get install -y '$name'" + local remove_cmd="$prefix apt-get remove -y '$name'" + local upgrade_cmd="$prefix apt-get update && $prefix apt-get install --only-upgrade -y '$name'" + + case "$state" in + present) ssh "$host" "$check_cmd || $install_cmd" ;; + absent) ssh "$host" "$check_cmd && $remove_cmd" ;; + latest) ssh "$host" "$check_cmd && $upgrade_cmd || $install_cmd" ;; + *) echo "$(render_msg "${tr[unsupported_state_apt]}" "state=$state")"; return 1 ;; + esac +} + +package_rpm() { + local host="$1" + local name="$2" + local state="$3" + local prefix="$4" + + local check_cmd="rpm -q '$name' &> /dev/null" + local install_cmd="$prefix yum install -y '$name' || $prefix dnf install -y '$name'" + local remove_cmd="$prefix yum remove -y '$name' || $prefix dnf remove -y '$name'" + local upgrade_cmd="$prefix yum update -y '$name' || $prefix dnf upgrade -y '$name'" + + case "$state" in + present) ssh "$host" "$check_cmd || $install_cmd" ;; + absent) ssh "$host" "$check_cmd && $remove_cmd" ;; + latest) ssh "$host" "$check_cmd && $upgrade_cmd || $install_cmd" ;; + *) echo "$(render_msg "${tr[unsupported_state_rpm]}" "state=$state")"; return 1 ;; + esac +} + +system_update_apt() { + local host="$1" + local prefix="$2" + echo "${tr[update_apt]:-🔄 [package] Actualización completa del sistema (.deb)}" + ssh "$host" "$prefix apt-get update && $prefix apt-get upgrade -y" +} + +system_update_rpm() { + local host="$1" + local prefix="$2" + local update_type="$3" + + if [ "$update_type" = "security" ]; then + echo "${tr[update_rpm_security]:-🔐 [package] Actualización de seguridad (.rpm)}" + ssh "$host" "$prefix dnf update --security -y || $prefix yum update --security -y" + else + echo "${tr[update_rpm_full]:-🔄 [package] Actualización completa del sistema (.rpm)}" + ssh "$host" "$prefix dnf upgrade --refresh -y || $prefix yum update -y" + fi +} + +check_dependencies_package() { + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [package] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [package] ssh disponible.}" + return 0 +} diff --git a/core/modules/package.tr.en b/core/modules/package.tr.en new file mode 100644 index 0000000..baae9c5 --- /dev/null +++ b/core/modules/package.tr.en @@ -0,0 +1,10 @@ +start=📦 [package] State: {state} | Package: {name} | Manager: detecting... +no_pkg_mgr=❌ [package] No compatible package manager detected on host. +unsupported_mgr=❌ [package] Manager '{mgr}' not supported. +unsupported_state_apt=❌ [package] State '{state}' not supported for APT. +unsupported_state_rpm=❌ [package] State '{state}' not supported for RPM. +update_apt=🔄 [package] Full system update (.deb) +update_rpm_security=🔐 [package] Security update (.rpm) +update_rpm_full=🔄 [package] Full system update (.rpm) +missing_deps=❌ [package] ssh is not available. +deps_ok=✅ [package] ssh is available. diff --git a/core/modules/package.tr.es b/core/modules/package.tr.es new file mode 100644 index 0000000..ed7c4fa --- /dev/null +++ b/core/modules/package.tr.es @@ -0,0 +1,10 @@ +start=📦 [package] Estado: {state} | Paquete: {name} | Gestor: detectando... +no_pkg_mgr=❌ [package] No se detectó gestor de paquetes compatible en el host. +unsupported_mgr=❌ [package] Gestor '{mgr}' no soportado. +unsupported_state_apt=❌ [package] Estado '{state}' no soportado para APT. +unsupported_state_rpm=❌ [package] Estado '{state}' no soportado para RPM. +update_apt=🔄 [package] Actualización completa del sistema (.deb) +update_rpm_security=🔐 [package] Actualización de seguridad (.rpm) +update_rpm_full=🔄 [package] Actualización completa del sistema (.rpm) +missing_deps=❌ [package] ssh no está disponible. +deps_ok=✅ [package] ssh disponible. diff --git a/core/modules/ping.sh b/core/modules/ping.sh new file mode 100644 index 0000000..79b72c6 --- /dev/null +++ b/core/modules/ping.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Module: ping +# Description: Verifica conectividad desde el host remoto hacia un destino específico +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2 +# Dependencies: ping, ssh + +ping_task() { + local host="$1"; shift + declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local count="${args[count]:-2}" + local timeout="${args[timeout]:-3}" + local target="${args[target]:-127.0.0.1}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ping.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + else + echo "⚠️ [ping] Archivo de traducción no encontrado: $trfile" + fi + + echo "$(render_msg "${tr[start]}" "host=$host" "target=$target")" + + if ssh "$host" "$prefix ping -c $count -W $timeout $target &>/dev/null"; then + echo " $(render_msg "${tr[success]}" "host=$host" "target=$target")" + return 0 + else + echo " $(render_msg "${tr[fail]}" "host=$host" "target=$target")" + return 1 + fi +} + +check_dependencies_ping() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ping.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null || ! command -v ping &> /dev/null; then + echo " ${tr[missing_deps]:-❌ [ping] ssh o ping no están disponibles.}" + return 1 + fi + echo " ${tr[deps_ok]:-✅ [ping] ssh y ping disponibles.}" + return 0 +} diff --git a/core/modules/ping.tr.en b/core/modules/ping.tr.en new file mode 100644 index 0000000..1faf4fe --- /dev/null +++ b/core/modules/ping.tr.en @@ -0,0 +1,5 @@ +start=📡 [ping] Testing connectivity from {host} to {target}... +success=✅ [ping] {host} can reach {target} +fail=❌ [ping] {host} cannot reach {target} +missing_deps=❌ [ping] ssh or ping are not available. +deps_ok=✅ [ping] ssh and ping are available. diff --git a/core/modules/ping.tr.es b/core/modules/ping.tr.es new file mode 100644 index 0000000..37c3805 --- /dev/null +++ b/core/modules/ping.tr.es @@ -0,0 +1,5 @@ +start=📡 [ping] Probando conectividad desde {host} hacia {target}... +success=✅ [ping] {host} puede alcanzar {target} +fail=❌ [ping] {host} no puede alcanzar {target} +missing_deps=❌ [ping] ssh o ping no están disponibles. +deps_ok=✅ [ping] ssh y ping disponibles. diff --git a/core/modules/replace.sh b/core/modules/replace.sh new file mode 100644 index 0000000..fb961ea --- /dev/null +++ b/core/modules/replace.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Module: replace +# Description: Reemplaza texto en archivos usando expresiones regulares +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: sed, cp, tee + +replace_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local path="${args[path]}" + local regexp="${args[regexp]}" + local replace="${args[replace]}" + local backup="${args[backup]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/replace.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ ! -f "$path" ]]; then + echo "$(render_msg "${tr[missing_file]}" "path=$path")" + return 1 + fi + + if [[ "$backup" == "true" ]]; then + cp "$path" "$path.bak" + echo "$(render_msg "${tr[backup_created]}" "path=$path")" + fi + + $prefix sed -i "s|$regexp|$replace|g" "$path" + echo "$(render_msg "${tr[replaced]}" "path=$path")" +} + +check_dependencies_replace() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/replace.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in sed cp tee; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [replace] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/replace.tr.en b/core/modules/replace.tr.en new file mode 100644 index 0000000..f4ec4ae --- /dev/null +++ b/core/modules/replace.tr.en @@ -0,0 +1,5 @@ +missing_file=❌ [replace] File not found: {path} +backup_created=📦 Backup created: {path}.bak +replaced=✅ [replace] Replacement applied to: {path} +missing_deps=❌ [replace] Missing dependencies: {cmds} +deps_ok=✅ [replace] All dependencies are available diff --git a/core/modules/replace.tr.es b/core/modules/replace.tr.es new file mode 100644 index 0000000..f6e647e --- /dev/null +++ b/core/modules/replace.tr.es @@ -0,0 +1,5 @@ +missing_file=❌ [replace] El archivo no existe: {path} +backup_created=📦 Copia de seguridad creada: {path}.bak +replaced=✅ [replace] Reemplazo aplicado en: {path} +missing_deps=❌ [replace] Dependencias faltantes: {cmds} +deps_ok=✅ [replace] Todas las dependencias están disponibles diff --git a/core/modules/run.sh b/core/modules/run.sh new file mode 100644 index 0000000..4ec4e9b --- /dev/null +++ b/core/modules/run.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Module: run +# Description: Ejecuta comandos remotos vía SSH, con soporte para vault y sudo +# License: GPLv3 +# Author: Luis GuLo +# Version: 2.0.0 +# Dependencies: ssh, core/utils/vault_utils.sh + +# Detectar raíz del proyecto si no está definida +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +# Cargar utilidades +source "$PROJECT_ROOT/core/utils/vault_utils.sh" + +run_task() { + local host="$1"; shift + declare -A args + + while [[ "$#" -gt 0 ]]; do + case "$1" in + *=*) + key="${1%%=*}" + value="${1#*=}" + args["$key"]="$value" + ;; + esac + shift + done + + local command="${args[command]}" + local become="${args[become]:-}" + local vault_key="${args[vault_key]:-}" + + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/run.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$val"; done < "$trfile" + fi + + # 🧠 Comandos que no deben ejecutarse con sudo + local safe_cmds=("echo" "true" "false" "command" "which" "exit" "test") + local first_cmd="${command%% *}" + for safe in "${safe_cmds[@]}"; do + if [[ "$first_cmd" == "$safe" ]]; then + prefix="" + break + fi + done + + # 🔁 Interpolación de variables ShFlow + for var in $(compgen -A variable | grep '^shflow_vars_'); do + key="${var#shflow_vars_}" + value="${!var}" + command="${command//\{\{ $key \}\}/$value}" + done + + echo "$(render_msg "${tr[start]}" "host=$host" "command=$command" "prefix=$prefix")" + + if [ -n "$vault_key" ]; then + local secret + secret=$(get_secret "$vault_key") || { + echo "$(render_msg "${tr[vault_fail]}" "vault_key=$vault_key")" + return 1 + } + ssh "$host" "$prefix TOKEN='$secret' $command" + else + ssh "$host" "$prefix $command" + fi +} + +check_dependencies_run() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/run.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [run] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [run] ssh disponible.}" + return 0 +} diff --git a/core/modules/run.tr.en b/core/modules/run.tr.en new file mode 100644 index 0000000..55a3f3e --- /dev/null +++ b/core/modules/run.tr.en @@ -0,0 +1,4 @@ +start=📡 [run] Executing on {host}: {prefix} {command} +vault_fail=❌ [run] Failed to retrieve secret '{vault_key}' +missing_deps=❌ [run] ssh is not available. +deps_ok=✅ [run] ssh is available. diff --git a/core/modules/run.tr.es b/core/modules/run.tr.es new file mode 100644 index 0000000..20b7f2f --- /dev/null +++ b/core/modules/run.tr.es @@ -0,0 +1,4 @@ +start=📡 [run] Ejecutando en {host}: {prefix} {command} +vault_fail=❌ [run] No se pudo obtener el secreto '{vault_key}' +missing_deps=❌ [run] ssh no está disponible. +deps_ok=✅ [run] ssh disponible. diff --git a/core/modules/service.sh b/core/modules/service.sh new file mode 100644 index 0000000..bda0cd3 --- /dev/null +++ b/core/modules/service.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Module: service +# Description: Controla servicios del sistema remoto (start, stop, restart, enable, disable) con idempotencia +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh, systemctl + +service_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local name="${args[name]}" + local state="${args[state]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/service.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$state" in + start|stop|restart|enable|disable) + echo "$(render_msg "${tr[executing]}" "state=$state" "name=$name" "host=$host")" + ssh "$host" "$prefix systemctl $state '$name'" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_service() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/service.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_ssh]:-❌ [service] ssh no está disponible.}" + return 1 + fi + echo "${tr[ssh_ok]:-✅ [service] ssh disponible.}" + + if ! command -v systemctl &> /dev/null; then + echo "${tr[missing_systemctl]:-⚠️ [service] systemctl no está disponible localmente. Se asumirá que existe en el host remoto.}" + else + echo "${tr[systemctl_ok]:-✅ [service] systemctl disponible localmente.}" + fi + return 0 +} diff --git a/core/modules/service.tr.en b/core/modules/service.tr.en new file mode 100644 index 0000000..87dc131 --- /dev/null +++ b/core/modules/service.tr.en @@ -0,0 +1,6 @@ +executing=🔧 [service] Executing '{state}' on service '{name}' at {host} +unsupported=❌ [service] State '{state}' not supported. Use start, stop, restart, enable or disable. +missing_ssh=❌ [service] ssh is not available. +ssh_ok=✅ [service] ssh is available. +missing_systemctl=⚠️ [service] systemctl is not available locally. Assuming it exists on the remote host. +systemctl_ok=✅ [service] systemctl is available locally. diff --git a/core/modules/service.tr.es b/core/modules/service.tr.es new file mode 100644 index 0000000..26c4032 --- /dev/null +++ b/core/modules/service.tr.es @@ -0,0 +1,6 @@ +executing=🔧 [service] Ejecutando '{state}' sobre el servicio '{name}' en {host} +unsupported=❌ [service] Estado '{state}' no soportado. Usa start, stop, restart, enable o disable. +missing_ssh=❌ [service] ssh no está disponible. +ssh_ok=✅ [service] ssh disponible. +missing_systemctl=⚠️ [service] systemctl no está disponible localmente. Se asumirá que existe en el host remoto. +systemctl_ok=✅ [service] systemctl disponible localmente. diff --git a/core/modules/smtp_send.sh b/core/modules/smtp_send.sh new file mode 100644 index 0000000..6961ec4 --- /dev/null +++ b/core/modules/smtp_send.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Module: smtp_send +# Description: Envía un correo de prueba usando SMTP con netcat o openssl s_client +# License: GPLv3 +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: nc o openssl, base64 + +smtp_send_task() { + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local smtp_server="${args[smtp_server]}" + local smtp_port="${args[smtp_port]:-587}" + local smtp_user="${args[smtp_user]}" + local smtp_pass="${args[smtp_pass]}" + local from="${args[from]}" + local to="${args[to]}" + local subject="${args[subject]:-Prueba desde ShFlow}" + local body="${args[body]:-Este es un correo de prueba enviado desde el módulo smtp_send.}" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/smtp_send.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ -z "$smtp_server" || -z "$smtp_user" || -z "$smtp_pass" || -z "$from" || -z "$to" ]]; then + echo "${tr[missing_args]:-❌ [smtp_send] Faltan argumentos obligatorios: smtp_server, smtp_user, smtp_pass, from, to}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "to=$to" "server=$smtp_server" "port=$smtp_port")" + + local auth_user auth_pass + auth_user=$(echo -n "$smtp_user" | base64) + auth_pass=$(echo -n "$smtp_pass" | base64) + + local smtp_script + smtp_script=$(cat <<EOF +EHLO localhost +AUTH LOGIN +$auth_user +$auth_pass +MAIL FROM:<$from> +RCPT TO:<$to> +DATA +Subject: $subject +From: $from +To: $to + +$body +. +QUIT +EOF +) + + if command -v nc &>/dev/null; then + echo "${tr[using_nc]:-🔧 Usando netcat para conexión SMTP...}" + echo "$smtp_script" | nc "$smtp_server" "$smtp_port" + elif command -v openssl &>/dev/null; then + echo "${tr[using_openssl]:-🔧 Usando openssl para conexión STARTTLS...}" + echo "$smtp_script" | openssl s_client -starttls smtp -crlf -connect "$smtp_server:$smtp_port" 2>/dev/null + else + echo "${tr[missing_tools]:-❌ [smtp_send] No se encontró ni netcat (nc) ni openssl en el sistema.}" + return 1 + fi + + echo "${tr[done]:-✅ [smtp_send] Comando ejecutado. Verifica si el correo fue recibido.}" +} + +check_dependencies_smtp_send() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/smtp_send.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if command -v nc &>/dev/null || command -v openssl &>/dev/null; then + if ! command -v base64 &>/dev/null; then + echo "${tr[missing_base64]:-❌ [smtp_send] Falta base64 en el sistema.}" + return 1 + fi + local tool=$(command -v nc &>/dev/null && echo "nc" || echo "openssl") + echo "$(render_msg "${tr[deps_ok]}" "tool=$tool")" + return 0 + else + echo "${tr[missing_tools]:-❌ [smtp_send] No se encontró ni nc ni openssl.}" + return 1 + fi +} diff --git a/core/modules/smtp_send.tr.en b/core/modules/smtp_send.tr.en new file mode 100644 index 0000000..f99ba99 --- /dev/null +++ b/core/modules/smtp_send.tr.en @@ -0,0 +1,8 @@ +missing_args=❌ [smtp_send] Missing required arguments: smtp_server, smtp_user, smtp_pass, from, to +start=📡 [smtp_send] Preparing to send to {to} via {server}:{port}... +using_nc=🔧 Using netcat for SMTP connection... +using_openssl=🔧 Using openssl for STARTTLS connection... +missing_tools=❌ [smtp_send] Neither netcat (nc) nor openssl found on system. +missing_base64=❌ [smtp_send] base64 is missing on the system. +deps_ok=✅ [smtp_send] Tools available: {tool}, base64 +done=✅ [smtp_send] Command executed. Check if the email was received. diff --git a/core/modules/smtp_send.tr.es b/core/modules/smtp_send.tr.es new file mode 100644 index 0000000..4fa0b99 --- /dev/null +++ b/core/modules/smtp_send.tr.es @@ -0,0 +1,8 @@ +missing_args=❌ [smtp_send] Faltan argumentos obligatorios: smtp_server, smtp_user, smtp_pass, from, to +start=📡 [smtp_send] Preparando envío a {to} vía {server}:{port}... +using_nc=🔧 Usando netcat para conexión SMTP... +using_openssl=🔧 Usando openssl para conexión STARTTLS... +missing_tools=❌ [smtp_send] No se encontró ni netcat (nc) ni openssl en el sistema. +missing_base64=❌ [smtp_send] Falta base64 en el sistema. +deps_ok=✅ [smtp_send] Herramientas disponibles: {tool}, base64 +done=✅ [smtp_send] Comando ejecutado. Verifica si el correo fue recibido. diff --git a/core/modules/template.sh b/core/modules/template.sh new file mode 100644 index 0000000..887a47b --- /dev/null +++ b/core/modules/template.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Module: template +# Description: Genera archivos a partir de plantillas con variables {{var}}, bucles, includes y delimitadores configurables +# Author: Luis GuLo +# Version: 0.4.0 +# Dependencies: bash, sed, tee, grep, cat + +# 🧭 Detección de raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +TEMPLATE_DIR="$PROJECT_ROOT/core/templates" + +template_task() { + local host="$1"; shift + declare -A args + local start_delim="{{" end_delim="}}" strict="false" + declare -A vars + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/template.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # Parsear argumentos + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + case "$key" in + src) src="$value" ;; + dest) dest="$value" ;; + become) become="$value" ;; + delimiters) + start_delim="${value%% *}" + end_delim="${value#* }" + ;; + strict) strict="$value" ;; + *) vars["$key"]="$value" ;; + esac + done + + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + local template_path="$TEMPLATE_DIR/$src" + [[ ! -f "$template_path" ]] && echo "$(render_msg "${tr[missing_template]}" "path=$template_path")" && return 1 + + local rendered="" + local line loop_active="false" loop_key="" loop_buffer=() + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ${start_delim}[[:space:]]*include[[:space:]]*\"([^\"]+)\"[[:space:]]*${end_delim} ]]; then + include_file="${BASH_REMATCH[1]}" + include_path="$TEMPLATE_DIR/$include_file" + [[ -f "$include_path" ]] && rendered+=$(cat "$include_path")$'\n' + continue + fi + + if [[ "$line" =~ ^#LOOP[[:space:]]+([a-zA-Z0-9_]+)$ ]]; then + loop_active="true" + loop_key="${BASH_REMATCH[1]}" + loop_buffer=() + continue + fi + + if [[ "$line" == "#ENDLOOP" ]]; then + loop_active="false" + IFS=',' read -r -a items <<< "${vars[$loop_key]}" + for item in "${items[@]}"; do + for loop_line in "${loop_buffer[@]}"; do + rendered+=$(replace_vars "$loop_line" "$item" "$start_delim" "$end_delim")$'\n' + done + done + continue + fi + + if [[ "$loop_active" == "true" ]]; then + loop_buffer+=("$line") + continue + fi + + rendered+=$(replace_vars "$line" "" "$start_delim" "$end_delim")$'\n' + done < "$template_path" + + if [[ "$strict" == "true" ]]; then + missing=$(echo "$rendered" | grep -o "${start_delim}[^${end_delim}]*${end_delim}" | sort -u) + if [[ -n "$missing" ]]; then + echo "${tr[missing_vars]:-❌ [template] Variables no definidas:}" + echo "$missing" + return 1 + fi + fi + + echo "$rendered" | $prefix tee "$dest" > /dev/null + echo "$(render_msg "${tr[generated]}" "dest=$dest")" +} + +replace_vars() { + local line="$1" + local item="$2" + local start_delim="$3" + local end_delim="$4" + for key in "${!vars[@]}"; do + line="${line//${start_delim}${key}${end_delim}/${vars[$key]}}" + done + [[ -n "$item" ]] && line="${line//${start_delim}item${end_delim}/$item}" + echo "$line" +} + +check_dependencies_template() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/template.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in sed tee grep cat; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [template] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/template.tr.en b/core/modules/template.tr.en new file mode 100644 index 0000000..8c985d8 --- /dev/null +++ b/core/modules/template.tr.en @@ -0,0 +1,5 @@ +missing_template=❌ [template] Template not found: {path} +missing_vars=❌ [template] Undefined variables: +generated=✅ [template] File generated: {dest} +missing_deps=❌ [template] Missing dependencies: {cmds} +deps_ok=✅ [template] All dependencies are available diff --git a/core/modules/template.tr.es b/core/modules/template.tr.es new file mode 100644 index 0000000..f9bdff4 --- /dev/null +++ b/core/modules/template.tr.es @@ -0,0 +1,5 @@ +missing_template=❌ [template] Plantilla no encontrada: {path} +missing_vars=❌ [template] Variables no definidas: +generated=✅ [template] Archivo generado: {dest} +missing_deps=❌ [template] Dependencias faltantes: {cmds} +deps_ok=✅ [template] Todas las dependencias están disponibles diff --git a/core/modules/user.tr.en b/core/modules/user.tr.en new file mode 100644 index 0000000..9e1d177 --- /dev/null +++ b/core/modules/user.tr.en @@ -0,0 +1,15 @@ +priv_required=❌ [users] Privileges required to manage users. Use 'become: true'. +missing_username=❌ [users] Missing required parameter 'username' +enter_create=🔧 [users] Entering create +enter_modify=🔧 [users] Entering modify +enter_absent=🔧 [users] Entering absent +exists=✅ [users] User '{username}' already exists +not_exists=⚠️ [users] User '{username}' does not exist +group_create=🔧 [users] Creating group '{groups}' +created=✅ [users] User '{username}' created +modified=✅ [users] User '{username}' modified +deleted=✅ [users] User '{username}' deleted +already_deleted=✅ [users] User '{username}' already deleted +unsupported_state=❌ [users] State '{state}' not supported. Use create, modify or absent. +missing_deps=❌ [users] Missing dependencies: {cmds} +deps_ok=✅ [users] All dependencies are present diff --git a/core/modules/user.tr.es b/core/modules/user.tr.es new file mode 100644 index 0000000..1820c74 --- /dev/null +++ b/core/modules/user.tr.es @@ -0,0 +1,15 @@ +priv_required=❌ [users] Se requieren privilegios para gestionar usuarios. Usa 'become: true'. +missing_username=❌ [users] Falta el parámetro obligatorio 'username' +enter_create=🔧 [users] Entrando en create +enter_modify=🔧 [users] Entrando en modify +enter_absent=🔧 [users] Entrando en absent +exists=✅ [users] Usuario '{username}' ya existe +not_exists=⚠️ [users] Usuario '{username}' no existe +group_create=🔧 [users] Creando grupo '{groups}' +created=✅ [users] Usuario '{username}' creado +modified=✅ [users] Usuario '{username}' modificado +deleted=✅ [users] Usuario '{username}' eliminado +already_deleted=✅ [users] Usuario '{username}' ya eliminado +unsupported_state=❌ [users] Estado '{state}' no soportado. Usa create, modify o absent. +missing_deps=❌ [users] Dependencias faltantes: {cmds} +deps_ok=✅ [users] Todas las dependencias están presentes diff --git a/core/modules/users.sh b/core/modules/users.sh new file mode 100644 index 0000000..e7306e2 --- /dev/null +++ b/core/modules/users.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Module: users +# Description: Gestiona usuarios del sistema (crear, modificar, eliminar) +# Author: Luis GuLo +# Version: 1.4.0 +# Dependencies: id, useradd, usermod, userdel, groupadd, sudo + +users_task() { + local host="$1"; shift + declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local username="${args[username]}" + local home="${args[home]:-/home/$username}" + local shell="${args[shell]:-/bin/bash}" + local groups="${args[groups]:-}" + local state="${args[state]:-create}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/users.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # 🛡️ Validación + if [[ "$become" != "true" && "$EUID" -ne 0 ]]; then + echo "${tr[priv_required]:-❌ [users] Se requieren privilegios para gestionar usuarios. Usa 'become: true'.}" + return 1 + fi + + if [[ -z "$username" ]]; then + echo "${tr[missing_username]:-❌ [users] Falta el parámetro obligatorio 'username'}" + return 1 + fi + + case "$state" in + create) + echo "${tr[enter_create]:-🔧 [users] Entrando en create}" + if id "$username" &>/dev/null; then + echo "$(render_msg "${tr[exists]}" "username=$username")" + return 0 + fi + if [[ -n "$groups" && "$groups" != "$username" ]]; then + if ! getent group "$groups" &>/dev/null; then + echo "$(render_msg "${tr[group_create]}" "groups=$groups")" + $prefix groupadd "$groups" + fi + fi + local cmd="$prefix useradd -m \"$username\" -s \"$shell\" -d \"$home\"" + [[ -n "$groups" ]] && cmd="$cmd -G \"$groups\"" + eval "$cmd" && echo "$(render_msg "${tr[created]}" "username=$username")" + ;; + modify) + echo "${tr[enter_modify]:-🔧 [users] Entrando en modify}" + if ! id "$username" &>/dev/null; then + echo "$(render_msg "${tr[not_exists]}" "username=$username")" + return 1 + fi + local cmd="$prefix usermod \"$username\"" + [[ -n "$shell" ]] && cmd="$cmd -s \"$shell\"" + [[ -n "$home" ]] && cmd="$cmd -d \"$home\"" + [[ -n "$groups" ]] && cmd="$cmd -G \"$groups\"" + eval "$cmd" && echo "$(render_msg "${tr[modified]}" "username=$username")" + ;; + absent) + echo "${tr[enter_absent]:-🔧 [users] Entrando en absent}" + if ! id "$username" &>/dev/null; then + echo "$(render_msg "${tr[already_deleted]}" "username=$username")" + return 0 + fi + eval "$prefix userdel -r \"$username\"" && echo "$(render_msg "${tr[deleted]}" "username=$username")" + ;; + *) + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_users() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/users.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in id sudo; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing+=("$cmd") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [users] Todas las dependencias están presentes}" + return 0 +} diff --git a/core/modules/vault-remote.sh b/core/modules/vault-remote.sh new file mode 100644 index 0000000..4b3a1c6 --- /dev/null +++ b/core/modules/vault-remote.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Module: vault-remote +# Description: Sincroniza secretos cifrados entre vault local y remoto +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: ssh, scp, gpg + +VAULT_DIR="core/vault" + +vault_remote_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local action="${args[action]}" + local key="${args[key]}" + local remote_path="${args[remote_path]:-/tmp/shflow_vault}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/vault-remote.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$action" in + push) + if [ ! -f "$VAULT_DIR/$key.gpg" ]; then + echo "$(render_msg "${tr[missing_local]}" "key=$key")" + return 1 + fi + scp "$VAULT_DIR/$key.gpg" "$host:$remote_path/$key.gpg" + ssh "$host" "$prefix mkdir -p '$remote_path'" + echo "$(render_msg "${tr[pushed]}" "key=$key" "host=$host" "path=$remote_path")" + ;; + pull) + ssh "$host" "$prefix test -f '$remote_path/$key.gpg'" || { + echo "$(render_msg "${tr[missing_remote]}" "key=$key")" + return 1 + } + scp "$host:$remote_path/$key.gpg" "$VAULT_DIR/$key.gpg" + echo "$(render_msg "${tr[pulled]}" "key=$key" "host=$host")" + ;; + sync) + ssh "$host" "$prefix mkdir -p '$remote_path'" + scp "$VAULT_DIR/"*.gpg "$host:$remote_path/" + echo "$(render_msg "${tr[synced]}" "host=$host" "path=$remote_path")" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +check_dependencies_vault_remote() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/vault-remote.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + for cmd in ssh scp gpg; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_deps]}" "cmd=$cmd")" + return 1 + fi + done + echo "${tr[deps_ok]:-✅ [vault-remote] Dependencias disponibles.}" + return 0 +} diff --git a/core/modules/vault-remote.tr.en b/core/modules/vault-remote.tr.en new file mode 100644 index 0000000..cb0b856 --- /dev/null +++ b/core/modules/vault-remote.tr.en @@ -0,0 +1,8 @@ +missing_local=❌ [vault-remote] Secret '{key}' does not exist locally. +missing_remote=❌ [vault-remote] Secret '{key}' does not exist on remote host. +pushed=📤 Secret '{key}' sent to {host}:{path} +pulled=📥 Secret '{key}' retrieved from {host} +synced=🔄 Vault synchronized with {host}:{path} +unsupported=❌ [vault-remote] Action '{action}' not supported. +missing_deps=❌ [vault-remote] Command '{cmd}' not available. +deps_ok=✅ [vault-remote] Dependencies available. diff --git a/core/modules/vault-remote.tr.es b/core/modules/vault-remote.tr.es new file mode 100644 index 0000000..79a6106 --- /dev/null +++ b/core/modules/vault-remote.tr.es @@ -0,0 +1,8 @@ +missing_local=❌ [vault-remote] Secreto '{key}' no existe localmente. +missing_remote=❌ [vault-remote] Secreto '{key}' no existe en el host remoto. +pushed=📤 Secreto '{key}' enviado a {host}:{path} +pulled=📥 Secreto '{key}' recuperado desde {host} +synced=🔄 Vault sincronizado con {host}:{path} +unsupported=❌ [vault-remote] Acción '{action}' no soportada. +missing_deps=❌ [vault-remote] Comando '{cmd}' no disponible. +deps_ok=✅ [vault-remote] Dependencias disponibles. diff --git a/core/modules/wait.sh b/core/modules/wait.sh new file mode 100644 index 0000000..409dfbe --- /dev/null +++ b/core/modules/wait.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Module: wait +# Description: Pausa la ejecución durante un número de segundos (soporta decimales) +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: sleep + +wait_task() { + local host="$1"; shift + declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local seconds="${args[seconds]:-1}" + + # Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/wait.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + fi + + if ! [[ "$seconds" =~ ^[0-9]+([.][0-9]+)?$ ]]; then + echo "${tr[invalid]:-❌ [wait] El parámetro 'seconds' debe ser un número válido (entero o decimal)}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "seconds=$seconds")" + sleep "$seconds" + echo "${tr[done]:-✅ [wait] Pausa completada}" +} + +check_dependencies_wait() { + command -v sleep &>/dev/null || { + echo "${tr[missing_deps]:-❌ [wait] El comando 'sleep' no está disponible}" + return 1 + } + echo "${tr[deps_ok]:-✅ [wait] Dependencias OK}" + return 0 +} diff --git a/core/modules/wait.tr.en b/core/modules/wait.tr.en new file mode 100644 index 0000000..a603364 --- /dev/null +++ b/core/modules/wait.tr.en @@ -0,0 +1,5 @@ +invalid=❌ [wait] The 'seconds' parameter must be a valid number (integer or decimal) +start=⏳ [wait] Waiting for {seconds} seconds... +done=✅ [wait] Pause completed +missing_deps=❌ [wait] The 'sleep' command is not available +deps_ok=✅ [wait] Dependencies OK diff --git a/core/modules/wait.tr.es b/core/modules/wait.tr.es new file mode 100644 index 0000000..764ed3f --- /dev/null +++ b/core/modules/wait.tr.es @@ -0,0 +1,5 @@ +invalid=❌ [wait] El parámetro 'seconds' debe ser un número válido (entero o decimal) +start=⏳ [wait] Esperando {seconds} segundos... +done=✅ [wait] Pausa completada +missing_deps=❌ [wait] El comando 'sleep' no está disponible +deps_ok=✅ [wait] Dependencias OK diff --git a/core/templates/.gitignore b/core/templates/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/core/templates/.gitignore diff --git a/core/utils/eg.sh b/core/utils/eg.sh new file mode 100755 index 0000000..551c9d0 --- /dev/null +++ b/core/utils/eg.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +Qes="8J+lmiBIYXMgZW5jb250cmFkbyBlbCBodWV2byBkZSBwYXNjdWEuIMKhQnVlbiBvam8h·4pyoIE5vIHRvZG9zIGxvcyBzY3JpcHRzIHRpZW5lbiBhbG1hLi4uIHBlcm8gZXN0ZSBzw60u·8J+noCBFbCBtZWpvciBidWcgZXMgZWwgcXVlIG51bmNhIGV4aXN0acOzLg==·8J+QoyBTaEZsb3cgdGUgc2FsdWRhIGRlc2RlIGxhcyBzb21icmFzLg==·8J+TnCBMYSBhdXRvbWF0aXphY2nDs24gdGFtYmnDqW4gdGllbmUgcG9lc8OtYS4=" +Qen="8J+lmiBZb3UgZm91bmQgdGhlIEVhc3RlciBlZ2cuIFNoYXJwIGV5ZSE=·4pyoIE5vdCBhbGwgc2NyaXB0cyBoYXZlIHNvdWwuLi4gYnV0IHRoaXMgb25lIGRvZXMu·8J+noCBUaGUgYmVzdCBidWcgaXMgdGhlIG9uZSB0aGF0IG5ldmVyIGV4aXN0ZWQu·8J+QoyBTaEZsb3cgZ3JlZXRzIHlvdSBmcm9tIHRoZSBzaGFkb3dzLg==·8J+TnCBBdXRvbWF0aW9uIGhhcyBwb2V0cnkgdG9vLg==" +vhs=([0]="448b55f2" [1]="c9f66247" [2]="154f020f" [3]="0e931208" [4]="d2e2fa57") + +sheg() { + local P="$1" ; P=$((P+1)) + echo "――――――" + echo "$Qes" |awk -F '·' -v p="$P" '{print $p}' | base64 -d; echo + echo "$Qen" |awk -F '·' -v p="$P" '{print $p}' | base64 -d; echo + echo "――――――" +} + +main() { + [[ $# -lt 1 ]] && return 0 + local input="$1" ; local hsh + hsh=$(echo -n "$input" | md5sum | cut -c1-8) + for n in "${!vhs[@]}"; do + if [[ "$hsh" == "${vhs[$n]}" ]]; then + sheg "$n" ; break + fi + done + return 0 +} + +main "$@" diff --git a/core/utils/module-docgen.sh b/core/utils/module-docgen.sh new file mode 100755 index 0000000..f5461cd --- /dev/null +++ b/core/utils/module-docgen.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# ShFlow Module Documentation Generator +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.4.0 + +set -euo pipefail + +# 🧭 Detección de la raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +OUTPUT="$PROJECT_ROOT/docs/modules-list.md" +MODULE_DIRS=("$PROJECT_ROOT/core/modules" "$PROJECT_ROOT/user_modules" "$PROJECT_ROOT/community_modules") + +export SHFLOW_LANG="${SHFLOW_LANG:-es}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" + +trfile="$PROJECT_ROOT/core/utils/module-docgen.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +# 📝 Encabezado del documento +{ + echo "${tr[title]:-# 🧩 Módulos en ShFlow}" + echo "" + echo "**$(render_msg "${tr[generated]}" "date=$(date '+%Y-%m-%d %H:%M:%S')")**" + echo "" + echo "| ${tr[col_module]:-Módulo} | ${tr[col_desc]:-Descripción} | ${tr[col_type]:-Tipo} | ${tr[col_author]:-Autor} | ${tr[col_version]:-Versión} | ${tr[col_deps]:-Dependencias} |" + echo "|--------|-------------|------|-------|---------|--------------|" +} > "$OUTPUT" + +# 🔁 Procesar módulos +for dir in "${MODULE_DIRS[@]}"; do + [ -d "$dir" ] || continue + TYPE=$(echo "$dir" | sed "s#$PROJECT_ROOT/##g") + while IFS= read -r -d '' file; do + name=$(basename "$file" .sh) + desc=$(grep -E '^# Description:' "$file" | sed 's/^# Description:[[:space:]]*//') + author=$(grep -E '^# Author:' "$file" | sed 's/^# Author:[[:space:]]*//') + version=$(grep -E '^# Version:' "$file" | sed 's/^# Version:[[:space:]]*//') + deps=$(grep -E '^# Dependencies:' "$file" | sed 's/^# Dependencies:[[:space:]]*//') + + # Asegurar valor minimo + name=${name:-""} + desc=${desc:-""} + author=${author:-""} + version=${version:-""} + deps=${deps:-""} + + [[ -z "$name" ]] && continue + + echo "| $name | $desc | $TYPE | $author | $version | $deps |" >> "$OUTPUT" + done < <(find "$dir" -type f -name "*.sh" -print0) +done + +# 📌 Pie de página +{ + echo "" + echo "${tr[footer]:-_Para actualizar esta tabla, ejecuta: \`module-docgen\`_}" +} >> "$OUTPUT" + +echo "$(render_msg "${tr[done]}" "path=$OUTPUT")" diff --git a/core/utils/module-docgen.tr.en b/core/utils/module-docgen.tr.en new file mode 100644 index 0000000..644b2c3 --- /dev/null +++ b/core/utils/module-docgen.tr.en @@ -0,0 +1,10 @@ +title=# 🧩 Modules in ShFlow +generated=Automatically generated on {date} +col_module=Module +col_desc=Description +col_type=Type +col_author=Author +col_version=Version +col_deps=Dependencies +footer=_To update this table, run: `module-docgen`_ +done=✅ Documentation generated at {path} diff --git a/core/utils/module-docgen.tr.es b/core/utils/module-docgen.tr.es new file mode 100644 index 0000000..14b68fa --- /dev/null +++ b/core/utils/module-docgen.tr.es @@ -0,0 +1,10 @@ +title=# 🧩 Módulos en ShFlow +generated=Generado automáticamente el {date} +col_module=Módulo +col_desc=Descripción +col_type=Tipo +col_author=Autor +col_version=Versión +col_deps=Dependencias +footer=_Para actualizar esta tabla, ejecuta: `module-docgen`_ +done=✅ Documentación generada en {path} diff --git a/core/utils/module-template.sh b/core/utils/module-template.sh new file mode 100755 index 0000000..a38624b --- /dev/null +++ b/core/utils/module-template.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# ShFlow Module Template Generator +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +MODULE_NAME="${1:-}" +MODULE_DIR="$PROJECT_ROOT/core/modules" +MODULE_FILE="$MODULE_DIR/$MODULE_NAME.sh" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/core/utils/module-template.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +# 🧪 Validar entrada +if [[ -z "$MODULE_NAME" ]]; then + echo "${tr[usage]:-❌ Uso: module-template.sh <nombre_modulo>}" + exit 1 +fi + +if [[ -f "$MODULE_FILE" ]]; then + echo "$(render_msg "${tr[exists]}" "name=$MODULE_NAME" "dir=$MODULE_DIR")" + exit 1 +fi + +mkdir -p "$MODULE_DIR" + +cat > "$MODULE_FILE" <<EOF +#!/bin/bash +# Module: $MODULE_NAME +# Description: <descripción breve del módulo> +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.0 +# Dependencies: <comandos externos si aplica> + +${MODULE_NAME}_task() { + local host="\$1"; shift + declare -A args + for arg in "\$@"; do + key="\${arg%%=*}" + value="\${arg#*=}" + args["\$key"]="\$value" + done + + echo "🚧 Ejecutando módulo '$MODULE_NAME' en \$host" + # Aquí va la lógica principal +} + +check_dependencies_$MODULE_NAME() { + # Verifica herramientas necesarias + for cmd in <comando1> <comando2>; do + if ! command -v "\$cmd" &> /dev/null; then + echo " ❌ [$MODULE_NAME] Falta: \$cmd" + return 1 + fi + done + echo " ✅ [$MODULE_NAME] Dependencias OK" + return 0 +} +EOF + +chmod +x "$MODULE_FILE" +echo "$(render_msg "${tr[created]}" "name=$MODULE_NAME" "path=$MODULE_FILE")" diff --git a/core/utils/module-template.tr.en b/core/utils/module-template.tr.en new file mode 100644 index 0000000..bc948d7 --- /dev/null +++ b/core/utils/module-template.tr.en @@ -0,0 +1,3 @@ +usage=❌ Usage: module-template.sh <module_name> +exists=⚠️ Module '{name}' already exists in {dir} +created=✅ Module '{name}' created at {path} diff --git a/core/utils/module-template.tr.es b/core/utils/module-template.tr.es new file mode 100644 index 0000000..4dc6e63 --- /dev/null +++ b/core/utils/module-template.tr.es @@ -0,0 +1,3 @@ +usage=❌ Uso: module-template.sh <nombre_modulo> +exists=⚠️ El módulo '{name}' ya existe en {dir} +created=✅ Módulo '{name}' creado en {path} diff --git a/core/utils/shflow-check.sh b/core/utils/shflow-check.sh new file mode 100755 index 0000000..130eca0 --- /dev/null +++ b/core/utils/shflow-check.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# ShFlow Environment Checker +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.5.0 + +set -e + +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +MODULE_PATHS=( + "$PROJECT_ROOT/core/modules" + "$PROJECT_ROOT/user_modules" + "$PROJECT_ROOT/community_modules" +) + +# 🧩 Cargar funciones comunes si no están disponibles +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +GLOBAL_TOOLS=("bash" "ssh" "scp" "git" "curl" "jq" "yq" "gpg") + +REQUIRED_PATHS=( + "$PROJECT_ROOT/core/modules" + "$PROJECT_ROOT/core/utils" + "$PROJECT_ROOT/core/inventory" + "$PROJECT_ROOT/examples" + "$PROJECT_ROOT/user_modules" + "$PROJECT_ROOT/community_modules" + "$PROJECT_ROOT/shflow.sh" + "$PROJECT_ROOT/vault.sh" +) + +# 🌐 Cargar traducciones +lang="${shflow_vars[language]:-es}" +trfile="$PROJECT_ROOT/core/utils/shflow-check.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +check_global_tools() { + echo "${tr[tools_header]:-🔍 Verificando herramientas globales...}" + local missing=0 + for tool in "${GLOBAL_TOOLS[@]}"; do + if ! command -v "$tool" &> /dev/null; then + echo "$(render_msg "${tr[tool_missing]}" "tool=$tool")" + missing=1 + else + echo "$(render_msg "${tr[tool_ok]}" "tool=$tool")" + fi + done + return $missing +} + +check_structure() { + echo "" + echo "${tr[structure_header]:-📁 Verificando estructura de ShFlow...}" + local missing=0 + for path in "${REQUIRED_PATHS[@]}"; do + if [ ! -e "$path" ]; then + echo "$(render_msg "${tr[path_missing]}" "path=$path")" + missing=1 + else + echo "$(render_msg "${tr[path_ok]}" "path=$path")" + fi + done + return $missing +} + +load_and_check_modules() { + echo "" + echo "${tr[modules_header]:-🔍 Verificando módulos ShFlow...}" + for dir in "${MODULE_PATHS[@]}"; do + [ -d "$dir" ] || continue + while IFS= read -r -d '' mod; do + source "$mod" + done < <(find "$dir" -type f -name "*.sh" -print0) + done + + for func in $(declare -F | awk '{print $3}' | grep '^check_dependencies_'); do + echo "" + echo "$(render_msg "${tr[checking_func]}" "func=$func")" + $func || echo "$(render_msg "${tr[func_warn]}" "func=$func")" + done +} + +main() { + echo "${tr[title]:-🧪 ShFlow Environment Check}" + echo "${tr[separator]:-=============================}" + + check_global_tools + check_structure + load_and_check_modules + + echo "" + echo "${tr[done]:-✅ Verificación completada.}" +} + +main "$@" diff --git a/core/utils/shflow-check.tr.en b/core/utils/shflow-check.tr.en new file mode 100644 index 0000000..6430ffd --- /dev/null +++ b/core/utils/shflow-check.tr.en @@ -0,0 +1,12 @@ +title=🧪 ShFlow Environment Check +separator=_____________________________ +tools_header=🔍 Checking global tools... +tool_missing=❌ {tool} not found +tool_ok=✅ {tool} available +structure_header=📁 Checking ShFlow structure... +path_missing=❌ Missing: {path} +path_ok=✅ Found: {path} +modules_header=🔍 Checking ShFlow modules... +checking_func=🔧 Running {func}... +func_warn=⚠️ Incomplete dependencies in {func} +done=✅ Check completed. diff --git a/core/utils/shflow-check.tr.es b/core/utils/shflow-check.tr.es new file mode 100644 index 0000000..983f318 --- /dev/null +++ b/core/utils/shflow-check.tr.es @@ -0,0 +1,12 @@ +title=🧪 ShFlow Environment Check +separator=_____________________________ +tools_header=🔍 Verificando herramientas globales... +tool_missing=❌ {tool} no encontrado +tool_ok=✅ {tool} disponible +structure_header=📁 Verificando estructura de ShFlow... +path_missing=❌ Falta: {path} +path_ok=✅ Encontrado: {path} +modules_header=🔍 Verificando módulos ShFlow... +checking_func=🔧 Ejecutando {func}... +func_warn=⚠️ Dependencias incompletas en {func} +done=✅ Verificación completada. diff --git a/core/utils/shflow-doc.sh b/core/utils/shflow-doc.sh new file mode 100755 index 0000000..861f1b9 --- /dev/null +++ b/core/utils/shflow-doc.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# ShFlow Doc Generator +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 + +set -e + +# ───────────────────────────────────────────── +# 🧭 Detección de la raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +# 🧩 Cargar funciones comunes si no están disponibles +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${shflow_vars[language]:-es}" +trfile="$PROJECT_ROOT/core/utils/shflow-doc.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +MODULE_PATHS=( + "$PROJECT_ROOT/core/modules" + "$PROJECT_ROOT/user_modules" + "$PROJECT_ROOT/community_modules" +) + +extract_metadata() { + local file="$1" + local module desc author version deps + + module=$(grep -m1 '^# Module:' "$file" | cut -d':' -f2- | xargs) + desc=$(grep -m1 '^# Description:' "$file" | cut -d':' -f2- | xargs) + author=$(grep -m1 '^# Author:' "$file" | cut -d':' -f2- | xargs) + version=$(grep -m1 '^# Version:' "$file" | cut -d':' -f2- | xargs) + deps=$(grep -m1 '^# Dependencies:' "$file" | cut -d':' -f2- | xargs) + + echo "$(render_msg "${tr[module]}" "name=$module")" + echo "$(render_msg "${tr[desc]}" "desc=$desc")" + echo "$(render_msg "${tr[author]}" "author=$author")" + echo "$(render_msg "${tr[version]}" "version=$version")" + echo "$(render_msg "${tr[deps]}" "deps=$deps")" + echo "${tr[separator]:- ————————————————————————}" +} + +main() { + echo "${tr[title]:-📚 ShFlow Modules Documentation}" + echo "${tr[separator_line]:-=================================}" + + for dir in "${MODULE_PATHS[@]}"; do + [ -d "$dir" ] || continue + ROUTE=$(echo "$dir" | sed "s#$PROJECT_ROOT/##g") + echo -e "\n$(render_msg "${tr[section]}" "type=$ROUTE")" + while IFS= read -r -d '' file; do + extract_metadata "$file" + done < <(find "$dir" -type f -name "*.sh" -print0) + done +} + +main "$@" diff --git a/core/utils/shflow-doc.tr.en b/core/utils/shflow-doc.tr.en new file mode 100644 index 0000000..5d3167e --- /dev/null +++ b/core/utils/shflow-doc.tr.en @@ -0,0 +1,9 @@ +title=📚 ShFlow Modules Documentation +separator_line================================== +section=🗃️ Module Type: {type} +module= 📦 Module: {name} +desc= 🔧 Description: {desc} +author= 👤 Author: {author} +version= 📌 Version: {version} +deps= 📎 Dependencies: {deps} +separator= ———————————————————————— diff --git a/core/utils/shflow-doc.tr.es b/core/utils/shflow-doc.tr.es new file mode 100644 index 0000000..b1150f7 --- /dev/null +++ b/core/utils/shflow-doc.tr.es @@ -0,0 +1,9 @@ +title=📚 ShFlow Modules Documentation +separator_line================================== +section=🗃️ Tipo de módulo: {type} +module= 📦 Módulo: {name} +desc= 🔧 Descripción: {desc} +author= 👤 Autor: {author} +version= 📌 Versión: {version} +deps= 📎 Dependencias: {deps} +separator= ———————————————————————— diff --git a/core/utils/shflow-ssh-init.sh b/core/utils/shflow-ssh-init.sh new file mode 100755 index 0000000..26d5e70 --- /dev/null +++ b/core/utils/shflow-ssh-init.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Utility: shflow-ssh-init +# Description: Inicializa acceso SSH sin contraseña en los hosts del inventario +# Author: Luis GuLo +# Version: 0.2.0 + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml" +TIMEOUT=5 +USER="${USER:-$(whoami)}" +KEY="${KEY:-$HOME/.ssh/id_rsa.pub}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +export SHFLOW_LANG="${SHFLOW_LANG:-es}" +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" + +trfile="$PROJECT_ROOT/core/utils/shflow-ssh-init.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +echo "$(render_msg "${tr[start]}" "user=$USER")" +echo "$(render_msg "${tr[inventory]}" "path=$INVENTORY")" +echo "$(render_msg "${tr[key]}" "key=$KEY")" +echo "" + +# 🧪 Validar dependencias +for cmd in yq ssh ssh-copy-id; do + if ! command -v "$cmd" &>/dev/null; then + echo "$(render_msg "${tr[missing_dep]}" "cmd=$cmd")" + exit 1 + fi +done + +# 🔁 Extraer hosts +HOSTS=() +HOSTS_RAW=$(yq ".all.hosts | keys | .[]" "$INVENTORY") +[ -z "$HOSTS_RAW" ] && echo "${tr[no_hosts]:-❌ No se encontraron hosts en el inventario.}" && exit 1 + +while IFS= read -r line; do + HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')") # Eliminar comillas +done <<< "$HOSTS_RAW" + +# 🔍 Evaluar cada host +for host in "${HOSTS[@]}"; do + IP=$(yq -r ".all.hosts.\"$host\".ansible_host" "$INVENTORY") + [[ "$IP" == "null" || -z "$IP" ]] && echo "$(render_msg "${tr[missing_ip]}" "host=$host")" && continue + + echo "$(render_msg "${tr[checking]}" "host=$host" "ip=$IP")" + + if ssh -o BatchMode=yes -o ConnectTimeout=$TIMEOUT "$USER@$IP" 'true' &>/dev/null; then + echo "${tr[skip]:- 🔁 Inicialización SSH no es necesaria}" + continue + fi + + echo "$(render_msg "${tr[copying]}" "user=$USER" "ip=$IP")" + if ssh-copy-id -i "$KEY" "$USER@$IP"; then + echo "${tr[success]:- ✅ Clave pública instalada correctamente}" + else + echo "${tr[fail]:- ❌ Fallo al instalar clave pública}" + fi + + echo "" +done + +echo "${tr[done]:-✅ Proceso de inicialización SSH completado}" diff --git a/core/utils/shflow-ssh-init.tr.en b/core/utils/shflow-ssh-init.tr.en new file mode 100644 index 0000000..1005b42 --- /dev/null +++ b/core/utils/shflow-ssh-init.tr.en @@ -0,0 +1,12 @@ +start=🔐 Initializing passwordless SSH access for user: {user} +inventory=📁 Inventory: {path} +key=🔑 Public key: {key} +missing_dep=❌ Requires '{cmd}' installed on the system +no_hosts=❌ No hosts found in inventory. +missing_ip=⚠️ Host '{host}' has no ansible_host defined +checking=🖥️ Host: {host} ({ip}) +skip= 🔁 SSH initialization not needed +copying= 🚀 Running ssh-copy-id for {user}@{ip} +success= ✅ Public key installed successfully +fail= ❌ Failed to install public key +done=✅ SSH initialization process completed diff --git a/core/utils/shflow-ssh-init.tr.es b/core/utils/shflow-ssh-init.tr.es new file mode 100644 index 0000000..c693c8f --- /dev/null +++ b/core/utils/shflow-ssh-init.tr.es @@ -0,0 +1,12 @@ +start=🔐 Inicializando acceso SSH sin contraseña para usuario: {user} +inventory=📁 Inventario: {path} +key=🔑 Clave pública: {key} +missing_dep=❌ Requiere '{cmd}' instalado en el sistema +no_hosts=❌ No se encontraron hosts en el inventario. +missing_ip=⚠️ Host '{host}' sin ansible_host definido +checking=🖥️ Host: {host} ({ip}) +skip= 🔁 Inicialización SSH no es necesaria +copying= 🚀 Ejecutando ssh-copy-id para {user}@{ip} +success= ✅ Clave pública instalada correctamente +fail= ❌ Fallo al instalar clave pública +done=✅ Proceso de inicialización SSH completado diff --git a/core/utils/shflow-trust.sh b/core/utils/shflow-trust.sh new file mode 100755 index 0000000..bc27d94 --- /dev/null +++ b/core/utils/shflow-trust.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Utility: shflow-trust +# Description: Evalúa acceso SSH y privilegios sudo para cada host del inventario +# Author: Luis GuLo +# Version: 0.4.0 + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml" +REPORT="$PROJECT_ROOT/core/inventory/trust_report.yaml" +TIMEOUT=5 +USER="${USER:-$(whoami)}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +export SHFLOW_LANG="${SHFLOW_LANG:-es}" +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" + +trfile="$PROJECT_ROOT/core/utils/shflow-trust.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +echo "$(render_msg "${tr[start]}" "user=$USER")" +echo "$(render_msg "${tr[inventory]}" "path=$INVENTORY")" +echo "$(render_msg "${tr[report]}" "path=$REPORT")" +echo "" + +# 🧪 Validar dependencia yq +if ! command -v yq &>/dev/null; then + echo "$(render_msg "${tr[missing_dep]}" "cmd=yq")" + exit 1 +fi + +# 🧹 Regenerar informe +{ + echo "# $(render_msg "${tr[report_title]}")" + echo "# $(render_msg "${tr[report_date]}" "date=$(date)")" + echo "" +} > "$REPORT" + +# 🔁 Extraer hosts +HOSTS=() +HOSTS_RAW=$(yq ".all.hosts | keys | .[]" "$INVENTORY") +[ -z "$HOSTS_RAW" ] && echo "${tr[no_hosts]:-❌ No se encontraron hosts en el inventario.}" && exit 1 + +while IFS= read -r line; do + HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')") # Eliminar comillas +done <<< "$HOSTS_RAW" + +# 🔍 Evaluar cada host +for host in "${HOSTS[@]}"; do + IP=$(yq -r ".all.hosts.\"$host\".ansible_host" "$INVENTORY") + [[ "$IP" == "null" || -z "$IP" ]] && echo "$(render_msg "${tr[missing_ip]}" "host=$host")" && continue + + echo "$(render_msg "${tr[checking]}" "host=$host" "ip=$IP")" + + if ssh -o BatchMode=yes -o ConnectTimeout=$TIMEOUT "$USER@$IP" 'echo ok' &>/dev/null; then + echo "${tr[ssh_ok]:- ✅ SSH: ok}" + SSH_STATUS="ok" + + if ssh -o BatchMode=yes "$USER@$IP" 'sudo -n true' &>/dev/null; then + echo "${tr[sudo_ok]:- ✅ SUDO: ok}" + SUDO_STATUS="ok" + else + echo "${tr[sudo_pw]:- ⚠️ SUDO: requiere contraseña o no permitido}" + SUDO_STATUS="password_required" + fi + else + echo "${tr[ssh_fail]:- ❌ SSH: fallo de conexión}" + SSH_STATUS="failed" + SUDO_STATUS="unknown" + fi + + { + echo "$host:" + echo " ip: $IP" + echo " ssh: $SSH_STATUS" + echo " sudo: $SUDO_STATUS" + echo "" + } >> "$REPORT" +done + +echo "$(render_msg "${tr[done]}" "path=$REPORT")" diff --git a/core/utils/shflow-trust.tr.en b/core/utils/shflow-trust.tr.en new file mode 100644 index 0000000..5e1209f --- /dev/null +++ b/core/utils/shflow-trust.tr.en @@ -0,0 +1,14 @@ +start=🔍 Evaluating SSH and sudo trust for user: {user} +inventory=📁 Inventory: {path} +report=📄 Report: {path} +missing_dep=❌ Requires '{cmd}' (Go version) to process YAML inventory +report_title=Trust report generated by shflow-trust +report_date=Date: {date} +no_hosts=❌ No hosts found in inventory. +missing_ip=⚠️ Host '{host}' has no ansible_host defined +checking=🖥️ Host: {host} ({ip}) +ssh_ok= ✅ SSH: ok +ssh_fail= ❌ SSH: connection failed +sudo_ok= ✅ SUDO: ok +sudo_pw= ⚠️ SUDO: requires password or not allowed +done=✅ Report completed: {path} diff --git a/core/utils/shflow-trust.tr.es b/core/utils/shflow-trust.tr.es new file mode 100644 index 0000000..9e930a9 --- /dev/null +++ b/core/utils/shflow-trust.tr.es @@ -0,0 +1,14 @@ +start=🔍 Evaluando confianza SSH y sudo para usuario: {user} +inventory=📁 Inventario: {path} +report=📄 Informe: {path} +missing_dep=❌ Requiere '{cmd}' versión Go para procesar el inventario YAML +report_title=Informe de confianza generado por shflow-trust +report_date=Fecha: {date} +no_hosts=❌ No se encontraron hosts en el inventario. +missing_ip=⚠️ Host '{host}' sin ansible_host definido +checking=🖥️ Host: {host} ({ip}) +ssh_ok= ✅ SSH: ok +ssh_fail= ❌ SSH: fallo de conexión +sudo_ok= ✅ SUDO: ok +sudo_pw= ⚠️ SUDO: requiere contraseña o no permitido +done=✅ Informe completado: {path} diff --git a/core/utils/vault-init.sh b/core/utils/vault-init.sh new file mode 100755 index 0000000..3055a10 --- /dev/null +++ b/core/utils/vault-init.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# ShFlow Vault Initializer +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.3.0 +# Dependencies: gpg + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +VAULT_DIR="$PROJECT_ROOT/core/vault" +VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}" +VAULT_PUBKEY="${VAULT_PUBKEY:-$HOME/.shflow.pub}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/core/utils/vault-init.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +generate_key() { + echo "${tr[gen_key]:-🔐 Generando nueva clave simétrica...}" + head -c 64 /dev/urandom | base64 > "$VAULT_KEY" + chmod 600 "$VAULT_KEY" + echo "$(render_msg "${tr[key_created]}" "path=$VAULT_KEY")" +} + +rotate_key() { + echo "${tr[rotate_start]:-🔄 Rotando clave y re-cifrando secretos...}" + local OLD_KEY="$VAULT_KEY.old" + + cp "$VAULT_KEY" "$OLD_KEY" + generate_key + + for file in "$VAULT_DIR"/*.gpg; do + key=$(basename "$file" .gpg) + echo "$(render_msg "${tr[recrypt]}" "key=$key")" + gpg --quiet --batch --yes --passphrase-file "$OLD_KEY" -d "$file" | \ + gpg --symmetric --batch --yes --passphrase-file "$VAULT_KEY" -o "$VAULT_DIR/$key.gpg.new" + mv "$VAULT_DIR/$key.gpg.new" "$VAULT_DIR/$key.gpg" + done + + echo "$(render_msg "${tr[rotate_done]}" "path=$OLD_KEY")" +} + +status() { + echo "${tr[status_title]:-📊 Estado del Vault}" + echo "-------------------" + echo "$(render_msg "${tr[sym_key]}" "status=$( [ -f "$VAULT_KEY" ] && echo "${tr[present]}" || echo "${tr[absent]}")")" + echo "$(render_msg "${tr[pub_key]}" "status=$( [ -f "$VAULT_PUBKEY" ] && echo "${tr[present]}" || echo "${tr[absent]}")")" + echo "$(render_msg "${tr[vault_path]}" "path=$VAULT_DIR")" + echo "$(render_msg "${tr[secrets]}" "count=$(ls "$VAULT_DIR"/*.gpg 2>/dev/null | wc -l)")" + echo "$(render_msg "${tr[last_mod]}" "date=$(date -r "$VAULT_KEY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)")" +} + +generate_pubkey() { + echo "${tr[asym_start]:-🔐 Configurando cifrado asimétrico...}" + echo "${tr[asym_hint]:-⚠️ Se requiere que la clave pública esté exportada previamente.}" + echo " gpg --export -a 'usuario@dominio' > $VAULT_PUBKEY" + if [ -f "$VAULT_PUBKEY" ]; then + echo "$(render_msg "${tr[pubkey_found]}" "path=$VAULT_PUBKEY")" + else + echo "${tr[pubkey_missing]:-❌ Clave pública no encontrada. Exporta primero con GPG.}" + exit 1 + fi +} + +main() { + case "${1:-}" in + --rotate) + [ -f "$VAULT_KEY" ] || { echo "${tr[no_key]:-❌ No existe clave actual. Ejecuta sin --rotate primero.}"; exit 1; } + rotate_key + ;; + --status) + status + ;; + --asymmetric) + generate_pubkey + ;; + *) + if [ -f "$VAULT_KEY" ]; then + echo "$(render_msg "${tr[key_exists]}" "path=$VAULT_KEY")" + else + generate_key + fi + ;; + esac +} + +main "$@" diff --git a/core/utils/vault-init.tr.en b/core/utils/vault-init.tr.en new file mode 100644 index 0000000..8004348 --- /dev/null +++ b/core/utils/vault-init.tr.en @@ -0,0 +1,19 @@ +gen_key=🔐 Generating new symmetric key... +key_created=✅ Key created at {path} +rotate_start=🔄 Rotating key and re-encrypting secrets... +recrypt=🔁 Re-encrypting '{key}'... +rotate_done=✅ Rotation completed. Old key saved at {path} +status_title=📊 Vault Status +sym_key=🔐 Symmetric key: {status} +pub_key=🔐 Public key: {status} +present=✅ present +absent=❌ absent +vault_path=📁 Vault path: {path} +secrets=📦 Secrets: {count} +last_mod=🕒 Last modified: {date} +asym_start=🔐 Setting up asymmetric encryption... +asym_hint=⚠️ Public key must be exported beforehand. +pubkey_found=✅ Public key found at {path} +pubkey_missing=❌ Public key not found. Export it first using GPG. +no_key=❌ No current key found. Run without --rotate first. +key_exists=🔐 Key already exists at {path} diff --git a/core/utils/vault-init.tr.es b/core/utils/vault-init.tr.es new file mode 100644 index 0000000..8ed9644 --- /dev/null +++ b/core/utils/vault-init.tr.es @@ -0,0 +1,19 @@ +gen_key=🔐 Generando nueva clave simétrica... +key_created=✅ Clave creada en {path} +rotate_start=🔄 Rotando clave y re-cifrando secretos... +recrypt=🔁 Re-cifrando '{key}'... +rotate_done=✅ Rotación completada. Clave antigua guardada en {path} +status_title=📊 Estado del Vault +sym_key=🔐 Clave simétrica: {status} +pub_key=🔐 Clave pública: {status} +present=✅ presente +absent=❌ ausente +vault_path=📁 Ruta del vault: {path} +secrets=📦 Secretos: {count} +last_mod=🕒 Última modificación: {date} +asym_start=🔐 Configurando cifrado asimétrico... +asym_hint=⚠️ Se requiere que la clave pública esté exportada previamente. +pubkey_found=✅ Clave pública detectada en {path} +pubkey_missing=❌ Clave pública no encontrada. Exporta primero con GPG. +no_key=❌ No existe clave actual. Ejecuta sin --rotate primero. +key_exists=🔐 Clave ya existe en {path} diff --git a/core/utils/vault_utils.sh b/core/utils/vault_utils.sh new file mode 100755 index 0000000..702265f --- /dev/null +++ b/core/utils/vault_utils.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Utility: vault_utils +# Description: Funciones para acceso seguro al vault de ShFlow +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: gpg + +VAULT_DIR="${VAULT_DIR:-core/vault}" +VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}" + +# 🧩 Cargar render_msg si no está disponible +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/core/utils/vault_utils.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +get_secret() { + local key="$1" + local value + + if [ ! -f "$VAULT_DIR/$key.gpg" ]; then + echo "$(render_msg "${tr[missing]}" "key=$key" "dir=$VAULT_DIR")" + return 1 + fi + + value=$(gpg --quiet --batch --yes --passphrase-file "$VAULT_KEY" -d "$VAULT_DIR/$key.gpg" 2>/dev/null) + if [ $? -ne 0 ]; then + echo "$(render_msg "${tr[decrypt_fail]}" "key=$key")" + return 1 + fi + + echo "$value" +} diff --git a/core/utils/vault_utils.tr.en b/core/utils/vault_utils.tr.en new file mode 100644 index 0000000..413952e --- /dev/null +++ b/core/utils/vault_utils.tr.en @@ -0,0 +1,2 @@ +missing=❌ [vault] Secret '{key}' not found in {dir} +decrypt_fail=❌ [vault] Failed to decrypt '{key}' diff --git a/core/utils/vault_utils.tr.es b/core/utils/vault_utils.tr.es new file mode 100644 index 0000000..f81ee15 --- /dev/null +++ b/core/utils/vault_utils.tr.es @@ -0,0 +1,2 @@ +missing=❌ [vault] Secreto '{key}' no encontrado en {dir} +decrypt_fail=❌ [vault] Error al descifrar '{key}' diff --git a/core/vault/.gitignore b/core/vault/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/core/vault/.gitignore |
