update
This commit is contained in:
@@ -16,6 +16,10 @@ interactive menu. Update availability is checked in the background and cached;
|
|||||||
updates are installed only when explicitly selected or requested with
|
updates are installed only when explicitly selected or requested with
|
||||||
`tapm update`.
|
`tapm update`.
|
||||||
|
|
||||||
|
Use `tapm V2` to switch an installed copy to the published V2 branch for
|
||||||
|
testing, and `tapm main` to switch it back. The command refuses to switch when
|
||||||
|
local changes, local-only commits, or diverged branch history would be at risk.
|
||||||
|
|
||||||
The required iDSSYS Defaults repository is handled separately: it is
|
The required iDSSYS Defaults repository is handled separately: it is
|
||||||
automatically refreshed before launch when its last successful check is more
|
automatically refreshed before launch when its last successful check is more
|
||||||
than four hours old. If the remote is unavailable, the installed copy is used.
|
than four hours old. If the remote is unavailable, the installed copy is used.
|
||||||
@@ -34,7 +38,7 @@ The menu script also supports these direct actions:
|
|||||||
```text
|
```text
|
||||||
pulse Install Pulse monitoring
|
pulse Install Pulse monitoring
|
||||||
rmm Install the ConnectWise RMM agent
|
rmm Install the ConnectWise RMM agent
|
||||||
omsa Install Dell OpenManage Server Administrator
|
omsa Install legacy Dell OMSA on supported PowerEdge x30/x40 hosts
|
||||||
glances Install Glances
|
glances Install Glances
|
||||||
acronis Install the Acronis agent
|
acronis Install the Acronis agent
|
||||||
post-install Run the ProxMenux post-install configuration
|
post-install Run the ProxMenux post-install configuration
|
||||||
@@ -66,3 +70,15 @@ portal without changing ProxMenu.
|
|||||||
|
|
||||||
The Keepalived deployment additionally requires a healthy, quorate Proxmox
|
The Keepalived deployment additionally requires a healthy, quorate Proxmox
|
||||||
cluster and passwordless root SSH between cluster nodes.
|
cluster and passwordless root SSH between cluster nodes.
|
||||||
|
|
||||||
|
The legacy Dell OMSA installer is limited to supported PowerEdge x30/x40
|
||||||
|
systems running Proxmox VE 9 on Debian 13 (Trixie), amd64.
|
||||||
|
|
||||||
|
CPU compatibility detection previews cluster-wide QEMU VM and template
|
||||||
|
changes before applying the ProxCLMC recommendation through the Proxmox CLI.
|
||||||
|
Running VMs are not restarted automatically.
|
||||||
|
|
||||||
|
Maintenance evacuation leaves HA-managed guests under Proxmox HA control.
|
||||||
|
Remaining shared-storage guests are routed to online, non-maintenance nodes
|
||||||
|
with the required storage, while local-storage guests are gracefully shut
|
||||||
|
down. HA node-affinity preferences are honored when an eligible node exists.
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
action="${1:-}"
|
action="${1:-}"
|
||||||
FOLDER='/opt/idssys/ta-proxmenu'
|
FOLDER='/opt/idssys/ta-proxmenu'
|
||||||
VERS='2026.7.25-30'
|
VERS='2026.7.25-34'
|
||||||
|
|
||||||
noupdate=' '
|
noupdate=' '
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
+669
-153
@@ -1,18 +1,21 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -u
|
set -u -o pipefail
|
||||||
|
|
||||||
# Evacuate non-HA guests after the local Proxmox node enters HA maintenance.
|
# Evacuate non-HA guests after the local Proxmox node enters HA maintenance.
|
||||||
# Guests using shared storage are migrated to one operator-selected node.
|
# HA-managed guests remain under Proxmox HA control. Non-HA guests using
|
||||||
# Guests with local storage remain on this node and are gracefully shut down.
|
# shared storage are migrated, while guests using local storage are shut down.
|
||||||
|
|
||||||
HA_WAIT_SECONDS=300
|
HA_WAIT_SECONDS=300
|
||||||
MAINTENANCE_WAIT_SECONDS=60
|
MAINTENANCE_WAIT_SECONDS=60
|
||||||
MAX_PARALLEL_MIGRATIONS=3
|
MAX_PARALLEL_MIGRATIONS=3
|
||||||
SHUTDOWN_TIMEOUT=180
|
SHUTDOWN_TIMEOUT=180
|
||||||
LOCAL_NODE="$(hostname -s)"
|
LOCAL_NODE="$(hostname -s)"
|
||||||
MIGRATION_LOG_DIR="$(mktemp -d)"
|
MIGRATION_LOG_DIR=''
|
||||||
|
PREFERRED_TARGET=''
|
||||||
|
HA_RULES_FILE="${HA_RULES_FILE:-/etc/pve/ha/rules.cfg}"
|
||||||
|
|
||||||
trap 'rm -rf "$MIGRATION_LOG_DIR"' EXIT
|
declare -a MIGRATION_NODES=()
|
||||||
|
declare -A NODE_STORAGE_CACHE=()
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '\n[%s] %s\n' "$(date '+%F %T')" "$*"
|
printf '\n[%s] %s\n' "$(date '+%F %T')" "$*"
|
||||||
@@ -27,18 +30,24 @@ die() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
command -v pvesh >/dev/null 2>&1 || die "pvesh is required."
|
load_lines() {
|
||||||
command -v ha-manager >/dev/null 2>&1 || die "ha-manager is required."
|
local destination_name="$1"
|
||||||
command -v python3 >/dev/null 2>&1 || die "python3 is required."
|
local output
|
||||||
|
local -n destination_ref="$destination_name"
|
||||||
|
shift
|
||||||
|
|
||||||
pvesh get /cluster/resources --type vm --output-format json >/dev/null 2>&1 ||
|
output="$("$@")" || return 1
|
||||||
die "Could not read cluster guest resources."
|
destination_ref=()
|
||||||
pvesh get /cluster/ha/resources --output-format json >/dev/null 2>&1 ||
|
# shellcheck disable=SC2034 # mapfile writes through the nameref.
|
||||||
die "Could not read HA resources."
|
[[ -z "$output" ]] || mapfile -t destination_ref <<< "$output"
|
||||||
pvesh get /nodes --output-format json >/dev/null 2>&1 ||
|
}
|
||||||
die "Could not read cluster node status."
|
|
||||||
pvesh get /storage --output-format json >/dev/null 2>&1 ||
|
cleanup() {
|
||||||
die "Could not read cluster storage configuration."
|
if [[ "$MIGRATION_LOG_DIR" == /tmp/ta-proxmenu-evacuation.* &&
|
||||||
|
-d "$MIGRATION_LOG_DIR" ]]; then
|
||||||
|
rm -rf -- "$MIGRATION_LOG_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
get_local_guests() {
|
get_local_guests() {
|
||||||
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
|
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
|
||||||
@@ -55,6 +64,7 @@ for guest in json.load(sys.stdin):
|
|||||||
guest.get("type", ""),
|
guest.get("type", ""),
|
||||||
guest.get("status", "unknown"),
|
guest.get("status", "unknown"),
|
||||||
str(guest.get("name", "")).replace("\x1f", " "),
|
str(guest.get("name", "")).replace("\x1f", " "),
|
||||||
|
int(guest.get("maxmem") or 0),
|
||||||
sep="\x1f",
|
sep="\x1f",
|
||||||
)
|
)
|
||||||
' "$LOCAL_NODE"
|
' "$LOCAL_NODE"
|
||||||
@@ -75,6 +85,16 @@ for resource in json.load(sys.stdin):
|
|||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guest_is_ha_managed() {
|
||||||
|
local vmid="$1"
|
||||||
|
local ha_id
|
||||||
|
|
||||||
|
for ha_id in "${CURRENT_HA_IDS[@]:-}"; do
|
||||||
|
[[ "$ha_id" == "$vmid" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
get_online_nodes() {
|
get_online_nodes() {
|
||||||
pvesh get /nodes --output-format json 2>/dev/null |
|
pvesh get /nodes --output-format json 2>/dev/null |
|
||||||
python3 -c '
|
python3 -c '
|
||||||
@@ -89,6 +109,14 @@ for node in json.load(sys.stdin):
|
|||||||
' "$LOCAL_NODE"
|
' "$LOCAL_NODE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node_in_maintenance() {
|
||||||
|
local node="$1"
|
||||||
|
|
||||||
|
ha-manager status 2>/dev/null |
|
||||||
|
grep -F "lrm ${node} " |
|
||||||
|
grep -q "maintenance mode"
|
||||||
|
}
|
||||||
|
|
||||||
get_shared_storages() {
|
get_shared_storages() {
|
||||||
pvesh get /storage --output-format json 2>/dev/null |
|
pvesh get /storage --output-format json 2>/dev/null |
|
||||||
python3 -c '
|
python3 -c '
|
||||||
@@ -101,6 +129,74 @@ for storage in json.load(sys.stdin):
|
|||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_node_active_storages() {
|
||||||
|
local node="$1"
|
||||||
|
|
||||||
|
pvesh get "/nodes/${node}/storage" --output-format json 2>/dev/null |
|
||||||
|
python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
for storage in json.load(sys.stdin):
|
||||||
|
active = storage.get("active")
|
||||||
|
enabled = storage.get("enabled", 1)
|
||||||
|
if active in (1, True, "1") and enabled not in (0, False, "0"):
|
||||||
|
storage_id = str(storage.get("storage", ""))
|
||||||
|
if storage_id:
|
||||||
|
print(storage_id)
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_migration_nodes() {
|
||||||
|
local ha_status
|
||||||
|
local node
|
||||||
|
local online_output
|
||||||
|
local storage_csv
|
||||||
|
local -a online_nodes=()
|
||||||
|
local -a usable_nodes=()
|
||||||
|
|
||||||
|
online_output="$(get_online_nodes)" || {
|
||||||
|
warn "Could not refresh online cluster nodes."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
[[ -z "$online_output" ]] ||
|
||||||
|
mapfile -t online_nodes <<< "$online_output"
|
||||||
|
|
||||||
|
ha_status="$(ha-manager status 2>/dev/null)" || {
|
||||||
|
warn "Could not refresh HA node status."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
NODE_STORAGE_CACHE=()
|
||||||
|
|
||||||
|
for node in "${online_nodes[@]}"; do
|
||||||
|
if grep -F "lrm ${node} " <<< "$ha_status" |
|
||||||
|
grep -q "maintenance mode"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
storage_csv="$(get_node_active_storages "$node" | paste -sd, -)" || {
|
||||||
|
warn "Could not read storage status from ${node}; it will not be used."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
NODE_STORAGE_CACHE["$node"]="$storage_csv"
|
||||||
|
usable_nodes+=("$node")
|
||||||
|
done
|
||||||
|
MIGRATION_NODES=("${usable_nodes[@]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
node_has_required_storages() {
|
||||||
|
local node="$1"
|
||||||
|
local required_csv="$2"
|
||||||
|
local available_csv="${NODE_STORAGE_CACHE[$node]-}"
|
||||||
|
local storage
|
||||||
|
local -a required_storages=()
|
||||||
|
|
||||||
|
[[ -n "$required_csv" ]] || return 0
|
||||||
|
IFS=',' read -r -a required_storages <<< "$required_csv"
|
||||||
|
for storage in "${required_storages[@]}"; do
|
||||||
|
[[ ",${available_csv}," == *",${storage},"* ]] || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
guest_storage_scope() {
|
guest_storage_scope() {
|
||||||
local guest_type="$1"
|
local guest_type="$1"
|
||||||
local vmid="$2"
|
local vmid="$2"
|
||||||
@@ -125,6 +221,7 @@ else:
|
|||||||
disk_key = re.compile(r"^(?:rootfs|mp\d+|unused\d+)$")
|
disk_key = re.compile(r"^(?:rootfs|mp\d+|unused\d+)$")
|
||||||
|
|
||||||
local_reasons = []
|
local_reasons = []
|
||||||
|
required_storages = set()
|
||||||
for key, raw_value in config.items():
|
for key, raw_value in config.items():
|
||||||
if not disk_key.match(key) or not isinstance(raw_value, str):
|
if not disk_key.match(key) or not isinstance(raw_value, str):
|
||||||
continue
|
continue
|
||||||
@@ -137,41 +234,221 @@ for key, raw_value in config.items():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
storage = volume.split(":", 1)[0]
|
storage = volume.split(":", 1)[0]
|
||||||
|
required_storages.add(storage)
|
||||||
if storage not in shared:
|
if storage not in shared:
|
||||||
local_reasons.append(f"{key}={storage}")
|
local_reasons.append(f"{key}={storage}")
|
||||||
|
|
||||||
if local_reasons:
|
scope = "local" if local_reasons else "shared"
|
||||||
print("local\x1f" + ", ".join(local_reasons))
|
print(
|
||||||
else:
|
scope,
|
||||||
print("shared\x1f")
|
", ".join(local_reasons),
|
||||||
|
",".join(sorted(required_storages)),
|
||||||
|
sep="\x1f",
|
||||||
|
)
|
||||||
' "$guest_type" "$shared_csv"
|
' "$guest_type" "$shared_csv"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_guest_node_affinity() {
|
||||||
|
local guest_type="$1"
|
||||||
|
local vmid="$2"
|
||||||
|
local sid_type='vm'
|
||||||
|
|
||||||
|
[[ "$guest_type" == "lxc" ]] && sid_type='ct'
|
||||||
|
python3 -c '
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sid = sys.argv[1]
|
||||||
|
path = sys.argv[2]
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print("none", "0", "", sep="\x1f")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
rules = []
|
||||||
|
current = None
|
||||||
|
with open(path, encoding="utf-8") as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.rstrip()
|
||||||
|
if not line or line.lstrip().startswith("#"):
|
||||||
|
continue
|
||||||
|
if not line[0].isspace() and ":" in line:
|
||||||
|
rule_type, rule_id = line.split(":", 1)
|
||||||
|
current = {
|
||||||
|
"type": rule_type.strip(),
|
||||||
|
"id": rule_id.strip(),
|
||||||
|
}
|
||||||
|
rules.append(current)
|
||||||
|
continue
|
||||||
|
if current is not None and line[0].isspace():
|
||||||
|
key_value = line.strip().split(None, 1)
|
||||||
|
if len(key_value) == 2:
|
||||||
|
current[key_value[0]] = key_value[1].strip()
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
if rule.get("type") != "node-affinity":
|
||||||
|
continue
|
||||||
|
if rule.get("disable", "0") in ("1", "yes", "true", "on"):
|
||||||
|
continue
|
||||||
|
resources = {
|
||||||
|
item.strip()
|
||||||
|
for item in rule.get("resources", "").replace(";", ",").split(",")
|
||||||
|
if item.strip()
|
||||||
|
}
|
||||||
|
if sid not in resources:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for position, item in enumerate(rule.get("nodes", "").split(",")):
|
||||||
|
item = item.strip()
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
node = item
|
||||||
|
priority = 0
|
||||||
|
if ":" in item:
|
||||||
|
possible_node, possible_priority = item.rsplit(":", 1)
|
||||||
|
try:
|
||||||
|
priority = int(possible_priority)
|
||||||
|
node = possible_node
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
nodes.append((node, priority, position))
|
||||||
|
nodes.sort(key=lambda value: (-value[1], value[2]))
|
||||||
|
strict = "1" if rule.get("strict", "0") in ("1", "yes", "true", "on") else "0"
|
||||||
|
print(rule.get("id", "node-affinity"), strict, ",".join(n[0] for n in nodes), sep="\x1f")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
print("none", "0", "", sep="\x1f")
|
||||||
|
' "${sid_type}:${vmid}" "$HA_RULES_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
select_target_node() {
|
select_target_node() {
|
||||||
local -a nodes
|
|
||||||
local choice
|
local choice
|
||||||
local index
|
local index
|
||||||
|
|
||||||
mapfile -t nodes < <(get_online_nodes)
|
(( ${#MIGRATION_NODES[@]} > 0 )) ||
|
||||||
(( ${#nodes[@]} > 0 )) || die "No other online cluster node is available."
|
die "No other online, non-maintenance cluster node is available."
|
||||||
|
|
||||||
printf '\nAvailable migration targets:\n\n'
|
printf '\nAvailable preferred migration targets:\n\n'
|
||||||
for index in "${!nodes[@]}"; do
|
for index in "${!MIGRATION_NODES[@]}"; do
|
||||||
printf ' %d) %s\n' "$((index + 1))" "${nodes[$index]}"
|
printf ' %d) %s\n' "$((index + 1))" "${MIGRATION_NODES[$index]}"
|
||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
printf '\n'
|
printf '\n'
|
||||||
read -r -p "Select migration target [1-${#nodes[@]}]: " choice
|
read -r -p "Select preferred migration target [1-${#MIGRATION_NODES[@]}]: " choice
|
||||||
if [[ "$choice" =~ ^[0-9]+$ ]] &&
|
if [[ "$choice" =~ ^[0-9]+$ ]] &&
|
||||||
(( choice >= 1 && choice <= ${#nodes[@]} )); then
|
(( choice >= 1 && choice <= ${#MIGRATION_NODES[@]} )); then
|
||||||
TARGET_NODE="${nodes[$((choice - 1))]}"
|
PREFERRED_TARGET="${MIGRATION_NODES[$((choice - 1))]}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
printf 'Invalid selection.\n' >&2
|
printf 'Invalid selection.\n' >&2
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CHOSEN_DESTINATION=''
|
||||||
|
CHOSEN_NOTE=''
|
||||||
|
|
||||||
|
choose_destination() {
|
||||||
|
local required_csv="$1"
|
||||||
|
local policy_nodes_csv="$2"
|
||||||
|
local policy_strict="$3"
|
||||||
|
local node
|
||||||
|
local policy_node
|
||||||
|
local -a storage_candidates=()
|
||||||
|
local -a policy_nodes=()
|
||||||
|
|
||||||
|
CHOSEN_DESTINATION=''
|
||||||
|
CHOSEN_NOTE=''
|
||||||
|
|
||||||
|
for node in "${MIGRATION_NODES[@]}"; do
|
||||||
|
if node_has_required_storages "$node" "$required_csv"; then
|
||||||
|
storage_candidates+=("$node")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
(( ${#storage_candidates[@]} > 0 )) || return 1
|
||||||
|
|
||||||
|
IFS=',' read -r -a policy_nodes <<< "$policy_nodes_csv"
|
||||||
|
if [[ -z "$policy_nodes_csv" ]]; then
|
||||||
|
for node in "${storage_candidates[@]}"; do
|
||||||
|
if [[ "$node" == "$PREFERRED_TARGET" ]]; then
|
||||||
|
CHOSEN_DESTINATION="$node"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
CHOSEN_DESTINATION="${storage_candidates[0]}"
|
||||||
|
CHOSEN_NOTE="preferred target unavailable for required storage"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for node in "${storage_candidates[@]}"; do
|
||||||
|
if [[ "$node" == "$PREFERRED_TARGET" &&
|
||||||
|
",${policy_nodes_csv}," == *",${node},"* ]]; then
|
||||||
|
CHOSEN_DESTINATION="$node"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for policy_node in "${policy_nodes[@]}"; do
|
||||||
|
for node in "${storage_candidates[@]}"; do
|
||||||
|
if [[ "$node" == "$policy_node" ]]; then
|
||||||
|
CHOSEN_DESTINATION="$node"
|
||||||
|
CHOSEN_NOTE="routed to satisfy HA node-affinity preference"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#storage_candidates[@]} == 1 )); then
|
||||||
|
CHOSEN_DESTINATION="${storage_candidates[0]}"
|
||||||
|
CHOSEN_NOTE="only eligible node; HA node-affinity preference overridden"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$policy_strict" == "1" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for node in "${storage_candidates[@]}"; do
|
||||||
|
if [[ "$node" == "$PREFERRED_TARGET" ]]; then
|
||||||
|
CHOSEN_DESTINATION="$node"
|
||||||
|
CHOSEN_NOTE="no preferred HA node was eligible; non-strict fallback used"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
CHOSEN_DESTINATION="${storage_candidates[0]}"
|
||||||
|
CHOSEN_NOTE="no preferred HA node was eligible; non-strict fallback used"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_node_available_memory() {
|
||||||
|
local node="$1"
|
||||||
|
|
||||||
|
pvesh get /nodes --output-format json 2>/dev/null |
|
||||||
|
python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
requested = sys.argv[1]
|
||||||
|
for node in json.load(sys.stdin):
|
||||||
|
if str(node.get("node", "")) != requested:
|
||||||
|
continue
|
||||||
|
total = int(node.get("maxmem") or 0)
|
||||||
|
used = int(node.get("mem") or 0)
|
||||||
|
print(max(0, total - used))
|
||||||
|
raise SystemExit
|
||||||
|
raise SystemExit(1)
|
||||||
|
' "$node"
|
||||||
|
}
|
||||||
|
|
||||||
|
format_bytes() {
|
||||||
|
local bytes="${1:-0}"
|
||||||
|
|
||||||
|
if command -v numfmt >/dev/null 2>&1; then
|
||||||
|
numfmt --to=iec-i --suffix=B "$bytes"
|
||||||
|
else
|
||||||
|
printf '%d MiB' "$((bytes / 1024 / 1024))"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_ha_evacuation() {
|
wait_for_ha_evacuation() {
|
||||||
local deadline=$((SECONDS + HA_WAIT_SECONDS))
|
local deadline=$((SECONDS + HA_WAIT_SECONDS))
|
||||||
local -a local_guests
|
local -a local_guests
|
||||||
@@ -183,8 +460,14 @@ wait_for_ha_evacuation() {
|
|||||||
log "Waiting for HA-managed guests to leave ${LOCAL_NODE}."
|
log "Waiting for HA-managed guests to leave ${LOCAL_NODE}."
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
mapfile -t local_guests < <(get_local_guests)
|
load_lines local_guests get_local_guests || {
|
||||||
mapfile -t ha_ids < <(get_ha_guest_ids)
|
warn "Could not refresh guests assigned to ${LOCAL_NODE}."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
load_lines ha_ids get_ha_guest_ids || {
|
||||||
|
warn "Could not refresh HA resources."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
remaining=()
|
remaining=()
|
||||||
|
|
||||||
for guest in "${local_guests[@]}"; do
|
for guest in "${local_guests[@]}"; do
|
||||||
@@ -214,23 +497,55 @@ migrate_guest() {
|
|||||||
local vmid="$1"
|
local vmid="$1"
|
||||||
local guest_type="$2"
|
local guest_type="$2"
|
||||||
local status="$3"
|
local status="$3"
|
||||||
|
local destination="$4"
|
||||||
|
|
||||||
if [[ "$guest_type" == "qemu" ]]; then
|
if [[ "$guest_type" == "qemu" ]]; then
|
||||||
if [[ "$status" == "running" ]]; then
|
if [[ "$status" == "running" ]]; then
|
||||||
qm migrate "$vmid" "$TARGET_NODE" --online
|
qm migrate "$vmid" "$destination" --online
|
||||||
else
|
else
|
||||||
qm migrate "$vmid" "$TARGET_NODE"
|
qm migrate "$vmid" "$destination"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ "$status" == "running" ]]; then
|
if [[ "$status" == "running" ]]; then
|
||||||
pct migrate "$vmid" "$TARGET_NODE" --restart 1 \
|
pct migrate "$vmid" "$destination" --restart 1 \
|
||||||
--timeout "$SHUTDOWN_TIMEOUT"
|
--timeout "$SHUTDOWN_TIMEOUT"
|
||||||
else
|
else
|
||||||
pct migrate "$vmid" "$TARGET_NODE"
|
pct migrate "$vmid" "$destination"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_guest_node() {
|
||||||
|
local vmid="$1"
|
||||||
|
|
||||||
|
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
|
||||||
|
python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
vmid = str(sys.argv[1])
|
||||||
|
for guest in json.load(sys.stdin):
|
||||||
|
if str(guest.get("vmid", "")) == vmid:
|
||||||
|
print(guest.get("node", ""))
|
||||||
|
raise SystemExit
|
||||||
|
raise SystemExit(1)
|
||||||
|
' "$vmid"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_guest_node() {
|
||||||
|
local vmid="$1"
|
||||||
|
local destination="$2"
|
||||||
|
local attempts="${3:-15}"
|
||||||
|
local attempt=0
|
||||||
|
|
||||||
|
while (( attempt < attempts )); do
|
||||||
|
[[ "$(get_guest_node "$vmid")" == "$destination" ]] && return 0
|
||||||
|
sleep 1
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_migration_batch() {
|
wait_for_migration_batch() {
|
||||||
local index
|
local index
|
||||||
local pid
|
local pid
|
||||||
@@ -240,17 +555,26 @@ wait_for_migration_batch() {
|
|||||||
local guest_type
|
local guest_type
|
||||||
local status
|
local status
|
||||||
local name
|
local name
|
||||||
|
local maxmem
|
||||||
|
local required_storages
|
||||||
|
local policy_nodes
|
||||||
|
local policy_strict
|
||||||
|
local policy_rule
|
||||||
|
local destination
|
||||||
|
local route_note
|
||||||
|
|
||||||
for index in "${!batch_pids[@]}"; do
|
for index in "${!batch_pids[@]}"; do
|
||||||
pid="${batch_pids[$index]}"
|
pid="${batch_pids[$index]}"
|
||||||
guest="${batch_guests[$index]}"
|
guest="${batch_guests[$index]}"
|
||||||
log_file="${batch_logs[$index]}"
|
log_file="${batch_logs[$index]}"
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name <<< "$guest"
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages \
|
||||||
|
policy_nodes policy_strict policy_rule destination route_note <<< "$guest"
|
||||||
|
|
||||||
if wait "$pid"; then
|
if wait "$pid" && wait_for_guest_node "$vmid" "$destination"; then
|
||||||
log "Migration completed for ${guest_type} ${vmid} (${name:-unnamed})."
|
log "Migration completed for ${guest_type} ${vmid} (${name:-unnamed}) to ${destination}."
|
||||||
|
migration_successes+=("$guest")
|
||||||
else
|
else
|
||||||
warn "Migration failed for ${guest_type} ${vmid} (${name:-unnamed})."
|
warn "Migration failed for ${guest_type} ${vmid} (${name:-unnamed}) to ${destination}."
|
||||||
migration_failures+=("$guest")
|
migration_failures+=("$guest")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -286,15 +610,27 @@ guest_status() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wait_for_guest_stopped() {
|
||||||
|
local vmid="$1"
|
||||||
|
local guest_type="$2"
|
||||||
|
local attempts="${3:-15}"
|
||||||
|
local attempt=0
|
||||||
|
|
||||||
|
while (( attempt < attempts )); do
|
||||||
|
[[ "$(guest_status "$vmid" "$guest_type")" == "stopped" ]] && return 0
|
||||||
|
sleep 1
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_maintenance_mode() {
|
wait_for_maintenance_mode() {
|
||||||
local deadline=$((SECONDS + MAINTENANCE_WAIT_SECONDS))
|
local deadline=$((SECONDS + MAINTENANCE_WAIT_SECONDS))
|
||||||
|
|
||||||
log "Waiting for ${LOCAL_NODE} to enter HA maintenance mode."
|
log "Waiting for ${LOCAL_NODE} to enter HA maintenance mode."
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
if ha-manager status |
|
if node_in_maintenance "$LOCAL_NODE"; then
|
||||||
grep -F "$LOCAL_NODE" |
|
|
||||||
grep -q "maintenance mode"; then
|
|
||||||
printf '\n'
|
printf '\n'
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -308,132 +644,312 @@ wait_for_maintenance_mode() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_maintenance_mode
|
preflight() {
|
||||||
|
local command
|
||||||
|
|
||||||
wait_for_ha_evacuation ||
|
for command in pvesh ha-manager python3 qm pct; do
|
||||||
die "Resolve the remaining HA guests before continuing the evacuation."
|
command -v "$command" >/dev/null 2>&1 ||
|
||||||
|
die "${command} is required."
|
||||||
select_target_node
|
|
||||||
|
|
||||||
mapfile -t shared_storages < <(get_shared_storages)
|
|
||||||
shared_csv="$(IFS=,; echo "${shared_storages[*]}")"
|
|
||||||
|
|
||||||
mapfile -t guests < <(get_local_guests)
|
|
||||||
if (( ${#guests[@]} == 0 )); then
|
|
||||||
log "No guests remain on ${LOCAL_NODE}."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
declare -a shared_guests=()
|
|
||||||
declare -a local_guests=()
|
|
||||||
|
|
||||||
for guest in "${guests[@]}"; do
|
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name <<< "$guest"
|
|
||||||
storage_result="$(guest_storage_scope "$guest_type" "$vmid" "$shared_csv")" ||
|
|
||||||
die "Could not inspect storage for ${guest_type} ${vmid}."
|
|
||||||
IFS=$'\x1f' read -r scope reason <<< "$storage_result"
|
|
||||||
|
|
||||||
if [[ "$scope" == "shared" ]]; then
|
|
||||||
shared_guests+=("$guest")
|
|
||||||
else
|
|
||||||
local_guests+=("${guest}"$'\x1f'"${reason:-local storage}")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
printf '\nEvacuation plan for %s:\n' "$LOCAL_NODE"
|
|
||||||
printf ' Migration target: %s\n' "$TARGET_NODE"
|
|
||||||
printf ' Shared-storage guests to migrate: %d\n' "${#shared_guests[@]}"
|
|
||||||
printf ' Local-storage guests to retain: %d\n' "${#local_guests[@]}"
|
|
||||||
|
|
||||||
if (( ${#shared_guests[@]} > 0 )); then
|
|
||||||
printf '\nShared-storage guests:\n'
|
|
||||||
for guest in "${shared_guests[@]}"; do
|
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name <<< "$guest"
|
|
||||||
printf ' %-6s %-5s %-8s %s\n' "$vmid" "$guest_type" "$status" "$name"
|
|
||||||
done
|
done
|
||||||
fi
|
|
||||||
|
|
||||||
if (( ${#local_guests[@]} > 0 )); then
|
pvesh get /cluster/resources --type vm --output-format json >/dev/null 2>&1 ||
|
||||||
printf '\nLocal-storage guests (will not migrate):\n'
|
die "Could not read cluster guest resources."
|
||||||
for guest in "${local_guests[@]}"; do
|
pvesh get /cluster/ha/resources --output-format json >/dev/null 2>&1 ||
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name reason <<< "$guest"
|
die "Could not read HA resources."
|
||||||
printf ' %-6s %-5s %-8s %-24s %s\n' \
|
pvesh get /nodes --output-format json >/dev/null 2>&1 ||
|
||||||
"$vmid" "$guest_type" "$status" "$name" "$reason"
|
die "Could not read cluster node status."
|
||||||
|
pvesh get /storage --output-format json >/dev/null 2>&1 ||
|
||||||
|
die "Could not read cluster storage configuration."
|
||||||
|
ha-manager status >/dev/null 2>&1 ||
|
||||||
|
die "Could not read HA node status."
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local answer
|
||||||
|
local available_memory
|
||||||
|
local destination
|
||||||
|
local guest
|
||||||
|
local guest_type
|
||||||
|
local log_file
|
||||||
|
local maxmem
|
||||||
|
local name
|
||||||
|
local policy_nodes
|
||||||
|
local policy_result
|
||||||
|
local policy_rule
|
||||||
|
local policy_strict
|
||||||
|
local reason
|
||||||
|
local required_memory
|
||||||
|
local required_storages
|
||||||
|
local route_note
|
||||||
|
local scope
|
||||||
|
local shared_csv
|
||||||
|
local status
|
||||||
|
local storage_result
|
||||||
|
local vmid
|
||||||
|
local -a guests=()
|
||||||
|
local -a shared_storages=()
|
||||||
|
local -a shared_guests=()
|
||||||
|
local -a local_guests=()
|
||||||
|
local -a unroutable_guests=()
|
||||||
|
local -a migration_failures=()
|
||||||
|
local -a migration_successes=()
|
||||||
|
local -a migration_skipped=()
|
||||||
|
local -a shutdown_failures=()
|
||||||
|
local -a shutdown_successes=()
|
||||||
|
local -a batch_pids=()
|
||||||
|
local -a batch_guests=()
|
||||||
|
local -a batch_logs=()
|
||||||
|
local -a final_guests=()
|
||||||
|
local -a running_final_guests=()
|
||||||
|
local -a CURRENT_HA_IDS=()
|
||||||
|
local -A DEST_REQUIRED_MEMORY=()
|
||||||
|
|
||||||
|
MIGRATION_LOG_DIR="$(mktemp -d /tmp/ta-proxmenu-evacuation.XXXXXX)" ||
|
||||||
|
die "Could not create the migration log directory."
|
||||||
|
chmod 0700 "$MIGRATION_LOG_DIR"
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
preflight
|
||||||
|
wait_for_maintenance_mode
|
||||||
|
wait_for_ha_evacuation ||
|
||||||
|
die "Resolve the remaining HA guests before continuing the evacuation."
|
||||||
|
|
||||||
|
load_lines shared_storages get_shared_storages ||
|
||||||
|
die "Could not read shared-storage configuration."
|
||||||
|
shared_csv="$(IFS=,; echo "${shared_storages[*]}")"
|
||||||
|
load_lines guests get_local_guests ||
|
||||||
|
die "Could not read guests assigned to ${LOCAL_NODE}."
|
||||||
|
|
||||||
|
if (( ${#guests[@]} == 0 )); then
|
||||||
|
log "No guests remain on ${LOCAL_NODE}."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for guest in "${guests[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem <<< "$guest"
|
||||||
|
storage_result="$(guest_storage_scope "$guest_type" "$vmid" "$shared_csv")" ||
|
||||||
|
die "Could not inspect storage for ${guest_type} ${vmid}."
|
||||||
|
IFS=$'\x1f' read -r scope reason required_storages <<< "$storage_result"
|
||||||
|
|
||||||
|
if [[ "$scope" == "shared" ]]; then
|
||||||
|
shared_guests+=(
|
||||||
|
"${guest}"$'\x1f'"${required_storages}"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
local_guests+=(
|
||||||
|
"${guest}"$'\x1f'"${reason:-local storage}"
|
||||||
|
)
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
|
||||||
|
|
||||||
printf '\n'
|
if (( ${#shared_guests[@]} > 0 )); then
|
||||||
read -r -p "Proceed with shared-storage guest migration? [y/N] " answer
|
refresh_migration_nodes ||
|
||||||
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Evacuation cancelled."; exit 0; }
|
die "Could not build a safe migration-target list."
|
||||||
|
select_target_node
|
||||||
|
|
||||||
declare -a migration_failures=()
|
guests=("${shared_guests[@]}")
|
||||||
declare -a batch_pids=()
|
shared_guests=()
|
||||||
declare -a batch_guests=()
|
for guest in "${guests[@]}"; do
|
||||||
declare -a batch_logs=()
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages <<< "$guest"
|
||||||
|
policy_result="$(get_guest_node_affinity "$guest_type" "$vmid")" ||
|
||||||
|
die "Could not inspect HA node-affinity policy for ${guest_type} ${vmid}."
|
||||||
|
IFS=$'\x1f' read -r policy_rule policy_strict policy_nodes <<< "$policy_result"
|
||||||
|
|
||||||
for guest in "${shared_guests[@]}"; do
|
if choose_destination "$required_storages" "$policy_nodes" "$policy_strict"; then
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name <<< "$guest"
|
destination="$CHOSEN_DESTINATION"
|
||||||
log "Starting migration for ${guest_type} ${vmid} (${name:-unnamed}) to ${TARGET_NODE}."
|
route_note="$CHOSEN_NOTE"
|
||||||
|
shared_guests+=(
|
||||||
log_file="${MIGRATION_LOG_DIR}/${guest_type}-${vmid}.log"
|
"${guest}"$'\x1f'"${policy_nodes}"$'\x1f'"${policy_strict}"$'\x1f'"${policy_rule}"$'\x1f'"${destination}"$'\x1f'"${route_note}"
|
||||||
migrate_guest "$vmid" "$guest_type" "$status" >"$log_file" 2>&1 &
|
)
|
||||||
batch_pids+=("$!")
|
if [[ "$status" == "running" && "$maxmem" =~ ^[0-9]+$ ]]; then
|
||||||
batch_guests+=("$guest")
|
DEST_REQUIRED_MEMORY["$destination"]="$(( ${DEST_REQUIRED_MEMORY[$destination]:-0} + maxmem ))"
|
||||||
batch_logs+=("$log_file")
|
fi
|
||||||
|
else
|
||||||
if (( ${#batch_pids[@]} >= MAX_PARALLEL_MIGRATIONS )); then
|
unroutable_guests+=(
|
||||||
wait_for_migration_batch
|
"${guest}"$'\x1f'"${policy_nodes}"$'\x1f'"${policy_strict}"$'\x1f'"${policy_rule}"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
(( ${#batch_pids[@]} == 0 )) || wait_for_migration_batch
|
printf '\nEvacuation plan for %s:\n' "$LOCAL_NODE"
|
||||||
|
[[ -n "$PREFERRED_TARGET" ]] &&
|
||||||
|
printf ' Preferred migration target: %s\n' "$PREFERRED_TARGET"
|
||||||
|
printf ' Shared-storage guests to migrate: %d\n' "${#shared_guests[@]}"
|
||||||
|
printf ' Local-storage guests to shut down: %d\n' "${#local_guests[@]}"
|
||||||
|
printf ' Shared guests without a valid route: %d\n' "${#unroutable_guests[@]}"
|
||||||
|
|
||||||
declare -a running_local_guests=()
|
if (( ${#shared_guests[@]} > 0 )); then
|
||||||
for guest in "${local_guests[@]}"; do
|
printf '\nShared-storage guests:\n'
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name reason <<< "$guest"
|
for guest in "${shared_guests[@]}"; do
|
||||||
status="$(guest_status "$vmid" "$guest_type")"
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages \
|
||||||
if [[ "$status" == "running" ]]; then
|
policy_nodes policy_strict policy_rule destination route_note <<< "$guest"
|
||||||
running_local_guests+=(
|
printf ' %-6s %-5s %-8s %-24s -> %s' \
|
||||||
"${vmid}"$'\x1f'"${guest_type}"$'\x1f'"${status}"$'\x1f'"${name}"$'\x1f'"${reason}"
|
"$vmid" "$guest_type" "$status" "$name" "$destination"
|
||||||
)
|
[[ -n "$route_note" ]] && printf ' (%s)' "$route_note"
|
||||||
|
printf '\n'
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
if (( ${#running_local_guests[@]} > 0 )); then
|
if (( ${#local_guests[@]} > 0 )); then
|
||||||
printf '\nThe following local-storage guests remain running:\n'
|
printf '\nLocal-storage guests (will not migrate):\n'
|
||||||
for guest in "${running_local_guests[@]}"; do
|
for guest in "${local_guests[@]}"; do
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name reason <<< "$guest"
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem reason <<< "$guest"
|
||||||
printf ' %-6s %-5s %-24s %s\n' "$vmid" "$guest_type" "$name" "$reason"
|
printf ' %-6s %-5s %-8s %-24s %s\n' \
|
||||||
|
"$vmid" "$guest_type" "$status" "$name" "$reason"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#unroutable_guests[@]} > 0 )); then
|
||||||
|
printf '\nShared guests with no eligible destination:\n'
|
||||||
|
for guest in "${unroutable_guests[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages \
|
||||||
|
policy_nodes policy_strict policy_rule <<< "$guest"
|
||||||
|
printf ' %-6s %-5s %-24s storages=%s' \
|
||||||
|
"$vmid" "$guest_type" "$name" "$required_storages"
|
||||||
|
[[ "$policy_rule" != "none" ]] &&
|
||||||
|
printf ' policy=%s strict=%s nodes=%s' \
|
||||||
|
"$policy_rule" "$policy_strict" "$policy_nodes"
|
||||||
|
printf '\n'
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for destination in "${!DEST_REQUIRED_MEMORY[@]}"; do
|
||||||
|
required_memory="${DEST_REQUIRED_MEMORY[$destination]}"
|
||||||
|
available_memory="$(get_node_available_memory "$destination" 2>/dev/null || echo 0)"
|
||||||
|
printf '\nTarget %s memory: %s available; %s configured for incoming running guests.\n' \
|
||||||
|
"$destination" "$(format_bytes "$available_memory")" \
|
||||||
|
"$(format_bytes "$required_memory")"
|
||||||
|
if (( available_memory > 0 && required_memory > available_memory )); then
|
||||||
|
warn "${destination} has less currently available memory than the incoming guests' configured memory."
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
printf '\n'
|
printf '\n'
|
||||||
read -r -p "Gracefully shut down these local-storage guests? [y/N] " answer
|
read -r -p "Proceed with guest migrations and local-storage guest shutdown? [y/N] " answer
|
||||||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
[[ "$answer" =~ ^[Yy]$ ]] || {
|
||||||
for guest in "${running_local_guests[@]}"; do
|
echo "Evacuation cancelled; ${LOCAL_NODE} remains in maintenance mode."
|
||||||
IFS=$'\x1f' read -r vmid guest_type status name reason <<< "$guest"
|
return 1
|
||||||
log "Shutting down ${guest_type} ${vmid} (${name:-unnamed})."
|
}
|
||||||
shutdown_guest "$vmid" "$guest_type" ||
|
|
||||||
warn "Graceful shutdown failed for ${guest_type} ${vmid}; it was not force-stopped."
|
load_lines CURRENT_HA_IDS get_ha_guest_ids ||
|
||||||
|
die "Could not safely recheck HA-managed guests."
|
||||||
|
refresh_migration_nodes ||
|
||||||
|
die "Could not safely recheck migration targets."
|
||||||
|
|
||||||
|
for guest in "${shared_guests[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages \
|
||||||
|
policy_nodes policy_strict policy_rule destination route_note <<< "$guest"
|
||||||
|
|
||||||
|
if guest_is_ha_managed "$vmid"; then
|
||||||
|
warn "${guest_type} ${vmid} became HA-managed; TAPM will not migrate it manually."
|
||||||
|
migration_skipped+=("$guest")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! choose_destination "$required_storages" "$policy_nodes" "$policy_strict"; then
|
||||||
|
warn "No eligible destination remains for ${guest_type} ${vmid}; migration skipped."
|
||||||
|
migration_skipped+=("$guest")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$destination" != "$CHOSEN_DESTINATION" ]]; then
|
||||||
|
warn "Destination for ${guest_type} ${vmid} changed from ${destination} to ${CHOSEN_DESTINATION} after recheck."
|
||||||
|
destination="$CHOSEN_DESTINATION"
|
||||||
|
route_note="$CHOSEN_NOTE"
|
||||||
|
guest="${vmid}"$'\x1f'"${guest_type}"$'\x1f'"${status}"$'\x1f'"${name}"$'\x1f'"${maxmem}"$'\x1f'"${required_storages}"$'\x1f'"${policy_nodes}"$'\x1f'"${policy_strict}"$'\x1f'"${policy_rule}"$'\x1f'"${destination}"$'\x1f'"${route_note}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting migration for ${guest_type} ${vmid} (${name:-unnamed}) to ${destination}."
|
||||||
|
log_file="${MIGRATION_LOG_DIR}/${guest_type}-${vmid}.log"
|
||||||
|
migrate_guest "$vmid" "$guest_type" "$status" "$destination" >"$log_file" 2>&1 &
|
||||||
|
batch_pids+=("$!")
|
||||||
|
batch_guests+=("$guest")
|
||||||
|
batch_logs+=("$log_file")
|
||||||
|
|
||||||
|
if (( ${#batch_pids[@]} >= MAX_PARALLEL_MIGRATIONS )); then
|
||||||
|
wait_for_migration_batch
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
(( ${#batch_pids[@]} == 0 )) || wait_for_migration_batch
|
||||||
|
|
||||||
|
for guest in "${local_guests[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem reason <<< "$guest"
|
||||||
|
status="$(guest_status "$vmid" "$guest_type")"
|
||||||
|
[[ "$status" == "running" ]] || continue
|
||||||
|
|
||||||
|
log "Shutting down local-storage ${guest_type} ${vmid} (${name:-unnamed})."
|
||||||
|
if shutdown_guest "$vmid" "$guest_type" &&
|
||||||
|
wait_for_guest_stopped "$vmid" "$guest_type"; then
|
||||||
|
shutdown_successes+=("$guest")
|
||||||
|
else
|
||||||
|
warn "Graceful shutdown failed for ${guest_type} ${vmid}; it was not force-stopped."
|
||||||
|
shutdown_failures+=("$guest")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
load_lines final_guests get_local_guests ||
|
||||||
|
die "Could not verify the final guest state on ${LOCAL_NODE}."
|
||||||
|
for guest in "${final_guests[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem <<< "$guest"
|
||||||
|
[[ "$status" == "running" ]] && running_final_guests+=("$guest")
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\nEvacuation summary:\n'
|
||||||
|
printf ' Successful migrations: %d\n' "${#migration_successes[@]}"
|
||||||
|
printf ' Failed migrations: %d\n' "${#migration_failures[@]}"
|
||||||
|
printf ' Skipped migrations: %d\n' "${#migration_skipped[@]}"
|
||||||
|
printf ' Unroutable shared guests: %d\n' "${#unroutable_guests[@]}"
|
||||||
|
printf ' Local guest shutdowns: %d\n' "${#shutdown_successes[@]}"
|
||||||
|
printf ' Failed local shutdowns: %d\n' "${#shutdown_failures[@]}"
|
||||||
|
printf ' Guests still running locally: %d\n' "${#running_final_guests[@]}"
|
||||||
|
|
||||||
|
if (( ${#migration_successes[@]} > 0 )); then
|
||||||
|
printf '\nMigrated guests:\n'
|
||||||
|
for guest in "${migration_successes[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages \
|
||||||
|
policy_nodes policy_strict policy_rule destination route_note <<< "$guest"
|
||||||
|
printf ' %-6s %-5s %-24s -> %s\n' \
|
||||||
|
"$vmid" "$guest_type" "$name" "$destination"
|
||||||
done
|
done
|
||||||
else
|
|
||||||
warn "Local-storage guests were left running."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if (( ${#migration_failures[@]} > 0 )); then
|
||||||
|
printf '\nFailed migrations (left unchanged and not force-stopped):\n'
|
||||||
|
for guest in "${migration_failures[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem required_storages \
|
||||||
|
policy_nodes policy_strict policy_rule destination route_note <<< "$guest"
|
||||||
|
printf ' %-6s %-5s %-24s target=%s\n' \
|
||||||
|
"$vmid" "$guest_type" "$name" "$destination"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#shutdown_successes[@]} > 0 )); then
|
||||||
|
printf '\nLocal-storage guests shut down:\n'
|
||||||
|
for guest in "${shutdown_successes[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem reason <<< "$guest"
|
||||||
|
printf ' %-6s %-5s %s\n' "$vmid" "$guest_type" "$name"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#final_guests[@]} > 0 )); then
|
||||||
|
printf '\nGuests still assigned to %s:\n' "$LOCAL_NODE"
|
||||||
|
for guest in "${final_guests[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid guest_type status name maxmem <<< "$guest"
|
||||||
|
printf ' %-6s %-5s %-8s %s\n' "$vmid" "$guest_type" "$status" "$name"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nNo guest was force-stopped.\n'
|
||||||
|
|
||||||
|
if (( ${#migration_failures[@]} > 0 ||
|
||||||
|
${#migration_skipped[@]} > 0 ||
|
||||||
|
${#unroutable_guests[@]} > 0 ||
|
||||||
|
${#shutdown_failures[@]} > 0 ||
|
||||||
|
${#running_final_guests[@]} > 0 )); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||||
|
main "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mapfile -t final_guests < <(get_local_guests)
|
|
||||||
|
|
||||||
printf '\nEvacuation summary:\n'
|
|
||||||
printf ' Migration failures: %d\n' "${#migration_failures[@]}"
|
|
||||||
printf ' Guests still assigned to %s: %d\n' "$LOCAL_NODE" "${#final_guests[@]}"
|
|
||||||
|
|
||||||
if (( ${#migration_failures[@]} > 0 )); then
|
|
||||||
printf '\nFailed migrations (left unchanged; not shut down automatically):\n'
|
|
||||||
printf ' %s\n' "${migration_failures[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( ${#final_guests[@]} > 0 )); then
|
|
||||||
printf '\nGuests still assigned to %s:\n' "$LOCAL_NODE"
|
|
||||||
printf ' %s\n' "${final_guests[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '\nNo guest was force-stopped.\n'
|
|
||||||
|
|||||||
+552
-95
@@ -18,6 +18,7 @@ FINISH_ACTION() {
|
|||||||
FINISH_FAILED_ACTION() {
|
FINISH_FAILED_ACTION() {
|
||||||
(( ACTION_REQUESTED == 1 )) && exit 1
|
(( ACTION_REQUESTED == 1 )) && exit 1
|
||||||
ENTER2CONTINUE
|
ENTER2CONTINUE
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
declare -a TAPM_TEMP_DIRS=()
|
declare -a TAPM_TEMP_DIRS=()
|
||||||
@@ -89,6 +90,22 @@ TAPM_DOWNLOAD_HTTPS() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TAPM_DOWNLOAD_SHA256() {
|
||||||
|
local url="$1"
|
||||||
|
local destination="$2"
|
||||||
|
local expected_sha256="${3,,}"
|
||||||
|
local label="${4:-Installer}"
|
||||||
|
local actual_sha256
|
||||||
|
|
||||||
|
TAPM_DOWNLOAD_HTTPS "$url" "$destination" "$label" || return 1
|
||||||
|
actual_sha256="$(sha256sum "$destination" | cut -d' ' -f1)"
|
||||||
|
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
|
||||||
|
rm -f -- "$destination"
|
||||||
|
echo -e "${idsCL[LightRed]}${label} checksum did not match; the file was removed.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
TAPM_PACKAGE_INSTALLED() {
|
TAPM_PACKAGE_INSTALLED() {
|
||||||
dpkg-query -W -f='${Status}' "$1" 2>/dev/null |
|
dpkg-query -W -f='${Status}' "$1" 2>/dev/null |
|
||||||
grep -q '^install ok installed$'
|
grep -q '^install ok installed$'
|
||||||
@@ -108,6 +125,23 @@ TAPM_WAIT_FOR_SERVICE() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TAPM_WAIT_FOR_TCP_PORT() {
|
||||||
|
local port="$1"
|
||||||
|
local attempts="${2:-30}"
|
||||||
|
local attempt=0
|
||||||
|
|
||||||
|
while (( attempt < attempts )); do
|
||||||
|
if ss -H -lnt 2>/dev/null |
|
||||||
|
awk -v port="$port" '$4 ~ (":" port "$") { found=1 } END { exit !found }'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
trap TAPM_CLEAN_ALL_TEMP_DIRS EXIT
|
trap TAPM_CLEAN_ALL_TEMP_DIRS EXIT
|
||||||
|
|
||||||
TAPM_CLEAR_AUTHORIZATION() {
|
TAPM_CLEAR_AUTHORIZATION() {
|
||||||
@@ -286,13 +320,68 @@ INSTALL_PROXMENUX() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PROXMENUX_POST_INSTALL() {
|
PROXMENUX_POST_INSTALL() {
|
||||||
PMFLDR='/usr/local/share/proxmenux/scripts/post_install'
|
local backup_dir='/var/backups/ta-proxmenu'
|
||||||
[ ! -f "${PMFLDR}/customizable_post_install.sh" ] && INSTALL_PROXMENUX
|
local backup_file=''
|
||||||
bash "${PMFLDR}/customizable_post_install.sh"
|
local post_install_dir='/usr/local/share/proxmenux/scripts/post_install'
|
||||||
|
local post_install_script="${post_install_dir}/customizable_post_install.sh"
|
||||||
touch /opt/.PROXMENUX_POST_INSTALL
|
local timestamp
|
||||||
[ -s /etc/apt/sources.list ] && cat /dev/null > /etc/apt/sources.list
|
|
||||||
|
if [[ ! -f "$post_install_script" ]]; then
|
||||||
|
INSTALL_PROXMENUX
|
||||||
|
[[ -f "$post_install_script" ]] || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! bash "$post_install_script"; then
|
||||||
|
echo -e "${idsCL[LightRed]}The ProxMenux post-install script failed. A completion marker was not created.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e /etc/apt/sources.list && ! -f /etc/apt/sources.list ]]; then
|
||||||
|
echo -e "${idsCL[LightRed]}/etc/apt/sources.list is not a regular file and was not changed.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s /etc/apt/sources.list ]]; then
|
||||||
|
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
if ! install -d -m 0700 "$backup_dir" ||
|
||||||
|
! backup_file="$(mktemp "${backup_dir}/sources.list.proxmenux.${timestamp}.XXXXXX")" ||
|
||||||
|
! install -m 0600 /etc/apt/sources.list "$backup_file"; then
|
||||||
|
[[ -n "$backup_file" ]] && rm -f -- "$backup_file"
|
||||||
|
echo -e "${idsCL[LightRed]}Could not back up /etc/apt/sources.list; it was not cleared.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! : >/etc/apt/sources.list ||
|
||||||
|
! chmod 0644 /etc/apt/sources.list; then
|
||||||
|
echo -e "${idsCL[LightRed]}Could not enforce an empty /etc/apt/sources.list.${idsCL[Default]}"
|
||||||
|
[[ -n "$backup_file" ]] &&
|
||||||
|
echo "Backup retained at: ${backup_file}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! apt-get update; then
|
||||||
|
echo -e "${idsCL[LightRed]}APT repository validation failed after ProxMenux post-install.${idsCL[Default]}"
|
||||||
|
[[ -n "$backup_file" ]] &&
|
||||||
|
echo "Removed sources were retained at: ${backup_file}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! touch /opt/.PROXMENUX_POST_INSTALL; then
|
||||||
|
echo -e "${idsCL[LightRed]}Post-install succeeded, but the completion marker could not be created.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${idsCL[Green]}ProxMenux post-install completed and APT sources were validated.${idsCL[Default]}"
|
||||||
|
[[ -n "$backup_file" ]] &&
|
||||||
|
echo "ProxMenux additions to sources.list were saved at: ${backup_file}"
|
||||||
|
FINISH_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
INSTALL_GLANCES() {
|
INSTALL_GLANCES() {
|
||||||
@@ -501,48 +590,193 @@ INSTALL_S1() {
|
|||||||
FINISH_ACTION
|
FINISH_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TAPM_SYSTEM_PRODUCT_NAME() {
|
||||||
|
local product_name=''
|
||||||
|
|
||||||
|
if [[ -r /sys/class/dmi/id/product_name ]]; then
|
||||||
|
read -r product_name </sys/class/dmi/id/product_name
|
||||||
|
elif command -v dmidecode >/dev/null 2>&1; then
|
||||||
|
product_name="$(dmidecode -s system-product-name 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$product_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_OMSA_SUPPORTED_HARDWARE() {
|
||||||
|
local product_name
|
||||||
|
|
||||||
|
product_name="$(TAPM_SYSTEM_PRODUCT_NAME)"
|
||||||
|
[[ "$product_name" =~ PowerEdge[[:space:]]+[[:alpha:]]+[0-9](3|4)[0-9][[:alnum:]-]* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_OMSA_SUPPORTED_PLATFORM() {
|
||||||
|
local architecture
|
||||||
|
local codename
|
||||||
|
local os_version
|
||||||
|
local pve_version
|
||||||
|
|
||||||
|
architecture="$(dpkg --print-architecture 2>/dev/null)"
|
||||||
|
os_version="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_ID:-}")"
|
||||||
|
codename="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_CODENAME:-}")"
|
||||||
|
pve_version="$(pveversion 2>/dev/null | head -n 1)"
|
||||||
|
|
||||||
|
[[ "$architecture" == "amd64" &&
|
||||||
|
"$os_version" == "13" &&
|
||||||
|
"$codename" == "trixie" &&
|
||||||
|
"$pve_version" == pve-manager/9.* ]]
|
||||||
|
}
|
||||||
|
|
||||||
INSTALL_OMSA() {
|
INSTALL_OMSA() {
|
||||||
echo
|
local base_url='https://archive.ubuntu.com/ubuntu/pool'
|
||||||
mkdir -p /tmp/omsa
|
local dell_key
|
||||||
cd /tmp/omsa || return 1
|
local dell_keyring
|
||||||
apt install -y gnupg libcurl4t64 libncurses6 libxslt1.1 libgpm2 libtinfo6
|
local dell_source
|
||||||
mkdir -p /etc/apt/keyrings
|
local index
|
||||||
wget -qO - https://linux.dell.com/repo/pgp_pubkeys/0x1285491434D8786F.asc | gpg --dearmor -o /etc/apt/keyrings/linux.dell.com.gpg
|
local product_name
|
||||||
chmod +r /etc/apt/keyrings/linux.dell.com.gpg
|
local temp_dir
|
||||||
echo "deb [signed-by=/etc/apt/keyrings/linux.dell.com.gpg] http://linux.dell.com/repo/community/openmanage/11000/jammy jammy main" > /etc/apt/sources.list.d/linux.dell.com.list
|
local -a filenames=(
|
||||||
apt update
|
'libwsman-curl-client-transport1_2.6.5-0ubuntu16_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-curl-client-transport1_2.6.5-0ubuntu16_amd64.deb
|
'libwsman-client4t64_2.6.5-0ubuntu16_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-client4t64_2.6.5-0ubuntu16_amd64.deb
|
'libxml2-16_2.15.2+dfsg-0.1_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman1t64_2.6.5-0ubuntu16_amd64.deb
|
'libwsman1t64_2.6.5-0ubuntu16_amd64.deb'
|
||||||
# wget -c http://http.us.debian.org/debian/pool/main/libx/libxml2/libxml2-16_2.15.1+dfsg-2+b1_amd64.deb
|
'libwsman-server1t64_2.6.5-0ubuntu16_amd64.deb'
|
||||||
wget -c http://http.us.debian.org/debian/pool/main/libx/libxml2/libxml2-16_2.15.2+dfsg-0.1_amd64.deb
|
'libcimcclient0_2.2.8-0ubuntu2_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-server1t64_2.6.5-0ubuntu16_amd64.deb
|
'openwsman_2.6.5-0ubuntu16_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-sfcc/libcimcclient0_2.2.8-0ubuntu2_amd64.deb
|
'cim-schema_2.48.0-0ubuntu1_all.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/openwsman_2.6.5-0ubuntu16_amd64.deb
|
'libsfcutil0_1.0.1-0ubuntu4_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/multiverse/c/cim-schema/cim-schema_2.48.0-0ubuntu1_all.deb
|
'sfcb_1.4.9-0ubuntu7_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-sfc-common/libsfcutil0_1.0.1-0ubuntu4_amd64.deb
|
'libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/multiverse/s/sblim-sfcb/sfcb_1.4.9-0ubuntu7_amd64.deb
|
'libssl1.1_1.1.1w-0+deb11u1_amd64.deb'
|
||||||
wget -c http://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-cmpi-devel/libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb
|
)
|
||||||
wget -c http://ftp.us.debian.org/debian/pool/main/o/openssl/libssl1.1_1.1.1w-0+deb11u1_amd64.deb
|
local -a urls=(
|
||||||
dpkg -i libwsman-curl-client-transport1_2.6.5-0ubuntu16_amd64.deb
|
"${base_url}/universe/o/openwsman/${filenames[0]}"
|
||||||
dpkg -i libwsman-client4t64_2.6.5-0ubuntu16_amd64.deb
|
"${base_url}/universe/o/openwsman/${filenames[1]}"
|
||||||
dpkg -i libxml2-16_2.15.2+dfsg-0.1_amd64.deb
|
'https://snapshot.debian.org/file/32f51d914435fd29ce31af7ba17525f6276ea58d'
|
||||||
dpkg -i libwsman1t64_2.6.5-0ubuntu16_amd64.deb
|
"${base_url}/universe/o/openwsman/${filenames[3]}"
|
||||||
dpkg -i libwsman-server1t64_2.6.5-0ubuntu16_amd64.deb
|
"${base_url}/universe/o/openwsman/${filenames[4]}"
|
||||||
dpkg -i libcimcclient0_2.2.8-0ubuntu2_amd64.deb
|
"${base_url}/universe/s/sblim-sfcc/${filenames[5]}"
|
||||||
dpkg -i openwsman_2.6.5-0ubuntu16_amd64.deb
|
"${base_url}/universe/o/openwsman/${filenames[6]}"
|
||||||
dpkg -i cim-schema_2.48.0-0ubuntu1_all.deb
|
"${base_url}/multiverse/c/cim-schema/${filenames[7]}"
|
||||||
dpkg -i libsfcutil0_1.0.1-0ubuntu4_amd64.deb
|
"${base_url}/universe/s/sblim-sfc-common/${filenames[8]}"
|
||||||
dpkg -i sfcb_1.4.9-0ubuntu7_amd64.deb
|
"${base_url}/multiverse/s/sblim-sfcb/${filenames[9]}"
|
||||||
dpkg -i libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb
|
"${base_url}/universe/s/sblim-cmpi-devel/${filenames[10]}"
|
||||||
dpkg -i libssl1.1_1.1.1w-0+deb11u1_amd64.deb
|
"https://deb.debian.org/debian/pool/main/o/openssl/${filenames[11]}"
|
||||||
apt install -y srvadmin-all
|
)
|
||||||
/opt/dell/srvadmin/sbin/srvadmin-services.sh start
|
local -a checksums=(
|
||||||
rm -rf -- /tmp/omsa
|
'42fdd34722ac1304427f80c4176ee781d057f05669d485f0ee1da4d87df7488c'
|
||||||
|
'6d1855a2e8263e9a578b4c0bb7a963a6d99dc8d2ef41e287725799dcad0c6cb3'
|
||||||
echo -e "\n${idsCL[Green]}Dell OMSA has been installed${idsCL[Default]}"
|
'8571682a07f329bb462569502b57aced4866e5b95c2db3ec7e5414a5b3bbdc14'
|
||||||
echo -e "\n${idsCL[LightCyan]}Available at: ${idsCL[LightGreen]}https://${RNIP}:1311${idsCL[Default]}"
|
'61b91e8f234c5f2f87b4bce3534bc2f2304d50cd2fd95f426f12fc73d80e27b4'
|
||||||
FINISH_ACTION
|
'5d3f948ab605b4973b399f53bd62bddd70eb01161769a0d39a811399fe7c2daf'
|
||||||
|
'14b9ac374f88bd44e57395e87faa76d99d02e242c813fe30083d5bbfafec5870'
|
||||||
|
'62b30fcf41dae0c1d841f67a46049b7dfa4dfffe314e4226a776eb134605b7fc'
|
||||||
|
'a87d16d41e81092c7ada43824a97cf79fab18c4a3722ef6f0476ad697a3d9ab7'
|
||||||
|
'ba890cf5f2359befd3da1e5763672ef8b03d5424fa4e3d1ef21c9d52884af247'
|
||||||
|
'3eb5dce0a873f8eb77174fdd5e02ac55a989f7a73bd5ef8c8aae501d225d7524'
|
||||||
|
'284acfbb6d675496046ee46e6d5ea6c70ceafa3781cf1eece04f612cbadf117c'
|
||||||
|
'aadf8b4b197335645b230c2839b4517aa444fd2e8f434e5438c48a18857988f7'
|
||||||
|
)
|
||||||
|
local -a package_paths=()
|
||||||
|
|
||||||
|
echo
|
||||||
|
product_name="$(TAPM_SYSTEM_PRODUCT_NAME)"
|
||||||
|
if ! TAPM_OMSA_SUPPORTED_HARDWARE; then
|
||||||
|
echo -e "${idsCL[LightRed]}Dell OMSA legacy installation is limited to PowerEdge x30/x40 systems.${idsCL[Default]}"
|
||||||
|
echo "Detected system: ${product_name:-Unknown}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! TAPM_OMSA_SUPPORTED_PLATFORM; then
|
||||||
|
echo -e "${idsCL[LightRed]}This legacy OMSA package set requires PVE 9 on Debian 13 (Trixie), amd64.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${idsCL[LightYellow]}Installing legacy Dell OMSA 11.0 compatibility packages on ${product_name}.${idsCL[Default]}"
|
||||||
|
|
||||||
|
if ! TAPM_CREATE_TEMP_DIR omsa; then
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
temp_dir="$TAPM_TEMP_DIR"
|
||||||
|
dell_key="${temp_dir}/dell-openmanage.asc"
|
||||||
|
dell_keyring="${temp_dir}/linux.dell.com.gpg"
|
||||||
|
dell_source="${temp_dir}/linux.dell.com.list"
|
||||||
|
|
||||||
|
if ! TAPM_DOWNLOAD_SHA256 \
|
||||||
|
'https://linux.dell.com/repo/pgp_pubkeys/0x1285491434D8786F.asc' \
|
||||||
|
"$dell_key" \
|
||||||
|
'92f9622bf300f1fc8a4ef12d8e5efef6511b089c63c15e635cfc7429499e86d4' \
|
||||||
|
'Dell OpenManage signing key'; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
for index in "${!filenames[@]}"; do
|
||||||
|
package_paths+=("${temp_dir}/${filenames[$index]}")
|
||||||
|
if ! TAPM_DOWNLOAD_SHA256 "${urls[$index]}" "${package_paths[$index]}" \
|
||||||
|
"${checksums[$index]}" "${filenames[$index]}"; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! apt-get update ||
|
||||||
|
! DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||||
|
gnupg libcurl4t64 libncurses6 libxslt1.1 libgpm2 libtinfo6 ||
|
||||||
|
! gpg --batch --yes --dearmor --output "$dell_keyring" "$dell_key"; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
echo -e "${idsCL[LightRed]}Unable to prepare the Dell OMSA repository.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! printf '%s\n' \
|
||||||
|
'deb [signed-by=/etc/apt/keyrings/linux.dell.com.gpg] https://linux.dell.com/repo/community/openmanage/11000/jammy jammy main' \
|
||||||
|
>"$dell_source" ||
|
||||||
|
! mkdir -p /etc/apt/keyrings ||
|
||||||
|
! install -m 0644 "$dell_keyring" /etc/apt/keyrings/linux.dell.com.gpg ||
|
||||||
|
! install -m 0644 "$dell_source" /etc/apt/sources.list.d/linux.dell.com.list; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
echo -e "${idsCL[LightRed]}Unable to write the Dell OMSA repository configuration.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! apt-get update; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
echo -e "${idsCL[LightRed]}The Dell OMSA repository could not be refreshed.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! dpkg -i "${package_paths[@]}"; then
|
||||||
|
if ! DEBIAN_FRONTEND=noninteractive apt-get install --fix-broken -y ||
|
||||||
|
! dpkg -i "${package_paths[@]}"; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
echo -e "${idsCL[LightRed]}The legacy OMSA compatibility packages could not be installed.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y srvadmin-all ||
|
||||||
|
! TAPM_PACKAGE_INSTALLED srvadmin-all ||
|
||||||
|
[[ ! -x /opt/dell/srvadmin/sbin/srvadmin-services.sh ]] ||
|
||||||
|
! /opt/dell/srvadmin/sbin/srvadmin-services.sh start ||
|
||||||
|
! TAPM_WAIT_FOR_TCP_PORT 1311 30; then
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
echo -e "${idsCL[LightRed]}Dell OMSA installation failed or port 1311 did not become available.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
echo -e "\n${idsCL[Green]}Dell OMSA has been installed and verified.${idsCL[Default]}"
|
||||||
|
echo -e "\n${idsCL[LightCyan]}Available at: ${idsCL[LightGreen]}https://${RNIP}:1311${idsCL[Default]}"
|
||||||
|
FINISH_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
DOWNLOAD_VIRTIO() {
|
DOWNLOAD_VIRTIO() {
|
||||||
@@ -558,36 +792,198 @@ DOWNLOAD_VIRTIO() {
|
|||||||
FINISH_ACTION
|
FINISH_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
DETECT_CPU(){
|
TAPM_INSTALL_PROXCLMC() {
|
||||||
# if [ ! -f /etc/apt/sources.list.d/proxlb.list ]; then
|
local key_url='https://git.gyptazy.com/api/packages/gyptazy/debian/repository.key'
|
||||||
# echo "deb https://repo.gyptazy.com/stable /" > /etc/apt/sources.list.d/proxlb.list
|
local keyring='/etc/apt/keyrings/gyptazy.asc'
|
||||||
# wget -O /etc/apt/trusted.gpg.d/proxlb.asc https://repo.gyptazy.com/repository.gpg
|
local repository_file='/etc/apt/sources.list.d/gyptazy.list'
|
||||||
# apt-get update
|
local repository_line='deb [signed-by=/etc/apt/keyrings/gyptazy.asc] https://packages.gyptazy.com/api/packages/gyptazy/debian trixie main'
|
||||||
# fi
|
local temp_dir
|
||||||
if [ ! -f /etc/apt/sources.list.d/gyptazy.list ]; then
|
local temp_key
|
||||||
curl https://git.gyptazy.com/api/packages/gyptazy/debian/repository.key -o /etc/apt/keyrings/gyptazy.asc
|
|
||||||
echo "deb [signed-by=/etc/apt/keyrings/gyptazy.asc] https://packages.gyptazy.com/api/packages/gyptazy/debian trixie main" | sudo tee -a /etc/apt/sources.list.d/gyptazy.list
|
|
||||||
apt update
|
|
||||||
fi
|
|
||||||
if [ "$(dpkg -l | awk '/proxclmc/ {print }'|wc -l)" -eq 0 ]; then
|
|
||||||
apt -y install proxclmc
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
TAPM_PACKAGE_INSTALLED proxclmc && command -v proxclmc >/dev/null 2>&1 &&
|
||||||
|
return 0
|
||||||
|
|
||||||
|
TAPM_CREATE_TEMP_DIR proxclmc || return 1
|
||||||
|
temp_dir="$TAPM_TEMP_DIR"
|
||||||
|
temp_key="${temp_dir}/gyptazy.asc"
|
||||||
|
|
||||||
|
TAPM_DOWNLOAD_HTTPS "$key_url" "$temp_key" "ProxCLMC repository key" ||
|
||||||
|
return 1
|
||||||
|
if ! command -v gpg >/dev/null 2>&1 ||
|
||||||
|
! gpg --batch --show-keys "$temp_key" >/dev/null 2>&1; then
|
||||||
|
echo -e "${idsCL[LightRed]}The downloaded ProxCLMC repository key is not a valid OpenPGP key.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! install -d -m 0755 /etc/apt/keyrings ||
|
||||||
|
! install -m 0644 "$temp_key" "$keyring" ||
|
||||||
|
! printf '%s\n' "$repository_line" >"$repository_file" ||
|
||||||
|
! chmod 0644 "$repository_file"; then
|
||||||
|
echo -e "${idsCL[LightRed]}Could not configure the ProxCLMC package repository.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! apt-get update ||
|
||||||
|
! apt-get install -y proxclmc ||
|
||||||
|
! command -v proxclmc >/dev/null 2>&1; then
|
||||||
|
echo -e "${idsCL[LightRed]}ProxCLMC could not be installed.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAPM_CLEAN_TEMP_DIR "$temp_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_CLUSTER_QEMU_GUESTS() {
|
||||||
|
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
|
||||||
|
python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
for guest in json.load(sys.stdin):
|
||||||
|
if guest.get("type") != "qemu":
|
||||||
|
continue
|
||||||
|
print(
|
||||||
|
guest.get("vmid", ""),
|
||||||
|
str(guest.get("name", ""))
|
||||||
|
.replace("\x1f", " ")
|
||||||
|
.replace("\r", " ")
|
||||||
|
.replace("\n", " "),
|
||||||
|
guest.get("node", ""),
|
||||||
|
"yes" if guest.get("template") in (1, True, "1") else "no",
|
||||||
|
sep="\x1f",
|
||||||
|
)
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_QEMU_CPU_MODEL() {
|
||||||
|
local vmid="$1"
|
||||||
|
local config_output
|
||||||
|
local cpu_model
|
||||||
|
|
||||||
|
config_output="$(qm config "$vmid" 2>/dev/null)" || return 1
|
||||||
|
cpu_model="$(
|
||||||
|
awk -F': ' '$1 == "cpu" { print $2; exit }' <<< "$config_output"
|
||||||
|
)"
|
||||||
|
[[ -n "$cpu_model" ]] || cpu_model='kvm64'
|
||||||
|
printf '%s\n' "$cpu_model"
|
||||||
|
}
|
||||||
|
|
||||||
|
DETECT_CPU() {
|
||||||
|
local answer
|
||||||
|
local cpu_model
|
||||||
|
local current_cpu
|
||||||
|
local guest
|
||||||
|
local guest_output
|
||||||
|
local name
|
||||||
|
local node
|
||||||
|
local template
|
||||||
|
local vmid
|
||||||
|
local -a changes=()
|
||||||
|
local -a failures=()
|
||||||
|
local -a successes=()
|
||||||
|
|
||||||
|
for command in apt-get curl gpg install pvesh python3 qm; do
|
||||||
|
if ! command -v "$command" >/dev/null 2>&1; then
|
||||||
|
echo -e "${idsCL[LightRed]}${command} is required for CPU compatibility detection.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! TAPM_INSTALL_PROXCLMC; then
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${idsCL[LightCyan]}Analyzing CPU compatibility across the cluster...${idsCL[Default]}"
|
||||||
|
cpu_model="$(proxclmc --list-only 2>/dev/null)" || {
|
||||||
|
echo -e "${idsCL[LightRed]}ProxCLMC could not determine a compatible CPU model.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cpu_model="${cpu_model//$'\r'/}"
|
||||||
|
cpu_model="${cpu_model//$'\n'/}"
|
||||||
|
if [[ ! "$cpu_model" =~ ^x86-64-v(1|2-AES|3|4)$ ]]; then
|
||||||
|
echo -e "${idsCL[LightRed]}ProxCLMC returned an unexpected CPU model: ${cpu_model:-empty}.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
guest_output="$(TAPM_CLUSTER_QEMU_GUESTS)" || {
|
||||||
|
echo -e "${idsCL[LightRed]}Could not read cluster QEMU resources.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if [[ -z "$guest_output" ]]; then
|
||||||
|
echo -e "\n${idsCL[LightCyan]}No QEMU VMs or templates were found in the cluster.${idsCL[Default]}"
|
||||||
|
FINISH_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS=$'\x1f' read -r vmid name node template; do
|
||||||
|
[[ -n "$vmid" ]] || continue
|
||||||
|
current_cpu="$(TAPM_QEMU_CPU_MODEL "$vmid")" || {
|
||||||
|
echo -e "${idsCL[LightRed]}Could not read the CPU configuration for VMID ${vmid}.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
}
|
||||||
|
[[ "$current_cpu" == "$cpu_model" ]] && continue
|
||||||
|
changes+=(
|
||||||
|
"${vmid}"$'\x1f'"${name}"$'\x1f'"${node}"$'\x1f'"${template}"$'\x1f'"${current_cpu}"
|
||||||
|
)
|
||||||
|
done <<< "$guest_output"
|
||||||
|
|
||||||
|
if (( ${#changes[@]} == 0 )); then
|
||||||
|
echo -e "\n${idsCL[Green]}All QEMU VMs and templates already use ${cpu_model}.${idsCL[Default]}"
|
||||||
|
FINISH_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nRecommended cluster CPU model: %s\n' "$cpu_model"
|
||||||
|
printf '\n%-7s %-28s %-20s %-10s %-24s %s\n' \
|
||||||
|
'VMID' 'NAME' 'NODE' 'TEMPLATE' 'CURRENT CPU' 'PROPOSED CPU'
|
||||||
|
printf '%-7s %-28s %-20s %-10s %-24s %s\n' \
|
||||||
|
'-------' '----------------------------' '--------------------' \
|
||||||
|
'----------' '------------------------' '----------------'
|
||||||
|
for guest in "${changes[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid name node template current_cpu <<< "$guest"
|
||||||
|
printf '%-7s %-28.28s %-20.20s %-10s %-24.24s %s\n' \
|
||||||
|
"$vmid" "${name:-unnamed}" "$node" "$template" "$current_cpu" "$cpu_model"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -en "\n${idsCL[LightCyan]}Apply this CPU model to ${#changes[@]} VM(s) and template(s) (y/N)?${idsCL[Default]} "
|
||||||
|
read -r -n 1 answer
|
||||||
echo
|
echo
|
||||||
proxclmc
|
[[ "$answer" =~ ^[Yy]$ ]] || return
|
||||||
echo
|
|
||||||
echo -en "${idsCL[LightCyan]}Would you like to set '${idsCL[LightGreen]}cpu: $(proxclmc --list-only)${idsCL[LightCyan]}' on all VMs (y/N)?${idsCL[Default]} "
|
for guest in "${changes[@]}"; do
|
||||||
read -n 1 choice
|
IFS=$'\x1f' read -r vmid name node template current_cpu <<< "$guest"
|
||||||
case "$choice" in
|
if qm set "$vmid" --cpu "$cpu_model" &&
|
||||||
[Yy])
|
[[ "$(TAPM_QEMU_CPU_MODEL "$vmid")" == "$cpu_model" ]]; then
|
||||||
sed -i "/cpu:/c cpu: $(proxclmc --list-only)" /etc/pve/nodes/*/qemu-server/*.conf
|
successes+=("$guest")
|
||||||
echo
|
else
|
||||||
echo -e "\n${idsCL[Green]}All VM's have been reconfigured\n${idsCL[LightCyan]}This will require the VM's to be powered off and then turned back on in order to take effect${idsCL[Default]}"
|
failures+=("$guest")
|
||||||
FINISH_ACTION
|
fi
|
||||||
;;
|
done
|
||||||
*) echo;;
|
|
||||||
esac
|
printf '\nCPU model update summary:\n'
|
||||||
|
printf ' Successful: %d\n' "${#successes[@]}"
|
||||||
|
printf ' Failed: %d\n' "${#failures[@]}"
|
||||||
|
|
||||||
|
if (( ${#failures[@]} > 0 )); then
|
||||||
|
printf '\nFailed VM/template updates:\n'
|
||||||
|
for guest in "${failures[@]}"; do
|
||||||
|
IFS=$'\x1f' read -r vmid name node template current_cpu <<< "$guest"
|
||||||
|
printf ' %s (%s) on %s\n' "$vmid" "${name:-unnamed}" "$node"
|
||||||
|
done
|
||||||
|
echo -e "\n${idsCL[LightRed]}One or more CPU model updates failed.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${idsCL[Green]}The CPU model was updated to ${cpu_model}.${idsCL[Default]}"
|
||||||
|
echo -e "${idsCL[LightCyan]}Running VMs must be fully shut down and started again before the new CPU model takes effect.${idsCL[Default]}"
|
||||||
|
FINISH_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
RESTART_SERVICE_GROUP() {
|
RESTART_SERVICE_GROUP() {
|
||||||
@@ -674,26 +1070,86 @@ RESTART_PVE_SERVICES() {
|
|||||||
pvedaemon pveproxy pvestatd pvescheduler
|
pvedaemon pveproxy pvestatd pvescheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
MAINTENANCE_MODE(){
|
HA_NODE_IN_MAINTENANCE() {
|
||||||
|
local node="$1"
|
||||||
|
local ha_status
|
||||||
|
|
||||||
|
ha_status="$(ha-manager status 2>/dev/null)" || return 2
|
||||||
|
grep -F "lrm ${node} " <<< "$ha_status" |
|
||||||
|
grep -q "maintenance mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
WAIT_FOR_HA_MAINTENANCE_STATE() {
|
||||||
|
local node="$1"
|
||||||
|
local expected_state="$2"
|
||||||
|
local attempts="${3:-30}"
|
||||||
|
local attempt=0
|
||||||
|
local active=0
|
||||||
|
local state_result
|
||||||
|
|
||||||
|
while (( attempt < attempts )); do
|
||||||
|
active=0
|
||||||
|
HA_NODE_IN_MAINTENANCE "$node"
|
||||||
|
state_result=$?
|
||||||
|
[[ "$state_result" -eq 0 ]] && active=1
|
||||||
|
if [[ "$state_result" -gt 1 ]]; then
|
||||||
|
sleep 1
|
||||||
|
((attempt++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$expected_state" == "enabled" && "$active" -eq 1 ]] ||
|
||||||
|
[[ "$expected_state" == "disabled" && "$active" -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
MAINTENANCE_MODE() {
|
||||||
|
local choice
|
||||||
local local_node
|
local local_node
|
||||||
|
local maintenance_state
|
||||||
local_node="$(hostname -s)"
|
local_node="$(hostname -s)"
|
||||||
|
|
||||||
if ha-manager status | grep -F "$local_node" | grep -q "maintenance mode"; then
|
HA_NODE_IN_MAINTENANCE "$local_node"
|
||||||
|
maintenance_state=$?
|
||||||
|
if [[ "$maintenance_state" -gt 1 ]]; then
|
||||||
|
echo -e "\n${idsCL[LightRed]}Could not read HA maintenance status for ${local_node}.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$maintenance_state" -eq 0 ]]; then
|
||||||
echo -en "${idsCL[LightCyan]}Take the local host out of maintenance mode (Y/n)?${idsCL[Default]} "
|
echo -en "${idsCL[LightCyan]}Take the local host out of maintenance mode (Y/n)?${idsCL[Default]} "
|
||||||
else
|
else
|
||||||
echo -en "${idsCL[LightCyan]}Put the local host into maintenance mode (Y/n)?${idsCL[Default]} "
|
echo -en "${idsCL[LightCyan]}Put the local host into maintenance mode (Y/n)?${idsCL[Default]} "
|
||||||
fi
|
fi
|
||||||
read -n 1 choice
|
read -r -n 1 choice
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
[Nn]) echo;;
|
[Nn]) echo;;
|
||||||
*) echo
|
*) echo
|
||||||
if ha-manager status | grep -F "$local_node" | grep -q "maintenance mode"; then
|
if [[ "$maintenance_state" -eq 0 ]]; then
|
||||||
ha-manager crm-command node-maintenance disable "$local_node"
|
if ! ha-manager crm-command node-maintenance disable "$local_node" ||
|
||||||
|
! WAIT_FOR_HA_MAINTENANCE_STATE "$local_node" disabled; then
|
||||||
|
echo -e "\n${idsCL[LightRed]}Failed to take ${local_node} out of HA maintenance mode.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
echo -e "\n${idsCL[Green]}This host will be taken out of maintenance mode${idsCL[Default]}\n"
|
echo -e "\n${idsCL[Green]}This host will be taken out of maintenance mode${idsCL[Default]}\n"
|
||||||
else
|
else
|
||||||
ha-manager crm-command node-maintenance enable "$local_node"
|
if ! ha-manager crm-command node-maintenance enable "$local_node"; then
|
||||||
|
echo -e "\n${idsCL[LightRed]}Failed to request HA maintenance mode for ${local_node}.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
echo -e "\n${idsCL[Green]}This host will be entered into maintenance mode${idsCL[Default]}\n"
|
echo -e "\n${idsCL[Green]}This host will be entered into maintenance mode${idsCL[Default]}\n"
|
||||||
bash /opt/idssys/ta-proxmenu/inc/evacuate-proxmox-node.sh
|
if ! bash /opt/idssys/ta-proxmenu/inc/evacuate-proxmox-node.sh; then
|
||||||
|
echo -e "\n${idsCL[LightRed]}Evacuation did not complete. ${local_node} remains in HA maintenance mode.${idsCL[Default]}"
|
||||||
|
FINISH_FAILED_ACTION
|
||||||
|
return
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
FINISH_ACTION
|
FINISH_ACTION
|
||||||
;;
|
;;
|
||||||
@@ -991,19 +1447,15 @@ SHOW_ABOUT() {
|
|||||||
|
|
||||||
HOST_SETUP_MENU() {
|
HOST_SETUP_MENU() {
|
||||||
local -a labels
|
local -a labels
|
||||||
local -a values=(
|
local -a values
|
||||||
"post_install"
|
|
||||||
"cpu"
|
|
||||||
"virtio"
|
|
||||||
"glances"
|
|
||||||
"omsa"
|
|
||||||
)
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
labels=("Run ProxMenux post-install configuration")
|
labels=("Run ProxMenux post-install configuration")
|
||||||
|
values=("post_install")
|
||||||
[ -f /opt/.PROXMENUX_POST_INSTALL ] &&
|
[ -f /opt/.PROXMENUX_POST_INSTALL ] &&
|
||||||
labels[0]="Run ProxMenux post-install configuration (previously run)"
|
labels[0]="Run ProxMenux post-install configuration (previously run)"
|
||||||
labels+=("Detect CPU model for live migrations")
|
labels+=("Detect CPU model for live migrations")
|
||||||
|
values+=("cpu")
|
||||||
|
|
||||||
if [ -f "${DLDIR}/${VIRTIO_FILE}" ]; then
|
if [ -f "${DLDIR}/${VIRTIO_FILE}" ]; then
|
||||||
labels+=("VirtIO drivers (${VIRTIO_FILE} already downloaded)")
|
labels+=("VirtIO drivers (${VIRTIO_FILE} already downloaded)")
|
||||||
@@ -1012,14 +1464,19 @@ HOST_SETUP_MENU() {
|
|||||||
else
|
else
|
||||||
labels+=("Download current VirtIO drivers")
|
labels+=("Download current VirtIO drivers")
|
||||||
fi
|
fi
|
||||||
|
values+=("virtio")
|
||||||
|
|
||||||
command -v glances >/dev/null 2>&1 &&
|
command -v glances >/dev/null 2>&1 &&
|
||||||
labels+=("Glances CLI monitor (installed)") ||
|
labels+=("Glances CLI monitor (installed)") ||
|
||||||
labels+=("Install Glances CLI monitor")
|
labels+=("Install Glances CLI monitor")
|
||||||
|
values+=("glances")
|
||||||
|
|
||||||
dpkg-query -W -f='${Status}' srvadmin-all 2>/dev/null | grep -q "install ok installed" &&
|
if TAPM_OMSA_SUPPORTED_HARDWARE; then
|
||||||
labels+=("Dell OpenManage Server Administrator (installed)") ||
|
TAPM_PACKAGE_INSTALLED srvadmin-all &&
|
||||||
labels+=("Install Dell OpenManage Server Administrator")
|
labels+=("Dell OMSA - Legacy Dell Hosts (installed)") ||
|
||||||
|
labels+=("Install Dell OMSA - Legacy Dell Hosts")
|
||||||
|
values+=("omsa")
|
||||||
|
fi
|
||||||
|
|
||||||
SELECT_MENU "Host Setup" labels values
|
SELECT_MENU "Host Setup" labels values
|
||||||
case "$MENU_SELECTION" in
|
case "$MENU_SELECTION" in
|
||||||
|
|||||||
@@ -156,6 +156,77 @@ UPDATE_REPOSITORY() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SWITCH_TAPM_BRANCH() {
|
||||||
|
local target_branch="$1"
|
||||||
|
local repository='/opt/idssys/ta-proxmenu'
|
||||||
|
local current_branch
|
||||||
|
local target_state=''
|
||||||
|
|
||||||
|
if ! git check-ref-format --branch "$target_branch" >/dev/null 2>&1; then
|
||||||
|
echo -e "${idsCL[Red]}'${target_branch}' is not a valid Git branch name.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! -d "${repository}/.git" ]]; then
|
||||||
|
echo -e "${idsCL[Red]}TA-ProxMenu is not a Git repository.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_branch="$(git -C "$repository" branch --show-current 2>/dev/null)"
|
||||||
|
if [[ -z "$current_branch" ]]; then
|
||||||
|
echo -e "${idsCL[Red]}TA-ProxMenu is in a detached HEAD state; branch switching was refused.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$target_branch" == "$current_branch" ]]; then
|
||||||
|
echo -e "${idsCL[Green]}TA-ProxMenu is already using '${target_branch}'.${idsCL[Default]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if TAPM_GIT_WORKTREE_DIRTY "$repository"; then
|
||||||
|
echo -e "${idsCL[LightYellow]}TA-ProxMenu has local changes; branch switching was refused to preserve them.${idsCL[Default]}"
|
||||||
|
git -C "$repository" status --short
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${idsCL[LightCyan]}Fetching TA-ProxMenu branch '${target_branch}'...${idsCL[Default]}"
|
||||||
|
if ! TAPM_GIT_FETCH_BRANCH "$repository" "$target_branch" 30 >/dev/null 2>&1; then
|
||||||
|
echo -e "${idsCL[Red]}Could not fetch branch '${target_branch}' from origin.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git -C "$repository" show-ref --verify --quiet "refs/heads/${target_branch}"; then
|
||||||
|
target_state="$(
|
||||||
|
TAPM_GIT_RELATION "$repository" "refs/heads/${target_branch}" \
|
||||||
|
"refs/remotes/origin/${target_branch}"
|
||||||
|
)" || {
|
||||||
|
echo -e "${idsCL[Red]}Could not compare local and remote '${target_branch}'.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
case "$target_state" in
|
||||||
|
ahead)
|
||||||
|
echo -e "${idsCL[LightYellow]}Local branch '${target_branch}' has commits not on origin; switching was refused.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
diverged)
|
||||||
|
echo -e "${idsCL[LightYellow]}Local branch '${target_branch}' has diverged from origin; switching was refused.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
git -C "$repository" switch "$target_branch" >/dev/null || return 1
|
||||||
|
else
|
||||||
|
git -C "$repository" switch --create "$target_branch" \
|
||||||
|
--track "origin/${target_branch}" >/dev/null || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$target_state" == "behind" ]] &&
|
||||||
|
! TAPM_GIT_FAST_FORWARD "$repository" "$target_branch" >/dev/null; then
|
||||||
|
echo -e "${idsCL[Red]}Could not fast-forward '${target_branch}'; no commits were discarded.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /var/cache/ta-proxmenu/update-status 2>/dev/null || true
|
||||||
|
echo -e "${idsCL[Green]}TA-ProxMenu is now using branch '${target_branch}'.${idsCL[Default]}"
|
||||||
|
}
|
||||||
|
|
||||||
INSTALL_UPDATES() {
|
INSTALL_UPDATES() {
|
||||||
local current_branch
|
local current_branch
|
||||||
local defaults_warning=0
|
local defaults_warning=0
|
||||||
@@ -199,6 +270,10 @@ case "${1:-}" in
|
|||||||
INSTALL_UPDATES
|
INSTALL_UPDATES
|
||||||
exit $?
|
exit $?
|
||||||
;;
|
;;
|
||||||
|
main|V[0-9]*)
|
||||||
|
SWITCH_TAPM_BRANCH "$1"
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
tapm)
|
tapm)
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
|||||||
Reference in New Issue
Block a user