]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
fix #4372: fix vm_resume migration callback
[qemu-server.git] / PVE / QemuServer.pm
index 8bebd0fbb608683596eaf796415d69c073bb59d9..a52a883eb29d3744deb7c68e37844013c1d2761b 100644 (file)
@@ -102,9 +102,11 @@ my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 # 'backup', 'snapshot' or 'rollback'. Most actions are not allowed when such lock is set.
 # But you can ignore this kind of lock with the --skiplock flag.
 
-cfs_register_file('/qemu-server/',
-                 \&parse_vm_config,
-                 \&write_vm_config);
+cfs_register_file(
+    '/qemu-server/',
+    \&parse_vm_config,
+    \&write_vm_config
+);
 
 PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
     description => "Some command save/restore state from this location.",
@@ -921,7 +923,7 @@ my $net_fmt = {
     }),
     queues => {
        type => 'integer',
-       minimum => 0, maximum => 16,
+       minimum => 0, maximum => 64,
        description => 'Number of packet queues to be used on the device.',
        optional => 1,
     },
@@ -2288,6 +2290,15 @@ sub cloudinit_config_properties {
     return dclone($confdesc_cloudinit);
 }
 
+sub cloudinit_pending_properties {
+    my $p = {
+       map { $_ => 1 } keys $confdesc_cloudinit->%*,
+       name => 1,
+    };
+    $p->{"net$_"} = 1 for 0..($MAX_NETS-1);
+    return $p;
+}
+
 sub check_type {
     my ($key, $value) = @_;
 
@@ -2481,6 +2492,11 @@ sub parse_vm_config {
        } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
            my $key = $1;
            my $value = $2;
+           if ($section eq 'cloudinit') {
+               # ignore validation only used for informative purpose
+               $conf->{$key} = $value;
+               next;
+           }
            eval { $value = check_type($key, $value); };
            if ($@) {
                $handle_error->("vm $vmid - unable to parse value of '$key' - $@");
@@ -2562,8 +2578,6 @@ sub write_vm_config {
 
     &$cleanup_config($conf->{pending}, 1);
 
-    &$cleanup_config($conf->{cloudinit});
-
     foreach my $snapname (keys %{$conf->{snapshots}}) {
        die "internal error: snapshot name '$snapname' is forbidden" if lc($snapname) eq 'pending';
        &$cleanup_config($conf->{snapshots}->{$snapname}, undef, $snapname);
@@ -2607,7 +2621,7 @@ sub write_vm_config {
        $raw .= &$generate_raw_config($conf->{pending}, 1);
     }
 
-    if (scalar(keys %{$conf->{cloudinit}})){
+    if (scalar(keys %{$conf->{cloudinit}}) && PVE::QemuConfig->has_cloudinit($conf)){
        $raw .= "\n[special:cloudinit]\n";
        $raw .= &$generate_raw_config($conf->{cloudinit});
     }
@@ -2774,6 +2788,12 @@ sub check_local_storage_availability {
 sub check_running {
     my ($vmid, $nocheck, $node) = @_;
 
+    # $nocheck is set when called during a migration, in which case the config
+    # file might still or already reside on the *other* node
+    # - because rename has already happened, and current node is source
+    # - because rename hasn't happened yet, and current node is target
+    # - because rename has happened, current node is target, but hasn't yet
+    # processed it yet
     PVE::QemuConfig::assert_config_exists_on_node($vmid, $node) if !$nocheck;
     return PVE::QemuServer::Helpers::vm_running_locally($vmid);
 }
@@ -4890,11 +4910,78 @@ sub vmconfig_hotplug_pending {
        $errors->{$opt} = "hotplug problem - $msg";
     };
 
+    my $cloudinit_pending_properties = PVE::QemuServer::cloudinit_pending_properties();
+
+    my $cloudinit_record_changed = sub {
+       my ($conf, $opt, $old, $new) = @_;
+       return if !$cloudinit_pending_properties->{$opt};
+
+       my $ci = ($conf->{cloudinit} //= {});
+
+       my $recorded = $ci->{$opt};
+       my %added = map { $_ => 1 } PVE::Tools::split_list(delete($ci->{added}) // '');
+
+       if (defined($new)) {
+           if (defined($old)) {
+               # an existing value is being modified
+               if (defined($recorded)) {
+                   # the value was already not in sync
+                   if ($new eq $recorded) {
+                       # a value is being reverted to the cloud-init state:
+                       delete $ci->{$opt};
+                       delete $added{$opt};
+                   } else {
+                       # the value was changed multiple times, do nothing
+                   }
+               } elsif ($added{$opt}) {
+                   # the value had been marked as added and is being changed, do nothing
+               } else {
+                   # the value is new, record it:
+                   $ci->{$opt} = $old;
+               }
+           } else {
+               # a new value is being added
+               if (defined($recorded)) {
+                   # it was already not in sync
+                   if ($new eq $recorded) {
+                       # a value is being reverted to the cloud-init state:
+                       delete $ci->{$opt};
+                       delete $added{$opt};
+                   } else {
+                       # the value had temporarily been removed, do nothing
+                   }
+               } elsif ($added{$opt}) {
+                   # the value had been marked as added already, do nothing
+               } else {
+                   # the value is new, add it
+                   $added{$opt} = 1;
+               }
+           }
+       } elsif (!defined($old)) {
+           # a non-existent value is being removed? ignore...
+       } else {
+           # a value is being deleted
+           if (defined($recorded)) {
+               # a value was already recorded, just keep it
+           } elsif ($added{$opt}) {
+               # the value was marked as added, remove it
+               delete $added{$opt};
+           } else {
+               # a previously unrecorded value is being removed, record the old value:
+               $ci->{$opt} = $old;
+           }
+       }
+
+       my $added = join(',', sort keys %added);
+       $ci->{added} = $added if length($added);
+    };
+
     my $changes = 0;
     foreach my $opt (keys %{$conf->{pending}}) { # add/change
        if ($fast_plug_option->{$opt}) {
-           $conf->{$opt} = $conf->{pending}->{$opt};
-           delete $conf->{pending}->{$opt};
+           my $new = delete $conf->{pending}->{$opt};
+           $cloudinit_record_changed->($conf, $opt, $conf->{$opt}, $new);
+           $conf->{$opt} = $new;
            $changes = 1;
        }
     }
@@ -4912,6 +4999,7 @@ sub vmconfig_hotplug_pending {
 
     my $cgroup = PVE::QemuServer::CGroup->new($vmid);
     my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
+
     foreach my $opt (sort keys %$pending_delete_hash) {
        next if $selection && !$selection->{$opt};
        my $force = $pending_delete_hash->{$opt}->{force};
@@ -4965,11 +5053,13 @@ sub vmconfig_hotplug_pending {
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
-           delete $conf->{$opt};
+           my $old = delete $conf->{$opt};
+           $cloudinit_record_changed->($conf, $opt, $old, undef);
            PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
        }
     }
 
+    my $cloudinit_opt;
     foreach my $opt (keys %{$conf->{pending}}) {
        next if $selection && !$selection->{$opt};
        my $value = $conf->{pending}->{$opt};
@@ -5018,7 +5108,9 @@ sub vmconfig_hotplug_pending {
                # some changes can be done without hotplug
                my $drive = parse_drive($opt, $value);
                if (drive_is_cloudinit($drive)) {
-                   $conf->{cloudinit} = PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+                   $cloudinit_opt = [$opt, $drive];
+                   # apply all the other changes first, then generate the cloudinit disk
+                   die "skip\n";
                }
                vmconfig_update_disk($storecfg, $conf, $hotplug_features->{disk},
                                     $vmid, $opt, $value, $arch, $machine_type);
@@ -5037,6 +5129,24 @@ sub vmconfig_hotplug_pending {
                die "skip\n";  # skip non-hot-pluggable options
            }
        };
+       if (my $err = $@) {
+           &$add_error($opt, $err) if $err ne "skip\n";
+       } else {
+           $cloudinit_record_changed->($conf, $opt, $conf->{$opt}, $value);
+           $conf->{$opt} = $value;
+           delete $conf->{pending}->{$opt};
+       }
+    }
+
+    if (defined($cloudinit_opt)) {
+       my ($opt, $drive) = @$cloudinit_opt;
+       my $value = $conf->{pending}->{$opt};
+       eval {
+           my $temp = {%$conf, $opt => $value};
+           PVE::QemuServer::Cloudinit::apply_cloudinit_config($temp, $vmid);
+           vmconfig_update_disk($storecfg, $conf, $hotplug_features->{disk},
+                                $vmid, $opt, $value, $arch, $machine_type);
+       };
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
@@ -5060,13 +5170,8 @@ sub vmconfig_hotplug_pending {
 
     PVE::QemuConfig->write_config($vmid, $conf);
 
-    if ($hotplug_features->{cloudinit}) {
-       my $pending = PVE::QemuServer::Cloudinit::get_pending_config($conf, $vmid);
-       my $regenerate = undef;
-       for my $item (@$pending) {
-           $regenerate = 1 if defined($item->{delete}) or defined($item->{pending});
-       }
-       PVE::QemuServer::vmconfig_update_cloudinit_drive($storecfg, $conf, $vmid) if $regenerate;
+    if ($hotplug_features->{cloudinit} && PVE::QemuServer::Cloudinit::has_changes($conf)) {
+       PVE::QemuServer::vmconfig_update_cloudinit_drive($storecfg, $conf, $vmid);
     }
 }
 
@@ -5112,7 +5217,7 @@ sub vmconfig_delete_or_detach_drive {
 
 
 sub vmconfig_apply_pending {
-    my ($vmid, $conf, $storecfg, $errors) = @_;
+    my ($vmid, $conf, $storecfg, $errors, $skip_cloud_init) = @_;
 
     return if !scalar(keys %{$conf->{pending}});
 
@@ -5145,7 +5250,7 @@ sub vmconfig_apply_pending {
 
     PVE::QemuConfig->cleanup_pending($conf);
 
-    my $generate_cloudnit = undef;
+    my $generate_cloudinit = $skip_cloud_init ? 0 : undef;
 
     foreach my $opt (keys %{$conf->{pending}}) { # add/change
        next if $opt eq 'delete'; # just to be sure
@@ -5160,18 +5265,22 @@ sub vmconfig_apply_pending {
 
            if (is_valid_drivename($opt)) {
                my $drive = parse_drive($opt, $conf->{pending}->{$opt});
-               $generate_cloudnit = 1 if drive_is_cloudinit($drive);
+               $generate_cloudinit //= 1 if drive_is_cloudinit($drive);
            }
 
            $conf->{$opt} = delete $conf->{pending}->{$opt};
        }
     }
 
-    $conf->{cloudinit} = PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid)
-       if $generate_cloudnit;
-
     # write all changes at once to avoid unnecessary i/o
     PVE::QemuConfig->write_config($vmid, $conf);
+    if ($generate_cloudinit) {
+       if (PVE::QemuServer::Cloudinit::apply_cloudinit_config($conf, $vmid)) {
+           # After successful generation and if there were changes to be applied, update the
+           # config to drop the {cloudinit} entry.
+           PVE::QemuConfig->write_config($vmid, $conf);
+       }
+    }
 }
 
 sub vmconfig_update_net {
@@ -5278,7 +5387,8 @@ sub vmconfig_update_disk {
                    safe_string_ne($drive->{iothread}, $old_drive->{iothread}) ||
                    safe_string_ne($drive->{queues}, $old_drive->{queues}) ||
                    safe_string_ne($drive->{cache}, $old_drive->{cache}) ||
-                   safe_string_ne($drive->{ssd}, $old_drive->{ssd})) {
+                   safe_string_ne($drive->{ssd}, $old_drive->{ssd}) ||
+                   safe_string_ne($drive->{ro}, $old_drive->{ro})) {
                    die "skip\n";
                }
 
@@ -5374,8 +5484,10 @@ sub vmconfig_update_cloudinit_drive {
 
     return if !$cloudinit_drive;
 
-    $conf->{cloudinit} = PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
-    # FIXME: write out changed config here? needs to be sure that config is locked though!
+    if (PVE::QemuServer::Cloudinit::apply_cloudinit_config($conf, $vmid)) {
+       PVE::QemuConfig->write_config($vmid, $conf);
+    }
+
     my $running = PVE::QemuServer::check_running($vmid);
 
     if ($running) {
@@ -5557,11 +5669,17 @@ sub vm_start_nolock {
     if (!$statefile && scalar(keys %{$conf->{pending}})) {
        vmconfig_apply_pending($vmid, $conf, $storecfg);
        $conf = PVE::QemuConfig->load_config($vmid); # update/reload
-    } elsif (!$migratedfrom) {
-       # don't regenerate the ISO if the VM is started as part of a live migration
-       # this way we can reuse the old ISO with the correct config
-       $conf->{cloudinit} = PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
-       # FIXME: write out changed config here? if any changes
+    }
+
+    # don't regenerate the ISO if the VM is started as part of a live migration
+    # this way we can reuse the old ISO with the correct config
+    if (!$migratedfrom) {
+       if (PVE::QemuServer::Cloudinit::apply_cloudinit_config($conf, $vmid)) {
+           # FIXME: apply_cloudinit_config updates $conf in this case, and it would only drop
+           # $conf->{cloudinit}, so we could just not do this?
+           # But we do it above, so for now let's be consistent.
+           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+       }
     }
 
     # override offline migrated volumes, conf is out of date still
@@ -5576,7 +5694,10 @@ sub vm_start_nolock {
     my $defaults = load_defaults();
 
     # set environment variable useful inside network script
-    $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
+    # for remote migration the config is available on the target node!
+    if (!$migrate_opts->{remote_node}) {
+       $ENV{PVE_MIGRATED_FROM} = $migratedfrom;
+    }
 
     PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
 
@@ -5623,10 +5744,10 @@ sub vm_start_nolock {
        return $migration_ip;
     };
 
-    my $migrate_uri;
     if ($statefile) {
        if ($statefile eq 'tcp') {
-           my $localip = "localhost";
+           my $migrate = $res->{migrate} = { proto => 'tcp' };
+           $migrate->{addr} = "localhost";
            my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
            my $nodename = nodename();
 
@@ -5639,26 +5760,26 @@ sub vm_start_nolock {
            }
 
            if ($migration_type eq 'insecure') {
-               $localip = $get_migration_ip->($nodename);
-               $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+               $migrate->{addr} = $get_migration_ip->($nodename);
+               $migrate->{addr} = "[$migrate->{addr}]" if Net::IP::ip_is_ipv6($migrate->{addr});
            }
 
            my $pfamily = PVE::Tools::get_host_address_family($nodename);
-           my $migrate_port = PVE::Tools::next_migrate_port($pfamily);
-           $migrate_uri = "tcp:${localip}:${migrate_port}";
-           push @$cmd, '-incoming', $migrate_uri;
+           $migrate->{port} = PVE::Tools::next_migrate_port($pfamily);
+           $migrate->{uri} = "tcp:$migrate->{addr}:$migrate->{port}";
+           push @$cmd, '-incoming', $migrate->{uri};
            push @$cmd, '-S';
 
        } elsif ($statefile eq 'unix') {
            # should be default for secure migrations as a ssh TCP forward
            # tunnel is not deterministic reliable ready and fails regurarly
            # to set up in time, so use UNIX socket forwards
-           my $socket_addr = "/run/qemu-server/$vmid.migrate";
-           unlink $socket_addr;
+           my $migrate = $res->{migrate} = { proto => 'unix' };
+           $migrate->{addr} = "/run/qemu-server/$vmid.migrate";
+           unlink $migrate->{addr};
 
-           $migrate_uri = "unix:$socket_addr";
-
-           push @$cmd, '-incoming', $migrate_uri;
+           $migrate->{uri} = "unix:$migrate->{addr}";
+           push @$cmd, '-incoming', $migrate->{uri};
            push @$cmd, '-S';
 
        } elsif (-e $statefile) {
@@ -5813,10 +5934,9 @@ sub vm_start_nolock {
     eval { PVE::QemuServer::PCI::reserve_pci_usage($pci_id_list, $vmid, undef, $pid) };
     warn $@ if $@;
 
-    print "migration listens on $migrate_uri\n" if $migrate_uri;
-    $res->{migrate_uri} = $migrate_uri;
-
-    if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
+    if (defined($res->{migrate})) {
+       print "migration listens on $res->{migrate}->{uri}\n";
+    } elsif ($statefile) {
        eval { mon_cmd($vmid, "cont"); };
        warn $@ if $@;
     }
@@ -5827,10 +5947,11 @@ sub vm_start_nolock {
 
        my $migrate_storage_uri;
        # nbd_protocol_version > 0 for unix socket support
-       if ($nbd_protocol_version > 0 && $migration_type eq 'secure') {
+       if ($nbd_protocol_version > 0 && ($migration_type eq 'secure' || $migration_type eq 'websocket')) {
            my $socket_path = "/run/qemu-server/$vmid\_nbd.migrate";
            mon_cmd($vmid, "nbd-server-start", addr => { type => 'unix', data => { path => $socket_path } } );
            $migrate_storage_uri = "nbd:unix:$socket_path";
+           $res->{migrate}->{unix_sockets} = [$socket_path];
        } else {
            my $nodename = nodename();
            my $localip = $get_migration_ip->($nodename);
@@ -5848,8 +5969,6 @@ sub vm_start_nolock {
            $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}";
        }
 
-       $res->{migrate_storage_uri} = $migrate_storage_uri;
-
        foreach my $opt (sort keys %$nbd) {
            my $drivestr = $nbd->{$opt}->{drivestr};
            my $volid = $nbd->{$opt}->{volid};
@@ -6246,6 +6365,9 @@ sub vm_suspend {
     }
 }
 
+# $nocheck is set when called as part of a migration - in this context the
+# location of the config file (source or target node) is not deterministic,
+# since migration cannot wait for pmxcfs to process the rename
 sub vm_resume {
     my ($vmid, $skiplock, $nocheck) = @_;
 
@@ -6253,7 +6375,23 @@ sub vm_resume {
        my $res = mon_cmd($vmid, 'query-status');
        my $resume_cmd = 'cont';
        my $reset = 0;
-       my $conf = PVE::QemuConfig->load_config($vmid);
+       my $conf;
+       if ($nocheck) {
+           $conf = eval { PVE::QemuConfig->load_config($vmid) }; # try on target node
+           if ($@) {
+               my $vmlist = PVE::Cluster::get_vmlist();
+               if (exists($vmlist->{ids}->{$vmid})) {
+                   my $node = $vmlist->{ids}->{$vmid}->{node};
+                   $conf = eval { PVE::QemuConfig->load_config($vmid, $node) }; # try on source node
+               }
+               if (!$conf) {
+                   PVE::Cluster::cfs_update(); # vmlist was wrong, invalidate cache
+                   $conf = PVE::QemuConfig->load_config($vmid); # last try on target node again
+               }
+           }
+       } else {
+           $conf = PVE::QemuConfig->load_config($vmid);
+       }
 
        if ($res->{status}) {
            return if $res->{status} eq 'running'; # job done, go home
@@ -6262,7 +6400,6 @@ sub vm_resume {
        }
 
        if (!$nocheck) {
-
            PVE::QemuConfig->check_lock($conf)
                if !($skiplock || PVE::QemuConfig->has_lock($conf, 'backup'));
        }
@@ -8335,9 +8472,10 @@ sub add_nets_bridge_fdb {
            next;
        }
 
+       my $bridge = $net->{bridge};
        if ($have_sdn) {
-           PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $net->{bridge}, $net->{firewall});
-       } else {
+           PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $bridge, $net->{firewall});
+       } elsif (-d "/sys/class/net/$bridge/bridge") { # avoid fdb management with OVS for now
            PVE::Network::add_bridge_fdb($iface, $mac, $net->{firewall});
        }
     }
@@ -8353,9 +8491,10 @@ sub del_nets_bridge_fdb {
        my $net = parse_net($conf->{$opt}) or next;
        my $mac = $net->{macaddr} or next;
 
+       my $bridge = $net->{bridge};
        if ($have_sdn) {
-           PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $net->{bridge}, $net->{firewall});
-       } else {
+           PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $bridge, $net->{firewall});
+       } elsif (-d "/sys/class/net/$bridge/bridge") { # avoid fdb management with OVS for now
            PVE::Network::del_bridge_fdb($iface, $mac, $net->{firewall});
        }
     }