]> git.proxmox.com Git - qemu-server.git/commitdiff
implement cloudinit
authorAlexandre Derumier <aderumier@odiso.com>
Tue, 16 Jun 2015 12:26:43 +0000 (14:26 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 7 Mar 2018 08:11:31 +0000 (09:11 +0100)
Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
Co-developed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
PVE/API2/Qemu.pm
PVE/QemuServer.pm
PVE/QemuServer/Cloudinit.pm [new file with mode: 0644]
PVE/QemuServer/Makefile
debian/control

index 5051cc9b1276600ce8c4a0984ca0ee48d7500533..8dea72cb23a806c88391a897a348a4691ae733c4 100644 (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();
index 8342f87d68d87b46124ca6d3214bf70cf77ac73b..8fab6b85e68caf651170426250b7f723c565944f 100644 (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;
diff --git a/PVE/QemuServer/Cloudinit.pm b/PVE/QemuServer/Cloudinit.pm
new file mode 100644 (file)
index 0000000..dd0be77
--- /dev/null
@@ -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;
index 49f65f3aae1764067cca04828c0f766b3b5e2bac..1aecc61cf07a3d8c45ad2f6a131cf6fb15cdb6ff 100644 (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
index 0fc29f935bfae3739bbc9798fe19d7ed64af8bef..b8aa4edf4ff54183a7f21d6e6f897747a1e8ea04 100644 (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,