With configdrives we end up with the /etc/network/interfaces file containing the interface names we use on the disk, ie. eth0/eth1/..., which doesn't work on systems which do not use this name.
With the 'nocloud' image type we can provide a network-config in yaml which matches mac addresses. Ideally we'd use version 2, but debian stretch ships with a too old cloud-init for this, so for now we're writing version 1. Signed-off-by: Wolfgang Bumiller <[email protected]> --- PVE/QemuServer.pm | 6 + PVE/QemuServer/Cloudinit.pm | 376 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 318 insertions(+), 64 deletions(-) diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index 3f7118d..79425eb 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -540,6 +540,12 @@ EODESCR }; my $confdesc_cloudinit = { + citype => { + optional => 1, + type => 'string', + description => 'Specifies the cloud-init configuration format.', + enum => ['configdrive2', 'nocloud'], + }, searchdomain => { optional => 1, type => 'string', diff --git a/PVE/QemuServer/Cloudinit.pm b/PVE/QemuServer/Cloudinit.pm index 566c723..14a5183 100644 --- a/PVE/QemuServer/Cloudinit.pm +++ b/PVE/QemuServer/Cloudinit.pm @@ -12,64 +12,59 @@ use PVE::Storage; use PVE::QemuServer; sub commit_cloudinit_disk { - my ($file_path, $iso_path, $format) = @_; + my ($conf, $drive, $volname, $storeid, $file_path, $label) = @_; + + my $storecfg = PVE::Storage::config(); + my $iso_path = PVE::Storage::path($storecfg, $drive->{file}); + my $scfg = PVE::Storage::storage_config($storecfg, $storeid); + my $format = PVE::QemuServer::qemu_img_format($scfg, $volname); my $size = PVE::Storage::file_size_info($iso_path); - run_command([['genisoimage', '-R', '-V', 'config-2', $file_path], + run_command([['genisoimage', '-R', '-V', $label, $file_path], ['qemu-img', 'dd', '-f', 'raw', '-O', $format, 'isize=0', "osize=$size", "of=$iso_path"]]); } -sub generate_cloudinitconfig { - my ($conf, $vmid) = @_; - - PVE::QemuServer::foreach_drive($conf, sub { - my ($ds, $drive) = @_; +sub get_cloudinit_format { + my ($conf) = @_; + if (defined(my $format = $conf->{citype})) { + return $format; + } - my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1); + # No format specified, default based on ostype because windows' + # cloudbased-init only supports configdrivev2, whereas on linux we need + # to use mac addresses because regular cloudinit doesn't map 'ethX' to + # the new predicatble network device naming scheme. + if (defined(my $ostype = $conf->{ostype})) { + return 'configdrive2' + if PVE::QemuServer::windows_version($ostype); + } - return if !$volname || $volname !~ m/vm-$vmid-cloudinit/; + return 'nocloud'; +} - my $path = "/tmp/cloudinit/$vmid"; - - mkdir "/tmp/cloudinit"; - mkdir $path; - mkdir "$path/drive"; - mkdir "$path/drive/openstack"; - mkdir "$path/drive/openstack/latest"; - mkdir "$path/drive/openstack/content"; - my $digest_data = generate_cloudinit_userdata($conf, $path) - . generate_cloudinit_network($conf, $path); - generate_cloudinit_metadata($conf, $path, $digest_data); - - my $storecfg = PVE::Storage::config(); - my $iso_path = PVE::Storage::path($storecfg, $drive->{file}); - my $scfg = PVE::Storage::storage_config($storecfg, $storeid); - my $format = PVE::QemuServer::qemu_img_format($scfg, $volname); - #fixme : add meta as drive property to compare - commit_cloudinit_disk("$path/drive", $iso_path, $format); - rmtree("$path/drive"); - }); +sub get_fqdn { + my ($conf) = @_; + my $fqdn = $conf->{hostname}; + if (!defined($fqdn)) { + $fqdn = $conf->{name}; + if (my $search = $conf->{searchdomain}) { + $fqdn .= ".$search"; + } + } + return $fqdn; } +sub configdrive2_userdata { + my ($conf) = @_; -sub generate_cloudinit_userdata { - my ($conf, $path) = @_; + my $fqdn = get_fqdn($conf); my $content = "#cloud-config\n"; - my $hostname = $conf->{hostname}; - if (!defined($hostname)) { - $hostname = $conf->{name}; - if (my $search = $conf->{searchdomain}) { - $hostname .= ".$search"; - } - } - $content .= "fqdn: $hostname\n"; + $content .= "fqdn: $fqdn\n"; $content .= "manage_etc_hosts: true\n"; - $content .= "bootcmd: \n"; - $content .= " - ifdown -a\n"; - $content .= " - ifup -a\n"; + $content .= "manage_resolv_conf: true\n"; my $keys = $conf->{sshkeys}; if ($keys) { @@ -88,28 +83,11 @@ sub generate_cloudinit_userdata { $content .= "package_upgrade: true\n"; - my $fn = "$path/drive/openstack/latest/user_data"; - file_set_contents($fn, $content); return $content; } -sub generate_cloudinit_metadata { - my ($conf, $path, $digest_data) = @_; - - my $uuid_str = Digest::SHA::sha1_hex($digest_data); - - my $content = "{\n"; - $content .= " \"uuid\": \"$uuid_str\",\n"; - $content .= " \"network_config\" :{ \"content_path\": \"/content/0000\"}\n"; - $content .= "}\n"; - - my $fn = "$path/drive/openstack/latest/meta_data.json"; - - file_set_contents($fn, $content); -} - -sub generate_cloudinit_network { - my ($conf, $path) = @_; +sub configdrive2_network { + my ($conf) = @_; my $content = "auto lo\n"; $content .="iface lo inet loopback\n\n"; @@ -118,7 +96,7 @@ sub generate_cloudinit_network { foreach my $iface (@ifaces) { (my $id = $iface) =~ s/^net//; next if !$conf->{"ipconfig$id"}; - my $net = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"}); + my $net = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"}); $id = "eth$id"; $content .="auto $id\n"; @@ -149,10 +127,280 @@ sub generate_cloudinit_network { $content .=" dns_nameservers $conf->{nameserver}\n" if $conf->{nameserver}; $content .=" dns_search $conf->{searchdomain}\n" if $conf->{searchdomain}; - my $fn = "$path/drive/openstack/content/0000"; - file_set_contents($fn, $content); return $content; } +sub configdrive2_metadata { + my ($uuid) = @_; + return <<"EOF"; +{ + "uuid": "$uuid", + "network_config": { "content_path": "/content/0000" } +} +EOF +} + +sub generate_configdrive2 { + my ($conf, $vmid, $drive, $volname, $storeid) = @_; + + my $user_data = configdrive2_userdata($conf); + my $network_data = configdrive2_network($conf); + + my $digest_data = $user_data . $network_data; + my $uuid_str = Digest::SHA::sha1_hex($digest_data); + + my $meta_data = configdrive2_metadata($uuid_str); + + mkdir "/tmp/cloudinit"; + my $path = "/tmp/cloudinit/$vmid"; + mkdir $path; + mkdir "$path/drive"; + mkdir "$path/drive/openstack"; + mkdir "$path/drive/openstack/latest"; + mkdir "$path/drive/openstack/content"; + file_set_contents("$path/drive/openstack/latest/user_data", $user_data); + file_set_contents("$path/drive/openstack/content/0000", $network_data); + file_set_contents("$path/drive/openstack/latest/meta_data.json", $meta_data); + + commit_cloudinit_disk($conf, $drive, $volname, $storeid, "$path/drive", 'config-2'); + + rmtree("$path/drive"); +} + +sub nocloud_userdata { + my ($conf) = @_; + + my $fqdn = get_fqdn($conf); + + my $content = <<"EOF"; +#cloud-config +manage_resolv_conf: true +EOF + + my $keys = $conf->{sshkeys}; + if ($keys) { + $keys = URI::Escape::uri_unescape($keys); + $keys = [map { chomp $_; $_ } split(/\n/, $keys)]; + $keys = [grep { /\S/ } @$keys]; + + $content .= "users:\n"; + $content .= " - default\n"; + $content .= " - name: root\n"; + $content .= " ssh-authorized-keys:\n"; + foreach my $k (@$keys) { + $content .= " - $k\n"; + } + } + + $content .= "package_upgrade: true\n"; + + return $content; +} + +sub nocloud_network_v2 { + my ($conf) = @_; + + my $content = ''; + + my $head = "version: 2\n" + . "ethernets:\n"; + + my $nameservers_done; + + my @ifaces = grep(/^net(\d+)$/, keys %$conf); + foreach my $iface (@ifaces) { + (my $id = $iface) =~ s/^net//; + next if !$conf->{"ipconfig$id"}; + + # indentation - network interfaces are inside an 'ethernets' hash + my $i = ' '; + + my $net = PVE::QemuServer::parse_net($conf->{$iface}); + my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"}); + + my $mac = $net->{macaddr} + or die "network interface '$iface' has no mac address\n"; + + my $data = "${i}$iface:\n"; + $i .= ' '; + $data .= "${i}match:\n" + . "${i} macaddress: \"$mac\"\n" + . "${i}set-name: eth$id\n"; + my @addresses; + if (defined(my $ip = $ipconfig->{ip})) { + if ($ip eq 'dhcp') { + $data .= "${i}dhcp4: true\n"; + } else { + push @addresses, $ip; + } + } + if (defined(my $ip = $ipconfig->{ip6})) { + if ($ip eq 'dhcp') { + $data .= "${i}dhcp6: true\n"; + } else { + push @addresses, $ip; + } + } + if (@addresses) { + $data .= "${i}addresses:\n"; + $data .= "${i}- $_\n" foreach @addresses; + } + if (defined(my $gw = $ipconfig->{gw})) { + $data .= "${i}gateway4: $gw\n"; + } + if (defined(my $gw = $ipconfig->{gw6})) { + $data .= "${i}gateway6: $gw\n"; + } + + if (!$nameservers_done) { + $nameservers_done = 1; + + my $nameserver = $conf->{nameserver} // ''; + my $searchdomain = $conf->{searchdomain} // ''; + my @nameservers = PVE::Tools::split_list($nameserver); + my @searchdomains = PVE::Tools::split_list($searchdomain); + if (@nameservers || @searchdomains) { + $data .= "${i}nameservers:\n"; + $data .= "${i} addresses: [".join(',', @nameservers)."]\n" + if @nameservers; + $data .= "${i} search: [".join(',', @searchdomains)."]\n" + if @searchdomains; + } + } + + + $content .= $data; + } + + return $head.$content; +} + +sub nocloud_network { + my ($conf) = @_; + + my $content = "version: 1\n" + . "config:\n"; + + my @ifaces = grep(/^net(\d+)$/, keys %$conf); + foreach my $iface (@ifaces) { + (my $id = $iface) =~ s/^net//; + next if !$conf->{"ipconfig$id"}; + + # indentation - network interfaces are inside an 'ethernets' hash + my $i = ' '; + + my $net = PVE::QemuServer::parse_net($conf->{$iface}); + my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"}); + + my $mac = $net->{macaddr} + or die "network interface '$iface' has no mac address\n"; + + my $data = "${i}- type: physical\n" + . "${i} name: eth$id\n" + . "${i} mac_address: $mac\n" + . "${i} subnets:\n"; + $i .= ' '; + if (defined(my $ip = $ipconfig->{ip})) { + if ($ip eq 'dhcp') { + $data .= "${i}- type: dhcp4\n"; + } else { + $data .= "${i}- type: static\n" + . "${i} address: $ip\n"; + if (defined(my $gw = $ipconfig->{gw})) { + $data .= "${i} gateway: $gw\n"; + } + } + } + if (defined(my $ip = $ipconfig->{ip6})) { + if ($ip eq 'dhcp') { + $data .= "${i}- type: dhcp6\n"; + } else { + $data .= "${i}- type: static6\n" + . "${i} address: $ip\n"; + if (defined(my $gw = $ipconfig->{gw6})) { + $data .= "${i} gateway: $gw\n"; + } + } + } + + $content .= $data; + } + + my $nameserver = $conf->{nameserver} // ''; + my $searchdomain = $conf->{searchdomain} // ''; + my @nameservers = PVE::Tools::split_list($nameserver); + my @searchdomains = PVE::Tools::split_list($searchdomain); + if (@nameservers || @searchdomains) { + my $i = ' '; + $content .= "${i}- type: nameserver\n"; + if (@nameservers) { + $content .= "${i} address:\n"; + $content .= "${i} - $_\n" foreach @nameservers; + } + if (@searchdomains) { + $content .= "${i} search:\n"; + $content .= "${i} - $_\n" foreach @searchdomains; + } + } + + return $content; +} + +sub nocloud_metadata { + my ($uuid, $hostname) = @_; + return <<"EOF"; +instance-id: $uuid +local-hostname: $hostname +EOF +} + +sub generate_nocloud { + my ($conf, $vmid, $drive, $volname, $storeid) = @_; + + my $hostname = $conf->{hostname} // ''; + my $user_data = nocloud_userdata($conf); + my $network_data = nocloud_network($conf); + + my $digest_data = $user_data . $network_data . "local-hostname: $hostname\n"; + my $uuid_str = Digest::SHA::sha1_hex($digest_data); + + my $meta_data = nocloud_metadata($uuid_str, $hostname); + + mkdir "/tmp/cloudinit"; + my $path = "/tmp/cloudinit/$vmid"; + mkdir $path; + rmtree("$path/drive"); + mkdir "$path/drive"; + file_set_contents("$path/drive/user-data", $user_data); + file_set_contents("$path/drive/network-config", $network_data); + file_set_contents("$path/drive/meta-data", $meta_data); + + commit_cloudinit_disk($conf, $drive, $volname, $storeid, "$path/drive", 'cidata'); + +} + +my $cloudinit_methods = { + configdrive2 => \&generate_configdrive2, + nocloud => \&generate_nocloud, +}; + +sub generate_cloudinitconfig { + my ($conf, $vmid) = @_; + + my $format = get_cloudinit_format($conf); + + PVE::QemuServer::foreach_drive($conf, sub { + my ($ds, $drive) = @_; + + my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1); + + return if !$volname || $volname !~ m/vm-$vmid-cloudinit/; + + my $generator = $cloudinit_methods->{$format} + or die "missing cloudinit methods for format '$format'\n"; + + $generator->($conf, $vmid, $drive, $volname, $storeid); + }); +} 1; -- 2.11.0 _______________________________________________ pve-devel mailing list [email protected] https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
