From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 762691FF0E0 for ; Thu, 09 Jul 2026 11:22:11 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B2B8F2145A; Thu, 09 Jul 2026 11:20:06 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network v2 13/27] sdn: dry-run: surface pending microseg changes Date: Thu, 9 Jul 2026 11:18:38 +0200 Message-ID: <20260709091852.538885-14-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260709091852.538885-1-h.laimer@proxmox.com> References: <20260709091852.538885-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1783588741223 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.278 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 3RH5H3AIHNG6RMDQ3JZMQWK3O54UR2NJ X-Message-ID-Hash: 3RH5H3AIHNG6RMDQ3JZMQWK3O54UR2NJ X-MailFrom: h.laimer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Microseg has no per-node generated file to diff, so the dry-run renders a summary of the state (allocated identities, per-NIC memberships, policies) for both the current and the pending config and returns their diff. This is where changes with no .cfg edit behind them, like assignment shifts caused by guest changes, become visible before an apply. Signed-off-by: Hannes Laimer --- src/PVE/API2/Network/SDN.pm | 19 +++++++++++ src/PVE/Network/SDN/Microseg.pm | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm index 5a31e6f..5cee5a1 100644 --- a/src/PVE/API2/Network/SDN.pm +++ b/src/PVE/API2/Network/SDN.pm @@ -420,6 +420,13 @@ __PACKAGE__->register_method({ description => 'The difference between the current and pending /etc/network/interfaces.d/sdn configuration.', }, + "microseg-diff" => { + type => 'string', + optional => 1, + description => + 'The difference between the current and pending microsegmentation state' + . ' (allocated identities and realized per-NIC group membership).', + }, }, }, code => sub { @@ -450,8 +457,20 @@ __PACKAGE__->register_method({ $return_value->{"interfaces-diff"} = get_diff('/etc/network/interfaces.d/sdn', $interfaces_tmp_filename); + my $current_cfg = PVE::Network::SDN::running_config(); + my $microseg_old = PVE::Network::SDN::Microseg::summary($current_cfg->{microseg}); + my $microseg_new = PVE::Network::SDN::Microseg::summary($running_cfg->{microseg}); + my ($microseg_old_filename, $microseg_old_fh) = + PVE::File::tempfile_contents($microseg_old, 700); + my ($microseg_new_filename, $microseg_new_fh) = + PVE::File::tempfile_contents($microseg_new, 700); + $return_value->{"microseg-diff"} = + get_diff($microseg_old_filename, $microseg_new_filename); + close($frr_tmp_fh); close($interfaces_tmp_fh); + close($microseg_old_fh); + close($microseg_new_fh); return $return_value; }, }); diff --git a/src/PVE/Network/SDN/Microseg.pm b/src/PVE/Network/SDN/Microseg.pm index 4f7398a..82ee52d 100644 --- a/src/PVE/Network/SDN/Microseg.pm +++ b/src/PVE/Network/SDN/Microseg.pm @@ -232,6 +232,66 @@ sub guest_inventory { return $inventory; } +# A stable, human-readable summary of a rendered microseg block ({ ids, identities, realized }), for +# the SDN dry-run diff. +sub summary { + my ($mseg) = @_; + + $mseg //= {}; + my $ids = $mseg->{ids} // {}; + my $identities = $mseg->{identities} // {}; + my $realized = $mseg->{realized} // []; + + my %mark2name; + for my $name (keys %$ids) { + my $object = $ids->{$name}; + next if !$object || ($object->{type} // '') ne 'group'; + $mark2name{ $object->{mark} } = $name if defined $object->{mark}; + } + my $group_names = sub { + my ($marks) = @_; + return join(',', sort map { $mark2name{$_} // "mark$_" } @{ $marks // [] }); + }; + + my @lines = ('identities:'); + my $classes = $identities->{classes} // {}; + for my $cid (sort { $a <=> $b } keys %$classes) { + push @lines, sprintf(' %s -> %s', $cid, $group_names->($classes->{$cid})); + } + + push @lines, 'assignments:'; + my @sorted = sort { + ($a->{vmid} // 0) <=> ($b->{vmid} // 0) || ($a->{iface} // 0) <=> ($b->{iface} // 0) + } @$realized; + for my $assignment (@sorted) { + push @lines, + sprintf( + ' vm%s net%s -> %s', + $assignment->{vmid} // '?', + $assignment->{iface} // '?', + join(',', sort @{ $assignment->{groups} // [] }), + ); + } + + push @lines, 'policies:'; + my @rule_ids = sort grep { ($ids->{$_}->{type} // '') eq 'rule' } keys %$ids; + for my $rule_id (@rule_ids) { + my $rule = $ids->{$rule_id}; + push @lines, + sprintf( + ' %s {%s} -> %s {%s}: %s prio %s', + $rule->{src_match} // 'any', + join(',', sort @{ $rule->{src} // [] }), + $rule->{dst_match} // 'any', + join(',', sort @{ $rule->{dst} // [] }), + ($rule->{allow} ? 'allow' : 'deny'), + $rule->{prio} // 0, + ); + } + + return join("\n", @lines) . "\n"; +} + # Shared CRUD helpers for the per-type API endpoints. Each takes the object's type, a single type # string, or an arrayref of acceptable types for an endpoint that fronts a family of stored types # (the assignment matcher kinds). The per-type modules only declare their schema and delegate here. -- 2.47.3