]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu.pm
0033d2bc0d5feef269ab7c7bc7ecbc3075b41a44
[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 raise_param_exc({ snapshot => "cannot use 'snapshot' parameter with 'current'",
869 current => "cannot use 'snapshot' parameter with 'current'"})
870 if ($param->{snapshot} && $param->{current});
871
872 my $conf;
873 if ($param->{snapshot}) {
874 $conf = PVE::QemuConfig->load_snapshot_config($param->{vmid}, $param->{snapshot});
875 } else {
876 $conf = PVE::QemuConfig->load_current_config($param->{vmid}, $param->{current});
877 }
878 $conf->{cipassword} = '**********' if $conf->{cipassword};
879 return $conf;
880
881 }});
882
883 __PACKAGE__->register_method({
884 name => 'vm_pending',
885 path => '{vmid}/pending',
886 method => 'GET',
887 proxyto => 'node',
888 description => "Get virtual machine configuration, including pending changes.",
889 permissions => {
890 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
891 },
892 parameters => {
893 additionalProperties => 0,
894 properties => {
895 node => get_standard_option('pve-node'),
896 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
897 },
898 },
899 returns => {
900 type => "array",
901 items => {
902 type => "object",
903 properties => {
904 key => {
905 description => "Configuration option name.",
906 type => 'string',
907 },
908 value => {
909 description => "Current value.",
910 type => 'string',
911 optional => 1,
912 },
913 pending => {
914 description => "Pending value.",
915 type => 'string',
916 optional => 1,
917 },
918 delete => {
919 description => "Indicates a pending delete request if present and not 0. " .
920 "The value 2 indicates a force-delete request.",
921 type => 'integer',
922 minimum => 0,
923 maximum => 2,
924 optional => 1,
925 },
926 },
927 },
928 },
929 code => sub {
930 my ($param) = @_;
931
932 my $conf = PVE::QemuConfig->load_config($param->{vmid});
933
934 my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
935
936 $conf->{cipassword} = '**********' if defined($conf->{cipassword});
937 $conf->{pending}->{cipassword} = '********** ' if defined($conf->{pending}->{cipassword});
938
939 return PVE::GuestHelpers::config_with_pending_array($conf, $pending_delete_hash);
940 }});
941
942 # POST/PUT {vmid}/config implementation
943 #
944 # The original API used PUT (idempotent) an we assumed that all operations
945 # are fast. But it turned out that almost any configuration change can
946 # involve hot-plug actions, or disk alloc/free. Such actions can take long
947 # time to complete and have side effects (not idempotent).
948 #
949 # The new implementation uses POST and forks a worker process. We added
950 # a new option 'background_delay'. If specified we wait up to
951 # 'background_delay' second for the worker task to complete. It returns null
952 # if the task is finished within that time, else we return the UPID.
953
954 my $update_vm_api = sub {
955 my ($param, $sync) = @_;
956
957 my $rpcenv = PVE::RPCEnvironment::get();
958
959 my $authuser = $rpcenv->get_user();
960
961 my $node = extract_param($param, 'node');
962
963 my $vmid = extract_param($param, 'vmid');
964
965 my $digest = extract_param($param, 'digest');
966
967 my $background_delay = extract_param($param, 'background_delay');
968
969 if (defined(my $cipassword = $param->{cipassword})) {
970 # Same logic as in cloud-init (but with the regex fixed...)
971 $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
972 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
973 }
974
975 my @paramarr = (); # used for log message
976 foreach my $key (sort keys %$param) {
977 my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
978 push @paramarr, "-$key", $value;
979 }
980
981 my $skiplock = extract_param($param, 'skiplock');
982 raise_param_exc({ skiplock => "Only root may use this option." })
983 if $skiplock && $authuser ne 'root@pam';
984
985 my $delete_str = extract_param($param, 'delete');
986
987 my $revert_str = extract_param($param, 'revert');
988
989 my $force = extract_param($param, 'force');
990
991 if (defined(my $ssh_keys = $param->{sshkeys})) {
992 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
993 PVE::Tools::validate_ssh_public_keys($ssh_keys);
994 }
995
996 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
997
998 my $storecfg = PVE::Storage::config();
999
1000 my $defaults = PVE::QemuServer::load_defaults();
1001
1002 &$resolve_cdrom_alias($param);
1003
1004 # now try to verify all parameters
1005
1006 my $revert = {};
1007 foreach my $opt (PVE::Tools::split_list($revert_str)) {
1008 if (!PVE::QemuServer::option_exists($opt)) {
1009 raise_param_exc({ revert => "unknown option '$opt'" });
1010 }
1011
1012 raise_param_exc({ delete => "you can't use '-$opt' and " .
1013 "-revert $opt' at the same time" })
1014 if defined($param->{$opt});
1015
1016 $revert->{$opt} = 1;
1017 }
1018
1019 my @delete = ();
1020 foreach my $opt (PVE::Tools::split_list($delete_str)) {
1021 $opt = 'ide2' if $opt eq 'cdrom';
1022
1023 raise_param_exc({ delete => "you can't use '-$opt' and " .
1024 "-delete $opt' at the same time" })
1025 if defined($param->{$opt});
1026
1027 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
1028 "-revert $opt' at the same time" })
1029 if $revert->{$opt};
1030
1031 if (!PVE::QemuServer::option_exists($opt)) {
1032 raise_param_exc({ delete => "unknown option '$opt'" });
1033 }
1034
1035 push @delete, $opt;
1036 }
1037
1038 my $repl_conf = PVE::ReplicationConfig->new();
1039 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1040 my $check_replication = sub {
1041 my ($drive) = @_;
1042 return if !$is_replicated;
1043 my $volid = $drive->{file};
1044 return if !$volid || !($drive->{replicate}//1);
1045 return if PVE::QemuServer::drive_is_cdrom($drive);
1046 my ($storeid, $format);
1047 if ($volid =~ $NEW_DISK_RE) {
1048 $storeid = $2;
1049 $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
1050 } else {
1051 ($storeid, undef) = PVE::Storage::parse_volume_id($volid, 1);
1052 $format = (PVE::Storage::parse_volname($storecfg, $volid))[6];
1053 }
1054 return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
1055 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1056 return if $scfg->{shared};
1057 die "cannot add non-replicatable volume to a replicated VM\n";
1058 };
1059
1060 foreach my $opt (keys %$param) {
1061 if (PVE::QemuServer::is_valid_drivename($opt)) {
1062 # cleanup drive path
1063 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1064 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
1065 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
1066 $check_replication->($drive);
1067 $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
1068 } elsif ($opt =~ m/^net(\d+)$/) {
1069 # add macaddr
1070 my $net = PVE::QemuServer::parse_net($param->{$opt});
1071 $param->{$opt} = PVE::QemuServer::print_net($net);
1072 } elsif ($opt eq 'vmgenid') {
1073 if ($param->{$opt} eq '1') {
1074 $param->{$opt} = PVE::QemuServer::generate_uuid();
1075 }
1076 } elsif ($opt eq 'hookscript') {
1077 eval { PVE::GuestHelpers::check_hookscript($param->{$opt}, $storecfg); };
1078 raise_param_exc({ $opt => $@ }) if $@;
1079 }
1080 }
1081
1082 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
1083
1084 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1085
1086 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1087
1088 my $updatefn = sub {
1089
1090 my $conf = PVE::QemuConfig->load_config($vmid);
1091
1092 die "checksum missmatch (file change by other user?)\n"
1093 if $digest && $digest ne $conf->{digest};
1094
1095 PVE::QemuConfig->check_lock($conf) if !$skiplock;
1096
1097 foreach my $opt (keys %$revert) {
1098 if (defined($conf->{$opt})) {
1099 $param->{$opt} = $conf->{$opt};
1100 } elsif (defined($conf->{pending}->{$opt})) {
1101 push @delete, $opt;
1102 }
1103 }
1104
1105 if ($param->{memory} || defined($param->{balloon})) {
1106 my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
1107 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
1108
1109 die "balloon value too large (must be smaller than assigned memory)\n"
1110 if $balloon && $balloon > $maxmem;
1111 }
1112
1113 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1114
1115 my $worker = sub {
1116
1117 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1118
1119 # write updates to pending section
1120
1121 my $modified = {}; # record what $option we modify
1122
1123 foreach my $opt (@delete) {
1124 $modified->{$opt} = 1;
1125 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1126
1127 # value of what we want to delete, independent if pending or not
1128 my $val = $conf->{$opt} // $conf->{pending}->{$opt};
1129 if (!defined($val)) {
1130 warn "cannot delete '$opt' - not set in current configuration!\n";
1131 $modified->{$opt} = 0;
1132 next;
1133 }
1134 my $is_pending_val = defined($conf->{pending}->{$opt});
1135
1136 if ($opt =~ m/^unused/) {
1137 my $drive = PVE::QemuServer::parse_drive($opt, $val);
1138 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
1139 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1140 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1141 delete $conf->{$opt};
1142 PVE::QemuConfig->write_config($vmid, $conf);
1143 }
1144 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1145 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
1146 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1147 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $val))
1148 if $is_pending_val;
1149 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
1150 PVE::QemuConfig->write_config($vmid, $conf);
1151 } elsif ($opt =~ m/^serial\d+$/) {
1152 if ($val eq 'socket') {
1153 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1154 } elsif ($authuser ne 'root@pam') {
1155 die "only root can delete '$opt' config for real devices\n";
1156 }
1157 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
1158 PVE::QemuConfig->write_config($vmid, $conf);
1159 } elsif ($opt =~ m/^usb\d+$/) {
1160 if ($val =~ m/spice/) {
1161 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1162 } elsif ($authuser ne 'root@pam') {
1163 die "only root can delete '$opt' config for real devices\n";
1164 }
1165 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
1166 PVE::QemuConfig->write_config($vmid, $conf);
1167 } else {
1168 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
1169 PVE::QemuConfig->write_config($vmid, $conf);
1170 }
1171 }
1172
1173 foreach my $opt (keys %$param) { # add/change
1174 $modified->{$opt} = 1;
1175 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1176 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1177
1178 my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf);
1179
1180 if (PVE::QemuServer::is_valid_drivename($opt)) {
1181 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
1182 # FIXME: cloudinit: CDROM or Disk?
1183 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1184 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1185 } else {
1186 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1187 }
1188 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
1189 if defined($conf->{pending}->{$opt});
1190
1191 &$create_disks($rpcenv, $authuser, $conf->{pending}, $arch, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1192 } elsif ($opt =~ m/^serial\d+/) {
1193 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1194 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1195 } elsif ($authuser ne 'root@pam') {
1196 die "only root can modify '$opt' config for real devices\n";
1197 }
1198 $conf->{pending}->{$opt} = $param->{$opt};
1199 } elsif ($opt =~ m/^usb\d+/) {
1200 if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
1201 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1202 } elsif ($authuser ne 'root@pam') {
1203 die "only root can modify '$opt' config for real devices\n";
1204 }
1205 $conf->{pending}->{$opt} = $param->{$opt};
1206 } else {
1207 $conf->{pending}->{$opt} = $param->{$opt};
1208 }
1209 PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
1210 PVE::QemuConfig->write_config($vmid, $conf);
1211 }
1212
1213 # remove pending changes when nothing changed
1214 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1215 my $changes = PVE::QemuConfig->cleanup_pending($conf);
1216 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
1217
1218 return if !scalar(keys %{$conf->{pending}});
1219
1220 my $running = PVE::QemuServer::check_running($vmid);
1221
1222 # apply pending changes
1223
1224 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
1225
1226 if ($running) {
1227 my $errors = {};
1228 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
1229 raise_param_exc($errors) if scalar(keys %$errors);
1230 } else {
1231 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $running);
1232 }
1233
1234 return;
1235 };
1236
1237 if ($sync) {
1238 &$worker();
1239 return undef;
1240 } else {
1241 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1242
1243 if ($background_delay) {
1244
1245 # Note: It would be better to do that in the Event based HTTPServer
1246 # to avoid blocking call to sleep.
1247
1248 my $end_time = time() + $background_delay;
1249
1250 my $task = PVE::Tools::upid_decode($upid);
1251
1252 my $running = 1;
1253 while (time() < $end_time) {
1254 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1255 last if !$running;
1256 sleep(1); # this gets interrupted when child process ends
1257 }
1258
1259 if (!$running) {
1260 my $status = PVE::Tools::upid_read_status($upid);
1261 return undef if $status eq 'OK';
1262 die $status;
1263 }
1264 }
1265
1266 return $upid;
1267 }
1268 };
1269
1270 return PVE::QemuConfig->lock_config($vmid, $updatefn);
1271 };
1272
1273 my $vm_config_perm_list = [
1274 'VM.Config.Disk',
1275 'VM.Config.CDROM',
1276 'VM.Config.CPU',
1277 'VM.Config.Memory',
1278 'VM.Config.Network',
1279 'VM.Config.HWType',
1280 'VM.Config.Options',
1281 ];
1282
1283 __PACKAGE__->register_method({
1284 name => 'update_vm_async',
1285 path => '{vmid}/config',
1286 method => 'POST',
1287 protected => 1,
1288 proxyto => 'node',
1289 description => "Set virtual machine options (asynchrounous API).",
1290 permissions => {
1291 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1292 },
1293 parameters => {
1294 additionalProperties => 0,
1295 properties => PVE::QemuServer::json_config_properties(
1296 {
1297 node => get_standard_option('pve-node'),
1298 vmid => get_standard_option('pve-vmid'),
1299 skiplock => get_standard_option('skiplock'),
1300 delete => {
1301 type => 'string', format => 'pve-configid-list',
1302 description => "A list of settings you want to delete.",
1303 optional => 1,
1304 },
1305 revert => {
1306 type => 'string', format => 'pve-configid-list',
1307 description => "Revert a pending change.",
1308 optional => 1,
1309 },
1310 force => {
1311 type => 'boolean',
1312 description => $opt_force_description,
1313 optional => 1,
1314 requires => 'delete',
1315 },
1316 digest => {
1317 type => 'string',
1318 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1319 maxLength => 40,
1320 optional => 1,
1321 },
1322 background_delay => {
1323 type => 'integer',
1324 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1325 minimum => 1,
1326 maximum => 30,
1327 optional => 1,
1328 },
1329 }),
1330 },
1331 returns => {
1332 type => 'string',
1333 optional => 1,
1334 },
1335 code => $update_vm_api,
1336 });
1337
1338 __PACKAGE__->register_method({
1339 name => 'update_vm',
1340 path => '{vmid}/config',
1341 method => 'PUT',
1342 protected => 1,
1343 proxyto => 'node',
1344 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1345 permissions => {
1346 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1347 },
1348 parameters => {
1349 additionalProperties => 0,
1350 properties => PVE::QemuServer::json_config_properties(
1351 {
1352 node => get_standard_option('pve-node'),
1353 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1354 skiplock => get_standard_option('skiplock'),
1355 delete => {
1356 type => 'string', format => 'pve-configid-list',
1357 description => "A list of settings you want to delete.",
1358 optional => 1,
1359 },
1360 revert => {
1361 type => 'string', format => 'pve-configid-list',
1362 description => "Revert a pending change.",
1363 optional => 1,
1364 },
1365 force => {
1366 type => 'boolean',
1367 description => $opt_force_description,
1368 optional => 1,
1369 requires => 'delete',
1370 },
1371 digest => {
1372 type => 'string',
1373 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1374 maxLength => 40,
1375 optional => 1,
1376 },
1377 }),
1378 },
1379 returns => { type => 'null' },
1380 code => sub {
1381 my ($param) = @_;
1382 &$update_vm_api($param, 1);
1383 return undef;
1384 }
1385 });
1386
1387 __PACKAGE__->register_method({
1388 name => 'destroy_vm',
1389 path => '{vmid}',
1390 method => 'DELETE',
1391 protected => 1,
1392 proxyto => 'node',
1393 description => "Destroy the vm (also delete all used/owned volumes).",
1394 permissions => {
1395 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1396 },
1397 parameters => {
1398 additionalProperties => 0,
1399 properties => {
1400 node => get_standard_option('pve-node'),
1401 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1402 skiplock => get_standard_option('skiplock'),
1403 },
1404 },
1405 returns => {
1406 type => 'string',
1407 },
1408 code => sub {
1409 my ($param) = @_;
1410
1411 my $rpcenv = PVE::RPCEnvironment::get();
1412 my $authuser = $rpcenv->get_user();
1413 my $vmid = $param->{vmid};
1414
1415 my $skiplock = $param->{skiplock};
1416 raise_param_exc({ skiplock => "Only root may use this option." })
1417 if $skiplock && $authuser ne 'root@pam';
1418
1419 # test if VM exists
1420 my $conf = PVE::QemuConfig->load_config($vmid);
1421 my $storecfg = PVE::Storage::config();
1422 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
1423 die "unable to remove VM $vmid - used in HA resources\n"
1424 if PVE::HA::Config::vm_is_ha_managed($vmid);
1425
1426 # do not allow destroy if there are replication jobs
1427 my $repl_conf = PVE::ReplicationConfig->new();
1428 $repl_conf->check_for_existing_jobs($vmid);
1429
1430 # early tests (repeat after locking)
1431 die "VM $vmid is running - destroy failed\n"
1432 if PVE::QemuServer::check_running($vmid);
1433
1434 my $realcmd = sub {
1435 my $upid = shift;
1436
1437 syslog('info', "destroy VM $vmid: $upid\n");
1438 PVE::QemuConfig->lock_config($vmid, sub {
1439 die "VM $vmid is running - destroy failed\n"
1440 if (PVE::QemuServer::check_running($vmid));
1441 PVE::QemuServer::destroy_vm($storecfg, $vmid, 1, $skiplock);
1442 PVE::AccessControl::remove_vm_access($vmid);
1443 PVE::Firewall::remove_vmfw_conf($vmid);
1444
1445 # only now remove the zombie config, else we can have reuse race
1446 PVE::QemuConfig->destroy_config($vmid);
1447 });
1448 };
1449
1450 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1451 }});
1452
1453 __PACKAGE__->register_method({
1454 name => 'unlink',
1455 path => '{vmid}/unlink',
1456 method => 'PUT',
1457 protected => 1,
1458 proxyto => 'node',
1459 description => "Unlink/delete disk images.",
1460 permissions => {
1461 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1462 },
1463 parameters => {
1464 additionalProperties => 0,
1465 properties => {
1466 node => get_standard_option('pve-node'),
1467 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1468 idlist => {
1469 type => 'string', format => 'pve-configid-list',
1470 description => "A list of disk IDs you want to delete.",
1471 },
1472 force => {
1473 type => 'boolean',
1474 description => $opt_force_description,
1475 optional => 1,
1476 },
1477 },
1478 },
1479 returns => { type => 'null'},
1480 code => sub {
1481 my ($param) = @_;
1482
1483 $param->{delete} = extract_param($param, 'idlist');
1484
1485 __PACKAGE__->update_vm($param);
1486
1487 return undef;
1488 }});
1489
1490 my $sslcert;
1491
1492 __PACKAGE__->register_method({
1493 name => 'vncproxy',
1494 path => '{vmid}/vncproxy',
1495 method => 'POST',
1496 protected => 1,
1497 permissions => {
1498 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1499 },
1500 description => "Creates a TCP VNC proxy connections.",
1501 parameters => {
1502 additionalProperties => 0,
1503 properties => {
1504 node => get_standard_option('pve-node'),
1505 vmid => get_standard_option('pve-vmid'),
1506 websocket => {
1507 optional => 1,
1508 type => 'boolean',
1509 description => "starts websockify instead of vncproxy",
1510 },
1511 },
1512 },
1513 returns => {
1514 additionalProperties => 0,
1515 properties => {
1516 user => { type => 'string' },
1517 ticket => { type => 'string' },
1518 cert => { type => 'string' },
1519 port => { type => 'integer' },
1520 upid => { type => 'string' },
1521 },
1522 },
1523 code => sub {
1524 my ($param) = @_;
1525
1526 my $rpcenv = PVE::RPCEnvironment::get();
1527
1528 my $authuser = $rpcenv->get_user();
1529
1530 my $vmid = $param->{vmid};
1531 my $node = $param->{node};
1532 my $websocket = $param->{websocket};
1533
1534 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1535 my $use_serial = ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/));
1536
1537 my $authpath = "/vms/$vmid";
1538
1539 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1540
1541 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1542 if !$sslcert;
1543
1544 my $family;
1545 my $remcmd = [];
1546
1547 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1548 (undef, $family) = PVE::Cluster::remote_node_ip($node);
1549 my $sshinfo = PVE::Cluster::get_ssh_info($node);
1550 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1551 $remcmd = PVE::Cluster::ssh_info_to_command($sshinfo, $use_serial ? '-t' : '-T');
1552 } else {
1553 $family = PVE::Tools::get_host_address_family($node);
1554 }
1555
1556 my $port = PVE::Tools::next_vnc_port($family);
1557
1558 my $timeout = 10;
1559
1560 my $realcmd = sub {
1561 my $upid = shift;
1562
1563 syslog('info', "starting vnc proxy $upid\n");
1564
1565 my $cmd;
1566
1567 if ($use_serial) {
1568
1569 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga}, '-escape', '0' ];
1570
1571 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1572 '-timeout', $timeout, '-authpath', $authpath,
1573 '-perm', 'Sys.Console'];
1574
1575 if ($param->{websocket}) {
1576 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
1577 push @$cmd, '-notls', '-listen', 'localhost';
1578 }
1579
1580 push @$cmd, '-c', @$remcmd, @$termcmd;
1581
1582 PVE::Tools::run_command($cmd);
1583
1584 } else {
1585
1586 $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
1587
1588 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1589
1590 my $sock = IO::Socket::IP->new(
1591 ReuseAddr => 1,
1592 Listen => 1,
1593 LocalPort => $port,
1594 Proto => 'tcp',
1595 GetAddrInfoFlags => 0,
1596 ) or die "failed to create socket: $!\n";
1597 # Inside the worker we shouldn't have any previous alarms
1598 # running anyway...:
1599 alarm(0);
1600 local $SIG{ALRM} = sub { die "connection timed out\n" };
1601 alarm $timeout;
1602 accept(my $cli, $sock) or die "connection failed: $!\n";
1603 alarm(0);
1604 close($sock);
1605 if (PVE::Tools::run_command($cmd,
1606 output => '>&'.fileno($cli),
1607 input => '<&'.fileno($cli),
1608 noerr => 1) != 0)
1609 {
1610 die "Failed to run vncproxy.\n";
1611 }
1612 }
1613
1614 return;
1615 };
1616
1617 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1618
1619 PVE::Tools::wait_for_vnc_port($port);
1620
1621 return {
1622 user => $authuser,
1623 ticket => $ticket,
1624 port => $port,
1625 upid => $upid,
1626 cert => $sslcert,
1627 };
1628 }});
1629
1630 __PACKAGE__->register_method({
1631 name => 'termproxy',
1632 path => '{vmid}/termproxy',
1633 method => 'POST',
1634 protected => 1,
1635 permissions => {
1636 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1637 },
1638 description => "Creates a TCP proxy connections.",
1639 parameters => {
1640 additionalProperties => 0,
1641 properties => {
1642 node => get_standard_option('pve-node'),
1643 vmid => get_standard_option('pve-vmid'),
1644 serial=> {
1645 optional => 1,
1646 type => 'string',
1647 enum => [qw(serial0 serial1 serial2 serial3)],
1648 description => "opens a serial terminal (defaults to display)",
1649 },
1650 },
1651 },
1652 returns => {
1653 additionalProperties => 0,
1654 properties => {
1655 user => { type => 'string' },
1656 ticket => { type => 'string' },
1657 port => { type => 'integer' },
1658 upid => { type => 'string' },
1659 },
1660 },
1661 code => sub {
1662 my ($param) = @_;
1663
1664 my $rpcenv = PVE::RPCEnvironment::get();
1665
1666 my $authuser = $rpcenv->get_user();
1667
1668 my $vmid = $param->{vmid};
1669 my $node = $param->{node};
1670 my $serial = $param->{serial};
1671
1672 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
1673
1674 if (!defined($serial)) {
1675 if ($conf->{vga} && $conf->{vga} =~ m/^serial\d+$/) {
1676 $serial = $conf->{vga};
1677 }
1678 }
1679
1680 my $authpath = "/vms/$vmid";
1681
1682 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
1683
1684 my $family;
1685 my $remcmd = [];
1686
1687 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1688 (undef, $family) = PVE::Cluster::remote_node_ip($node);
1689 my $sshinfo = PVE::Cluster::get_ssh_info($node);
1690 $remcmd = PVE::Cluster::ssh_info_to_command($sshinfo, '-t');
1691 push @$remcmd, '--';
1692 } else {
1693 $family = PVE::Tools::get_host_address_family($node);
1694 }
1695
1696 my $port = PVE::Tools::next_vnc_port($family);
1697
1698 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
1699 push @$termcmd, '-iface', $serial if $serial;
1700
1701 my $realcmd = sub {
1702 my $upid = shift;
1703
1704 syslog('info', "starting qemu termproxy $upid\n");
1705
1706 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
1707 '--perm', 'VM.Console', '--'];
1708 push @$cmd, @$remcmd, @$termcmd;
1709
1710 PVE::Tools::run_command($cmd);
1711 };
1712
1713 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1714
1715 PVE::Tools::wait_for_vnc_port($port);
1716
1717 return {
1718 user => $authuser,
1719 ticket => $ticket,
1720 port => $port,
1721 upid => $upid,
1722 };
1723 }});
1724
1725 __PACKAGE__->register_method({
1726 name => 'vncwebsocket',
1727 path => '{vmid}/vncwebsocket',
1728 method => 'GET',
1729 permissions => {
1730 description => "You also need to pass a valid ticket (vncticket).",
1731 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1732 },
1733 description => "Opens a weksocket for VNC traffic.",
1734 parameters => {
1735 additionalProperties => 0,
1736 properties => {
1737 node => get_standard_option('pve-node'),
1738 vmid => get_standard_option('pve-vmid'),
1739 vncticket => {
1740 description => "Ticket from previous call to vncproxy.",
1741 type => 'string',
1742 maxLength => 512,
1743 },
1744 port => {
1745 description => "Port number returned by previous vncproxy call.",
1746 type => 'integer',
1747 minimum => 5900,
1748 maximum => 5999,
1749 },
1750 },
1751 },
1752 returns => {
1753 type => "object",
1754 properties => {
1755 port => { type => 'string' },
1756 },
1757 },
1758 code => sub {
1759 my ($param) = @_;
1760
1761 my $rpcenv = PVE::RPCEnvironment::get();
1762
1763 my $authuser = $rpcenv->get_user();
1764
1765 my $vmid = $param->{vmid};
1766 my $node = $param->{node};
1767
1768 my $authpath = "/vms/$vmid";
1769
1770 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1771
1772 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
1773
1774 # Note: VNC ports are acessible from outside, so we do not gain any
1775 # security if we verify that $param->{port} belongs to VM $vmid. This
1776 # check is done by verifying the VNC ticket (inside VNC protocol).
1777
1778 my $port = $param->{port};
1779
1780 return { port => $port };
1781 }});
1782
1783 __PACKAGE__->register_method({
1784 name => 'spiceproxy',
1785 path => '{vmid}/spiceproxy',
1786 method => 'POST',
1787 protected => 1,
1788 proxyto => 'node',
1789 permissions => {
1790 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1791 },
1792 description => "Returns a SPICE configuration to connect to the VM.",
1793 parameters => {
1794 additionalProperties => 0,
1795 properties => {
1796 node => get_standard_option('pve-node'),
1797 vmid => get_standard_option('pve-vmid'),
1798 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1799 },
1800 },
1801 returns => get_standard_option('remote-viewer-config'),
1802 code => sub {
1803 my ($param) = @_;
1804
1805 my $rpcenv = PVE::RPCEnvironment::get();
1806
1807 my $authuser = $rpcenv->get_user();
1808
1809 my $vmid = $param->{vmid};
1810 my $node = $param->{node};
1811 my $proxy = $param->{proxy};
1812
1813 my $conf = PVE::QemuConfig->load_config($vmid, $node);
1814 my $title = "VM $vmid";
1815 $title .= " - ". $conf->{name} if $conf->{name};
1816
1817 my $port = PVE::QemuServer::spice_port($vmid);
1818
1819 my ($ticket, undef, $remote_viewer_config) =
1820 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
1821
1822 PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
1823 PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
1824
1825 return $remote_viewer_config;
1826 }});
1827
1828 __PACKAGE__->register_method({
1829 name => 'vmcmdidx',
1830 path => '{vmid}/status',
1831 method => 'GET',
1832 proxyto => 'node',
1833 description => "Directory index",
1834 permissions => {
1835 user => 'all',
1836 },
1837 parameters => {
1838 additionalProperties => 0,
1839 properties => {
1840 node => get_standard_option('pve-node'),
1841 vmid => get_standard_option('pve-vmid'),
1842 },
1843 },
1844 returns => {
1845 type => 'array',
1846 items => {
1847 type => "object",
1848 properties => {
1849 subdir => { type => 'string' },
1850 },
1851 },
1852 links => [ { rel => 'child', href => "{subdir}" } ],
1853 },
1854 code => sub {
1855 my ($param) = @_;
1856
1857 # test if VM exists
1858 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1859
1860 my $res = [
1861 { subdir => 'current' },
1862 { subdir => 'start' },
1863 { subdir => 'stop' },
1864 { subdir => 'reset' },
1865 { subdir => 'shutdown' },
1866 { subdir => 'suspend' },
1867 { subdir => 'reboot' },
1868 ];
1869
1870 return $res;
1871 }});
1872
1873 __PACKAGE__->register_method({
1874 name => 'vm_status',
1875 path => '{vmid}/status/current',
1876 method => 'GET',
1877 proxyto => 'node',
1878 protected => 1, # qemu pid files are only readable by root
1879 description => "Get virtual machine status.",
1880 permissions => {
1881 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1882 },
1883 parameters => {
1884 additionalProperties => 0,
1885 properties => {
1886 node => get_standard_option('pve-node'),
1887 vmid => get_standard_option('pve-vmid'),
1888 },
1889 },
1890 returns => {
1891 type => 'object',
1892 properties => {
1893 %$PVE::QemuServer::vmstatus_return_properties,
1894 ha => {
1895 description => "HA manager service status.",
1896 type => 'object',
1897 },
1898 spice => {
1899 description => "Qemu VGA configuration supports spice.",
1900 type => 'boolean',
1901 optional => 1,
1902 },
1903 agent => {
1904 description => "Qemu GuestAgent enabled in config.",
1905 type => 'boolean',
1906 optional => 1,
1907 },
1908 },
1909 },
1910 code => sub {
1911 my ($param) = @_;
1912
1913 # test if VM exists
1914 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1915
1916 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
1917 my $status = $vmstatus->{$param->{vmid}};
1918
1919 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
1920
1921 $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
1922 $status->{agent} = 1 if (PVE::QemuServer::parse_guest_agent($conf)->{enabled});
1923
1924 return $status;
1925 }});
1926
1927 __PACKAGE__->register_method({
1928 name => 'vm_start',
1929 path => '{vmid}/status/start',
1930 method => 'POST',
1931 protected => 1,
1932 proxyto => 'node',
1933 description => "Start virtual machine.",
1934 permissions => {
1935 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1936 },
1937 parameters => {
1938 additionalProperties => 0,
1939 properties => {
1940 node => get_standard_option('pve-node'),
1941 vmid => get_standard_option('pve-vmid',
1942 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
1943 skiplock => get_standard_option('skiplock'),
1944 stateuri => get_standard_option('pve-qm-stateuri'),
1945 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
1946 migration_type => {
1947 type => 'string',
1948 enum => ['secure', 'insecure'],
1949 description => "Migration traffic is encrypted using an SSH " .
1950 "tunnel by default. On secure, completely private networks " .
1951 "this can be disabled to increase performance.",
1952 optional => 1,
1953 },
1954 migration_network => {
1955 type => 'string', format => 'CIDR',
1956 description => "CIDR of the (sub) network that is used for migration.",
1957 optional => 1,
1958 },
1959 machine => get_standard_option('pve-qm-machine'),
1960 targetstorage => {
1961 description => "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
1962 type => 'string',
1963 optional => 1
1964 }
1965 },
1966 },
1967 returns => {
1968 type => 'string',
1969 },
1970 code => sub {
1971 my ($param) = @_;
1972
1973 my $rpcenv = PVE::RPCEnvironment::get();
1974 my $authuser = $rpcenv->get_user();
1975
1976 my $node = extract_param($param, 'node');
1977 my $vmid = extract_param($param, 'vmid');
1978
1979 my $machine = extract_param($param, 'machine');
1980
1981 my $get_root_param = sub {
1982 my $value = extract_param($param, $_[0]);
1983 raise_param_exc({ "$_[0]" => "Only root may use this option." })
1984 if $value && $authuser ne 'root@pam';
1985 return $value;
1986 };
1987
1988 my $stateuri = $get_root_param->('stateuri');
1989 my $skiplock = $get_root_param->('skiplock');
1990 my $migratedfrom = $get_root_param->('migratedfrom');
1991 my $migration_type = $get_root_param->('migration_type');
1992 my $migration_network = $get_root_param->('migration_network');
1993 my $targetstorage = $get_root_param->('targetstorage');
1994
1995 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
1996 if $targetstorage && !$migratedfrom;
1997
1998 # read spice ticket from STDIN
1999 my $spice_ticket;
2000 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
2001 if (defined(my $line = <STDIN>)) {
2002 chomp $line;
2003 $spice_ticket = $line;
2004 }
2005 }
2006
2007 PVE::Cluster::check_cfs_quorum();
2008
2009 my $storecfg = PVE::Storage::config();
2010
2011 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && $rpcenv->{type} ne 'ha') {
2012 my $hacmd = sub {
2013 my $upid = shift;
2014
2015 print "Requesting HA start for VM $vmid\n";
2016
2017 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
2018 PVE::Tools::run_command($cmd);
2019 return;
2020 };
2021
2022 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2023
2024 } else {
2025
2026 my $realcmd = sub {
2027 my $upid = shift;
2028
2029 syslog('info', "start VM $vmid: $upid\n");
2030
2031 PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
2032 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
2033 return;
2034 };
2035
2036 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2037 }
2038 }});
2039
2040 __PACKAGE__->register_method({
2041 name => 'vm_stop',
2042 path => '{vmid}/status/stop',
2043 method => 'POST',
2044 protected => 1,
2045 proxyto => 'node',
2046 description => "Stop virtual machine. The qemu process will exit immediately. This" .
2047 "is akin to pulling the power plug of a running computer and may damage the VM data",
2048 permissions => {
2049 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2050 },
2051 parameters => {
2052 additionalProperties => 0,
2053 properties => {
2054 node => get_standard_option('pve-node'),
2055 vmid => get_standard_option('pve-vmid',
2056 { completion => \&PVE::QemuServer::complete_vmid_running }),
2057 skiplock => get_standard_option('skiplock'),
2058 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
2059 timeout => {
2060 description => "Wait maximal timeout seconds.",
2061 type => 'integer',
2062 minimum => 0,
2063 optional => 1,
2064 },
2065 keepActive => {
2066 description => "Do not deactivate storage volumes.",
2067 type => 'boolean',
2068 optional => 1,
2069 default => 0,
2070 }
2071 },
2072 },
2073 returns => {
2074 type => 'string',
2075 },
2076 code => sub {
2077 my ($param) = @_;
2078
2079 my $rpcenv = PVE::RPCEnvironment::get();
2080 my $authuser = $rpcenv->get_user();
2081
2082 my $node = extract_param($param, 'node');
2083 my $vmid = extract_param($param, 'vmid');
2084
2085 my $skiplock = extract_param($param, 'skiplock');
2086 raise_param_exc({ skiplock => "Only root may use this option." })
2087 if $skiplock && $authuser ne 'root@pam';
2088
2089 my $keepActive = extract_param($param, 'keepActive');
2090 raise_param_exc({ keepActive => "Only root may use this option." })
2091 if $keepActive && $authuser ne 'root@pam';
2092
2093 my $migratedfrom = extract_param($param, 'migratedfrom');
2094 raise_param_exc({ migratedfrom => "Only root may use this option." })
2095 if $migratedfrom && $authuser ne 'root@pam';
2096
2097
2098 my $storecfg = PVE::Storage::config();
2099
2100 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
2101
2102 my $hacmd = sub {
2103 my $upid = shift;
2104
2105 print "Requesting HA stop for VM $vmid\n";
2106
2107 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped'];
2108 PVE::Tools::run_command($cmd);
2109 return;
2110 };
2111
2112 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2113
2114 } else {
2115 my $realcmd = sub {
2116 my $upid = shift;
2117
2118 syslog('info', "stop VM $vmid: $upid\n");
2119
2120 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
2121 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
2122 return;
2123 };
2124
2125 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2126 }
2127 }});
2128
2129 __PACKAGE__->register_method({
2130 name => 'vm_reset',
2131 path => '{vmid}/status/reset',
2132 method => 'POST',
2133 protected => 1,
2134 proxyto => 'node',
2135 description => "Reset virtual machine.",
2136 permissions => {
2137 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2138 },
2139 parameters => {
2140 additionalProperties => 0,
2141 properties => {
2142 node => get_standard_option('pve-node'),
2143 vmid => get_standard_option('pve-vmid',
2144 { completion => \&PVE::QemuServer::complete_vmid_running }),
2145 skiplock => get_standard_option('skiplock'),
2146 },
2147 },
2148 returns => {
2149 type => 'string',
2150 },
2151 code => sub {
2152 my ($param) = @_;
2153
2154 my $rpcenv = PVE::RPCEnvironment::get();
2155
2156 my $authuser = $rpcenv->get_user();
2157
2158 my $node = extract_param($param, 'node');
2159
2160 my $vmid = extract_param($param, 'vmid');
2161
2162 my $skiplock = extract_param($param, 'skiplock');
2163 raise_param_exc({ skiplock => "Only root may use this option." })
2164 if $skiplock && $authuser ne 'root@pam';
2165
2166 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2167
2168 my $realcmd = sub {
2169 my $upid = shift;
2170
2171 PVE::QemuServer::vm_reset($vmid, $skiplock);
2172
2173 return;
2174 };
2175
2176 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
2177 }});
2178
2179 __PACKAGE__->register_method({
2180 name => 'vm_shutdown',
2181 path => '{vmid}/status/shutdown',
2182 method => 'POST',
2183 protected => 1,
2184 proxyto => 'node',
2185 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
2186 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
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 timeout => {
2198 description => "Wait maximal timeout seconds.",
2199 type => 'integer',
2200 minimum => 0,
2201 optional => 1,
2202 },
2203 forceStop => {
2204 description => "Make sure the VM stops.",
2205 type => 'boolean',
2206 optional => 1,
2207 default => 0,
2208 },
2209 keepActive => {
2210 description => "Do not deactivate storage volumes.",
2211 type => 'boolean',
2212 optional => 1,
2213 default => 0,
2214 }
2215 },
2216 },
2217 returns => {
2218 type => 'string',
2219 },
2220 code => sub {
2221 my ($param) = @_;
2222
2223 my $rpcenv = PVE::RPCEnvironment::get();
2224 my $authuser = $rpcenv->get_user();
2225
2226 my $node = extract_param($param, 'node');
2227 my $vmid = extract_param($param, 'vmid');
2228
2229 my $skiplock = extract_param($param, 'skiplock');
2230 raise_param_exc({ skiplock => "Only root may use this option." })
2231 if $skiplock && $authuser ne 'root@pam';
2232
2233 my $keepActive = extract_param($param, 'keepActive');
2234 raise_param_exc({ keepActive => "Only root may use this option." })
2235 if $keepActive && $authuser ne 'root@pam';
2236
2237 my $storecfg = PVE::Storage::config();
2238
2239 my $shutdown = 1;
2240
2241 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2242 # otherwise, we will infer a shutdown command, but run into the timeout,
2243 # then when the vm is resumed, it will instantly shutdown
2244 #
2245 # checking the qmp status here to get feedback to the gui/cli/api
2246 # and the status query should not take too long
2247 my $qmpstatus = eval {
2248 PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
2249 };
2250 my $err = $@ if $@;
2251
2252 if (!$err && $qmpstatus->{status} eq "paused") {
2253 if ($param->{forceStop}) {
2254 warn "VM is paused - stop instead of shutdown\n";
2255 $shutdown = 0;
2256 } else {
2257 die "VM is paused - cannot shutdown\n";
2258 }
2259 }
2260
2261 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
2262
2263 my $hacmd = sub {
2264 my $upid = shift;
2265
2266 print "Requesting HA stop for VM $vmid\n";
2267
2268 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped'];
2269 PVE::Tools::run_command($cmd);
2270 return;
2271 };
2272
2273 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2274
2275 } else {
2276
2277 my $realcmd = sub {
2278 my $upid = shift;
2279
2280 syslog('info', "shutdown VM $vmid: $upid\n");
2281
2282 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2283 $shutdown, $param->{forceStop}, $keepActive);
2284 return;
2285 };
2286
2287 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2288 }
2289 }});
2290
2291 __PACKAGE__->register_method({
2292 name => 'vm_reboot',
2293 path => '{vmid}/status/reboot',
2294 method => 'POST',
2295 protected => 1,
2296 proxyto => 'node',
2297 description => "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
2298 permissions => {
2299 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2300 },
2301 parameters => {
2302 additionalProperties => 0,
2303 properties => {
2304 node => get_standard_option('pve-node'),
2305 vmid => get_standard_option('pve-vmid',
2306 { completion => \&PVE::QemuServer::complete_vmid_running }),
2307 timeout => {
2308 description => "Wait maximal timeout seconds for the shutdown.",
2309 type => 'integer',
2310 minimum => 0,
2311 optional => 1,
2312 },
2313 },
2314 },
2315 returns => {
2316 type => 'string',
2317 },
2318 code => sub {
2319 my ($param) = @_;
2320
2321 my $rpcenv = PVE::RPCEnvironment::get();
2322 my $authuser = $rpcenv->get_user();
2323
2324 my $node = extract_param($param, 'node');
2325 my $vmid = extract_param($param, 'vmid');
2326
2327 my $qmpstatus = eval {
2328 PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0);
2329 };
2330 my $err = $@ if $@;
2331
2332 if (!$err && $qmpstatus->{status} eq "paused") {
2333 die "VM is paused - cannot shutdown\n";
2334 }
2335
2336 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2337
2338 my $realcmd = sub {
2339 my $upid = shift;
2340
2341 syslog('info', "requesting reboot of VM $vmid: $upid\n");
2342 PVE::QemuServer::vm_reboot($vmid, $param->{timeout});
2343 return;
2344 };
2345
2346 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
2347 }});
2348
2349 __PACKAGE__->register_method({
2350 name => 'vm_suspend',
2351 path => '{vmid}/status/suspend',
2352 method => 'POST',
2353 protected => 1,
2354 proxyto => 'node',
2355 description => "Suspend virtual machine.",
2356 permissions => {
2357 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2358 },
2359 parameters => {
2360 additionalProperties => 0,
2361 properties => {
2362 node => get_standard_option('pve-node'),
2363 vmid => get_standard_option('pve-vmid',
2364 { completion => \&PVE::QemuServer::complete_vmid_running }),
2365 skiplock => get_standard_option('skiplock'),
2366 todisk => {
2367 type => 'boolean',
2368 default => 0,
2369 optional => 1,
2370 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
2371 },
2372 statestorage => get_standard_option('pve-storage-id', {
2373 description => "The storage for the VM state",
2374 requires => 'todisk',
2375 optional => 1,
2376 completion => \&PVE::Storage::complete_storage_enabled,
2377 }),
2378 },
2379 },
2380 returns => {
2381 type => 'string',
2382 },
2383 code => sub {
2384 my ($param) = @_;
2385
2386 my $rpcenv = PVE::RPCEnvironment::get();
2387 my $authuser = $rpcenv->get_user();
2388
2389 my $node = extract_param($param, 'node');
2390 my $vmid = extract_param($param, 'vmid');
2391
2392 my $todisk = extract_param($param, 'todisk') // 0;
2393
2394 my $statestorage = extract_param($param, 'statestorage');
2395
2396 my $skiplock = extract_param($param, 'skiplock');
2397 raise_param_exc({ skiplock => "Only root may use this option." })
2398 if $skiplock && $authuser ne 'root@pam';
2399
2400 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2401
2402 die "Cannot suspend HA managed VM to disk\n"
2403 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
2404
2405 my $realcmd = sub {
2406 my $upid = shift;
2407
2408 syslog('info', "suspend VM $vmid: $upid\n");
2409
2410 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
2411
2412 return;
2413 };
2414
2415 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
2416 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
2417 }});
2418
2419 __PACKAGE__->register_method({
2420 name => 'vm_resume',
2421 path => '{vmid}/status/resume',
2422 method => 'POST',
2423 protected => 1,
2424 proxyto => 'node',
2425 description => "Resume virtual machine.",
2426 permissions => {
2427 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2428 },
2429 parameters => {
2430 additionalProperties => 0,
2431 properties => {
2432 node => get_standard_option('pve-node'),
2433 vmid => get_standard_option('pve-vmid',
2434 { completion => \&PVE::QemuServer::complete_vmid_running }),
2435 skiplock => get_standard_option('skiplock'),
2436 nocheck => { type => 'boolean', optional => 1 },
2437
2438 },
2439 },
2440 returns => {
2441 type => 'string',
2442 },
2443 code => sub {
2444 my ($param) = @_;
2445
2446 my $rpcenv = PVE::RPCEnvironment::get();
2447
2448 my $authuser = $rpcenv->get_user();
2449
2450 my $node = extract_param($param, 'node');
2451
2452 my $vmid = extract_param($param, 'vmid');
2453
2454 my $skiplock = extract_param($param, 'skiplock');
2455 raise_param_exc({ skiplock => "Only root may use this option." })
2456 if $skiplock && $authuser ne 'root@pam';
2457
2458 my $nocheck = extract_param($param, 'nocheck');
2459
2460 my $to_disk_suspended;
2461 eval {
2462 PVE::QemuConfig->lock_config($vmid, sub {
2463 my $conf = PVE::QemuConfig->load_config($vmid);
2464 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
2465 });
2466 };
2467
2468 die "VM $vmid not running\n"
2469 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
2470
2471 my $realcmd = sub {
2472 my $upid = shift;
2473
2474 syslog('info', "resume VM $vmid: $upid\n");
2475
2476 if (!$to_disk_suspended) {
2477 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
2478 } else {
2479 my $storecfg = PVE::Storage::config();
2480 PVE::QemuServer::vm_start($storecfg, $vmid, undef, $skiplock);
2481 }
2482
2483 return;
2484 };
2485
2486 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2487 }});
2488
2489 __PACKAGE__->register_method({
2490 name => 'vm_sendkey',
2491 path => '{vmid}/sendkey',
2492 method => 'PUT',
2493 protected => 1,
2494 proxyto => 'node',
2495 description => "Send key event to virtual machine.",
2496 permissions => {
2497 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2498 },
2499 parameters => {
2500 additionalProperties => 0,
2501 properties => {
2502 node => get_standard_option('pve-node'),
2503 vmid => get_standard_option('pve-vmid',
2504 { completion => \&PVE::QemuServer::complete_vmid_running }),
2505 skiplock => get_standard_option('skiplock'),
2506 key => {
2507 description => "The key (qemu monitor encoding).",
2508 type => 'string'
2509 }
2510 },
2511 },
2512 returns => { type => 'null'},
2513 code => sub {
2514 my ($param) = @_;
2515
2516 my $rpcenv = PVE::RPCEnvironment::get();
2517
2518 my $authuser = $rpcenv->get_user();
2519
2520 my $node = extract_param($param, 'node');
2521
2522 my $vmid = extract_param($param, 'vmid');
2523
2524 my $skiplock = extract_param($param, 'skiplock');
2525 raise_param_exc({ skiplock => "Only root may use this option." })
2526 if $skiplock && $authuser ne 'root@pam';
2527
2528 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
2529
2530 return;
2531 }});
2532
2533 __PACKAGE__->register_method({
2534 name => 'vm_feature',
2535 path => '{vmid}/feature',
2536 method => 'GET',
2537 proxyto => 'node',
2538 protected => 1,
2539 description => "Check if feature for virtual machine is available.",
2540 permissions => {
2541 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2542 },
2543 parameters => {
2544 additionalProperties => 0,
2545 properties => {
2546 node => get_standard_option('pve-node'),
2547 vmid => get_standard_option('pve-vmid'),
2548 feature => {
2549 description => "Feature to check.",
2550 type => 'string',
2551 enum => [ 'snapshot', 'clone', 'copy' ],
2552 },
2553 snapname => get_standard_option('pve-snapshot-name', {
2554 optional => 1,
2555 }),
2556 },
2557 },
2558 returns => {
2559 type => "object",
2560 properties => {
2561 hasFeature => { type => 'boolean' },
2562 nodes => {
2563 type => 'array',
2564 items => { type => 'string' },
2565 }
2566 },
2567 },
2568 code => sub {
2569 my ($param) = @_;
2570
2571 my $node = extract_param($param, 'node');
2572
2573 my $vmid = extract_param($param, 'vmid');
2574
2575 my $snapname = extract_param($param, 'snapname');
2576
2577 my $feature = extract_param($param, 'feature');
2578
2579 my $running = PVE::QemuServer::check_running($vmid);
2580
2581 my $conf = PVE::QemuConfig->load_config($vmid);
2582
2583 if($snapname){
2584 my $snap = $conf->{snapshots}->{$snapname};
2585 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2586 $conf = $snap;
2587 }
2588 my $storecfg = PVE::Storage::config();
2589
2590 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
2591 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
2592
2593 return {
2594 hasFeature => $hasFeature,
2595 nodes => [ keys %$nodelist ],
2596 };
2597 }});
2598
2599 __PACKAGE__->register_method({
2600 name => 'clone_vm',
2601 path => '{vmid}/clone',
2602 method => 'POST',
2603 protected => 1,
2604 proxyto => 'node',
2605 description => "Create a copy of virtual machine/template.",
2606 permissions => {
2607 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2608 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2609 "'Datastore.AllocateSpace' on any used storage.",
2610 check =>
2611 [ 'and',
2612 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2613 [ 'or',
2614 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2615 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
2616 ],
2617 ]
2618 },
2619 parameters => {
2620 additionalProperties => 0,
2621 properties => {
2622 node => get_standard_option('pve-node'),
2623 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2624 newid => get_standard_option('pve-vmid', {
2625 completion => \&PVE::Cluster::complete_next_vmid,
2626 description => 'VMID for the clone.' }),
2627 name => {
2628 optional => 1,
2629 type => 'string', format => 'dns-name',
2630 description => "Set a name for the new VM.",
2631 },
2632 description => {
2633 optional => 1,
2634 type => 'string',
2635 description => "Description for the new VM.",
2636 },
2637 pool => {
2638 optional => 1,
2639 type => 'string', format => 'pve-poolid',
2640 description => "Add the new VM to the specified pool.",
2641 },
2642 snapname => get_standard_option('pve-snapshot-name', {
2643 optional => 1,
2644 }),
2645 storage => get_standard_option('pve-storage-id', {
2646 description => "Target storage for full clone.",
2647 optional => 1,
2648 }),
2649 'format' => {
2650 description => "Target format for file storage. Only valid for full clone.",
2651 type => 'string',
2652 optional => 1,
2653 enum => [ 'raw', 'qcow2', 'vmdk'],
2654 },
2655 full => {
2656 optional => 1,
2657 type => 'boolean',
2658 description => "Create a full copy of all disks. This is always done when " .
2659 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2660 },
2661 target => get_standard_option('pve-node', {
2662 description => "Target node. Only allowed if the original VM is on shared storage.",
2663 optional => 1,
2664 }),
2665 bwlimit => {
2666 description => "Override I/O bandwidth limit (in KiB/s).",
2667 optional => 1,
2668 type => 'integer',
2669 minimum => '0',
2670 default => 'clone limit from datacenter or storage config',
2671 },
2672 },
2673 },
2674 returns => {
2675 type => 'string',
2676 },
2677 code => sub {
2678 my ($param) = @_;
2679
2680 my $rpcenv = PVE::RPCEnvironment::get();
2681
2682 my $authuser = $rpcenv->get_user();
2683
2684 my $node = extract_param($param, 'node');
2685
2686 my $vmid = extract_param($param, 'vmid');
2687
2688 my $newid = extract_param($param, 'newid');
2689
2690 my $pool = extract_param($param, 'pool');
2691
2692 if (defined($pool)) {
2693 $rpcenv->check_pool_exist($pool);
2694 }
2695
2696 my $snapname = extract_param($param, 'snapname');
2697
2698 my $storage = extract_param($param, 'storage');
2699
2700 my $format = extract_param($param, 'format');
2701
2702 my $target = extract_param($param, 'target');
2703
2704 my $localnode = PVE::INotify::nodename();
2705
2706 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2707
2708 PVE::Cluster::check_node_exists($target) if $target;
2709
2710 my $storecfg = PVE::Storage::config();
2711
2712 if ($storage) {
2713 # check if storage is enabled on local node
2714 PVE::Storage::storage_check_enabled($storecfg, $storage);
2715 if ($target) {
2716 # check if storage is available on target node
2717 PVE::Storage::storage_check_node($storecfg, $storage, $target);
2718 # clone only works if target storage is shared
2719 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
2720 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
2721 }
2722 }
2723
2724 PVE::Cluster::check_cfs_quorum();
2725
2726 my $running = PVE::QemuServer::check_running($vmid) || 0;
2727
2728 # exclusive lock if VM is running - else shared lock is enough;
2729 my $shared_lock = $running ? 0 : 1;
2730
2731 my $clonefn = sub {
2732
2733 # do all tests after lock
2734 # we also try to do all tests before we fork the worker
2735
2736 my $conf = PVE::QemuConfig->load_config($vmid);
2737
2738 PVE::QemuConfig->check_lock($conf);
2739
2740 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
2741
2742 die "unexpected state change\n" if $verify_running != $running;
2743
2744 die "snapshot '$snapname' does not exist\n"
2745 if $snapname && !defined( $conf->{snapshots}->{$snapname});
2746
2747 my $full = extract_param($param, 'full');
2748 if (!defined($full)) {
2749 $full = !PVE::QemuConfig->is_template($conf);
2750 }
2751
2752 die "parameter 'storage' not allowed for linked clones\n"
2753 if defined($storage) && !$full;
2754
2755 die "parameter 'format' not allowed for linked clones\n"
2756 if defined($format) && !$full;
2757
2758 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
2759
2760 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2761
2762 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2763
2764 my $conffile = PVE::QemuConfig->config_file($newid);
2765
2766 die "unable to create VM $newid: config file already exists\n"
2767 if -f $conffile;
2768
2769 my $newconf = { lock => 'clone' };
2770 my $drives = {};
2771 my $fullclone = {};
2772 my $vollist = [];
2773
2774 foreach my $opt (keys %$oldconf) {
2775 my $value = $oldconf->{$opt};
2776
2777 # do not copy snapshot related info
2778 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2779 $opt eq 'vmstate' || $opt eq 'snapstate';
2780
2781 # no need to copy unused images, because VMID(owner) changes anyways
2782 next if $opt =~ m/^unused\d+$/;
2783
2784 # always change MAC! address
2785 if ($opt =~ m/^net(\d+)$/) {
2786 my $net = PVE::QemuServer::parse_net($value);
2787 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
2788 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
2789 $newconf->{$opt} = PVE::QemuServer::print_net($net);
2790 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
2791 my $drive = PVE::QemuServer::parse_drive($opt, $value);
2792 die "unable to parse drive options for '$opt'\n" if !$drive;
2793 if (PVE::QemuServer::drive_is_cdrom($drive)) {
2794 $newconf->{$opt} = $value; # simply copy configuration
2795 } else {
2796 if ($full) {
2797 die "Full clone feature is not supported for drive '$opt'\n"
2798 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
2799 $fullclone->{$opt} = 1;
2800 } else {
2801 # not full means clone instead of copy
2802 die "Linked clone feature is not supported for drive '$opt'\n"
2803 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
2804 }
2805 $drives->{$opt} = $drive;
2806 push @$vollist, $drive->{file};
2807 }
2808 } else {
2809 # copy everything else
2810 $newconf->{$opt} = $value;
2811 }
2812 }
2813
2814 # auto generate a new uuid
2815 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
2816 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
2817 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
2818
2819 # auto generate a new vmgenid if the option was set
2820 if ($newconf->{vmgenid}) {
2821 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
2822 }
2823
2824 delete $newconf->{template};
2825
2826 if ($param->{name}) {
2827 $newconf->{name} = $param->{name};
2828 } else {
2829 if ($oldconf->{name}) {
2830 $newconf->{name} = "Copy-of-$oldconf->{name}";
2831 } else {
2832 $newconf->{name} = "Copy-of-VM-$vmid";
2833 }
2834 }
2835
2836 if ($param->{description}) {
2837 $newconf->{description} = $param->{description};
2838 }
2839
2840 # create empty/temp config - this fails if VM already exists on other node
2841 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
2842
2843 my $realcmd = sub {
2844 my $upid = shift;
2845
2846 my $newvollist = [];
2847 my $jobs = {};
2848
2849 eval {
2850 local $SIG{INT} =
2851 local $SIG{TERM} =
2852 local $SIG{QUIT} =
2853 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
2854
2855 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
2856
2857 my $bwlimit = extract_param($param, 'bwlimit');
2858
2859 my $total_jobs = scalar(keys %{$drives});
2860 my $i = 1;
2861
2862 foreach my $opt (keys %$drives) {
2863 my $drive = $drives->{$opt};
2864 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2865
2866 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
2867 my $storage_list = [ $src_sid ];
2868 push @$storage_list, $storage if defined($storage);
2869 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
2870
2871 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname,
2872 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2873 $jobs, $skipcomplete, $oldconf->{agent}, $clonelimit);
2874
2875 $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive);
2876
2877 PVE::QemuConfig->write_config($newid, $newconf);
2878 $i++;
2879 }
2880
2881 delete $newconf->{lock};
2882
2883 # do not write pending changes
2884 if (my @changes = keys %{$newconf->{pending}}) {
2885 my $pending = join(',', @changes);
2886 warn "found pending changes for '$pending', discarding for clone\n";
2887 delete $newconf->{pending};
2888 }
2889
2890 PVE::QemuConfig->write_config($newid, $newconf);
2891
2892 if ($target) {
2893 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2894 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
2895 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
2896
2897 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
2898 die "Failed to move config to node '$target' - rename failed: $!\n"
2899 if !rename($conffile, $newconffile);
2900 }
2901
2902 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
2903 };
2904 if (my $err = $@) {
2905 unlink $conffile;
2906
2907 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
2908
2909 sleep 1; # some storage like rbd need to wait before release volume - really?
2910
2911 foreach my $volid (@$newvollist) {
2912 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
2913 warn $@ if $@;
2914 }
2915 die "clone failed: $err";
2916 }
2917
2918 return;
2919 };
2920
2921 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
2922
2923 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2924 };
2925
2926 return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
2927 # Aquire exclusive lock lock for $newid
2928 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
2929 });
2930
2931 }});
2932
2933 __PACKAGE__->register_method({
2934 name => 'move_vm_disk',
2935 path => '{vmid}/move_disk',
2936 method => 'POST',
2937 protected => 1,
2938 proxyto => 'node',
2939 description => "Move volume to different storage.",
2940 permissions => {
2941 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2942 check => [ 'and',
2943 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2944 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2945 ],
2946 },
2947 parameters => {
2948 additionalProperties => 0,
2949 properties => {
2950 node => get_standard_option('pve-node'),
2951 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2952 disk => {
2953 type => 'string',
2954 description => "The disk you want to move.",
2955 enum => [ PVE::QemuServer::valid_drive_names() ],
2956 },
2957 storage => get_standard_option('pve-storage-id', {
2958 description => "Target storage.",
2959 completion => \&PVE::QemuServer::complete_storage,
2960 }),
2961 'format' => {
2962 type => 'string',
2963 description => "Target Format.",
2964 enum => [ 'raw', 'qcow2', 'vmdk' ],
2965 optional => 1,
2966 },
2967 delete => {
2968 type => 'boolean',
2969 description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2970 optional => 1,
2971 default => 0,
2972 },
2973 digest => {
2974 type => 'string',
2975 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2976 maxLength => 40,
2977 optional => 1,
2978 },
2979 bwlimit => {
2980 description => "Override I/O bandwidth limit (in KiB/s).",
2981 optional => 1,
2982 type => 'integer',
2983 minimum => '0',
2984 default => 'move limit from datacenter or storage config',
2985 },
2986 },
2987 },
2988 returns => {
2989 type => 'string',
2990 description => "the task ID.",
2991 },
2992 code => sub {
2993 my ($param) = @_;
2994
2995 my $rpcenv = PVE::RPCEnvironment::get();
2996
2997 my $authuser = $rpcenv->get_user();
2998
2999 my $node = extract_param($param, 'node');
3000
3001 my $vmid = extract_param($param, 'vmid');
3002
3003 my $digest = extract_param($param, 'digest');
3004
3005 my $disk = extract_param($param, 'disk');
3006
3007 my $storeid = extract_param($param, 'storage');
3008
3009 my $format = extract_param($param, 'format');
3010
3011 my $storecfg = PVE::Storage::config();
3012
3013 my $updatefn = sub {
3014
3015 my $conf = PVE::QemuConfig->load_config($vmid);
3016
3017 PVE::QemuConfig->check_lock($conf);
3018
3019 die "checksum missmatch (file change by other user?)\n"
3020 if $digest && $digest ne $conf->{digest};
3021
3022 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3023
3024 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3025
3026 my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
3027
3028 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
3029
3030 my $oldfmt;
3031 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
3032 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3033 $oldfmt = $1;
3034 }
3035
3036 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
3037 (!$format || !$oldfmt || $oldfmt eq $format);
3038
3039 # this only checks snapshots because $disk is passed!
3040 my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid);
3041 die "you can't move a disk with snapshots and delete the source\n"
3042 if $snapshotted && $param->{delete};
3043
3044 PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
3045
3046 my $running = PVE::QemuServer::check_running($vmid);
3047
3048 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
3049
3050 my $realcmd = sub {
3051
3052 my $newvollist = [];
3053
3054 eval {
3055 local $SIG{INT} =
3056 local $SIG{TERM} =
3057 local $SIG{QUIT} =
3058 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
3059
3060 warn "moving disk with snapshots, snapshots will not be moved!\n"
3061 if $snapshotted;
3062
3063 my $bwlimit = extract_param($param, 'bwlimit');
3064 my $movelimit = PVE::Storage::get_bandwidth_limit('move', [$oldstoreid, $storeid], $bwlimit);
3065
3066 my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef,
3067 $vmid, $storeid, $format, 1, $newvollist, undef, undef, undef, $movelimit);
3068
3069 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive);
3070
3071 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
3072
3073 # convert moved disk to base if part of template
3074 PVE::QemuServer::template_create($vmid, $conf, $disk)
3075 if PVE::QemuConfig->is_template($conf);
3076
3077 PVE::QemuConfig->write_config($vmid, $conf);
3078
3079 if ($running && PVE::QemuServer::parse_guest_agent($conf)->{fstrim_cloned_disks} && PVE::QemuServer::qga_check_running($vmid)) {
3080 eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fstrim"); };
3081 }
3082
3083 eval {
3084 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
3085 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
3086 if !$running;
3087 };
3088 warn $@ if $@;
3089 };
3090 if (my $err = $@) {
3091
3092 foreach my $volid (@$newvollist) {
3093 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3094 warn $@ if $@;
3095 }
3096 die "storage migration failed: $err";
3097 }
3098
3099 if ($param->{delete}) {
3100 eval {
3101 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
3102 PVE::Storage::vdisk_free($storecfg, $old_volid);
3103 };
3104 warn $@ if $@;
3105 }
3106 };
3107
3108 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
3109 };
3110
3111 return PVE::QemuConfig->lock_config($vmid, $updatefn);
3112 }});
3113
3114 my $check_vm_disks_local = sub {
3115 my ($storecfg, $vmconf, $vmid) = @_;
3116
3117 my $local_disks = {};
3118
3119 # add some more information to the disks e.g. cdrom
3120 PVE::QemuServer::foreach_volid($vmconf, sub {
3121 my ($volid, $attr) = @_;
3122
3123 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
3124 if ($storeid) {
3125 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
3126 return if $scfg->{shared};
3127 }
3128 # The shared attr here is just a special case where the vdisk
3129 # is marked as shared manually
3130 return if $attr->{shared};
3131 return if $attr->{cdrom} and $volid eq "none";
3132
3133 if (exists $local_disks->{$volid}) {
3134 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
3135 } else {
3136 $local_disks->{$volid} = $attr;
3137 # ensure volid is present in case it's needed
3138 $local_disks->{$volid}->{volid} = $volid;
3139 }
3140 });
3141
3142 return $local_disks;
3143 };
3144
3145 __PACKAGE__->register_method({
3146 name => 'migrate_vm_precondition',
3147 path => '{vmid}/migrate',
3148 method => 'GET',
3149 protected => 1,
3150 proxyto => 'node',
3151 description => "Get preconditions for migration.",
3152 permissions => {
3153 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3154 },
3155 parameters => {
3156 additionalProperties => 0,
3157 properties => {
3158 node => get_standard_option('pve-node'),
3159 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3160 target => get_standard_option('pve-node', {
3161 description => "Target node.",
3162 completion => \&PVE::Cluster::complete_migration_target,
3163 optional => 1,
3164 }),
3165 },
3166 },
3167 returns => {
3168 type => "object",
3169 properties => {
3170 running => { type => 'boolean' },
3171 allowed_nodes => {
3172 type => 'array',
3173 optional => 1,
3174 description => "List nodes allowed for offline migration, only passed if VM is offline"
3175 },
3176 not_allowed_nodes => {
3177 type => 'object',
3178 optional => 1,
3179 description => "List not allowed nodes with additional informations, only passed if VM is offline"
3180 },
3181 local_disks => {
3182 type => 'array',
3183 description => "List local disks including CD-Rom, unsused and not referenced disks"
3184 },
3185 local_resources => {
3186 type => 'array',
3187 description => "List local resources e.g. pci, usb"
3188 }
3189 },
3190 },
3191 code => sub {
3192 my ($param) = @_;
3193
3194 my $rpcenv = PVE::RPCEnvironment::get();
3195
3196 my $authuser = $rpcenv->get_user();
3197
3198 PVE::Cluster::check_cfs_quorum();
3199
3200 my $res = {};
3201
3202 my $vmid = extract_param($param, 'vmid');
3203 my $target = extract_param($param, 'target');
3204 my $localnode = PVE::INotify::nodename();
3205
3206
3207 # test if VM exists
3208 my $vmconf = PVE::QemuConfig->load_config($vmid);
3209 my $storecfg = PVE::Storage::config();
3210
3211
3212 # try to detect errors early
3213 PVE::QemuConfig->check_lock($vmconf);
3214
3215 $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
3216
3217 # if vm is not running, return target nodes where local storage is available
3218 # for offline migration
3219 if (!$res->{running}) {
3220 $res->{allowed_nodes} = [];
3221 my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg);
3222 delete $checked_nodes->{$localnode};
3223
3224 foreach my $node (keys %$checked_nodes) {
3225 if (!defined $checked_nodes->{$node}->{unavailable_storages}) {
3226 push @{$res->{allowed_nodes}}, $node;
3227 }
3228
3229 }
3230 $res->{not_allowed_nodes} = $checked_nodes;
3231 }
3232
3233
3234 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
3235 $res->{local_disks} = [ values %$local_disks ];;
3236
3237 my $local_resources = PVE::QemuServer::check_local_resources($vmconf, 1);
3238
3239 $res->{local_resources} = $local_resources;
3240
3241 return $res;
3242
3243
3244 }});
3245
3246 __PACKAGE__->register_method({
3247 name => 'migrate_vm',
3248 path => '{vmid}/migrate',
3249 method => 'POST',
3250 protected => 1,
3251 proxyto => 'node',
3252 description => "Migrate virtual machine. Creates a new migration task.",
3253 permissions => {
3254 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
3255 },
3256 parameters => {
3257 additionalProperties => 0,
3258 properties => {
3259 node => get_standard_option('pve-node'),
3260 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3261 target => get_standard_option('pve-node', {
3262 description => "Target node.",
3263 completion => \&PVE::Cluster::complete_migration_target,
3264 }),
3265 online => {
3266 type => 'boolean',
3267 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
3268 optional => 1,
3269 },
3270 force => {
3271 type => 'boolean',
3272 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
3273 optional => 1,
3274 },
3275 migration_type => {
3276 type => 'string',
3277 enum => ['secure', 'insecure'],
3278 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
3279 optional => 1,
3280 },
3281 migration_network => {
3282 type => 'string', format => 'CIDR',
3283 description => "CIDR of the (sub) network that is used for migration.",
3284 optional => 1,
3285 },
3286 "with-local-disks" => {
3287 type => 'boolean',
3288 description => "Enable live storage migration for local disk",
3289 optional => 1,
3290 },
3291 targetstorage => get_standard_option('pve-storage-id', {
3292 description => "Default target storage.",
3293 optional => 1,
3294 completion => \&PVE::QemuServer::complete_storage,
3295 }),
3296 bwlimit => {
3297 description => "Override I/O bandwidth limit (in KiB/s).",
3298 optional => 1,
3299 type => 'integer',
3300 minimum => '0',
3301 default => 'migrate limit from datacenter or storage config',
3302 },
3303 },
3304 },
3305 returns => {
3306 type => 'string',
3307 description => "the task ID.",
3308 },
3309 code => sub {
3310 my ($param) = @_;
3311
3312 my $rpcenv = PVE::RPCEnvironment::get();
3313 my $authuser = $rpcenv->get_user();
3314
3315 my $target = extract_param($param, 'target');
3316
3317 my $localnode = PVE::INotify::nodename();
3318 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
3319
3320 PVE::Cluster::check_cfs_quorum();
3321
3322 PVE::Cluster::check_node_exists($target);
3323
3324 my $targetip = PVE::Cluster::remote_node_ip($target);
3325
3326 my $vmid = extract_param($param, 'vmid');
3327
3328 raise_param_exc({ force => "Only root may use this option." })
3329 if $param->{force} && $authuser ne 'root@pam';
3330
3331 raise_param_exc({ migration_type => "Only root may use this option." })
3332 if $param->{migration_type} && $authuser ne 'root@pam';
3333
3334 # allow root only until better network permissions are available
3335 raise_param_exc({ migration_network => "Only root may use this option." })
3336 if $param->{migration_network} && $authuser ne 'root@pam';
3337
3338 # test if VM exists
3339 my $conf = PVE::QemuConfig->load_config($vmid);
3340
3341 # try to detect errors early
3342
3343 PVE::QemuConfig->check_lock($conf);
3344
3345 if (PVE::QemuServer::check_running($vmid)) {
3346 die "can't migrate running VM without --online\n" if !$param->{online};
3347 } else {
3348 warn "VM isn't running. Doing offline migration instead\n." if $param->{online};
3349 $param->{online} = 0;
3350 }
3351
3352 raise_param_exc({ targetstorage => "Live storage migration can only be done online." })
3353 if !$param->{online} && $param->{targetstorage};
3354
3355 my $storecfg = PVE::Storage::config();
3356
3357 if( $param->{targetstorage}) {
3358 PVE::Storage::storage_check_node($storecfg, $param->{targetstorage}, $target);
3359 } else {
3360 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
3361 }
3362
3363 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3364
3365 my $hacmd = sub {
3366 my $upid = shift;
3367
3368 print "Requesting HA migration for VM $vmid to node $target\n";
3369
3370 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
3371 PVE::Tools::run_command($cmd);
3372 return;
3373 };
3374
3375 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
3376
3377 } else {
3378
3379 my $realcmd = sub {
3380 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
3381 };
3382
3383 my $worker = sub {
3384 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3385 };
3386
3387 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
3388 }
3389
3390 }});
3391
3392 __PACKAGE__->register_method({
3393 name => 'monitor',
3394 path => '{vmid}/monitor',
3395 method => 'POST',
3396 protected => 1,
3397 proxyto => 'node',
3398 description => "Execute Qemu monitor commands.",
3399 permissions => {
3400 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
3401 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
3402 },
3403 parameters => {
3404 additionalProperties => 0,
3405 properties => {
3406 node => get_standard_option('pve-node'),
3407 vmid => get_standard_option('pve-vmid'),
3408 command => {
3409 type => 'string',
3410 description => "The monitor command.",
3411 }
3412 },
3413 },
3414 returns => { type => 'string'},
3415 code => sub {
3416 my ($param) = @_;
3417
3418 my $rpcenv = PVE::RPCEnvironment::get();
3419 my $authuser = $rpcenv->get_user();
3420
3421 my $is_ro = sub {
3422 my $command = shift;
3423 return $command =~ m/^\s*info(\s+|$)/
3424 || $command =~ m/^\s*help\s*$/;
3425 };
3426
3427 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
3428 if !&$is_ro($param->{command});
3429
3430 my $vmid = $param->{vmid};
3431
3432 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
3433
3434 my $res = '';
3435 eval {
3436 $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command});
3437 };
3438 $res = "ERROR: $@" if $@;
3439
3440 return $res;
3441 }});
3442
3443 __PACKAGE__->register_method({
3444 name => 'resize_vm',
3445 path => '{vmid}/resize',
3446 method => 'PUT',
3447 protected => 1,
3448 proxyto => 'node',
3449 description => "Extend volume size.",
3450 permissions => {
3451 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3452 },
3453 parameters => {
3454 additionalProperties => 0,
3455 properties => {
3456 node => get_standard_option('pve-node'),
3457 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3458 skiplock => get_standard_option('skiplock'),
3459 disk => {
3460 type => 'string',
3461 description => "The disk you want to resize.",
3462 enum => [PVE::QemuServer::valid_drive_names()],
3463 },
3464 size => {
3465 type => 'string',
3466 pattern => '\+?\d+(\.\d+)?[KMGT]?',
3467 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.",
3468 },
3469 digest => {
3470 type => 'string',
3471 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3472 maxLength => 40,
3473 optional => 1,
3474 },
3475 },
3476 },
3477 returns => { type => 'null'},
3478 code => sub {
3479 my ($param) = @_;
3480
3481 my $rpcenv = PVE::RPCEnvironment::get();
3482
3483 my $authuser = $rpcenv->get_user();
3484
3485 my $node = extract_param($param, 'node');
3486
3487 my $vmid = extract_param($param, 'vmid');
3488
3489 my $digest = extract_param($param, 'digest');
3490
3491 my $disk = extract_param($param, 'disk');
3492
3493 my $sizestr = extract_param($param, 'size');
3494
3495 my $skiplock = extract_param($param, 'skiplock');
3496 raise_param_exc({ skiplock => "Only root may use this option." })
3497 if $skiplock && $authuser ne 'root@pam';
3498
3499 my $storecfg = PVE::Storage::config();
3500
3501 my $updatefn = sub {
3502
3503 my $conf = PVE::QemuConfig->load_config($vmid);
3504
3505 die "checksum missmatch (file change by other user?)\n"
3506 if $digest && $digest ne $conf->{digest};
3507 PVE::QemuConfig->check_lock($conf) if !$skiplock;
3508
3509 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3510
3511 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3512
3513 my (undef, undef, undef, undef, undef, undef, $format) =
3514 PVE::Storage::parse_volname($storecfg, $drive->{file});
3515
3516 die "can't resize volume: $disk if snapshot exists\n"
3517 if %{$conf->{snapshots}} && $format eq 'qcow2';
3518
3519 my $volid = $drive->{file};
3520
3521 die "disk '$disk' has no associated volume\n" if !$volid;
3522
3523 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
3524
3525 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
3526
3527 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3528
3529 PVE::Storage::activate_volumes($storecfg, [$volid]);
3530 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
3531
3532 die "Could not determine current size of volume '$volid'\n" if !defined($size);
3533
3534 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3535 my ($ext, $newsize, $unit) = ($1, $2, $4);
3536 if ($unit) {
3537 if ($unit eq 'K') {
3538 $newsize = $newsize * 1024;
3539 } elsif ($unit eq 'M') {
3540 $newsize = $newsize * 1024 * 1024;
3541 } elsif ($unit eq 'G') {
3542 $newsize = $newsize * 1024 * 1024 * 1024;
3543 } elsif ($unit eq 'T') {
3544 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3545 }
3546 }
3547 $newsize += $size if $ext;
3548 $newsize = int($newsize);
3549
3550 die "shrinking disks is not supported\n" if $newsize < $size;
3551
3552 return if $size == $newsize;
3553
3554 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3555
3556 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3557
3558 $drive->{size} = $newsize;
3559 $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive);
3560
3561 PVE::QemuConfig->write_config($vmid, $conf);
3562 };
3563
3564 PVE::QemuConfig->lock_config($vmid, $updatefn);
3565 return undef;
3566 }});
3567
3568 __PACKAGE__->register_method({
3569 name => 'snapshot_list',
3570 path => '{vmid}/snapshot',
3571 method => 'GET',
3572 description => "List all snapshots.",
3573 permissions => {
3574 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3575 },
3576 proxyto => 'node',
3577 protected => 1, # qemu pid files are only readable by root
3578 parameters => {
3579 additionalProperties => 0,
3580 properties => {
3581 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3582 node => get_standard_option('pve-node'),
3583 },
3584 },
3585 returns => {
3586 type => 'array',
3587 items => {
3588 type => "object",
3589 properties => {
3590 name => {
3591 description => "Snapshot identifier. Value 'current' identifies the current VM.",
3592 type => 'string',
3593 },
3594 vmstate => {
3595 description => "Snapshot includes RAM.",
3596 type => 'boolean',
3597 optional => 1,
3598 },
3599 description => {
3600 description => "Snapshot description.",
3601 type => 'string',
3602 },
3603 snaptime => {
3604 description => "Snapshot creation time",
3605 type => 'integer',
3606 renderer => 'timestamp',
3607 optional => 1,
3608 },
3609 parent => {
3610 description => "Parent snapshot identifier.",
3611 type => 'string',
3612 optional => 1,
3613 },
3614 },
3615 },
3616 links => [ { rel => 'child', href => "{name}" } ],
3617 },
3618 code => sub {
3619 my ($param) = @_;
3620
3621 my $vmid = $param->{vmid};
3622
3623 my $conf = PVE::QemuConfig->load_config($vmid);
3624 my $snaphash = $conf->{snapshots} || {};
3625
3626 my $res = [];
3627
3628 foreach my $name (keys %$snaphash) {
3629 my $d = $snaphash->{$name};
3630 my $item = {
3631 name => $name,
3632 snaptime => $d->{snaptime} || 0,
3633 vmstate => $d->{vmstate} ? 1 : 0,
3634 description => $d->{description} || '',
3635 };
3636 $item->{parent} = $d->{parent} if $d->{parent};
3637 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
3638 push @$res, $item;
3639 }
3640
3641 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
3642 my $current = {
3643 name => 'current',
3644 digest => $conf->{digest},
3645 running => $running,
3646 description => "You are here!",
3647 };
3648 $current->{parent} = $conf->{parent} if $conf->{parent};
3649
3650 push @$res, $current;
3651
3652 return $res;
3653 }});
3654
3655 __PACKAGE__->register_method({
3656 name => 'snapshot',
3657 path => '{vmid}/snapshot',
3658 method => 'POST',
3659 protected => 1,
3660 proxyto => 'node',
3661 description => "Snapshot a VM.",
3662 permissions => {
3663 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3664 },
3665 parameters => {
3666 additionalProperties => 0,
3667 properties => {
3668 node => get_standard_option('pve-node'),
3669 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3670 snapname => get_standard_option('pve-snapshot-name'),
3671 vmstate => {
3672 optional => 1,
3673 type => 'boolean',
3674 description => "Save the vmstate",
3675 },
3676 description => {
3677 optional => 1,
3678 type => 'string',
3679 description => "A textual description or comment.",
3680 },
3681 },
3682 },
3683 returns => {
3684 type => 'string',
3685 description => "the task ID.",
3686 },
3687 code => sub {
3688 my ($param) = @_;
3689
3690 my $rpcenv = PVE::RPCEnvironment::get();
3691
3692 my $authuser = $rpcenv->get_user();
3693
3694 my $node = extract_param($param, 'node');
3695
3696 my $vmid = extract_param($param, 'vmid');
3697
3698 my $snapname = extract_param($param, 'snapname');
3699
3700 die "unable to use snapshot name 'current' (reserved name)\n"
3701 if $snapname eq 'current';
3702
3703 my $realcmd = sub {
3704 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
3705 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
3706 $param->{description});
3707 };
3708
3709 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3710 }});
3711
3712 __PACKAGE__->register_method({
3713 name => 'snapshot_cmd_idx',
3714 path => '{vmid}/snapshot/{snapname}',
3715 description => '',
3716 method => 'GET',
3717 permissions => {
3718 user => 'all',
3719 },
3720 parameters => {
3721 additionalProperties => 0,
3722 properties => {
3723 vmid => get_standard_option('pve-vmid'),
3724 node => get_standard_option('pve-node'),
3725 snapname => get_standard_option('pve-snapshot-name'),
3726 },
3727 },
3728 returns => {
3729 type => 'array',
3730 items => {
3731 type => "object",
3732 properties => {},
3733 },
3734 links => [ { rel => 'child', href => "{cmd}" } ],
3735 },
3736 code => sub {
3737 my ($param) = @_;
3738
3739 my $res = [];
3740
3741 push @$res, { cmd => 'rollback' };
3742 push @$res, { cmd => 'config' };
3743
3744 return $res;
3745 }});
3746
3747 __PACKAGE__->register_method({
3748 name => 'update_snapshot_config',
3749 path => '{vmid}/snapshot/{snapname}/config',
3750 method => 'PUT',
3751 protected => 1,
3752 proxyto => 'node',
3753 description => "Update snapshot metadata.",
3754 permissions => {
3755 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3756 },
3757 parameters => {
3758 additionalProperties => 0,
3759 properties => {
3760 node => get_standard_option('pve-node'),
3761 vmid => get_standard_option('pve-vmid'),
3762 snapname => get_standard_option('pve-snapshot-name'),
3763 description => {
3764 optional => 1,
3765 type => 'string',
3766 description => "A textual description or comment.",
3767 },
3768 },
3769 },
3770 returns => { type => 'null' },
3771 code => sub {
3772 my ($param) = @_;
3773
3774 my $rpcenv = PVE::RPCEnvironment::get();
3775
3776 my $authuser = $rpcenv->get_user();
3777
3778 my $vmid = extract_param($param, 'vmid');
3779
3780 my $snapname = extract_param($param, 'snapname');
3781
3782 return undef if !defined($param->{description});
3783
3784 my $updatefn = sub {
3785
3786 my $conf = PVE::QemuConfig->load_config($vmid);
3787
3788 PVE::QemuConfig->check_lock($conf);
3789
3790 my $snap = $conf->{snapshots}->{$snapname};
3791
3792 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3793
3794 $snap->{description} = $param->{description} if defined($param->{description});
3795
3796 PVE::QemuConfig->write_config($vmid, $conf);
3797 };
3798
3799 PVE::QemuConfig->lock_config($vmid, $updatefn);
3800
3801 return undef;
3802 }});
3803
3804 __PACKAGE__->register_method({
3805 name => 'get_snapshot_config',
3806 path => '{vmid}/snapshot/{snapname}/config',
3807 method => 'GET',
3808 proxyto => 'node',
3809 description => "Get snapshot configuration",
3810 permissions => {
3811 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
3812 },
3813 parameters => {
3814 additionalProperties => 0,
3815 properties => {
3816 node => get_standard_option('pve-node'),
3817 vmid => get_standard_option('pve-vmid'),
3818 snapname => get_standard_option('pve-snapshot-name'),
3819 },
3820 },
3821 returns => { type => "object" },
3822 code => sub {
3823 my ($param) = @_;
3824
3825 my $rpcenv = PVE::RPCEnvironment::get();
3826
3827 my $authuser = $rpcenv->get_user();
3828
3829 my $vmid = extract_param($param, 'vmid');
3830
3831 my $snapname = extract_param($param, 'snapname');
3832
3833 my $conf = PVE::QemuConfig->load_config($vmid);
3834
3835 my $snap = $conf->{snapshots}->{$snapname};
3836
3837 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3838
3839 return $snap;
3840 }});
3841
3842 __PACKAGE__->register_method({
3843 name => 'rollback',
3844 path => '{vmid}/snapshot/{snapname}/rollback',
3845 method => 'POST',
3846 protected => 1,
3847 proxyto => 'node',
3848 description => "Rollback VM state to specified snapshot.",
3849 permissions => {
3850 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
3851 },
3852 parameters => {
3853 additionalProperties => 0,
3854 properties => {
3855 node => get_standard_option('pve-node'),
3856 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3857 snapname => get_standard_option('pve-snapshot-name'),
3858 },
3859 },
3860 returns => {
3861 type => 'string',
3862 description => "the task ID.",
3863 },
3864 code => sub {
3865 my ($param) = @_;
3866
3867 my $rpcenv = PVE::RPCEnvironment::get();
3868
3869 my $authuser = $rpcenv->get_user();
3870
3871 my $node = extract_param($param, 'node');
3872
3873 my $vmid = extract_param($param, 'vmid');
3874
3875 my $snapname = extract_param($param, 'snapname');
3876
3877 my $realcmd = sub {
3878 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3879 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
3880 };
3881
3882 my $worker = sub {
3883 # hold migration lock, this makes sure that nobody create replication snapshots
3884 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
3885 };
3886
3887 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
3888 }});
3889
3890 __PACKAGE__->register_method({
3891 name => 'delsnapshot',
3892 path => '{vmid}/snapshot/{snapname}',
3893 method => 'DELETE',
3894 protected => 1,
3895 proxyto => 'node',
3896 description => "Delete a VM snapshot.",
3897 permissions => {
3898 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3899 },
3900 parameters => {
3901 additionalProperties => 0,
3902 properties => {
3903 node => get_standard_option('pve-node'),
3904 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
3905 snapname => get_standard_option('pve-snapshot-name'),
3906 force => {
3907 optional => 1,
3908 type => 'boolean',
3909 description => "For removal from config file, even if removing disk snapshots fails.",
3910 },
3911 },
3912 },
3913 returns => {
3914 type => 'string',
3915 description => "the task ID.",
3916 },
3917 code => sub {
3918 my ($param) = @_;
3919
3920 my $rpcenv = PVE::RPCEnvironment::get();
3921
3922 my $authuser = $rpcenv->get_user();
3923
3924 my $node = extract_param($param, 'node');
3925
3926 my $vmid = extract_param($param, 'vmid');
3927
3928 my $snapname = extract_param($param, 'snapname');
3929
3930 my $realcmd = sub {
3931 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
3932 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
3933 };
3934
3935 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3936 }});
3937
3938 __PACKAGE__->register_method({
3939 name => 'template',
3940 path => '{vmid}/template',
3941 method => 'POST',
3942 protected => 1,
3943 proxyto => 'node',
3944 description => "Create a Template.",
3945 permissions => {
3946 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
3947 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3948 },
3949 parameters => {
3950 additionalProperties => 0,
3951 properties => {
3952 node => get_standard_option('pve-node'),
3953 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3954 disk => {
3955 optional => 1,
3956 type => 'string',
3957 description => "If you want to convert only 1 disk to base image.",
3958 enum => [PVE::QemuServer::valid_drive_names()],
3959 },
3960
3961 },
3962 },
3963 returns => { type => 'null'},
3964 code => sub {
3965 my ($param) = @_;
3966
3967 my $rpcenv = PVE::RPCEnvironment::get();
3968
3969 my $authuser = $rpcenv->get_user();
3970
3971 my $node = extract_param($param, 'node');
3972
3973 my $vmid = extract_param($param, 'vmid');
3974
3975 my $disk = extract_param($param, 'disk');
3976
3977 my $updatefn = sub {
3978
3979 my $conf = PVE::QemuConfig->load_config($vmid);
3980
3981 PVE::QemuConfig->check_lock($conf);
3982
3983 die "unable to create template, because VM contains snapshots\n"
3984 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
3985
3986 die "you can't convert a template to a template\n"
3987 if PVE::QemuConfig->is_template($conf) && !$disk;
3988
3989 die "you can't convert a VM to template if VM is running\n"
3990 if PVE::QemuServer::check_running($vmid);
3991
3992 my $realcmd = sub {
3993 PVE::QemuServer::template_create($vmid, $conf, $disk);
3994 };
3995
3996 $conf->{template} = 1;
3997 PVE::QemuConfig->write_config($vmid, $conf);
3998
3999 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
4000 };
4001
4002 PVE::QemuConfig->lock_config($vmid, $updatefn);
4003 return undef;
4004 }});
4005
4006 __PACKAGE__->register_method({
4007 name => 'cloudinit_generated_config_dump',
4008 path => '{vmid}/cloudinit/dump',
4009 method => 'GET',
4010 proxyto => 'node',
4011 description => "Get automatically generated cloudinit config.",
4012 permissions => {
4013 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4014 },
4015 parameters => {
4016 additionalProperties => 0,
4017 properties => {
4018 node => get_standard_option('pve-node'),
4019 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4020 type => {
4021 description => 'Config type.',
4022 type => 'string',
4023 enum => ['user', 'network', 'meta'],
4024 },
4025 },
4026 },
4027 returns => {
4028 type => 'string',
4029 },
4030 code => sub {
4031 my ($param) = @_;
4032
4033 my $conf = PVE::QemuConfig->load_config($param->{vmid});
4034
4035 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
4036 }});
4037
4038 1;