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