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