5
0
mirror of git://git.proxmox.com/git/qemu-server.git synced 2025-01-11 05:17:57 +03:00

implement cloudinit

Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
Co-developed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Alexandre Derumier 2015-06-16 14:26:43 +02:00 committed by Wolfgang Bumiller
parent 15cf76988d
commit 0c9a7596f6
5 changed files with 335 additions and 10 deletions

View File

@ -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();

View File

@ -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;

180
PVE/QemuServer/Cloudinit.pm Normal file
View File

@ -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;

View File

@ -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

1
debian/control vendored
View File

@ -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,