menu update
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/perl
|
||||
# Internal bridge to Proxmox's notification and cluster configuration libraries.
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON;
|
||||
use PVE::Cluster;
|
||||
use PVE::DataCenterConfig;
|
||||
use PVE::INotify;
|
||||
use PVE::Notify;
|
||||
use PVE::Tools;
|
||||
|
||||
my $json = JSON->new->canonical;
|
||||
my $policy_file = '/etc/pve/priv/tapm-update-policy.json';
|
||||
my $receipts_file = '/etc/pve/priv/tapm-update-receipts.json';
|
||||
|
||||
sub read_json {
|
||||
my ($path) = @_;
|
||||
return {} if !-e $path;
|
||||
return $json->decode(PVE::Tools::file_get_contents($path));
|
||||
}
|
||||
|
||||
sub snapshot {
|
||||
PVE::Cluster::cfs_update();
|
||||
my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
return { policy => read_json($policy_file), notify => $dc->{notify} // {} };
|
||||
}
|
||||
|
||||
sub locked {
|
||||
my ($code) = @_;
|
||||
my $result;
|
||||
PVE::Cluster::cfs_lock_domain('tapm-update-notifications', 10, sub {
|
||||
$result = $code->();
|
||||
});
|
||||
die $@ if $@;
|
||||
return $result;
|
||||
}
|
||||
|
||||
my $result;
|
||||
my $ok = eval {
|
||||
die "root required" if $> != 0;
|
||||
PVE::INotify::inotify_init();
|
||||
my $input = do { local $/; <STDIN> };
|
||||
my $request = $json->decode($input);
|
||||
my $action = $request->{action} // '';
|
||||
if ($action eq 'read') {
|
||||
$result = snapshot();
|
||||
} elsif ($action eq 'apply') {
|
||||
$result = locked(sub {
|
||||
my $current = snapshot();
|
||||
die "configuration changed" if $json->encode($current) ne $json->encode($request->{expected});
|
||||
my $policy = $request->{policy};
|
||||
die "invalid mode" if ($policy->{mode} // '') !~ /\A(?:all|version|disabled)\z/;
|
||||
die "invalid owner" if ($policy->{owner} // '') !~ /\A[A-Za-z0-9][A-Za-z0-9.-]{0,62}\z/;
|
||||
my $response;
|
||||
PVE::Cluster::cfs_lock_file('datacenter.cfg', 10, sub {
|
||||
my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
die "notification settings changed" if $json->encode($dc->{notify} // {}) ne $json->encode($current->{notify});
|
||||
# Preserve all unrelated notification options and datacenter settings.
|
||||
my $before = $json->decode($json->encode($dc));
|
||||
PVE::Tools::file_set_contents('/etc/pve/priv/tapm-update-policy.previous.json', $json->encode($current));
|
||||
$dc->{notify} //= {};
|
||||
$dc->{notify}->{'package-updates'} = $policy->{mode} eq 'all' ? 'always' : 'never';
|
||||
PVE::Cluster::cfs_write_file('datacenter.cfg', $dc);
|
||||
eval { PVE::Tools::file_set_contents($policy_file, $json->encode($policy)); };
|
||||
if (my $err = $@) {
|
||||
PVE::Cluster::cfs_write_file('datacenter.cfg', $before);
|
||||
die $err;
|
||||
}
|
||||
$response = { applied => JSON::true };
|
||||
});
|
||||
die $@ if $@;
|
||||
return $response;
|
||||
});
|
||||
} elsif ($action eq 'send') {
|
||||
$result = locked(sub {
|
||||
my $current = snapshot();
|
||||
my $policy = $current->{policy};
|
||||
# An old scheduler must not send after a policy/owner change.
|
||||
return { sent => JSON::false } if ($policy->{mode} // '') ne 'version'
|
||||
|| ($policy->{owner} // '') ne PVE::INotify::nodename()
|
||||
|| ($policy->{generation} // '') ne ($request->{generation} // '')
|
||||
|| ($current->{notify}->{'package-updates'} // '') ne 'never';
|
||||
my $node = $request->{node};
|
||||
die "invalid node" if ($node // '') !~ /\A[A-Za-z0-9][A-Za-z0-9.-]{0,62}\z/;
|
||||
for my $key (qw(installed available)) {
|
||||
die "invalid version" if ($request->{$key} // '') !~ /\A[0-9][A-Za-z0-9.+:~\-]{0,100}\z/;
|
||||
}
|
||||
my $receipts = read_json($receipts_file);
|
||||
# Keep history so repository switches do not re-announce an old candidate.
|
||||
return { sent => JSON::false } if $receipts->{$node}->{$request->{available}};
|
||||
my $data = { hostname => $node, installed => $request->{installed}, available => $request->{available} };
|
||||
PVE::Notify::info('tapm-pve-version', $data, { type => 'package-updates', hostname => $node });
|
||||
$receipts->{$node}->{$request->{available}} = time();
|
||||
PVE::Tools::file_set_contents($receipts_file, $json->encode($receipts));
|
||||
return { sent => JSON::true };
|
||||
});
|
||||
} else {
|
||||
die "unknown operation";
|
||||
}
|
||||
1;
|
||||
};
|
||||
if (!$ok) {
|
||||
# Do not expose package names, credentials or raw backend errors in output.
|
||||
print STDERR "Proxmox notification operation failed; verify cluster quorum, configuration and notification delivery.\n";
|
||||
exit 1;
|
||||
}
|
||||
print $json->encode($result), "\n";
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Interactive entry points; backend operations run only after selection.
|
||||
|
||||
TAPM_NOTIFICATION_ACTION() {
|
||||
python3 /opt/idssys/ta-proxmenu/inc/notifications.py "$@"
|
||||
local result=$?
|
||||
ENTER2CONTINUE
|
||||
return "$result"
|
||||
}
|
||||
|
||||
TAPM_UPDATE_NOTIFICATION_MENU() {
|
||||
local -a labels=(
|
||||
"All available package updates"
|
||||
"Proxmox VE version updates only"
|
||||
"Disable update notifications"
|
||||
)
|
||||
local -a values=(all version disabled)
|
||||
while true; do
|
||||
SELECT_MENU "Update Notification Policy" labels values
|
||||
case "$MENU_SELECTION" in
|
||||
all|version|disabled) TAPM_NOTIFICATION_ACTION apply "$MENU_SELECTION" ;;
|
||||
back) return ;;
|
||||
quit) EXIT1; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
TAPM_NOTIFICATIONS_MENU() {
|
||||
local -a labels=(
|
||||
"Update notification policy"
|
||||
"Review notification settings and destinations"
|
||||
"Send a test notification"
|
||||
)
|
||||
local -a values=(policy delivery test)
|
||||
while true; do
|
||||
SELECT_MENU "Proxmox Notifications" labels values
|
||||
case "$MENU_SELECTION" in
|
||||
policy) TAPM_UPDATE_NOTIFICATION_MENU ;;
|
||||
delivery) TAPM_NOTIFICATION_ACTION delivery ;;
|
||||
test) TAPM_NOTIFICATION_ACTION test ;;
|
||||
back) return ;;
|
||||
quit) EXIT1; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""TAPM notification policy UI and scheduled Proxmox VE version checks."""
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
UNIT_DIR = Path('/etc/systemd/system')
|
||||
TEMPLATE_DIR = Path('/etc/pve/notification-templates/default')
|
||||
STATE_DIR = Path('/var/lib/ta-proxmenu/notifications')
|
||||
SERVICE = 'tapm-version-notifications'
|
||||
LABELS = {
|
||||
'all': 'All available package updates',
|
||||
'version': 'Proxmox VE version updates only',
|
||||
'disabled': 'Disable update notifications',
|
||||
}
|
||||
NAME = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,62}\Z')
|
||||
VERSION = re.compile(r'[0-9][A-Za-z0-9.+:~\-]{0,100}\Z')
|
||||
|
||||
|
||||
class NotificationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def command(args, payload=None, timeout=30):
|
||||
try:
|
||||
result = subprocess.run(args, input=payload, capture_output=True,
|
||||
text=True, timeout=timeout)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise NotificationError('The requested notification operation could not be completed.') from exc
|
||||
if result.returncode:
|
||||
raise NotificationError('The requested notification operation failed. Check Proxmox configuration and connectivity.')
|
||||
return result.stdout
|
||||
|
||||
|
||||
def api(method, path, **params):
|
||||
args = ['pvesh', method, path, '--output-format', 'json']
|
||||
for key, value in params.items():
|
||||
args += ['--' + key.replace('_', '-'), str(value)]
|
||||
raw = command(args)
|
||||
try:
|
||||
return json.loads(raw) if raw.strip() else None
|
||||
except ValueError as exc:
|
||||
raise NotificationError('Proxmox returned an unreadable response.') from exc
|
||||
|
||||
|
||||
def bridge(action, **params):
|
||||
raw = command(['perl', str(HERE / 'notifications-bridge.pl')],
|
||||
json.dumps(dict(action=action, **params)), timeout=75)
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except ValueError as exc:
|
||||
raise NotificationError('Notification configuration could not be read.') from exc
|
||||
|
||||
|
||||
def local_node():
|
||||
name = command(['hostname', '-s']).strip()
|
||||
if not NAME.fullmatch(name):
|
||||
raise NotificationError('The host name is not valid.')
|
||||
return name
|
||||
|
||||
|
||||
def effective_mode(snapshot):
|
||||
policy = snapshot.get('policy', {})
|
||||
native = snapshot.get('notify', {}).get('package-updates', 'auto')
|
||||
if policy.get('mode') == 'version' and native == 'never':
|
||||
return 'version'
|
||||
return {'always': 'all', 'never': 'disabled'}.get(native, 'auto')
|
||||
|
||||
|
||||
def current_status():
|
||||
snapshot = bridge('read')
|
||||
mode = effective_mode(snapshot)
|
||||
print('Update notification policy: ' + LABELS.get(mode, 'Proxmox default (subscription-dependent)'))
|
||||
print('Scope: this Proxmox datacenter (all nodes when clustered).')
|
||||
if mode == 'version':
|
||||
owner = snapshot['policy'].get('owner', 'unknown')
|
||||
print('Hourly version checker host: ' + owner)
|
||||
if owner == local_node():
|
||||
try:
|
||||
command(['systemctl', 'is-enabled', SERVICE + '.timer'])
|
||||
command(['systemctl', 'is-active', SERVICE + '.timer'])
|
||||
print('Version checker timer: enabled and active')
|
||||
except NotificationError:
|
||||
print('Version checker timer: needs attention; reapply the version-only policy.')
|
||||
else:
|
||||
print('Timer status must be checked on that host; checks pause while it is offline.')
|
||||
return snapshot
|
||||
|
||||
|
||||
def atomic_write(path, content):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + '.' + uuid.uuid4().hex + '.tmp')
|
||||
try:
|
||||
with temporary.open('x') as stream:
|
||||
stream.write(content)
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
if temporary.exists():
|
||||
temporary.unlink()
|
||||
|
||||
|
||||
def prepare_checker():
|
||||
# Separate unit: never disable or replace Proxmox's daily update service.
|
||||
service = f'''[Unit]
|
||||
Description=TAPM Proxmox VE version notifications
|
||||
After=pve-cluster.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 {HERE}/notifications.py run
|
||||
TimeoutStartSec=15min
|
||||
UMask=0077
|
||||
'''
|
||||
timer = '''[Unit]
|
||||
Description=Check for Proxmox VE version updates hourly
|
||||
|
||||
[Timer]
|
||||
OnCalendar=hourly
|
||||
RandomizedDelaySec=5min
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
'''
|
||||
atomic_write(UNIT_DIR / (SERVICE + '.service'), service)
|
||||
atomic_write(UNIT_DIR / (SERVICE + '.timer'), timer)
|
||||
templates = {
|
||||
'subject.txt': 'Proxmox VE version update available on {{hostname}}\n',
|
||||
'body.txt': 'Host: {{hostname}}\nInstalled Proxmox VE version: {{installed}}\nAvailable Proxmox VE version: {{available}}\n\nA newer version is available from this host\'s configured repositories.\n',
|
||||
'body.html': '<p>Host: {{hostname}}</p><p>Installed Proxmox VE version: {{installed}}<br>Available Proxmox VE version: {{available}}</p><p>A newer version is available from this host's configured repositories.</p>\n',
|
||||
}
|
||||
for suffix, content in templates.items():
|
||||
atomic_write(TEMPLATE_DIR / ('tapm-pve-version-' + suffix + '.hbs'), content)
|
||||
command(['systemctl', 'daemon-reload'])
|
||||
command(['systemctl', 'enable', '--now', SERVICE + '.timer'])
|
||||
command(['systemctl', 'is-active', SERVICE + '.timer'])
|
||||
|
||||
|
||||
def apply_policy(mode):
|
||||
expected = current_status()
|
||||
owner = local_node()
|
||||
print('\nProposed policy: ' + LABELS[mode])
|
||||
print('This changes update alerts across this datacenter. Other notification types retain their settings.')
|
||||
print('Normal Proxmox repository checks remain enabled. No packages will be upgraded.')
|
||||
if mode == 'version':
|
||||
print(f'An hourly checker on {owner} will inspect online nodes using their existing repository indexes.')
|
||||
print('Checks pause while this host is offline; reapply here to repair, or on another host to move the checker.')
|
||||
print('Only new Proxmox VE versions trigger alerts, including patch releases. Kernel-only updates do not.')
|
||||
print('Existing update notification routing and destinations are used. No post-upgrade alerts are sent.')
|
||||
elif mode == 'all':
|
||||
print('The native Proxmox daily update job sends alerts through existing notification routing.')
|
||||
if input('Apply this policy (type yes to continue)? ').strip().lower() != 'yes':
|
||||
print('No changes made.')
|
||||
return
|
||||
if mode == 'version':
|
||||
prepare_checker()
|
||||
policy = dict(mode=mode, owner=owner, generation=uuid.uuid4().hex)
|
||||
bridge('apply', expected=expected, policy=policy)
|
||||
print('Update notification policy applied: ' + LABELS[mode])
|
||||
if mode != 'version' and (UNIT_DIR / (SERVICE + '.timer')).exists():
|
||||
try:
|
||||
command(['systemctl', 'disable', '--now', SERVICE + '.timer'])
|
||||
except NotificationError:
|
||||
print('The local timer could not be stopped. It will remain inactive in this notification mode.')
|
||||
print('Previous policy saved in /etc/pve/priv/tapm-update-policy.previous.json.')
|
||||
|
||||
|
||||
def candidate_update(rows):
|
||||
if not isinstance(rows, list):
|
||||
raise NotificationError('The update list could not be read.')
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or row.get('Package') != 'pve-manager':
|
||||
continue
|
||||
installed, available = row.get('OldVersion', ''), row.get('Version', '')
|
||||
if not isinstance(installed, str) or not isinstance(available, str) or not VERSION.fullmatch(installed) or not VERSION.fullmatch(available):
|
||||
raise NotificationError('The Proxmox VE versions could not be read.')
|
||||
# Use Debian version ordering, including epochs, revisions and tildes.
|
||||
comparison = subprocess.run(['dpkg', '--compare-versions', available, 'gt', installed],
|
||||
capture_output=True, timeout=10)
|
||||
if comparison.returncode == 0:
|
||||
return installed, available
|
||||
if comparison.returncode != 1:
|
||||
raise NotificationError('The Proxmox VE versions could not be compared.')
|
||||
return None
|
||||
|
||||
|
||||
def check_versions():
|
||||
snapshot = bridge('read')
|
||||
if effective_mode(snapshot) != 'version' or snapshot['policy'].get('owner') != local_node():
|
||||
return 0
|
||||
nodes = api('get', '/nodes')
|
||||
if not isinstance(nodes, list) or not nodes:
|
||||
raise NotificationError('The Proxmox node list could not be read.')
|
||||
failures = 0
|
||||
for node in nodes:
|
||||
name = node.get('node', '') if isinstance(node, dict) else ''
|
||||
if not NAME.fullmatch(name):
|
||||
failures += 1
|
||||
continue
|
||||
if node.get('status') != 'online':
|
||||
print(f'{name}: offline; version check deferred.')
|
||||
failures += 1
|
||||
continue
|
||||
try:
|
||||
versions = candidate_update(api('get', f'/nodes/{name}/apt/update'))
|
||||
if not versions:
|
||||
continue
|
||||
installed, available = versions
|
||||
result = bridge('send', generation=snapshot['policy']['generation'],
|
||||
node=name, installed=installed, available=available)
|
||||
if result.get('sent'):
|
||||
print(f'{name}: Proxmox VE {installed} -> {available}; notification submitted to configured routing.')
|
||||
except (NotificationError, OSError, subprocess.TimeoutExpired):
|
||||
print(f'{name}: version check or notification failed; will retry on the next check.')
|
||||
failures += 1
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
def delivery_status():
|
||||
current_status()
|
||||
targets = api('get', '/cluster/notifications/targets')
|
||||
matchers = api('get', '/cluster/notifications/matchers')
|
||||
print('\nNotification destinations:')
|
||||
for target in targets:
|
||||
state = 'disabled' if target.get('disable') else 'enabled'
|
||||
print(f" {target['name']} ({target['type']}, {state})")
|
||||
if not targets:
|
||||
print(' None configured.')
|
||||
print('\nRouting rules:')
|
||||
for matcher in matchers:
|
||||
state = 'disabled' if matcher.get('disable') else 'enabled'
|
||||
print(f" {matcher['name']} ({state})")
|
||||
for key in ('target', 'match-field', 'match-severity', 'match-calendar', 'mode', 'invert-match'):
|
||||
if key in matcher:
|
||||
print(f" {key}: {matcher[key]}")
|
||||
print('\nConfigure recipients, delivery credentials and routing in the Proxmox Web UI: Datacenter > Notifications.')
|
||||
print('Version-only alerts use the existing system-update routing rules.')
|
||||
|
||||
|
||||
def test_delivery():
|
||||
targets = [t for t in api('get', '/cluster/notifications/targets') if not t.get('disable')]
|
||||
if not targets:
|
||||
print('No enabled notification destinations. Configure one in Datacenter > Notifications first.')
|
||||
return
|
||||
for index, target in enumerate(targets, 1):
|
||||
print(f" {index}. {target['name']} ({target['type']})")
|
||||
choice = input('Destination number (Enter to cancel): ').strip()
|
||||
if not choice:
|
||||
return
|
||||
if not choice.isdigit() or not 1 <= int(choice) <= len(targets):
|
||||
raise NotificationError('Choose a destination from the list.')
|
||||
target = targets[int(choice) - 1]['name']
|
||||
if not NAME.fullmatch(target):
|
||||
raise NotificationError('The destination name is not valid.')
|
||||
if input(f'Send a test notification to {target} (type yes to continue)? ').strip().lower() != 'yes':
|
||||
return
|
||||
api('create', f'/cluster/notifications/targets/{target}/test')
|
||||
print('Test notification submitted. Confirm receipt at the destination; this tests delivery, not routing rules.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Proxmox notification settings')
|
||||
parser.add_argument('action', choices=['status', 'delivery', 'test', 'apply', 'run'])
|
||||
parser.add_argument('mode', nargs='?', choices=list(LABELS))
|
||||
args = parser.parse_args()
|
||||
if os.geteuid() != 0:
|
||||
print('Run this action as root on a Proxmox host.', file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
if args.action == 'status':
|
||||
current_status()
|
||||
elif args.action == 'delivery':
|
||||
delivery_status()
|
||||
elif args.action == 'test':
|
||||
test_delivery()
|
||||
elif args.action == 'apply':
|
||||
if args.mode is None:
|
||||
raise NotificationError('Select an update notification policy.')
|
||||
apply_policy(args.mode)
|
||||
else:
|
||||
STATE_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
with (STATE_DIR / 'check.lock').open('a') as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
return 0
|
||||
return check_versions()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print('\nCancelled.')
|
||||
return 1
|
||||
except (NotificationError, OSError, ValueError, KeyError, TypeError) as exc:
|
||||
print(str(exc) if isinstance(exc, NotificationError) else 'Notification operation failed; verify configuration and permissions.', file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+22
-14
@@ -67,34 +67,42 @@ TAPM_UI_MENU_META() {
|
||||
TAPM_UI_KIND=task
|
||||
TAPM_UI_HINT=''
|
||||
case "$title:$value" in
|
||||
'Main Menu:'*|'Host Setup:post_install'|'Host Setup:virtio'|\
|
||||
'Cluster & Maintenance:services'|'TA-ProxMenu Management:branches'|\
|
||||
'TAPM Host Configuration:customize'|'TAPM Host Configuration:vzdump'|\
|
||||
'TAPM Host Configuration:restore'|'Configure vzdump Performance:restore')
|
||||
'Main Menu:'*|'Host Configuration & Recovery:post_install'|'Storage, ISOs & Backups:virtio'|\
|
||||
'Host Configuration & Recovery:services'|'TAPM Management:branches'|\
|
||||
'TAPM Host Profile:customize'|'Storage, ISOs & Backups:vzdump'|\
|
||||
'TAPM Host Profile:restore'|'Proxmox Backup Settings:restore'|\
|
||||
'Monitoring & Agents:notifications'|'Proxmox Notifications:policy')
|
||||
TAPM_UI_KIND=submenu ;;
|
||||
'Select ISO Storage:'*|'Restore TAPM Host Configuration Backup:'*|\
|
||||
'Git Branch Management:'*|'Configure vzdump Performance:'*)
|
||||
'Switch TAPM Git Branch:'*|'Proxmox Backup Settings:'*|\
|
||||
'Update Notification Policy:'*)
|
||||
TAPM_UI_KIND=choice ;;
|
||||
'Customize TAPM Host Profile:'*)
|
||||
[[ "$value" == apply ]] || TAPM_UI_KIND=choice ;;
|
||||
'TA-ProxMenu Management:install_update')
|
||||
'TAPM Management:install_update')
|
||||
# The update banner is printed after the user confirms installation.
|
||||
TAPM_UI_KIND=choice ;;
|
||||
esac
|
||||
case "$value" in back|quit) TAPM_UI_KIND=navigation ;; esac
|
||||
case "$title:$value" in
|
||||
'Cluster & Maintenance:maintenance')
|
||||
TAPM_UI_HINT='Applies to this host; entering maintenance evacuates guests.' ;;
|
||||
'Cluster & Maintenance:keepalived')
|
||||
'Cluster & High Availability:maintenance')
|
||||
TAPM_UI_HINT='This host: eligible guests migrate; local-storage guests shut down gracefully.' ;;
|
||||
'Cluster & High Availability:keepalived')
|
||||
TAPM_UI_HINT='Deploy or reconfigure Keepalived across cluster hosts.' ;;
|
||||
'Cluster & Maintenance:iso_nfs')
|
||||
'Storage, ISOs & Backups:iso_nfs')
|
||||
TAPM_UI_HINT='Create an LXC NFS server and register shared ISO storage.' ;;
|
||||
'Cluster & Maintenance:orphan_disks')
|
||||
'Storage, ISOs & Backups:orphan_disks')
|
||||
TAPM_UI_HINT='Read-only cluster scan with optional CSV export.' ;;
|
||||
'Host Setup:cpu')
|
||||
'Cluster & High Availability:cpu')
|
||||
TAPM_UI_HINT='Analyze migration compatibility; review before applying a CPU model.' ;;
|
||||
'TA-ProxMenu Management:install_update')
|
||||
TAPM_UI_HINT="Update status: ${UPDATE_STATUS:-unknown}" ;;
|
||||
'Host Configuration & Recovery:services')
|
||||
TAPM_UI_HINT='Restart Proxmox management services on this host.' ;;
|
||||
'Storage, ISOs & Backups:vzdump')
|
||||
TAPM_UI_HINT='Set backup bandwidth and I/O priority on this host.' ;;
|
||||
'Proxmox Notifications:policy')
|
||||
TAPM_UI_HINT='Choose all updates, Proxmox VE version updates only, or no update alerts.' ;;
|
||||
'TAPM Management:install_update')
|
||||
TAPM_UI_HINT='Update TAPM on online cluster nodes, or on this standalone host.' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user