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