Files
TA-ProxMenu/inc/notifications-bridge.pl
David Schroeder 6f4c95e0f2 menu update
2026-09-24 19:22:04 -05:00

108 lines
4.5 KiB
Perl

#!/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";