use File::Path;
use Digest::SHA;
use URI::Escape;
+use MIME::Base64 qw(encode_base64);
use PVE::Tools qw(run_command file_set_contents);
use PVE::Storage;
use PVE::QemuServer;
+use constant CLOUDINIT_DISK_SIZE => 4 * 1024 * 1024; # 4MiB in bytes
+
sub commit_cloudinit_disk {
my ($conf, $vmid, $drive, $volname, $storeid, $files, $label) = @_;
my $storecfg = PVE::Storage::config();
my $iso_path = PVE::Storage::path($storecfg, $drive->{file});
my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
- my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
- $plugin->activate_volume($storeid, $scfg, $volname);
my $format = PVE::QemuServer::qemu_img_format($scfg, $volname);
- my $size = PVE::Storage::file_size_info($iso_path);
+ my $size = eval { PVE::Storage::volume_size_info($storecfg, $drive->{file}) };
+ if (!defined($size) || $size <= 0) {
+ $volname =~ m/(vm-$vmid-cloudinit(.\Q$format\E)?)/;
+ my $name = $1;
+ $size = 4 * 1024;
+ PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $format, $name, $size);
+ $size *= 1024; # vdisk alloc takes KB, qemu-img dd's osize takes byte
+ }
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+ $plugin->activate_volume($storeid, $scfg, $volname);
+ print "generating cloud-init ISO\n";
eval {
- run_command([['genisoimage', '-R', '-V', $label, $path],
- ['qemu-img', 'dd', '-n', '-f', 'raw', '-O', $format,
- 'isize=0', "osize=$size", "of=$iso_path"]]);
+ run_command([
+ ['genisoimage', '-quiet', '-iso-level', '3', '-R', '-V', $label, $path],
+ ['qemu-img', 'dd', '-n', '-f', 'raw', '-O', $format, 'isize=0', "osize=$size", "of=$iso_path"]
+ ]);
};
my $err = $@;
rmtree($path);
if (defined(my $keys = $conf->{sshkeys})) {
$keys = URI::Escape::uri_unescape($keys);
- $keys = [map { chomp $_; $_ } split(/\n/, $keys)];
+ $keys = [map { my $key = $_; chomp $key; $key } split(/\n/, $keys)];
$keys = [grep { /\S/ } @$keys];
$content .= "ssh_authorized_keys:\n";
foreach my $k (@$keys) {
return $content;
}
+sub split_ip4 {
+ my ($ip) = @_;
+ my ($addr, $mask) = split('/', $ip);
+ die "not a CIDR: $ip\n" if !defined $mask;
+ return ($addr, $PVE::Network::ipv4_reverse_mask->[$mask]);
+}
+
sub configdrive2_network {
my ($conf) = @_;
$content .= " dns_search $searchdomains\n";
}
- my @ifaces = grep(/^net(\d+)$/, keys %$conf);
- foreach my $iface (@ifaces) {
+ 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"});
if ($net->{ip} eq 'dhcp') {
$content .= "iface $id inet dhcp\n";
} else {
- my ($addr, $mask) = split('/', $net->{ip});
+ my ($addr, $mask) = split_ip4($net->{ip});
$content .= "iface $id inet static\n";
$content .= " address $addr\n";
- $content .= " netmask $PVE::Network::ipv4_reverse_mask->[$mask]\n";
+ $content .= " netmask $mask\n";
$content .= " gateway $net->{gw}\n" if $net->{gw};
}
}
return $content;
}
+sub configdrive2_gen_metadata {
+ my ($user, $network) = @_;
+
+ my $uuid_str = Digest::SHA::sha1_hex($user.$network);
+ return configdrive2_metadata($uuid_str);
+}
+
sub configdrive2_metadata {
my ($uuid) = @_;
return <<"EOF";
sub generate_configdrive2 {
my ($conf, $vmid, $drive, $volname, $storeid) = @_;
- my $user_data = cloudinit_userdata($conf, $vmid);
- my $network_data = configdrive2_network($conf);
+ 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);
- my $digest_data = $user_data . $network_data;
- my $uuid_str = Digest::SHA::sha1_hex($digest_data);
+ if (!defined($meta_data)) {
+ $meta_data = configdrive2_gen_metadata($user_data, $network_data);
+ }
- my $meta_data = configdrive2_metadata($uuid_str);
+ my $sum = length($user_data) + length($network_data) + length($meta_data) + length($vendor_data);
+ die "Cloud-Init sum of snippets too big (> 3 MiB)\n" if $sum > (3 * 1024 * 1024);
my $files = {
'/openstack/latest/user_data' => $user_data,
'/openstack/content/0000' => $network_data,
- '/openstack/latest/meta_data.json' => $meta_data
+ '/openstack/latest/meta_data.json' => $meta_data,
+ '/openstack/latest/vendor_data.json' => $vendor_data
};
commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'config-2');
}
+sub generate_opennebula {
+ my ($conf, $vmid, $drive, $volname, $storeid) = @_;
+
+ my $content = "";
+
+ my $username = $conf->{ciuser} || "root";
+ $content .= "USERNAME=$username\n" if defined($username);
+
+ if (defined(my $password = $conf->{cipassword})) {
+ $content .= "CRYPTED_PASSWORD_BASE64=". encode_base64($password) ."\n";
+ }
+
+ if (defined($conf->{sshkeys})) {
+ my $keys = [ split(/\s*\n\s*/, URI::Escape::uri_unescape($conf->{sshkeys})) ];
+ $content .= "SSH_PUBLIC_KEY=\"". join("\n", $keys->@*) ."\"\n";
+ }
+
+ my ($hostname, $fqdn) = get_hostname_fqdn($conf, $vmid);
+ $content .= "SET_HOSTNAME=$hostname\n";
+
+ my ($searchdomains, $nameservers) = get_dns_conf($conf);
+ $content .= 'DNS="' . join(' ', @$nameservers) ."\"\n" if $nameservers && @$nameservers;
+ $content .= 'SEARCH_DOMAIN="'. join(' ', @$searchdomains) ."\"\n" if $searchdomains && @$searchdomains;
+
+ my $networkenabled = undef;
+ my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
+ foreach my $iface (sort @ifaces) {
+ (my $id = $iface) =~ s/^net//;
+ my $net = PVE::QemuServer::parse_net($conf->{$iface});
+ next if !$conf->{"ipconfig$id"};
+ my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+ my $ethid = "ETH$id";
+
+ my $mac = lc $net->{hwaddr};
+
+ if ($ipconfig->{ip}) {
+ $networkenabled = 1;
+
+ if ($ipconfig->{ip} eq 'dhcp') {
+ $content .= "${ethid}_DHCP=YES\n";
+ } else {
+ my ($addr, $mask) = split_ip4($ipconfig->{ip});
+ $content .= "${ethid}_IP=$addr\n";
+ $content .= "${ethid}_MASK=$mask\n";
+ $content .= "${ethid}_MAC=$mac\n";
+ $content .= "${ethid}_GATEWAY=$ipconfig->{gw}\n" if $ipconfig->{gw};
+ }
+ $content .= "${ethid}_MTU=$net->{mtu}\n" if $net->{mtu};
+ }
+
+ if ($ipconfig->{ip6}) {
+ $networkenabled = 1;
+ if ($ipconfig->{ip6} eq 'dhcp') {
+ $content .= "${ethid}_DHCP6=YES\n";
+ } elsif ($ipconfig->{ip6} eq 'auto') {
+ $content .= "${ethid}_AUTO6=YES\n";
+ } else {
+ my ($addr, $mask) = split('/', $ipconfig->{ip6});
+ $content .= "${ethid}_IP6=$addr\n";
+ $content .= "${ethid}_MASK6=$mask\n";
+ $content .= "${ethid}_MAC6=$mac\n";
+ $content .= "${ethid}_GATEWAY6=$ipconfig->{gw6}\n" if $ipconfig->{gw6};
+ }
+ $content .= "${ethid}_MTU=$net->{mtu}\n" if $net->{mtu};
+ }
+ }
+
+ $content .= "NETWORK=YES\n" if $networkenabled;
+
+ my $files = { '/context.sh' => $content };
+ commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'CONTEXT');
+}
+
sub nocloud_network_v2 {
my ($conf) = @_;
my $dns_done;
- my @ifaces = grep(/^net(\d+)$/, keys %$conf);
- foreach my $iface (@ifaces) {
+ my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
+ foreach my $iface (sort @ifaces) {
(my $id = $iface) =~ s/^net//;
next if !$conf->{"ipconfig$id"};
}
if (@addresses) {
$content .= "${i}addresses:\n";
- $content .= "${i}- $_\n" foreach @addresses;
+ $content .= "${i}- '$_'\n" foreach @addresses;
}
if (defined(my $gw = $ipconfig->{gw})) {
- $content .= "${i}gateway4: $gw\n";
+ $content .= "${i}gateway4: '$gw'\n";
}
if (defined(my $gw = $ipconfig->{gw6})) {
- $content .= "${i}gateway6: $gw\n";
+ $content .= "${i}gateway6: '$gw'\n";
}
next if $dns_done;
$content .= "${i}nameservers:\n";
if (defined($nameservers) && @$nameservers) {
$content .= "${i} addresses:\n";
- $content .= "${i} - $_\n" foreach @$nameservers;
+ $content .= "${i} - '$_'\n" foreach @$nameservers;
}
if (defined($searchdomains) && @$searchdomains) {
$content .= "${i} search:\n";
- $content .= "${i} - $_\n" foreach @$searchdomains;
+ $content .= "${i} - '$_'\n" foreach @$searchdomains;
}
}
}
my $content = "version: 1\n"
. "config:\n";
- my @ifaces = grep(/^net(\d+)$/, keys %$conf);
- foreach my $iface (@ifaces) {
+ 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_net($conf->{$iface});
my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
- my $mac = $net->{macaddr}
+ my $mac = lc($net->{macaddr})
or die "network interface '$iface' has no mac address\n";
$content .= "${i}- type: physical\n"
. "${i} name: eth$id\n"
- . "${i} mac_address: $mac\n"
+ . "${i} mac_address: '$mac'\n"
. "${i} subnets:\n";
$i .= ' ';
if (defined(my $ip = $ipconfig->{ip})) {
if ($ip eq 'dhcp') {
$content .= "${i}- type: dhcp4\n";
} else {
+ my ($addr, $mask) = split_ip4($ip);
$content .= "${i}- type: static\n"
- . "${i} address: $ip\n";
+ . "${i} address: '$addr'\n"
+ . "${i} netmask: '$mask'\n";
if (defined(my $gw = $ipconfig->{gw})) {
- $content .= "${i} gateway: $gw\n";
+ $content .= "${i} gateway: '$gw'\n";
}
}
}
if (defined(my $ip = $ipconfig->{ip6})) {
if ($ip eq 'dhcp') {
$content .= "${i}- type: dhcp6\n";
+ } elsif ($ip eq 'auto') {
+ # SLAAC is only supported by cloud-init since 19.4
+ $content .= "${i}- type: ipv6_slaac\n";
} else {
$content .= "${i}- type: static6\n"
- . "${i} address: $ip\n";
+ . "${i} address: '$ip'\n";
if (defined(my $gw = $ipconfig->{gw6})) {
- $content .= "${i} gateway: $gw\n";
+ $content .= "${i} gateway: '$gw'\n";
}
}
}
$content .= "${i}- type: nameserver\n";
if (defined($nameservers) && @$nameservers) {
$content .= "${i} address:\n";
- $content .= "${i} - $_\n" foreach @$nameservers;
+ $content .= "${i} - '$_'\n" foreach @$nameservers;
}
if (defined($searchdomains) && @$searchdomains) {
$content .= "${i} search:\n";
- $content .= "${i} - $_\n" foreach @$searchdomains;
+ $content .= "${i} - '$_'\n" foreach @$searchdomains;
}
}
return "instance-id: $uuid\n";
}
+sub nocloud_gen_metadata {
+ my ($user, $network) = @_;
+
+ my $uuid_str = Digest::SHA::sha1_hex($user.$network);
+ return nocloud_metadata($uuid_str);
+}
+
sub generate_nocloud {
my ($conf, $vmid, $drive, $volname, $storeid) = @_;
- my $user_data = cloudinit_userdata($conf, $vmid);
- my $network_data = nocloud_network($conf);
+ 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 = nocloud_network($conf) if !defined($network_data);
- my $digest_data = $user_data . $network_data;
- my $uuid_str = Digest::SHA::sha1_hex($digest_data);
+ if (!defined($meta_data)) {
+ $meta_data = nocloud_gen_metadata($user_data, $network_data);
+ }
- my $meta_data = nocloud_metadata($uuid_str);
+ my $sum = length($user_data) + length($network_data) + length($meta_data) + length($vendor_data);
+ die "Cloud-Init sum of snippets too big (> 3 MiB)\n" if $sum > (3 * 1024 * 1024);
my $files = {
'/user-data' => $user_data,
'/network-config' => $network_data,
- '/meta-data' => $meta_data
+ '/meta-data' => $meta_data,
+ '/vendor-data' => $vendor_data
};
commit_cloudinit_disk($conf, $vmid, $drive, $volname, $storeid, $files, 'cidata');
}
+sub get_custom_cloudinit_files {
+ my ($conf) = @_;
+
+ my $cicustom = $conf->{cicustom};
+ my $files = $cicustom ? PVE::JSONSchema::parse_property_string('pve-qm-cicustom', $cicustom) : {};
+
+ my $network_volid = $files->{network};
+ my $user_volid = $files->{user};
+ my $meta_volid = $files->{meta};
+ my $vendor_volid = $files->{vendor};
+
+ my $storage_conf = PVE::Storage::config();
+
+ my $network_data;
+ if ($network_volid) {
+ $network_data = read_cloudinit_snippets_file($storage_conf, $network_volid);
+ }
+
+ my $user_data;
+ if ($user_volid) {
+ $user_data = read_cloudinit_snippets_file($storage_conf, $user_volid);
+ }
+
+ my $meta_data;
+ if ($meta_volid) {
+ $meta_data = read_cloudinit_snippets_file($storage_conf, $meta_volid);
+ }
+
+ my $vendor_data;
+ if ($vendor_volid) {
+ $vendor_data = read_cloudinit_snippets_file($storage_conf, $vendor_volid);
+ }
+
+ return ($user_data, $network_data, $meta_data, $vendor_data);
+}
+
+sub read_cloudinit_snippets_file {
+ my ($storage_conf, $volid) = @_;
+
+ my ($full_path, undef, $type) = PVE::Storage::path($storage_conf, $volid);
+ die "$volid is not in the snippets directory\n" if $type ne 'snippets';
+ return PVE::Tools::file_get_contents($full_path, 1 * 1024 * 1024);
+}
+
my $cloudinit_methods = {
configdrive2 => \&generate_configdrive2,
nocloud => \&generate_nocloud,
+ opennebula => \&generate_opennebula,
};
sub generate_cloudinitconfig {
my $format = get_cloudinit_format($conf);
- PVE::QemuServer::foreach_drive($conf, sub {
+ PVE::QemuConfig->foreach_volume($conf, sub {
my ($ds, $drive) = @_;
my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
});
}
+sub dump_cloudinit_config {
+ my ($conf, $vmid, $type) = @_;
+
+ my $format = get_cloudinit_format($conf);
+
+ if ($type eq 'user') {
+ return cloudinit_userdata($conf, $vmid);
+ } elsif ($type eq 'network') {
+ if ($format eq 'nocloud') {
+ return nocloud_network($conf);
+ } else {
+ return configdrive2_network($conf);
+ }
+ } else { # metadata config
+ my $user = cloudinit_userdata($conf, $vmid);
+ if ($format eq 'nocloud') {
+ my $network = nocloud_network($conf);
+ return nocloud_gen_metadata($user, $network);
+ } else {
+ my $network = configdrive2_network($conf);
+ return configdrive2_gen_metadata($user, $network);
+ }
+ }
+}
+
1;