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