]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu.pm
Remove guest states to ensure no old states are exists
[qemu-server.git] / PVE / API2 / Qemu.pm
1 package PVE::API2::Qemu;
2
3 use strict;
4 use warnings;
5 use Cwd 'abs_path';
6 use Net::SSLeay;
7 use UUID;
8 use POSIX;
9 use IO::Socket::IP;
10
11 use PVE::Cluster qw (cfs_read_file cfs_write_file);;
12 use PVE::SafeSyslog;
13 use PVE::Tools qw(extract_param);
14 use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
15 use PVE::Storage;
16 use PVE::JSONSchema qw(get_standard_option);
17 use PVE::RESTHandler;
18 use PVE::ReplicationConfig;
19 use PVE::GuestHelpers;
20 use PVE::QemuConfig;
21 use PVE::QemuServer;
22 use PVE::QemuMigrate;
23 use PVE::RPCEnvironment;
24 use PVE::AccessControl;
25 use PVE::INotify;
26 use PVE::Network;
27 use PVE::Firewall;
28 use PVE::API2::Firewall::VM;
29
30 BEGIN {
31 if (!$ENV{PVE_GENERATING_DOCS}) {
32 require PVE::HA::Env::PVE2;
33 import PVE::HA::Env::PVE2;
34 require PVE::HA::Config;
35 import PVE::HA::Config;
36 }
37 }
38
39 use Data::Dumper; # fixme: remove
40
41 use base qw(PVE::RESTHandler);
42
43 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.";
44
45 my $resolve_cdrom_alias = sub {
46 my $param = shift;
47
48 if (my $value = $param->{cdrom}) {
49 $value .= ",media=cdrom" if $value !~ m/media=/;
50 $param->{ide2} = $value;
51 delete $param->{cdrom};
52 }
53 };
54
55 my $check_storage_access = sub {
56 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
57
58 PVE::QemuServer::foreach_drive($settings, sub {
59 my ($ds, $drive) = @_;
60
61 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
62
63 my $volid = $drive->{file};
64
65 if (!$volid || $volid eq 'none') {
66 # nothing to check
67 } elsif ($isCDROM && ($volid eq 'cdrom')) {
68 $rpcenv->check($authuser, "/", ['Sys.Console']);
69 } elsif (!$isCDROM && ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/)) {
70 my ($storeid, $size) = ($2 || $default_storage, $3);
71 die "no storage ID specified (and no default storage)\n" if !$storeid;
72 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
73 } else {
74 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
75 }
76 });
77 };
78
79 my $check_storage_access_clone = sub {
80 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
81
82 my $sharedvm = 1;
83
84 PVE::QemuServer::foreach_drive($conf, sub {
85 my ($ds, $drive) = @_;
86
87 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
88
89 my $volid = $drive->{file};
90
91 return if !$volid || $volid eq 'none';
92
93 if ($isCDROM) {
94 if ($volid eq 'cdrom') {
95 $rpcenv->check($authuser, "/", ['Sys.Console']);
96 } else {
97 # we simply allow access
98 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
99 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
100 $sharedvm = 0 if !$scfg->{shared};
101
102 }
103 } else {
104 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
105 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
106 $sharedvm = 0 if !$scfg->{shared};
107
108 $sid = $storage if $storage;
109 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
110 }
111 });
112
113 return $sharedvm;
114 };
115
116 # Note: $pool is only needed when creating a VM, because pool permissions
117 # are automatically inherited if VM already exists inside a pool.
118 my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
119 my $create_disks = sub {
120 my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
121
122 my $vollist = [];
123
124 my $res = {};
125
126 my $code = sub {
127 my ($ds, $disk) = @_;
128
129 my $volid = $disk->{file};
130
131 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
132 delete $disk->{size};
133 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
134 } elsif ($volid =~ $NEW_DISK_RE) {
135 my ($storeid, $size) = ($2 || $default_storage, $3);
136 die "no storage ID specified (and no default storage)\n" if !$storeid;
137 my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
138 my $fmt = $disk->{format} || $defformat;
139
140 my $volid;
141 if ($ds eq 'efidisk0') {
142 # handle efidisk
143 my $ovmfvars = '/usr/share/kvm/OVMF_VARS-pure-efi.fd';
144 die "uefi vars image not found\n" if ! -f $ovmfvars;
145 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
146 $fmt, undef, 128);
147 $disk->{file} = $volid;
148 $disk->{size} = 128*1024;
149 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
150 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
151 my $qemufmt = PVE::QemuServer::qemu_img_format($scfg, $volname);
152 my $path = PVE::Storage::path($storecfg, $volid);
153 my $efidiskcmd = ['/usr/bin/qemu-img', 'convert', '-n', '-f', 'raw', '-O', $qemufmt];
154 push @$efidiskcmd, $ovmfvars;
155 push @$efidiskcmd, $path;
156
157 PVE::Storage::activate_volumes($storecfg, [$volid]);
158
159 eval { PVE::Tools::run_command($efidiskcmd); };
160 my $err = $@;
161 die "Copying of EFI Vars image failed: $err" if $err;
162 } else {
163 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
164 $fmt, undef, $size*1024*1024);
165 $disk->{file} = $volid;
166 $disk->{size} = $size*1024*1024*1024;
167 }
168 push @$vollist, $volid;
169 delete $disk->{format}; # no longer needed
170 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
171 } else {
172
173 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
174
175 my $volid_is_new = 1;
176
177 if ($conf->{$ds}) {
178 my $olddrive = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
179 $volid_is_new = undef if $olddrive->{file} && $olddrive->{file} eq $volid;
180 }
181
182 if ($volid_is_new) {
183
184 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
185
186 PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid;
187
188 my $size = PVE::Storage::volume_size_info($storecfg, $volid);
189
190 die "volume $volid does not exists\n" if !$size;
191
192 $disk->{size} = $size;
193 }
194
195 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
196 }
197 };
198
199 eval { PVE::QemuServer::foreach_drive($settings, $code); };
200
201 # free allocated images on error
202 if (my $err = $@) {
203 syslog('err', "VM $vmid creating disks failed");
204 foreach my $volid (@$vollist) {
205 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
206 warn $@ if $@;
207 }
208 die $err;
209 }
210
211 # modify vm config if everything went well
212 foreach my $ds (keys %$res) {
213 $conf->{$ds} = $res->{$ds};
214 }
215
216 return $vollist;
217 };
218
219 my $cpuoptions = {
220 'cores' => 1,
221 'cpu' => 1,
222 'cpulimit' => 1,
223 'cpuunits' => 1,
224 'numa' => 1,
225 'smp' => 1,
226 'sockets' => 1,
227 'vcpus' => 1,
228 };
229
230 my $memoryoptions = {
231 'memory' => 1,
232 'balloon' => 1,
233 'shares' => 1,
234 };
235
236 my $hwtypeoptions = {
237 'acpi' => 1,
238 'hotplug' => 1,
239 'kvm' => 1,
240 'machine' => 1,
241 'scsihw' => 1,
242 'smbios1' => 1,
243 'tablet' => 1,
244 'vga' => 1,
245 'watchdog' => 1,
246 };
247
248 my $generaloptions = {
249 'agent' => 1,
250 'autostart' => 1,
251 'bios' => 1,
252 'description' => 1,
253 'keyboard' => 1,
254 'localtime' => 1,
255 'migrate_downtime' => 1,
256 'migrate_speed' => 1,
257 'name' => 1,
258 'onboot' => 1,
259 'ostype' => 1,
260 'protection' => 1,
261 'reboot' => 1,
262 'startdate' => 1,
263 'startup' => 1,
264 'tdf' => 1,
265 'template' => 1,
266 };
267
268 my $vmpoweroptions = {
269 'freeze' => 1,
270 };
271
272 my $diskoptions = {
273 'boot' => 1,
274 'bootdisk' => 1,
275 };
276
277 my $check_vm_modify_config_perm = sub {
278 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
279
280 return 1 if $authuser eq 'root@pam';
281
282 foreach my $opt (@$key_list) {
283 # disk checks need to be done somewhere else
284 next if PVE::QemuServer::is_valid_drivename($opt);
285 next if $opt eq 'cdrom';
286 next if $opt =~ m/^unused\d+$/;
287
288 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
289 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
290 } elsif ($memoryoptions->{$opt}) {
291 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
292 } elsif ($hwtypeoptions->{$opt}) {
293 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
294 } elsif ($generaloptions->{$opt}) {
295 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
296 # special case for startup since it changes host behaviour
297 if ($opt eq 'startup') {
298 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
299 }
300 } elsif ($vmpoweroptions->{$opt}) {
301 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
302 } elsif ($diskoptions->{$opt}) {
303 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
304 } elsif ($opt =~ m/^net\d+$/) {
305 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
306 } else {
307 # catches usb\d+, hostpci\d+, args, lock, etc.
308 # new options will be checked here
309 die "only root can set '$opt' config\n";
310 }
311 }
312
313 return 1;
314 };
315
316 __PACKAGE__->register_method({
317 name => 'vmlist',
318 path => '',
319 method => 'GET',
320 description => "Virtual machine index (per node).",
321 permissions => {
322 description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
323 user => 'all',
324 },
325 proxyto => 'node',
326 protected => 1, # qemu pid files are only readable by root
327 parameters => {
328 additionalProperties => 0,
329 properties => {
330 node => get_standard_option('pve-node'),
331 full => {
332 type => 'boolean',
333 optional => 1,
334 description => "Determine the full status of active VMs.",
335 },
336 },
337 },
338 returns => {
339 type => 'array',
340 items => {
341 type => "object",
342 properties => {},
343 },
344 links => [ { rel => 'child', href => "{vmid}" } ],
345 },
346 code => sub {
347 my ($param) = @_;
348
349 my $rpcenv = PVE::RPCEnvironment::get();
350 my $authuser = $rpcenv->get_user();
351
352 my $vmstatus = PVE::QemuServer::vmstatus(undef, $param->{full});
353
354 my $res = [];
355 foreach my $vmid (keys %$vmstatus) {
356 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
357
358 my $data = $vmstatus->{$vmid};
359 $data->{vmid} = int($vmid);
360 push @$res, $data;
361 }
362
363 return $res;
364 }});
365
366
367
368 __PACKAGE__->register_method({
369 name => 'create_vm',
370 path => '',
371 method => 'POST',
372 description => "Create or restore a virtual machine.",
373 permissions => {
374 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
375 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
376 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
377 user => 'all', # check inside
378 },
379 protected => 1,
380 proxyto => 'node',
381 parameters => {
382 additionalProperties => 0,
383 properties => PVE::QemuServer::json_config_properties(
384 {
385 node => get_standard_option('pve-node'),
386 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
387 archive => {
388 description => "The backup file.",
389 type => 'string',
390 optional => 1,
391 maxLength => 255,
392 completion => \&PVE::QemuServer::complete_backup_archives,
393 },
394 storage => get_standard_option('pve-storage-id', {
395 description => "Default storage.",
396 optional => 1,
397 completion => \&PVE::QemuServer::complete_storage,
398 }),
399 force => {
400 optional => 1,
401 type => 'boolean',
402 description => "Allow to overwrite existing VM.",
403 requires => 'archive',
404 },
405 unique => {
406 optional => 1,
407 type => 'boolean',
408 description => "Assign a unique random ethernet address.",
409 requires => 'archive',
410 },
411 pool => {
412 optional => 1,
413 type => 'string', format => 'pve-poolid',
414 description => "Add the VM to the specified pool.",
415 },
416 }),
417 },
418 returns => {
419 type => 'string',
420 },
421 code => sub {
422 my ($param) = @_;
423
424 my $rpcenv = PVE::RPCEnvironment::get();
425
426 my $authuser = $rpcenv->get_user();
427
428 my $node = extract_param($param, 'node');
429
430 my $vmid = extract_param($param, 'vmid');
431
432 my $archive = extract_param($param, 'archive');
433
434 my $storage = extract_param($param, 'storage');
435
436 my $force = extract_param($param, 'force');
437
438 my $unique = extract_param($param, 'unique');
439
440 my $pool = extract_param($param, 'pool');
441
442 my $filename = PVE::QemuConfig->config_file($vmid);
443
444 my $storecfg = PVE::Storage::config();
445
446 PVE::Cluster::check_cfs_quorum();
447
448 if (defined($pool)) {
449 $rpcenv->check_pool_exist($pool);
450 }
451
452 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
453 if defined($storage);
454
455 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
456 # OK
457 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
458 # OK
459 } elsif ($archive && $force && (-f $filename) &&
460 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
461 # OK: user has VM.Backup permissions, and want to restore an existing VM
462 } else {
463 raise_perm_exc();
464 }
465
466 if (!$archive) {
467 &$resolve_cdrom_alias($param);
468
469 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
470
471 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
472
473 foreach my $opt (keys %$param) {
474 if (PVE::QemuServer::is_valid_drivename($opt)) {
475 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
476 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
477
478 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
479 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
480 }
481 }
482
483 PVE::QemuServer::add_random_macs($param);
484 } else {
485 my $keystr = join(' ', keys %$param);
486 raise_param_exc({ archive => "option conflicts with other options ($keystr)"}) if $keystr;
487
488 if ($archive eq '-') {
489 die "pipe requires cli environment\n"
490 if $rpcenv->{type} ne 'cli';
491 } else {
492 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $archive);
493 $archive = PVE::Storage::abs_filesystem_path($storecfg, $archive);
494 }
495 }
496
497 my $restorefn = sub {
498 my $vmlist = PVE::Cluster::get_vmlist();
499 if ($vmlist->{ids}->{$vmid}) {
500 my $current_node = $vmlist->{ids}->{$vmid}->{node};
501 if ($current_node eq $node) {
502 my $conf = PVE::QemuConfig->load_config($vmid);
503
504 PVE::QemuConfig->check_protection($conf, "unable to restore VM $vmid");
505
506 die "unable to restore vm $vmid - config file already exists\n"
507 if !$force;
508
509 die "unable to restore vm $vmid - vm is running\n"
510 if PVE::QemuServer::check_running($vmid);
511
512 die "unable to restore vm $vmid - vm is a template\n"
513 if PVE::QemuConfig->is_template($conf);
514
515 } else {
516 die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n";
517 }
518 }
519
520 my $realcmd = sub {
521 PVE::QemuServer::restore_archive($archive, $vmid, $authuser, {
522 storage => $storage,
523 pool => $pool,
524 unique => $unique });
525
526 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
527 };
528
529 # ensure no old replication state are exists
530 PVE::ReplicationState::delete_guest_states($vmid);
531
532 return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd);
533 };
534
535 my $createfn = sub {
536
537 # test after locking
538 PVE::Cluster::check_vmid_unused($vmid);
539
540 # ensure no old replication state are exists
541 PVE::ReplicationState::delete_guest_states($vmid);
542
543 my $realcmd = sub {
544
545 my $vollist = [];
546
547 my $conf = $param;
548
549 eval {
550
551 $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage);
552
553 # try to be smart about bootdisk
554 my @disks = PVE::QemuServer::valid_drive_names();
555 my $firstdisk;
556 foreach my $ds (reverse @disks) {
557 next if !$conf->{$ds};
558 my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
559 next if PVE::QemuServer::drive_is_cdrom($disk);
560 $firstdisk = $ds;
561 }
562
563 if (!$conf->{bootdisk} && $firstdisk) {
564 $conf->{bootdisk} = $firstdisk;
565 }
566
567 # auto generate uuid if user did not specify smbios1 option
568 if (!$conf->{smbios1}) {
569 my ($uuid, $uuid_str);
570 UUID::generate($uuid);
571 UUID::unparse($uuid, $uuid_str);
572 $conf->{smbios1} = "uuid=$uuid_str";
573 }
574
575 PVE::QemuConfig->write_config($vmid, $conf);
576
577 };
578 my $err = $@;
579
580 if ($err) {
581 foreach my $volid (@$vollist) {
582 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
583 warn $@ if $@;
584 }
585 die "create failed - $err";
586 }
587
588 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
589 };
590
591 return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd);
592 };
593
594 return PVE::QemuConfig->lock_config_full($vmid, 1, $archive ? $restorefn : $createfn);
595 }});
596
597 __PACKAGE__->register_method({
598 name => 'vmdiridx',
599 path => '{vmid}',
600 method => 'GET',
601 proxyto => 'node',
602 description => "Directory index",
603 permissions => {
604 user => 'all',
605 },
606 parameters => {
607 additionalProperties => 0,
608 properties => {
609 node => get_standard_option('pve-node'),
610 vmid => get_standard_option('pve-vmid'),
611 },
612 },
613 returns => {
614 type => 'array',
615 items => {
616 type => "object",
617 properties => {
618 subdir => { type => 'string' },
619 },
620 },
621 links => [ { rel => 'child', href => "{subdir}" } ],
622 },
623 code => sub {
624 my ($param) = @_;
625
626 my $res = [
627 { subdir => 'config' },
628 { subdir => 'pending' },
629 { subdir => 'status' },
630 { subdir => 'unlink' },
631 { subdir => 'vncproxy' },
632 { subdir => 'migrate' },
633 { subdir => 'resize' },
634 { subdir => 'move' },
635 { subdir => 'rrd' },
636 { subdir => 'rrddata' },
637 { subdir => 'monitor' },
638 { subdir => 'agent' },
639 { subdir => 'snapshot' },
640 { subdir => 'spiceproxy' },
641 { subdir => 'sendkey' },
642 { subdir => 'firewall' },
643 ];
644
645 return $res;
646 }});
647
648 __PACKAGE__->register_method ({
649 subclass => "PVE::API2::Firewall::VM",
650 path => '{vmid}/firewall',
651 });
652
653 __PACKAGE__->register_method({
654 name => 'rrd',
655 path => '{vmid}/rrd',
656 method => 'GET',
657 protected => 1, # fixme: can we avoid that?
658 permissions => {
659 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
660 },
661 description => "Read VM RRD statistics (returns PNG)",
662 parameters => {
663 additionalProperties => 0,
664 properties => {
665 node => get_standard_option('pve-node'),
666 vmid => get_standard_option('pve-vmid'),
667 timeframe => {
668 description => "Specify the time frame you are interested in.",
669 type => 'string',
670 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
671 },
672 ds => {
673 description => "The list of datasources you want to display.",
674 type => 'string', format => 'pve-configid-list',
675 },
676 cf => {
677 description => "The RRD consolidation function",
678 type => 'string',
679 enum => [ 'AVERAGE', 'MAX' ],
680 optional => 1,
681 },
682 },
683 },
684 returns => {
685 type => "object",
686 properties => {
687 filename => { type => 'string' },
688 },
689 },
690 code => sub {
691 my ($param) = @_;
692
693 return PVE::Cluster::create_rrd_graph(
694 "pve2-vm/$param->{vmid}", $param->{timeframe},
695 $param->{ds}, $param->{cf});
696
697 }});
698
699 __PACKAGE__->register_method({
700 name => 'rrddata',
701 path => '{vmid}/rrddata',
702 method => 'GET',
703 protected => 1, # fixme: can we avoid that?
704 permissions => {
705 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
706 },
707 description => "Read VM RRD statistics",
708 parameters => {
709 additionalProperties => 0,
710 properties => {
711 node => get_standard_option('pve-node'),
712 vmid => get_standard_option('pve-vmid'),
713 timeframe => {
714 description => "Specify the time frame you are interested in.",
715 type => 'string',
716 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
717 },
718 cf => {
719 description => "The RRD consolidation function",
720 type => 'string',
721 enum => [ 'AVERAGE', 'MAX' ],
722 optional => 1,
723 },
724 },
725 },
726 returns => {
727 type => "array",
728 items => {
729 type => "object",
730 properties => {},
731 },
732 },
733 code => sub {
734 my ($param) = @_;
735
736 return PVE::Cluster::create_rrd_data(
737 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
738 }});
739
740
741 __PACKAGE__->register_method({
742 name => 'vm_config',
743 path => '{vmid}/config',
744 method => 'GET',
745 proxyto => 'node',
746 description => "Get current virtual machine configuration. This does not include pending configuration changes (see 'pending' API).",
747 permissions => {
748 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
749 },
750 parameters => {
751 additionalProperties => 0,
752 properties => {
753 node => get_standard_option('pve-node'),
754 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
755 current => {
756 description => "Get current values (instead of pending values).",
757 optional => 1,
758 default => 0,
759 type => 'boolean',
760 },
761 },
762 },
763 returns => {
764 type => "object",
765 properties => {
766 digest => {
767 type => 'string',
768 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
769 }
770 },
771 },
772 code => sub {
773 my ($param) = @_;
774
775 my $conf = PVE::QemuConfig->load_config($param->{vmid});
776
777 delete $conf->{snapshots};
778
779 if (!$param->{current}) {
780 foreach my $opt (keys %{$conf->{pending}}) {
781 next if $opt eq 'delete';
782 my $value = $conf->{pending}->{$opt};
783 next if ref($value); # just to be sure
784 $conf->{$opt} = $value;
785 }
786 my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete});
787 foreach my $opt (keys %$pending_delete_hash) {
788 delete $conf->{$opt} if $conf->{$opt};
789 }
790 }
791
792 delete $conf->{pending};
793
794 return $conf;
795 }});
796
797 __PACKAGE__->register_method({
798 name => 'vm_pending',
799 path => '{vmid}/pending',
800 method => 'GET',
801 proxyto => 'node',
802 description => "Get virtual machine configuration, including pending changes.",
803 permissions => {
804 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
805 },
806 parameters => {
807 additionalProperties => 0,
808 properties => {
809 node => get_standard_option('pve-node'),
810 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
811 },
812 },
813 returns => {
814 type => "array",
815 items => {
816 type => "object",
817 properties => {
818 key => {
819 description => "Configuration option name.",
820 type => 'string',
821 },
822 value => {
823 description => "Current value.",
824 type => 'string',
825 optional => 1,
826 },
827 pending => {
828 description => "Pending value.",
829 type => 'string',
830 optional => 1,
831 },
832 delete => {
833 description => "Indicates a pending delete request if present and not 0. " .
834 "The value 2 indicates a force-delete request.",
835 type => 'integer',
836 minimum => 0,
837 maximum => 2,
838 optional => 1,
839 },
840 },
841 },
842 },
843 code => sub {
844 my ($param) = @_;
845
846 my $conf = PVE::QemuConfig->load_config($param->{vmid});
847
848 my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete});
849
850 my $res = [];
851
852 foreach my $opt (keys %$conf) {
853 next if ref($conf->{$opt});
854 my $item = { key => $opt };
855 $item->{value} = $conf->{$opt} if defined($conf->{$opt});
856 $item->{pending} = $conf->{pending}->{$opt} if defined($conf->{pending}->{$opt});
857 $item->{delete} = ($pending_delete_hash->{$opt} ? 2 : 1) if exists $pending_delete_hash->{$opt};
858 push @$res, $item;
859 }
860
861 foreach my $opt (keys %{$conf->{pending}}) {
862 next if $opt eq 'delete';
863 next if ref($conf->{pending}->{$opt}); # just to be sure
864 next if defined($conf->{$opt});
865 my $item = { key => $opt };
866 $item->{pending} = $conf->{pending}->{$opt};
867 push @$res, $item;
868 }
869
870 while (my ($opt, $force) = each %$pending_delete_hash) {
871 next if $conf->{pending}->{$opt}; # just to be sure
872 next if $conf->{$opt};
873 my $item = { key => $opt, delete => ($force ? 2 : 1)};
874 push @$res, $item;
875 }
876
877 return $res;
878 }});
879
880 # POST/PUT {vmid}/config implementation
881 #
882 # The original API used PUT (idempotent) an we assumed that all operations
883 # are fast. But it turned out that almost any configuration change can
884 # involve hot-plug actions, or disk alloc/free. Such actions can take long
885 # time to complete and have side effects (not idempotent).
886 #
887 # The new implementation uses POST and forks a worker process. We added
888 # a new option 'background_delay'. If specified we wait up to
889 # 'background_delay' second for the worker task to complete. It returns null
890 # if the task is finished within that time, else we return the UPID.
891
892 my $update_vm_api = sub {
893 my ($param, $sync) = @_;
894
895 my $rpcenv = PVE::RPCEnvironment::get();
896
897 my $authuser = $rpcenv->get_user();
898
899 my $node = extract_param($param, 'node');
900
901 my $vmid = extract_param($param, 'vmid');
902
903 my $digest = extract_param($param, 'digest');
904
905 my $background_delay = extract_param($param, 'background_delay');
906
907 my @paramarr = (); # used for log message
908 foreach my $key (keys %$param) {
909 push @paramarr, "-$key", $param->{$key};
910 }
911
912 my $skiplock = extract_param($param, 'skiplock');
913 raise_param_exc({ skiplock => "Only root may use this option." })
914 if $skiplock && $authuser ne 'root@pam';
915
916 my $delete_str = extract_param($param, 'delete');
917
918 my $revert_str = extract_param($param, 'revert');
919
920 my $force = extract_param($param, 'force');
921
922 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
923
924 my $storecfg = PVE::Storage::config();
925
926 my $defaults = PVE::QemuServer::load_defaults();
927
928 &$resolve_cdrom_alias($param);
929
930 # now try to verify all parameters
931
932 my $revert = {};
933 foreach my $opt (PVE::Tools::split_list($revert_str)) {
934 if (!PVE::QemuServer::option_exists($opt)) {
935 raise_param_exc({ revert => "unknown option '$opt'" });
936 }
937
938 raise_param_exc({ delete => "you can't use '-$opt' and " .
939 "-revert $opt' at the same time" })
940 if defined($param->{$opt});
941
942 $revert->{$opt} = 1;
943 }
944
945 my @delete = ();
946 foreach my $opt (PVE::Tools::split_list($delete_str)) {
947 $opt = 'ide2' if $opt eq 'cdrom';
948
949 raise_param_exc({ delete => "you can't use '-$opt' and " .
950 "-delete $opt' at the same time" })
951 if defined($param->{$opt});
952
953 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
954 "-revert $opt' at the same time" })
955 if $revert->{$opt};
956
957 if (!PVE::QemuServer::option_exists($opt)) {
958 raise_param_exc({ delete => "unknown option '$opt'" });
959 }
960
961 push @delete, $opt;
962 }
963
964 my $repl_conf = PVE::ReplicationConfig->new();
965 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
966 my $check_replication = sub {
967 my ($drive) = @_;
968 return if !$is_replicated;
969 my $volid = $drive->{file};
970 return if !$volid || !($drive->{replicate}//1);
971 return if PVE::QemuServer::drive_is_cdrom($drive);
972 my ($storeid, $format);
973 if ($volid =~ $NEW_DISK_RE) {
974 $storeid = $2;
975 $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
976 } else {
977 ($storeid, undef) = PVE::Storage::parse_volume_id($volid, 1);
978 $format = (PVE::Storage::parse_volname($storecfg, $volid))[6];
979 }
980 return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
981 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
982 return if $scfg->{shared};
983 die "cannot add non-replicatable volume to a replicated VM\n";
984 };
985
986 foreach my $opt (keys %$param) {
987 if (PVE::QemuServer::is_valid_drivename($opt)) {
988 # cleanup drive path
989 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
990 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
991 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
992 $check_replication->($drive);
993 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
994 } elsif ($opt =~ m/^net(\d+)$/) {
995 # add macaddr
996 my $net = PVE::QemuServer::parse_net($param->{$opt});
997 $param->{$opt} = PVE::QemuServer::print_net($net);
998 }
999 }
1000
1001 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1002
1003 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1004
1005 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1006
1007 my $updatefn = sub {
1008
1009 my $conf = PVE::QemuConfig->load_config($vmid);
1010
1011 die "checksum missmatch (file change by other user?)\n"
1012 if $digest && $digest ne $conf->{digest};
1013
1014 PVE::QemuConfig->check_lock($conf) if !$skiplock;
1015
1016 foreach my $opt (keys %$revert) {
1017 if (defined($conf->{$opt})) {
1018 $param->{$opt} = $conf->{$opt};
1019 } elsif (defined($conf->{pending}->{$opt})) {
1020 push @delete, $opt;
1021 }
1022 }
1023
1024 if ($param->{memory} || defined($param->{balloon})) {
1025 my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
1026 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
1027
1028 die "balloon value too large (must be smaller than assigned memory)\n"
1029 if $balloon && $balloon > $maxmem;
1030 }
1031
1032 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1033
1034 my $worker = sub {
1035
1036 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1037
1038 # write updates to pending section
1039
1040 my $modified = {}; # record what $option we modify
1041
1042 foreach my $opt (@delete) {
1043 $modified->{$opt} = 1;
1044 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1045 if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
1046 warn "cannot delete '$opt' - not set in current configuration!\n";
1047 $modified->{$opt} = 0;
1048 next;
1049 }
1050
1051 if ($opt =~ m/^unused/) {
1052 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
1053 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
1054 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1055 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1056 delete $conf->{$opt};
1057 PVE::QemuConfig->write_config($vmid, $conf);
1058 }
1059 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1060 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
1061 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1062 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1063 if defined($conf->{pending}->{$opt});
1064 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1065 PVE::QemuConfig->write_config($vmid, $conf);
1066 } else {
1067 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1068 PVE::QemuConfig->write_config($vmid, $conf);
1069 }
1070 }
1071
1072 foreach my $opt (keys %$param) { # add/change
1073 $modified->{$opt} = 1;
1074 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1075 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1076
1077 if (PVE::QemuServer::is_valid_drivename($opt)) {
1078 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1079 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1080 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1081 } else {
1082 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1083 }
1084 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1085 if defined($conf->{pending}->{$opt});
1086
1087 &$create_disks($rpcenv, $authuser, $conf->{pending}, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1088 } else {
1089 $conf->{pending}->{$opt} = $param->{$opt};
1090 }
1091 PVE::QemuServer::vmconfig_undelete_pending_option($conf, $opt);
1092 PVE::QemuConfig->write_config($vmid, $conf);
1093 }
1094
1095 # remove pending changes when nothing changed
1096 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1097 my $changes = PVE::QemuServer::vmconfig_cleanup_pending($conf);
1098 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
1099
1100 return if !scalar(keys %{$conf->{pending}});
1101
1102 my $running = PVE::QemuServer::check_running($vmid);
1103
1104 # apply pending changes
1105
1106 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1107
1108 if ($running) {
1109 my $errors = {};
1110 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
1111 raise_param_exc($errors) if scalar(keys %$errors);
1112 } else {
1113 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $running);
1114 }
1115
1116 return;
1117 };
1118
1119 if ($sync) {
1120 &$worker();
1121 return undef;
1122 } else {
1123 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1124
1125 if ($background_delay) {
1126
1127 # Note: It would be better to do that in the Event based HTTPServer
1128 # to avoid blocking call to sleep.
1129
1130 my $end_time = time() + $background_delay;
1131
1132 my $task = PVE::Tools::upid_decode($upid);
1133
1134 my $running = 1;
1135 while (time() < $end_time) {
1136 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1137 last if !$running;
1138 sleep(1); # this gets interrupted when child process ends
1139 }
1140
1141 if (!$running) {
1142 my $status = PVE::Tools::upid_read_status($upid);
1143 return undef if $status eq 'OK';
1144 die $status;
1145 }
1146 }
1147
1148 return $upid;
1149 }
1150 };
1151
1152 return PVE::QemuConfig->lock_config($vmid, $updatefn);
1153 };
1154
1155 my $vm_config_perm_list = [
1156 'VM.Config.Disk',
1157 'VM.Config.CDROM',
1158 'VM.Config.CPU',
1159 'VM.Config.Memory',
1160 'VM.Config.Network',
1161 'VM.Config.HWType',
1162 'VM.Config.Options',
1163 ];
1164
1165 __PACKAGE__->register_method({
1166 name => 'update_vm_async',
1167 path => '{vmid}/config',
1168 method => 'POST',
1169 protected => 1,
1170 proxyto => 'node',
1171 description => "Set virtual machine options (asynchrounous API).",
1172 permissions => {
1173 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1174 },
1175 parameters => {
1176 additionalProperties => 0,
1177 properties => PVE::QemuServer::json_config_properties(
1178 {
1179 node => get_standard_option('pve-node'),
1180 vmid => get_standard_option('pve-vmid'),
1181 skiplock => get_standard_option('skiplock'),
1182 delete => {
1183 type => 'string', format => 'pve-configid-list',
1184 description => "A list of settings you want to delete.",
1185 optional => 1,
1186 },
1187 revert => {
1188 type => 'string', format => 'pve-configid-list',
1189 description => "Revert a pending change.",
1190 optional => 1,
1191 },
1192 force => {
1193 type => 'boolean',
1194 description => $opt_force_description,
1195 optional => 1,
1196 requires => 'delete',
1197 },
1198 digest => {
1199 type => 'string',
1200 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1201 maxLength => 40,
1202 optional => 1,
1203 },
1204 background_delay => {
1205 type => 'integer',
1206 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1207 minimum => 1,
1208 maximum => 30,
1209 optional => 1,
1210 },
1211 }),
1212 },
1213 returns => {
1214 type => 'string',
1215 optional => 1,
1216 },
1217 code => $update_vm_api,
1218 });
1219
1220 __PACKAGE__->register_method({
1221 name => 'update_vm',
1222 path => '{vmid}/config',
1223 method => 'PUT',
1224 protected => 1,
1225 proxyto => 'node',
1226 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1227 permissions => {
1228 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1229 },
1230 parameters => {
1231 additionalProperties => 0,
1232 properties => PVE::QemuServer::json_config_properties(
1233 {
1234 node => get_standard_option('pve-node'),
1235 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1236 skiplock => get_standard_option('skiplock'),
1237 delete => {
1238 type => 'string', format => 'pve-configid-list',
1239 description => "A list of settings you want to delete.",
1240 optional => 1,
1241 },
1242 revert => {
1243 type => 'string', format => 'pve-configid-list',
1244 description => "Revert a pending change.",
1245 optional => 1,
1246 },
1247 force => {
1248 type => 'boolean',
1249 description => $opt_force_description,
1250 optional => 1,
1251 requires => 'delete',
1252 },
1253 digest => {
1254 type => 'string',
1255 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1256 maxLength => 40,
1257 optional => 1,
1258 },
1259 }),
1260 },
1261 returns => { type => 'null' },
1262 code => sub {
1263 my ($param) = @_;
1264 &$update_vm_api($param, 1);
1265 return undef;
1266 }
1267 });
1268
1269
1270 __PACKAGE__->register_method({
1271 name => 'destroy_vm',
1272 path => '{vmid}',
1273 method => 'DELETE',
1274 protected => 1,
1275 proxyto => 'node',
1276 description => "Destroy the vm (also delete all used/owned volumes).",
1277 permissions => {
1278 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1279 },
1280 parameters => {
1281 additionalProperties => 0,
1282 properties => {
1283 node => get_standard_option('pve-node'),
1284 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1285 skiplock => get_standard_option('skiplock'),
1286 },
1287 },
1288 returns => {
1289 type => 'string',
1290 },
1291 code => sub {
1292 my ($param) = @_;
1293
1294 my $rpcenv = PVE::RPCEnvironment::get();
1295
1296 my $authuser = $rpcenv->get_user();
1297
1298 my $vmid = $param->{vmid};
1299
1300 my $skiplock = $param->{skiplock};
1301 raise_param_exc({ skiplock => "Only root may use this option." })
1302 if $skiplock && $authuser ne 'root@pam';
1303
1304 # test if VM exists
1305 my $conf = PVE::QemuConfig->load_config($vmid);
1306
1307 my $storecfg = PVE::Storage::config();
1308
1309 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
1310
1311 die "unable to remove VM $vmid - used in HA resources\n"
1312 if PVE::HA::Config::vm_is_ha_managed($vmid);
1313
1314 # do not allow destroy if there are replication jobs
1315 my $repl_conf = PVE::ReplicationConfig->new();
1316 $repl_conf->check_for_existing_jobs($vmid);
1317
1318 # early tests (repeat after locking)
1319 die "VM $vmid is running - destroy failed\n"
1320 if PVE::QemuServer::check_running($vmid);
1321
1322 my $realcmd = sub {
1323 my $upid = shift;
1324
1325 syslog('info', "destroy VM $vmid: $upid\n");
1326
1327 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
1328
1329 PVE::AccessControl::remove_vm_access($vmid);
1330
1331 PVE::Firewall::remove_vmfw_conf($vmid);
1332 };
1333
1334 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1335 }});
1336
1337 __PACKAGE__->register_method({
1338 name => 'unlink',
1339 path => '{vmid}/unlink',
1340 method => 'PUT',
1341 protected => 1,
1342 proxyto => 'node',
1343 description => "Unlink/delete disk images.",
1344 permissions => {
1345 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1346 },
1347 parameters => {
1348 additionalProperties => 0,
1349 properties => {
1350 node => get_standard_option('pve-node'),
1351 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1352 idlist => {
1353 type => 'string', format => 'pve-configid-list',
1354 description => "A list of disk IDs you want to delete.",
1355 },
1356 force => {
1357 type => 'boolean',
1358 description => $opt_force_description,
1359 optional => 1,
1360 },
1361 },
1362 },
1363 returns => { type => 'null'},
1364 code => sub {
1365 my ($param) = @_;
1366
1367 $param->{delete} = extract_param($param, 'idlist');
1368
1369 __PACKAGE__->update_vm($param);
1370
1371 return undef;
1372 }});
1373
1374 my $sslcert;
1375
1376 __PACKAGE__->register_method({
1377 name => 'vncproxy',
1378 path => '{vmid}/vncproxy',
1379 method => 'POST',
1380 protected => 1,
1381 permissions => {
1382 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1383 },
1384 description => "Creates a TCP VNC proxy connections.",
1385 parameters => {
1386 additionalProperties => 0,
1387 properties => {
1388 node => get_standard_option('pve-node'),
1389 vmid => get_standard_option('pve-vmid'),
1390 websocket => {
1391 optional => 1,
1392 type => 'boolean',
1393 description => "starts websockify instead of vncproxy",
1394 },
1395 },
1396 },
1397 returns => {
1398 additionalProperties => 0,
1399 properties => {
1400 user => { type => 'string' },
1401 ticket => { type => 'string' },
1402 cert => { type => 'string' },
1403 port => { type => 'integer' },
1404 upid => { type => 'string' },
1405 },
1406 },
1407 code => sub {
1408 my ($param) = @_;
1409
1410 my $rpcenv = PVE::RPCEnvironment::get();
1411
1412 my $authuser = $rpcenv->get_user();
1413
1414 my $vmid = $param->{vmid};
1415 my $node = $param->{node};
1416 my $websocket = $param->{websocket};
1417
1418 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1419
1420 my $authpath = "/vms/$vmid";
1421
1422 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1423
1424 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1425 if !$sslcert;
1426
1427 my ($remip, $family);
1428 my $remcmd = [];
1429
1430 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1431 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
1432 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1433 $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
1434 } else {
1435 $family = PVE::Tools::get_host_address_family($node);
1436 }
1437
1438 my $port = PVE::Tools::next_vnc_port($family);
1439
1440 my $timeout = 10;
1441
1442 my $realcmd = sub {
1443 my $upid = shift;
1444
1445 syslog('info', "starting vnc proxy $upid\n");
1446
1447 my $cmd;
1448
1449 if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) {
1450
1451 die "Websocket mode is not supported in vga serial mode!" if $websocket;
1452
1453 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ];
1454 #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
1455 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1456 '-timeout', $timeout, '-authpath', $authpath,
1457 '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
1458 PVE::Tools::run_command($cmd);
1459 } else {
1460
1461 $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
1462
1463 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1464
1465 my $sock = IO::Socket::IP->new(
1466 ReuseAddr => 1,
1467 Listen => 1,
1468 LocalPort => $port,
1469 Proto => 'tcp',
1470 GetAddrInfoFlags => 0,
1471 ) or die "failed to create socket: $!\n";
1472 # Inside the worker we shouldn't have any previous alarms
1473 # running anyway...:
1474 alarm(0);
1475 local $SIG{ALRM} = sub { die "connection timed out\n" };
1476 alarm $timeout;
1477 accept(my $cli, $sock) or die "connection failed: $!\n";
1478 alarm(0);
1479 close($sock);
1480 if (PVE::Tools::run_command($cmd,
1481 output => '>&'.fileno($cli),
1482 input => '<&'.fileno($cli),
1483 noerr => 1) != 0)
1484 {
1485 die "Failed to run vncproxy.\n";
1486 }
1487 }
1488
1489 return;
1490 };
1491
1492 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1493
1494 PVE::Tools::wait_for_vnc_port($port);
1495
1496 return {
1497 user => $authuser,
1498 ticket => $ticket,
1499 port => $port,
1500 upid => $upid,
1501 cert => $sslcert,
1502 };
1503 }});
1504
1505 __PACKAGE__->register_method({
1506 name => 'vncwebsocket',
1507 path => '{vmid}/vncwebsocket',
1508 method => 'GET',
1509 permissions => {
1510 description => "You also need to pass a valid ticket (vncticket).",
1511 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1512 },
1513 description => "Opens a weksocket for VNC traffic.",
1514 parameters => {
1515 additionalProperties => 0,
1516 properties => {
1517 node => get_standard_option('pve-node'),
1518 vmid => get_standard_option('pve-vmid'),
1519 vncticket => {
1520 description => "Ticket from previous call to vncproxy.",
1521 type => 'string',
1522 maxLength => 512,
1523 },
1524 port => {
1525 description => "Port number returned by previous vncproxy call.",
1526 type => 'integer',
1527 minimum => 5900,
1528 maximum => 5999,
1529 },
1530 },
1531 },
1532 returns => {
1533 type => "object",
1534 properties => {
1535 port => { type => 'string' },
1536 },
1537 },
1538 code => sub {
1539 my ($param) = @_;
1540
1541 my $rpcenv = PVE::RPCEnvironment::get();
1542
1543 my $authuser = $rpcenv->get_user();
1544
1545 my $vmid = $param->{vmid};
1546 my $node = $param->{node};
1547
1548 my $authpath = "/vms/$vmid";
1549
1550 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1551
1552 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
1553
1554 # Note: VNC ports are acessible from outside, so we do not gain any
1555 # security if we verify that $param->{port} belongs to VM $vmid. This
1556 # check is done by verifying the VNC ticket (inside VNC protocol).
1557
1558 my $port = $param->{port};
1559
1560 return { port => $port };
1561 }});
1562
1563 __PACKAGE__->register_method({
1564 name => 'spiceproxy',
1565 path => '{vmid}/spiceproxy',
1566 method => 'POST',
1567 protected => 1,
1568 proxyto => 'node',
1569 permissions => {
1570 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1571 },
1572 description => "Returns a SPICE configuration to connect to the VM.",
1573 parameters => {
1574 additionalProperties => 0,
1575 properties => {
1576 node => get_standard_option('pve-node'),
1577 vmid => get_standard_option('pve-vmid'),
1578 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1579 },
1580 },
1581 returns => get_standard_option('remote-viewer-config'),
1582 code => sub {
1583 my ($param) = @_;
1584
1585 my $rpcenv = PVE::RPCEnvironment::get();
1586
1587 my $authuser = $rpcenv->get_user();
1588
1589 my $vmid = $param->{vmid};
1590 my $node = $param->{node};
1591 my $proxy = $param->{proxy};
1592
1593 my $conf = PVE::QemuConfig->load_config($vmid, $node);
1594 my $title = "VM $vmid";
1595 $title .= " - ". $conf->{name} if $conf->{name};
1596
1597 my $port = PVE::QemuServer::spice_port($vmid);
1598
1599 my ($ticket, undef, $remote_viewer_config) =
1600 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
1601
1602 PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
1603 PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
1604
1605 return $remote_viewer_config;
1606 }});
1607
1608 __PACKAGE__->register_method({
1609 name => 'vmcmdidx',
1610 path => '{vmid}/status',
1611 method => 'GET',
1612 proxyto => 'node',
1613 description => "Directory index",
1614 permissions => {
1615 user => 'all',
1616 },
1617 parameters => {
1618 additionalProperties => 0,
1619 properties => {
1620 node => get_standard_option('pve-node'),
1621 vmid => get_standard_option('pve-vmid'),
1622 },
1623 },
1624 returns => {
1625 type => 'array',
1626 items => {
1627 type => "object",
1628 properties => {
1629 subdir => { type => 'string' },
1630 },
1631 },
1632 links => [ { rel => 'child', href => "{subdir}" } ],
1633 },
1634 code => sub {
1635 my ($param) = @_;
1636
1637 # test if VM exists
1638 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1639
1640 my $res = [
1641 { subdir => 'current' },
1642 { subdir => 'start' },
1643 { subdir => 'stop' },
1644 ];
1645
1646 return $res;
1647 }});
1648
1649 __PACKAGE__->register_method({
1650 name => 'vm_status',
1651 path => '{vmid}/status/current',
1652 method => 'GET',
1653 proxyto => 'node',
1654 protected => 1, # qemu pid files are only readable by root
1655 description => "Get virtual machine status.",
1656 permissions => {
1657 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1658 },
1659 parameters => {
1660 additionalProperties => 0,
1661 properties => {
1662 node => get_standard_option('pve-node'),
1663 vmid => get_standard_option('pve-vmid'),
1664 },
1665 },
1666 returns => { type => 'object' },
1667 code => sub {
1668 my ($param) = @_;
1669
1670 # test if VM exists
1671 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1672
1673 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
1674 my $status = $vmstatus->{$param->{vmid}};
1675
1676 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
1677
1678 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
1679
1680 return $status;
1681 }});
1682
1683 __PACKAGE__->register_method({
1684 name => 'vm_start',
1685 path => '{vmid}/status/start',
1686 method => 'POST',
1687 protected => 1,
1688 proxyto => 'node',
1689 description => "Start virtual machine.",
1690 permissions => {
1691 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1692 },
1693 parameters => {
1694 additionalProperties => 0,
1695 properties => {
1696 node => get_standard_option('pve-node'),
1697 vmid => get_standard_option('pve-vmid',
1698 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1699 skiplock => get_standard_option('skiplock'),
1700 stateuri => get_standard_option('pve-qm-stateuri'),
1701 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1702 migration_type => {
1703 type => 'string',
1704 enum => ['secure', 'insecure'],
1705 description => "Migration traffic is encrypted using an SSH " .
1706 "tunnel by default. On secure, completely private networks " .
1707 "this can be disabled to increase performance.",
1708 optional => 1,
1709 },
1710 migration_network => {
1711 type => 'string', format => 'CIDR',
1712 description => "CIDR of the (sub) network that is used for migration.",
1713 optional => 1,
1714 },
1715 machine => get_standard_option('pve-qm-machine'),
1716 targetstorage => {
1717 description => "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
1718 type => 'string',
1719 optional => 1
1720 }
1721 },
1722 },
1723 returns => {
1724 type => 'string',
1725 },
1726 code => sub {
1727 my ($param) = @_;
1728
1729 my $rpcenv = PVE::RPCEnvironment::get();
1730
1731 my $authuser = $rpcenv->get_user();
1732
1733 my $node = extract_param($param, 'node');
1734
1735 my $vmid = extract_param($param, 'vmid');
1736
1737 my $machine = extract_param($param, 'machine');
1738
1739 my $stateuri = extract_param($param, 'stateuri');
1740 raise_param_exc({ stateuri => "Only root may use this option." })
1741 if $stateuri && $authuser ne 'root@pam';
1742
1743 my $skiplock = extract_param($param, 'skiplock');
1744 raise_param_exc({ skiplock => "Only root may use this option." })
1745 if $skiplock && $authuser ne 'root@pam';
1746
1747 my $migratedfrom = extract_param($param, 'migratedfrom');
1748 raise_param_exc({ migratedfrom => "Only root may use this option." })
1749 if $migratedfrom && $authuser ne 'root@pam';
1750
1751 my $migration_type = extract_param($param, 'migration_type');
1752 raise_param_exc({ migration_type => "Only root may use this option." })
1753 if $migration_type && $authuser ne 'root@pam';
1754
1755 my $migration_network = extract_param($param, 'migration_network');
1756 raise_param_exc({ migration_network => "Only root may use this option." })
1757 if $migration_network && $authuser ne 'root@pam';
1758
1759 my $targetstorage = extract_param($param, 'targetstorage');
1760 raise_param_exc({ targetstorage => "Only root may use this option." })
1761 if $targetstorage && $authuser ne 'root@pam';
1762
1763 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
1764 if $targetstorage && !$migratedfrom;
1765
1766 # read spice ticket from STDIN
1767 my $spice_ticket;
1768 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
1769 if (defined(my $line = <>)) {
1770 chomp $line;
1771 $spice_ticket = $line;
1772 }
1773 }
1774
1775 PVE::Cluster::check_cfs_quorum();
1776
1777 my $storecfg = PVE::Storage::config();
1778
1779 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri &&
1780 $rpcenv->{type} ne 'ha') {
1781
1782 my $hacmd = sub {
1783 my $upid = shift;
1784
1785 my $service = "vm:$vmid";
1786
1787 my $cmd = ['ha-manager', 'set', $service, '--state', 'started'];
1788
1789 print "Requesting HA start for VM $vmid\n";
1790
1791 PVE::Tools::run_command($cmd);
1792
1793 return;
1794 };
1795
1796 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
1797
1798 } else {
1799
1800 my $realcmd = sub {
1801 my $upid = shift;
1802
1803 syslog('info', "start VM $vmid: $upid\n");
1804
1805 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
1806 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
1807
1808 return;
1809 };
1810
1811 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
1812 }
1813 }});
1814
1815 __PACKAGE__->register_method({
1816 name => 'vm_stop',
1817 path => '{vmid}/status/stop',
1818 method => 'POST',
1819 protected => 1,
1820 proxyto => 'node',
1821 description => "Stop virtual machine. The qemu process will exit immediately. This" .
1822 "is akin to pulling the power plug of a running computer and may damage the VM data",
1823 permissions => {
1824 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1825 },
1826 parameters => {
1827 additionalProperties => 0,
1828 properties => {
1829 node => get_standard_option('pve-node'),
1830 vmid => get_standard_option('pve-vmid',
1831 { completion => \&PVE::QemuServer::complete_vmid_running }),
1832 skiplock => get_standard_option('skiplock'),
1833 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
1834 timeout => {
1835 description => "Wait maximal timeout seconds.",
1836 type => 'integer',
1837 minimum => 0,
1838 optional => 1,
1839 },
1840 keepActive => {
1841 description => "Do not deactivate storage volumes.",
1842 type => 'boolean',
1843 optional => 1,
1844 default => 0,
1845 }
1846 },
1847 },
1848 returns => {
1849 type => 'string',
1850 },
1851 code => sub {
1852 my ($param) = @_;
1853
1854 my $rpcenv = PVE::RPCEnvironment::get();
1855
1856 my $authuser = $rpcenv->get_user();
1857
1858 my $node = extract_param($param, 'node');
1859
1860 my $vmid = extract_param($param, 'vmid');
1861
1862 my $skiplock = extract_param($param, 'skiplock');
1863 raise_param_exc({ skiplock => "Only root may use this option." })
1864 if $skiplock && $authuser ne 'root@pam';
1865
1866 my $keepActive = extract_param($param, 'keepActive');
1867 raise_param_exc({ keepActive => "Only root may use this option." })
1868 if $keepActive && $authuser ne 'root@pam';
1869
1870 my $migratedfrom = extract_param($param, 'migratedfrom');
1871 raise_param_exc({ migratedfrom => "Only root may use this option." })
1872 if $migratedfrom && $authuser ne 'root@pam';
1873
1874
1875 my $storecfg = PVE::Storage::config();
1876
1877 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
1878
1879 my $hacmd = sub {
1880 my $upid = shift;
1881
1882 my $service = "vm:$vmid";
1883
1884 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
1885
1886 print "Requesting HA stop for VM $vmid\n";
1887
1888 PVE::Tools::run_command($cmd);
1889
1890 return;
1891 };
1892
1893 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
1894
1895 } else {
1896 my $realcmd = sub {
1897 my $upid = shift;
1898
1899 syslog('info', "stop VM $vmid: $upid\n");
1900
1901 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
1902 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
1903
1904 return;
1905 };
1906
1907 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
1908 }
1909 }});
1910
1911 __PACKAGE__->register_method({
1912 name => 'vm_reset',
1913 path => '{vmid}/status/reset',
1914 method => 'POST',
1915 protected => 1,
1916 proxyto => 'node',
1917 description => "Reset virtual machine.",
1918 permissions => {
1919 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1920 },
1921 parameters => {
1922 additionalProperties => 0,
1923 properties => {
1924 node => get_standard_option('pve-node'),
1925 vmid => get_standard_option('pve-vmid',
1926 { completion => \&PVE::QemuServer::complete_vmid_running }),
1927 skiplock => get_standard_option('skiplock'),
1928 },
1929 },
1930 returns => {
1931 type => 'string',
1932 },
1933 code => sub {
1934 my ($param) = @_;
1935
1936 my $rpcenv = PVE::RPCEnvironment::get();
1937
1938 my $authuser = $rpcenv->get_user();
1939
1940 my $node = extract_param($param, 'node');
1941
1942 my $vmid = extract_param($param, 'vmid');
1943
1944 my $skiplock = extract_param($param, 'skiplock');
1945 raise_param_exc({ skiplock => "Only root may use this option." })
1946 if $skiplock && $authuser ne 'root@pam';
1947
1948 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1949
1950 my $realcmd = sub {
1951 my $upid = shift;
1952
1953 PVE::QemuServer::vm_reset($vmid, $skiplock);
1954
1955 return;
1956 };
1957
1958 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
1959 }});
1960
1961 __PACKAGE__->register_method({
1962 name => 'vm_shutdown',
1963 path => '{vmid}/status/shutdown',
1964 method => 'POST',
1965 protected => 1,
1966 proxyto => 'node',
1967 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
1968 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
1969 permissions => {
1970 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1971 },
1972 parameters => {
1973 additionalProperties => 0,
1974 properties => {
1975 node => get_standard_option('pve-node'),
1976 vmid => get_standard_option('pve-vmid',
1977 { completion => \&PVE::QemuServer::complete_vmid_running }),
1978 skiplock => get_standard_option('skiplock'),
1979 timeout => {
1980 description => "Wait maximal timeout seconds.",
1981 type => 'integer',
1982 minimum => 0,
1983 optional => 1,
1984 },
1985 forceStop => {
1986 description => "Make sure the VM stops.",
1987 type => 'boolean',
1988 optional => 1,
1989 default => 0,
1990 },
1991 keepActive => {
1992 description => "Do not deactivate storage volumes.",
1993 type => 'boolean',
1994 optional => 1,
1995 default => 0,
1996 }
1997 },
1998 },
1999 returns => {
2000 type => 'string',
2001 },
2002 code => sub {
2003 my ($param) = @_;
2004
2005 my $rpcenv = PVE::RPCEnvironment::get();
2006
2007 my $authuser = $rpcenv->get_user();
2008
2009 my $node = extract_param($param, 'node');
2010
2011 my $vmid = extract_param($param, 'vmid');
2012
2013 my $skiplock = extract_param($param, 'skiplock');
2014 raise_param_exc({ skiplock => "Only root may use this option." })
2015 if $skiplock && $authuser ne 'root@pam';
2016
2017 my $keepActive = extract_param($param, 'keepActive');
2018 raise_param_exc({ keepActive => "Only root may use this option." })
2019 if $keepActive && $authuser ne 'root@pam';
2020
2021 my $storecfg = PVE::Storage::config();
2022
2023 my $shutdown = 1;
2024
2025 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2026 # otherwise, we will infer a shutdown command, but run into the timeout,
2027 # then when the vm is resumed, it will instantly shutdown
2028 #
2029 # checking the qmp status here to get feedback to the gui/cli/api
2030 # and the status query should not take too long
2031 my $qmpstatus;
2032 eval {
2033 $qmpstatus = PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
2034 };
2035 my $err = $@ if $@;
2036
2037 if (!$err && $qmpstatus->{status} eq "paused") {
2038 if ($param->{forceStop}) {
2039 warn "VM is paused - stop instead of shutdown\n";
2040 $shutdown = 0;
2041 } else {
2042 die "VM is paused - cannot shutdown\n";
2043 }
2044 }
2045
2046 if (PVE::HA::Config::vm_is_ha_managed($vmid) &&
2047 ($rpcenv->{type} ne 'ha')) {
2048
2049 my $hacmd = sub {
2050 my $upid = shift;
2051
2052 my $service = "vm:$vmid";
2053
2054 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
2055
2056 print "Requesting HA stop for VM $vmid\n";
2057
2058 PVE::Tools::run_command($cmd);
2059
2060 return;
2061 };
2062
2063 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2064
2065 } else {
2066
2067 my $realcmd = sub {
2068 my $upid = shift;
2069
2070 syslog('info', "shutdown VM $vmid: $upid\n");
2071
2072 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2073 $shutdown, $param->{forceStop}, $keepActive);
2074
2075 return;
2076 };
2077
2078 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2079 }
2080 }});
2081
2082 __PACKAGE__->register_method({
2083 name => 'vm_suspend',
2084 path => '{vmid}/status/suspend',
2085 method => 'POST',
2086 protected => 1,
2087 proxyto => 'node',
2088 description => "Suspend virtual machine.",
2089 permissions => {
2090 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2091 },
2092 parameters => {
2093 additionalProperties => 0,
2094 properties => {
2095 node => get_standard_option('pve-node'),
2096 vmid => get_standard_option('pve-vmid',
2097 { completion => \&PVE::QemuServer::complete_vmid_running }),
2098 skiplock => get_standard_option('skiplock'),
2099 },
2100 },
2101 returns => {
2102 type => 'string',
2103 },
2104 code => sub {
2105 my ($param) = @_;
2106
2107 my $rpcenv = PVE::RPCEnvironment::get();
2108
2109 my $authuser = $rpcenv->get_user();
2110
2111 my $node = extract_param($param, 'node');
2112
2113 my $vmid = extract_param($param, 'vmid');
2114
2115 my $skiplock = extract_param($param, 'skiplock');
2116 raise_param_exc({ skiplock => "Only root may use this option." })
2117 if $skiplock && $authuser ne 'root@pam';
2118
2119 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2120
2121 my $realcmd = sub {
2122 my $upid = shift;
2123
2124 syslog('info', "suspend VM $vmid: $upid\n");
2125
2126 PVE::QemuServer::vm_suspend($vmid, $skiplock);
2127
2128 return;
2129 };
2130
2131 return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd);
2132 }});
2133
2134 __PACKAGE__->register_method({
2135 name => 'vm_resume',
2136 path => '{vmid}/status/resume',
2137 method => 'POST',
2138 protected => 1,
2139 proxyto => 'node',
2140 description => "Resume virtual machine.",
2141 permissions => {
2142 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2143 },
2144 parameters => {
2145 additionalProperties => 0,
2146 properties => {
2147 node => get_standard_option('pve-node'),
2148 vmid => get_standard_option('pve-vmid',
2149 { completion => \&PVE::QemuServer::complete_vmid_running }),
2150 skiplock => get_standard_option('skiplock'),
2151 nocheck => { type => 'boolean', optional => 1 },
2152
2153 },
2154 },
2155 returns => {
2156 type => 'string',
2157 },
2158 code => sub {
2159 my ($param) = @_;
2160
2161 my $rpcenv = PVE::RPCEnvironment::get();
2162
2163 my $authuser = $rpcenv->get_user();
2164
2165 my $node = extract_param($param, 'node');
2166
2167 my $vmid = extract_param($param, 'vmid');
2168
2169 my $skiplock = extract_param($param, 'skiplock');
2170 raise_param_exc({ skiplock => "Only root may use this option." })
2171 if $skiplock && $authuser ne 'root@pam';
2172
2173 my $nocheck = extract_param($param, 'nocheck');
2174
2175 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid, $nocheck);
2176
2177 my $realcmd = sub {
2178 my $upid = shift;
2179
2180 syslog('info', "resume VM $vmid: $upid\n");
2181
2182 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2183
2184 return;
2185 };
2186
2187 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2188 }});
2189
2190 __PACKAGE__->register_method({
2191 name => 'vm_sendkey',
2192 path => '{vmid}/sendkey',
2193 method => 'PUT',
2194 protected => 1,
2195 proxyto => 'node',
2196 description => "Send key event to virtual machine.",
2197 permissions => {
2198 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2199 },
2200 parameters => {
2201 additionalProperties => 0,
2202 properties => {
2203 node => get_standard_option('pve-node'),
2204 vmid => get_standard_option('pve-vmid',
2205 { completion => \&PVE::QemuServer::complete_vmid_running }),
2206 skiplock => get_standard_option('skiplock'),
2207 key => {
2208 description => "The key (qemu monitor encoding).",
2209 type => 'string'
2210 }
2211 },
2212 },
2213 returns => { type => 'null'},
2214 code => sub {
2215 my ($param) = @_;
2216
2217 my $rpcenv = PVE::RPCEnvironment::get();
2218
2219 my $authuser = $rpcenv->get_user();
2220
2221 my $node = extract_param($param, 'node');
2222
2223 my $vmid = extract_param($param, 'vmid');
2224
2225 my $skiplock = extract_param($param, 'skiplock');
2226 raise_param_exc({ skiplock => "Only root may use this option." })
2227 if $skiplock && $authuser ne 'root@pam';
2228
2229 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2230
2231 return;
2232 }});
2233
2234 __PACKAGE__->register_method({
2235 name => 'vm_feature',
2236 path => '{vmid}/feature',
2237 method => 'GET',
2238 proxyto => 'node',
2239 protected => 1,
2240 description => "Check if feature for virtual machine is available.",
2241 permissions => {
2242 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2243 },
2244 parameters => {
2245 additionalProperties => 0,
2246 properties => {
2247 node => get_standard_option('pve-node'),
2248 vmid => get_standard_option('pve-vmid'),
2249 feature => {
2250 description => "Feature to check.",
2251 type => 'string',
2252 enum => [ 'snapshot', 'clone', 'copy' ],
2253 },
2254 snapname => get_standard_option('pve-snapshot-name', {
2255 optional => 1,
2256 }),
2257 },
2258 },
2259 returns => {
2260 type => "object",
2261 properties => {
2262 hasFeature => { type => 'boolean' },
2263 nodes => {
2264 type => 'array',
2265 items => { type => 'string' },
2266 }
2267 },
2268 },
2269 code => sub {
2270 my ($param) = @_;
2271
2272 my $node = extract_param($param, 'node');
2273
2274 my $vmid = extract_param($param, 'vmid');
2275
2276 my $snapname = extract_param($param, 'snapname');
2277
2278 my $feature = extract_param($param, 'feature');
2279
2280 my $running = PVE::QemuServer::check_running($vmid);
2281
2282 my $conf = PVE::QemuConfig->load_config($vmid);
2283
2284 if($snapname){
2285 my $snap = $conf->{snapshots}->{$snapname};
2286 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2287 $conf = $snap;
2288 }
2289 my $storecfg = PVE::Storage::config();
2290
2291 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
2292 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
2293
2294 return {
2295 hasFeature => $hasFeature,
2296 nodes => [ keys %$nodelist ],
2297 };
2298 }});
2299
2300 __PACKAGE__->register_method({
2301 name => 'clone_vm',
2302 path => '{vmid}/clone',
2303 method => 'POST',
2304 protected => 1,
2305 proxyto => 'node',
2306 description => "Create a copy of virtual machine/template.",
2307 permissions => {
2308 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2309 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2310 "'Datastore.AllocateSpace' on any used storage.",
2311 check =>
2312 [ 'and',
2313 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2314 [ 'or',
2315 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2316 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2317 ],
2318 ]
2319 },
2320 parameters => {
2321 additionalProperties => 0,
2322 properties => {
2323 node => get_standard_option('pve-node'),
2324 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2325 newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }),
2326 name => {
2327 optional => 1,
2328 type => 'string', format => 'dns-name',
2329 description => "Set a name for the new VM.",
2330 },
2331 description => {
2332 optional => 1,
2333 type => 'string',
2334 description => "Description for the new VM.",
2335 },
2336 pool => {
2337 optional => 1,
2338 type => 'string', format => 'pve-poolid',
2339 description => "Add the new VM to the specified pool.",
2340 },
2341 snapname => get_standard_option('pve-snapshot-name', {
2342 optional => 1,
2343 }),
2344 storage => get_standard_option('pve-storage-id', {
2345 description => "Target storage for full clone.",
2346 requires => 'full',
2347 optional => 1,
2348 }),
2349 'format' => {
2350 description => "Target format for file storage.",
2351 requires => 'full',
2352 type => 'string',
2353 optional => 1,
2354 enum => [ 'raw', 'qcow2', 'vmdk'],
2355 },
2356 full => {
2357 optional => 1,
2358 type => 'boolean',
2359 description => "Create a full copy of all disk. This is always done when " .
2360 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2361 default => 0,
2362 },
2363 target => get_standard_option('pve-node', {
2364 description => "Target node. Only allowed if the original VM is on shared storage.",
2365 optional => 1,
2366 }),
2367 },
2368 },
2369 returns => {
2370 type => 'string',
2371 },
2372 code => sub {
2373 my ($param) = @_;
2374
2375 my $rpcenv = PVE::RPCEnvironment::get();
2376
2377 my $authuser = $rpcenv->get_user();
2378
2379 my $node = extract_param($param, 'node');
2380
2381 my $vmid = extract_param($param, 'vmid');
2382
2383 my $newid = extract_param($param, 'newid');
2384
2385 my $pool = extract_param($param, 'pool');
2386
2387 if (defined($pool)) {
2388 $rpcenv->check_pool_exist($pool);
2389 }
2390
2391 my $snapname = extract_param($param, 'snapname');
2392
2393 my $storage = extract_param($param, 'storage');
2394
2395 my $format = extract_param($param, 'format');
2396
2397 my $target = extract_param($param, 'target');
2398
2399 my $localnode = PVE::INotify::nodename();
2400
2401 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2402
2403 PVE::Cluster::check_node_exists($target) if $target;
2404
2405 my $storecfg = PVE::Storage::config();
2406
2407 if ($storage) {
2408 # check if storage is enabled on local node
2409 PVE::Storage::storage_check_enabled($storecfg, $storage);
2410 if ($target) {
2411 # check if storage is available on target node
2412 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2413 # clone only works if target storage is shared
2414 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2415 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2416 }
2417 }
2418
2419 PVE::Cluster::check_cfs_quorum();
2420
2421 my $running = PVE::QemuServer::check_running($vmid) || 0;
2422
2423 # exclusive lock if VM is running - else shared lock is enough;
2424 my $shared_lock = $running ? 0 : 1;
2425
2426 my $clonefn = sub {
2427
2428 # do all tests after lock
2429 # we also try to do all tests before we fork the worker
2430
2431 my $conf = PVE::QemuConfig->load_config($vmid);
2432
2433 PVE::QemuConfig->check_lock($conf);
2434
2435 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
2436
2437 die "unexpected state change\n" if $verify_running != $running;
2438
2439 die "snapshot '$snapname' does not exist\n"
2440 if $snapname && !defined( $conf->{snapshots}->{$snapname});
2441
2442 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
2443
2444 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2445
2446 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2447
2448 my $conffile = PVE::QemuConfig->config_file($newid);
2449
2450 die "unable to create VM $newid: config file already exists\n"
2451 if -f $conffile;
2452
2453 my $newconf = { lock => 'clone' };
2454 my $drives = {};
2455 my $fullclone = {};
2456 my $vollist = [];
2457
2458 foreach my $opt (keys %$oldconf) {
2459 my $value = $oldconf->{$opt};
2460
2461 # do not copy snapshot related info
2462 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2463 $opt eq 'vmstate' || $opt eq 'snapstate';
2464
2465 # no need to copy unused images, because VMID(owner) changes anyways
2466 next if $opt =~ m/^unused\d+$/;
2467
2468 # always change MAC! address
2469 if ($opt =~ m/^net(\d+)$/) {
2470 my $net = PVE::QemuServer::parse_net($value);
2471 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
2472 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
2473 $newconf->{$opt} = PVE::QemuServer::print_net($net);
2474 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
2475 my $drive = PVE::QemuServer::parse_drive($opt, $value);
2476 die "unable to parse drive options for '$opt'\n" if !$drive;
2477 if (PVE::QemuServer::drive_is_cdrom($drive)) {
2478 $newconf->{$opt} = $value; # simply copy configuration
2479 } else {
2480 if ($param->{full}) {
2481 die "Full clone feature is not supported for drive '$opt'\n"
2482 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
2483 $fullclone->{$opt} = 1;
2484 } else {
2485 # not full means clone instead of copy
2486 die "Linked clone feature is not supported for drive '$opt'\n"
2487 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
2488 }
2489 $drives->{$opt} = $drive;
2490 push @$vollist, $drive->{file};
2491 }
2492 } else {
2493 # copy everything else
2494 $newconf->{$opt} = $value;
2495 }
2496 }
2497
2498 # auto generate a new uuid
2499 my ($uuid, $uuid_str);
2500 UUID::generate($uuid);
2501 UUID::unparse($uuid, $uuid_str);
2502 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
2503 $smbios1->{uuid} = $uuid_str;
2504 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
2505
2506 delete $newconf->{template};
2507
2508 if ($param->{name}) {
2509 $newconf->{name} = $param->{name};
2510 } else {
2511 if ($oldconf->{name}) {
2512 $newconf->{name} = "Copy-of-$oldconf->{name}";
2513 } else {
2514 $newconf->{name} = "Copy-of-VM-$vmid";
2515 }
2516 }
2517
2518 if ($param->{description}) {
2519 $newconf->{description} = $param->{description};
2520 }
2521
2522 # create empty/temp config - this fails if VM already exists on other node
2523 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
2524
2525 my $realcmd = sub {
2526 my $upid = shift;
2527
2528 my $newvollist = [];
2529 my $jobs = {};
2530
2531 eval {
2532 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2533
2534 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
2535
2536 my $total_jobs = scalar(keys %{$drives});
2537 my $i = 1;
2538
2539 foreach my $opt (keys %$drives) {
2540 my $drive = $drives->{$opt};
2541 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2542
2543 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
2544 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2545 $jobs, $skipcomplete, $oldconf->{agent});
2546
2547 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2548
2549 PVE::QemuConfig->write_config($newid, $newconf);
2550 $i++;
2551 }
2552
2553 delete $newconf->{lock};
2554 PVE::QemuConfig->write_config($newid, $newconf);
2555
2556 if ($target) {
2557 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2558 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
2559 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
2560
2561 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
2562 die "Failed to move config to node '$target' - rename failed: $!\n"
2563 if !rename($conffile, $newconffile);
2564 }
2565
2566 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
2567 };
2568 if (my $err = $@) {
2569 unlink $conffile;
2570
2571 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2572
2573 sleep 1; # some storage like rbd need to wait before release volume - really?
2574
2575 foreach my $volid (@$newvollist) {
2576 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2577 warn $@ if $@;
2578 }
2579 die "clone failed: $err";
2580 }
2581
2582 return;
2583 };
2584
2585 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2586
2587 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2588 };
2589
2590 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
2591 # Aquire exclusive lock lock for $newid
2592 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
2593 });
2594
2595 }});
2596
2597 __PACKAGE__->register_method({
2598 name => 'move_vm_disk',
2599 path => '{vmid}/move_disk',
2600 method => 'POST',
2601 protected => 1,
2602 proxyto => 'node',
2603 description => "Move volume to different storage.",
2604 permissions => {
2605 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2606 check => [ 'and',
2607 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2608 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2609 ],
2610 },
2611 parameters => {
2612 additionalProperties => 0,
2613 properties => {
2614 node => get_standard_option('pve-node'),
2615 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2616 disk => {
2617 type => 'string',
2618 description => "The disk you want to move.",
2619 enum => [ PVE::QemuServer::valid_drive_names() ],
2620 },
2621 storage => get_standard_option('pve-storage-id', {
2622 description => "Target storage.",
2623 completion => \&PVE::QemuServer::complete_storage,
2624 }),
2625 'format' => {
2626 type => 'string',
2627 description => "Target Format.",
2628 enum => [ 'raw', 'qcow2', 'vmdk' ],
2629 optional => 1,
2630 },
2631 delete => {
2632 type => 'boolean',
2633 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2634 optional => 1,
2635 default => 0,
2636 },
2637 digest => {
2638 type => 'string',
2639 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2640 maxLength => 40,
2641 optional => 1,
2642 },
2643 },
2644 },
2645 returns => {
2646 type => 'string',
2647 description => "the task ID.",
2648 },
2649 code => sub {
2650 my ($param) = @_;
2651
2652 my $rpcenv = PVE::RPCEnvironment::get();
2653
2654 my $authuser = $rpcenv->get_user();
2655
2656 my $node = extract_param($param, 'node');
2657
2658 my $vmid = extract_param($param, 'vmid');
2659
2660 my $digest = extract_param($param, 'digest');
2661
2662 my $disk = extract_param($param, 'disk');
2663
2664 my $storeid = extract_param($param, 'storage');
2665
2666 my $format = extract_param($param, 'format');
2667
2668 my $storecfg = PVE::Storage::config();
2669
2670 my $updatefn = sub {
2671
2672 my $conf = PVE::QemuConfig->load_config($vmid);
2673
2674 PVE::QemuConfig->check_lock($conf);
2675
2676 die "checksum missmatch (file change by other user?)\n"
2677 if $digest && $digest ne $conf->{digest};
2678
2679 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2680
2681 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
2682
2683 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
2684
2685 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
2686
2687 my $oldfmt;
2688 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
2689 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2690 $oldfmt = $1;
2691 }
2692
2693 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
2694 (!$format || !$oldfmt || $oldfmt eq $format);
2695
2696 # this only checks snapshots because $disk is passed!
2697 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
2698 die "you can't move a disk with snapshots and delete the source\n"
2699 if $snapshotted && $param->{delete};
2700
2701 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2702
2703 my $running = PVE::QemuServer::check_running($vmid);
2704
2705 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
2706
2707 my $realcmd = sub {
2708
2709 my $newvollist = [];
2710
2711 eval {
2712 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2713
2714 warn "moving disk with snapshots, snapshots will not be moved!\n"
2715 if $snapshotted;
2716
2717 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
2718 $vmid, $storeid, $format, 1, $newvollist);
2719
2720 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
2721
2722 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
2723
2724 # convert moved disk to base if part of template
2725 PVE::QemuServer::template_create($vmid, $conf, $disk)
2726 if PVE::QemuConfig->is_template($conf);
2727
2728 PVE::QemuConfig->write_config($vmid, $conf);
2729
2730 eval {
2731 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2732 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
2733 if !$running;
2734 };
2735 warn $@ if $@;
2736 };
2737 if (my $err = $@) {
2738
2739 foreach my $volid (@$newvollist) {
2740 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2741 warn $@ if $@;
2742 }
2743 die "storage migration failed: $err";
2744 }
2745
2746 if ($param->{delete}) {
2747 eval {
2748 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
2749 PVE::Storage::vdisk_free($storecfg, $old_volid);
2750 };
2751 warn $@ if $@;
2752 }
2753 };
2754
2755 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
2756 };
2757
2758 return PVE::QemuConfig->lock_config($vmid, $updatefn);
2759 }});
2760
2761 __PACKAGE__->register_method({
2762 name => 'migrate_vm',
2763 path => '{vmid}/migrate',
2764 method => 'POST',
2765 protected => 1,
2766 proxyto => 'node',
2767 description => "Migrate virtual machine. Creates a new migration task.",
2768 permissions => {
2769 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
2770 },
2771 parameters => {
2772 additionalProperties => 0,
2773 properties => {
2774 node => get_standard_option('pve-node'),
2775 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2776 target => get_standard_option('pve-node', {
2777 description => "Target node.",
2778 completion => \&PVE::Cluster::complete_migration_target,
2779 }),
2780 online => {
2781 type => 'boolean',
2782 description => "Use online/live migration.",
2783 optional => 1,
2784 },
2785 force => {
2786 type => 'boolean',
2787 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
2788 optional => 1,
2789 },
2790 migration_type => {
2791 type => 'string',
2792 enum => ['secure', 'insecure'],
2793 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2794 optional => 1,
2795 },
2796 migration_network => {
2797 type => 'string', format => 'CIDR',
2798 description => "CIDR of the (sub) network that is used for migration.",
2799 optional => 1,
2800 },
2801 "with-local-disks" => {
2802 type => 'boolean',
2803 description => "Enable live storage migration for local disk",
2804 optional => 1,
2805 },
2806 targetstorage => get_standard_option('pve-storage-id', {
2807 description => "Default target storage.",
2808 optional => 1,
2809 completion => \&PVE::QemuServer::complete_storage,
2810 }),
2811 },
2812 },
2813 returns => {
2814 type => 'string',
2815 description => "the task ID.",
2816 },
2817 code => sub {
2818 my ($param) = @_;
2819
2820 my $rpcenv = PVE::RPCEnvironment::get();
2821
2822 my $authuser = $rpcenv->get_user();
2823
2824 my $target = extract_param($param, 'target');
2825
2826 my $localnode = PVE::INotify::nodename();
2827 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
2828
2829 PVE::Cluster::check_cfs_quorum();
2830
2831 PVE::Cluster::check_node_exists($target);
2832
2833 my $targetip = PVE::Cluster::remote_node_ip($target);
2834
2835 my $vmid = extract_param($param, 'vmid');
2836
2837 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
2838 if !$param->{online} && $param->{targetstorage};
2839
2840 raise_param_exc({ force => "Only root may use this option." })
2841 if $param->{force} && $authuser ne 'root@pam';
2842
2843 raise_param_exc({ migration_type => "Only root may use this option." })
2844 if $param->{migration_type} && $authuser ne 'root@pam';
2845
2846 # allow root only until better network permissions are available
2847 raise_param_exc({ migration_network => "Only root may use this option." })
2848 if $param->{migration_network} && $authuser ne 'root@pam';
2849
2850 # test if VM exists
2851 my $conf = PVE::QemuConfig->load_config($vmid);
2852
2853 # try to detect errors early
2854
2855 PVE::QemuConfig->check_lock($conf);
2856
2857 if (PVE::QemuServer::check_running($vmid)) {
2858 die "cant migrate running VM without --online\n"
2859 if !$param->{online};
2860 }
2861
2862 my $storecfg = PVE::Storage::config();
2863
2864 if( $param->{targetstorage}) {
2865 PVE::Storage::storage_check_node($storecfg, $param->{targetstorage}, $target);
2866 } else {
2867 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
2868 }
2869
2870 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
2871
2872 my $hacmd = sub {
2873 my $upid = shift;
2874
2875 my $service = "vm:$vmid";
2876
2877 my $cmd = ['ha-manager', 'migrate', $service, $target];
2878
2879 print "Requesting HA migration for VM $vmid to node $target\n";
2880
2881 PVE::Tools::run_command($cmd);
2882
2883 return;
2884 };
2885
2886 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
2887
2888 } else {
2889
2890 my $realcmd = sub {
2891 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
2892 };
2893
2894 my $worker = sub {
2895 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
2896 };
2897
2898 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
2899 }
2900
2901 }});
2902
2903 __PACKAGE__->register_method({
2904 name => 'monitor',
2905 path => '{vmid}/monitor',
2906 method => 'POST',
2907 protected => 1,
2908 proxyto => 'node',
2909 description => "Execute Qemu monitor commands.",
2910 permissions => {
2911 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
2912 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2913 },
2914 parameters => {
2915 additionalProperties => 0,
2916 properties => {
2917 node => get_standard_option('pve-node'),
2918 vmid => get_standard_option('pve-vmid'),
2919 command => {
2920 type => 'string',
2921 description => "The monitor command.",
2922 }
2923 },
2924 },
2925 returns => { type => 'string'},
2926 code => sub {
2927 my ($param) = @_;
2928
2929 my $rpcenv = PVE::RPCEnvironment::get();
2930 my $authuser = $rpcenv->get_user();
2931
2932 my $is_ro = sub {
2933 my $command = shift;
2934 return $command =~ m/^\s*info(\s+|$)/
2935 || $command =~ m/^\s*help\s*$/;
2936 };
2937
2938 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
2939 if !&$is_ro($param->{command});
2940
2941 my $vmid = $param->{vmid};
2942
2943 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
2944
2945 my $res = '';
2946 eval {
2947 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
2948 };
2949 $res = "ERROR: $@" if $@;
2950
2951 return $res;
2952 }});
2953
2954 my $guest_agent_commands = [
2955 'ping',
2956 'get-time',
2957 'info',
2958 'fsfreeze-status',
2959 'fsfreeze-freeze',
2960 'fsfreeze-thaw',
2961 'fstrim',
2962 'network-get-interfaces',
2963 'get-vcpus',
2964 'get-fsinfo',
2965 'get-memory-blocks',
2966 'get-memory-block-info',
2967 'suspend-hybrid',
2968 'suspend-ram',
2969 'suspend-disk',
2970 'shutdown',
2971 ];
2972
2973 __PACKAGE__->register_method({
2974 name => 'agent',
2975 path => '{vmid}/agent',
2976 method => 'POST',
2977 protected => 1,
2978 proxyto => 'node',
2979 description => "Execute Qemu Guest Agent commands.",
2980 permissions => {
2981 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2982 },
2983 parameters => {
2984 additionalProperties => 0,
2985 properties => {
2986 node => get_standard_option('pve-node'),
2987 vmid => get_standard_option('pve-vmid', {
2988 completion => \&PVE::QemuServer::complete_vmid_running }),
2989 command => {
2990 type => 'string',
2991 description => "The QGA command.",
2992 enum => $guest_agent_commands,
2993 },
2994 },
2995 },
2996 returns => {
2997 type => 'object',
2998 description => "Returns an object with a single `result` property. The type of that
2999 property depends on the executed command.",
3000 },
3001 code => sub {
3002 my ($param) = @_;
3003
3004 my $vmid = $param->{vmid};
3005
3006 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
3007
3008 die "No Qemu Guest Agent\n" if !defined($conf->{agent});
3009 die "VM $vmid is not running\n" if !PVE::QemuServer::check_running($vmid);
3010
3011 my $cmd = $param->{command};
3012
3013 my $res = PVE::QemuServer::vm_mon_cmd($vmid, "guest-$cmd");
3014
3015 return { result => $res };
3016 }});
3017
3018 __PACKAGE__->register_method({
3019 name => 'resize_vm',
3020 path => '{vmid}/resize',
3021 method => 'PUT',
3022 protected => 1,
3023 proxyto => 'node',
3024 description => "Extend volume size.",
3025 permissions => {
3026 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3027 },
3028 parameters => {
3029 additionalProperties => 0,
3030 properties => {
3031 node => get_standard_option('pve-node'),
3032 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3033 skiplock => get_standard_option('skiplock'),
3034 disk => {
3035 type => 'string',
3036 description => "The disk you want to resize.",
3037 enum => [PVE::QemuServer::valid_drive_names()],
3038 },
3039 size => {
3040 type => 'string',
3041 pattern => '\+?\d+(\.\d+)?[KMGT]?',
3042 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.",
3043 },
3044 digest => {
3045 type => 'string',
3046 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3047 maxLength => 40,
3048 optional => 1,
3049 },
3050 },
3051 },
3052 returns => { type => 'null'},
3053 code => sub {
3054 my ($param) = @_;
3055
3056 my $rpcenv = PVE::RPCEnvironment::get();
3057
3058 my $authuser = $rpcenv->get_user();
3059
3060 my $node = extract_param($param, 'node');
3061
3062 my $vmid = extract_param($param, 'vmid');
3063
3064 my $digest = extract_param($param, 'digest');
3065
3066 my $disk = extract_param($param, 'disk');
3067
3068 my $sizestr = extract_param($param, 'size');
3069
3070 my $skiplock = extract_param($param, 'skiplock');
3071 raise_param_exc({ skiplock => "Only root may use this option." })
3072 if $skiplock && $authuser ne 'root@pam';
3073
3074 my $storecfg = PVE::Storage::config();
3075
3076 my $updatefn = sub {
3077
3078 my $conf = PVE::QemuConfig->load_config($vmid);
3079
3080 die "checksum missmatch (file change by other user?)\n"
3081 if $digest && $digest ne $conf->{digest};
3082 PVE::QemuConfig->check_lock($conf) if !$skiplock;
3083
3084 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3085
3086 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3087
3088 my (undef, undef, undef, undef, undef, undef, $format) =
3089 PVE::Storage::parse_volname($storecfg, $drive->{file});
3090
3091 die "can't resize volume: $disk if snapshot exists\n"
3092 if %{$conf->{snapshots}} && $format eq 'qcow2';
3093
3094 my $volid = $drive->{file};
3095
3096 die "disk '$disk' has no associated volume\n" if !$volid;
3097
3098 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3099
3100 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3101
3102 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3103
3104 PVE::Storage::activate_volumes($storecfg, [$volid]);
3105 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3106
3107 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3108 my ($ext, $newsize, $unit) = ($1, $2, $4);
3109 if ($unit) {
3110 if ($unit eq 'K') {
3111 $newsize = $newsize * 1024;
3112 } elsif ($unit eq 'M') {
3113 $newsize = $newsize * 1024 * 1024;
3114 } elsif ($unit eq 'G') {
3115 $newsize = $newsize * 1024 * 1024 * 1024;
3116 } elsif ($unit eq 'T') {
3117 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3118 }
3119 }
3120 $newsize += $size if $ext;
3121 $newsize = int($newsize);
3122
3123 die "shrinking disks is not supported\n" if $newsize < $size;
3124
3125 return if $size == $newsize;
3126
3127 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3128
3129 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3130
3131 $drive->{size} = $newsize;
3132 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3133
3134 PVE::QemuConfig->write_config($vmid, $conf);
3135 };
3136
3137 PVE::QemuConfig->lock_config($vmid, $updatefn);
3138 return undef;
3139 }});
3140
3141 __PACKAGE__->register_method({
3142 name => 'snapshot_list',
3143 path => '{vmid}/snapshot',
3144 method => 'GET',
3145 description => "List all snapshots.",
3146 permissions => {
3147 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3148 },
3149 proxyto => 'node',
3150 protected => 1, # qemu pid files are only readable by root
3151 parameters => {
3152 additionalProperties => 0,
3153 properties => {
3154 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3155 node => get_standard_option('pve-node'),
3156 },
3157 },
3158 returns => {
3159 type => 'array',
3160 items => {
3161 type => "object",
3162 properties => {},
3163 },
3164 links => [ { rel => 'child', href => "{name}" } ],
3165 },
3166 code => sub {
3167 my ($param) = @_;
3168
3169 my $vmid = $param->{vmid};
3170
3171 my $conf = PVE::QemuConfig->load_config($vmid);
3172 my $snaphash = $conf->{snapshots} || {};
3173
3174 my $res = [];
3175
3176 foreach my $name (keys %$snaphash) {
3177 my $d = $snaphash->{$name};
3178 my $item = {
3179 name => $name,
3180 snaptime => $d->{snaptime} || 0,
3181 vmstate => $d->{vmstate} ? 1 : 0,
3182 description => $d->{description} || '',
3183 };
3184 $item->{parent} = $d->{parent} if $d->{parent};
3185 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
3186 push @$res, $item;
3187 }
3188
3189 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
3190 my $current = { name => 'current', digest => $conf->{digest}, running => $running };
3191 $current->{parent} = $conf->{parent} if $conf->{parent};
3192
3193 push @$res, $current;
3194
3195 return $res;
3196 }});
3197
3198 __PACKAGE__->register_method({
3199 name => 'snapshot',
3200 path => '{vmid}/snapshot',
3201 method => 'POST',
3202 protected => 1,
3203 proxyto => 'node',
3204 description => "Snapshot a VM.",
3205 permissions => {
3206 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3207 },
3208 parameters => {
3209 additionalProperties => 0,
3210 properties => {
3211 node => get_standard_option('pve-node'),
3212 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3213 snapname => get_standard_option('pve-snapshot-name'),
3214 vmstate => {
3215 optional => 1,
3216 type => 'boolean',
3217 description => "Save the vmstate",
3218 },
3219 description => {
3220 optional => 1,
3221 type => 'string',
3222 description => "A textual description or comment.",
3223 },
3224 },
3225 },
3226 returns => {
3227 type => 'string',
3228 description => "the task ID.",
3229 },
3230 code => sub {
3231 my ($param) = @_;
3232
3233 my $rpcenv = PVE::RPCEnvironment::get();
3234
3235 my $authuser = $rpcenv->get_user();
3236
3237 my $node = extract_param($param, 'node');
3238
3239 my $vmid = extract_param($param, 'vmid');
3240
3241 my $snapname = extract_param($param, 'snapname');
3242
3243 die "unable to use snapshot name 'current' (reserved name)\n"
3244 if $snapname eq 'current';
3245
3246 my $realcmd = sub {
3247 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
3248 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
3249 $param->{description});
3250 };
3251
3252 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3253 }});
3254
3255 __PACKAGE__->register_method({
3256 name => 'snapshot_cmd_idx',
3257 path => '{vmid}/snapshot/{snapname}',
3258 description => '',
3259 method => 'GET',
3260 permissions => {
3261 user => 'all',
3262 },
3263 parameters => {
3264 additionalProperties => 0,
3265 properties => {
3266 vmid => get_standard_option('pve-vmid'),
3267 node => get_standard_option('pve-node'),
3268 snapname => get_standard_option('pve-snapshot-name'),
3269 },
3270 },
3271 returns => {
3272 type => 'array',
3273 items => {
3274 type => "object",
3275 properties => {},
3276 },
3277 links => [ { rel => 'child', href => "{cmd}" } ],
3278 },
3279 code => sub {
3280 my ($param) = @_;
3281
3282 my $res = [];
3283
3284 push @$res, { cmd => 'rollback' };
3285 push @$res, { cmd => 'config' };
3286
3287 return $res;
3288 }});
3289
3290 __PACKAGE__->register_method({
3291 name => 'update_snapshot_config',
3292 path => '{vmid}/snapshot/{snapname}/config',
3293 method => 'PUT',
3294 protected => 1,
3295 proxyto => 'node',
3296 description => "Update snapshot metadata.",
3297 permissions => {
3298 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3299 },
3300 parameters => {
3301 additionalProperties => 0,
3302 properties => {
3303 node => get_standard_option('pve-node'),
3304 vmid => get_standard_option('pve-vmid'),
3305 snapname => get_standard_option('pve-snapshot-name'),
3306 description => {
3307 optional => 1,
3308 type => 'string',
3309 description => "A textual description or comment.",
3310 },
3311 },
3312 },
3313 returns => { type => 'null' },
3314 code => sub {
3315 my ($param) = @_;
3316
3317 my $rpcenv = PVE::RPCEnvironment::get();
3318
3319 my $authuser = $rpcenv->get_user();
3320
3321 my $vmid = extract_param($param, 'vmid');
3322
3323 my $snapname = extract_param($param, 'snapname');
3324
3325 return undef if !defined($param->{description});
3326
3327 my $updatefn = sub {
3328
3329 my $conf = PVE::QemuConfig->load_config($vmid);
3330
3331 PVE::QemuConfig->check_lock($conf);
3332
3333 my $snap = $conf->{snapshots}->{$snapname};
3334
3335 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3336
3337 $snap->{description} = $param->{description} if defined($param->{description});
3338
3339 PVE::QemuConfig->write_config($vmid, $conf);
3340 };
3341
3342 PVE::QemuConfig->lock_config($vmid, $updatefn);
3343
3344 return undef;
3345 }});
3346
3347 __PACKAGE__->register_method({
3348 name => 'get_snapshot_config',
3349 path => '{vmid}/snapshot/{snapname}/config',
3350 method => 'GET',
3351 proxyto => 'node',
3352 description => "Get snapshot configuration",
3353 permissions => {
3354 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3355 },
3356 parameters => {
3357 additionalProperties => 0,
3358 properties => {
3359 node => get_standard_option('pve-node'),
3360 vmid => get_standard_option('pve-vmid'),
3361 snapname => get_standard_option('pve-snapshot-name'),
3362 },
3363 },
3364 returns => { type => "object" },
3365 code => sub {
3366 my ($param) = @_;
3367
3368 my $rpcenv = PVE::RPCEnvironment::get();
3369
3370 my $authuser = $rpcenv->get_user();
3371
3372 my $vmid = extract_param($param, 'vmid');
3373
3374 my $snapname = extract_param($param, 'snapname');
3375
3376 my $conf = PVE::QemuConfig->load_config($vmid);
3377
3378 my $snap = $conf->{snapshots}->{$snapname};
3379
3380 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3381
3382 return $snap;
3383 }});
3384
3385 __PACKAGE__->register_method({
3386 name => 'rollback',
3387 path => '{vmid}/snapshot/{snapname}/rollback',
3388 method => 'POST',
3389 protected => 1,
3390 proxyto => 'node',
3391 description => "Rollback VM state to specified snapshot.",
3392 permissions => {
3393 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3394 },
3395 parameters => {
3396 additionalProperties => 0,
3397 properties => {
3398 node => get_standard_option('pve-node'),
3399 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3400 snapname => get_standard_option('pve-snapshot-name'),
3401 },
3402 },
3403 returns => {
3404 type => 'string',
3405 description => "the task ID.",
3406 },
3407 code => sub {
3408 my ($param) = @_;
3409
3410 my $rpcenv = PVE::RPCEnvironment::get();
3411
3412 my $authuser = $rpcenv->get_user();
3413
3414 my $node = extract_param($param, 'node');
3415
3416 my $vmid = extract_param($param, 'vmid');
3417
3418 my $snapname = extract_param($param, 'snapname');
3419
3420 my $realcmd = sub {
3421 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3422 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
3423 };
3424
3425 my $worker = sub {
3426 # hold migration lock, this makes sure that nobody create replication snapshots
3427 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3428 };
3429
3430 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
3431 }});
3432
3433 __PACKAGE__->register_method({
3434 name => 'delsnapshot',
3435 path => '{vmid}/snapshot/{snapname}',
3436 method => 'DELETE',
3437 protected => 1,
3438 proxyto => 'node',
3439 description => "Delete a VM snapshot.",
3440 permissions => {
3441 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3442 },
3443 parameters => {
3444 additionalProperties => 0,
3445 properties => {
3446 node => get_standard_option('pve-node'),
3447 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3448 snapname => get_standard_option('pve-snapshot-name'),
3449 force => {
3450 optional => 1,
3451 type => 'boolean',
3452 description => "For removal from config file, even if removing disk snapshots fails.",
3453 },
3454 },
3455 },
3456 returns => {
3457 type => 'string',
3458 description => "the task ID.",
3459 },
3460 code => sub {
3461 my ($param) = @_;
3462
3463 my $rpcenv = PVE::RPCEnvironment::get();
3464
3465 my $authuser = $rpcenv->get_user();
3466
3467 my $node = extract_param($param, 'node');
3468
3469 my $vmid = extract_param($param, 'vmid');
3470
3471 my $snapname = extract_param($param, 'snapname');
3472
3473 my $realcmd = sub {
3474 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
3475 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
3476 };
3477
3478 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3479 }});
3480
3481 __PACKAGE__->register_method({
3482 name => 'template',
3483 path => '{vmid}/template',
3484 method => 'POST',
3485 protected => 1,
3486 proxyto => 'node',
3487 description => "Create a Template.",
3488 permissions => {
3489 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3490 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3491 },
3492 parameters => {
3493 additionalProperties => 0,
3494 properties => {
3495 node => get_standard_option('pve-node'),
3496 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3497 disk => {
3498 optional => 1,
3499 type => 'string',
3500 description => "If you want to convert only 1 disk to base image.",
3501 enum => [PVE::QemuServer::valid_drive_names()],
3502 },
3503
3504 },
3505 },
3506 returns => { type => 'null'},
3507 code => sub {
3508 my ($param) = @_;
3509
3510 my $rpcenv = PVE::RPCEnvironment::get();
3511
3512 my $authuser = $rpcenv->get_user();
3513
3514 my $node = extract_param($param, 'node');
3515
3516 my $vmid = extract_param($param, 'vmid');
3517
3518 my $disk = extract_param($param, 'disk');
3519
3520 my $updatefn = sub {
3521
3522 my $conf = PVE::QemuConfig->load_config($vmid);
3523
3524 PVE::QemuConfig->check_lock($conf);
3525
3526 die "unable to create template, because VM contains snapshots\n"
3527 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
3528
3529 die "you can't convert a template to a template\n"
3530 if PVE::QemuConfig->is_template($conf) && !$disk;
3531
3532 die "you can't convert a VM to template if VM is running\n"
3533 if PVE::QemuServer::check_running($vmid);
3534
3535 my $realcmd = sub {
3536 PVE::QemuServer::template_create($vmid, $conf, $disk);
3537 };
3538
3539 $conf->{template} = 1;
3540 PVE::QemuConfig->write_config($vmid, $conf);
3541
3542 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3543 };
3544
3545 PVE::QemuConfig->lock_config($vmid, $updatefn);
3546 return undef;
3547 }});
3548
3549 1;