]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
cloud-init: clone/move support
[qemu-server.git] / PVE / QemuServer.pm
index ebdca42ba330c18e9ce6d2974ebb5de17a0e84af..bfab406067e1d3ed77ab6b84861de07b76a18586 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;
@@ -44,6 +45,8 @@ my $qemu_snap_storage = {rbd => 1, sheepdog => 1};
 
 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 
+my $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
+
 # Note about locking: we use flock on the config file protect
 # against concurent actions.
 # Aditionaly, we have a 'lock' setting in the config file. This
@@ -138,6 +141,8 @@ my $cpu_vendor_list = {
     Opteron_G3  => 'AuthenticAMD',
     Opteron_G4  => 'AuthenticAMD',
     Opteron_G5  => 'AuthenticAMD',
+    EPYC => 'AuthenticAMD',
+    'EPYC-IBPB' => 'AuthenticAMD',
 
     # generic types, use vendor from host node
     host => 'default',
@@ -534,6 +539,32 @@ EODESCR
     },
 };
 
+my $confdesc_cloudinit = {
+    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 ?
 #cpu => 'string',
 #machine => 'string',
@@ -691,8 +722,64 @@ 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_cloudinit->{"ipconfig$i"} = $ipconfigdesc;
+}
+
+foreach my $key (keys %$confdesc_cloudinit) {
+    $confdesc->{$key} = $confdesc_cloudinit->{$key};
 }
 
 PVE::JSONSchema::register_format('pve-volume-id-or-qm-path', \&verify_volume_id_or_qm_path);
@@ -1275,7 +1362,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|/|;
@@ -1800,8 +1887,15 @@ sub print_cpu_device {
     return "$cpu-x86_64-cpu,id=cpu$id,socket-id=$current_socket,core-id=$current_core,thread-id=0";
 }
 
-sub drive_is_cdrom {
+sub drive_is_cloudinit {
     my ($drive) = @_;
+    return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
+}
+
+sub drive_is_cdrom {
+    my ($drive, $exclude_cloudinit) = @_;
+
+    return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
 
     return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
 
@@ -1868,6 +1962,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;
 }
 
@@ -1939,7 +2069,10 @@ sub vmconfig_undelete_pending_option {
 sub vmconfig_register_unused_drive {
     my ($storecfg, $vmid, $conf, $drive) = @_;
 
-    if (!drive_is_cdrom($drive)) {
+    if (drive_is_cloudinit($drive)) {
+       eval { PVE::Storage::vdisk_free($storecfg, $drive->{file}) };
+       warn $@ if $@;
+    } elsif (!drive_is_cdrom($drive)) {
        my $volid = $drive->{file};
        if (vm_is_volid_owner($storecfg, $vmid, $volid)) {
            PVE::QemuConfig->add_unused_volume($conf, $volid, $vmid);
@@ -2173,7 +2306,7 @@ sub destroy_vm {
     foreach_drive($conf, sub {
        my ($ds, $drive) = @_;
 
-       return if drive_is_cdrom($drive);
+       return if drive_is_cdrom($drive, 1);
 
        my $volid = $drive->{file};
 
@@ -2278,7 +2411,7 @@ sub parse_vm_config {
            } else {
                warn "vm $vmid - propertry 'delete' is only allowed in [PENDING]\n";
            }
-       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S+)\s*$/) {
+       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
            my $key = $1;
            my $value = $2;
            eval { $value = check_type($key, $value); };
@@ -3642,10 +3775,6 @@ sub vm_deviceunplug {
 
     } elsif ($deviceid =~ m/^(scsi)(\d+)$/) {
 
-       #qemu 2.3 segfault on drive_del with virtioscsi + iothread
-       my $device = parse_drive($deviceid, $conf->{$deviceid});
-       die "virtioscsi with iothread is not hot-unplugglable currently" if $device->{iothread};
-
         qemu_devicedel($vmid, $deviceid);
         qemu_drivedel($vmid, $deviceid);
        qemu_deletescsihw($conf, $vmid, $deviceid);
@@ -4262,6 +4391,22 @@ sub vmconfig_hotplug_pending {
        }
     }
 
+    my $apply_pending_cloudinit;
+    $apply_pending_cloudinit = sub {
+       my ($key, $value) = @_;
+       $apply_pending_cloudinit = sub {}; # once is enough
+
+       my @cloudinit_opts = keys %$confdesc_cloudinit;
+       foreach my $opt (keys %{$conf->{pending}}) {
+           next if !grep { $_ eq $opt } @cloudinit_opts;
+           $conf->{$opt} = delete $conf->{pending}->{$opt};
+       }
+
+       my $new_conf = { %$conf };
+       $new_conf->{$key} = $value;
+       PVE::QemuServer::Cloudinit::generate_cloudinitconfig($new_conf, $vmid);
+    };
+
     foreach my $opt (keys %{$conf->{pending}}) {
        next if $selection && !$selection->{$opt};
        my $value = $conf->{pending}->{$opt};
@@ -4303,6 +4448,10 @@ sub vmconfig_hotplug_pending {
                                    $vmid, $opt, $value);
            } elsif (is_valid_drivename($opt)) {
                # some changes can be done without hotplug
+               my $drive = parse_drive($opt, $value);
+               if (drive_is_cloudinit($drive)) {
+                   &$apply_pending_cloudinit($opt, $value);
+               }
                vmconfig_update_disk($storecfg, $conf, $hotplug_features->{disk},
                                     $vmid, $opt, $value, 1);
            } elsif ($opt =~ m/^memory$/) { #dimms
@@ -4565,6 +4714,9 @@ sub vmconfig_update_disk {
 
                if ($drive->{file} eq 'none') {
                    vm_mon_cmd($vmid, "eject",force => JSON::true,device => "drive-$opt");
+                   if (drive_is_cloudinit($old_drive)) {
+                       vmconfig_register_unused_drive($storecfg, $vmid, $conf, $old_drive);
+                   }
                } else {
                    my $path = get_iso_path($storecfg, $vmid, $drive->{file});
                    vm_mon_cmd($vmid, "eject", force => JSON::true,device => "drive-$opt"); # force eject if locked
@@ -4600,6 +4752,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
@@ -6045,7 +6199,7 @@ sub qemu_img_convert {
 sub qemu_img_format {
     my ($scfg, $volname) = @_;
 
-    if ($scfg->{path} && $volname =~ m/\.(raw|cow|qcow|qcow2|qed|vmdk|cloop)$/) {
+    if ($scfg->{path} && $volname =~ m/\.($QEMU_FORMAT_RE)$/) {
        return $1;
     } else {
        return "raw";
@@ -6241,7 +6395,17 @@ sub clone_disk {
        my ($size) = PVE::Storage::volume_size_info($storecfg, $drive->{file}, 3);
 
        print "create full clone of drive $drivename ($drive->{file})\n";
-       $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newvmid, $dst_format, undef, ($size/1024));
+       my $name = undef;
+       if (drive_is_cloudinit($drive)) {
+           $name = "vm-$newvmid-cloudinit";
+           # cloudinit only supports raw and qcow2 atm:
+           if ($dst_format eq 'qcow2') {
+               $name .= '.qcow2';
+           } elsif ($dst_format ne 'raw') {
+               die "clone: unhandled format for cloudinit image\n";
+           }
+       }
+       $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newvmid, $dst_format, $name, ($size/1024));
        push @$newvollist, $newvolid;
 
        PVE::Storage::activate_volumes($storecfg, [$newvolid]);
@@ -6583,10 +6747,4 @@ sub complete_storage {
     return $res;
 }
 
-sub nbd_stop {
-    my ($vmid) = @_;
-
-    vm_mon_cmd($vmid, 'nbd-server-stop');
-}
-
 1;