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) = @_;
122 PVE
::QemuServer
::foreach_drive
($settings, sub {
123 my ($ds, $disk) = @_;
125 my $volid = $disk->{file
};
127 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
128 delete $disk->{size
};
129 $res->{$ds} = PVE
::QemuServer
::print_drive
($vmid, $disk);
130 } elsif ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/) {
131 my ($storeid, $size) = ($2 || $default_storage, $3);
132 die "no storage ID specified (and no default storage)\n" if !$storeid;
133 my $defformat = PVE
::Storage
::storage_default_format
($storecfg, $storeid);
134 my $fmt = $disk->{format
} || $defformat;
137 if ($ds eq 'efidisk0') {
139 my $ovmfvars = '/usr/share/kvm/OVMF_VARS-pure-efi.fd';
140 die "uefi vars image not found\n" if ! -f
$ovmfvars;
141 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid,
143 $disk->{file
} = $volid;
144 $disk->{size
} = 128*1024;
145 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
146 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storeid);
147 my $qemufmt = PVE
::QemuServer
::qemu_img_format
($scfg, $volname);
148 my $path = PVE
::Storage
::path
($storecfg, $volid);
149 my $efidiskcmd = ['/usr/bin/qemu-img', 'convert', '-n', '-f', 'raw', '-O', $qemufmt];
150 push @$efidiskcmd, $ovmfvars;
151 push @$efidiskcmd, $path;
153 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
155 eval { PVE
::Tools
::run_command
($efidiskcmd); };
157 die "Copying of EFI Vars image failed: $err" if $err;
159 $volid = PVE
::Storage
::vdisk_alloc
($storecfg, $storeid, $vmid,
160 $fmt, undef, $size*1024*1024);
161 $disk->{file
} = $volid;
162 $disk->{size
} = $size*1024*1024*1024;
164 push @$vollist, $volid;
165 delete $disk->{format
}; # no longer needed
166 $res->{$ds} = PVE
::QemuServer
::print_drive
($vmid, $disk);
169 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $volid);
171 my $volid_is_new = 1;
174 my $olddrive = PVE
::QemuServer
::parse_drive
($ds, $conf->{$ds});
175 $volid_is_new = undef if $olddrive->{file
} && $olddrive->{file
} eq $volid;
180 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid, 1);
182 PVE
::Storage
::activate_volumes
($storecfg, [ $volid ]) if $storeid;
184 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid);
186 die "volume $volid does not exists\n" if !$size;
188 $disk->{size
} = $size;
191 $res->{$ds} = PVE
::QemuServer
::print_drive
($vmid, $disk);
195 # free allocated images on error
197 syslog
('err', "VM $vmid creating disks failed");
198 foreach my $volid (@$vollist) {
199 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
205 # modify vm config if everything went well
206 foreach my $ds (keys %$res) {
207 $conf->{$ds} = $res->{$ds};
224 my $memoryoptions = {
230 my $hwtypeoptions = {
242 my $generaloptions = {
249 'migrate_downtime' => 1,
250 'migrate_speed' => 1,
262 my $vmpoweroptions = {
271 my $check_vm_modify_config_perm = sub {
272 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
274 return 1 if $authuser eq 'root@pam';
276 foreach my $opt (@$key_list) {
277 # disk checks need to be done somewhere else
278 next if PVE
::QemuServer
::is_valid_drivename
($opt);
279 next if $opt eq 'cdrom';
280 next if $opt =~ m/^unused\d+$/;
282 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
283 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
284 } elsif ($memoryoptions->{$opt}) {
285 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
286 } elsif ($hwtypeoptions->{$opt}) {
287 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
288 } elsif ($generaloptions->{$opt}) {
289 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
290 # special case for startup since it changes host behaviour
291 if ($opt eq 'startup') {
292 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
294 } elsif ($vmpoweroptions->{$opt}) {
295 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
296 } elsif ($diskoptions->{$opt}) {
297 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
298 } elsif ($opt =~ m/^net\d+$/) {
299 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
301 # catches usb\d+, hostpci\d+, args, lock, etc.
302 # new options will be checked here
303 die "only root can set '$opt' config\n";
310 __PACKAGE__-
>register_method({
314 description
=> "Virtual machine index (per node).",
316 description
=> "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
320 protected
=> 1, # qemu pid files are only readable by root
322 additionalProperties
=> 0,
324 node
=> get_standard_option
('pve-node'),
328 description
=> "Determine the full status of active VMs.",
338 links
=> [ { rel
=> 'child', href
=> "{vmid}" } ],
343 my $rpcenv = PVE
::RPCEnvironment
::get
();
344 my $authuser = $rpcenv->get_user();
346 my $vmstatus = PVE
::QemuServer
::vmstatus
(undef, $param->{full
});
349 foreach my $vmid (keys %$vmstatus) {
350 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
352 my $data = $vmstatus->{$vmid};
353 $data->{vmid
} = int($vmid);
362 __PACKAGE__-
>register_method({
366 description
=> "Create or restore a virtual machine.",
368 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
369 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
370 "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
371 user
=> 'all', # check inside
376 additionalProperties
=> 0,
377 properties
=> PVE
::QemuServer
::json_config_properties
(
379 node
=> get_standard_option
('pve-node'),
380 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::Cluster
::complete_next_vmid
}),
382 description
=> "The backup file.",
386 completion
=> \
&PVE
::QemuServer
::complete_backup_archives
,
388 storage
=> get_standard_option
('pve-storage-id', {
389 description
=> "Default storage.",
391 completion
=> \
&PVE
::QemuServer
::complete_storage
,
396 description
=> "Allow to overwrite existing VM.",
397 requires
=> 'archive',
402 description
=> "Assign a unique random ethernet address.",
403 requires
=> 'archive',
407 type
=> 'string', format
=> 'pve-poolid',
408 description
=> "Add the VM to the specified pool.",
418 my $rpcenv = PVE
::RPCEnvironment
::get
();
420 my $authuser = $rpcenv->get_user();
422 my $node = extract_param
($param, 'node');
424 my $vmid = extract_param
($param, 'vmid');
426 my $archive = extract_param
($param, 'archive');
428 my $storage = extract_param
($param, 'storage');
430 my $force = extract_param
($param, 'force');
432 my $unique = extract_param
($param, 'unique');
434 my $pool = extract_param
($param, 'pool');
436 my $filename = PVE
::QemuConfig-
>config_file($vmid);
438 my $storecfg = PVE
::Storage
::config
();
440 PVE
::Cluster
::check_cfs_quorum
();
442 if (defined($pool)) {
443 $rpcenv->check_pool_exist($pool);
446 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
447 if defined($storage);
449 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
451 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
453 } elsif ($archive && $force && (-f
$filename) &&
454 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
455 # OK: user has VM.Backup permissions, and want to restore an existing VM
461 &$resolve_cdrom_alias($param);
463 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
465 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
467 foreach my $opt (keys %$param) {
468 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
469 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt});
470 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
472 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
473 $param->{$opt} = PVE
::QemuServer
::print_drive
($vmid, $drive);
477 PVE
::QemuServer
::add_random_macs
($param);
479 my $keystr = join(' ', keys %$param);
480 raise_param_exc
({ archive
=> "option conflicts with other options ($keystr)"}) if $keystr;
482 if ($archive eq '-') {
483 die "pipe requires cli environment\n"
484 if $rpcenv->{type
} ne 'cli';
486 PVE
::Storage
::check_volume_access
($rpcenv, $authuser, $storecfg, $vmid, $archive);
487 $archive = PVE
::Storage
::abs_filesystem_path
($storecfg, $archive);
491 my $restorefn = sub {
492 my $vmlist = PVE
::Cluster
::get_vmlist
();
493 if ($vmlist->{ids
}->{$vmid}) {
494 my $current_node = $vmlist->{ids
}->{$vmid}->{node
};
495 if ($current_node eq $node) {
496 my $conf = PVE
::QemuConfig-
>load_config($vmid);
498 PVE
::QemuConfig-
>check_protection($conf, "unable to restore VM $vmid");
500 die "unable to restore vm $vmid - config file already exists\n"
503 die "unable to restore vm $vmid - vm is running\n"
504 if PVE
::QemuServer
::check_running
($vmid);
506 die "unable to restore vm $vmid - vm is a template\n"
507 if PVE
::QemuConfig-
>is_template($conf);
510 die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n";
515 PVE
::QemuServer
::restore_archive
($archive, $vmid, $authuser, {
518 unique
=> $unique });
520 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
523 return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd);
529 PVE
::Cluster
::check_vmid_unused
($vmid);
539 $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage);
541 # try to be smart about bootdisk
542 my @disks = PVE
::QemuServer
::valid_drive_names
();
544 foreach my $ds (reverse @disks) {
545 next if !$conf->{$ds};
546 my $disk = PVE
::QemuServer
::parse_drive
($ds, $conf->{$ds});
547 next if PVE
::QemuServer
::drive_is_cdrom
($disk);
551 if (!$conf->{bootdisk
} && $firstdisk) {
552 $conf->{bootdisk
} = $firstdisk;
555 # auto generate uuid if user did not specify smbios1 option
556 if (!$conf->{smbios1
}) {
557 my ($uuid, $uuid_str);
558 UUID
::generate
($uuid);
559 UUID
::unparse
($uuid, $uuid_str);
560 $conf->{smbios1
} = "uuid=$uuid_str";
563 PVE
::QemuConfig-
>write_config($vmid, $conf);
569 foreach my $volid (@$vollist) {
570 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
573 die "create failed - $err";
576 PVE
::AccessControl
::add_vm_to_pool
($vmid, $pool) if $pool;
579 return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd);
582 return PVE
::QemuConfig-
>lock_config_full($vmid, 1, $archive ?
$restorefn : $createfn);
585 __PACKAGE__-
>register_method({
590 description
=> "Directory index",
595 additionalProperties
=> 0,
597 node
=> get_standard_option
('pve-node'),
598 vmid
=> get_standard_option
('pve-vmid'),
606 subdir
=> { type
=> 'string' },
609 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
615 { subdir
=> 'config' },
616 { subdir
=> 'pending' },
617 { subdir
=> 'status' },
618 { subdir
=> 'unlink' },
619 { subdir
=> 'vncproxy' },
620 { subdir
=> 'migrate' },
621 { subdir
=> 'resize' },
622 { subdir
=> 'move' },
624 { subdir
=> 'rrddata' },
625 { subdir
=> 'monitor' },
626 { subdir
=> 'agent' },
627 { subdir
=> 'snapshot' },
628 { subdir
=> 'spiceproxy' },
629 { subdir
=> 'sendkey' },
630 { subdir
=> 'firewall' },
636 __PACKAGE__-
>register_method ({
637 subclass
=> "PVE::API2::Firewall::VM",
638 path
=> '{vmid}/firewall',
641 __PACKAGE__-
>register_method({
643 path
=> '{vmid}/rrd',
645 protected
=> 1, # fixme: can we avoid that?
647 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
649 description
=> "Read VM RRD statistics (returns PNG)",
651 additionalProperties
=> 0,
653 node
=> get_standard_option
('pve-node'),
654 vmid
=> get_standard_option
('pve-vmid'),
656 description
=> "Specify the time frame you are interested in.",
658 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
661 description
=> "The list of datasources you want to display.",
662 type
=> 'string', format
=> 'pve-configid-list',
665 description
=> "The RRD consolidation function",
667 enum
=> [ 'AVERAGE', 'MAX' ],
675 filename
=> { type
=> 'string' },
681 return PVE
::Cluster
::create_rrd_graph
(
682 "pve2-vm/$param->{vmid}", $param->{timeframe
},
683 $param->{ds
}, $param->{cf
});
687 __PACKAGE__-
>register_method({
689 path
=> '{vmid}/rrddata',
691 protected
=> 1, # fixme: can we avoid that?
693 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
695 description
=> "Read VM RRD statistics",
697 additionalProperties
=> 0,
699 node
=> get_standard_option
('pve-node'),
700 vmid
=> get_standard_option
('pve-vmid'),
702 description
=> "Specify the time frame you are interested in.",
704 enum
=> [ 'hour', 'day', 'week', 'month', 'year' ],
707 description
=> "The RRD consolidation function",
709 enum
=> [ 'AVERAGE', 'MAX' ],
724 return PVE
::Cluster
::create_rrd_data
(
725 "pve2-vm/$param->{vmid}", $param->{timeframe
}, $param->{cf
});
729 __PACKAGE__-
>register_method({
731 path
=> '{vmid}/config',
734 description
=> "Get current virtual machine configuration. This does not include pending configuration changes (see 'pending' API).",
736 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
739 additionalProperties
=> 0,
741 node
=> get_standard_option
('pve-node'),
742 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
744 description
=> "Get current values (instead of pending values).",
756 description
=> 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
763 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
765 delete $conf->{snapshots
};
767 if (!$param->{current
}) {
768 foreach my $opt (keys %{$conf->{pending
}}) {
769 next if $opt eq 'delete';
770 my $value = $conf->{pending
}->{$opt};
771 next if ref($value); # just to be sure
772 $conf->{$opt} = $value;
774 my $pending_delete_hash = PVE
::QemuServer
::split_flagged_list
($conf->{pending
}->{delete});
775 foreach my $opt (keys %$pending_delete_hash) {
776 delete $conf->{$opt} if $conf->{$opt};
780 delete $conf->{pending
};
785 __PACKAGE__-
>register_method({
786 name
=> 'vm_pending',
787 path
=> '{vmid}/pending',
790 description
=> "Get virtual machine configuration, including pending changes.",
792 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
795 additionalProperties
=> 0,
797 node
=> get_standard_option
('pve-node'),
798 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
807 description
=> "Configuration option name.",
811 description
=> "Current value.",
816 description
=> "Pending value.",
821 description
=> "Indicates a pending delete request if present and not 0. " .
822 "The value 2 indicates a force-delete request.",
834 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
836 my $pending_delete_hash = PVE
::QemuServer
::split_flagged_list
($conf->{pending
}->{delete});
840 foreach my $opt (keys %$conf) {
841 next if ref($conf->{$opt});
842 my $item = { key
=> $opt };
843 $item->{value
} = $conf->{$opt} if defined($conf->{$opt});
844 $item->{pending
} = $conf->{pending
}->{$opt} if defined($conf->{pending
}->{$opt});
845 $item->{delete} = ($pending_delete_hash->{$opt} ?
2 : 1) if exists $pending_delete_hash->{$opt};
849 foreach my $opt (keys %{$conf->{pending
}}) {
850 next if $opt eq 'delete';
851 next if ref($conf->{pending
}->{$opt}); # just to be sure
852 next if defined($conf->{$opt});
853 my $item = { key
=> $opt };
854 $item->{pending
} = $conf->{pending
}->{$opt};
858 while (my ($opt, $force) = each %$pending_delete_hash) {
859 next if $conf->{pending
}->{$opt}; # just to be sure
860 next if $conf->{$opt};
861 my $item = { key
=> $opt, delete => ($force ?
2 : 1)};
868 # POST/PUT {vmid}/config implementation
870 # The original API used PUT (idempotent) an we assumed that all operations
871 # are fast. But it turned out that almost any configuration change can
872 # involve hot-plug actions, or disk alloc/free. Such actions can take long
873 # time to complete and have side effects (not idempotent).
875 # The new implementation uses POST and forks a worker process. We added
876 # a new option 'background_delay'. If specified we wait up to
877 # 'background_delay' second for the worker task to complete. It returns null
878 # if the task is finished within that time, else we return the UPID.
880 my $update_vm_api = sub {
881 my ($param, $sync) = @_;
883 my $rpcenv = PVE
::RPCEnvironment
::get
();
885 my $authuser = $rpcenv->get_user();
887 my $node = extract_param
($param, 'node');
889 my $vmid = extract_param
($param, 'vmid');
891 my $digest = extract_param
($param, 'digest');
893 my $background_delay = extract_param
($param, 'background_delay');
895 my @paramarr = (); # used for log message
896 foreach my $key (keys %$param) {
897 push @paramarr, "-$key", $param->{$key};
900 my $skiplock = extract_param
($param, 'skiplock');
901 raise_param_exc
({ skiplock
=> "Only root may use this option." })
902 if $skiplock && $authuser ne 'root@pam';
904 my $delete_str = extract_param
($param, 'delete');
906 my $revert_str = extract_param
($param, 'revert');
908 my $force = extract_param
($param, 'force');
910 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
912 my $storecfg = PVE
::Storage
::config
();
914 my $defaults = PVE
::QemuServer
::load_defaults
();
916 &$resolve_cdrom_alias($param);
918 # now try to verify all parameters
921 foreach my $opt (PVE
::Tools
::split_list
($revert_str)) {
922 if (!PVE
::QemuServer
::option_exists
($opt)) {
923 raise_param_exc
({ revert
=> "unknown option '$opt'" });
926 raise_param_exc
({ delete => "you can't use '-$opt' and " .
927 "-revert $opt' at the same time" })
928 if defined($param->{$opt});
934 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
935 $opt = 'ide2' if $opt eq 'cdrom';
937 raise_param_exc
({ delete => "you can't use '-$opt' and " .
938 "-delete $opt' at the same time" })
939 if defined($param->{$opt});
941 raise_param_exc
({ revert
=> "you can't use '-delete $opt' and " .
942 "-revert $opt' at the same time" })
945 if (!PVE
::QemuServer
::option_exists
($opt)) {
946 raise_param_exc
({ delete => "unknown option '$opt'" });
952 foreach my $opt (keys %$param) {
953 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
955 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt});
956 raise_param_exc
({ $opt => "unable to parse drive options" }) if !$drive;
957 PVE
::QemuServer
::cleanup_drive_path
($opt, $storecfg, $drive);
958 $param->{$opt} = PVE
::QemuServer
::print_drive
($vmid, $drive);
959 } elsif ($opt =~ m/^net(\d+)$/) {
961 my $net = PVE
::QemuServer
::parse_net
($param->{$opt});
962 $param->{$opt} = PVE
::QemuServer
::print_net
($net);
966 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
968 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
970 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
974 my $conf = PVE
::QemuConfig-
>load_config($vmid);
976 die "checksum missmatch (file change by other user?)\n"
977 if $digest && $digest ne $conf->{digest
};
979 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
981 foreach my $opt (keys %$revert) {
982 if (defined($conf->{$opt})) {
983 $param->{$opt} = $conf->{$opt};
984 } elsif (defined($conf->{pending
}->{$opt})) {
989 if ($param->{memory
} || defined($param->{balloon
})) {
990 my $maxmem = $param->{memory
} || $conf->{pending
}->{memory
} || $conf->{memory
} || $defaults->{memory
};
991 my $balloon = defined($param->{balloon
}) ?
$param->{balloon
} : $conf->{pending
}->{balloon
} || $conf->{balloon
};
993 die "balloon value too large (must be smaller than assigned memory)\n"
994 if $balloon && $balloon > $maxmem;
997 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1001 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
1003 # write updates to pending section
1005 my $modified = {}; # record what $option we modify
1007 foreach my $opt (@delete) {
1008 $modified->{$opt} = 1;
1009 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1010 if (!defined($conf->{$opt})) {
1011 warn "cannot delete '$opt' - not set in current configuration!\n";
1012 $modified->{$opt} = 0;
1016 if ($opt =~ m/^unused/) {
1017 my $drive = PVE
::QemuServer
::parse_drive
($opt, $conf->{$opt});
1018 PVE
::QemuConfig-
>check_protection($conf, "can't remove unused disk '$drive->{file}'");
1019 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1020 if (PVE
::QemuServer
::try_deallocate_drive
($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1021 delete $conf->{$opt};
1022 PVE
::QemuConfig-
>write_config($vmid, $conf);
1024 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1025 PVE
::QemuConfig-
>check_protection($conf, "can't remove drive '$opt'");
1026 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1027 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1028 if defined($conf->{pending
}->{$opt});
1029 PVE
::QemuServer
::vmconfig_delete_pending_option
($conf, $opt, $force);
1030 PVE
::QemuConfig-
>write_config($vmid, $conf);
1032 PVE
::QemuServer
::vmconfig_delete_pending_option
($conf, $opt, $force);
1033 PVE
::QemuConfig-
>write_config($vmid, $conf);
1037 foreach my $opt (keys %$param) { # add/change
1038 $modified->{$opt} = 1;
1039 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1040 next if defined($conf->{pending
}->{$opt}) && ($param->{$opt} eq $conf->{pending
}->{$opt}); # skip if nothing changed
1042 if (PVE
::QemuServer
::is_valid_drivename
($opt)) {
1043 my $drive = PVE
::QemuServer
::parse_drive
($opt, $param->{$opt});
1044 if (PVE
::QemuServer
::drive_is_cdrom
($drive)) { # CDROM
1045 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1047 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1049 PVE
::QemuServer
::vmconfig_register_unused_drive
($storecfg, $vmid, $conf, PVE
::QemuServer
::parse_drive
($opt, $conf->{pending
}->{$opt}))
1050 if defined($conf->{pending
}->{$opt});
1052 &$create_disks($rpcenv, $authuser, $conf->{pending
}, $storecfg, $vmid, undef, {$opt => $param->{$opt}});
1053 } elsif ($opt eq "replicate") {
1054 # check if all volumes have replicate feature
1055 PVE
::QemuServer
::get_replicatable_volumes
($storecfg, $conf);
1056 my $repl = PVE
::JSONSchema
::check_format
('pve-replicate', $param->{opt
});
1057 PVE
::Cluster
::check_node_exists
($repl->{target
});
1058 $conf->{$opt} = $param->{$opt};
1060 $conf->{pending
}->{$opt} = $param->{$opt};
1062 PVE
::QemuServer
::vmconfig_undelete_pending_option
($conf, $opt);
1063 PVE
::QemuConfig-
>write_config($vmid, $conf);
1066 # remove pending changes when nothing changed
1067 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1068 my $changes = PVE
::QemuServer
::vmconfig_cleanup_pending
($conf);
1069 PVE
::QemuConfig-
>write_config($vmid, $conf) if $changes;
1071 return if !scalar(keys %{$conf->{pending
}});
1073 my $running = PVE
::QemuServer
::check_running
($vmid);
1075 # apply pending changes
1077 $conf = PVE
::QemuConfig-
>load_config($vmid); # update/reload
1081 PVE
::QemuServer
::vmconfig_hotplug_pending
($vmid, $conf, $storecfg, $modified, $errors);
1082 raise_param_exc
($errors) if scalar(keys %$errors);
1084 PVE
::QemuServer
::vmconfig_apply_pending
($vmid, $conf, $storecfg, $running);
1094 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
1096 if ($background_delay) {
1098 # Note: It would be better to do that in the Event based HTTPServer
1099 # to avoid blocking call to sleep.
1101 my $end_time = time() + $background_delay;
1103 my $task = PVE
::Tools
::upid_decode
($upid);
1106 while (time() < $end_time) {
1107 $running = PVE
::ProcFSTools
::check_process_running
($task->{pid
}, $task->{pstart
});
1109 sleep(1); # this gets interrupted when child process ends
1113 my $status = PVE
::Tools
::upid_read_status
($upid);
1114 return undef if $status eq 'OK';
1123 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
1126 my $vm_config_perm_list = [
1131 'VM.Config.Network',
1133 'VM.Config.Options',
1136 __PACKAGE__-
>register_method({
1137 name
=> 'update_vm_async',
1138 path
=> '{vmid}/config',
1142 description
=> "Set virtual machine options (asynchrounous API).",
1144 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1147 additionalProperties
=> 0,
1148 properties
=> PVE
::QemuServer
::json_config_properties
(
1150 node
=> get_standard_option
('pve-node'),
1151 vmid
=> get_standard_option
('pve-vmid'),
1152 skiplock
=> get_standard_option
('skiplock'),
1154 type
=> 'string', format
=> 'pve-configid-list',
1155 description
=> "A list of settings you want to delete.",
1159 type
=> 'string', format
=> 'pve-configid-list',
1160 description
=> "Revert a pending change.",
1165 description
=> $opt_force_description,
1167 requires
=> 'delete',
1171 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1175 background_delay
=> {
1177 description
=> "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1188 code
=> $update_vm_api,
1191 __PACKAGE__-
>register_method({
1192 name
=> 'update_vm',
1193 path
=> '{vmid}/config',
1197 description
=> "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1199 check
=> ['perm', '/vms/{vmid}', $vm_config_perm_list, any
=> 1],
1202 additionalProperties
=> 0,
1203 properties
=> PVE
::QemuServer
::json_config_properties
(
1205 node
=> get_standard_option
('pve-node'),
1206 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1207 skiplock
=> get_standard_option
('skiplock'),
1209 type
=> 'string', format
=> 'pve-configid-list',
1210 description
=> "A list of settings you want to delete.",
1214 type
=> 'string', format
=> 'pve-configid-list',
1215 description
=> "Revert a pending change.",
1220 description
=> $opt_force_description,
1222 requires
=> 'delete',
1226 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1232 returns
=> { type
=> 'null' },
1235 &$update_vm_api($param, 1);
1241 __PACKAGE__-
>register_method({
1242 name
=> 'destroy_vm',
1247 description
=> "Destroy the vm (also delete all used/owned volumes).",
1249 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1252 additionalProperties
=> 0,
1254 node
=> get_standard_option
('pve-node'),
1255 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
1256 skiplock
=> get_standard_option
('skiplock'),
1265 my $rpcenv = PVE
::RPCEnvironment
::get
();
1267 my $authuser = $rpcenv->get_user();
1269 my $vmid = $param->{vmid
};
1271 my $skiplock = $param->{skiplock
};
1272 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1273 if $skiplock && $authuser ne 'root@pam';
1276 my $conf = PVE
::QemuConfig-
>load_config($vmid);
1278 my $storecfg = PVE
::Storage
::config
();
1280 PVE
::QemuConfig-
>check_protection($conf, "can't remove VM $vmid");
1282 die "unable to remove VM $vmid - used in HA resources\n"
1283 if PVE
::HA
::Config
::vm_is_ha_managed
($vmid);
1285 # early tests (repeat after locking)
1286 die "VM $vmid is running - destroy failed\n"
1287 if PVE
::QemuServer
::check_running
($vmid);
1292 syslog
('info', "destroy VM $vmid: $upid\n");
1294 PVE
::QemuServer
::vm_destroy
($storecfg, $vmid, $skiplock);
1296 PVE
::AccessControl
::remove_vm_access
($vmid);
1298 PVE
::Firewall
::remove_vmfw_conf
($vmid);
1301 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1304 __PACKAGE__-
>register_method({
1306 path
=> '{vmid}/unlink',
1310 description
=> "Unlink/delete disk images.",
1312 check
=> [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
1315 additionalProperties
=> 0,
1317 node
=> get_standard_option
('pve-node'),
1318 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
1320 type
=> 'string', format
=> 'pve-configid-list',
1321 description
=> "A list of disk IDs you want to delete.",
1325 description
=> $opt_force_description,
1330 returns
=> { type
=> 'null'},
1334 $param->{delete} = extract_param
($param, 'idlist');
1336 __PACKAGE__-
>update_vm($param);
1343 __PACKAGE__-
>register_method({
1345 path
=> '{vmid}/vncproxy',
1349 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1351 description
=> "Creates a TCP VNC proxy connections.",
1353 additionalProperties
=> 0,
1355 node
=> get_standard_option
('pve-node'),
1356 vmid
=> get_standard_option
('pve-vmid'),
1360 description
=> "starts websockify instead of vncproxy",
1365 additionalProperties
=> 0,
1367 user
=> { type
=> 'string' },
1368 ticket
=> { type
=> 'string' },
1369 cert
=> { type
=> 'string' },
1370 port
=> { type
=> 'integer' },
1371 upid
=> { type
=> 'string' },
1377 my $rpcenv = PVE
::RPCEnvironment
::get
();
1379 my $authuser = $rpcenv->get_user();
1381 my $vmid = $param->{vmid
};
1382 my $node = $param->{node
};
1383 my $websocket = $param->{websocket
};
1385 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # check if VM exists
1387 my $authpath = "/vms/$vmid";
1389 my $ticket = PVE
::AccessControl
::assemble_vnc_ticket
($authuser, $authpath);
1391 $sslcert = PVE
::Tools
::file_get_contents
("/etc/pve/pve-root-ca.pem", 8192)
1394 my ($remip, $family);
1397 if ($node ne 'localhost' && $node ne PVE
::INotify
::nodename
()) {
1398 ($remip, $family) = PVE
::Cluster
::remote_node_ip
($node);
1399 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
1400 $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
1402 $family = PVE
::Tools
::get_host_address_family
($node);
1405 my $port = PVE
::Tools
::next_vnc_port
($family);
1412 syslog
('info', "starting vnc proxy $upid\n");
1416 if ($conf->{vga
} && ($conf->{vga
} =~ m/^serial\d+$/)) {
1418 die "Websocket mode is not supported in vga serial mode!" if $websocket;
1420 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga
} ];
1421 #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
1422 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
1423 '-timeout', $timeout, '-authpath', $authpath,
1424 '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
1425 PVE
::Tools
::run_command
($cmd);
1428 $ENV{LC_PVE_TICKET
} = $ticket if $websocket; # set ticket with "qm vncproxy"
1430 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
1432 my $sock = IO
::Socket
::IP-
>new(
1436 GetAddrInfoFlags
=> 0,
1437 ) or die "failed to create socket: $!\n";
1438 # Inside the worker we shouldn't have any previous alarms
1439 # running anyway...:
1441 local $SIG{ALRM
} = sub { die "connection timed out\n" };
1443 accept(my $cli, $sock) or die "connection failed: $!\n";
1446 if (PVE
::Tools
::run_command
($cmd,
1447 output
=> '>&'.fileno($cli),
1448 input
=> '<&'.fileno($cli),
1451 die "Failed to run vncproxy.\n";
1458 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1460 PVE
::Tools
::wait_for_vnc_port
($port);
1471 __PACKAGE__-
>register_method({
1472 name
=> 'vncwebsocket',
1473 path
=> '{vmid}/vncwebsocket',
1476 description
=> "You also need to pass a valid ticket (vncticket).",
1477 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1479 description
=> "Opens a weksocket for VNC traffic.",
1481 additionalProperties
=> 0,
1483 node
=> get_standard_option
('pve-node'),
1484 vmid
=> get_standard_option
('pve-vmid'),
1486 description
=> "Ticket from previous call to vncproxy.",
1491 description
=> "Port number returned by previous vncproxy call.",
1501 port
=> { type
=> 'string' },
1507 my $rpcenv = PVE
::RPCEnvironment
::get
();
1509 my $authuser = $rpcenv->get_user();
1511 my $vmid = $param->{vmid
};
1512 my $node = $param->{node
};
1514 my $authpath = "/vms/$vmid";
1516 PVE
::AccessControl
::verify_vnc_ticket
($param->{vncticket
}, $authuser, $authpath);
1518 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node); # VM exists ?
1520 # Note: VNC ports are acessible from outside, so we do not gain any
1521 # security if we verify that $param->{port} belongs to VM $vmid. This
1522 # check is done by verifying the VNC ticket (inside VNC protocol).
1524 my $port = $param->{port
};
1526 return { port
=> $port };
1529 __PACKAGE__-
>register_method({
1530 name
=> 'spiceproxy',
1531 path
=> '{vmid}/spiceproxy',
1536 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1538 description
=> "Returns a SPICE configuration to connect to the VM.",
1540 additionalProperties
=> 0,
1542 node
=> get_standard_option
('pve-node'),
1543 vmid
=> get_standard_option
('pve-vmid'),
1544 proxy
=> get_standard_option
('spice-proxy', { optional
=> 1 }),
1547 returns
=> get_standard_option
('remote-viewer-config'),
1551 my $rpcenv = PVE
::RPCEnvironment
::get
();
1553 my $authuser = $rpcenv->get_user();
1555 my $vmid = $param->{vmid
};
1556 my $node = $param->{node
};
1557 my $proxy = $param->{proxy
};
1559 my $conf = PVE
::QemuConfig-
>load_config($vmid, $node);
1560 my $title = "VM $vmid";
1561 $title .= " - ". $conf->{name
} if $conf->{name
};
1563 my $port = PVE
::QemuServer
::spice_port
($vmid);
1565 my ($ticket, undef, $remote_viewer_config) =
1566 PVE
::AccessControl
::remote_viewer_config
($authuser, $vmid, $node, $proxy, $title, $port);
1568 PVE
::QemuServer
::vm_mon_cmd
($vmid, "set_password", protocol
=> 'spice', password
=> $ticket);
1569 PVE
::QemuServer
::vm_mon_cmd
($vmid, "expire_password", protocol
=> 'spice', time => "+30");
1571 return $remote_viewer_config;
1574 __PACKAGE__-
>register_method({
1576 path
=> '{vmid}/status',
1579 description
=> "Directory index",
1584 additionalProperties
=> 0,
1586 node
=> get_standard_option
('pve-node'),
1587 vmid
=> get_standard_option
('pve-vmid'),
1595 subdir
=> { type
=> 'string' },
1598 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
1604 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1607 { subdir
=> 'current' },
1608 { subdir
=> 'start' },
1609 { subdir
=> 'stop' },
1615 __PACKAGE__-
>register_method({
1616 name
=> 'vm_status',
1617 path
=> '{vmid}/status/current',
1620 protected
=> 1, # qemu pid files are only readable by root
1621 description
=> "Get virtual machine status.",
1623 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1626 additionalProperties
=> 0,
1628 node
=> get_standard_option
('pve-node'),
1629 vmid
=> get_standard_option
('pve-vmid'),
1632 returns
=> { type
=> 'object' },
1637 my $conf = PVE
::QemuConfig-
>load_config($param->{vmid
});
1639 my $vmstatus = PVE
::QemuServer
::vmstatus
($param->{vmid
}, 1);
1640 my $status = $vmstatus->{$param->{vmid
}};
1642 $status->{ha
} = PVE
::HA
::Config
::get_service_status
("vm:$param->{vmid}");
1644 $status->{spice
} = 1 if PVE
::QemuServer
::vga_conf_has_spice
($conf->{vga
});
1649 __PACKAGE__-
>register_method({
1651 path
=> '{vmid}/status/start',
1655 description
=> "Start virtual machine.",
1657 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1660 additionalProperties
=> 0,
1662 node
=> get_standard_option
('pve-node'),
1663 vmid
=> get_standard_option
('pve-vmid',
1664 { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
1665 skiplock
=> get_standard_option
('skiplock'),
1666 stateuri
=> get_standard_option
('pve-qm-stateuri'),
1667 migratedfrom
=> get_standard_option
('pve-node',{ optional
=> 1 }),
1670 enum
=> ['secure', 'insecure'],
1671 description
=> "Migration traffic is encrypted using an SSH " .
1672 "tunnel by default. On secure, completely private networks " .
1673 "this can be disabled to increase performance.",
1676 migration_network
=> {
1677 type
=> 'string', format
=> 'CIDR',
1678 description
=> "CIDR of the (sub) network that is used for migration.",
1681 machine
=> get_standard_option
('pve-qm-machine'),
1683 description
=> "Target storage for the migration. (Can be '1' to use the same storage id as on the source node.)",
1695 my $rpcenv = PVE
::RPCEnvironment
::get
();
1697 my $authuser = $rpcenv->get_user();
1699 my $node = extract_param
($param, 'node');
1701 my $vmid = extract_param
($param, 'vmid');
1703 my $machine = extract_param
($param, 'machine');
1705 my $stateuri = extract_param
($param, 'stateuri');
1706 raise_param_exc
({ stateuri
=> "Only root may use this option." })
1707 if $stateuri && $authuser ne 'root@pam';
1709 my $skiplock = extract_param
($param, 'skiplock');
1710 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1711 if $skiplock && $authuser ne 'root@pam';
1713 my $migratedfrom = extract_param
($param, 'migratedfrom');
1714 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
1715 if $migratedfrom && $authuser ne 'root@pam';
1717 my $migration_type = extract_param
($param, 'migration_type');
1718 raise_param_exc
({ migration_type
=> "Only root may use this option." })
1719 if $migration_type && $authuser ne 'root@pam';
1721 my $migration_network = extract_param
($param, 'migration_network');
1722 raise_param_exc
({ migration_network
=> "Only root may use this option." })
1723 if $migration_network && $authuser ne 'root@pam';
1725 my $targetstorage = extract_param
($param, 'targetstorage');
1726 raise_param_exc
({ targetstorage
=> "Only root may use this option." })
1727 if $targetstorage && $authuser ne 'root@pam';
1729 raise_param_exc
({ targetstorage
=> "targetstorage can only by used with migratedfrom." })
1730 if $targetstorage && !$migratedfrom;
1732 # read spice ticket from STDIN
1734 if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type
} eq 'cli')) {
1735 if (defined(my $line = <>)) {
1737 $spice_ticket = $line;
1741 PVE
::Cluster
::check_cfs_quorum
();
1743 my $storecfg = PVE
::Storage
::config
();
1745 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && !$stateuri &&
1746 $rpcenv->{type
} ne 'ha') {
1751 my $service = "vm:$vmid";
1753 my $cmd = ['ha-manager', 'set', $service, '--state', 'started'];
1755 print "Executing HA start for VM $vmid\n";
1757 PVE
::Tools
::run_command
($cmd);
1762 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
1769 syslog
('info', "start VM $vmid: $upid\n");
1771 PVE
::QemuServer
::vm_start
($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef,
1772 $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage);
1777 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
1781 __PACKAGE__-
>register_method({
1783 path
=> '{vmid}/status/stop',
1787 description
=> "Stop virtual machine. The qemu process will exit immediately. This" .
1788 "is akin to pulling the power plug of a running computer and may damage the VM data",
1790 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1793 additionalProperties
=> 0,
1795 node
=> get_standard_option
('pve-node'),
1796 vmid
=> get_standard_option
('pve-vmid',
1797 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
1798 skiplock
=> get_standard_option
('skiplock'),
1799 migratedfrom
=> get_standard_option
('pve-node', { optional
=> 1 }),
1801 description
=> "Wait maximal timeout seconds.",
1807 description
=> "Do not deactivate storage volumes.",
1820 my $rpcenv = PVE
::RPCEnvironment
::get
();
1822 my $authuser = $rpcenv->get_user();
1824 my $node = extract_param
($param, 'node');
1826 my $vmid = extract_param
($param, 'vmid');
1828 my $skiplock = extract_param
($param, 'skiplock');
1829 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1830 if $skiplock && $authuser ne 'root@pam';
1832 my $keepActive = extract_param
($param, 'keepActive');
1833 raise_param_exc
({ keepActive
=> "Only root may use this option." })
1834 if $keepActive && $authuser ne 'root@pam';
1836 my $migratedfrom = extract_param
($param, 'migratedfrom');
1837 raise_param_exc
({ migratedfrom
=> "Only root may use this option." })
1838 if $migratedfrom && $authuser ne 'root@pam';
1841 my $storecfg = PVE
::Storage
::config
();
1843 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && ($rpcenv->{type
} ne 'ha') && !defined($migratedfrom)) {
1848 my $service = "vm:$vmid";
1850 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
1852 print "Executing HA stop for VM $vmid\n";
1854 PVE
::Tools
::run_command
($cmd);
1859 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
1865 syslog
('info', "stop VM $vmid: $upid\n");
1867 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0,
1868 $param->{timeout
}, 0, 1, $keepActive, $migratedfrom);
1873 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
1877 __PACKAGE__-
>register_method({
1879 path
=> '{vmid}/status/reset',
1883 description
=> "Reset virtual machine.",
1885 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1888 additionalProperties
=> 0,
1890 node
=> get_standard_option
('pve-node'),
1891 vmid
=> get_standard_option
('pve-vmid',
1892 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
1893 skiplock
=> get_standard_option
('skiplock'),
1902 my $rpcenv = PVE
::RPCEnvironment
::get
();
1904 my $authuser = $rpcenv->get_user();
1906 my $node = extract_param
($param, 'node');
1908 my $vmid = extract_param
($param, 'vmid');
1910 my $skiplock = extract_param
($param, 'skiplock');
1911 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1912 if $skiplock && $authuser ne 'root@pam';
1914 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
1919 PVE
::QemuServer
::vm_reset
($vmid, $skiplock);
1924 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
1927 __PACKAGE__-
>register_method({
1928 name
=> 'vm_shutdown',
1929 path
=> '{vmid}/status/shutdown',
1933 description
=> "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
1934 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
1936 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
1939 additionalProperties
=> 0,
1941 node
=> get_standard_option
('pve-node'),
1942 vmid
=> get_standard_option
('pve-vmid',
1943 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
1944 skiplock
=> get_standard_option
('skiplock'),
1946 description
=> "Wait maximal timeout seconds.",
1952 description
=> "Make sure the VM stops.",
1958 description
=> "Do not deactivate storage volumes.",
1971 my $rpcenv = PVE
::RPCEnvironment
::get
();
1973 my $authuser = $rpcenv->get_user();
1975 my $node = extract_param
($param, 'node');
1977 my $vmid = extract_param
($param, 'vmid');
1979 my $skiplock = extract_param
($param, 'skiplock');
1980 raise_param_exc
({ skiplock
=> "Only root may use this option." })
1981 if $skiplock && $authuser ne 'root@pam';
1983 my $keepActive = extract_param
($param, 'keepActive');
1984 raise_param_exc
({ keepActive
=> "Only root may use this option." })
1985 if $keepActive && $authuser ne 'root@pam';
1987 my $storecfg = PVE
::Storage
::config
();
1991 # if vm is paused, do not shutdown (but stop if forceStop = 1)
1992 # otherwise, we will infer a shutdown command, but run into the timeout,
1993 # then when the vm is resumed, it will instantly shutdown
1995 # checking the qmp status here to get feedback to the gui/cli/api
1996 # and the status query should not take too long
1999 $qmpstatus = PVE
::QemuServer
::vm_qmp_command
($vmid, { execute
=> "query-status" }, 0);
2003 if (!$err && $qmpstatus->{status
} eq "paused") {
2004 if ($param->{forceStop
}) {
2005 warn "VM is paused - stop instead of shutdown\n";
2008 die "VM is paused - cannot shutdown\n";
2012 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) &&
2013 ($rpcenv->{type
} ne 'ha')) {
2018 my $service = "vm:$vmid";
2020 my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped'];
2022 print "Executing HA stop for VM $vmid\n";
2024 PVE
::Tools
::run_command
($cmd);
2029 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2036 syslog
('info', "shutdown VM $vmid: $upid\n");
2038 PVE
::QemuServer
::vm_stop
($storecfg, $vmid, $skiplock, 0, $param->{timeout
},
2039 $shutdown, $param->{forceStop
}, $keepActive);
2044 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2048 __PACKAGE__-
>register_method({
2049 name
=> 'vm_suspend',
2050 path
=> '{vmid}/status/suspend',
2054 description
=> "Suspend virtual machine.",
2056 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2059 additionalProperties
=> 0,
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'),
2073 my $rpcenv = PVE
::RPCEnvironment
::get
();
2075 my $authuser = $rpcenv->get_user();
2077 my $node = extract_param
($param, 'node');
2079 my $vmid = extract_param
($param, 'vmid');
2081 my $skiplock = extract_param
($param, 'skiplock');
2082 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2083 if $skiplock && $authuser ne 'root@pam';
2085 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid);
2090 syslog
('info', "suspend VM $vmid: $upid\n");
2092 PVE
::QemuServer
::vm_suspend
($vmid, $skiplock);
2097 return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd);
2100 __PACKAGE__-
>register_method({
2101 name
=> 'vm_resume',
2102 path
=> '{vmid}/status/resume',
2106 description
=> "Resume virtual machine.",
2108 check
=> ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2111 additionalProperties
=> 0,
2113 node
=> get_standard_option
('pve-node'),
2114 vmid
=> get_standard_option
('pve-vmid',
2115 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2116 skiplock
=> get_standard_option
('skiplock'),
2117 nocheck
=> { type
=> 'boolean', optional
=> 1 },
2127 my $rpcenv = PVE
::RPCEnvironment
::get
();
2129 my $authuser = $rpcenv->get_user();
2131 my $node = extract_param
($param, 'node');
2133 my $vmid = extract_param
($param, 'vmid');
2135 my $skiplock = extract_param
($param, 'skiplock');
2136 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2137 if $skiplock && $authuser ne 'root@pam';
2139 my $nocheck = extract_param
($param, 'nocheck');
2141 die "VM $vmid not running\n" if !PVE
::QemuServer
::check_running
($vmid, $nocheck);
2146 syslog
('info', "resume VM $vmid: $upid\n");
2148 PVE
::QemuServer
::vm_resume
($vmid, $skiplock, $nocheck);
2153 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
2156 __PACKAGE__-
>register_method({
2157 name
=> 'vm_sendkey',
2158 path
=> '{vmid}/sendkey',
2162 description
=> "Send key event to virtual machine.",
2164 check
=> ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2167 additionalProperties
=> 0,
2169 node
=> get_standard_option
('pve-node'),
2170 vmid
=> get_standard_option
('pve-vmid',
2171 { completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2172 skiplock
=> get_standard_option
('skiplock'),
2174 description
=> "The key (qemu monitor encoding).",
2179 returns
=> { type
=> 'null'},
2183 my $rpcenv = PVE
::RPCEnvironment
::get
();
2185 my $authuser = $rpcenv->get_user();
2187 my $node = extract_param
($param, 'node');
2189 my $vmid = extract_param
($param, 'vmid');
2191 my $skiplock = extract_param
($param, 'skiplock');
2192 raise_param_exc
({ skiplock
=> "Only root may use this option." })
2193 if $skiplock && $authuser ne 'root@pam';
2195 PVE
::QemuServer
::vm_sendkey
($vmid, $skiplock, $param->{key
});
2200 __PACKAGE__-
>register_method({
2201 name
=> 'vm_feature',
2202 path
=> '{vmid}/feature',
2206 description
=> "Check if feature for virtual machine is available.",
2208 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2211 additionalProperties
=> 0,
2213 node
=> get_standard_option
('pve-node'),
2214 vmid
=> get_standard_option
('pve-vmid'),
2216 description
=> "Feature to check.",
2218 enum
=> [ 'snapshot', 'clone', 'copy' ],
2220 snapname
=> get_standard_option
('pve-snapshot-name', {
2228 hasFeature
=> { type
=> 'boolean' },
2231 items
=> { type
=> 'string' },
2238 my $node = extract_param
($param, 'node');
2240 my $vmid = extract_param
($param, 'vmid');
2242 my $snapname = extract_param
($param, 'snapname');
2244 my $feature = extract_param
($param, 'feature');
2246 my $running = PVE
::QemuServer
::check_running
($vmid);
2248 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2251 my $snap = $conf->{snapshots
}->{$snapname};
2252 die "snapshot '$snapname' does not exist\n" if !defined($snap);
2255 my $storecfg = PVE
::Storage
::config
();
2257 my $nodelist = PVE
::QemuServer
::shared_nodes
($conf, $storecfg);
2258 my $hasFeature = PVE
::QemuConfig-
>has_feature($feature, $conf, $storecfg, $snapname, $running);
2261 hasFeature
=> $hasFeature,
2262 nodes
=> [ keys %$nodelist ],
2266 __PACKAGE__-
>register_method({
2268 path
=> '{vmid}/clone',
2272 description
=> "Create a copy of virtual machine/template.",
2274 description
=> "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
2275 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
2276 "'Datastore.AllocateSpace' on any used storage.",
2279 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
2281 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
2282 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param
=> 'pool'],
2287 additionalProperties
=> 0,
2289 node
=> get_standard_option
('pve-node'),
2290 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2291 newid
=> get_standard_option
('pve-vmid', { description
=> 'VMID for the clone.' }),
2294 type
=> 'string', format
=> 'dns-name',
2295 description
=> "Set a name for the new VM.",
2300 description
=> "Description for the new VM.",
2304 type
=> 'string', format
=> 'pve-poolid',
2305 description
=> "Add the new VM to the specified pool.",
2307 snapname
=> get_standard_option
('pve-snapshot-name', {
2310 storage
=> get_standard_option
('pve-storage-id', {
2311 description
=> "Target storage for full clone.",
2316 description
=> "Target format for file storage.",
2320 enum
=> [ 'raw', 'qcow2', 'vmdk'],
2325 description
=> "Create a full copy of all disk. This is always done when " .
2326 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
2329 target
=> get_standard_option
('pve-node', {
2330 description
=> "Target node. Only allowed if the original VM is on shared storage.",
2341 my $rpcenv = PVE
::RPCEnvironment
::get
();
2343 my $authuser = $rpcenv->get_user();
2345 my $node = extract_param
($param, 'node');
2347 my $vmid = extract_param
($param, 'vmid');
2349 my $newid = extract_param
($param, 'newid');
2351 my $pool = extract_param
($param, 'pool');
2353 if (defined($pool)) {
2354 $rpcenv->check_pool_exist($pool);
2357 my $snapname = extract_param
($param, 'snapname');
2359 my $storage = extract_param
($param, 'storage');
2361 my $format = extract_param
($param, 'format');
2363 my $target = extract_param
($param, 'target');
2365 my $localnode = PVE
::INotify
::nodename
();
2367 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
2369 PVE
::Cluster
::check_node_exists
($target) if $target;
2371 my $storecfg = PVE
::Storage
::config
();
2374 # check if storage is enabled on local node
2375 PVE
::Storage
::storage_check_enabled
($storecfg, $storage);
2377 # check if storage is available on target node
2378 PVE
::Storage
::storage_check_node
($storecfg, $storage, $target);
2379 # clone only works if target storage is shared
2380 my $scfg = PVE
::Storage
::storage_config
($storecfg, $storage);
2381 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared
};
2385 PVE
::Cluster
::check_cfs_quorum
();
2387 my $running = PVE
::QemuServer
::check_running
($vmid) || 0;
2389 # exclusive lock if VM is running - else shared lock is enough;
2390 my $shared_lock = $running ?
0 : 1;
2394 # do all tests after lock
2395 # we also try to do all tests before we fork the worker
2397 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2399 PVE
::QemuConfig-
>check_lock($conf);
2401 my $verify_running = PVE
::QemuServer
::check_running
($vmid) || 0;
2403 die "unexpected state change\n" if $verify_running != $running;
2405 die "snapshot '$snapname' does not exist\n"
2406 if $snapname && !defined( $conf->{snapshots
}->{$snapname});
2408 my $oldconf = $snapname ?
$conf->{snapshots
}->{$snapname} : $conf;
2410 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
2412 die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm;
2414 my $conffile = PVE
::QemuConfig-
>config_file($newid);
2416 die "unable to create VM $newid: config file already exists\n"
2419 my $newconf = { lock => 'clone' };
2424 foreach my $opt (keys %$oldconf) {
2425 my $value = $oldconf->{$opt};
2427 # do not copy snapshot related info
2428 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
2429 $opt eq 'vmstate' || $opt eq 'snapstate';
2431 # no need to copy unused images, because VMID(owner) changes anyways
2432 next if $opt =~ m/^unused\d+$/;
2434 # always change MAC! address
2435 if ($opt =~ m/^net(\d+)$/) {
2436 my $net = PVE
::QemuServer
::parse_net
($value);
2437 my $dc = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
2438 $net->{macaddr
} = PVE
::Tools
::random_ether_addr
($dc->{mac_prefix
});
2439 $newconf->{$opt} = PVE
::QemuServer
::print_net
($net);
2440 } elsif (PVE
::QemuServer
::is_valid_drivename
($opt)) {
2441 my $drive = PVE
::QemuServer
::parse_drive
($opt, $value);
2442 die "unable to parse drive options for '$opt'\n" if !$drive;
2443 if (PVE
::QemuServer
::drive_is_cdrom
($drive)) {
2444 $newconf->{$opt} = $value; # simply copy configuration
2446 if ($param->{full
}) {
2447 die "Full clone feature is not available"
2448 if !PVE
::Storage
::volume_has_feature
($storecfg, 'copy', $drive->{file
}, $snapname, $running);
2449 $fullclone->{$opt} = 1;
2451 # not full means clone instead of copy
2452 die "Linked clone feature is not available"
2453 if !PVE
::Storage
::volume_has_feature
($storecfg, 'clone', $drive->{file
}, $snapname, $running);
2455 $drives->{$opt} = $drive;
2456 push @$vollist, $drive->{file
};
2459 # copy everything else
2460 $newconf->{$opt} = $value;
2464 # auto generate a new uuid
2465 my ($uuid, $uuid_str);
2466 UUID
::generate
($uuid);
2467 UUID
::unparse
($uuid, $uuid_str);
2468 my $smbios1 = PVE
::QemuServer
::parse_smbios1
($newconf->{smbios1
} || '');
2469 $smbios1->{uuid
} = $uuid_str;
2470 $newconf->{smbios1
} = PVE
::QemuServer
::print_smbios1
($smbios1);
2472 delete $newconf->{template
};
2474 if ($param->{name
}) {
2475 $newconf->{name
} = $param->{name
};
2477 if ($oldconf->{name
}) {
2478 $newconf->{name
} = "Copy-of-$oldconf->{name}";
2480 $newconf->{name
} = "Copy-of-VM-$vmid";
2484 if ($param->{description
}) {
2485 $newconf->{description
} = $param->{description
};
2488 # create empty/temp config - this fails if VM already exists on other node
2489 PVE
::Tools
::file_set_contents
($conffile, "# qmclone temporary file\nlock: clone\n");
2494 my $newvollist = [];
2498 local $SIG{INT
} = $SIG{TERM
} = $SIG{QUIT
} = $SIG{HUP
} = sub { die "interrupted by signal\n"; };
2500 PVE
::Storage
::activate_volumes
($storecfg, $vollist, $snapname);
2502 my $total_jobs = scalar(keys %{$drives});
2505 foreach my $opt (keys %$drives) {
2506 my $drive = $drives->{$opt};
2507 my $skipcomplete = ($total_jobs != $i); # finish after last drive
2509 my $newdrive = PVE
::QemuServer
::clone_disk
($storecfg, $vmid, $running, $opt, $drive, $snapname,
2510 $newid, $storage, $format, $fullclone->{$opt}, $newvollist,
2511 $jobs, $skipcomplete, $oldconf->{agent
});
2513 $newconf->{$opt} = PVE
::QemuServer
::print_drive
($vmid, $newdrive);
2515 PVE
::QemuConfig-
>write_config($newid, $newconf);
2519 delete $newconf->{lock};
2520 PVE
::QemuConfig-
>write_config($newid, $newconf);
2523 # always deactivate volumes - avoid lvm LVs to be active on several nodes
2524 PVE
::Storage
::deactivate_volumes
($storecfg, $vollist, $snapname) if !$running;
2525 PVE
::Storage
::deactivate_volumes
($storecfg, $newvollist);
2527 my $newconffile = PVE
::QemuConfig-
>config_file($newid, $target);
2528 die "Failed to move config to node '$target' - rename failed: $!\n"
2529 if !rename($conffile, $newconffile);
2532 PVE
::AccessControl
::add_vm_to_pool
($newid, $pool) if $pool;
2537 eval { PVE
::QemuServer
::qemu_blockjobs_cancel
($vmid, $jobs) };
2539 sleep 1; # some storage like rbd need to wait before release volume - really?
2541 foreach my $volid (@$newvollist) {
2542 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
2545 die "clone failed: $err";
2551 PVE
::Firewall
::clone_vmfw_conf
($vmid, $newid);
2553 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
2556 return PVE
::QemuConfig-
>lock_config_mode($vmid, 1, $shared_lock, sub {
2557 # Aquire exclusive lock lock for $newid
2558 return PVE
::QemuConfig-
>lock_config_full($newid, 1, $clonefn);
2563 __PACKAGE__-
>register_method({
2564 name
=> 'move_vm_disk',
2565 path
=> '{vmid}/move_disk',
2569 description
=> "Move volume to different storage.",
2571 description
=> "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
2573 ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2574 ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
2578 additionalProperties
=> 0,
2580 node
=> get_standard_option
('pve-node'),
2581 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2584 description
=> "The disk you want to move.",
2585 enum
=> [ PVE
::QemuServer
::valid_drive_names
() ],
2587 storage
=> get_standard_option
('pve-storage-id', {
2588 description
=> "Target storage.",
2589 completion
=> \
&PVE
::QemuServer
::complete_storage
,
2593 description
=> "Target Format.",
2594 enum
=> [ 'raw', 'qcow2', 'vmdk' ],
2599 description
=> "Delete the original disk after successful copy. By default the original disk is kept as unused disk.",
2605 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2613 description
=> "the task ID.",
2618 my $rpcenv = PVE
::RPCEnvironment
::get
();
2620 my $authuser = $rpcenv->get_user();
2622 my $node = extract_param
($param, 'node');
2624 my $vmid = extract_param
($param, 'vmid');
2626 my $digest = extract_param
($param, 'digest');
2628 my $disk = extract_param
($param, 'disk');
2630 my $storeid = extract_param
($param, 'storage');
2632 my $format = extract_param
($param, 'format');
2634 my $storecfg = PVE
::Storage
::config
();
2636 my $updatefn = sub {
2638 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2640 PVE
::QemuConfig-
>check_lock($conf);
2642 die "checksum missmatch (file change by other user?)\n"
2643 if $digest && $digest ne $conf->{digest
};
2645 die "disk '$disk' does not exist\n" if !$conf->{$disk};
2647 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
2649 my $old_volid = $drive->{file
} || die "disk '$disk' has no associated volume\n";
2651 die "you can't move a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
2654 my ($oldstoreid, $oldvolname) = PVE
::Storage
::parse_volume_id
($old_volid);
2655 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
2659 die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid &&
2660 (!$format || !$oldfmt || $oldfmt eq $format);
2662 # this only checks snapshots because $disk is passed!
2663 my $snapshotted = PVE
::QemuServer
::is_volume_in_use
($storecfg, $conf, $disk, $old_volid);
2664 die "you can't move a disk with snapshots and delete the source\n"
2665 if $snapshotted && $param->{delete};
2667 PVE
::Cluster
::log_msg
('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid");
2669 my $running = PVE
::QemuServer
::check_running
($vmid);
2671 PVE
::Storage
::activate_volumes
($storecfg, [ $drive->{file
} ]);
2675 my $newvollist = [];
2678 local $SIG{INT
} = $SIG{TERM
} = $SIG{QUIT
} = $SIG{HUP
} = sub { die "interrupted by signal\n"; };
2680 warn "moving disk with snapshots, snapshots will not be moved!\n"
2683 my $newdrive = PVE
::QemuServer
::clone_disk
($storecfg, $vmid, $running, $disk, $drive, undef,
2684 $vmid, $storeid, $format, 1, $newvollist);
2686 $conf->{$disk} = PVE
::QemuServer
::print_drive
($vmid, $newdrive);
2688 PVE
::QemuConfig-
>add_unused_volume($conf, $old_volid) if !$param->{delete};
2690 # convert moved disk to base if part of template
2691 PVE
::QemuServer
::template_create
($vmid, $conf, $disk)
2692 if PVE
::QemuConfig-
>is_template($conf);
2694 PVE
::QemuConfig-
>write_config($vmid, $conf);
2697 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2698 PVE
::Storage
::deactivate_volumes
($storecfg, [ $newdrive->{file
} ])
2705 foreach my $volid (@$newvollist) {
2706 eval { PVE
::Storage
::vdisk_free
($storecfg, $volid); };
2709 die "storage migration failed: $err";
2712 if ($param->{delete}) {
2714 PVE
::Storage
::deactivate_volumes
($storecfg, [$old_volid]);
2715 PVE
::Storage
::vdisk_free
($storecfg, $old_volid);
2721 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
2724 return PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
2727 __PACKAGE__-
>register_method({
2728 name
=> 'migrate_vm',
2729 path
=> '{vmid}/migrate',
2733 description
=> "Migrate virtual machine. Creates a new migration task.",
2735 check
=> ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
2738 additionalProperties
=> 0,
2740 node
=> get_standard_option
('pve-node'),
2741 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2742 target
=> get_standard_option
('pve-node', {
2743 description
=> "Target node.",
2744 completion
=> \
&PVE
::Cluster
::complete_migration_target
,
2748 description
=> "Use online/live migration.",
2753 description
=> "Allow to migrate VMs which use local devices. Only root may use this option.",
2758 enum
=> ['secure', 'insecure'],
2759 description
=> "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2762 migration_network
=> {
2763 type
=> 'string', format
=> 'CIDR',
2764 description
=> "CIDR of the (sub) network that is used for migration.",
2767 "with-local-disks" => {
2769 description
=> "Enable live storage migration for local disk",
2772 targetstorage
=> get_standard_option
('pve-storage-id', {
2773 description
=> "Default target storage.",
2775 completion
=> \
&PVE
::QemuServer
::complete_storage
,
2781 description
=> "the task ID.",
2786 my $rpcenv = PVE
::RPCEnvironment
::get
();
2788 my $authuser = $rpcenv->get_user();
2790 my $target = extract_param
($param, 'target');
2792 my $localnode = PVE
::INotify
::nodename
();
2793 raise_param_exc
({ target
=> "target is local node."}) if $target eq $localnode;
2795 PVE
::Cluster
::check_cfs_quorum
();
2797 PVE
::Cluster
::check_node_exists
($target);
2799 my $targetip = PVE
::Cluster
::remote_node_ip
($target);
2801 my $vmid = extract_param
($param, 'vmid');
2803 raise_param_exc
({ targetstorage
=> "Live storage migration can only be done online." })
2804 if !$param->{online
} && $param->{targetstorage
};
2806 raise_param_exc
({ force
=> "Only root may use this option." })
2807 if $param->{force
} && $authuser ne 'root@pam';
2809 raise_param_exc
({ migration_type
=> "Only root may use this option." })
2810 if $param->{migration_type
} && $authuser ne 'root@pam';
2812 # allow root only until better network permissions are available
2813 raise_param_exc
({ migration_network
=> "Only root may use this option." })
2814 if $param->{migration_network
} && $authuser ne 'root@pam';
2817 my $conf = PVE
::QemuConfig-
>load_config($vmid);
2819 # try to detect errors early
2821 PVE
::QemuConfig-
>check_lock($conf);
2823 if (PVE
::QemuServer
::check_running
($vmid)) {
2824 die "cant migrate running VM without --online\n"
2825 if !$param->{online
};
2828 my $storecfg = PVE
::Storage
::config
();
2830 if( $param->{targetstorage
}) {
2831 PVE
::Storage
::storage_check_node
($storecfg, $param->{targetstorage
}, $target);
2833 PVE
::QemuServer
::check_storage_availability
($storecfg, $conf, $target);
2836 if (PVE
::HA
::Config
::vm_is_ha_managed
($vmid) && $rpcenv->{type
} ne 'ha') {
2841 my $service = "vm:$vmid";
2843 my $cmd = ['ha-manager', 'migrate', $service, $target];
2845 print "Executing HA migrate for VM $vmid to node $target\n";
2847 PVE
::Tools
::run_command
($cmd);
2852 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
2859 PVE
::QemuMigrate-
>migrate($target, $targetip, $vmid, $param);
2862 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd);
2867 __PACKAGE__-
>register_method({
2869 path
=> '{vmid}/monitor',
2873 description
=> "Execute Qemu monitor commands.",
2875 description
=> "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
2876 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2879 additionalProperties
=> 0,
2881 node
=> get_standard_option
('pve-node'),
2882 vmid
=> get_standard_option
('pve-vmid'),
2885 description
=> "The monitor command.",
2889 returns
=> { type
=> 'string'},
2893 my $rpcenv = PVE
::RPCEnvironment
::get
();
2894 my $authuser = $rpcenv->get_user();
2897 my $command = shift;
2898 return $command =~ m/^\s*info(\s+|$)/
2899 || $command =~ m/^\s*help\s*$/;
2902 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
2903 if !&$is_ro($param->{command
});
2905 my $vmid = $param->{vmid
};
2907 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
2911 $res = PVE
::QemuServer
::vm_human_monitor_command
($vmid, $param->{command
});
2913 $res = "ERROR: $@" if $@;
2918 my $guest_agent_commands = [
2926 'network-get-interfaces',
2929 'get-memory-blocks',
2930 'get-memory-block-info',
2937 __PACKAGE__-
>register_method({
2939 path
=> '{vmid}/agent',
2943 description
=> "Execute Qemu Guest Agent commands.",
2945 check
=> ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
2948 additionalProperties
=> 0,
2950 node
=> get_standard_option
('pve-node'),
2951 vmid
=> get_standard_option
('pve-vmid', {
2952 completion
=> \
&PVE
::QemuServer
::complete_vmid_running
}),
2955 description
=> "The QGA command.",
2956 enum
=> $guest_agent_commands,
2962 description
=> "Returns an object with a single `result` property. The type of that
2963 property depends on the executed command.",
2968 my $vmid = $param->{vmid
};
2970 my $conf = PVE
::QemuConfig-
>load_config ($vmid); # check if VM exists
2972 die "No Qemu Guest Agent\n" if !defined($conf->{agent
});
2973 die "VM $vmid is not running\n" if !PVE
::QemuServer
::check_running
($vmid);
2975 my $cmd = $param->{command
};
2977 my $res = PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-$cmd");
2979 return { result
=> $res };
2982 __PACKAGE__-
>register_method({
2983 name
=> 'resize_vm',
2984 path
=> '{vmid}/resize',
2988 description
=> "Extend volume size.",
2990 check
=> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2993 additionalProperties
=> 0,
2995 node
=> get_standard_option
('pve-node'),
2996 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
2997 skiplock
=> get_standard_option
('skiplock'),
3000 description
=> "The disk you want to resize.",
3001 enum
=> [PVE
::QemuServer
::valid_drive_names
()],
3005 pattern
=> '\+?\d+(\.\d+)?[KMGT]?',
3006 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.",
3010 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
3016 returns
=> { type
=> 'null'},
3020 my $rpcenv = PVE
::RPCEnvironment
::get
();
3022 my $authuser = $rpcenv->get_user();
3024 my $node = extract_param
($param, 'node');
3026 my $vmid = extract_param
($param, 'vmid');
3028 my $digest = extract_param
($param, 'digest');
3030 my $disk = extract_param
($param, 'disk');
3032 my $sizestr = extract_param
($param, 'size');
3034 my $skiplock = extract_param
($param, 'skiplock');
3035 raise_param_exc
({ skiplock
=> "Only root may use this option." })
3036 if $skiplock && $authuser ne 'root@pam';
3038 my $storecfg = PVE
::Storage
::config
();
3040 my $updatefn = sub {
3042 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3044 die "checksum missmatch (file change by other user?)\n"
3045 if $digest && $digest ne $conf->{digest
};
3046 PVE
::QemuConfig-
>check_lock($conf) if !$skiplock;
3048 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3050 my $drive = PVE
::QemuServer
::parse_drive
($disk, $conf->{$disk});
3052 my (undef, undef, undef, undef, undef, undef, $format) =
3053 PVE
::Storage
::parse_volname
($storecfg, $drive->{file
});
3055 die "can't resize volume: $disk if snapshot exists\n"
3056 if %{$conf->{snapshots
}} && $format eq 'qcow2';
3058 my $volid = $drive->{file
};
3060 die "disk '$disk' has no associated volume\n" if !$volid;
3062 die "you can't resize a cdrom\n" if PVE
::QemuServer
::drive_is_cdrom
($drive);
3064 my ($storeid, $volname) = PVE
::Storage
::parse_volume_id
($volid);
3066 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
3068 PVE
::Storage
::activate_volumes
($storecfg, [$volid]);
3069 my $size = PVE
::Storage
::volume_size_info
($storecfg, $volid, 5);
3071 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
3072 my ($ext, $newsize, $unit) = ($1, $2, $4);
3075 $newsize = $newsize * 1024;
3076 } elsif ($unit eq 'M') {
3077 $newsize = $newsize * 1024 * 1024;
3078 } elsif ($unit eq 'G') {
3079 $newsize = $newsize * 1024 * 1024 * 1024;
3080 } elsif ($unit eq 'T') {
3081 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
3084 $newsize += $size if $ext;
3085 $newsize = int($newsize);
3087 die "shrinking disks is not supported\n" if $newsize < $size;
3089 return if $size == $newsize;
3091 PVE
::Cluster
::log_msg
('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
3093 PVE
::QemuServer
::qemu_block_resize
($vmid, "drive-$disk", $storecfg, $volid, $newsize);
3095 $drive->{size
} = $newsize;
3096 $conf->{$disk} = PVE
::QemuServer
::print_drive
($vmid, $drive);
3098 PVE
::QemuConfig-
>write_config($vmid, $conf);
3101 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
3105 __PACKAGE__-
>register_method({
3106 name
=> 'snapshot_list',
3107 path
=> '{vmid}/snapshot',
3109 description
=> "List all snapshots.",
3111 check
=> ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3114 protected
=> 1, # qemu pid files are only readable by root
3116 additionalProperties
=> 0,
3118 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3119 node
=> get_standard_option
('pve-node'),
3128 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
3133 my $vmid = $param->{vmid
};
3135 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3136 my $snaphash = $conf->{snapshots
} || {};
3140 foreach my $name (keys %$snaphash) {
3141 my $d = $snaphash->{$name};
3144 snaptime
=> $d->{snaptime
} || 0,
3145 vmstate
=> $d->{vmstate
} ?
1 : 0,
3146 description
=> $d->{description
} || '',
3148 $item->{parent
} = $d->{parent
} if $d->{parent
};
3149 $item->{snapstate
} = $d->{snapstate
} if $d->{snapstate
};
3153 my $running = PVE
::QemuServer
::check_running
($vmid, 1) ?
1 : 0;
3154 my $current = { name
=> 'current', digest
=> $conf->{digest
}, running
=> $running };
3155 $current->{parent
} = $conf->{parent
} if $conf->{parent
};
3157 push @$res, $current;
3162 __PACKAGE__-
>register_method({
3164 path
=> '{vmid}/snapshot',
3168 description
=> "Snapshot a VM.",
3170 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3173 additionalProperties
=> 0,
3175 node
=> get_standard_option
('pve-node'),
3176 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3177 snapname
=> get_standard_option
('pve-snapshot-name'),
3181 description
=> "Save the vmstate",
3186 description
=> "A textual description or comment.",
3192 description
=> "the task ID.",
3197 my $rpcenv = PVE
::RPCEnvironment
::get
();
3199 my $authuser = $rpcenv->get_user();
3201 my $node = extract_param
($param, 'node');
3203 my $vmid = extract_param
($param, 'vmid');
3205 my $snapname = extract_param
($param, 'snapname');
3207 die "unable to use snapshot name 'current' (reserved name)\n"
3208 if $snapname eq 'current';
3211 PVE
::Cluster
::log_msg
('info', $authuser, "snapshot VM $vmid: $snapname");
3212 PVE
::QemuConfig-
>snapshot_create($vmid, $snapname, $param->{vmstate
},
3213 $param->{description
});
3216 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
3219 __PACKAGE__-
>register_method({
3220 name
=> 'snapshot_cmd_idx',
3221 path
=> '{vmid}/snapshot/{snapname}',
3228 additionalProperties
=> 0,
3230 vmid
=> get_standard_option
('pve-vmid'),
3231 node
=> get_standard_option
('pve-node'),
3232 snapname
=> get_standard_option
('pve-snapshot-name'),
3241 links
=> [ { rel
=> 'child', href
=> "{cmd}" } ],
3248 push @$res, { cmd
=> 'rollback' };
3249 push @$res, { cmd
=> 'config' };
3254 __PACKAGE__-
>register_method({
3255 name
=> 'update_snapshot_config',
3256 path
=> '{vmid}/snapshot/{snapname}/config',
3260 description
=> "Update snapshot metadata.",
3262 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3265 additionalProperties
=> 0,
3267 node
=> get_standard_option
('pve-node'),
3268 vmid
=> get_standard_option
('pve-vmid'),
3269 snapname
=> get_standard_option
('pve-snapshot-name'),
3273 description
=> "A textual description or comment.",
3277 returns
=> { type
=> 'null' },
3281 my $rpcenv = PVE
::RPCEnvironment
::get
();
3283 my $authuser = $rpcenv->get_user();
3285 my $vmid = extract_param
($param, 'vmid');
3287 my $snapname = extract_param
($param, 'snapname');
3289 return undef if !defined($param->{description
});
3291 my $updatefn = sub {
3293 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3295 PVE
::QemuConfig-
>check_lock($conf);
3297 my $snap = $conf->{snapshots
}->{$snapname};
3299 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3301 $snap->{description
} = $param->{description
} if defined($param->{description
});
3303 PVE
::QemuConfig-
>write_config($vmid, $conf);
3306 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);
3311 __PACKAGE__-
>register_method({
3312 name
=> 'get_snapshot_config',
3313 path
=> '{vmid}/snapshot/{snapname}/config',
3316 description
=> "Get snapshot configuration",
3318 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3321 additionalProperties
=> 0,
3323 node
=> get_standard_option
('pve-node'),
3324 vmid
=> get_standard_option
('pve-vmid'),
3325 snapname
=> get_standard_option
('pve-snapshot-name'),
3328 returns
=> { type
=> "object" },
3332 my $rpcenv = PVE
::RPCEnvironment
::get
();
3334 my $authuser = $rpcenv->get_user();
3336 my $vmid = extract_param
($param, 'vmid');
3338 my $snapname = extract_param
($param, 'snapname');
3340 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3342 my $snap = $conf->{snapshots
}->{$snapname};
3344 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3349 __PACKAGE__-
>register_method({
3351 path
=> '{vmid}/snapshot/{snapname}/rollback',
3355 description
=> "Rollback VM state to specified snapshot.",
3357 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3360 additionalProperties
=> 0,
3362 node
=> get_standard_option
('pve-node'),
3363 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3364 snapname
=> get_standard_option
('pve-snapshot-name'),
3369 description
=> "the task ID.",
3374 my $rpcenv = PVE
::RPCEnvironment
::get
();
3376 my $authuser = $rpcenv->get_user();
3378 my $node = extract_param
($param, 'node');
3380 my $vmid = extract_param
($param, 'vmid');
3382 my $snapname = extract_param
($param, 'snapname');
3385 PVE
::Cluster
::log_msg
('info', $authuser, "rollback snapshot VM $vmid: $snapname");
3386 PVE
::QemuConfig-
>snapshot_rollback($vmid, $snapname);
3389 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd);
3392 __PACKAGE__-
>register_method({
3393 name
=> 'delsnapshot',
3394 path
=> '{vmid}/snapshot/{snapname}',
3398 description
=> "Delete a VM snapshot.",
3400 check
=> ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
3403 additionalProperties
=> 0,
3405 node
=> get_standard_option
('pve-node'),
3406 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid
}),
3407 snapname
=> get_standard_option
('pve-snapshot-name'),
3411 description
=> "For removal from config file, even if removing disk snapshots fails.",
3417 description
=> "the task ID.",
3422 my $rpcenv = PVE
::RPCEnvironment
::get
();
3424 my $authuser = $rpcenv->get_user();
3426 my $node = extract_param
($param, 'node');
3428 my $vmid = extract_param
($param, 'vmid');
3430 my $snapname = extract_param
($param, 'snapname');
3433 PVE
::Cluster
::log_msg
('info', $authuser, "delete snapshot VM $vmid: $snapname");
3434 PVE
::QemuConfig-
>snapshot_delete($vmid, $snapname, $param->{force
});
3437 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
3440 __PACKAGE__-
>register_method({
3442 path
=> '{vmid}/template',
3446 description
=> "Create a Template.",
3448 description
=> "You need 'VM.Allocate' permissions on /vms/{vmid}",
3449 check
=> [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
3452 additionalProperties
=> 0,
3454 node
=> get_standard_option
('pve-node'),
3455 vmid
=> get_standard_option
('pve-vmid', { completion
=> \
&PVE
::QemuServer
::complete_vmid_stopped
}),
3459 description
=> "If you want to convert only 1 disk to base image.",
3460 enum
=> [PVE
::QemuServer
::valid_drive_names
()],
3465 returns
=> { type
=> 'null'},
3469 my $rpcenv = PVE
::RPCEnvironment
::get
();
3471 my $authuser = $rpcenv->get_user();
3473 my $node = extract_param
($param, 'node');
3475 my $vmid = extract_param
($param, 'vmid');
3477 my $disk = extract_param
($param, 'disk');
3479 my $updatefn = sub {
3481 my $conf = PVE
::QemuConfig-
>load_config($vmid);
3483 PVE
::QemuConfig-
>check_lock($conf);
3485 die "unable to create template, because VM contains snapshots\n"
3486 if $conf->{snapshots
} && scalar(keys %{$conf->{snapshots
}});
3488 die "you can't convert a template to a template\n"
3489 if PVE
::QemuConfig-
>is_template($conf) && !$disk;
3491 die "you can't convert a VM to template if VM is running\n"
3492 if PVE
::QemuServer
::check_running
($vmid);
3495 PVE
::QemuServer
::template_create
($vmid, $conf, $disk);
3498 $conf->{template
} = 1;
3499 PVE
::QemuConfig-
>write_config($vmid, $conf);
3501 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
3504 PVE
::QemuConfig-
>lock_config($vmid, $updatefn);