]> git.proxmox.com Git - qemu-server.git/blame - PVE/QemuServer/Cloudinit.pm
fix #4284: add read-only to non-hotpluggable disk options
[qemu-server.git] / PVE / QemuServer / Cloudinit.pm
CommitLineData
0c9a7596
AD
1package PVE::QemuServer::Cloudinit;
2
3use strict;
4use warnings;
5
6use File::Path;
7use Digest::SHA;
8use URI::Escape;
545eec65 9use MIME::Base64 qw(encode_base64);
2be1fb0a 10use Storable qw(dclone);
0c9a7596
AD
11
12use PVE::Tools qw(run_command file_set_contents);
13use PVE::Storage;
14use PVE::QemuServer;
238af88e 15use PVE::QemuServer::Helpers;
0c9a7596 16
7d761a01
ML
17use constant CLOUDINIT_DISK_SIZE => 4 * 1024 * 1024; # 4MiB in bytes
18
0c9a7596 19sub commit_cloudinit_disk {
4a853915 20 my ($conf, $vmid, $drive, $volname, $storeid, $files, $label) = @_;
f62c36cf
WB
21
22 my $path = "/run/pve/cloudinit/$vmid/";
23 mkpath $path;
24 foreach my $filepath (keys %$files) {
25 if ($filepath !~ m@^(.*)\/[^/]+$@) {
26 die "internal error: bad file name in cloud-init image: $filepath\n";
27 }
28 my $dirname = $1;
29 mkpath "$path/$dirname";
30
31 my $contents = $files->{$filepath};
32 file_set_contents("$path/$filepath", $contents);
33 }
41cd94a0
WB
34
35 my $storecfg = PVE::Storage::config();
36 my $iso_path = PVE::Storage::path($storecfg, $drive->{file});
37 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
7e8ab2a9 38 my $format = PVE::QemuServer::qemu_img_format($scfg, $volname);
b56d56cf 39
9a13f0fe
ML
40 my $size = eval { PVE::Storage::volume_size_info($storecfg, $drive->{file}) };
41 if (!defined($size) || $size <= 0) {
92fcaab7 42 $volname =~ m/(vm-$vmid-cloudinit(.\Q$format\E)?)/;
7e8ab2a9 43 my $name = $1;
84821d15
TL
44 $size = 4 * 1024;
45 PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $format, $name, $size);
46 $size *= 1024; # vdisk alloc takes KB, qemu-img dd's osize takes byte
7e8ab2a9 47 }
9a13f0fe
ML
48 my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
49 $plugin->activate_volume($storeid, $scfg, $volname);
7e8ab2a9 50
86a2e85a 51 print "generating cloud-init ISO\n";
f62c36cf 52 eval {
86a2e85a
TL
53 run_command([
54 ['genisoimage', '-quiet', '-iso-level', '3', '-R', '-V', $label, $path],
55 ['qemu-img', 'dd', '-n', '-f', 'raw', '-O', $format, 'isize=0', "osize=$size", "of=$iso_path"]
56 ]);
f62c36cf
WB
57 };
58 my $err = $@;
59 rmtree($path);
60 die $err if $err;
0c9a7596
AD
61}
62
41cd94a0
WB
63sub get_cloudinit_format {
64 my ($conf) = @_;
65 if (defined(my $format = $conf->{citype})) {
66 return $format;
67 }
0c9a7596 68
41cd94a0
WB
69 # No format specified, default based on ostype because windows'
70 # cloudbased-init only supports configdrivev2, whereas on linux we need
71 # to use mac addresses because regular cloudinit doesn't map 'ethX' to
72 # the new predicatble network device naming scheme.
73 if (defined(my $ostype = $conf->{ostype})) {
74 return 'configdrive2'
238af88e 75 if PVE::QemuServer::Helpers::windows_version($ostype);
41cd94a0 76 }
0c9a7596 77
41cd94a0 78 return 'nocloud';
0c9a7596
AD
79}
80
e8ac2138 81sub get_hostname_fqdn {
9a6ccb12
DC
82 my ($conf, $vmid) = @_;
83 my $hostname = $conf->{name} // "VM$vmid";
e8ac2138
WB
84 my $fqdn;
85 if ($hostname =~ /\./) {
86 $fqdn = $hostname;
87 $hostname =~ s/\..*$//;
88 } elsif (my $search = $conf->{searchdomain}) {
89 $fqdn = "$hostname.$search";
0c9a7596 90 }
e8ac2138 91 return ($hostname, $fqdn);
41cd94a0
WB
92}
93
67864d19
WB
94sub get_dns_conf {
95 my ($conf) = @_;
96
97 # Same logic as in pve-container, but without the testcase special case
98 my $host_resolv_conf = PVE::INotify::read_file('resolvconf');
99
100 my $searchdomains = [
101 split(/\s+/, $conf->{searchdomain} // $host_resolv_conf->{search})
102 ];
103
104 my $nameserver = $conf->{nameserver};
105 if (!defined($nameserver)) {
106 $nameserver = [grep { $_ } $host_resolv_conf->@{qw(dns1 dns2 dns3)}];
107 } else {
108 $nameserver = [split(/\s+/, $nameserver)];
109 }
110
111 return ($searchdomains, $nameserver);
112}
113
41cd94a0 114sub cloudinit_userdata {
9a6ccb12 115 my ($conf, $vmid) = @_;
41cd94a0 116
9a6ccb12 117 my ($hostname, $fqdn) = get_hostname_fqdn($conf, $vmid);
41cd94a0 118
7b42f951 119 my $content = "#cloud-config\n";
41cd94a0 120
e8ac2138 121 $content .= "hostname: $hostname\n";
8de34458 122 $content .= "manage_etc_hosts: true\n";
e8ac2138
WB
123 $content .= "fqdn: $fqdn\n" if defined($fqdn);
124
7b42f951
WB
125 my $username = $conf->{ciuser};
126 my $password = $conf->{cipassword};
41cd94a0
WB
127
128 $content .= "user: $username\n" if defined($username);
7b42f951
WB
129 $content .= "disable_root: False\n" if defined($username) && $username eq 'root';
130 $content .= "password: $password\n" if defined($password);
41cd94a0
WB
131
132 if (defined(my $keys = $conf->{sshkeys})) {
0c9a7596 133 $keys = URI::Escape::uri_unescape($keys);
f7d1505b 134 $keys = [map { my $key = $_; chomp $key; $key } split(/\n/, $keys)];
0c9a7596 135 $keys = [grep { /\S/ } @$keys];
41cd94a0 136 $content .= "ssh_authorized_keys:\n";
0c9a7596 137 foreach my $k (@$keys) {
41cd94a0 138 $content .= " - $k\n";
0c9a7596
AD
139 }
140 }
41cd94a0
WB
141 $content .= "chpasswd:\n";
142 $content .= " expire: False\n";
143
7b42f951
WB
144 if (!defined($username) || $username ne 'root') {
145 $content .= "users:\n";
146 $content .= " - default\n";
147 }
0c9a7596
AD
148
149 $content .= "package_upgrade: true\n";
150
0c9a7596
AD
151 return $content;
152}
153
86280789
WB
154sub split_ip4 {
155 my ($ip) = @_;
156 my ($addr, $mask) = split('/', $ip);
157 die "not a CIDR: $ip\n" if !defined $mask;
158 return ($addr, $PVE::Network::ipv4_reverse_mask->[$mask]);
159}
160
41cd94a0
WB
161sub configdrive2_network {
162 my ($conf) = @_;
0c9a7596
AD
163
164 my $content = "auto lo\n";
67864d19
WB
165 $content .= "iface lo inet loopback\n\n";
166
167 my ($searchdomains, $nameservers) = get_dns_conf($conf);
168 if ($nameservers && @$nameservers) {
169 $nameservers = join(' ', @$nameservers);
170 $content .= " dns_nameservers $nameservers\n";
171 }
172 if ($searchdomains && @$searchdomains) {
173 $searchdomains = join(' ', @$searchdomains);
174 $content .= " dns_search $searchdomains\n";
175 }
0c9a7596 176
6ef6d68f 177 my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
d4fa9981 178 foreach my $iface (sort @ifaces) {
0c9a7596
AD
179 (my $id = $iface) =~ s/^net//;
180 next if !$conf->{"ipconfig$id"};
41cd94a0 181 my $net = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
0c9a7596
AD
182 $id = "eth$id";
183
184 $content .="auto $id\n";
185 if ($net->{ip}) {
186 if ($net->{ip} eq 'dhcp') {
187 $content .= "iface $id inet dhcp\n";
188 } else {
86280789 189 my ($addr, $mask) = split_ip4($net->{ip});
0c9a7596 190 $content .= "iface $id inet static\n";
6f3999e0
ML
191 $content .= " address $addr\n";
192 $content .= " netmask $mask\n";
193 $content .= " gateway $net->{gw}\n" if $net->{gw};
0c9a7596
AD
194 }
195 }
196 if ($net->{ip6}) {
197 if ($net->{ip6} =~ /^(auto|dhcp)$/) {
198 $content .= "iface $id inet6 $1\n";
199 } else {
200 my ($addr, $mask) = split('/', $net->{ip6});
201 $content .= "iface $id inet6 static\n";
6f3999e0
ML
202 $content .= " address $addr\n";
203 $content .= " netmask $mask\n";
204 $content .= " gateway $net->{gw6}\n" if $net->{gw6};
0c9a7596
AD
205 }
206 }
207 }
208
0c9a7596
AD
209 return $content;
210}
211
e73ca4d0
ML
212sub configdrive2_gen_metadata {
213 my ($user, $network) = @_;
214
215 my $uuid_str = Digest::SHA::sha1_hex($user.$network);
216 return configdrive2_metadata($uuid_str);
217}
218
41cd94a0
WB
219sub configdrive2_metadata {
220 my ($uuid) = @_;
221 return <<"EOF";
222{
223 "uuid": "$uuid",
224 "network_config": { "content_path": "/content/0000" }
225}
226EOF
227}
228
229sub generate_configdrive2 {
230 my ($conf, $vmid, $drive, $volname, $storeid) = @_;
231
101beafe 232 my ($user_data, $network_data, $meta_data, $vendor_data) = get_custom_cloudinit_files($conf);
cb702ebe
DL
233 $user_data = cloudinit_userdata($conf, $vmid) if !defined($user_data);
234 $network_data = configdrive2_network($conf) if !defined($network_data);
ea18b604 235 $vendor_data = '' if !defined($vendor_data);
41cd94a0 236
cb702ebe 237 if (!defined($meta_data)) {
e73ca4d0 238 $meta_data = configdrive2_gen_metadata($user_data, $network_data);
cb702ebe 239 }
101beafe 240
115cb432
TL
241 # we always allocate a 4MiB disk for cloudinit and with the overhead of the ISO
242 # make sure we always stay below it by keeping the sum of all files below 3 MiB
101beafe
CH
243 my $sum = length($user_data) + length($network_data) + length($meta_data) + length($vendor_data);
244 die "Cloud-Init sum of snippets too big (> 3 MiB)\n" if $sum > (3 * 1024 * 1024);
245
f62c36cf
WB
246 my $files = {
247 '/openstack/latest/user_data' => $user_data,
248 '/openstack/content/0000' => $network_data,
101beafe
CH
249 '/openstack/latest/meta_data.json' => $meta_data,
250 '/openstack/latest/vendor_data.json' => $vendor_data
f62c36cf 251 };
4a853915 252 commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'config-2');
41cd94a0
WB
253}
254
545eec65
AD
255sub generate_opennebula {
256 my ($conf, $vmid, $drive, $volname, $storeid) = @_;
257
545eec65
AD
258 my $content = "";
259
260 my $username = $conf->{ciuser} || "root";
545eec65 261 $content .= "USERNAME=$username\n" if defined($username);
545eec65 262
c077cc16
TL
263 if (defined(my $password = $conf->{cipassword})) {
264 $content .= "CRYPTED_PASSWORD_BASE64=". encode_base64($password) ."\n";
545eec65
AD
265 }
266
c077cc16
TL
267 if (defined($conf->{sshkeys})) {
268 my $keys = [ split(/\s*\n\s*/, URI::Escape::uri_unescape($conf->{sshkeys})) ];
269 $content .= "SSH_PUBLIC_KEY=\"". join("\n", $keys->@*) ."\"\n";
545eec65
AD
270 }
271
c077cc16 272 my ($hostname, $fqdn) = get_hostname_fqdn($conf, $vmid);
545eec65
AD
273 $content .= "SET_HOSTNAME=$hostname\n";
274
c077cc16
TL
275 my ($searchdomains, $nameservers) = get_dns_conf($conf);
276 $content .= 'DNS="' . join(' ', @$nameservers) ."\"\n" if $nameservers && @$nameservers;
277 $content .= 'SEARCH_DOMAIN="'. join(' ', @$searchdomains) ."\"\n" if $searchdomains && @$searchdomains;
545eec65
AD
278
279 my $networkenabled = undef;
280 my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
281 foreach my $iface (sort @ifaces) {
282 (my $id = $iface) =~ s/^net//;
283 my $net = PVE::QemuServer::parse_net($conf->{$iface});
284 next if !$conf->{"ipconfig$id"};
285 my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
286 my $ethid = "ETH$id";
287
288 my $mac = lc $net->{hwaddr};
289
290 if ($ipconfig->{ip}) {
291 $networkenabled = 1;
292
293 if ($ipconfig->{ip} eq 'dhcp') {
2eee6748 294 $content .= "${ethid}_DHCP=YES\n";
545eec65
AD
295 } else {
296 my ($addr, $mask) = split_ip4($ipconfig->{ip});
2eee6748
TL
297 $content .= "${ethid}_IP=$addr\n";
298 $content .= "${ethid}_MASK=$mask\n";
299 $content .= "${ethid}_MAC=$mac\n";
300 $content .= "${ethid}_GATEWAY=$ipconfig->{gw}\n" if $ipconfig->{gw};
545eec65 301 }
2eee6748 302 $content .= "${ethid}_MTU=$net->{mtu}\n" if $net->{mtu};
545eec65
AD
303 }
304
305 if ($ipconfig->{ip6}) {
306 $networkenabled = 1;
307 if ($ipconfig->{ip6} eq 'dhcp') {
2eee6748 308 $content .= "${ethid}_DHCP6=YES\n";
545eec65 309 } elsif ($ipconfig->{ip6} eq 'auto') {
2eee6748 310 $content .= "${ethid}_AUTO6=YES\n";
545eec65
AD
311 } else {
312 my ($addr, $mask) = split('/', $ipconfig->{ip6});
2eee6748
TL
313 $content .= "${ethid}_IP6=$addr\n";
314 $content .= "${ethid}_MASK6=$mask\n";
315 $content .= "${ethid}_MAC6=$mac\n";
316 $content .= "${ethid}_GATEWAY6=$ipconfig->{gw6}\n" if $ipconfig->{gw6};
545eec65 317 }
2eee6748 318 $content .= "${ethid}_MTU=$net->{mtu}\n" if $net->{mtu};
545eec65
AD
319 }
320 }
321
322 $content .= "NETWORK=YES\n" if $networkenabled;
323
c077cc16 324 my $files = { '/context.sh' => $content };
545eec65
AD
325 commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'CONTEXT');
326}
327
41cd94a0
WB
328sub nocloud_network_v2 {
329 my ($conf) = @_;
330
331 my $content = '';
332
333 my $head = "version: 2\n"
334 . "ethernets:\n";
335
67864d19 336 my $dns_done;
41cd94a0 337
6ef6d68f 338 my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
d4fa9981 339 foreach my $iface (sort @ifaces) {
41cd94a0
WB
340 (my $id = $iface) =~ s/^net//;
341 next if !$conf->{"ipconfig$id"};
342
343 # indentation - network interfaces are inside an 'ethernets' hash
344 my $i = ' ';
345
346 my $net = PVE::QemuServer::parse_net($conf->{$iface});
347 my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
348
349 my $mac = $net->{macaddr}
350 or die "network interface '$iface' has no mac address\n";
351
67864d19 352 $content .= "${i}$iface:\n";
41cd94a0 353 $i .= ' ';
67864d19
WB
354 $content .= "${i}match:\n"
355 . "${i} macaddress: \"$mac\"\n"
356 . "${i}set-name: eth$id\n";
41cd94a0
WB
357 my @addresses;
358 if (defined(my $ip = $ipconfig->{ip})) {
359 if ($ip eq 'dhcp') {
67864d19 360 $content .= "${i}dhcp4: true\n";
41cd94a0
WB
361 } else {
362 push @addresses, $ip;
363 }
364 }
365 if (defined(my $ip = $ipconfig->{ip6})) {
366 if ($ip eq 'dhcp') {
67864d19 367 $content .= "${i}dhcp6: true\n";
41cd94a0
WB
368 } else {
369 push @addresses, $ip;
370 }
371 }
372 if (@addresses) {
67864d19 373 $content .= "${i}addresses:\n";
4efb58a9 374 $content .= "${i}- '$_'\n" foreach @addresses;
41cd94a0
WB
375 }
376 if (defined(my $gw = $ipconfig->{gw})) {
4efb58a9 377 $content .= "${i}gateway4: '$gw'\n";
41cd94a0
WB
378 }
379 if (defined(my $gw = $ipconfig->{gw6})) {
4efb58a9 380 $content .= "${i}gateway6: '$gw'\n";
41cd94a0
WB
381 }
382
67864d19
WB
383 next if $dns_done;
384 $dns_done = 1;
385
386 my ($searchdomains, $nameservers) = get_dns_conf($conf);
387 if ($searchdomains || $nameservers) {
388 $content .= "${i}nameservers:\n";
389 if (defined($nameservers) && @$nameservers) {
390 $content .= "${i} addresses:\n";
4efb58a9 391 $content .= "${i} - '$_'\n" foreach @$nameservers;
67864d19
WB
392 }
393 if (defined($searchdomains) && @$searchdomains) {
394 $content .= "${i} search:\n";
4efb58a9 395 $content .= "${i} - '$_'\n" foreach @$searchdomains;
41cd94a0
WB
396 }
397 }
41cd94a0
WB
398 }
399
400 return $head.$content;
401}
402
403sub nocloud_network {
404 my ($conf) = @_;
405
406 my $content = "version: 1\n"
407 . "config:\n";
408
6ef6d68f 409 my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
d4fa9981 410 foreach my $iface (sort @ifaces) {
41cd94a0
WB
411 (my $id = $iface) =~ s/^net//;
412 next if !$conf->{"ipconfig$id"};
413
414 # indentation - network interfaces are inside an 'ethernets' hash
415 my $i = ' ';
416
417 my $net = PVE::QemuServer::parse_net($conf->{$iface});
418 my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
419
c3cedb3d 420 my $mac = lc($net->{macaddr})
41cd94a0
WB
421 or die "network interface '$iface' has no mac address\n";
422
67864d19
WB
423 $content .= "${i}- type: physical\n"
424 . "${i} name: eth$id\n"
4efb58a9 425 . "${i} mac_address: '$mac'\n"
67864d19 426 . "${i} subnets:\n";
41cd94a0
WB
427 $i .= ' ';
428 if (defined(my $ip = $ipconfig->{ip})) {
429 if ($ip eq 'dhcp') {
67864d19 430 $content .= "${i}- type: dhcp4\n";
41cd94a0 431 } else {
86280789 432 my ($addr, $mask) = split_ip4($ip);
67864d19 433 $content .= "${i}- type: static\n"
4efb58a9
DL
434 . "${i} address: '$addr'\n"
435 . "${i} netmask: '$mask'\n";
41cd94a0 436 if (defined(my $gw = $ipconfig->{gw})) {
4efb58a9 437 $content .= "${i} gateway: '$gw'\n";
41cd94a0
WB
438 }
439 }
440 }
441 if (defined(my $ip = $ipconfig->{ip6})) {
442 if ($ip eq 'dhcp') {
67864d19 443 $content .= "${i}- type: dhcp6\n";
c701be32 444 } elsif ($ip eq 'auto') {
988be8d0
ML
445 # SLAAC is only supported by cloud-init since 19.4
446 $content .= "${i}- type: ipv6_slaac\n";
41cd94a0 447 } else {
617a864a 448 $content .= "${i}- type: static6\n"
4efb58a9 449 . "${i} address: '$ip'\n";
41cd94a0 450 if (defined(my $gw = $ipconfig->{gw6})) {
4efb58a9 451 $content .= "${i} gateway: '$gw'\n";
41cd94a0
WB
452 }
453 }
454 }
41cd94a0
WB
455 }
456
67864d19
WB
457 my $i = ' ';
458 my ($searchdomains, $nameservers) = get_dns_conf($conf);
459 if ($searchdomains || $nameservers) {
41cd94a0 460 $content .= "${i}- type: nameserver\n";
67864d19 461 if (defined($nameservers) && @$nameservers) {
41cd94a0 462 $content .= "${i} address:\n";
4efb58a9 463 $content .= "${i} - '$_'\n" foreach @$nameservers;
41cd94a0 464 }
67864d19 465 if (defined($searchdomains) && @$searchdomains) {
41cd94a0 466 $content .= "${i} search:\n";
4efb58a9 467 $content .= "${i} - '$_'\n" foreach @$searchdomains;
41cd94a0
WB
468 }
469 }
470
471 return $content;
472}
473
474sub nocloud_metadata {
e8ac2138
WB
475 my ($uuid) = @_;
476 return "instance-id: $uuid\n";
41cd94a0
WB
477}
478
e73ca4d0
ML
479sub nocloud_gen_metadata {
480 my ($user, $network) = @_;
481
482 my $uuid_str = Digest::SHA::sha1_hex($user.$network);
483 return nocloud_metadata($uuid_str);
484}
485
41cd94a0
WB
486sub generate_nocloud {
487 my ($conf, $vmid, $drive, $volname, $storeid) = @_;
488
101beafe 489 my ($user_data, $network_data, $meta_data, $vendor_data) = get_custom_cloudinit_files($conf);
cb702ebe
DL
490 $user_data = cloudinit_userdata($conf, $vmid) if !defined($user_data);
491 $network_data = nocloud_network($conf) if !defined($network_data);
ea18b604 492 $vendor_data = '' if !defined($vendor_data);
41cd94a0 493
cb702ebe 494 if (!defined($meta_data)) {
e73ca4d0 495 $meta_data = nocloud_gen_metadata($user_data, $network_data);
cb702ebe 496 }
41cd94a0 497
115cb432
TL
498 # we always allocate a 4MiB disk for cloudinit and with the overhead of the ISO
499 # make sure we always stay below it by keeping the sum of all files below 3 MiB
101beafe
CH
500 my $sum = length($user_data) + length($network_data) + length($meta_data) + length($vendor_data);
501 die "Cloud-Init sum of snippets too big (> 3 MiB)\n" if $sum > (3 * 1024 * 1024);
502
f62c36cf
WB
503 my $files = {
504 '/user-data' => $user_data,
505 '/network-config' => $network_data,
101beafe
CH
506 '/meta-data' => $meta_data,
507 '/vendor-data' => $vendor_data
f62c36cf 508 };
4a853915 509 commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'cidata');
41cd94a0
WB
510}
511
cb702ebe
DL
512sub get_custom_cloudinit_files {
513 my ($conf) = @_;
514
515 my $cicustom = $conf->{cicustom};
516 my $files = $cicustom ? PVE::JSONSchema::parse_property_string('pve-qm-cicustom', $cicustom) : {};
517
518 my $network_volid = $files->{network};
519 my $user_volid = $files->{user};
520 my $meta_volid = $files->{meta};
101beafe 521 my $vendor_volid = $files->{vendor};
cb702ebe
DL
522
523 my $storage_conf = PVE::Storage::config();
524
525 my $network_data;
526 if ($network_volid) {
527 $network_data = read_cloudinit_snippets_file($storage_conf, $network_volid);
528 }
529
530 my $user_data;
531 if ($user_volid) {
532 $user_data = read_cloudinit_snippets_file($storage_conf, $user_volid);
533 }
534
535 my $meta_data;
536 if ($meta_volid) {
537 $meta_data = read_cloudinit_snippets_file($storage_conf, $meta_volid);
538 }
539
101beafe
CH
540 my $vendor_data;
541 if ($vendor_volid) {
542 $vendor_data = read_cloudinit_snippets_file($storage_conf, $vendor_volid);
543 }
544
545 return ($user_data, $network_data, $meta_data, $vendor_data);
cb702ebe
DL
546}
547
548sub read_cloudinit_snippets_file {
549 my ($storage_conf, $volid) = @_;
550
551 my ($full_path, undef, $type) = PVE::Storage::path($storage_conf, $volid);
552 die "$volid is not in the snippets directory\n" if $type ne 'snippets';
7e8ab2a9 553 return PVE::Tools::file_get_contents($full_path, 1 * 1024 * 1024);
cb702ebe
DL
554}
555
41cd94a0
WB
556my $cloudinit_methods = {
557 configdrive2 => \&generate_configdrive2,
558 nocloud => \&generate_nocloud,
545eec65 559 opennebula => \&generate_opennebula,
41cd94a0
WB
560};
561
562sub generate_cloudinitconfig {
563 my ($conf, $vmid) = @_;
564
565 my $format = get_cloudinit_format($conf);
566
912792e2 567 PVE::QemuConfig->foreach_volume($conf, sub {
41cd94a0 568 my ($ds, $drive) = @_;
41cd94a0
WB
569 my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
570
571 return if !$volname || $volname !~ m/vm-$vmid-cloudinit/;
572
573 my $generator = $cloudinit_methods->{$format}
574 or die "missing cloudinit methods for format '$format'\n";
575
576 $generator->($conf, $vmid, $drive, $volname, $storeid);
577 });
95a5135d 578
b137c30c 579 my $cloudinit_conf = {};
95a5135d
AD
580
581 my @cloudinit_opts = keys %{PVE::QemuServer::cloudinit_config_properties()};
582 push @cloudinit_opts, 'name';
583
584 for my $opt (@cloudinit_opts) {
95a5135d
AD
585 if ($opt =~ m/^ipconfig(\d+)/) {
586 my $netid = "net$1";
587 next if !defined($conf->{$netid});
b137c30c 588 $cloudinit_conf->{$netid} = $conf->{$netid};
95a5135d
AD
589 }
590
b137c30c 591 $cloudinit_conf->{$opt} = $conf->{$opt} if $conf->{$opt};
95a5135d
AD
592 }
593
b137c30c 594 my $has_cloudinit_drive = 0;
95a5135d
AD
595 for my $opt (keys %{$conf}) {
596 if (PVE::QemuServer::is_valid_drivename($opt)) {
597 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
598 if (PVE::QemuServer::drive_is_cloudinit($drive)) {
b137c30c
TL
599 $has_cloudinit_drive = 1;
600 $cloudinit_conf->{$opt} = $conf->{$opt};
95a5135d
AD
601 }
602 }
603 }
b137c30c 604 $cloudinit_conf->{name} //= "VM$vmid" if $has_cloudinit_drive;
95a5135d 605
b137c30c 606 return $cloudinit_conf;
41cd94a0 607}
0c9a7596 608
e73ca4d0
ML
609sub dump_cloudinit_config {
610 my ($conf, $vmid, $type) = @_;
611
612 my $format = get_cloudinit_format($conf);
613
614 if ($type eq 'user') {
615 return cloudinit_userdata($conf, $vmid);
616 } elsif ($type eq 'network') {
617 if ($format eq 'nocloud') {
618 return nocloud_network($conf);
619 } else {
620 return configdrive2_network($conf);
621 }
622 } else { # metadata config
623 my $user = cloudinit_userdata($conf, $vmid);
624 if ($format eq 'nocloud') {
625 my $network = nocloud_network($conf);
626 return nocloud_gen_metadata($user, $network);
627 } else {
628 my $network = configdrive2_network($conf);
629 return configdrive2_gen_metadata($user, $network);
630 }
631 }
632}
633
2be1fb0a
AD
634sub get_pending_config {
635 my ($conf, $vmid) = @_;
636
637 my $newconf = dclone($conf);
638
639 my $cloudinit_current = $newconf->{cloudinit};
640 my @cloudinit_opts = keys %{PVE::QemuServer::cloudinit_config_properties()};
641 push @cloudinit_opts, 'name';
642
643 #add cloud-init drive
644 my $drives = {};
645 PVE::QemuConfig->foreach_volume($newconf, sub {
646 my ($ds, $drive) = @_;
647 $drives->{$ds} = 1 if PVE::QemuServer::drive_is_cloudinit($drive);
648 });
649
650 PVE::QemuConfig->foreach_volume($cloudinit_current, sub {
651 my ($ds, $drive) = @_;
652 $drives->{$ds} = 1 if PVE::QemuServer::drive_is_cloudinit($drive);
653 });
654 for my $ds (keys %{$drives}) {
655 push @cloudinit_opts, $ds;
656 }
657
658 $newconf->{name} = "VM$vmid" if !$newconf->{name};
659 $cloudinit_current->{name} = "VM$vmid" if !$cloudinit_current->{name};
660
661 #only mac-address is used in cloud-init config.
662 #We don't want to display other pending net changes.
663 my $print_cloudinit_net = sub {
664 my ($conf, $opt) = @_;
665
666 if (defined($conf->{$opt})) {
667 my $net = PVE::QemuServer::parse_net($conf->{$opt});
668 $conf->{$opt} = "macaddr=".$net->{macaddr} if $net->{macaddr};
669 }
670 };
671
672 my $cloudinit_options = {};
673 for my $opt (@cloudinit_opts) {
674 if ($opt =~ m/^ipconfig(\d+)/) {
675 my $netid = "net$1";
676
677 next if !defined($newconf->{$netid}) && !defined($cloudinit_current->{$netid}) &&
678 !defined($newconf->{$opt}) && !defined($cloudinit_current->{$opt});
679
680 &$print_cloudinit_net($newconf, $netid);
681 &$print_cloudinit_net($cloudinit_current, $netid);
682 $cloudinit_options->{$netid} = 1;
683 }
684 $cloudinit_options->{$opt} = 1;
685 }
686
687 my $res = [];
688
689 for my $opt (keys %{$cloudinit_options}) {
690
691 my $item = {
692 key => $opt,
693 };
694 if ($cloudinit_current->{$opt}) {
695 $item->{value} = $cloudinit_current->{$opt};
696 if (defined($newconf->{$opt})) {
697 $item->{pending} = $newconf->{$opt}
698 if $newconf->{$opt} ne $cloudinit_current->{$opt};
699 } else {
700 $item->{delete} = 1;
701 }
702 } else {
703 $item->{pending} = $newconf->{$opt} if $newconf->{$opt}
704 }
705
706 push @$res, $item;
707 }
708
709 return $res;
710}
711
0c9a7596 7121;