]> git.proxmox.com Git - pve-container.git/blame - src/PVE/LXC/Config.pm
config: apply_pending_mountpoint helper
[pve-container.git] / src / PVE / LXC / Config.pm
CommitLineData
67afe46e
FG
1package PVE::LXC::Config;
2
3use strict;
4use warnings;
5
6use PVE::AbstractConfig;
7use PVE::Cluster qw(cfs_register_file);
8b076579 8use PVE::DataCenterConfig;
1a416433 9use PVE::GuestHelpers;
67afe46e
FG
10use PVE::INotify;
11use PVE::JSONSchema qw(get_standard_option);
12use PVE::Tools;
13
14use base qw(PVE::AbstractConfig);
15
16my $nodename = PVE::INotify::nodename();
17my $lock_handles = {};
18my $lockdir = "/run/lock/lxc";
19mkdir $lockdir;
20mkdir "/etc/pve/nodes/$nodename/lxc";
717f70b7 21my $MAX_MOUNT_POINTS = 256;
67afe46e
FG
22my $MAX_UNUSED_DISKS = $MAX_MOUNT_POINTS;
23
24# BEGIN implemented abstract methods from PVE::AbstractConfig
25
26sub guest_type {
27 return "CT";
28}
29
4518000b
FG
30sub __config_max_unused_disks {
31 my ($class) = @_;
32
33 return $MAX_UNUSED_DISKS;
34}
35
67afe46e
FG
36sub config_file_lock {
37 my ($class, $vmid) = @_;
38
39 return "$lockdir/pve-config-${vmid}.lock";
40}
41
42sub cfs_config_path {
43 my ($class, $vmid, $node) = @_;
44
45 $node = $nodename if !$node;
46 return "nodes/$node/lxc/$vmid.conf";
47}
48
1a8269bc 49sub mountpoint_backup_enabled {
ec7f0f09 50 my ($class, $mp_key, $mountpoint) = @_;
1a8269bc
DM
51
52 return 1 if $mp_key eq 'rootfs';
53
f59c9670
FG
54 return 0 if $mountpoint->{type} ne 'volume';
55
1a8269bc
DM
56 return 1 if $mountpoint->{backup};
57
58 return 0;
59}
60
4518000b
FG
61sub has_feature {
62 my ($class, $feature, $conf, $storecfg, $snapname, $running, $backup_only) = @_;
63 my $err;
64
d250604f 65 $class->foreach_mountpoint($conf, sub {
4518000b
FG
66 my ($ms, $mountpoint) = @_;
67
68 return if $err; # skip further test
ec7f0f09 69 return if $backup_only && !$class->mountpoint_backup_enabled($ms, $mountpoint);
4518000b
FG
70
71 $err = 1
72 if !PVE::Storage::volume_has_feature($storecfg, $feature,
73 $mountpoint->{volume},
74 $snapname, $running);
75 });
76
77 return $err ? 0 : 1;
78}
79
80sub __snapshot_save_vmstate {
81 my ($class, $vmid, $conf, $snapname, $storecfg) = @_;
82 die "implement me - snapshot_save_vmstate\n";
83}
84
85sub __snapshot_check_running {
86 my ($class, $vmid) = @_;
87 return PVE::LXC::check_running($vmid);
88}
89
90sub __snapshot_check_freeze_needed {
91 my ($class, $vmid, $config, $save_vmstate) = @_;
92
93 my $ret = $class->__snapshot_check_running($vmid);
94 return ($ret, $ret);
95}
96
97sub __snapshot_freeze {
98 my ($class, $vmid, $unfreeze) = @_;
99
100 if ($unfreeze) {
101 eval { PVE::Tools::run_command(['/usr/bin/lxc-unfreeze', '-n', $vmid]); };
102 warn $@ if $@;
103 } else {
104 PVE::Tools::run_command(['/usr/bin/lxc-freeze', '-n', $vmid]);
105 PVE::LXC::sync_container_namespace($vmid);
106 }
107}
108
109sub __snapshot_create_vol_snapshot {
110 my ($class, $vmid, $ms, $mountpoint, $snapname) = @_;
111
112 my $storecfg = PVE::Storage::config();
113
1a8269bc 114 return if $snapname eq 'vzdump' &&
ec7f0f09 115 !$class->mountpoint_backup_enabled($ms, $mountpoint);
1a8269bc 116
4518000b
FG
117 PVE::Storage::volume_snapshot($storecfg, $mountpoint->{volume}, $snapname);
118}
119
120sub __snapshot_delete_remove_drive {
121 my ($class, $snap, $remove_drive) = @_;
122
123 if ($remove_drive eq 'vmstate') {
124 die "implement me - saving vmstate\n";
125 } else {
126 my $value = $snap->{$remove_drive};
1b4cf758 127 my $mountpoint = $remove_drive eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
4518000b 128 delete $snap->{$remove_drive};
d103721f
FG
129
130 $class->add_unused_volume($snap, $mountpoint->{volume})
131 if ($mountpoint->{type} eq 'volume');
4518000b
FG
132 }
133}
134
135sub __snapshot_delete_vmstate_file {
136 my ($class, $snap, $force) = @_;
137
138 die "implement me - saving vmstate\n";
139}
140
141sub __snapshot_delete_vol_snapshot {
a8e9a4ea 142 my ($class, $vmid, $ms, $mountpoint, $snapname, $unused) = @_;
4518000b 143
d103721f
FG
144 return if $snapname eq 'vzdump' &&
145 !$class->mountpoint_backup_enabled($ms, $mountpoint);
146
4518000b
FG
147 my $storecfg = PVE::Storage::config();
148 PVE::Storage::volume_snapshot_delete($storecfg, $mountpoint->{volume}, $snapname);
a8e9a4ea 149 push @$unused, $mountpoint->{volume};
4518000b
FG
150}
151
152sub __snapshot_rollback_vol_possible {
153 my ($class, $mountpoint, $snapname) = @_;
154
155 my $storecfg = PVE::Storage::config();
156 PVE::Storage::volume_rollback_is_possible($storecfg, $mountpoint->{volume}, $snapname);
157}
158
159sub __snapshot_rollback_vol_rollback {
160 my ($class, $mountpoint, $snapname) = @_;
161
162 my $storecfg = PVE::Storage::config();
163 PVE::Storage::volume_snapshot_rollback($storecfg, $mountpoint->{volume}, $snapname);
164}
165
166sub __snapshot_rollback_vm_stop {
167 my ($class, $vmid) = @_;
168
b1bad293 169 PVE::LXC::vm_stop($vmid, 1)
4518000b
FG
170 if $class->__snapshot_check_running($vmid);
171}
172
173sub __snapshot_rollback_vm_start {
67779c3c 174 my ($class, $vmid, $vmstate, $data);
4518000b
FG
175
176 die "implement me - save vmstate\n";
177}
178
a8656869
FG
179sub __snapshot_rollback_get_unused {
180 my ($class, $conf, $snap) = @_;
181
182 my $unused = [];
183
184 $class->__snapshot_foreach_volume($conf, sub {
185 my ($vs, $volume) = @_;
186
187 return if $volume->{type} ne 'volume';
188
189 my $found = 0;
190 my $volid = $volume->{volume};
191
192 $class->__snapshot_foreach_volume($snap, sub {
193 my ($ms, $mountpoint) = @_;
194
195 return if $found;
196 return if ($mountpoint->{type} ne 'volume');
197
198 $found = 1
199 if ($mountpoint->{volume} && $mountpoint->{volume} eq $volid);
200 });
201
202 push @$unused, $volid if !$found;
203 });
204
205 return $unused;
206}
207
4518000b
FG
208sub __snapshot_foreach_volume {
209 my ($class, $conf, $func) = @_;
210
d250604f 211 $class->foreach_mountpoint($conf, $func);
4518000b
FG
212}
213
67afe46e
FG
214# END implemented abstract methods from PVE::AbstractConfig
215
1b4cf758
FG
216# BEGIN JSON config code
217
218cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
219
2bf24eb3 220
e80cb0cd
TL
221my $valid_mount_option_re = qr/(noatime|nodev|nosuid|noexec)/;
222
223sub is_valid_mount_option {
224 my ($option) = @_;
225 return $option =~ $valid_mount_option_re;
2bf24eb3
OB
226}
227
1b4cf758
FG
228my $rootfs_desc = {
229 volume => {
230 type => 'string',
231 default_key => 1,
232 format => 'pve-lxc-mp-string',
233 format_description => 'volume',
234 description => 'Volume, device or directory to mount into the container.',
235 },
1b4cf758
FG
236 size => {
237 type => 'string',
238 format => 'disk-size',
239 format_description => 'DiskSize',
240 description => 'Volume size (read only value).',
241 optional => 1,
242 },
243 acl => {
244 type => 'boolean',
1b4cf758
FG
245 description => 'Explicitly enable or disable ACL support.',
246 optional => 1,
247 },
2bf24eb3
OB
248 mountoptions => {
249 optional => 1,
250 type => 'string',
251 description => 'Extra mount options for rootfs/mps.',
252 format_description => 'opt[;opt...]',
e80cb0cd 253 pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
2bf24eb3 254 },
1b4cf758
FG
255 ro => {
256 type => 'boolean',
235dbdf3 257 description => 'Read-only mount point',
1b4cf758
FG
258 optional => 1,
259 },
260 quota => {
261 type => 'boolean',
1b4cf758
FG
262 description => 'Enable user quotas inside the container (not supported with zfs subvolumes)',
263 optional => 1,
264 },
76ec0820 265 replicate => {
f8aa3d35
WL
266 type => 'boolean',
267 description => 'Will include this volume to a storage replica job.',
268 optional => 1,
269 default => 1,
270 },
552e168f
FG
271 shared => {
272 type => 'boolean',
273 description => 'Mark this non-volume mount point as available on multiple nodes (see \'nodes\')',
274 verbose_description => "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!",
275 optional => 1,
276 default => 0,
277 },
1b4cf758
FG
278};
279
280PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
281 type => 'string', format => $rootfs_desc,
282 description => "Use volume as container root.",
283 optional => 1,
284});
285
5a63f1c5
WB
286my $features_desc = {
287 mount => {
288 optional => 1,
289 type => 'string',
290 description => "Allow mounting file systems of specific types."
291 ." This should be a list of file system types as used with the mount command."
292 ." Note that this can have negative effects on the container's security."
293 ." With access to a loop device, mounting a file can circumvent the mknod"
294 ." permission of the devices cgroup, mounting an NFS file system can"
295 ." block the host's I/O completely and prevent it from rebooting, etc.",
296 format_description => 'fstype;fstype;...',
e188f1bb 297 pattern => qr/[a-zA-Z0-9_; ]+/,
5a63f1c5
WB
298 },
299 nesting => {
300 optional => 1,
301 type => 'boolean',
302 default => 0,
303 description => "Allow nesting."
304 ." Best used with unprivileged containers with additional id mapping."
305 ." Note that this will expose procfs and sysfs contents of the host"
306 ." to the guest.",
307 },
308 keyctl => {
309 optional => 1,
310 type => 'boolean',
311 default => 0,
312 description => "For unprivileged containers only: Allow the use of the keyctl() system call."
313 ." This is required to use docker inside a container."
314 ." By default unprivileged containers will see this system call as non-existent."
315 ." This is mostly a workaround for systemd-networkd, as it will treat it as a fatal"
316 ." error when some keyctl() operations are denied by the kernel due to lacking permissions."
317 ." Essentially, you can choose between running systemd-networkd or docker.",
318 },
96f8d2a2
WB
319 fuse => {
320 optional => 1,
321 type => 'boolean',
322 default => 0,
323 description => "Allow using 'fuse' file systems in a container."
324 ." Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.",
325 },
5a63f1c5
WB
326};
327
1b4cf758
FG
328my $confdesc = {
329 lock => {
330 optional => 1,
331 type => 'string',
332 description => "Lock/unlock the VM.",
7fc1d9eb 333 enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
1b4cf758
FG
334 },
335 onboot => {
336 optional => 1,
337 type => 'boolean',
338 description => "Specifies whether a VM will be started during system bootup.",
339 default => 0,
340 },
341 startup => get_standard_option('pve-startup-order'),
342 template => {
343 optional => 1,
344 type => 'boolean',
345 description => "Enable/disable Template.",
346 default => 0,
347 },
348 arch => {
349 optional => 1,
350 type => 'string',
e1d54a38 351 enum => ['amd64', 'i386', 'arm64', 'armhf'],
1b4cf758
FG
352 description => "OS architecture type.",
353 default => 'amd64',
354 },
355 ostype => {
356 optional => 1,
357 type => 'string',
ed027b58 358 enum => [qw(debian ubuntu centos fedora opensuse archlinux alpine gentoo unmanaged)],
1b4cf758
FG
359 description => "OS type. This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/<ostype>.common.conf. Value 'unmanaged' can be used to skip and OS specific setup.",
360 },
361 console => {
362 optional => 1,
363 type => 'boolean',
364 description => "Attach a console device (/dev/console) to the container.",
365 default => 1,
366 },
367 tty => {
368 optional => 1,
369 type => 'integer',
370 description => "Specify the number of tty available to the container",
371 minimum => 0,
372 maximum => 6,
373 default => 2,
374 },
f2357408
DM
375 cores => {
376 optional => 1,
377 type => 'integer',
378 description => "The number of cores assigned to the container. A container can use all available cores by default.",
379 minimum => 1,
380 maximum => 128,
381 },
1b4cf758
FG
382 cpulimit => {
383 optional => 1,
384 type => 'number',
064529c3 385 description => "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.",
1b4cf758
FG
386 minimum => 0,
387 maximum => 128,
388 default => 0,
389 },
390 cpuunits => {
391 optional => 1,
392 type => 'integer',
393 description => "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to the weights of all the other running VMs.\n\nNOTE: You can disable fair-scheduler configuration by setting this to 0.",
394 minimum => 0,
395 maximum => 500000,
396 default => 1024,
397 },
398 memory => {
399 optional => 1,
400 type => 'integer',
401 description => "Amount of RAM for the VM in MB.",
402 minimum => 16,
403 default => 512,
404 },
405 swap => {
406 optional => 1,
407 type => 'integer',
408 description => "Amount of SWAP for the VM in MB.",
409 minimum => 0,
410 default => 512,
411 },
412 hostname => {
413 optional => 1,
414 description => "Set a host name for the container.",
415 type => 'string', format => 'dns-name',
416 maxLength => 255,
417 },
418 description => {
419 optional => 1,
420 type => 'string',
a069f163 421 description => "Container description. Only used on the configuration web interface.",
1b4cf758
FG
422 },
423 searchdomain => {
424 optional => 1,
425 type => 'string', format => 'dns-name-list',
426 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
427 },
428 nameserver => {
429 optional => 1,
430 type => 'string', format => 'address-list',
431 description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
432 },
433 rootfs => get_standard_option('pve-ct-rootfs'),
434 parent => {
435 optional => 1,
436 type => 'string', format => 'pve-configid',
437 maxLength => 40,
438 description => "Parent snapshot name. This is used internally, and should not be modified.",
439 },
440 snaptime => {
441 optional => 1,
442 description => "Timestamp for snapshots.",
443 type => 'integer',
444 minimum => 0,
445 },
446 cmode => {
447 optional => 1,
448 description => "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).",
449 type => 'string',
450 enum => ['shell', 'console', 'tty'],
451 default => 'tty',
452 },
453 protection => {
454 optional => 1,
455 type => 'boolean',
456 description => "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.",
457 default => 0,
458 },
459 unprivileged => {
460 optional => 1,
461 type => 'boolean',
462 description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
463 default => 0,
464 },
5a63f1c5
WB
465 features => {
466 optional => 1,
467 type => 'string',
468 format => $features_desc,
469 description => "Allow containers access to advanced features.",
470 },
1a416433
DC
471 hookscript => {
472 optional => 1,
473 type => 'string',
474 format => 'pve-volume-id',
475 description => 'Script that will be exectued during various steps in the containers lifetime.',
476 },
1b4cf758
FG
477};
478
479my $valid_lxc_conf_keys = {
108c6cab
WB
480 'lxc.apparmor.profile' => 1,
481 'lxc.apparmor.allow_incomplete' => 1,
d494e03c
WB
482 'lxc.apparmor.allow_nesting' => 1,
483 'lxc.apparmor.raw' => 1,
108c6cab 484 'lxc.selinux.context' => 1,
1b4cf758
FG
485 'lxc.include' => 1,
486 'lxc.arch' => 1,
108c6cab
WB
487 'lxc.uts.name' => 1,
488 'lxc.signal.halt' => 1,
489 'lxc.signal.reboot' => 1,
490 'lxc.signal.stop' => 1,
491 'lxc.init.cmd' => 1,
492 'lxc.pty.max' => 1,
1b4cf758 493 'lxc.console.logfile' => 1,
108c6cab
WB
494 'lxc.console.path' => 1,
495 'lxc.tty.max' => 1,
496 'lxc.devtty.dir' => 1,
1b4cf758
FG
497 'lxc.hook.autodev' => 1,
498 'lxc.autodev' => 1,
499 'lxc.kmsg' => 1,
108c6cab 500 'lxc.mount.fstab' => 1,
1b4cf758
FG
501 'lxc.mount.entry' => 1,
502 'lxc.mount.auto' => 1,
108c6cab 503 'lxc.rootfs.path' => 'lxc.rootfs.path is auto generated from rootfs',
1b4cf758
FG
504 'lxc.rootfs.mount' => 1,
505 'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
235dbdf3 506 ', please use mount point options in the "rootfs" key',
1b4cf758 507 # lxc.cgroup.*
108c6cab 508 # lxc.prlimit.*
6eb23d1e 509 # lxc.net.*
1b4cf758
FG
510 'lxc.cap.drop' => 1,
511 'lxc.cap.keep' => 1,
108c6cab 512 'lxc.seccomp.profile' => 1,
832b4a02
WB
513 'lxc.seccomp.notify.proxy' => 1,
514 'lxc.seccomp.notify.cookie' => 1,
108c6cab 515 'lxc.idmap' => 1,
1b4cf758
FG
516 'lxc.hook.pre-start' => 1,
517 'lxc.hook.pre-mount' => 1,
518 'lxc.hook.mount' => 1,
519 'lxc.hook.start' => 1,
520 'lxc.hook.stop' => 1,
521 'lxc.hook.post-stop' => 1,
522 'lxc.hook.clone' => 1,
523 'lxc.hook.destroy' => 1,
f71db91b 524 'lxc.hook.version' => 1,
108c6cab
WB
525 'lxc.log.level' => 1,
526 'lxc.log.file' => 1,
1b4cf758
FG
527 'lxc.start.auto' => 1,
528 'lxc.start.delay' => 1,
529 'lxc.start.order' => 1,
530 'lxc.group' => 1,
531 'lxc.environment' => 1,
d4a135f7
WB
532
533 # All these are namespaced via CLONE_NEWIPC (see namespaces(7)).
534 'lxc.sysctl.fs.mqueue' => 1,
535 'lxc.sysctl.kernel.msgmax' => 1,
536 'lxc.sysctl.kernel.msgmnb' => 1,
537 'lxc.sysctl.kernel.msgmni' => 1,
538 'lxc.sysctl.kernel.sem' => 1,
539 'lxc.sysctl.kernel.shmall' => 1,
540 'lxc.sysctl.kernel.shmmax' => 1,
541 'lxc.sysctl.kernel.shmmni' => 1,
542 'lxc.sysctl.kernel.shm_rmid_forced' => 1,
1b4cf758
FG
543};
544
108c6cab
WB
545my $deprecated_lxc_conf_keys = {
546 # Deprecated (removed with lxc 3.0):
547 'lxc.aa_profile' => 'lxc.apparmor.profile',
548 'lxc.aa_allow_incomplete' => 'lxc.apparmor.allow_incomplete',
549 'lxc.console' => 'lxc.console.path',
550 'lxc.devttydir' => 'lxc.tty.dir',
551 'lxc.haltsignal' => 'lxc.signal.halt',
552 'lxc.rebootsignal' => 'lxc.signal.reboot',
553 'lxc.stopsignal' => 'lxc.signal.stop',
554 'lxc.id_map' => 'lxc.idmap',
555 'lxc.init_cmd' => 'lxc.init.cmd',
556 'lxc.loglevel' => 'lxc.log.level',
557 'lxc.logfile' => 'lxc.log.file',
558 'lxc.mount' => 'lxc.mount.fstab',
559 'lxc.network.type' => 'lxc.net.INDEX.type',
560 'lxc.network.flags' => 'lxc.net.INDEX.flags',
561 'lxc.network.link' => 'lxc.net.INDEX.link',
562 'lxc.network.mtu' => 'lxc.net.INDEX.mtu',
563 'lxc.network.name' => 'lxc.net.INDEX.name',
564 'lxc.network.hwaddr' => 'lxc.net.INDEX.hwaddr',
565 'lxc.network.ipv4' => 'lxc.net.INDEX.ipv4.address',
566 'lxc.network.ipv4.gateway' => 'lxc.net.INDEX.ipv4.gateway',
567 'lxc.network.ipv6' => 'lxc.net.INDEX.ipv6.address',
568 'lxc.network.ipv6.gateway' => 'lxc.net.INDEX.ipv6.gateway',
569 'lxc.network.script.up' => 'lxc.net.INDEX.script.up',
570 'lxc.network.script.down' => 'lxc.net.INDEX.script.down',
571 'lxc.pts' => 'lxc.pty.max',
572 'lxc.se_context' => 'lxc.selinux.context',
573 'lxc.seccomp' => 'lxc.seccomp.profile',
574 'lxc.tty' => 'lxc.tty.max',
575 'lxc.utsname' => 'lxc.uts.name',
576};
577
578sub is_valid_lxc_conf_key {
579 my ($vmid, $key) = @_;
580 if ($key =~ /^lxc\.limit\./) {
581 warn "vm $vmid - $key: lxc.limit.* was renamed to lxc.prlimit.*\n";
582 return 1;
583 }
584 if (defined(my $new_name = $deprecated_lxc_conf_keys->{$key})) {
585 warn "vm $vmid - $key is deprecated and was renamed to $new_name\n";
586 return 1;
587 }
588 my $validity = $valid_lxc_conf_keys->{$key};
589 return $validity if defined($validity);
590 return 1 if $key =~ /^lxc\.cgroup\./ # allow all cgroup values
591 || $key =~ /^lxc\.prlimit\./ # allow all prlimits
592 || $key =~ /^lxc\.net\./; # allow custom network definitions
593 return 0;
594}
595
5e5915c5 596our $netconf_desc = {
1b4cf758
FG
597 type => {
598 type => 'string',
599 optional => 1,
600 description => "Network interface type.",
601 enum => [qw(veth)],
602 },
603 name => {
604 type => 'string',
a069f163
DM
605 format_description => 'string',
606 description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
1b4cf758
FG
607 pattern => '[-_.\w\d]+',
608 },
609 bridge => {
610 type => 'string',
a069f163 611 format_description => 'bridge',
1b4cf758
FG
612 description => 'Bridge to attach the network device to.',
613 pattern => '[-_.\w\d]+',
614 optional => 1,
615 },
603536e0 616 hwaddr => get_standard_option('mac-addr', {
e6f20294 617 description => 'The interface MAC address. This is dynamically allocated by default, but you can set that statically if needed, for example to always have the same link-local IPv6 address. (lxc.network.hwaddr)',
603536e0 618 }),
1b4cf758
FG
619 mtu => {
620 type => 'integer',
1b4cf758
FG
621 description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
622 minimum => 64, # minimum ethernet frame is 64 bytes
623 optional => 1,
624 },
625 ip => {
626 type => 'string',
627 format => 'pve-ipv4-config',
718a67d6 628 format_description => '(IPv4/CIDR|dhcp|manual)',
1b4cf758
FG
629 description => 'IPv4 address in CIDR format.',
630 optional => 1,
631 },
632 gw => {
633 type => 'string',
634 format => 'ipv4',
635 format_description => 'GatewayIPv4',
636 description => 'Default gateway for IPv4 traffic.',
637 optional => 1,
638 },
639 ip6 => {
640 type => 'string',
641 format => 'pve-ipv6-config',
6ea7095c 642 format_description => '(IPv6/CIDR|auto|dhcp|manual)',
1b4cf758
FG
643 description => 'IPv6 address in CIDR format.',
644 optional => 1,
645 },
646 gw6 => {
647 type => 'string',
648 format => 'ipv6',
649 format_description => 'GatewayIPv6',
650 description => 'Default gateway for IPv6 traffic.',
651 optional => 1,
652 },
653 firewall => {
654 type => 'boolean',
1b4cf758
FG
655 description => "Controls whether this interface's firewall rules should be used.",
656 optional => 1,
657 },
658 tag => {
659 type => 'integer',
6b202dd5
DM
660 minimum => 1,
661 maximum => 4094,
1b4cf758
FG
662 description => "VLAN tag for this interface.",
663 optional => 1,
664 },
665 trunks => {
666 type => 'string',
667 pattern => qr/\d+(?:;\d+)*/,
668 format_description => 'vlanid[;vlanid...]',
669 description => "VLAN ids to pass through the interface",
670 optional => 1,
671 },
380962c7
WB
672 rate => {
673 type => 'number',
674 format_description => 'mbps',
675 description => "Apply rate limiting to the interface",
676 optional => 1,
677 },
1b4cf758
FG
678};
679PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
680
681my $MAX_LXC_NETWORKS = 10;
682for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
683 $confdesc->{"net$i"} = {
684 optional => 1,
685 type => 'string', format => $netconf_desc,
686 description => "Specifies network interfaces for the container.",
687 };
688}
689
690PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
691sub verify_lxc_mp_string {
692 my ($mp, $noerr) = @_;
693
694 # do not allow:
695 # /./ or /../
696 # /. or /.. at the end
697 # ../ at the beginning
698
699 if($mp =~ m@/\.\.?/@ ||
700 $mp =~ m@/\.\.?$@ ||
701 $mp =~ m@^\.\./@) {
702 return undef if $noerr;
703 die "$mp contains illegal character sequences\n";
704 }
705 return $mp;
706}
707
708my $mp_desc = {
709 %$rootfs_desc,
84820d40
DM
710 backup => {
711 type => 'boolean',
235dbdf3
FG
712 description => 'Whether to include the mount point in backups.',
713 verbose_description => 'Whether to include the mount point in backups '.
714 '(only used for volume mount points).',
84820d40
DM
715 optional => 1,
716 },
1b4cf758
FG
717 mp => {
718 type => 'string',
719 format => 'pve-lxc-mp-string',
720 format_description => 'Path',
235dbdf3 721 description => 'Path to the mount point as seen from inside the container '.
52b6f941 722 '(must not contain symlinks).',
235dbdf3 723 verbose_description => "Path to the mount point as seen from inside the container.\n\n".
52b6f941 724 "NOTE: Must not contain any symlinks for security reasons."
1b4cf758
FG
725 },
726};
727PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
728
729my $unuseddesc = {
730 optional => 1,
731 type => 'string', format => 'pve-volume-id',
2928e616 732 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
1b4cf758
FG
733};
734
735for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
736 $confdesc->{"mp$i"} = {
737 optional => 1,
738 type => 'string', format => $mp_desc,
2928e616 739 description => "Use volume as container mount point.",
1b4cf758
FG
740 optional => 1,
741 };
742}
743
744for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
745 $confdesc->{"unused$i"} = $unuseddesc;
746}
747
748sub parse_pct_config {
749 my ($filename, $raw) = @_;
750
751 return undef if !defined($raw);
752
753 my $res = {
754 digest => Digest::SHA::sha1_hex($raw),
755 snapshots => {},
7547dc63 756 pending => {},
1b4cf758
FG
757 };
758
759 $filename =~ m|/lxc/(\d+).conf$|
760 || die "got strange filename '$filename'";
761
762 my $vmid = $1;
763
764 my $conf = $res;
765 my $descr = '';
766 my $section = '';
767
768 my @lines = split(/\n/, $raw);
769 foreach my $line (@lines) {
770 next if $line =~ m/^\s*$/;
771
7547dc63
OB
772 if ($line =~ m/^\[pve:pending\]\s*$/i) {
773 $section = 'pending';
774 $conf->{description} = $descr if $descr;
775 $descr = '';
776 $conf = $res->{$section} = {};
777 next;
778 } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
1b4cf758
FG
779 $section = $1;
780 $conf->{description} = $descr if $descr;
781 $descr = '';
782 $conf = $res->{snapshots}->{$section} = {};
783 next;
784 }
785
786 if ($line =~ m/^\#(.*)\s*$/) {
787 $descr .= PVE::Tools::decode_text($1) . "\n";
788 next;
789 }
790
791 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
792 my $key = $1;
793 my $value = $3;
108c6cab
WB
794 my $validity = is_valid_lxc_conf_key($vmid, $key);
795 if ($validity eq 1) {
1b4cf758
FG
796 push @{$conf->{lxc}}, [$key, $value];
797 } elsif (my $errmsg = $validity) {
798 warn "vm $vmid - $key: $errmsg\n";
799 } else {
800 warn "vm $vmid - unable to parse config: $line\n";
801 }
802 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
803 $descr .= PVE::Tools::decode_text($2);
804 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
805 $conf->{snapstate} = $1;
7547dc63
OB
806 } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
807 my $value = $1;
808 if ($section eq 'pending') {
809 $conf->{delete} = $value;
810 } else {
811 warn "vm $vmid - property 'delete' is only allowed in [pve:pending]\n";
812 }
1b4cf758
FG
813 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
814 my $key = $1;
815 my $value = $2;
816 eval { $value = PVE::LXC::Config->check_type($key, $value); };
817 warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
818 $conf->{$key} = $value;
819 } else {
820 warn "vm $vmid - unable to parse config: $line\n";
821 }
822 }
823
824 $conf->{description} = $descr if $descr;
825
826 delete $res->{snapstate}; # just to be sure
827
828 return $res;
829}
830
831sub write_pct_config {
832 my ($filename, $conf) = @_;
833
834 delete $conf->{snapstate}; # just to be sure
835
836 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
837 my $used_volids = {};
838 foreach my $vid (@$volidlist) {
839 $used_volids->{$vid} = 1;
840 }
841
842 # remove 'unusedX' settings if the volume is still used
843 foreach my $key (keys %$conf) {
844 my $value = $conf->{$key};
845 if ($key =~ m/^unused/ && $used_volids->{$value}) {
846 delete $conf->{$key};
847 }
848 }
849
850 my $generate_raw_config = sub {
851 my ($conf) = @_;
852
853 my $raw = '';
854
855 # add description as comment to top of file
856 my $descr = $conf->{description} || '';
857 foreach my $cl (split(/\n/, $descr)) {
7547dc63 858 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
1b4cf758
FG
859 }
860
861 foreach my $key (sort keys %$conf) {
862 next if $key eq 'digest' || $key eq 'description' ||
863 $key eq 'pending' || $key eq 'snapshots' ||
864 $key eq 'snapname' || $key eq 'lxc';
865 my $value = $conf->{$key};
866 die "detected invalid newline inside property '$key'\n"
867 if $value =~ m/\n/;
868 $raw .= "$key: $value\n";
869 }
870
871 if (my $lxcconf = $conf->{lxc}) {
872 foreach my $entry (@$lxcconf) {
873 my ($k, $v) = @$entry;
874 $raw .= "$k: $v\n";
875 }
876 }
877
878 return $raw;
879 };
880
881 my $raw = &$generate_raw_config($conf);
882
7547dc63
OB
883 if (scalar(keys %{$conf->{pending}})){
884 $raw .= "\n[pve:pending]\n";
885 $raw .= &$generate_raw_config($conf->{pending});
886 }
887
1b4cf758
FG
888 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
889 $raw .= "\n[$snapname]\n";
890 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
891 }
892
893 return $raw;
894}
895
896sub update_pct_config {
6517e001 897 my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
1b4cf758 898
6517e001 899 my $storage_cfg = PVE::Storage::config();
1b4cf758 900
6517e001
OB
901 foreach my $opt (@$revert) {
902 delete $conf->{pending}->{$opt};
903 $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
1b4cf758
FG
904 }
905
6517e001
OB
906 # write updates to pending section
907 my $modified = {}; # record modified options
1b4cf758 908
6517e001
OB
909 foreach my $opt (@$delete) {
910 if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
911 warn "cannot delete '$opt' - not set in current configuration!\n";
912 next;
1b4cf758 913 }
6517e001
OB
914 $modified->{$opt} = 1;
915 if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
916 die "unable to delete required option '$opt'\n";
917 } elsif ($opt =~ m/^unused(\d+)$/) {
918 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
919 } elsif ($opt =~ m/^mp(\d+)$/) {
920 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
921 } elsif ($opt eq 'unprivileged') {
922 die "unable to delete read-only option: '$opt'\n";
1b4cf758 923 }
6517e001 924 $class->add_to_pending_delete($conf, $opt);
1b4cf758
FG
925 }
926
1b3213ae
FG
927 my $check_content_type = sub {
928 my ($mp) = @_;
929 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
6517e001 930 my $storage_config = PVE::Storage::storage_config($storage_cfg, $sid);
1b3213ae
FG
931 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
932 if !$storage_config->{content}->{rootdir};
933 };
1b4cf758 934
c10951f7 935 foreach my $opt (sort keys %$param) { # add/change
6517e001 936 $modified->{$opt} = 1;
1b4cf758 937 my $value = $param->{$opt};
6517e001
OB
938 if ($opt =~ m/^mp(\d+)$/ || $opt eq 'rootfs') {
939 $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
940 my $mp = $opt eq 'rootfs' ? $class->parse_ct_rootfs($value) : $class->parse_ct_mountpoint($value);
3927ae96 941 $check_content_type->($mp) if ($mp->{type} eq 'volume');
6517e001
OB
942 } elsif ($opt eq 'hookscript') {
943 PVE::GuestHelpers::check_hookscript($value);
1b4cf758 944 } elsif ($opt eq 'nameserver') {
6517e001 945 $value = PVE::LXC::verify_nameserver_list($value);
1b4cf758 946 } elsif ($opt eq 'searchdomain') {
6517e001 947 $value = PVE::LXC::verify_searchdomain_list($value);
1b4cf758
FG
948 } elsif ($opt eq 'unprivileged') {
949 die "unable to modify read-only option: '$opt'\n";
1b4cf758 950 }
6517e001
OB
951 $conf->{pending}->{$opt} = $value;
952 $class->remove_from_pending_delete($conf, $opt);
f8aa3d35
WL
953 }
954
6517e001 955 my $changes = $class->cleanup_pending($conf);
1b4cf758 956
6517e001
OB
957 my $errors = {};
958 if ($running) {
959 $class->vmconfig_hotplug_pending($vmid, $conf, $storage_cfg, $modified, $errors);
960 } else {
961 $class->vmconfig_apply_pending($vmid, $conf, $storage_cfg, $modified, $errors);
1b4cf758
FG
962 }
963
6517e001 964 return $errors;
1b4cf758
FG
965}
966
967sub check_type {
968 my ($class, $key, $value) = @_;
969
970 die "unknown setting '$key'\n" if !$confdesc->{$key};
971
972 my $type = $confdesc->{$key}->{type};
973
974 if (!defined($value)) {
975 die "got undefined value\n";
976 }
977
978 if ($value =~ m/[\n\r]/) {
979 die "property contains a line feed\n";
980 }
981
982 if ($type eq 'boolean') {
983 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
984 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
985 die "type check ('boolean') failed - got '$value'\n";
986 } elsif ($type eq 'integer') {
987 return int($1) if $value =~ m/^(\d+)$/;
988 die "type check ('integer') failed - got '$value'\n";
989 } elsif ($type eq 'number') {
990 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
991 die "type check ('number') failed - got '$value'\n";
992 } elsif ($type eq 'string') {
993 if (my $fmt = $confdesc->{$key}->{format}) {
994 PVE::JSONSchema::check_format($fmt, $value);
995 return $value;
996 }
997 return $value;
998 } else {
999 die "internal error"
1000 }
1001}
1002
1003
1004# add JSON properties for create and set function
1005sub json_config_properties {
1006 my ($class, $prop) = @_;
1007
1008 foreach my $opt (keys %$confdesc) {
1009 next if $opt eq 'parent' || $opt eq 'snaptime';
1010 next if $prop->{$opt};
1011 $prop->{$opt} = $confdesc->{$opt};
1012 }
1013
1014 return $prop;
1015}
1016
1017sub __parse_ct_mountpoint_full {
1018 my ($class, $desc, $data, $noerr) = @_;
1019
1020 $data //= '';
1021
1022 my $res;
1023 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
1024 if ($@) {
1025 return undef if $noerr;
1026 die $@;
1027 }
1028
1029 if (defined(my $size = $res->{size})) {
1030 $size = PVE::JSONSchema::parse_size($size);
1031 if (!defined($size)) {
1032 return undef if $noerr;
1033 die "invalid size: $size\n";
1034 }
1035 $res->{size} = $size;
1036 }
1037
1038 $res->{type} = $class->classify_mountpoint($res->{volume});
1039
1040 return $res;
1041};
1042
1043sub parse_ct_rootfs {
1044 my ($class, $data, $noerr) = @_;
1045
1046 my $res = $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
1047
1048 $res->{mp} = '/' if defined($res);
1049
1050 return $res;
1051}
1052
1053sub parse_ct_mountpoint {
1054 my ($class, $data, $noerr) = @_;
1055
1056 return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
1057}
1058
1059sub print_ct_mountpoint {
1060 my ($class, $info, $nomp) = @_;
1061 my $skip = [ 'type' ];
1062 push @$skip, 'mp' if $nomp;
1063 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1064}
1065
1066sub print_lxc_network {
1067 my ($class, $net) = @_;
1068 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1069}
1070
1071sub parse_lxc_network {
1072 my ($class, $data) = @_;
1073
1074 my $res = {};
1075
1076 return $res if !$data;
1077
1078 $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1079
1080 $res->{type} = 'veth';
2f19133b
WB
1081 if (!$res->{hwaddr}) {
1082 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1083 $res->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1084 }
1b4cf758
FG
1085
1086 return $res;
1087}
1088
5a63f1c5
WB
1089sub parse_features {
1090 my ($class, $data) = @_;
1091 return {} if !$data;
1092 return PVE::JSONSchema::parse_property_string($features_desc, $data);
1093}
1094
1b4cf758
FG
1095sub option_exists {
1096 my ($class, $name) = @_;
1097
1098 return defined($confdesc->{$name});
1099}
1100# END JSON config code
1101
32e15a2b
OB
1102my $LXC_FASTPLUG_OPTIONS= {
1103 'description' => 1,
1104 'onboot' => 1,
1105 'startup' => 1,
1106 'protection' => 1,
1107 'hostname' => 1,
1108 'hookscript' => 1,
1109 'cores' => 1,
1110 'tags' => 1,
db15c375 1111 'lock' => 1,
32e15a2b
OB
1112};
1113
1114sub vmconfig_hotplug_pending {
1115 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1116
1117 my $pid = PVE::LXC::find_lxc_pid($vmid);
1118 my $rootdir = "/proc/$pid/root";
1119
1120 my $add_hotplug_error = sub {
1121 my ($opt, $msg) = @_;
1122 $errors->{$opt} = "unable to hotplug $opt: $msg";
1123 };
1124
1125 my $changes;
c10951f7 1126 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
32e15a2b
OB
1127 next if $selection && !$selection->{$opt};
1128 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1129 $conf->{$opt} = delete $conf->{pending}->{$opt};
1130 $changes = 1;
1131 }
1132 }
1133
1134 if ($changes) {
1135 $class->write_config($vmid, $conf);
1136 }
1137
1138 # There's no separate swap size to configure, there's memory and "total"
1139 # memory (iow. memory+swap). This means we have to change them together.
1140 my $hotplug_memory_done;
1141 my $hotplug_memory = sub {
1142 my ($wanted_memory, $wanted_swap) = @_;
1143 my $old_memory = ($conf->{memory} || $confdesc->{memory}->{default});
1144 my $old_swap = ($conf->{swap} || $confdesc->{swap}->{default});
1145
1146 $wanted_memory //= $old_memory;
1147 $wanted_swap //= $old_swap;
1148
1149 my $total = $wanted_memory + $wanted_swap;
1150 my $old_total = $old_memory + $old_swap;
1151
1152 if ($total > $old_total) {
1153 PVE::LXC::write_cgroup_value("memory", $vmid,
1154 "memory.memsw.limit_in_bytes",
1155 int($total*1024*1024));
1156 PVE::LXC::write_cgroup_value("memory", $vmid,
1157 "memory.limit_in_bytes",
1158 int($wanted_memory*1024*1024));
1159 } else {
1160 PVE::LXC::write_cgroup_value("memory", $vmid,
1161 "memory.limit_in_bytes",
1162 int($wanted_memory*1024*1024));
1163 PVE::LXC::write_cgroup_value("memory", $vmid,
1164 "memory.memsw.limit_in_bytes",
1165 int($total*1024*1024));
1166 }
1167 $hotplug_memory_done = 1;
1168 };
1169
1170 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1171 # FIXME: $force deletion is not implemented for CTs
8ec10817 1172 foreach my $opt (sort keys %$pending_delete_hash) {
32e15a2b
OB
1173 next if $selection && !$selection->{$opt};
1174 eval {
1175 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1176 # pass
1177 } elsif ($opt =~ m/^unused(\d+)$/) {
1178 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1179 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1180 } elsif ($opt eq 'swap') {
1181 $hotplug_memory->(undef, 0);
1182 } elsif ($opt eq 'cpulimit') {
1183 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", -1);
1184 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
1185 } elsif ($opt eq 'cpuunits') {
1186 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shared", $confdesc->{cpuunits}->{default});
1187 } elsif ($opt =~ m/^net(\d)$/) {
1188 my $netid = $1;
1189 PVE::Network::veth_delete("veth${vmid}i$netid");
1190 } else {
1191 die "skip\n"; # skip non-hotpluggable opts
1192 }
1193 };
1194 if (my $err = $@) {
1195 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1196 } else {
1197 delete $conf->{$opt};
1198 $class->remove_from_pending_delete($conf, $opt);
1199 }
1200 }
1201
c10951f7 1202 foreach my $opt (sort keys %{$conf->{pending}}) {
32e15a2b
OB
1203 next if $opt eq 'delete'; # just to be sure
1204 next if $selection && !$selection->{$opt};
1205 my $value = $conf->{pending}->{$opt};
1206 eval {
1207 if ($opt eq 'cpulimit') {
1208 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", 100000);
1209 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
1210 } elsif ($opt eq 'cpuunits') {
1211 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
1212 } elsif ($opt =~ m/^net(\d+)$/) {
1213 my $netid = $1;
1214 my $net = $class->parse_lxc_network($value);
7eff309b 1215 $value = $class->print_lxc_network($net);
32e15a2b
OB
1216 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
1217 } elsif ($opt eq 'memory' || $opt eq 'swap') {
1218 if (!$hotplug_memory_done) { # don't call twice if both opts are passed
1219 $hotplug_memory->($conf->{pending}->{memory}, $conf->{pending}->{swap});
1220 }
1221 } else {
1222 die "skip\n"; # skip non-hotpluggable
1223 }
1224 };
1225 if (my $err = $@) {
1226 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1227 } else {
1228 $conf->{$opt} = $value;
1229 delete $conf->{pending}->{$opt};
1230 }
1231 }
1232
1233 $class->write_config($vmid, $conf);
1234}
1235
1236sub vmconfig_apply_pending {
1237 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1238
1239 my $add_apply_error = sub {
1240 my ($opt, $msg) = @_;
1241 my $err_msg = "unable to apply pending change $opt : $msg";
1242 $errors->{$opt} = $err_msg;
1243 warn $err_msg;
1244 };
1245
32e15a2b
OB
1246 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1247 # FIXME: $force deletion is not implemented for CTs
8ec10817 1248 foreach my $opt (sort keys %$pending_delete_hash) {
32e15a2b
OB
1249 next if $selection && !$selection->{$opt};
1250 $class->cleanup_pending($conf);
1251 eval {
1252 if ($opt =~ m/^mp(\d+)$/) {
1253 my $mp = $class->parse_ct_mountpoint($conf->{$opt});
1254 if ($mp->{type} eq 'volume') {
1255 $class->add_unused_volume($conf, $mp->{volume})
1256 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1257 }
1258 } elsif ($opt =~ m/^unused(\d+)$/) {
1259 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1260 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1261 }
1262 };
1263 if (my $err = $@) {
1264 $add_apply_error->($opt, $err);
1265 } else {
1266 delete $conf->{$opt};
1267 $class->remove_from_pending_delete($conf, $opt);
1268 }
1269 }
1270
c10951f7 1271 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
32e15a2b
OB
1272 next if $opt eq 'delete'; # just to be sure
1273 next if $selection && !$selection->{$opt};
1274 eval {
1275 if ($opt =~ m/^mp(\d+)$/) {
869081a2 1276 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 0);
7eff309b
OB
1277 } elsif ($opt =~ m/^net(\d+)$/) {
1278 my $netid = $1;
1279 my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
1280 $conf->{pending}->{$opt} = $class->print_lxc_network($net);
32e15a2b
OB
1281 }
1282 };
1283 if (my $err = $@) {
1284 $add_apply_error->($opt, $err);
1285 } else {
1286 $class->cleanup_pending($conf);
1287 $conf->{$opt} = delete $conf->{pending}->{$opt};
1288 }
1289 }
1290
1291 $class->write_config($vmid, $conf);
1292}
1293
869081a2
WB
1294my $rescan_volume = sub {
1295 my ($storecfg, $mp) = @_;
1296 eval {
1297 $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5)
1298 if !defined($mp->{size});
1299 };
1300 warn "Could not rescan volume size - $@\n" if $@;
1301};
1302
1303sub apply_pending_mountpoint {
1304 my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
1305
1306 my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
1307 my $old = $conf->{$opt};
1308 if ($mp->{type} eq 'volume') {
1309 if ($mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
1310 my $vollist = PVE::LXC::create_disks(
1311 $storecfg,
1312 $vmid,
1313 { $opt => $conf->{pending}->{$opt} },
1314 $conf,
1315 1,
1316 );
1317 } else {
1318 $rescan_volume->($storecfg, $mp);
1319 $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
1320 }
1321 }
1322
1323 if (defined($old)) {
1324 my $mp = $class->parse_ct_mountpoint($old);
1325 if ($mp->{type} eq 'volume') {
1326 $class->add_unused_volume($conf, $mp->{volume})
1327 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1328 }
1329 }
1330}
1331
d250604f
FG
1332sub classify_mountpoint {
1333 my ($class, $vol) = @_;
1334 if ($vol =~ m!^/!) {
1335 return 'device' if $vol =~ m!^/dev/!;
1336 return 'bind';
1337 }
1338 return 'volume';
1339}
1340
7b4237c5 1341my $__is_volume_in_use = sub {
72e6bc20 1342 my ($class, $config, $volid) = @_;
d250604f
FG
1343 my $used = 0;
1344
1345 $class->foreach_mountpoint($config, sub {
1346 my ($ms, $mountpoint) = @_;
1347 return if $used;
1348 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1349 });
1350
72e6bc20
WB
1351 return $used;
1352};
1353
1354sub is_volume_in_use_by_snapshots {
1355 my ($class, $config, $volid) = @_;
1356
1357 if (my $snapshots = $config->{snapshots}) {
d250604f 1358 foreach my $snap (keys %$snapshots) {
7b4237c5 1359 return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
d250604f
FG
1360 }
1361 }
1362
72e6bc20 1363 return 0;
7b4237c5 1364}
72e6bc20
WB
1365
1366sub is_volume_in_use {
d063af00 1367 my ($class, $config, $volid, $include_snapshots, $include_pending) = @_;
7b4237c5 1368 return 1 if $__is_volume_in_use->($class, $config, $volid);
72e6bc20 1369 return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
d063af00 1370 return 1 if $include_pending && $__is_volume_in_use->($class, $config->{pending}, $volid);
72e6bc20 1371 return 0;
d250604f
FG
1372}
1373
1374sub has_dev_console {
1375 my ($class, $conf) = @_;
1376
1377 return !(defined($conf->{console}) && !$conf->{console});
1378}
1379
be7942f0
DM
1380sub has_lxc_entry {
1381 my ($class, $conf, $keyname) = @_;
1382
1383 if (my $lxcconf = $conf->{lxc}) {
1384 foreach my $entry (@$lxcconf) {
1385 my ($key, undef) = @$entry;
1386 return 1 if $key eq $keyname;
1387 }
1388 }
1389
1390 return 0;
1391}
1392
1b4cf758
FG
1393sub get_tty_count {
1394 my ($class, $conf) = @_;
1395
1396 return $conf->{tty} // $confdesc->{tty}->{default};
1397}
1398
1399sub get_cmode {
1400 my ($class, $conf) = @_;
1401
1402 return $conf->{cmode} // $confdesc->{cmode}->{default};
1403}
1404
d250604f
FG
1405sub mountpoint_names {
1406 my ($class, $reverse) = @_;
1407
1408 my @names = ('rootfs');
1409
1410 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1411 push @names, "mp$i";
1412 }
1413
1414 return $reverse ? reverse @names : @names;
1415}
1416
1417sub foreach_mountpoint_full {
8915b450 1418 my ($class, $conf, $reverse, $func, @param) = @_;
d250604f 1419
717f70b7
FG
1420 my $mps = [ grep { defined($conf->{$_}) } $class->mountpoint_names($reverse) ];
1421 foreach my $key (@$mps) {
d250604f 1422 my $value = $conf->{$key};
1b4cf758 1423 my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
d250604f
FG
1424 next if !defined($mountpoint);
1425
8915b450 1426 &$func($key, $mountpoint, @param);
d250604f
FG
1427 }
1428}
1429
1430sub foreach_mountpoint {
8915b450 1431 my ($class, $conf, $func, @param) = @_;
d250604f 1432
8915b450 1433 $class->foreach_mountpoint_full($conf, 0, $func, @param);
d250604f
FG
1434}
1435
1436sub foreach_mountpoint_reverse {
8915b450 1437 my ($class, $conf, $func, @param) = @_;
d250604f 1438
8915b450 1439 $class->foreach_mountpoint_full($conf, 1, $func, @param);
d250604f
FG
1440}
1441
1442sub get_vm_volumes {
1443 my ($class, $conf, $excludes) = @_;
1444
1445 my $vollist = [];
1446
1447 $class->foreach_mountpoint($conf, sub {
1448 my ($ms, $mountpoint) = @_;
1449
1450 return if $excludes && $ms eq $excludes;
1451
1452 my $volid = $mountpoint->{volume};
1453 return if !$volid || $mountpoint->{type} ne 'volume';
1454
1455 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1456 return if !$sid;
1457
1458 push @$vollist, $volid;
1459 });
1460
1461 return $vollist;
1462}
1463
f78c87a8 1464sub get_replicatable_volumes {
70996986 1465 my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
f78c87a8
DM
1466
1467 my $volhash = {};
1468
1469 my $test_volid = sub {
1470 my ($volid, $mountpoint) = @_;
1471
1472 return if !$volid;
1473
5cf90b0c 1474 my $mptype = $mountpoint->{type};
896cd762 1475 my $replicate = $mountpoint->{replicate} // 1;
d2a046b7
DC
1476
1477 if ($mptype ne 'volume') {
1478 # skip bindmounts if replicate = 0 even for cleanup,
1479 # since bind mounts could not have been replicated ever
1480 return if !$replicate;
1481 die "unable to replicate mountpoint type '$mptype'\n";
1482 }
5cf90b0c
DM
1483
1484 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
1485 return if !$storeid;
1486
af21e699 1487 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
5cf90b0c
DM
1488 return if $scfg->{shared};
1489
1490 my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
1491 return if !$owner || ($owner != $vmid);
1492
1493 die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
1494
d2a046b7 1495 return if !$cleanup && !$replicate;
f78c87a8
DM
1496
1497 if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
e65dce6b 1498 return if $cleanup || $noerr;
f78c87a8
DM
1499 die "missing replicate feature on volume '$volid'\n";
1500 }
1501
1502 $volhash->{$volid} = 1;
1503 };
1504
1505 $class->foreach_mountpoint($conf, sub {
1506 my ($ms, $mountpoint) = @_;
1507 $test_volid->($mountpoint->{volume}, $mountpoint);
1508 });
1509
1510 foreach my $snapname (keys %{$conf->{snapshots}}) {
1511 my $snap = $conf->{snapshots}->{$snapname};
1512 $class->foreach_mountpoint($snap, sub {
1513 my ($ms, $mountpoint) = @_;
1514 $test_volid->($mountpoint->{volume}, $mountpoint);
1515 });
1516 }
1517
b03664ae
DM
1518 # add 'unusedX' volumes to volhash
1519 foreach my $key (keys %$conf) {
1520 if ($key =~ m/^unused/) {
1521 $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
1522 }
1523 }
1524
f78c87a8
DM
1525 return $volhash;
1526}
1527
15281;