]> git.proxmox.com Git - pve-container.git/blame - src/PVE/LXC/Config.pm
bump version to 3.0-22
[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 },
2df08734
WB
326 mknod => {
327 optional => 1,
328 type => 'boolean',
329 default => 0,
330 description => "Allow unprivileged containers to use mknod() to add certain device nodes."
331 ." This requires a kernel with seccomp trap to user space support (5.3 or newer)."
332 ." This is experimental.",
333 },
5a63f1c5
WB
334};
335
1b4cf758
FG
336my $confdesc = {
337 lock => {
338 optional => 1,
339 type => 'string',
340 description => "Lock/unlock the VM.",
7fc1d9eb 341 enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
1b4cf758
FG
342 },
343 onboot => {
344 optional => 1,
345 type => 'boolean',
346 description => "Specifies whether a VM will be started during system bootup.",
347 default => 0,
348 },
349 startup => get_standard_option('pve-startup-order'),
350 template => {
351 optional => 1,
352 type => 'boolean',
353 description => "Enable/disable Template.",
354 default => 0,
355 },
356 arch => {
357 optional => 1,
358 type => 'string',
e1d54a38 359 enum => ['amd64', 'i386', 'arm64', 'armhf'],
1b4cf758
FG
360 description => "OS architecture type.",
361 default => 'amd64',
362 },
363 ostype => {
364 optional => 1,
365 type => 'string',
ed027b58 366 enum => [qw(debian ubuntu centos fedora opensuse archlinux alpine gentoo unmanaged)],
1b4cf758
FG
367 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.",
368 },
369 console => {
370 optional => 1,
371 type => 'boolean',
372 description => "Attach a console device (/dev/console) to the container.",
373 default => 1,
374 },
375 tty => {
376 optional => 1,
377 type => 'integer',
378 description => "Specify the number of tty available to the container",
379 minimum => 0,
380 maximum => 6,
381 default => 2,
382 },
f2357408
DM
383 cores => {
384 optional => 1,
385 type => 'integer',
386 description => "The number of cores assigned to the container. A container can use all available cores by default.",
387 minimum => 1,
388 maximum => 128,
389 },
1b4cf758
FG
390 cpulimit => {
391 optional => 1,
392 type => 'number',
064529c3 393 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
394 minimum => 0,
395 maximum => 128,
396 default => 0,
397 },
398 cpuunits => {
399 optional => 1,
400 type => 'integer',
401 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.",
402 minimum => 0,
403 maximum => 500000,
404 default => 1024,
405 },
406 memory => {
407 optional => 1,
408 type => 'integer',
409 description => "Amount of RAM for the VM in MB.",
410 minimum => 16,
411 default => 512,
412 },
413 swap => {
414 optional => 1,
415 type => 'integer',
416 description => "Amount of SWAP for the VM in MB.",
417 minimum => 0,
418 default => 512,
419 },
420 hostname => {
421 optional => 1,
422 description => "Set a host name for the container.",
423 type => 'string', format => 'dns-name',
424 maxLength => 255,
425 },
426 description => {
427 optional => 1,
428 type => 'string',
a069f163 429 description => "Container description. Only used on the configuration web interface.",
1b4cf758
FG
430 },
431 searchdomain => {
432 optional => 1,
433 type => 'string', format => 'dns-name-list',
434 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
435 },
436 nameserver => {
437 optional => 1,
438 type => 'string', format => 'address-list',
439 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.",
440 },
441 rootfs => get_standard_option('pve-ct-rootfs'),
442 parent => {
443 optional => 1,
444 type => 'string', format => 'pve-configid',
445 maxLength => 40,
446 description => "Parent snapshot name. This is used internally, and should not be modified.",
447 },
448 snaptime => {
449 optional => 1,
450 description => "Timestamp for snapshots.",
451 type => 'integer',
452 minimum => 0,
453 },
454 cmode => {
455 optional => 1,
456 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).",
457 type => 'string',
458 enum => ['shell', 'console', 'tty'],
459 default => 'tty',
460 },
461 protection => {
462 optional => 1,
463 type => 'boolean',
464 description => "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.",
465 default => 0,
466 },
467 unprivileged => {
468 optional => 1,
469 type => 'boolean',
470 description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
471 default => 0,
472 },
5a63f1c5
WB
473 features => {
474 optional => 1,
475 type => 'string',
476 format => $features_desc,
477 description => "Allow containers access to advanced features.",
478 },
1a416433
DC
479 hookscript => {
480 optional => 1,
481 type => 'string',
482 format => 'pve-volume-id',
483 description => 'Script that will be exectued during various steps in the containers lifetime.',
484 },
733e52ec
DC
485 tags => {
486 type => 'string', format => 'pve-tag-list',
487 description => 'Tags of the Container. This is only meta information.',
488 optional => 1,
489 },
1b4cf758
FG
490};
491
492my $valid_lxc_conf_keys = {
108c6cab
WB
493 'lxc.apparmor.profile' => 1,
494 'lxc.apparmor.allow_incomplete' => 1,
d494e03c
WB
495 'lxc.apparmor.allow_nesting' => 1,
496 'lxc.apparmor.raw' => 1,
108c6cab 497 'lxc.selinux.context' => 1,
1b4cf758
FG
498 'lxc.include' => 1,
499 'lxc.arch' => 1,
108c6cab
WB
500 'lxc.uts.name' => 1,
501 'lxc.signal.halt' => 1,
502 'lxc.signal.reboot' => 1,
503 'lxc.signal.stop' => 1,
504 'lxc.init.cmd' => 1,
505 'lxc.pty.max' => 1,
1b4cf758 506 'lxc.console.logfile' => 1,
108c6cab
WB
507 'lxc.console.path' => 1,
508 'lxc.tty.max' => 1,
509 'lxc.devtty.dir' => 1,
1b4cf758
FG
510 'lxc.hook.autodev' => 1,
511 'lxc.autodev' => 1,
512 'lxc.kmsg' => 1,
108c6cab 513 'lxc.mount.fstab' => 1,
1b4cf758
FG
514 'lxc.mount.entry' => 1,
515 'lxc.mount.auto' => 1,
108c6cab 516 'lxc.rootfs.path' => 'lxc.rootfs.path is auto generated from rootfs',
1b4cf758
FG
517 'lxc.rootfs.mount' => 1,
518 'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
235dbdf3 519 ', please use mount point options in the "rootfs" key',
1b4cf758 520 # lxc.cgroup.*
108c6cab 521 # lxc.prlimit.*
6eb23d1e 522 # lxc.net.*
1b4cf758
FG
523 'lxc.cap.drop' => 1,
524 'lxc.cap.keep' => 1,
108c6cab 525 'lxc.seccomp.profile' => 1,
832b4a02
WB
526 'lxc.seccomp.notify.proxy' => 1,
527 'lxc.seccomp.notify.cookie' => 1,
108c6cab 528 'lxc.idmap' => 1,
1b4cf758
FG
529 'lxc.hook.pre-start' => 1,
530 'lxc.hook.pre-mount' => 1,
531 'lxc.hook.mount' => 1,
532 'lxc.hook.start' => 1,
533 'lxc.hook.stop' => 1,
534 'lxc.hook.post-stop' => 1,
535 'lxc.hook.clone' => 1,
536 'lxc.hook.destroy' => 1,
f71db91b 537 'lxc.hook.version' => 1,
108c6cab
WB
538 'lxc.log.level' => 1,
539 'lxc.log.file' => 1,
1b4cf758
FG
540 'lxc.start.auto' => 1,
541 'lxc.start.delay' => 1,
542 'lxc.start.order' => 1,
543 'lxc.group' => 1,
544 'lxc.environment' => 1,
d4a135f7
WB
545
546 # All these are namespaced via CLONE_NEWIPC (see namespaces(7)).
547 'lxc.sysctl.fs.mqueue' => 1,
548 'lxc.sysctl.kernel.msgmax' => 1,
549 'lxc.sysctl.kernel.msgmnb' => 1,
550 'lxc.sysctl.kernel.msgmni' => 1,
551 'lxc.sysctl.kernel.sem' => 1,
552 'lxc.sysctl.kernel.shmall' => 1,
553 'lxc.sysctl.kernel.shmmax' => 1,
554 'lxc.sysctl.kernel.shmmni' => 1,
555 'lxc.sysctl.kernel.shm_rmid_forced' => 1,
1b4cf758
FG
556};
557
108c6cab
WB
558my $deprecated_lxc_conf_keys = {
559 # Deprecated (removed with lxc 3.0):
560 'lxc.aa_profile' => 'lxc.apparmor.profile',
561 'lxc.aa_allow_incomplete' => 'lxc.apparmor.allow_incomplete',
562 'lxc.console' => 'lxc.console.path',
563 'lxc.devttydir' => 'lxc.tty.dir',
564 'lxc.haltsignal' => 'lxc.signal.halt',
565 'lxc.rebootsignal' => 'lxc.signal.reboot',
566 'lxc.stopsignal' => 'lxc.signal.stop',
567 'lxc.id_map' => 'lxc.idmap',
568 'lxc.init_cmd' => 'lxc.init.cmd',
569 'lxc.loglevel' => 'lxc.log.level',
570 'lxc.logfile' => 'lxc.log.file',
571 'lxc.mount' => 'lxc.mount.fstab',
572 'lxc.network.type' => 'lxc.net.INDEX.type',
573 'lxc.network.flags' => 'lxc.net.INDEX.flags',
574 'lxc.network.link' => 'lxc.net.INDEX.link',
575 'lxc.network.mtu' => 'lxc.net.INDEX.mtu',
576 'lxc.network.name' => 'lxc.net.INDEX.name',
577 'lxc.network.hwaddr' => 'lxc.net.INDEX.hwaddr',
578 'lxc.network.ipv4' => 'lxc.net.INDEX.ipv4.address',
579 'lxc.network.ipv4.gateway' => 'lxc.net.INDEX.ipv4.gateway',
580 'lxc.network.ipv6' => 'lxc.net.INDEX.ipv6.address',
581 'lxc.network.ipv6.gateway' => 'lxc.net.INDEX.ipv6.gateway',
582 'lxc.network.script.up' => 'lxc.net.INDEX.script.up',
583 'lxc.network.script.down' => 'lxc.net.INDEX.script.down',
584 'lxc.pts' => 'lxc.pty.max',
585 'lxc.se_context' => 'lxc.selinux.context',
586 'lxc.seccomp' => 'lxc.seccomp.profile',
587 'lxc.tty' => 'lxc.tty.max',
588 'lxc.utsname' => 'lxc.uts.name',
589};
590
591sub is_valid_lxc_conf_key {
592 my ($vmid, $key) = @_;
593 if ($key =~ /^lxc\.limit\./) {
594 warn "vm $vmid - $key: lxc.limit.* was renamed to lxc.prlimit.*\n";
595 return 1;
596 }
597 if (defined(my $new_name = $deprecated_lxc_conf_keys->{$key})) {
598 warn "vm $vmid - $key is deprecated and was renamed to $new_name\n";
599 return 1;
600 }
601 my $validity = $valid_lxc_conf_keys->{$key};
602 return $validity if defined($validity);
603 return 1 if $key =~ /^lxc\.cgroup\./ # allow all cgroup values
604 || $key =~ /^lxc\.prlimit\./ # allow all prlimits
605 || $key =~ /^lxc\.net\./; # allow custom network definitions
606 return 0;
607}
608
5e5915c5 609our $netconf_desc = {
1b4cf758
FG
610 type => {
611 type => 'string',
612 optional => 1,
613 description => "Network interface type.",
614 enum => [qw(veth)],
615 },
616 name => {
617 type => 'string',
a069f163
DM
618 format_description => 'string',
619 description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
1b4cf758
FG
620 pattern => '[-_.\w\d]+',
621 },
622 bridge => {
623 type => 'string',
a069f163 624 format_description => 'bridge',
1b4cf758
FG
625 description => 'Bridge to attach the network device to.',
626 pattern => '[-_.\w\d]+',
627 optional => 1,
628 },
603536e0 629 hwaddr => get_standard_option('mac-addr', {
e6f20294 630 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 631 }),
1b4cf758
FG
632 mtu => {
633 type => 'integer',
1b4cf758
FG
634 description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
635 minimum => 64, # minimum ethernet frame is 64 bytes
636 optional => 1,
637 },
638 ip => {
639 type => 'string',
640 format => 'pve-ipv4-config',
718a67d6 641 format_description => '(IPv4/CIDR|dhcp|manual)',
1b4cf758
FG
642 description => 'IPv4 address in CIDR format.',
643 optional => 1,
644 },
645 gw => {
646 type => 'string',
647 format => 'ipv4',
648 format_description => 'GatewayIPv4',
649 description => 'Default gateway for IPv4 traffic.',
650 optional => 1,
651 },
652 ip6 => {
653 type => 'string',
654 format => 'pve-ipv6-config',
6ea7095c 655 format_description => '(IPv6/CIDR|auto|dhcp|manual)',
1b4cf758
FG
656 description => 'IPv6 address in CIDR format.',
657 optional => 1,
658 },
659 gw6 => {
660 type => 'string',
661 format => 'ipv6',
662 format_description => 'GatewayIPv6',
663 description => 'Default gateway for IPv6 traffic.',
664 optional => 1,
665 },
666 firewall => {
667 type => 'boolean',
1b4cf758
FG
668 description => "Controls whether this interface's firewall rules should be used.",
669 optional => 1,
670 },
671 tag => {
672 type => 'integer',
6b202dd5
DM
673 minimum => 1,
674 maximum => 4094,
1b4cf758
FG
675 description => "VLAN tag for this interface.",
676 optional => 1,
677 },
678 trunks => {
679 type => 'string',
680 pattern => qr/\d+(?:;\d+)*/,
681 format_description => 'vlanid[;vlanid...]',
682 description => "VLAN ids to pass through the interface",
683 optional => 1,
684 },
380962c7
WB
685 rate => {
686 type => 'number',
687 format_description => 'mbps',
688 description => "Apply rate limiting to the interface",
689 optional => 1,
690 },
1b4cf758
FG
691};
692PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
693
694my $MAX_LXC_NETWORKS = 10;
695for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
696 $confdesc->{"net$i"} = {
697 optional => 1,
698 type => 'string', format => $netconf_desc,
699 description => "Specifies network interfaces for the container.",
700 };
701}
702
703PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
704sub verify_lxc_mp_string {
705 my ($mp, $noerr) = @_;
706
707 # do not allow:
708 # /./ or /../
709 # /. or /.. at the end
710 # ../ at the beginning
711
712 if($mp =~ m@/\.\.?/@ ||
713 $mp =~ m@/\.\.?$@ ||
714 $mp =~ m@^\.\./@) {
715 return undef if $noerr;
716 die "$mp contains illegal character sequences\n";
717 }
718 return $mp;
719}
720
721my $mp_desc = {
722 %$rootfs_desc,
84820d40
DM
723 backup => {
724 type => 'boolean',
235dbdf3
FG
725 description => 'Whether to include the mount point in backups.',
726 verbose_description => 'Whether to include the mount point in backups '.
727 '(only used for volume mount points).',
84820d40
DM
728 optional => 1,
729 },
1b4cf758
FG
730 mp => {
731 type => 'string',
732 format => 'pve-lxc-mp-string',
733 format_description => 'Path',
235dbdf3 734 description => 'Path to the mount point as seen from inside the container '.
52b6f941 735 '(must not contain symlinks).',
235dbdf3 736 verbose_description => "Path to the mount point as seen from inside the container.\n\n".
52b6f941 737 "NOTE: Must not contain any symlinks for security reasons."
1b4cf758
FG
738 },
739};
740PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
741
742my $unuseddesc = {
743 optional => 1,
744 type => 'string', format => 'pve-volume-id',
2928e616 745 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
1b4cf758
FG
746};
747
748for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
749 $confdesc->{"mp$i"} = {
750 optional => 1,
751 type => 'string', format => $mp_desc,
2928e616 752 description => "Use volume as container mount point.",
1b4cf758
FG
753 optional => 1,
754 };
755}
756
757for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
758 $confdesc->{"unused$i"} = $unuseddesc;
759}
760
761sub parse_pct_config {
762 my ($filename, $raw) = @_;
763
764 return undef if !defined($raw);
765
766 my $res = {
767 digest => Digest::SHA::sha1_hex($raw),
768 snapshots => {},
7547dc63 769 pending => {},
1b4cf758
FG
770 };
771
772 $filename =~ m|/lxc/(\d+).conf$|
773 || die "got strange filename '$filename'";
774
775 my $vmid = $1;
776
777 my $conf = $res;
778 my $descr = '';
779 my $section = '';
780
781 my @lines = split(/\n/, $raw);
782 foreach my $line (@lines) {
783 next if $line =~ m/^\s*$/;
784
7547dc63
OB
785 if ($line =~ m/^\[pve:pending\]\s*$/i) {
786 $section = 'pending';
787 $conf->{description} = $descr if $descr;
788 $descr = '';
789 $conf = $res->{$section} = {};
790 next;
791 } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
1b4cf758
FG
792 $section = $1;
793 $conf->{description} = $descr if $descr;
794 $descr = '';
795 $conf = $res->{snapshots}->{$section} = {};
796 next;
797 }
798
799 if ($line =~ m/^\#(.*)\s*$/) {
800 $descr .= PVE::Tools::decode_text($1) . "\n";
801 next;
802 }
803
804 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
805 my $key = $1;
806 my $value = $3;
108c6cab
WB
807 my $validity = is_valid_lxc_conf_key($vmid, $key);
808 if ($validity eq 1) {
1b4cf758
FG
809 push @{$conf->{lxc}}, [$key, $value];
810 } elsif (my $errmsg = $validity) {
811 warn "vm $vmid - $key: $errmsg\n";
812 } else {
813 warn "vm $vmid - unable to parse config: $line\n";
814 }
815 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
816 $descr .= PVE::Tools::decode_text($2);
817 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
818 $conf->{snapstate} = $1;
7547dc63
OB
819 } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
820 my $value = $1;
821 if ($section eq 'pending') {
822 $conf->{delete} = $value;
823 } else {
824 warn "vm $vmid - property 'delete' is only allowed in [pve:pending]\n";
825 }
1b4cf758
FG
826 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
827 my $key = $1;
828 my $value = $2;
829 eval { $value = PVE::LXC::Config->check_type($key, $value); };
830 warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
831 $conf->{$key} = $value;
832 } else {
833 warn "vm $vmid - unable to parse config: $line\n";
834 }
835 }
836
837 $conf->{description} = $descr if $descr;
838
839 delete $res->{snapstate}; # just to be sure
840
841 return $res;
842}
843
844sub write_pct_config {
845 my ($filename, $conf) = @_;
846
847 delete $conf->{snapstate}; # just to be sure
848
849 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
850 my $used_volids = {};
851 foreach my $vid (@$volidlist) {
852 $used_volids->{$vid} = 1;
853 }
854
855 # remove 'unusedX' settings if the volume is still used
856 foreach my $key (keys %$conf) {
857 my $value = $conf->{$key};
858 if ($key =~ m/^unused/ && $used_volids->{$value}) {
859 delete $conf->{$key};
860 }
861 }
862
863 my $generate_raw_config = sub {
864 my ($conf) = @_;
865
866 my $raw = '';
867
868 # add description as comment to top of file
869 my $descr = $conf->{description} || '';
870 foreach my $cl (split(/\n/, $descr)) {
7547dc63 871 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
1b4cf758
FG
872 }
873
874 foreach my $key (sort keys %$conf) {
875 next if $key eq 'digest' || $key eq 'description' ||
876 $key eq 'pending' || $key eq 'snapshots' ||
877 $key eq 'snapname' || $key eq 'lxc';
878 my $value = $conf->{$key};
879 die "detected invalid newline inside property '$key'\n"
880 if $value =~ m/\n/;
881 $raw .= "$key: $value\n";
882 }
883
884 if (my $lxcconf = $conf->{lxc}) {
885 foreach my $entry (@$lxcconf) {
886 my ($k, $v) = @$entry;
887 $raw .= "$k: $v\n";
888 }
889 }
890
891 return $raw;
892 };
893
894 my $raw = &$generate_raw_config($conf);
895
7547dc63
OB
896 if (scalar(keys %{$conf->{pending}})){
897 $raw .= "\n[pve:pending]\n";
898 $raw .= &$generate_raw_config($conf->{pending});
899 }
900
1b4cf758
FG
901 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
902 $raw .= "\n[$snapname]\n";
903 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
904 }
905
906 return $raw;
907}
908
909sub update_pct_config {
6517e001 910 my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
1b4cf758 911
6517e001 912 my $storage_cfg = PVE::Storage::config();
1b4cf758 913
6517e001
OB
914 foreach my $opt (@$revert) {
915 delete $conf->{pending}->{$opt};
916 $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
1b4cf758
FG
917 }
918
6517e001
OB
919 # write updates to pending section
920 my $modified = {}; # record modified options
1b4cf758 921
6517e001
OB
922 foreach my $opt (@$delete) {
923 if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
924 warn "cannot delete '$opt' - not set in current configuration!\n";
925 next;
1b4cf758 926 }
6517e001
OB
927 $modified->{$opt} = 1;
928 if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
929 die "unable to delete required option '$opt'\n";
930 } elsif ($opt =~ m/^unused(\d+)$/) {
931 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
932 } elsif ($opt =~ m/^mp(\d+)$/) {
933 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
934 } elsif ($opt eq 'unprivileged') {
935 die "unable to delete read-only option: '$opt'\n";
1b4cf758 936 }
6517e001 937 $class->add_to_pending_delete($conf, $opt);
1b4cf758
FG
938 }
939
1b3213ae
FG
940 my $check_content_type = sub {
941 my ($mp) = @_;
942 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
6517e001 943 my $storage_config = PVE::Storage::storage_config($storage_cfg, $sid);
1b3213ae
FG
944 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
945 if !$storage_config->{content}->{rootdir};
946 };
1b4cf758 947
c10951f7 948 foreach my $opt (sort keys %$param) { # add/change
6517e001 949 $modified->{$opt} = 1;
1b4cf758 950 my $value = $param->{$opt};
6517e001
OB
951 if ($opt =~ m/^mp(\d+)$/ || $opt eq 'rootfs') {
952 $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
953 my $mp = $opt eq 'rootfs' ? $class->parse_ct_rootfs($value) : $class->parse_ct_mountpoint($value);
3927ae96 954 $check_content_type->($mp) if ($mp->{type} eq 'volume');
6517e001
OB
955 } elsif ($opt eq 'hookscript') {
956 PVE::GuestHelpers::check_hookscript($value);
1b4cf758 957 } elsif ($opt eq 'nameserver') {
6517e001 958 $value = PVE::LXC::verify_nameserver_list($value);
1b4cf758 959 } elsif ($opt eq 'searchdomain') {
6517e001 960 $value = PVE::LXC::verify_searchdomain_list($value);
1b4cf758
FG
961 } elsif ($opt eq 'unprivileged') {
962 die "unable to modify read-only option: '$opt'\n";
1b4cf758 963 }
6517e001
OB
964 $conf->{pending}->{$opt} = $value;
965 $class->remove_from_pending_delete($conf, $opt);
f8aa3d35
WL
966 }
967
6517e001 968 my $changes = $class->cleanup_pending($conf);
1b4cf758 969
6517e001
OB
970 my $errors = {};
971 if ($running) {
972 $class->vmconfig_hotplug_pending($vmid, $conf, $storage_cfg, $modified, $errors);
973 } else {
974 $class->vmconfig_apply_pending($vmid, $conf, $storage_cfg, $modified, $errors);
1b4cf758
FG
975 }
976
6517e001 977 return $errors;
1b4cf758
FG
978}
979
980sub check_type {
981 my ($class, $key, $value) = @_;
982
983 die "unknown setting '$key'\n" if !$confdesc->{$key};
984
985 my $type = $confdesc->{$key}->{type};
986
987 if (!defined($value)) {
988 die "got undefined value\n";
989 }
990
991 if ($value =~ m/[\n\r]/) {
992 die "property contains a line feed\n";
993 }
994
995 if ($type eq 'boolean') {
996 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
997 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
998 die "type check ('boolean') failed - got '$value'\n";
999 } elsif ($type eq 'integer') {
1000 return int($1) if $value =~ m/^(\d+)$/;
1001 die "type check ('integer') failed - got '$value'\n";
1002 } elsif ($type eq 'number') {
1003 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
1004 die "type check ('number') failed - got '$value'\n";
1005 } elsif ($type eq 'string') {
1006 if (my $fmt = $confdesc->{$key}->{format}) {
1007 PVE::JSONSchema::check_format($fmt, $value);
1008 return $value;
1009 }
1010 return $value;
1011 } else {
1012 die "internal error"
1013 }
1014}
1015
1016
1017# add JSON properties for create and set function
1018sub json_config_properties {
1019 my ($class, $prop) = @_;
1020
1021 foreach my $opt (keys %$confdesc) {
1022 next if $opt eq 'parent' || $opt eq 'snaptime';
1023 next if $prop->{$opt};
1024 $prop->{$opt} = $confdesc->{$opt};
1025 }
1026
1027 return $prop;
1028}
1029
1030sub __parse_ct_mountpoint_full {
1031 my ($class, $desc, $data, $noerr) = @_;
1032
1033 $data //= '';
1034
1035 my $res;
1036 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
1037 if ($@) {
1038 return undef if $noerr;
1039 die $@;
1040 }
1041
1042 if (defined(my $size = $res->{size})) {
1043 $size = PVE::JSONSchema::parse_size($size);
1044 if (!defined($size)) {
1045 return undef if $noerr;
1046 die "invalid size: $size\n";
1047 }
1048 $res->{size} = $size;
1049 }
1050
1051 $res->{type} = $class->classify_mountpoint($res->{volume});
1052
1053 return $res;
1054};
1055
1056sub parse_ct_rootfs {
1057 my ($class, $data, $noerr) = @_;
1058
1059 my $res = $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
1060
1061 $res->{mp} = '/' if defined($res);
1062
1063 return $res;
1064}
1065
1066sub parse_ct_mountpoint {
1067 my ($class, $data, $noerr) = @_;
1068
1069 return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
1070}
1071
1072sub print_ct_mountpoint {
1073 my ($class, $info, $nomp) = @_;
1074 my $skip = [ 'type' ];
1075 push @$skip, 'mp' if $nomp;
1076 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1077}
1078
1079sub print_lxc_network {
1080 my ($class, $net) = @_;
1081 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1082}
1083
1084sub parse_lxc_network {
1085 my ($class, $data) = @_;
1086
1087 my $res = {};
1088
1089 return $res if !$data;
1090
1091 $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1092
1093 $res->{type} = 'veth';
2f19133b
WB
1094 if (!$res->{hwaddr}) {
1095 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1096 $res->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1097 }
1b4cf758
FG
1098
1099 return $res;
1100}
1101
5a63f1c5
WB
1102sub parse_features {
1103 my ($class, $data) = @_;
1104 return {} if !$data;
1105 return PVE::JSONSchema::parse_property_string($features_desc, $data);
1106}
1107
1b4cf758
FG
1108sub option_exists {
1109 my ($class, $name) = @_;
1110
1111 return defined($confdesc->{$name});
1112}
1113# END JSON config code
1114
32e15a2b
OB
1115my $LXC_FASTPLUG_OPTIONS= {
1116 'description' => 1,
1117 'onboot' => 1,
1118 'startup' => 1,
1119 'protection' => 1,
1120 'hostname' => 1,
1121 'hookscript' => 1,
1122 'cores' => 1,
1123 'tags' => 1,
db15c375 1124 'lock' => 1,
32e15a2b
OB
1125};
1126
1127sub vmconfig_hotplug_pending {
1128 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1129
1130 my $pid = PVE::LXC::find_lxc_pid($vmid);
1131 my $rootdir = "/proc/$pid/root";
1132
1133 my $add_hotplug_error = sub {
1134 my ($opt, $msg) = @_;
1135 $errors->{$opt} = "unable to hotplug $opt: $msg";
1136 };
1137
1138 my $changes;
c10951f7 1139 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
32e15a2b
OB
1140 next if $selection && !$selection->{$opt};
1141 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1142 $conf->{$opt} = delete $conf->{pending}->{$opt};
1143 $changes = 1;
1144 }
1145 }
1146
1147 if ($changes) {
1148 $class->write_config($vmid, $conf);
1149 }
1150
1151 # There's no separate swap size to configure, there's memory and "total"
1152 # memory (iow. memory+swap). This means we have to change them together.
1153 my $hotplug_memory_done;
1154 my $hotplug_memory = sub {
1155 my ($wanted_memory, $wanted_swap) = @_;
1156 my $old_memory = ($conf->{memory} || $confdesc->{memory}->{default});
1157 my $old_swap = ($conf->{swap} || $confdesc->{swap}->{default});
1158
1159 $wanted_memory //= $old_memory;
1160 $wanted_swap //= $old_swap;
1161
1162 my $total = $wanted_memory + $wanted_swap;
1163 my $old_total = $old_memory + $old_swap;
1164
1165 if ($total > $old_total) {
1166 PVE::LXC::write_cgroup_value("memory", $vmid,
1167 "memory.memsw.limit_in_bytes",
1168 int($total*1024*1024));
1169 PVE::LXC::write_cgroup_value("memory", $vmid,
1170 "memory.limit_in_bytes",
1171 int($wanted_memory*1024*1024));
1172 } else {
1173 PVE::LXC::write_cgroup_value("memory", $vmid,
1174 "memory.limit_in_bytes",
1175 int($wanted_memory*1024*1024));
1176 PVE::LXC::write_cgroup_value("memory", $vmid,
1177 "memory.memsw.limit_in_bytes",
1178 int($total*1024*1024));
1179 }
1180 $hotplug_memory_done = 1;
1181 };
1182
1183 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1184 # FIXME: $force deletion is not implemented for CTs
8ec10817 1185 foreach my $opt (sort keys %$pending_delete_hash) {
32e15a2b
OB
1186 next if $selection && !$selection->{$opt};
1187 eval {
1188 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1189 # pass
1190 } elsif ($opt =~ m/^unused(\d+)$/) {
1191 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1192 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1193 } elsif ($opt eq 'swap') {
1194 $hotplug_memory->(undef, 0);
1195 } elsif ($opt eq 'cpulimit') {
1196 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", -1);
1197 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
1198 } elsif ($opt eq 'cpuunits') {
6f433df0 1199 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
32e15a2b
OB
1200 } elsif ($opt =~ m/^net(\d)$/) {
1201 my $netid = $1;
1202 PVE::Network::veth_delete("veth${vmid}i$netid");
1203 } else {
1204 die "skip\n"; # skip non-hotpluggable opts
1205 }
1206 };
1207 if (my $err = $@) {
1208 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1209 } else {
1210 delete $conf->{$opt};
1211 $class->remove_from_pending_delete($conf, $opt);
1212 }
1213 }
1214
c10951f7 1215 foreach my $opt (sort keys %{$conf->{pending}}) {
32e15a2b
OB
1216 next if $opt eq 'delete'; # just to be sure
1217 next if $selection && !$selection->{$opt};
1218 my $value = $conf->{pending}->{$opt};
1219 eval {
1220 if ($opt eq 'cpulimit') {
1221 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", 100000);
1222 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
1223 } elsif ($opt eq 'cpuunits') {
1224 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
1225 } elsif ($opt =~ m/^net(\d+)$/) {
1226 my $netid = $1;
1227 my $net = $class->parse_lxc_network($value);
7eff309b 1228 $value = $class->print_lxc_network($net);
32e15a2b
OB
1229 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
1230 } elsif ($opt eq 'memory' || $opt eq 'swap') {
1231 if (!$hotplug_memory_done) { # don't call twice if both opts are passed
1232 $hotplug_memory->($conf->{pending}->{memory}, $conf->{pending}->{swap});
1233 }
b2de4c04
WB
1234 } elsif ($opt =~ m/^mp(\d+)$/) {
1235 if (!PVE::LXC::Tools::can_use_new_mount_api()) {
1236 die "skip\n";
1237 }
1238
1239 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 1);
1240 # apply_pending_mountpoint modifies the value if it creates a new disk
1241 $value = $conf->{pending}->{$opt};
32e15a2b
OB
1242 } else {
1243 die "skip\n"; # skip non-hotpluggable
1244 }
1245 };
1246 if (my $err = $@) {
1247 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1248 } else {
1249 $conf->{$opt} = $value;
1250 delete $conf->{pending}->{$opt};
1251 }
1252 }
1253
1254 $class->write_config($vmid, $conf);
1255}
1256
1257sub vmconfig_apply_pending {
1258 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1259
1260 my $add_apply_error = sub {
1261 my ($opt, $msg) = @_;
1262 my $err_msg = "unable to apply pending change $opt : $msg";
1263 $errors->{$opt} = $err_msg;
1264 warn $err_msg;
1265 };
1266
32e15a2b
OB
1267 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1268 # FIXME: $force deletion is not implemented for CTs
8ec10817 1269 foreach my $opt (sort keys %$pending_delete_hash) {
32e15a2b 1270 next if $selection && !$selection->{$opt};
32e15a2b
OB
1271 eval {
1272 if ($opt =~ m/^mp(\d+)$/) {
1273 my $mp = $class->parse_ct_mountpoint($conf->{$opt});
1274 if ($mp->{type} eq 'volume') {
1275 $class->add_unused_volume($conf, $mp->{volume})
1276 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1277 }
1278 } elsif ($opt =~ m/^unused(\d+)$/) {
1279 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1280 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1281 }
1282 };
1283 if (my $err = $@) {
1284 $add_apply_error->($opt, $err);
1285 } else {
1286 delete $conf->{$opt};
1287 $class->remove_from_pending_delete($conf, $opt);
1288 }
1289 }
1290
132c0a90
OB
1291 $class->cleanup_pending($conf);
1292
c10951f7 1293 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
32e15a2b
OB
1294 next if $opt eq 'delete'; # just to be sure
1295 next if $selection && !$selection->{$opt};
1296 eval {
1297 if ($opt =~ m/^mp(\d+)$/) {
869081a2 1298 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 0);
7eff309b
OB
1299 } elsif ($opt =~ m/^net(\d+)$/) {
1300 my $netid = $1;
1301 my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
1302 $conf->{pending}->{$opt} = $class->print_lxc_network($net);
32e15a2b
OB
1303 }
1304 };
1305 if (my $err = $@) {
1306 $add_apply_error->($opt, $err);
1307 } else {
32e15a2b
OB
1308 $conf->{$opt} = delete $conf->{pending}->{$opt};
1309 }
1310 }
1311
1312 $class->write_config($vmid, $conf);
1313}
1314
869081a2
WB
1315my $rescan_volume = sub {
1316 my ($storecfg, $mp) = @_;
1317 eval {
0c69dcfc 1318 $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5);
869081a2
WB
1319 };
1320 warn "Could not rescan volume size - $@\n" if $@;
1321};
1322
1323sub apply_pending_mountpoint {
1324 my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
1325
1326 my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
1327 my $old = $conf->{$opt};
1328 if ($mp->{type} eq 'volume') {
1329 if ($mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
b2de4c04 1330 my $original_value = $conf->{pending}->{$opt};
869081a2
WB
1331 my $vollist = PVE::LXC::create_disks(
1332 $storecfg,
1333 $vmid,
b2de4c04 1334 { $opt => $original_value },
869081a2
WB
1335 $conf,
1336 1,
1337 );
b2de4c04
WB
1338 if ($running) {
1339 # Re-parse mount point:
1340 my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
1341 eval {
1342 PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
1343 };
1344 my $err = $@;
1345 if ($err) {
1346 PVE::LXC::destroy_disks($storecfg, $vollist);
1347 # The pending-changes code collects errors but keeps on looping through further
1348 # pending changes, so unroll the change in $conf as well if destroy_disks()
1349 # didn't die().
1350 $conf->{pending}->{$opt} = $original_value;
1351 die $err;
1352 }
1353 }
869081a2 1354 } else {
b2de4c04 1355 die "skip\n" if $running && defined($old); # TODO: "changing" mount points?
869081a2 1356 $rescan_volume->($storecfg, $mp);
b2de4c04
WB
1357 if ($running) {
1358 PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
1359 }
869081a2
WB
1360 $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
1361 }
1362 }
1363
1364 if (defined($old)) {
1365 my $mp = $class->parse_ct_mountpoint($old);
1366 if ($mp->{type} eq 'volume') {
1367 $class->add_unused_volume($conf, $mp->{volume})
1368 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1369 }
1370 }
1371}
1372
d250604f
FG
1373sub classify_mountpoint {
1374 my ($class, $vol) = @_;
1375 if ($vol =~ m!^/!) {
1376 return 'device' if $vol =~ m!^/dev/!;
1377 return 'bind';
1378 }
1379 return 'volume';
1380}
1381
7b4237c5 1382my $__is_volume_in_use = sub {
72e6bc20 1383 my ($class, $config, $volid) = @_;
d250604f
FG
1384 my $used = 0;
1385
1386 $class->foreach_mountpoint($config, sub {
1387 my ($ms, $mountpoint) = @_;
1388 return if $used;
1389 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1390 });
1391
72e6bc20
WB
1392 return $used;
1393};
1394
1395sub is_volume_in_use_by_snapshots {
1396 my ($class, $config, $volid) = @_;
1397
1398 if (my $snapshots = $config->{snapshots}) {
d250604f 1399 foreach my $snap (keys %$snapshots) {
7b4237c5 1400 return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
d250604f
FG
1401 }
1402 }
1403
72e6bc20 1404 return 0;
7b4237c5 1405}
72e6bc20
WB
1406
1407sub is_volume_in_use {
d063af00 1408 my ($class, $config, $volid, $include_snapshots, $include_pending) = @_;
7b4237c5 1409 return 1 if $__is_volume_in_use->($class, $config, $volid);
72e6bc20 1410 return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
d063af00 1411 return 1 if $include_pending && $__is_volume_in_use->($class, $config->{pending}, $volid);
72e6bc20 1412 return 0;
d250604f
FG
1413}
1414
1415sub has_dev_console {
1416 my ($class, $conf) = @_;
1417
1418 return !(defined($conf->{console}) && !$conf->{console});
1419}
1420
be7942f0
DM
1421sub has_lxc_entry {
1422 my ($class, $conf, $keyname) = @_;
1423
1424 if (my $lxcconf = $conf->{lxc}) {
1425 foreach my $entry (@$lxcconf) {
1426 my ($key, undef) = @$entry;
1427 return 1 if $key eq $keyname;
1428 }
1429 }
1430
1431 return 0;
1432}
1433
1b4cf758
FG
1434sub get_tty_count {
1435 my ($class, $conf) = @_;
1436
1437 return $conf->{tty} // $confdesc->{tty}->{default};
1438}
1439
1440sub get_cmode {
1441 my ($class, $conf) = @_;
1442
1443 return $conf->{cmode} // $confdesc->{cmode}->{default};
1444}
1445
d250604f
FG
1446sub mountpoint_names {
1447 my ($class, $reverse) = @_;
1448
1449 my @names = ('rootfs');
1450
1451 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1452 push @names, "mp$i";
1453 }
1454
1455 return $reverse ? reverse @names : @names;
1456}
1457
1458sub foreach_mountpoint_full {
8915b450 1459 my ($class, $conf, $reverse, $func, @param) = @_;
d250604f 1460
717f70b7
FG
1461 my $mps = [ grep { defined($conf->{$_}) } $class->mountpoint_names($reverse) ];
1462 foreach my $key (@$mps) {
d250604f 1463 my $value = $conf->{$key};
1b4cf758 1464 my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
d250604f
FG
1465 next if !defined($mountpoint);
1466
8915b450 1467 &$func($key, $mountpoint, @param);
d250604f
FG
1468 }
1469}
1470
1471sub foreach_mountpoint {
8915b450 1472 my ($class, $conf, $func, @param) = @_;
d250604f 1473
8915b450 1474 $class->foreach_mountpoint_full($conf, 0, $func, @param);
d250604f
FG
1475}
1476
1477sub foreach_mountpoint_reverse {
8915b450 1478 my ($class, $conf, $func, @param) = @_;
d250604f 1479
8915b450 1480 $class->foreach_mountpoint_full($conf, 1, $func, @param);
d250604f
FG
1481}
1482
1483sub get_vm_volumes {
1484 my ($class, $conf, $excludes) = @_;
1485
1486 my $vollist = [];
1487
1488 $class->foreach_mountpoint($conf, sub {
1489 my ($ms, $mountpoint) = @_;
1490
1491 return if $excludes && $ms eq $excludes;
1492
1493 my $volid = $mountpoint->{volume};
1494 return if !$volid || $mountpoint->{type} ne 'volume';
1495
1496 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1497 return if !$sid;
1498
1499 push @$vollist, $volid;
1500 });
1501
1502 return $vollist;
1503}
1504
f78c87a8 1505sub get_replicatable_volumes {
70996986 1506 my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
f78c87a8
DM
1507
1508 my $volhash = {};
1509
1510 my $test_volid = sub {
1511 my ($volid, $mountpoint) = @_;
1512
1513 return if !$volid;
1514
5cf90b0c 1515 my $mptype = $mountpoint->{type};
896cd762 1516 my $replicate = $mountpoint->{replicate} // 1;
d2a046b7
DC
1517
1518 if ($mptype ne 'volume') {
1519 # skip bindmounts if replicate = 0 even for cleanup,
1520 # since bind mounts could not have been replicated ever
1521 return if !$replicate;
1522 die "unable to replicate mountpoint type '$mptype'\n";
1523 }
5cf90b0c
DM
1524
1525 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
1526 return if !$storeid;
1527
af21e699 1528 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
5cf90b0c
DM
1529 return if $scfg->{shared};
1530
1531 my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
1532 return if !$owner || ($owner != $vmid);
1533
1534 die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
1535
d2a046b7 1536 return if !$cleanup && !$replicate;
f78c87a8
DM
1537
1538 if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
e65dce6b 1539 return if $cleanup || $noerr;
f78c87a8
DM
1540 die "missing replicate feature on volume '$volid'\n";
1541 }
1542
1543 $volhash->{$volid} = 1;
1544 };
1545
1546 $class->foreach_mountpoint($conf, sub {
1547 my ($ms, $mountpoint) = @_;
1548 $test_volid->($mountpoint->{volume}, $mountpoint);
1549 });
1550
1551 foreach my $snapname (keys %{$conf->{snapshots}}) {
1552 my $snap = $conf->{snapshots}->{$snapname};
1553 $class->foreach_mountpoint($snap, sub {
1554 my ($ms, $mountpoint) = @_;
1555 $test_volid->($mountpoint->{volume}, $mountpoint);
1556 });
1557 }
1558
b03664ae
DM
1559 # add 'unusedX' volumes to volhash
1560 foreach my $key (keys %$conf) {
1561 if ($key =~ m/^unused/) {
1562 $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
1563 }
1564 }
1565
f78c87a8
DM
1566 return $volhash;
1567}
1568
15691;