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