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