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