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:
parent
15cf76988d
commit
0c9a7596f6
@ -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();
|
||||
|
@ -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
180
PVE/QemuServer/Cloudinit.pm
Normal 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;
|
@ -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
1
debian/control
vendored
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user