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 # nocheck is used as part of migration when config file might be still
3205 my $nocheck = extract_param
($param, 'nocheck');
3206 raise_param_exc
({ nocheck
=> "Only root may use this option." })
3207 if $nocheck && $authuser ne 'root@pam';
3209 my $to_disk_suspended;
3211 PVE
::QemuConfig-
>lock_config($vmid, sub {
3212 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3213 $to_disk_suspended = PVE
::QemuConfig-
>has_lock($conf, 'suspended');
3217 die "VM $vmid not running\n"
3218 if !$to_disk_suspended && !PVE
::QemuServer
::check_running
($vmid, $nocheck);
3223 syslog
('info', "resume VM $vmid: $upid\n");
3225 if (!$to_disk_suspended) {
3226 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
3228 my $storecfg = PVE
::Storage
::config
();
3229 PVE
::QemuServer
::vm_start
($storecfg, $vmid, { skiplock
=> $skiplock });
3235 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
3238 __PACKAGE__-
>register_method({
3239 name
=> 'vm_sendkey',
3240 path
=> '{vmid}/sendkey',
3244 description
=> "Send key event to virtual machine.",
3246 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3249 additionalProperties
=> 0,
3251 node
=> get_standard_option
('pve-node'),
3252 vmid
=> get_standard_option
('pve-vmid',
3253 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
3254 skiplock
=> get_standard_option
('skiplock'),
3256 description
=> "The key (qemu monitor encoding).",
3261 returns
=> { type
=> 'null'},
3265 my $rpcenv = PVE
::RPCEnvironment
::get
();
3267 my $authuser = $rpcenv->get_user();
3269 my $node = extract_param
($param, 'node');
3271 my $vmid = extract_param
($param, 'vmid');
3273 my $skiplock = extract_param
($param, 'skiplock');
3274 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3275 if $skiplock && $authuser ne 'root@pam';
3277 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
3282 __PACKAGE__-
>register_method({
3283 name
=> 'vm_feature',
3284 path
=> '{vmid}/feature',
3288 description
=> "Check if feature for virtual machine is available.",
3290 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3293 additionalProperties
=> 0,
3295 node
=> get_standard_option
('pve-node'),
3296 vmid
=> get_standard_option
('pve-vmid'),
3298 description
=> "Feature to check.",
3300 enum
=> [ 'snapshot', 'clone', 'copy' ],
3302 snapname
=> get_standard_option
('pve-snapshot-name', {
3310 hasFeature
=> { type
=> 'boolean' },
3313 items
=> { type
=> 'string' },
3320 my $node = extract_param
($param, 'node');
3322 my $vmid = extract_param
($param, 'vmid');
3324 my $snapname = extract_param
($param, 'snapname');
3326 my $feature = extract_param
($param, 'feature');
3328 my $running = PVE
::QemuServer
::check_running
($vmid);
3330 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3333 my $snap = $conf->{snapshots
}->{$snapname};
3334 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3337 my $storecfg = PVE
::Storage
::config
();
3339 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
3340 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
3343 hasFeature
=> $hasFeature,
3344 nodes
=> [ keys %$nodelist ],
3348 __PACKAGE__-
>register_method({
3350 path
=> '{vmid}/clone',
3354 description
=> "Create a copy of virtual machine/template.",
3356 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
3357 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3358 "'Datastore.AllocateSpace' on any used storage.",
3361 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
3363 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3364 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
3369 additionalProperties
=> 0,
3371 node
=> get_standard_option
('pve-node'),
3372 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3373 newid
=> get_standard_option
('pve-vmid', {
3374 completion
=> \
&PVE
::Cluster
::complete_next_vmid
,
3375 description
=> 'VMID for the clone.' }),
3378 type
=> 'string', format
=> 'dns-name',
3379 description
=> "Set a name for the new VM.",
3384 description
=> "Description for the new VM.",
3388 type
=> 'string', format
=> 'pve-poolid',
3389 description
=> "Add the new VM to the specified pool.",
3391 snapname
=> get_standard_option
('pve-snapshot-name', {
3394 storage
=> get_standard_option
('pve-storage-id', {
3395 description
=> "Target storage for full clone.",
3399 description
=> "Target format for file storage. Only valid for full clone.",
3402 enum
=> [ 'raw', 'qcow2', 'vmdk'],
3407 description
=> "Create a full copy of all disks. This is always done when " .
3408 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
3410 target
=> get_standard_option
('pve-node', {
3411 description
=> "Target node. Only allowed if the original VM is on shared storage.",
3415 description
=> "Override I/O bandwidth limit (in KiB/s).",
3419 default => 'clone limit from datacenter or storage config',
3429 my $rpcenv = PVE
::RPCEnvironment
::get
();
3430 my $authuser = $rpcenv->get_user();
3432 my $node = extract_param
($param, 'node');
3433 my $vmid = extract_param
($param, 'vmid');
3434 my $newid = extract_param
($param, 'newid');
3435 my $pool = extract_param
($param, 'pool');
3437 my $snapname = extract_param
($param, 'snapname');
3438 my $storage = extract_param
($param, 'storage');
3439 my $format = extract_param
($param, 'format');
3440 my $target = extract_param
($param, 'target');
3442 my $localnode = PVE
::INotify
::nodename
();
3444 if ($target && ($target eq $localnode || $target eq 'localhost')) {
3448 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
3450 my $load_and_check = sub {
3451 $rpcenv->check_pool_exist($pool) if defined($pool);
3452 PVE
::Cluster
::check_node_exists
($target) if $target;
3454 my $storecfg = PVE
::Storage
::config
();
3457 # check if storage is enabled on local node
3458 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
3460 # check if storage is available on target node
3461 PVE
::Storage
::storage_check_enabled
($storecfg, $storage, $target);
3462 # clone only works if target storage is shared
3463 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
3464 die "can't clone to non-shared storage '$storage'\n"
3465 if !$scfg->{shared
};
3469 PVE
::Cluster
::check_cfs_quorum
();
3471 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3472 PVE
::QemuConfig-
>check_lock($conf);
3474 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
3475 die "unexpected state change\n" if $verify_running != $running;
3477 die "snapshot '$snapname' does not exist\n"
3478 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
3480 my $full = $param->{full
} // !PVE
::QemuConfig-
>is_template($conf);
3482 die "parameter 'storage' not allowed for linked clones\n"
3483 if defined($storage) && !$full;
3485 die "parameter 'format' not allowed for linked clones\n"
3486 if defined($format) && !$full;
3488 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
3490 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
3492 die "can't clone VM to node '$target' (VM uses local storage)\n"
3493 if $target && !$sharedvm;
3495 my $conffile = PVE
::QemuConfig-
>config_file($newid);
3496 die "unable to create VM $newid: config file already exists\n"
3499 my $newconf = { lock => 'clone' };
3504 foreach my $opt (keys %$oldconf) {
3505 my $value = $oldconf->{$opt};
3507 # do not copy snapshot related info
3508 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3509 $opt eq 'vmstate' || $opt eq 'snapstate';
3511 # no need to copy unused images, because VMID(owner) changes anyways
3512 next if $opt =~ m/^unused\d+$/;
3514 die "cannot clone TPM state while VM is running\n"
3515 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3517 # always change MAC! address
3518 if ($opt =~ m/^net(\d+)$/) {
3519 my $net = PVE
::QemuServer
::parse_net
($value);
3520 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
3521 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
3522 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
3523 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
3524 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
3525 die "unable to parse drive options for '$opt'\n" if !$drive;
3526 if (PVE
::QemuServer
::drive_is_cdrom
($drive, 1)) {
3527 $newconf->{$opt} = $value; # simply copy configuration
3529 if ($full || PVE
::QemuServer
::drive_is_cloudinit
($drive)) {
3530 die "Full clone feature is not supported for drive '$opt'\n"
3531 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
3532 $fullclone->{$opt} = 1;
3534 # not full means clone instead of copy
3535 die "Linked clone feature is not supported for drive '$opt'\n"
3536 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
3538 $drives->{$opt} = $drive;
3539 next if PVE
::QemuServer
::drive_is_cloudinit
($drive);
3540 push @$vollist, $drive->{file
};
3543 # copy everything else
3544 $newconf->{$opt} = $value;
3548 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3552 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
3553 my $storecfg = PVE
::Storage
::config
();
3555 # auto generate a new uuid
3556 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
3557 $smbios1->{uuid
} = PVE
::QemuServer
::generate_uuid
();
3558 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
3559 # auto generate a new vmgenid only if the option was set for template
3560 if ($newconf->{vmgenid
}) {
3561 $newconf->{vmgenid
} = PVE
::QemuServer
::generate_uuid
();
3564 delete $newconf->{template
};
3566 if ($param->{name
}) {
3567 $newconf->{name
} = $param->{name
};
3569 $newconf->{name
} = "Copy-of-VM-" . ($oldconf->{name
} // $vmid);
3572 if ($param->{description
}) {
3573 $newconf->{description
} = $param->{description
};
3576 # create empty/temp config - this fails if VM already exists on other node
3577 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
3578 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
3580 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
3582 my $newvollist = [];
3589 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3591 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
3593 my $bwlimit = extract_param
($param, 'bwlimit');
3595 my $total_jobs = scalar(keys %{$drives});
3598 foreach my $opt (sort keys %$drives) {
3599 my $drive = $drives->{$opt};
3600 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3601 my $completion = $skipcomplete ?
'skip' : 'complete';
3603 my $src_sid = PVE
::Storage
::parse_volume_id
($drive->{file
});
3604 my $storage_list = [ $src_sid ];
3605 push @$storage_list, $storage if defined($storage);
3606 my $clonelimit = PVE
::Storage
::get_bandwidth_limit
('clone', $storage_list, $bwlimit);
3610 running
=> $running,
3613 snapname
=> $snapname,
3619 storage
=> $storage,
3623 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($oldconf)
3624 if $opt eq 'efidisk0';
3626 my $newdrive = PVE
::QemuServer
::clone_disk
(
3638 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($newdrive);
3640 PVE
::QemuConfig-
>write_config($newid, $newconf);
3644 delete $newconf->{lock};
3646 # do not write pending changes
3647 if (my @changes = keys %{$newconf->{pending
}}) {
3648 my $pending = join(',', @changes);
3649 warn "found pending changes for '$pending', discarding for clone\n";
3650 delete $newconf->{pending
};
3653 PVE
::QemuConfig-
>write_config($newid, $newconf);
3656 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3657 PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) if !$running;
3658 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
3660 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
3661 die "Failed to move config to node '$target' - rename failed: $!\n"
3662 if !rename($conffile, $newconffile);
3665 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
3668 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
3669 sleep 1; # some storage like rbd need to wait before release volume - really?
3671 foreach my $volid (@$newvollist) {
3672 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
3676 PVE
::Firewall
::remove_vmfw_conf
($newid);
3678 unlink $conffile; # avoid races -> last thing before die
3680 die "clone failed: $err";
3686 # Aquire exclusive lock lock for $newid
3687 my $lock_target_vm = sub {
3688 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
3691 my $lock_source_vm = sub {
3692 # exclusive lock if VM is running - else shared lock is enough;
3694 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $lock_target_vm);
3696 return PVE
::QemuConfig-
>lock_config_shared($vmid, 1, $lock_target_vm);
3700 $load_and_check->(); # early checks before forking/locking
3702 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
3705 __PACKAGE__-
>register_method({
3706 name
=> 'move_vm_disk',
3707 path
=> '{vmid}/move_disk',
3711 description
=> "Move volume to different storage or to a different VM.",
3713 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3714 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3715 "a disk to another VM, you need the permissions on the target VM as well.",
3716 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3719 additionalProperties
=> 0,
3721 node
=> get_standard_option
('pve-node'),
3722 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3723 'target-vmid' => get_standard_option
('pve-vmid', {
3724 completion
=> \
&PVE
::QemuServer
::complete_vmid
,
3729 description
=> "The disk you want to move.",
3730 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3732 storage
=> get_standard_option
('pve-storage-id', {
3733 description
=> "Target storage.",
3734 completion
=> \
&PVE
::QemuServer
::complete_storage
,
3739 description
=> "Target Format.",
3740 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
3745 description
=> "Delete the original disk after successful copy. By default the"
3746 ." original disk is kept as unused disk.",
3752 description
=> 'Prevent changes if current configuration file has different SHA1"
3753 ." digest. This can be used to prevent concurrent modifications.',
3758 description
=> "Override I/O bandwidth limit (in KiB/s).",
3762 default => 'move limit from datacenter or storage config',
3766 description
=> "The config key the disk will be moved to on the target VM"
3767 ." (for example, ide0 or scsi1). Default is the source disk key.",
3768 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names_with_unused
()],
3771 'target-digest' => {
3773 description
=> 'Prevent changes if the current config file of the target VM has a"
3774 ." different SHA1 digest. This can be used to detect concurrent modifications.',
3782 description
=> "the task ID.",
3787 my $rpcenv = PVE
::RPCEnvironment
::get
();
3788 my $authuser = $rpcenv->get_user();
3790 my $node = extract_param
($param, 'node');
3791 my $vmid = extract_param
($param, 'vmid');
3792 my $target_vmid = extract_param
($param, 'target-vmid');
3793 my $digest = extract_param
($param, 'digest');
3794 my $target_digest = extract_param
($param, 'target-digest');
3795 my $disk = extract_param
($param, 'disk');
3796 my $target_disk = extract_param
($param, 'target-disk') // $disk;
3797 my $storeid = extract_param
($param, 'storage');
3798 my $format = extract_param
($param, 'format');
3800 my $storecfg = PVE
::Storage
::config
();
3802 my $load_and_check_move = sub {
3803 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3804 PVE
::QemuConfig-
>check_lock($conf);
3806 PVE
::Tools
::assert_if_modified
($digest, $conf->{digest
});
3808 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3810 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
3812 die "disk '$disk' has no associated volume\n" if !$drive->{file
};
3813 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
3815 my $old_volid = $drive->{file
};
3817 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
3818 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3822 die "you can't move to the same storage with same format\n"
3823 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
3825 # this only checks snapshots because $disk is passed!
3826 my $snapshotted = PVE
::QemuServer
::Drive
::is_volume_in_use
(
3832 die "you can't move a disk with snapshots and delete the source\n"
3833 if $snapshotted && $param->{delete};
3835 return ($conf, $drive, $oldstoreid, $snapshotted);
3838 my $move_updatefn = sub {
3839 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
3840 my $old_volid = $drive->{file
};
3842 PVE
::Cluster
::log_msg
(
3845 "move disk VM $vmid: move --disk $disk --storage $storeid"
3848 my $running = PVE
::QemuServer
::check_running
($vmid);
3850 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
3852 my $newvollist = [];
3858 local $SIG{HUP
} = sub { die "interrupted by signal\n"; };
3860 warn "moving disk with snapshots, snapshots will not be moved!\n"
3863 my $bwlimit = extract_param
($param, 'bwlimit');
3864 my $movelimit = PVE
::Storage
::get_bandwidth_limit
(
3866 [$oldstoreid, $storeid],
3872 running
=> $running,
3881 storage
=> $storeid,
3885 $dest_info->{efisize
} = PVE
::QemuServer
::get_efivars_size
($conf)
3886 if $disk eq 'efidisk0';
3888 my $newdrive = PVE
::QemuServer
::clone_disk
(
3899 $conf->{$disk} = PVE
::QemuServer
::print_drive
($newdrive);
3901 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
3903 # convert moved disk to base if part of template
3904 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
3905 if PVE
::QemuConfig-
>is_template($conf);
3907 PVE
::QemuConfig-
>write_config($vmid, $conf);
3909 my $do_trim = PVE
::QemuServer
::get_qga_key
($conf, 'fstrim_cloned_disks');
3910 if ($running && $do_trim && PVE
::QemuServer
::qga_check_running
($vmid)) {
3911 eval { mon_cmd
($vmid, "guest-fstrim") };
3915 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
3916 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
3922 foreach my $volid (@$newvollist) {
3923 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid) };
3926 die "storage migration failed: $err";
3929 if ($param->{delete}) {
3931 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
3932 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
3938 my $load_and_check_reassign_configs = sub {
3939 my $vmlist = PVE
::Cluster
::get_vmlist
()->{ids
};
3941 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
3942 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
3944 my $source_node = $vmlist->{$vmid}->{node
};
3945 my $target_node = $vmlist->{$target_vmid}->{node
};
3947 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
3948 if $source_node ne $target_node;
3950 my $source_conf = PVE
::QemuConfig-
>load_config($vmid);
3951 PVE
::QemuConfig-
>check_lock($source_conf);
3952 my $target_conf = PVE
::QemuConfig-
>load_config($target_vmid);
3953 PVE
::QemuConfig-
>check_lock($target_conf);
3955 die "Can't move disks from or to template VMs\n"
3956 if ($source_conf->{template
} || $target_conf->{template
});
3959 eval { PVE
::Tools
::assert_if_modified
($digest, $source_conf->{digest
}) };
3960 die "VM ${vmid}: $@" if $@;
3963 if ($target_digest) {
3964 eval { PVE
::Tools
::assert_if_modified
($target_digest, $target_conf->{digest
}) };
3965 die "VM ${target_vmid}: $@" if $@;
3968 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
3970 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
3971 if $target_conf->{$target_disk};
3973 my $drive = PVE
::QemuServer
::parse_drive
(
3975 $source_conf->{$disk},
3977 die "failed to parse source disk - $@\n" if !$drive;
3979 my $source_volid = $drive->{file
};
3981 die "disk '${disk}' has no associated volume\n" if !$source_volid;
3982 die "CD drive contents can't be moved to another VM\n"
3983 if PVE
::QemuServer
::drive_is_cdrom
($drive, 1);
3985 my $storeid = PVE
::Storage
::parse_volume_id
($source_volid, 1);
3986 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
3988 die "Can't move disk used by a snapshot to another VM\n"
3989 if PVE
::QemuServer
::Drive
::is_volume_in_use
($storecfg, $source_conf, $disk, $source_volid);
3990 die "Storage does not support moving of this disk to another VM\n"
3991 if (!PVE
::Storage
::volume_has_feature
($storecfg, 'rename', $source_volid));
3992 die "Cannot move disk to another VM while the source VM is running - detach first\n"
3993 if PVE
::QemuServer
::check_running
($vmid) && $disk !~ m/^unused\d+$/;
3995 # now re-parse using target disk slot format
3996 if ($target_disk =~ /^unused\d+$/) {
3997 $drive = PVE
::QemuServer
::parse_drive
(
4002 $drive = PVE
::QemuServer
::parse_drive
(
4004 $source_conf->{$disk},
4007 die "failed to parse source disk for target disk format - $@\n" if !$drive;
4009 my $repl_conf = PVE
::ReplicationConfig-
>new();
4010 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4011 my $format = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4012 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4013 if !PVE
::Storage
::storage_can_replicate
($storecfg, $storeid, $format);
4016 return ($source_conf, $target_conf, $drive);
4021 print STDERR
"$msg\n";
4024 my $disk_reassignfn = sub {
4025 return PVE
::QemuConfig-
>lock_config($vmid, sub {
4026 return PVE
::QemuConfig-
>lock_config($target_vmid, sub {
4027 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
4029 my $source_volid = $drive->{file
};
4031 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4032 my ($storeid, $source_volname) = PVE
::Storage
::parse_volume_id
($source_volid);
4034 my $fmt = (PVE
::Storage
::parse_volname
($storecfg, $source_volid))[6];
4036 my $new_volid = PVE
::Storage
::rename_volume
(
4042 $drive->{file
} = $new_volid;
4044 my $boot_order = PVE
::QemuServer
::device_bootorder
($source_conf);
4045 if (defined(delete $boot_order->{$disk})) {
4046 print "removing disk '$disk' from boot order config\n";
4047 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4048 $source_conf->{boot
} = PVE
::QemuServer
::print_bootorder
($boot_devs);
4051 delete $source_conf->{$disk};
4052 print "removing disk '${disk}' from VM '${vmid}' config\n";
4053 PVE
::QemuConfig-
>write_config($vmid, $source_conf);
4055 my $drive_string = PVE
::QemuServer
::print_drive
($drive);
4057 if ($target_disk =~ /^unused\d+$/) {
4058 $target_conf->{$target_disk} = $drive_string;
4059 PVE
::QemuConfig-
>write_config($target_vmid, $target_conf);
4064 vmid
=> $target_vmid,
4065 digest
=> $target_digest,
4066 $target_disk => $drive_string,
4072 # remove possible replication snapshots
4073 if (PVE
::Storage
::volume_has_feature
(
4079 PVE
::Replication
::prepare
(
4089 print "Failed to remove replication snapshots on moved disk " .
4090 "'$target_disk'. Manual cleanup could be necessary.\n";
4097 if ($target_vmid && $storeid) {
4098 my $msg = "either set 'storage' or 'target-vmid', but not both";
4099 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4100 } elsif ($target_vmid) {
4101 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4102 if $authuser ne 'root@pam';
4104 raise_param_exc
({ 'target-vmid' => "must be different than source VMID to reassign disk" })
4105 if $vmid eq $target_vmid;
4107 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
4108 my $storage = PVE
::Storage
::parse_volume_id
($drive->{file
});
4109 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
4111 return $rpcenv->fork_worker(
4113 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4117 } elsif ($storeid) {
4118 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4120 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4121 if $disk =~ m/^unused\d+$/;
4123 $load_and_check_move->(); # early checks before forking/locking
4126 PVE
::QemuConfig-
>lock_config($vmid, $move_updatefn);
4129 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
4131 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4132 raise_param_exc
({ 'target-vmid' => $msg, 'storage' => $msg });
4136 my $check_vm_disks_local = sub {
4137 my ($storecfg, $vmconf, $vmid) = @_;
4139 my $local_disks = {};
4141 # add some more information to the disks e.g. cdrom
4142 PVE
::QemuServer
::foreach_volid
($vmconf, sub {
4143 my ($volid, $attr) = @_;
4145 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
4147 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
4148 return if $scfg->{shared
};
4150 # The shared attr here is just a special case where the vdisk
4151 # is marked as shared manually
4152 return if $attr->{shared
};
4153 return if $attr->{cdrom
} and $volid eq "none";
4155 if (exists $local_disks->{$volid}) {
4156 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4158 $local_disks->{$volid} = $attr;
4159 # ensure volid is present in case it's needed
4160 $local_disks->{$volid}->{volid
} = $volid;
4164 return $local_disks;
4167 __PACKAGE__-
>register_method({
4168 name
=> 'migrate_vm_precondition',
4169 path
=> '{vmid}/migrate',
4173 description
=> "Get preconditions for migration.",
4175 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4178 additionalProperties
=> 0,
4180 node
=> get_standard_option
('pve-node'),
4181 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4182 target
=> get_standard_option
('pve-node', {
4183 description
=> "Target node.",
4184 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4192 running
=> { type
=> 'boolean' },
4196 description
=> "List nodes allowed for offline migration, only passed if VM is offline"
4198 not_allowed_nodes
=> {
4201 description
=> "List not allowed nodes with additional informations, only passed if VM is offline"
4205 description
=> "List local disks including CD-Rom, unsused and not referenced disks"
4207 local_resources
=> {
4209 description
=> "List local resources e.g. pci, usb"
4216 my $rpcenv = PVE
::RPCEnvironment
::get
();
4218 my $authuser = $rpcenv->get_user();
4220 PVE
::Cluster
::check_cfs_quorum
();
4224 my $vmid = extract_param
($param, 'vmid');
4225 my $target = extract_param
($param, 'target');
4226 my $localnode = PVE
::INotify
::nodename
();
4230 my $vmconf = PVE
::QemuConfig-
>load_config($vmid);
4231 my $storecfg = PVE
::Storage
::config
();
4234 # try to detect errors early
4235 PVE
::QemuConfig-
>check_lock($vmconf);
4237 $res->{running
} = PVE
::QemuServer
::check_running
($vmid) ?
1:0;
4239 # if vm is not running, return target nodes where local storage is available
4240 # for offline migration
4241 if (!$res->{running
}) {
4242 $res->{allowed_nodes
} = [];
4243 my $checked_nodes = PVE
::QemuServer
::check_local_storage_availability
($vmconf, $storecfg);
4244 delete $checked_nodes->{$localnode};
4246 foreach my $node (keys %$checked_nodes) {
4247 if (!defined $checked_nodes->{$node}->{unavailable_storages
}) {
4248 push @{$res->{allowed_nodes
}}, $node;
4252 $res->{not_allowed_nodes
} = $checked_nodes;
4256 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4257 $res->{local_disks
} = [ values %$local_disks ];;
4259 my $local_resources = PVE
::QemuServer
::check_local_resources
($vmconf, 1);
4261 $res->{local_resources
} = $local_resources;
4268 __PACKAGE__-
>register_method({
4269 name
=> 'migrate_vm',
4270 path
=> '{vmid}/migrate',
4274 description
=> "Migrate virtual machine. Creates a new migration task.",
4276 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4279 additionalProperties
=> 0,
4281 node
=> get_standard_option
('pve-node'),
4282 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4283 target
=> get_standard_option
('pve-node', {
4284 description
=> "Target node.",
4285 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
4289 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4294 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
4299 enum
=> ['secure', 'insecure'],
4300 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
4303 migration_network
=> {
4304 type
=> 'string', format
=> 'CIDR',
4305 description
=> "CIDR of the (sub) network that is used for migration.",
4308 "with-local-disks" => {
4310 description
=> "Enable live storage migration for local disk",
4313 targetstorage
=> get_standard_option
('pve-targetstorage', {
4314 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4317 description
=> "Override I/O bandwidth limit (in KiB/s).",
4321 default => 'migrate limit from datacenter or storage config',
4327 description
=> "the task ID.",
4332 my $rpcenv = PVE
::RPCEnvironment
::get
();
4333 my $authuser = $rpcenv->get_user();
4335 my $target = extract_param
($param, 'target');
4337 my $localnode = PVE
::INotify
::nodename
();
4338 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
4340 PVE
::Cluster
::check_cfs_quorum
();
4342 PVE
::Cluster
::check_node_exists
($target);
4344 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
4346 my $vmid = extract_param
($param, 'vmid');
4348 raise_param_exc
({ force
=> "Only root may use this option." })
4349 if $param->{force
} && $authuser ne 'root@pam';
4351 raise_param_exc
({ migration_type
=> "Only root may use this option." })
4352 if $param->{migration_type
} && $authuser ne 'root@pam';
4354 # allow root only until better network permissions are available
4355 raise_param_exc
({ migration_network
=> "Only root may use this option." })
4356 if $param->{migration_network
} && $authuser ne 'root@pam';
4359 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4361 # try to detect errors early
4363 PVE
::QemuConfig-
>check_lock($conf);
4365 if (PVE
::QemuServer
::check_running
($vmid)) {
4366 die "can't migrate running VM without --online\n" if !$param->{online
};
4368 my $repl_conf = PVE
::ReplicationConfig-
>new();
4369 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4370 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
4371 if (!$param->{force
} && $is_replicated && !$is_replicated_to_target) {
4372 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4373 "target. Use 'force' to override.\n";
4376 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4377 $param->{online
} = 0;
4380 my $storecfg = PVE
::Storage
::config
();
4381 if (my $targetstorage = $param->{targetstorage
}) {
4382 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($targetstorage, 'pve-storage-id') };
4383 raise_param_exc
({ targetstorage
=> "failed to parse storage map: $@" })
4386 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4387 if !defined($storagemap->{identity
});
4389 foreach my $target_sid (values %{$storagemap->{entries
}}) {
4390 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
4393 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
4394 if $storagemap->{default};
4396 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target)
4397 if $storagemap->{identity
};
4399 $param->{storagemap
} = $storagemap;
4401 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
4404 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
4409 print "Requesting HA migration for VM $vmid to node $target\n";
4411 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
4412 PVE
::Tools
::run_command
($cmd);
4416 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4421 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
4425 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
4428 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
4433 __PACKAGE__-
>register_method({
4434 name
=> 'remote_migrate_vm',
4435 path
=> '{vmid}/remote_migrate',
4439 description
=> "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4441 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4444 additionalProperties
=> 0,
4446 node
=> get_standard_option
('pve-node'),
4447 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4448 'target-vmid' => get_standard_option
('pve-vmid', { optional
=> 1 }),
4449 'target-endpoint' => get_standard_option
('proxmox-remote', {
4450 description
=> "Remote target endpoint",
4454 description
=> "Use online/live migration if VM is running. Ignored if VM is stopped.",
4459 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.",
4463 'target-storage' => get_standard_option
('pve-targetstorage', {
4464 completion
=> \
&PVE
::QemuServer
::complete_migration_storage
,
4467 'target-bridge' => {
4469 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.",
4470 format
=> 'bridge-pair-list',
4473 description
=> "Override I/O bandwidth limit (in KiB/s).",
4477 default => 'migrate limit from datacenter or storage config',
4483 description
=> "the task ID.",
4488 my $rpcenv = PVE
::RPCEnvironment
::get
();
4489 my $authuser = $rpcenv->get_user();
4491 my $source_vmid = extract_param
($param, 'vmid');
4492 my $target_endpoint = extract_param
($param, 'target-endpoint');
4493 my $target_vmid = extract_param
($param, 'target-vmid') // $source_vmid;
4495 my $delete = extract_param
($param, 'delete') // 0;
4497 PVE
::Cluster
::check_cfs_quorum
();
4500 my $conf = PVE
::QemuConfig-
>load_config($source_vmid);
4502 PVE
::QemuConfig-
>check_lock($conf);
4504 raise_param_exc
({ vmid
=> "cannot migrate HA-managed VM to remote cluster" })
4505 if PVE
::HA
::Config
::vm_is_ha_managed
($source_vmid);
4507 my $remote = PVE
::JSONSchema
::parse_property_string
('proxmox-remote', $target_endpoint);
4509 # TODO: move this as helper somewhere appropriate?
4511 protocol
=> 'https',
4512 host
=> $remote->{host
},
4513 port
=> $remote->{port
} // 8006,
4514 apitoken
=> $remote->{apitoken
},
4518 if ($fp = $remote->{fingerprint
}) {
4519 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
4522 print "Establishing API connection with remote at '$remote->{host}'\n";
4524 my $api_client = PVE
::APIClient
::LWP-
>new(%$conn_args);
4526 if (!defined($fp)) {
4527 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4528 foreach my $cert (@$cert_info) {
4529 my $filename = $cert->{filename
};
4530 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4531 $fp = $cert->{fingerprint
} if !$fp || $filename eq 'pveproxy-ssl.pem';
4533 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 }
4537 my $repl_conf = PVE
::ReplicationConfig-
>new();
4538 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4539 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4541 if (PVE
::QemuServer
::check_running
($source_vmid)) {
4542 die "can't migrate running VM without --online\n" if !$param->{online
};
4545 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online
};
4546 $param->{online
} = 0;
4549 my $storecfg = PVE
::Storage
::config
();
4550 my $target_storage = extract_param
($param, 'target-storage');
4551 my $storagemap = eval { PVE
::JSONSchema
::parse_idmap
($target_storage, 'pve-storage-id') };
4552 raise_param_exc
({ 'target-storage' => "failed to parse storage map: $@" })
4555 my $target_bridge = extract_param
($param, 'target-bridge');
4556 my $bridgemap = eval { PVE
::JSONSchema
::parse_idmap
($target_bridge, 'pve-bridge-id') };
4557 raise_param_exc
({ 'target-bridge' => "failed to parse bridge map: $@" })
4560 die "remote migration requires explicit storage mapping!\n"
4561 if $storagemap->{identity
};
4563 $param->{storagemap
} = $storagemap;
4564 $param->{bridgemap
} = $bridgemap;
4565 $param->{remote
} = {
4566 conn
=> $conn_args, # re-use fingerprint for tunnel
4567 client
=> $api_client,
4568 vmid
=> $target_vmid,
4570 $param->{migration_type
} = 'websocket';
4571 $param->{'with-local-disks'} = 1;
4572 $param->{delete} = $delete if $delete;
4574 my $cluster_status = $api_client->get("/cluster/status");
4576 foreach my $entry (@$cluster_status) {
4577 next if $entry->{type
} ne 'node';
4578 if ($entry->{local}) {
4579 $target_node = $entry->{name
};
4584 die "couldn't determine endpoint's node name\n"
4585 if !defined($target_node);
4588 PVE
::QemuMigrate-
>migrate($target_node, $remote->{host
}, $source_vmid, $param);
4592 return PVE
::GuestHelpers
::guest_migration_lock
($source_vmid, 10, $realcmd);
4595 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4598 __PACKAGE__-
>register_method({
4600 path
=> '{vmid}/monitor',
4604 description
=> "Execute Qemu monitor commands.",
4606 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
4607 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
4610 additionalProperties
=> 0,
4612 node
=> get_standard_option
('pve-node'),
4613 vmid
=> get_standard_option
('pve-vmid'),
4616 description
=> "The monitor command.",
4620 returns
=> { type
=> 'string'},
4624 my $rpcenv = PVE
::RPCEnvironment
::get
();
4625 my $authuser = $rpcenv->get_user();
4628 my $command = shift;
4629 return $command =~ m/^\s*info(\s+|$)/
4630 || $command =~ m/^\s*help\s*$/;
4633 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4634 if !&$is_ro($param->{command
});
4636 my $vmid = $param->{vmid
};
4638 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
4642 $res = PVE
::QemuServer
::Monitor
::hmp_cmd
($vmid, $param->{command
});
4644 $res = "ERROR: $@" if $@;
4649 __PACKAGE__-
>register_method({
4650 name
=> 'resize_vm',
4651 path
=> '{vmid}/resize',
4655 description
=> "Extend volume size.",
4657 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
4660 additionalProperties
=> 0,
4662 node
=> get_standard_option
('pve-node'),
4663 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4664 skiplock
=> get_standard_option
('skiplock'),
4667 description
=> "The disk you want to resize.",
4668 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
4672 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
4673 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.",
4677 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4683 returns
=> { type
=> 'null'},
4687 my $rpcenv = PVE
::RPCEnvironment
::get
();
4689 my $authuser = $rpcenv->get_user();
4691 my $node = extract_param
($param, 'node');
4693 my $vmid = extract_param
($param, 'vmid');
4695 my $digest = extract_param
($param, 'digest');
4697 my $disk = extract_param
($param, 'disk');
4699 my $sizestr = extract_param
($param, 'size');
4701 my $skiplock = extract_param
($param, 'skiplock');
4702 raise_param_exc
({ skiplock
=> "Only root may use this option." })
4703 if $skiplock && $authuser ne 'root@pam';
4705 my $storecfg = PVE
::Storage
::config
();
4707 my $updatefn = sub {
4709 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4711 die "checksum missmatch (file change by other user?)\n"
4712 if $digest && $digest ne $conf->{digest
};
4713 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
4715 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4717 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
4719 my (undef, undef, undef, undef, undef, undef, $format) =
4720 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
4722 die "can't resize volume: $disk if snapshot exists\n"
4723 if %{$conf->{snapshots
}} && $format eq 'qcow2';
4725 my $volid = $drive->{file
};
4727 die "disk '$disk' has no associated volume\n" if !$volid;
4729 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
4731 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
4733 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4735 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
4736 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
4738 die "Could not determine current size of volume '$volid'\n" if !defined($size);
4740 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4741 my ($ext, $newsize, $unit) = ($1, $2, $4);
4744 $newsize = $newsize * 1024;
4745 } elsif ($unit eq 'M') {
4746 $newsize = $newsize * 1024 * 1024;
4747 } elsif ($unit eq 'G') {
4748 $newsize = $newsize * 1024 * 1024 * 1024;
4749 } elsif ($unit eq 'T') {
4750 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4753 $newsize += $size if $ext;
4754 $newsize = int($newsize);
4756 die "shrinking disks is not supported\n" if $newsize < $size;
4758 return if $size == $newsize;
4760 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
4762 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
4764 $drive->{size
} = $newsize;
4765 $conf->{$disk} = PVE
::QemuServer
::print_drive
($drive);
4767 PVE
::QemuConfig-
>write_config($vmid, $conf);
4770 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
4774 __PACKAGE__-
>register_method({
4775 name
=> 'snapshot_list',
4776 path
=> '{vmid}/snapshot',
4778 description
=> "List all snapshots.",
4780 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4783 protected
=> 1, # qemu pid files are only readable by root
4785 additionalProperties
=> 0,
4787 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4788 node
=> get_standard_option
('pve-node'),
4797 description
=> "Snapshot identifier. Value 'current' identifies the current VM.",
4801 description
=> "Snapshot includes RAM.",
4806 description
=> "Snapshot description.",
4810 description
=> "Snapshot creation time",
4812 renderer
=> 'timestamp',
4816 description
=> "Parent snapshot identifier.",
4822 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
4827 my $vmid = $param->{vmid
};
4829 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4830 my $snaphash = $conf->{snapshots
} || {};
4834 foreach my $name (keys %$snaphash) {
4835 my $d = $snaphash->{$name};
4838 snaptime
=> $d->{snaptime
} || 0,
4839 vmstate
=> $d->{vmstate
} ?
1 : 0,
4840 description
=> $d->{description
} || '',
4842 $item->{parent
} = $d->{parent
} if $d->{parent
};
4843 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
4847 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
4850 digest
=> $conf->{digest
},
4851 running
=> $running,
4852 description
=> "You are here!",
4854 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
4856 push @$res, $current;
4861 __PACKAGE__-
>register_method({
4863 path
=> '{vmid}/snapshot',
4867 description
=> "Snapshot a VM.",
4869 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4872 additionalProperties
=> 0,
4874 node
=> get_standard_option
('pve-node'),
4875 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
4876 snapname
=> get_standard_option
('pve-snapshot-name'),
4880 description
=> "Save the vmstate",
4885 description
=> "A textual description or comment.",
4891 description
=> "the task ID.",
4896 my $rpcenv = PVE
::RPCEnvironment
::get
();
4898 my $authuser = $rpcenv->get_user();
4900 my $node = extract_param
($param, 'node');
4902 my $vmid = extract_param
($param, 'vmid');
4904 my $snapname = extract_param
($param, 'snapname');
4906 die "unable to use snapshot name 'current' (reserved name)\n"
4907 if $snapname eq 'current';
4909 die "unable to use snapshot name 'pending' (reserved name)\n"
4910 if lc($snapname) eq 'pending';
4913 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
4914 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
4915 $param->{description
});
4918 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
4921 __PACKAGE__-
>register_method({
4922 name
=> 'snapshot_cmd_idx',
4923 path
=> '{vmid}/snapshot/{snapname}',
4930 additionalProperties
=> 0,
4932 vmid
=> get_standard_option
('pve-vmid'),
4933 node
=> get_standard_option
('pve-node'),
4934 snapname
=> get_standard_option
('pve-snapshot-name'),
4943 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
4950 push @$res, { cmd
=> 'rollback' };
4951 push @$res, { cmd
=> 'config' };
4956 __PACKAGE__-
>register_method({
4957 name
=> 'update_snapshot_config',
4958 path
=> '{vmid}/snapshot/{snapname}/config',
4962 description
=> "Update snapshot metadata.",
4964 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4967 additionalProperties
=> 0,
4969 node
=> get_standard_option
('pve-node'),
4970 vmid
=> get_standard_option
('pve-vmid'),
4971 snapname
=> get_standard_option
('pve-snapshot-name'),
4975 description
=> "A textual description or comment.",
4979 returns
=> { type
=> 'null' },
4983 my $rpcenv = PVE
::RPCEnvironment
::get
();
4985 my $authuser = $rpcenv->get_user();
4987 my $vmid = extract_param
($param, 'vmid');
4989 my $snapname = extract_param
($param, 'snapname');
4991 return if !defined($param->{description
});
4993 my $updatefn = sub {
4995 my $conf = PVE
::QemuConfig-
>load_config($vmid);
4997 PVE
::QemuConfig-
>check_lock($conf);
4999 my $snap = $conf->{snapshots
}->{$snapname};
5001 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5003 $snap->{description
} = $param->{description
} if defined($param->{description
});
5005 PVE
::QemuConfig-
>write_config($vmid, $conf);
5008 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
5013 __PACKAGE__-
>register_method({
5014 name
=> 'get_snapshot_config',
5015 path
=> '{vmid}/snapshot/{snapname}/config',
5018 description
=> "Get snapshot configuration",
5020 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any
=> 1],
5023 additionalProperties
=> 0,
5025 node
=> get_standard_option
('pve-node'),
5026 vmid
=> get_standard_option
('pve-vmid'),
5027 snapname
=> get_standard_option
('pve-snapshot-name'),
5030 returns
=> { type
=> "object" },
5034 my $rpcenv = PVE
::RPCEnvironment
::get
();
5036 my $authuser = $rpcenv->get_user();
5038 my $vmid = extract_param
($param, 'vmid');
5040 my $snapname = extract_param
($param, 'snapname');
5042 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5044 my $snap = $conf->{snapshots
}->{$snapname};
5046 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5051 __PACKAGE__-
>register_method({
5053 path
=> '{vmid}/snapshot/{snapname}/rollback',
5057 description
=> "Rollback VM state to specified snapshot.",
5059 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any
=> 1],
5062 additionalProperties
=> 0,
5064 node
=> get_standard_option
('pve-node'),
5065 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5066 snapname
=> get_standard_option
('pve-snapshot-name'),
5069 description
=> "Whether the VM should get started after rolling back successfully",
5077 description
=> "the task ID.",
5082 my $rpcenv = PVE
::RPCEnvironment
::get
();
5084 my $authuser = $rpcenv->get_user();
5086 my $node = extract_param
($param, 'node');
5088 my $vmid = extract_param
($param, 'vmid');
5090 my $snapname = extract_param
($param, 'snapname');
5093 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
5094 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
5096 if ($param->{start
}) {
5097 PVE
::API2
::Qemu-
>vm_start({ vmid
=> $vmid, node
=> $node });
5102 # hold migration lock, this makes sure that nobody create replication snapshots
5103 return PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $realcmd);
5106 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
5109 __PACKAGE__-
>register_method({
5110 name
=> 'delsnapshot',
5111 path
=> '{vmid}/snapshot/{snapname}',
5115 description
=> "Delete a VM snapshot.",
5117 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5120 additionalProperties
=> 0,
5122 node
=> get_standard_option
('pve-node'),
5123 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5124 snapname
=> get_standard_option
('pve-snapshot-name'),
5128 description
=> "For removal from config file, even if removing disk snapshots fails.",
5134 description
=> "the task ID.",
5139 my $rpcenv = PVE
::RPCEnvironment
::get
();
5141 my $authuser = $rpcenv->get_user();
5143 my $node = extract_param
($param, 'node');
5145 my $vmid = extract_param
($param, 'vmid');
5147 my $snapname = extract_param
($param, 'snapname');
5150 my $do_delete = sub {
5152 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
5153 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
5157 if ($param->{force
}) {
5160 eval { PVE
::GuestHelpers
::guest_migration_lock
($vmid, 10, $do_delete); };
5162 die $err if $lock_obtained;
5163 die "Failed to obtain guest migration lock - replication running?\n";
5168 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
5171 __PACKAGE__-
>register_method({
5173 path
=> '{vmid}/template',
5177 description
=> "Create a Template.",
5179 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
5180 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
5183 additionalProperties
=> 0,
5185 node
=> get_standard_option
('pve-node'),
5186 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
5190 description
=> "If you want to convert only 1 disk to base image.",
5191 enum
=> [PVE
::QemuServer
::Drive
::valid_drive_names
()],
5198 description
=> "the task ID.",
5203 my $rpcenv = PVE
::RPCEnvironment
::get
();
5205 my $authuser = $rpcenv->get_user();
5207 my $node = extract_param
($param, 'node');
5209 my $vmid = extract_param
($param, 'vmid');
5211 my $disk = extract_param
($param, 'disk');
5213 my $load_and_check = sub {
5214 my $conf = PVE
::QemuConfig-
>load_config($vmid);
5216 PVE
::QemuConfig-
>check_lock($conf);
5218 die "unable to create template, because VM contains snapshots\n"
5219 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
5221 die "you can't convert a template to a template\n"
5222 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
5224 die "you can't convert a VM to template if VM is running\n"
5225 if PVE
::QemuServer
::check_running
($vmid);
5230 $load_and_check->();
5233 PVE
::QemuConfig-
>lock_config($vmid, sub {
5234 my $conf = $load_and_check->();
5236 $conf->{template
} = 1;
5237 PVE
::QemuConfig-
>write_config($vmid, $conf);
5239 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
5243 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
5246 __PACKAGE__-
>register_method({
5247 name
=> 'cloudinit_generated_config_dump',
5248 path
=> '{vmid}/cloudinit/dump',
5251 description
=> "Get automatically generated cloudinit config.",
5253 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5256 additionalProperties
=> 0,
5258 node
=> get_standard_option
('pve-node'),
5259 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
5261 description
=> 'Config type.',
5263 enum
=> ['user', 'network', 'meta'],
5273 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
5275 return PVE
::QemuServer
::Cloudinit
::dump_cloudinit_config
($conf, $param->{vmid
}, $param->{type
});
5278 __PACKAGE__-
>register_method({
5280 path
=> '{vmid}/mtunnel',
5283 description
=> 'Migration tunnel endpoint - only for internal use by VM migration.',
5287 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5288 ['perm', '/', [ 'Sys.Incoming' ]],
5290 description
=> "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5291 " on '/'. Further permission checks happen during the actual migration.",
5294 additionalProperties
=> 0,
5296 node
=> get_standard_option
('pve-node'),
5297 vmid
=> get_standard_option
('pve-vmid'),
5300 format
=> 'pve-storage-id-list',
5302 description
=> 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5306 format
=> 'pve-bridge-id-list',
5308 description
=> 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5313 additionalProperties
=> 0,
5315 upid
=> { type
=> 'string' },
5316 ticket
=> { type
=> 'string' },
5317 socket => { type
=> 'string' },
5323 my $rpcenv = PVE
::RPCEnvironment
::get
();
5324 my $authuser = $rpcenv->get_user();
5326 my $node = extract_param
($param, 'node');
5327 my $vmid = extract_param
($param, 'vmid');
5329 my $storages = extract_param
($param, 'storages');
5330 my $bridges = extract_param
($param, 'bridges');
5332 my $nodename = PVE
::INotify
::nodename
();
5334 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5335 if $node ne 'localhost' && $node ne $nodename;
5339 my $storecfg = PVE
::Storage
::config
();
5340 foreach my $storeid (PVE
::Tools
::split_list
($storages)) {
5341 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5344 foreach my $bridge (PVE
::Tools
::split_list
($bridges)) {
5345 PVE
::Network
::read_bridge_mtu
($bridge);
5348 PVE
::Cluster
::check_cfs_quorum
();
5350 my $lock = 'create';
5351 eval { PVE
::QemuConfig-
>create_and_lock_config($vmid, 0, $lock); };
5353 raise_param_exc
({ vmid
=> "unable to create empty VM config - $@"})
5358 storecfg
=> PVE
::Storage
::config
(),
5363 my $run_locked = sub {
5364 my ($code, $params) = @_;
5365 return PVE
::QemuConfig-
>lock_config($state->{vmid
}, sub {
5366 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5368 $state->{conf
} = $conf;
5370 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5371 if $state->{lock} && !PVE
::QemuConfig-
>has_lock($conf, $state->{lock});
5373 return $code->($params);
5381 description
=> 'Full VM config, adapted for target cluster/node',
5383 'firewall-config' => {
5385 description
=> 'VM firewall config',
5390 format
=> PVE
::JSONSchema
::get_standard_option
('pve-qm-image-format'),
5393 format
=> 'pve-storage-id',
5397 description
=> 'parsed drive information without volid and format',
5403 description
=> 'params passed to vm_start_nolock',
5407 description
=> 'migrate_opts passed to vm_start_nolock',
5413 description
=> 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5419 description
=> 'remove VM config and disks, aborting migration',
5423 'disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'disk-import'},
5424 'query-disk-import' => $PVE::StorageTunnel
::cmd_schema-
>{'query-disk-import'},
5425 bwlimit
=> $PVE::StorageTunnel
::cmd_schema-
>{bwlimit
},
5428 my $cmd_handlers = {
5430 # compared against other end's version
5431 # bump/reset for breaking changes
5432 # bump/bump for opt-in changes
5434 api
=> $PVE::QemuMigrate
::WS_TUNNEL_VERSION
,
5441 # parse and write out VM FW config if given
5442 if (my $fw_conf = $params->{'firewall-config'}) {
5443 my ($path, $fh) = PVE
::Tools
::tempfile_contents
($fw_conf, 700);
5450 ipset_comments
=> {},
5452 my $cluster_fw_conf = PVE
::Firewall
::load_clusterfw_conf
();
5454 # TODO: add flag for strict parsing?
5455 # TODO: add import sub that does all this given raw content?
5456 my $vmfw_conf = PVE
::Firewall
::generic_fw_config_parser
($path, $cluster_fw_conf, $empty_conf, 'vm');
5457 $vmfw_conf->{vmid
} = $state->{vmid
};
5458 PVE
::Firewall
::save_vmfw_conf
($state->{vmid
}, $vmfw_conf);
5460 $state->{cleanup
}->{fw
} = 1;
5463 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5464 my $new_conf = PVE
::QemuServer
::parse_vm_config
($conf_fn, $params->{conf
}, 1);
5465 delete $new_conf->{lock};
5466 delete $new_conf->{digest
};
5468 # TODO handle properly?
5469 delete $new_conf->{snapshots
};
5470 delete $new_conf->{parent
};
5471 delete $new_conf->{pending
};
5473 # not handled by update_vm_api
5474 my $vmgenid = delete $new_conf->{vmgenid
};
5475 my $meta = delete $new_conf->{meta
};
5476 my $cloudinit = delete $new_conf->{cloudinit
}; # this is informational only
5477 $new_conf->{skip_cloud_init
} = 1; # re-use image from source side
5479 $new_conf->{vmid
} = $state->{vmid
};
5480 $new_conf->{node
} = $node;
5482 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, 'create');
5485 $update_vm_api->($new_conf, 1);
5488 # revert to locked previous config
5489 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5490 $conf->{lock} = 'create';
5491 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5496 my $conf = PVE
::QemuConfig-
>load_config($state->{vmid
});
5497 $conf->{lock} = 'migrate';
5498 $conf->{vmgenid
} = $vmgenid if defined($vmgenid);
5499 $conf->{meta
} = $meta if defined($meta);
5500 $conf->{cloudinit
} = $cloudinit if defined($cloudinit);
5501 PVE
::QemuConfig-
>write_config($state->{vmid
}, $conf);
5503 $state->{lock} = 'migrate';
5509 return PVE
::StorageTunnel
::handle_bwlimit
($params);
5514 my $format = $params->{format
};
5515 my $storeid = $params->{storage
};
5516 my $drive = $params->{drive
};
5518 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg
}, $storeid, $node);
5521 default => $storeid,
5524 my $source_volumes = {
5535 my $res = PVE
::QemuServer
::vm_migrate_alloc_nbd_disks
($state->{storecfg
}, $state->{vmid
}, $source_volumes, $storagemap);
5536 if (defined($res->{disk
})) {
5537 $state->{cleanup
}->{volumes
}->{$res->{disk
}->{volid
}} = 1;
5538 return $res->{disk
};
5540 die "failed to allocate NBD disk..\n";
5543 'disk-import' => sub {
5546 $check_storage_access_migrate->(
5554 $params->{unix
} = "/run/qemu-server/$state->{vmid}.storage";
5556 return PVE
::StorageTunnel
::handle_disk_import
($state, $params);
5558 'query-disk-import' => sub {
5561 return PVE
::StorageTunnel
::handle_query_disk_import
($state, $params);
5566 my $info = PVE
::QemuServer
::vm_start_nolock
(
5570 $params->{start_params
},
5571 $params->{migrate_opts
},
5575 if ($info->{migrate
}->{proto
} ne 'unix') {
5576 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5577 die "migration over non-UNIX sockets not possible\n";
5580 my $socket = $info->{migrate
}->{addr
};
5581 chown $state->{socket_uid
}, -1, $socket;
5582 $state->{sockets
}->{$socket} = 1;
5584 my $unix_sockets = $info->{migrate
}->{unix_sockets
};
5585 foreach my $socket (@$unix_sockets) {
5586 chown $state->{socket_uid
}, -1, $socket;
5587 $state->{sockets
}->{$socket} = 1;
5592 if (PVE
::QemuServer
::qga_check_running
($state->{vmid
})) {
5593 eval { mon_cmd
($state->{vmid
}, "guest-fstrim") };
5594 warn "fstrim failed: $@\n" if $@;
5599 PVE
::QemuServer
::vm_stop
(undef, $state->{vmid
}, 1, 1);
5603 PVE
::QemuServer
::nbd_stop
($state->{vmid
});
5607 if (PVE
::QemuServer
::Helpers
::vm_running_locally
($state->{vmid
})) {
5608 PVE
::QemuServer
::vm_resume
($state->{vmid
}, 1, 1);
5610 die "VM $state->{vmid} not running\n";
5615 PVE
::QemuConfig-
>remove_lock($state->{vmid
}, $state->{lock});
5616 delete $state->{lock};
5622 my $path = $params->{path
};
5624 die "Not allowed to generate ticket for unknown socket '$path'\n"
5625 if !defined($state->{sockets
}->{$path});
5627 return { ticket
=> PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$path") };
5632 if ($params->{cleanup
}) {
5633 if ($state->{cleanup
}->{fw
}) {
5634 PVE
::Firewall
::remove_vmfw_conf
($state->{vmid
});
5637 for my $volid (keys $state->{cleanup
}->{volumes
}->%*) {
5638 print "freeing volume '$volid' as part of cleanup\n";
5639 eval { PVE
::Storage
::vdisk_free
($state->{storecfg
}, $volid) };
5643 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5646 print "switching to exit-mode, waiting for client to disconnect\n";
5653 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5654 unlink $socket_addr;
5656 $state->{socket} = IO
::Socket
::UNIX-
>new(
5657 Type
=> SOCK_STREAM
(),
5658 Local
=> $socket_addr,
5662 $state->{socket_uid
} = getpwnam('www-data')
5663 or die "Failed to resolve user 'www-data' to numeric UID\n";
5664 chown $state->{socket_uid
}, -1, $socket_addr;
5667 print "mtunnel started\n";
5669 my $conn = eval { PVE
::Tools
::run_with_timeout
(300, sub { $state->{socket}->accept() }) };
5671 warn "Failed to accept tunnel connection - $@\n";
5673 warn "Removing tunnel socket..\n";
5674 unlink $state->{socket};
5676 warn "Removing temporary VM config..\n";
5678 PVE
::QemuServer
::destroy_vm
($state->{storecfg
}, $state->{vmid
}, 1);
5681 die "Exiting mtunnel\n";
5684 $state->{conn
} = $conn;
5686 my $reply_err = sub {
5689 my $reply = JSON
::encode_json
({
5690 success
=> JSON
::false
,
5693 $conn->print("$reply\n");
5697 my $reply_ok = sub {
5700 $res->{success
} = JSON
::true
;
5701 my $reply = JSON
::encode_json
($res);
5702 $conn->print("$reply\n");
5706 while (my $line = <$conn>) {
5709 # untaint, we validate below if needed
5710 ($line) = $line =~ /^(.*)$/;
5711 my $parsed = eval { JSON
::decode_json
($line) };
5713 $reply_err->("failed to parse command - $@");
5717 my $cmd = delete $parsed->{cmd
};
5718 if (!defined($cmd)) {
5719 $reply_err->("'cmd' missing");
5720 } elsif ($state->{exit}) {
5721 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5723 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5724 print "received command '$cmd'\n";
5726 if ($cmd_desc->{$cmd}) {
5727 PVE
::JSONSchema
::validate
($parsed, $cmd_desc->{$cmd});
5731 my $res = $run_locked->($handler, $parsed);
5734 $reply_err->("failed to handle '$cmd' command - $@")
5737 $reply_err->("unknown command '$cmd' given");
5741 if ($state->{exit}) {
5742 print "mtunnel exited\n";
5744 die "mtunnel exited unexpectedly\n";
5748 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
5749 my $ticket = PVE
::AccessControl
::assemble_tunnel_ticket
($authuser, "/socket/$socket_addr");
5750 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
5755 socket => $socket_addr,
5759 __PACKAGE__-
>register_method({
5760 name
=> 'mtunnelwebsocket',
5761 path
=> '{vmid}/mtunnelwebsocket',
5764 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.",
5765 user
=> 'all', # check inside
5767 description
=> 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
5769 additionalProperties
=> 0,
5771 node
=> get_standard_option
('pve-node'),
5772 vmid
=> get_standard_option
('pve-vmid'),
5775 description
=> "unix socket to forward to",
5779 description
=> "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
5786 port
=> { type
=> 'string', optional
=> 1 },
5787 socket => { type
=> 'string', optional
=> 1 },
5793 my $rpcenv = PVE
::RPCEnvironment
::get
();
5794 my $authuser = $rpcenv->get_user();
5796 my $nodename = PVE
::INotify
::nodename
();
5797 my $node = extract_param
($param, 'node');
5799 raise_param_exc
({ node
=> "node needs to be 'localhost' or local hostname '$nodename'" })
5800 if $node ne 'localhost' && $node ne $nodename;
5802 my $vmid = $param->{vmid
};
5804 PVE
::QemuConfig-
>load_config($vmid);
5806 my $socket = $param->{socket};
5807 PVE
::AccessControl
::verify_tunnel_ticket
($param->{ticket
}, $authuser, "/socket/$socket");
5809 return { socket => $socket };