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