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