[pve-devel] [PATCH v4 qemu-server 03/10] implement cloudinit

Wolfgang Bumiller w.bumiller at proxmox.com
Thu Mar 1 12:44:10 CET 2018


From: Alexandre Derumier <aderumier at odiso.com>

Signed-off-by: Alexandre Derumier <aderumier at odiso.com>
Co-developed-by: Wolfgang Bumiller <w.bumiller at proxmox.com>
---
 PVE/API2/Qemu.pm            |  39 +++++++++-
 PVE/QemuServer.pm           | 124 ++++++++++++++++++++++++++++--
 PVE/QemuServer/Cloudinit.pm | 180 ++++++++++++++++++++++++++++++++++++++++++++
 PVE/QemuServer/Makefile     |   1 +
 debian/control              |   1 +
 5 files changed, 335 insertions(+), 10 deletions(-)
 create mode 100644 PVE/QemuServer/Cloudinit.pm

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index 5051cc9..8dea72c 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -7,6 +7,7 @@ use Net::SSLeay;
 use UUID;
 use POSIX;
 use IO::Socket::IP;
+use URI::Escape;
 
 use PVE::Cluster qw (cfs_read_file cfs_write_file);;
 use PVE::SafeSyslog;
@@ -64,7 +65,9 @@ my $check_storage_access = sub {
 
 	my $volid = $drive->{file};
 
-	if (!$volid || $volid eq 'none') {
+	if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit')) {
+	    # nothing to check
+	} elsif ($volid =~ m/^(([^:\s]+):)?(cloudinit)$/) {
 	    # nothing to check
 	} elsif ($isCDROM && ($volid eq 'cdrom')) {
 	    $rpcenv->check($authuser, "/", ['Sys.Console']);
@@ -141,6 +144,27 @@ my $create_disks = sub {
 	if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
 	    delete $disk->{size};
 	    $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
+	} elsif ($volid =~ m!^(?:([^/:\s]+):)?cloudinit$!) {
+	    my $storeid = $1 || $default_storage;
+	    die "no storage ID specified (and no default storage)\n" if !$storeid;
+	    my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+	    my $name = "vm-$vmid-cloudinit";
+	    my $fmt = undef;
+	    if ($scfg->{path}) {
+		$name .= ".qcow2";
+		$fmt = 'qcow2';
+	    }else{
+		$fmt = 'raw';
+	    }
+	    # FIXME: Reasonable size? qcow2 shouldn't grow if the space isn't used anyway?
+	    my $cloudinit_iso_size = 5; # in MB
+	    my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, 
+						  $fmt, $name, $cloudinit_iso_size*1024);
+	    $disk->{file} = $volid;
+	    $disk->{media} = 'cdrom';
+	    push @$vollist, $volid;
+	    delete $disk->{format}; # no longer needed
+	    $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
 	} elsif ($volid =~ $NEW_DISK_RE) {
 	    my ($storeid, $size) = ($2 || $default_storage, $3);
 	    die "no storage ID specified (and no default storage)\n" if !$storeid;
@@ -294,7 +318,7 @@ my $check_vm_modify_config_perm = sub {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
 	} elsif ($diskoptions->{$opt}) {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
-	} elsif ($opt =~ m/^net\d+$/) {
+	} elsif ($opt =~ m/^(?:net|ipconfig)\d+$/) {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
 	} else {
 	    # catches usb\d+, hostpci\d+, args, lock, etc.
@@ -436,6 +460,11 @@ __PACKAGE__->register_method({
 
 	my $storecfg = PVE::Storage::config();
 
+	if (defined(my $ssh_keys = $param->{sshkeys})) {
+		$ssh_keys = URI::Escape::uri_unescape($ssh_keys);
+		PVE::Tools::validate_ssh_public_keys($ssh_keys);
+	}
+
 	PVE::Cluster::check_cfs_quorum();
 
 	if (defined($pool)) {
@@ -891,6 +920,7 @@ my $update_vm_api  = sub {
 
     my $background_delay = extract_param($param, 'background_delay');
 
+
     my @paramarr = (); # used for log message
     foreach my $key (sort keys %$param) {
 	push @paramarr, "-$key", $param->{$key};
@@ -906,6 +936,11 @@ my $update_vm_api  = sub {
 
     my $force = extract_param($param, 'force');
 
+    if (defined(my $ssh_keys = $param->{sshkeys})) {
+	    $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
+	    PVE::Tools::validate_ssh_public_keys($ssh_keys);
+    }
+
     die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
 
     my $storecfg = PVE::Storage::config();
diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm
index 8342f87..172dd5f 100644
--- a/PVE/QemuServer.pm
+++ b/PVE/QemuServer.pm
@@ -22,7 +22,7 @@ use PVE::SafeSyslog;
 use Storable qw(dclone);
 use PVE::Exception qw(raise raise_param_exc);
 use PVE::Storage;
-use PVE::Tools qw(run_command lock_file lock_file_full file_read_firstline dir_glob_foreach);
+use PVE::Tools qw(run_command lock_file lock_file_full file_read_firstline dir_glob_foreach $IPV6RE);
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
 use PVE::INotify;
@@ -33,6 +33,7 @@ use PVE::RPCEnvironment;
 use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr);
 use PVE::QemuServer::Memory;
 use PVE::QemuServer::USB qw(parse_usb_device);
+use PVE::QemuServer::Cloudinit;
 use Time::HiRes qw(gettimeofday);
 use File::Copy qw(copy);
 use URI::Escape;
@@ -534,6 +535,29 @@ EODESCR
 	description => "Select BIOS implementation.",
 	default => 'seabios',
     },
+    searchdomain => {
+	optional => 1,
+	type => 'string',
+	description => "cloud-init: Sets DNS search domains for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.",
+    },
+    nameserver => {
+	optional => 1,
+	type => 'string', format => 'address-list',
+	description => "cloud-init: Sets DNS server IP address for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.",
+    },
+    sshkeys => {
+	optional => 1,
+	type => 'string',
+	format => 'urlencoded',
+	description => "cloud-init : Setup public SSH keys (one key per line, " .
+			"OpenSSH format).",
+    },
+    hostname => {
+	optional => 1,
+	description => "cloud-init: Hostname to use instead of the vm-name + search-domain.",
+	type => 'string', format => 'dns-name',
+	maxLength => 255,
+    },
 };
 
 # what about other qemu settings ?
@@ -693,8 +717,60 @@ my $netdesc = {
 
 PVE::JSONSchema::register_standard_option("pve-qm-net", $netdesc);
 
+my $ipconfig_fmt = {
+    ip => {
+	type => 'string',
+	format => 'pve-ipv4-config',
+	format_description => 'IPv4Format/CIDR',
+	description => 'IPv4 address in CIDR format.',
+	optional => 1,
+	default => 'dhcp',
+    },
+    gw => {
+	type => 'string',
+	format => 'ipv4',
+	format_description => 'GatewayIPv4',
+	description => 'Default gateway for IPv4 traffic.',
+	optional => 1,
+	requires => 'ip',
+    },
+    ip6 => {
+	type => 'string',
+	format => 'pve-ipv6-config',
+	format_description => 'IPv6Format/CIDR',
+	description => 'IPv6 address in CIDR format.',
+	optional => 1,
+	default => 'dhcp',
+    },
+    gw6 => {
+	type => 'string',
+	format => 'ipv6',
+	format_description => 'GatewayIPv6',
+	description => 'Default gateway for IPv6 traffic.',
+	optional => 1,
+	requires => 'ip6',
+    },
+};
+PVE::JSONSchema::register_format('pve-qm-ipconfig', $ipconfig_fmt);
+my $ipconfigdesc = {
+    optional => 1,
+    type => 'string', format => 'pve-qm-ipconfig',
+    description => <<'EODESCR',
+Specify IP addresses and gateways for the corresponding interface.
+
+IP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.
+
+The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit gateway should be provided.
+For IPv6 the special string 'auto' can be used to use stateless autoconfiguration.
+
+If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using dhcp on IPv4.
+EODESCR
+};
+PVE::JSONSchema::register_standard_option("pve-qm-ipconfig", $netdesc);
+
 for (my $i = 0; $i < $MAX_NETS; $i++)  {
     $confdesc->{"net$i"} = $netdesc;
+    $confdesc->{"ipconfig$i"} = $ipconfigdesc;
 }
 
 PVE::JSONSchema::register_format('pve-volume-id-or-qm-path', \&verify_volume_id_or_qm_path);
@@ -1277,7 +1353,7 @@ sub get_iso_path {
 sub filename_to_volume_id {
     my ($vmid, $file, $media) = @_;
 
-    if (!($file eq 'none' || $file eq 'cdrom' ||
+     if (!($file eq 'none' || $file eq 'cdrom' ||
 	  $file =~ m|^/dev/.+| || $file =~ m/^([^:]+):(.+)$/)) {
 
 	return undef if $file =~ m|/|;
@@ -1870,6 +1946,42 @@ sub parse_net {
 	my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
 	$res->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
     }
+    $res->{macaddr} = PVE::Tools::random_ether_addr() if !defined($res->{macaddr});
+    return $res;
+}
+
+# ipconfigX ip=cidr,gw=ip,ip6=cidr,gw6=ip
+sub parse_ipconfig {
+    my ($data) = @_;
+
+    my $res = eval { PVE::JSONSchema::parse_property_string($ipconfig_fmt, $data) };
+    if ($@) {
+	warn $@;
+	return undef;
+    }
+
+    if ($res->{gw} && !$res->{ip}) {
+	warn 'gateway specified without specifying an IP address';
+	return undef;
+    }
+    if ($res->{gw6} && !$res->{ip6}) {
+	warn 'IPv6 gateway specified without specifying an IPv6 address';
+	return undef;
+    }
+    if ($res->{gw} && $res->{ip} eq 'dhcp') {
+	warn 'gateway specified together with DHCP';
+	return undef;
+    }
+    if ($res->{gw6} && $res->{ip6} !~ /^$IPV6RE/) {
+	# gw6 + auto/dhcp
+	warn "IPv6 gateway specified together with $res->{ip6} address";
+	return undef;
+    }
+
+    if (!$res->{ip} && !$res->{ip6}) {
+	return { ip => 'dhcp', ip6 => 'dhcp' };
+    }
+
     return $res;
 }
 
@@ -4598,6 +4710,8 @@ sub vm_start {
 	    $conf = PVE::QemuConfig->load_config($vmid); # update/reload
 	}
 
+	PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+
 	my $defaults = load_defaults();
 
 	# set environment variable useful inside network script
@@ -6581,10 +6695,4 @@ sub complete_storage {
     return $res;
 }
 
-sub nbd_stop {
-    my ($vmid) = @_;
-
-    vm_mon_cmd($vmid, 'nbd-server-stop');
-}
-
 1;
diff --git a/PVE/QemuServer/Cloudinit.pm b/PVE/QemuServer/Cloudinit.pm
new file mode 100644
index 0000000..dd0be77
--- /dev/null
+++ b/PVE/QemuServer/Cloudinit.pm
@@ -0,0 +1,180 @@
+package PVE::QemuServer::Cloudinit;
+
+use strict;
+use warnings;
+
+use File::Path;
+use Digest::SHA;
+use URI::Escape;
+
+use PVE::Tools qw(run_command file_set_contents);
+use PVE::Storage;
+use PVE::QemuServer;
+
+sub nbd_stop {
+    my ($vmid) = @_;
+
+    PVE::QemuServer::vm_mon_cmd($vmid, 'nbd-server-stop');
+}
+
+sub next_free_nbd_dev {
+    for(my $i = 0;;$i++) {
+	my $dev = "/dev/nbd$i";
+	last if ! -b $dev;
+	next if -f "/sys/block/nbd$i/pid"; # busy
+	return $dev;
+    }
+    die "unable to find free nbd device\n";
+}
+
+sub commit_cloudinit_disk {
+    my ($file_path, $iso_path, $format) = @_;
+
+    my $nbd_dev = next_free_nbd_dev();
+    run_command(['qemu-nbd', '-c', $nbd_dev, $iso_path, '-f', $format]);
+
+    eval {
+	run_command([['genisoimage', '-R', '-V', 'config-2', $file_path],
+		     ['dd', "of=$nbd_dev", 'conv=fsync']]);
+    };
+    my $err = $@;
+    eval { run_command(['qemu-nbd', '-d', $nbd_dev]); };
+    warn $@ if $@;
+    die $err if $err;
+}
+
+sub generate_cloudinitconfig {
+    my ($conf, $vmid) = @_;
+
+    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 $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 generate_cloudinit_userdata {
+    my ($conf, $path) = @_;
+
+    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 .= "manage_etc_hosts: true\n";
+    $content .= "bootcmd: \n";
+    $content .= "  - ifdown -a\n";
+    $content .= "  - ifup -a\n";
+
+    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";
+
+    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) = @_;
+
+    my $content = "auto lo\n";
+    $content .="iface lo inet loopback\n\n";
+
+    my @ifaces = grep(/^net(\d+)$/, keys %$conf);
+    foreach my $iface (@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('/', $net->{ip});
+		$content .= "iface $id inet static\n";
+		$content .= "        address $addr\n";
+		$content .= "        netmask $PVE::Network::ipv4_reverse_mask->[$mask]\n";
+		$content .= "        gateway $net->{gw}\n" if $net->{gw};
+	    }
+	}
+	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 $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;
+}
+
+
+1;
diff --git a/PVE/QemuServer/Makefile b/PVE/QemuServer/Makefile
index 49f65f3..1aecc61 100644
--- a/PVE/QemuServer/Makefile
+++ b/PVE/QemuServer/Makefile
@@ -5,3 +5,4 @@ install:
 	install -D -m 0644 Memory.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/Memory.pm
 	install -D -m 0644 ImportDisk.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/ImportDisk.pm
 	install -D -m 0644 OVF.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/OVF.pm
+	install -D -m 0644 Cloudinit.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/Cloudinit.pm
diff --git a/debian/control b/debian/control
index 0fc29f9..b8aa4ed 100644
--- a/debian/control
+++ b/debian/control
@@ -13,6 +13,7 @@ Homepage: http://www.proxmox.com
 Package: qemu-server
 Architecture: any
 Depends: dbus,
+         genisoimage,
          libc6 (>= 2.7-18),
          libio-multiplex-perl,
          libjson-perl,
-- 
2.11.0





More information about the pve-devel mailing list