[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