1 package PVE
::API2
::Qemu
;
11 use PVE
::Cluster qw
(cfs_read_file cfs_write_file
);;
13 use PVE
::Tools
qw(extract_param);
14 use PVE
::Exception
qw(raise raise_param_exc raise_perm_exc);
16 use PVE
::JSONSchema
qw(get_standard_option);
21 use PVE
::RPCEnvironment
;
22 use PVE
::AccessControl
;
26 use PVE
::API2
::Firewall
::VM
;
29 if (!$ENV{PVE_GENERATING_DOCS
}) {
30 require PVE
::HA
::Env
::PVE2
;
31 import PVE
::HA
::Env
::PVE2
;
32 require PVE
::HA
::Config
;
33 import PVE
::HA
::Config
;
37 use Data
::Dumper
; # fixme: remove
39 use base
qw(PVE::RESTHandler);
41 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.";
43 my $resolve_cdrom_alias = sub {
46 if (my $value = $param->{cdrom
}) {
47 $value .= ",media=cdrom" if $value !~ m/media=/;
48 $param->{ide2
} = $value;
49 delete $param->{cdrom
};
53 my $check_storage_access = sub {
54 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
56 PVE
::QemuServer
::foreach_drive
($settings, sub {
57 my ($ds, $drive) = @_;
59 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
61 my $volid = $drive->{file
};
63 if (!$volid || $volid eq 'none') {
65 } elsif ($isCDROM && ($volid eq 'cdrom')) {
66 $rpcenv->check($authuser, "/", ['Sys.Console']);
67 } elsif (!$isCDROM && ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/)) {
68 my ($storeid, $size) = ($2 || $default_storage, $3);
69 die "no storage ID specified (and no default storage)\n" if !$storeid;
70 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
72 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
77 my $check_storage_access_clone = sub {
78 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
82 PVE
::QemuServer
::foreach_drive
($conf, sub {
83 my ($ds, $drive) = @_;
85 my $isCDROM = PVE
::QemuServer
::drive_is_cdrom
($drive);
87 my $volid = $drive->{file
};
89 return if !$volid || $volid eq 'none';
92 if ($volid eq 'cdrom') {
93 $rpcenv->check($authuser, "/", ['Sys.Console']);
95 # we simply allow access
96 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
97 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
98 $sharedvm = 0 if !$scfg->{shared
};
102 my ($sid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
103 my $scfg = PVE
::Storage
::storage_config
($storecfg, $sid);
104 $sharedvm = 0 if !$scfg->{shared
};
106 $sid = $storage if $storage;
107 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
114 # Note: $pool is only needed when creating a VM, because pool permissions
115 # are automatically inherited if VM already exists inside a pool.
116 my $create_disks = sub {
117 my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
124 my ($ds, $disk) = @_;
126 my $volid = $disk->{file
};
128 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
129 delete $disk->{size
};
130 $res->{$ds} = PVE
::QemuServer
::print_drive
($vmid, $disk);
131 } elsif ($volid =~ m!^(([^/:\s]+):)?(\d+(\.\d+)?)$!) {
132 my ($storeid, $size) = ($2 || $default_storage, $3);
133 die "no storage ID specified (and no default storage)\n" if !$storeid;
134 my $defformat = PVE
::Storage
::storage_default_format
($storecfg, $storeid);
135 my $fmt = $disk->{format
} || $defformat;
138 if ($ds eq 'efidisk0') {
140 my $ovmfvars = '/usr/share/kvm/OVMF_VARS-pure-efi.fd';
141 die "uefi vars image not found\n" if ! -f
$ovmfvars;
142 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid,
144 $disk->{file
} = $volid;
145 $disk->{size
} = 128*1024;
146 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
147 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
148 my $qemufmt = PVE
::QemuServer
::qemu_img_format
($scfg, $volname);
149 my $path = PVE
::Storage
::path
($storecfg, $volid);
150 my $efidiskcmd = ['/usr/bin/qemu-img', 'convert', '-n', '-f', 'raw', '-O', $qemufmt];
151 push @$efidiskcmd, $ovmfvars;
152 push @$efidiskcmd, $path;
154 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
156 eval { PVE
::Tools
::run_command
($efidiskcmd); };
158 die "Copying of EFI Vars image failed: $err" if $err;
160 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid,
161 $fmt, undef, $size*1024*1024);
162 $disk->{file
} = $volid;
163 $disk->{size
} = $size*1024*1024*1024;
165 push @$vollist, $volid;
166 delete $disk->{format
}; # no longer needed
167 $res->{$ds} = PVE
::QemuServer
::print_drive
($vmid, $disk);
170 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
172 my $volid_is_new = 1;
175 my $olddrive = PVE
::QemuServer
::parse_drive
($ds, $conf->{$ds});
176 $volid_is_new = undef if $olddrive->{file
} && $olddrive->{file
} eq $volid;
181 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
183 PVE
::Storage
::activate_volumes
($storecfg, [ $volid ]) if $storeid;
185 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid);
187 die "volume $volid does not exists\n" if !$size;
189 $disk->{size
} = $size;
192 $res->{$ds} = PVE
::QemuServer
::print_drive
($vmid, $disk);
196 eval { PVE
::QemuServer
::foreach_drive
($settings, $code); };
198 # free allocated images on error
200 syslog
('err', "VM $vmid creating disks failed");
201 foreach my $volid (@$vollist) {
202 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
208 # modify vm config if everything went well
209 foreach my $ds (keys %$res) {
210 $conf->{$ds} = $res->{$ds};
227 my $memoryoptions = {
233 my $hwtypeoptions = {
245 my $generaloptions = {
252 'migrate_downtime' => 1,
253 'migrate_speed' => 1,
265 my $vmpoweroptions = {
274 my $check_vm_modify_config_perm = sub {
275 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
277 return 1 if $authuser eq 'root@pam';
279 foreach my $opt (@$key_list) {
280 # disk checks need to be done somewhere else
281 next if PVE
::QemuServer
::is_valid_drivename
($opt);
282 next if $opt eq 'cdrom';
283 next if $opt =~ m/^unused\d+$/;
285 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
286 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
287 } elsif ($memoryoptions->{$opt}) {
288 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
289 } elsif ($hwtypeoptions->{$opt}) {
290 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
291 } elsif ($generaloptions->{$opt}) {
292 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
293 # special case for startup since it changes host behaviour
294 if ($opt eq 'startup') {
295 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
297 } elsif ($vmpoweroptions->{$opt}) {
298 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
299 } elsif ($diskoptions->{$opt}) {
300 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
301 } elsif ($opt =~ m/^net\d+$/) {
302 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
304 # catches usb\d+, hostpci\d+, args, lock, etc.
305 # new options will be checked here
306 die "only root can set '$opt' config\n";
313 __PACKAGE__-
>register_method({
317 description
=> "Virtual machine index (per node).",
319 description
=> "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
323 protected
=> 1, # qemu pid files are only readable by root
325 additionalProperties
=> 0,
327 node
=> get_standard_option
('pve-node'),
331 description
=> "Determine the full status of active VMs.",
341 links
=> [ { rel
=> 'child', href
=> "{vmid}" } ],
346 my $rpcenv = PVE
::RPCEnvironment
::get
();
347 my $authuser = $rpcenv->get_user();
349 my $vmstatus = PVE
::QemuServer
::vmstatus
(undef, $param->{full
});
352 foreach my $vmid (keys %$vmstatus) {
353 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
355 my $data = $vmstatus->{$vmid};
356 $data->{vmid
} = int($vmid);
365 __PACKAGE__-
>register_method({
369 description
=> "Create or restore a virtual machine.",
371 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
372 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
373 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
374 user
=> 'all', # check inside
379 additionalProperties
=> 0,
380 properties
=> PVE
::QemuServer
::json_config_properties
(
382 node
=> get_standard_option
('pve-node'),
383 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::Cluster
::complete_next_vmid
}),
385 description
=> "The backup file.",
389 completion
=> \
&PVE
::QemuServer
::complete_backup_archives
,
391 storage
=> get_standard_option
('pve-storage-id', {
392 description
=> "Default storage.",
394 completion
=> \
&PVE
::QemuServer
::complete_storage
,
399 description
=> "Allow to overwrite existing VM.",
400 requires
=> 'archive',
405 description
=> "Assign a unique random ethernet address.",
406 requires
=> 'archive',
410 type
=> 'string', format
=> 'pve-poolid',
411 description
=> "Add the VM to the specified pool.",
421 my $rpcenv = PVE
::RPCEnvironment
::get
();
423 my $authuser = $rpcenv->get_user();
425 my $node = extract_param
($param, 'node');
427 my $vmid = extract_param
($param, 'vmid');
429 my $archive = extract_param
($param, 'archive');
431 my $storage = extract_param
($param, 'storage');
433 my $force = extract_param
($param, 'force');
435 my $unique = extract_param
($param, 'unique');
437 my $pool = extract_param
($param, 'pool');
439 my $filename = PVE
::QemuConfig-
>config_file($vmid);
441 my $storecfg = PVE
::Storage
::config
();
443 PVE
::Cluster
::check_cfs_quorum
();
445 if (defined($pool)) {
446 $rpcenv->check_pool_exist($pool);
449 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
450 if defined($storage);
452 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
454 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
456 } elsif ($archive && $force && (-f
$filename) &&
457 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
458 # OK: user has VM.Backup permissions, and want to restore an existing VM
464 &$resolve_cdrom_alias($param);
466 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
468 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
470 foreach my $opt (keys %$param) {
471 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
472 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt});
473 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
475 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
476 $param->{$opt} = PVE
::QemuServer
::print_drive
($vmid, $drive);
480 PVE
::QemuServer
::add_random_macs
($param);
482 my $keystr = join(' ', keys %$param);
483 raise_param_exc
({ archive
=> "option conflicts with other options ($keystr)"}) if $keystr;
485 if ($archive eq '-') {
486 die "pipe requires cli environment\n"
487 if $rpcenv->{type
} ne 'cli';
489 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $archive);
490 $archive = PVE
::Storage
::abs_filesystem_path
($storecfg, $archive);
494 my $restorefn = sub {
495 my $vmlist = PVE
::Cluster
::get_vmlist
();
496 if ($vmlist->{ids
}->{$vmid}) {
497 my $current_node = $vmlist->{ids
}->{$vmid}->{node
};
498 if ($current_node eq $node) {
499 my $conf = PVE
::QemuConfig-
>load_config($vmid);
501 PVE
::QemuConfig-
>check_protection($conf, "unable to restore VM $vmid");
503 die "unable to restore vm $vmid - config file already exists\n"
506 die "unable to restore vm $vmid - vm is running\n"
507 if PVE
::QemuServer
::check_running
($vmid);
509 die "unable to restore vm $vmid - vm is a template\n"
510 if PVE
::QemuConfig-
>is_template($conf);
513 die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n";
518 PVE
::QemuServer
::restore_archive
($archive, $vmid, $authuser, {
521 unique
=> $unique });
523 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
526 return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd);
532 PVE
::Cluster
::check_vmid_unused
($vmid);
542 $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage);
544 # try to be smart about bootdisk
545 my @disks = PVE
::QemuServer
::valid_drive_names
();
547 foreach my $ds (reverse @disks) {
548 next if !$conf->{$ds};
549 my $disk = PVE
::QemuServer
::parse_drive
($ds, $conf->{$ds});
550 next if PVE
::QemuServer
::drive_is_cdrom
($disk);
554 if (!$conf->{bootdisk
} && $firstdisk) {
555 $conf->{bootdisk
} = $firstdisk;
558 # auto generate uuid if user did not specify smbios1 option
559 if (!$conf->{smbios1
}) {
560 my ($uuid, $uuid_str);
561 UUID
::generate
($uuid);
562 UUID
::unparse
($uuid, $uuid_str);
563 $conf->{smbios1
} = "uuid=$uuid_str";
566 PVE
::QemuConfig-
>write_config($vmid, $conf);
572 foreach my $volid (@$vollist) {
573 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
576 die "create failed - $err";
579 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
582 return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd);
585 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $archive ?
$restorefn : $createfn);
588 __PACKAGE__-
>register_method({
593 description
=> "Directory index",
598 additionalProperties
=> 0,
600 node
=> get_standard_option
('pve-node'),
601 vmid
=> get_standard_option
('pve-vmid'),
609 subdir
=> { type
=> 'string' },
612 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
618 { subdir
=> 'config' },
619 { subdir
=> 'pending' },
620 { subdir
=> 'status' },
621 { subdir
=> 'unlink' },
622 { subdir
=> 'vncproxy' },
623 { subdir
=> 'migrate' },
624 { subdir
=> 'resize' },
625 { subdir
=> 'move' },
627 { subdir
=> 'rrddata' },
628 { subdir
=> 'monitor' },
629 { subdir
=> 'agent' },
630 { subdir
=> 'snapshot' },
631 { subdir
=> 'spiceproxy' },
632 { subdir
=> 'sendkey' },
633 { subdir
=> 'firewall' },
639 __PACKAGE__-
>register_method ({
640 subclass
=> "PVE::API2::Firewall::VM",
641 path
=> '{vmid}/firewall',
644 __PACKAGE__-
>register_method({
646 path
=> '{vmid}/rrd',
648 protected
=> 1, # fixme: can we avoid that?
650 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
652 description
=> "Read VM RRD statistics (returns PNG)",
654 additionalProperties
=> 0,
656 node
=> get_standard_option
('pve-node'),
657 vmid
=> get_standard_option
('pve-vmid'),
659 description
=> "Specify the time frame you are interested in.",
661 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
664 description
=> "The list of datasources you want to display.",
665 type
=> 'string', format
=> 'pve-configid-list',
668 description
=> "The RRD consolidation function",
670 enum
=> [ 'AVERAGE', 'MAX' ],
678 filename
=> { type
=> 'string' },
684 return PVE
::Cluster
::create_rrd_graph
(
685 "pve2-vm/$param->{vmid}", $param->{timeframe
},
686 $param->{ds
}, $param->{cf
});
690 __PACKAGE__-
>register_method({
692 path
=> '{vmid}/rrddata',
694 protected
=> 1, # fixme: can we avoid that?
696 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
698 description
=> "Read VM RRD statistics",
700 additionalProperties
=> 0,
702 node
=> get_standard_option
('pve-node'),
703 vmid
=> get_standard_option
('pve-vmid'),
705 description
=> "Specify the time frame you are interested in.",
707 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
710 description
=> "The RRD consolidation function",
712 enum
=> [ 'AVERAGE', 'MAX' ],
727 return PVE
::Cluster
::create_rrd_data
(
728 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
732 __PACKAGE__-
>register_method({
734 path
=> '{vmid}/config',
737 description
=> "Get current virtual machine configuration. This does not include pending configuration changes (see 'pending' API).",
739 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
742 additionalProperties
=> 0,
744 node
=> get_standard_option
('pve-node'),
745 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
747 description
=> "Get current values (instead of pending values).",
759 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
766 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
768 delete $conf->{snapshots
};
770 if (!$param->{current
}) {
771 foreach my $opt (keys %{$conf->{pending
}}) {
772 next if $opt eq 'delete';
773 my $value = $conf->{pending
}->{$opt};
774 next if ref($value); # just to be sure
775 $conf->{$opt} = $value;
777 my $pending_delete_hash = PVE
::QemuServer
::split_flagged_list
($conf->{pending
}->{delete});
778 foreach my $opt (keys %$pending_delete_hash) {
779 delete $conf->{$opt} if $conf->{$opt};
783 delete $conf->{pending
};
788 __PACKAGE__-
>register_method({
789 name
=> 'vm_pending',
790 path
=> '{vmid}/pending',
793 description
=> "Get virtual machine configuration, including pending changes.",
795 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
798 additionalProperties
=> 0,
800 node
=> get_standard_option
('pve-node'),
801 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
810 description
=> "Configuration option name.",
814 description
=> "Current value.",
819 description
=> "Pending value.",
824 description
=> "Indicates a pending delete request if present and not 0. " .
825 "The value 2 indicates a force-delete request.",
837 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
839 my $pending_delete_hash = PVE
::QemuServer
::split_flagged_list
($conf->{pending
}->{delete});
843 foreach my $opt (keys %$conf) {
844 next if ref($conf->{$opt});
845 my $item = { key
=> $opt };
846 $item->{value
} = $conf->{$opt} if defined($conf->{$opt});
847 $item->{pending
} = $conf->{pending
}->{$opt} if defined($conf->{pending
}->{$opt});
848 $item->{delete} = ($pending_delete_hash->{$opt} ?
2 : 1) if exists $pending_delete_hash->{$opt};
852 foreach my $opt (keys %{$conf->{pending
}}) {
853 next if $opt eq 'delete';
854 next if ref($conf->{pending
}->{$opt}); # just to be sure
855 next if defined($conf->{$opt});
856 my $item = { key
=> $opt };
857 $item->{pending
} = $conf->{pending
}->{$opt};
861 while (my ($opt, $force) = each %$pending_delete_hash) {
862 next if $conf->{pending
}->{$opt}; # just to be sure
863 next if $conf->{$opt};
864 my $item = { key
=> $opt, delete => ($force ?
2 : 1)};
871 # POST/PUT {vmid}/config implementation
873 # The original API used PUT (idempotent) an we assumed that all operations
874 # are fast. But it turned out that almost any configuration change can
875 # involve hot-plug actions, or disk alloc/free. Such actions can take long
876 # time to complete and have side effects (not idempotent).
878 # The new implementation uses POST and forks a worker process. We added
879 # a new option 'background_delay'. If specified we wait up to
880 # 'background_delay' second for the worker task to complete. It returns null
881 # if the task is finished within that time, else we return the UPID.
883 my $update_vm_api = sub {
884 my ($param, $sync) = @_;
886 my $rpcenv = PVE
::RPCEnvironment
::get
();
888 my $authuser = $rpcenv->get_user();
890 my $node = extract_param
($param, 'node');
892 my $vmid = extract_param
($param, 'vmid');
894 my $digest = extract_param
($param, 'digest');
896 my $background_delay = extract_param
($param, 'background_delay');
898 my @paramarr = (); # used for log message
899 foreach my $key (keys %$param) {
900 push @paramarr, "-$key", $param->{$key};
903 my $skiplock = extract_param
($param, 'skiplock');
904 raise_param_exc
({ skiplock
=> "Only root may use this option." })
905 if $skiplock && $authuser ne 'root@pam';
907 my $delete_str = extract_param
($param, 'delete');
909 my $revert_str = extract_param
($param, 'revert');
911 my $force = extract_param
($param, 'force');
913 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
915 my $storecfg = PVE
::Storage
::config
();
917 my $defaults = PVE
::QemuServer
::load_defaults
();
919 &$resolve_cdrom_alias($param);
921 # now try to verify all parameters
924 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
925 if (!PVE
::QemuServer
::option_exists
($opt)) {
926 raise_param_exc
({ revert
=> "unknown option '$opt'" });
929 raise_param_exc
({ delete => "you can't use '-$opt' and " .
930 "-revert $opt' at the same time" })
931 if defined($param->{$opt});
937 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
938 $opt = 'ide2' if $opt eq 'cdrom';
940 raise_param_exc
({ delete => "you can't use '-$opt' and " .
941 "-delete $opt' at the same time" })
942 if defined($param->{$opt});
944 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
945 "-revert $opt' at the same time" })
948 if (!PVE
::QemuServer
::option_exists
($opt)) {
949 raise_param_exc
({ delete => "unknown option '$opt'" });
955 foreach my $opt (keys %$param) {
956 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
958 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt});
959 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
960 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
961 $param->{$opt} = PVE
::QemuServer
::print_drive
($vmid, $drive);
962 } elsif ($opt =~ m/^net(\d+)$/) {
964 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
965 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
969 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
971 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
973 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
977 my $conf = PVE
::QemuConfig-
>load_config($vmid);
979 die "checksum missmatch (file change by other user?)\n"
980 if $digest && $digest ne $conf->{digest
};
982 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
984 foreach my $opt (keys %$revert) {
985 if (defined($conf->{$opt})) {
986 $param->{$opt} = $conf->{$opt};
987 } elsif (defined($conf->{pending
}->{$opt})) {
992 if ($param->{memory
} || defined($param->{balloon
})) {
993 my $maxmem = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
} || $defaults->{memory
};
994 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
996 die "balloon value too large (must be smaller than assigned memory)\n"
997 if $balloon && $balloon > $maxmem;
1000 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1004 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1006 # write updates to pending section
1008 my $modified = {}; # record what $option we modify
1010 foreach my $opt (@delete) {
1011 $modified->{$opt} = 1;
1012 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1013 if (!defined($conf->{$opt})) {
1014 warn "cannot delete '$opt' - not set in current configuration!\n";
1015 $modified->{$opt} = 0;
1019 if ($opt =~ m/^unused/) {
1020 my $drive = PVE
::QemuServer
::parse_drive
($opt, $conf->{$opt});
1021 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1022 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1023 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1024 delete $conf->{$opt};
1025 PVE
::QemuConfig-
>write_config($vmid, $conf);
1027 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1028 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1029 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1030 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1031 if defined($conf->{pending
}->{$opt});
1032 PVE
::QemuServer
::vmconfig_delete_pending_option
($conf, $opt, $force);
1033 PVE
::QemuConfig-
>write_config($vmid, $conf);
1035 PVE
::QemuServer
::vmconfig_delete_pending_option
($conf, $opt, $force);
1036 PVE
::QemuConfig-
>write_config($vmid, $conf);
1040 foreach my $opt (keys %$param) { # add/change
1041 $modified->{$opt} = 1;
1042 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1043 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1045 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1046 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt});
1047 if (PVE
::QemuServer
::drive_is_cdrom
($drive)) { # CDROM
1048 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1050 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1052 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1053 if defined($conf->{pending
}->{$opt});
1055 &$create_disks($rpcenv, $authuser, $conf->{pending
}, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1056 } elsif ($opt eq "replicate") {
1057 # check if all volumes have replicate feature
1058 PVE
::QemuConfig-
>get_replicatable_volumes($storecfg, $conf);
1059 my $repl = PVE
::JSONSchema
::check_format
('pve-replicate', $param->{opt
});
1060 PVE
::Cluster
::check_node_exists
($repl->{target
});
1061 $conf->{$opt} = $param->{$opt};
1063 $conf->{pending
}->{$opt} = $param->{$opt};
1065 PVE
::QemuServer
::vmconfig_undelete_pending_option
($conf, $opt);
1066 PVE
::QemuConfig-
>write_config($vmid, $conf);
1069 # remove pending changes when nothing changed
1070 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1071 my $changes = PVE
::QemuServer
::vmconfig_cleanup_pending
($conf);
1072 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
1074 return if !scalar(keys %{$conf->{pending
}});
1076 my $running = PVE
::QemuServer
::check_running
($vmid);
1078 # apply pending changes
1080 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1084 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
1085 raise_param_exc
($errors) if scalar(keys %$errors);
1087 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $running);
1097 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1099 if ($background_delay) {
1101 # Note: It would be better to do that in the Event based HTTPServer
1102 # to avoid blocking call to sleep.
1104 my $end_time = time() + $background_delay;
1106 my $task = PVE
::Tools
::upid_decode
($upid);
1109 while (time() < $end_time) {
1110 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
1112 sleep(1); # this gets interrupted when child process ends
1116 my $status = PVE
::Tools
::upid_read_status
($upid);
1117 return undef if $status eq 'OK';
1126 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
1129 my $vm_config_perm_list = [
1134 'VM.Config.Network',
1136 'VM.Config.Options',
1139 __PACKAGE__-
>register_method({
1140 name
=> 'update_vm_async',
1141 path
=> '{vmid}/config',
1145 description
=> "Set virtual machine options (asynchrounous API).",
1147 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1150 additionalProperties
=> 0,
1151 properties
=> PVE
::QemuServer
::json_config_properties
(
1153 node
=> get_standard_option
('pve-node'),
1154 vmid
=> get_standard_option
('pve-vmid'),
1155 skiplock
=> get_standard_option
('skiplock'),
1157 type
=> 'string', format
=> 'pve-configid-list',
1158 description
=> "A list of settings you want to delete.",
1162 type
=> 'string', format
=> 'pve-configid-list',
1163 description
=> "Revert a pending change.",
1168 description
=> $opt_force_description,
1170 requires
=> 'delete',
1174 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1178 background_delay
=> {
1180 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1191 code
=> $update_vm_api,
1194 __PACKAGE__-
>register_method({
1195 name
=> 'update_vm',
1196 path
=> '{vmid}/config',
1200 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1202 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1205 additionalProperties
=> 0,
1206 properties
=> PVE
::QemuServer
::json_config_properties
(
1208 node
=> get_standard_option
('pve-node'),
1209 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1210 skiplock
=> get_standard_option
('skiplock'),
1212 type
=> 'string', format
=> 'pve-configid-list',
1213 description
=> "A list of settings you want to delete.",
1217 type
=> 'string', format
=> 'pve-configid-list',
1218 description
=> "Revert a pending change.",
1223 description
=> $opt_force_description,
1225 requires
=> 'delete',
1229 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1235 returns
=> { type
=> 'null' },
1238 &$update_vm_api($param, 1);
1244 __PACKAGE__-
>register_method({
1245 name
=> 'destroy_vm',
1250 description
=> "Destroy the vm (also delete all used/owned volumes).",
1252 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1255 additionalProperties
=> 0,
1257 node
=> get_standard_option
('pve-node'),
1258 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
1259 skiplock
=> get_standard_option
('skiplock'),
1268 my $rpcenv = PVE
::RPCEnvironment
::get
();
1270 my $authuser = $rpcenv->get_user();
1272 my $vmid = $param->{vmid
};
1274 my $skiplock = $param->{skiplock
};
1275 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1276 if $skiplock && $authuser ne 'root@pam';
1279 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1281 my $storecfg = PVE
::Storage
::config
();
1283 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
1285 die "unable to remove VM $vmid - used in HA resources\n"
1286 if PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
1288 # early tests (repeat after locking)
1289 die "VM $vmid is running - destroy failed\n"
1290 if PVE
::QemuServer
::check_running
($vmid);
1295 syslog
('info', "destroy VM $vmid: $upid\n");
1297 PVE
::QemuServer
::vm_destroy
($storecfg, $vmid, $skiplock);
1299 PVE
::AccessControl
::remove_vm_access
($vmid);
1301 PVE
::Firewall
::remove_vmfw_conf
($vmid);
1304 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1307 __PACKAGE__-
>register_method({
1309 path
=> '{vmid}/unlink',
1313 description
=> "Unlink/delete disk images.",
1315 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1318 additionalProperties
=> 0,
1320 node
=> get_standard_option
('pve-node'),
1321 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1323 type
=> 'string', format
=> 'pve-configid-list',
1324 description
=> "A list of disk IDs you want to delete.",
1328 description
=> $opt_force_description,
1333 returns
=> { type
=> 'null'},
1337 $param->{delete} = extract_param
($param, 'idlist');
1339 __PACKAGE__-
>update_vm($param);
1346 __PACKAGE__-
>register_method({
1348 path
=> '{vmid}/vncproxy',
1352 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1354 description
=> "Creates a TCP VNC proxy connections.",
1356 additionalProperties
=> 0,
1358 node
=> get_standard_option
('pve-node'),
1359 vmid
=> get_standard_option
('pve-vmid'),
1363 description
=> "starts websockify instead of vncproxy",
1368 additionalProperties
=> 0,
1370 user
=> { type
=> 'string' },
1371 ticket
=> { type
=> 'string' },
1372 cert
=> { type
=> 'string' },
1373 port
=> { type
=> 'integer' },
1374 upid
=> { type
=> 'string' },
1380 my $rpcenv = PVE
::RPCEnvironment
::get
();
1382 my $authuser = $rpcenv->get_user();
1384 my $vmid = $param->{vmid
};
1385 my $node = $param->{node
};
1386 my $websocket = $param->{websocket
};
1388 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
1390 my $authpath = "/vms/$vmid";
1392 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
1394 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
1397 my ($remip, $family);
1400 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
1401 ($remip, $family) = PVE
::Cluster
::remote_node_ip
($node);
1402 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1403 $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
1405 $family = PVE
::Tools
::get_host_address_family
($node);
1408 my $port = PVE
::Tools
::next_vnc_port
($family);
1415 syslog
('info', "starting vnc proxy $upid\n");
1419 if ($conf->{vga
} && ($conf->{vga
} =~ m/^serial\d+$/)) {
1421 die "Websocket mode is not supported in vga serial mode!" if $websocket;
1423 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga
} ];
1424 #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
1425 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1426 '-timeout', $timeout, '-authpath', $authpath,
1427 '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
1428 PVE
::Tools
::run_command
($cmd);
1431 $ENV{LC_PVE_TICKET
} = $ticket if $websocket; # set ticket with "qm vncproxy"
1433 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1435 my $sock = IO
::Socket
::IP-
>new(
1440 GetAddrInfoFlags
=> 0,
1441 ) or die "failed to create socket: $!\n";
1442 # Inside the worker we shouldn't have any previous alarms
1443 # running anyway...:
1445 local $SIG{ALRM
} = sub { die "connection timed out\n" };
1447 accept(my $cli, $sock) or die "connection failed: $!\n";
1450 if (PVE
::Tools
::run_command
($cmd,
1451 output
=> '>&'.fileno($cli),
1452 input
=> '<&'.fileno($cli),
1455 die "Failed to run vncproxy.\n";
1462 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1464 PVE
::Tools
::wait_for_vnc_port
($port);
1475 __PACKAGE__-
>register_method({
1476 name
=> 'vncwebsocket',
1477 path
=> '{vmid}/vncwebsocket',
1480 description
=> "You also need to pass a valid ticket (vncticket).",
1481 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1483 description
=> "Opens a weksocket for VNC traffic.",
1485 additionalProperties
=> 0,
1487 node
=> get_standard_option
('pve-node'),
1488 vmid
=> get_standard_option
('pve-vmid'),
1490 description
=> "Ticket from previous call to vncproxy.",
1495 description
=> "Port number returned by previous vncproxy call.",
1505 port
=> { type
=> 'string' },
1511 my $rpcenv = PVE
::RPCEnvironment
::get
();
1513 my $authuser = $rpcenv->get_user();
1515 my $vmid = $param->{vmid
};
1516 my $node = $param->{node
};
1518 my $authpath = "/vms/$vmid";
1520 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
1522 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
1524 # Note: VNC ports are acessible from outside, so we do not gain any
1525 # security if we verify that $param->{port} belongs to VM $vmid. This
1526 # check is done by verifying the VNC ticket (inside VNC protocol).
1528 my $port = $param->{port
};
1530 return { port
=> $port };
1533 __PACKAGE__-
>register_method({
1534 name
=> 'spiceproxy',
1535 path
=> '{vmid}/spiceproxy',
1540 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1542 description
=> "Returns a SPICE configuration to connect to the VM.",
1544 additionalProperties
=> 0,
1546 node
=> get_standard_option
('pve-node'),
1547 vmid
=> get_standard_option
('pve-vmid'),
1548 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
1551 returns
=> get_standard_option
('remote-viewer-config'),
1555 my $rpcenv = PVE
::RPCEnvironment
::get
();
1557 my $authuser = $rpcenv->get_user();
1559 my $vmid = $param->{vmid
};
1560 my $node = $param->{node
};
1561 my $proxy = $param->{proxy
};
1563 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
1564 my $title = "VM $vmid";
1565 $title .= " - ". $conf->{name
} if $conf->{name
};
1567 my $port = PVE
::QemuServer
::spice_port
($vmid);
1569 my ($ticket, undef, $remote_viewer_config) =
1570 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
1572 PVE
::QemuServer
::vm_mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
1573 PVE
::QemuServer
::vm_mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
1575 return $remote_viewer_config;
1578 __PACKAGE__-
>register_method({
1580 path
=> '{vmid}/status',
1583 description
=> "Directory index",
1588 additionalProperties
=> 0,
1590 node
=> get_standard_option
('pve-node'),
1591 vmid
=> get_standard_option
('pve-vmid'),
1599 subdir
=> { type
=> 'string' },
1602 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1608 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1611 { subdir
=> 'current' },
1612 { subdir
=> 'start' },
1613 { subdir
=> 'stop' },
1619 __PACKAGE__-
>register_method({
1620 name
=> 'vm_status',
1621 path
=> '{vmid}/status/current',
1624 protected
=> 1, # qemu pid files are only readable by root
1625 description
=> "Get virtual machine status.",
1627 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1630 additionalProperties
=> 0,
1632 node
=> get_standard_option
('pve-node'),
1633 vmid
=> get_standard_option
('pve-vmid'),
1636 returns
=> { type
=> 'object' },
1641 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1643 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
1644 my $status = $vmstatus->{$param->{vmid
}};
1646 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
1648 $status->{spice
} = 1 if PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
1653 __PACKAGE__-
>register_method({
1655 path
=> '{vmid}/status/start',
1659 description
=> "Start virtual machine.",
1661 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1664 additionalProperties
=> 0,
1666 node
=> get_standard_option
('pve-node'),
1667 vmid
=> get_standard_option
('pve-vmid',
1668 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
1669 skiplock
=> get_standard_option
('skiplock'),
1670 stateuri
=> get_standard_option
('pve-qm-stateuri'),
1671 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
1674 enum
=> ['secure', 'insecure'],
1675 description
=> "Migration traffic is encrypted using an SSH " .
1676 "tunnel by default. On secure, completely private networks " .
1677 "this can be disabled to increase performance.",
1680 migration_network
=> {
1681 type
=> 'string', format
=> 'CIDR',
1682 description
=> "CIDR of the (sub) network that is used for migration.",
1685 machine
=> get_standard_option
('pve-qm-machine'),
1687 description
=> "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
1699 my $rpcenv = PVE
::RPCEnvironment
::get
();
1701 my $authuser = $rpcenv->get_user();
1703 my $node = extract_param
($param, 'node');
1705 my $vmid = extract_param
($param, 'vmid');
1707 my $machine = extract_param
($param, 'machine');
1709 my $stateuri = extract_param
($param, 'stateuri');
1710 raise_param_exc
({ stateuri
=> "Only root may use this option." })
1711 if $stateuri && $authuser ne 'root@pam';
1713 my $skiplock = extract_param
($param, 'skiplock');
1714 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1715 if $skiplock && $authuser ne 'root@pam';
1717 my $migratedfrom = extract_param
($param, 'migratedfrom');
1718 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
1719 if $migratedfrom && $authuser ne 'root@pam';
1721 my $migration_type = extract_param
($param, 'migration_type');
1722 raise_param_exc
({ migration_type
=> "Only root may use this option." })
1723 if $migration_type && $authuser ne 'root@pam';
1725 my $migration_network = extract_param
($param, 'migration_network');
1726 raise_param_exc
({ migration_network
=> "Only root may use this option." })
1727 if $migration_network && $authuser ne 'root@pam';
1729 my $targetstorage = extract_param
($param, 'targetstorage');
1730 raise_param_exc
({ targetstorage
=> "Only root may use this option." })
1731 if $targetstorage && $authuser ne 'root@pam';
1733 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
1734 if $targetstorage && !$migratedfrom;
1736 # read spice ticket from STDIN
1738 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
1739 if (defined(my $line = <>)) {
1741 $spice_ticket = $line;
1745 PVE
::Cluster
::check_cfs_quorum
();
1747 my $storecfg = PVE
::Storage
::config
();
1749 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri &&
1750 $rpcenv->{type
} ne 'ha') {
1755 my $service = "vm:$vmid";
1757 my $cmd = ['ha-manager', 'set', $service, '--state', 'started'];
1759 print "Executing HA start for VM $vmid\n";
1761 PVE
::Tools
::run_command
($cmd);
1766 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
1773 syslog
('info', "start VM $vmid: $upid\n");
1775 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
1776 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
1781 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
1785 __PACKAGE__-
>register_method({
1787 path
=> '{vmid}/status/stop',
1791 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
1792 "is akin to pulling the power plug of a running computer and may damage the VM data",
1794 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1797 additionalProperties
=> 0,
1799 node
=> get_standard_option
('pve-node'),
1800 vmid
=> get_standard_option
('pve-vmid',
1801 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
1802 skiplock
=> get_standard_option
('skiplock'),
1803 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
1805 description
=> "Wait maximal timeout seconds.",
1811 description
=> "Do not deactivate storage volumes.",
1824 my $rpcenv = PVE
::RPCEnvironment
::get
();
1826 my $authuser = $rpcenv->get_user();
1828 my $node = extract_param
($param, 'node');
1830 my $vmid = extract_param
($param, 'vmid');
1832 my $skiplock = extract_param
($param, 'skiplock');
1833 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1834 if $skiplock && $authuser ne 'root@pam';
1836 my $keepActive = extract_param
($param, 'keepActive');
1837 raise_param_exc
({ keepActive
=> "Only root may use this option." })
1838 if $keepActive && $authuser ne 'root@pam';
1840 my $migratedfrom = extract_param
($param, 'migratedfrom');
1841 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
1842 if $migratedfrom && $authuser ne 'root@pam';
1845 my $storecfg = PVE
::Storage
::config
();
1847 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
1852 my $service = "vm:$vmid";
1854 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
1856 print "Executing HA stop for VM $vmid\n";
1858 PVE
::Tools
::run_command
($cmd);
1863 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
1869 syslog
('info', "stop VM $vmid: $upid\n");
1871 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
1872 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
1877 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
1881 __PACKAGE__-
>register_method({
1883 path
=> '{vmid}/status/reset',
1887 description
=> "Reset virtual machine.",
1889 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1892 additionalProperties
=> 0,
1894 node
=> get_standard_option
('pve-node'),
1895 vmid
=> get_standard_option
('pve-vmid',
1896 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
1897 skiplock
=> get_standard_option
('skiplock'),
1906 my $rpcenv = PVE
::RPCEnvironment
::get
();
1908 my $authuser = $rpcenv->get_user();
1910 my $node = extract_param
($param, 'node');
1912 my $vmid = extract_param
($param, 'vmid');
1914 my $skiplock = extract_param
($param, 'skiplock');
1915 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1916 if $skiplock && $authuser ne 'root@pam';
1918 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
1923 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
1928 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
1931 __PACKAGE__-
>register_method({
1932 name
=> 'vm_shutdown',
1933 path
=> '{vmid}/status/shutdown',
1937 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
1938 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
1940 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1943 additionalProperties
=> 0,
1945 node
=> get_standard_option
('pve-node'),
1946 vmid
=> get_standard_option
('pve-vmid',
1947 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
1948 skiplock
=> get_standard_option
('skiplock'),
1950 description
=> "Wait maximal timeout seconds.",
1956 description
=> "Make sure the VM stops.",
1962 description
=> "Do not deactivate storage volumes.",
1975 my $rpcenv = PVE
::RPCEnvironment
::get
();
1977 my $authuser = $rpcenv->get_user();
1979 my $node = extract_param
($param, 'node');
1981 my $vmid = extract_param
($param, 'vmid');
1983 my $skiplock = extract_param
($param, 'skiplock');
1984 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1985 if $skiplock && $authuser ne 'root@pam';
1987 my $keepActive = extract_param
($param, 'keepActive');
1988 raise_param_exc
({ keepActive
=> "Only root may use this option." })
1989 if $keepActive && $authuser ne 'root@pam';
1991 my $storecfg = PVE
::Storage
::config
();
1995 # if vm is paused, do not shutdown (but stop if forceStop = 1)
1996 # otherwise, we will infer a shutdown command, but run into the timeout,
1997 # then when the vm is resumed, it will instantly shutdown
1999 # checking the qmp status here to get feedback to the gui/cli/api
2000 # and the status query should not take too long
2003 $qmpstatus = PVE
::QemuServer
::vm_qmp_command
($vmid, { execute
=> "query-status" }, 0);
2007 if (!$err && $qmpstatus->{status
} eq "paused") {
2008 if ($param->{forceStop
}) {
2009 warn "VM is paused - stop instead of shutdown\n";
2012 die "VM is paused - cannot shutdown\n";
2016 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) &&
2017 ($rpcenv->{type
} ne 'ha')) {
2022 my $service = "vm:$vmid";
2024 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
2026 print "Executing HA stop for VM $vmid\n";
2028 PVE
::Tools
::run_command
($cmd);
2033 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2040 syslog
('info', "shutdown VM $vmid: $upid\n");
2042 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
2043 $shutdown, $param->{forceStop
}, $keepActive);
2048 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2052 __PACKAGE__-
>register_method({
2053 name
=> 'vm_suspend',
2054 path
=> '{vmid}/status/suspend',
2058 description
=> "Suspend virtual machine.",
2060 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2063 additionalProperties
=> 0,
2065 node
=> get_standard_option
('pve-node'),
2066 vmid
=> get_standard_option
('pve-vmid',
2067 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2068 skiplock
=> get_standard_option
('skiplock'),
2077 my $rpcenv = PVE
::RPCEnvironment
::get
();
2079 my $authuser = $rpcenv->get_user();
2081 my $node = extract_param
($param, 'node');
2083 my $vmid = extract_param
($param, 'vmid');
2085 my $skiplock = extract_param
($param, 'skiplock');
2086 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2087 if $skiplock && $authuser ne 'root@pam';
2089 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
2094 syslog
('info', "suspend VM $vmid: $upid\n");
2096 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock);
2101 return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd);
2104 __PACKAGE__-
>register_method({
2105 name
=> 'vm_resume',
2106 path
=> '{vmid}/status/resume',
2110 description
=> "Resume virtual machine.",
2112 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2115 additionalProperties
=> 0,
2117 node
=> get_standard_option
('pve-node'),
2118 vmid
=> get_standard_option
('pve-vmid',
2119 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2120 skiplock
=> get_standard_option
('skiplock'),
2121 nocheck
=> { type
=> 'boolean', optional
=> 1 },
2131 my $rpcenv = PVE
::RPCEnvironment
::get
();
2133 my $authuser = $rpcenv->get_user();
2135 my $node = extract_param
($param, 'node');
2137 my $vmid = extract_param
($param, 'vmid');
2139 my $skiplock = extract_param
($param, 'skiplock');
2140 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2141 if $skiplock && $authuser ne 'root@pam';
2143 my $nocheck = extract_param
($param, 'nocheck');
2145 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid, $nocheck);
2150 syslog
('info', "resume VM $vmid: $upid\n");
2152 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
2157 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2160 __PACKAGE__-
>register_method({
2161 name
=> 'vm_sendkey',
2162 path
=> '{vmid}/sendkey',
2166 description
=> "Send key event to virtual machine.",
2168 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2171 additionalProperties
=> 0,
2173 node
=> get_standard_option
('pve-node'),
2174 vmid
=> get_standard_option
('pve-vmid',
2175 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2176 skiplock
=> get_standard_option
('skiplock'),
2178 description
=> "The key (qemu monitor encoding).",
2183 returns
=> { type
=> 'null'},
2187 my $rpcenv = PVE
::RPCEnvironment
::get
();
2189 my $authuser = $rpcenv->get_user();
2191 my $node = extract_param
($param, 'node');
2193 my $vmid = extract_param
($param, 'vmid');
2195 my $skiplock = extract_param
($param, 'skiplock');
2196 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2197 if $skiplock && $authuser ne 'root@pam';
2199 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
2204 __PACKAGE__-
>register_method({
2205 name
=> 'vm_feature',
2206 path
=> '{vmid}/feature',
2210 description
=> "Check if feature for virtual machine is available.",
2212 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2215 additionalProperties
=> 0,
2217 node
=> get_standard_option
('pve-node'),
2218 vmid
=> get_standard_option
('pve-vmid'),
2220 description
=> "Feature to check.",
2222 enum
=> [ 'snapshot', 'clone', 'copy' ],
2224 snapname
=> get_standard_option
('pve-snapshot-name', {
2232 hasFeature
=> { type
=> 'boolean' },
2235 items
=> { type
=> 'string' },
2242 my $node = extract_param
($param, 'node');
2244 my $vmid = extract_param
($param, 'vmid');
2246 my $snapname = extract_param
($param, 'snapname');
2248 my $feature = extract_param
($param, 'feature');
2250 my $running = PVE
::QemuServer
::check_running
($vmid);
2252 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2255 my $snap = $conf->{snapshots
}->{$snapname};
2256 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2259 my $storecfg = PVE
::Storage
::config
();
2261 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
2262 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
2265 hasFeature
=> $hasFeature,
2266 nodes
=> [ keys %$nodelist ],
2270 __PACKAGE__-
>register_method({
2272 path
=> '{vmid}/clone',
2276 description
=> "Create a copy of virtual machine/template.",
2278 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2279 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2280 "'Datastore.AllocateSpace' on any used storage.",
2283 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2285 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2286 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
2291 additionalProperties
=> 0,
2293 node
=> get_standard_option
('pve-node'),
2294 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2295 newid
=> get_standard_option
('pve-vmid', { description
=> 'VMID for the clone.' }),
2298 type
=> 'string', format
=> 'dns-name',
2299 description
=> "Set a name for the new VM.",
2304 description
=> "Description for the new VM.",
2308 type
=> 'string', format
=> 'pve-poolid',
2309 description
=> "Add the new VM to the specified pool.",
2311 snapname
=> get_standard_option
('pve-snapshot-name', {
2314 storage
=> get_standard_option
('pve-storage-id', {
2315 description
=> "Target storage for full clone.",
2320 description
=> "Target format for file storage.",
2324 enum
=> [ 'raw', 'qcow2', 'vmdk'],
2329 description
=> "Create a full copy of all disk. This is always done when " .
2330 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2333 target
=> get_standard_option
('pve-node', {
2334 description
=> "Target node. Only allowed if the original VM is on shared storage.",
2345 my $rpcenv = PVE
::RPCEnvironment
::get
();
2347 my $authuser = $rpcenv->get_user();
2349 my $node = extract_param
($param, 'node');
2351 my $vmid = extract_param
($param, 'vmid');
2353 my $newid = extract_param
($param, 'newid');
2355 my $pool = extract_param
($param, 'pool');
2357 if (defined($pool)) {
2358 $rpcenv->check_pool_exist($pool);
2361 my $snapname = extract_param
($param, 'snapname');
2363 my $storage = extract_param
($param, 'storage');
2365 my $format = extract_param
($param, 'format');
2367 my $target = extract_param
($param, 'target');
2369 my $localnode = PVE
::INotify
::nodename
();
2371 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2373 PVE
::Cluster
::check_node_exists
($target) if $target;
2375 my $storecfg = PVE
::Storage
::config
();
2378 # check if storage is enabled on local node
2379 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
2381 # check if storage is available on target node
2382 PVE
::Storage
::storage_check_node
($storecfg, $storage, $target);
2383 # clone only works if target storage is shared
2384 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
2385 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared
};
2389 PVE
::Cluster
::check_cfs_quorum
();
2391 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
2393 # exclusive lock if VM is running - else shared lock is enough;
2394 my $shared_lock = $running ?
0 : 1;
2398 # do all tests after lock
2399 # we also try to do all tests before we fork the worker
2401 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2403 PVE
::QemuConfig-
>check_lock($conf);
2405 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
2407 die "unexpected state change\n" if $verify_running != $running;
2409 die "snapshot '$snapname' does not exist\n"
2410 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
2412 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
2414 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2416 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2418 my $conffile = PVE
::QemuConfig-
>config_file($newid);
2420 die "unable to create VM $newid: config file already exists\n"
2423 my $newconf = { lock => 'clone' };
2428 foreach my $opt (keys %$oldconf) {
2429 my $value = $oldconf->{$opt};
2431 # do not copy snapshot related info
2432 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2433 $opt eq 'vmstate' || $opt eq 'snapstate';
2435 # no need to copy unused images, because VMID(owner) changes anyways
2436 next if $opt =~ m/^unused\d+$/;
2438 # always change MAC! address
2439 if ($opt =~ m/^net(\d+)$/) {
2440 my $net = PVE
::QemuServer
::parse_net
($value);
2441 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
2442 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
2443 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
2444 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
2445 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
2446 die "unable to parse drive options for '$opt'\n" if !$drive;
2447 if (PVE
::QemuServer
::drive_is_cdrom
($drive)) {
2448 $newconf->{$opt} = $value; # simply copy configuration
2450 if ($param->{full
}) {
2451 die "Full clone feature is not available"
2452 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
2453 $fullclone->{$opt} = 1;
2455 # not full means clone instead of copy
2456 die "Linked clone feature is not available"
2457 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
2459 $drives->{$opt} = $drive;
2460 push @$vollist, $drive->{file
};
2463 # copy everything else
2464 $newconf->{$opt} = $value;
2468 # auto generate a new uuid
2469 my ($uuid, $uuid_str);
2470 UUID
::generate
($uuid);
2471 UUID
::unparse
($uuid, $uuid_str);
2472 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
2473 $smbios1->{uuid
} = $uuid_str;
2474 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
2476 delete $newconf->{template
};
2478 if ($param->{name
}) {
2479 $newconf->{name
} = $param->{name
};
2481 if ($oldconf->{name
}) {
2482 $newconf->{name
} = "Copy-of-$oldconf->{name}";
2484 $newconf->{name
} = "Copy-of-VM-$vmid";
2488 if ($param->{description
}) {
2489 $newconf->{description
} = $param->{description
};
2492 # create empty/temp config - this fails if VM already exists on other node
2493 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
2498 my $newvollist = [];
2502 local $SIG{INT
} = $SIG{TERM
} = $SIG{QUIT
} = $SIG{HUP
} = sub { die "interrupted by signal\n"; };
2504 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
2506 my $total_jobs = scalar(keys %{$drives});
2509 foreach my $opt (keys %$drives) {
2510 my $drive = $drives->{$opt};
2511 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2513 my $newdrive = PVE
::QemuServer
::clone_disk
($storecfg, $vmid, $running, $opt, $drive, $snapname,
2514 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2515 $jobs, $skipcomplete, $oldconf->{agent
});
2517 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($vmid, $newdrive);
2519 PVE
::QemuConfig-
>write_config($newid, $newconf);
2523 delete $newconf->{lock};
2524 PVE
::QemuConfig-
>write_config($newid, $newconf);
2527 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2528 PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) if !$running;
2529 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
2531 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
2532 die "Failed to move config to node '$target' - rename failed: $!\n"
2533 if !rename($conffile, $newconffile);
2536 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
2541 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
2543 sleep 1; # some storage like rbd need to wait before release volume - really?
2545 foreach my $volid (@$newvollist) {
2546 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
2549 die "clone failed: $err";
2555 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
2557 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2560 return PVE
::QemuConfig-
>lock_config_mode($vmid, 1, $shared_lock, sub {
2561 # Aquire exclusive lock lock for $newid
2562 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
2567 __PACKAGE__-
>register_method({
2568 name
=> 'move_vm_disk',
2569 path
=> '{vmid}/move_disk',
2573 description
=> "Move volume to different storage.",
2575 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2577 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2578 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2582 additionalProperties
=> 0,
2584 node
=> get_standard_option
('pve-node'),
2585 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2588 description
=> "The disk you want to move.",
2589 enum
=> [ PVE
::QemuServer
::valid_drive_names
() ],
2591 storage
=> get_standard_option
('pve-storage-id', {
2592 description
=> "Target storage.",
2593 completion
=> \
&PVE
::QemuServer
::complete_storage
,
2597 description
=> "Target Format.",
2598 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
2603 description
=> "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2609 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2617 description
=> "the task ID.",
2622 my $rpcenv = PVE
::RPCEnvironment
::get
();
2624 my $authuser = $rpcenv->get_user();
2626 my $node = extract_param
($param, 'node');
2628 my $vmid = extract_param
($param, 'vmid');
2630 my $digest = extract_param
($param, 'digest');
2632 my $disk = extract_param
($param, 'disk');
2634 my $storeid = extract_param
($param, 'storage');
2636 my $format = extract_param
($param, 'format');
2638 my $storecfg = PVE
::Storage
::config
();
2640 my $updatefn = sub {
2642 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2644 PVE
::QemuConfig-
>check_lock($conf);
2646 die "checksum missmatch (file change by other user?)\n"
2647 if $digest && $digest ne $conf->{digest
};
2649 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2651 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
2653 my $old_volid = $drive->{file
} || die "disk '$disk' has no associated volume\n";
2655 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
2658 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
2659 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2663 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
2664 (!$format || !$oldfmt || $oldfmt eq $format);
2666 # this only checks snapshots because $disk is passed!
2667 my $snapshotted = PVE
::QemuServer
::is_volume_in_use
($storecfg, $conf, $disk, $old_volid);
2668 die "you can't move a disk with snapshots and delete the source\n"
2669 if $snapshotted && $param->{delete};
2671 PVE
::Cluster
::log_msg
('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2673 my $running = PVE
::QemuServer
::check_running
($vmid);
2675 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
2679 my $newvollist = [];
2682 local $SIG{INT
} = $SIG{TERM
} = $SIG{QUIT
} = $SIG{HUP
} = sub { die "interrupted by signal\n"; };
2684 warn "moving disk with snapshots, snapshots will not be moved!\n"
2687 my $newdrive = PVE
::QemuServer
::clone_disk
($storecfg, $vmid, $running, $disk, $drive, undef,
2688 $vmid, $storeid, $format, 1, $newvollist);
2690 $conf->{$disk} = PVE
::QemuServer
::print_drive
($vmid, $newdrive);
2692 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
2694 # convert moved disk to base if part of template
2695 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
2696 if PVE
::QemuConfig-
>is_template($conf);
2698 PVE
::QemuConfig-
>write_config($vmid, $conf);
2701 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2702 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
2709 foreach my $volid (@$newvollist) {
2710 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
2713 die "storage migration failed: $err";
2716 if ($param->{delete}) {
2718 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
2719 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
2725 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
2728 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
2731 __PACKAGE__-
>register_method({
2732 name
=> 'migrate_vm',
2733 path
=> '{vmid}/migrate',
2737 description
=> "Migrate virtual machine. Creates a new migration task.",
2739 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
2742 additionalProperties
=> 0,
2744 node
=> get_standard_option
('pve-node'),
2745 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2746 target
=> get_standard_option
('pve-node', {
2747 description
=> "Target node.",
2748 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
2752 description
=> "Use online/live migration.",
2757 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
2762 enum
=> ['secure', 'insecure'],
2763 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2766 migration_network
=> {
2767 type
=> 'string', format
=> 'CIDR',
2768 description
=> "CIDR of the (sub) network that is used for migration.",
2771 "with-local-disks" => {
2773 description
=> "Enable live storage migration for local disk",
2776 targetstorage
=> get_standard_option
('pve-storage-id', {
2777 description
=> "Default target storage.",
2779 completion
=> \
&PVE
::QemuServer
::complete_storage
,
2785 description
=> "the task ID.",
2790 my $rpcenv = PVE
::RPCEnvironment
::get
();
2792 my $authuser = $rpcenv->get_user();
2794 my $target = extract_param
($param, 'target');
2796 my $localnode = PVE
::INotify
::nodename
();
2797 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
2799 PVE
::Cluster
::check_cfs_quorum
();
2801 PVE
::Cluster
::check_node_exists
($target);
2803 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
2805 my $vmid = extract_param
($param, 'vmid');
2807 raise_param_exc
({ targetstorage
=> "Live storage migration can only be done online." })
2808 if !$param->{online
} && $param->{targetstorage
};
2810 raise_param_exc
({ force
=> "Only root may use this option." })
2811 if $param->{force
} && $authuser ne 'root@pam';
2813 raise_param_exc
({ migration_type
=> "Only root may use this option." })
2814 if $param->{migration_type
} && $authuser ne 'root@pam';
2816 # allow root only until better network permissions are available
2817 raise_param_exc
({ migration_network
=> "Only root may use this option." })
2818 if $param->{migration_network
} && $authuser ne 'root@pam';
2821 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2823 # try to detect errors early
2825 PVE
::QemuConfig-
>check_lock($conf);
2827 if (PVE
::QemuServer
::check_running
($vmid)) {
2828 die "cant migrate running VM without --online\n"
2829 if !$param->{online
};
2832 my $storecfg = PVE
::Storage
::config
();
2834 if( $param->{targetstorage
}) {
2835 PVE
::Storage
::storage_check_node
($storecfg, $param->{targetstorage
}, $target);
2837 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
2840 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
2845 my $service = "vm:$vmid";
2847 my $cmd = ['ha-manager', 'migrate', $service, $target];
2849 print "Executing HA migrate for VM $vmid to node $target\n";
2851 PVE
::Tools
::run_command
($cmd);
2856 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
2863 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
2866 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd);
2871 __PACKAGE__-
>register_method({
2873 path
=> '{vmid}/monitor',
2877 description
=> "Execute Qemu monitor commands.",
2879 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
2880 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2883 additionalProperties
=> 0,
2885 node
=> get_standard_option
('pve-node'),
2886 vmid
=> get_standard_option
('pve-vmid'),
2889 description
=> "The monitor command.",
2893 returns
=> { type
=> 'string'},
2897 my $rpcenv = PVE
::RPCEnvironment
::get
();
2898 my $authuser = $rpcenv->get_user();
2901 my $command = shift;
2902 return $command =~ m/^\s*info(\s+|$)/
2903 || $command =~ m/^\s*help\s*$/;
2906 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
2907 if !&$is_ro($param->{command
});
2909 my $vmid = $param->{vmid
};
2911 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
2915 $res = PVE
::QemuServer
::vm_human_monitor_command
($vmid, $param->{command
});
2917 $res = "ERROR: $@" if $@;
2922 my $guest_agent_commands = [
2930 'network-get-interfaces',
2933 'get-memory-blocks',
2934 'get-memory-block-info',
2941 __PACKAGE__-
>register_method({
2943 path
=> '{vmid}/agent',
2947 description
=> "Execute Qemu Guest Agent commands.",
2949 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2952 additionalProperties
=> 0,
2954 node
=> get_standard_option
('pve-node'),
2955 vmid
=> get_standard_option
('pve-vmid', {
2956 completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2959 description
=> "The QGA command.",
2960 enum
=> $guest_agent_commands,
2966 description
=> "Returns an object with a single `result` property. The type of that
2967 property depends on the executed command.",
2972 my $vmid = $param->{vmid
};
2974 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
2976 die "No Qemu Guest Agent\n" if !defined($conf->{agent
});
2977 die "VM $vmid is not running\n" if !PVE
::QemuServer
::check_running
($vmid);
2979 my $cmd = $param->{command
};
2981 my $res = PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-$cmd");
2983 return { result
=> $res };
2986 __PACKAGE__-
>register_method({
2987 name
=> 'resize_vm',
2988 path
=> '{vmid}/resize',
2992 description
=> "Extend volume size.",
2994 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2997 additionalProperties
=> 0,
2999 node
=> get_standard_option
('pve-node'),
3000 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3001 skiplock
=> get_standard_option
('skiplock'),
3004 description
=> "The disk you want to resize.",
3005 enum
=> [PVE
::QemuServer
::valid_drive_names
()],
3009 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
3010 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.",
3014 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3020 returns
=> { type
=> 'null'},
3024 my $rpcenv = PVE
::RPCEnvironment
::get
();
3026 my $authuser = $rpcenv->get_user();
3028 my $node = extract_param
($param, 'node');
3030 my $vmid = extract_param
($param, 'vmid');
3032 my $digest = extract_param
($param, 'digest');
3034 my $disk = extract_param
($param, 'disk');
3036 my $sizestr = extract_param
($param, 'size');
3038 my $skiplock = extract_param
($param, 'skiplock');
3039 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3040 if $skiplock && $authuser ne 'root@pam';
3042 my $storecfg = PVE
::Storage
::config
();
3044 my $updatefn = sub {
3046 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3048 die "checksum missmatch (file change by other user?)\n"
3049 if $digest && $digest ne $conf->{digest
};
3050 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
3052 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3054 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
3056 my (undef, undef, undef, undef, undef, undef, $format) =
3057 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
3059 die "can't resize volume: $disk if snapshot exists\n"
3060 if %{$conf->{snapshots
}} && $format eq 'qcow2';
3062 my $volid = $drive->{file
};
3064 die "disk '$disk' has no associated volume\n" if !$volid;
3066 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
3068 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
3070 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3072 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
3073 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
3075 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3076 my ($ext, $newsize, $unit) = ($1, $2, $4);
3079 $newsize = $newsize * 1024;
3080 } elsif ($unit eq 'M') {
3081 $newsize = $newsize * 1024 * 1024;
3082 } elsif ($unit eq 'G') {
3083 $newsize = $newsize * 1024 * 1024 * 1024;
3084 } elsif ($unit eq 'T') {
3085 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3088 $newsize += $size if $ext;
3089 $newsize = int($newsize);
3091 die "shrinking disks is not supported\n" if $newsize < $size;
3093 return if $size == $newsize;
3095 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3097 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3099 $drive->{size
} = $newsize;
3100 $conf->{$disk} = PVE
::QemuServer
::print_drive
($vmid, $drive);
3102 PVE
::QemuConfig-
>write_config($vmid, $conf);
3105 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
3109 __PACKAGE__-
>register_method({
3110 name
=> 'snapshot_list',
3111 path
=> '{vmid}/snapshot',
3113 description
=> "List all snapshots.",
3115 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3118 protected
=> 1, # qemu pid files are only readable by root
3120 additionalProperties
=> 0,
3122 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3123 node
=> get_standard_option
('pve-node'),
3132 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
3137 my $vmid = $param->{vmid
};
3139 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3140 my $snaphash = $conf->{snapshots
} || {};
3144 foreach my $name (keys %$snaphash) {
3145 my $d = $snaphash->{$name};
3148 snaptime
=> $d->{snaptime
} || 0,
3149 vmstate
=> $d->{vmstate
} ?
1 : 0,
3150 description
=> $d->{description
} || '',
3152 $item->{parent
} = $d->{parent
} if $d->{parent
};
3153 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
3157 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
3158 my $current = { name
=> 'current', digest
=> $conf->{digest
}, running
=> $running };
3159 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
3161 push @$res, $current;
3166 __PACKAGE__-
>register_method({
3168 path
=> '{vmid}/snapshot',
3172 description
=> "Snapshot a VM.",
3174 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3177 additionalProperties
=> 0,
3179 node
=> get_standard_option
('pve-node'),
3180 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3181 snapname
=> get_standard_option
('pve-snapshot-name'),
3185 description
=> "Save the vmstate",
3190 description
=> "A textual description or comment.",
3196 description
=> "the task ID.",
3201 my $rpcenv = PVE
::RPCEnvironment
::get
();
3203 my $authuser = $rpcenv->get_user();
3205 my $node = extract_param
($param, 'node');
3207 my $vmid = extract_param
($param, 'vmid');
3209 my $snapname = extract_param
($param, 'snapname');
3211 die "unable to use snapshot name 'current' (reserved name)\n"
3212 if $snapname eq 'current';
3215 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
3216 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
3217 $param->{description
});
3220 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3223 __PACKAGE__-
>register_method({
3224 name
=> 'snapshot_cmd_idx',
3225 path
=> '{vmid}/snapshot/{snapname}',
3232 additionalProperties
=> 0,
3234 vmid
=> get_standard_option
('pve-vmid'),
3235 node
=> get_standard_option
('pve-node'),
3236 snapname
=> get_standard_option
('pve-snapshot-name'),
3245 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
3252 push @$res, { cmd
=> 'rollback' };
3253 push @$res, { cmd
=> 'config' };
3258 __PACKAGE__-
>register_method({
3259 name
=> 'update_snapshot_config',
3260 path
=> '{vmid}/snapshot/{snapname}/config',
3264 description
=> "Update snapshot metadata.",
3266 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3269 additionalProperties
=> 0,
3271 node
=> get_standard_option
('pve-node'),
3272 vmid
=> get_standard_option
('pve-vmid'),
3273 snapname
=> get_standard_option
('pve-snapshot-name'),
3277 description
=> "A textual description or comment.",
3281 returns
=> { type
=> 'null' },
3285 my $rpcenv = PVE
::RPCEnvironment
::get
();
3287 my $authuser = $rpcenv->get_user();
3289 my $vmid = extract_param
($param, 'vmid');
3291 my $snapname = extract_param
($param, 'snapname');
3293 return undef if !defined($param->{description
});
3295 my $updatefn = sub {
3297 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3299 PVE
::QemuConfig-
>check_lock($conf);
3301 my $snap = $conf->{snapshots
}->{$snapname};
3303 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3305 $snap->{description
} = $param->{description
} if defined($param->{description
});
3307 PVE
::QemuConfig-
>write_config($vmid, $conf);
3310 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
3315 __PACKAGE__-
>register_method({
3316 name
=> 'get_snapshot_config',
3317 path
=> '{vmid}/snapshot/{snapname}/config',
3320 description
=> "Get snapshot configuration",
3322 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3325 additionalProperties
=> 0,
3327 node
=> get_standard_option
('pve-node'),
3328 vmid
=> get_standard_option
('pve-vmid'),
3329 snapname
=> get_standard_option
('pve-snapshot-name'),
3332 returns
=> { type
=> "object" },
3336 my $rpcenv = PVE
::RPCEnvironment
::get
();
3338 my $authuser = $rpcenv->get_user();
3340 my $vmid = extract_param
($param, 'vmid');
3342 my $snapname = extract_param
($param, 'snapname');
3344 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3346 my $snap = $conf->{snapshots
}->{$snapname};
3348 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3353 __PACKAGE__-
>register_method({
3355 path
=> '{vmid}/snapshot/{snapname}/rollback',
3359 description
=> "Rollback VM state to specified snapshot.",
3361 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3364 additionalProperties
=> 0,
3366 node
=> get_standard_option
('pve-node'),
3367 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3368 snapname
=> get_standard_option
('pve-snapshot-name'),
3373 description
=> "the task ID.",
3378 my $rpcenv = PVE
::RPCEnvironment
::get
();
3380 my $authuser = $rpcenv->get_user();
3382 my $node = extract_param
($param, 'node');
3384 my $vmid = extract_param
($param, 'vmid');
3386 my $snapname = extract_param
($param, 'snapname');
3389 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3390 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
3393 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd);
3396 __PACKAGE__-
>register_method({
3397 name
=> 'delsnapshot',
3398 path
=> '{vmid}/snapshot/{snapname}',
3402 description
=> "Delete a VM snapshot.",
3404 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3407 additionalProperties
=> 0,
3409 node
=> get_standard_option
('pve-node'),
3410 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3411 snapname
=> get_standard_option
('pve-snapshot-name'),
3415 description
=> "For removal from config file, even if removing disk snapshots fails.",
3421 description
=> "the task ID.",
3426 my $rpcenv = PVE
::RPCEnvironment
::get
();
3428 my $authuser = $rpcenv->get_user();
3430 my $node = extract_param
($param, 'node');
3432 my $vmid = extract_param
($param, 'vmid');
3434 my $snapname = extract_param
($param, 'snapname');
3437 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
3438 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
3441 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3444 __PACKAGE__-
>register_method({
3446 path
=> '{vmid}/template',
3450 description
=> "Create a Template.",
3452 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
3453 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3456 additionalProperties
=> 0,
3458 node
=> get_standard_option
('pve-node'),
3459 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
3463 description
=> "If you want to convert only 1 disk to base image.",
3464 enum
=> [PVE
::QemuServer
::valid_drive_names
()],
3469 returns
=> { type
=> 'null'},
3473 my $rpcenv = PVE
::RPCEnvironment
::get
();
3475 my $authuser = $rpcenv->get_user();
3477 my $node = extract_param
($param, 'node');
3479 my $vmid = extract_param
($param, 'vmid');
3481 my $disk = extract_param
($param, 'disk');
3483 my $updatefn = sub {
3485 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3487 PVE
::QemuConfig-
>check_lock($conf);
3489 die "unable to create template, because VM contains snapshots\n"
3490 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
3492 die "you can't convert a template to a template\n"
3493 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
3495 die "you can't convert a VM to template if VM is running\n"
3496 if PVE
::QemuServer
::check_running
($vmid);
3499 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
3502 $conf->{template
} = 1;
3503 PVE
::QemuConfig-
>write_config($vmid, $conf);
3505 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3508 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);