From: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-firewall v4 10/27] firewall: api: run reference updates as tasks
Date: Thu, 22 Oct 2026 14:46:51 +0200 [thread overview]
Message-ID: <20261022124708.429069-11-a.bied-charreton@proxmox.com> (raw)
In-Reply-To: <20261022124708.429069-1-a.bied-charreton@proxmox.com>
A 'dangling-references' action other than 'keep' rewrites the rules that
referenced the object, which for a cluster-scoped object means going
through all guest, host and vnet firewall configs in the cluster,
potentially updating each of them.
The update_refs() helper logs each change made to each config. Fork
workers for renames and removals in order for the logs to land in the
task log, which is visible cluster-wide, instead of the syslog, which
is only visible on the node that happened to handle the request. Plain
creates or updates that only change a comment still run synchronously.
Checks that can be performed without holding the lock are made before
forking so a rejected request can still fail right away instead of
requiring the caller to go read the task.
Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
src/PVE/API2/Firewall/Aliases.pm | 238 +++++++++++++++++-------------
src/PVE/API2/Firewall/IPSet.pm | 241 ++++++++++++++++++-------------
src/PVE/Firewall/References.pm | 20 +++
3 files changed, 297 insertions(+), 202 deletions(-)
diff --git a/src/PVE/API2/Firewall/Aliases.pm b/src/PVE/API2/Firewall/Aliases.pm
index cb9b448..36d091e 100644
--- a/src/PVE/API2/Firewall/Aliases.pm
+++ b/src/PVE/API2/Firewall/Aliases.pm
@@ -3,7 +3,7 @@ package PVE::API2::Firewall::AliasesBase;
use strict;
use warnings;
-use PVE::Firewall::References qw(update_refs);
+use PVE::Firewall::References qw(update_refs run_ref_update);
use PVE::Exception qw(raise raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
@@ -84,6 +84,36 @@ my $aliases_to_list = sub {
return $list;
};
+my $check_digest = sub {
+ my ($aliases, $param) = @_;
+
+ my $list = &$aliases_to_list($aliases);
+ my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
+
+ PVE::Tools::assert_if_modified($digest, $param->{digest});
+};
+
+my $check_update = sub {
+ my ($aliases, $param, $dangling_refs) = @_;
+
+ $check_digest->($aliases, $param);
+
+ my $name = lc($param->{name});
+ raise_param_exc({ name => "no such alias" }) if !$aliases->{$name};
+
+ my $rename = $param->{rename};
+ $rename = lc($rename) if $rename;
+
+ if (
+ $rename
+ && $name ne $rename
+ && defined($aliases->{$rename})
+ && $dangling_refs ne 'force-rename'
+ ) {
+ raise_param_exc({ name => "alias '$param->{rename}' already exists" });
+ }
+};
+
sub register_get_aliases {
my ($class) = @_;
@@ -245,87 +275,93 @@ sub register_update_alias {
additionalProperties => 0,
properties => $properties,
},
- returns => { type => "null" },
+ returns => { type => 'string', optional => 1 },
code => sub {
my ($param) = @_;
- $class->lock_config(
- $param,
- sub {
- my ($param) = @_;
-
- my $dangling_refs = $param->{'dangling-references'} // 'keep';
-
- # 'force-rename' only differs in skipping the check above, the references are
- # renamed either way
- my $ref_action;
- if ($dangling_refs eq 'force-rename') {
- $ref_action = 'rename';
- } elsif ($dangling_refs ne 'keep') {
- $ref_action = $dangling_refs;
- }
-
- my ($fw_conf, $aliases) = $class->load_config($param);
-
- my $list = &$aliases_to_list($aliases);
-
- my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
-
- PVE::Tools::assert_if_modified($digest, $param->{digest});
-
- my $name = lc($param->{name});
-
- raise_param_exc({ name => "no such alias" }) if !$aliases->{$name};
-
- my $data = { name => $param->{name}, cidr => $param->{cidr} };
- $data->{comment} = $param->{comment} if $param->{comment};
+ my $dangling_refs = $param->{'dangling-references'} // 'keep';
+
+ # 'force-rename' only differs in skipping the check above, the references are renamed
+ # either way
+ my $ref_action;
+ if ($dangling_refs eq 'force-rename') {
+ $ref_action = 'rename';
+ } elsif ($dangling_refs ne 'keep') {
+ $ref_action = $dangling_refs;
+ }
+
+ my $renaming = $param->{rename} && lc($param->{name}) ne lc($param->{rename});
+ my $subject =
+ $renaming ? "alias '$param->{name}' renamed to '$param->{rename}'" : undef;
+
+ my $code = sub {
+ $class->lock_config(
+ $param,
+ sub {
+ my ($param) = @_;
+
+ my ($fw_conf, $aliases) = $class->load_config($param);
+
+ $check_update->($aliases, $param, $dangling_refs);
+
+ my $name = lc($param->{name});
+
+ my $data = { name => $param->{name}, cidr => $param->{cidr} };
+ $data->{comment} = $param->{comment} if $param->{comment};
+
+ $aliases->{$name} = $data;
+
+ my $rename = $param->{rename};
+ $rename = lc($rename) if $rename;
+
+ if ($rename && ($name ne $rename)) {
+ $aliases->{$rename} =
+ { $aliases->{$name}->%*, name => $param->{rename} };
+
+ my $saved = 0;
+ my $save = sub {
+ $class->save_aliases($param, $fw_conf, $aliases);
+ $saved = 1;
+ };
+
+ eval {
+ update_refs({
+ conf => $fw_conf,
+ kind => 'aliases',
+ old => $name,
+ new => $param->{rename},
+ env => $class->rule_env(),
+ action => $ref_action,
+ subject => $subject,
+ save => $save,
+ });
+ };
+ if (my $err = $@) {
+ die "rename interrupted, the alias exists under both names; "
+ . "retry with 'force-rename' to finish: $err"
+ if $saved;
+ die "could not update references to alias '$name': $err";
+ }
+
+ delete $aliases->{$name};
+ }
- $aliases->{$name} = $data;
+ $class->save_aliases($param, $fw_conf, $aliases);
+ },
+ );
+ };
- my $rename = $param->{rename};
- $rename = lc($rename) if $rename;
-
- if ($rename && ($name ne $rename)) {
- raise_param_exc({ name => "alias '$param->{rename}' already exists" })
- if defined($aliases->{$rename})
- && $dangling_refs ne 'force-rename';
-
- $aliases->{$rename} =
- { $aliases->{$name}->%*, name => $param->{rename} };
-
- my $saved = 0;
- my $save = sub {
- $class->save_aliases($param, $fw_conf, $aliases);
- $saved = 1;
- };
-
- eval {
- update_refs({
- conf => $fw_conf,
- kind => 'aliases',
- old => $name,
- new => $param->{rename},
- env => $class->rule_env(),
- action => $ref_action,
- subject => "alias '$name' renamed to '$rename'",
- save => $save,
- });
- };
- if (my $err = $@) {
- die "rename interrupted, the alias exists under both names; "
- . "retry with 'force-rename' to finish: $err"
- if $saved;
- die "could not update references to alias '$name': $err";
- }
+ if (!$renaming) {
+ $code->();
+ return undef;
+ }
- delete $aliases->{$name};
- }
+ # fail on the request instead of inside the task.
+ my (undef, $aliases) = $class->load_config($param);
- $class->save_aliases($param, $fw_conf, $aliases);
- },
- );
+ $check_update->($aliases, $param, $dangling_refs);
- return undef;
+ return run_ref_update('fw-alias-rename', $param->{vmid} // $param->{name}, $code);
},
});
}
@@ -359,41 +395,47 @@ sub register_delete_alias {
additionalProperties => 0,
properties => $properties,
},
- returns => { type => "null" },
+ returns => { type => 'string', optional => 1 },
code => sub {
my ($param) = @_;
- $class->lock_config(
- $param,
- sub {
- my ($param) = @_;
+ my $action = $param->{'dangling-references'} // 'keep';
+ my $subject = "alias '$param->{name}' deleted";
- my ($fw_conf, $aliases) = $class->load_config($param);
+ my $code = sub {
+ $class->lock_config(
+ $param,
+ sub {
+ my ($param) = @_;
- my $list = &$aliases_to_list($aliases);
- my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
- PVE::Tools::assert_if_modified($digest, $param->{digest});
+ my ($fw_conf, $aliases) = $class->load_config($param);
- my $name = lc($param->{name});
+ $check_digest->($aliases, $param);
- my $action = $param->{'dangling-references'} // 'keep';
+ my $name = lc($param->{name});
- update_refs({
- conf => $fw_conf,
- kind => 'aliases',
- old => $param->{name},
- env => $class->rule_env(),
- action => $action eq 'keep' ? undef : $action,
- subject => "alias '$param->{name}' deleted",
- });
+ update_refs({
+ conf => $fw_conf,
+ kind => 'aliases',
+ old => $param->{name},
+ env => $class->rule_env(),
+ action => $action eq 'keep' ? undef : $action,
+ subject => $subject,
+ });
- delete $aliases->{$name};
+ delete $aliases->{$name};
- $class->save_aliases($param, $fw_conf, $aliases);
- },
- );
+ $class->save_aliases($param, $fw_conf, $aliases);
+ },
+ );
+ };
- return undef;
+ # fail on the request instead of inside the task
+ my (undef, $aliases) = $class->load_config($param);
+
+ $check_digest->($aliases, $param);
+
+ return run_ref_update('fw-alias-delete', $param->{vmid} // $param->{name}, $code);
},
});
}
diff --git a/src/PVE/API2/Firewall/IPSet.pm b/src/PVE/API2/Firewall/IPSet.pm
index fe4f8c9..a4356e4 100644
--- a/src/PVE/API2/Firewall/IPSet.pm
+++ b/src/PVE/API2/Firewall/IPSet.pm
@@ -2,7 +2,7 @@ package PVE::API2::Firewall::IPSetBase;
use strict;
use warnings;
-use PVE::Firewall::References qw(update_refs);
+use PVE::Firewall::References qw(update_refs run_ref_update);
use PVE::Exception qw(raise raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
@@ -162,37 +162,45 @@ sub register_delete_ipset {
additionalProperties => 0,
properties => $properties,
},
- returns => { type => 'null' },
+ returns => { type => 'string', optional => 1 },
code => sub {
my ($param) = @_;
- $class->lock_config(
- $param,
- sub {
- my ($param) = @_;
+ my $action = $param->{'dangling-references'} // 'keep';
+ my $subject = "ipset '$param->{name}' deleted";
- my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
+ my $code = sub {
+ $class->lock_config(
+ $param,
+ sub {
+ my ($param) = @_;
- die "IPSet '$param->{name}' is not empty\n"
- if scalar(@$ipset) && !$param->{force};
+ my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
- my $action = $param->{'dangling-references'} // 'keep';
+ die "IPSet '$param->{name}' is not empty\n"
+ if scalar(@$ipset) && !$param->{force};
- update_refs({
- conf => $fw_conf,
- kind => 'ipset',
- old => $param->{name},
- env => $class->rule_env(),
- action => $action eq 'keep' ? undef : $action,
- subject => "ipset '$param->{name}' deleted",
- });
+ update_refs({
+ conf => $fw_conf,
+ kind => 'ipset',
+ old => $param->{name},
+ env => $class->rule_env(),
+ action => $action eq 'keep' ? undef : $action,
+ subject => $subject,
+ });
- $class->save_ipset($param, $fw_conf, undef);
+ $class->save_ipset($param, $fw_conf, undef);
+ },
+ );
+ };
- },
- );
+ # fail on the request instead of inside the task
+ my (undef, undef, $ipset) = $class->load_config($param);
- return undef;
+ die "IPSet '$param->{name}' is not empty\n"
+ if scalar($ipset->@*) && !$param->{force};
+
+ return run_ref_update('fw-ipset-delete', $param->{vmid} // $param->{name}, $code);
},
});
}
@@ -555,7 +563,7 @@ package PVE::API2::Firewall::BaseIPSetList;
use strict;
use warnings;
-use PVE::Firewall::References qw(update_refs);
+use PVE::Firewall::References qw(update_refs run_ref_update);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise_param_exc);
use PVE::Firewall;
@@ -623,6 +631,25 @@ my $get_ipset_list = sub {
return wantarray ? ($list, $digest) : $list;
};
+my $check_rename = sub {
+ my ($conf, $param, $dangling_refs) = @_;
+
+ my (undef, $digest) = &$get_ipset_list($conf);
+ PVE::Tools::assert_if_modified($digest, $param->{digest});
+
+ raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" })
+ if !$conf->{ipset}->{ $param->{rename} };
+
+ # prevent overwriting existing ipset
+ if (
+ $param->{name} ne $param->{rename}
+ && $conf->{ipset}->{ $param->{name} }
+ && $dangling_refs ne 'force-rename'
+ ) {
+ raise_param_exc({ name => "IPSet '$param->{name}' already exists" });
+ }
+};
+
sub register_index {
my ($class) = @_;
@@ -707,100 +734,106 @@ sub register_create {
additionalProperties => 0,
properties => $properties,
},
- returns => { type => 'null' },
+ returns => { type => 'string', optional => 1 },
code => sub {
my ($param) = @_;
- $class->lock_config(
- $param,
- sub {
- my ($param) = @_;
-
- my $dangling_refs = $param->{'dangling-references'} // 'keep';
+ my $dangling_refs = $param->{'dangling-references'} // 'keep';
- # 'force-rename' only differs in skipping the check below, the references are
- # renamed either way
- my $ref_action;
- if ($dangling_refs eq 'force-rename') {
- $ref_action = 'rename';
- } elsif ($dangling_refs ne 'keep') {
- $ref_action = $dangling_refs;
- }
+ # 'force-rename' only differs in skipping the check above, the references are renamed
+ # either way
+ my $ref_action;
+ if ($dangling_refs eq 'force-rename') {
+ $ref_action = 'rename';
+ } elsif ($dangling_refs ne 'keep') {
+ $ref_action = $dangling_refs;
+ }
- my ($cluster_conf, $fw_conf) = $class->load_config($param);
-
- if ($param->{rename}) {
- my (undef, $digest) = &$get_ipset_list($fw_conf);
- PVE::Tools::assert_if_modified($digest, $param->{digest});
-
- raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" })
- if !$fw_conf->{ipset}->{ $param->{rename} };
-
- if ($param->{name} ne $param->{rename}) {
- # prevent overwriting existing ipset
- raise_param_exc({
- name => "IPSet '$param->{name}' does already exist" })
- if $fw_conf->{ipset}->{ $param->{name} }
- && $dangling_refs ne 'force-rename';
-
- $fw_conf->{ipset}->{ $param->{name} } =
- $fw_conf->{ipset}->{ $param->{rename} };
-
- my $saved = 0;
- my $save = sub {
- $class->save_config($param, $fw_conf);
- $saved = 1;
- };
-
- eval {
- update_refs({
- conf => $fw_conf,
- kind => 'ipset',
- old => $param->{rename},
- new => $param->{name},
- env => $class->rule_env(),
- action => $ref_action,
- subject =>
- "ipset '$param->{rename}' renamed to '$param->{name}'",
- save => $save,
- });
- };
- if (my $err = $@) {
- die "rename interrupted, the IPSet exists under both names; "
- . "retry with 'force-rename' to finish: $err"
- if $saved;
- die "could not update references to IPSet "
- . "'$param->{rename}': $err";
+ my $renaming = $param->{rename} && $param->{name} ne $param->{rename};
+ my $subject =
+ $renaming ? "ipset '$param->{rename}' renamed to '$param->{name}'" : undef;
+
+ my $code = sub {
+ $class->lock_config(
+ $param,
+ sub {
+ my ($param) = @_;
+
+ my ($cluster_conf, $fw_conf) = $class->load_config($param);
+
+ if ($param->{rename}) {
+ $check_rename->($fw_conf, $param, $dangling_refs);
+
+ if ($param->{name} ne $param->{rename}) {
+ $fw_conf->{ipset}->{ $param->{name} } =
+ $fw_conf->{ipset}->{ $param->{rename} };
+
+ my $saved = 0;
+ my $save = sub {
+ $class->save_config($param, $fw_conf);
+ $saved = 1;
+ };
+
+ eval {
+ update_refs({
+ conf => $fw_conf,
+ kind => 'ipset',
+ old => $param->{rename},
+ new => $param->{name},
+ env => $class->rule_env(),
+ action => $ref_action,
+ subject => $subject,
+ save => $save,
+ });
+ };
+ if (my $err = $@) {
+ die
+ "rename interrupted, the IPSet exists under both names; "
+ . "retry with 'force-rename' to finish: $err"
+ if $saved;
+ die "could not update references to IPSet "
+ . "'$param->{rename}': $err";
+ }
+
+ delete $fw_conf->{ipset}->{ $param->{rename} };
+
+ if (
+ my $comment =
+ delete $fw_conf->{ipset_comments}->{ $param->{rename} }
+ ) {
+ $fw_conf->{ipset_comments}->{ $param->{name} } = $comment;
+ }
}
- delete $fw_conf->{ipset}->{ $param->{rename} };
-
- if (
- my $comment =
- delete $fw_conf->{ipset_comments}->{ $param->{rename} }
- ) {
- $fw_conf->{ipset_comments}->{ $param->{name} } = $comment;
+ $fw_conf->{ipset_comments}->{ $param->{name} } = $param->{comment}
+ if defined($param->{comment});
+ } else {
+ foreach my $name (keys %{ $fw_conf->{ipset} }) {
+ raise_param_exc({ name => "IPSet '$name' already exists" })
+ if $name eq $param->{name};
}
- }
- $fw_conf->{ipset_comments}->{ $param->{name} } = $param->{comment}
- if defined($param->{comment});
- } else {
- foreach my $name (keys %{ $fw_conf->{ipset} }) {
- raise_param_exc({ name => "IPSet '$name' already exists" })
- if $name eq $param->{name};
+ $fw_conf->{ipset}->{ $param->{name} } = [];
+ $fw_conf->{ipset_comments}->{ $param->{name} } = $param->{comment}
+ if defined($param->{comment});
}
- $fw_conf->{ipset}->{ $param->{name} } = [];
- $fw_conf->{ipset_comments}->{ $param->{name} } = $param->{comment}
- if defined($param->{comment});
- }
+ $class->save_config($param, $fw_conf);
+ },
+ );
+ };
- $class->save_config($param, $fw_conf);
- },
- );
+ if (!$renaming) {
+ $code->();
+ return undef;
+ }
- return undef;
+ # fail on the request instead of inside the task
+ my (undef, $fw_conf) = $class->load_config($param);
+
+ $check_rename->($fw_conf, $param, $dangling_refs);
+
+ return run_ref_update('fw-ipset-rename', $param->{vmid} // $param->{rename}, $code);
},
});
}
diff --git a/src/PVE/Firewall/References.pm b/src/PVE/Firewall/References.pm
index 37df434..f32c7db 100644
--- a/src/PVE/Firewall/References.pm
+++ b/src/PVE/Firewall/References.pm
@@ -13,6 +13,7 @@ use base 'Exporter';
our @EXPORT_OK = qw(
collect_refs
update_refs
+ run_ref_update
);
sub collect_refs {
@@ -422,6 +423,25 @@ sub update_refs {
return $count;
}
+=head3 run_ref_update($type, $id, $code)
+
+Run C<$code> as a worker task of type C<$type> for C<$id>.
+
+C<$id> is the guest a guest-scoped object belongs to so the task is logged with that guest, and the
+name of the object otherwise.
+
+Return the UPID of the task.
+
+=cut
+
+sub run_ref_update {
+ my ($type, $id, $code) = @_;
+
+ my $rpcenv = PVE::RESTEnvironment::get();
+
+ return $rpcenv->fork_worker($type, $id, $rpcenv->get_user(), $code);
+}
+
=head3 format_ref_change($change)
Format one recorded config change as a single line, e.g.
--
2.47.3
next prev parent reply other threads:[~2026-10-22 12:51 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 12:46 [PATCH container/docs/firewall/manager/network/qemu-server v4 00/27] handle dangling references when firewall objects go away Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 01/27] firewall: move object reference helpers to their own module Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 02/27] firewall: references: add helpers to update object references Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 03/27] firewall: references: log the references an object update touched Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 04/27] firewall: references: add wrappers for the SDN-generated IPSets Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 05/27] firewall: parser: do not log errors for disabled rules Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 06/27] firewall: ipset: add option to handle dangling references on rename Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 07/27] firewall: ipset: add option to handle dangling references on delete Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 08/27] firewall: aliases: add option to handle dangling references on rename Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 09/27] firewall: aliases: add option to handle dangling references on delete Arthur Bied-Charreton
2026-10-22 12:46 ` Arthur Bied-Charreton [this message]
2026-10-22 12:46 ` [PATCH pve-firewall v4 11/27] firewall: tests: add tests for object reference updates Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 12/27] firewall: references: handle security groups Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 13/27] firewall: groups: add option to handle dangling references on rename Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-firewall v4 14/27] fix #8176: firewall: groups: handle dangling references on delete Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-network v4 15/27] apply: add option to handle dangling references on VNet deletion Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH qemu-server v4 16/27] api: destroy_vm: add option to handle dangling IPSet references Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-container v4 17/27] " Arthur Bied-Charreton
2026-10-22 12:46 ` [PATCH pve-docs v4 18/27] firewall: remove trailing spaces Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-docs v4 19/27] firewall: document renaming and removing objects Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 20/27] ui: firewall: add a common widget for handling dangling references Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 21/27] ui: firewall: ipset: add controls for " Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 22/27] ui: firewall: aliases: " Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 23/27] ui: sdn: apply: add control for dangling IPSet references Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 24/27] ui: guest destroy: use let for non-constant variable bindings Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 25/27] ui: guest destroy: add control for dangling IPSet references Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 26/27] ui: firewall: show the task for reference updates Arthur Bied-Charreton
2026-10-22 12:47 ` [PATCH pve-manager v4 27/27] ui: firewall: groups: add controls for dangling references Arthur Bied-Charreton
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261022124708.429069-11-a.bied-charreton@proxmox.com \
--to=a.bied-charreton@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox