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