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