update
This commit is contained in:
+414
-8
@@ -54,6 +54,65 @@ TAPM_PULSE_ARCH() {
|
||||
esac
|
||||
}
|
||||
|
||||
TAPM_PULSE_BOOTSTRAP_TOKEN_FROM_OUTPUT() {
|
||||
local output="${1:-}"
|
||||
|
||||
BOOTSTRAP_OUTPUT="$output" python3 -c '
|
||||
import os, re
|
||||
match = re.search(r"Token:\s*([0-9a-fA-F]{48})(?:\s|$)", os.environ["BOOTSTRAP_OUTPUT"])
|
||||
if not match:
|
||||
raise SystemExit(1)
|
||||
print(match.group(1).lower())
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
TAPM_PULSE_TOKEN_FROM_RESPONSE() {
|
||||
local response="${1:-}"
|
||||
|
||||
TOKEN_RESPONSE="$response" python3 -c '
|
||||
import json, os, re
|
||||
try:
|
||||
token = json.loads(os.environ["TOKEN_RESPONSE"]).get("token", "")
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
raise SystemExit(1)
|
||||
if not isinstance(token, str) or not re.fullmatch(r"[0-9a-fA-F]+", token):
|
||||
raise SystemExit(1)
|
||||
print(token.lower())
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
TAPM_PULSE_ONLINE_NODES_FROM_JSON() {
|
||||
local nodes_json="${1:-[]}"
|
||||
|
||||
NODES_JSON="$nodes_json" python3 -c '
|
||||
import json, os
|
||||
try:
|
||||
nodes = json.loads(os.environ["NODES_JSON"])
|
||||
except (TypeError, ValueError):
|
||||
raise SystemExit(1)
|
||||
for item in sorted(nodes, key=lambda value: str(value.get("node", ""))):
|
||||
node = str(item.get("node", "")).strip()
|
||||
if item.get("status") == "online" and node:
|
||||
print(node)
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
TAPM_PULSE_OFFLINE_NODES_FROM_JSON() {
|
||||
local nodes_json="${1:-[]}"
|
||||
|
||||
NODES_JSON="$nodes_json" python3 -c '
|
||||
import json, os
|
||||
try:
|
||||
nodes = json.loads(os.environ["NODES_JSON"])
|
||||
except (TypeError, ValueError):
|
||||
raise SystemExit(1)
|
||||
for item in sorted(nodes, key=lambda value: str(value.get("node", ""))):
|
||||
node = str(item.get("node", "")).strip()
|
||||
if item.get("status") != "online" and node:
|
||||
print(node)
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
TAPM_PULSE_RESOURCE_INSTALLED() {
|
||||
local resources_json="${1:-[]}"
|
||||
|
||||
@@ -202,6 +261,323 @@ TAPM_PULSE_REMOVE_PARTIAL_LXC() {
|
||||
pct destroy "$ctid" --purge 1 >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
TAPM_PULSE_CONFIGURE_SECURITY() {
|
||||
local ctid="$1"
|
||||
local pulse_url="$2"
|
||||
local temp_dir="$3"
|
||||
local username_variable="$4"
|
||||
local password_variable="$5"
|
||||
local token_variable="$6"
|
||||
local bootstrap_output bootstrap_token generated_username generated_password generated_api_token
|
||||
local request_file curl_config response security_ready='no' attempt
|
||||
|
||||
generated_username='admin'
|
||||
generated_password="Ta!9-$(openssl rand -hex 18)" || return 1
|
||||
generated_api_token="$(openssl rand -hex 32)" || return 1
|
||||
request_file="${temp_dir}/pulse-quick-setup.json"
|
||||
curl_config="${temp_dir}/pulse-quick-setup.curl"
|
||||
|
||||
bootstrap_output="$(
|
||||
pct exec "$ctid" -- env PULSE_DATA_DIR=/etc/pulse \
|
||||
/usr/local/bin/pulse bootstrap-token 2>/dev/null
|
||||
)" || {
|
||||
TAPM_PULSE_FAIL "Pulse did not provide its first-run bootstrap token."
|
||||
return 1
|
||||
}
|
||||
bootstrap_token="$(TAPM_PULSE_BOOTSTRAP_TOKEN_FROM_OUTPUT "$bootstrap_output")" || {
|
||||
unset bootstrap_output
|
||||
TAPM_PULSE_FAIL "The Pulse bootstrap-token output could not be validated."
|
||||
return 1
|
||||
}
|
||||
unset bootstrap_output
|
||||
|
||||
TAPM_PULSE_ADMIN_USER="$generated_username" \
|
||||
TAPM_PULSE_ADMIN_PASSWORD="$generated_password" \
|
||||
TAPM_PULSE_PRIMARY_TOKEN="$generated_api_token" \
|
||||
python3 -c '
|
||||
import json, os, sys
|
||||
json.dump({
|
||||
"username": os.environ["TAPM_PULSE_ADMIN_USER"],
|
||||
"password": os.environ["TAPM_PULSE_ADMIN_PASSWORD"],
|
||||
"apiToken": os.environ["TAPM_PULSE_PRIMARY_TOKEN"],
|
||||
"enableNotifications": False,
|
||||
"darkMode": False,
|
||||
"force": False,
|
||||
}, sys.stdout)
|
||||
' >"$request_file" || {
|
||||
unset bootstrap_token generated_password generated_api_token
|
||||
TAPM_PULSE_FAIL "Could not prepare the Pulse security configuration."
|
||||
return 1
|
||||
}
|
||||
chmod 0600 "$request_file" || return 1
|
||||
{
|
||||
printf 'header = "Content-Type: application/json"\n'
|
||||
printf 'header = "X-Setup-Token: %s"\n' "$bootstrap_token"
|
||||
printf 'data-binary = "@%s"\n' "$request_file"
|
||||
} >"$curl_config" || return 1
|
||||
chmod 0600 "$curl_config" || return 1
|
||||
|
||||
response="$(
|
||||
curl --fail --silent --show-error \
|
||||
--request POST \
|
||||
--config "$curl_config" \
|
||||
"${pulse_url}/api/security/quick-setup"
|
||||
)" || {
|
||||
unset bootstrap_token generated_password generated_api_token
|
||||
TAPM_PULSE_FAIL "Pulse rejected the automated first-time security setup."
|
||||
return 1
|
||||
}
|
||||
if ! SETUP_RESPONSE="$response" python3 -c '
|
||||
import json, os
|
||||
try:
|
||||
success = json.loads(os.environ["SETUP_RESPONSE"]).get("success")
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(0 if success is True else 1)
|
||||
' 2>/dev/null; then
|
||||
unset bootstrap_token generated_password generated_api_token response
|
||||
TAPM_PULSE_FAIL "Pulse did not confirm that first-time setup completed."
|
||||
return 1
|
||||
fi
|
||||
unset bootstrap_token response
|
||||
|
||||
{
|
||||
printf 'header = "X-API-Token: %s"\n' "$generated_api_token"
|
||||
} >"$curl_config" || return 1
|
||||
for (( attempt = 1; attempt <= 15; attempt++ )); do
|
||||
if curl --fail --silent --show-error \
|
||||
--connect-timeout 3 --max-time 5 \
|
||||
--config "$curl_config" \
|
||||
"${pulse_url}/api/security/status" >/dev/null 2>&1; then
|
||||
security_ready='yes'
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "$security_ready" != 'yes' ]]; then
|
||||
unset generated_password generated_api_token
|
||||
TAPM_PULSE_FAIL "The generated Pulse administrator token could not be verified."
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "$username_variable" '%s' "$generated_username"
|
||||
printf -v "$password_variable" '%s' "$generated_password"
|
||||
printf -v "$token_variable" '%s' "$generated_api_token"
|
||||
unset generated_password generated_api_token
|
||||
}
|
||||
|
||||
TAPM_PULSE_ISSUE_AGENT_TOKEN() {
|
||||
local pulse_url="$1"
|
||||
local primary_token="$2"
|
||||
local node="$3"
|
||||
local temp_dir="$4"
|
||||
local request_file="${temp_dir}/agent-${node}.json"
|
||||
local curl_config="${temp_dir}/agent-${node}.curl"
|
||||
local response
|
||||
|
||||
TAPM_PULSE_AGENT_NAME="tapm-pve-${node}" python3 -c '
|
||||
import json, os, sys
|
||||
json.dump({
|
||||
"type": "host",
|
||||
"name": os.environ["TAPM_PULSE_AGENT_NAME"],
|
||||
"enableCommands": False,
|
||||
}, sys.stdout)
|
||||
' >"$request_file" || return 1
|
||||
chmod 0600 "$request_file" || return 1
|
||||
{
|
||||
printf 'header = "Content-Type: application/json"\n'
|
||||
printf 'header = "X-API-Token: %s"\n' "$primary_token"
|
||||
printf 'data-binary = "@%s"\n' "$request_file"
|
||||
} >"$curl_config" || return 1
|
||||
chmod 0600 "$curl_config" || return 1
|
||||
|
||||
response="$(
|
||||
curl --fail --silent --show-error \
|
||||
--request POST \
|
||||
--config "$curl_config" \
|
||||
"${pulse_url}/api/agent-install-command"
|
||||
)" || return 1
|
||||
TAPM_PULSE_TOKEN_FROM_RESPONSE "$response"
|
||||
}
|
||||
|
||||
TAPM_PULSE_INSTALL_AGENT_LOCAL() {
|
||||
local pulse_url="$1"
|
||||
local token="$2"
|
||||
local token_file installer_file artifact
|
||||
local status=0
|
||||
local -a old_artifacts=(
|
||||
/usr/local/bin/pulse-agent
|
||||
/var/lib/pulse-agent
|
||||
/var/log/pulse-agent.log
|
||||
/etc/systemd/system/pulse-agent.service
|
||||
/etc/systemd/system/multi-user.target.wants/pulse-agent.service
|
||||
)
|
||||
|
||||
token_file="$(mktemp /tmp/tapm-pulse-agent-token.XXXXXX)" || return 1
|
||||
installer_file="$(mktemp /tmp/tapm-pulse-agent-installer.XXXXXX)" || {
|
||||
rm -f -- "$token_file"
|
||||
return 1
|
||||
}
|
||||
chmod 0600 "$token_file" "$installer_file" || {
|
||||
rm -f -- "$token_file" "$installer_file"
|
||||
return 1
|
||||
}
|
||||
printf '%s' "$token" >"$token_file" || {
|
||||
rm -f -- "$token_file" "$installer_file"
|
||||
return 1
|
||||
}
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "$installer_file" "${pulse_url}/install.sh" || status=$?
|
||||
if (( status == 0 )); then
|
||||
echo " Removing any previous Pulse Unified Agent installation..."
|
||||
bash "$installer_file" --uninstall --non-interactive || status=$?
|
||||
fi
|
||||
if (( status == 0 )) && systemctl is-active --quiet pulse-agent; then
|
||||
echo " The previous pulse-agent service is still active." >&2
|
||||
status=1
|
||||
fi
|
||||
if (( status == 0 )) && command -v pgrep >/dev/null 2>&1 &&
|
||||
pgrep -x pulse-agent >/dev/null 2>&1; then
|
||||
echo " A previous pulse-agent process is still running." >&2
|
||||
status=1
|
||||
fi
|
||||
if (( status == 0 )); then
|
||||
for artifact in "${old_artifacts[@]}"; do
|
||||
if [[ -e "$artifact" || -L "$artifact" ]]; then
|
||||
echo " Previous Pulse agent artifact remains: ${artifact}" >&2
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if (( status == 0 )); then
|
||||
bash "$installer_file" \
|
||||
--url "$pulse_url" \
|
||||
--token-file "$token_file" \
|
||||
--enable-proxmox \
|
||||
--proxmox-type pve \
|
||||
--non-interactive \
|
||||
--insecure || status=$?
|
||||
fi
|
||||
rm -f -- "$token_file" "$installer_file"
|
||||
(( status == 0 )) || return "$status"
|
||||
systemctl is-active --quiet pulse-agent
|
||||
}
|
||||
|
||||
TAPM_PULSE_INSTALL_AGENT_REMOTE() {
|
||||
local node="$1"
|
||||
local pulse_url="$2"
|
||||
local token="$3"
|
||||
local remote_token_file
|
||||
local -a ssh_args=(
|
||||
ssh
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout=10
|
||||
"root@${node}"
|
||||
)
|
||||
|
||||
remote_token_file="$(
|
||||
printf '%s' "$token" |
|
||||
"${ssh_args[@]}" \
|
||||
'umask 077; token_file=$(mktemp /tmp/tapm-pulse-agent-token.XXXXXX) || exit 1; cat >"$token_file" || exit 1; printf "%s\n" "$token_file"' |
|
||||
tail -1
|
||||
)" || return 1
|
||||
[[ "$remote_token_file" =~ ^/tmp/tapm-pulse-agent-token\.[A-Za-z0-9]+$ ]] ||
|
||||
return 1
|
||||
|
||||
"${ssh_args[@]}" bash -s -- "$pulse_url" "$remote_token_file" <<'TAPM_PULSE_REMOTE_AGENT'
|
||||
set -eu -o pipefail
|
||||
pulse_url="$1"
|
||||
token_file="$2"
|
||||
installer_file="$(mktemp /tmp/tapm-pulse-agent-installer.XXXXXX)"
|
||||
trap 'rm -f -- "$token_file" "$installer_file"' EXIT
|
||||
chmod 0600 "$token_file" "$installer_file"
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "$installer_file" "${pulse_url}/install.sh"
|
||||
|
||||
echo " Removing any previous Pulse Unified Agent installation..."
|
||||
bash "$installer_file" --uninstall --non-interactive
|
||||
if systemctl is-active --quiet pulse-agent; then
|
||||
echo " The previous pulse-agent service is still active." >&2
|
||||
exit 1
|
||||
fi
|
||||
if command -v pgrep >/dev/null 2>&1 &&
|
||||
pgrep -x pulse-agent >/dev/null 2>&1; then
|
||||
echo " A previous pulse-agent process is still running." >&2
|
||||
exit 1
|
||||
fi
|
||||
for artifact in \
|
||||
/usr/local/bin/pulse-agent \
|
||||
/var/lib/pulse-agent \
|
||||
/var/log/pulse-agent.log \
|
||||
/etc/systemd/system/pulse-agent.service \
|
||||
/etc/systemd/system/multi-user.target.wants/pulse-agent.service; do
|
||||
if [[ -e "$artifact" || -L "$artifact" ]]; then
|
||||
echo " Previous Pulse agent artifact remains: ${artifact}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
bash "$installer_file" \
|
||||
--url "$pulse_url" \
|
||||
--token-file "$token_file" \
|
||||
--enable-proxmox \
|
||||
--proxmox-type pve \
|
||||
--non-interactive \
|
||||
--insecure
|
||||
systemctl is-active --quiet pulse-agent
|
||||
TAPM_PULSE_REMOTE_AGENT
|
||||
}
|
||||
|
||||
TAPM_PULSE_DEPLOY_CLUSTER_AGENTS() {
|
||||
local pulse_url="$1"
|
||||
local primary_token="$2"
|
||||
local temp_dir="$3"
|
||||
local nodes_json node agent_token current_node
|
||||
local installed=0 failed=0
|
||||
|
||||
nodes_json="$(pvesh get /nodes --output-format json 2>/dev/null)" || return 1
|
||||
current_node="$(hostname -s)"
|
||||
while IFS= read -r node; do
|
||||
[[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ ]] || {
|
||||
echo -e "${idsCL[LightYellow]}Skipping invalid cluster node name '${node}'.${idsCL[Default]}"
|
||||
((failed += 1))
|
||||
continue
|
||||
}
|
||||
echo -e "${idsCL[LightCyan]}Resetting and installing the Pulse Unified Agent on ${node}...${idsCL[Default]}"
|
||||
agent_token="$(
|
||||
TAPM_PULSE_ISSUE_AGENT_TOKEN \
|
||||
"$pulse_url" "$primary_token" "$node" "$temp_dir"
|
||||
)" || {
|
||||
echo -e "${idsCL[LightRed]}Could not create an enrollment token for ${node}.${idsCL[Default]}"
|
||||
((failed += 1))
|
||||
continue
|
||||
}
|
||||
|
||||
if [[ "$node" == "$current_node" || "$node" == "$(hostname)" ]]; then
|
||||
TAPM_PULSE_INSTALL_AGENT_LOCAL "$pulse_url" "$agent_token"
|
||||
else
|
||||
TAPM_PULSE_INSTALL_AGENT_REMOTE "$node" "$pulse_url" "$agent_token"
|
||||
fi
|
||||
if (( $? == 0 )); then
|
||||
echo -e "${idsCL[Green]}Pulse Unified Agent is active on ${node}.${idsCL[Default]}"
|
||||
((installed += 1))
|
||||
else
|
||||
echo -e "${idsCL[LightRed]}Pulse Unified Agent installation failed on ${node}.${idsCL[Default]}"
|
||||
((failed += 1))
|
||||
fi
|
||||
unset agent_token
|
||||
done < <(TAPM_PULSE_ONLINE_NODES_FROM_JSON "$nodes_json")
|
||||
while IFS= read -r node; do
|
||||
[[ -n "$node" ]] || continue
|
||||
echo -e "${idsCL[LightYellow]}Pulse agent installation skipped on offline node ${node}.${idsCL[Default]}"
|
||||
((failed += 1))
|
||||
done < <(TAPM_PULSE_OFFLINE_NODES_FROM_JSON "$nodes_json")
|
||||
|
||||
TAPM_PULSE_AGENTS_INSTALLED="$installed"
|
||||
TAPM_PULSE_AGENTS_FAILED="$failed"
|
||||
(( installed > 0 || failed == 0 ))
|
||||
}
|
||||
|
||||
TAPM_DEPLOY_PULSE_LXC() {
|
||||
local release="${PULSE_RELEASE:-v6.1.1}"
|
||||
local pulse_port="${PULSE_PORT:-7655}" auto_update_flag='--disable-auto-updates'
|
||||
@@ -210,7 +586,7 @@ TAPM_DEPLOY_PULSE_LXC() {
|
||||
local arch archive_name base_url installer archive signature installer_signature
|
||||
local memory disk cores cpulimit swap onboot firewall unprivileged nameserver startup
|
||||
local network_config choice add_ha='no' auto_updates='yes' container_ip timezone temp_dir
|
||||
local default_bridge
|
||||
local default_bridge pulse_url admin_username admin_password primary_api_token
|
||||
local container_created=0
|
||||
local -a create_args=()
|
||||
|
||||
@@ -235,7 +611,7 @@ TAPM_DEPLOY_PULSE_LXC() {
|
||||
|
||||
[[ $EUID -eq 0 ]] ||
|
||||
{ TAPM_PULSE_FAIL "Run this action as root on a Proxmox VE host."; return 1; }
|
||||
for command in pct pvesm pveam pvesh ssh-keygen python3; do
|
||||
for command in pct pvesm pveam pvesh ssh-keygen python3 curl openssl ssh; do
|
||||
command -v "$command" >/dev/null 2>&1 ||
|
||||
{ TAPM_PULSE_FAIL "Required command '${command}' was not found."; return 1; }
|
||||
done
|
||||
@@ -490,6 +866,7 @@ TAPM_DEPLOY_PULSE_LXC() {
|
||||
)"
|
||||
|
||||
if [[ -n "$container_ip" ]]; then
|
||||
pulse_url="http://${container_ip}:${pulse_port}"
|
||||
echo -e "\n${idsCL[LightCyan]}Registering the Proxmox cluster with Pulse...${idsCL[Default]}"
|
||||
(
|
||||
trap 'rm -f /tmp/pulse-auto-register-request.json /tmp/pulse-auto-register-response.json' EXIT
|
||||
@@ -498,7 +875,7 @@ TAPM_DEPLOY_PULSE_LXC() {
|
||||
# shellcheck disable=SC1090
|
||||
source "$installer"
|
||||
IN_CONTAINER=false
|
||||
wait_for_pulse_ready "http://${container_ip}:${pulse_port}" 120 1 || true
|
||||
wait_for_pulse_ready "$pulse_url" 120 1 || true
|
||||
auto_register_pve_node "$ctid" "$container_ip" "$pulse_port"
|
||||
) || echo -e "${idsCL[LightYellow]}Pulse is installed, but automatic Proxmox registration did not complete.${idsCL[Default]}"
|
||||
fi
|
||||
@@ -508,6 +885,33 @@ TAPM_DEPLOY_PULSE_LXC() {
|
||||
echo -e "${idsCL[LightYellow]}Pulse is running, but it could not be added to HA.${idsCL[Default]}"
|
||||
fi
|
||||
|
||||
if [[ -z "$container_ip" ]]; then
|
||||
pct exec "$ctid" -- rm -f \
|
||||
/tmp/install.sh "/tmp/${archive_name}" "/tmp/${archive_name}.sshsig" || true
|
||||
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||
TAPM_PULSE_FAIL "Pulse is running, but its LXC address could not be determined. The LXC was kept."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "\n${idsCL[LightCyan]}Configuring Pulse administrator security...${idsCL[Default]}"
|
||||
if ! TAPM_PULSE_CONFIGURE_SECURITY \
|
||||
"$ctid" "$pulse_url" "$temp_dir" \
|
||||
admin_username admin_password primary_api_token; then
|
||||
pct exec "$ctid" -- rm -f \
|
||||
/tmp/install.sh "/tmp/${archive_name}" "/tmp/${archive_name}.sshsig" || true
|
||||
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||
echo " The Pulse LXC was kept so setup can be recovered without reinstalling it."
|
||||
echo " Run this on the Proxmox host to request a fresh setup token:"
|
||||
echo " pct exec ${ctid} -- env PULSE_DATA_DIR=/etc/pulse /usr/local/bin/pulse bootstrap-token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "\n${idsCL[LightCyan]}Deploying clean Pulse Unified Agents to cluster nodes...${idsCL[Default]}"
|
||||
TAPM_PULSE_AGENTS_INSTALLED=0
|
||||
TAPM_PULSE_AGENTS_FAILED=0
|
||||
TAPM_PULSE_DEPLOY_CLUSTER_AGENTS \
|
||||
"$pulse_url" "$primary_api_token" "$temp_dir" || true
|
||||
|
||||
pct exec "$ctid" -- rm -f \
|
||||
/tmp/install.sh "/tmp/${archive_name}" "/tmp/${archive_name}.sshsig" || true
|
||||
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||
@@ -515,9 +919,11 @@ TAPM_DEPLOY_PULSE_LXC() {
|
||||
|
||||
echo
|
||||
echo -e "${idsCL[Green]}Pulse ${release} was installed and its service is active.${idsCL[Default]}"
|
||||
if [[ -n "$container_ip" ]]; then
|
||||
echo -e " Open ${idsCL[LightCyan]}http://${container_ip}:${pulse_port}${idsCL[Default]} to finish setup."
|
||||
else
|
||||
echo " Open the Pulse LXC address on port ${pulse_port} to finish setup."
|
||||
fi
|
||||
echo -e " Open: ${idsCL[LightCyan]}${pulse_url}${idsCL[Default]}"
|
||||
echo " Username: ${admin_username}"
|
||||
echo " Password: ${admin_password}"
|
||||
echo " API token: ${primary_api_token}"
|
||||
echo " Agents: ${TAPM_PULSE_AGENTS_INSTALLED} installed, ${TAPM_PULSE_AGENTS_FAILED} failed or offline"
|
||||
echo
|
||||
echo -e "${idsCL[LightYellow]}Save the generated password and API token now; they are only shown once.${idsCL[Default]}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user