From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5C1A71FF143 for ; Sat, 25 Apr 2026 14:40:28 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 579CD7D5; Sat, 25 Apr 2026 14:40:24 +0200 (CEST) DKIM-Filter: OpenDKIM Filter v2.11.0 smtp.gnome.org 6EC011099C1DE DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gnome.org; s=default; t=1777120346; bh=EuLfH1lQCMd6GQrdFHxlleUxA/BWbeEF3q+xYNvOl3E=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=aWLB0dpn18O0DjSq2X2RTgtjoP51vXZYDD6ULc6LczVEfpU502GVMRYQJ+xoEhnoD TFRzzYyjTPCMtCGtouBuN5LGr4s8vQFNi7RYvOKCAucPkiaUBfa9sIg2Try8JDy+IF jyQInIS1O3IOFZaSXAplo/boo/9/KlEaKCri0F6s= From: Yuri Konotopov To: pve-devel@lists.proxmox.com Subject: [PATCH qemu-server 2/2] fix #1739: cloud-init: add persistent instance-id mode Date: Sat, 25 Apr 2026 16:31:24 +0400 Message-ID: <20260425123125.797431-3-ykonotopov@gnome.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260425123125.797431-1-ykonotopov@gnome.org> References: <20260425123125.797431-1-ykonotopov@gnome.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -1.000 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's domain DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from domain DMARC_PASS -0.1 DMARC pass policy KAM_MAILER 2 Automated Mailer Tag Left in Email RCVD_IN_DNSWL_LOW -0.7 Sender listed at https://www.dnswl.org/, low trust SPF_HELO_PASS -0.001 SPF: HELO matches SPF record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: WTQIGQEHE35UDDX4URT5MGGG3OGFLAHU X-Message-ID-Hash: WTQIGQEHE35UDDX4URT5MGGG3OGFLAHU X-MailFrom: ykonotopov@gnome.org 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 CC: Yuri Konotopov X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Signed-off-by: Yuri Konotopov --- src/PVE/API2/Qemu.pm | 38 ++++++++++++++++ src/PVE/QemuServer.pm | 54 ++++++++++++++++++++++ src/PVE/QemuServer/Cloudinit.pm | 80 +++++++++++++++++++++++++++------ 3 files changed, 159 insertions(+), 13 deletions(-) diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm index dadc06b0..102b8d24 100644 --- a/src/PVE/API2/Qemu.pm +++ b/src/PVE/API2/Qemu.pm @@ -826,6 +826,7 @@ my $diskoptions = { my $cloudinitoptions = { cicustom => 1, + ciinstanceid => 1, cipassword => 1, citype => 1, ciuser => 1, @@ -4544,6 +4545,7 @@ __PACKAGE__->register_method({ if ($newconf->{vmgenid}) { $newconf->{vmgenid} = PVE::QemuServer::generate_uuid(); } + PVE::QemuServer::reset_ciinstanceid($newconf); delete $newconf->{template}; @@ -6509,6 +6511,42 @@ __PACKAGE__->register_method({ }, }); +__PACKAGE__->register_method({ + name => 'cloudinit_instance_id', + path => '{vmid}/cloudinit/instance-id', + method => 'GET', + proxyto => 'node', + description => "Get the effective cloud-init instance-id.", + permissions => { + check => ['perm', '/vms/{vmid}', ['VM.Audit']], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => + get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), + }, + }, + returns => { + type => 'object', + properties => { + id => { + type => 'string', + optional => 1, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::QemuConfig->load_current_config($param->{vmid}); + my $id = PVE::QemuServer::Cloudinit::get_cloudinit_instance_id($conf, $param->{vmid}); + + return defined($id) ? { id => $id } : {}; + }, +}); + __PACKAGE__->register_method({ name => 'cloudinit_generated_config_dump', path => '{vmid}/cloudinit/dump', diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm index 2a469fff..c569f437 100644 --- a/src/PVE/QemuServer.pm +++ b/src/PVE/QemuServer.pm @@ -774,6 +774,25 @@ my $cicustom_fmt = { }; PVE::JSONSchema::register_format('pve-qm-cicustom', $cicustom_fmt); +my $ciinstanceid_fmt = { + mode => { + type => 'string', + enum => ['legacy', 'persistent'], + description => 'Controls how the cloud-init instance-id is generated.', + default => 'legacy', + optional => 1, + }, + id => { + type => 'string', + pattern => '[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}', + maxLength => 64, + format_description => 'instance-id', + description => 'Use this cloud-init instance-id in persistent mode.', + optional => 1, + }, +}; +PVE::JSONSchema::register_format('pve-qm-ciinstanceid', $ciinstanceid_fmt); + # any new option might need to be added to $cloudinitoptions in PVE::API2::Qemu my $confdesc_cloudinit = { citype => { @@ -805,6 +824,12 @@ my $confdesc_cloudinit = { description => 'cloud-init: do an automatic package upgrade after the first boot.', default => 1, }, + ciinstanceid => { + optional => 1, + type => 'string', + description => 'cloud-init: Configure how the instance-id is generated.', + format => 'pve-qm-ciinstanceid', + }, cicustom => { optional => 1, type => 'string', @@ -1656,6 +1681,35 @@ sub parse_vga { return $res; } +sub parse_ciinstanceid { + my ($value) = @_; + + return {} if !$value; + + my $res = eval { parse_property_string($ciinstanceid_fmt, $value) }; + warn $@ if $@; + return $res // {}; +} + +sub print_ciinstanceid { + my ($ciinstanceid) = @_; + + my $res = {%$ciinstanceid}; + $res->{mode} = 'legacy' if !defined($res->{mode}); + + return PVE::JSONSchema::print_property_string($res, $ciinstanceid_fmt); +} + +sub reset_ciinstanceid { + my ($conf) = @_; + + my $ciinstanceid = parse_ciinstanceid($conf->{ciinstanceid}); + return if ($ciinstanceid->{mode} // 'legacy') ne 'persistent'; + + delete $ciinstanceid->{id}; + $conf->{ciinstanceid} = print_ciinstanceid($ciinstanceid); +} + sub qemu_created_version_fixups { my ($conf, $forcemachine, $kvmver) = @_; diff --git a/src/PVE/QemuServer/Cloudinit.pm b/src/PVE/QemuServer/Cloudinit.pm index 67f83d3a..587f7f3f 100644 --- a/src/PVE/QemuServer/Cloudinit.pm +++ b/src/PVE/QemuServer/Cloudinit.pm @@ -229,9 +229,9 @@ sub configdrive2_network { } sub configdrive2_gen_metadata { - my ($user, $network) = @_; + my ($conf, $vmid, $user, $network) = @_; - my $uuid_str = Digest::SHA::sha1_hex($user . $network); + my $uuid_str = get_cloudinit_instance_id_from_content($conf, $user, $network); return configdrive2_metadata($uuid_str); } @@ -260,12 +260,12 @@ sub generate_configdrive2 { get_cloudinit_files($conf, $vmid, configdrive2_network_generator($conf)); if (PVE::QemuServer::Helpers::windows_version($conf->{ostype})) { if (!defined($meta_data)) { - my $instance_id = cloudbase_gen_instance_id($user_data, $network_data); + my $instance_id = cloudbase_gen_instance_id($conf, $vmid, $user_data, $network_data); $meta_data = cloudbase_configdrive2_metadata($instance_id, $conf); } } else { if (!defined($meta_data)) { - $meta_data = configdrive2_gen_metadata($user_data, $network_data); + $meta_data = configdrive2_gen_metadata($conf, $vmid, $user_data, $network_data); } } @@ -355,10 +355,9 @@ sub cloudbase_configdrive2_metadata { } sub cloudbase_gen_instance_id { - my ($user, $network) = @_; + my ($conf, $vmid, $user, $network) = @_; - my $uuid_str = Digest::SHA::sha1_hex($user . $network); - return $uuid_str; + return get_cloudinit_instance_id_from_content($conf, $user, $network); } sub generate_opennebula { @@ -584,12 +583,57 @@ sub nocloud_metadata { } sub nocloud_gen_metadata { - my ($user, $network) = @_; + my ($conf, $vmid, $user, $network) = @_; - my $uuid_str = Digest::SHA::sha1_hex($user . $network); + my $uuid_str = get_cloudinit_instance_id_from_content($conf, $user, $network); return nocloud_metadata($uuid_str); } +sub get_cloudinit_instance_id_from_content { + my ($conf, $user, $network) = @_; + + my $ciinstanceid = PVE::QemuServer::parse_ciinstanceid($conf->{ciinstanceid}); + return $ciinstanceid->{id} + if ($ciinstanceid->{mode} // 'legacy') eq 'persistent' && $ciinstanceid->{id}; + + return Digest::SHA::sha1_hex($user . $network); +} + +sub get_cloudinit_instance_id { + my ($conf, $vmid) = @_; + + my $format = get_cloudinit_format($conf); + return if $format eq 'opennebula'; + return if has_custom_cloudinit_metadata($conf); + + my $ciinstanceid = PVE::QemuServer::parse_ciinstanceid($conf->{ciinstanceid}); + return $ciinstanceid->{id} + if ($ciinstanceid->{mode} // 'legacy') eq 'persistent' && $ciinstanceid->{id}; + + my $network_generator = + $format eq 'nocloud' ? \&nocloud_network : configdrive2_network_generator($conf); + + my ($user, $network, $meta_data) = get_cloudinit_files($conf, $vmid, $network_generator); + return if defined($meta_data); + + return get_cloudinit_instance_id_from_content($conf, $user, $network); +} + +sub ensure_cloudinit_instance_id { + my ($conf, $vmid) = @_; + + my $ciinstanceid = PVE::QemuServer::parse_ciinstanceid($conf->{ciinstanceid}); + return if ($ciinstanceid->{mode} // 'legacy') ne 'persistent'; + return if $ciinstanceid->{id}; + + my $instance_id = get_cloudinit_instance_id($conf, $vmid); + return if !defined($instance_id); + + $ciinstanceid->{id} = $instance_id; + $conf->{ciinstanceid} = PVE::QemuServer::print_ciinstanceid($ciinstanceid); + return 1; +} + sub generate_nocloud { my ($conf, $vmid, $drive, $volname, $storeid) = @_; @@ -597,7 +641,7 @@ sub generate_nocloud { get_cloudinit_files($conf, $vmid, \&nocloud_network); if (!defined($meta_data)) { - $meta_data = nocloud_gen_metadata($user_data, $network_data); + $meta_data = nocloud_gen_metadata($conf, $vmid, $user_data, $network_data); } # we always allocate a 4MiB disk for cloudinit and with the overhead of the ISO @@ -615,6 +659,15 @@ sub generate_nocloud { commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'cidata'); } +sub has_custom_cloudinit_metadata { + my ($conf) = @_; + + return if !$conf->{cicustom}; + + my $files = PVE::JSONSchema::parse_property_string('pve-qm-cicustom', $conf->{cicustom}); + return !!$files->{meta}; +} + sub get_cloudinit_files { my ($conf, $vmid, $network_generator) = @_; @@ -698,6 +751,7 @@ sub generate_cloudinit_config { my $format = get_cloudinit_format($conf); + my $instance_id_changed = ensure_cloudinit_instance_id($conf, $vmid); my $has_changes = has_changes($conf); PVE::QemuConfig->foreach_volume( @@ -716,7 +770,7 @@ sub generate_cloudinit_config { }, ); - return $has_changes; + return $has_changes || $instance_id_changed; } sub apply_cloudinit_config { @@ -751,10 +805,10 @@ sub dump_cloudinit_config { my $user = cloudinit_userdata($conf, $vmid); if ($format eq 'nocloud') { my $network = nocloud_network($conf); - return nocloud_gen_metadata($user, $network); + return nocloud_gen_metadata($conf, $vmid, $user, $network); } else { my $network = configdrive2_network($conf); - return configdrive2_gen_metadata($user, $network); + return configdrive2_gen_metadata($conf, $vmid, $user, $network); } } } -- 2.47.3