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