]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Config.pm
5ac1446514bb0fe88037efdaa380726393ea60f8
[pve-container.git] / src / PVE / LXC / Config.pm
1 package PVE::LXC::Config;
2
3 use strict;
4 use warnings;
5
6 use Fcntl qw(O_RDONLY);
7
8 use PVE::AbstractConfig;
9 use PVE::Cluster qw(cfs_register_file);
10 use PVE::DataCenterConfig;
11 use PVE::GuestHelpers;
12 use PVE::INotify;
13 use PVE::JSONSchema qw(get_standard_option);
14 use PVE::Tools;
15
16 use PVE::LXC;
17
18 use base qw(PVE::AbstractConfig);
19
20 use constant {
21 FIFREEZE => 0xc0045877,
22 FITHAW => 0xc0045878,
23 };
24
25 my $have_sdn;
26 eval {
27 require PVE::Network::SDN::Vnets;
28 $have_sdn = 1;
29 };
30
31 my $nodename = PVE::INotify::nodename();
32 my $lock_handles = {};
33 my $lockdir = "/run/lock/lxc";
34 mkdir $lockdir;
35 mkdir "/etc/pve/nodes/$nodename/lxc";
36 my $MAX_MOUNT_POINTS = 256;
37 my $MAX_UNUSED_DISKS = $MAX_MOUNT_POINTS;
38 my $MAX_DEVICES = 256;
39
40 # BEGIN implemented abstract methods from PVE::AbstractConfig
41
42 sub guest_type {
43 return "CT";
44 }
45
46 sub __config_max_unused_disks {
47 my ($class) = @_;
48
49 return $MAX_UNUSED_DISKS;
50 }
51
52 sub config_file_lock {
53 my ($class, $vmid) = @_;
54
55 return "$lockdir/pve-config-${vmid}.lock";
56 }
57
58 sub cfs_config_path {
59 my ($class, $vmid, $node) = @_;
60
61 $node = $nodename if !$node;
62 return "nodes/$node/lxc/$vmid.conf";
63 }
64
65 sub mountpoint_backup_enabled {
66 my ($class, $mp_key, $mountpoint) = @_;
67
68 my $enabled;
69 my $reason;
70
71 if ($mp_key eq 'rootfs') {
72 $enabled = 1;
73 $reason = 'rootfs';
74 } elsif ($mountpoint->{type} ne 'volume') {
75 $enabled = 0;
76 $reason = 'not a volume';
77 } elsif ($mountpoint->{backup}) {
78 $enabled = 1;
79 $reason = 'enabled';
80 } else {
81 $enabled = 0;
82 $reason = 'disabled';
83 }
84 return wantarray ? ($enabled, $reason) : $enabled;
85 }
86
87 sub has_feature {
88 my ($class, $feature, $conf, $storecfg, $snapname, $running, $backup_only) = @_;
89 my $err;
90
91 my $opts;
92 if ($feature eq 'copy' || $feature eq 'clone') {
93 $opts = {'valid_target_formats' => ['raw', 'subvol']};
94 }
95
96 $class->foreach_volume($conf, sub {
97 my ($ms, $mountpoint) = @_;
98
99 return if $err; # skip further test
100 return if $backup_only && !$class->mountpoint_backup_enabled($ms, $mountpoint);
101
102 $err = 1 if !PVE::Storage::volume_has_feature(
103 $storecfg, $feature, $mountpoint->{volume}, $snapname, $running, $opts);
104 });
105
106 return $err ? 0 : 1;
107 }
108
109 sub __snapshot_save_vmstate {
110 my ($class, $vmid, $conf, $snapname, $storecfg) = @_;
111 die "implement me - snapshot_save_vmstate\n";
112 }
113
114 sub __snapshot_activate_storages {
115 my ($class, $conf, $include_vmstate) = @_;
116
117 my $storecfg = PVE::Storage::config();
118 my $opts = $include_vmstate ? { 'extra_keys' => ['vmstate'] } : {};
119 my $storage_hash = {};
120
121 $class->foreach_volume_full($conf, $opts, sub {
122 my ($vs, $mountpoint) = @_;
123
124 return if $mountpoint->{type} ne 'volume';
125
126 my ($storeid) = PVE::Storage::parse_volume_id($mountpoint->{volume});
127 $storage_hash->{$storeid} = 1;
128 });
129
130 PVE::Storage::activate_storage_list($storecfg, [ sort keys $storage_hash->%* ]);
131 }
132
133 sub __snapshot_check_running {
134 my ($class, $vmid) = @_;
135 return PVE::LXC::check_running($vmid);
136 }
137
138 sub __snapshot_check_freeze_needed {
139 my ($class, $vmid, $config, $save_vmstate) = @_;
140
141 my $ret = $class->__snapshot_check_running($vmid);
142 return ($ret, $ret);
143 }
144
145 # implements similar functionality to fsfreeze(8)
146 sub fsfreeze_mountpoint {
147 my ($path, $thaw) = @_;
148
149 my $op = $thaw ? 'thaw' : 'freeze';
150 my $ioctl = $thaw ? FITHAW : FIFREEZE;
151
152 sysopen my $fd, $path, O_RDONLY or die "failed to open $path: $!\n";
153 my $ioctl_err;
154 if (!ioctl($fd, $ioctl, 0)) {
155 $ioctl_err = "$!";
156 }
157 close($fd);
158 die "fs$op '$path' failed - $ioctl_err\n" if defined $ioctl_err;
159 }
160
161 sub __snapshot_freeze {
162 my ($class, $vmid, $unfreeze) = @_;
163
164 my $conf = $class->load_config($vmid);
165 my $storagecfg = PVE::Storage::config();
166
167 my $freeze_mps = [];
168 $class->foreach_volume($conf, sub {
169 my ($ms, $mountpoint) = @_;
170
171 return if $mountpoint->{type} ne 'volume';
172
173 if (PVE::Storage::volume_snapshot_needs_fsfreeze($storagecfg, $mountpoint->{volume})) {
174 push @$freeze_mps, $mountpoint->{mp};
175 }
176 });
177
178 my $freeze_mountpoints = sub {
179 my ($thaw) = @_;
180
181 return if scalar(@$freeze_mps) == 0;
182
183 my $pid = PVE::LXC::find_lxc_pid($vmid);
184
185 for my $mp (@$freeze_mps) {
186 eval{ fsfreeze_mountpoint("/proc/${pid}/root/${mp}", $thaw); };
187 warn $@ if $@;
188 }
189 };
190
191 if ($unfreeze) {
192 eval { PVE::LXC::thaw($vmid); };
193 warn $@ if $@;
194 $freeze_mountpoints->(1);
195 } else {
196 PVE::LXC::freeze($vmid);
197 PVE::LXC::sync_container_namespace($vmid);
198 $freeze_mountpoints->(0);
199 }
200 }
201
202 sub __snapshot_create_vol_snapshot {
203 my ($class, $vmid, $ms, $mountpoint, $snapname) = @_;
204
205 my $storecfg = PVE::Storage::config();
206
207 return if $snapname eq 'vzdump' &&
208 !$class->mountpoint_backup_enabled($ms, $mountpoint);
209
210 PVE::Storage::volume_snapshot($storecfg, $mountpoint->{volume}, $snapname);
211 }
212
213 sub __snapshot_delete_remove_drive {
214 my ($class, $snap, $remove_drive) = @_;
215
216 if ($remove_drive eq 'vmstate') {
217 die "implement me - saving vmstate\n";
218 } else {
219 my $value = $snap->{$remove_drive};
220 my $mountpoint = $class->parse_volume($remove_drive, $value, 1);
221 delete $snap->{$remove_drive};
222
223 $class->add_unused_volume($snap, $mountpoint->{volume})
224 if $mountpoint && ($mountpoint->{type} eq 'volume');
225 }
226 }
227
228 sub __snapshot_delete_vmstate_file {
229 my ($class, $snap, $force) = @_;
230
231 die "implement me - saving vmstate\n";
232 }
233
234 sub __snapshot_delete_vol_snapshot {
235 my ($class, $vmid, $ms, $mountpoint, $snapname, $unused) = @_;
236
237 return if $snapname eq 'vzdump' &&
238 !$class->mountpoint_backup_enabled($ms, $mountpoint);
239
240 my $storecfg = PVE::Storage::config();
241 PVE::Storage::volume_snapshot_delete($storecfg, $mountpoint->{volume}, $snapname);
242 push @$unused, $mountpoint->{volume};
243 }
244
245 sub __snapshot_rollback_vol_possible {
246 my ($class, $mountpoint, $snapname, $blockers) = @_;
247
248 my $storecfg = PVE::Storage::config();
249 PVE::Storage::volume_rollback_is_possible(
250 $storecfg,
251 $mountpoint->{volume},
252 $snapname,
253 $blockers,
254 );
255 }
256
257 sub __snapshot_rollback_vol_rollback {
258 my ($class, $mountpoint, $snapname) = @_;
259
260 my $storecfg = PVE::Storage::config();
261 PVE::Storage::volume_snapshot_rollback($storecfg, $mountpoint->{volume}, $snapname);
262 }
263
264 sub __snapshot_rollback_vm_stop {
265 my ($class, $vmid) = @_;
266
267 PVE::LXC::vm_stop($vmid, 1)
268 if $class->__snapshot_check_running($vmid);
269 }
270
271 sub __snapshot_rollback_vm_start {
272 my ($class, $vmid, $vmstate, $data);
273
274 die "implement me - save vmstate\n";
275 }
276
277 sub __snapshot_rollback_get_unused {
278 my ($class, $conf, $snap) = @_;
279
280 my $unused = [];
281
282 $class->foreach_volume($conf, sub {
283 my ($vs, $volume) = @_;
284
285 return if $volume->{type} ne 'volume';
286
287 my $found = 0;
288 my $volid = $volume->{volume};
289
290 $class->foreach_volume($snap, sub {
291 my ($ms, $mountpoint) = @_;
292
293 return if $found;
294 return if ($mountpoint->{type} ne 'volume');
295
296 $found = 1
297 if ($mountpoint->{volume} && $mountpoint->{volume} eq $volid);
298 });
299
300 push @$unused, $volid if !$found;
301 });
302
303 return $unused;
304 }
305
306 # END implemented abstract methods from PVE::AbstractConfig
307
308 # BEGIN JSON config code
309
310 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
311
312
313 my $valid_mount_option_re = qr/(noatime|lazytime|nodev|nosuid|noexec)/;
314
315 sub is_valid_mount_option {
316 my ($option) = @_;
317 return $option =~ $valid_mount_option_re;
318 }
319
320 my $rootfs_desc = {
321 volume => {
322 type => 'string',
323 default_key => 1,
324 format => 'pve-lxc-mp-string',
325 format_description => 'volume',
326 description => 'Volume, device or directory to mount into the container.',
327 },
328 size => {
329 type => 'string',
330 format => 'disk-size',
331 format_description => 'DiskSize',
332 description => 'Volume size (read only value).',
333 optional => 1,
334 },
335 acl => {
336 type => 'boolean',
337 description => 'Explicitly enable or disable ACL support.',
338 optional => 1,
339 },
340 mountoptions => {
341 optional => 1,
342 type => 'string',
343 description => 'Extra mount options for rootfs/mps.',
344 format_description => 'opt[;opt...]',
345 pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
346 },
347 ro => {
348 type => 'boolean',
349 description => 'Read-only mount point',
350 optional => 1,
351 },
352 quota => {
353 type => 'boolean',
354 description => 'Enable user quotas inside the container (not supported with zfs subvolumes)',
355 optional => 1,
356 },
357 replicate => {
358 type => 'boolean',
359 description => 'Will include this volume to a storage replica job.',
360 optional => 1,
361 default => 1,
362 },
363 shared => {
364 type => 'boolean',
365 description => 'Mark this non-volume mount point as available on multiple nodes (see \'nodes\')',
366 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!",
367 optional => 1,
368 default => 0,
369 },
370 };
371
372 PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
373 type => 'string', format => $rootfs_desc,
374 description => "Use volume as container root.",
375 optional => 1,
376 });
377
378 # IP address with optional interface suffix for link local ipv6 addresses
379 PVE::JSONSchema::register_format('lxc-ip-with-ll-iface', \&verify_ip_with_ll_iface);
380 sub verify_ip_with_ll_iface {
381 my ($addr, $noerr) = @_;
382
383 if (my ($addr, $iface) = ($addr =~ /^(fe80:[^%]+)%(.*)$/)) {
384 if (PVE::JSONSchema::pve_verify_ip($addr, 1)
385 && PVE::JSONSchema::pve_verify_iface($iface, 1))
386 {
387 return $addr;
388 }
389 }
390
391 return PVE::JSONSchema::pve_verify_ip($addr, $noerr);
392 }
393
394
395 my $features_desc = {
396 mount => {
397 optional => 1,
398 type => 'string',
399 description => "Allow mounting file systems of specific types."
400 ." This should be a list of file system types as used with the mount command."
401 ." Note that this can have negative effects on the container's security."
402 ." With access to a loop device, mounting a file can circumvent the mknod"
403 ." permission of the devices cgroup, mounting an NFS file system can"
404 ." block the host's I/O completely and prevent it from rebooting, etc.",
405 format_description => 'fstype;fstype;...',
406 pattern => qr/[a-zA-Z0-9_; ]+/,
407 },
408 nesting => {
409 optional => 1,
410 type => 'boolean',
411 default => 0,
412 description => "Allow nesting."
413 ." Best used with unprivileged containers with additional id mapping."
414 ." Note that this will expose procfs and sysfs contents of the host"
415 ." to the guest.",
416 },
417 keyctl => {
418 optional => 1,
419 type => 'boolean',
420 default => 0,
421 description => "For unprivileged containers only: Allow the use of the keyctl() system call."
422 ." This is required to use docker inside a container."
423 ." By default unprivileged containers will see this system call as non-existent."
424 ." This is mostly a workaround for systemd-networkd, as it will treat it as a fatal"
425 ." error when some keyctl() operations are denied by the kernel due to lacking permissions."
426 ." Essentially, you can choose between running systemd-networkd or docker.",
427 },
428 fuse => {
429 optional => 1,
430 type => 'boolean',
431 default => 0,
432 description => "Allow using 'fuse' file systems in a container."
433 ." Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.",
434 },
435 mknod => {
436 optional => 1,
437 type => 'boolean',
438 default => 0,
439 description => "Allow unprivileged containers to use mknod() to add certain device nodes."
440 ." This requires a kernel with seccomp trap to user space support (5.3 or newer)."
441 ." This is experimental.",
442 },
443 force_rw_sys => {
444 optional => 1,
445 type => 'boolean',
446 default => 0,
447 description => "Mount /sys in unprivileged containers as `rw` instead of `mixed`."
448 ." This can break networking under newer (>= v245) systemd-network use."
449 },
450 };
451
452 my $confdesc = {
453 lock => {
454 optional => 1,
455 type => 'string',
456 description => "Lock/unlock the container.",
457 enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
458 },
459 onboot => {
460 optional => 1,
461 type => 'boolean',
462 description => "Specifies whether a container will be started during system bootup.",
463 default => 0,
464 },
465 startup => get_standard_option('pve-startup-order'),
466 template => {
467 optional => 1,
468 type => 'boolean',
469 description => "Enable/disable Template.",
470 default => 0,
471 },
472 arch => {
473 optional => 1,
474 type => 'string',
475 enum => ['amd64', 'i386', 'arm64', 'armhf', 'riscv32', 'riscv64'],
476 description => "OS architecture type.",
477 default => 'amd64',
478 },
479 ostype => {
480 optional => 1,
481 type => 'string',
482 enum => [qw(debian devuan ubuntu centos fedora opensuse archlinux alpine gentoo nixos unmanaged)],
483 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.",
484 },
485 console => {
486 optional => 1,
487 type => 'boolean',
488 description => "Attach a console device (/dev/console) to the container.",
489 default => 1,
490 },
491 tty => {
492 optional => 1,
493 type => 'integer',
494 description => "Specify the number of tty available to the container",
495 minimum => 0,
496 maximum => 6,
497 default => 2,
498 },
499 cores => {
500 optional => 1,
501 type => 'integer',
502 description => "The number of cores assigned to the container. A container can use all available cores by default.",
503 minimum => 1,
504 maximum => 8192,
505 },
506 cpulimit => {
507 optional => 1,
508 type => 'number',
509 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.",
510 minimum => 0,
511 maximum => 8192,
512 default => 0,
513 },
514 cpuunits => {
515 optional => 1,
516 type => 'integer',
517 description => "CPU weight for a container, will be clamped to [1, 10000] in cgroup v2.",
518 verbose_description => "CPU weight for a container. Argument is used in the kernel fair "
519 ."scheduler. The larger the number is, the more CPU time this container gets. Number "
520 ."is relative to the weights of all the other running guests.",
521 minimum => 0,
522 maximum => 500000,
523 default => 'cgroup v1: 1024, cgroup v2: 100',
524 },
525 memory => {
526 optional => 1,
527 type => 'integer',
528 description => "Amount of RAM for the container in MB.",
529 minimum => 16,
530 default => 512,
531 },
532 swap => {
533 optional => 1,
534 type => 'integer',
535 description => "Amount of SWAP for the container in MB.",
536 minimum => 0,
537 default => 512,
538 },
539 hostname => {
540 optional => 1,
541 description => "Set a host name for the container.",
542 type => 'string', format => 'dns-name',
543 maxLength => 255,
544 },
545 description => {
546 optional => 1,
547 type => 'string',
548 description => "Description for the Container. Shown in the web-interface CT's summary."
549 ." This is saved as comment inside the configuration file.",
550 maxLength => 1024 * 8,
551 },
552 searchdomain => {
553 optional => 1,
554 type => 'string', format => 'dns-name-list',
555 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
556 },
557 nameserver => {
558 optional => 1,
559 type => 'string', format => 'lxc-ip-with-ll-iface-list',
560 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.",
561 },
562 timezone => {
563 optional => 1,
564 type => 'string', format => 'pve-ct-timezone',
565 description => "Time zone to use in the container. If option isn't set, then nothing will be done. Can be set to 'host' to match the host time zone, or an arbitrary time zone option from /usr/share/zoneinfo/zone.tab",
566 },
567 rootfs => get_standard_option('pve-ct-rootfs'),
568 parent => {
569 optional => 1,
570 type => 'string', format => 'pve-configid',
571 maxLength => 40,
572 description => "Parent snapshot name. This is used internally, and should not be modified.",
573 },
574 snaptime => {
575 optional => 1,
576 description => "Timestamp for snapshots.",
577 type => 'integer',
578 minimum => 0,
579 },
580 cmode => {
581 optional => 1,
582 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).",
583 type => 'string',
584 enum => ['shell', 'console', 'tty'],
585 default => 'tty',
586 },
587 protection => {
588 optional => 1,
589 type => 'boolean',
590 description => "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.",
591 default => 0,
592 },
593 unprivileged => {
594 optional => 1,
595 type => 'boolean',
596 description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
597 default => 0,
598 },
599 features => {
600 optional => 1,
601 type => 'string',
602 format => $features_desc,
603 description => "Allow containers access to advanced features.",
604 },
605 hookscript => {
606 optional => 1,
607 type => 'string',
608 format => 'pve-volume-id',
609 description => 'Script that will be exectued during various steps in the containers lifetime.',
610 },
611 tags => {
612 type => 'string', format => 'pve-tag-list',
613 description => 'Tags of the Container. This is only meta information.',
614 optional => 1,
615 },
616 debug => {
617 optional => 1,
618 type => 'boolean',
619 description => "Try to be more verbose. For now this only enables debug log-level on start.",
620 default => 0,
621 },
622 };
623
624 my $valid_lxc_conf_keys = {
625 'lxc.apparmor.profile' => 1,
626 'lxc.apparmor.allow_incomplete' => 1,
627 'lxc.apparmor.allow_nesting' => 1,
628 'lxc.apparmor.raw' => 1,
629 'lxc.selinux.context' => 1,
630 'lxc.include' => 1,
631 'lxc.arch' => 1,
632 'lxc.uts.name' => 1,
633 'lxc.signal.halt' => 1,
634 'lxc.signal.reboot' => 1,
635 'lxc.signal.stop' => 1,
636 'lxc.init.cmd' => 1,
637 'lxc.pty.max' => 1,
638 'lxc.console.logfile' => 1,
639 'lxc.console.path' => 1,
640 'lxc.tty.max' => 1,
641 'lxc.devtty.dir' => 1,
642 'lxc.hook.autodev' => 1,
643 'lxc.autodev' => 1,
644 'lxc.kmsg' => 1,
645 'lxc.mount.fstab' => 1,
646 'lxc.mount.entry' => 1,
647 'lxc.mount.auto' => 1,
648 'lxc.rootfs.path' => 'lxc.rootfs.path is auto generated from rootfs',
649 'lxc.rootfs.mount' => 1,
650 'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
651 ', please use mount point options in the "rootfs" key',
652 # lxc.cgroup.*
653 # lxc.prlimit.*
654 # lxc.net.*
655 'lxc.cap.drop' => 1,
656 'lxc.cap.keep' => 1,
657 'lxc.seccomp.profile' => 1,
658 'lxc.seccomp.notify.proxy' => 1,
659 'lxc.seccomp.notify.cookie' => 1,
660 'lxc.idmap' => 1,
661 'lxc.hook.pre-start' => 1,
662 'lxc.hook.pre-mount' => 1,
663 'lxc.hook.mount' => 1,
664 'lxc.hook.start' => 1,
665 'lxc.hook.stop' => 1,
666 'lxc.hook.post-stop' => 1,
667 'lxc.hook.clone' => 1,
668 'lxc.hook.destroy' => 1,
669 'lxc.hook.version' => 1,
670 'lxc.log.level' => 1,
671 'lxc.log.file' => 1,
672 'lxc.start.auto' => 1,
673 'lxc.start.delay' => 1,
674 'lxc.start.order' => 1,
675 'lxc.group' => 1,
676 'lxc.environment' => 1,
677
678 # All these are namespaced via CLONE_NEWIPC (see namespaces(7)).
679 'lxc.sysctl.fs.mqueue' => 1,
680 'lxc.sysctl.kernel.msgmax' => 1,
681 'lxc.sysctl.kernel.msgmnb' => 1,
682 'lxc.sysctl.kernel.msgmni' => 1,
683 'lxc.sysctl.kernel.sem' => 1,
684 'lxc.sysctl.kernel.shmall' => 1,
685 'lxc.sysctl.kernel.shmmax' => 1,
686 'lxc.sysctl.kernel.shmmni' => 1,
687 'lxc.sysctl.kernel.shm_rmid_forced' => 1,
688 };
689
690 my $deprecated_lxc_conf_keys = {
691 # Deprecated (removed with lxc 3.0):
692 'lxc.aa_profile' => 'lxc.apparmor.profile',
693 'lxc.aa_allow_incomplete' => 'lxc.apparmor.allow_incomplete',
694 'lxc.console' => 'lxc.console.path',
695 'lxc.devttydir' => 'lxc.tty.dir',
696 'lxc.haltsignal' => 'lxc.signal.halt',
697 'lxc.rebootsignal' => 'lxc.signal.reboot',
698 'lxc.stopsignal' => 'lxc.signal.stop',
699 'lxc.id_map' => 'lxc.idmap',
700 'lxc.init_cmd' => 'lxc.init.cmd',
701 'lxc.loglevel' => 'lxc.log.level',
702 'lxc.logfile' => 'lxc.log.file',
703 'lxc.mount' => 'lxc.mount.fstab',
704 'lxc.network.type' => 'lxc.net.INDEX.type',
705 'lxc.network.flags' => 'lxc.net.INDEX.flags',
706 'lxc.network.link' => 'lxc.net.INDEX.link',
707 'lxc.network.mtu' => 'lxc.net.INDEX.mtu',
708 'lxc.network.name' => 'lxc.net.INDEX.name',
709 'lxc.network.hwaddr' => 'lxc.net.INDEX.hwaddr',
710 'lxc.network.ipv4' => 'lxc.net.INDEX.ipv4.address',
711 'lxc.network.ipv4.gateway' => 'lxc.net.INDEX.ipv4.gateway',
712 'lxc.network.ipv6' => 'lxc.net.INDEX.ipv6.address',
713 'lxc.network.ipv6.gateway' => 'lxc.net.INDEX.ipv6.gateway',
714 'lxc.network.script.up' => 'lxc.net.INDEX.script.up',
715 'lxc.network.script.down' => 'lxc.net.INDEX.script.down',
716 'lxc.pts' => 'lxc.pty.max',
717 'lxc.se_context' => 'lxc.selinux.context',
718 'lxc.seccomp' => 'lxc.seccomp.profile',
719 'lxc.tty' => 'lxc.tty.max',
720 'lxc.utsname' => 'lxc.uts.name',
721 };
722
723 sub is_valid_lxc_conf_key {
724 my ($vmid, $key) = @_;
725 if ($key =~ /^lxc\.limit\./) {
726 warn "vm $vmid - $key: lxc.limit.* was renamed to lxc.prlimit.*\n";
727 return 1;
728 }
729 if (defined(my $new_name = $deprecated_lxc_conf_keys->{$key})) {
730 warn "vm $vmid - $key is deprecated and was renamed to $new_name\n";
731 return 1;
732 }
733 my $validity = $valid_lxc_conf_keys->{$key};
734 return $validity if defined($validity);
735 return 1 if $key =~ /^lxc\.cgroup2?\./ # allow all cgroup values
736 || $key =~ /^lxc\.prlimit\./ # allow all prlimits
737 || $key =~ /^lxc\.net\./; # allow custom network definitions
738 return 0;
739 }
740
741 our $netconf_desc = {
742 type => {
743 type => 'string',
744 optional => 1,
745 description => "Network interface type.",
746 enum => [qw(veth)],
747 },
748 name => {
749 type => 'string',
750 format_description => 'string',
751 description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
752 pattern => '[-_.\w\d]+',
753 },
754 bridge => {
755 type => 'string',
756 format_description => 'bridge',
757 description => 'Bridge to attach the network device to.',
758 pattern => '[-_.\w\d]+',
759 optional => 1,
760 },
761 hwaddr => get_standard_option('mac-addr', {
762 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)',
763 }),
764 mtu => {
765 type => 'integer',
766 description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
767 minimum => 64, # minimum ethernet frame is 64 bytes
768 maximum => 65535,
769 optional => 1,
770 },
771 ip => {
772 type => 'string',
773 format => 'pve-ipv4-config',
774 format_description => '(IPv4/CIDR|dhcp|manual)',
775 description => 'IPv4 address in CIDR format.',
776 optional => 1,
777 },
778 gw => {
779 type => 'string',
780 format => 'ipv4',
781 format_description => 'GatewayIPv4',
782 description => 'Default gateway for IPv4 traffic.',
783 optional => 1,
784 },
785 ip6 => {
786 type => 'string',
787 format => 'pve-ipv6-config',
788 format_description => '(IPv6/CIDR|auto|dhcp|manual)',
789 description => 'IPv6 address in CIDR format.',
790 optional => 1,
791 },
792 gw6 => {
793 type => 'string',
794 format => 'ipv6',
795 format_description => 'GatewayIPv6',
796 description => 'Default gateway for IPv6 traffic.',
797 optional => 1,
798 },
799 firewall => {
800 type => 'boolean',
801 description => "Controls whether this interface's firewall rules should be used.",
802 optional => 1,
803 },
804 tag => {
805 type => 'integer',
806 minimum => 1,
807 maximum => 4094,
808 description => "VLAN tag for this interface.",
809 optional => 1,
810 },
811 trunks => {
812 type => 'string',
813 pattern => qr/\d+(?:;\d+)*/,
814 format_description => 'vlanid[;vlanid...]',
815 description => "VLAN ids to pass through the interface",
816 optional => 1,
817 },
818 rate => {
819 type => 'number',
820 format_description => 'mbps',
821 description => "Apply rate limiting to the interface",
822 optional => 1,
823 },
824 # TODO: Rename this option and the qemu-server one to `link-down` for PVE 8.0
825 link_down => {
826 type => 'boolean',
827 description => 'Whether this interface should be disconnected (like pulling the plug).',
828 optional => 1,
829 },
830 };
831 PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
832
833 my $MAX_LXC_NETWORKS = 32;
834 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
835 $confdesc->{"net$i"} = {
836 optional => 1,
837 type => 'string', format => $netconf_desc,
838 description => "Specifies network interfaces for the container.",
839 };
840 }
841
842 PVE::JSONSchema::register_format('pve-ct-timezone', \&verify_ct_timezone);
843 sub verify_ct_timezone {
844 my ($timezone, $noerr) = @_;
845
846 return if $timezone eq 'host'; # using host settings
847
848 PVE::JSONSchema::pve_verify_timezone($timezone);
849 }
850
851 PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
852 sub verify_lxc_mp_string {
853 my ($mp, $noerr) = @_;
854
855 # do not allow:
856 # /./ or /../
857 # /. or /.. at the end
858 # ../ at the beginning
859
860 if($mp =~ m@/\.\.?/@ ||
861 $mp =~ m@/\.\.?$@ ||
862 $mp =~ m@^\.\./@) {
863 return undef if $noerr;
864 die "$mp contains illegal character sequences\n";
865 }
866 return $mp;
867 }
868
869 my $mp_desc = {
870 %$rootfs_desc,
871 backup => {
872 type => 'boolean',
873 description => 'Whether to include the mount point in backups.',
874 verbose_description => 'Whether to include the mount point in backups '.
875 '(only used for volume mount points).',
876 optional => 1,
877 },
878 mp => {
879 type => 'string',
880 format => 'pve-lxc-mp-string',
881 format_description => 'Path',
882 description => 'Path to the mount point as seen from inside the container '.
883 '(must not contain symlinks).',
884 verbose_description => "Path to the mount point as seen from inside the container.\n\n".
885 "NOTE: Must not contain any symlinks for security reasons."
886 },
887 };
888 PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
889
890 my $unused_desc = {
891 volume => {
892 type => 'string',
893 default_key => 1,
894 format => 'pve-volume-id',
895 format_description => 'volume',
896 description => 'The volume that is not used currently.',
897 }
898 };
899
900 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
901 $confdesc->{"mp$i"} = {
902 optional => 1,
903 type => 'string', format => $mp_desc,
904 description => "Use volume as container mount point. Use the special " .
905 "syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.",
906 };
907 }
908
909 for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
910 $confdesc->{"unused$i"} = {
911 optional => 1,
912 type => 'string', format => $unused_desc,
913 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
914 }
915 }
916
917 PVE::JSONSchema::register_format('pve-lxc-dev-string', \&verify_lxc_dev_string);
918 sub verify_lxc_dev_string {
919 my ($dev, $noerr) = @_;
920
921 # do not allow /./ or /../ or /.$ or /..$
922 # enforce /dev/ at the beginning
923
924 if (
925 $dev =~ m@/\.\.?(?:/|$)@ ||
926 $dev !~ m!^/dev/!
927 ) {
928 return undef if $noerr;
929 die "$dev is not a valid device path\n";
930 }
931
932 return $dev;
933 }
934
935 my $dev_desc = {
936 path => {
937 optional => 1,
938 type => 'string',
939 default_key => 1,
940 format => 'pve-lxc-dev-string',
941 format_description => 'Path',
942 description => 'Device to pass through to the container',
943 verbose_description => 'Path to the device to pass through to the container',
944 },
945 mode => {
946 optional => 1,
947 type => 'string',
948 pattern => '0[0-7]{3}',
949 format_description => 'Octal access mode',
950 description => 'Access mode to be set on the device node',
951 },
952 uid => {
953 optional => 1,
954 type => 'integer',
955 minimum => 0,
956 description => 'User ID to be assigned to the device node',
957 },
958 gid => {
959 optional => 1,
960 type => 'integer',
961 minimum => 0,
962 description => 'Group ID to be assigned to the device node',
963 },
964 };
965
966 for (my $i = 0; $i < $MAX_DEVICES; $i++) {
967 $confdesc->{"dev$i"} = {
968 optional => 1,
969 type => 'string', format => $dev_desc,
970 description => "Device to pass through to the container",
971 }
972 }
973
974 sub parse_pct_config {
975 my ($filename, $raw, $strict) = @_;
976
977 return undef if !defined($raw);
978
979 my $res = {
980 digest => Digest::SHA::sha1_hex($raw),
981 snapshots => {},
982 pending => {},
983 };
984
985 my $handle_error = sub {
986 my ($msg) = @_;
987
988 if ($strict) {
989 die $msg;
990 } else {
991 warn $msg;
992 }
993 };
994
995 $filename =~ m|/lxc/(\d+).conf$|
996 || die "got strange filename '$filename'";
997
998 my $vmid = $1;
999
1000 my $conf = $res;
1001 my $descr = '';
1002 my $section = '';
1003
1004 my @lines = split(/\n/, $raw);
1005 foreach my $line (@lines) {
1006 next if $line =~ m/^\s*$/;
1007
1008 if ($line =~ m/^\[pve:pending\]\s*$/i) {
1009 $section = 'pending';
1010 $conf->{description} = $descr if $descr;
1011 $descr = '';
1012 $conf = $res->{$section} = {};
1013 next;
1014 } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
1015 $section = $1;
1016 $conf->{description} = $descr if $descr;
1017 $descr = '';
1018 $conf = $res->{snapshots}->{$section} = {};
1019 next;
1020 }
1021
1022 if ($line =~ m/^\#(.*)$/) {
1023 $descr .= PVE::Tools::decode_text($1) . "\n";
1024 next;
1025 }
1026
1027 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
1028 my $key = $1;
1029 my $value = $3;
1030 my $validity = is_valid_lxc_conf_key($vmid, $key);
1031 if ($validity eq 1) {
1032 push @{$conf->{lxc}}, [$key, $value];
1033 } elsif (my $errmsg = $validity) {
1034 $handle_error->("vm $vmid - $key: $errmsg\n");
1035 } else {
1036 $handle_error->("vm $vmid - unable to parse config: $line\n");
1037 }
1038 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
1039 $descr .= PVE::Tools::decode_text($2);
1040 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
1041 $conf->{snapstate} = $1;
1042 } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
1043 my $value = $1;
1044 if ($section eq 'pending') {
1045 $conf->{delete} = $value;
1046 } else {
1047 $handle_error->("vm $vmid - property 'delete' is only allowed in [pve:pending]\n");
1048 }
1049 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
1050 my $key = $1;
1051 my $value = $2;
1052 eval { $value = PVE::LXC::Config->check_type($key, $value); };
1053 $handle_error->("vm $vmid - unable to parse value of '$key' - $@") if $@;
1054 $conf->{$key} = $value;
1055 } else {
1056 $handle_error->("vm $vmid - unable to parse config: $line\n");
1057 }
1058 }
1059
1060 $conf->{description} = $descr if $descr;
1061
1062 delete $res->{snapstate}; # just to be sure
1063
1064 return $res;
1065 }
1066
1067 sub write_pct_config {
1068 my ($filename, $conf) = @_;
1069
1070 delete $conf->{snapstate}; # just to be sure
1071
1072 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
1073 my $used_volids = {};
1074 foreach my $vid (@$volidlist) {
1075 $used_volids->{$vid} = 1;
1076 }
1077
1078 # remove 'unusedX' settings if the volume is still used
1079 foreach my $key (keys %$conf) {
1080 my $value = $conf->{$key};
1081 if ($key =~ m/^unused/ && $used_volids->{$value}) {
1082 delete $conf->{$key};
1083 }
1084 }
1085
1086 my $generate_raw_config = sub {
1087 my ($conf) = @_;
1088
1089 my $raw = '';
1090
1091 # add description as comment to top of file
1092 my $descr = $conf->{description} || '';
1093 foreach my $cl (split(/\n/, $descr)) {
1094 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
1095 }
1096
1097 foreach my $key (sort keys %$conf) {
1098 next if $key eq 'digest' || $key eq 'description' ||
1099 $key eq 'pending' || $key eq 'snapshots' ||
1100 $key eq 'snapname' || $key eq 'lxc';
1101 my $value = $conf->{$key};
1102 die "detected invalid newline inside property '$key'\n"
1103 if $value =~ m/\n/;
1104 $raw .= "$key: $value\n";
1105 }
1106
1107 if (my $lxcconf = $conf->{lxc}) {
1108 foreach my $entry (@$lxcconf) {
1109 my ($k, $v) = @$entry;
1110 $raw .= "$k: $v\n";
1111 }
1112 }
1113
1114 return $raw;
1115 };
1116
1117 my $raw = &$generate_raw_config($conf);
1118
1119 if (scalar(keys %{$conf->{pending}})){
1120 $raw .= "\n[pve:pending]\n";
1121 $raw .= &$generate_raw_config($conf->{pending});
1122 }
1123
1124 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
1125 $raw .= "\n[$snapname]\n";
1126 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
1127 }
1128
1129 return $raw;
1130 }
1131
1132 sub update_pct_config {
1133 my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
1134
1135 my $storage_cfg = PVE::Storage::config();
1136
1137 foreach my $opt (@$revert) {
1138 delete $conf->{pending}->{$opt};
1139 $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
1140 }
1141
1142 # write updates to pending section
1143 my $modified = {}; # record modified options
1144
1145 foreach my $opt (@$delete) {
1146 if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
1147 warn "cannot delete '$opt' - not set in current configuration!\n";
1148 next;
1149 }
1150 $modified->{$opt} = 1;
1151 if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
1152 die "unable to delete required option '$opt'\n";
1153 } elsif ($opt =~ m/^unused(\d+)$/) {
1154 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
1155 } elsif ($opt =~ m/^mp(\d+)$/) {
1156 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
1157 } elsif ($opt eq 'unprivileged') {
1158 die "unable to delete read-only option: '$opt'\n";
1159 }
1160 $class->add_to_pending_delete($conf, $opt);
1161 }
1162
1163 my $check_content_type = sub {
1164 my ($mp) = @_;
1165 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
1166 my $storage_config = PVE::Storage::storage_config($storage_cfg, $sid);
1167 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
1168 if !$storage_config->{content}->{rootdir};
1169 };
1170
1171 foreach my $opt (sort keys %$param) { # add/change
1172 $modified->{$opt} = 1;
1173 my $value = $param->{$opt};
1174 if ($opt =~ m/^mp(\d+)$/ || $opt eq 'rootfs') {
1175 $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
1176 my $mp = $class->parse_volume($opt, $value);
1177 $check_content_type->($mp) if ($mp->{type} eq 'volume');
1178 } elsif ($opt eq 'hookscript') {
1179 PVE::GuestHelpers::check_hookscript($value);
1180 } elsif ($opt eq 'nameserver') {
1181 $value = PVE::LXC::verify_nameserver_list($value);
1182 } elsif ($opt eq 'searchdomain') {
1183 $value = PVE::LXC::verify_searchdomain_list($value);
1184 } elsif ($opt eq 'unprivileged') {
1185 die "unable to modify read-only option: '$opt'\n";
1186 } elsif ($opt eq 'tags') {
1187 $value = PVE::GuestHelpers::get_unique_tags($value);
1188 } elsif ($opt =~ m/^net(\d+)$/) {
1189 my $res = PVE::JSONSchema::parse_property_string($netconf_desc, $value);
1190
1191 if (my $mtu = $res->{mtu}) {
1192 my $bridge_mtu = PVE::Network::read_bridge_mtu($res->{bridge});
1193 die "$opt: MTU size '$mtu' is bigger than bridge MTU '$bridge_mtu'\n"
1194 if ($mtu > $bridge_mtu);
1195 }
1196 }
1197 $conf->{pending}->{$opt} = $value;
1198 $class->remove_from_pending_delete($conf, $opt);
1199 }
1200
1201 my $changes = $class->cleanup_pending($conf);
1202
1203 my $errors = {};
1204 if ($running) {
1205 $class->vmconfig_hotplug_pending($vmid, $conf, $storage_cfg, $modified, $errors);
1206 } else {
1207 $class->vmconfig_apply_pending($vmid, $conf, $storage_cfg, $modified, $errors);
1208 }
1209
1210 return $errors;
1211 }
1212
1213 sub check_type {
1214 my ($class, $key, $value) = @_;
1215
1216 die "unknown setting '$key'\n" if !$confdesc->{$key};
1217
1218 my $type = $confdesc->{$key}->{type};
1219
1220 if (!defined($value)) {
1221 die "got undefined value\n";
1222 }
1223
1224 if ($value =~ m/[\n\r]/) {
1225 die "property contains a line feed\n";
1226 }
1227
1228 if ($type eq 'boolean') {
1229 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
1230 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
1231 die "type check ('boolean') failed - got '$value'\n";
1232 } elsif ($type eq 'integer') {
1233 return int($1) if $value =~ m/^(\d+)$/;
1234 die "type check ('integer') failed - got '$value'\n";
1235 } elsif ($type eq 'number') {
1236 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
1237 die "type check ('number') failed - got '$value'\n";
1238 } elsif ($type eq 'string') {
1239 if (my $fmt = $confdesc->{$key}->{format}) {
1240 PVE::JSONSchema::check_format($fmt, $value);
1241 return $value;
1242 }
1243 return $value;
1244 } else {
1245 die "internal error"
1246 }
1247 }
1248
1249
1250 # add JSON properties for create and set function
1251 sub json_config_properties {
1252 my ($class, $prop) = @_;
1253
1254 foreach my $opt (keys %$confdesc) {
1255 next if $opt eq 'parent' || $opt eq 'snaptime';
1256 next if $prop->{$opt};
1257 $prop->{$opt} = $confdesc->{$opt};
1258 }
1259
1260 return $prop;
1261 }
1262
1263 my $parse_ct_mountpoint_full = sub {
1264 my ($class, $desc, $data, $noerr) = @_;
1265
1266 $data //= '';
1267
1268 my $res;
1269 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
1270 if ($@) {
1271 return undef if $noerr;
1272 die $@;
1273 }
1274
1275 if (defined(my $size = $res->{size})) {
1276 $size = PVE::JSONSchema::parse_size($size);
1277 if (!defined($size)) {
1278 return undef if $noerr;
1279 die "invalid size: $size\n";
1280 }
1281 $res->{size} = $size;
1282 }
1283
1284 $res->{type} = $class->classify_mountpoint($res->{volume});
1285
1286 return $res;
1287 };
1288
1289 sub print_ct_mountpoint {
1290 my ($class, $info, $nomp) = @_;
1291 my $skip = [ 'type' ];
1292 push @$skip, 'mp' if $nomp;
1293 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1294 }
1295
1296 sub print_ct_unused {
1297 my ($class, $info) = @_;
1298
1299 my $skip = [ 'type' ];
1300 return PVE::JSONSchema::print_property_string($info, $unused_desc, $skip);
1301 }
1302
1303 sub parse_volume {
1304 my ($class, $key, $volume_string, $noerr) = @_;
1305
1306 if ($key eq 'rootfs') {
1307 my $res = $parse_ct_mountpoint_full->($class, $rootfs_desc, $volume_string, $noerr);
1308 $res->{mp} = '/' if defined($res);
1309 return $res;
1310 } elsif ($key =~ m/^mp\d+$/) {
1311 return $parse_ct_mountpoint_full->($class, $mp_desc, $volume_string, $noerr);
1312 } elsif ($key =~ m/^unused\d+$/) {
1313 return $parse_ct_mountpoint_full->($class, $unused_desc, $volume_string, $noerr);
1314 }
1315
1316 die "parse_volume - unknown type: $key\n" if !$noerr;
1317
1318 return;
1319 }
1320
1321 sub parse_device {
1322 my ($class, $device_string, $noerr) = @_;
1323
1324 my $res = eval { PVE::JSONSchema::parse_property_string($dev_desc, $device_string) };
1325 if ($@) {
1326 return undef if $noerr;
1327 die $@;
1328 }
1329
1330 if (!defined($res->{path})) {
1331 return undef if $noerr;
1332 die "Path has to be defined\n";
1333 }
1334
1335 return $res;
1336 }
1337
1338 sub print_volume {
1339 my ($class, $key, $volume) = @_;
1340
1341 return $class->print_ct_unused($volume) if $key =~ m/^unused(\d+)$/;
1342
1343 return $class->print_ct_mountpoint($volume, $key eq 'rootfs');
1344 }
1345
1346 sub volid_key {
1347 my ($class) = @_;
1348
1349 return 'volume';
1350 }
1351
1352 sub print_lxc_network {
1353 my ($class, $net) = @_;
1354 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1355 }
1356
1357 sub parse_lxc_network {
1358 my ($class, $data) = @_;
1359
1360 return {} if !$data;
1361
1362 my $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1363
1364 $res->{type} = 'veth';
1365 if (!$res->{hwaddr}) {
1366 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1367 $res->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1368 }
1369
1370 return $res;
1371 }
1372
1373 sub parse_features {
1374 my ($class, $data) = @_;
1375 return {} if !$data;
1376 return PVE::JSONSchema::parse_property_string($features_desc, $data);
1377 }
1378
1379 sub option_exists {
1380 my ($class, $name) = @_;
1381
1382 return defined($confdesc->{$name});
1383 }
1384 # END JSON config code
1385
1386 # takes a max memory value as KiB and returns an tuple with max and high values
1387 sub calculate_memory_constraints {
1388 my ($memory) = @_;
1389
1390 return if !defined($memory);
1391
1392 # cgroup memory usage is limited by the hard 'max' limit (OOM-killer enforced) and the soft
1393 # 'high' limit (cgroup processes get throttled and put under heavy reclaim pressure).
1394 my $memory_max = int($memory * 1024 * 1024);
1395 # Set the high to 1016/1024 (~99.2%) of the 'max' hard limit clamped to 128 MiB max, to scale
1396 # it for the lower range while having a decent 2^x based rest for 2^y memory configs.
1397 my $memory_high = $memory >= 16 * 1024 ? int(($memory - 128) * 1024 * 1024) : int($memory * 1024 * 1016);
1398
1399 return ($memory_max, $memory_high);
1400 }
1401
1402 my $LXC_FASTPLUG_OPTIONS= {
1403 'description' => 1,
1404 'onboot' => 1,
1405 'startup' => 1,
1406 'protection' => 1,
1407 'hostname' => 1,
1408 'hookscript' => 1,
1409 'cores' => 1,
1410 'tags' => 1,
1411 'lock' => 1,
1412 };
1413
1414 sub vmconfig_hotplug_pending {
1415 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1416
1417 my $pid = PVE::LXC::find_lxc_pid($vmid);
1418 my $rootdir = "/proc/$pid/root";
1419
1420 my $add_hotplug_error = sub {
1421 my ($opt, $msg) = @_;
1422 $errors->{$opt} = "unable to hotplug $opt: $msg";
1423 };
1424
1425 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
1426 next if $selection && !$selection->{$opt};
1427 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1428 $conf->{$opt} = delete $conf->{pending}->{$opt};
1429 }
1430 }
1431
1432 my $cgroup = PVE::LXC::CGroup->new($vmid);
1433
1434 # There's no separate swap size to configure, there's memory and "total"
1435 # memory (iow. memory+swap). This means we have to change them together.
1436 my $hotplug_memory_done;
1437 my $hotplug_memory = sub {
1438 my ($new_memory, $new_swap) = @_;
1439
1440 ($new_memory, my $new_memory_high) = calculate_memory_constraints($new_memory);
1441 $new_swap = int($new_swap * 1024 * 1024) if defined($new_swap);
1442 $cgroup->change_memory_limit($new_memory, $new_swap, $new_memory_high);
1443
1444 $hotplug_memory_done = 1;
1445 };
1446
1447 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1448 # FIXME: $force deletion is not implemented for CTs
1449 foreach my $opt (sort keys %$pending_delete_hash) {
1450 next if $selection && !$selection->{$opt};
1451 eval {
1452 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1453 # pass
1454 } elsif ($opt =~ m/^unused(\d+)$/) {
1455 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1456 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1457 } elsif ($opt eq 'swap') {
1458 $hotplug_memory->(undef, 0);
1459 } elsif ($opt eq 'cpulimit') {
1460 $cgroup->change_cpu_quota(undef, undef); # reset, cgroup module can better decide values
1461 } elsif ($opt eq 'cpuunits') {
1462 $cgroup->change_cpu_shares(undef);
1463 } elsif ($opt =~ m/^net(\d)$/) {
1464 my $netid = $1;
1465 PVE::Network::veth_delete("veth${vmid}i$netid");
1466 if ($have_sdn) {
1467 my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
1468 print "delete ips from $opt\n";
1469 eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
1470 warn $@ if $@;
1471 }
1472 } else {
1473 die "skip\n"; # skip non-hotpluggable opts
1474 }
1475 };
1476 if (my $err = $@) {
1477 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1478 } else {
1479 delete $conf->{$opt};
1480 $class->remove_from_pending_delete($conf, $opt);
1481 }
1482 }
1483
1484 foreach my $opt (sort keys %{$conf->{pending}}) {
1485 next if $opt eq 'delete'; # just to be sure
1486 next if $selection && !$selection->{$opt};
1487 my $value = $conf->{pending}->{$opt};
1488 eval {
1489 if ($opt eq 'cpulimit') {
1490 my $quota = 100000 * $value;
1491 $cgroup->change_cpu_quota(int(100000 * $value), 100000);
1492 } elsif ($opt eq 'cpuunits') {
1493 $cgroup->change_cpu_shares($value);
1494 } elsif ($opt =~ m/^net(\d+)$/) {
1495 my $netid = $1;
1496 my $net = $class->parse_lxc_network($value);
1497 $value = $class->print_lxc_network($net);
1498 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
1499 } elsif ($opt eq 'memory' || $opt eq 'swap') {
1500 if (!$hotplug_memory_done) { # don't call twice if both opts are passed
1501 $hotplug_memory->($conf->{pending}->{memory}, $conf->{pending}->{swap});
1502 }
1503 } elsif ($opt =~ m/^mp(\d+)$/) {
1504 if (exists($conf->{$opt})) {
1505 die "skip\n"; # don't try to hotplug over existing mp
1506 }
1507
1508 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 1);
1509 # apply_pending_mountpoint modifies the value if it creates a new disk
1510 $value = $conf->{pending}->{$opt};
1511 } else {
1512 die "skip\n"; # skip non-hotpluggable
1513 }
1514 };
1515 if (my $err = $@) {
1516 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1517 } else {
1518 $conf->{$opt} = $value;
1519 delete $conf->{pending}->{$opt};
1520 }
1521 }
1522 }
1523
1524 sub vmconfig_apply_pending {
1525 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1526
1527 my $add_apply_error = sub {
1528 my ($opt, $msg) = @_;
1529 my $err_msg = "unable to apply pending change $opt : $msg";
1530 $errors->{$opt} = $err_msg;
1531 warn $err_msg;
1532 };
1533
1534 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1535 # FIXME: $force deletion is not implemented for CTs
1536 foreach my $opt (sort keys %$pending_delete_hash) {
1537 next if $selection && !$selection->{$opt};
1538 eval {
1539 if ($opt =~ m/^mp(\d+)$/) {
1540 my $mp = $class->parse_volume($opt, $conf->{$opt});
1541 if ($mp->{type} eq 'volume') {
1542 $class->add_unused_volume($conf, $mp->{volume})
1543 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1544 }
1545 } elsif ($opt =~ m/^unused(\d+)$/) {
1546 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1547 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1548 } elsif ($opt =~ m/^net(\d+)$/) {
1549 if ($have_sdn) {
1550 my $net = $class->parse_lxc_network($conf->{$opt});
1551 eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
1552 warn $@ if $@;
1553 }
1554 }
1555 };
1556 if (my $err = $@) {
1557 $add_apply_error->($opt, $err);
1558 } else {
1559 delete $conf->{$opt};
1560 $class->remove_from_pending_delete($conf, $opt);
1561 }
1562 }
1563
1564 $class->cleanup_pending($conf);
1565
1566 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
1567 next if $opt eq 'delete'; # just to be sure
1568 next if $selection && !$selection->{$opt};
1569 eval {
1570 if ($opt =~ m/^mp(\d+)$/) {
1571 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 0);
1572 } elsif ($opt =~ m/^net(\d+)$/) {
1573 my $netid = $1;
1574 my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
1575 $conf->{pending}->{$opt} = $class->print_lxc_network($net);
1576 if ($have_sdn) {
1577 if ($conf->{$opt}) {
1578 my $old_net = $class->parse_lxc_network($conf->{$opt});
1579 if ($old_net->{bridge} ne $net->{bridge} || $old_net->{hwaddr} ne $net->{hwaddr}) {
1580 PVE::Network::SDN::Vnets::del_ips_from_mac($old_net->{bridge}, $old_net->{hwaddr}, $conf->{name});
1581 PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
1582 }
1583 } else {
1584 PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
1585 }
1586 }
1587 }
1588 };
1589 if (my $err = $@) {
1590 $add_apply_error->($opt, $err);
1591 } else {
1592 $conf->{$opt} = delete $conf->{pending}->{$opt};
1593 }
1594 }
1595 }
1596
1597 my $rescan_volume = sub {
1598 my ($storecfg, $mp) = @_;
1599 eval {
1600 $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5);
1601 };
1602 warn "Could not rescan volume size - $@\n" if $@;
1603 };
1604
1605 sub apply_pending_mountpoint {
1606 my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
1607
1608 my $mp = $class->parse_volume($opt, $conf->{pending}->{$opt});
1609 my $old = $conf->{$opt};
1610 if ($mp->{type} eq 'volume' && $mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
1611 my $original_value = $conf->{pending}->{$opt};
1612 my $vollist = PVE::LXC::create_disks(
1613 $storecfg,
1614 $vmid,
1615 { $opt => $original_value },
1616 $conf,
1617 1,
1618 );
1619 if ($running) {
1620 # Re-parse mount point:
1621 my $mp = $class->parse_volume($opt, $conf->{pending}->{$opt});
1622 eval {
1623 PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
1624 };
1625 my $err = $@;
1626 if ($err) {
1627 PVE::LXC::destroy_disks($storecfg, $vollist);
1628 # The pending-changes code collects errors but keeps on looping through further
1629 # pending changes, so unroll the change in $conf as well if destroy_disks()
1630 # didn't die().
1631 $conf->{pending}->{$opt} = $original_value;
1632 die $err;
1633 }
1634 }
1635 } else {
1636 die "skip\n" if $running && defined($old); # TODO: "changing" mount points?
1637 $rescan_volume->($storecfg, $mp) if $mp->{type} eq 'volume';
1638 if ($running) {
1639 PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
1640 }
1641 $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
1642 }
1643
1644 if (defined($old)) {
1645 my $mp = $class->parse_volume($opt, $old);
1646 if ($mp->{type} eq 'volume') {
1647 $class->add_unused_volume($conf, $mp->{volume})
1648 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1649 }
1650 }
1651 }
1652
1653 sub classify_mountpoint {
1654 my ($class, $vol) = @_;
1655 if ($vol =~ m!^/!) {
1656 return 'device' if $vol =~ m!^/dev/!;
1657 return 'bind';
1658 }
1659 return 'volume';
1660 }
1661
1662 my $__is_volume_in_use = sub {
1663 my ($class, $config, $volid) = @_;
1664 my $used = 0;
1665
1666 $class->foreach_volume($config, sub {
1667 my ($ms, $mountpoint) = @_;
1668 return if $used;
1669 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1670 });
1671
1672 return $used;
1673 };
1674
1675 sub is_volume_in_use_by_snapshots {
1676 my ($class, $config, $volid) = @_;
1677
1678 if (my $snapshots = $config->{snapshots}) {
1679 foreach my $snap (keys %$snapshots) {
1680 return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
1681 }
1682 }
1683
1684 return 0;
1685 }
1686
1687 sub is_volume_in_use {
1688 my ($class, $config, $volid, $include_snapshots, $include_pending) = @_;
1689 return 1 if $__is_volume_in_use->($class, $config, $volid);
1690 return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
1691 return 1 if $include_pending && $__is_volume_in_use->($class, $config->{pending}, $volid);
1692 return 0;
1693 }
1694
1695 sub has_dev_console {
1696 my ($class, $conf) = @_;
1697
1698 return !(defined($conf->{console}) && !$conf->{console});
1699 }
1700
1701 sub has_lxc_entry {
1702 my ($class, $conf, $keyname) = @_;
1703
1704 if (my $lxcconf = $conf->{lxc}) {
1705 foreach my $entry (@$lxcconf) {
1706 my ($key, undef) = @$entry;
1707 return 1 if $key eq $keyname;
1708 }
1709 }
1710
1711 return 0;
1712 }
1713
1714 sub get_tty_count {
1715 my ($class, $conf) = @_;
1716
1717 return $conf->{tty} // $confdesc->{tty}->{default};
1718 }
1719
1720 sub get_cmode {
1721 my ($class, $conf) = @_;
1722
1723 return $conf->{cmode} // $confdesc->{cmode}->{default};
1724 }
1725
1726 sub valid_volume_keys {
1727 my ($class, $reverse) = @_;
1728
1729 my @names = ('rootfs');
1730
1731 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1732 push @names, "mp$i";
1733 }
1734
1735 return $reverse ? reverse @names : @names;
1736 }
1737
1738 sub valid_volume_keys_with_unused {
1739 my ($class, $reverse) = @_;
1740 my @names = $class->valid_volume_keys();
1741 for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
1742 push @names, "unused$i";
1743 }
1744 return $reverse ? reverse @names : @names;
1745 }
1746
1747 sub get_vm_volumes {
1748 my ($class, $conf, $excludes) = @_;
1749
1750 my $vollist = [];
1751
1752 $class->foreach_volume($conf, sub {
1753 my ($ms, $mountpoint) = @_;
1754
1755 return if $excludes && $ms eq $excludes;
1756
1757 my $volid = $mountpoint->{volume};
1758 return if !$volid || $mountpoint->{type} ne 'volume';
1759
1760 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1761 return if !$sid;
1762
1763 push @$vollist, $volid;
1764 });
1765
1766 return $vollist;
1767 }
1768
1769 sub get_replicatable_volumes {
1770 my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
1771
1772 my $volhash = {};
1773
1774 my $test_volid = sub {
1775 my ($volid, $mountpoint) = @_;
1776
1777 return if !$volid;
1778
1779 my $mptype = $mountpoint->{type};
1780 my $replicate = $mountpoint->{replicate} // 1;
1781
1782 if ($mptype ne 'volume') {
1783 # skip bindmounts if replicate = 0 even for cleanup,
1784 # since bind mounts could not have been replicated ever
1785 return if !$replicate;
1786 die "unable to replicate mountpoint type '$mptype'\n";
1787 }
1788
1789 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
1790 return if !$storeid;
1791
1792 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1793 return if $scfg->{shared};
1794
1795 my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
1796 return if !$owner || ($owner != $vmid);
1797
1798 die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
1799
1800 return if !$cleanup && !$replicate;
1801
1802 if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
1803 return if $cleanup || $noerr;
1804 die "missing replicate feature on volume '$volid'\n";
1805 }
1806
1807 $volhash->{$volid} = 1;
1808 };
1809
1810 $class->foreach_volume($conf, sub {
1811 my ($ms, $mountpoint) = @_;
1812 $test_volid->($mountpoint->{volume}, $mountpoint);
1813 });
1814
1815 foreach my $snapname (keys %{$conf->{snapshots}}) {
1816 my $snap = $conf->{snapshots}->{$snapname};
1817 $class->foreach_volume($snap, sub {
1818 my ($ms, $mountpoint) = @_;
1819 $test_volid->($mountpoint->{volume}, $mountpoint);
1820 });
1821 }
1822
1823 # add 'unusedX' volumes to volhash
1824 foreach my $key (keys %$conf) {
1825 if ($key =~ m/^unused/) {
1826 $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
1827 }
1828 }
1829
1830 return $volhash;
1831 }
1832
1833 sub get_backup_volumes {
1834 my ($class, $conf) = @_;
1835
1836 my $return_volumes = [];
1837
1838 my $test_mountpoint = sub {
1839 my ($key, $volume) = @_;
1840
1841 my ($included, $reason) = $class->mountpoint_backup_enabled($key, $volume);
1842
1843 push @$return_volumes, {
1844 key => $key,
1845 included => $included,
1846 reason => $reason,
1847 volume_config => $volume,
1848 };
1849 };
1850
1851 PVE::LXC::Config->foreach_volume($conf, $test_mountpoint);
1852
1853 return $return_volumes;
1854 }
1855
1856 sub get_derived_property {
1857 my ($class, $conf, $name) = @_;
1858
1859 if ($name eq 'max-cpu') {
1860 return $conf->{cpulimit} || $conf->{cores} || 0;
1861 } elsif ($name eq 'max-memory') {
1862 return ($conf->{memory} || 512) * 1024 * 1024;
1863 } else {
1864 die "unknown derived property - $name\n";
1865 }
1866 }
1867
1868 sub foreach_passthrough_device {
1869 my ($class, $conf, $func, @param) = @_;
1870
1871 for my $key (keys %$conf) {
1872 next if $key !~ m/^dev(\d+)$/;
1873
1874 my $device = $class->parse_device($conf->{$key});
1875
1876 $func->($key, $device, @param);
1877 }
1878 }
1879
1880 1;