From 0c9a7596f6b686ead232927851200554c997fa44 Mon Sep 17 00:00:00 2001 From: Alexandre Derumier Date: Tue, 16 Jun 2015 14:26:43 +0200 Subject: [PATCH] implement cloudinit Signed-off-by: Alexandre Derumier Co-developed-by: Wolfgang Bumiller --- 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..8fab6b8 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', +cloud-init: 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.39.2