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