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