]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu.pm
whitespace cleanups
[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.AllocateSpace']);
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 $conf = PVE::QemuServer::load_config($vmid, $node); # check if VM exists
1277
1278 my $authpath = "/vms/$vmid";
1279
1280 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1281
1282 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1283 if !$sslcert;
1284
1285 my $port = PVE::Tools::next_vnc_port();
1286
1287 my $remip;
1288 my $remcmd = [];
1289
1290 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1291 $remip = PVE::Cluster::remote_node_ip($node);
1292 # NOTE: kvm VNC traffic is already TLS encrypted
1293 $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
1294 }
1295
1296 my $timeout = 10;
1297
1298 my $realcmd = sub {
1299 my $upid = shift;
1300
1301 syslog('info', "starting vnc proxy $upid\n");
1302
1303 my $cmd;
1304
1305 if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) {
1306
1307 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ];
1308 #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
1309 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1310 '-timeout', $timeout, '-authpath', $authpath,
1311 '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
1312 } else {
1313
1314 my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1315
1316 my $qmstr = join(' ', @$qmcmd);
1317
1318 # also redirect stderr (else we get RFB protocol errors)
1319 $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"];
1320 }
1321
1322 PVE::Tools::run_command($cmd);
1323
1324 return;
1325 };
1326
1327 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
1328
1329 PVE::Tools::wait_for_vnc_port($port);
1330
1331 return {
1332 user => $authuser,
1333 ticket => $ticket,
1334 port => $port,
1335 upid => $upid,
1336 cert => $sslcert,
1337 };
1338 }});
1339
1340 __PACKAGE__->register_method({
1341 name => 'spiceproxy',
1342 path => '{vmid}/spiceproxy',
1343 method => 'GET',
1344 protected => 1,
1345 proxyto => 'node', # fixme: use direct connections or ssh tunnel?
1346 permissions => {
1347 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1348 },
1349 description => "Returns a SPICE configuration to connect to the VM.",
1350 parameters => {
1351 additionalProperties => 0,
1352 properties => {
1353 node => get_standard_option('pve-node'),
1354 vmid => get_standard_option('pve-vmid'),
1355 proxy => {
1356 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).",
1357 type => 'string', format => 'dns-name',
1358 optional => 1,
1359 },
1360 },
1361 },
1362 returns => {
1363 description => "Returned values can be directly passed to the 'remote-viewer' application.",
1364 additionalProperties => 1,
1365 properties => {
1366 type => { type => 'string' },
1367 password => { type => 'string' },
1368 proxy => { type => 'string' },
1369 host => { type => 'string' },
1370 'tls-port' => { type => 'integer' },
1371 },
1372 },
1373 code => sub {
1374 my ($param) = @_;
1375
1376 my $rpcenv = PVE::RPCEnvironment::get();
1377
1378 my $authuser = $rpcenv->get_user();
1379
1380 my $vmid = $param->{vmid};
1381 my $node = $param->{node};
1382 my $proxy = $param->{proxy};
1383
1384 my ($ticket, $proxyticket) = PVE::AccessControl::assemble_spice_ticket($authuser, $vmid, $node);
1385
1386 my $timeout = 10;
1387
1388 my $port = PVE::QemuServer::spice_port($vmid);
1389 PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
1390 PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
1391
1392 if (!$proxy) {
1393 my $host = `hostname -f` || PVE::INotify::nodename();
1394 chomp $host;
1395 $proxy = $host;
1396 }
1397
1398 my $filename = "/etc/pve/local/pve-ssl.pem";
1399 my $subject = PVE::QemuServer::read_x509_subject_spice($filename);
1400
1401 my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
1402 $cacert =~ s/\n/\\n/g;
1403
1404 return {
1405 type => 'spice',
1406 title => "VM $vmid",
1407 host => $proxyticket, # this break tls hostname verification, so we need to use 'host-subject'
1408 proxy => "http://$proxy:3128",
1409 'tls-port' => $port,
1410 'host-subject' => $subject,
1411 ca => $cacert,
1412 password => $ticket,
1413 'delete-this-file' => 1,
1414 };
1415 }});
1416
1417 __PACKAGE__->register_method({
1418 name => 'vmcmdidx',
1419 path => '{vmid}/status',
1420 method => 'GET',
1421 proxyto => 'node',
1422 description => "Directory index",
1423 permissions => {
1424 user => 'all',
1425 },
1426 parameters => {
1427 additionalProperties => 0,
1428 properties => {
1429 node => get_standard_option('pve-node'),
1430 vmid => get_standard_option('pve-vmid'),
1431 },
1432 },
1433 returns => {
1434 type => 'array',
1435 items => {
1436 type => "object",
1437 properties => {
1438 subdir => { type => 'string' },
1439 },
1440 },
1441 links => [ { rel => 'child', href => "{subdir}" } ],
1442 },
1443 code => sub {
1444 my ($param) = @_;
1445
1446 # test if VM exists
1447 my $conf = PVE::QemuServer::load_config($param->{vmid});
1448
1449 my $res = [
1450 { subdir => 'current' },
1451 { subdir => 'start' },
1452 { subdir => 'stop' },
1453 ];
1454
1455 return $res;
1456 }});
1457
1458 my $vm_is_ha_managed = sub {
1459 my ($vmid) = @_;
1460
1461 my $cc = PVE::Cluster::cfs_read_file('cluster.conf');
1462 if (PVE::Cluster::cluster_conf_lookup_pvevm($cc, 0, $vmid, 1)) {
1463 return 1;
1464 }
1465 return 0;
1466 };
1467
1468 __PACKAGE__->register_method({
1469 name => 'vm_status',
1470 path => '{vmid}/status/current',
1471 method => 'GET',
1472 proxyto => 'node',
1473 protected => 1, # qemu pid files are only readable by root
1474 description => "Get virtual machine status.",
1475 permissions => {
1476 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1477 },
1478 parameters => {
1479 additionalProperties => 0,
1480 properties => {
1481 node => get_standard_option('pve-node'),
1482 vmid => get_standard_option('pve-vmid'),
1483 },
1484 },
1485 returns => { type => 'object' },
1486 code => sub {
1487 my ($param) = @_;
1488
1489 # test if VM exists
1490 my $conf = PVE::QemuServer::load_config($param->{vmid});
1491
1492 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
1493 my $status = $vmstatus->{$param->{vmid}};
1494
1495 $status->{ha} = &$vm_is_ha_managed($param->{vmid});
1496
1497 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
1498
1499 return $status;
1500 }});
1501
1502 __PACKAGE__->register_method({
1503 name => 'vm_start',
1504 path => '{vmid}/status/start',
1505 method => 'POST',
1506 protected => 1,
1507 proxyto => 'node',
1508 description => "Start virtual machine.",
1509 permissions => {
1510 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1511 },
1512 parameters => {
1513 additionalProperties => 0,
1514 properties => {
1515 node => get_standard_option('pve-node'),
1516 vmid => get_standard_option('pve-vmid'),
1517 skiplock => get_standard_option('skiplock'),
1518 stateuri => get_standard_option('pve-qm-stateuri'),
1519 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1520 machine => get_standard_option('pve-qm-machine'),
1521 },
1522 },
1523 returns => {
1524 type => 'string',
1525 },
1526 code => sub {
1527 my ($param) = @_;
1528
1529 my $rpcenv = PVE::RPCEnvironment::get();
1530
1531 my $authuser = $rpcenv->get_user();
1532
1533 my $node = extract_param($param, 'node');
1534
1535 my $vmid = extract_param($param, 'vmid');
1536
1537 my $machine = extract_param($param, 'machine');
1538
1539 my $stateuri = extract_param($param, 'stateuri');
1540 raise_param_exc({ stateuri => "Only root may use this option." })
1541 if $stateuri && $authuser ne 'root@pam';
1542
1543 my $skiplock = extract_param($param, 'skiplock');
1544 raise_param_exc({ skiplock => "Only root may use this option." })
1545 if $skiplock && $authuser ne 'root@pam';
1546
1547 my $migratedfrom = extract_param($param, 'migratedfrom');
1548 raise_param_exc({ migratedfrom => "Only root may use this option." })
1549 if $migratedfrom && $authuser ne 'root@pam';
1550
1551 # read spice ticket from STDIN
1552 my $spice_ticket;
1553 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
1554 if (defined(my $line = <>)) {
1555 chomp $line;
1556 $spice_ticket = $line;
1557 }
1558 }
1559
1560 my $storecfg = PVE::Storage::config();
1561
1562 if (&$vm_is_ha_managed($vmid) && !$stateuri &&
1563 $rpcenv->{type} ne 'ha') {
1564
1565 my $hacmd = sub {
1566 my $upid = shift;
1567
1568 my $service = "pvevm:$vmid";
1569
1570 my $cmd = ['clusvcadm', '-e', $service, '-m', $node];
1571
1572 print "Executing HA start for VM $vmid\n";
1573
1574 PVE::Tools::run_command($cmd);
1575
1576 return;
1577 };
1578
1579 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
1580
1581 } else {
1582
1583 my $realcmd = sub {
1584 my $upid = shift;
1585
1586 syslog('info', "start VM $vmid: $upid\n");
1587
1588 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
1589 $machine, $spice_ticket);
1590
1591 return;
1592 };
1593
1594 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
1595 }
1596 }});
1597
1598 __PACKAGE__->register_method({
1599 name => 'vm_stop',
1600 path => '{vmid}/status/stop',
1601 method => 'POST',
1602 protected => 1,
1603 proxyto => 'node',
1604 description => "Stop virtual machine.",
1605 permissions => {
1606 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1607 },
1608 parameters => {
1609 additionalProperties => 0,
1610 properties => {
1611 node => get_standard_option('pve-node'),
1612 vmid => get_standard_option('pve-vmid'),
1613 skiplock => get_standard_option('skiplock'),
1614 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1615 timeout => {
1616 description => "Wait maximal timeout seconds.",
1617 type => 'integer',
1618 minimum => 0,
1619 optional => 1,
1620 },
1621 keepActive => {
1622 description => "Do not decativate storage volumes.",
1623 type => 'boolean',
1624 optional => 1,
1625 default => 0,
1626 }
1627 },
1628 },
1629 returns => {
1630 type => 'string',
1631 },
1632 code => sub {
1633 my ($param) = @_;
1634
1635 my $rpcenv = PVE::RPCEnvironment::get();
1636
1637 my $authuser = $rpcenv->get_user();
1638
1639 my $node = extract_param($param, 'node');
1640
1641 my $vmid = extract_param($param, 'vmid');
1642
1643 my $skiplock = extract_param($param, 'skiplock');
1644 raise_param_exc({ skiplock => "Only root may use this option." })
1645 if $skiplock && $authuser ne 'root@pam';
1646
1647 my $keepActive = extract_param($param, 'keepActive');
1648 raise_param_exc({ keepActive => "Only root may use this option." })
1649 if $keepActive && $authuser ne 'root@pam';
1650
1651 my $migratedfrom = extract_param($param, 'migratedfrom');
1652 raise_param_exc({ migratedfrom => "Only root may use this option." })
1653 if $migratedfrom && $authuser ne 'root@pam';
1654
1655
1656 my $storecfg = PVE::Storage::config();
1657
1658 if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
1659
1660 my $hacmd = sub {
1661 my $upid = shift;
1662
1663 my $service = "pvevm:$vmid";
1664
1665 my $cmd = ['clusvcadm', '-d', $service];
1666
1667 print "Executing HA stop for VM $vmid\n";
1668
1669 PVE::Tools::run_command($cmd);
1670
1671 return;
1672 };
1673
1674 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
1675
1676 } else {
1677 my $realcmd = sub {
1678 my $upid = shift;
1679
1680 syslog('info', "stop VM $vmid: $upid\n");
1681
1682 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
1683 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
1684
1685 return;
1686 };
1687
1688 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
1689 }
1690 }});
1691
1692 __PACKAGE__->register_method({
1693 name => 'vm_reset',
1694 path => '{vmid}/status/reset',
1695 method => 'POST',
1696 protected => 1,
1697 proxyto => 'node',
1698 description => "Reset virtual machine.",
1699 permissions => {
1700 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1701 },
1702 parameters => {
1703 additionalProperties => 0,
1704 properties => {
1705 node => get_standard_option('pve-node'),
1706 vmid => get_standard_option('pve-vmid'),
1707 skiplock => get_standard_option('skiplock'),
1708 },
1709 },
1710 returns => {
1711 type => 'string',
1712 },
1713 code => sub {
1714 my ($param) = @_;
1715
1716 my $rpcenv = PVE::RPCEnvironment::get();
1717
1718 my $authuser = $rpcenv->get_user();
1719
1720 my $node = extract_param($param, 'node');
1721
1722 my $vmid = extract_param($param, 'vmid');
1723
1724 my $skiplock = extract_param($param, 'skiplock');
1725 raise_param_exc({ skiplock => "Only root may use this option." })
1726 if $skiplock && $authuser ne 'root@pam';
1727
1728 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1729
1730 my $realcmd = sub {
1731 my $upid = shift;
1732
1733 PVE::QemuServer::vm_reset($vmid, $skiplock);
1734
1735 return;
1736 };
1737
1738 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
1739 }});
1740
1741 __PACKAGE__->register_method({
1742 name => 'vm_shutdown',
1743 path => '{vmid}/status/shutdown',
1744 method => 'POST',
1745 protected => 1,
1746 proxyto => 'node',
1747 description => "Shutdown virtual machine.",
1748 permissions => {
1749 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1750 },
1751 parameters => {
1752 additionalProperties => 0,
1753 properties => {
1754 node => get_standard_option('pve-node'),
1755 vmid => get_standard_option('pve-vmid'),
1756 skiplock => get_standard_option('skiplock'),
1757 timeout => {
1758 description => "Wait maximal timeout seconds.",
1759 type => 'integer',
1760 minimum => 0,
1761 optional => 1,
1762 },
1763 forceStop => {
1764 description => "Make sure the VM stops.",
1765 type => 'boolean',
1766 optional => 1,
1767 default => 0,
1768 },
1769 keepActive => {
1770 description => "Do not decativate storage volumes.",
1771 type => 'boolean',
1772 optional => 1,
1773 default => 0,
1774 }
1775 },
1776 },
1777 returns => {
1778 type => 'string',
1779 },
1780 code => sub {
1781 my ($param) = @_;
1782
1783 my $rpcenv = PVE::RPCEnvironment::get();
1784
1785 my $authuser = $rpcenv->get_user();
1786
1787 my $node = extract_param($param, 'node');
1788
1789 my $vmid = extract_param($param, 'vmid');
1790
1791 my $skiplock = extract_param($param, 'skiplock');
1792 raise_param_exc({ skiplock => "Only root may use this option." })
1793 if $skiplock && $authuser ne 'root@pam';
1794
1795 my $keepActive = extract_param($param, 'keepActive');
1796 raise_param_exc({ keepActive => "Only root may use this option." })
1797 if $keepActive && $authuser ne 'root@pam';
1798
1799 my $storecfg = PVE::Storage::config();
1800
1801 my $realcmd = sub {
1802 my $upid = shift;
1803
1804 syslog('info', "shutdown VM $vmid: $upid\n");
1805
1806 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
1807 1, $param->{forceStop}, $keepActive);
1808
1809 return;
1810 };
1811
1812 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
1813 }});
1814
1815 __PACKAGE__->register_method({
1816 name => 'vm_suspend',
1817 path => '{vmid}/status/suspend',
1818 method => 'POST',
1819 protected => 1,
1820 proxyto => 'node',
1821 description => "Suspend virtual machine.",
1822 permissions => {
1823 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1824 },
1825 parameters => {
1826 additionalProperties => 0,
1827 properties => {
1828 node => get_standard_option('pve-node'),
1829 vmid => get_standard_option('pve-vmid'),
1830 skiplock => get_standard_option('skiplock'),
1831 },
1832 },
1833 returns => {
1834 type => 'string',
1835 },
1836 code => sub {
1837 my ($param) = @_;
1838
1839 my $rpcenv = PVE::RPCEnvironment::get();
1840
1841 my $authuser = $rpcenv->get_user();
1842
1843 my $node = extract_param($param, 'node');
1844
1845 my $vmid = extract_param($param, 'vmid');
1846
1847 my $skiplock = extract_param($param, 'skiplock');
1848 raise_param_exc({ skiplock => "Only root may use this option." })
1849 if $skiplock && $authuser ne 'root@pam';
1850
1851 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1852
1853 my $realcmd = sub {
1854 my $upid = shift;
1855
1856 syslog('info', "suspend VM $vmid: $upid\n");
1857
1858 PVE::QemuServer::vm_suspend($vmid, $skiplock);
1859
1860 return;
1861 };
1862
1863 return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd);
1864 }});
1865
1866 __PACKAGE__->register_method({
1867 name => 'vm_resume',
1868 path => '{vmid}/status/resume',
1869 method => 'POST',
1870 protected => 1,
1871 proxyto => 'node',
1872 description => "Resume virtual machine.",
1873 permissions => {
1874 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1875 },
1876 parameters => {
1877 additionalProperties => 0,
1878 properties => {
1879 node => get_standard_option('pve-node'),
1880 vmid => get_standard_option('pve-vmid'),
1881 skiplock => get_standard_option('skiplock'),
1882 },
1883 },
1884 returns => {
1885 type => 'string',
1886 },
1887 code => sub {
1888 my ($param) = @_;
1889
1890 my $rpcenv = PVE::RPCEnvironment::get();
1891
1892 my $authuser = $rpcenv->get_user();
1893
1894 my $node = extract_param($param, 'node');
1895
1896 my $vmid = extract_param($param, 'vmid');
1897
1898 my $skiplock = extract_param($param, 'skiplock');
1899 raise_param_exc({ skiplock => "Only root may use this option." })
1900 if $skiplock && $authuser ne 'root@pam';
1901
1902 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
1903
1904 my $realcmd = sub {
1905 my $upid = shift;
1906
1907 syslog('info', "resume VM $vmid: $upid\n");
1908
1909 PVE::QemuServer::vm_resume($vmid, $skiplock);
1910
1911 return;
1912 };
1913
1914 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
1915 }});
1916
1917 __PACKAGE__->register_method({
1918 name => 'vm_sendkey',
1919 path => '{vmid}/sendkey',
1920 method => 'PUT',
1921 protected => 1,
1922 proxyto => 'node',
1923 description => "Send key event to virtual machine.",
1924 permissions => {
1925 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1926 },
1927 parameters => {
1928 additionalProperties => 0,
1929 properties => {
1930 node => get_standard_option('pve-node'),
1931 vmid => get_standard_option('pve-vmid'),
1932 skiplock => get_standard_option('skiplock'),
1933 key => {
1934 description => "The key (qemu monitor encoding).",
1935 type => 'string'
1936 }
1937 },
1938 },
1939 returns => { type => 'null'},
1940 code => sub {
1941 my ($param) = @_;
1942
1943 my $rpcenv = PVE::RPCEnvironment::get();
1944
1945 my $authuser = $rpcenv->get_user();
1946
1947 my $node = extract_param($param, 'node');
1948
1949 my $vmid = extract_param($param, 'vmid');
1950
1951 my $skiplock = extract_param($param, 'skiplock');
1952 raise_param_exc({ skiplock => "Only root may use this option." })
1953 if $skiplock && $authuser ne 'root@pam';
1954
1955 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
1956
1957 return;
1958 }});
1959
1960 __PACKAGE__->register_method({
1961 name => 'vm_feature',
1962 path => '{vmid}/feature',
1963 method => 'GET',
1964 proxyto => 'node',
1965 protected => 1,
1966 description => "Check if feature for virtual machine is available.",
1967 permissions => {
1968 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1969 },
1970 parameters => {
1971 additionalProperties => 0,
1972 properties => {
1973 node => get_standard_option('pve-node'),
1974 vmid => get_standard_option('pve-vmid'),
1975 feature => {
1976 description => "Feature to check.",
1977 type => 'string',
1978 enum => [ 'snapshot', 'clone', 'copy' ],
1979 },
1980 snapname => get_standard_option('pve-snapshot-name', {
1981 optional => 1,
1982 }),
1983 },
1984 },
1985 returns => {
1986 type => "object",
1987 properties => {
1988 hasFeature => { type => 'boolean' },
1989 nodes => {
1990 type => 'array',
1991 items => { type => 'string' },
1992 }
1993 },
1994 },
1995 code => sub {
1996 my ($param) = @_;
1997
1998 my $node = extract_param($param, 'node');
1999
2000 my $vmid = extract_param($param, 'vmid');
2001
2002 my $snapname = extract_param($param, 'snapname');
2003
2004 my $feature = extract_param($param, 'feature');
2005
2006 my $running = PVE::QemuServer::check_running($vmid);
2007
2008 my $conf = PVE::QemuServer::load_config($vmid);
2009
2010 if($snapname){
2011 my $snap = $conf->{snapshots}->{$snapname};
2012 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2013 $conf = $snap;
2014 }
2015 my $storecfg = PVE::Storage::config();
2016
2017 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
2018 my $hasFeature = PVE::QemuServer::has_feature($feature, $conf, $storecfg, $snapname, $running);
2019
2020 return {
2021 hasFeature => $hasFeature,
2022 nodes => [ keys %$nodelist ],
2023 };
2024 }});
2025
2026 __PACKAGE__->register_method({
2027 name => 'clone_vm',
2028 path => '{vmid}/clone',
2029 method => 'POST',
2030 protected => 1,
2031 proxyto => 'node',
2032 description => "Create a copy of virtual machine/template.",
2033 permissions => {
2034 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2035 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2036 "'Datastore.AllocateSpace' on any used storage.",
2037 check =>
2038 [ 'and',
2039 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2040 [ 'or',
2041 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2042 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2043 ],
2044 ]
2045 },
2046 parameters => {
2047 additionalProperties => 0,
2048 properties => {
2049 node => get_standard_option('pve-node'),
2050 vmid => get_standard_option('pve-vmid'),
2051 newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }),
2052 name => {
2053 optional => 1,
2054 type => 'string', format => 'dns-name',
2055 description => "Set a name for the new VM.",
2056 },
2057 description => {
2058 optional => 1,
2059 type => 'string',
2060 description => "Description for the new VM.",
2061 },
2062 pool => {
2063 optional => 1,
2064 type => 'string', format => 'pve-poolid',
2065 description => "Add the new VM to the specified pool.",
2066 },
2067 snapname => get_standard_option('pve-snapshot-name', {
2068 requires => 'full',
2069 optional => 1,
2070 }),
2071 storage => get_standard_option('pve-storage-id', {
2072 description => "Target storage for full clone.",
2073 requires => 'full',
2074 optional => 1,
2075 }),
2076 'format' => {
2077 description => "Target format for file storage.",
2078 requires => 'full',
2079 type => 'string',
2080 optional => 1,
2081 enum => [ 'raw', 'qcow2', 'vmdk'],
2082 },
2083 full => {
2084 optional => 1,
2085 type => 'boolean',
2086 description => "Create a full copy of all disk. This is always done when " .
2087 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2088 default => 0,
2089 },
2090 target => get_standard_option('pve-node', {
2091 description => "Target node. Only allowed if the original VM is on shared storage.",
2092 optional => 1,
2093 }),
2094 },
2095 },
2096 returns => {
2097 type => 'string',
2098 },
2099 code => sub {
2100 my ($param) = @_;
2101
2102 my $rpcenv = PVE::RPCEnvironment::get();
2103
2104 my $authuser = $rpcenv->get_user();
2105
2106 my $node = extract_param($param, 'node');
2107
2108 my $vmid = extract_param($param, 'vmid');
2109
2110 my $newid = extract_param($param, 'newid');
2111
2112 my $pool = extract_param($param, 'pool');
2113
2114 if (defined($pool)) {
2115 $rpcenv->check_pool_exist($pool);
2116 }
2117
2118 my $snapname = extract_param($param, 'snapname');
2119
2120 my $storage = extract_param($param, 'storage');
2121
2122 my $format = extract_param($param, 'format');
2123
2124 my $target = extract_param($param, 'target');
2125
2126 my $localnode = PVE::INotify::nodename();
2127
2128 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2129
2130 PVE::Cluster::check_node_exists($target) if $target;
2131
2132 my $storecfg = PVE::Storage::config();
2133
2134 if ($storage) {
2135 # check if storage is enabled on local node
2136 PVE::Storage::storage_check_enabled($storecfg, $storage);
2137 if ($target) {
2138 # check if storage is available on target node
2139 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2140 # clone only works if target storage is shared
2141 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2142 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2143 }
2144 }
2145
2146 PVE::Cluster::check_cfs_quorum();
2147
2148 my $running = PVE::QemuServer::check_running($vmid) || 0;
2149
2150 # exclusive lock if VM is running - else shared lock is enough;
2151 my $shared_lock = $running ? 0 : 1;
2152
2153 my $clonefn = sub {
2154
2155 # do all tests after lock
2156 # we also try to do all tests before we fork the worker
2157
2158 my $conf = PVE::QemuServer::load_config($vmid);
2159
2160 PVE::QemuServer::check_lock($conf);
2161
2162 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
2163
2164 die "unexpected state change\n" if $verify_running != $running;
2165
2166 die "snapshot '$snapname' does not exist\n"
2167 if $snapname && !defined( $conf->{snapshots}->{$snapname});
2168
2169 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
2170
2171 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2172
2173 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2174
2175 my $conffile = PVE::QemuServer::config_file($newid);
2176
2177 die "unable to create VM $newid: config file already exists\n"
2178 if -f $conffile;
2179
2180 my $newconf = { lock => 'clone' };
2181 my $drives = {};
2182 my $vollist = [];
2183
2184 foreach my $opt (keys %$oldconf) {
2185 my $value = $oldconf->{$opt};
2186
2187 # do not copy snapshot related info
2188 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2189 $opt eq 'vmstate' || $opt eq 'snapstate';
2190
2191 # always change MAC! address
2192 if ($opt =~ m/^net(\d+)$/) {
2193 my $net = PVE::QemuServer::parse_net($value);
2194 $net->{macaddr} = PVE::Tools::random_ether_addr();
2195 $newconf->{$opt} = PVE::QemuServer::print_net($net);
2196 } elsif (my $drive = PVE::QemuServer::parse_drive($opt, $value)) {
2197 if (PVE::QemuServer::drive_is_cdrom($drive)) {
2198 $newconf->{$opt} = $value; # simply copy configuration
2199 } else {
2200 if ($param->{full} || !PVE::Storage::volume_is_base($storecfg, $drive->{file})) {
2201 die "Full clone feature is not available"
2202 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
2203 $drive->{full} = 1;
2204 }
2205 $drives->{$opt} = $drive;
2206 push @$vollist, $drive->{file};
2207 }
2208 } else {
2209 # copy everything else
2210 $newconf->{$opt} = $value;
2211 }
2212 }
2213
2214 delete $newconf->{template};
2215
2216 if ($param->{name}) {
2217 $newconf->{name} = $param->{name};
2218 } else {
2219 if ($oldconf->{name}) {
2220 $newconf->{name} = "Copy-of-$oldconf->{name}";
2221 } else {
2222 $newconf->{name} = "Copy-of-VM-$vmid";
2223 }
2224 }
2225
2226 if ($param->{description}) {
2227 $newconf->{description} = $param->{description};
2228 }
2229
2230 # create empty/temp config - this fails if VM already exists on other node
2231 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
2232
2233 my $realcmd = sub {
2234 my $upid = shift;
2235
2236 my $newvollist = [];
2237
2238 eval {
2239 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2240
2241 PVE::Storage::activate_volumes($storecfg, $vollist);
2242
2243 foreach my $opt (keys %$drives) {
2244 my $drive = $drives->{$opt};
2245
2246 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
2247 $newid, $storage, $format, $drive->{full}, $newvollist);
2248
2249 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2250
2251 PVE::QemuServer::update_config_nolock($newid, $newconf, 1);
2252 }
2253
2254 delete $newconf->{lock};
2255 PVE::QemuServer::update_config_nolock($newid, $newconf, 1);
2256
2257 if ($target) {
2258 my $newconffile = PVE::QemuServer::config_file($newid, $target);
2259 die "Failed to move config to node '$target' - rename failed: $!\n"
2260 if !rename($conffile, $newconffile);
2261 }
2262
2263 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
2264 };
2265 if (my $err = $@) {
2266 unlink $conffile;
2267
2268 sleep 1; # some storage like rbd need to wait before release volume - really?
2269
2270 foreach my $volid (@$newvollist) {
2271 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2272 warn $@ if $@;
2273 }
2274 die "clone failed: $err";
2275 }
2276
2277 return;
2278 };
2279
2280 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2281 };
2282
2283 return PVE::QemuServer::lock_config_mode($vmid, 1, $shared_lock, sub {
2284 # Aquire exclusive lock lock for $newid
2285 return PVE::QemuServer::lock_config_full($newid, 1, $clonefn);
2286 });
2287
2288 }});
2289
2290 __PACKAGE__->register_method({
2291 name => 'move_vm_disk',
2292 path => '{vmid}/move_disk',
2293 method => 'POST',
2294 protected => 1,
2295 proxyto => 'node',
2296 description => "Move volume to different storage.",
2297 permissions => {
2298 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
2299 "and 'Datastore.AllocateSpace' permissions on the storage.",
2300 check =>
2301 [ 'and',
2302 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2303 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2304 ],
2305 },
2306 parameters => {
2307 additionalProperties => 0,
2308 properties => {
2309 node => get_standard_option('pve-node'),
2310 vmid => get_standard_option('pve-vmid'),
2311 disk => {
2312 type => 'string',
2313 description => "The disk you want to move.",
2314 enum => [ PVE::QemuServer::disknames() ],
2315 },
2316 storage => get_standard_option('pve-storage-id', { description => "Target Storage." }),
2317 'format' => {
2318 type => 'string',
2319 description => "Target Format.",
2320 enum => [ 'raw', 'qcow2', 'vmdk' ],
2321 optional => 1,
2322 },
2323 delete => {
2324 type => 'boolean',
2325 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2326 optional => 1,
2327 default => 0,
2328 },
2329 digest => {
2330 type => 'string',
2331 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2332 maxLength => 40,
2333 optional => 1,
2334 },
2335 },
2336 },
2337 returns => {
2338 type => 'string',
2339 description => "the task ID.",
2340 },
2341 code => sub {
2342 my ($param) = @_;
2343
2344 my $rpcenv = PVE::RPCEnvironment::get();
2345
2346 my $authuser = $rpcenv->get_user();
2347
2348 my $node = extract_param($param, 'node');
2349
2350 my $vmid = extract_param($param, 'vmid');
2351
2352 my $digest = extract_param($param, 'digest');
2353
2354 my $disk = extract_param($param, 'disk');
2355
2356 my $storeid = extract_param($param, 'storage');
2357
2358 my $format = extract_param($param, 'format');
2359
2360 my $storecfg = PVE::Storage::config();
2361
2362 my $updatefn = sub {
2363
2364 my $conf = PVE::QemuServer::load_config($vmid);
2365
2366 die "checksum missmatch (file change by other user?)\n"
2367 if $digest && $digest ne $conf->{digest};
2368
2369 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2370
2371 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
2372
2373 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
2374
2375 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
2376
2377 my $oldfmt;
2378 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
2379 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2380 $oldfmt = $1;
2381 }
2382
2383 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
2384 (!$format || !$oldfmt || $oldfmt eq $format);
2385
2386 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2387
2388 my $running = PVE::QemuServer::check_running($vmid);
2389
2390 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
2391
2392 my $realcmd = sub {
2393
2394 my $newvollist = [];
2395
2396 eval {
2397 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
2398
2399 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
2400 $vmid, $storeid, $format, 1, $newvollist);
2401
2402 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
2403
2404 PVE::QemuServer::add_unused_volume($conf, $old_volid) if !$param->{delete};
2405
2406 PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
2407 };
2408 if (my $err = $@) {
2409
2410 foreach my $volid (@$newvollist) {
2411 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2412 warn $@ if $@;
2413 }
2414 die "storage migration failed: $err";
2415 }
2416
2417 if ($param->{delete}) {
2418 eval { PVE::Storage::vdisk_free($storecfg, $old_volid); };
2419 warn $@ if $@;
2420 }
2421 };
2422
2423 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
2424 };
2425
2426 return PVE::QemuServer::lock_config($vmid, $updatefn);
2427 }});
2428
2429 __PACKAGE__->register_method({
2430 name => 'migrate_vm',
2431 path => '{vmid}/migrate',
2432 method => 'POST',
2433 protected => 1,
2434 proxyto => 'node',
2435 description => "Migrate virtual machine. Creates a new migration task.",
2436 permissions => {
2437 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
2438 },
2439 parameters => {
2440 additionalProperties => 0,
2441 properties => {
2442 node => get_standard_option('pve-node'),
2443 vmid => get_standard_option('pve-vmid'),
2444 target => get_standard_option('pve-node', { description => "Target node." }),
2445 online => {
2446 type => 'boolean',
2447 description => "Use online/live migration.",
2448 optional => 1,
2449 },
2450 force => {
2451 type => 'boolean',
2452 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
2453 optional => 1,
2454 },
2455 },
2456 },
2457 returns => {
2458 type => 'string',
2459 description => "the task ID.",
2460 },
2461 code => sub {
2462 my ($param) = @_;
2463
2464 my $rpcenv = PVE::RPCEnvironment::get();
2465
2466 my $authuser = $rpcenv->get_user();
2467
2468 my $target = extract_param($param, 'target');
2469
2470 my $localnode = PVE::INotify::nodename();
2471 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
2472
2473 PVE::Cluster::check_cfs_quorum();
2474
2475 PVE::Cluster::check_node_exists($target);
2476
2477 my $targetip = PVE::Cluster::remote_node_ip($target);
2478
2479 my $vmid = extract_param($param, 'vmid');
2480
2481 raise_param_exc({ force => "Only root may use this option." })
2482 if $param->{force} && $authuser ne 'root@pam';
2483
2484 # test if VM exists
2485 my $conf = PVE::QemuServer::load_config($vmid);
2486
2487 # try to detect errors early
2488
2489 PVE::QemuServer::check_lock($conf);
2490
2491 if (PVE::QemuServer::check_running($vmid)) {
2492 die "cant migrate running VM without --online\n"
2493 if !$param->{online};
2494 }
2495
2496 my $storecfg = PVE::Storage::config();
2497 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
2498
2499 if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
2500
2501 my $hacmd = sub {
2502 my $upid = shift;
2503
2504 my $service = "pvevm:$vmid";
2505
2506 my $cmd = ['clusvcadm', '-M', $service, '-m', $target];
2507
2508 print "Executing HA migrate for VM $vmid to node $target\n";
2509
2510 PVE::Tools::run_command($cmd);
2511
2512 return;
2513 };
2514
2515 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
2516
2517 } else {
2518
2519 my $realcmd = sub {
2520 my $upid = shift;
2521
2522 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
2523 };
2524
2525 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd);
2526 }
2527
2528 }});
2529
2530 __PACKAGE__->register_method({
2531 name => 'monitor',
2532 path => '{vmid}/monitor',
2533 method => 'POST',
2534 protected => 1,
2535 proxyto => 'node',
2536 description => "Execute Qemu monitor commands.",
2537 permissions => {
2538 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2539 },
2540 parameters => {
2541 additionalProperties => 0,
2542 properties => {
2543 node => get_standard_option('pve-node'),
2544 vmid => get_standard_option('pve-vmid'),
2545 command => {
2546 type => 'string',
2547 description => "The monitor command.",
2548 }
2549 },
2550 },
2551 returns => { type => 'string'},
2552 code => sub {
2553 my ($param) = @_;
2554
2555 my $vmid = $param->{vmid};
2556
2557 my $conf = PVE::QemuServer::load_config ($vmid); # check if VM exists
2558
2559 my $res = '';
2560 eval {
2561 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
2562 };
2563 $res = "ERROR: $@" if $@;
2564
2565 return $res;
2566 }});
2567
2568 __PACKAGE__->register_method({
2569 name => 'resize_vm',
2570 path => '{vmid}/resize',
2571 method => 'PUT',
2572 protected => 1,
2573 proxyto => 'node',
2574 description => "Extend volume size.",
2575 permissions => {
2576 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2577 },
2578 parameters => {
2579 additionalProperties => 0,
2580 properties => {
2581 node => get_standard_option('pve-node'),
2582 vmid => get_standard_option('pve-vmid'),
2583 skiplock => get_standard_option('skiplock'),
2584 disk => {
2585 type => 'string',
2586 description => "The disk you want to resize.",
2587 enum => [PVE::QemuServer::disknames()],
2588 },
2589 size => {
2590 type => 'string',
2591 pattern => '\+?\d+(\.\d+)?[KMGT]?',
2592 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.",
2593 },
2594 digest => {
2595 type => 'string',
2596 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2597 maxLength => 40,
2598 optional => 1,
2599 },
2600 },
2601 },
2602 returns => { type => 'null'},
2603 code => sub {
2604 my ($param) = @_;
2605
2606 my $rpcenv = PVE::RPCEnvironment::get();
2607
2608 my $authuser = $rpcenv->get_user();
2609
2610 my $node = extract_param($param, 'node');
2611
2612 my $vmid = extract_param($param, 'vmid');
2613
2614 my $digest = extract_param($param, 'digest');
2615
2616 my $disk = extract_param($param, 'disk');
2617
2618 my $sizestr = extract_param($param, 'size');
2619
2620 my $skiplock = extract_param($param, 'skiplock');
2621 raise_param_exc({ skiplock => "Only root may use this option." })
2622 if $skiplock && $authuser ne 'root@pam';
2623
2624 my $storecfg = PVE::Storage::config();
2625
2626 my $updatefn = sub {
2627
2628 my $conf = PVE::QemuServer::load_config($vmid);
2629
2630 die "checksum missmatch (file change by other user?)\n"
2631 if $digest && $digest ne $conf->{digest};
2632 PVE::QemuServer::check_lock($conf) if !$skiplock;
2633
2634 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2635
2636 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
2637
2638 my $volid = $drive->{file};
2639
2640 die "disk '$disk' has no associated volume\n" if !$volid;
2641
2642 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
2643
2644 die "you can't online resize a virtio windows bootdisk\n"
2645 if PVE::QemuServer::check_running($vmid) && $conf->{bootdisk} eq $disk && $conf->{ostype} =~ m/^w/ && $disk =~ m/^virtio/;
2646
2647 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
2648
2649 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
2650
2651 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
2652
2653 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
2654 my ($ext, $newsize, $unit) = ($1, $2, $4);
2655 if ($unit) {
2656 if ($unit eq 'K') {
2657 $newsize = $newsize * 1024;
2658 } elsif ($unit eq 'M') {
2659 $newsize = $newsize * 1024 * 1024;
2660 } elsif ($unit eq 'G') {
2661 $newsize = $newsize * 1024 * 1024 * 1024;
2662 } elsif ($unit eq 'T') {
2663 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
2664 }
2665 }
2666 $newsize += $size if $ext;
2667 $newsize = int($newsize);
2668
2669 die "unable to skrink disk size\n" if $newsize < $size;
2670
2671 return if $size == $newsize;
2672
2673 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
2674
2675 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
2676
2677 $drive->{size} = $newsize;
2678 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
2679
2680 PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
2681 };
2682
2683 PVE::QemuServer::lock_config($vmid, $updatefn);
2684 return undef;
2685 }});
2686
2687 __PACKAGE__->register_method({
2688 name => 'snapshot_list',
2689 path => '{vmid}/snapshot',
2690 method => 'GET',
2691 description => "List all snapshots.",
2692 permissions => {
2693 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2694 },
2695 proxyto => 'node',
2696 protected => 1, # qemu pid files are only readable by root
2697 parameters => {
2698 additionalProperties => 0,
2699 properties => {
2700 vmid => get_standard_option('pve-vmid'),
2701 node => get_standard_option('pve-node'),
2702 },
2703 },
2704 returns => {
2705 type => 'array',
2706 items => {
2707 type => "object",
2708 properties => {},
2709 },
2710 links => [ { rel => 'child', href => "{name}" } ],
2711 },
2712 code => sub {
2713 my ($param) = @_;
2714
2715 my $vmid = $param->{vmid};
2716
2717 my $conf = PVE::QemuServer::load_config($vmid);
2718 my $snaphash = $conf->{snapshots} || {};
2719
2720 my $res = [];
2721
2722 foreach my $name (keys %$snaphash) {
2723 my $d = $snaphash->{$name};
2724 my $item = {
2725 name => $name,
2726 snaptime => $d->{snaptime} || 0,
2727 vmstate => $d->{vmstate} ? 1 : 0,
2728 description => $d->{description} || '',
2729 };
2730 $item->{parent} = $d->{parent} if $d->{parent};
2731 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
2732 push @$res, $item;
2733 }
2734
2735 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
2736 my $current = { name => 'current', digest => $conf->{digest}, running => $running };
2737 $current->{parent} = $conf->{parent} if $conf->{parent};
2738
2739 push @$res, $current;
2740
2741 return $res;
2742 }});
2743
2744 __PACKAGE__->register_method({
2745 name => 'snapshot',
2746 path => '{vmid}/snapshot',
2747 method => 'POST',
2748 protected => 1,
2749 proxyto => 'node',
2750 description => "Snapshot a VM.",
2751 permissions => {
2752 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
2753 },
2754 parameters => {
2755 additionalProperties => 0,
2756 properties => {
2757 node => get_standard_option('pve-node'),
2758 vmid => get_standard_option('pve-vmid'),
2759 snapname => get_standard_option('pve-snapshot-name'),
2760 vmstate => {
2761 optional => 1,
2762 type => 'boolean',
2763 description => "Save the vmstate",
2764 },
2765 freezefs => {
2766 optional => 1,
2767 type => 'boolean',
2768 description => "Freeze the filesystem",
2769 },
2770 description => {
2771 optional => 1,
2772 type => 'string',
2773 description => "A textual description or comment.",
2774 },
2775 },
2776 },
2777 returns => {
2778 type => 'string',
2779 description => "the task ID.",
2780 },
2781 code => sub {
2782 my ($param) = @_;
2783
2784 my $rpcenv = PVE::RPCEnvironment::get();
2785
2786 my $authuser = $rpcenv->get_user();
2787
2788 my $node = extract_param($param, 'node');
2789
2790 my $vmid = extract_param($param, 'vmid');
2791
2792 my $snapname = extract_param($param, 'snapname');
2793
2794 die "unable to use snapshot name 'current' (reserved name)\n"
2795 if $snapname eq 'current';
2796
2797 my $realcmd = sub {
2798 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
2799 PVE::QemuServer::snapshot_create($vmid, $snapname, $param->{vmstate},
2800 $param->{freezefs}, $param->{description});
2801 };
2802
2803 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
2804 }});
2805
2806 __PACKAGE__->register_method({
2807 name => 'snapshot_cmd_idx',
2808 path => '{vmid}/snapshot/{snapname}',
2809 description => '',
2810 method => 'GET',
2811 permissions => {
2812 user => 'all',
2813 },
2814 parameters => {
2815 additionalProperties => 0,
2816 properties => {
2817 vmid => get_standard_option('pve-vmid'),
2818 node => get_standard_option('pve-node'),
2819 snapname => get_standard_option('pve-snapshot-name'),
2820 },
2821 },
2822 returns => {
2823 type => 'array',
2824 items => {
2825 type => "object",
2826 properties => {},
2827 },
2828 links => [ { rel => 'child', href => "{cmd}" } ],
2829 },
2830 code => sub {
2831 my ($param) = @_;
2832
2833 my $res = [];
2834
2835 push @$res, { cmd => 'rollback' };
2836 push @$res, { cmd => 'config' };
2837
2838 return $res;
2839 }});
2840
2841 __PACKAGE__->register_method({
2842 name => 'update_snapshot_config',
2843 path => '{vmid}/snapshot/{snapname}/config',
2844 method => 'PUT',
2845 protected => 1,
2846 proxyto => 'node',
2847 description => "Update snapshot metadata.",
2848 permissions => {
2849 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
2850 },
2851 parameters => {
2852 additionalProperties => 0,
2853 properties => {
2854 node => get_standard_option('pve-node'),
2855 vmid => get_standard_option('pve-vmid'),
2856 snapname => get_standard_option('pve-snapshot-name'),
2857 description => {
2858 optional => 1,
2859 type => 'string',
2860 description => "A textual description or comment.",
2861 },
2862 },
2863 },
2864 returns => { type => 'null' },
2865 code => sub {
2866 my ($param) = @_;
2867
2868 my $rpcenv = PVE::RPCEnvironment::get();
2869
2870 my $authuser = $rpcenv->get_user();
2871
2872 my $vmid = extract_param($param, 'vmid');
2873
2874 my $snapname = extract_param($param, 'snapname');
2875
2876 return undef if !defined($param->{description});
2877
2878 my $updatefn = sub {
2879
2880 my $conf = PVE::QemuServer::load_config($vmid);
2881
2882 PVE::QemuServer::check_lock($conf);
2883
2884 my $snap = $conf->{snapshots}->{$snapname};
2885
2886 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2887
2888 $snap->{description} = $param->{description} if defined($param->{description});
2889
2890 PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
2891 };
2892
2893 PVE::QemuServer::lock_config($vmid, $updatefn);
2894
2895 return undef;
2896 }});
2897
2898 __PACKAGE__->register_method({
2899 name => 'get_snapshot_config',
2900 path => '{vmid}/snapshot/{snapname}/config',
2901 method => 'GET',
2902 proxyto => 'node',
2903 description => "Get snapshot configuration",
2904 permissions => {
2905 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
2906 },
2907 parameters => {
2908 additionalProperties => 0,
2909 properties => {
2910 node => get_standard_option('pve-node'),
2911 vmid => get_standard_option('pve-vmid'),
2912 snapname => get_standard_option('pve-snapshot-name'),
2913 },
2914 },
2915 returns => { type => "object" },
2916 code => sub {
2917 my ($param) = @_;
2918
2919 my $rpcenv = PVE::RPCEnvironment::get();
2920
2921 my $authuser = $rpcenv->get_user();
2922
2923 my $vmid = extract_param($param, 'vmid');
2924
2925 my $snapname = extract_param($param, 'snapname');
2926
2927 my $conf = PVE::QemuServer::load_config($vmid);
2928
2929 my $snap = $conf->{snapshots}->{$snapname};
2930
2931 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2932
2933 return $snap;
2934 }});
2935
2936 __PACKAGE__->register_method({
2937 name => 'rollback',
2938 path => '{vmid}/snapshot/{snapname}/rollback',
2939 method => 'POST',
2940 protected => 1,
2941 proxyto => 'node',
2942 description => "Rollback VM state to specified snapshot.",
2943 permissions => {
2944 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
2945 },
2946 parameters => {
2947 additionalProperties => 0,
2948 properties => {
2949 node => get_standard_option('pve-node'),
2950 vmid => get_standard_option('pve-vmid'),
2951 snapname => get_standard_option('pve-snapshot-name'),
2952 },
2953 },
2954 returns => {
2955 type => 'string',
2956 description => "the task ID.",
2957 },
2958 code => sub {
2959 my ($param) = @_;
2960
2961 my $rpcenv = PVE::RPCEnvironment::get();
2962
2963 my $authuser = $rpcenv->get_user();
2964
2965 my $node = extract_param($param, 'node');
2966
2967 my $vmid = extract_param($param, 'vmid');
2968
2969 my $snapname = extract_param($param, 'snapname');
2970
2971 my $realcmd = sub {
2972 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
2973 PVE::QemuServer::snapshot_rollback($vmid, $snapname);
2974 };
2975
2976 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd);
2977 }});
2978
2979 __PACKAGE__->register_method({
2980 name => 'delsnapshot',
2981 path => '{vmid}/snapshot/{snapname}',
2982 method => 'DELETE',
2983 protected => 1,
2984 proxyto => 'node',
2985 description => "Delete a VM snapshot.",
2986 permissions => {
2987 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
2988 },
2989 parameters => {
2990 additionalProperties => 0,
2991 properties => {
2992 node => get_standard_option('pve-node'),
2993 vmid => get_standard_option('pve-vmid'),
2994 snapname => get_standard_option('pve-snapshot-name'),
2995 force => {
2996 optional => 1,
2997 type => 'boolean',
2998 description => "For removal from config file, even if removing disk snapshots fails.",
2999 },
3000 },
3001 },
3002 returns => {
3003 type => 'string',
3004 description => "the task ID.",
3005 },
3006 code => sub {
3007 my ($param) = @_;
3008
3009 my $rpcenv = PVE::RPCEnvironment::get();
3010
3011 my $authuser = $rpcenv->get_user();
3012
3013 my $node = extract_param($param, 'node');
3014
3015 my $vmid = extract_param($param, 'vmid');
3016
3017 my $snapname = extract_param($param, 'snapname');
3018
3019 my $realcmd = sub {
3020 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
3021 PVE::QemuServer::snapshot_delete($vmid, $snapname, $param->{force});
3022 };
3023
3024 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3025 }});
3026
3027 __PACKAGE__->register_method({
3028 name => 'template',
3029 path => '{vmid}/template',
3030 method => 'POST',
3031 protected => 1,
3032 proxyto => 'node',
3033 description => "Create a Template.",
3034 permissions => {
3035 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3036 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3037 },
3038 parameters => {
3039 additionalProperties => 0,
3040 properties => {
3041 node => get_standard_option('pve-node'),
3042 vmid => get_standard_option('pve-vmid'),
3043 disk => {
3044 optional => 1,
3045 type => 'string',
3046 description => "If you want to convert only 1 disk to base image.",
3047 enum => [PVE::QemuServer::disknames()],
3048 },
3049
3050 },
3051 },
3052 returns => { type => 'null'},
3053 code => sub {
3054 my ($param) = @_;
3055
3056 my $rpcenv = PVE::RPCEnvironment::get();
3057
3058 my $authuser = $rpcenv->get_user();
3059
3060 my $node = extract_param($param, 'node');
3061
3062 my $vmid = extract_param($param, 'vmid');
3063
3064 my $disk = extract_param($param, 'disk');
3065
3066 my $updatefn = sub {
3067
3068 my $conf = PVE::QemuServer::load_config($vmid);
3069
3070 PVE::QemuServer::check_lock($conf);
3071
3072 die "unable to create template, because VM contains snapshots\n"
3073 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
3074
3075 die "you can't convert a template to a template\n"
3076 if PVE::QemuServer::is_template($conf) && !$disk;
3077
3078 die "you can't convert a VM to template if VM is running\n"
3079 if PVE::QemuServer::check_running($vmid);
3080
3081 my $realcmd = sub {
3082 PVE::QemuServer::template_create($vmid, $conf, $disk);
3083 };
3084
3085 $conf->{template} = 1;
3086 PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
3087
3088 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3089 };
3090
3091 PVE::QemuServer::lock_config($vmid, $updatefn);
3092 return undef;
3093 }});
3094
3095 1;