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