This commit is contained in:
2026-07-25 17:48:04 -05:00
parent bd005495ce
commit d19400a732
5 changed files with 1314 additions and 250 deletions
+17 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+75
View File
@@ -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
;; ;;