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::Network;
use PVE::RPCEnvironment;
use PVE::SafeSyslog;
-use PVE::Tools qw(extract_param);
+use PVE::Tools qw(extract_param file_get_contents);
use PVE::API2::Qemu::Agent;
use PVE::API2::Qemu;
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 (or missing permission)!\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',
last;
} elsif ($line =~ /^resume (\d+)$/) {
my $vmid = $1;
+ # check_running and vm_resume with nocheck, since local node
+ # might not have processed config move/rename yet
if (PVE::QemuServer::check_running($vmid, 1)) {
eval { PVE::QemuServer::vm_resume($vmid, 1, 1); };
if ($@) {
name => 'monitor',
path => 'monitor',
method => 'POST',
- description => "Enter Qemu Monitor interface.",
+ description => "Enter QEMU Monitor interface.",
parameters => {
additionalProperties => 0,
properties => {
my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
- print "Entering Qemu Monitor for VM $vmid - type 'help' for help\n";
+ print "Entering QEMU Monitor for VM $vmid - type 'help' for help\n";
my $term = Term::ReadLine->new('qm');
my $storecfg = PVE::Storage::config();
warn "Starting cleanup for $vmid\n";
- PVE::QemuConfig->lock_config($vmid, sub {
+ # mdev cleanup can take a while, so wait up to 60 seconds
+ PVE::QemuConfig->lock_config_full($vmid, 60, sub {
my $conf = PVE::QemuConfig->load_config ($vmid);
my $pid = PVE::QemuServer::check_running ($vmid);
die "vm still running\n" if $pid;
return;
}});
+__PACKAGE__->register_method({
+ name => 'vm_import',
+ path => 'vm-import',
+ description => "Import a foreign virtual guest from a supported import source, such as an ESXi storage.",
+ parameters => {
+ additionalProperties => 0,
+ properties => PVE::QemuServer::json_config_properties({
+ vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
+ 'source' => {
+ type => 'string',
+ description => 'The import source volume id.',
+ },
+ storage => get_standard_option('pve-storage-id', {
+ description => "Default storage.",
+ completion => \&PVE::QemuServer::complete_storage,
+ }),
+ 'live-import' => {
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ description => "Immediately start the VM and copy the data in the background.",
+ },
+ 'dryrun' => {
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ description => "Show the create command and exit without doing anything.",
+ },
+ delete => {
+ type => 'string', format => 'pve-configid-list',
+ description => "A list of settings you want to delete.",
+ optional => 1,
+ },
+ format => {
+ type => 'string',
+ description => 'Target format',
+ enum => [ 'raw', 'qcow2', 'vmdk' ],
+ optional => 1,
+ },
+ }),
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my ($vmid, $source, $storage, $format, $live_import, $dryrun, $delete) =
+ delete $param->@{qw(vmid source storage format live-import dryrun delete)};
+
+ if (defined($format)) {
+ $format = ",format=$format";
+ } else {
+ $format = '';
+ }
+
+ my $storecfg = PVE::Storage::config();
+ my $metadata = PVE::Storage::get_import_metadata($storecfg, $source);
+
+ my $create_args = $metadata->{'create-args'};
+ if (my $netdevs = $metadata->{net}) {
+ for my $net (keys $netdevs->%*) {
+ my $value = $netdevs->{$net};
+ $create_args->{$net} = join(',', map { $_ . '=' . $value->{$_} } sort keys %$value);
+ }
+ }
+ if (my $disks = $metadata->{disks}) {
+ if (delete $disks->{efidisk0}) {
+ $create_args->{efidisk0} = "$storage:1$format,efitype=4m";
+ }
+ for my $disk (keys $disks->%*) {
+ my $value = $disks->{$disk}->{volid};
+ $create_args->{$disk} = "$storage:0${format},import-from=$value";
+ }
+ }
+
+ $create_args->{'live-restore'} = 1 if $live_import;
+
+ $create_args->{$_} = $param->{$_} for keys $param->%*;
+ delete $create_args->{$_} for PVE::Tools::split_list($delete);
+
+ if ($dryrun) {
+ print("# dry-run – the resulting create command for the import would be:\n");
+ print("qm create $vmid \\\n ");
+ print(join(" \\\n ", map { "--$_ $create_args->{$_}" } sort keys $create_args->%*));
+ print("\n");
+ return;
+ }
+
+ PVE::API2::Qemu->create_vm({
+ %node,
+ vmid => $vmid,
+ %$create_args,
+ });
+ return;
+ }
+});
+
my $print_agent_result = sub {
my ($data) = @_;
my ($name) = @_;
my $ssh_key_map = ['sshkeys', sub {
- return URI::Escape::uri_escape(PVE::Tools::file_get_contents($_[0]));
+ return URI::Escape::uri_escape(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');
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 } ],
- resize => [ "PVE::API2::Qemu", 'resize_vm', ['vmid', 'disk', 'size'], { %node } ],
- 'move-disk' => [ "PVE::API2::Qemu", 'move_vm_disk', ['vmid', 'disk', 'storage'], { %node }, $upid_exit ],
- move_disk => { alias => 'move-disk' },
-
- unlink => [ "PVE::API2::Qemu", 'unlink', ['vmid'], { %node } ],
-
config => [ "PVE::API2::Qemu", 'vm_config', ['vmid'], { %node }, sub {
my $config = shift;
foreach my $k (sort (keys %$config)) {
# 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 }],
unlock => [ __PACKAGE__, 'unlock', ['vmid']],
- # FIXME: should this be in a 'disk' command group with move and resize and import?
- rescan => [ __PACKAGE__, 'rescan', []],
+ # 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']],
terminal => [ __PACKAGE__, 'terminal', ['vmid']],
- importdisk => [ __PACKAGE__, 'importdisk', ['vmid', 'source', 'storage']],
-
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 }],
},
+ import => [ __PACKAGE__, 'vm_import', ['vmid', 'source']],
};
1;