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