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