[pve-devel] [PATCH v2 qemu-server] fix 4493: cloud-init: fix generated Windows config
Mira Limbeck
m.limbeck at proxmox.com
Mon Jul 29 17:19:30 CEST 2024
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.
[0] https://cloudbase-init.readthedocs.io/en/latest/index.html
Signed-off-by: Mira Limbeck <m.limbeck at proxmox.com>
---
v2:
- unchanged
v1:
DNS search domains are not handled at all by the cloudbase-init ENI
parser.
The password is used for the Admin user specified in the
cloudbase-init.conf inside the guest. Specifying a different user does
not work. This would require rewriting the userdata handling as
described in #5384 [1] which is a breaking change. Userdata generation
is currently shared between all implementations, but the new one could
be made cloudbase-init only for now.
To know if the password needs to be unencrypted, we have to check the
`ostype`. For this we need access to the config. That's why I moved the
`cipassword` handling inside $updatefn.
When no `citype` is specified, it will default to `configdrive2` on
Windows. The check requires `ostype` to be set correctly.
Any other `citype`s may not work correctly if used for cloudbase-init.
The docs patch adds a section on how to configure a Windows guest for
cloudbase-init.
[1] https://bugzilla.proxmox.com/show_bug.cgi?id=5384
PVE/API2/Qemu.pm | 13 ++---
PVE/QemuServer/Cloudinit.pm | 100 ++++++++++++++++++++++++++++++++++--
2 files changed, 102 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..d4ecfac 100644
--- a/PVE/QemuServer/Cloudinit.pm
+++ b/PVE/QemuServer/Cloudinit.pm
@@ -8,6 +8,8 @@ use Digest::SHA;
use URI::Escape;
use MIME::Base64 qw(encode_base64);
use Storable qw(dclone);
+use JSON;
+use URI;
use PVE::Tools qw(run_command file_set_contents);
use PVE::Storage;
@@ -232,12 +234,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 +267,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
More information about the pve-devel
mailing list