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