summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Hanreich <s.hanreich@proxmox.com>2025-07-29 11:29:29 +0200
committerThomas Lamprecht <t.lamprecht@proxmox.com>2025-07-29 12:59:46 +0200
commitf7d4f22a42a506e6cd60f0de8fbac9f580b85c6f (patch)
tree640fcfa41d99079fbdf1e9758cce0a955f855f33
parentd969147891eff88d6be121d10137d3a8a1c79201 (diff)
sdn: add global lock for configuration
Add a new cluster-wide lock for SDN that prevents any changes to the configuration if the generated lock-token is not provided. It works by generating and storing a token in sdn/.lock which gets checked by lock_sdn_config on every invocation. If the lock file exists, then the lock token has to be supplied in order to make changes to the SDN configuration. Lock using the domain lock (`PVE::Cluster::cfs_lock_domain`) and "sdn" string. This is mainly a preparation for PDM, where PDM can take the lock and prevent concurrent modifications to the SDN configuration from other sources, even across multiple API calls. Co-authored-by: Gabriel Goller <g.goller@proxmox.com> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com> Link: https://lore.proxmox.com/20250729092933.90118-2-g.goller@proxmox.com [TL: fix tests failing build as unprivileged users by mocking the cfs_domain_lock method] Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
-rw-r--r--debian/control1
-rw-r--r--src/PVE/Network/SDN.pm54
-rwxr-xr-xsrc/test/run_test_vnets_blackbox.pl26
3 files changed, 73 insertions, 8 deletions
diff --git a/debian/control b/debian/control
index f64a2d4..b45d126 100644
--- a/debian/control
+++ b/debian/control
@@ -26,6 +26,7 @@ Depends: libpve-common-perl (>= 9.0.2),
libnet-ip-perl,
libnetaddr-ip-perl,
libpve-rs-perl (>= 0.10.3),
+ libuuid-perl,
${misc:Depends},
${perl:Depends},
Recommends: frr-pythontools (>= 10.3.1-1+pve2~),
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 0e7d1df..9f18f76 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -8,12 +8,13 @@ use IO::Socket::SSL; # important for SSL_verify_callback
use JSON qw(decode_json from_json to_json);
use LWP::UserAgent;
use Net::SSLeay;
+use UUID;
use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
use PVE::INotify;
use PVE::RESTEnvironment qw(log_warn);
use PVE::RPCEnvironment;
-use PVE::Tools qw(extract_param dir_glob_regex run_command);
+use PVE::Tools qw(file_get_contents file_set_contents extract_param dir_glob_regex run_command);
use PVE::Network::SDN::Vnets;
use PVE::Network::SDN::Zones;
@@ -48,6 +49,8 @@ my $write_running_cfg = sub {
PVE::Cluster::cfs_register_file($running_cfg, $parse_running_cfg, $write_running_cfg);
+my $LOCK_TOKEN_FILE = "/etc/pve/sdn/.lock";
+
# improve me : move status code inside plugins ?
sub ifquery_check {
@@ -197,14 +200,57 @@ sub commit_config {
cfs_write_file($running_cfg, $cfg);
}
+sub generate_lock_token {
+ my $str;
+ my $uuid;
+
+ UUID::generate_v7($uuid);
+ UUID::unparse($uuid, $str);
+
+ return $str;
+}
+
+sub create_global_lock {
+ my $token = generate_lock_token();
+ PVE::Tools::file_set_contents($LOCK_TOKEN_FILE, $token);
+ return $token;
+}
+
+sub delete_global_lock {
+ unlink $LOCK_TOKEN_FILE if -e $LOCK_TOKEN_FILE;
+}
+
sub lock_sdn_config {
- my ($code, $errmsg) = @_;
+ my ($code, $errmsg, $lock_token_user) = @_;
- cfs_lock_file($running_cfg, undef, $code);
+ my $lock_wrapper = sub {
+ my $lock_token = undef;
+ if (-e $LOCK_TOKEN_FILE) {
+ $lock_token = PVE::Tools::file_get_contents($LOCK_TOKEN_FILE);
+ }
+
+ if (
+ defined($lock_token)
+ && (!defined($lock_token_user) || $lock_token ne $lock_token_user)
+ ) {
+ die "invalid lock token provided!";
+ }
+
+ return $code->();
+ };
- if (my $err = $@) {
+ return lock_sdn_domain($lock_wrapper, $errmsg);
+}
+
+sub lock_sdn_domain {
+ my ($code, $errmsg) = @_;
+
+ my $res = PVE::Cluster::cfs_lock_domain("sdn", undef, $code);
+ my $err = $@;
+ if ($err) {
$errmsg ? die "$errmsg: $err" : die $err;
}
+ return $res;
}
sub get_local_vnets {
diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnets_blackbox.pl
index 2e1c505..468b1ed 100755
--- a/src/test/run_test_vnets_blackbox.pl
+++ b/src/test/run_test_vnets_blackbox.pl
@@ -33,7 +33,10 @@ my $test_state = undef;
sub clear_test_state {
$test_state = {
- locks => {},
+ locks => {
+ file => {},
+ domain => {},
+ },
datacenter_config => {},
subnets_config => {},
controller_config => {},
@@ -57,13 +60,27 @@ clear_test_state();
my $mocked_cfs_lock_file = sub {
my ($filename, $timeout, $code, @param) = @_;
- die "$filename already locked\n" if ($test_state->{locks}->{$filename});
+ die "$filename already locked\n" if $test_state->{locks}->{file}->{$filename};
- $test_state->{locks}->{$filename} = 1;
+ $test_state->{locks}->{file}->{$filename} = 1;
my $res = eval { $code->(@param); };
- delete $test_state->{locks}->{$filename};
+ delete $test_state->{locks}->{file}->{$filename};
+
+ return $res;
+};
+
+my $mocked_cfs_lock_domain = sub {
+ my ($domain, $timeout, $code, @param) = @_;
+
+ die "domain '$domain' already locked\n" if $test_state->{locks}->{domain}->{$domain};
+
+ $test_state->{locks}->{domain}->{$domain} = 1;
+
+ my $res = eval { $code->(@param) };
+
+ delete $test_state->{locks}->{domain}->{$domain};
return $res;
};
@@ -224,6 +241,7 @@ $mocked_rpc_env_obj->mock(
my $mocked_pve_cluster_obj = Test::MockModule->new('PVE::Cluster');
$mocked_pve_cluster_obj->mock(
check_cfs_quorum => sub { return 1; },
+ cfs_lock_domain => $mocked_cfs_lock_domain,
);
# ------- TEST FUNCTIONS --------------