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