1 package PVE
::API2
::Qemu
;
12 use Crypt
::OpenSSL
::Random
;
13 use Socket
qw(SOCK_STREAM);
15 use PVE
::APIClient
::LWP
;
17 use PVE
::Cluster qw
(cfs_read_file cfs_write_file
);;
20 use PVE
::Tools
qw(extract_param);
21 use PVE
::Exception
qw(raise raise_param_exc raise_perm_exc);
23 use PVE
::JSONSchema
qw(get_standard_option);
25 use PVE
::ReplicationConfig
;
26 use PVE
::GuestHelpers
qw(assert_tag_permissions);
29 use PVE
::QemuServer
::Cloudinit
;
30 use PVE
::QemuServer
::CPUConfig
;
31 use PVE
::QemuServer
::Drive
;
32 use PVE
::QemuServer
::ImportDisk
;
33 use PVE
::QemuServer
::Monitor
qw(mon_cmd);
34 use PVE
::QemuServer
::Machine
;
36 use PVE
::RPCEnvironment
;
37 use PVE
::AccessControl
;
41 use PVE
::API2
::Firewall
::VM
;
42 use PVE
::API2
::Qemu
::Agent
;
43 use PVE
::VZDump
::Plugin
;
44 use PVE
::DataCenterConfig
;
47 use PVE
::StorageTunnel
;
50 if (!$ENV{PVE_GENERATING_DOCS
}) {
51 require PVE
::HA
::Env
::PVE2
;
52 import PVE
::HA
::Env
::PVE2
;
53 require PVE
::HA
::Config
;
54 import PVE
::HA
::Config
;
58 use base
qw(PVE::RESTHandler);
60 my $opt_force_description = "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.";
62 my $resolve_cdrom_alias = sub {
65 if (my $value = $param->{cdrom
}) {
66 $value .= ",media=cdrom" if $value !~ m/media=/;
67 $param->{ide2
} = $value;
68 delete $param->{cdrom
};
72 # Used in import-enabled API endpoints. Parses drives using the extended '_with_alloc' schema.
73 my $foreach_volume_with_alloc = sub {
74 my ($param, $func) = @_;
76 for my $opt (sort keys $param->%*) {
77 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
79 my $drive = PVE
::QemuServer
::Drive
::parse_drive
($opt, $param->{$opt}, 1);
82 $func->($opt, $drive);
86 my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
88 my $check_drive_param = sub {
89 my ($param, $storecfg, $extra_checks) = @_;
91 for my $opt (sort keys $param->%*) {
92 next if !PVE
::QemuServer
::is_valid_drivename
($opt);
94 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
95 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
97 if ($drive->{'import-from'}) {
98 if ($drive->{file
} !~ $NEW_DISK_RE || $3 != 0) {
100 $opt => "'import-from' requires special syntax - ".
101 "use <storage ID>:0,import-from=<source>",
105 if ($opt eq 'efidisk0') {
106 for my $required (qw(efitype pre-enrolled-keys)) {
107 if (!defined($drive->{$required})) {
109 $opt => "need to specify '$required' when using 'import-from'",
113 } elsif ($opt eq 'tpmstate0') {
114 raise_param_exc
({ $opt => "need to specify 'version' when using 'import-from'" })
115 if !defined($drive->{version
});
119 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
121 $extra_checks->($drive) if $extra_checks;
123 $param->{$opt} = PVE
::QemuServer
::print_drive
($drive, 1);
127 my $check_storage_access = sub {
128 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
130 $foreach_volume_with_alloc->($settings, sub {
131 my ($ds, $drive) = @_;
133 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
135 my $volid = $drive->{file
};
136 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
138 if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit' || (defined($volname) && $volname eq 'cloudinit'))) {
140 } elsif ($isCDROM && ($volid eq 'cdrom')) {
141 $rpcenv->check($authuser, "/", ['Sys.Console']);
142 } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) {
143 my ($storeid, $size) = ($2 || $default_storage, $3);
144 die "no storage ID specified (and no default storage)\n" if !$storeid;
145 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
146 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
147 raise_param_exc
({ storage
=> "storage '$storeid' does not support vm images"})
148 if !$scfg->{content
}->{images
};
150 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
152 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
153 raise_param_exc
({ $ds => "content type needs to be 'images' or 'iso'" })
154 if $vtype ne 'images' && $vtype ne 'iso';
158 if (my $src_image = $drive->{'import-from'}) {
160 if (PVE
::Storage
::parse_volume_id
($src_image, 1)) { # PVE-managed volume
161 (my $vtype, undef, $src_vmid) = PVE
::Storage
::parse_volname
($storecfg, $src_image);
162 raise_param_exc
({ $ds => "$src_image has wrong type '$vtype' - not an image" })
163 if $vtype ne 'images';
166 if ($src_vmid) { # might be actively used by VM and will be copied via clone_disk()
167 $rpcenv->check($authuser, "/vms/${src_vmid}", ['VM.Clone']);
169 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $src_image);
174 $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
175 if defined($settings->{vmstatestorage
});
178 my $check_storage_access_clone = sub {
179 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
183 PVE
::QemuConfig-
>foreach_volume($conf, sub {
184 my ($ds, $drive) = @_;
186 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
188 my $volid = $drive->{file
};
190 return if !$volid || $volid eq 'none';
193 if ($volid eq 'cdrom') {
194 $rpcenv->check($authuser, "/", ['Sys.Console']);
196 # we simply allow access
197 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
198 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
199 $sharedvm = 0 if !$scfg->{shared
};
203 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
204 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
205 $sharedvm = 0 if !$scfg->{shared
};
207 $sid = $storage if $storage;
208 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
212 $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
213 if defined($conf->{vmstatestorage
});
218 my $check_storage_access_migrate = sub {
219 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
221 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $node);
223 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
225 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
226 die "storage '$storage' does not support vm images\n"
227 if !$scfg->{content
}->{images
};
230 my $import_from_volid = sub {
231 my ($storecfg, $src_volid, $dest_info, $vollist) = @_;
233 die "could not get size of $src_volid\n"
234 if !PVE
::Storage
::volume_size_info
($storecfg, $src_volid, 10);
236 die "cannot import from cloudinit disk\n"
237 if PVE
::QemuServer
::Drive
::drive_is_cloudinit
({ file
=> $src_volid });
239 my $src_vmid = (PVE
::Storage
::parse_volname
($storecfg, $src_volid))[2];
241 my $src_vm_state = sub {
242 my $exists = $src_vmid && PVE
::Cluster
::get_vmlist
()->{ids
}->{$src_vmid} ?
1 : 0;
246 eval { PVE
::QemuConfig
::assert_config_exists_on_node
($src_vmid); };
247 die "owner VM $src_vmid not on local node\n" if $@;
248 $runs = PVE
::QemuServer
::Helpers
::vm_running_locally
($src_vmid) || 0;
251 return ($exists, $runs);
254 my ($src_vm_exists, $running) = $src_vm_state->();
256 die "cannot import from '$src_volid' - full clone feature is not supported\n"
257 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $src_volid, undef, $running);
260 my ($src_vm_exists_now, $running_now) = $src_vm_state->();
262 die "owner VM $src_vmid changed state unexpectedly\n"
263 if $src_vm_exists_now != $src_vm_exists || $running_now != $running;
265 my $src_conf = $src_vm_exists_now ? PVE
::QemuConfig-
>load_config($src_vmid) : {};
267 my $src_drive = { file
=> $src_volid };
269 PVE
::QemuConfig-
>foreach_volume($src_conf, sub {
270 my ($ds, $drive) = @_;
272 return if $src_drivename;
274 if ($drive->{file
} eq $src_volid) {
276 $src_drivename = $ds;
282 running
=> $running_now,
283 drivename
=> $src_drivename,
288 my ($src_storeid) = PVE
::Storage
::parse_volume_id
($src_volid);
290 return PVE
::QemuServer
::clone_disk
(
299 PVE
::Storage
::get_bandwidth_limit
('clone', [$src_storeid, $dest_info->{storage
}]),
305 $cloned = PVE
::QemuConfig-
>lock_config_full($src_vmid, 30, $clonefn);
306 } elsif ($src_vmid) {
307 $cloned = PVE
::QemuConfig-
>lock_config_shared($src_vmid, 30, $clonefn);
309 $cloned = $clonefn->();
312 return $cloned->@{qw(file size)};
315 # Note: $pool is only needed when creating a VM, because pool permissions
316 # are automatically inherited if VM already exists inside a pool.
317 my $create_disks = sub {
318 my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
325 my ($ds, $disk) = @_;
327 my $volid = $disk->{file
};
328 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
330 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
331 delete $disk->{size
};
332 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
333 } elsif (defined($volname) && $volname eq 'cloudinit') {
334 $storeid = $storeid // $default_storage;
335 die "no storage ID specified (and no default storage)\n" if !$storeid;
338 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
339 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
340 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
342 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
345 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
346 my $name = "vm-$vmid-cloudinit";
350 $fmt = $disk->{format
} // "qcow2";
353 $fmt = $disk->{format
} // "raw";
356 # Initial disk created with 4 MB and aligned to 4MB on regeneration
357 my $ci_size = PVE
::QemuServer
::Cloudinit
::CLOUDINIT_DISK_SIZE
;
358 my $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
359 $disk->{file
} = $volid;
360 $disk->{media
} = 'cdrom';
361 push @$vollist, $volid;
362 delete $disk->{format
}; # no longer needed
363 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
364 print "$ds: successfully created disk '$res->{$ds}'\n";
365 } elsif ($volid =~ $NEW_DISK_RE) {
366 my ($storeid, $size) = ($2 || $default_storage, $3);
367 die "no storage ID specified (and no default storage)\n" if !$storeid;
369 if (my $source = delete $disk->{'import-from'}) {
372 if (PVE
::Storage
::parse_volume_id
($source, 1)) { # PVE-managed volume
377 format
=> $disk->{format
},
380 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf, $disk)
381 if $ds eq 'efidisk0';
383 ($dst_volid, $size) = eval {
384 $import_from_volid->($storecfg, $source, $dest_info, $vollist);
386 die "cannot import from '$source' - $@" if $@;
388 $source = PVE
::Storage
::abs_filesystem_path
($storecfg, $source, 1);
389 $size = PVE
::Storage
::file_size_info
($source);
390 die "could not get file size of $source\n" if !$size;
392 (undef, $dst_volid) = PVE
::QemuServer
::ImportDisk
::do_import
(
398 format
=> $disk->{format
},
399 'skip-config-update' => 1,
402 push @$vollist, $dst_volid;
405 $disk->{file
} = $dst_volid;
406 $disk->{size
} = $size;
407 delete $disk->{format
}; # no longer needed
408 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
410 my $defformat = PVE
::Storage
::storage_default_format
($storecfg, $storeid);
411 my $fmt = $disk->{format
} || $defformat;
413 $size = PVE
::Tools
::convert_size
($size, 'gb' => 'kb'); # vdisk_alloc uses kb
416 if ($ds eq 'efidisk0') {
417 my $smm = PVE
::QemuServer
::Machine
::machine_type_is_q35
($conf);
418 ($volid, $size) = PVE
::QemuServer
::create_efidisk
(
419 $storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
420 } elsif ($ds eq 'tpmstate0') {
421 # swtpm can only use raw volumes, and uses a fixed size
422 $size = PVE
::Tools
::convert_size
(PVE
::QemuServer
::Drive
::TPMSTATE_DISK_SIZE
, 'b' => 'kb');
423 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, "raw", undef, $size);
425 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid, $fmt, undef, $size);
427 push @$vollist, $volid;
428 $disk->{file
} = $volid;
429 $disk->{size
} = PVE
::Tools
::convert_size
($size, 'kb' => 'b');
430 delete $disk->{format
}; # no longer needed
431 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
434 print "$ds: successfully created disk '$res->{$ds}'\n";
436 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
438 my ($vtype) = PVE
::Storage
::parse_volname
($storecfg, $volid);
439 die "cannot use volume $volid - content type needs to be 'images' or 'iso'"
440 if $vtype ne 'images' && $vtype ne 'iso';
442 if (PVE
::QemuServer
::Drive
::drive_is_cloudinit
($disk)) {
444 my $ci_key = PVE
::QemuConfig-
>has_cloudinit($conf, $ds)
445 || PVE
::QemuConfig-
>has_cloudinit($conf->{pending
} || {}, $ds)
446 || PVE
::QemuConfig-
>has_cloudinit($res, $ds)
448 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
453 PVE
::Storage
::activate_volumes
($storecfg, [ $volid ]) if $storeid;
455 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid);
456 die "volume $volid does not exist\n" if !$size;
457 $disk->{size
} = $size;
459 $res->{$ds} = PVE
::QemuServer
::print_drive
($disk);
463 eval { $foreach_volume_with_alloc->($settings, $code); };
465 # free allocated images on error
467 syslog
('err', "VM $vmid creating disks failed");
468 foreach my $volid (@$vollist) {
469 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
475 return ($vollist, $res);
478 my $check_cpu_model_access = sub {
479 my ($rpcenv, $authuser, $new, $existing) = @_;
481 return if !defined($new->{cpu
});
483 my $cpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $new->{cpu
});
484 return if !$cpu || !$cpu->{cputype
}; # always allow default
485 my $cputype = $cpu->{cputype
};
487 if ($existing && $existing->{cpu
}) {
488 # changing only other settings doesn't require permissions for CPU model
489 my $existingCpu = PVE
::JSONSchema
::check_format
('pve-vm-cpu-conf', $existing->{cpu
});
490 return if $existingCpu->{cputype
} eq $cputype;
493 if (PVE
::QemuServer
::CPUConfig
::is_custom_model
($cputype)) {
494 $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
509 my $memoryoptions = {
515 my $hwtypeoptions = {
528 my $generaloptions = {
535 'migrate_downtime' => 1,
536 'migrate_speed' => 1,
548 my $vmpoweroptions = {
555 'vmstatestorage' => 1,
558 my $cloudinitoptions = {
568 my $check_vm_create_serial_perm = sub {
569 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
571 return 1 if $authuser eq 'root@pam';
573 foreach my $opt (keys %{$param}) {
574 next if $opt !~ m/^serial\d+$/;
576 if ($param->{$opt} eq 'socket') {
577 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
579 die "only root can set '$opt' config for real devices\n";
586 my $check_vm_create_usb_perm = sub {
587 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
589 return 1 if $authuser eq 'root@pam';
591 foreach my $opt (keys %{$param}) {
592 next if $opt !~ m/^usb\d+$/;
594 if ($param->{$opt} =~ m/spice/) {
595 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
597 die "only root can set '$opt' config for real devices\n";
604 my $check_vm_modify_config_perm = sub {
605 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
607 return 1 if $authuser eq 'root@pam';
609 foreach my $opt (@$key_list) {
610 # some checks (e.g., disk, serial port, usb) need to be done somewhere
611 # else, as there the permission can be value dependend
612 next if PVE
::QemuServer
::is_valid_drivename
($opt);
613 next if $opt eq 'cdrom';
614 next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
615 next if $opt eq 'tags';
618 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
619 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
620 } elsif ($memoryoptions->{$opt}) {
621 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
622 } elsif ($hwtypeoptions->{$opt}) {
623 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
624 } elsif ($generaloptions->{$opt}) {
625 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
626 # special case for startup since it changes host behaviour
627 if ($opt eq 'startup') {
628 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
630 } elsif ($vmpoweroptions->{$opt}) {
631 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
632 } elsif ($diskoptions->{$opt}) {
633 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
634 } elsif ($opt =~ m/^(?:net|ipconfig)\d+$/) {
635 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
636 } elsif ($cloudinitoptions->{$opt}) {
637 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
638 } elsif ($opt eq 'vmstate') {
639 # the user needs Disk and PowerMgmt privileges to change the vmstate
640 # also needs privileges on the storage, that will be checked later
641 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
643 # catches hostpci\d+, args, lock, etc.
644 # new options will be checked here
645 die "only root can set '$opt' config\n";
652 __PACKAGE__-
>register_method({
656 description
=> "Virtual machine index (per node).",
658 description
=> "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
662 protected
=> 1, # qemu pid files are only readable by root
664 additionalProperties
=> 0,
666 node
=> get_standard_option
('pve-node'),
670 description
=> "Determine the full status of active VMs.",
678 properties
=> $PVE::QemuServer
::vmstatus_return_properties
,
680 links
=> [ { rel
=> 'child', href
=> "{vmid}" } ],
685 my $rpcenv = PVE
::RPCEnvironment
::get
();
686 my $authuser = $rpcenv->get_user();
688 my $vmstatus = PVE
::QemuServer
::vmstatus
(undef, $param->{full
});
691 foreach my $vmid (keys %$vmstatus) {
692 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
694 my $data = $vmstatus->{$vmid};
701 my $parse_restore_archive = sub {
702 my ($storecfg, $archive) = @_;
704 my ($archive_storeid, $archive_volname) = PVE
::Storage
::parse_volume_id
($archive, 1);
706 if (defined($archive_storeid)) {
707 my $scfg = PVE
::Storage
::storage_config
($storecfg, $archive_storeid);
708 if ($scfg->{type
} eq 'pbs') {
715 my $path = PVE
::Storage
::abs_filesystem_path
($storecfg, $archive);
723 __PACKAGE__-
>register_method({
727 description
=> "Create or restore a virtual machine.",
729 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
730 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
731 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
732 user
=> 'all', # check inside
737 additionalProperties
=> 0,
738 properties
=> PVE
::QemuServer
::json_config_properties
(
740 node
=> get_standard_option
('pve-node'),
741 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::Cluster
::complete_next_vmid
}),
743 description
=> "The backup archive. Either the file system path to a .tar or .vma file (use '-' to pipe data from stdin) or a proxmox storage backup volume identifier.",
747 completion
=> \
&PVE
::QemuServer
::complete_backup_archives
,
749 storage
=> get_standard_option
('pve-storage-id', {
750 description
=> "Default storage.",
752 completion
=> \
&PVE
::QemuServer
::complete_storage
,
757 description
=> "Allow to overwrite existing VM.",
758 requires
=> 'archive',
763 description
=> "Assign a unique random ethernet address.",
764 requires
=> 'archive',
769 description
=> "Start the VM immediately from the backup and restore in background. PBS only.",
770 requires
=> 'archive',
774 type
=> 'string', format
=> 'pve-poolid',
775 description
=> "Add the VM to the specified pool.",
778 description
=> "Override I/O bandwidth limit (in KiB/s).",
782 default => 'restore limit from datacenter or storage config',
788 description
=> "Start VM after it was created successfully.",
800 my $rpcenv = PVE
::RPCEnvironment
::get
();
801 my $authuser = $rpcenv->get_user();
803 my $node = extract_param
($param, 'node');
804 my $vmid = extract_param
($param, 'vmid');
806 my $archive = extract_param
($param, 'archive');
807 my $is_restore = !!$archive;
809 my $bwlimit = extract_param
($param, 'bwlimit');
810 my $force = extract_param
($param, 'force');
811 my $pool = extract_param
($param, 'pool');
812 my $start_after_create = extract_param
($param, 'start');
813 my $storage = extract_param
($param, 'storage');
814 my $unique = extract_param
($param, 'unique');
815 my $live_restore = extract_param
($param, 'live-restore');
817 if (defined(my $ssh_keys = $param->{sshkeys
})) {
818 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
819 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
822 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
823 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
825 PVE
::Cluster
::check_cfs_quorum
();
827 my $filename = PVE
::QemuConfig-
>config_file($vmid);
828 my $storecfg = PVE
::Storage
::config
();
830 if (defined($pool)) {
831 $rpcenv->check_pool_exist($pool);
834 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
835 if defined($storage);
837 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
839 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
841 } elsif ($archive && $force && (-f
$filename) &&
842 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
843 # OK: user has VM.Backup permissions, and want to restore an existing VM
849 for my $opt (sort keys $param->%*) {
850 if (PVE
::QemuServer
::Drive
::is_valid_drivename
($opt)) {
851 raise_param_exc
({ $opt => "option conflicts with option 'archive'" });
855 if ($archive eq '-') {
856 die "pipe requires cli environment\n" if $rpcenv->{type
} ne 'cli';
857 $archive = { type
=> 'pipe' };
859 PVE
::Storage
::check_volume_access
(
868 $archive = $parse_restore_archive->($storecfg, $archive);
872 if (scalar(keys $param->%*) > 0) {
873 &$resolve_cdrom_alias($param);
875 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
877 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
879 &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
880 &$check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
882 &$check_cpu_model_access($rpcenv, $authuser, $param);
884 $check_drive_param->($param, $storecfg);
886 PVE
::QemuServer
::add_random_macs
($param);
889 my $emsg = $is_restore ?
"unable to restore VM $vmid -" : "unable to create VM $vmid -";
891 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, $force) };
892 die "$emsg $@" if $@;
894 my $restored_data = 0;
895 my $restorefn = sub {
896 my $conf = PVE
::QemuConfig-
>load_config($vmid);
898 PVE
::QemuConfig-
>check_protection($conf, $emsg);
900 die "$emsg vm is running\n" if PVE
::QemuServer
::check_running
($vmid);
903 my $restore_options = {
908 live
=> $live_restore,
909 override_conf
=> $param,
911 if ($archive->{type
} eq 'file' || $archive->{type
} eq 'pipe') {
912 die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
914 PVE
::QemuServer
::restore_file_archive
($archive->{path
} // '-', $vmid, $authuser, $restore_options);
915 } elsif ($archive->{type
} eq 'pbs') {
916 PVE
::QemuServer
::restore_proxmox_backup_archive
($archive->{volid
}, $vmid, $authuser, $restore_options);
918 die "unknown backup archive type\n";
922 my $restored_conf = PVE
::QemuConfig-
>load_config($vmid);
923 # Convert restored VM to template if backup was VM template
924 if (PVE
::QemuConfig-
>is_template($restored_conf)) {
925 warn "Convert to template.\n";
926 eval { PVE
::QemuServer
::template_create
($vmid, $restored_conf) };
931 # ensure no old replication state are exists
932 PVE
::ReplicationState
::delete_guest_states
($vmid);
934 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
936 if ($start_after_create && !$live_restore) {
937 print "Execute autostart\n";
938 eval { PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node }) };
944 # ensure no old replication state are exists
945 PVE
::ReplicationState
::delete_guest_states
($vmid);
949 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
951 $conf->{meta
} = PVE
::QemuServer
::new_meta_info_string
();
955 ($vollist, my $created_opts) = $create_disks->(
966 $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
968 if (!$conf->{boot
}) {
969 my $devs = PVE
::QemuServer
::get_default_bootdevices
($conf);
970 $conf->{boot
} = PVE
::QemuServer
::print_bootorder
($devs);
973 # auto generate uuid if user did not specify smbios1 option
974 if (!$conf->{smbios1
}) {
975 $conf->{smbios1
} = PVE
::QemuServer
::generate_smbios1_uuid
();
978 if ((!defined($conf->{vmgenid
}) || $conf->{vmgenid
} eq '1') && $arch ne 'aarch64') {
979 $conf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
982 my $machine = $conf->{machine
};
983 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
984 # always pin Windows' machine version on create, they get to easily confused
985 if (PVE
::QemuServer
::Helpers
::windows_version
($conf->{ostype
})) {
986 $conf->{machine
} = PVE
::QemuServer
::windows_get_pinned_machine_version
($machine);
990 PVE
::QemuConfig-
>write_config($vmid, $conf);
996 foreach my $volid (@$vollist) {
997 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
1003 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
1006 PVE
::QemuConfig-
>lock_config_full($vmid, 1, $realcmd);
1008 if ($start_after_create) {
1009 print "Execute autostart\n";
1010 eval { PVE
::API2
::Qemu-
>vm_start({vmid
=> $vmid, node
=> $node}) };
1015 my ($code, $worker_name);
1017 $worker_name = 'qmrestore';
1019 eval { $restorefn->() };
1021 eval { PVE
::QemuConfig-
>remove_lock($vmid, 'create') };
1023 if ($restored_data) {
1024 warn "error after data was restored, VM disks should be OK but config may "
1025 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
1027 warn "error before or during data restore, some or all disks were not "
1028 ."completely restored. VM $vmid state is NOT cleaned up.\n";
1034 $worker_name = 'qmcreate';
1036 eval { $createfn->() };
1039 my $conffile = PVE
::QemuConfig-
>config_file($vmid);
1040 unlink($conffile) or die "failed to remove config file: $!\n";
1048 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1051 __PACKAGE__-
>register_method({
1056 description
=> "Directory index",
1061 additionalProperties
=> 0,
1063 node
=> get_standard_option
('pve-node'),
1064 vmid
=> get_standard_option
('pve-vmid'),
1072 subdir
=> { type
=> 'string' },
1075 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1081 { subdir
=> 'config' },
1082 { subdir
=> 'cloudinit' },
1083 { subdir
=> 'pending' },
1084 { subdir
=> 'status' },
1085 { subdir
=> 'unlink' },
1086 { subdir
=> 'vncproxy' },
1087 { subdir
=> 'termproxy' },
1088 { subdir
=> 'migrate' },
1089 { subdir
=> 'resize' },
1090 { subdir
=> 'move' },
1091 { subdir
=> 'rrd' },
1092 { subdir
=> 'rrddata' },
1093 { subdir
=> 'monitor' },
1094 { subdir
=> 'agent' },
1095 { subdir
=> 'snapshot' },
1096 { subdir
=> 'spiceproxy' },
1097 { subdir
=> 'sendkey' },
1098 { subdir
=> 'firewall' },
1099 { subdir
=> 'mtunnel' },
1100 { subdir
=> 'remote_migrate' },
1106 __PACKAGE__-
>register_method ({
1107 subclass
=> "PVE::API2::Firewall::VM",
1108 path
=> '{vmid}/firewall',
1111 __PACKAGE__-
>register_method ({
1112 subclass
=> "PVE::API2::Qemu::Agent",
1113 path
=> '{vmid}/agent',
1116 __PACKAGE__-
>register_method({
1118 path
=> '{vmid}/rrd',
1120 protected
=> 1, # fixme: can we avoid that?
1122 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1124 description
=> "Read VM RRD statistics (returns PNG)",
1126 additionalProperties
=> 0,
1128 node
=> get_standard_option
('pve-node'),
1129 vmid
=> get_standard_option
('pve-vmid'),
1131 description
=> "Specify the time frame you are interested in.",
1133 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1136 description
=> "The list of datasources you want to display.",
1137 type
=> 'string', format
=> 'pve-configid-list',
1140 description
=> "The RRD consolidation function",
1142 enum
=> [ 'AVERAGE', 'MAX' ],
1150 filename
=> { type
=> 'string' },
1156 return PVE
::RRD
::create_rrd_graph
(
1157 "pve2-vm/$param->{vmid}", $param->{timeframe
},
1158 $param->{ds
}, $param->{cf
});
1162 __PACKAGE__-
>register_method({
1164 path
=> '{vmid}/rrddata',
1166 protected
=> 1, # fixme: can we avoid that?
1168 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1170 description
=> "Read VM RRD statistics",
1172 additionalProperties
=> 0,
1174 node
=> get_standard_option
('pve-node'),
1175 vmid
=> get_standard_option
('pve-vmid'),
1177 description
=> "Specify the time frame you are interested in.",
1179 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
1182 description
=> "The RRD consolidation function",
1184 enum
=> [ 'AVERAGE', 'MAX' ],
1199 return PVE
::RRD
::create_rrd_data
(
1200 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
1204 __PACKAGE__-
>register_method({
1205 name
=> 'vm_config',
1206 path
=> '{vmid}/config',
1209 description
=> "Get the virtual machine configuration with pending configuration " .
1210 "changes applied. Set the 'current' parameter to get the current configuration instead.",
1212 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1215 additionalProperties
=> 0,
1217 node
=> get_standard_option
('pve-node'),
1218 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1220 description
=> "Get current values (instead of pending values).",
1225 snapshot
=> get_standard_option
('pve-snapshot-name', {
1226 description
=> "Fetch config values from given snapshot.",
1229 my ($cmd, $pname, $cur, $args) = @_;
1230 PVE
::QemuConfig-
>snapshot_list($args->[0]);
1236 description
=> "The VM configuration.",
1238 properties
=> PVE
::QemuServer
::json_config_properties
({
1241 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1248 raise_param_exc
({ snapshot
=> "cannot use 'snapshot' parameter with 'current'",
1249 current
=> "cannot use 'snapshot' parameter with 'current'"})
1250 if ($param->{snapshot
} && $param->{current
});
1253 if ($param->{snapshot
}) {
1254 $conf = PVE
::QemuConfig-
>load_snapshot_config($param->{vmid
}, $param->{snapshot
});
1256 $conf = PVE
::QemuConfig-
>load_current_config($param->{vmid
}, $param->{current
});
1258 $conf->{cipassword
} = '**********' if $conf->{cipassword
};
1263 __PACKAGE__-
>register_method({
1264 name
=> 'vm_pending',
1265 path
=> '{vmid}/pending',
1268 description
=> "Get the virtual machine configuration with both current and pending values.",
1270 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1273 additionalProperties
=> 0,
1275 node
=> get_standard_option
('pve-node'),
1276 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1285 description
=> "Configuration option name.",
1289 description
=> "Current value.",
1294 description
=> "Pending value.",
1299 description
=> "Indicates a pending delete request if present and not 0. " .
1300 "The value 2 indicates a force-delete request.",
1312 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1314 my $pending_delete_hash = PVE
::QemuConfig-
>parse_pending_delete($conf->{pending
}->{delete});
1316 $conf->{cipassword
} = '**********' if defined($conf->{cipassword
});
1317 $conf->{pending
}->{cipassword
} = '********** ' if defined($conf->{pending
}->{cipassword
});
1319 return PVE
::GuestHelpers
::config_with_pending_array
($conf, $pending_delete_hash);
1322 __PACKAGE__-
>register_method({
1323 name
=> 'cloudinit_pending',
1324 path
=> '{vmid}/cloudinit',
1327 description
=> "Get the cloudinit configuration with both current and pending values.",
1329 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1332 additionalProperties
=> 0,
1334 node
=> get_standard_option
('pve-node'),
1335 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1344 description
=> "Configuration option name.",
1348 description
=> "Value as it was used to generate the current cloudinit image.",
1353 description
=> "The new pending value.",
1363 my $vmid = $param->{vmid
};
1364 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1366 my $ci = $conf->{cloudinit
};
1369 my $added = delete($ci->{added
}) // '';
1370 for my $key (PVE
::Tools
::split_list
($added)) {
1371 $res->{$key} = { new
=> $conf->{$key} };
1374 for my $key (keys %$ci) {
1375 if (!exists($conf->{$key})) {
1376 $res->{$key} = { old
=> $ci->{$key} };
1380 new
=> $conf->{$key},
1385 if (defined(my $pw = $res->{cipassword
})) {
1386 $pw->{old
} = '**********' if exists $pw->{old
};
1387 $pw->{new
} = '**********' if exists $pw->{new
};
1393 __PACKAGE__-
>register_method({
1394 name
=> 'cloudinit_update',
1395 path
=> '{vmid}/cloudinit',
1399 description
=> "Regenerate and change cloudinit config drive.",
1401 check
=> ['perm', '/vms/{vmid}', 'VM.Config.Cloudinit'],
1404 additionalProperties
=> 0,
1406 node
=> get_standard_option
('pve-node'),
1407 vmid
=> get_standard_option
('pve-vmid'),
1410 returns
=> { type
=> 'null' },
1414 my $rpcenv = PVE
::RPCEnvironment
::get
();
1415 my $authuser = $rpcenv->get_user();
1417 my $vmid = extract_param
($param, 'vmid');
1419 PVE
::QemuConfig-
>lock_config($vmid, sub {
1420 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1421 PVE
::QemuConfig-
>check_lock($conf);
1423 my $storecfg = PVE
::Storage
::config
();
1424 PVE
::QemuServer
::vmconfig_update_cloudinit_drive
($storecfg, $conf, $vmid);
1429 # POST/PUT {vmid}/config implementation
1431 # The original API used PUT (idempotent) an we assumed that all operations
1432 # are fast. But it turned out that almost any configuration change can
1433 # involve hot-plug actions, or disk alloc/free. Such actions can take long
1434 # time to complete and have side effects (not idempotent).
1436 # The new implementation uses POST and forks a worker process. We added
1437 # a new option 'background_delay'. If specified we wait up to
1438 # 'background_delay' second for the worker task to complete. It returns null
1439 # if the task is finished within that time, else we return the UPID.
1441 my $update_vm_api = sub {
1442 my ($param, $sync) = @_;
1444 my $rpcenv = PVE
::RPCEnvironment
::get
();
1446 my $authuser = $rpcenv->get_user();
1448 my $node = extract_param
($param, 'node');
1450 my $vmid = extract_param
($param, 'vmid');
1452 my $digest = extract_param
($param, 'digest');
1454 my $background_delay = extract_param
($param, 'background_delay');
1456 my $skip_cloud_init = extract_param
($param, 'skip_cloud_init');
1458 if (defined(my $cipassword = $param->{cipassword
})) {
1459 # Same logic as in cloud-init (but with the regex fixed...)
1460 $param->{cipassword
} = PVE
::Tools
::encrypt_pw
($cipassword)
1461 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1464 my @paramarr = (); # used for log message
1465 foreach my $key (sort keys %$param) {
1466 my $value = $key eq 'cipassword' ?
'<hidden>' : $param->{$key};
1467 push @paramarr, "-$key", $value;
1470 my $skiplock = extract_param
($param, 'skiplock');
1471 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1472 if $skiplock && $authuser ne 'root@pam';
1474 my $delete_str = extract_param
($param, 'delete');
1476 my $revert_str = extract_param
($param, 'revert');
1478 my $force = extract_param
($param, 'force');
1480 if (defined(my $ssh_keys = $param->{sshkeys
})) {
1481 $ssh_keys = URI
::Escape
::uri_unescape
($ssh_keys);
1482 PVE
::Tools
::validate_ssh_public_keys
($ssh_keys);
1485 $param->{cpuunits
} = PVE
::CGroup
::clamp_cpu_shares
($param->{cpuunits
})
1486 if defined($param->{cpuunits
}); # clamp value depending on cgroup version
1488 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
1490 my $storecfg = PVE
::Storage
::config
();
1492 my $defaults = PVE
::QemuServer
::load_defaults
();
1494 &$resolve_cdrom_alias($param);
1496 # now try to verify all parameters
1499 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
1500 if (!PVE
::QemuServer
::option_exists
($opt)) {
1501 raise_param_exc
({ revert
=> "unknown option '$opt'" });
1504 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1505 "-revert $opt' at the same time" })
1506 if defined($param->{$opt});
1508 $revert->{$opt} = 1;
1512 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
1513 $opt = 'ide2' if $opt eq 'cdrom';
1515 raise_param_exc
({ delete => "you can't use '-$opt' and " .
1516 "-delete $opt' at the same time" })
1517 if defined($param->{$opt});
1519 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
1520 "-revert $opt' at the same time" })
1523 if (!PVE
::QemuServer
::option_exists
($opt)) {
1524 raise_param_exc
({ delete => "unknown option '$opt'" });
1530 my $repl_conf = PVE
::ReplicationConfig-
>new();
1531 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1532 my $check_replication = sub {
1534 return if !$is_replicated;
1535 my $volid = $drive->{file
};
1536 return if !$volid || !($drive->{replicate
}//1);
1537 return if PVE
::QemuServer
::drive_is_cdrom
($drive);
1539 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
1540 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1541 if !defined($storeid);
1543 return if defined($volname) && $volname eq 'cloudinit';
1546 if ($volid =~ $NEW_DISK_RE) {
1548 $format = $drive->{format
} || PVE
::Storage
::storage_default_format
($storecfg, $storeid);
1550 $format = (PVE
::Storage
::parse_volname
($storecfg, $volid))[6];
1552 return if PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
1553 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
1554 return if $scfg->{shared
};
1555 die "cannot add non-replicatable volume to a replicated VM\n";
1558 $check_drive_param->($param, $storecfg, $check_replication);
1560 foreach my $opt (keys %$param) {
1561 if ($opt =~ m/^net(\d+)$/) {
1563 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
1564 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
1565 } elsif ($opt eq 'vmgenid') {
1566 if ($param->{$opt} eq '1') {
1567 $param->{$opt} = PVE
::QemuServer
::generate_uuid
();
1569 } elsif ($opt eq 'hookscript') {
1570 eval { PVE
::GuestHelpers
::check_hookscript
($param->{$opt}, $storecfg); };
1571 raise_param_exc
({ $opt => $@ }) if $@;
1575 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1577 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1579 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1581 my $updatefn = sub {
1583 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1585 die "checksum missmatch (file change by other user?)\n"
1586 if $digest && $digest ne $conf->{digest
};
1588 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1590 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1591 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1592 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1593 delete $conf->{lock}; # for check lock check, not written out
1594 push @delete, 'lock'; # this is the real deal to write it out
1596 push @delete, 'runningmachine' if $conf->{runningmachine
};
1597 push @delete, 'runningcpu' if $conf->{runningcpu
};
1600 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
1602 foreach my $opt (keys %$revert) {
1603 if (defined($conf->{$opt})) {
1604 $param->{$opt} = $conf->{$opt};
1605 } elsif (defined($conf->{pending
}->{$opt})) {
1610 if ($param->{memory
} || defined($param->{balloon
})) {
1611 my $maxmem = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
} || $defaults->{memory
};
1612 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
1614 die "balloon value too large (must be smaller than assigned memory)\n"
1615 if $balloon && $balloon > $maxmem;
1618 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1622 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1624 # write updates to pending section
1626 my $modified = {}; # record what $option we modify
1629 if (my $boot = $conf->{boot
}) {
1630 my $bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $boot);
1631 @bootorder = PVE
::Tools
::split_list
($bootcfg->{order
}) if $bootcfg && $bootcfg->{order
};
1633 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1635 my $check_drive_perms = sub {
1636 my ($opt, $val) = @_;
1637 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val, 1);
1638 if (PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
1639 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Cloudinit', 'VM.Config.CDROM']);
1640 } elsif (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) { # CDROM
1641 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1643 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1648 foreach my $opt (@delete) {
1649 $modified->{$opt} = 1;
1650 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1652 # value of what we want to delete, independent if pending or not
1653 my $val = $conf->{$opt} // $conf->{pending
}->{$opt};
1654 if (!defined($val)) {
1655 warn "cannot delete '$opt' - not set in current configuration!\n";
1656 $modified->{$opt} = 0;
1659 my $is_pending_val = defined($conf->{pending
}->{$opt});
1660 delete $conf->{pending
}->{$opt};
1662 # remove from bootorder if necessary
1663 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1664 @bootorder = grep {$_ ne $opt} @bootorder;
1665 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1666 $modified->{boot
} = 1;
1669 if ($opt =~ m/^unused/) {
1670 my $drive = PVE
::QemuServer
::parse_drive
($opt, $val);
1671 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1672 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1673 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1674 delete $conf->{$opt};
1675 PVE
::QemuConfig-
>write_config($vmid, $conf);
1677 } elsif ($opt eq 'vmstate') {
1678 PVE
::QemuConfig-
>check_protection($conf, "can't remove vmstate '$val'");
1679 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, { file
=> $val }, $rpcenv, $authuser, 1)) {
1680 delete $conf->{$opt};
1681 PVE
::QemuConfig-
>write_config($vmid, $conf);
1683 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1684 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1685 $check_drive_perms->($opt, $val);
1686 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $val))
1688 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1689 PVE
::QemuConfig-
>write_config($vmid, $conf);
1690 } elsif ($opt =~ m/^serial\d+$/) {
1691 if ($val eq 'socket') {
1692 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1693 } elsif ($authuser ne 'root@pam') {
1694 die "only root can delete '$opt' config for real devices\n";
1696 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1697 PVE
::QemuConfig-
>write_config($vmid, $conf);
1698 } elsif ($opt =~ m/^usb\d+$/) {
1699 if ($val =~ m/spice/) {
1700 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1701 } elsif ($authuser ne 'root@pam') {
1702 die "only root can delete '$opt' config for real devices\n";
1704 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1705 PVE
::QemuConfig-
>write_config($vmid, $conf);
1706 } elsif ($opt eq 'tags') {
1707 assert_tag_permissions
($vmid, $val, '', $rpcenv, $authuser);
1708 delete $conf->{$opt};
1709 PVE
::QemuConfig-
>write_config($vmid, $conf);
1711 PVE
::QemuConfig-
>add_to_pending_delete($conf, $opt, $force);
1712 PVE
::QemuConfig-
>write_config($vmid, $conf);
1716 foreach my $opt (keys %$param) { # add/change
1717 $modified->{$opt} = 1;
1718 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1719 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1721 my $arch = PVE
::QemuServer
::get_vm_arch
($conf);
1723 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1725 if ($conf->{$opt}) {
1726 $check_drive_perms->($opt, $conf->{$opt});
1730 $check_drive_perms->($opt, $param->{$opt});
1731 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1732 if defined($conf->{pending
}->{$opt});
1734 my (undef, $created_opts) = $create_disks->(
1742 {$opt => $param->{$opt}},
1744 $conf->{pending
}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1746 # default legacy boot order implies all cdroms anyway
1748 # append new CD drives to bootorder to mark them bootable
1749 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt}, 1);
1750 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1751 push @bootorder, $opt;
1752 $conf->{pending
}->{boot
} = PVE
::QemuServer
::print_bootorder
(\
@bootorder);
1753 $modified->{boot
} = 1;
1756 } elsif ($opt =~ m/^serial\d+/) {
1757 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1758 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1759 } elsif ($authuser ne 'root@pam') {
1760 die "only root can modify '$opt' config for real devices\n";
1762 $conf->{pending
}->{$opt} = $param->{$opt};
1763 } elsif ($opt =~ m/^usb\d+/) {
1764 if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
1765 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1766 } elsif ($authuser ne 'root@pam') {
1767 die "only root can modify '$opt' config for real devices\n";
1769 $conf->{pending
}->{$opt} = $param->{$opt};
1770 } elsif ($opt eq 'tags') {
1771 assert_tag_permissions
($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
1772 $conf->{pending
}->{$opt} = PVE
::GuestHelpers
::get_unique_tags
($param->{$opt});
1774 $conf->{pending
}->{$opt} = $param->{$opt};
1776 if ($opt eq 'boot') {
1777 my $new_bootcfg = PVE
::JSONSchema
::parse_property_string
('pve-qm-boot', $param->{$opt});
1778 if ($new_bootcfg->{order
}) {
1779 my @devs = PVE
::Tools
::split_list
($new_bootcfg->{order
});
1780 for my $dev (@devs) {
1781 my $exists = $conf->{$dev} || $conf->{pending
}->{$dev} || $param->{$dev};
1782 my $deleted = grep {$_ eq $dev} @delete;
1783 die "invalid bootorder: device '$dev' does not exist'\n"
1784 if !$exists || $deleted;
1787 # remove legacy boot order settings if new one set
1788 $conf->{pending
}->{$opt} = PVE
::QemuServer
::print_bootorder
(\
@devs);
1789 PVE
::QemuConfig-
>add_to_pending_delete($conf, "bootdisk")
1790 if $conf->{bootdisk
};
1794 PVE
::QemuConfig-
>remove_from_pending_delete($conf, $opt);
1795 PVE
::QemuConfig-
>write_config($vmid, $conf);
1798 # remove pending changes when nothing changed
1799 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1800 my $changes = PVE
::QemuConfig-
>cleanup_pending($conf);
1801 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
1803 return if !scalar(keys %{$conf->{pending
}});
1805 my $running = PVE
::QemuServer
::check_running
($vmid);
1807 # apply pending changes
1809 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1813 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
1815 # cloud_init must be skipped if we are in an incoming, remote live migration
1816 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $errors, $skip_cloud_init);
1818 raise_param_exc
($errors) if scalar(keys %$errors);
1827 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1829 if ($background_delay) {
1831 # Note: It would be better to do that in the Event based HTTPServer
1832 # to avoid blocking call to sleep.
1834 my $end_time = time() + $background_delay;
1836 my $task = PVE
::Tools
::upid_decode
($upid);
1839 while (time() < $end_time) {
1840 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
1842 sleep(1); # this gets interrupted when child process ends
1846 my $status = PVE
::Tools
::upid_read_status
($upid);
1847 return if !PVE
::Tools
::upid_status_is_error
($status);
1848 die "failed to update VM $vmid: $status\n";
1856 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
1859 my $vm_config_perm_list = [
1864 'VM.Config.Network',
1866 'VM.Config.Options',
1867 'VM.Config.Cloudinit',
1870 __PACKAGE__-
>register_method({
1871 name
=> 'update_vm_async',
1872 path
=> '{vmid}/config',
1876 description
=> "Set virtual machine options (asynchrounous API).",
1878 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1881 additionalProperties
=> 0,
1882 properties
=> PVE
::QemuServer
::json_config_properties
(
1884 node
=> get_standard_option
('pve-node'),
1885 vmid
=> get_standard_option
('pve-vmid'),
1886 skiplock
=> get_standard_option
('skiplock'),
1888 type
=> 'string', format
=> 'pve-configid-list',
1889 description
=> "A list of settings you want to delete.",
1893 type
=> 'string', format
=> 'pve-configid-list',
1894 description
=> "Revert a pending change.",
1899 description
=> $opt_force_description,
1901 requires
=> 'delete',
1905 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1909 background_delay
=> {
1911 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1917 1, # with_disk_alloc
1924 code
=> $update_vm_api,
1927 __PACKAGE__-
>register_method({
1928 name
=> 'update_vm',
1929 path
=> '{vmid}/config',
1933 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1935 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1938 additionalProperties
=> 0,
1939 properties
=> PVE
::QemuServer
::json_config_properties
(
1941 node
=> get_standard_option
('pve-node'),
1942 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1943 skiplock
=> get_standard_option
('skiplock'),
1945 type
=> 'string', format
=> 'pve-configid-list',
1946 description
=> "A list of settings you want to delete.",
1950 type
=> 'string', format
=> 'pve-configid-list',
1951 description
=> "Revert a pending change.",
1956 description
=> $opt_force_description,
1958 requires
=> 'delete',
1962 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1967 1, # with_disk_alloc
1970 returns
=> { type
=> 'null' },
1973 &$update_vm_api($param, 1);
1978 __PACKAGE__-
>register_method({
1979 name
=> 'destroy_vm',
1984 description
=> "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
1985 ." and firewall rules",
1987 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1990 additionalProperties
=> 0,
1992 node
=> get_standard_option
('pve-node'),
1993 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
1994 skiplock
=> get_standard_option
('skiplock'),
1997 description
=> "Remove VMID from configurations, like backup & replication jobs and HA.",
2000 'destroy-unreferenced-disks' => {
2002 description
=> "If set, destroy additionally all disks not referenced in the config"
2003 ." but with a matching VMID from all enabled storages.",
2015 my $rpcenv = PVE
::RPCEnvironment
::get
();
2016 my $authuser = $rpcenv->get_user();
2017 my $vmid = $param->{vmid
};
2019 my $skiplock = $param->{skiplock
};
2020 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2021 if $skiplock && $authuser ne 'root@pam';
2023 my $early_checks = sub {
2025 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2026 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
2028 my $ha_managed = PVE
::HA
::Config
::service_is_configured
("vm:$vmid");
2030 if (!$param->{purge
}) {
2031 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
2033 # don't allow destroy if with replication jobs but no purge param
2034 my $repl_conf = PVE
::ReplicationConfig-
>new();
2035 $repl_conf->check_for_existing_jobs($vmid);
2038 die "VM $vmid is running - destroy failed\n"
2039 if PVE
::QemuServer
::check_running
($vmid);
2049 my $storecfg = PVE
::Storage
::config
();
2051 syslog
('info', "destroy VM $vmid: $upid\n");
2052 PVE
::QemuConfig-
>lock_config($vmid, sub {
2053 # repeat, config might have changed
2054 my $ha_managed = $early_checks->();
2056 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
2058 PVE
::QemuServer
::destroy_vm
(
2061 $skiplock, { lock => 'destroyed' },
2062 $purge_unreferenced,
2065 PVE
::AccessControl
::remove_vm_access
($vmid);
2066 PVE
::Firewall
::remove_vmfw_conf
($vmid);
2067 if ($param->{purge
}) {
2068 print "purging VM $vmid from related configurations..\n";
2069 PVE
::ReplicationConfig
::remove_vmid_jobs
($vmid);
2070 PVE
::VZDump
::Plugin
::remove_vmid_from_backup_jobs
($vmid);
2073 PVE
::HA
::Config
::delete_service_from_config
("vm:$vmid");
2074 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2078 # only now remove the zombie config, else we can have reuse race
2079 PVE
::QemuConfig-
>destroy_config($vmid);
2083 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
2086 __PACKAGE__-
>register_method({
2088 path
=> '{vmid}/unlink',
2092 description
=> "Unlink/delete disk images.",
2094 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2097 additionalProperties
=> 0,
2099 node
=> get_standard_option
('pve-node'),
2100 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2102 type
=> 'string', format
=> 'pve-configid-list',
2103 description
=> "A list of disk IDs you want to delete.",
2107 description
=> $opt_force_description,
2112 returns
=> { type
=> 'null'},
2116 $param->{delete} = extract_param
($param, 'idlist');
2118 __PACKAGE__-
>update_vm($param);
2123 # uses good entropy, each char is limited to 6 bit to get printable chars simply
2124 my $gen_rand_chars = sub {
2127 die "invalid length $length" if $length < 1;
2129 my $min = ord('!'); # first printable ascii
2131 my $rand_bytes = Crypt
::OpenSSL
::Random
::random_bytes
($length);
2132 die "failed to generate random bytes!\n"
2135 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
2142 __PACKAGE__-
>register_method({
2144 path
=> '{vmid}/vncproxy',
2148 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2150 description
=> "Creates a TCP VNC proxy connections.",
2152 additionalProperties
=> 0,
2154 node
=> get_standard_option
('pve-node'),
2155 vmid
=> get_standard_option
('pve-vmid'),
2159 description
=> "starts websockify instead of vncproxy",
2161 'generate-password' => {
2165 description
=> "Generates a random password to be used as ticket instead of the API ticket.",
2170 additionalProperties
=> 0,
2172 user
=> { type
=> 'string' },
2173 ticket
=> { type
=> 'string' },
2176 description
=> "Returned if requested with 'generate-password' param."
2177 ." Consists of printable ASCII characters ('!' .. '~').",
2180 cert
=> { type
=> 'string' },
2181 port
=> { type
=> 'integer' },
2182 upid
=> { type
=> 'string' },
2188 my $rpcenv = PVE
::RPCEnvironment
::get
();
2190 my $authuser = $rpcenv->get_user();
2192 my $vmid = $param->{vmid
};
2193 my $node = $param->{node
};
2194 my $websocket = $param->{websocket
};
2196 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2200 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2201 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2204 my $authpath = "/vms/$vmid";
2206 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2207 my $password = $ticket;
2208 if ($param->{'generate-password'}) {
2209 $password = $gen_rand_chars->(8);
2212 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
2218 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2219 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2220 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2221 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
2222 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, defined($serial) ?
'-t' : '-T');
2224 $family = PVE
::Tools
::get_host_address_family
($node);
2227 my $port = PVE
::Tools
::next_vnc_port
($family);
2234 syslog
('info', "starting vnc proxy $upid\n");
2238 if (defined($serial)) {
2240 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
2242 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
2243 '-timeout', $timeout, '-authpath', $authpath,
2244 '-perm', 'Sys.Console'];
2246 if ($param->{websocket
}) {
2247 $ENV{PVE_VNC_TICKET
} = $password; # pass ticket to vncterm
2248 push @$cmd, '-notls', '-listen', 'localhost';
2251 push @$cmd, '-c', @$remcmd, @$termcmd;
2253 PVE
::Tools
::run_command
($cmd);
2257 $ENV{LC_PVE_TICKET
} = $password if $websocket; # set ticket with "qm vncproxy"
2259 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2261 my $sock = IO
::Socket
::IP-
>new(
2266 GetAddrInfoFlags
=> 0,
2267 ) or die "failed to create socket: $!\n";
2268 # Inside the worker we shouldn't have any previous alarms
2269 # running anyway...:
2271 local $SIG{ALRM
} = sub { die "connection timed out\n" };
2273 accept(my $cli, $sock) or die "connection failed: $!\n";
2276 if (PVE
::Tools
::run_command
($cmd,
2277 output
=> '>&'.fileno($cli),
2278 input
=> '<&'.fileno($cli),
2281 die "Failed to run vncproxy.\n";
2288 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2290 PVE
::Tools
::wait_for_vnc_port
($port);
2299 $res->{password
} = $password if $param->{'generate-password'};
2304 __PACKAGE__-
>register_method({
2305 name
=> 'termproxy',
2306 path
=> '{vmid}/termproxy',
2310 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2312 description
=> "Creates a TCP proxy connections.",
2314 additionalProperties
=> 0,
2316 node
=> get_standard_option
('pve-node'),
2317 vmid
=> get_standard_option
('pve-vmid'),
2321 enum
=> [qw(serial0 serial1 serial2 serial3)],
2322 description
=> "opens a serial terminal (defaults to display)",
2327 additionalProperties
=> 0,
2329 user
=> { type
=> 'string' },
2330 ticket
=> { type
=> 'string' },
2331 port
=> { type
=> 'integer' },
2332 upid
=> { type
=> 'string' },
2338 my $rpcenv = PVE
::RPCEnvironment
::get
();
2340 my $authuser = $rpcenv->get_user();
2342 my $vmid = $param->{vmid
};
2343 my $node = $param->{node
};
2344 my $serial = $param->{serial
};
2346 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
2348 if (!defined($serial)) {
2350 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2351 $serial = $vga->{type
} if $vga->{type
} =~ m/^serial\d+$/;
2355 my $authpath = "/vms/$vmid";
2357 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
2362 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
2363 (undef, $family) = PVE
::Cluster
::remote_node_ip
($node);
2364 my $sshinfo = PVE
::SSHInfo
::get_ssh_info
($node);
2365 $remcmd = PVE
::SSHInfo
::ssh_info_to_command
($sshinfo, '-t');
2366 push @$remcmd, '--';
2368 $family = PVE
::Tools
::get_host_address_family
($node);
2371 my $port = PVE
::Tools
::next_vnc_port
($family);
2373 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
2374 push @$termcmd, '-iface', $serial if $serial;
2379 syslog
('info', "starting qemu termproxy $upid\n");
2381 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2382 '--perm', 'VM.Console', '--'];
2383 push @$cmd, @$remcmd, @$termcmd;
2385 PVE
::Tools
::run_command
($cmd);
2388 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2390 PVE
::Tools
::wait_for_vnc_port
($port);
2400 __PACKAGE__-
>register_method({
2401 name
=> 'vncwebsocket',
2402 path
=> '{vmid}/vncwebsocket',
2405 description
=> "You also need to pass a valid ticket (vncticket).",
2406 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2408 description
=> "Opens a weksocket for VNC traffic.",
2410 additionalProperties
=> 0,
2412 node
=> get_standard_option
('pve-node'),
2413 vmid
=> get_standard_option
('pve-vmid'),
2415 description
=> "Ticket from previous call to vncproxy.",
2420 description
=> "Port number returned by previous vncproxy call.",
2430 port
=> { type
=> 'string' },
2436 my $rpcenv = PVE
::RPCEnvironment
::get
();
2438 my $authuser = $rpcenv->get_user();
2440 my $vmid = $param->{vmid
};
2441 my $node = $param->{node
};
2443 my $authpath = "/vms/$vmid";
2445 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
2447 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
2449 # Note: VNC ports are acessible from outside, so we do not gain any
2450 # security if we verify that $param->{port} belongs to VM $vmid. This
2451 # check is done by verifying the VNC ticket (inside VNC protocol).
2453 my $port = $param->{port
};
2455 return { port
=> $port };
2458 __PACKAGE__-
>register_method({
2459 name
=> 'spiceproxy',
2460 path
=> '{vmid}/spiceproxy',
2465 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2467 description
=> "Returns a SPICE configuration to connect to the VM.",
2469 additionalProperties
=> 0,
2471 node
=> get_standard_option
('pve-node'),
2472 vmid
=> get_standard_option
('pve-vmid'),
2473 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
2476 returns
=> get_standard_option
('remote-viewer-config'),
2480 my $rpcenv = PVE
::RPCEnvironment
::get
();
2482 my $authuser = $rpcenv->get_user();
2484 my $vmid = $param->{vmid
};
2485 my $node = $param->{node
};
2486 my $proxy = $param->{proxy
};
2488 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
2489 my $title = "VM $vmid";
2490 $title .= " - ". $conf->{name
} if $conf->{name
};
2492 my $port = PVE
::QemuServer
::spice_port
($vmid);
2494 my ($ticket, undef, $remote_viewer_config) =
2495 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
2497 mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
2498 mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
2500 return $remote_viewer_config;
2503 __PACKAGE__-
>register_method({
2505 path
=> '{vmid}/status',
2508 description
=> "Directory index",
2513 additionalProperties
=> 0,
2515 node
=> get_standard_option
('pve-node'),
2516 vmid
=> get_standard_option
('pve-vmid'),
2524 subdir
=> { type
=> 'string' },
2527 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
2533 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2536 { subdir
=> 'current' },
2537 { subdir
=> 'start' },
2538 { subdir
=> 'stop' },
2539 { subdir
=> 'reset' },
2540 { subdir
=> 'shutdown' },
2541 { subdir
=> 'suspend' },
2542 { subdir
=> 'reboot' },
2548 __PACKAGE__-
>register_method({
2549 name
=> 'vm_status',
2550 path
=> '{vmid}/status/current',
2553 protected
=> 1, # qemu pid files are only readable by root
2554 description
=> "Get virtual machine status.",
2556 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2559 additionalProperties
=> 0,
2561 node
=> get_standard_option
('pve-node'),
2562 vmid
=> get_standard_option
('pve-vmid'),
2568 %$PVE::QemuServer
::vmstatus_return_properties
,
2570 description
=> "HA manager service status.",
2574 description
=> "Qemu VGA configuration supports spice.",
2579 description
=> "Qemu GuestAgent enabled in config.",
2589 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
2591 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
2592 my $status = $vmstatus->{$param->{vmid
}};
2594 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
2597 my $vga = PVE
::QemuServer
::parse_vga
($conf->{vga
});
2598 my $spice = defined($vga->{type
}) && $vga->{type
} =~ /^virtio/;
2599 $spice ||= PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
2600 $status->{spice
} = 1 if $spice;
2602 $status->{agent
} = 1 if PVE
::QemuServer
::get_qga_key
($conf, 'enabled');
2607 __PACKAGE__-
>register_method({
2609 path
=> '{vmid}/status/start',
2613 description
=> "Start virtual machine.",
2615 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2618 additionalProperties
=> 0,
2620 node
=> get_standard_option
('pve-node'),
2621 vmid
=> get_standard_option
('pve-vmid',
2622 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
2623 skiplock
=> get_standard_option
('skiplock'),
2624 stateuri
=> get_standard_option
('pve-qm-stateuri'),
2625 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
2628 enum
=> ['secure', 'insecure'],
2629 description
=> "Migration traffic is encrypted using an SSH " .
2630 "tunnel by default. On secure, completely private networks " .
2631 "this can be disabled to increase performance.",
2634 migration_network
=> {
2635 type
=> 'string', format
=> 'CIDR',
2636 description
=> "CIDR of the (sub) network that is used for migration.",
2639 machine
=> get_standard_option
('pve-qemu-machine'),
2641 description
=> "Override QEMU's -cpu argument with the given string.",
2645 targetstorage
=> get_standard_option
('pve-targetstorage'),
2647 description
=> "Wait maximal timeout seconds.",
2650 default => 'max(30, vm memory in GiB)',
2661 my $rpcenv = PVE
::RPCEnvironment
::get
();
2662 my $authuser = $rpcenv->get_user();
2664 my $node = extract_param
($param, 'node');
2665 my $vmid = extract_param
($param, 'vmid');
2666 my $timeout = extract_param
($param, 'timeout');
2667 my $machine = extract_param
($param, 'machine');
2669 my $get_root_param = sub {
2670 my $value = extract_param
($param, $_[0]);
2671 raise_param_exc
({ "$_[0]" => "Only root may use this option." })
2672 if $value && $authuser ne 'root@pam';
2676 my $stateuri = $get_root_param->('stateuri');
2677 my $skiplock = $get_root_param->('skiplock');
2678 my $migratedfrom = $get_root_param->('migratedfrom');
2679 my $migration_type = $get_root_param->('migration_type');
2680 my $migration_network = $get_root_param->('migration_network');
2681 my $targetstorage = $get_root_param->('targetstorage');
2682 my $force_cpu = $get_root_param->('force-cpu');
2686 if ($targetstorage) {
2687 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
2689 $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
2690 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
2694 # read spice ticket from STDIN
2696 my $nbd_protocol_version = 0;
2697 my $replicated_volumes = {};
2698 my $offline_volumes = {};
2699 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
2700 while (defined(my $line = <STDIN
>)) {
2702 if ($line =~ m/^spice_ticket: (.+)$/) {
2704 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2705 $nbd_protocol_version = $1;
2706 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2707 $replicated_volumes->{$1} = 1;
2708 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2709 $offline_volumes->{tpmstate0
} = $1;
2710 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2711 $offline_volumes->{$1} = $2;
2712 } elsif (!$spice_ticket) {
2713 # fallback for old source node
2714 $spice_ticket = $line;
2716 warn "unknown 'start' parameter on STDIN: '$line'\n";
2721 PVE
::Cluster
::check_cfs_quorum
();
2723 my $storecfg = PVE
::Storage
::config
();
2725 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri && $rpcenv->{type
} ne 'ha') {
2729 print "Requesting HA start for VM $vmid\n";
2731 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2732 PVE
::Tools
::run_command
($cmd);
2736 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2743 syslog
('info', "start VM $vmid: $upid\n");
2745 my $migrate_opts = {
2746 migratedfrom
=> $migratedfrom,
2747 spice_ticket
=> $spice_ticket,
2748 network
=> $migration_network,
2749 type
=> $migration_type,
2750 storagemap
=> $storagemap,
2751 nbd_proto_version
=> $nbd_protocol_version,
2752 replicated_volumes
=> $replicated_volumes,
2753 offline_volumes
=> $offline_volumes,
2757 statefile
=> $stateuri,
2758 skiplock
=> $skiplock,
2759 forcemachine
=> $machine,
2760 timeout
=> $timeout,
2761 forcecpu
=> $force_cpu,
2764 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $params, $migrate_opts);
2768 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2772 __PACKAGE__-
>register_method({
2774 path
=> '{vmid}/status/stop',
2778 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
2779 "is akin to pulling the power plug of a running computer and may damage the VM data",
2781 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2784 additionalProperties
=> 0,
2786 node
=> get_standard_option
('pve-node'),
2787 vmid
=> get_standard_option
('pve-vmid',
2788 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2789 skiplock
=> get_standard_option
('skiplock'),
2790 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
2792 description
=> "Wait maximal timeout seconds.",
2798 description
=> "Do not deactivate storage volumes.",
2811 my $rpcenv = PVE
::RPCEnvironment
::get
();
2812 my $authuser = $rpcenv->get_user();
2814 my $node = extract_param
($param, 'node');
2815 my $vmid = extract_param
($param, 'vmid');
2817 my $skiplock = extract_param
($param, 'skiplock');
2818 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2819 if $skiplock && $authuser ne 'root@pam';
2821 my $keepActive = extract_param
($param, 'keepActive');
2822 raise_param_exc
({ keepActive
=> "Only root may use this option." })
2823 if $keepActive && $authuser ne 'root@pam';
2825 my $migratedfrom = extract_param
($param, 'migratedfrom');
2826 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
2827 if $migratedfrom && $authuser ne 'root@pam';
2830 my $storecfg = PVE
::Storage
::config
();
2832 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
2837 print "Requesting HA stop for VM $vmid\n";
2839 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
2840 PVE
::Tools
::run_command
($cmd);
2844 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2850 syslog
('info', "stop VM $vmid: $upid\n");
2852 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
2853 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
2857 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2861 __PACKAGE__-
>register_method({
2863 path
=> '{vmid}/status/reset',
2867 description
=> "Reset virtual machine.",
2869 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2872 additionalProperties
=> 0,
2874 node
=> get_standard_option
('pve-node'),
2875 vmid
=> get_standard_option
('pve-vmid',
2876 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2877 skiplock
=> get_standard_option
('skiplock'),
2886 my $rpcenv = PVE
::RPCEnvironment
::get
();
2888 my $authuser = $rpcenv->get_user();
2890 my $node = extract_param
($param, 'node');
2892 my $vmid = extract_param
($param, 'vmid');
2894 my $skiplock = extract_param
($param, 'skiplock');
2895 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2896 if $skiplock && $authuser ne 'root@pam';
2898 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
2903 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
2908 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
2911 __PACKAGE__-
>register_method({
2912 name
=> 'vm_shutdown',
2913 path
=> '{vmid}/status/shutdown',
2917 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
2918 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
2920 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2923 additionalProperties
=> 0,
2925 node
=> get_standard_option
('pve-node'),
2926 vmid
=> get_standard_option
('pve-vmid',
2927 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2928 skiplock
=> get_standard_option
('skiplock'),
2930 description
=> "Wait maximal timeout seconds.",
2936 description
=> "Make sure the VM stops.",
2942 description
=> "Do not deactivate storage volumes.",
2955 my $rpcenv = PVE
::RPCEnvironment
::get
();
2956 my $authuser = $rpcenv->get_user();
2958 my $node = extract_param
($param, 'node');
2959 my $vmid = extract_param
($param, 'vmid');
2961 my $skiplock = extract_param
($param, 'skiplock');
2962 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2963 if $skiplock && $authuser ne 'root@pam';
2965 my $keepActive = extract_param
($param, 'keepActive');
2966 raise_param_exc
({ keepActive
=> "Only root may use this option." })
2967 if $keepActive && $authuser ne 'root@pam';
2969 my $storecfg = PVE
::Storage
::config
();
2973 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2974 # otherwise, we will infer a shutdown command, but run into the timeout,
2975 # then when the vm is resumed, it will instantly shutdown
2977 # checking the qmp status here to get feedback to the gui/cli/api
2978 # and the status query should not take too long
2979 if (PVE
::QemuServer
::vm_is_paused
($vmid)) {
2980 if ($param->{forceStop
}) {
2981 warn "VM is paused - stop instead of shutdown\n";
2984 die "VM is paused - cannot shutdown\n";
2988 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
2990 my $timeout = $param->{timeout
} // 60;
2994 print "Requesting HA stop for VM $vmid\n";
2996 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
2997 PVE
::Tools
::run_command
($cmd);
3001 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3008 syslog
('info', "shutdown VM $vmid: $upid\n");
3010 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
3011 $shutdown, $param->{forceStop
}, $keepActive);
3015 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3019 __PACKAGE__-
>register_method({
3020 name
=> 'vm_reboot',
3021 path
=> '{vmid}/status/reboot',
3025 description
=> "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3027 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3030 additionalProperties
=> 0,
3032 node
=> get_standard_option
('pve-node'),
3033 vmid
=> get_standard_option
('pve-vmid',
3034 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3036 description
=> "Wait maximal timeout seconds for the shutdown.",
3049 my $rpcenv = PVE
::RPCEnvironment
::get
();
3050 my $authuser = $rpcenv->get_user();
3052 my $node = extract_param
($param, 'node');
3053 my $vmid = extract_param
($param, 'vmid');
3055 die "VM is paused - cannot shutdown\n" if PVE
::QemuServer
::vm_is_paused
($vmid);
3057 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3062 syslog
('info', "requesting reboot of VM $vmid: $upid\n");
3063 PVE
::QemuServer
::vm_reboot
($vmid, $param->{timeout
});
3067 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3070 __PACKAGE__-
>register_method({
3071 name
=> 'vm_suspend',
3072 path
=> '{vmid}/status/suspend',
3076 description
=> "Suspend virtual machine.",
3078 description
=> "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3079 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3080 " on the storage for the vmstate.",
3081 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3084 additionalProperties
=> 0,
3086 node
=> get_standard_option
('pve-node'),
3087 vmid
=> get_standard_option
('pve-vmid',
3088 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3089 skiplock
=> get_standard_option
('skiplock'),
3094 description
=> 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3096 statestorage
=> get_standard_option
('pve-storage-id', {
3097 description
=> "The storage for the VM state",
3098 requires
=> 'todisk',
3100 completion
=> \
&PVE
::Storage
::complete_storage_enabled
,
3110 my $rpcenv = PVE
::RPCEnvironment
::get
();
3111 my $authuser = $rpcenv->get_user();
3113 my $node = extract_param
($param, 'node');
3114 my $vmid = extract_param
($param, 'vmid');
3116 my $todisk = extract_param
($param, 'todisk') // 0;
3118 my $statestorage = extract_param
($param, 'statestorage');
3120 my $skiplock = extract_param
($param, 'skiplock');
3121 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3122 if $skiplock && $authuser ne 'root@pam';
3124 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
3126 die "Cannot suspend HA managed VM to disk\n"
3127 if $todisk && PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
3129 # early check for storage permission, for better user feedback
3131 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3132 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3134 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
3135 for my $key (keys %$conf) {
3136 next if $key !~ /^hostpci\d+/;
3137 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3138 ." possibility to save/restore their internal state\n";
3141 if (!$statestorage) {
3142 # get statestorage from config if none is given
3143 my $storecfg = PVE
::Storage
::config
();
3144 $statestorage = PVE
::QemuServer
::find_vmstate_storage
($conf, $storecfg);
3147 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3153 syslog
('info', "suspend VM $vmid: $upid\n");
3155 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock, $todisk, $statestorage);
3160 my $taskname = $todisk ?
'qmsuspend' : 'qmpause';
3161 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
3164 __PACKAGE__-
>register_method({
3165 name
=> 'vm_resume',
3166 path
=> '{vmid}/status/resume',
3170 description
=> "Resume virtual machine.",
3172 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3175 additionalProperties
=> 0,
3177 node
=> get_standard_option
('pve-node'),
3178 vmid
=> get_standard_option
('pve-vmid',
3179 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3180 skiplock
=> get_standard_option
('skiplock'),
3181 nocheck
=> { type
=> 'boolean', optional
=> 1 },
3191 my $rpcenv = PVE
::RPCEnvironment
::get
();
3193 my $authuser = $rpcenv->get_user();
3195 my $node = extract_param
($param, 'node');
3197 my $vmid = extract_param
($param, 'vmid');
3199 my $skiplock = extract_param
($param, 'skiplock');
3200 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3201 if $skiplock && $authuser ne 'root@pam';
3203 my $nocheck = extract_param
($param, 'nocheck');
3204 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3205 if $nocheck && $authuser ne 'root@pam';
3207 my $to_disk_suspended;
3209 PVE
::QemuConfig-
>lock_config($vmid, sub {
3210 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3211 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3215 die "VM $vmid not running\n"
3216 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3221 syslog
('info', "resume VM $vmid: $upid\n");
3223 if (!$to_disk_suspended) {
3224 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3226 my $storecfg = PVE
::Storage
::config
();
3227 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3233 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3236 __PACKAGE__-
>register_method({
3237 name
=> 'vm_sendkey',
3238 path
=> '{vmid}/sendkey',
3242 description
=> "Send key event to virtual machine.",
3244 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3247 additionalProperties
=> 0,
3249 node
=> get_standard_option
('pve-node'),
3250 vmid
=> get_standard_option
('pve-vmid',
3251 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3252 skiplock
=> get_standard_option
('skiplock'),
3254 description
=> "The key (qemu monitor encoding).",
3259 returns
=> { type
=> 'null'},
3263 my $rpcenv = PVE
::RPCEnvironment
::get
();
3265 my $authuser = $rpcenv->get_user();
3267 my $node = extract_param
($param, 'node');
3269 my $vmid = extract_param
($param, 'vmid');
3271 my $skiplock = extract_param
($param, 'skiplock');
3272 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3273 if $skiplock && $authuser ne 'root@pam';
3275 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3280 __PACKAGE__-
>register_method({
3281 name
=> 'vm_feature',
3282 path
=> '{vmid}/feature',
3286 description
=> "Check if feature for virtual machine is available.",
3288 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3291 additionalProperties
=> 0,
3293 node
=> get_standard_option
('pve-node'),
3294 vmid
=> get_standard_option
('pve-vmid'),
3296 description
=> "Feature to check.",
3298 enum
=> [ 'snapshot', 'clone', 'copy' ],
3300 snapname
=> get_standard_option
('pve-snapshot-name', {
3308 hasFeature
=> { type
=> 'boolean' },
3311 items
=> { type
=> 'string' },
3318 my $node = extract_param
($param, 'node');
3320 my $vmid = extract_param
($param, 'vmid');
3322 my $snapname = extract_param
($param, 'snapname');
3324 my $feature = extract_param
($param, 'feature');
3326 my $running = PVE
::QemuServer
::check_running
($vmid);
3328 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3331 my $snap = $conf->{snapshots
}->{$snapname};
3332 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3335 my $storecfg = PVE
::Storage
::config
();
3337 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3338 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3341 hasFeature
=> $hasFeature,
3342 nodes
=> [ keys %$nodelist ],
3346 __PACKAGE__-
>register_method({
3348 path
=> '{vmid}/clone',
3352 description
=> "Create a copy of virtual machine/template.",
3354 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3355 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3356 "'Datastore.AllocateSpace' on any used storage.",
3359 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3361 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3362 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3367 additionalProperties
=> 0,
3369 node
=> get_standard_option
('pve-node'),
3370 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3371 newid
=> get_standard_option
('pve-vmid', {
3372 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3373 description
=> 'VMID for the clone.' }),
3376 type
=> 'string', format
=> 'dns-name',
3377 description
=> "Set a name for the new VM.",
3382 description
=> "Description for the new VM.",
3386 type
=> 'string', format
=> 'pve-poolid',
3387 description
=> "Add the new VM to the specified pool.",
3389 snapname
=> get_standard_option
('pve-snapshot-name', {
3392 storage
=> get_standard_option
('pve-storage-id', {
3393 description
=> "Target storage for full clone.",
3397 description
=> "Target format for file storage. Only valid for full clone.",
3400 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3405 description
=> "Create a full copy of all disks. This is always done when " .
3406 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3408 target
=> get_standard_option
('pve-node', {
3409 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3413 description
=> "Override I/O bandwidth limit (in KiB/s).",
3417 default => 'clone limit from datacenter or storage config',
3427 my $rpcenv = PVE
::RPCEnvironment
::get
();
3428 my $authuser = $rpcenv->get_user();
3430 my $node = extract_param
($param, 'node');
3431 my $vmid = extract_param
($param, 'vmid');
3432 my $newid = extract_param
($param, 'newid');
3433 my $pool = extract_param
($param, 'pool');
3435 my $snapname = extract_param
($param, 'snapname');
3436 my $storage = extract_param
($param, 'storage');
3437 my $format = extract_param
($param, 'format');
3438 my $target = extract_param
($param, 'target');
3440 my $localnode = PVE
::INotify
::nodename
();
3442 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3446 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3448 my $load_and_check = sub {
3449 $rpcenv->check_pool_exist($pool) if defined($pool);
3450 PVE
::Cluster
::check_node_exists
($target) if $target;
3452 my $storecfg = PVE
::Storage
::config
();
3455 # check if storage is enabled on local node
3456 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3458 # check if storage is available on target node
3459 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3460 # clone only works if target storage is shared
3461 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3462 die "can't clone to non-shared storage '$storage'\n"
3463 if !$scfg->{shared
};
3467 PVE
::Cluster
::check_cfs_quorum
();
3469 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3470 PVE
::QemuConfig-
>check_lock($conf);
3472 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3473 die "unexpected state change\n" if $verify_running != $running;
3475 die "snapshot '$snapname' does not exist\n"
3476 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3478 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3480 die "parameter 'storage' not allowed for linked clones\n"
3481 if defined($storage) && !$full;
3483 die "parameter 'format' not allowed for linked clones\n"
3484 if defined($format) && !$full;
3486 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3488 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3490 die "can't clone VM to node '$target' (VM uses local storage)\n"
3491 if $target && !$sharedvm;
3493 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3494 die "unable to create VM $newid: config file already exists\n"
3497 my $newconf = { lock => 'clone' };
3502 foreach my $opt (keys %$oldconf) {
3503 my $value = $oldconf->{$opt};
3505 # do not copy snapshot related info
3506 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3507 $opt eq 'vmstate' || $opt eq 'snapstate';
3509 # no need to copy unused images, because VMID(owner) changes anyways
3510 next if $opt =~ m/^unused\d+$/;
3512 die "cannot clone TPM state while VM is running\n"
3513 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3515 # always change MAC! address
3516 if ($opt =~ m/^net(\d+)$/) {
3517 my $net = PVE
::QemuServer
::parse_net
($value);
3518 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3519 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3520 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3521 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3522 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3523 die "unable to parse drive options for '$opt'\n" if !$drive;
3524 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3525 $newconf->{$opt} = $value; # simply copy configuration
3527 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3528 die "Full clone feature is not supported for drive '$opt'\n"
3529 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3530 $fullclone->{$opt} = 1;
3532 # not full means clone instead of copy
3533 die "Linked clone feature is not supported for drive '$opt'\n"
3534 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3536 $drives->{$opt} = $drive;
3537 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3538 push @$vollist, $drive->{file
};
3541 # copy everything else
3542 $newconf->{$opt} = $value;
3546 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3550 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3551 my $storecfg = PVE
::Storage
::config
();
3553 # auto generate a new uuid
3554 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3555 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3556 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3557 # auto generate a new vmgenid only if the option was set for template
3558 if ($newconf->{vmgenid
}) {
3559 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3562 delete $newconf->{template
};
3564 if ($param->{name
}) {
3565 $newconf->{name
} = $param->{name
};
3567 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3570 if ($param->{description
}) {
3571 $newconf->{description
} = $param->{description
};
3574 # create empty/temp config - this fails if VM already exists on other node
3575 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3576 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3578 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3580 my $newvollist = [];
3587 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3589 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3591 my $bwlimit = extract_param
($param, 'bwlimit');
3593 my $total_jobs = scalar(keys %{$drives});
3596 foreach my $opt (sort keys %$drives) {
3597 my $drive = $drives->{$opt};
3598 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3599 my $completion = $skipcomplete ?
'skip' : 'complete';
3601 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3602 my $storage_list = [ $src_sid ];
3603 push @$storage_list, $storage if defined($storage);
3604 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3608 running
=> $running,
3611 snapname
=> $snapname,
3617 storage
=> $storage,
3621 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3622 if $opt eq 'efidisk0';
3624 my $newdrive = PVE
::QemuServer
::clone_disk
(
3636 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3638 PVE
::QemuConfig-
>write_config($newid, $newconf);
3642 delete $newconf->{lock};
3644 # do not write pending changes
3645 if (my @changes = keys %{$newconf->{pending
}}) {
3646 my $pending = join(',', @changes);
3647 warn "found pending changes for '$pending', discarding for clone\n";
3648 delete $newconf->{pending
};
3651 PVE
::QemuConfig-
>write_config($newid, $newconf);
3654 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3655 PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) if !$running;
3656 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3658 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3659 die "Failed to move config to node '$target' - rename failed: $!\n"
3660 if !rename($conffile, $newconffile);
3663 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3666 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3667 sleep 1; # some storage like rbd need to wait before release volume - really?
3669 foreach my $volid (@$newvollist) {
3670 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3674 PVE
::Firewall
::remove_vmfw_conf
($newid);
3676 unlink $conffile; # avoid races -> last thing before die
3678 die "clone failed: $err";
3684 # Aquire exclusive lock lock for $newid
3685 my $lock_target_vm = sub {
3686 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3689 my $lock_source_vm = sub {
3690 # exclusive lock if VM is running - else shared lock is enough;
3692 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3694 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3698 $load_and_check->(); # early checks before forking/locking
3700 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3703 __PACKAGE__-
>register_method({
3704 name
=> 'move_vm_disk',
3705 path
=> '{vmid}/move_disk',
3709 description
=> "Move volume to different storage or to a different VM.",
3711 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3712 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3713 "a disk to another VM, you need the permissions on the target VM as well.",
3714 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3717 additionalProperties
=> 0,
3719 node
=> get_standard_option
('pve-node'),
3720 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3721 'target-vmid' => get_standard_option
('pve-vmid', {
3722 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3727 description
=> "The disk you want to move.",
3728 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3730 storage
=> get_standard_option
('pve-storage-id', {
3731 description
=> "Target storage.",
3732 completion
=> \
&PVE
::QemuServer
::complete_storage
,
3737 description
=> "Target Format.",
3738 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
3743 description
=> "Delete the original disk after successful copy. By default the"
3744 ." original disk is kept as unused disk.",
3750 description
=> 'Prevent changes if current configuration file has different SHA1"
3751 ." digest. This can be used to prevent concurrent modifications.',
3756 description
=> "Override I/O bandwidth limit (in KiB/s).",
3760 default => 'move limit from datacenter or storage config',
3764 description
=> "The config key the disk will be moved to on the target VM"
3765 ." (for example, ide0 or scsi1). Default is the source disk key.",
3766 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3769 'target-digest' => {
3771 description
=> 'Prevent changes if the current config file of the target VM has a"
3772 ." different SHA1 digest. This can be used to detect concurrent modifications.',
3780 description
=> "the task ID.",
3785 my $rpcenv = PVE
::RPCEnvironment
::get
();
3786 my $authuser = $rpcenv->get_user();
3788 my $node = extract_param
($param, 'node');
3789 my $vmid = extract_param
($param, 'vmid');
3790 my $target_vmid = extract_param
($param, 'target-vmid');
3791 my $digest = extract_param
($param, 'digest');
3792 my $target_digest = extract_param
($param, 'target-digest');
3793 my $disk = extract_param
($param, 'disk');
3794 my $target_disk = extract_param
($param, 'target-disk') // $disk;
3795 my $storeid = extract_param
($param, 'storage');
3796 my $format = extract_param
($param, 'format');
3798 my $storecfg = PVE
::Storage
::config
();
3800 my $load_and_check_move = sub {
3801 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3802 PVE
::QemuConfig-
>check_lock($conf);
3804 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
3806 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3808 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
3810 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
3811 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
3813 my $old_volid = $drive->{file
};
3815 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
3816 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3820 die "you can't move to the same storage with same format\n"
3821 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
3823 # this only checks snapshots because $disk is passed!
3824 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
3830 die "you can't move a disk with snapshots and delete the source\n"
3831 if $snapshotted && $param->{delete};
3833 return ($conf, $drive, $oldstoreid, $snapshotted);
3836 my $move_updatefn = sub {
3837 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
3838 my $old_volid = $drive->{file
};
3840 PVE
::Cluster
::log_msg
(
3843 "move disk VM $vmid: move --disk $disk --storage $storeid"
3846 my $running = PVE
::QemuServer
::check_running
($vmid);
3848 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
3850 my $newvollist = [];
3856 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3858 warn "moving disk with snapshots, snapshots will not be moved!\n"
3861 my $bwlimit = extract_param
($param, 'bwlimit');
3862 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
3864 [$oldstoreid, $storeid],
3870 running
=> $running,
3879 storage
=> $storeid,
3883 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
3884 if $disk eq 'efidisk0';
3886 my $newdrive = PVE
::QemuServer
::clone_disk
(
3897 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
3899 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
3901 # convert moved disk to base if part of template
3902 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
3903 if PVE
::QemuConfig-
>is_template($conf);
3905 PVE
::QemuConfig-
>write_config($vmid, $conf);
3907 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
3908 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
3909 eval { mon_cmd
($vmid, "guest-fstrim") };
3913 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
3914 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
3920 foreach my $volid (@$newvollist) {
3921 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
3924 die "storage migration failed: $err";
3927 if ($param->{delete}) {
3929 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
3930 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
3936 my $load_and_check_reassign_configs = sub {
3937 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
3939 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
3940 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
3942 my $source_node = $vmlist->{$vmid}->{node
};
3943 my $target_node = $vmlist->{$target_vmid}->{node
};
3945 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
3946 if $source_node ne $target_node;
3948 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
3949 PVE
::QemuConfig-
>check_lock($source_conf);
3950 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
3951 PVE
::QemuConfig-
>check_lock($target_conf);
3953 die "Can't move disks from or to template VMs\n"
3954 if ($source_conf->{template
} || $target_conf->{template
});
3957 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
3958 die "VM ${vmid}: $@" if $@;
3961 if ($target_digest) {
3962 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
3963 die "VM ${target_vmid}: $@" if $@;
3966 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
3968 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
3969 if $target_conf->{$target_disk};
3971 my $drive = PVE
::QemuServer
::parse_drive
(
3973 $source_conf->{$disk},
3975 die "failed to parse source disk - $@\n" if !$drive;
3977 my $source_volid = $drive->{file
};
3979 die "disk '${disk}' has no associated volume\n" if !$source_volid;
3980 die "CD drive contents can't be moved to another VM\n"
3981 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
3983 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
3984 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
3986 die "Can't move disk used by a snapshot to another VM\n"
3987 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
3988 die "Storage does not support moving of this disk to another VM\n"
3989 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
3990 die "Cannot move disk to another VM while the source VM is running - detach first\n"
3991 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
3993 # now re-parse using target disk slot format
3994 if ($target_disk =~ /^unused\d+$/) {
3995 $drive = PVE
::QemuServer
::parse_drive
(
4000 $drive = PVE
::QemuServer
::parse_drive
(
4002 $source_conf->{$disk},
4005 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4007 my $repl_conf = PVE
::ReplicationConfig-
>new();
4008 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4009 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4010 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4011 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4014 return ($source_conf, $target_conf, $drive);
4019 print STDERR
"$msg\n";
4022 my $disk_reassignfn = sub {
4023 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4024 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4025 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4027 my $source_volid = $drive->{file
};
4029 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4030 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4032 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4034 my $new_volid = PVE
::Storage
::rename_volume
(
4040 $drive->{file
} = $new_volid;
4042 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4043 if (defined(delete $boot_order->{$disk})) {
4044 print "removing disk '$disk' from boot order config\n";
4045 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4046 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4049 delete $source_conf->{$disk};
4050 print "removing disk '${disk}' from VM '${vmid}' config\n";
4051 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4053 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4055 if ($target_disk =~ /^unused\d+$/) {
4056 $target_conf->{$target_disk} = $drive_string;
4057 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4062 vmid
=> $target_vmid,
4063 digest
=> $target_digest,
4064 $target_disk => $drive_string,
4070 # remove possible replication snapshots
4071 if (PVE
::Storage
::volume_has_feature
(
4077 PVE
::Replication
::prepare
(
4087 print "Failed to remove replication snapshots on moved disk " .
4088 "'$target_disk'. Manual cleanup could be necessary.\n";
4095 if ($target_vmid && $storeid) {
4096 my $msg = "either set 'storage' or 'target-vmid', but not both";
4097 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4098 } elsif ($target_vmid) {
4099 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4100 if $authuser ne 'root@pam';
4102 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4103 if $vmid eq $target_vmid;
4105 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4106 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4107 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4109 return $rpcenv->fork_worker(
4111 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4115 } elsif ($storeid) {
4116 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4118 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4119 if $disk =~ m/^unused\d+$/;
4121 $load_and_check_move->(); # early checks before forking/locking
4124 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4127 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4129 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4130 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4134 my $check_vm_disks_local = sub {
4135 my ($storecfg, $vmconf, $vmid) = @_;
4137 my $local_disks = {};
4139 # add some more information to the disks e.g. cdrom
4140 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4141 my ($volid, $attr) = @_;
4143 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4145 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4146 return if $scfg->{shared
};
4148 # The shared attr here is just a special case where the vdisk
4149 # is marked as shared manually
4150 return if $attr->{shared
};
4151 return if $attr->{cdrom
} and $volid eq "none";
4153 if (exists $local_disks->{$volid}) {
4154 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4156 $local_disks->{$volid} = $attr;
4157 # ensure volid is present in case it's needed
4158 $local_disks->{$volid}->{volid
} = $volid;
4162 return $local_disks;
4165 __PACKAGE__-
>register_method({
4166 name
=> 'migrate_vm_precondition',
4167 path
=> '{vmid}/migrate',
4171 description
=> "Get preconditions for migration.",
4173 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4176 additionalProperties
=> 0,
4178 node
=> get_standard_option
('pve-node'),
4179 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4180 target
=> get_standard_option
('pve-node', {
4181 description
=> "Target node.",
4182 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4190 running
=> { type
=> 'boolean' },
4194 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4196 not_allowed_nodes
=> {
4199 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4203 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4205 local_resources
=> {
4207 description
=> "List local resources e.g. pci, usb"
4214 my $rpcenv = PVE
::RPCEnvironment
::get
();
4216 my $authuser = $rpcenv->get_user();
4218 PVE
::Cluster
::check_cfs_quorum
();
4222 my $vmid = extract_param
($param, 'vmid');
4223 my $target = extract_param
($param, 'target');
4224 my $localnode = PVE
::INotify
::nodename
();
4228 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4229 my $storecfg = PVE
::Storage
::config
();
4232 # try to detect errors early
4233 PVE
::QemuConfig-
>check_lock($vmconf);
4235 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4237 # if vm is not running, return target nodes where local storage is available
4238 # for offline migration
4239 if (!$res->{running
}) {
4240 $res->{allowed_nodes
} = [];
4241 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4242 delete $checked_nodes->{$localnode};
4244 foreach my $node (keys %$checked_nodes) {
4245 if (!defined $checked_nodes->{$node}->{unavailable_storages
}) {
4246 push @{$res->{allowed_nodes
}}, $node;
4250 $res->{not_allowed_nodes
} = $checked_nodes;
4254 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4255 $res->{local_disks
} = [ values %$local_disks ];;
4257 my $local_resources = PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4259 $res->{local_resources
} = $local_resources;
4266 __PACKAGE__-
>register_method({
4267 name
=> 'migrate_vm',
4268 path
=> '{vmid}/migrate',
4272 description
=> "Migrate virtual machine. Creates a new migration task.",
4274 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4277 additionalProperties
=> 0,
4279 node
=> get_standard_option
('pve-node'),
4280 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4281 target
=> get_standard_option
('pve-node', {
4282 description
=> "Target node.",
4283 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4287 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4292 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4297 enum
=> ['secure', 'insecure'],
4298 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4301 migration_network
=> {
4302 type
=> 'string', format
=> 'CIDR',
4303 description
=> "CIDR of the (sub) network that is used for migration.",
4306 "with-local-disks" => {
4308 description
=> "Enable live storage migration for local disk",
4311 targetstorage
=> get_standard_option
('pve-targetstorage', {
4312 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4315 description
=> "Override I/O bandwidth limit (in KiB/s).",
4319 default => 'migrate limit from datacenter or storage config',
4325 description
=> "the task ID.",
4330 my $rpcenv = PVE
::RPCEnvironment
::get
();
4331 my $authuser = $rpcenv->get_user();
4333 my $target = extract_param
($param, 'target');
4335 my $localnode = PVE
::INotify
::nodename
();
4336 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4338 PVE
::Cluster
::check_cfs_quorum
();
4340 PVE
::Cluster
::check_node_exists
($target);
4342 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4344 my $vmid = extract_param
($param, 'vmid');
4346 raise_param_exc
({ force
=> "Only root may use this option." })
4347 if $param->{force
} && $authuser ne 'root@pam';
4349 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4350 if $param->{migration_type
} && $authuser ne 'root@pam';
4352 # allow root only until better network permissions are available
4353 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4354 if $param->{migration_network
} && $authuser ne 'root@pam';
4357 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4359 # try to detect errors early
4361 PVE
::QemuConfig-
>check_lock($conf);
4363 if (PVE
::QemuServer
::check_running
($vmid)) {
4364 die "can't migrate running VM without --online\n" if !$param->{online
};
4366 my $repl_conf = PVE
::ReplicationConfig-
>new();
4367 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4368 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4369 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4370 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4371 "target. Use 'force' to override.\n";
4374 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4375 $param->{online
} = 0;
4378 my $storecfg = PVE
::Storage
::config
();
4379 if (my $targetstorage = $param->{targetstorage
}) {
4380 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4381 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4384 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4385 if !defined($storagemap->{identity
});
4387 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4388 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4391 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4392 if $storagemap->{default};
4394 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4395 if $storagemap->{identity
};
4397 $param->{storagemap
} = $storagemap;
4399 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4402 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4407 print "Requesting HA migration for VM $vmid to node $target\n";
4409 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4410 PVE
::Tools
::run_command
($cmd);
4414 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4419 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4423 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4426 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4431 __PACKAGE__-
>register_method({
4432 name
=> 'remote_migrate_vm',
4433 path
=> '{vmid}/remote_migrate',
4437 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4439 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4442 additionalProperties
=> 0,
4444 node
=> get_standard_option
('pve-node'),
4445 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4446 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4447 'target-endpoint' => get_standard_option
('proxmox-remote', {
4448 description
=> "Remote target endpoint",
4452 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4457 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.",
4461 'target-storage' => get_standard_option
('pve-targetstorage', {
4462 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4465 'target-bridge' => {
4467 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.",
4468 format
=> 'bridge-pair-list',
4471 description
=> "Override I/O bandwidth limit (in KiB/s).",
4475 default => 'migrate limit from datacenter or storage config',
4481 description
=> "the task ID.",
4486 my $rpcenv = PVE
::RPCEnvironment
::get
();
4487 my $authuser = $rpcenv->get_user();
4489 my $source_vmid = extract_param
($param, 'vmid');
4490 my $target_endpoint = extract_param
($param, 'target-endpoint');
4491 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4493 my $delete = extract_param
($param, 'delete') // 0;
4495 PVE
::Cluster
::check_cfs_quorum
();
4498 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4500 PVE
::QemuConfig-
>check_lock($conf);
4502 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4503 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4505 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4507 # TODO: move this as helper somewhere appropriate?
4509 protocol
=> 'https',
4510 host
=> $remote->{host
},
4511 port
=> $remote->{port
} // 8006,
4512 apitoken
=> $remote->{apitoken
},
4516 if ($fp = $remote->{fingerprint
}) {
4517 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4520 print "Establishing API connection with remote at '$remote->{host}'\n";
4522 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4524 if (!defined($fp)) {
4525 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4526 foreach my $cert (@$cert_info) {
4527 my $filename = $cert->{filename
};
4528 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4529 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4531 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4535 my $repl_conf = PVE
::ReplicationConfig-
>new();
4536 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4537 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4539 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4540 die "can't migrate running VM without --online\n" if !$param->{online
};
4543 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4544 $param->{online
} = 0;
4547 my $storecfg = PVE
::Storage
::config
();
4548 my $target_storage = extract_param
($param, 'target-storage');
4549 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4550 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4553 my $target_bridge = extract_param
($param, 'target-bridge');
4554 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4555 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4558 die "remote migration requires explicit storage mapping!\n"
4559 if $storagemap->{identity
};
4561 $param->{storagemap
} = $storagemap;
4562 $param->{bridgemap
} = $bridgemap;
4563 $param->{remote
} = {
4564 conn
=> $conn_args, # re-use fingerprint for tunnel
4565 client
=> $api_client,
4566 vmid
=> $target_vmid,
4568 $param->{migration_type
} = 'websocket';
4569 $param->{'with-local-disks'} = 1;
4570 $param->{delete} = $delete if $delete;
4572 my $cluster_status = $api_client->get("/cluster/status");
4574 foreach my $entry (@$cluster_status) {
4575 next if $entry->{type
} ne 'node';
4576 if ($entry->{local}) {
4577 $target_node = $entry->{name
};
4582 die "couldn't determine endpoint's node name\n"
4583 if !defined($target_node);
4586 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4590 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4593 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4596 __PACKAGE__-
>register_method({
4598 path
=> '{vmid}/monitor',
4602 description
=> "Execute Qemu monitor commands.",
4604 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4605 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4608 additionalProperties
=> 0,
4610 node
=> get_standard_option
('pve-node'),
4611 vmid
=> get_standard_option
('pve-vmid'),
4614 description
=> "The monitor command.",
4618 returns
=> { type
=> 'string'},
4622 my $rpcenv = PVE
::RPCEnvironment
::get
();
4623 my $authuser = $rpcenv->get_user();
4626 my $command = shift;
4627 return $command =~ m/^\s*info(\s+|$)/
4628 || $command =~ m/^\s*help\s*$/;
4631 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4632 if !&$is_ro($param->{command
});
4634 my $vmid = $param->{vmid
};
4636 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4640 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4642 $res = "ERROR: $@" if $@;
4647 __PACKAGE__-
>register_method({
4648 name
=> 'resize_vm',
4649 path
=> '{vmid}/resize',
4653 description
=> "Extend volume size.",
4655 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4658 additionalProperties
=> 0,
4660 node
=> get_standard_option
('pve-node'),
4661 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4662 skiplock
=> get_standard_option
('skiplock'),
4665 description
=> "The disk you want to resize.",
4666 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4670 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4671 description
=> "The new size. With the `+` sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.",
4675 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4681 returns
=> { type
=> 'null'},
4685 my $rpcenv = PVE
::RPCEnvironment
::get
();
4687 my $authuser = $rpcenv->get_user();
4689 my $node = extract_param
($param, 'node');
4691 my $vmid = extract_param
($param, 'vmid');
4693 my $digest = extract_param
($param, 'digest');
4695 my $disk = extract_param
($param, 'disk');
4697 my $sizestr = extract_param
($param, 'size');
4699 my $skiplock = extract_param
($param, 'skiplock');
4700 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4701 if $skiplock && $authuser ne 'root@pam';
4703 my $storecfg = PVE
::Storage
::config
();
4705 my $updatefn = sub {
4707 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4709 die "checksum missmatch (file change by other user?)\n"
4710 if $digest && $digest ne $conf->{digest
};
4711 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4713 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4715 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4717 my (undef, undef, undef, undef, undef, undef, $format) =
4718 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
4720 die "can't resize volume: $disk if snapshot exists\n"
4721 if %{$conf->{snapshots
}} && $format eq 'qcow2';
4723 my $volid = $drive->{file
};
4725 die "disk '$disk' has no associated volume\n" if !$volid;
4727 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
4729 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
4731 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4733 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
4734 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
4736 die "Could not determine current size of volume '$volid'\n" if !defined($size);
4738 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4739 my ($ext, $newsize, $unit) = ($1, $2, $4);
4742 $newsize = $newsize * 1024;
4743 } elsif ($unit eq 'M') {
4744 $newsize = $newsize * 1024 * 1024;
4745 } elsif ($unit eq 'G') {
4746 $newsize = $newsize * 1024 * 1024 * 1024;
4747 } elsif ($unit eq 'T') {
4748 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4751 $newsize += $size if $ext;
4752 $newsize = int($newsize);
4754 die "shrinking disks is not supported\n" if $newsize < $size;
4756 return if $size == $newsize;
4758 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
4760 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
4762 $drive->{size
} = $newsize;
4763 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
4765 PVE
::QemuConfig-
>write_config($vmid, $conf);
4768 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
4772 __PACKAGE__-
>register_method({
4773 name
=> 'snapshot_list',
4774 path
=> '{vmid}/snapshot',
4776 description
=> "List all snapshots.",
4778 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4781 protected
=> 1, # qemu pid files are only readable by root
4783 additionalProperties
=> 0,
4785 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4786 node
=> get_standard_option
('pve-node'),
4795 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
4799 description
=> "Snapshot includes RAM.",
4804 description
=> "Snapshot description.",
4808 description
=> "Snapshot creation time",
4810 renderer
=> 'timestamp',
4814 description
=> "Parent snapshot identifier.",
4820 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
4825 my $vmid = $param->{vmid
};
4827 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4828 my $snaphash = $conf->{snapshots
} || {};
4832 foreach my $name (keys %$snaphash) {
4833 my $d = $snaphash->{$name};
4836 snaptime
=> $d->{snaptime
} || 0,
4837 vmstate
=> $d->{vmstate
} ?
1 : 0,
4838 description
=> $d->{description
} || '',
4840 $item->{parent
} = $d->{parent
} if $d->{parent
};
4841 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
4845 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
4848 digest
=> $conf->{digest
},
4849 running
=> $running,
4850 description
=> "You are here!",
4852 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
4854 push @$res, $current;
4859 __PACKAGE__-
>register_method({
4861 path
=> '{vmid}/snapshot',
4865 description
=> "Snapshot a VM.",
4867 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4870 additionalProperties
=> 0,
4872 node
=> get_standard_option
('pve-node'),
4873 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4874 snapname
=> get_standard_option
('pve-snapshot-name'),
4878 description
=> "Save the vmstate",
4883 description
=> "A textual description or comment.",
4889 description
=> "the task ID.",
4894 my $rpcenv = PVE
::RPCEnvironment
::get
();
4896 my $authuser = $rpcenv->get_user();
4898 my $node = extract_param
($param, 'node');
4900 my $vmid = extract_param
($param, 'vmid');
4902 my $snapname = extract_param
($param, 'snapname');
4904 die "unable to use snapshot name 'current' (reserved name)\n"
4905 if $snapname eq 'current';
4907 die "unable to use snapshot name 'pending' (reserved name)\n"
4908 if lc($snapname) eq 'pending';
4911 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
4912 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
4913 $param->{description
});
4916 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
4919 __PACKAGE__-
>register_method({
4920 name
=> 'snapshot_cmd_idx',
4921 path
=> '{vmid}/snapshot/{snapname}',
4928 additionalProperties
=> 0,
4930 vmid
=> get_standard_option
('pve-vmid'),
4931 node
=> get_standard_option
('pve-node'),
4932 snapname
=> get_standard_option
('pve-snapshot-name'),
4941 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
4948 push @$res, { cmd
=> 'rollback' };
4949 push @$res, { cmd
=> 'config' };
4954 __PACKAGE__-
>register_method({
4955 name
=> 'update_snapshot_config',
4956 path
=> '{vmid}/snapshot/{snapname}/config',
4960 description
=> "Update snapshot metadata.",
4962 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4965 additionalProperties
=> 0,
4967 node
=> get_standard_option
('pve-node'),
4968 vmid
=> get_standard_option
('pve-vmid'),
4969 snapname
=> get_standard_option
('pve-snapshot-name'),
4973 description
=> "A textual description or comment.",
4977 returns
=> { type
=> 'null' },
4981 my $rpcenv = PVE
::RPCEnvironment
::get
();
4983 my $authuser = $rpcenv->get_user();
4985 my $vmid = extract_param
($param, 'vmid');
4987 my $snapname = extract_param
($param, 'snapname');
4989 return if !defined($param->{description
});
4991 my $updatefn = sub {
4993 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4995 PVE
::QemuConfig-
>check_lock($conf);
4997 my $snap = $conf->{snapshots
}->{$snapname};
4999 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5001 $snap->{description
} = $param->{description
} if defined($param->{description
});
5003 PVE
::QemuConfig-
>write_config($vmid, $conf);
5006 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5011 __PACKAGE__-
>register_method({
5012 name
=> 'get_snapshot_config',
5013 path
=> '{vmid}/snapshot/{snapname}/config',
5016 description
=> "Get snapshot configuration",
5018 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5021 additionalProperties
=> 0,
5023 node
=> get_standard_option
('pve-node'),
5024 vmid
=> get_standard_option
('pve-vmid'),
5025 snapname
=> get_standard_option
('pve-snapshot-name'),
5028 returns
=> { type
=> "object" },
5032 my $rpcenv = PVE
::RPCEnvironment
::get
();
5034 my $authuser = $rpcenv->get_user();
5036 my $vmid = extract_param
($param, 'vmid');
5038 my $snapname = extract_param
($param, 'snapname');
5040 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5042 my $snap = $conf->{snapshots
}->{$snapname};
5044 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5049 __PACKAGE__-
>register_method({
5051 path
=> '{vmid}/snapshot/{snapname}/rollback',
5055 description
=> "Rollback VM state to specified snapshot.",
5057 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5060 additionalProperties
=> 0,
5062 node
=> get_standard_option
('pve-node'),
5063 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5064 snapname
=> get_standard_option
('pve-snapshot-name'),
5067 description
=> "Whether the VM should get started after rolling back successfully",
5075 description
=> "the task ID.",
5080 my $rpcenv = PVE
::RPCEnvironment
::get
();
5082 my $authuser = $rpcenv->get_user();
5084 my $node = extract_param
($param, 'node');
5086 my $vmid = extract_param
($param, 'vmid');
5088 my $snapname = extract_param
($param, 'snapname');
5091 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5092 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5094 if ($param->{start
}) {
5095 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5100 # hold migration lock, this makes sure that nobody create replication snapshots
5101 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5104 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5107 __PACKAGE__-
>register_method({
5108 name
=> 'delsnapshot',
5109 path
=> '{vmid}/snapshot/{snapname}',
5113 description
=> "Delete a VM snapshot.",
5115 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5118 additionalProperties
=> 0,
5120 node
=> get_standard_option
('pve-node'),
5121 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5122 snapname
=> get_standard_option
('pve-snapshot-name'),
5126 description
=> "For removal from config file, even if removing disk snapshots fails.",
5132 description
=> "the task ID.",
5137 my $rpcenv = PVE
::RPCEnvironment
::get
();
5139 my $authuser = $rpcenv->get_user();
5141 my $node = extract_param
($param, 'node');
5143 my $vmid = extract_param
($param, 'vmid');
5145 my $snapname = extract_param
($param, 'snapname');
5148 my $do_delete = sub {
5150 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5151 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5155 if ($param->{force
}) {
5158 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5160 die $err if $lock_obtained;
5161 die "Failed to obtain guest migration lock - replication running?\n";
5166 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5169 __PACKAGE__-
>register_method({
5171 path
=> '{vmid}/template',
5175 description
=> "Create a Template.",
5177 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5178 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5181 additionalProperties
=> 0,
5183 node
=> get_standard_option
('pve-node'),
5184 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5188 description
=> "If you want to convert only 1 disk to base image.",
5189 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5196 description
=> "the task ID.",
5201 my $rpcenv = PVE
::RPCEnvironment
::get
();
5203 my $authuser = $rpcenv->get_user();
5205 my $node = extract_param
($param, 'node');
5207 my $vmid = extract_param
($param, 'vmid');
5209 my $disk = extract_param
($param, 'disk');
5211 my $load_and_check = sub {
5212 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5214 PVE
::QemuConfig-
>check_lock($conf);
5216 die "unable to create template, because VM contains snapshots\n"
5217 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5219 die "you can't convert a template to a template\n"
5220 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5222 die "you can't convert a VM to template if VM is running\n"
5223 if PVE
::QemuServer
::check_running
($vmid);
5228 $load_and_check->();
5231 PVE
::QemuConfig-
>lock_config($vmid, sub {
5232 my $conf = $load_and_check->();
5234 $conf->{template
} = 1;
5235 PVE
::QemuConfig-
>write_config($vmid, $conf);
5237 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5241 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5244 __PACKAGE__-
>register_method({
5245 name
=> 'cloudinit_generated_config_dump',
5246 path
=> '{vmid}/cloudinit/dump',
5249 description
=> "Get automatically generated cloudinit config.",
5251 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5254 additionalProperties
=> 0,
5256 node
=> get_standard_option
('pve-node'),
5257 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5259 description
=> 'Config type.',
5261 enum
=> ['user', 'network', 'meta'],
5271 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5273 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5276 __PACKAGE__-
>register_method({
5278 path
=> '{vmid}/mtunnel',
5281 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5285 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5286 ['perm', '/', [ 'Sys.Incoming' ]],
5288 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5289 " on '/'. Further permission checks happen during the actual migration.",
5292 additionalProperties
=> 0,
5294 node
=> get_standard_option
('pve-node'),
5295 vmid
=> get_standard_option
('pve-vmid'),
5298 format
=> 'pve-storage-id-list',
5300 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5304 format
=> 'pve-bridge-id-list',
5306 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5311 additionalProperties
=> 0,
5313 upid
=> { type
=> 'string' },
5314 ticket
=> { type
=> 'string' },
5315 socket => { type
=> 'string' },
5321 my $rpcenv = PVE
::RPCEnvironment
::get
();
5322 my $authuser = $rpcenv->get_user();
5324 my $node = extract_param
($param, 'node');
5325 my $vmid = extract_param
($param, 'vmid');
5327 my $storages = extract_param
($param, 'storages');
5328 my $bridges = extract_param
($param, 'bridges');
5330 my $nodename = PVE
::INotify
::nodename
();
5332 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5333 if $node ne 'localhost' && $node ne $nodename;
5337 my $storecfg = PVE
::Storage
::config
();
5338 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5339 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5342 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5343 PVE
::Network
::read_bridge_mtu
($bridge);
5346 PVE
::Cluster
::check_cfs_quorum
();
5348 my $lock = 'create';
5349 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5351 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5356 storecfg
=> PVE
::Storage
::config
(),
5361 my $run_locked = sub {
5362 my ($code, $params) = @_;
5363 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5364 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5366 $state->{conf
} = $conf;
5368 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5369 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5371 return $code->($params);
5379 description
=> 'Full VM config, adapted for target cluster/node',
5381 'firewall-config' => {
5383 description
=> 'VM firewall config',
5388 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5391 format
=> 'pve-storage-id',
5395 description
=> 'parsed drive information without volid and format',
5401 description
=> 'params passed to vm_start_nolock',
5405 description
=> 'migrate_opts passed to vm_start_nolock',
5411 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5417 description
=> 'remove VM config and disks, aborting migration',
5421 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5422 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5423 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5426 my $cmd_handlers = {
5428 # compared against other end's version
5429 # bump/reset for breaking changes
5430 # bump/bump for opt-in changes
5432 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5439 # parse and write out VM FW config if given
5440 if (my $fw_conf = $params->{'firewall-config'}) {
5441 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5448 ipset_comments
=> {},
5450 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5452 # TODO: add flag for strict parsing?
5453 # TODO: add import sub that does all this given raw content?
5454 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5455 $vmfw_conf->{vmid
} = $state->{vmid
};
5456 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5458 $state->{cleanup
}->{fw
} = 1;
5461 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5462 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5463 delete $new_conf->{lock};
5464 delete $new_conf->{digest
};
5466 # TODO handle properly?
5467 delete $new_conf->{snapshots
};
5468 delete $new_conf->{parent
};
5469 delete $new_conf->{pending
};
5471 # not handled by update_vm_api
5472 my $vmgenid = delete $new_conf->{vmgenid
};
5473 my $meta = delete $new_conf->{meta
};
5474 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5475 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5477 $new_conf->{vmid
} = $state->{vmid
};
5478 $new_conf->{node
} = $node;
5480 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5483 $update_vm_api->($new_conf, 1);
5486 # revert to locked previous config
5487 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5488 $conf->{lock} = 'create';
5489 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5494 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5495 $conf->{lock} = 'migrate';
5496 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5497 $conf->{meta
} = $meta if defined($meta);
5498 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5499 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5501 $state->{lock} = 'migrate';
5507 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5512 my $format = $params->{format
};
5513 my $storeid = $params->{storage
};
5514 my $drive = $params->{drive
};
5516 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5519 default => $storeid,
5522 my $source_volumes = {
5533 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5534 if (defined($res->{disk
})) {
5535 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5536 return $res->{disk
};
5538 die "failed to allocate NBD disk..\n";
5541 'disk-import' => sub {
5544 $check_storage_access_migrate->(
5552 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5554 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5556 'query-disk-import' => sub {
5559 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5564 my $info = PVE
::QemuServer
::vm_start_nolock
(
5568 $params->{start_params
},
5569 $params->{migrate_opts
},
5573 if ($info->{migrate
}->{proto
} ne 'unix') {
5574 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5575 die "migration over non-UNIX sockets not possible\n";
5578 my $socket = $info->{migrate
}->{addr
};
5579 chown $state->{socket_uid
}, -1, $socket;
5580 $state->{sockets
}->{$socket} = 1;
5582 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5583 foreach my $socket (@$unix_sockets) {
5584 chown $state->{socket_uid
}, -1, $socket;
5585 $state->{sockets
}->{$socket} = 1;
5590 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5591 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5592 warn "fstrim failed: $@\n" if $@;
5597 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5601 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5605 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5606 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5608 die "VM $state->{vmid} not running\n";
5613 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5614 delete $state->{lock};
5620 my $path = $params->{path
};
5622 die "Not allowed to generate ticket for unknown socket '$path'\n"
5623 if !defined($state->{sockets
}->{$path});
5625 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5630 if ($params->{cleanup
}) {
5631 if ($state->{cleanup
}->{fw
}) {
5632 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5635 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5636 print "freeing volume '$volid' as part of cleanup\n";
5637 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5641 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5644 print "switching to exit-mode, waiting for client to disconnect\n";
5651 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5652 unlink $socket_addr;
5654 $state->{socket} = IO
::Socket
::UNIX-
>new(
5655 Type
=> SOCK_STREAM
(),
5656 Local
=> $socket_addr,
5660 $state->{socket_uid
} = getpwnam('www-data')
5661 or die "Failed to resolve user 'www-data' to numeric UID\n";
5662 chown $state->{socket_uid
}, -1, $socket_addr;
5665 print "mtunnel started\n";
5667 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5669 warn "Failed to accept tunnel connection - $@\n";
5671 warn "Removing tunnel socket..\n";
5672 unlink $state->{socket};
5674 warn "Removing temporary VM config..\n";
5676 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5679 die "Exiting mtunnel\n";
5682 $state->{conn
} = $conn;
5684 my $reply_err = sub {
5687 my $reply = JSON
::encode_json
({
5688 success
=> JSON
::false
,
5691 $conn->print("$reply\n");
5695 my $reply_ok = sub {
5698 $res->{success
} = JSON
::true
;
5699 my $reply = JSON
::encode_json
($res);
5700 $conn->print("$reply\n");
5704 while (my $line = <$conn>) {
5707 # untaint, we validate below if needed
5708 ($line) = $line =~ /^(.*)$/;
5709 my $parsed = eval { JSON
::decode_json
($line) };
5711 $reply_err->("failed to parse command - $@");
5715 my $cmd = delete $parsed->{cmd
};
5716 if (!defined($cmd)) {
5717 $reply_err->("'cmd' missing");
5718 } elsif ($state->{exit}) {
5719 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5721 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5722 print "received command '$cmd'\n";
5724 if ($cmd_desc->{$cmd}) {
5725 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
5729 my $res = $run_locked->($handler, $parsed);
5732 $reply_err->("failed to handle '$cmd' command - $@")
5735 $reply_err->("unknown command '$cmd' given");
5739 if ($state->{exit}) {
5740 print "mtunnel exited\n";
5742 die "mtunnel exited unexpectedly\n";
5746 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
5747 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
5748 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
5753 socket => $socket_addr,
5757 __PACKAGE__-
>register_method({
5758 name
=> 'mtunnelwebsocket',
5759 path
=> '{vmid}/mtunnelwebsocket',
5762 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.",
5763 user
=> 'all', # check inside
5765 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
5767 additionalProperties
=> 0,
5769 node
=> get_standard_option
('pve-node'),
5770 vmid
=> get_standard_option
('pve-vmid'),
5773 description
=> "unix socket to forward to",
5777 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
5784 port
=> { type
=> 'string', optional
=> 1 },
5785 socket => { type
=> 'string', optional
=> 1 },
5791 my $rpcenv = PVE
::RPCEnvironment
::get
();
5792 my $authuser = $rpcenv->get_user();
5794 my $nodename = PVE
::INotify
::nodename
();
5795 my $node = extract_param
($param, 'node');
5797 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5798 if $node ne 'localhost' && $node ne $nodename;
5800 my $vmid = $param->{vmid
};
5802 PVE
::QemuConfig-
>load_config($vmid);
5804 my $socket = $param->{socket};
5805 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
5807 return { socket => $socket };