public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Mira Limbeck <m.limbeck@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v3 qemu-server] fix 4493: cloud-init: fix generated Windows config
Date: Tue, 30 Jul 2024 17:15:39 +0200	[thread overview]
Message-ID: <20240730151540.308217-1-m.limbeck@proxmox.com> (raw)

Cloudbase-Init, a cloud-init reimplementation for Windows, supports only
a subset of the configuration options of cloud-init. Some features
depend on support by the Metadata Service (ConfigDrive2 here) and have
further limitations [0].

To support a basic setup the following changes were made:
 - password is saved as plaintext for any Windows guests (ostype)
 - DNS servers are added to each of the interfaces
 - SSH public keys are passed via metadata

Network and metadata generation for Cloudbase-Init is separate from the
default ConfigDrive2 one so as to not interfere with any other OSes that
depend on the current ConfigDrive2 implementation.

DNS search domains were removed because Cloudbase-Init's ENI parser
doesn't handle it at all.
The password set via `cipassword` is used for the Admin user configured
in the cloudbase-init.conf in the guest while the `ciuser` parameter is
ignored. The Admin user has to be set in the cloudbase-init.conf file
instead.
Specifying a different user does not work.

For the password to work the `ostype` needs to be any Windows variant
before `cipassword` is set. Otherwise the password will be encrypted and
the encrypted password used as plaintext password in the guest.

The `citype` needs to be `configdrive2`, which is the default for
Windows guests, for the generated configs to be compatible with
Cloudbase-Init.

[0] https://cloudbase-init.readthedocs.io/en/latest/index.html

Signed-off-by: Mira Limbeck <m.limbeck@proxmox.com>
---
v3:
 - removed `use URI` since we already `use URI::Escape`
 - sent a separate patch adding `liburi-perl` dependency in d/control
v2:
 - unchanged

 PVE/API2/Qemu.pm            | 13 ++---
 PVE/QemuServer/Cloudinit.pm | 99 +++++++++++++++++++++++++++++++++++--
 2 files changed, 101 insertions(+), 11 deletions(-)

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index a3313f3..d25a79f 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -1674,12 +1674,6 @@ my $update_vm_api  = sub {
 
     my $skip_cloud_init = extract_param($param, 'skip_cloud_init');
 
-    if (defined(my $cipassword = $param->{cipassword})) {
-	# Same logic as in cloud-init (but with the regex fixed...)
-	$param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
-	    if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
-    }
-
     my @paramarr = (); # used for log message
     foreach my $key (sort keys %$param) {
 	my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
@@ -2029,6 +2023,13 @@ my $update_vm_api  = sub {
 		    my $machine_conf = PVE::QemuServer::Machine::parse_machine($param->{$opt});
 		    PVE::QemuServer::Machine::assert_valid_machine_property($conf, $machine_conf);
 		    $conf->{pending}->{$opt} = $param->{$opt};
+		} elsif ($opt eq 'cipassword') {
+		    if (!PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
+			# Same logic as in cloud-init (but with the regex fixed...)
+			$param->{cipassword} = PVE::Tools::encrypt_pw($param->{cipassword})
+			    if $param->{cipassword} !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
+		    }
+		    $conf->{cipassword} = $param->{cipassword};
 		} else {
 		    $conf->{pending}->{$opt} = $param->{$opt};
 
diff --git a/PVE/QemuServer/Cloudinit.pm b/PVE/QemuServer/Cloudinit.pm
index abc6b14..4efbdf5 100644
--- a/PVE/QemuServer/Cloudinit.pm
+++ b/PVE/QemuServer/Cloudinit.pm
@@ -8,6 +8,7 @@ use Digest::SHA;
 use URI::Escape;
 use MIME::Base64 qw(encode_base64);
 use Storable qw(dclone);
+use JSON;
 
 use PVE::Tools qw(run_command file_set_contents);
 use PVE::Storage;
@@ -232,12 +233,23 @@ sub generate_configdrive2 {
     my ($conf, $vmid, $drive, $volname, $storeid) = @_;
 
     my ($user_data, $network_data, $meta_data, $vendor_data) = get_custom_cloudinit_files($conf);
-    $user_data = cloudinit_userdata($conf, $vmid) if !defined($user_data);
-    $network_data = configdrive2_network($conf) if !defined($network_data);
-    $vendor_data = '' if !defined($vendor_data);
+    if (PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
+	$user_data = cloudinit_userdata($conf, $vmid) if !defined($user_data);
+	$network_data = cloudbase_network_eni($conf) if !defined($network_data);
+	$vendor_data = '' if !defined($vendor_data);
+
+	if (!defined($meta_data)) {
+	    my $instance_id = cloudbase_gen_instance_id($user_data, $network_data);
+	    $meta_data = cloudbase_configdrive2_metadata($instance_id, $conf);
+	}
+    } else {
+	$user_data = cloudinit_userdata($conf, $vmid) if !defined($user_data);
+	$network_data = configdrive2_network($conf) if !defined($network_data);
+	$vendor_data = '' if !defined($vendor_data);
 
-    if (!defined($meta_data)) {
-	$meta_data = configdrive2_gen_metadata($user_data, $network_data);
+	if (!defined($meta_data)) {
+	    $meta_data = configdrive2_gen_metadata($user_data, $network_data);
+	}
     }
 
     # we always allocate a 4MiB disk for cloudinit and with the overhead of the ISO
@@ -254,6 +266,83 @@ sub generate_configdrive2 {
     commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'config-2');
 }
 
+sub cloudbase_network_eni {
+    my ($conf) = @_;
+
+    my $content = "";
+
+    my ($searchdomains, $nameservers) = get_dns_conf($conf);
+    if ($nameservers && @$nameservers) {
+	$nameservers = join(' ', @$nameservers);
+    }
+
+    my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
+    foreach my $iface (sort @ifaces) {
+	(my $id = $iface) =~ s/^net//;
+	next if !$conf->{"ipconfig$id"};
+	my $net = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+	$id = "eth$id";
+
+	$content .="auto $id\n";
+	if ($net->{ip}) {
+	    if ($net->{ip} eq 'dhcp') {
+		$content .= "iface $id inet dhcp\n";
+	    } else {
+		my ($addr, $mask) = split_ip4($net->{ip});
+		$content .= "iface $id inet static\n";
+		$content .= "        address $addr\n";
+		$content .= "        netmask $mask\n";
+		$content .= "        gateway $net->{gw}\n" if $net->{gw};
+		$content .= "        dns-nameservers $nameservers\n" if $nameservers;
+	    }
+	}
+	if ($net->{ip6}) {
+	    if ($net->{ip6} =~ /^(auto|dhcp)$/) {
+		$content .= "iface $id inet6 $1\n";
+	    } else {
+		my ($addr, $mask) = split('/', $net->{ip6});
+		$content .= "iface $id inet6 static\n";
+		$content .= "        address $addr\n";
+		$content .= "        netmask $mask\n";
+		$content .= "        gateway $net->{gw6}\n" if $net->{gw6};
+		$content .= "        dns-nameservers $nameservers\n" if $nameservers;
+	    }
+	}
+    }
+
+    return $content;
+}
+
+sub cloudbase_configdrive2_metadata {
+    my ($uuid, $conf) = @_;
+    my $meta_data = {
+	uuid => $uuid,
+	'network_config' => {
+	    'content_path' => '/content/0000',
+	},
+    };
+    $meta_data->{'admin_pass'} = $conf->{cipassword} if $conf->{cipassword};
+    if (defined(my $keys = $conf->{sshkeys})) {
+	$keys = URI::Escape::uri_unescape($keys);
+	$keys = [map { my $key = $_; chomp $key; $key } split(/\n/, $keys)];
+	$keys = [grep { /\S/ } @$keys];
+	my $i = 0;
+	foreach my $k (@$keys) {
+	    $meta_data->{'public_keys'}->{"key-$i"} = $k;
+	    $i++;
+	}
+    }
+    my $json = encode_json($meta_data);
+    return $json;
+}
+
+sub cloudbase_gen_instance_id {
+    my ($user, $network) = @_;
+
+    my $uuid_str = Digest::SHA::sha1_hex($user.$network);
+    return $uuid_str;
+}
+
 sub generate_opennebula {
     my ($conf, $vmid, $drive, $volname, $storeid) = @_;
 
-- 
2.39.2


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


             reply	other threads:[~2024-07-30 15:15 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-07-30 15:15 Mira Limbeck [this message]
2024-07-30 15:15 ` [pve-devel] [PATCH v3 docs] cloudinit: add Windows cloudbase-init section Mira Limbeck
2024-07-30 19:14   ` [pve-devel] applied: " Thomas Lamprecht
2024-07-30 19:14 ` [pve-devel] applied: [PATCH v3 qemu-server] fix 4493: cloud-init: fix generated Windows config Thomas Lamprecht
2024-07-31  8:02   ` Mira Limbeck

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=20240730151540.308217-1-m.limbeck@proxmox.com \
    --to=m.limbeck@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal