]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu.pm
Add audio device to HW permissions
[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 use UUID;
8 use POSIX;
9 use IO::Socket::IP;
10 use URI::Escape;
11
12 use PVE::Cluster qw (cfs_read_file cfs_write_file);;
13 use PVE::SafeSyslog;
14 use PVE::Tools qw(extract_param);
15 use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
16 use PVE::Storage;
17 use PVE::JSONSchema qw(get_standard_option);
18 use PVE::RESTHandler;
19 use PVE::ReplicationConfig;
20 use PVE::GuestHelpers;
21 use PVE::QemuConfig;
22 use PVE::QemuServer;
23 use PVE::QemuMigrate;
24 use PVE::RPCEnvironment;
25 use PVE::AccessControl;
26 use PVE::INotify;
27 use PVE::Network;
28 use PVE::Firewall;
29 use PVE::API2::Firewall::VM;
30 use PVE::API2::Qemu::Agent;
31
32 BEGIN {
33 if (!$ENV{PVE_GENERATING_DOCS}) {
34 require PVE::HA::Env::PVE2;
35 import PVE::HA::Env::PVE2;
36 require PVE::HA::Config;
37 import PVE::HA::Config;
38 }
39 }
40
41 use Data::Dumper; # fixme: remove
42
43 use base qw(PVE::RESTHandler);
44
45 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.";
46
47 my $resolve_cdrom_alias = sub {
48 my $param = shift;
49
50 if (my $value = $param->{cdrom}) {
51 $value .= ",media=cdrom" if $value !~ m/media=/;
52 $param->{ide2} = $value;
53 delete $param->{cdrom};
54 }
55 };
56
57 my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
58 my $check_storage_access = sub {
59 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
60
61 PVE::QemuServer::foreach_drive($settings, sub {
62 my ($ds, $drive) = @_;
63
64 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
65
66 my $volid = $drive->{file};
67
68 if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit')) {
69 # nothing to check
70 } elsif ($volid =~ m/^(([^:\s]+):)?(cloudinit)$/) {
71 # nothing to check
72 } elsif ($isCDROM && ($volid eq 'cdrom')) {
73 $rpcenv->check($authuser, "/", ['Sys.Console']);
74 } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) {
75 my ($storeid, $size) = ($2 || $default_storage, $3);
76 die "no storage ID specified (and no default storage)\n" if !$storeid;
77 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
78 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
79 raise_param_exc({ storage => "storage '$storeid' does not support vm images"})
80 if !$scfg->{content}->{images};
81 } else {
82 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
83 }
84 });
85
86 $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
87 if defined($settings->{vmstatestorage});
88 };
89
90 my $check_storage_access_clone = sub {
91 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
92
93 my $sharedvm = 1;
94
95 PVE::QemuServer::foreach_drive($conf, sub {
96 my ($ds, $drive) = @_;
97
98 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
99
100 my $volid = $drive->{file};
101
102 return if !$volid || $volid eq 'none';
103
104 if ($isCDROM) {
105 if ($volid eq 'cdrom') {
106 $rpcenv->check($authuser, "/", ['Sys.Console']);
107 } else {
108 # we simply allow access
109 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
110 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
111 $sharedvm = 0 if !$scfg->{shared};
112
113 }
114 } else {
115 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
116 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
117 $sharedvm = 0 if !$scfg->{shared};
118
119 $sid = $storage if $storage;
120 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
121 }
122 });
123
124 $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
125 if defined($conf->{vmstatestorage});
126
127 return $sharedvm;
128 };
129
130 # Note: $pool is only needed when creating a VM, because pool permissions
131 # are automatically inherited if VM already exists inside a pool.
132 my $create_disks = sub {
133 my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
134
135 my $vollist = [];
136
137 my $res = {};
138
139 my $code = sub {
140 my ($ds, $disk) = @_;
141
142 my $volid = $disk->{file};
143
144 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
145 delete $disk->{size};
146 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
147 } elsif ($volid =~ m!^(?:([^/:\s]+):)?cloudinit$!) {
148 my $storeid = $1 || $default_storage;
149 die "no storage ID specified (and no default storage)\n" if !$storeid;
150 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
151 my $name = "vm-$vmid-cloudinit";
152
153 my $fmt = undef;
154 if ($scfg->{path}) {
155 $fmt = $disk->{format} // "qcow2";
156 $name .= ".$fmt";
157 } else {
158 $fmt = $disk->{format} // "raw";
159 }
160
161 # Initial disk created with 4 MB and aligned to 4MB on regeneration
162 my $ci_size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE;
163 my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
164 $disk->{file} = $volid;
165 $disk->{media} = 'cdrom';
166 push @$vollist, $volid;
167 delete $disk->{format}; # no longer needed
168 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
169 } elsif ($volid =~ $NEW_DISK_RE) {
170 my ($storeid, $size) = ($2 || $default_storage, $3);
171 die "no storage ID specified (and no default storage)\n" if !$storeid;
172 my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
173 my $fmt = $disk->{format} || $defformat;
174
175 $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
176
177 my $volid;
178 if ($ds eq 'efidisk0') {
179 ($volid, $size) = PVE::QemuServer::create_efidisk($storecfg, $storeid, $vmid, $fmt, $arch);
180 } else {
181 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size);
182 }
183 push @$vollist, $volid;
184 $disk->{file} = $volid;
185 $disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b');
186 delete $disk->{format}; # no longer needed
187 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
188 } else {
189
190 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
191
192 my $volid_is_new = 1;
193
194 if ($conf->{$ds}) {
195 my $olddrive = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
196 $volid_is_new = undef if $olddrive->{file} && $olddrive->{file} eq $volid;
197 }
198
199 if ($volid_is_new) {
200
201 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
202
203 PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid;
204
205 my $size = PVE::Storage::volume_size_info($storecfg, $volid);
206
207 die "volume $volid does not exists\n" if !$size;
208
209 $disk->{size} = $size;
210 }
211
212 $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
213 }
214 };
215
216 eval { PVE::QemuServer::foreach_drive($settings, $code); };
217
218 # free allocated images on error
219 if (my $err = $@) {
220 syslog('err', "VM $vmid creating disks failed");
221 foreach my $volid (@$vollist) {
222 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
223 warn $@ if $@;
224 }
225 die $err;
226 }
227
228 # modify vm config if everything went well
229 foreach my $ds (keys %$res) {
230 $conf->{$ds} = $res->{$ds};
231 }
232
233 return $vollist;
234 };
235
236 my $cpuoptions = {
237 'cores' => 1,
238 'cpu' => 1,
239 'cpulimit' => 1,
240 'cpuunits' => 1,
241 'numa' => 1,
242 'smp' => 1,
243 'sockets' => 1,
244 'vcpus' => 1,
245 };
246
247 my $memoryoptions = {
248 'memory' => 1,
249 'balloon' => 1,
250 'shares' => 1,
251 };
252
253 my $hwtypeoptions = {
254 'acpi' => 1,
255 'hotplug' => 1,
256 'kvm' => 1,
257 'machine' => 1,
258 'scsihw' => 1,
259 'smbios1' => 1,
260 'tablet' => 1,
261 'vga' => 1,
262 'watchdog' => 1,
263 'audio0' => 1,
264 };
265
266 my $generaloptions = {
267 'agent' => 1,
268 'autostart' => 1,
269 'bios' => 1,
270 'description' => 1,
271 'keyboard' => 1,
272 'localtime' => 1,
273 'migrate_downtime' => 1,
274 'migrate_speed' => 1,
275 'name' => 1,
276 'onboot' => 1,
277 'ostype' => 1,
278 'protection' => 1,
279 'reboot' => 1,
280 'startdate' => 1,
281 'startup' => 1,
282 'tdf' => 1,
283 'template' => 1,
284 };
285
286 my $vmpoweroptions = {
287 'freeze' => 1,
288 };
289
290 my $diskoptions = {
291 'boot' => 1,
292 'bootdisk' => 1,
293 'vmstatestorage' => 1,
294 };
295
296 my $cloudinitoptions = {
297 cicustom => 1,
298 cipassword => 1,
299 citype => 1,
300 ciuser => 1,
301 nameserver => 1,
302 searchdomain => 1,
303 sshkeys => 1,
304 };
305
306 my $check_vm_modify_config_perm = sub {
307 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
308
309 return 1 if $authuser eq 'root@pam';
310
311 foreach my $opt (@$key_list) {
312 # some checks (e.g., disk, serial port, usb) need to be done somewhere
313 # else, as there the permission can be value dependend
314 next if PVE::QemuServer::is_valid_drivename($opt);
315 next if $opt eq 'cdrom';
316 next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
317
318
319 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
320 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
321 } elsif ($memoryoptions->{$opt}) {
322 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
323 } elsif ($hwtypeoptions->{$opt}) {
324 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
325 } elsif ($generaloptions->{$opt}) {
326 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
327 # special case for startup since it changes host behaviour
328 if ($opt eq 'startup') {
329 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
330 }
331 } elsif ($vmpoweroptions->{$opt}) {
332 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
333 } elsif ($diskoptions->{$opt}) {
334 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
335 } elsif ($cloudinitoptions->{$opt} || ($opt =~ m/^(?:net|ipconfig)\d+$/)) {
336 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
337 } else {
338 # catches hostpci\d+, args, lock, etc.
339 # new options will be checked here
340 die "only root can set '$opt' config\n";
341 }
342 }
343
344 return 1;
345 };
346
347 __PACKAGE__->register_method({
348 name => 'vmlist',
349 path => '',
350 method => 'GET',
351 description => "Virtual machine index (per node).",
352 permissions => {
353 description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
354 user => 'all',
355 },
356 proxyto => 'node',
357 protected => 1, # qemu pid files are only readable by root
358 parameters => {
359 additionalProperties => 0,
360 properties => {
361 node => get_standard_option('pve-node'),
362 full => {
363 type => 'boolean',
364 optional => 1,
365 description => "Determine the full status of active VMs.",
366 },
367 },
368 },
369 returns => {
370 type => 'array',
371 items => {
372 type => "object",
373 properties => $PVE::QemuServer::vmstatus_return_properties,
374 },
375 links => [ { rel => 'child', href => "{vmid}" } ],
376 },
377 code => sub {
378 my ($param) = @_;
379
380 my $rpcenv = PVE::RPCEnvironment::get();
381 my $authuser = $rpcenv->get_user();
382
383 my $vmstatus = PVE::QemuServer::vmstatus(undef, $param->{full});
384
385 my $res = [];
386 foreach my $vmid (keys %$vmstatus) {
387 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
388
389 my $data = $vmstatus->{$vmid};
390 push @$res, $data;
391 }
392
393 return $res;
394 }});
395
396
397
398 __PACKAGE__->register_method({
399 name => 'create_vm',
400 path => '',
401 method => 'POST',
402 description => "Create or restore a virtual machine.",
403 permissions => {
404 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
405 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
406 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
407 user => 'all', # check inside
408 },
409 protected => 1,
410 proxyto => 'node',
411 parameters => {
412 additionalProperties => 0,
413 properties => PVE::QemuServer::json_config_properties(
414 {
415 node => get_standard_option('pve-node'),
416 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
417 archive => {
418 description => "The backup file.",
419 type => 'string',
420 optional => 1,
421 maxLength => 255,
422 completion => \&PVE::QemuServer::complete_backup_archives,
423 },
424 storage => get_standard_option('pve-storage-id', {
425 description => "Default storage.",
426 optional => 1,
427 completion => \&PVE::QemuServer::complete_storage,
428 }),
429 force => {
430 optional => 1,
431 type => 'boolean',
432 description => "Allow to overwrite existing VM.",
433 requires => 'archive',
434 },
435 unique => {
436 optional => 1,
437 type => 'boolean',
438 description => "Assign a unique random ethernet address.",
439 requires => 'archive',
440 },
441 pool => {
442 optional => 1,
443 type => 'string', format => 'pve-poolid',
444 description => "Add the VM to the specified pool.",
445 },
446 bwlimit => {
447 description => "Override I/O bandwidth limit (in KiB/s).",
448 optional => 1,
449 type => 'integer',
450 minimum => '0',
451 default => 'restore limit from datacenter or storage config',
452 },
453 start => {
454 optional => 1,
455 type => 'boolean',
456 default => 0,
457 description => "Start VM after it was created successfully.",
458 },
459 }),
460 },
461 returns => {
462 type => 'string',
463 },
464 code => sub {
465 my ($param) = @_;
466
467 my $rpcenv = PVE::RPCEnvironment::get();
468
469 my $authuser = $rpcenv->get_user();
470
471 my $node = extract_param($param, 'node');
472
473 my $vmid = extract_param($param, 'vmid');
474
475 my $archive = extract_param($param, 'archive');
476 my $is_restore = !!$archive;
477
478 my $storage = extract_param($param, 'storage');
479
480 my $force = extract_param($param, 'force');
481
482 my $unique = extract_param($param, 'unique');
483
484 my $pool = extract_param($param, 'pool');
485
486 my $bwlimit = extract_param($param, 'bwlimit');
487
488 my $start_after_create = extract_param($param, 'start');
489
490 my $filename = PVE::QemuConfig->config_file($vmid);
491
492 my $storecfg = PVE::Storage::config();
493
494 if (defined(my $ssh_keys = $param->{sshkeys})) {
495 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
496 PVE::Tools::validate_ssh_public_keys($ssh_keys);
497 }
498
499 PVE::Cluster::check_cfs_quorum();
500
501 if (defined($pool)) {
502 $rpcenv->check_pool_exist($pool);
503 }
504
505 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
506 if defined($storage);
507
508 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
509 # OK
510 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
511 # OK
512 } elsif ($archive && $force && (-f $filename) &&
513 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
514 # OK: user has VM.Backup permissions, and want to restore an existing VM
515 } else {
516 raise_perm_exc();
517 }
518
519 if (!$archive) {
520 &$resolve_cdrom_alias($param);
521
522 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
523
524 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
525
526 foreach my $opt (keys %$param) {
527 if (PVE::QemuServer::is_valid_drivename($opt)) {
528 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
529 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
530
531 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
532 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
533 }
534 }
535
536 PVE::QemuServer::add_random_macs($param);
537 } else {
538 my $keystr = join(' ', keys %$param);
539 raise_param_exc({ archive => "option conflicts with other options ($keystr)"}) if $keystr;
540
541 if ($archive eq '-') {
542 die "pipe requires cli environment\n"
543 if $rpcenv->{type} ne 'cli';
544 } else {
545 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $archive);
546 $archive = PVE::Storage::abs_filesystem_path($storecfg, $archive);
547 }
548 }
549
550 my $emsg = $is_restore ? "unable to restore VM $vmid -" : "unable to create VM $vmid -";
551
552 eval { PVE::QemuConfig->create_and_lock_config($vmid, $force) };
553 die "$emsg $@" if $@;
554
555 my $restorefn = sub {
556 my $conf = PVE::QemuConfig->load_config($vmid);
557
558 PVE::QemuConfig->check_protection($conf, $emsg);
559
560 die "$emsg vm is running\n" if PVE::QemuServer::check_running($vmid);
561
562 my $realcmd = sub {
563 PVE::QemuServer::restore_archive($archive, $vmid, $authuser, {
564 storage => $storage,
565 pool => $pool,
566 unique => $unique,
567 bwlimit => $bwlimit,
568 });
569 my $restored_conf = PVE::QemuConfig->load_config($vmid);
570 # Convert restored VM to template if backup was VM template
571 if (PVE::QemuConfig->is_template($restored_conf)) {
572 warn "Convert to template.\n";
573 eval { PVE::QemuServer::template_create($vmid, $restored_conf) };
574 warn $@ if $@;
575 }
576
577 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
578
579 if ($start_after_create) {
580 eval { PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node }) };
581 warn $@ if $@;
582 }
583 };
584
585 # ensure no old replication state are exists
586 PVE::ReplicationState::delete_guest_states($vmid);
587
588 return PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
589 };
590
591 my $createfn = sub {
592 # ensure no old replication state are exists
593 PVE::ReplicationState::delete_guest_states($vmid);
594
595 my $realcmd = sub {
596
597 my $vollist = [];
598
599 my $conf = $param;
600
601 my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf);
602
603 eval {
604
605 $vollist = &$create_disks($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $param, $storage);
606
607 if (!$conf->{bootdisk}) {
608 my $firstdisk = PVE::QemuServer::resolve_first_disk($conf);
609 $conf->{bootdisk} = $firstdisk if $firstdisk;
610 }
611
612 # auto generate uuid if user did not specify smbios1 option
613 if (!$conf->{smbios1}) {
614 $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
615 }
616
617 if ((!defined($conf->{vmgenid}) || $conf->{vmgenid} eq '1') && $arch ne 'aarch64') {
618 $conf->{vmgenid} = PVE::QemuServer::generate_uuid();
619 }
620
621 PVE::QemuConfig->write_config($vmid, $conf);
622
623 };
624 my $err = $@;
625
626 if ($err) {
627 foreach my $volid (@$vollist) {
628 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
629 warn $@ if $@;
630 }
631 die "$emsg $err";
632 }
633
634 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
635 };
636
637 PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
638
639 if ($start_after_create) {
640 print "Execute autostart\n";
641 eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
642 warn $@ if $@;
643 }
644 };
645
646 my ($code, $worker_name);
647 if ($is_restore) {
648 $worker_name = 'qmrestore';
649 $code = sub {
650 eval { $restorefn->() };
651 if (my $err = $@) {
652 eval { PVE::QemuConfig->remove_lock($vmid, 'create') };
653 warn $@ if $@;
654 die $err;
655 }
656 };
657 } else {
658 $worker_name = 'qmcreate';
659 $code = sub {
660 eval { $createfn->() };
661 if (my $err = $@) {
662 eval {
663 my $conffile = PVE::QemuConfig->config_file($vmid);
664 unlink($conffile) or die "failed to remove config file: $!\n";
665 };
666 warn $@ if $@;
667 die $err;
668 }
669 };
670 }
671
672 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
673 }});
674
675 __PACKAGE__->register_method({
676 name => 'vmdiridx',
677 path => '{vmid}',
678 method => 'GET',
679 proxyto => 'node',
680 description => "Directory index",
681 permissions => {
682 user => 'all',
683 },
684 parameters => {
685 additionalProperties => 0,
686 properties => {
687 node => get_standard_option('pve-node'),
688 vmid => get_standard_option('pve-vmid'),
689 },
690 },
691 returns => {
692 type => 'array',
693 items => {
694 type => "object",
695 properties => {
696 subdir => { type => 'string' },
697 },
698 },
699 links => [ { rel => 'child', href => "{subdir}" } ],
700 },
701 code => sub {
702 my ($param) = @_;
703
704 my $res = [
705 { subdir => 'config' },
706 { subdir => 'pending' },
707 { subdir => 'status' },
708 { subdir => 'unlink' },
709 { subdir => 'vncproxy' },
710 { subdir => 'termproxy' },
711 { subdir => 'migrate' },
712 { subdir => 'resize' },
713 { subdir => 'move' },
714 { subdir => 'rrd' },
715 { subdir => 'rrddata' },
716 { subdir => 'monitor' },
717 { subdir => 'agent' },
718 { subdir => 'snapshot' },
719 { subdir => 'spiceproxy' },
720 { subdir => 'sendkey' },
721 { subdir => 'firewall' },
722 ];
723
724 return $res;
725 }});
726
727 __PACKAGE__->register_method ({
728 subclass => "PVE::API2::Firewall::VM",
729 path => '{vmid}/firewall',
730 });
731
732 __PACKAGE__->register_method ({
733 subclass => "PVE::API2::Qemu::Agent",
734 path => '{vmid}/agent',
735 });
736
737 __PACKAGE__->register_method({
738 name => 'rrd',
739 path => '{vmid}/rrd',
740 method => 'GET',
741 protected => 1, # fixme: can we avoid that?
742 permissions => {
743 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
744 },
745 description => "Read VM RRD statistics (returns PNG)",
746 parameters => {
747 additionalProperties => 0,
748 properties => {
749 node => get_standard_option('pve-node'),
750 vmid => get_standard_option('pve-vmid'),
751 timeframe => {
752 description => "Specify the time frame you are interested in.",
753 type => 'string',
754 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
755 },
756 ds => {
757 description => "The list of datasources you want to display.",
758 type => 'string', format => 'pve-configid-list',
759 },
760 cf => {
761 description => "The RRD consolidation function",
762 type => 'string',
763 enum => [ 'AVERAGE', 'MAX' ],
764 optional => 1,
765 },
766 },
767 },
768 returns => {
769 type => "object",
770 properties => {
771 filename => { type => 'string' },
772 },
773 },
774 code => sub {
775 my ($param) = @_;
776
777 return PVE::Cluster::create_rrd_graph(
778 "pve2-vm/$param->{vmid}", $param->{timeframe},
779 $param->{ds}, $param->{cf});
780
781 }});
782
783 __PACKAGE__->register_method({
784 name => 'rrddata',
785 path => '{vmid}/rrddata',
786 method => 'GET',
787 protected => 1, # fixme: can we avoid that?
788 permissions => {
789 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
790 },
791 description => "Read VM RRD statistics",
792 parameters => {
793 additionalProperties => 0,
794 properties => {
795 node => get_standard_option('pve-node'),
796 vmid => get_standard_option('pve-vmid'),
797 timeframe => {
798 description => "Specify the time frame you are interested in.",
799 type => 'string',
800 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
801 },
802 cf => {
803 description => "The RRD consolidation function",
804 type => 'string',
805 enum => [ 'AVERAGE', 'MAX' ],
806 optional => 1,
807 },
808 },
809 },
810 returns => {
811 type => "array",
812 items => {
813 type => "object",
814 properties => {},
815 },
816 },
817 code => sub {
818 my ($param) = @_;
819
820 return PVE::Cluster::create_rrd_data(
821 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
822 }});
823
824
825 __PACKAGE__->register_method({
826 name => 'vm_config',
827 path => '{vmid}/config',
828 method => 'GET',
829 proxyto => 'node',
830 description => "Get current virtual machine configuration. This does not include pending configuration changes (see 'pending' API).",
831 permissions => {
832 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
833 },
834 parameters => {
835 additionalProperties => 0,
836 properties => {
837 node => get_standard_option('pve-node'),
838 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
839 current => {
840 description => "Get current values (instead of pending values).",
841 optional => 1,
842 default => 0,
843 type => 'boolean',
844 },
845 snapshot => get_standard_option('pve-snapshot-name', {
846 description => "Fetch config values from given snapshot.",
847 optional => 1,
848 completion => sub {
849 my ($cmd, $pname, $cur, $args) = @_;
850 PVE::QemuConfig->snapshot_list($args->[0]);
851 },
852 }),
853 },
854 },
855 returns => {
856 description => "The current VM configuration.",
857 type => "object",
858 properties => PVE::QemuServer::json_config_properties({
859 digest => {
860 type => 'string',
861 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
862 }
863 }),
864 },
865 code => sub {
866 my ($param) = @_;
867
868 my $conf = PVE::QemuConfig->load_config($param->{vmid});
869
870 if (my $snapname = $param->{snapshot}) {
871 my $snapshot = $conf->{snapshots}->{$snapname};
872 die "snapshot '$snapname' does not exist\n" if !defined($snapshot);
873
874 $snapshot->{digest} = $conf->{digest}; # keep file digest for API
875
876 $conf = $snapshot;
877 }
878
879 delete $conf->{snapshots};
880
881 if (!$param->{current}) {
882 foreach my $opt (keys %{$conf->{pending}}) {
883 next if $opt eq 'delete';
884 my $value = $conf->{pending}->{$opt};
885 next if ref($value); # just to be sure
886 $conf->{$opt} = $value;
887 }
888 my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete});
889 foreach my $opt (keys %$pending_delete_hash) {
890 delete $conf->{$opt} if $conf->{$opt};
891 }
892 }
893
894 delete $conf->{pending};
895
896 # hide cloudinit password
897 if ($conf->{cipassword}) {
898 $conf->{cipassword} = '**********';
899 }
900
901 return $conf;
902 }});
903
904 __PACKAGE__->register_method({
905 name => 'vm_pending',
906 path => '{vmid}/pending',
907 method => 'GET',
908 proxyto => 'node',
909 description => "Get virtual machine configuration, including pending changes.",
910 permissions => {
911 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
912 },
913 parameters => {
914 additionalProperties => 0,
915 properties => {
916 node => get_standard_option('pve-node'),
917 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
918 },
919 },
920 returns => {
921 type => "array",
922 items => {
923 type => "object",
924 properties => {
925 key => {
926 description => "Configuration option name.",
927 type => 'string',
928 },
929 value => {
930 description => "Current value.",
931 type => 'string',
932 optional => 1,
933 },
934 pending => {
935 description => "Pending value.",
936 type => 'string',
937 optional => 1,
938 },
939 delete => {
940 description => "Indicates a pending delete request if present and not 0. " .
941 "The value 2 indicates a force-delete request.",
942 type => 'integer',
943 minimum => 0,
944 maximum => 2,
945 optional => 1,
946 },
947 },
948 },
949 },
950 code => sub {
951 my ($param) = @_;
952
953 my $conf = PVE::QemuConfig->load_config($param->{vmid});
954
955 my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete});
956
957 my $res = [];
958
959 foreach my $opt (keys %$conf) {
960 next if ref($conf->{$opt});
961 my $item = { key => $opt };
962 $item->{value} = $conf->{$opt} if defined($conf->{$opt});
963 $item->{pending} = $conf->{pending}->{$opt} if defined($conf->{pending}->{$opt});
964 $item->{delete} = ($pending_delete_hash->{$opt} ? 2 : 1) if exists $pending_delete_hash->{$opt};
965
966 # hide cloudinit password
967 if ($opt eq 'cipassword') {
968 $item->{value} = '**********' if defined($item->{value});
969 # the trailing space so that the pending string is different
970 $item->{pending} = '********** ' if defined($item->{pending});
971 }
972 push @$res, $item;
973 }
974
975 foreach my $opt (keys %{$conf->{pending}}) {
976 next if $opt eq 'delete';
977 next if ref($conf->{pending}->{$opt}); # just to be sure
978 next if defined($conf->{$opt});
979 my $item = { key => $opt };
980 $item->{pending} = $conf->{pending}->{$opt};
981
982 # hide cloudinit password
983 if ($opt eq 'cipassword') {
984 $item->{pending} = '**********' if defined($item->{pending});
985 }
986 push @$res, $item;
987 }
988
989 while (my ($opt, $force) = each %$pending_delete_hash) {
990 next if $conf->{pending}->{$opt}; # just to be sure
991 next if $conf->{$opt};
992 my $item = { key => $opt, delete => ($force ? 2 : 1)};
993 push @$res, $item;
994 }
995
996 return $res;
997 }});
998
999 # POST/PUT {vmid}/config implementation
1000 #
1001 # The original API used PUT (idempotent) an we assumed that all operations
1002 # are fast. But it turned out that almost any configuration change can
1003 # involve hot-plug actions, or disk alloc/free. Such actions can take long
1004 # time to complete and have side effects (not idempotent).
1005 #
1006 # The new implementation uses POST and forks a worker process. We added
1007 # a new option 'background_delay'. If specified we wait up to
1008 # 'background_delay' second for the worker task to complete. It returns null
1009 # if the task is finished within that time, else we return the UPID.
1010
1011 my $update_vm_api = sub {
1012 my ($param, $sync) = @_;
1013
1014 my $rpcenv = PVE::RPCEnvironment::get();
1015
1016 my $authuser = $rpcenv->get_user();
1017
1018 my $node = extract_param($param, 'node');
1019
1020 my $vmid = extract_param($param, 'vmid');
1021
1022 my $digest = extract_param($param, 'digest');
1023
1024 my $background_delay = extract_param($param, 'background_delay');
1025
1026 if (defined(my $cipassword = $param->{cipassword})) {
1027 # Same logic as in cloud-init (but with the regex fixed...)
1028 $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
1029 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1030 }
1031
1032 my @paramarr = (); # used for log message
1033 foreach my $key (sort keys %$param) {
1034 my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
1035 push @paramarr, "-$key", $value;
1036 }
1037
1038 my $skiplock = extract_param($param, 'skiplock');
1039 raise_param_exc({ skiplock => "Only root may use this option." })
1040 if $skiplock && $authuser ne 'root@pam';
1041
1042 my $delete_str = extract_param($param, 'delete');
1043
1044 my $revert_str = extract_param($param, 'revert');
1045
1046 my $force = extract_param($param, 'force');
1047
1048 if (defined(my $ssh_keys = $param->{sshkeys})) {
1049 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
1050 PVE::Tools::validate_ssh_public_keys($ssh_keys);
1051 }
1052
1053 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
1054
1055 my $storecfg = PVE::Storage::config();
1056
1057 my $defaults = PVE::QemuServer::load_defaults();
1058
1059 &$resolve_cdrom_alias($param);
1060
1061 # now try to verify all parameters
1062
1063 my $revert = {};
1064 foreach my $opt (PVE::Tools::split_list($revert_str)) {
1065 if (!PVE::QemuServer::option_exists($opt)) {
1066 raise_param_exc({ revert => "unknown option '$opt'" });
1067 }
1068
1069 raise_param_exc({ delete => "you can't use '-$opt' and " .
1070 "-revert $opt' at the same time" })
1071 if defined($param->{$opt});
1072
1073 $revert->{$opt} = 1;
1074 }
1075
1076 my @delete = ();
1077 foreach my $opt (PVE::Tools::split_list($delete_str)) {
1078 $opt = 'ide2' if $opt eq 'cdrom';
1079
1080 raise_param_exc({ delete => "you can't use '-$opt' and " .
1081 "-delete $opt' at the same time" })
1082 if defined($param->{$opt});
1083
1084 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
1085 "-revert $opt' at the same time" })
1086 if $revert->{$opt};
1087
1088 if (!PVE::QemuServer::option_exists($opt)) {
1089 raise_param_exc({ delete => "unknown option '$opt'" });
1090 }
1091
1092 push @delete, $opt;
1093 }
1094
1095 my $repl_conf = PVE::ReplicationConfig->new();
1096 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1097 my $check_replication = sub {
1098 my ($drive) = @_;
1099 return if !$is_replicated;
1100 my $volid = $drive->{file};
1101 return if !$volid || !($drive->{replicate}//1);
1102 return if PVE::QemuServer::drive_is_cdrom($drive);
1103 my ($storeid, $format);
1104 if ($volid =~ $NEW_DISK_RE) {
1105 $storeid = $2;
1106 $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
1107 } else {
1108 ($storeid, undef) = PVE::Storage::parse_volume_id($volid, 1);
1109 $format = (PVE::Storage::parse_volname($storecfg, $volid))[6];
1110 }
1111 return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
1112 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1113 return if $scfg->{shared};
1114 die "cannot add non-replicatable volume to a replicated VM\n";
1115 };
1116
1117 foreach my $opt (keys %$param) {
1118 if (PVE::QemuServer::is_valid_drivename($opt)) {
1119 # cleanup drive path
1120 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1121 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
1122 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
1123 $check_replication->($drive);
1124 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
1125 } elsif ($opt =~ m/^net(\d+)$/) {
1126 # add macaddr
1127 my $net = PVE::QemuServer::parse_net($param->{$opt});
1128 $param->{$opt} = PVE::QemuServer::print_net($net);
1129 } elsif ($opt eq 'vmgenid') {
1130 if ($param->{$opt} eq '1') {
1131 $param->{$opt} = PVE::QemuServer::generate_uuid();
1132 }
1133 } elsif ($opt eq 'hookscript') {
1134 eval { PVE::GuestHelpers::check_hookscript($param->{$opt}, $storecfg); };
1135 raise_param_exc({ $opt => $@ }) if $@;
1136 }
1137 }
1138
1139 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1140
1141 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1142
1143 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1144
1145 my $updatefn = sub {
1146
1147 my $conf = PVE::QemuConfig->load_config($vmid);
1148
1149 die "checksum missmatch (file change by other user?)\n"
1150 if $digest && $digest ne $conf->{digest};
1151
1152 PVE::QemuConfig->check_lock($conf) if !$skiplock;
1153
1154 foreach my $opt (keys %$revert) {
1155 if (defined($conf->{$opt})) {
1156 $param->{$opt} = $conf->{$opt};
1157 } elsif (defined($conf->{pending}->{$opt})) {
1158 push @delete, $opt;
1159 }
1160 }
1161
1162 if ($param->{memory} || defined($param->{balloon})) {
1163 my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
1164 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
1165
1166 die "balloon value too large (must be smaller than assigned memory)\n"
1167 if $balloon && $balloon > $maxmem;
1168 }
1169
1170 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1171
1172 my $worker = sub {
1173
1174 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1175
1176 # write updates to pending section
1177
1178 my $modified = {}; # record what $option we modify
1179
1180 foreach my $opt (@delete) {
1181 $modified->{$opt} = 1;
1182 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1183 if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
1184 warn "cannot delete '$opt' - not set in current configuration!\n";
1185 $modified->{$opt} = 0;
1186 next;
1187 }
1188
1189 if ($opt =~ m/^unused/) {
1190 my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
1191 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
1192 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1193 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1194 delete $conf->{$opt};
1195 PVE::QemuConfig->write_config($vmid, $conf);
1196 }
1197 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1198 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
1199 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1200 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1201 if defined($conf->{pending}->{$opt});
1202 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1203 PVE::QemuConfig->write_config($vmid, $conf);
1204 } elsif ($opt =~ m/^serial\d+$/) {
1205 if ($conf->{$opt} eq 'socket') {
1206 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1207 } elsif ($authuser ne 'root@pam') {
1208 die "only root can delete '$opt' config for real devices\n";
1209 }
1210 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1211 PVE::QemuConfig->write_config($vmid, $conf);
1212 } elsif ($opt =~ m/^usb\d+$/) {
1213 if ($conf->{$opt} =~ m/spice/) {
1214 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1215 } elsif ($authuser ne 'root@pam') {
1216 die "only root can delete '$opt' config for real devices\n";
1217 }
1218 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1219 PVE::QemuConfig->write_config($vmid, $conf);
1220 } else {
1221 PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force);
1222 PVE::QemuConfig->write_config($vmid, $conf);
1223 }
1224 }
1225
1226 foreach my $opt (keys %$param) { # add/change
1227 $modified->{$opt} = 1;
1228 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1229 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1230
1231 my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf);
1232
1233 if (PVE::QemuServer::is_valid_drivename($opt)) {
1234 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1235 # FIXME: cloudinit: CDROM or Disk?
1236 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1237 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1238 } else {
1239 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1240 }
1241 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1242 if defined($conf->{pending}->{$opt});
1243
1244 &$create_disks($rpcenv, $authuser, $conf->{pending}, $arch, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1245 } elsif ($opt =~ m/^serial\d+/) {
1246 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1247 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1248 } elsif ($authuser ne 'root@pam') {
1249 die "only root can modify '$opt' config for real devices\n";
1250 }
1251 $conf->{pending}->{$opt} = $param->{$opt};
1252 } elsif ($opt =~ m/^usb\d+/) {
1253 if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
1254 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1255 } elsif ($authuser ne 'root@pam') {
1256 die "only root can modify '$opt' config for real devices\n";
1257 }
1258 $conf->{pending}->{$opt} = $param->{$opt};
1259 } else {
1260 $conf->{pending}->{$opt} = $param->{$opt};
1261 }
1262 PVE::QemuServer::vmconfig_undelete_pending_option($conf, $opt);
1263 PVE::QemuConfig->write_config($vmid, $conf);
1264 }
1265
1266 # remove pending changes when nothing changed
1267 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1268 my $changes = PVE::QemuServer::vmconfig_cleanup_pending($conf);
1269 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
1270
1271 return if !scalar(keys %{$conf->{pending}});
1272
1273 my $running = PVE::QemuServer::check_running($vmid);
1274
1275 # apply pending changes
1276
1277 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1278
1279 if ($running) {
1280 my $errors = {};
1281 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
1282 raise_param_exc($errors) if scalar(keys %$errors);
1283 } else {
1284 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $running);
1285 }
1286
1287 return;
1288 };
1289
1290 if ($sync) {
1291 &$worker();
1292 return undef;
1293 } else {
1294 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1295
1296 if ($background_delay) {
1297
1298 # Note: It would be better to do that in the Event based HTTPServer
1299 # to avoid blocking call to sleep.
1300
1301 my $end_time = time() + $background_delay;
1302
1303 my $task = PVE::Tools::upid_decode($upid);
1304
1305 my $running = 1;
1306 while (time() < $end_time) {
1307 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1308 last if !$running;
1309 sleep(1); # this gets interrupted when child process ends
1310 }
1311
1312 if (!$running) {
1313 my $status = PVE::Tools::upid_read_status($upid);
1314 return undef if $status eq 'OK';
1315 die $status;
1316 }
1317 }
1318
1319 return $upid;
1320 }
1321 };
1322
1323 return PVE::QemuConfig->lock_config($vmid, $updatefn);
1324 };
1325
1326 my $vm_config_perm_list = [
1327 'VM.Config.Disk',
1328 'VM.Config.CDROM',
1329 'VM.Config.CPU',
1330 'VM.Config.Memory',
1331 'VM.Config.Network',
1332 'VM.Config.HWType',
1333 'VM.Config.Options',
1334 ];
1335
1336 __PACKAGE__->register_method({
1337 name => 'update_vm_async',
1338 path => '{vmid}/config',
1339 method => 'POST',
1340 protected => 1,
1341 proxyto => 'node',
1342 description => "Set virtual machine options (asynchrounous API).",
1343 permissions => {
1344 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1345 },
1346 parameters => {
1347 additionalProperties => 0,
1348 properties => PVE::QemuServer::json_config_properties(
1349 {
1350 node => get_standard_option('pve-node'),
1351 vmid => get_standard_option('pve-vmid'),
1352 skiplock => get_standard_option('skiplock'),
1353 delete => {
1354 type => 'string', format => 'pve-configid-list',
1355 description => "A list of settings you want to delete.",
1356 optional => 1,
1357 },
1358 revert => {
1359 type => 'string', format => 'pve-configid-list',
1360 description => "Revert a pending change.",
1361 optional => 1,
1362 },
1363 force => {
1364 type => 'boolean',
1365 description => $opt_force_description,
1366 optional => 1,
1367 requires => 'delete',
1368 },
1369 digest => {
1370 type => 'string',
1371 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1372 maxLength => 40,
1373 optional => 1,
1374 },
1375 background_delay => {
1376 type => 'integer',
1377 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1378 minimum => 1,
1379 maximum => 30,
1380 optional => 1,
1381 },
1382 }),
1383 },
1384 returns => {
1385 type => 'string',
1386 optional => 1,
1387 },
1388 code => $update_vm_api,
1389 });
1390
1391 __PACKAGE__->register_method({
1392 name => 'update_vm',
1393 path => '{vmid}/config',
1394 method => 'PUT',
1395 protected => 1,
1396 proxyto => 'node',
1397 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1398 permissions => {
1399 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1400 },
1401 parameters => {
1402 additionalProperties => 0,
1403 properties => PVE::QemuServer::json_config_properties(
1404 {
1405 node => get_standard_option('pve-node'),
1406 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1407 skiplock => get_standard_option('skiplock'),
1408 delete => {
1409 type => 'string', format => 'pve-configid-list',
1410 description => "A list of settings you want to delete.",
1411 optional => 1,
1412 },
1413 revert => {
1414 type => 'string', format => 'pve-configid-list',
1415 description => "Revert a pending change.",
1416 optional => 1,
1417 },
1418 force => {
1419 type => 'boolean',
1420 description => $opt_force_description,
1421 optional => 1,
1422 requires => 'delete',
1423 },
1424 digest => {
1425 type => 'string',
1426 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1427 maxLength => 40,
1428 optional => 1,
1429 },
1430 }),
1431 },
1432 returns => { type => 'null' },
1433 code => sub {
1434 my ($param) = @_;
1435 &$update_vm_api($param, 1);
1436 return undef;
1437 }
1438 });
1439
1440
1441 __PACKAGE__->register_method({
1442 name => 'destroy_vm',
1443 path => '{vmid}',
1444 method => 'DELETE',
1445 protected => 1,
1446 proxyto => 'node',
1447 description => "Destroy the vm (also delete all used/owned volumes).",
1448 permissions => {
1449 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1450 },
1451 parameters => {
1452 additionalProperties => 0,
1453 properties => {
1454 node => get_standard_option('pve-node'),
1455 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1456 skiplock => get_standard_option('skiplock'),
1457 },
1458 },
1459 returns => {
1460 type => 'string',
1461 },
1462 code => sub {
1463 my ($param) = @_;
1464
1465 my $rpcenv = PVE::RPCEnvironment::get();
1466
1467 my $authuser = $rpcenv->get_user();
1468
1469 my $vmid = $param->{vmid};
1470
1471 my $skiplock = $param->{skiplock};
1472 raise_param_exc({ skiplock => "Only root may use this option." })
1473 if $skiplock && $authuser ne 'root@pam';
1474
1475 # test if VM exists
1476 my $conf = PVE::QemuConfig->load_config($vmid);
1477
1478 my $storecfg = PVE::Storage::config();
1479
1480 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
1481
1482 die "unable to remove VM $vmid - used in HA resources\n"
1483 if PVE::HA::Config::vm_is_ha_managed($vmid);
1484
1485 # do not allow destroy if there are replication jobs
1486 my $repl_conf = PVE::ReplicationConfig->new();
1487 $repl_conf->check_for_existing_jobs($vmid);
1488
1489 # early tests (repeat after locking)
1490 die "VM $vmid is running - destroy failed\n"
1491 if PVE::QemuServer::check_running($vmid);
1492
1493 my $realcmd = sub {
1494 my $upid = shift;
1495
1496 syslog('info', "destroy VM $vmid: $upid\n");
1497
1498 PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock);
1499
1500 PVE::AccessControl::remove_vm_access($vmid);
1501
1502 PVE::Firewall::remove_vmfw_conf($vmid);
1503 };
1504
1505 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1506 }});
1507
1508 __PACKAGE__->register_method({
1509 name => 'unlink',
1510 path => '{vmid}/unlink',
1511 method => 'PUT',
1512 protected => 1,
1513 proxyto => 'node',
1514 description => "Unlink/delete disk images.",
1515 permissions => {
1516 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1517 },
1518 parameters => {
1519 additionalProperties => 0,
1520 properties => {
1521 node => get_standard_option('pve-node'),
1522 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1523 idlist => {
1524 type => 'string', format => 'pve-configid-list',
1525 description => "A list of disk IDs you want to delete.",
1526 },
1527 force => {
1528 type => 'boolean',
1529 description => $opt_force_description,
1530 optional => 1,
1531 },
1532 },
1533 },
1534 returns => { type => 'null'},
1535 code => sub {
1536 my ($param) = @_;
1537
1538 $param->{delete} = extract_param($param, 'idlist');
1539
1540 __PACKAGE__->update_vm($param);
1541
1542 return undef;
1543 }});
1544
1545 my $sslcert;
1546
1547 __PACKAGE__->register_method({
1548 name => 'vncproxy',
1549 path => '{vmid}/vncproxy',
1550 method => 'POST',
1551 protected => 1,
1552 permissions => {
1553 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1554 },
1555 description => "Creates a TCP VNC proxy connections.",
1556 parameters => {
1557 additionalProperties => 0,
1558 properties => {
1559 node => get_standard_option('pve-node'),
1560 vmid => get_standard_option('pve-vmid'),
1561 websocket => {
1562 optional => 1,
1563 type => 'boolean',
1564 description => "starts websockify instead of vncproxy",
1565 },
1566 },
1567 },
1568 returns => {
1569 additionalProperties => 0,
1570 properties => {
1571 user => { type => 'string' },
1572 ticket => { type => 'string' },
1573 cert => { type => 'string' },
1574 port => { type => 'integer' },
1575 upid => { type => 'string' },
1576 },
1577 },
1578 code => sub {
1579 my ($param) = @_;
1580
1581 my $rpcenv = PVE::RPCEnvironment::get();
1582
1583 my $authuser = $rpcenv->get_user();
1584
1585 my $vmid = $param->{vmid};
1586 my $node = $param->{node};
1587 my $websocket = $param->{websocket};
1588
1589 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1590 my $use_serial = ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/));
1591
1592 my $authpath = "/vms/$vmid";
1593
1594 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1595
1596 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1597 if !$sslcert;
1598
1599 my $family;
1600 my $remcmd = [];
1601
1602 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1603 (undef, $family) = PVE::Cluster::remote_node_ip($node);
1604 my $sshinfo = PVE::Cluster::get_ssh_info($node);
1605 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1606 $remcmd = PVE::Cluster::ssh_info_to_command($sshinfo, $use_serial ? '-t' : '-T');
1607 } else {
1608 $family = PVE::Tools::get_host_address_family($node);
1609 }
1610
1611 my $port = PVE::Tools::next_vnc_port($family);
1612
1613 my $timeout = 10;
1614
1615 my $realcmd = sub {
1616 my $upid = shift;
1617
1618 syslog('info', "starting vnc proxy $upid\n");
1619
1620 my $cmd;
1621
1622 if ($use_serial) {
1623
1624 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga}, '-escape', '0' ];
1625
1626 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1627 '-timeout', $timeout, '-authpath', $authpath,
1628 '-perm', 'Sys.Console'];
1629
1630 if ($param->{websocket}) {
1631 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
1632 push @$cmd, '-notls', '-listen', 'localhost';
1633 }
1634
1635 push @$cmd, '-c', @$remcmd, @$termcmd;
1636
1637 PVE::Tools::run_command($cmd);
1638
1639 } else {
1640
1641 $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
1642
1643 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1644
1645 my $sock = IO::Socket::IP->new(
1646 ReuseAddr => 1,
1647 Listen => 1,
1648 LocalPort => $port,
1649 Proto => 'tcp',
1650 GetAddrInfoFlags => 0,
1651 ) or die "failed to create socket: $!\n";
1652 # Inside the worker we shouldn't have any previous alarms
1653 # running anyway...:
1654 alarm(0);
1655 local $SIG{ALRM} = sub { die "connection timed out\n" };
1656 alarm $timeout;
1657 accept(my $cli, $sock) or die "connection failed: $!\n";
1658 alarm(0);
1659 close($sock);
1660 if (PVE::Tools::run_command($cmd,
1661 output => '>&'.fileno($cli),
1662 input => '<&'.fileno($cli),
1663 noerr => 1) != 0)
1664 {
1665 die "Failed to run vncproxy.\n";
1666 }
1667 }
1668
1669 return;
1670 };
1671
1672 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1673
1674 PVE::Tools::wait_for_vnc_port($port);
1675
1676 return {
1677 user => $authuser,
1678 ticket => $ticket,
1679 port => $port,
1680 upid => $upid,
1681 cert => $sslcert,
1682 };
1683 }});
1684
1685 __PACKAGE__->register_method({
1686 name => 'termproxy',
1687 path => '{vmid}/termproxy',
1688 method => 'POST',
1689 protected => 1,
1690 permissions => {
1691 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1692 },
1693 description => "Creates a TCP proxy connections.",
1694 parameters => {
1695 additionalProperties => 0,
1696 properties => {
1697 node => get_standard_option('pve-node'),
1698 vmid => get_standard_option('pve-vmid'),
1699 serial=> {
1700 optional => 1,
1701 type => 'string',
1702 enum => [qw(serial0 serial1 serial2 serial3)],
1703 description => "opens a serial terminal (defaults to display)",
1704 },
1705 },
1706 },
1707 returns => {
1708 additionalProperties => 0,
1709 properties => {
1710 user => { type => 'string' },
1711 ticket => { type => 'string' },
1712 port => { type => 'integer' },
1713 upid => { type => 'string' },
1714 },
1715 },
1716 code => sub {
1717 my ($param) = @_;
1718
1719 my $rpcenv = PVE::RPCEnvironment::get();
1720
1721 my $authuser = $rpcenv->get_user();
1722
1723 my $vmid = $param->{vmid};
1724 my $node = $param->{node};
1725 my $serial = $param->{serial};
1726
1727 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1728
1729 if (!defined($serial)) {
1730 if ($conf->{vga} && $conf->{vga} =~ m/^serial\d+$/) {
1731 $serial = $conf->{vga};
1732 }
1733 }
1734
1735 my $authpath = "/vms/$vmid";
1736
1737 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1738
1739 my $family;
1740 my $remcmd = [];
1741
1742 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1743 (undef, $family) = PVE::Cluster::remote_node_ip($node);
1744 my $sshinfo = PVE::Cluster::get_ssh_info($node);
1745 $remcmd = PVE::Cluster::ssh_info_to_command($sshinfo, '-t');
1746 push @$remcmd, '--';
1747 } else {
1748 $family = PVE::Tools::get_host_address_family($node);
1749 }
1750
1751 my $port = PVE::Tools::next_vnc_port($family);
1752
1753 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
1754 push @$termcmd, '-iface', $serial if $serial;
1755
1756 my $realcmd = sub {
1757 my $upid = shift;
1758
1759 syslog('info', "starting qemu termproxy $upid\n");
1760
1761 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
1762 '--perm', 'VM.Console', '--'];
1763 push @$cmd, @$remcmd, @$termcmd;
1764
1765 PVE::Tools::run_command($cmd);
1766 };
1767
1768 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1769
1770 PVE::Tools::wait_for_vnc_port($port);
1771
1772 return {
1773 user => $authuser,
1774 ticket => $ticket,
1775 port => $port,
1776 upid => $upid,
1777 };
1778 }});
1779
1780 __PACKAGE__->register_method({
1781 name => 'vncwebsocket',
1782 path => '{vmid}/vncwebsocket',
1783 method => 'GET',
1784 permissions => {
1785 description => "You also need to pass a valid ticket (vncticket).",
1786 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1787 },
1788 description => "Opens a weksocket for VNC traffic.",
1789 parameters => {
1790 additionalProperties => 0,
1791 properties => {
1792 node => get_standard_option('pve-node'),
1793 vmid => get_standard_option('pve-vmid'),
1794 vncticket => {
1795 description => "Ticket from previous call to vncproxy.",
1796 type => 'string',
1797 maxLength => 512,
1798 },
1799 port => {
1800 description => "Port number returned by previous vncproxy call.",
1801 type => 'integer',
1802 minimum => 5900,
1803 maximum => 5999,
1804 },
1805 },
1806 },
1807 returns => {
1808 type => "object",
1809 properties => {
1810 port => { type => 'string' },
1811 },
1812 },
1813 code => sub {
1814 my ($param) = @_;
1815
1816 my $rpcenv = PVE::RPCEnvironment::get();
1817
1818 my $authuser = $rpcenv->get_user();
1819
1820 my $vmid = $param->{vmid};
1821 my $node = $param->{node};
1822
1823 my $authpath = "/vms/$vmid";
1824
1825 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1826
1827 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
1828
1829 # Note: VNC ports are acessible from outside, so we do not gain any
1830 # security if we verify that $param->{port} belongs to VM $vmid. This
1831 # check is done by verifying the VNC ticket (inside VNC protocol).
1832
1833 my $port = $param->{port};
1834
1835 return { port => $port };
1836 }});
1837
1838 __PACKAGE__->register_method({
1839 name => 'spiceproxy',
1840 path => '{vmid}/spiceproxy',
1841 method => 'POST',
1842 protected => 1,
1843 proxyto => 'node',
1844 permissions => {
1845 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1846 },
1847 description => "Returns a SPICE configuration to connect to the VM.",
1848 parameters => {
1849 additionalProperties => 0,
1850 properties => {
1851 node => get_standard_option('pve-node'),
1852 vmid => get_standard_option('pve-vmid'),
1853 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1854 },
1855 },
1856 returns => get_standard_option('remote-viewer-config'),
1857 code => sub {
1858 my ($param) = @_;
1859
1860 my $rpcenv = PVE::RPCEnvironment::get();
1861
1862 my $authuser = $rpcenv->get_user();
1863
1864 my $vmid = $param->{vmid};
1865 my $node = $param->{node};
1866 my $proxy = $param->{proxy};
1867
1868 my $conf = PVE::QemuConfig->load_config($vmid, $node);
1869 my $title = "VM $vmid";
1870 $title .= " - ". $conf->{name} if $conf->{name};
1871
1872 my $port = PVE::QemuServer::spice_port($vmid);
1873
1874 my ($ticket, undef, $remote_viewer_config) =
1875 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
1876
1877 PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
1878 PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
1879
1880 return $remote_viewer_config;
1881 }});
1882
1883 __PACKAGE__->register_method({
1884 name => 'vmcmdidx',
1885 path => '{vmid}/status',
1886 method => 'GET',
1887 proxyto => 'node',
1888 description => "Directory index",
1889 permissions => {
1890 user => 'all',
1891 },
1892 parameters => {
1893 additionalProperties => 0,
1894 properties => {
1895 node => get_standard_option('pve-node'),
1896 vmid => get_standard_option('pve-vmid'),
1897 },
1898 },
1899 returns => {
1900 type => 'array',
1901 items => {
1902 type => "object",
1903 properties => {
1904 subdir => { type => 'string' },
1905 },
1906 },
1907 links => [ { rel => 'child', href => "{subdir}" } ],
1908 },
1909 code => sub {
1910 my ($param) = @_;
1911
1912 # test if VM exists
1913 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1914
1915 my $res = [
1916 { subdir => 'current' },
1917 { subdir => 'start' },
1918 { subdir => 'stop' },
1919 ];
1920
1921 return $res;
1922 }});
1923
1924 __PACKAGE__->register_method({
1925 name => 'vm_status',
1926 path => '{vmid}/status/current',
1927 method => 'GET',
1928 proxyto => 'node',
1929 protected => 1, # qemu pid files are only readable by root
1930 description => "Get virtual machine status.",
1931 permissions => {
1932 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1933 },
1934 parameters => {
1935 additionalProperties => 0,
1936 properties => {
1937 node => get_standard_option('pve-node'),
1938 vmid => get_standard_option('pve-vmid'),
1939 },
1940 },
1941 returns => {
1942 type => 'object',
1943 properties => {
1944 %$PVE::QemuServer::vmstatus_return_properties,
1945 ha => {
1946 description => "HA manager service status.",
1947 type => 'object',
1948 },
1949 spice => {
1950 description => "Qemu VGA configuration supports spice.",
1951 type => 'boolean',
1952 optional => 1,
1953 },
1954 agent => {
1955 description => "Qemu GuestAgent enabled in config.",
1956 type => 'boolean',
1957 optional => 1,
1958 },
1959 },
1960 },
1961 code => sub {
1962 my ($param) = @_;
1963
1964 # test if VM exists
1965 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1966
1967 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
1968 my $status = $vmstatus->{$param->{vmid}};
1969
1970 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
1971
1972 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
1973 $status->{agent} = 1 if (PVE::QemuServer::parse_guest_agent($conf)->{enabled});
1974
1975 return $status;
1976 }});
1977
1978 __PACKAGE__->register_method({
1979 name => 'vm_start',
1980 path => '{vmid}/status/start',
1981 method => 'POST',
1982 protected => 1,
1983 proxyto => 'node',
1984 description => "Start virtual machine.",
1985 permissions => {
1986 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1987 },
1988 parameters => {
1989 additionalProperties => 0,
1990 properties => {
1991 node => get_standard_option('pve-node'),
1992 vmid => get_standard_option('pve-vmid',
1993 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1994 skiplock => get_standard_option('skiplock'),
1995 stateuri => get_standard_option('pve-qm-stateuri'),
1996 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1997 migration_type => {
1998 type => 'string',
1999 enum => ['secure', 'insecure'],
2000 description => "Migration traffic is encrypted using an SSH " .
2001 "tunnel by default. On secure, completely private networks " .
2002 "this can be disabled to increase performance.",
2003 optional => 1,
2004 },
2005 migration_network => {
2006 type => 'string', format => 'CIDR',
2007 description => "CIDR of the (sub) network that is used for migration.",
2008 optional => 1,
2009 },
2010 machine => get_standard_option('pve-qm-machine'),
2011 targetstorage => {
2012 description => "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
2013 type => 'string',
2014 optional => 1
2015 }
2016 },
2017 },
2018 returns => {
2019 type => 'string',
2020 },
2021 code => sub {
2022 my ($param) = @_;
2023
2024 my $rpcenv = PVE::RPCEnvironment::get();
2025 my $authuser = $rpcenv->get_user();
2026
2027 my $node = extract_param($param, 'node');
2028 my $vmid = extract_param($param, 'vmid');
2029
2030 my $machine = extract_param($param, 'machine');
2031
2032 my $get_root_param = sub {
2033 my $value = extract_param($param, $_[0]);
2034 raise_param_exc({ "$_[0]" => "Only root may use this option." })
2035 if $value && $authuser ne 'root@pam';
2036 return $value;
2037 };
2038
2039 my $stateuri = $get_root_param->('stateuri');
2040 my $skiplock = $get_root_param->('skiplock');
2041 my $migratedfrom = $get_root_param->('migratedfrom');
2042 my $migration_type = $get_root_param->('migration_type');
2043 my $migration_network = $get_root_param->('migration_network');
2044 my $targetstorage = $get_root_param->('targetstorage');
2045
2046 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
2047 if $targetstorage && !$migratedfrom;
2048
2049 # read spice ticket from STDIN
2050 my $spice_ticket;
2051 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
2052 if (defined(my $line = <STDIN>)) {
2053 chomp $line;
2054 $spice_ticket = $line;
2055 }
2056 }
2057
2058 PVE::Cluster::check_cfs_quorum();
2059
2060 my $storecfg = PVE::Storage::config();
2061
2062 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && $rpcenv->{type} ne 'ha') {
2063 my $hacmd = sub {
2064 my $upid = shift;
2065
2066 print "Requesting HA start for VM $vmid\n";
2067
2068 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2069 PVE::Tools::run_command($cmd);
2070 return;
2071 };
2072
2073 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2074
2075 } else {
2076
2077 my $realcmd = sub {
2078 my $upid = shift;
2079
2080 syslog('info', "start VM $vmid: $upid\n");
2081
2082 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
2083 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
2084 return;
2085 };
2086
2087 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2088 }
2089 }});
2090
2091 __PACKAGE__->register_method({
2092 name => 'vm_stop',
2093 path => '{vmid}/status/stop',
2094 method => 'POST',
2095 protected => 1,
2096 proxyto => 'node',
2097 description => "Stop virtual machine. The qemu process will exit immediately. This" .
2098 "is akin to pulling the power plug of a running computer and may damage the VM data",
2099 permissions => {
2100 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2101 },
2102 parameters => {
2103 additionalProperties => 0,
2104 properties => {
2105 node => get_standard_option('pve-node'),
2106 vmid => get_standard_option('pve-vmid',
2107 { completion => \&PVE::QemuServer::complete_vmid_running }),
2108 skiplock => get_standard_option('skiplock'),
2109 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
2110 timeout => {
2111 description => "Wait maximal timeout seconds.",
2112 type => 'integer',
2113 minimum => 0,
2114 optional => 1,
2115 },
2116 keepActive => {
2117 description => "Do not deactivate storage volumes.",
2118 type => 'boolean',
2119 optional => 1,
2120 default => 0,
2121 }
2122 },
2123 },
2124 returns => {
2125 type => 'string',
2126 },
2127 code => sub {
2128 my ($param) = @_;
2129
2130 my $rpcenv = PVE::RPCEnvironment::get();
2131 my $authuser = $rpcenv->get_user();
2132
2133 my $node = extract_param($param, 'node');
2134 my $vmid = extract_param($param, 'vmid');
2135
2136 my $skiplock = extract_param($param, 'skiplock');
2137 raise_param_exc({ skiplock => "Only root may use this option." })
2138 if $skiplock && $authuser ne 'root@pam';
2139
2140 my $keepActive = extract_param($param, 'keepActive');
2141 raise_param_exc({ keepActive => "Only root may use this option." })
2142 if $keepActive && $authuser ne 'root@pam';
2143
2144 my $migratedfrom = extract_param($param, 'migratedfrom');
2145 raise_param_exc({ migratedfrom => "Only root may use this option." })
2146 if $migratedfrom && $authuser ne 'root@pam';
2147
2148
2149 my $storecfg = PVE::Storage::config();
2150
2151 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
2152
2153 my $hacmd = sub {
2154 my $upid = shift;
2155
2156 print "Requesting HA stop for VM $vmid\n";
2157
2158 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped'];
2159 PVE::Tools::run_command($cmd);
2160 return;
2161 };
2162
2163 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2164
2165 } else {
2166 my $realcmd = sub {
2167 my $upid = shift;
2168
2169 syslog('info', "stop VM $vmid: $upid\n");
2170
2171 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
2172 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
2173 return;
2174 };
2175
2176 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2177 }
2178 }});
2179
2180 __PACKAGE__->register_method({
2181 name => 'vm_reset',
2182 path => '{vmid}/status/reset',
2183 method => 'POST',
2184 protected => 1,
2185 proxyto => 'node',
2186 description => "Reset virtual machine.",
2187 permissions => {
2188 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2189 },
2190 parameters => {
2191 additionalProperties => 0,
2192 properties => {
2193 node => get_standard_option('pve-node'),
2194 vmid => get_standard_option('pve-vmid',
2195 { completion => \&PVE::QemuServer::complete_vmid_running }),
2196 skiplock => get_standard_option('skiplock'),
2197 },
2198 },
2199 returns => {
2200 type => 'string',
2201 },
2202 code => sub {
2203 my ($param) = @_;
2204
2205 my $rpcenv = PVE::RPCEnvironment::get();
2206
2207 my $authuser = $rpcenv->get_user();
2208
2209 my $node = extract_param($param, 'node');
2210
2211 my $vmid = extract_param($param, 'vmid');
2212
2213 my $skiplock = extract_param($param, 'skiplock');
2214 raise_param_exc({ skiplock => "Only root may use this option." })
2215 if $skiplock && $authuser ne 'root@pam';
2216
2217 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2218
2219 my $realcmd = sub {
2220 my $upid = shift;
2221
2222 PVE::QemuServer::vm_reset($vmid, $skiplock);
2223
2224 return;
2225 };
2226
2227 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
2228 }});
2229
2230 __PACKAGE__->register_method({
2231 name => 'vm_shutdown',
2232 path => '{vmid}/status/shutdown',
2233 method => 'POST',
2234 protected => 1,
2235 proxyto => 'node',
2236 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
2237 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
2238 permissions => {
2239 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2240 },
2241 parameters => {
2242 additionalProperties => 0,
2243 properties => {
2244 node => get_standard_option('pve-node'),
2245 vmid => get_standard_option('pve-vmid',
2246 { completion => \&PVE::QemuServer::complete_vmid_running }),
2247 skiplock => get_standard_option('skiplock'),
2248 timeout => {
2249 description => "Wait maximal timeout seconds.",
2250 type => 'integer',
2251 minimum => 0,
2252 optional => 1,
2253 },
2254 forceStop => {
2255 description => "Make sure the VM stops.",
2256 type => 'boolean',
2257 optional => 1,
2258 default => 0,
2259 },
2260 keepActive => {
2261 description => "Do not deactivate storage volumes.",
2262 type => 'boolean',
2263 optional => 1,
2264 default => 0,
2265 }
2266 },
2267 },
2268 returns => {
2269 type => 'string',
2270 },
2271 code => sub {
2272 my ($param) = @_;
2273
2274 my $rpcenv = PVE::RPCEnvironment::get();
2275 my $authuser = $rpcenv->get_user();
2276
2277 my $node = extract_param($param, 'node');
2278 my $vmid = extract_param($param, 'vmid');
2279
2280 my $skiplock = extract_param($param, 'skiplock');
2281 raise_param_exc({ skiplock => "Only root may use this option." })
2282 if $skiplock && $authuser ne 'root@pam';
2283
2284 my $keepActive = extract_param($param, 'keepActive');
2285 raise_param_exc({ keepActive => "Only root may use this option." })
2286 if $keepActive && $authuser ne 'root@pam';
2287
2288 my $storecfg = PVE::Storage::config();
2289
2290 my $shutdown = 1;
2291
2292 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2293 # otherwise, we will infer a shutdown command, but run into the timeout,
2294 # then when the vm is resumed, it will instantly shutdown
2295 #
2296 # checking the qmp status here to get feedback to the gui/cli/api
2297 # and the status query should not take too long
2298 my $qmpstatus = eval {
2299 PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
2300 };
2301 my $err = $@ if $@;
2302
2303 if (!$err && $qmpstatus->{status} eq "paused") {
2304 if ($param->{forceStop}) {
2305 warn "VM is paused - stop instead of shutdown\n";
2306 $shutdown = 0;
2307 } else {
2308 die "VM is paused - cannot shutdown\n";
2309 }
2310 }
2311
2312 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
2313
2314 my $hacmd = sub {
2315 my $upid = shift;
2316
2317 print "Requesting HA stop for VM $vmid\n";
2318
2319 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped'];
2320 PVE::Tools::run_command($cmd);
2321 return;
2322 };
2323
2324 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2325
2326 } else {
2327
2328 my $realcmd = sub {
2329 my $upid = shift;
2330
2331 syslog('info', "shutdown VM $vmid: $upid\n");
2332
2333 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2334 $shutdown, $param->{forceStop}, $keepActive);
2335 return;
2336 };
2337
2338 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2339 }
2340 }});
2341
2342 __PACKAGE__->register_method({
2343 name => 'vm_suspend',
2344 path => '{vmid}/status/suspend',
2345 method => 'POST',
2346 protected => 1,
2347 proxyto => 'node',
2348 description => "Suspend virtual machine.",
2349 permissions => {
2350 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2351 },
2352 parameters => {
2353 additionalProperties => 0,
2354 properties => {
2355 node => get_standard_option('pve-node'),
2356 vmid => get_standard_option('pve-vmid',
2357 { completion => \&PVE::QemuServer::complete_vmid_running }),
2358 skiplock => get_standard_option('skiplock'),
2359 todisk => {
2360 type => 'boolean',
2361 default => 0,
2362 optional => 1,
2363 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
2364 },
2365 statestorage => get_standard_option('pve-storage-id', {
2366 description => "The storage for the VM state",
2367 requires => 'todisk',
2368 optional => 1,
2369 completion => \&PVE::Storage::complete_storage_enabled,
2370 }),
2371 },
2372 },
2373 returns => {
2374 type => 'string',
2375 },
2376 code => sub {
2377 my ($param) = @_;
2378
2379 my $rpcenv = PVE::RPCEnvironment::get();
2380 my $authuser = $rpcenv->get_user();
2381
2382 my $node = extract_param($param, 'node');
2383 my $vmid = extract_param($param, 'vmid');
2384
2385 my $todisk = extract_param($param, 'todisk') // 0;
2386
2387 my $statestorage = extract_param($param, 'statestorage');
2388
2389 my $skiplock = extract_param($param, 'skiplock');
2390 raise_param_exc({ skiplock => "Only root may use this option." })
2391 if $skiplock && $authuser ne 'root@pam';
2392
2393 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2394
2395 die "Cannot suspend HA managed VM to disk\n"
2396 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
2397
2398 my $realcmd = sub {
2399 my $upid = shift;
2400
2401 syslog('info', "suspend VM $vmid: $upid\n");
2402
2403 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
2404
2405 return;
2406 };
2407
2408 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
2409 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
2410 }});
2411
2412 __PACKAGE__->register_method({
2413 name => 'vm_resume',
2414 path => '{vmid}/status/resume',
2415 method => 'POST',
2416 protected => 1,
2417 proxyto => 'node',
2418 description => "Resume virtual machine.",
2419 permissions => {
2420 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2421 },
2422 parameters => {
2423 additionalProperties => 0,
2424 properties => {
2425 node => get_standard_option('pve-node'),
2426 vmid => get_standard_option('pve-vmid',
2427 { completion => \&PVE::QemuServer::complete_vmid_running }),
2428 skiplock => get_standard_option('skiplock'),
2429 nocheck => { type => 'boolean', optional => 1 },
2430
2431 },
2432 },
2433 returns => {
2434 type => 'string',
2435 },
2436 code => sub {
2437 my ($param) = @_;
2438
2439 my $rpcenv = PVE::RPCEnvironment::get();
2440
2441 my $authuser = $rpcenv->get_user();
2442
2443 my $node = extract_param($param, 'node');
2444
2445 my $vmid = extract_param($param, 'vmid');
2446
2447 my $skiplock = extract_param($param, 'skiplock');
2448 raise_param_exc({ skiplock => "Only root may use this option." })
2449 if $skiplock && $authuser ne 'root@pam';
2450
2451 my $nocheck = extract_param($param, 'nocheck');
2452
2453 my $to_disk_suspended;
2454 eval {
2455 PVE::QemuConfig->lock_config($vmid, sub {
2456 my $conf = PVE::QemuConfig->load_config($vmid);
2457 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
2458 });
2459 };
2460
2461 die "VM $vmid not running\n"
2462 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
2463
2464 my $realcmd = sub {
2465 my $upid = shift;
2466
2467 syslog('info', "resume VM $vmid: $upid\n");
2468
2469 if (!$to_disk_suspended) {
2470 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2471 } else {
2472 my $storecfg = PVE::Storage::config();
2473 PVE::QemuServer::vm_start($storecfg, $vmid, undef, $skiplock);
2474 }
2475
2476 return;
2477 };
2478
2479 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2480 }});
2481
2482 __PACKAGE__->register_method({
2483 name => 'vm_sendkey',
2484 path => '{vmid}/sendkey',
2485 method => 'PUT',
2486 protected => 1,
2487 proxyto => 'node',
2488 description => "Send key event to virtual machine.",
2489 permissions => {
2490 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2491 },
2492 parameters => {
2493 additionalProperties => 0,
2494 properties => {
2495 node => get_standard_option('pve-node'),
2496 vmid => get_standard_option('pve-vmid',
2497 { completion => \&PVE::QemuServer::complete_vmid_running }),
2498 skiplock => get_standard_option('skiplock'),
2499 key => {
2500 description => "The key (qemu monitor encoding).",
2501 type => 'string'
2502 }
2503 },
2504 },
2505 returns => { type => 'null'},
2506 code => sub {
2507 my ($param) = @_;
2508
2509 my $rpcenv = PVE::RPCEnvironment::get();
2510
2511 my $authuser = $rpcenv->get_user();
2512
2513 my $node = extract_param($param, 'node');
2514
2515 my $vmid = extract_param($param, 'vmid');
2516
2517 my $skiplock = extract_param($param, 'skiplock');
2518 raise_param_exc({ skiplock => "Only root may use this option." })
2519 if $skiplock && $authuser ne 'root@pam';
2520
2521 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2522
2523 return;
2524 }});
2525
2526 __PACKAGE__->register_method({
2527 name => 'vm_feature',
2528 path => '{vmid}/feature',
2529 method => 'GET',
2530 proxyto => 'node',
2531 protected => 1,
2532 description => "Check if feature for virtual machine is available.",
2533 permissions => {
2534 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2535 },
2536 parameters => {
2537 additionalProperties => 0,
2538 properties => {
2539 node => get_standard_option('pve-node'),
2540 vmid => get_standard_option('pve-vmid'),
2541 feature => {
2542 description => "Feature to check.",
2543 type => 'string',
2544 enum => [ 'snapshot', 'clone', 'copy' ],
2545 },
2546 snapname => get_standard_option('pve-snapshot-name', {
2547 optional => 1,
2548 }),
2549 },
2550 },
2551 returns => {
2552 type => "object",
2553 properties => {
2554 hasFeature => { type => 'boolean' },
2555 nodes => {
2556 type => 'array',
2557 items => { type => 'string' },
2558 }
2559 },
2560 },
2561 code => sub {
2562 my ($param) = @_;
2563
2564 my $node = extract_param($param, 'node');
2565
2566 my $vmid = extract_param($param, 'vmid');
2567
2568 my $snapname = extract_param($param, 'snapname');
2569
2570 my $feature = extract_param($param, 'feature');
2571
2572 my $running = PVE::QemuServer::check_running($vmid);
2573
2574 my $conf = PVE::QemuConfig->load_config($vmid);
2575
2576 if($snapname){
2577 my $snap = $conf->{snapshots}->{$snapname};
2578 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2579 $conf = $snap;
2580 }
2581 my $storecfg = PVE::Storage::config();
2582
2583 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
2584 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
2585
2586 return {
2587 hasFeature => $hasFeature,
2588 nodes => [ keys %$nodelist ],
2589 };
2590 }});
2591
2592 __PACKAGE__->register_method({
2593 name => 'clone_vm',
2594 path => '{vmid}/clone',
2595 method => 'POST',
2596 protected => 1,
2597 proxyto => 'node',
2598 description => "Create a copy of virtual machine/template.",
2599 permissions => {
2600 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2601 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2602 "'Datastore.AllocateSpace' on any used storage.",
2603 check =>
2604 [ 'and',
2605 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2606 [ 'or',
2607 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2608 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2609 ],
2610 ]
2611 },
2612 parameters => {
2613 additionalProperties => 0,
2614 properties => {
2615 node => get_standard_option('pve-node'),
2616 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2617 newid => get_standard_option('pve-vmid', {
2618 completion => \&PVE::Cluster::complete_next_vmid,
2619 description => 'VMID for the clone.' }),
2620 name => {
2621 optional => 1,
2622 type => 'string', format => 'dns-name',
2623 description => "Set a name for the new VM.",
2624 },
2625 description => {
2626 optional => 1,
2627 type => 'string',
2628 description => "Description for the new VM.",
2629 },
2630 pool => {
2631 optional => 1,
2632 type => 'string', format => 'pve-poolid',
2633 description => "Add the new VM to the specified pool.",
2634 },
2635 snapname => get_standard_option('pve-snapshot-name', {
2636 optional => 1,
2637 }),
2638 storage => get_standard_option('pve-storage-id', {
2639 description => "Target storage for full clone.",
2640 optional => 1,
2641 }),
2642 'format' => {
2643 description => "Target format for file storage. Only valid for full clone.",
2644 type => 'string',
2645 optional => 1,
2646 enum => [ 'raw', 'qcow2', 'vmdk'],
2647 },
2648 full => {
2649 optional => 1,
2650 type => 'boolean',
2651 description => "Create a full copy of all disks. This is always done when " .
2652 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2653 },
2654 target => get_standard_option('pve-node', {
2655 description => "Target node. Only allowed if the original VM is on shared storage.",
2656 optional => 1,
2657 }),
2658 bwlimit => {
2659 description => "Override I/O bandwidth limit (in KiB/s).",
2660 optional => 1,
2661 type => 'integer',
2662 minimum => '0',
2663 default => 'clone limit from datacenter or storage config',
2664 },
2665 },
2666 },
2667 returns => {
2668 type => 'string',
2669 },
2670 code => sub {
2671 my ($param) = @_;
2672
2673 my $rpcenv = PVE::RPCEnvironment::get();
2674
2675 my $authuser = $rpcenv->get_user();
2676
2677 my $node = extract_param($param, 'node');
2678
2679 my $vmid = extract_param($param, 'vmid');
2680
2681 my $newid = extract_param($param, 'newid');
2682
2683 my $pool = extract_param($param, 'pool');
2684
2685 if (defined($pool)) {
2686 $rpcenv->check_pool_exist($pool);
2687 }
2688
2689 my $snapname = extract_param($param, 'snapname');
2690
2691 my $storage = extract_param($param, 'storage');
2692
2693 my $format = extract_param($param, 'format');
2694
2695 my $target = extract_param($param, 'target');
2696
2697 my $localnode = PVE::INotify::nodename();
2698
2699 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2700
2701 PVE::Cluster::check_node_exists($target) if $target;
2702
2703 my $storecfg = PVE::Storage::config();
2704
2705 if ($storage) {
2706 # check if storage is enabled on local node
2707 PVE::Storage::storage_check_enabled($storecfg, $storage);
2708 if ($target) {
2709 # check if storage is available on target node
2710 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2711 # clone only works if target storage is shared
2712 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2713 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2714 }
2715 }
2716
2717 PVE::Cluster::check_cfs_quorum();
2718
2719 my $running = PVE::QemuServer::check_running($vmid) || 0;
2720
2721 # exclusive lock if VM is running - else shared lock is enough;
2722 my $shared_lock = $running ? 0 : 1;
2723
2724 my $clonefn = sub {
2725
2726 # do all tests after lock
2727 # we also try to do all tests before we fork the worker
2728
2729 my $conf = PVE::QemuConfig->load_config($vmid);
2730
2731 PVE::QemuConfig->check_lock($conf);
2732
2733 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
2734
2735 die "unexpected state change\n" if $verify_running != $running;
2736
2737 die "snapshot '$snapname' does not exist\n"
2738 if $snapname && !defined( $conf->{snapshots}->{$snapname});
2739
2740 my $full = extract_param($param, 'full');
2741 if (!defined($full)) {
2742 $full = !PVE::QemuConfig->is_template($conf);
2743 }
2744
2745 die "parameter 'storage' not allowed for linked clones\n"
2746 if defined($storage) && !$full;
2747
2748 die "parameter 'format' not allowed for linked clones\n"
2749 if defined($format) && !$full;
2750
2751 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
2752
2753 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2754
2755 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2756
2757 my $conffile = PVE::QemuConfig->config_file($newid);
2758
2759 die "unable to create VM $newid: config file already exists\n"
2760 if -f $conffile;
2761
2762 my $newconf = { lock => 'clone' };
2763 my $drives = {};
2764 my $fullclone = {};
2765 my $vollist = [];
2766
2767 foreach my $opt (keys %$oldconf) {
2768 my $value = $oldconf->{$opt};
2769
2770 # do not copy snapshot related info
2771 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2772 $opt eq 'vmstate' || $opt eq 'snapstate';
2773
2774 # no need to copy unused images, because VMID(owner) changes anyways
2775 next if $opt =~ m/^unused\d+$/;
2776
2777 # always change MAC! address
2778 if ($opt =~ m/^net(\d+)$/) {
2779 my $net = PVE::QemuServer::parse_net($value);
2780 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
2781 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
2782 $newconf->{$opt} = PVE::QemuServer::print_net($net);
2783 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
2784 my $drive = PVE::QemuServer::parse_drive($opt, $value);
2785 die "unable to parse drive options for '$opt'\n" if !$drive;
2786 if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
2787 $newconf->{$opt} = $value; # simply copy configuration
2788 } else {
2789 if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
2790 die "Full clone feature is not supported for drive '$opt'\n"
2791 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
2792 $fullclone->{$opt} = 1;
2793 } else {
2794 # not full means clone instead of copy
2795 die "Linked clone feature is not supported for drive '$opt'\n"
2796 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
2797 }
2798 $drives->{$opt} = $drive;
2799 push @$vollist, $drive->{file};
2800 }
2801 } else {
2802 # copy everything else
2803 $newconf->{$opt} = $value;
2804 }
2805 }
2806
2807 # auto generate a new uuid
2808 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
2809 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
2810 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
2811
2812 # auto generate a new vmgenid if the option was set
2813 if ($newconf->{vmgenid}) {
2814 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
2815 }
2816
2817 delete $newconf->{template};
2818
2819 if ($param->{name}) {
2820 $newconf->{name} = $param->{name};
2821 } else {
2822 if ($oldconf->{name}) {
2823 $newconf->{name} = "Copy-of-$oldconf->{name}";
2824 } else {
2825 $newconf->{name} = "Copy-of-VM-$vmid";
2826 }
2827 }
2828
2829 if ($param->{description}) {
2830 $newconf->{description} = $param->{description};
2831 }
2832
2833 # create empty/temp config - this fails if VM already exists on other node
2834 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
2835
2836 my $realcmd = sub {
2837 my $upid = shift;
2838
2839 my $newvollist = [];
2840 my $jobs = {};
2841
2842 eval {
2843 local $SIG{INT} =
2844 local $SIG{TERM} =
2845 local $SIG{QUIT} =
2846 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
2847
2848 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
2849
2850 my $bwlimit = extract_param($param, 'bwlimit');
2851
2852 my $total_jobs = scalar(keys %{$drives});
2853 my $i = 1;
2854
2855 foreach my $opt (keys %$drives) {
2856 my $drive = $drives->{$opt};
2857 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2858
2859 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
2860 my $storage_list = [ $src_sid ];
2861 push @$storage_list, $storage if defined($storage);
2862 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
2863
2864 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
2865 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2866 $jobs, $skipcomplete, $oldconf->{agent}, $clonelimit);
2867
2868 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2869
2870 PVE::QemuConfig->write_config($newid, $newconf);
2871 $i++;
2872 }
2873
2874 delete $newconf->{lock};
2875
2876 # do not write pending changes
2877 if (my @changes = keys %{$newconf->{pending}}) {
2878 my $pending = join(',', @changes);
2879 warn "found pending changes for '$pending', discarding for clone\n";
2880 delete $newconf->{pending};
2881 }
2882
2883 PVE::QemuConfig->write_config($newid, $newconf);
2884
2885 if ($target) {
2886 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2887 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
2888 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
2889
2890 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
2891 die "Failed to move config to node '$target' - rename failed: $!\n"
2892 if !rename($conffile, $newconffile);
2893 }
2894
2895 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
2896 };
2897 if (my $err = $@) {
2898 unlink $conffile;
2899
2900 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2901
2902 sleep 1; # some storage like rbd need to wait before release volume - really?
2903
2904 foreach my $volid (@$newvollist) {
2905 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2906 warn $@ if $@;
2907 }
2908 die "clone failed: $err";
2909 }
2910
2911 return;
2912 };
2913
2914 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2915
2916 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2917 };
2918
2919 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
2920 # Aquire exclusive lock lock for $newid
2921 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
2922 });
2923
2924 }});
2925
2926 __PACKAGE__->register_method({
2927 name => 'move_vm_disk',
2928 path => '{vmid}/move_disk',
2929 method => 'POST',
2930 protected => 1,
2931 proxyto => 'node',
2932 description => "Move volume to different storage.",
2933 permissions => {
2934 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2935 check => [ 'and',
2936 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2937 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2938 ],
2939 },
2940 parameters => {
2941 additionalProperties => 0,
2942 properties => {
2943 node => get_standard_option('pve-node'),
2944 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2945 disk => {
2946 type => 'string',
2947 description => "The disk you want to move.",
2948 enum => [ PVE::QemuServer::valid_drive_names() ],
2949 },
2950 storage => get_standard_option('pve-storage-id', {
2951 description => "Target storage.",
2952 completion => \&PVE::QemuServer::complete_storage,
2953 }),
2954 'format' => {
2955 type => 'string',
2956 description => "Target Format.",
2957 enum => [ 'raw', 'qcow2', 'vmdk' ],
2958 optional => 1,
2959 },
2960 delete => {
2961 type => 'boolean',
2962 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2963 optional => 1,
2964 default => 0,
2965 },
2966 digest => {
2967 type => 'string',
2968 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2969 maxLength => 40,
2970 optional => 1,
2971 },
2972 bwlimit => {
2973 description => "Override I/O bandwidth limit (in KiB/s).",
2974 optional => 1,
2975 type => 'integer',
2976 minimum => '0',
2977 default => 'move limit from datacenter or storage config',
2978 },
2979 },
2980 },
2981 returns => {
2982 type => 'string',
2983 description => "the task ID.",
2984 },
2985 code => sub {
2986 my ($param) = @_;
2987
2988 my $rpcenv = PVE::RPCEnvironment::get();
2989
2990 my $authuser = $rpcenv->get_user();
2991
2992 my $node = extract_param($param, 'node');
2993
2994 my $vmid = extract_param($param, 'vmid');
2995
2996 my $digest = extract_param($param, 'digest');
2997
2998 my $disk = extract_param($param, 'disk');
2999
3000 my $storeid = extract_param($param, 'storage');
3001
3002 my $format = extract_param($param, 'format');
3003
3004 my $storecfg = PVE::Storage::config();
3005
3006 my $updatefn = sub {
3007
3008 my $conf = PVE::QemuConfig->load_config($vmid);
3009
3010 PVE::QemuConfig->check_lock($conf);
3011
3012 die "checksum missmatch (file change by other user?)\n"
3013 if $digest && $digest ne $conf->{digest};
3014
3015 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3016
3017 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3018
3019 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
3020
3021 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
3022
3023 my $oldfmt;
3024 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
3025 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3026 $oldfmt = $1;
3027 }
3028
3029 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
3030 (!$format || !$oldfmt || $oldfmt eq $format);
3031
3032 # this only checks snapshots because $disk is passed!
3033 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
3034 die "you can't move a disk with snapshots and delete the source\n"
3035 if $snapshotted && $param->{delete};
3036
3037 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
3038
3039 my $running = PVE::QemuServer::check_running($vmid);
3040
3041 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
3042
3043 my $realcmd = sub {
3044
3045 my $newvollist = [];
3046
3047 eval {
3048 local $SIG{INT} =
3049 local $SIG{TERM} =
3050 local $SIG{QUIT} =
3051 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
3052
3053 warn "moving disk with snapshots, snapshots will not be moved!\n"
3054 if $snapshotted;
3055
3056 my $bwlimit = extract_param($param, 'bwlimit');
3057 my $movelimit = PVE::Storage::get_bandwidth_limit('move', [$oldstoreid, $storeid], $bwlimit);
3058
3059 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
3060 $vmid, $storeid, $format, 1, $newvollist, undef, undef, undef, $movelimit);
3061
3062 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
3063
3064 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
3065
3066 # convert moved disk to base if part of template
3067 PVE::QemuServer::template_create($vmid, $conf, $disk)
3068 if PVE::QemuConfig->is_template($conf);
3069
3070 PVE::QemuConfig->write_config($vmid, $conf);
3071
3072 if ($running && PVE::QemuServer::parse_guest_agent($conf)->{fstrim_cloned_disks} && PVE::QemuServer::qga_check_running($vmid)) {
3073 eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fstrim"); };
3074 }
3075
3076 eval {
3077 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
3078 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
3079 if !$running;
3080 };
3081 warn $@ if $@;
3082 };
3083 if (my $err = $@) {
3084
3085 foreach my $volid (@$newvollist) {
3086 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3087 warn $@ if $@;
3088 }
3089 die "storage migration failed: $err";
3090 }
3091
3092 if ($param->{delete}) {
3093 eval {
3094 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
3095 PVE::Storage::vdisk_free($storecfg, $old_volid);
3096 };
3097 warn $@ if $@;
3098 }
3099 };
3100
3101 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
3102 };
3103
3104 return PVE::QemuConfig->lock_config($vmid, $updatefn);
3105 }});
3106
3107 my $check_vm_disks_local = sub {
3108 my ($storecfg, $vmconf, $vmid) = @_;
3109
3110 my $local_disks = {};
3111
3112 # add some more information to the disks e.g. cdrom
3113 PVE::QemuServer::foreach_volid($vmconf, sub {
3114 my ($volid, $attr) = @_;
3115
3116 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
3117 if ($storeid) {
3118 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
3119 return if $scfg->{shared};
3120 }
3121 # The shared attr here is just a special case where the vdisk
3122 # is marked as shared manually
3123 return if $attr->{shared};
3124 return if $attr->{cdrom} and $volid eq "none";
3125
3126 if (exists $local_disks->{$volid}) {
3127 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
3128 } else {
3129 $local_disks->{$volid} = $attr;
3130 # ensure volid is present in case it's needed
3131 $local_disks->{$volid}->{volid} = $volid;
3132 }
3133 });
3134
3135 return $local_disks;
3136 };
3137
3138 __PACKAGE__->register_method({
3139 name => 'migrate_vm_precondition',
3140 path => '{vmid}/migrate',
3141 method => 'GET',
3142 protected => 1,
3143 proxyto => 'node',
3144 description => "Get preconditions for migration.",
3145 permissions => {
3146 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3147 },
3148 parameters => {
3149 additionalProperties => 0,
3150 properties => {
3151 node => get_standard_option('pve-node'),
3152 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3153 target => get_standard_option('pve-node', {
3154 description => "Target node.",
3155 completion => \&PVE::Cluster::complete_migration_target,
3156 optional => 1,
3157 }),
3158 },
3159 },
3160 returns => {
3161 type => "object",
3162 properties => {
3163 running => { type => 'boolean' },
3164 allowed_nodes => {
3165 type => 'array',
3166 optional => 1,
3167 description => "List nodes allowed for offline migration, only passed if VM is offline"
3168 },
3169 not_allowed_nodes => {
3170 type => 'object',
3171 optional => 1,
3172 description => "List not allowed nodes with additional informations, only passed if VM is offline"
3173 },
3174 local_disks => {
3175 type => 'array',
3176 description => "List local disks including CD-Rom, unsused and not referenced disks"
3177 },
3178 local_resources => {
3179 type => 'array',
3180 description => "List local resources e.g. pci, usb"
3181 }
3182 },
3183 },
3184 code => sub {
3185 my ($param) = @_;
3186
3187 my $rpcenv = PVE::RPCEnvironment::get();
3188
3189 my $authuser = $rpcenv->get_user();
3190
3191 PVE::Cluster::check_cfs_quorum();
3192
3193 my $res = {};
3194
3195 my $vmid = extract_param($param, 'vmid');
3196 my $target = extract_param($param, 'target');
3197 my $localnode = PVE::INotify::nodename();
3198
3199
3200 # test if VM exists
3201 my $vmconf = PVE::QemuConfig->load_config($vmid);
3202 my $storecfg = PVE::Storage::config();
3203
3204
3205 # try to detect errors early
3206 PVE::QemuConfig->check_lock($vmconf);
3207
3208 $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
3209
3210 # if vm is not running, return target nodes where local storage is available
3211 # for offline migration
3212 if (!$res->{running}) {
3213 $res->{allowed_nodes} = [];
3214 my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg);
3215 delete $checked_nodes->{$localnode};
3216
3217 foreach my $node (keys %$checked_nodes) {
3218 if (!defined $checked_nodes->{$node}->{unavailable_storages}) {
3219 push @{$res->{allowed_nodes}}, $node;
3220 }
3221
3222 }
3223 $res->{not_allowed_nodes} = $checked_nodes;
3224 }
3225
3226
3227 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
3228 $res->{local_disks} = [ values %$local_disks ];;
3229
3230 my $local_resources = PVE::QemuServer::check_local_resources($vmconf, 1);
3231
3232 $res->{local_resources} = $local_resources;
3233
3234 return $res;
3235
3236
3237 }});
3238
3239 __PACKAGE__->register_method({
3240 name => 'migrate_vm',
3241 path => '{vmid}/migrate',
3242 method => 'POST',
3243 protected => 1,
3244 proxyto => 'node',
3245 description => "Migrate virtual machine. Creates a new migration task.",
3246 permissions => {
3247 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3248 },
3249 parameters => {
3250 additionalProperties => 0,
3251 properties => {
3252 node => get_standard_option('pve-node'),
3253 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3254 target => get_standard_option('pve-node', {
3255 description => "Target node.",
3256 completion => \&PVE::Cluster::complete_migration_target,
3257 }),
3258 online => {
3259 type => 'boolean',
3260 description => "Use online/live migration.",
3261 optional => 1,
3262 },
3263 force => {
3264 type => 'boolean',
3265 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
3266 optional => 1,
3267 },
3268 migration_type => {
3269 type => 'string',
3270 enum => ['secure', 'insecure'],
3271 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
3272 optional => 1,
3273 },
3274 migration_network => {
3275 type => 'string', format => 'CIDR',
3276 description => "CIDR of the (sub) network that is used for migration.",
3277 optional => 1,
3278 },
3279 "with-local-disks" => {
3280 type => 'boolean',
3281 description => "Enable live storage migration for local disk",
3282 optional => 1,
3283 },
3284 targetstorage => get_standard_option('pve-storage-id', {
3285 description => "Default target storage.",
3286 optional => 1,
3287 completion => \&PVE::QemuServer::complete_storage,
3288 }),
3289 bwlimit => {
3290 description => "Override I/O bandwidth limit (in KiB/s).",
3291 optional => 1,
3292 type => 'integer',
3293 minimum => '0',
3294 default => 'migrate limit from datacenter or storage config',
3295 },
3296 },
3297 },
3298 returns => {
3299 type => 'string',
3300 description => "the task ID.",
3301 },
3302 code => sub {
3303 my ($param) = @_;
3304
3305 my $rpcenv = PVE::RPCEnvironment::get();
3306 my $authuser = $rpcenv->get_user();
3307
3308 my $target = extract_param($param, 'target');
3309
3310 my $localnode = PVE::INotify::nodename();
3311 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
3312
3313 PVE::Cluster::check_cfs_quorum();
3314
3315 PVE::Cluster::check_node_exists($target);
3316
3317 my $targetip = PVE::Cluster::remote_node_ip($target);
3318
3319 my $vmid = extract_param($param, 'vmid');
3320
3321 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
3322 if !$param->{online} && $param->{targetstorage};
3323
3324 raise_param_exc({ force => "Only root may use this option." })
3325 if $param->{force} && $authuser ne 'root@pam';
3326
3327 raise_param_exc({ migration_type => "Only root may use this option." })
3328 if $param->{migration_type} && $authuser ne 'root@pam';
3329
3330 # allow root only until better network permissions are available
3331 raise_param_exc({ migration_network => "Only root may use this option." })
3332 if $param->{migration_network} && $authuser ne 'root@pam';
3333
3334 # test if VM exists
3335 my $conf = PVE::QemuConfig->load_config($vmid);
3336
3337 # try to detect errors early
3338
3339 PVE::QemuConfig->check_lock($conf);
3340
3341 if (PVE::QemuServer::check_running($vmid)) {
3342 die "cant migrate running VM without --online\n"
3343 if !$param->{online};
3344 }
3345
3346 my $storecfg = PVE::Storage::config();
3347
3348 if( $param->{targetstorage}) {
3349 PVE::Storage::storage_check_node($storecfg, $param->{targetstorage}, $target);
3350 } else {
3351 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
3352 }
3353
3354 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3355
3356 my $hacmd = sub {
3357 my $upid = shift;
3358
3359 print "Requesting HA migration for VM $vmid to node $target\n";
3360
3361 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
3362 PVE::Tools::run_command($cmd);
3363 return;
3364 };
3365
3366 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
3367
3368 } else {
3369
3370 my $realcmd = sub {
3371 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
3372 };
3373
3374 my $worker = sub {
3375 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3376 };
3377
3378 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
3379 }
3380
3381 }});
3382
3383 __PACKAGE__->register_method({
3384 name => 'monitor',
3385 path => '{vmid}/monitor',
3386 method => 'POST',
3387 protected => 1,
3388 proxyto => 'node',
3389 description => "Execute Qemu monitor commands.",
3390 permissions => {
3391 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
3392 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
3393 },
3394 parameters => {
3395 additionalProperties => 0,
3396 properties => {
3397 node => get_standard_option('pve-node'),
3398 vmid => get_standard_option('pve-vmid'),
3399 command => {
3400 type => 'string',
3401 description => "The monitor command.",
3402 }
3403 },
3404 },
3405 returns => { type => 'string'},
3406 code => sub {
3407 my ($param) = @_;
3408
3409 my $rpcenv = PVE::RPCEnvironment::get();
3410 my $authuser = $rpcenv->get_user();
3411
3412 my $is_ro = sub {
3413 my $command = shift;
3414 return $command =~ m/^\s*info(\s+|$)/
3415 || $command =~ m/^\s*help\s*$/;
3416 };
3417
3418 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
3419 if !&$is_ro($param->{command});
3420
3421 my $vmid = $param->{vmid};
3422
3423 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
3424
3425 my $res = '';
3426 eval {
3427 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
3428 };
3429 $res = "ERROR: $@" if $@;
3430
3431 return $res;
3432 }});
3433
3434 __PACKAGE__->register_method({
3435 name => 'resize_vm',
3436 path => '{vmid}/resize',
3437 method => 'PUT',
3438 protected => 1,
3439 proxyto => 'node',
3440 description => "Extend volume size.",
3441 permissions => {
3442 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3443 },
3444 parameters => {
3445 additionalProperties => 0,
3446 properties => {
3447 node => get_standard_option('pve-node'),
3448 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3449 skiplock => get_standard_option('skiplock'),
3450 disk => {
3451 type => 'string',
3452 description => "The disk you want to resize.",
3453 enum => [PVE::QemuServer::valid_drive_names()],
3454 },
3455 size => {
3456 type => 'string',
3457 pattern => '\+?\d+(\.\d+)?[KMGT]?',
3458 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.",
3459 },
3460 digest => {
3461 type => 'string',
3462 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3463 maxLength => 40,
3464 optional => 1,
3465 },
3466 },
3467 },
3468 returns => { type => 'null'},
3469 code => sub {
3470 my ($param) = @_;
3471
3472 my $rpcenv = PVE::RPCEnvironment::get();
3473
3474 my $authuser = $rpcenv->get_user();
3475
3476 my $node = extract_param($param, 'node');
3477
3478 my $vmid = extract_param($param, 'vmid');
3479
3480 my $digest = extract_param($param, 'digest');
3481
3482 my $disk = extract_param($param, 'disk');
3483
3484 my $sizestr = extract_param($param, 'size');
3485
3486 my $skiplock = extract_param($param, 'skiplock');
3487 raise_param_exc({ skiplock => "Only root may use this option." })
3488 if $skiplock && $authuser ne 'root@pam';
3489
3490 my $storecfg = PVE::Storage::config();
3491
3492 my $updatefn = sub {
3493
3494 my $conf = PVE::QemuConfig->load_config($vmid);
3495
3496 die "checksum missmatch (file change by other user?)\n"
3497 if $digest && $digest ne $conf->{digest};
3498 PVE::QemuConfig->check_lock($conf) if !$skiplock;
3499
3500 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3501
3502 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3503
3504 my (undef, undef, undef, undef, undef, undef, $format) =
3505 PVE::Storage::parse_volname($storecfg, $drive->{file});
3506
3507 die "can't resize volume: $disk if snapshot exists\n"
3508 if %{$conf->{snapshots}} && $format eq 'qcow2';
3509
3510 my $volid = $drive->{file};
3511
3512 die "disk '$disk' has no associated volume\n" if !$volid;
3513
3514 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3515
3516 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3517
3518 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3519
3520 PVE::Storage::activate_volumes($storecfg, [$volid]);
3521 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3522
3523 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3524 my ($ext, $newsize, $unit) = ($1, $2, $4);
3525 if ($unit) {
3526 if ($unit eq 'K') {
3527 $newsize = $newsize * 1024;
3528 } elsif ($unit eq 'M') {
3529 $newsize = $newsize * 1024 * 1024;
3530 } elsif ($unit eq 'G') {
3531 $newsize = $newsize * 1024 * 1024 * 1024;
3532 } elsif ($unit eq 'T') {
3533 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3534 }
3535 }
3536 $newsize += $size if $ext;
3537 $newsize = int($newsize);
3538
3539 die "shrinking disks is not supported\n" if $newsize < $size;
3540
3541 return if $size == $newsize;
3542
3543 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3544
3545 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3546
3547 $drive->{size} = $newsize;
3548 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3549
3550 PVE::QemuConfig->write_config($vmid, $conf);
3551 };
3552
3553 PVE::QemuConfig->lock_config($vmid, $updatefn);
3554 return undef;
3555 }});
3556
3557 __PACKAGE__->register_method({
3558 name => 'snapshot_list',
3559 path => '{vmid}/snapshot',
3560 method => 'GET',
3561 description => "List all snapshots.",
3562 permissions => {
3563 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3564 },
3565 proxyto => 'node',
3566 protected => 1, # qemu pid files are only readable by root
3567 parameters => {
3568 additionalProperties => 0,
3569 properties => {
3570 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3571 node => get_standard_option('pve-node'),
3572 },
3573 },
3574 returns => {
3575 type => 'array',
3576 items => {
3577 type => "object",
3578 properties => {
3579 name => {
3580 description => "Snapshot identifier. Value 'current' identifies the current VM.",
3581 type => 'string',
3582 },
3583 vmstate => {
3584 description => "Snapshot includes RAM.",
3585 type => 'boolean',
3586 optional => 1,
3587 },
3588 description => {
3589 description => "Snapshot description.",
3590 type => 'string',
3591 },
3592 snaptime => {
3593 description => "Snapshot creation time",
3594 type => 'integer',
3595 renderer => 'timestamp',
3596 optional => 1,
3597 },
3598 parent => {
3599 description => "Parent snapshot identifier.",
3600 type => 'string',
3601 optional => 1,
3602 },
3603 },
3604 },
3605 links => [ { rel => 'child', href => "{name}" } ],
3606 },
3607 code => sub {
3608 my ($param) = @_;
3609
3610 my $vmid = $param->{vmid};
3611
3612 my $conf = PVE::QemuConfig->load_config($vmid);
3613 my $snaphash = $conf->{snapshots} || {};
3614
3615 my $res = [];
3616
3617 foreach my $name (keys %$snaphash) {
3618 my $d = $snaphash->{$name};
3619 my $item = {
3620 name => $name,
3621 snaptime => $d->{snaptime} || 0,
3622 vmstate => $d->{vmstate} ? 1 : 0,
3623 description => $d->{description} || '',
3624 };
3625 $item->{parent} = $d->{parent} if $d->{parent};
3626 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
3627 push @$res, $item;
3628 }
3629
3630 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
3631 my $current = {
3632 name => 'current',
3633 digest => $conf->{digest},
3634 running => $running,
3635 description => "You are here!",
3636 };
3637 $current->{parent} = $conf->{parent} if $conf->{parent};
3638
3639 push @$res, $current;
3640
3641 return $res;
3642 }});
3643
3644 __PACKAGE__->register_method({
3645 name => 'snapshot',
3646 path => '{vmid}/snapshot',
3647 method => 'POST',
3648 protected => 1,
3649 proxyto => 'node',
3650 description => "Snapshot a VM.",
3651 permissions => {
3652 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3653 },
3654 parameters => {
3655 additionalProperties => 0,
3656 properties => {
3657 node => get_standard_option('pve-node'),
3658 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3659 snapname => get_standard_option('pve-snapshot-name'),
3660 vmstate => {
3661 optional => 1,
3662 type => 'boolean',
3663 description => "Save the vmstate",
3664 },
3665 description => {
3666 optional => 1,
3667 type => 'string',
3668 description => "A textual description or comment.",
3669 },
3670 },
3671 },
3672 returns => {
3673 type => 'string',
3674 description => "the task ID.",
3675 },
3676 code => sub {
3677 my ($param) = @_;
3678
3679 my $rpcenv = PVE::RPCEnvironment::get();
3680
3681 my $authuser = $rpcenv->get_user();
3682
3683 my $node = extract_param($param, 'node');
3684
3685 my $vmid = extract_param($param, 'vmid');
3686
3687 my $snapname = extract_param($param, 'snapname');
3688
3689 die "unable to use snapshot name 'current' (reserved name)\n"
3690 if $snapname eq 'current';
3691
3692 my $realcmd = sub {
3693 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
3694 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
3695 $param->{description});
3696 };
3697
3698 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3699 }});
3700
3701 __PACKAGE__->register_method({
3702 name => 'snapshot_cmd_idx',
3703 path => '{vmid}/snapshot/{snapname}',
3704 description => '',
3705 method => 'GET',
3706 permissions => {
3707 user => 'all',
3708 },
3709 parameters => {
3710 additionalProperties => 0,
3711 properties => {
3712 vmid => get_standard_option('pve-vmid'),
3713 node => get_standard_option('pve-node'),
3714 snapname => get_standard_option('pve-snapshot-name'),
3715 },
3716 },
3717 returns => {
3718 type => 'array',
3719 items => {
3720 type => "object",
3721 properties => {},
3722 },
3723 links => [ { rel => 'child', href => "{cmd}" } ],
3724 },
3725 code => sub {
3726 my ($param) = @_;
3727
3728 my $res = [];
3729
3730 push @$res, { cmd => 'rollback' };
3731 push @$res, { cmd => 'config' };
3732
3733 return $res;
3734 }});
3735
3736 __PACKAGE__->register_method({
3737 name => 'update_snapshot_config',
3738 path => '{vmid}/snapshot/{snapname}/config',
3739 method => 'PUT',
3740 protected => 1,
3741 proxyto => 'node',
3742 description => "Update snapshot metadata.",
3743 permissions => {
3744 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3745 },
3746 parameters => {
3747 additionalProperties => 0,
3748 properties => {
3749 node => get_standard_option('pve-node'),
3750 vmid => get_standard_option('pve-vmid'),
3751 snapname => get_standard_option('pve-snapshot-name'),
3752 description => {
3753 optional => 1,
3754 type => 'string',
3755 description => "A textual description or comment.",
3756 },
3757 },
3758 },
3759 returns => { type => 'null' },
3760 code => sub {
3761 my ($param) = @_;
3762
3763 my $rpcenv = PVE::RPCEnvironment::get();
3764
3765 my $authuser = $rpcenv->get_user();
3766
3767 my $vmid = extract_param($param, 'vmid');
3768
3769 my $snapname = extract_param($param, 'snapname');
3770
3771 return undef if !defined($param->{description});
3772
3773 my $updatefn = sub {
3774
3775 my $conf = PVE::QemuConfig->load_config($vmid);
3776
3777 PVE::QemuConfig->check_lock($conf);
3778
3779 my $snap = $conf->{snapshots}->{$snapname};
3780
3781 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3782
3783 $snap->{description} = $param->{description} if defined($param->{description});
3784
3785 PVE::QemuConfig->write_config($vmid, $conf);
3786 };
3787
3788 PVE::QemuConfig->lock_config($vmid, $updatefn);
3789
3790 return undef;
3791 }});
3792
3793 __PACKAGE__->register_method({
3794 name => 'get_snapshot_config',
3795 path => '{vmid}/snapshot/{snapname}/config',
3796 method => 'GET',
3797 proxyto => 'node',
3798 description => "Get snapshot configuration",
3799 permissions => {
3800 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
3801 },
3802 parameters => {
3803 additionalProperties => 0,
3804 properties => {
3805 node => get_standard_option('pve-node'),
3806 vmid => get_standard_option('pve-vmid'),
3807 snapname => get_standard_option('pve-snapshot-name'),
3808 },
3809 },
3810 returns => { type => "object" },
3811 code => sub {
3812 my ($param) = @_;
3813
3814 my $rpcenv = PVE::RPCEnvironment::get();
3815
3816 my $authuser = $rpcenv->get_user();
3817
3818 my $vmid = extract_param($param, 'vmid');
3819
3820 my $snapname = extract_param($param, 'snapname');
3821
3822 my $conf = PVE::QemuConfig->load_config($vmid);
3823
3824 my $snap = $conf->{snapshots}->{$snapname};
3825
3826 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3827
3828 return $snap;
3829 }});
3830
3831 __PACKAGE__->register_method({
3832 name => 'rollback',
3833 path => '{vmid}/snapshot/{snapname}/rollback',
3834 method => 'POST',
3835 protected => 1,
3836 proxyto => 'node',
3837 description => "Rollback VM state to specified snapshot.",
3838 permissions => {
3839 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
3840 },
3841 parameters => {
3842 additionalProperties => 0,
3843 properties => {
3844 node => get_standard_option('pve-node'),
3845 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3846 snapname => get_standard_option('pve-snapshot-name'),
3847 },
3848 },
3849 returns => {
3850 type => 'string',
3851 description => "the task ID.",
3852 },
3853 code => sub {
3854 my ($param) = @_;
3855
3856 my $rpcenv = PVE::RPCEnvironment::get();
3857
3858 my $authuser = $rpcenv->get_user();
3859
3860 my $node = extract_param($param, 'node');
3861
3862 my $vmid = extract_param($param, 'vmid');
3863
3864 my $snapname = extract_param($param, 'snapname');
3865
3866 my $realcmd = sub {
3867 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3868 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
3869 };
3870
3871 my $worker = sub {
3872 # hold migration lock, this makes sure that nobody create replication snapshots
3873 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3874 };
3875
3876 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
3877 }});
3878
3879 __PACKAGE__->register_method({
3880 name => 'delsnapshot',
3881 path => '{vmid}/snapshot/{snapname}',
3882 method => 'DELETE',
3883 protected => 1,
3884 proxyto => 'node',
3885 description => "Delete a VM snapshot.",
3886 permissions => {
3887 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3888 },
3889 parameters => {
3890 additionalProperties => 0,
3891 properties => {
3892 node => get_standard_option('pve-node'),
3893 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3894 snapname => get_standard_option('pve-snapshot-name'),
3895 force => {
3896 optional => 1,
3897 type => 'boolean',
3898 description => "For removal from config file, even if removing disk snapshots fails.",
3899 },
3900 },
3901 },
3902 returns => {
3903 type => 'string',
3904 description => "the task ID.",
3905 },
3906 code => sub {
3907 my ($param) = @_;
3908
3909 my $rpcenv = PVE::RPCEnvironment::get();
3910
3911 my $authuser = $rpcenv->get_user();
3912
3913 my $node = extract_param($param, 'node');
3914
3915 my $vmid = extract_param($param, 'vmid');
3916
3917 my $snapname = extract_param($param, 'snapname');
3918
3919 my $realcmd = sub {
3920 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
3921 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
3922 };
3923
3924 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3925 }});
3926
3927 __PACKAGE__->register_method({
3928 name => 'template',
3929 path => '{vmid}/template',
3930 method => 'POST',
3931 protected => 1,
3932 proxyto => 'node',
3933 description => "Create a Template.",
3934 permissions => {
3935 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3936 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3937 },
3938 parameters => {
3939 additionalProperties => 0,
3940 properties => {
3941 node => get_standard_option('pve-node'),
3942 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3943 disk => {
3944 optional => 1,
3945 type => 'string',
3946 description => "If you want to convert only 1 disk to base image.",
3947 enum => [PVE::QemuServer::valid_drive_names()],
3948 },
3949
3950 },
3951 },
3952 returns => { type => 'null'},
3953 code => sub {
3954 my ($param) = @_;
3955
3956 my $rpcenv = PVE::RPCEnvironment::get();
3957
3958 my $authuser = $rpcenv->get_user();
3959
3960 my $node = extract_param($param, 'node');
3961
3962 my $vmid = extract_param($param, 'vmid');
3963
3964 my $disk = extract_param($param, 'disk');
3965
3966 my $updatefn = sub {
3967
3968 my $conf = PVE::QemuConfig->load_config($vmid);
3969
3970 PVE::QemuConfig->check_lock($conf);
3971
3972 die "unable to create template, because VM contains snapshots\n"
3973 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
3974
3975 die "you can't convert a template to a template\n"
3976 if PVE::QemuConfig->is_template($conf) && !$disk;
3977
3978 die "you can't convert a VM to template if VM is running\n"
3979 if PVE::QemuServer::check_running($vmid);
3980
3981 my $realcmd = sub {
3982 PVE::QemuServer::template_create($vmid, $conf, $disk);
3983 };
3984
3985 $conf->{template} = 1;
3986 PVE::QemuConfig->write_config($vmid, $conf);
3987
3988 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3989 };
3990
3991 PVE::QemuConfig->lock_config($vmid, $updatefn);
3992 return undef;
3993 }});
3994
3995 __PACKAGE__->register_method({
3996 name => 'cloudinit_generated_config_dump',
3997 path => '{vmid}/cloudinit/dump',
3998 method => 'GET',
3999 proxyto => 'node',
4000 description => "Get automatically generated cloudinit config.",
4001 permissions => {
4002 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4003 },
4004 parameters => {
4005 additionalProperties => 0,
4006 properties => {
4007 node => get_standard_option('pve-node'),
4008 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4009 type => {
4010 description => 'Config type.',
4011 type => 'string',
4012 enum => ['user', 'network', 'meta'],
4013 },
4014 },
4015 },
4016 returns => {
4017 type => 'string',
4018 },
4019 code => sub {
4020 my ($param) = @_;
4021
4022 my $conf = PVE::QemuConfig->load_config($param->{vmid});
4023
4024 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
4025 }});
4026
4027 1;