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