On November 9, 2021 1:46 pm, Fabian Ebner wrote:
> Am 05.11.21 um 14:03 schrieb Fabian Grünbichler:
>> the following two endpoints are used for migration on the remote side
>> 
>> POST /nodes/NODE/qemu/VMID/mtunnel
>> 
>> which creates and locks an empty VM config, and spawns the main qmtunnel
>> worker which binds to a VM-specific UNIX socket.
>> 
>> this worker handles JSON-encoded migration commands coming in via this
>> UNIX socket:
>> - config (set target VM config)
>> -- checks permissions for updating config
>> -- strips pending changes and snapshots
>> -- sets (optional) firewall config
>> - disk (allocate disk for NBD migration)
>> -- checks permission for target storage
>> -- returns drive string for allocated volume
>> - disk-import (import 'pvesm export' stream for offline migration)
>> -- checks permission for target storage
>> -- forks a child running 'pvesm import' reading from a UNIX socket
>> -- only one import allowed to run at any given moment
>> - query-disk-import
>> -- checks output of 'pvesm import' for volume ID message
>> -- returns volid + success, or 'pending', or 'error'
>> - start (returning migration info)
>> - fstrim (via agent)
>> - bwlimit (query bwlimit for storage)
>> - ticket (creates a ticket for a WS connection to a specific socket)
>> - resume
>> - stop
>> - nbdstop
>> - unlock
>> - quit (+ cleanup)
>> 
>> this worker serves as a replacement for both 'qm mtunnel' and various
>> manual calls via SSH. the API call will return a ticket valid for
>> connecting to the worker's UNIX socket via a websocket connection.
>> 
>> GET+WebSocket upgrade /nodes/NODE/qemu/VMID/mtunnelwebsocket
>> 
>> gets called for connecting to a UNIX socket via websocket forwarding,
>> i.e. once for the main command mtunnel, and once each for the memory
>> migration and each NBD drive-mirror/storage migration.
>> 
>> access is guarded by a short-lived ticket binding the authenticated user
>> to the socket path. such tickets can be requested over the main mtunnel,
>> which keeps track of socket paths currently used by that
>> mtunnel/migration instance.
>> 
>> each command handler should check privileges for the requested action if
>> necessary.
>> 
>> Signed-off-by: Fabian Grünbichler <f.gruenbich...@proxmox.com>
>> ---
>> 
>> Notes:
>>      requires
>>      - pve-storage with UNIX import support
>>      - pve-access-control with tunnel ticket support
>>      - pve-http-server with websocket fixes
>> 
>>   PVE/API2/Qemu.pm | 627 +++++++++++++++++++++++++++++++++++++++++++++++
>>   1 file changed, 627 insertions(+)
>> 
>> diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
>> index faf028b..a1a1813 100644
>> --- a/PVE/API2/Qemu.pm
>> +++ b/PVE/API2/Qemu.pm
>> @@ -6,8 +6,13 @@ use Cwd 'abs_path';
>>   use Net::SSLeay;
>>   use POSIX;
>>   use IO::Socket::IP;
>> +use IO::Socket::UNIX;
>> +use IPC::Open3;
>> +use JSON;
>> +use MIME::Base64;
>>   use URI::Escape;
>>   use Crypt::OpenSSL::Random;
>> +use Socket qw(SOCK_STREAM);
>>   
>>   use PVE::Cluster qw (cfs_read_file cfs_write_file);;
>>   use PVE::RRD;
>> @@ -856,6 +861,7 @@ __PACKAGE__->register_method({
>>          { subdir => 'spiceproxy' },
>>          { subdir => 'sendkey' },
>>          { subdir => 'firewall' },
>> +        { subdir => 'mtunnel' },
>>          ];
>>   
>>      return $res;
>> @@ -4428,4 +4434,625 @@ __PACKAGE__->register_method({
>>      return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, 
>> $param->{vmid}, $param->{type});
>>       }});
>>   
>> +__PACKAGE__->register_method({
>> +    name => 'mtunnel',
>> +    path => '{vmid}/mtunnel',
>> +    method => 'POST',
>> +    protected => 1,
>> +    proxyto => 'node',
>> +    description => 'Migration tunnel endpoint - only for internal use by VM 
>> migration.',
>> +    permissions => {
>> +    check => ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
>> +    description => "You need 'VM.Allocate' permissions on /vms/{vmid}. 
>> Further permission checks happen during the actual migration.",
>> +    },
>> +    parameters => {
>> +    additionalProperties => 0,
>> +    properties => {
>> +        node => get_standard_option('pve-node'),
>> +        vmid => get_standard_option('pve-vmid'),
>> +        storages => {
>> +            type => 'string',
>> +            format => 'pve-storage-id-list',
>> +            optional => 1,
>> +            description => 'List of storages to check permission and 
>> availability. Will be checked again for all actually used storages during 
>> migration.',
>> +        },
>> +    },
>> +    },
>> +    returns => {
>> +    additionalProperties => 0,
>> +    properties => {
>> +        upid => { type => 'string' },
>> +        ticket => { type => 'string' },
>> +        socket => { type => 'string' },
>> +    },
>> +    },
>> +    code => sub {
>> +    my ($param) = @_;
>> +
>> +    my $rpcenv = PVE::RPCEnvironment::get();
>> +    my $authuser = $rpcenv->get_user();
>> +
>> +    my $node = extract_param($param, 'node');
>> +    my $vmid = extract_param($param, 'vmid');
>> +
>> +    my $storages = extract_param($param, 'storages');
>> +
>> +    my $storecfg = PVE::Storage::config();
>> +    foreach my $storeid (PVE::Tools::split_list($storages)) {
>> +        $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, 
>> $storeid, $node);
>> +    }
>> +
>> +    PVE::Cluster::check_cfs_quorum();
>> +
>> +    my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
>> +
>> +    my $lock = 'create';
>> +    eval { PVE::QemuConfig->create_and_lock_config($vmid, 0, $lock); };
>> +
>> +    raise_param_exc({ vmid => "unable to create empty VM config - $@"})
>> +        if $@;
>> +
>> +    my $realcmd = sub {
>> +        my $pveproxy_uid;
>> +
>> +        my $state = {
>> +            storecfg => PVE::Storage::config(),
>> +            lock => $lock,
>> +        };
>> +
>> +        my $run_locked = sub {
>> +            my ($code, $params) = @_;
>> +            return PVE::QemuConfig->lock_config($vmid, sub {
>> +                my $conf = PVE::QemuConfig->load_config($vmid);
>> +
>> +                $state->{conf} = $conf;
>> +
>> +                die "Encountered wrong lock - aborting mtunnel command 
>> handling.\n"
>> +                    if $state->{lock} && !PVE::QemuConfig->has_lock($conf, 
>> $state->{lock});
>> +
>> +                return $code->($params);
>> +            });
>> +        };
>> +
>> +        my $cmd_desc = {
>> +            bwlimit => {
>> +                storage => {
>> +                    type => 'string',
>> +                    format => 'pve-storage-id',
>> +                    description => "Storage for which bwlimit is queried",
>> +                },
>> +                bwlimit => {
>> +                    description => "Override I/O bandwidth limit (in 
>> KiB/s).",
>> +                    optional => 1,
>> +                    type => 'integer',
>> +                    minimum => '0',
>> +                },
>> +            },
>> +            config => {
>> +                conf => {
>> +                    type => 'string',
>> +                    description => 'Full VM config, adapted for target 
>> cluster/node',
>> +                },
>> +                'firewall-conf' => {
> 
> Here and thus for parsing, it's 'firewall-conf', but in the command 
> handler 'firewall-config' is accessed.
> 

thanks! the joys of additionalproperties defaulting to 1 ;)

>> +                    type => 'string',
>> +                    description => 'VM firewall config',
>> +                    optional => 1,
>> +                },
>> +            },
>> +            disk => {
>> +                format => 
>> PVE::JSONSchema::get_standard_option('pve-qm-image-format'),
>> +                storage => {
>> +                    type => 'string',
>> +                    format => 'pve-storage-id',
>> +                },
>> +                drive => {
>> +                    type => 'object',
>> +                    description => 'parsed drive information without volid 
>> and format',
>> +                },
>> +            },
>> +            'disk-import' => {
>> +                volname => {
>> +                    type => 'string',
>> +                    description => 'volume name to use prefered target 
>> volume name',
> 
> Nit: I wasn't able to parse this description ;) (also missing r in 
> preferred)
> 

probably because it's missing an 'as':

'volume name to use as preferred target volume name'

as in, we try to keep that name, but if it's already taken you get a 
different one if allow-rename is set, or an error other wise ;)

>> +                },
>> +                format => 
>> PVE::JSONSchema::get_standard_option('pve-qm-image-format'),
>> +                'export-formats' => {
>> +                    type => 'string',
>> +                    description => 'list of supported export formats',
>> +                },
>> +                storage => {
>> +                    type => 'string',
>> +                    format => 'pve-storage-id',
>> +                },
>> +                'with-snapshots' => {
>> +                    description =>
>> +                        "Whether the stream includes intermediate 
>> snapshots",
>> +                    type => 'boolean',
>> +                    optional => 1,
>> +                    default => 0,
>> +                },
>> +                'allow-rename' => {
>> +                    description => "Choose a new volume ID if the requested 
>> " .
>> +                      "volume ID already exists, instead of throwing an 
>> error.",
>> +                    type => 'boolean',
>> +                    optional => 1,
>> +                    default => 0,
>> +                },
>> +            },
>> +            start => {
>> +                start_params => {
>> +                    type => 'object',
>> +                    description => 'params passed to vm_start_nolock',
>> +                },
>> +                migrate_opts => {
>> +                    type => 'object',
>> +                    description => 'migrate_opts passed to vm_start_nolock',
>> +                },
>> +            },
>> +            ticket => {
>> +                path => {
>> +                    type => 'string',
>> +                    description => 'socket path for which the ticket should 
>> be valid. must be known to current mtunnel instance.',
>> +                },
>> +            },
>> +            quit => {
>> +                cleanup => {
>> +                    type => 'boolean',
>> +                    description => 'remove VM config and disks, aborting 
>> migration',
>> +                    default => 0,
>> +                },
>> +            },
>> +        };
>> +
>> +        my $cmd_handlers = {
>> +            'version' => sub {
>> +                # compared against other end's version
>> +                # bump/reset both for breaking changes
>> +                # bump tunnel only for opt-in changes
>> +                return {
>> +                    api => 2,
>> +                    age => 0,
>> +                };
>> +            },
>> +            'config' => sub {
>> +                my ($params) = @_;
>> +
>> +                # parse and write out VM FW config if given
>> +                if (my $fw_conf = $params->{'firewall-config'}) {
>> +                    my ($path, $fh) = 
>> PVE::Tools::tempfile_contents($fw_conf, 700);
>> +
>> +                    my $empty_conf = {
>> +                        rules => [],
>> +                        options => {},
>> +                        aliases => {},
>> +                        ipset => {} ,
>> +                        ipset_comments => {},
>> +                    };
>> +                    my $cluster_fw_conf = 
>> PVE::Firewall::load_clusterfw_conf();
>> +
>> +                    # TODO: add flag for strict parsing?
>> +                    # TODO: add import sub that does all this given raw 
>> content?
>> +                    my $vmfw_conf = 
>> PVE::Firewall::generic_fw_config_parser($path, $cluster_fw_conf, 
>> $empty_conf, 'vm');
>> +                    $vmfw_conf->{vmid} = $vmid;
>> +                    PVE::Firewall::save_vmfw_conf($vmid, $vmfw_conf);
>> +
>> +                    $state->{cleanup}->{fw} = 1;
>> +                }
>> +
>> +                PVE::QemuConfig->remove_lock($vmid, 'create');
>> +
>> +                # TODO add flag for strict parsing?
>> +                my $new_conf = 
>> PVE::QemuServer::parse_vm_config("incoming/qemu-server/$vmid.conf", 
>> $params->{conf});
>> +                delete $new_conf->{lock};
>> +                delete $new_conf->{digest};
>> +
>> +                # TODO handle properly?
>> +                delete $new_conf->{snapshots};
>> +                delete $new_conf->{pending};
> 
> 'parent' should also be deleted if the snapshots are.
> 

yes

>> +
>> +                # not handled by update_vm_api
>> +                my $vmgenid = delete $new_conf->{vmgenid};
>> +                my $meta = delete $new_conf->{meta};
>> +
>> +                $new_conf->{vmid} = $vmid;
>> +                $new_conf->{node} = $node;
>> +
>> +                $update_vm_api->($new_conf, 1);
>> +
>> +                my $conf = PVE::QemuConfig->load_config($vmid);
>> +                $conf->{lock} = 'migrate';
>> +                $conf->{vmgenid} = $vmgenid;
>> +                $conf->{meta} = $meta;
>> +                PVE::QemuConfig->write_config($vmid, $conf);
>> +
>> +                $state->{lock} = 'migrate';
>> +
>> +                return;
>> +            },
>> +            'bwlimit' => sub {
>> +                my ($params) = @_;
>> +
>> +                my $bwlimit = 
>> PVE::Storage::get_bandwidth_limit('migration', [$params->{storage}], 
>> $params->{bwlimit});
>> +                return { bwlimit => $bwlimit };
>> +
>> +            },
>> +            'disk' => sub {
>> +                my ($params) = @_;
> 
> Feels like some deduplication between here and 
> vm_migrate_alloc_nbd_disks should be possible.
> 

yes, I seem to have forgotten to do that (this series predates 
vm_migrate_alloc_nbd_disks, but I now remember thinking back then that 
this is a good addition and I should fold it in)

adapted it a bit and merged the two.

>> +
>> +                my $format = $params->{format};
>> +                my $storeid = $params->{storage};
>> +                my $drive = $params->{drive};
>> +
>> +                $check_storage_access_migrate->($rpcenv, $authuser, 
>> $state->{storecfg}, $storeid, $node);
>> +
>> +                my ($default_format, $valid_formats) = 
>> PVE::Storage::storage_default_format($state->{storecfg}, $storeid);
>> +                my $scfg = PVE::Storage::storage_config($storecfg, 
>> $storeid);
>> +                $format = $default_format
>> +                    if !grep {$format eq $_} @{$valid_formats};
>> +
>> +                my $size = int($drive->{size})/1024;
>> +                my $newvolid = 
>> PVE::Storage::vdisk_alloc($state->{storecfg}, $storeid, $vmid, $format, 
>> undef, $size);
>> +
>> +                my $newdrive = $drive;
>> +                $newdrive->{format} = $format;
>> +                $newdrive->{file} = $newvolid;
>> +
>> +                $state->{cleanup}->{volumes}->{$newvolid} = 1;
>> +                my $drivestr = PVE::QemuServer::print_drive($newdrive);
>> +                return {
>> +                    drivestr => $drivestr,
>> +                    volid => $newvolid,
>> +                };
>> +            },
>> +            'disk-import' => sub {
>> +                my ($params) = @_;
> 
> Similarly here with storage_migrate. Having the checks and deciding on 
> name+format be its own function would also make it possible to abort 
> early, which is especially useful if there are multiple disks. But would 
> require a precondition handler for remote migration of course.
> 

yeah, this part (and some of the counterpart in QemuMigrate) will move 
to the storage layer one way or another for re-using in pve-container 
and the replication code.

>> +
>> +                die "disk import already running as PID 
>> '$state->{disk_import}->{pid}'\n"
>> +                    if $state->{disk_import}->{pid};
>> +
>> +                my $format = $params->{format};
>> +                my $storeid = $params->{storage};
>> +                $check_storage_access_migrate->($rpcenv, $authuser, 
>> $state->{storecfg}, $storeid, $node);
>> +
>> +                my $with_snapshots = $params->{'with-snapshots'} ? 1 : 0;
>> +
>> +                my ($default_format, $valid_formats) = 
>> PVE::Storage::storage_default_format($state->{storecfg}, $storeid);
>> +                my $scfg = PVE::Storage::storage_config($storecfg, 
>> $storeid);
>> +                die "unsupported format '$format' for storage '$storeid'\n"
>> +                    if !grep {$format eq $_} @{$valid_formats};
>> +
>> +                my $volname = $params->{volname};
>> +
>> +                # get target volname, taken from PVE::Storage
>> +                (my $name_without_extension = $volname) =~ s/\.$format$//;
>> +                if ($scfg->{path}) {
>> +                    $volname = "$vmid/$name_without_extension.$format";
>> +                } else {
>> +                    $volname = "$name_without_extension";
>> +                }
> 
> This is just a best-effort for guessing a valid volname that was 
> intended only as a fall-back when target and source storage have 
> different types. If the storage type is the same, the volname should be 
> kept, so that e.g. an external plugin with $scfg->{path} and no 
> extension also works.

but we don't have a guarantee that type foo on cluster A and type foo on 
cluster B are identical, support the same formats, etc. (might be a 
different version with different support, or a different plugin 
altogether). I think this part can improve when we improve our name 
handling in general, but I'd leave it like it is atm..

>> +
>> +                my $migration_snapshot;
>> +                if ($scfg->{type} eq 'zfspool' || $scfg->{type} eq 'btrfs') 
>> {
>> +                    $migration_snapshot = '__migration__';
>> +                }
>> +
>> +                my $volid = "$storeid:$volname";
>> +
>> +                # find common import/export format, taken from PVE::Storage
>> +                my @import_formats = 
>> PVE::Storage::volume_import_formats($state->{storecfg}, $volid, 
>> $migration_snapshot, undef, $with_snapshots);
>> +                my @export_formats = 
>> PVE::Tools::split_list($params->{'export-formats'});
>> +                my %import_hash = map { $_ => 1 } @import_formats;
>> +                my @common = grep { $import_hash{$_} } @export_formats;
>> +                die "no matching import/export format found for storage 
>> '$storeid'\n"
>> +                    if !@common;
>> +                $format = $common[0];
>> +
>> +                my $input = IO::File->new();
>> +                my $info = IO::File->new();
>> +                my $unix = "/run/qemu-server/$vmid.storage";
>> +
>> +                my $import_cmd = ['pvesm', 'import', $volid, $format, 
>> "unix://$unix", '-with-snapshots', $with_snapshots];
>> +                if ($params->{'allow-rename'}) {
>> +                    push @$import_cmd, '-allow-rename', 
>> $params->{'allow-rename'};
>> +                }
>> +                if ($migration_snapshot) {
>> +                    push @$import_cmd, '-delete-snapshot', 
>> $migration_snapshot;
> 
> Missing '-snapshot $migration_snapshot'? While the parameter is ignored 
> by our ZFSPoolPlugin, the BTRFSPlugin aborts if it's not specified 
> AFAICS. And external plugins might require it too.

done

> 
> In general, we'll need to be careful not to introduce mismatches between 
> the import and the export parameters. Might it be better if the client 
> would pass along (most of) the parameters for the import command (which 
> basically is how it's done for the existing storage_migrate)?
> 

see next mail

>> +                }
>> +
>> +                unlink $unix;
>> +                my $cpid = open3($input, $info, $info, @{$import_cmd})
>> +                    or die "failed to spawn disk-import child - $!\n";
>> +
>> +                $state->{disk_import}->{pid} = $cpid;
>> +                my $ready;
>> +                eval {
>> +                    PVE::Tools::run_with_timeout(5, sub { $ready = <$info>; 
>> });
>> +                };
>> +                die "failed to read readyness from disk import child: $@\n" 
>> if $@;
>> +                print "$ready\n";
>> +
>> +                chown $pveproxy_uid, -1, $unix;
>> +
>> +                $state->{disk_import}->{fh} = $info;
>> +                $state->{disk_import}->{socket} = $unix;
>> +
>> +                $state->{sockets}->{$unix} = 1;
>> +
>> +                return {
>> +                    socket => $unix,
>> +                    format => $format,
>> +                };
>> +            },
>> +            'query-disk-import' => sub {
>> +                my ($params) = @_;
>> +
>> +                die "no disk import running\n"
>> +                    if !$state->{disk_import}->{pid};
>> +
>> +                my $pattern = PVE::Storage::volume_imported_message(undef, 
>> 1);
>> +                my $result;
>> +                eval {
>> +                    my $fh = $state->{disk_import}->{fh};
>> +                    PVE::Tools::run_with_timeout(5, sub { $result = <$fh>; 
>> });
>> +                    print "disk-import: $result\n" if $result;
>> +                };
>> +                if ($result && $result =~ $pattern) {
>> +                    my $volid = $1;
>> +                    waitpid($state->{disk_import}->{pid}, 0);
>> +
>> +                    my $unix = $state->{disk_import}->{socket};
>> +                    unlink $unix;
>> +                    delete $state->{sockets}->{$unix};
>> +                    delete $state->{disk_import};
> 
> $volid should be registered for potential cleanup.
> 

done

>> +                    return {
>> +                        status => "complete",
>> +                        volid => $volid,
>> +                    };
>> +                } elsif (!$result && waitpid($state->{disk_import}->{pid}, 
>> WNOHANG)) {
>> +                    my $unix = $state->{disk_import}->{socket};
>> +                    unlink $unix;
>> +                    delete $state->{sockets}->{$unix};
>> +                    delete $state->{disk_import};
>> +
>> +                    return {
>> +                        status => "error",
>> +                    };
>> +                } else {
>> +                    return {
>> +                        status => "pending",
>> +                    };
>> +                }
>> +            },
>> +            'start' => sub {
>> +                my ($params) = @_;
>> +
>> +                my $info = PVE::QemuServer::vm_start_nolock(
>> +                    $state->{storecfg},
>> +                    $vmid,
>> +                    $state->{conf},
>> +                    $params->{start_params},
>> +                    $params->{migrate_opts},
>> +                );
>> +
>> +
>> +                if ($info->{migrate}->{proto} ne 'unix') {
>> +                    PVE::QemuServer::vm_stop(undef, $vmid, 1, 1);
>> +                    die "migration over non-UNIX sockets not possible\n";
>> +                }
>> +
>> +                my $socket = $info->{migrate}->{addr};
>> +                chown $pveproxy_uid, -1, $socket;
>> +                $state->{sockets}->{$socket} = 1;
>> +
>> +                my $unix_sockets = $info->{migrate}->{unix_sockets};
>> +                foreach my $socket (@$unix_sockets) {
>> +                    chown $pveproxy_uid, -1, $socket;
>> +                    $state->{sockets}->{$socket} = 1;
>> +                }
>> +                return $info;
>> +            },
>> +            'fstrim' => sub {
>> +                if (PVE::QemuServer::qga_check_running($vmid)) {
>> +                    eval { mon_cmd($vmid, "guest-fstrim") };
>> +                    warn "fstrim failed: $@\n" if $@;
>> +                }
>> +                return;
>> +            },
>> +            'stop' => sub {
>> +                PVE::QemuServer::vm_stop(undef, $vmid, 1, 1);
>> +                return;
>> +            },
>> +            'nbdstop' => sub {
>> +                PVE::QemuServer::nbd_stop($vmid);
>> +                return;
>> +            },
>> +            'resume' => sub {
>> +                if (PVE::QemuServer::check_running($vmid, 1)) {
>> +                    PVE::QemuServer::vm_resume($vmid, 1, 1);
>> +                } else {
>> +                    die "VM $vmid not running\n";
>> +                }
>> +                return;
>> +            },
>> +            'unlock' => sub {
>> +                PVE::QemuConfig->remove_lock($vmid, $state->{lock});
>> +                delete $state->{lock};
>> +                return;
>> +            },
>> +            'ticket' => sub {
>> +                my ($params) = @_;
>> +
>> +                my $path = $params->{path};
>> +
>> +                die "Not allowed to generate ticket for unknown socket 
>> '$path'\n"
>> +                    if !defined($state->{sockets}->{$path});
>> +
>> +                return { ticket => 
>> PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$path") };
>> +            },
>> +            'quit' => sub {
>> +                my ($params) = @_;
>> +
>> +                if ($params->{cleanup}) {
>> +                    if ($state->{cleanup}->{fw}) {
>> +                        PVE::Firewall::remove_vmfw_conf($vmid);
>> +                    }
>> +
>> +                    if (my @volumes = keys 
>> $state->{cleanup}->{volumes}->$%) {
> 
> keys on scalar? This is fixed in a later patch, but...

yeah, that was a rebase gone wrong ;)

> 
>> +                        PVE::Storage::foreach_volid(@volumes, sub {
> 
> ...PVE::Storage::foreach_volid does not have this signature. It needs 
> what vdisk_list returns. A simple 'for' should be good enough here.
> 

ack, I guess that was the source of a stray volume I had in one of my 
last tests..

>> +                            my ($volid, $sid, $volname, $d) = @_;
>> +
>> +                            print "freeing volume '$volid' as part of 
>> cleanup\n";
>> +                            eval { PVE::Storage::vdisk_free($storecfg, 
>> $volid) };
>> +                            warn $@ if $@;
>> +                        });
>> +                    }
>> +
>> +                    PVE::QemuServer::destroy_vm($state->{storecfg}, $vmid, 
>> 1);
>> +                }
>> +
>> +                $state->{exit} = 1;
>> +                return;
>> +            },
>> +        };
>> +
>> +        $run_locked->(sub {
>> +            my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
>> +            unlink $socket_addr;
>> +
>> +            $state->{socket} = IO::Socket::UNIX->new(
>> +                Type => SOCK_STREAM(),
>> +                Local => $socket_addr,
>> +                Listen => 1,
>> +            );
>> +
>> +            $pveproxy_uid = getpwnam('www-data')
>> +                or die "Failed to resolve user 'www-data' to numeric UID\n";
>> +            chown $pveproxy_uid, -1, $socket_addr;
>> +        });
>> +
>> +        print "mtunnel started\n";
>> +
>> +        my $conn = $state->{socket}->accept();
>> +
>> +        $state->{conn} = $conn;
>> +
>> +        my $reply_err = sub {
>> +            my ($msg) = @_;
>> +
>> +            my $reply = JSON::encode_json({
>> +                success => JSON::false,
>> +                msg => $msg,
>> +            });
>> +            $conn->print("$reply\n");
>> +            $conn->flush();
>> +        };
>> +
>> +        my $reply_ok = sub {
>> +            my ($res) = @_;
>> +
>> +            $res->{success} = JSON::true;
>> +            my $reply = JSON::encode_json($res);
>> +            $conn->print("$reply\n");
>> +            $conn->flush();
>> +        };
>> +
>> +        while (my $line = <$conn>) {
>> +            chomp $line;
>> +
>> +            # untaint, we validate below if needed
>> +            ($line) = $line =~ /^(.*)$/;
>> +            my $parsed = eval { JSON::decode_json($line) };
>> +            if ($@) {
>> +                $reply_err->("failed to parse command - $@");
>> +                next;
>> +            }
>> +
>> +            my $cmd = delete $parsed->{cmd};
>> +            if (!defined($cmd)) {
>> +                $reply_err->("'cmd' missing");
>> +            } elsif (my $handler = $cmd_handlers->{$cmd}) {
>> +                print "received command '$cmd'\n";
>> +                eval {
>> +                    if ($cmd_desc->{$cmd}) {
>> +                        PVE::JSONSchema::validate($cmd_desc->{$cmd}, 
>> $parsed);
>> +                    } else {
>> +                        $parsed = {};
>> +                    }
>> +                    my $res = $run_locked->($handler, $parsed);
>> +                    $reply_ok->($res);
>> +                };
>> +                $reply_err->("failed to handle '$cmd' command - $@")
>> +                    if $@;
>> +            } else {
>> +                $reply_err->("unknown command '$cmd' given");
>> +            }
>> +
>> +            if ($state->{exit}) {
>> +                $state->{conn}->close();
>> +                $state->{socket}->close();
>> +                last;
>> +            }
>> +        }
>> +
>> +        print "mtunnel exited\n";
>> +    };
>> +
>> +    my $ticket = PVE::AccessControl::assemble_tunnel_ticket($authuser, 
>> "/socket/$socket_addr");
>> +    my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
>> +
>> +    return {
>> +        ticket => $ticket,
>> +        upid => $upid,
>> +        socket => $socket_addr,
>> +    };
>> +    }});
>> +
>> +__PACKAGE__->register_method({
>> +    name => 'mtunnelwebsocket',
>> +    path => '{vmid}/mtunnelwebsocket',
>> +    method => 'GET',
>> +    proxyto => 'node',
>> +    permissions => {
>> +    description => "You need to pass a ticket valid for the selected 
>> socket. Tickets can be created via the mtunnel API call, which will check 
>> permissions accordingly.",
>> +        user => 'all', # check inside
>> +    },
>> +    description => 'Migration tunnel endpoint for websocket upgrade - only 
>> for internal use by VM migration.',
>> +    parameters => {
>> +    additionalProperties => 0,
>> +    properties => {
>> +        node => get_standard_option('pve-node'),
>> +        vmid => get_standard_option('pve-vmid'),
>> +        socket => {
>> +            type => "string",
>> +            description => "unix socket to forward to",
>> +        },
>> +        ticket => {
>> +            type => "string",
>> +            description => "ticket return by initial 'mtunnel' API call, or 
>> retrieved via 'ticket' tunnel command",
>> +        },
>> +    },
>> +    },
>> +    returns => {
>> +    type => "object",
>> +    properties => {
>> +        port => { type => 'string', optional => 1 },
>> +        socket => { type => 'string', optional => 1 },
>> +    },
>> +    },
>> +    code => sub {
>> +    my ($param) = @_;
>> +
>> +    my $rpcenv = PVE::RPCEnvironment::get();
>> +    my $authuser = $rpcenv->get_user();
>> +
>> +    my $vmid = $param->{vmid};
>> +    # check VM exists
>> +    PVE::QemuConfig->load_config($vmid);
>> +
>> +    my $socket = $param->{socket};
>> +    PVE::AccessControl::verify_tunnel_ticket($param->{ticket}, $authuser, 
>> "/socket/$socket");
>> +
>> +    return { socket => $socket };
>> +    }});
>> +
>>   1;
>> 
> 


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to