public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Fiona Ebner <f.ebner@proxmox.com>
To: Erik Fastermann <e.fastermann@proxmox.com>, pve-devel@lists.proxmox.com
Subject: Re: [RFC qemu-server 2/4] remote migrate: collect preconditions as structured findings
Date: Wed, 29 Jul 2026 11:56:37 +0200	[thread overview]
Message-ID: <26a8a6f4-0870-4bfc-807e-b75275f6d743@proxmox.com> (raw)
In-Reply-To: <20260721115827.163442-3-e.fastermann@proxmox.com>

Am 21.07.26 um 1:58 PM schrieb Erik Fastermann:
> Extract the remote-migration precondition checks into a
> validate_remote_migrate_preconditions helper that records each problem
> as a {severity, code, message} finding and returns the derived
> migration parameters. The endpoint fails on the first error finding,
> preserving the previous abort behavior.
> 
> It also pulls the checks that previously lived only in the qm CLI into
> the endpoint. Direct API callers such as the web UI now run them too,
> so fewer migrations start only to fail partway. This prepares for a
> precondition endpoint that returns the full findings list instead of
> dying on the first error.

Nit: that part could be it's own patch

> User-visible effect is minimal: successful migrations are unchanged.
> Some precondition error messages are reworded and the "Establishing
> API connection" info line is no longer printed.

Do we gain anything from not printing it? Such changes can also be nicer
in separate patches to ease reviewability and discussion.

> 
> Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
> ---
>  src/PVE/API2/Qemu.pm | 304 ++++++++++++++++++++++++++++++-------------
>  1 file changed, 211 insertions(+), 93 deletions(-)
> 
> diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
> index 68f1800c..9b487457 100644
> --- a/src/PVE/API2/Qemu.pm
> +++ b/src/PVE/API2/Qemu.pm
> @@ -1042,6 +1042,195 @@ sub assert_scsi_feature_compatibility {
>      }
>  }
>  
> +my sub validate_remote_migrate_preconditions {
> +    my ($param, $findings) = @_;
> +
> +    my $add_error = sub {

Style nit: please collect the subroutine arguments as named arguments
like usual. In particular, you can use a hash like %extra_info as the
last one.

> +        push @$findings,
> +            {
> +                severity => 'error',
> +                code => $_[0],

Nit: for the {storage}/import-metadata endpoint we use 'type' rather
than 'code'.

> +                message => ("$_[1]" =~ s/\s+$//r),

Please use chomp() rather than such a regex (or PVE::Tools::trim, but I
don't think there'll be trailing whitespace, else we should rather fix
it where the messsage originates).

> +                @_[2 .. $#_],
> +            };
> +    };
> +
> +    my $add_warning = sub {

Style nit: please collect the subroutine arguments as named arguments
like usual.

> +        push @$findings,
> +            {
> +                severity => 'warning',
> +                code => $_[0],
> +                message => ("$_[1]" =~ s/\s+$//r),

Same as above

> +                @_[2 .. $#_],
> +            };
> +    };
> +
> +    my $source_vmid = extract_param($param, 'vmid');
> +    my $target_endpoint = extract_param($param, 'target-endpoint');
> +    my $remote = PVE::JSONSchema::parse_property_string('proxmox-remote', $target_endpoint);
> +    my $target_vmid = extract_param($param, 'target-vmid') // $source_vmid;
> +
> +    my $target_storage = extract_param($param, 'target-storage');
> +    my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
> +    raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
> +        if $@;
> +
> +    my $target_bridge = extract_param($param, 'target-bridge');
> +    my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 'pve-bridge-id') };
> +    raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
> +        if $@;
> +
> +    my $delete = extract_param($param, 'delete') // 0;
> +
> +    PVE::Cluster::check_cfs_quorum();
> +
> +    # test if VM exists
> +    my $conf = eval { PVE::QemuConfig->load_config($source_vmid) };
> +    if ($@) {

Style nit: please use if (my $err = $@) { to immediately assign the
error to a variable. Same for the other instances.

> +        $add_error->('load-vm-config', $@);
> +        return;
> +    }
> +
> +    if (PVE::QemuConfig->has_lock($conf)) {
> +        $add_error->('vm-locked', "VM is locked ($conf->{lock})");
> +    }
> +
> +    if (PVE::HA::Config::service_is_configured("vm:$source_vmid")) {
> +        $add_error->('vm-ha-configured', 'cannot remote-migrate VM that is configured for HA');
> +    }
> +
> +    my $conn_args = {
> +        protocol => 'https',
> +        host => $remote->{host},
> +        port => $remote->{port} // 8006,
> +        apitoken => $remote->{apitoken},
> +    };
> +
> +    if ($remote->{fingerprint}) {
> +        $conn_args->{cached_fingerprints} = { uc($remote->{fingerprint}) => 1 };
> +    }
> +
> +    my $api_client = PVE::APIClient::LWP->new(%$conn_args);
> +
> +    # check connection once at the start
> +    eval { $api_client->get("/version") };
> +    if ($@) {
> +        $add_error->('remote-conn', $@);
> +        return;
> +    }
> +
> +    my $resources = $api_client->get("/cluster/resources", { type => 'vm' });
> +    if (grep { defined($_->{vmid}) && $_->{vmid} eq $target_vmid } @$resources) {
> +        $add_error->('target-vmid-exists', "remote: guest with ID '$target_vmid' already exists");
> +    }
> +
> +    my $storages = $api_client->get("/nodes/localhost/storage", { enabled => 1 });
> +
> +    my $check_remote_storage = sub {
> +        my ($storage) = @_;
> +        my $found = [grep { $_->{storage} eq $storage } @$storages];
> +
> +        if (!@$found) {
> +            $add_error->(
> +                'storage-missing',
> +                "remote: storage '$storage' does not exist (or missing permission)",
> +                storage => $storage,
> +            );
> +            return;
> +        }
> +
> +        $found = $found->[0];
> +        my $content_types = [PVE::Tools::split_list($found->{content})];
> +        $add_error->(
> +            'storage-no-images',
> +            "remote: storage '$storage' cannot store images",
> +            storage => $storage,
> +        ) if !grep { $_ eq 'images' } @$content_types;
> +    };
> +
> +    foreach my $target_sid (values %{ $storagemap->{entries} }) {

Style nit: please use for instead of foreach, please use post deref ->%*

> +        $check_remote_storage->($target_sid);
> +    }
> +

---snip 8<---

> @@ -5713,104 +5902,26 @@ __PACKAGE__->register_method({
>          my $rpcenv = PVE::RPCEnvironment::get();
>          my $authuser = $rpcenv->get_user();
>  
> -        my $source_vmid = extract_param($param, 'vmid');
> -        my $target_endpoint = extract_param($param, 'target-endpoint');
> -        my $target_vmid = extract_param($param, 'target-vmid') // $source_vmid;
> -
> -        my $delete = extract_param($param, 'delete') // 0;
> -
> -        PVE::Cluster::check_cfs_quorum();
> -
> -        # test if VM exists
> -        my $conf = PVE::QemuConfig->load_config($source_vmid);
> -
> -        PVE::QemuConfig->check_lock($conf);
> -
> -        raise_param_exc({ vmid => "cannot remote-migrate VM that is configured for HA" })
> -            if PVE::HA::Config::service_is_configured("vm:$source_vmid");
> -
> -        my $remote = PVE::JSONSchema::parse_property_string('proxmox-remote', $target_endpoint);
> -
> -        # TODO: move this as helper somewhere appropriate?
> -        my $conn_args = {
> -            protocol => 'https',
> -            host => $remote->{host},
> -            port => $remote->{port} // 8006,
> -            apitoken => $remote->{apitoken},
> -        };
> -
> -        if ($remote->{fingerprint}) {
> -            $conn_args->{cached_fingerprints} = { uc($remote->{fingerprint}) => 1 };
> -        }
> -
> -        print "Establishing API connection with remote at '$remote->{host}'\n";
> -
> -        my $api_client = PVE::APIClient::LWP->new(%$conn_args);
> -
> -        my $repl_conf = PVE::ReplicationConfig->new();
> -        my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
> -        die "cannot remote-migrate replicated VM\n" if $is_replicated;
> +        my $findings = [];
> +        my $plan = validate_remote_migrate_preconditions($param, $findings);

Style nit: why pass in $findings like this? You can just return multiple
values instead. I'd prefer a name like $migration_info rather than $plan.

>  
> -        if (PVE::QemuServer::check_running($source_vmid)) {
> -            die "can't migrate running VM without --online\n" if !$param->{online};
> -
> -        } else {
> -            warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
> -            $param->{online} = 0;
> +        foreach my $finding (@$findings) {

Style nit: please use for instead of foreach

> +            warn "$finding->{message}\n" if $finding->{severity} eq 'warning';
> +            die "$finding->{message}\n" if $finding->{severity} eq 'error';

We can print all warnings and errors and then die with a message saying
how many warnings and errors there are.

>          }
>  
> -        if (defined($conf->{cpu})) {
> -            my $cpu = PVE::JSONSchema::parse_property_string('pve-vm-cpu-conf', $conf->{cpu});
> -            my $cputype = $cpu->{cputype};
> -            if (defined($cputype) && PVE::QemuServer::CPUConfig::is_custom_model($cputype)) {
> -                my $custom_cpu = PVE::QemuServer::CPUConfig::get_custom_model($cputype);
> -
> -                my $remote_custom_cpu = eval {
> -                    $api_client->get("/cluster/qemu/custom-cpu-models/"
> -                        . URI::Escape::uri_escape_utf8($cputype));
> -                };
> -                die "could not validate custom CPU model compatibility: $@\n" if $@;
> -
> -                my $cpu_schema = {
> -                    type => 'object',
> -                    properties => PVE::QemuServer::CPUConfig->options(),
> -                };
> -                eval { PVE::JSONSchema::validate($remote_custom_cpu, $cpu_schema); };
> -                die "could not validate custom CPU model compatibility: $@\n" if $@;
> -
> -                PVE::QemuServer::CPUConfig::assert_custom_model_compatibility(
> -                    $custom_cpu, $remote_custom_cpu,
> -                );
> -            }
> -        }
> -
> -        my $storecfg = PVE::Storage::config();
> -        my $target_storage = extract_param($param, 'target-storage');
> -        my $storagemap =
> -            eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
> -        raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
> -            if $@;
> -
> -        my $target_bridge = extract_param($param, 'target-bridge');
> -        my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 'pve-bridge-id') };
> -        raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
> -            if $@;
> -
> -        die "remote migration requires explicit storage mapping!\n"
> -            if $storagemap->{identity};
> -
> -        $param->{storagemap} = $storagemap;
> -        $param->{bridgemap} = $bridgemap;
> +        $param->{storagemap} = $plan->{storagemap};
> +        $param->{bridgemap} = $plan->{bridgemap};
>          $param->{remote} = {
> -            conn => $conn_args, # re-use fingerprint for tunnel
> -            client => $api_client,
> -            vmid => $target_vmid,
> +            conn => $plan->{conn_args}, # re-use fingerprint for tunnel
> +            client => $plan->{api_client},
> +            vmid => $plan->{target_vmid},
>          };
>          $param->{migration_type} = 'websocket';
>          $param->{'with-local-disks'} = 1;
> -        $param->{delete} = $delete if $delete;
> +        $param->{delete} = $plan->{delete} if $plan->{delete};
>  
> -        my $cluster_status = $api_client->get("/cluster/status");
> +        my $cluster_status = $plan->{api_client}->get("/cluster/status");
>          my $target_node;
>          foreach my $entry (@$cluster_status) {
>              next if $entry->{type} ne 'node';
> @@ -5824,14 +5935,21 @@ __PACKAGE__->register_method({
>              if !defined($target_node);
>  
>          my $realcmd = sub {
> -            PVE::QemuMigrate->migrate($target_node, $remote->{host}, $source_vmid, $param);
> +            PVE::QemuMigrate->migrate(
> +                $target_node,
> +                $plan->{conn_args}->{host},
> +                $plan->{source_vmid},
> +                $param,
> +            );
>          };
>  
>          my $worker = sub {
> -            return PVE::GuestHelpers::guest_migration_lock($source_vmid, 10, $realcmd);
> +            return PVE::GuestHelpers::guest_migration_lock($plan->{source_vmid}, 10, $realcmd);
>          };
>  
> -        return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
> +        return $rpcenv->fork_worker(
> +            'qmigrate', $plan->{source_vmid}, $authuser, $worker,
> +        );
>      },
>  });
>  





  reply	other threads:[~2026-07-29  9:57 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-21 11:58 [RFC qemu-server 0/4] remote migrate: extract preconditions and add check endpoint Erik Fastermann
2026-07-21 11:58 ` [RFC qemu-server 1/4] remote migrate: drop ineffective fingerprint auto-detection Erik Fastermann
2026-07-29  9:56   ` Fiona Ebner
2026-07-29  9:59     ` Fiona Ebner
2026-07-29 10:02       ` Fiona Ebner
2026-07-29 10:07     ` Fabian Grünbichler
2026-07-29 10:17       ` Fiona Ebner
2026-07-21 11:58 ` [RFC qemu-server 2/4] remote migrate: collect preconditions as structured findings Erik Fastermann
2026-07-29  9:56   ` Fiona Ebner [this message]
2026-07-31 13:42     ` Erik Fastermann
2026-07-21 11:58 ` [RFC qemu-server 3/4] qm: remote-migrate: call API endpoint directly Erik Fastermann
2026-07-21 11:58 ` [RFC qemu-server 4/4] remote migrate: add precondition check endpoint Erik Fastermann
2026-07-29  9:56   ` Fiona Ebner
2026-07-31 13:43     ` Erik Fastermann
2026-07-29  9:56 ` [RFC qemu-server 0/4] remote migrate: extract preconditions and add " Fiona Ebner
2026-07-31 13:42   ` Erik Fastermann
2026-07-31 14:44     ` Daniel Kral

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=26a8a6f4-0870-4bfc-807e-b75275f6d743@proxmox.com \
    --to=f.ebner@proxmox.com \
    --cc=e.fastermann@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal