]> git.proxmox.com Git - pve-container.git/blame - src/PVE/API2/LXC.pm
api: create/update vm: clamp cpu unit value
[pve-container.git] / src / PVE / API2 / LXC.pm
CommitLineData
f76a2828
DM
1package PVE::API2::LXC;
2
3use strict;
4use warnings;
5
6use PVE::SafeSyslog;
7use PVE::Tools qw(extract_param run_command);
0620edda 8use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
f76a2828 9use PVE::INotify;
9c2d4ce9 10use PVE::Cluster qw(cfs_read_file);
d949527f 11use PVE::RRD;
8b076579 12use PVE::DataCenterConfig;
f76a2828 13use PVE::AccessControl;
2f9b5ead 14use PVE::Firewall;
f76a2828
DM
15use PVE::Storage;
16use PVE::RESTHandler;
17use PVE::RPCEnvironment;
c5a8e04f 18use PVE::ReplicationConfig;
f76a2828 19use PVE::LXC;
7af97ad5 20use PVE::LXC::Create;
6f42807e 21use PVE::LXC::Migrate;
56462053 22use PVE::GuestHelpers;
c5a09704 23use PVE::VZDump::Plugin;
52389a07
DM
24use PVE::API2::LXC::Config;
25use PVE::API2::LXC::Status;
26use PVE::API2::LXC::Snapshot;
f76a2828
DM
27use PVE::JSONSchema qw(get_standard_option);
28use base qw(PVE::RESTHandler);
29
6c2e9377
WB
30BEGIN {
31 if (!$ENV{PVE_GENERATING_DOCS}) {
32 require PVE::HA::Env::PVE2;
33 import PVE::HA::Env::PVE2;
34 require PVE::HA::Config;
35 import PVE::HA::Config;
36 }
37}
f76a2828 38
e90ddc4c
FG
39my $check_storage_access_migrate = sub {
40 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
41
42 PVE::Storage::storage_check_enabled($storecfg, $storage, $node);
43
44 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
45
46 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
47 die "storage '$storage' does not support CT rootdirs\n"
48 if !$scfg->{content}->{rootdir};
49};
50
52389a07
DM
51__PACKAGE__->register_method ({
52 subclass => "PVE::API2::LXC::Config",
fa91e46f 53 path => '{vmid}/config',
52389a07 54});
f76a2828 55
52389a07
DM
56__PACKAGE__->register_method ({
57 subclass => "PVE::API2::LXC::Status",
58 path => '{vmid}/status',
59});
f76a2828 60
52389a07
DM
61__PACKAGE__->register_method ({
62 subclass => "PVE::API2::LXC::Snapshot",
63 path => '{vmid}/snapshot',
64});
f76a2828 65
52389a07
DM
66__PACKAGE__->register_method ({
67 subclass => "PVE::API2::Firewall::CT",
68 path => '{vmid}/firewall',
69});
1e6c8d5b 70
f76a2828 71__PACKAGE__->register_method({
5c752bbf
DM
72 name => 'vmlist',
73 path => '',
f76a2828
DM
74 method => 'GET',
75 description => "LXC container index (per node).",
76 permissions => {
77 description => "Only list CTs where you have VM.Audit permissons on /vms/<vmid>.",
78 user => 'all',
79 },
80 proxyto => 'node',
81 protected => 1, # /proc files are only readable by root
82 parameters => {
83 additionalProperties => 0,
84 properties => {
85 node => get_standard_option('pve-node'),
86 },
87 },
88 returns => {
89 type => 'array',
90 items => {
91 type => "object",
8233d33d 92 properties => $PVE::LXC::vmstatus_return_properties,
f76a2828
DM
93 },
94 links => [ { rel => 'child', href => "{vmid}" } ],
95 },
96 code => sub {
97 my ($param) = @_;
98
99 my $rpcenv = PVE::RPCEnvironment::get();
100 my $authuser = $rpcenv->get_user();
101
102 my $vmstatus = PVE::LXC::vmstatus();
103
104 my $res = [];
105 foreach my $vmid (keys %$vmstatus) {
106 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
107
108 my $data = $vmstatus->{$vmid};
f76a2828
DM
109 push @$res, $data;
110 }
111
112 return $res;
5c752bbf 113
f76a2828
DM
114 }});
115
9c2d4ce9 116__PACKAGE__->register_method({
5c752bbf
DM
117 name => 'create_vm',
118 path => '',
9c2d4ce9
DM
119 method => 'POST',
120 description => "Create or restore a container.",
121 permissions => {
122 user => 'all', # check inside
123 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
124 "For restore, it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
125 "You also need 'Datastore.AllocateSpace' permissions on the storage.",
126 },
127 protected => 1,
128 proxyto => 'node',
129 parameters => {
130 additionalProperties => 0,
1b4cf758 131 properties => PVE::LXC::Config->json_config_properties({
9c2d4ce9 132 node => get_standard_option('pve-node'),
781e26b2 133 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
9c2d4ce9
DM
134 ostemplate => {
135 description => "The OS template or backup file.",
5c752bbf 136 type => 'string',
9c2d4ce9 137 maxLength => 255,
68e8f3c5 138 completion => \&PVE::LXC::complete_os_templates,
9c2d4ce9 139 },
5c752bbf
DM
140 password => {
141 optional => 1,
9c2d4ce9
DM
142 type => 'string',
143 description => "Sets root password inside container.",
168d6b07 144 minLength => 5,
9c2d4ce9
DM
145 },
146 storage => get_standard_option('pve-storage-id', {
eb35f9c0 147 description => "Default Storage.",
9c2d4ce9
DM
148 default => 'local',
149 optional => 1,
c5362cda 150 completion => \&PVE::Storage::complete_storage_enabled,
9c2d4ce9
DM
151 }),
152 force => {
5c752bbf 153 optional => 1,
9c2d4ce9
DM
154 type => 'boolean',
155 description => "Allow to overwrite existing container.",
156 },
157 restore => {
5c752bbf 158 optional => 1,
9c2d4ce9
DM
159 type => 'boolean',
160 description => "Mark this as restore task.",
161 },
43912111
CE
162 unique => {
163 optional => 1,
164 type => 'boolean',
165 description => "Assign a unique random ethernet address.",
166 requires => 'restore',
167 },
5c752bbf 168 pool => {
9c2d4ce9
DM
169 optional => 1,
170 type => 'string', format => 'pve-poolid',
171 description => "Add the VM to the specified pool.",
172 },
7c78b6cc
WB
173 'ignore-unpack-errors' => {
174 optional => 1,
175 type => 'boolean',
176 description => "Ignore errors when extracting the template.",
177 },
34ddbf08
FG
178 'ssh-public-keys' => {
179 optional => 1,
180 type => 'string',
181 description => "Setup public SSH keys (one key per line, " .
182 "OpenSSH format).",
183 },
f9b4407e 184 bwlimit => {
8ead60ef 185 description => "Override I/O bandwidth limit (in KiB/s).",
f9b4407e
WB
186 optional => 1,
187 type => 'number',
188 minimum => '0',
41e9b65c 189 default => 'restore limit from datacenter or storage config',
f9b4407e 190 },
9d0225fb
TL
191 start => {
192 optional => 1,
193 type => 'boolean',
194 default => 0,
195 description => "Start the CT after its creation finished successfully.",
196 },
9c2d4ce9
DM
197 }),
198 },
5c752bbf 199 returns => {
9c2d4ce9
DM
200 type => 'string',
201 },
202 code => sub {
203 my ($param) = @_;
204
61783321
TL
205 PVE::Cluster::check_cfs_quorum();
206
9c2d4ce9 207 my $rpcenv = PVE::RPCEnvironment::get();
9c2d4ce9
DM
208 my $authuser = $rpcenv->get_user();
209
210 my $node = extract_param($param, 'node');
9c2d4ce9 211 my $vmid = extract_param($param, 'vmid');
7c78b6cc 212 my $ignore_unpack_errors = extract_param($param, 'ignore-unpack-errors');
f9b4407e 213 my $bwlimit = extract_param($param, 'bwlimit');
9d0225fb
TL
214 my $start_after_create = extract_param($param, 'start');
215
67afe46e 216 my $basecfg_fn = PVE::LXC::Config->config_file($vmid);
9c2d4ce9
DM
217 my $same_container_exists = -f $basecfg_fn;
218
425b62cb
WB
219 # 'unprivileged' is read-only, so we can't pass it to update_pct_config
220 my $unprivileged = extract_param($param, 'unprivileged');
9c2d4ce9 221 my $restore = extract_param($param, 'restore');
43912111 222 my $unique = extract_param($param, 'unique');
9c2d4ce9 223
8fe13f0c
FE
224 $param->{cpuunits} = PVE::CGroup::clamp_cpu_shares($param->{cpuunits})
225 if defined($param->{cpuunits}); # clamp value depending on cgroup version
226
39170644
FG
227 # used to skip firewall config restore if user lacks permission
228 my $skip_fw_config_restore = 0;
229
148d1cb4
DM
230 if ($restore) {
231 # fixme: limit allowed parameters
148d1cb4 232 }
351bc0c4 233
9c2d4ce9
DM
234 my $force = extract_param($param, 'force');
235
236 if (!($same_container_exists && $restore && $force)) {
237 PVE::Cluster::check_vmid_unused($vmid);
e22af68f 238 } else {
61783321 239 die "can't overwrite running container\n" if PVE::LXC::check_running($vmid);
67afe46e
FG
240 my $conf = PVE::LXC::Config->load_config($vmid);
241 PVE::LXC::Config->check_protection($conf, "unable to restore CT $vmid");
9c2d4ce9 242 }
5c752bbf 243
9c2d4ce9 244 my $password = extract_param($param, 'password');
34ddbf08 245 my $ssh_keys = extract_param($param, 'ssh-public-keys');
2130286f 246 PVE::Tools::validate_ssh_public_keys($ssh_keys) if defined($ssh_keys);
34ddbf08 247
27916659 248 my $pool = extract_param($param, 'pool');
f4b4443d 249 $rpcenv->check_pool_exist($pool) if defined($pool);
9c2d4ce9
DM
250
251 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
252 # OK
253 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
254 # OK
255 } elsif ($restore && $force && $same_container_exists &&
256 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
257 # OK: user has VM.Backup permissions, and want to restore an existing VM
39170644
FG
258
259 # we don't want to restore a container-provided FW conf in this case
260 # since the user is lacking permission to configure the container's FW
261 $skip_fw_config_restore = 1;
b9623ef8
DC
262
263 # error out if a user tries to change from unprivileged to privileged
264 # explicit change is checked here, implicit is checked down below or happening in root-only paths
265 my $conf = PVE::LXC::Config->load_config($vmid);
266 if ($conf->{unprivileged} && defined($unprivileged) && !$unprivileged) {
267 raise_perm_exc("cannot change from unprivileged to privileged without VM.Allocate");
268 }
9c2d4ce9
DM
269 } else {
270 raise_perm_exc();
271 }
272
9eb69152 273 my $ostemplate = extract_param($param, 'ostemplate');
bb6afcb0
DM
274 my $storage = extract_param($param, 'storage') // 'local';
275
de41bced 276 PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, $pool, undef, $param, [], $unprivileged);
9eb69152 277
bb6afcb0 278 my $storage_cfg = cfs_read_file("storage.cfg");
9c2d4ce9 279
9c2d4ce9 280 my $archive;
9c2d4ce9 281 if ($ostemplate eq '-') {
351bc0c4
TM
282 die "pipe requires cli environment\n"
283 if $rpcenv->{type} ne 'cli';
284 die "pipe can only be used with restore tasks\n"
148d1cb4
DM
285 if !$restore;
286 $archive = '-';
b51a98d4 287 die "restore from pipe requires rootfs parameter\n" if !defined($param->{rootfs});
9c2d4ce9 288 } else {
d387c0be
FE
289 my $content_type = $restore ? 'backup' : 'vztmpl';
290 PVE::Storage::check_volume_access(
291 $rpcenv,
292 $authuser,
293 $storage_cfg,
294 $vmid,
295 $ostemplate,
296 $content_type,
297 );
99a0bcb0 298 $archive = $ostemplate;
9c2d4ce9
DM
299 }
300
f9b4407e 301 my %used_storages;
bb6afcb0
DM
302 my $check_and_activate_storage = sub {
303 my ($sid) = @_;
304
a1c27f86 305 my $scfg = PVE::Storage::storage_check_enabled($storage_cfg, $sid, $node);
bb6afcb0
DM
306
307 raise_param_exc({ storage => "storage '$sid' does not support container directories"})
308 if !$scfg->{content}->{rootdir};
309
310 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
311
312 PVE::Storage::activate_storage($storage_cfg, $sid);
f9b4407e 313 $used_storages{$sid} = 1;
bb6afcb0
DM
314 };
315
9c2d4ce9 316 my $conf = {};
5b4657d0 317
99132ce0
WB
318 my $is_root = $authuser eq 'root@pam';
319
b51a98d4 320 my $no_disk_param = {};
db18c1e4
FG
321 my $mp_param = {};
322 my $storage_only_mode = 1;
b51a98d4 323 foreach my $opt (keys %$param) {
78ccc99b
DM
324 my $value = $param->{$opt};
325 if ($opt eq 'rootfs' || $opt =~ m/^mp\d+$/) {
326 # allow to use simple numbers (add default storage in that case)
db18c1e4
FG
327 if ($value =~ m/^\d+(\.\d+)?$/) {
328 $mp_param->{$opt} = "$storage:$value";
329 } else {
330 $mp_param->{$opt} = $value;
331 }
332 $storage_only_mode = 0;
a9dd8015
FG
333 } elsif ($opt =~ m/^unused\d+$/) {
334 warn "ignoring '$opt', cannot create/restore with unused volume\n";
335 delete $param->{$opt};
78ccc99b
DM
336 } else {
337 $no_disk_param->{$opt} = $value;
338 }
b51a98d4 339 }
bb6afcb0 340
235dbdf3 341 die "mount points configured, but 'rootfs' not set - aborting\n"
db18c1e4
FG
342 if !$storage_only_mode && !defined($mp_param->{rootfs});
343
bb6afcb0 344 # check storage access, activate storage
db18c1e4 345 my $delayed_mp_param = {};
015740e6 346 PVE::LXC::Config->foreach_volume($mp_param, sub {
bb6afcb0
DM
347 my ($ms, $mountpoint) = @_;
348
349 my $volid = $mountpoint->{volume};
350 my $mp = $mountpoint->{mp};
351
e2007ac2
DM
352 if ($mountpoint->{type} ne 'volume') { # bind or device
353 die "Only root can pass arbitrary filesystem paths.\n"
99132ce0 354 if !$is_root;
e2007ac2
DM
355 } else {
356 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
357 &$check_and_activate_storage($sid);
358 }
bb6afcb0
DM
359 });
360
361 # check/activate default storage
db18c1e4 362 &$check_and_activate_storage($storage) if !defined($mp_param->{rootfs});
bb6afcb0 363
1b4cf758 364 PVE::LXC::Config->update_pct_config($vmid, $conf, 0, $no_disk_param);
9c2d4ce9 365
425b62cb
WB
366 $conf->{unprivileged} = 1 if $unprivileged;
367
61783321 368 my $emsg = $restore ? "unable to restore CT $vmid -" : "unable to create CT $vmid -";
bccaa371 369
61783321
TL
370 eval { PVE::LXC::Config->create_and_lock_config($vmid, $force) };
371 die "$emsg $@" if $@;
bccaa371 372
b9623ef8
DC
373 my $remove_lock = 1;
374
61783321
TL
375 my $code = sub {
376 my $old_conf = PVE::LXC::Config->load_config($vmid);
a2d3e5c3 377 my $was_template;
bccaa371 378
27916659 379 eval {
5a7a51d0
WB
380 my $orig_mp_param; # only used if $restore
381 if ($restore) {
61783321 382 die "can't overwrite running container\n" if PVE::LXC::check_running($vmid);
32e56bed 383 if ($archive ne '-') {
a48e5562 384 my $orig_conf;
bc871ffb 385 print "recovering backed-up configuration from '$archive'\n";
12aec1d2 386 ($orig_conf, $orig_mp_param) = PVE::LXC::Create::recover_config($storage_cfg, $archive, $vmid);
6597f369 387
a2d3e5c3 388 $was_template = delete $orig_conf->{template};
6597f369 389
a67908f1 390 # When we're root call 'restore_configuration' with restricted=0,
5a7a51d0
WB
391 # causing it to restore the raw lxc entries, among which there may be
392 # 'lxc.idmap' entries. We need to make sure that the extracted contents
393 # of the container match up with the restored configuration afterwards:
32e56bed 394 $conf->{lxc} = $orig_conf->{lxc} if $is_root;
8ffdc116
OB
395
396 $conf->{unprivileged} = $orig_conf->{unprivileged}
397 if !defined($unprivileged) && defined($orig_conf->{unprivileged});
b9623ef8
DC
398
399 # implicit privileged change is checked here
400 if ($old_conf->{unprivileged} && !$conf->{unprivileged}) {
401 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Allocate']);
402 }
5a7a51d0 403 }
f360d7f1 404 }
db18c1e4 405 if ($storage_only_mode) {
b51a98d4 406 if ($restore) {
a48e5562 407 if (!defined($orig_mp_param)) {
bc871ffb 408 print "recovering backed-up configuration from '$archive'\n";
12aec1d2 409 (undef, $orig_mp_param) = PVE::LXC::Create::recover_config($storage_cfg, $archive, $vmid);
a5246958 410 }
f360d7f1 411 $mp_param = $orig_mp_param;
db18c1e4
FG
412 die "rootfs configuration could not be recovered, please check and specify manually!\n"
413 if !defined($mp_param->{rootfs});
015740e6 414 PVE::LXC::Config->foreach_volume($mp_param, sub {
db18c1e4
FG
415 my ($ms, $mountpoint) = @_;
416 my $type = $mountpoint->{type};
417 if ($type eq 'volume') {
418 die "unable to detect disk size - please specify $ms (size)\n"
419 if !defined($mountpoint->{size});
420 my $disksize = $mountpoint->{size} / (1024 * 1024 * 1024); # create_disks expects GB as unit size
421 delete $mountpoint->{size};
422 $mountpoint->{volume} = "$storage:$disksize";
423 $mp_param->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
424 } else {
15e4d443 425 my $type = $mountpoint->{type};
7e0e7f38
FG
426 die "restoring rootfs to $type mount is only possible by specifying -rootfs manually!\n"
427 if ($ms eq 'rootfs');
20f9339d 428 die "restoring '$ms' to $type mount is only possible for root\n"
99132ce0 429 if !$is_root;
7e0e7f38 430
15e4d443
FG
431 if ($mountpoint->{backup}) {
432 warn "WARNING - unsupported configuration!\n";
235dbdf3
FG
433 warn "backup was enabled for $type mount point $ms ('$mountpoint->{mp}')\n";
434 warn "mount point configuration will be restored after archive extraction!\n";
15e4d443
FG
435 warn "contained files will be restored to wrong directory!\n";
436 }
136040f4 437 delete $mp_param->{$ms}; # actually delay bind/dev mps
db18c1e4
FG
438 $delayed_mp_param->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
439 }
440 });
b51a98d4 441 } else {
db18c1e4 442 $mp_param->{rootfs} = "$storage:4"; # defaults to 4GB
b51a98d4
DM
443 }
444 }
b9623ef8
DC
445 };
446 die "$emsg $@" if $@;
447
448 # up until here we did not modify the container, besides the lock
449 $remove_lock = 0;
b51a98d4 450
b9623ef8
DC
451 my $vollist = [];
452 eval {
db18c1e4
FG
453 $vollist = PVE::LXC::create_disks($storage_cfg, $vmid, $mp_param, $conf);
454
61783321
TL
455 # we always have the 'create' lock so check for more than 1 entry
456 if (scalar(keys %$old_conf) > 1) {
51665c2d 457 # destroy old container volumes
61783321 458 PVE::LXC::destroy_lxc_container($storage_cfg, $vmid, $old_conf, { lock => 'create' });
51665c2d 459 }
51665c2d
FG
460
461 eval {
462 my $rootdir = PVE::LXC::mount_all($vmid, $storage_cfg, $conf, 1);
f9b4407e 463 $bwlimit = PVE::Storage::get_bandwidth_limit('restore', [keys %used_storages], $bwlimit);
bc871ffb
FG
464 print "restoring '$archive' now..\n"
465 if $restore && $archive ne '-';
99a0bcb0 466 PVE::LXC::Create::restore_archive($storage_cfg, $archive, $rootdir, $conf, $ignore_unpack_errors, $bwlimit);
51665c2d
FG
467
468 if ($restore) {
bc871ffb 469 print "merging backed-up and given configuration..\n";
99a0bcb0 470 PVE::LXC::Create::restore_configuration($vmid, $storage_cfg, $archive, $rootdir, $conf, !$is_root, $unique, $skip_fw_config_restore);
4b4bbe55
OB
471 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
472 $lxc_setup->template_fixup($conf);
51665c2d
FG
473 } else {
474 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir); # detect OS
475 PVE::LXC::Config->write_config($vmid, $conf); # safe config (after OS detection)
476 $lxc_setup->post_create_hook($password, $ssh_keys);
477 }
478 };
479 my $err = $@;
480 PVE::LXC::umount_all($vmid, $storage_cfg, $conf, $err ? 1 : 0);
481 PVE::Storage::deactivate_volumes($storage_cfg, PVE::LXC::Config->get_vm_volumes($conf));
482 die $err if $err;
27916659
DM
483 # set some defaults
484 $conf->{hostname} ||= "CT$vmid";
485 $conf->{memory} ||= 512;
486 $conf->{swap} //= 512;
db18c1e4
FG
487 foreach my $mp (keys %$delayed_mp_param) {
488 $conf->{$mp} = $delayed_mp_param->{$mp};
489 }
a2d3e5c3
CE
490 # If the template flag was set, we try to convert again to template after restore
491 if ($was_template) {
492 print STDERR "Convert restored container to template...\n";
4c64d0db
FE
493 PVE::LXC::template_create($vmid, $conf);
494 $conf->{template} = 1;
a2d3e5c3 495 }
67afe46e 496 PVE::LXC::Config->write_config($vmid, $conf);
27916659
DM
497 };
498 if (my $err = $@) {
6c871c36 499 PVE::LXC::destroy_disks($storage_cfg, $vollist);
4eeb9af1 500 eval { PVE::LXC::Config->destroy_config($vmid) };
d7d48be6 501 warn $@ if $@;
61783321 502 die "$emsg $err";
6d098bf4 503 }
87273b2b 504 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
9d0225fb
TL
505
506 PVE::API2::LXC::Status->vm_start({ vmid => $vmid, node => $node })
507 if $start_after_create;
9c2d4ce9 508 };
5c752bbf 509
8a6fc617 510 my $workername = $restore ? 'vzrestore' : 'vzcreate';
b9623ef8
DC
511 my $realcmd = sub {
512 eval {
513 PVE::LXC::Config->lock_config($vmid, $code);
514 };
515 if (my $err = $@) {
516 # if we aborted before changing the container, we must remove the create lock
517 if ($remove_lock) {
518 PVE::LXC::Config->remove_lock($vmid, 'create');
519 }
520 die $err;
521 }
522 };
9c2d4ce9 523
8a6fc617 524 return $rpcenv->fork_worker($workername, $vmid, $authuser, $realcmd);
9c2d4ce9
DM
525 }});
526
f76a2828
DM
527__PACKAGE__->register_method({
528 name => 'vmdiridx',
5c752bbf 529 path => '{vmid}',
f76a2828
DM
530 method => 'GET',
531 proxyto => 'node',
532 description => "Directory index",
533 permissions => {
534 user => 'all',
535 },
536 parameters => {
537 additionalProperties => 0,
538 properties => {
539 node => get_standard_option('pve-node'),
540 vmid => get_standard_option('pve-vmid'),
541 },
542 },
543 returns => {
544 type => 'array',
545 items => {
546 type => "object",
547 properties => {
548 subdir => { type => 'string' },
549 },
550 },
551 links => [ { rel => 'child', href => "{subdir}" } ],
552 },
553 code => sub {
554 my ($param) = @_;
555
556 # test if VM exists
67afe46e 557 my $conf = PVE::LXC::Config->load_config($param->{vmid});
f76a2828
DM
558
559 my $res = [
560 { subdir => 'config' },
37c2dba8 561 { subdir => 'pending' },
fff3a342
DM
562 { subdir => 'status' },
563 { subdir => 'vncproxy' },
a0226a00 564 { subdir => 'termproxy' },
fff3a342
DM
565 { subdir => 'vncwebsocket' },
566 { subdir => 'spiceproxy' },
567 { subdir => 'migrate' },
c4a33727 568 { subdir => 'clone' },
f76a2828
DM
569# { subdir => 'initlog' },
570 { subdir => 'rrd' },
571 { subdir => 'rrddata' },
572 { subdir => 'firewall' },
cc5392c8 573 { subdir => 'snapshot' },
985b18ed 574 { subdir => 'resize' },
f76a2828 575 ];
5c752bbf 576
f76a2828
DM
577 return $res;
578 }});
579
c4a33727 580
f76a2828 581__PACKAGE__->register_method({
5c752bbf
DM
582 name => 'rrd',
583 path => '{vmid}/rrd',
f76a2828
DM
584 method => 'GET',
585 protected => 1, # fixme: can we avoid that?
586 permissions => {
587 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
588 },
589 description => "Read VM RRD statistics (returns PNG)",
590 parameters => {
591 additionalProperties => 0,
592 properties => {
593 node => get_standard_option('pve-node'),
594 vmid => get_standard_option('pve-vmid'),
595 timeframe => {
596 description => "Specify the time frame you are interested in.",
597 type => 'string',
598 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
599 },
600 ds => {
601 description => "The list of datasources you want to display.",
602 type => 'string', format => 'pve-configid-list',
603 },
604 cf => {
605 description => "The RRD consolidation function",
606 type => 'string',
607 enum => [ 'AVERAGE', 'MAX' ],
608 optional => 1,
609 },
610 },
611 },
612 returns => {
613 type => "object",
614 properties => {
615 filename => { type => 'string' },
616 },
617 },
618 code => sub {
619 my ($param) = @_;
620
d949527f 621 return PVE::RRD::create_rrd_graph(
5c752bbf 622 "pve2-vm/$param->{vmid}", $param->{timeframe},
f76a2828 623 $param->{ds}, $param->{cf});
5c752bbf 624
f76a2828
DM
625 }});
626
627__PACKAGE__->register_method({
5c752bbf
DM
628 name => 'rrddata',
629 path => '{vmid}/rrddata',
f76a2828
DM
630 method => 'GET',
631 protected => 1, # fixme: can we avoid that?
632 permissions => {
633 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
634 },
635 description => "Read VM RRD statistics",
636 parameters => {
637 additionalProperties => 0,
638 properties => {
639 node => get_standard_option('pve-node'),
640 vmid => get_standard_option('pve-vmid'),
641 timeframe => {
642 description => "Specify the time frame you are interested in.",
643 type => 'string',
644 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
645 },
646 cf => {
647 description => "The RRD consolidation function",
648 type => 'string',
649 enum => [ 'AVERAGE', 'MAX' ],
650 optional => 1,
651 },
652 },
653 },
654 returns => {
655 type => "array",
656 items => {
657 type => "object",
658 properties => {},
659 },
660 },
661 code => sub {
662 my ($param) = @_;
663
d949527f 664 return PVE::RRD::create_rrd_data(
f76a2828
DM
665 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
666 }});
667
f76a2828 668__PACKAGE__->register_method({
5c752bbf
DM
669 name => 'destroy_vm',
670 path => '{vmid}',
f76a2828
DM
671 method => 'DELETE',
672 protected => 1,
673 proxyto => 'node',
674 description => "Destroy the container (also delete all uses files).",
675 permissions => {
676 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
677 },
678 parameters => {
679 additionalProperties => 0,
680 properties => {
681 node => get_standard_option('pve-node'),
68e8f3c5 682 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid_stopped }),
d51b84ff
TL
683 force => {
684 type => 'boolean',
685 description => "Force destroy, even if running.",
686 default => 0,
687 optional => 1,
688 },
c5a09704
CE
689 purge => {
690 type => 'boolean',
796e8eee
TL
691 description => "Remove container from all related configurations."
692 ." For example, backup jobs, replication jobs or HA."
693 ." Related ACLs and Firewall entries will *always* be removed.",
694 default => 0,
c5a09704
CE
695 optional => 1,
696 },
3b1c0970
TL
697 'destroy-unreferenced-disks' => {
698 type => 'boolean',
699 description => "If set, destroy additionally all disks with the VMID from all"
700 ." enabled storages which are not referenced in the config.",
701 optional => 1,
702 },
f76a2828
DM
703 },
704 },
5c752bbf 705 returns => {
f76a2828
DM
706 type => 'string',
707 },
708 code => sub {
709 my ($param) = @_;
710
711 my $rpcenv = PVE::RPCEnvironment::get();
f76a2828 712 my $authuser = $rpcenv->get_user();
f76a2828
DM
713 my $vmid = $param->{vmid};
714
715 # test if container exists
0512c360 716
67afe46e 717 my $conf = PVE::LXC::Config->load_config($vmid);
0512c360
FG
718 my $early_checks = sub {
719 my ($conf) = @_;
720 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid");
721 PVE::LXC::Config->check_lock($conf);
7e806596 722
0512c360 723 my $ha_managed = PVE::HA::Config::service_is_configured("ct:$vmid");
b6e0b774 724
0512c360
FG
725 if (!$param->{purge}) {
726 die "unable to remove CT $vmid - used in HA resources and purge parameter not set.\n"
727 if $ha_managed;
3207b81a 728
0512c360
FG
729 # do not allow destroy if there are replication jobs without purge
730 my $repl_conf = PVE::ReplicationConfig->new();
731 $repl_conf->check_for_existing_jobs($vmid);
732 }
733
734 return $ha_managed;
735 };
736
737 $early_checks->($conf);
c5a8e04f 738
d607c17d 739 my $running_error_msg = "unable to destroy CT $vmid - container is running\n";
d51b84ff 740 die $running_error_msg if !$param->{force} && PVE::LXC::check_running($vmid); # check early
d607c17d 741
611fe3aa 742 my $code = sub {
673cf209 743 # reload config after lock
67afe46e 744 $conf = PVE::LXC::Config->load_config($vmid);
0512c360 745 my $ha_managed = $early_checks->($conf);
673cf209 746
d51b84ff
TL
747 if (PVE::LXC::check_running($vmid)) {
748 die $running_error_msg if !$param->{force};
749 warn "forced to stop CT $vmid before destroying!\n";
750 if (!$ha_managed) {
751 PVE::LXC::vm_stop($vmid, 1);
752 } else {
753 run_command(['ha-manager', 'crm-command', 'stop', "ct:$vmid", '120']);
754 }
755 }
d607c17d 756
0512c360 757 my $storage_cfg = cfs_read_file("storage.cfg");
3b1c0970
TL
758 PVE::LXC::destroy_lxc_container(
759 $storage_cfg,
760 $vmid,
761 $conf,
762 { lock => 'destroyed' },
763 $param->{'destroy-unreferenced-disks'},
764 );
7fc1d9eb 765
be5fc936 766 PVE::AccessControl::remove_vm_access($vmid);
2f9b5ead 767 PVE::Firewall::remove_vmfw_conf($vmid);
c5a09704 768 if ($param->{purge}) {
3207b81a 769 print "purging CT $vmid from related configurations..\n";
c5a09704
CE
770 PVE::ReplicationConfig::remove_vmid_jobs($vmid);
771 PVE::VZDump::Plugin::remove_vmid_from_backup_jobs($vmid);
3207b81a
TL
772
773 if ($ha_managed) {
774 PVE::HA::Config::delete_service_from_config("ct:$vmid");
775 print "NOTE: removed CT $vmid from HA resource configuration.\n";
776 }
c5a09704 777 }
7fc1d9eb
TL
778
779 # only now remove the zombie config, else we can have reuse race
780 PVE::LXC::Config->destroy_config($vmid);
f76a2828
DM
781 };
782
67afe46e 783 my $realcmd = sub { PVE::LXC::Config->lock_config($vmid, $code); };
351bc0c4 784
f76a2828
DM
785 return $rpcenv->fork_worker('vzdestroy', $vmid, $authuser, $realcmd);
786 }});
787
fff3a342
DM
788my $sslcert;
789
790__PACKAGE__->register_method ({
5b4657d0
DM
791 name => 'vncproxy',
792 path => '{vmid}/vncproxy',
fff3a342
DM
793 method => 'POST',
794 protected => 1,
795 permissions => {
796 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
797 },
798 description => "Creates a TCP VNC proxy connections.",
799 parameters => {
800 additionalProperties => 0,
801 properties => {
802 node => get_standard_option('pve-node'),
803 vmid => get_standard_option('pve-vmid'),
804 websocket => {
805 optional => 1,
806 type => 'boolean',
807 description => "use websocket instead of standard VNC.",
808 },
bd0f4d6d
DC
809 width => {
810 optional => 1,
811 description => "sets the width of the console in pixels.",
812 type => 'integer',
813 minimum => 16,
814 maximum => 4096,
815 },
816 height => {
817 optional => 1,
818 description => "sets the height of the console in pixels.",
819 type => 'integer',
820 minimum => 16,
821 maximum => 2160,
822 },
fff3a342
DM
823 },
824 },
5b4657d0 825 returns => {
fff3a342
DM
826 additionalProperties => 0,
827 properties => {
828 user => { type => 'string' },
829 ticket => { type => 'string' },
830 cert => { type => 'string' },
831 port => { type => 'integer' },
832 upid => { type => 'string' },
833 },
834 },
835 code => sub {
836 my ($param) = @_;
837
838 my $rpcenv = PVE::RPCEnvironment::get();
839
840 my $authuser = $rpcenv->get_user();
841
842 my $vmid = $param->{vmid};
843 my $node = $param->{node};
844
845 my $authpath = "/vms/$vmid";
846
847 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
848
849 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
850 if !$sslcert;
851
ec2c57eb 852 my ($remip, $family);
5b4657d0 853
fff3a342 854 if ($node ne PVE::INotify::nodename()) {
85ae6211 855 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
ec2c57eb
WB
856 } else {
857 $family = PVE::Tools::get_host_address_family($node);
fff3a342
DM
858 }
859
ec2c57eb
WB
860 my $port = PVE::Tools::next_vnc_port($family);
861
fff3a342
DM
862 # NOTE: vncterm VNC traffic is already TLS encrypted,
863 # so we select the fastest chipher here (or 'none'?)
5b4657d0 864 my $remcmd = $remip ?
806eae70 865 ['/usr/bin/ssh', '-e', 'none', '-t', $remip] : [];
fff3a342 866
67afe46e 867 my $conf = PVE::LXC::Config->load_config($vmid, $node);
65213b67 868 my $concmd = PVE::LXC::get_console_command($vmid, $conf, -1);
aca816ad 869
5b4657d0
DM
870 my $shcmd = [ '/usr/bin/dtach', '-A',
871 "/var/run/dtach/vzctlconsole$vmid",
aca816ad 872 '-r', 'winch', '-z', @$concmd];
fff3a342
DM
873
874 my $realcmd = sub {
875 my $upid = shift;
876
5b4657d0 877 syslog ('info', "starting lxc vnc proxy $upid\n");
fff3a342 878
5b4657d0 879 my $timeout = 10;
fff3a342
DM
880
881 my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
5b4657d0 882 '-timeout', $timeout, '-authpath', $authpath,
fff3a342
DM
883 '-perm', 'VM.Console'];
884
bd0f4d6d
DC
885 if ($param->{width}) {
886 push @$cmd, '-width', $param->{width};
887 }
888
889 if ($param->{height}) {
890 push @$cmd, '-height', $param->{height};
891 }
892
fff3a342 893 if ($param->{websocket}) {
5b4657d0 894 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
fff3a342
DM
895 push @$cmd, '-notls', '-listen', 'localhost';
896 }
897
898 push @$cmd, '-c', @$remcmd, @$shcmd;
899
37634d3d 900 run_command($cmd, keeplocale => 1);
fff3a342
DM
901
902 return;
903 };
904
905 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
906
907 PVE::Tools::wait_for_vnc_port($port);
908
909 return {
910 user => $authuser,
911 ticket => $ticket,
5b4657d0
DM
912 port => $port,
913 upid => $upid,
914 cert => $sslcert,
fff3a342
DM
915 };
916 }});
917
a0226a00
DC
918__PACKAGE__->register_method ({
919 name => 'termproxy',
920 path => '{vmid}/termproxy',
921 method => 'POST',
922 protected => 1,
923 permissions => {
924 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
925 },
926 description => "Creates a TCP proxy connection.",
927 parameters => {
928 additionalProperties => 0,
929 properties => {
930 node => get_standard_option('pve-node'),
931 vmid => get_standard_option('pve-vmid'),
932 },
933 },
934 returns => {
935 additionalProperties => 0,
936 properties => {
937 user => { type => 'string' },
938 ticket => { type => 'string' },
939 port => { type => 'integer' },
940 upid => { type => 'string' },
941 },
942 },
943 code => sub {
944 my ($param) = @_;
945
946 my $rpcenv = PVE::RPCEnvironment::get();
947
948 my $authuser = $rpcenv->get_user();
949
950 my $vmid = $param->{vmid};
951 my $node = $param->{node};
952
953 my $authpath = "/vms/$vmid";
954
955 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
956
957 my ($remip, $family);
958
959 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
960 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
961 } else {
962 $family = PVE::Tools::get_host_address_family($node);
963 }
964
965 my $port = PVE::Tools::next_vnc_port($family);
966
967 my $remcmd = $remip ?
968 ['/usr/bin/ssh', '-e', 'none', '-t', $remip, '--'] : [];
969
970 my $conf = PVE::LXC::Config->load_config($vmid, $node);
65213b67 971 my $concmd = PVE::LXC::get_console_command($vmid, $conf, -1);
a0226a00
DC
972
973 my $shcmd = [ '/usr/bin/dtach', '-A',
974 "/var/run/dtach/vzctlconsole$vmid",
975 '-r', 'winch', '-z', @$concmd];
976
977 my $realcmd = sub {
978 my $upid = shift;
979
980 syslog ('info', "starting lxc termproxy $upid\n");
981
982 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
983 '--perm', 'VM.Console', '--'];
984 push @$cmd, @$remcmd, @$shcmd;
985
986 PVE::Tools::run_command($cmd);
987 };
988
989 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
990
991 PVE::Tools::wait_for_vnc_port($port);
992
993 return {
994 user => $authuser,
995 ticket => $ticket,
996 port => $port,
997 upid => $upid,
998 };
999 }});
1000
fff3a342
DM
1001__PACKAGE__->register_method({
1002 name => 'vncwebsocket',
1003 path => '{vmid}/vncwebsocket',
1004 method => 'GET',
5b4657d0 1005 permissions => {
fff3a342
DM
1006 description => "You also need to pass a valid ticket (vncticket).",
1007 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1008 },
1009 description => "Opens a weksocket for VNC traffic.",
1010 parameters => {
1011 additionalProperties => 0,
1012 properties => {
1013 node => get_standard_option('pve-node'),
1014 vmid => get_standard_option('pve-vmid'),
1015 vncticket => {
1016 description => "Ticket from previous call to vncproxy.",
1017 type => 'string',
1018 maxLength => 512,
1019 },
1020 port => {
1021 description => "Port number returned by previous vncproxy call.",
1022 type => 'integer',
1023 minimum => 5900,
1024 maximum => 5999,
1025 },
1026 },
1027 },
1028 returns => {
1029 type => "object",
1030 properties => {
1031 port => { type => 'string' },
1032 },
1033 },
1034 code => sub {
1035 my ($param) = @_;
1036
1037 my $rpcenv = PVE::RPCEnvironment::get();
1038
1039 my $authuser = $rpcenv->get_user();
1040
1041 my $authpath = "/vms/$param->{vmid}";
1042
1043 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1044
1045 my $port = $param->{port};
5b4657d0 1046
fff3a342
DM
1047 return { port => $port };
1048 }});
1049
1050__PACKAGE__->register_method ({
5b4657d0
DM
1051 name => 'spiceproxy',
1052 path => '{vmid}/spiceproxy',
fff3a342
DM
1053 method => 'POST',
1054 protected => 1,
1055 proxyto => 'node',
1056 permissions => {
1057 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1058 },
1059 description => "Returns a SPICE configuration to connect to the CT.",
1060 parameters => {
1061 additionalProperties => 0,
1062 properties => {
1063 node => get_standard_option('pve-node'),
1064 vmid => get_standard_option('pve-vmid'),
1065 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1066 },
1067 },
1068 returns => get_standard_option('remote-viewer-config'),
1069 code => sub {
1070 my ($param) = @_;
1071
1072 my $vmid = $param->{vmid};
1073 my $node = $param->{node};
1074 my $proxy = $param->{proxy};
1075
1076 my $authpath = "/vms/$vmid";
1077 my $permissions = 'VM.Console';
1078
67afe46e 1079 my $conf = PVE::LXC::Config->load_config($vmid);
da4db334
TL
1080
1081 die "CT $vmid not running\n" if !PVE::LXC::check_running($vmid);
1082
aca816ad
DM
1083 my $concmd = PVE::LXC::get_console_command($vmid, $conf);
1084
5b4657d0
DM
1085 my $shcmd = ['/usr/bin/dtach', '-A',
1086 "/var/run/dtach/vzctlconsole$vmid",
aca816ad 1087 '-r', 'winch', '-z', @$concmd];
fff3a342
DM
1088
1089 my $title = "CT $vmid";
1090
1091 return PVE::API2Tools::run_spiceterm($authpath, $permissions, $vmid, $node, $proxy, $title, $shcmd);
1092 }});
5c752bbf 1093
5c752bbf
DM
1094
1095__PACKAGE__->register_method({
52389a07
DM
1096 name => 'migrate_vm',
1097 path => '{vmid}/migrate',
5c752bbf
DM
1098 method => 'POST',
1099 protected => 1,
1100 proxyto => 'node',
52389a07 1101 description => "Migrate the container to another node. Creates a new migration task.",
5c752bbf 1102 permissions => {
52389a07 1103 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
5c752bbf
DM
1104 },
1105 parameters => {
1106 additionalProperties => 0,
1107 properties => {
1108 node => get_standard_option('pve-node'),
68e8f3c5
DM
1109 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1110 target => get_standard_option('pve-node', {
1111 description => "Target node.",
39bb88df 1112 completion => \&PVE::Cluster::complete_migration_target,
68e8f3c5 1113 }),
e90ddc4c 1114 'target-storage' => get_standard_option('pve-targetstorage'),
52389a07
DM
1115 online => {
1116 type => 'boolean',
1117 description => "Use online/live migration.",
1118 optional => 1,
1119 },
00d8cdc0
DC
1120 restart => {
1121 type => 'boolean',
1122 description => "Use restart migration",
1123 optional => 1,
1124 },
1125 timeout => {
1126 type => 'integer',
1127 description => "Timeout in seconds for shutdown for restart migration",
1128 optional => 1,
1129 default => 180,
1130 },
8ead60ef
SI
1131 bwlimit => {
1132 description => "Override I/O bandwidth limit (in KiB/s).",
1133 optional => 1,
1134 type => 'number',
1135 minimum => '0',
41e9b65c 1136 default => 'migrate limit from datacenter or storage config',
8ead60ef 1137 },
5c752bbf
DM
1138 },
1139 },
1140 returns => {
1141 type => 'string',
52389a07 1142 description => "the task ID.",
5c752bbf
DM
1143 },
1144 code => sub {
1145 my ($param) = @_;
1146
1147 my $rpcenv = PVE::RPCEnvironment::get();
1148
1149 my $authuser = $rpcenv->get_user();
1150
52389a07 1151 my $target = extract_param($param, 'target');
bb1ac2de 1152
52389a07
DM
1153 my $localnode = PVE::INotify::nodename();
1154 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
bb1ac2de 1155
52389a07 1156 PVE::Cluster::check_cfs_quorum();
5c752bbf 1157
52389a07 1158 PVE::Cluster::check_node_exists($target);
27916659 1159
52389a07 1160 my $targetip = PVE::Cluster::remote_node_ip($target);
5c752bbf 1161
52389a07 1162 my $vmid = extract_param($param, 'vmid');
5c752bbf 1163
52389a07 1164 # test if VM exists
67afe46e 1165 PVE::LXC::Config->load_config($vmid);
5c752bbf 1166
52389a07
DM
1167 # try to detect errors early
1168 if (PVE::LXC::check_running($vmid)) {
00d8cdc0
DC
1169 die "can't migrate running container without --online or --restart\n"
1170 if !$param->{online} && !$param->{restart};
5c752bbf 1171 }
5c752bbf 1172
e90ddc4c
FG
1173 if (my $targetstorage = delete $param->{'target-storage'}) {
1174 my $storecfg = PVE::Storage::config();
1175 my $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
26115ef2 1176 raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
e90ddc4c
FG
1177 if $@;
1178
1179 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
1180 if !defined($storagemap->{identity});
1181
1182 foreach my $target_sid (values %{$storagemap->{entries}}) {
1183 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
1184 }
1185
1186 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
1187 if $storagemap->{default};
1188
1189 $param->{storagemap} = $storagemap;
1190 }
1191
5c752bbf
DM
1192 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
1193
1194 my $hacmd = sub {
1195 my $upid = shift;
1196
1197 my $service = "ct:$vmid";
1198
52389a07 1199 my $cmd = ['ha-manager', 'migrate', $service, $target];
5c752bbf 1200
545dd4e1 1201 print "Requesting HA migration for CT $vmid to node $target\n";
5c752bbf
DM
1202
1203 PVE::Tools::run_command($cmd);
1204
1205 return;
1206 };
1207
52389a07 1208 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
5c752bbf
DM
1209
1210 } else {
1211
22557519
DM
1212 my $realcmd = sub {
1213 PVE::LXC::Migrate->migrate($target, $targetip, $vmid, $param);
1214 };
56462053 1215
22557519
DM
1216 my $worker = sub {
1217 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
5c752bbf
DM
1218 };
1219
22557519 1220 return $rpcenv->fork_worker('vzmigrate', $vmid, $authuser, $worker);
5c752bbf
DM
1221 }
1222 }});
1223
cc5392c8
WL
1224__PACKAGE__->register_method({
1225 name => 'vm_feature',
1226 path => '{vmid}/feature',
1227 method => 'GET',
1228 proxyto => 'node',
1229 protected => 1,
1230 description => "Check if feature for virtual machine is available.",
1231 permissions => {
1232 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1233 },
1234 parameters => {
1235 additionalProperties => 0,
1236 properties => {
1237 node => get_standard_option('pve-node'),
1238 vmid => get_standard_option('pve-vmid'),
1239 feature => {
1240 description => "Feature to check.",
1241 type => 'string',
d53e5c58 1242 enum => [ 'snapshot', 'clone', 'copy' ],
cc5392c8 1243 },
5ec3949c 1244 snapname => get_standard_option('pve-snapshot-name', {
cc5392c8
WL
1245 optional => 1,
1246 }),
1247 },
1248 },
1249 returns => {
1250 type => "object",
1251 properties => {
1252 hasFeature => { type => 'boolean' },
1253 #nodes => {
1254 #type => 'array',
1255 #items => { type => 'string' },
1256 #}
1257 },
1258 },
1259 code => sub {
1260 my ($param) = @_;
1261
1262 my $node = extract_param($param, 'node');
1263
1264 my $vmid = extract_param($param, 'vmid');
1265
1266 my $snapname = extract_param($param, 'snapname');
1267
1268 my $feature = extract_param($param, 'feature');
1269
67afe46e 1270 my $conf = PVE::LXC::Config->load_config($vmid);
cc5392c8
WL
1271
1272 if($snapname){
1273 my $snap = $conf->{snapshots}->{$snapname};
1274 die "snapshot '$snapname' does not exist\n" if !defined($snap);
1275 $conf = $snap;
1276 }
ef241384 1277 my $storage_cfg = PVE::Storage::config();
cc5392c8 1278 #Maybe include later
ef241384 1279 #my $nodelist = PVE::LXC::shared_nodes($conf, $storage_cfg);
4518000b 1280 my $hasFeature = PVE::LXC::Config->has_feature($feature, $conf, $storage_cfg, $snapname);
cc5392c8
WL
1281
1282 return {
1283 hasFeature => $hasFeature,
1284 #nodes => [ keys %$nodelist ],
1285 };
1286 }});
bb1ac2de
DM
1287
1288__PACKAGE__->register_method({
1289 name => 'template',
1290 path => '{vmid}/template',
1291 method => 'POST',
1292 protected => 1,
1293 proxyto => 'node',
1294 description => "Create a Template.",
1295 permissions => {
1296 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
1297 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1298 },
1299 parameters => {
1300 additionalProperties => 0,
1301 properties => {
1302 node => get_standard_option('pve-node'),
68e8f3c5 1303 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid_stopped }),
bb1ac2de
DM
1304 },
1305 },
1306 returns => { type => 'null'},
1307 code => sub {
1308 my ($param) = @_;
1309
1310 my $rpcenv = PVE::RPCEnvironment::get();
1311
1312 my $authuser = $rpcenv->get_user();
1313
1314 my $node = extract_param($param, 'node');
1315
1316 my $vmid = extract_param($param, 'vmid');
1317
1318 my $updatefn = sub {
1319
67afe46e
FG
1320 my $conf = PVE::LXC::Config->load_config($vmid);
1321 PVE::LXC::Config->check_lock($conf);
bb1ac2de
DM
1322
1323 die "unable to create template, because CT contains snapshots\n"
1324 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
1325
1326 die "you can't convert a template to a template\n"
67afe46e 1327 if PVE::LXC::Config->is_template($conf);
bb1ac2de
DM
1328
1329 die "you can't convert a CT to template if the CT is running\n"
1330 if PVE::LXC::check_running($vmid);
1331
1332 my $realcmd = sub {
1333 PVE::LXC::template_create($vmid, $conf);
bb1ac2de 1334
c2182c49 1335 $conf->{template} = 1;
bb1ac2de 1336
c2182c49
WL
1337 PVE::LXC::Config->write_config($vmid, $conf);
1338 # and remove lxc config
1339 PVE::LXC::update_lxc_config($vmid, $conf);
1340 };
bb1ac2de
DM
1341
1342 return $rpcenv->fork_worker('vztemplate', $vmid, $authuser, $realcmd);
1343 };
1344
67afe46e 1345 PVE::LXC::Config->lock_config($vmid, $updatefn);
bb1ac2de
DM
1346
1347 return undef;
1348 }});
1349
c4a33727
DM
1350__PACKAGE__->register_method({
1351 name => 'clone_vm',
1352 path => '{vmid}/clone',
1353 method => 'POST',
1354 protected => 1,
1355 proxyto => 'node',
1356 description => "Create a container clone/copy",
1357 permissions => {
1358 description => "You need 'VM.Clone' permissions on /vms/{vmid}, " .
1359 "and 'VM.Allocate' permissions " .
1360 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
1361 "'Datastore.AllocateSpace' on any used storage.",
1362 check =>
1363 [ 'and',
1364 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
1365 [ 'or',
1366 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
1367 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
1368 ],
1369 ]
1370 },
1371 parameters => {
1372 additionalProperties => 0,
1373 properties => {
1374 node => get_standard_option('pve-node'),
1375 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1376 newid => get_standard_option('pve-vmid', {
1377 completion => \&PVE::Cluster::complete_next_vmid,
1378 description => 'VMID for the clone.' }),
1379 hostname => {
1380 optional => 1,
1381 type => 'string', format => 'dns-name',
1382 description => "Set a hostname for the new CT.",
1383 },
1384 description => {
1385 optional => 1,
1386 type => 'string',
1387 description => "Description for the new CT.",
1388 },
1389 pool => {
1390 optional => 1,
1391 type => 'string', format => 'pve-poolid',
1392 description => "Add the new CT to the specified pool.",
1393 },
5ec3949c 1394 snapname => get_standard_option('pve-snapshot-name', {
c4a33727
DM
1395 optional => 1,
1396 }),
1397 storage => get_standard_option('pve-storage-id', {
1398 description => "Target storage for full clone.",
c4a33727
DM
1399 optional => 1,
1400 }),
1401 full => {
1402 optional => 1,
1403 type => 'boolean',
c4b4cb83 1404 description => "Create a full copy of all disks. This is always done when " .
c4a33727 1405 "you clone a normal CT. For CT templates, we try to create a linked clone by default.",
c4a33727 1406 },
411ae12c
DM
1407 target => get_standard_option('pve-node', {
1408 description => "Target node. Only allowed if the original VM is on shared storage.",
1409 optional => 1,
1410 }),
8ead60ef
SI
1411 bwlimit => {
1412 description => "Override I/O bandwidth limit (in KiB/s).",
1413 optional => 1,
1414 type => 'number',
1415 minimum => '0',
41e9b65c 1416 default => 'clone limit from datacenter or storage config',
8ead60ef 1417 },
c4a33727
DM
1418 },
1419 },
1420 returns => {
1421 type => 'string',
1422 },
1423 code => sub {
1424 my ($param) = @_;
1425
1426 my $rpcenv = PVE::RPCEnvironment::get();
59b0ce55 1427 my $authuser = $rpcenv->get_user();
c4a33727
DM
1428
1429 my $node = extract_param($param, 'node');
c4a33727 1430 my $vmid = extract_param($param, 'vmid');
c4a33727 1431 my $newid = extract_param($param, 'newid');
c4a33727 1432 my $pool = extract_param($param, 'pool');
c4a33727
DM
1433 if (defined($pool)) {
1434 $rpcenv->check_pool_exist($pool);
1435 }
c4a33727 1436 my $snapname = extract_param($param, 'snapname');
c4a33727 1437 my $storage = extract_param($param, 'storage');
411ae12c 1438 my $target = extract_param($param, 'target');
c4a33727
DM
1439 my $localnode = PVE::INotify::nodename();
1440
59b0ce55 1441 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
411ae12c
DM
1442
1443 PVE::Cluster::check_node_exists($target) if $target;
1444
c4a33727
DM
1445 my $storecfg = PVE::Storage::config();
1446
1447 if ($storage) {
1448 # check if storage is enabled on local node
1449 PVE::Storage::storage_check_enabled($storecfg, $storage);
411ae12c
DM
1450 if ($target) {
1451 # check if storage is available on target node
a1c27f86 1452 PVE::Storage::storage_check_enabled($storecfg, $storage, $target);
411ae12c
DM
1453 # clone only works if target storage is shared
1454 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
1455 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
1456 }
c4a33727
DM
1457 }
1458
1459 PVE::Cluster::check_cfs_quorum();
1460
db01d674
WB
1461 my $newconf = {};
1462 my $mountpoints = {};
1463 my $fullclone = {};
1464 my $vollist = [];
411ae12c 1465 my $running;
c4a33727 1466
b062187a
FG
1467 my $lock_and_reload = sub {
1468 my ($vmid, $code) = @_;
1469 return PVE::LXC::Config->lock_config($vmid, sub {
1470 my $conf = PVE::LXC::Config->load_config($vmid);
1471 die "Lost 'create' config lock, aborting.\n"
1472 if !PVE::LXC::Config->has_lock($conf, 'create');
1473
1474 return $code->($conf);
1475 });
1476 };
e7d553c7 1477
cc1ffe7e 1478 my $src_conf = PVE::LXC::Config->set_lock($vmid, 'disk');
411ae12c 1479
becea45d
DT
1480 eval {
1481 PVE::LXC::Config->create_and_lock_config($newid, 0);
1482 };
1483 if (my $err = $@) {
1484 eval { PVE::LXC::Config->remove_lock($vmid, 'disk') };
1485 warn "Failed to remove source CT config lock - $@\n" if $@;
e7d553c7 1486
becea45d 1487 die $err;
cc1ffe7e 1488 }
c4a33727 1489
cc1ffe7e 1490 eval {
becea45d
DT
1491 $running = PVE::LXC::check_running($vmid) || 0;
1492
1493 my $full = extract_param($param, 'full');
1494 if (!defined($full)) {
1495 $full = !PVE::LXC::Config->is_template($src_conf);
1496 }
1497
1498 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
1499
5da8f412
FG
1500 die "parameter 'storage' not allowed for linked clones\n"
1501 if defined($storage) && !$full;
1502
cc1ffe7e
FG
1503 die "snapshot '$snapname' does not exist\n"
1504 if $snapname && !defined($src_conf->{snapshots}->{$snapname});
c4a33727 1505
cc1ffe7e 1506 my $src_conf = $snapname ? $src_conf->{snapshots}->{$snapname} : $src_conf;
c4a33727 1507
cc1ffe7e
FG
1508 my $sharedvm = 1;
1509 for my $opt (sort keys %$src_conf) {
1510 next if $opt =~ m/^unused\d+$/;
c4a33727 1511
cc1ffe7e 1512 my $value = $src_conf->{$opt};
c4a33727 1513
cc1ffe7e
FG
1514 if (($opt eq 'rootfs') || ($opt =~ m/^mp\d+$/)) {
1515 my $mp = PVE::LXC::Config->parse_volume($opt, $value);
09687674 1516
cc1ffe7e
FG
1517 if ($mp->{type} eq 'volume') {
1518 my $volid = $mp->{volume};
09687674 1519
cc1ffe7e
FG
1520 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
1521 $sid = $storage if defined($storage);
1522 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
1523 if (!$scfg->{shared}) {
1524 $sharedvm = 0;
1525 warn "found non-shared volume: $volid\n" if $target;
1526 }
c4a33727 1527
cc1ffe7e 1528 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
c4a33727 1529
cc1ffe7e
FG
1530 if ($full) {
1531 die "Cannot do full clones on a running container without snapshots\n"
1532 if $running && !defined($snapname);
1533 $fullclone->{$opt} = 1;
c4a33727 1534 } else {
cc1ffe7e
FG
1535 # not full means clone instead of copy
1536 die "Linked clone feature for '$volid' is not available\n"
1537 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $volid, $snapname, $running, {'valid_target_formats' => ['raw', 'subvol']});
c4a33727 1538 }
cc1ffe7e
FG
1539
1540 $mountpoints->{$opt} = $mp;
1541 push @$vollist, $volid;
1542
c4a33727 1543 } else {
cc1ffe7e
FG
1544 # TODO: allow bind mounts?
1545 die "unable to clone mountpoint '$opt' (type $mp->{type})\n";
c4a33727 1546 }
cc1ffe7e
FG
1547 } elsif ($opt =~ m/^net(\d+)$/) {
1548 # always change MAC! address
1549 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1550 my $net = PVE::LXC::Config->parse_lxc_network($value);
1551 $net->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1552 $newconf->{$opt} = PVE::LXC::Config->print_lxc_network($net);
1553 } else {
1554 # copy everything else
1555 $newconf->{$opt} = $value;
db01d674 1556 }
cc1ffe7e
FG
1557 }
1558 die "can't clone CT to node '$target' (CT uses local storage)\n"
1559 if $target && !$sharedvm;
c4a33727 1560
cc1ffe7e
FG
1561 # Replace the 'disk' lock with a 'create' lock.
1562 $newconf->{lock} = 'create';
db01d674 1563
04a34c56 1564 # delete all snapshot related config options
a0167a5c 1565 delete $newconf->@{qw(snapshots parent snaptime snapstate)};
04a34c56 1566
cc1ffe7e
FG
1567 delete $newconf->{pending};
1568 delete $newconf->{template};
c4a33727 1569
a0167a5c
TL
1570 $newconf->{hostname} = $param->{hostname} if $param->{hostname};
1571 $newconf->{description} = $param->{description} if $param->{description};
c4a33727 1572
b062187a 1573 $lock_and_reload->($newid, sub {
cc1ffe7e
FG
1574 PVE::LXC::Config->write_config($newid, $newconf);
1575 });
1576 };
1577 if (my $err = $@) {
1578 eval { PVE::LXC::Config->remove_lock($vmid, 'disk') };
1579 warn "Failed to remove source CT config lock - $@\n" if $@;
1580
1581 eval {
b062187a 1582 $lock_and_reload->($newid, sub {
cc1ffe7e 1583 PVE::LXC::Config->destroy_config($newid);
5eacc654 1584 PVE::Firewall::remove_vmfw_conf($newid);
7b290bf3 1585 });
db01d674 1586 };
cc1ffe7e 1587 warn "Failed to remove target CT config - $@\n" if $@;
52d287d9 1588
cc1ffe7e
FG
1589 die $err;
1590 }
c4a33727 1591
db01d674
WB
1592 my $update_conf = sub {
1593 my ($key, $value) = @_;
b062187a
FG
1594 return $lock_and_reload->($newid, sub {
1595 my $conf = shift;
db01d674
WB
1596 $conf->{$key} = $value;
1597 PVE::LXC::Config->write_config($newid, $conf);
1598 });
1599 };
c4a33727 1600
db01d674
WB
1601 my $realcmd = sub {
1602 my ($upid) = @_;
c4a33727 1603
db01d674 1604 my $newvollist = [];
c4a33727 1605
411ae12c
DM
1606 my $verify_running = PVE::LXC::check_running($vmid) || 0;
1607 die "unexpected state change\n" if $verify_running != $running;
1608
db01d674
WB
1609 eval {
1610 local $SIG{INT} =
1611 local $SIG{TERM} =
1612 local $SIG{QUIT} =
1613 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
c4a33727 1614
db01d674 1615 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
8ead60ef 1616 my $bwlimit = extract_param($param, 'bwlimit');
c4a33727 1617
db01d674
WB
1618 foreach my $opt (keys %$mountpoints) {
1619 my $mp = $mountpoints->{$opt};
1620 my $volid = $mp->{volume};
c4a33727 1621
db01d674
WB
1622 my $newvolid;
1623 if ($fullclone->{$opt}) {
1624 print "create full clone of mountpoint $opt ($volid)\n";
8ead60ef
SI
1625 my $source_storage = PVE::Storage::parse_volume_id($volid);
1626 my $target_storage = $storage // $source_storage;
1627 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', [$source_storage, $target_storage], $bwlimit);
1628 $newvolid = PVE::LXC::copy_volume($mp, $newid, $target_storage, $storecfg, $newconf, $snapname, $clonelimit);
db01d674
WB
1629 } else {
1630 print "create linked clone of mount point $opt ($volid)\n";
1631 $newvolid = PVE::Storage::vdisk_clone($storecfg, $volid, $newid, $snapname);
c4a33727
DM
1632 }
1633
db01d674
WB
1634 push @$newvollist, $newvolid;
1635 $mp->{volume} = $newvolid;
c4a33727 1636
db01d674 1637 $update_conf->($opt, PVE::LXC::Config->print_ct_mountpoint($mp, $opt eq 'rootfs'));
c4a33727
DM
1638 }
1639
db01d674 1640 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
8e08cd17 1641
b062187a
FG
1642 $lock_and_reload->($newid, sub {
1643 my $conf = shift;
1644 my $rootdir = PVE::LXC::mount_all($newid, $storecfg, $conf, 1);
1645 eval {
1646 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
1647 $lxc_setup->post_clone_hook($conf);
1648 };
1649 my $err = $@;
1650 eval { PVE::LXC::umount_all($newid, $storecfg, $conf, 1); };
1651 if ($err) {
1652 warn "$@\n" if $@;
1653 die $err;
1654 } else {
1655 die $@ if $@;
7b290bf3
OB
1656 }
1657 });
c4a33727 1658 };
db01d674 1659 my $err = $@;
db01d674
WB
1660 # Unlock the source config in any case:
1661 eval { PVE::LXC::Config->remove_lock($vmid, 'disk') };
1662 warn $@ if $@;
c4a33727 1663
db01d674
WB
1664 if ($err) {
1665 # Now cleanup the config & disks:
db01d674
WB
1666 sleep 1; # some storages like rbd need to wait before release volume - really?
1667
1668 foreach my $volid (@$newvollist) {
1669 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
1670 warn $@ if $@;
1671 }
52d287d9
FG
1672
1673 eval {
b062187a 1674 $lock_and_reload->($newid, sub {
52d287d9 1675 PVE::LXC::Config->destroy_config($newid);
5eacc654 1676 PVE::Firewall::remove_vmfw_conf($newid);
52d287d9
FG
1677 });
1678 };
1679 warn "Failed to remove target CT config - $@\n" if $@;
1680
db01d674
WB
1681 die "clone failed: $err";
1682 }
1683
b062187a
FG
1684 $lock_and_reload->($newid, sub {
1685 PVE::LXC::Config->remove_lock($newid, 'create');
1686
1687 if ($target) {
1688 # always deactivate volumes - avoid lvm LVs to be active on several nodes
1689 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
1690 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
1691
1692 PVE::LXC::Config->move_config_to_node($newid, $target);
1693 }
1694 });
1695
db01d674 1696 return;
c4a33727
DM
1697 };
1698
db01d674 1699 return $rpcenv->fork_worker('vzclone', $vmid, $authuser, $realcmd);
c4a33727
DM
1700 }});
1701
1702
985b18ed
WL
1703__PACKAGE__->register_method({
1704 name => 'resize_vm',
1705 path => '{vmid}/resize',
1706 method => 'PUT',
1707 protected => 1,
1708 proxyto => 'node',
235dbdf3 1709 description => "Resize a container mount point.",
985b18ed
WL
1710 permissions => {
1711 check => ['perm', '/vms/{vmid}', ['VM.Config.Disk'], any => 1],
1712 },
1713 parameters => {
1714 additionalProperties => 0,
b8c5a95f
WB
1715 properties => {
1716 node => get_standard_option('pve-node'),
1717 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1718 disk => {
1719 type => 'string',
1720 description => "The disk you want to resize.",
5e5d76cf 1721 enum => [PVE::LXC::Config->valid_volume_keys()],
b8c5a95f
WB
1722 },
1723 size => {
1724 type => 'string',
1725 pattern => '\+?\d+(\.\d+)?[KMGT]?',
1726 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.",
1727 },
1728 digest => {
1729 type => 'string',
1730 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1731 maxLength => 40,
1732 optional => 1,
1733 }
1734 },
985b18ed 1735 },
9f3f7963
WL
1736 returns => {
1737 type => 'string',
1738 description => "the task ID.",
1739 },
985b18ed
WL
1740 code => sub {
1741 my ($param) = @_;
1742
1743 my $rpcenv = PVE::RPCEnvironment::get();
1744
1745 my $authuser = $rpcenv->get_user();
1746
1747 my $node = extract_param($param, 'node');
1748
1749 my $vmid = extract_param($param, 'vmid');
1750
1751 my $digest = extract_param($param, 'digest');
1752
1753 my $sizestr = extract_param($param, 'size');
1754 my $ext = ($sizestr =~ s/^\+//);
1755 my $newsize = PVE::JSONSchema::parse_size($sizestr);
1756 die "invalid size string" if !defined($newsize);
1757
1758 die "no options specified\n" if !scalar(keys %$param);
1759
985b18ed
WL
1760 my $storage_cfg = cfs_read_file("storage.cfg");
1761
985b18ed
WL
1762 my $code = sub {
1763
67afe46e
FG
1764 my $conf = PVE::LXC::Config->load_config($vmid);
1765 PVE::LXC::Config->check_lock($conf);
985b18ed 1766
de41bced
DC
1767 PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, $conf, $param, [], $conf->{unprivileged});
1768
985b18ed
WL
1769 PVE::Tools::assert_if_modified($digest, $conf->{digest});
1770
1771 my $running = PVE::LXC::check_running($vmid);
1772
1773 my $disk = $param->{disk};
e4034859 1774 my $mp = PVE::LXC::Config->parse_volume($disk, $conf->{$disk});
44a9face 1775
985b18ed
WL
1776 my $volid = $mp->{volume};
1777
1778 my (undef, undef, $owner, undef, undef, undef, $format) =
1779 PVE::Storage::parse_volname($storage_cfg, $volid);
1780
235dbdf3 1781 die "can't resize mount point owned by another container ($owner)"
985b18ed
WL
1782 if $vmid != $owner;
1783
1784 die "can't resize volume: $disk if snapshot exists\n"
1785 if %{$conf->{snapshots}} && $format eq 'qcow2';
1786
1787 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
1788
1789 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
1790
4e1e1791
WB
1791 PVE::Storage::activate_volumes($storage_cfg, [$volid]);
1792
985b18ed 1793 my $size = PVE::Storage::volume_size_info($storage_cfg, $volid, 5);
2e8bcfef 1794
16f6c7c6 1795 die "Could not determine current size of volume '$volid'\n" if !defined($size);
2e8bcfef 1796
985b18ed
WL
1797 $newsize += $size if $ext;
1798 $newsize = int($newsize);
1799
1800 die "unable to shrink disk size\n" if $newsize < $size;
1801
5c36822a 1802 die "disk is already at specified size\n" if $size == $newsize;
985b18ed
WL
1803
1804 PVE::Cluster::log_msg('info', $authuser, "update CT $vmid: resize --disk $disk --size $sizestr");
9f3f7963 1805 my $realcmd = sub {
e72c8b0e
DM
1806 # Note: PVE::Storage::volume_resize doesn't do anything if $running=1, so
1807 # we pass 0 here (parameter only makes sense for qemu)
9f3f7963
WL
1808 PVE::Storage::volume_resize($storage_cfg, $volid, $newsize, 0);
1809
1810 $mp->{size} = $newsize;
1b4cf758 1811 $conf->{$disk} = PVE::LXC::Config->print_ct_mountpoint($mp, $disk eq 'rootfs');
9f3f7963 1812
67afe46e 1813 PVE::LXC::Config->write_config($vmid, $conf);
9f3f7963
WL
1814
1815 if ($format eq 'raw') {
46e62d2d
TL
1816 # we need to ensure that the volume is mapped, if not needed this is a NOP
1817 my $path = PVE::Storage::map_volume($storage_cfg, $volid);
1818 $path = PVE::Storage::path($storage_cfg, $volid) if !defined($path);
9f3f7963 1819 if ($running) {
2a993004
WL
1820
1821 $mp->{mp} = '/';
1822 my $use_loopdev = (PVE::LXC::mountpoint_mount_path($mp, $storage_cfg))[1];
21f292ff 1823 $path = PVE::LXC::query_loopdev($path) if $use_loopdev;
235dbdf3 1824 die "internal error: CT running but mount point not attached to a loop device"
2a993004
WL
1825 if !$path;
1826 PVE::Tools::run_command(['losetup', '--set-capacity', $path]) if $use_loopdev;
9f3f7963
WL
1827
1828 # In order for resize2fs to know that we need online-resizing a mountpoint needs
1829 # to be visible to it in its namespace.
1830 # To not interfere with the rest of the system we unshare the current mount namespace,
1831 # mount over /tmp and then run resize2fs.
1832
1833 # interestingly we don't need to e2fsck on mounted systems...
1834 my $quoted = PVE::Tools::shellquote($path);
1835 my $cmd = "mount --make-rprivate / && mount $quoted /tmp && resize2fs $quoted";
ce9e6458
WB
1836 eval {
1837 PVE::Tools::run_command(['unshare', '-m', '--', 'sh', '-c', $cmd]);
1838 };
1839 warn "Failed to update the container's filesystem: $@\n" if $@;
9f3f7963 1840 } else {
ce9e6458
WB
1841 eval {
1842 PVE::Tools::run_command(['e2fsck', '-f', '-y', $path]);
1843 PVE::Tools::run_command(['resize2fs', $path]);
1844 };
1845 warn "Failed to update the container's filesystem: $@\n" if $@;
80440faa 1846
46ea6368
TL
1847 # always un-map if not running, this is a NOP if not needed
1848 PVE::Storage::unmap_volume($storage_cfg, $volid);
9f3f7963 1849 }
985b18ed 1850 }
9f3f7963 1851 };
985b18ed 1852
9f3f7963
WL
1853 return $rpcenv->fork_worker('resize', $vmid, $authuser, $realcmd);
1854 };
985b18ed 1855
67afe46e 1856 return PVE::LXC::Config->lock_config($vmid, $code);;
985b18ed
WL
1857 }});
1858
3c0f6806
WB
1859__PACKAGE__->register_method({
1860 name => 'move_volume',
1861 path => '{vmid}/move_volume',
1862 method => 'POST',
1863 protected => 1,
1864 proxyto => 'node',
a337a832 1865 description => "Move a rootfs-/mp-volume to a different storage or to a different container.",
3c0f6806
WB
1866 permissions => {
1867 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
a337a832
AL
1868 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
1869 "a volume to another container, you need the permissions on the ".
1870 "target container as well.",
d568e8db 1871 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
3c0f6806
WB
1872 },
1873 parameters => {
1874 additionalProperties => 0,
1875 properties => {
1876 node => get_standard_option('pve-node'),
1877 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
a337a832
AL
1878 'target-vmid' => get_standard_option('pve-vmid', {
1879 completion => \&PVE::LXC::complete_ctid,
1880 optional => 1,
1881 }),
3c0f6806
WB
1882 volume => {
1883 type => 'string',
a337a832
AL
1884 #TODO: check how to handle unused mount points as the mp parameter is not configured
1885 enum => [ PVE::LXC::Config->valid_volume_keys_with_unused() ],
3c0f6806
WB
1886 description => "Volume which will be moved.",
1887 },
1888 storage => get_standard_option('pve-storage-id', {
1889 description => "Target Storage.",
1890 completion => \&PVE::Storage::complete_storage_enabled,
a337a832 1891 optional => 1,
3c0f6806
WB
1892 }),
1893 delete => {
1894 type => 'boolean',
d2f08c5a
AL
1895 description => "Delete the original volume after successful copy. By default the " .
1896 "original is kept as an unused volume entry.",
3c0f6806
WB
1897 optional => 1,
1898 default => 0,
1899 },
1900 digest => {
1901 type => 'string',
d2f08c5a
AL
1902 description => 'Prevent changes if current configuration file has different SHA1 " .
1903 "digest. This can be used to prevent concurrent modifications.',
3c0f6806
WB
1904 maxLength => 40,
1905 optional => 1,
8ead60ef
SI
1906 },
1907 bwlimit => {
1908 description => "Override I/O bandwidth limit (in KiB/s).",
1909 optional => 1,
1910 type => 'number',
1911 minimum => '0',
41e9b65c 1912 default => 'clone limit from datacenter or storage config',
8ead60ef 1913 },
a337a832
AL
1914 'target-volume' => {
1915 type => 'string',
1916 description => "The config key the volume will be moved to. Default is the " .
1917 "source volume key.",
1918 enum => [PVE::LXC::Config->valid_volume_keys_with_unused()],
1919 optional => 1,
1920 },
1921 'target-digest' => {
1922 type => 'string',
1923 description => 'Prevent changes if current configuration file of the target " .
1924 "container has a different SHA1 digest. This can be used to prevent " .
1925 "concurrent modifications.',
1926 maxLength => 40,
1927 optional => 1,
1928 },
3c0f6806
WB
1929 },
1930 },
1931 returns => {
1932 type => 'string',
1933 },
1934 code => sub {
1935 my ($param) = @_;
1936
1937 my $rpcenv = PVE::RPCEnvironment::get();
1938
1939 my $authuser = $rpcenv->get_user();
1940
1941 my $vmid = extract_param($param, 'vmid');
1942
a337a832
AL
1943 my $target_vmid = extract_param($param, 'target-vmid');
1944
3c0f6806
WB
1945 my $storage = extract_param($param, 'storage');
1946
1947 my $mpkey = extract_param($param, 'volume');
1948
a337a832
AL
1949 my $target_mpkey = extract_param($param, 'target-volume') // $mpkey;
1950
1951 my $digest = extract_param($param, 'digest');
1952
1953 my $target_digest = extract_param($param, 'target-digest');
1954
3c0f6806
WB
1955 my $lockname = 'disk';
1956
1957 my ($mpdata, $old_volid);
1958
a337a832
AL
1959 die "either set storage or target-vmid, but not both\n"
1960 if $storage && $target_vmid;
3c0f6806 1961
a337a832 1962 my $storecfg = PVE::Storage::config();
3c0f6806 1963
a337a832
AL
1964 my $move_to_storage_checks = sub {
1965 PVE::LXC::Config->lock_config($vmid, sub {
1966 my $conf = PVE::LXC::Config->load_config($vmid);
1967 PVE::LXC::Config->check_lock($conf);
3c0f6806 1968
a337a832
AL
1969 die "cannot move volumes of a running container\n"
1970 if PVE::LXC::check_running($vmid);
3c0f6806 1971
a337a832
AL
1972 if ($mpkey =~ m/^unused\d+$/) {
1973 die "cannot move volume '$mpkey', only configured volumes can be moved to ".
1974 "another storage\n";
1975 }
3c0f6806 1976
a337a832
AL
1977 $mpdata = PVE::LXC::Config->parse_volume($mpkey, $conf->{$mpkey});
1978 $old_volid = $mpdata->{volume};
3c0f6806 1979
a337a832
AL
1980 die "you can't move a volume with snapshots and delete the source\n"
1981 if $param->{delete} && PVE::LXC::Config->is_volume_in_use_by_snapshots($conf, $old_volid);
1982
1983 PVE::Tools::assert_if_modified($digest, $conf->{digest});
1984
1985 PVE::LXC::Config->set_lock($vmid, $lockname);
1986 });
1987 };
1988
1989 my $storage_realcmd = sub {
3c0f6806 1990 eval {
d2f08c5a
AL
1991 PVE::Cluster::log_msg(
1992 'info',
1993 $authuser,
1994 "move volume CT $vmid: move --volume $mpkey --storage $storage"
1995 );
3c0f6806
WB
1996
1997 my $conf = PVE::LXC::Config->load_config($vmid);
1998 my $storage_cfg = PVE::Storage::config();
1999
2000 my $new_volid;
2001
2002 eval {
2003 PVE::Storage::activate_volumes($storage_cfg, [ $old_volid ]);
8ead60ef
SI
2004 my $bwlimit = extract_param($param, 'bwlimit');
2005 my $source_storage = PVE::Storage::parse_volume_id($old_volid);
d2f08c5a
AL
2006 my $movelimit = PVE::Storage::get_bandwidth_limit(
2007 'move',
2008 [$source_storage, $storage],
2009 $bwlimit
2010 );
2011 $new_volid = PVE::LXC::copy_volume(
2012 $mpdata,
2013 $vmid,
2014 $storage,
2015 $storage_cfg,
2016 $conf,
2017 undef,
2018 $movelimit
2019 );
3bfbd900
FE
2020 if (PVE::LXC::Config->is_template($conf)) {
2021 PVE::Storage::activate_volumes($storage_cfg, [ $new_volid ]);
2022 my $template_volid = PVE::Storage::vdisk_create_base($storage_cfg, $new_volid);
2023 $mpdata->{volume} = $template_volid;
2024 } else {
2025 $mpdata->{volume} = $new_volid;
2026 }
3c0f6806
WB
2027
2028 PVE::LXC::Config->lock_config($vmid, sub {
2029 my $digest = $conf->{digest};
2030 $conf = PVE::LXC::Config->load_config($vmid);
2031 PVE::Tools::assert_if_modified($digest, $conf->{digest});
2032
d2f08c5a
AL
2033 $conf->{$mpkey} = PVE::LXC::Config->print_ct_mountpoint(
2034 $mpdata,
2035 $mpkey eq 'rootfs'
2036 );
3c0f6806
WB
2037
2038 PVE::LXC::Config->add_unused_volume($conf, $old_volid) if !$param->{delete};
2039
2040 PVE::LXC::Config->write_config($vmid, $conf);
2041 });
2042
2043 eval {
2044 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2045 PVE::Storage::deactivate_volumes($storage_cfg, [ $new_volid ])
2046 };
2047 warn $@ if $@;
2048 };
2049 if (my $err = $@) {
2050 eval {
2051 PVE::Storage::vdisk_free($storage_cfg, $new_volid)
2052 if defined($new_volid);
2053 };
2054 warn $@ if $@;
2055 die $err;
2056 }
2057
4be329cb
DC
2058 my $deactivated = 0;
2059 eval {
2060 PVE::Storage::deactivate_volumes($storage_cfg, [ $old_volid ]);
2061 $deactivated = 1;
2062 };
2063 warn $@ if $@;
2064
3c0f6806 2065 if ($param->{delete}) {
4be329cb
DC
2066 my $removed = 0;
2067 if ($deactivated) {
2068 eval {
2069 PVE::Storage::vdisk_free($storage_cfg, $old_volid);
2070 $removed = 1;
2071 };
2072 warn $@ if $@;
2073 }
2074 if (!$removed) {
9abb0f8a
FE
2075 PVE::LXC::Config->lock_config($vmid, sub {
2076 my $conf = PVE::LXC::Config->load_config($vmid);
2077 PVE::LXC::Config->add_unused_volume($conf, $old_volid);
2078 PVE::LXC::Config->write_config($vmid, $conf);
2079 });
2080 }
3c0f6806
WB
2081 }
2082 };
2083 my $err = $@;
2084 eval { PVE::LXC::Config->remove_lock($vmid, $lockname) };
2085 warn $@ if $@;
2086 die $err if $err;
2087 };
a337a832
AL
2088
2089 my $load_and_check_reassign_configs = sub {
2090 my $vmlist = PVE::Cluster::get_vmlist()->{ids};
2091
2092 die "Cannot move to/from 'rootfs'\n" if $mpkey eq "rootfs" || $target_mpkey eq "rootfs";
2093
2094 if ($mpkey =~ m/^unused\d+$/ && $target_mpkey !~ m/^unused\d+$/) {
2095 die "Moving an unused volume to a used one is not possible\n";
2096 }
2097 die "could not find CT ${vmid}\n" if !exists($vmlist->{$vmid});
27e28f79 2098 die "could not find CT ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
a337a832 2099
27e28f79
FG
2100 my $source_node = $vmlist->{$vmid}->{node};
2101 my $target_node = $vmlist->{$target_vmid}->{node};
2102
2103 die "Both containers need to be on the same node ($source_node != $target_node)\n"
2104 if $source_node ne $target_node;
a337a832
AL
2105
2106 my $source_conf = PVE::LXC::Config->load_config($vmid);
2107 PVE::LXC::Config->check_lock($source_conf);
1e5a3948
TL
2108 my $target_conf;
2109 if ($target_vmid eq $vmid) {
2110 $target_conf = $source_conf;
2111 } else {
2112 $target_conf = PVE::LXC::Config->load_config($target_vmid);
2113 PVE::LXC::Config->check_lock($target_conf);
2114 }
a337a832 2115
27e28f79 2116 die "Can't move volumes from or to template CT\n"
a337a832
AL
2117 if ($source_conf->{template} || $target_conf->{template});
2118
2119 if ($digest) {
2120 eval { PVE::Tools::assert_if_modified($digest, $source_conf->{digest}) };
2121 die "Container ${vmid}: $@" if $@;
2122 }
2123
2124 if ($target_digest) {
2125 eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
2126 die "Container ${target_vmid}: $@" if $@;
2127 }
2128
2129 die "volume '${mpkey}' for container '$vmid' does not exist\n"
2130 if !defined($source_conf->{$mpkey});
2131
2132 die "Target volume key '${target_mpkey}' is already in use for container '$target_vmid'\n"
2133 if exists $target_conf->{$target_mpkey};
2134
3785fe32
TL
2135 my $drive = PVE::LXC::Config->parse_volume($mpkey, $source_conf->{$mpkey});
2136 my $source_volid = $drive->{volume} or die "Volume '${mpkey}' has no associated image\n";
a337a832
AL
2137 die "Cannot move volume used by a snapshot to another container\n"
2138 if PVE::LXC::Config->is_volume_in_use_by_snapshots($source_conf, $source_volid);
2139 die "Storage does not support moving of this disk to another container\n"
2140 if !PVE::Storage::volume_has_feature($storecfg, 'rename', $source_volid);
27e28f79 2141 die "Cannot move a bindmount or device mount to another container\n"
a337a832 2142 if $drive->{type} ne "volume";
3785fe32 2143 die "Cannot move in-use volume while the source CT is running - detach or shutdown first\n"
a337a832
AL
2144 if PVE::LXC::check_running($vmid) && $mpkey !~ m/^unused\d+$/;
2145
2146 my $repl_conf = PVE::ReplicationConfig->new();
27e28f79
FG
2147 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
2148 my ($storeid, undef) = PVE::Storage::parse_volume_id($source_volid);
2149 my $format = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
2150
2151 die "Cannot move volume on storage '$storeid' to a replicated container - missing replication support\n"
2152 if !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
a337a832 2153 }
27e28f79
FG
2154
2155 return ($source_conf, $target_conf, $drive);
3c0f6806 2156 };
a337a832 2157
3785fe32 2158 my $logfunc = sub { print STDERR "$_[0]\n"; };
a337a832
AL
2159
2160 my $volume_reassignfn = sub {
2161 return PVE::LXC::Config->lock_config($vmid, sub {
2162 return PVE::LXC::Config->lock_config($target_vmid, sub {
3785fe32 2163 my ($source_conf, $target_conf, $drive) = $load_and_check_reassign_configs->();
27e28f79 2164 my $source_volid = $drive->{volume};
a337a832
AL
2165
2166 my $target_unused = $target_mpkey =~ m/^unused\d+$/;
2167
a337a832 2168 print "moving volume '$mpkey' from container '$vmid' to '$target_vmid'\n";
27e28f79 2169
a337a832
AL
2170 my ($storage, $source_volname) = PVE::Storage::parse_volume_id($source_volid);
2171
2172 my $fmt = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
2173
2174 my $new_volid = PVE::Storage::rename_volume(
2175 $storecfg,
2176 $source_volid,
2177 $target_vmid,
2178 );
2179
27e28f79 2180 $drive->{volume} = $new_volid;
a337a832
AL
2181
2182 delete $source_conf->{$mpkey};
2183 print "removing volume '${mpkey}' from container '${vmid}' config\n";
2184 PVE::LXC::Config->write_config($vmid, $source_conf);
2185
2186 my $drive_string;
a337a832
AL
2187 if ($target_unused) {
2188 $drive_string = $new_volid;
2189 } else {
27e28f79 2190 $drive_string = PVE::LXC::Config->print_volume($target_mpkey, $drive);
a337a832
AL
2191 }
2192
2193 if ($target_unused) {
2194 $target_conf->{$target_mpkey} = $drive_string;
2195 } else {
2196 my $running = PVE::LXC::check_running($target_vmid);
2197 my $param = { $target_mpkey => $drive_string };
2198 my $errors = PVE::LXC::Config->update_pct_config(
2199 $target_vmid,
2200 $target_conf,
2201 $running,
2202 $param
2203 );
3785fe32 2204 $rpcenv->warn($errors->{$_}) for keys $errors->%*;
a337a832
AL
2205 }
2206
2207 PVE::LXC::Config->write_config($target_vmid, $target_conf);
2208 $target_conf = PVE::LXC::Config->load_config($target_vmid);
2209
2210 PVE::LXC::update_lxc_config($target_vmid, $target_conf) if !$target_unused;
2211 print "target container '$target_vmid' updated with '$target_mpkey'\n";
2212
2213 # remove possible replication snapshots
2214 if (PVE::Storage::volume_has_feature($storecfg,'replicate', $source_volid)) {
2215 eval {
2216 PVE::Replication::prepare(
2217 $storecfg,
2218 [$new_volid],
2219 undef,
2220 1,
2221 undef,
2222 $logfunc,
2223 )
2224 };
2225 if (my $err = $@) {
2226 $rpcenv->warn("Failed to remove replication snapshots on volume ".
2227 "'${target_mpkey}'. Manual cleanup could be necessary. " .
2228 "Error: ${err}\n");
2229 }
2230 }
2231 });
2232 });
2233 };
2234
27e28f79
FG
2235 if ($target_vmid && $storage) {
2236 my $msg = "either set 'storage' or 'target-vmid', but not both";
2237 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
2238 } elsif ($target_vmid) {
a337a832
AL
2239 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
2240 if $authuser ne 'root@pam';
2241
3785fe32 2242 my (undef, undef, $drive) = $load_and_check_reassign_configs->();
d568e8db
FG
2243 my $storeid = PVE::Storage::parse_volume_id($drive->{volume});
2244 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
a337a832
AL
2245 return $rpcenv->fork_worker(
2246 'move_volume',
2247 "${vmid}-${mpkey}>${target_vmid}-${target_mpkey}",
2248 $authuser,
2249 $volume_reassignfn
2250 );
2251 } elsif ($storage) {
d568e8db 2252 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
a337a832
AL
2253 &$move_to_storage_checks();
2254 my $task = eval {
2255 $rpcenv->fork_worker('move_volume', $vmid, $authuser, $storage_realcmd);
2256 };
2257 if (my $err = $@) {
2258 eval { PVE::LXC::Config->remove_lock($vmid, $lockname) };
2259 warn $@ if $@;
2260 die $err;
2261 }
2262 return $task;
2263 } else {
27e28f79
FG
2264 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
2265 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
3c0f6806 2266 }
3c0f6806
WB
2267 }});
2268
37c2dba8
OB
2269__PACKAGE__->register_method({
2270 name => 'vm_pending',
2271 path => '{vmid}/pending',
2272 method => 'GET',
2273 proxyto => 'node',
2274 description => 'Get container configuration, including pending changes.',
2275 permissions => {
2276 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2277 },
2278 parameters => {
2279 additionalProperties => 0,
2280 properties => {
2281 node => get_standard_option('pve-node'),
2282 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
2283 },
2284 },
2285 returns => {
2286 type => "array",
2287 items => {
2288 type => "object",
2289 properties => {
2290 key => {
2291 description => 'Configuration option name.',
2292 type => 'string',
2293 },
2294 value => {
2295 description => 'Current value.',
2296 type => 'string',
2297 optional => 1,
2298 },
2299 pending => {
2300 description => 'Pending value.',
2301 type => 'string',
2302 optional => 1,
2303 },
2304 delete => {
2305 description => "Indicates a pending delete request if present and not 0.",
2306 type => 'integer',
2307 minimum => 0,
2308 maximum => 2,
2309 optional => 1,
2310 },
2311 },
2312 },
2313 },
2314 code => sub {
2315 my ($param) = @_;
2316
2317 my $conf = PVE::LXC::Config->load_config($param->{vmid});
2318
2319 my $pending_delete_hash = PVE::LXC::Config->parse_pending_delete($conf->{pending}->{delete});
2320
f622e7dc 2321 return PVE::GuestHelpers::config_with_pending_array($conf, $pending_delete_hash);
37c2dba8
OB
2322 }});
2323
f76a2828 23241;