qemu-server/PVE/CLI/qm.pm
Fabian Grünbichler 192bbfda82 qm: add remote-migrate command
which wraps the remote_migrate_vm API endpoint, but does the
precondition checks that can be done up front itself.

this now just leaves the FP retrieval and target node name lookup to the
sync part of the API endpoint, which should be do-able in <30s ..

an example invocation:

$ qm remote-migrate 1234 4321 'host=123.123.123.123,apitoken=PVEAPIToken=user@pve!incoming=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee,fingerprint=aa:bb:cc:dd:ee:ff:aa:bb:cc:dd:ee:ff:aa:bb:cc:dd:ee:ff:aa:bb:cc:dd:ee:ff:aa:bb:cc:dd:ee:ff:aa:bb' --target-bridge vmbr0 --target-storage zfs-a:rbd-b,nfs-c:dir-d,zfs-e --online

will migrate the local VM 1234 to the host 123.123.1232.123 using the
given API token, mapping the VMID to 4321 on the target cluster, all its
virtual NICs to the target vm bridge 'vmbr0', any volumes on storage
zfs-a to storage rbd-b, any on storage nfs-c to storage dir-d, and any
other volumes to storage zfs-e. the source VM will be stopped but remain
on the source node/cluster after the migration has finished.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2022-11-17 15:21:39 +01:00

1107 lines
32 KiB
Perl
Executable File

package PVE::CLI::qm;
use strict;
use warnings;
# Note: disable '+' prefix for Getopt::Long (for resize command)
use Getopt::Long qw(:config no_getopt_compat);
use Fcntl ':flock';
use File::Path;
use IO::Select;
use IO::Socket::UNIX;
use JSON;
use POSIX qw(strftime);
use Term::ReadLine;
use URI::Escape;
use PVE::APIClient::LWP;
use PVE::Cluster;
use PVE::Exception qw(raise_param_exc);
use PVE::GuestHelpers;
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Network;
use PVE::RPCEnvironment;
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::API2::Qemu::Agent;
use PVE::API2::Qemu;
use PVE::QemuConfig;
use PVE::QemuServer::Drive;
use PVE::QemuServer::Helpers;
use PVE::QemuServer::Agent qw(agent_available);
use PVE::QemuServer::ImportDisk;
use PVE::QemuServer::Monitor qw(mon_cmd);
use PVE::QemuServer::OVF;
use PVE::QemuServer;
use PVE::CLIHandler;
use base qw(PVE::CLIHandler);
my $upid_exit = sub {
my $upid = shift;
my $status = PVE::Tools::upid_read_status($upid);
exit(PVE::Tools::upid_status_is_error($status) ? -1 : 0);
};
my $nodename = PVE::INotify::nodename();
my %node = (node => $nodename);
sub setup_environment {
PVE::RPCEnvironment->setup_default_cli_env();
}
sub run_vnc_proxy {
my ($path) = @_;
my $c;
while ( ++$c < 10 && !-e $path ) { sleep(1); }
my $s = IO::Socket::UNIX->new(Peer => $path, Timeout => 120);
die "unable to connect to socket '$path' - $!" if !$s;
my $select = IO::Select->new();
$select->add(\*STDIN);
$select->add($s);
my $timeout = 60*15; # 15 minutes
my @handles;
while ($select->count &&
scalar(@handles = $select->can_read ($timeout))) {
foreach my $h (@handles) {
my $buf;
my $n = $h->sysread($buf, 4096);
if ($h == \*STDIN) {
if ($n) {
syswrite($s, $buf);
} else {
exit(0);
}
} elsif ($h == $s) {
if ($n) {
syswrite(\*STDOUT, $buf);
} else {
exit(0);
}
}
}
}
exit(0);
}
sub print_recursive_hash {
my ($prefix, $hash, $key) = @_;
if (ref($hash) eq 'HASH') {
if (defined($key)) {
print "$prefix$key:\n";
}
for my $itemkey (sort keys %$hash) {
print_recursive_hash("\t$prefix", $hash->{$itemkey}, $itemkey);
}
} elsif (ref($hash) eq 'ARRAY') {
if (defined($key)) {
print "$prefix$key:\n";
}
for my $item (@$hash) {
print_recursive_hash("\t$prefix", $item);
}
} elsif ((!ref($hash) && defined($hash)) || ref($hash) eq 'JSON::PP::Boolean') {
if (defined($key)) {
print "$prefix$key: $hash\n";
} else {
print "$prefix$hash\n";
}
}
}
__PACKAGE__->register_method ({
name => 'showcmd',
path => 'showcmd',
method => 'GET',
description => "Show command line which is used to start the VM (debug info).",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
pretty => {
description => "Puts each option on a new line to enhance human readability",
type => 'boolean',
optional => 1,
default => 0,
},
snapshot => get_standard_option('pve-snapshot-name', {
description => "Fetch config values from given snapshot.",
optional => 1,
completion => sub {
my ($cmd, $pname, $cur, $args) = @_;
PVE::QemuConfig->snapshot_list($args->[0]);
}
}),
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $storecfg = PVE::Storage::config();
my $cmdline = PVE::QemuServer::vm_commandline($storecfg, $param->{vmid}, $param->{snapshot});
$cmdline =~ s/ -/ \\\n -/g if $param->{pretty};
print "$cmdline\n";
return;
}});
__PACKAGE__->register_method({
name => 'remote_migrate_vm',
path => 'remote_migrate_vm',
method => 'POST',
description => "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
'target-vmid' => get_standard_option('pve-vmid', { optional => 1 }),
'target-endpoint' => get_standard_option('proxmox-remote', {
description => "Remote target endpoint",
}),
online => {
type => 'boolean',
description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
optional => 1,
},
delete => {
type => 'boolean',
description => "Delete the original VM and related data after successful migration. By default the original VM is kept on the source cluster in a stopped state.",
optional => 1,
default => 0,
},
'target-storage' => get_standard_option('pve-targetstorage', {
completion => \&PVE::QemuServer::complete_migration_storage,
optional => 0,
}),
'target-bridge' => {
type => 'string',
description => "Mapping from source to target bridges. Providing only a single bridge ID maps all source bridges to that bridge. Providing the special value '1' will map each source bridge to itself.",
format => 'bridge-pair-list',
},
bwlimit => {
description => "Override I/O bandwidth limit (in KiB/s).",
optional => 1,
type => 'integer',
minimum => '0',
default => 'migrate limit from datacenter or storage config',
},
},
},
returns => {
type => 'string',
description => "the task ID.",
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $source_vmid = $param->{vmid};
my $target_endpoint = $param->{'target-endpoint'};
my $target_vmid = $param->{'target-vmid'} // $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},
};
$conn_args->{cached_fingerprints} = { uc($remote->{fingerprint}) => 1 }
if defined($remote->{fingerprint});
my $api_client = PVE::APIClient::LWP->new(%$conn_args);
my $resources = $api_client->get("/cluster/resources", { type => 'vm' });
if (grep { defined($_->{vmid}) && $_->{vmid} eq $target_vmid } @$resources) {
raise_param_exc({ target_vmid => "Guest with ID '$target_vmid' already exists on remote cluster" });
}
my $storages = $api_client->get("/nodes/localhost/storage", { enabled => 1 });
my $storecfg = PVE::Storage::config();
my $target_storage = $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 $check_remote_storage = sub {
my ($storage) = @_;
my $found = [ grep { $_->{storage} eq $storage } @$storages ];
die "remote: storage '$storage' does not exist!\n"
if !@$found;
$found = @$found[0];
my $content_types = [ PVE::Tools::split_list($found->{content}) ];
die "remote: storage '$storage' cannot store images\n"
if !grep { $_ eq 'images' } @$content_types;
};
foreach my $target_sid (values %{$storagemap->{entries}}) {
$check_remote_storage->($target_sid);
}
$check_remote_storage->($storagemap->{default})
if $storagemap->{default};
return PVE::API2::Qemu->remote_migrate_vm($param);
}});
__PACKAGE__->register_method ({
name => 'status',
path => 'status',
method => 'GET',
description => "Show VM status.",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
verbose => {
description => "Verbose output format",
type => 'boolean',
optional => 1,
}
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
# test if VM exists
my $conf = PVE::QemuConfig->load_config ($param->{vmid});
my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
my $stat = $vmstatus->{$param->{vmid}};
if ($param->{verbose}) {
foreach my $k (sort (keys %$stat)) {
next if $k eq 'cpu' || $k eq 'relcpu'; # always 0
my $v = $stat->{$k};
print_recursive_hash("", $v, $k);
}
} else {
my $status = $stat->{qmpstatus} || 'unknown';
print "status: $status\n";
}
return;
}});
__PACKAGE__->register_method ({
name => 'vncproxy',
path => 'vncproxy',
method => 'PUT',
description => "Proxy VM VNC traffic to stdin/stdout",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_running }),
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
PVE::QemuConfig::assert_config_exists_on_node($vmid);
my $vnc_socket = PVE::QemuServer::Helpers::vnc_socket($vmid);
if (my $ticket = $ENV{LC_PVE_TICKET}) { # NOTE: ssh on debian only pass LC_* variables
mon_cmd($vmid, "set_password", protocol => 'vnc', password => $ticket);
mon_cmd($vmid, "expire_password", protocol => 'vnc', time => "+30");
} else {
die "LC_PVE_TICKET not set, VNC proxy without password is forbidden\n";
}
run_vnc_proxy($vnc_socket);
return;
}});
__PACKAGE__->register_method ({
name => 'unlock',
path => 'unlock',
method => 'PUT',
description => "Unlock the VM.",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
PVE::QemuConfig->lock_config ($vmid, sub {
my $conf = PVE::QemuConfig->load_config($vmid);
delete $conf->{lock};
delete $conf->{pending}->{lock} if $conf->{pending}; # just to be sure
PVE::QemuConfig->write_config($vmid, $conf);
});
return;
}});
__PACKAGE__->register_method ({
name => 'nbdstop',
path => 'nbdstop',
method => 'PUT',
description => "Stop embedded nbd server.",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
eval { PVE::QemuServer::nbd_stop($vmid) };
warn $@ if $@;
return;
}});
__PACKAGE__->register_method ({
name => 'mtunnel',
path => 'mtunnel',
method => 'POST',
description => "Used by qmigrate - do not use manually.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
if (!PVE::Cluster::check_cfs_quorum(1)) {
print "no quorum\n";
return;
}
my $tunnel_write = sub {
my $text = shift;
chomp $text;
print "$text\n";
*STDOUT->flush();
};
$tunnel_write->("tunnel online");
$tunnel_write->("ver 1");
while (my $line = <STDIN>) {
chomp $line;
if ($line =~ /^quit$/) {
$tunnel_write->("OK");
last;
} elsif ($line =~ /^resume (\d+)$/) {
my $vmid = $1;
if (PVE::QemuServer::check_running($vmid, 1)) {
eval { PVE::QemuServer::vm_resume($vmid, 1, 1); };
if ($@) {
$tunnel_write->("ERR: resume failed - $@");
} else {
$tunnel_write->("OK");
}
} else {
$tunnel_write->("ERR: resume failed - VM $vmid not running");
}
}
}
return;
}});
__PACKAGE__->register_method ({
name => 'wait',
path => 'wait',
method => 'GET',
description => "Wait until the VM is stopped.",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_running }),
timeout => {
description => "Timeout in seconds. Default is to wait forever.",
type => 'integer',
minimum => 1,
optional => 1,
}
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
my $timeout = $param->{timeout};
my $pid = PVE::QemuServer::check_running ($vmid);
return if !$pid;
print "waiting until VM $vmid stopps (PID $pid)\n";
my $count = 0;
while ((!$timeout || ($count < $timeout)) && PVE::QemuServer::check_running ($vmid)) {
$count++;
sleep 1;
}
die "wait failed - got timeout\n" if PVE::QemuServer::check_running ($vmid);
return;
}});
__PACKAGE__->register_method ({
name => 'monitor',
path => 'monitor',
method => 'POST',
description => "Enter Qemu Monitor interface.",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_running }),
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
print "Entering Qemu Monitor for VM $vmid - type 'help' for help\n";
my $term = Term::ReadLine->new('qm');
while (defined(my $input = $term->readline('qm> '))) {
chomp $input;
next if $input =~ m/^\s*$/;
last if $input =~ m/^\s*q(uit)?\s*$/;
eval { print PVE::QemuServer::Monitor::hmp_cmd($vmid, $input) };
print "ERROR: $@" if $@;
}
return;
}});
__PACKAGE__->register_method ({
name => 'rescan',
path => 'rescan',
method => 'POST',
description => "Rescan all storages and update disk sizes and unused disk images.",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', {
optional => 1,
completion => \&PVE::QemuServer::complete_vmid,
}),
dryrun => {
type => 'boolean',
optional => 1,
default => 0,
description => 'Do not actually write changes out to VM config(s).',
},
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $dryrun = $param->{dryrun};
print "NOTE: running in dry-run mode, won't write changes out!\n" if $dryrun;
PVE::QemuServer::rescan($param->{vmid}, 0, $dryrun);
return;
}});
__PACKAGE__->register_method ({
name => 'importdisk',
path => 'importdisk',
method => 'POST',
description => "Import an external disk image as an unused disk in a VM. The
image format has to be supported by qemu-img(1).",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', {completion => \&PVE::QemuServer::complete_vmid}),
source => {
description => 'Path to the disk image to import',
type => 'string',
optional => 0,
},
storage => get_standard_option('pve-storage-id', {
description => 'Target storage ID',
completion => \&PVE::QemuServer::complete_storage,
optional => 0,
}),
format => {
type => 'string',
description => 'Target format',
enum => [ 'raw', 'qcow2', 'vmdk' ],
optional => 1,
},
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = extract_param($param, 'vmid');
my $source = extract_param($param, 'source');
my $storeid = extract_param($param, 'storage');
my $format = extract_param($param, 'format');
my $vm_conf = PVE::QemuConfig->load_config($vmid);
PVE::QemuConfig->check_lock($vm_conf);
die "$source: non-existent or non-regular file\n" if (! -f $source);
my $storecfg = PVE::Storage::config();
PVE::Storage::storage_check_enabled($storecfg, $storeid);
my $target_storage_config = PVE::Storage::storage_config($storecfg, $storeid);
die "storage $storeid does not support vm images\n"
if !$target_storage_config->{content}->{images};
print "importing disk '$source' to VM $vmid ...\n";
my ($drive_id, $volid) = PVE::QemuServer::ImportDisk::do_import($source, $vmid, $storeid, { format => $format });
print "Successfully imported disk as '$drive_id:$volid'\n";
return;
}});
__PACKAGE__->register_method ({
name => 'terminal',
path => 'terminal',
method => 'POST',
description => "Open a terminal using a serial device (The VM need to have a serial device configured, for example 'serial0: socket')",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_running }),
iface => {
description => "Select the serial device. By default we simply use the first suitable device.",
type => 'string',
optional => 1,
enum => [qw(serial0 serial1 serial2 serial3)],
},
escape => {
description => "Escape character.",
type => 'string',
optional => 1,
default => '^O',
},
},
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
my $escape = $param->{escape} // '^O';
if ($escape =~ /^\^([\x40-\x7a])$/) {
$escape = ord($1) & 0x1F;
} elsif ($escape =~ /^0x[0-9a-f]+$/i) {
$escape = hex($escape);
} elsif ($escape =~ /^[0-9]+$/) {
$escape = int($escape);
} else {
die "invalid escape character definition: $escape\n";
}
my $escapemsg = '';
if ($escape) {
$escapemsg = sprintf(' (press Ctrl+%c to exit)', $escape+0x40);
$escape = sprintf(',escape=0x%x', $escape);
} else {
$escape = '';
}
my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
my $iface = $param->{iface};
if ($iface) {
die "serial interface '$iface' is not configured\n" if !$conf->{$iface};
die "wrong serial type on interface '$iface'\n" if $conf->{$iface} ne 'socket';
} else {
foreach my $opt (qw(serial0 serial1 serial2 serial3)) {
if ($conf->{$opt} && ($conf->{$opt} eq 'socket')) {
$iface = $opt;
last;
}
}
die "unable to find a serial interface\n" if !$iface;
}
die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
my $socket = "/var/run/qemu-server/${vmid}.$iface";
my $cmd = "socat UNIX-CONNECT:$socket STDIO,raw,echo=0$escape";
print "starting serial terminal on interface ${iface}${escapemsg}\n";
system($cmd);
return;
}});
__PACKAGE__->register_method ({
name => 'importovf',
path => 'importovf',
description => "Create a new VM using parameters read from an OVF manifest",
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
manifest => {
type => 'string',
description => 'path to the ovf file',
},
storage => get_standard_option('pve-storage-id', {
description => 'Target storage ID',
completion => \&PVE::QemuServer::complete_storage,
optional => 0,
}),
format => {
type => 'string',
description => 'Target format',
enum => [ 'raw', 'qcow2', 'vmdk' ],
optional => 1,
},
dryrun => {
type => 'boolean',
description => 'Print a parsed representation of the extracted OVF parameters, but do not create a VM',
optional => 1,
}
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my $vmid = PVE::Tools::extract_param($param, 'vmid');
my $ovf_file = PVE::Tools::extract_param($param, 'manifest');
my $storeid = PVE::Tools::extract_param($param, 'storage');
my $format = PVE::Tools::extract_param($param, 'format');
my $dryrun = PVE::Tools::extract_param($param, 'dryrun');
die "$ovf_file: non-existent or non-regular file\n" if (! -f $ovf_file);
my $storecfg = PVE::Storage::config();
PVE::Storage::storage_check_enabled($storecfg, $storeid);
my $parsed = PVE::QemuServer::OVF::parse_ovf($ovf_file);
if ($dryrun) {
print to_json($parsed, { pretty => 1, canonical => 1});
return;
}
eval { PVE::QemuConfig->create_and_lock_config($vmid) };
die "Reserving empty config for OVF import to VM $vmid failed: $@" if $@;
my $conf = PVE::QemuConfig->load_config($vmid);
die "Internal error: Expected 'create' lock in config of VM $vmid!"
if !PVE::QemuConfig->has_lock($conf, "create");
$conf->{name} = $parsed->{qm}->{name} if defined($parsed->{qm}->{name});
$conf->{memory} = $parsed->{qm}->{memory} if defined($parsed->{qm}->{memory});
$conf->{cores} = $parsed->{qm}->{cores} if defined($parsed->{qm}->{cores});
my $imported_disks = [];
eval {
# order matters, as do_import() will load_config() internally
$conf->{vmgenid} = PVE::QemuServer::generate_uuid();
$conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
PVE::QemuConfig->write_config($vmid, $conf);
foreach my $disk (@{ $parsed->{disks} }) {
my ($file, $drive) = ($disk->{backing_file}, $disk->{disk_address});
my ($name, $volid) = PVE::QemuServer::ImportDisk::do_import($file, $vmid, $storeid, {
drive_name => $drive,
format => $format,
skiplock => 1,
});
# for cleanup on (later) error
push @$imported_disks, $volid;
}
# reload after disks entries have been created
$conf = PVE::QemuConfig->load_config($vmid);
my $devs = PVE::QemuServer::get_default_bootdevices($conf);
$conf->{boot} = PVE::QemuServer::print_bootorder($devs);
PVE::QemuConfig->write_config($vmid, $conf);
};
if (my $err = $@) {
my $skiplock = 1;
warn "error during import, cleaning up created resources...\n";
for my $volid (@$imported_disks) {
eval { PVE::Storage::vdisk_free($storecfg, $volid) };
warn "cleanup of $volid failed: $@\n" if $@;
}
eval { PVE::QemuServer::destroy_vm($storecfg, $vmid, $skiplock) };
warn "Could not destroy VM $vmid: $@" if "$@";
die "import failed - $err";
}
PVE::QemuConfig->remove_lock($vmid, "create");
return;
}
});
__PACKAGE__->register_method({
name => 'exec',
path => 'exec',
method => 'POST',
protected => 1,
description => "Executes the given command via the guest agent",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid', {
completion => \&PVE::QemuServer::complete_vmid_running }),
synchronous => {
type => 'boolean',
optional => 1,
default => 1,
description => "If set to off, returns the pid immediately instead of waiting for the commmand to finish or the timeout.",
},
'timeout' => {
type => 'integer',
description => "The maximum time to wait synchronously for the command to finish. If reached, the pid gets returned. Set to 0 to deactivate",
minimum => 0,
optional => 1,
default => 30,
},
'pass-stdin' => {
type => 'boolean',
description => "When set, read STDIN until EOF and forward to guest agent via 'input-data' (usually treated as STDIN to process launched by guest agent). Allows maximal 1 MiB.",
optional => 1,
default => 0,
},
'extra-args' => get_standard_option('extra-args'),
},
},
returns => {
type => 'object',
},
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
my $sync = $param->{synchronous} // 1;
my $pass_stdin = $param->{'pass-stdin'};
if (defined($param->{timeout}) && !$sync) {
raise_param_exc({ synchronous => "needs to be set for 'timeout'"});
}
my $input_data = undef;
if ($pass_stdin) {
$input_data = '';
while (my $line = <STDIN>) {
$input_data .= $line;
if (length($input_data) > 1024*1024) {
# not sure how QEMU handles large amounts of data being
# passed into the QMP socket, so limit to be safe
die "'input-data' (STDIN) is limited to 1 MiB, aborting\n";
}
}
}
my $args = $param->{'extra-args'};
$args = undef if !$args || !@$args;
my $res = PVE::QemuServer::Agent::qemu_exec($vmid, $input_data, $args);
if ($sync) {
my $pid = $res->{pid};
my $timeout = $param->{timeout} // 30;
my $starttime = time();
while ($timeout == 0 || (time() - $starttime) < $timeout) {
my $out = PVE::QemuServer::Agent::qemu_exec_status($vmid, $pid);
if ($out->{exited}) {
$res = $out;
last;
}
sleep 1;
}
if (!$res->{exited}) {
warn "timeout reached, returning pid\n";
}
}
return { result => $res };
}});
__PACKAGE__->register_method({
name => 'cleanup',
path => 'cleanup',
method => 'POST',
protected => 1,
description => "Cleans up resources like tap devices, vgpus, etc. Called after a vm shuts down, crashes, etc.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid', {
completion => \&PVE::QemuServer::complete_vmid_running }),
'clean-shutdown' => {
type => 'boolean',
description => "Indicates if qemu shutdown cleanly.",
},
'guest-requested' => {
type => 'boolean',
description => "Indicates if the shutdown was requested by the guest or via qmp.",
},
},
},
returns => { type => 'null', },
code => sub {
my ($param) = @_;
my $vmid = $param->{vmid};
my $clean = $param->{'clean-shutdown'};
my $guest = $param->{'guest-requested'};
my $restart = 0;
# return if we do not have the config anymore
return if !-f PVE::QemuConfig->config_file($vmid);
my $storecfg = PVE::Storage::config();
warn "Starting cleanup for $vmid\n";
PVE::QemuConfig->lock_config($vmid, sub {
my $conf = PVE::QemuConfig->load_config ($vmid);
my $pid = PVE::QemuServer::check_running ($vmid);
die "vm still running\n" if $pid;
if (!$clean) {
# we have to cleanup the tap devices after a crash
foreach my $opt (keys %$conf) {
next if $opt !~ m/^net(\d+)$/;
my $interface = $1;
PVE::Network::tap_unplug("tap${vmid}i${interface}");
}
}
if (!$clean || $guest) {
# vm was shutdown from inside the guest or crashed, doing api cleanup
PVE::QemuServer::vm_stop_cleanup($storecfg, $vmid, $conf, 0, 0);
}
PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-stop');
$restart = eval { PVE::QemuServer::clear_reboot_request($vmid) };
warn $@ if $@;
});
warn "Finished cleanup for $vmid\n";
if ($restart) {
warn "Restarting VM $vmid\n";
PVE::API2::Qemu->vm_start({
vmid => $vmid,
%node,
});
}
return;
}});
my $print_agent_result = sub {
my ($data) = @_;
my $result = $data->{result} // $data;
return if !defined($result);
my $class = ref($result);
if (!$class) {
chomp $result;
return if $result =~ m/^\s*$/;
print "$result\n";
return;
}
if (($class eq 'HASH') && !scalar(keys %$result)) { # empty hash
return;
}
print to_json($result, { pretty => 1, canonical => 1, utf8 => 1});
};
sub param_mapping {
my ($name) = @_;
my $ssh_key_map = ['sshkeys', sub {
return URI::Escape::uri_escape(PVE::Tools::file_get_contents($_[0]));
}];
my $cipassword_map = PVE::CLIHandler::get_standard_mapping('pve-password', { name => 'cipassword' });
my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password');
my $mapping = {
'update_vm' => [$ssh_key_map, $cipassword_map],
'create_vm' => [$ssh_key_map, $cipassword_map],
'set-user-password' => [$password_map],
};
return $mapping->{$name};
}
our $cmddef = {
list=> [ "PVE::API2::Qemu", 'vmlist', [], { %node }, sub {
my $vmlist = shift;
exit 0 if (!scalar(@$vmlist));
printf "%10s %-20s %-10s %-10s %12s %-10s\n",
qw(VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID);
foreach my $rec (sort { $a->{vmid} <=> $b->{vmid} } @$vmlist) {
printf "%10s %-20s %-10s %-10s %12.2f %-10s\n", $rec->{vmid}, $rec->{name},
$rec->{qmpstatus} || $rec->{status},
($rec->{maxmem} || 0)/(1024*1024),
($rec->{maxdisk} || 0)/(1024*1024*1024),
$rec->{pid} || 0;
}
}],
create => [ "PVE::API2::Qemu", 'create_vm', ['vmid'], { %node }, $upid_exit ],
destroy => [ "PVE::API2::Qemu", 'destroy_vm', ['vmid'], { %node }, $upid_exit ],
clone => [ "PVE::API2::Qemu", 'clone_vm', ['vmid', 'newid'], { %node }, $upid_exit ],
migrate => [ "PVE::API2::Qemu", 'migrate_vm', ['vmid', 'target'], { %node }, $upid_exit ],
'remote-migrate' => [ __PACKAGE__, 'remote_migrate_vm', ['vmid', 'target-vmid', 'target-endpoint'], { %node }, $upid_exit ],
set => [ "PVE::API2::Qemu", 'update_vm', ['vmid'], { %node } ],
config => [ "PVE::API2::Qemu", 'vm_config', ['vmid'], { %node }, sub {
my $config = shift;
foreach my $k (sort (keys %$config)) {
next if $k eq 'digest';
my $v = $config->{$k};
if ($k eq 'description') {
$v = PVE::Tools::encode_text($v);
}
print "$k: $v\n";
}
}],
pending => [ "PVE::API2::Qemu", 'vm_pending', ['vmid'], { %node }, \&PVE::GuestHelpers::format_pending ],
showcmd => [ __PACKAGE__, 'showcmd', ['vmid']],
status => [ __PACKAGE__, 'status', ['vmid']],
# FIXME: for 8.0 move to command group snapshot { create, list, destroy, rollback }
snapshot => [ "PVE::API2::Qemu", 'snapshot', ['vmid', 'snapname'], { %node } , $upid_exit ],
delsnapshot => [ "PVE::API2::Qemu", 'delsnapshot', ['vmid', 'snapname'], { %node } , $upid_exit ],
listsnapshot => [ "PVE::API2::Qemu", 'snapshot_list', ['vmid'], { %node }, \&PVE::GuestHelpers::print_snapshot_tree],
rollback => [ "PVE::API2::Qemu", 'rollback', ['vmid', 'snapname'], { %node } , $upid_exit ],
template => [ "PVE::API2::Qemu", 'template', ['vmid'], { %node }],
# FIXME: should be in a power command group?
start => [ "PVE::API2::Qemu", 'vm_start', ['vmid'], { %node } , $upid_exit ],
stop => [ "PVE::API2::Qemu", 'vm_stop', ['vmid'], { %node }, $upid_exit ],
reset => [ "PVE::API2::Qemu", 'vm_reset', ['vmid'], { %node }, $upid_exit ],
shutdown => [ "PVE::API2::Qemu", 'vm_shutdown', ['vmid'], { %node }, $upid_exit ],
reboot => [ "PVE::API2::Qemu", 'vm_reboot', ['vmid'], { %node }, $upid_exit ],
suspend => [ "PVE::API2::Qemu", 'vm_suspend', ['vmid'], { %node }, $upid_exit ],
resume => [ "PVE::API2::Qemu", 'vm_resume', ['vmid'], { %node }, $upid_exit ],
sendkey => [ "PVE::API2::Qemu", 'vm_sendkey', ['vmid', 'key'], { %node } ],
vncproxy => [ __PACKAGE__, 'vncproxy', ['vmid']],
wait => [ __PACKAGE__, 'wait', ['vmid']],
unlock => [ __PACKAGE__, 'unlock', ['vmid']],
# TODO: evluate dropping below aliases for 8.0, if no usage is left
importdisk => { alias => 'disk import' },
'move-disk' => { alias => 'disk move' },
move_disk => { alias => 'disk move' },
rescan => { alias => 'disk rescan' },
resize => { alias => 'disk resize' },
unlink => { alias => 'disk unlink' },
disk => {
import => [ __PACKAGE__, 'importdisk', ['vmid', 'source', 'storage']],
'move' => [ "PVE::API2::Qemu", 'move_vm_disk', ['vmid', 'disk', 'storage'], { %node }, $upid_exit ],
rescan => [ __PACKAGE__, 'rescan', []],
resize => [ "PVE::API2::Qemu", 'resize_vm', ['vmid', 'disk', 'size'], { %node } ],
unlink => [ "PVE::API2::Qemu", 'unlink', ['vmid'], { %node } ],
},
monitor => [ __PACKAGE__, 'monitor', ['vmid']],
agent => { alias => 'guest cmd' }, # FIXME: remove with PVE 8.0
guest => {
cmd => [ "PVE::API2::Qemu::Agent", 'agent', ['vmid', 'command'], { %node }, $print_agent_result ],
passwd => [ "PVE::API2::Qemu::Agent", 'set-user-password', [ 'vmid', 'username' ], { %node }],
exec => [ __PACKAGE__, 'exec', [ 'vmid', 'extra-args' ], { %node }, $print_agent_result],
'exec-status' => [ "PVE::API2::Qemu::Agent", 'exec-status', [ 'vmid', 'pid' ], { %node }, $print_agent_result],
},
mtunnel => [ __PACKAGE__, 'mtunnel', []],
nbdstop => [ __PACKAGE__, 'nbdstop', ['vmid']],
terminal => [ __PACKAGE__, 'terminal', ['vmid']],
importovf => [ __PACKAGE__, 'importovf', ['vmid', 'manifest', 'storage']],
cleanup => [ __PACKAGE__, 'cleanup', ['vmid', 'clean-shutdown', 'guest-requested'], { %node }],
cloudinit => {
dump => [ "PVE::API2::Qemu", 'cloudinit_generated_config_dump', ['vmid', 'type'], { %node }, sub { print "$_[0]\n"; }],
pending => [ "PVE::API2::Qemu", 'cloudinit_pending', ['vmid'], { %node }, \&PVE::GuestHelpers::format_pending ],
update => [ "PVE::API2::Qemu", 'cloudinit_update', ['vmid'], { node => $nodename }],
},
};
1;