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