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