Eisfair64 LXC Container für Proxmox

Ich mag eisfair, mit seiner leichten und unkomplizierten Konfiguration und habe mal probiert, ob man eisfair nicht auch als LXC-Container in Proxmox laufen lassen kann. Ein LXC-Container bietet gegenüber einer VM einige Vorteile, hat aber natürlich auch einige Nachteile.

Vorteile:

  • Nutzen denselben Kernel wie das Host-System, bieten aber isolierte Benutzerbereiche für Anwendungen.
  • Schneller einzurichten und verbrauchen weniger Ressourcen im Vergleich zu VMs.
  • Schnelleres Starten: Starten viel schneller als VMs, da kein eigener Kernel und Bootloader geladen wird
  • Einfachheit: Einfacher einzurichten und zu verwalten als VMs.

Nachteile:

  • Beschränkt auf Linux-Systeme
  • Schwierigere Integration von Speicherlösungen wie NFS, etc.

Ich wollte das aber einfach mal probieren um mir einen leichten Mail-, Web-, Nextcloud-, etc. Server zu installieren und weil ich einfach Bock drauf hatte. Das alles hier ist nicht in Stein gemeißelt, sondern eher ein work in progress aber soweit läuft es schonmal.

Also los gehts.

  1. Eisfair als VM istallieren
  2. DHCPC-Paket in der VM installieren
  3. RSYNC-Paket in der VM installieren
  4. SSH-Zugriff einrichten.

Jetzt müssen wir uns das Root-Dateisystem auf den Proxmox sichern.

rsync -aAXv --exclude={/dev,/proc,/sys,/tmp,/run,/mnt,/media,/lost+found,/boot,/var/log/*,/var/tmp/*,/usr/lib/firmware,/usr/lib/modules,/var/lib/alt-kernel,/var/lib/eisman/*.db,/var/lib/eisman/info/*,System.map*} root@<EISFAIR-IP>:/ /path/to/rootfs/

Jetzt haben wir das grundlegende Root-Dateisystem des eisfair und die VM wird eigentlich nicht mehr benötigt.

Damit später im LXC-Container die Funktionstasten ordnungsgemäß funktionieren einfach die folgenden zwei Befehle ausführen.

echo "export TERM='xterm-256color'" >> /path/to/rootfs/root/.bashrc
echo "[[ -f ~/.bashrc ]] && . ~/.bashrc" >> /path/to/rootfs/root/.bash_profile

Im Root-Dateisystem noch die folgenden Gerätedaten anlegen.

  • -console
  • -null
  • -tty

Das hier jetzt ohne Gewähr, ich habe mir die entsprechenden „Dateien“ aus einem anderen tar-Archiv kopiert. Ob das überhaupt notwendig ist und/oder ob das anders gemacht werden kann weiß ich noch nicht.

sudo mknod /path/to/rootfs/dev/console c 5 1
sudo mknod /path/to/rootfs/dev/null c 1 3
sudo mknod /path/to/rootfs/dev/tty c 5 0

Rechte setzen

sudo chmod 600 /path/to/rootfs/dev/console
sudo chmod 666 /path/to/rootfs/dev/null
sudo chmod 600 /path/to/rootfs/dev/tty

Jetzt ist das Eisfair root-fs soweit vorbereitet, dass wir es packen können

tar -czvf eisfair.tar.gz -C /path/to/rootfs .

Somit haben wir ein LXC-Template für den Proxmox erstellt. Dieses Template können wir jetzt z.B. nach /var/lib/vz/template/cache/ kopieren.

Der Einfachheit halber gib es ein fertiges Image hier.

ACHTUNG: die Passwörter der User „eis“, „halt“ und „reboot“ habe ich entweder gelöscht oder sie heißen eis, halt und reboot

Damit wir das Template auch installieren und auch die Netzwerkeinstellungen, etc. im Proxmox konfigurieren können müssen wir auch noch ein paar Dateien auf dem Proxmox-Host anlegen/ändern.

Auf dem Proxmox Host im Verzeichnis /usr/share/lcx/config/ die Datei eisfair.common.conf mit folgenden Inhalt anlegen.

/usr/share/lcx/config/eisfair.common.conf
# This derives from the global common config
lxc.include = /usr/share/lxc/config/common.conf
 
 
# If you wish to allow mounting block filesystems, then use the following
# line instead, and make sure to grant access to the block device and/or loop
# devices below in lxc.cgroup.devices.allow.
#lxc.apparmor.profile = lxc-container-default-with-mounting
 
# Extra cgroup device access
## rtc
lxc.cgroup.devices.allow = c 254:0 rm
## tun
lxc.cgroup.devices.allow = c 10:200 rwm
## hpet
lxc.cgroup.devices.allow = c 10:228 rwm
## kvm
lxc.cgroup.devices.allow = c 10:232 rwm

Und auch noch die eisfair.userns.conf

/usr/share/lxc/config/eisfair.userns.conf
# This derives from the global userns config
lxc.include = /usr/share/lxc/config/userns.conf

Sucht in der Datei /usr/share/perl5/PVE/LXC/Config.pm nach enum ⇒ [qw(debian devuan ubuntu centos fedora opensuse archlinux alpine gentoo nixos unmanaged)],

und ersetzt diese Zeile durch enum ⇒ [qw(debian devuan eisfair ubuntu centos fedora opensuse archlinux alpine gentoo nixos unmanaged)],

usr/share/perl5/PVE/LXC/Config.pm einblenden

ausblenden

Die entsprechenden Zeilen sind farblich hervorgehoben, (Zeile 483)

usr/share/perl5/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. use PVE::LXC::Tools;
  18.  
  19. use base qw(PVE::AbstractConfig);
  20.  
  21. use constant {
  22. FIFREEZE => 0xc0045877,
  23. FITHAW => 0xc0045878,
  24. };
  25.  
  26. my $have_sdn;
  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/(discard|lazytime|noatime|nodev|noexec|nosuid)/;
  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 eisfair 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 executed 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. 'deny-write' => {
  965. optional => 1,
  966. type => 'boolean',
  967. description => 'Deny the container to write to the device',
  968. default => 0,
  969. },
  970. };
  971.  
  972. for (my $i = 0; $i < $MAX_DEVICES; $i++) {
  973. $confdesc->{"dev$i"} = {
  974. optional => 1,
  975. type => 'string', format => $dev_desc,
  976. description => "Device to pass through to the container",
  977. }
  978. }
  979.  
  980. sub parse_pct_config {
  981. my ($filename, $raw, $strict) = @_;
  982.  
  983. return undef if !defined($raw);
  984.  
  985. my $res = {
  986. digest => Digest::SHA::sha1_hex($raw),
  987. snapshots => {},
  988. pending => {},
  989. };
  990.  
  991. my $handle_error = sub {
  992. my ($msg) = @_;
  993.  
  994. if ($strict) {
  995. die $msg;
  996. } else {
  997. warn $msg;
  998. }
  999. };
  1000.  
  1001. $filename =~ m|/lxc/(\d+).conf$|
  1002. || die "got strange filename '$filename'";
  1003.  
  1004. my $vmid = $1;
  1005.  
  1006. my $conf = $res;
  1007. my $descr = '';
  1008. my $section = '';
  1009.  
  1010. my @lines = split(/\n/, $raw);
  1011. foreach my $line (@lines) {
  1012. next if $line =~ m/^\s*$/;
  1013.  
  1014. if ($line =~ m/^\[pve:pending\]\s*$/i) {
  1015. $section = 'pending';
  1016. $conf->{description} = $descr if $descr;
  1017. $descr = '';
  1018. $conf = $res->{$section} = {};
  1019. next;
  1020. } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
  1021. $section = $1;
  1022. $conf->{description} = $descr if $descr;
  1023. $descr = '';
  1024. $conf = $res->{snapshots}->{$section} = {};
  1025. next;
  1026. }
  1027.  
  1028. if ($line =~ m/^\#(.*)$/) {
  1029. $descr .= PVE::Tools::decode_text($1) . "\n";
  1030. next;
  1031. }
  1032.  
  1033. if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
  1034. my $key = $1;
  1035. my $value = $3;
  1036. my $validity = is_valid_lxc_conf_key($vmid, $key);
  1037. if ($validity eq 1) {
  1038. push @{$conf->{lxc}}, [$key, $value];
  1039. } elsif (my $errmsg = $validity) {
  1040. $handle_error->("vm $vmid - $key: $errmsg\n");
  1041. } else {
  1042. $handle_error->("vm $vmid - unable to parse config: $line\n");
  1043. }
  1044. } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
  1045. $descr .= PVE::Tools::decode_text($2);
  1046. } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
  1047. $conf->{snapstate} = $1;
  1048. } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
  1049. my $value = $1;
  1050. if ($section eq 'pending') {
  1051. $conf->{delete} = $value;
  1052. } else {
  1053. $handle_error->("vm $vmid - property 'delete' is only allowed in [pve:pending]\n");
  1054. }
  1055. } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
  1056. my $key = $1;
  1057. my $value = $2;
  1058. eval { $value = PVE::LXC::Config->check_type($key, $value); };
  1059. $handle_error->("vm $vmid - unable to parse value of '$key' - $@") if $@;
  1060. $conf->{$key} = $value;
  1061. } else {
  1062. $handle_error->("vm $vmid - unable to parse config: $line\n");
  1063. }
  1064. }
  1065.  
  1066. $conf->{description} = $descr if $descr;
  1067.  
  1068. delete $res->{snapstate}; # just to be sure
  1069.  
  1070. return $res;
  1071. }
  1072.  
  1073. sub write_pct_config {
  1074. my ($filename, $conf) = @_;
  1075.  
  1076. delete $conf->{snapstate}; # just to be sure
  1077.  
  1078. my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
  1079. my $used_volids = {};
  1080. foreach my $vid (@$volidlist) {
  1081. $used_volids->{$vid} = 1;
  1082. }
  1083.  
  1084. # remove 'unusedX' settings if the volume is still used
  1085. foreach my $key (keys %$conf) {
  1086. my $value = $conf->{$key};
  1087. if ($key =~ m/^unused/ && $used_volids->{$value}) {
  1088. delete $conf->{$key};
  1089. }
  1090. }
  1091.  
  1092. my $generate_raw_config = sub {
  1093. my ($conf) = @_;
  1094.  
  1095. my $raw = '';
  1096.  
  1097. # add description as comment to top of file
  1098. my $descr = $conf->{description} || '';
  1099. foreach my $cl (split(/\n/, $descr)) {
  1100. $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
  1101. }
  1102.  
  1103. foreach my $key (sort keys %$conf) {
  1104. next if $key eq 'digest' || $key eq 'description' ||
  1105. $key eq 'pending' || $key eq 'snapshots' ||
  1106. $key eq 'snapname' || $key eq 'lxc';
  1107. my $value = $conf->{$key};
  1108. die "detected invalid newline inside property '$key'\n"
  1109. if $value =~ m/\n/;
  1110. $raw .= "$key: $value\n";
  1111. }
  1112.  
  1113. if (my $lxcconf = $conf->{lxc}) {
  1114. foreach my $entry (@$lxcconf) {
  1115. my ($k, $v) = @$entry;
  1116. $raw .= "$k: $v\n";
  1117. }
  1118. }
  1119.  
  1120. return $raw;
  1121. };
  1122.  
  1123. my $raw = &$generate_raw_config($conf);
  1124.  
  1125. if (scalar(keys %{$conf->{pending}})){
  1126. $raw .= "\n[pve:pending]\n";
  1127. $raw .= &$generate_raw_config($conf->{pending});
  1128. }
  1129.  
  1130. foreach my $snapname (sort keys %{$conf->{snapshots}}) {
  1131. $raw .= "\n[$snapname]\n";
  1132. $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
  1133. }
  1134.  
  1135. return $raw;
  1136. }
  1137.  
  1138. sub update_pct_config {
  1139. my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
  1140.  
  1141. my $storage_cfg = PVE::Storage::config();
  1142.  
  1143. foreach my $opt (@$revert) {
  1144. delete $conf->{pending}->{$opt};
  1145. $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
  1146. }
  1147.  
  1148. # write updates to pending section
  1149. my $modified = {}; # record modified options
  1150.  
  1151. foreach my $opt (@$delete) {
  1152. if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
  1153. warn "cannot delete '$opt' - not set in current configuration!\n";
  1154. next;
  1155. }
  1156. $modified->{$opt} = 1;
  1157. if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
  1158. die "unable to delete required option '$opt'\n";
  1159. } elsif ($opt =~ m/^unused(\d+)$/) {
  1160. $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
  1161. } elsif ($opt =~ m/^mp(\d+)$/) {
  1162. $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
  1163. } elsif ($opt eq 'unprivileged') {
  1164. die "unable to delete read-only option: '$opt'\n";
  1165. }
  1166. $class->add_to_pending_delete($conf, $opt);
  1167. }
  1168.  
  1169. my $check_content_type = sub {
  1170. my ($mp) = @_;
  1171. my $sid = PVE::Storage::parse_volume_id($mp->{volume});
  1172. my $storage_config = PVE::Storage::storage_config($storage_cfg, $sid);
  1173. die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
  1174. if !$storage_config->{content}->{rootdir};
  1175. };
  1176.  
  1177. foreach my $opt (sort keys %$param) { # add/change
  1178. $modified->{$opt} = 1;
  1179. my $value = $param->{$opt};
  1180. if ($opt =~ m/^mp(\d+)$/ || $opt eq 'rootfs') {
  1181. $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
  1182. my $mp = $class->parse_volume($opt, $value);
  1183. $check_content_type->($mp) if ($mp->{type} eq 'volume');
  1184. } elsif ($opt eq 'hookscript') {
  1185. PVE::GuestHelpers::check_hookscript($value);
  1186. } elsif ($opt eq 'nameserver') {
  1187. $value = PVE::LXC::verify_nameserver_list($value);
  1188. } elsif ($opt eq 'searchdomain') {
  1189. $value = PVE::LXC::verify_searchdomain_list($value);
  1190. } elsif ($opt eq 'unprivileged') {
  1191. die "unable to modify read-only option: '$opt'\n";
  1192. } elsif ($opt eq 'tags') {
  1193. $value = PVE::GuestHelpers::get_unique_tags($value);
  1194. } elsif ($opt =~ m/^net(\d+)$/) {
  1195. my $res = PVE::JSONSchema::parse_property_string($netconf_desc, $value);
  1196.  
  1197. if (my $mtu = $res->{mtu}) {
  1198. my $bridge_mtu = PVE::Network::read_bridge_mtu($res->{bridge});
  1199. die "$opt: MTU size '$mtu' is bigger than bridge MTU '$bridge_mtu'\n"
  1200. if ($mtu > $bridge_mtu);
  1201. }
  1202. } elsif ($opt =~ m/^dev(\d+)$/) {
  1203. my $device = $class->parse_device($value);
  1204.  
  1205. die "Path is not defined for passthrough device $opt"
  1206. if !defined($device->{path});
  1207.  
  1208. # Validate device
  1209. PVE::LXC::Tools::get_device_mode_and_rdev($device->{path});
  1210. }
  1211. $conf->{pending}->{$opt} = $value;
  1212. $class->remove_from_pending_delete($conf, $opt);
  1213. }
  1214.  
  1215. my $changes = $class->cleanup_pending($conf);
  1216.  
  1217. my $errors = {};
  1218. if ($running) {
  1219. $class->vmconfig_hotplug_pending($vmid, $conf, $storage_cfg, $modified, $errors);
  1220. } else {
  1221. $class->vmconfig_apply_pending($vmid, $conf, $storage_cfg, $modified, $errors);
  1222. }
  1223.  
  1224. return $errors;
  1225. }
  1226.  
  1227. sub check_type {
  1228. my ($class, $key, $value) = @_;
  1229.  
  1230. die "unknown setting '$key'\n" if !$confdesc->{$key};
  1231.  
  1232. my $type = $confdesc->{$key}->{type};
  1233.  
  1234. if (!defined($value)) {
  1235. die "got undefined value\n";
  1236. }
  1237.  
  1238. if ($value =~ m/[\n\r]/) {
  1239. die "property contains a line feed\n";
  1240. }
  1241.  
  1242. if ($type eq 'boolean') {
  1243. return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
  1244. return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
  1245. die "type check ('boolean') failed - got '$value'\n";
  1246. } elsif ($type eq 'integer') {
  1247. return int($1) if $value =~ m/^(\d+)$/;
  1248. die "type check ('integer') failed - got '$value'\n";
  1249. } elsif ($type eq 'number') {
  1250. return $value if $value =~ m/^(\d+)(\.\d+)?$/;
  1251. die "type check ('number') failed - got '$value'\n";
  1252. } elsif ($type eq 'string') {
  1253. if (my $fmt = $confdesc->{$key}->{format}) {
  1254. PVE::JSONSchema::check_format($fmt, $value);
  1255. return $value;
  1256. }
  1257. return $value;
  1258. } else {
  1259. die "internal error"
  1260. }
  1261. }
  1262.  
  1263.  
  1264. # add JSON properties for create and set function
  1265. sub json_config_properties {
  1266. my ($class, $prop) = @_;
  1267.  
  1268. foreach my $opt (keys %$confdesc) {
  1269. next if $opt eq 'parent' || $opt eq 'snaptime';
  1270. next if $prop->{$opt};
  1271. $prop->{$opt} = $confdesc->{$opt};
  1272. }
  1273.  
  1274. return $prop;
  1275. }
  1276.  
  1277. my $parse_ct_mountpoint_full = sub {
  1278. my ($class, $desc, $data, $noerr) = @_;
  1279.  
  1280. $data //= '';
  1281.  
  1282. my $res;
  1283. eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
  1284. if ($@) {
  1285. return undef if $noerr;
  1286. die $@;
  1287. }
  1288.  
  1289. if (defined(my $size = $res->{size})) {
  1290. $size = PVE::JSONSchema::parse_size($size);
  1291. if (!defined($size)) {
  1292. return undef if $noerr;
  1293. die "invalid size: $size\n";
  1294. }
  1295. $res->{size} = $size;
  1296. }
  1297.  
  1298. $res->{type} = $class->classify_mountpoint($res->{volume});
  1299.  
  1300. return $res;
  1301. };
  1302.  
  1303. sub print_ct_mountpoint {
  1304. my ($class, $info, $nomp) = @_;
  1305. my $skip = [ 'type' ];
  1306. push @$skip, 'mp' if $nomp;
  1307. return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
  1308. }
  1309.  
  1310. sub print_ct_unused {
  1311. my ($class, $info) = @_;
  1312.  
  1313. my $skip = [ 'type' ];
  1314. return PVE::JSONSchema::print_property_string($info, $unused_desc, $skip);
  1315. }
  1316.  
  1317. sub parse_volume {
  1318. my ($class, $key, $volume_string, $noerr) = @_;
  1319.  
  1320. if ($key eq 'rootfs') {
  1321. my $res = $parse_ct_mountpoint_full->($class, $rootfs_desc, $volume_string, $noerr);
  1322. $res->{mp} = '/' if defined($res);
  1323. return $res;
  1324. } elsif ($key =~ m/^mp\d+$/) {
  1325. return $parse_ct_mountpoint_full->($class, $mp_desc, $volume_string, $noerr);
  1326. } elsif ($key =~ m/^unused\d+$/) {
  1327. return $parse_ct_mountpoint_full->($class, $unused_desc, $volume_string, $noerr);
  1328. }
  1329.  
  1330. die "parse_volume - unknown type: $key\n" if !$noerr;
  1331.  
  1332. }
  1333.  
  1334. sub parse_device {
  1335. my ($class, $device_string, $noerr) = @_;
  1336.  
  1337. my $res = eval { PVE::JSONSchema::parse_property_string($dev_desc, $device_string) };
  1338. if ($@) {
  1339. return undef if $noerr;
  1340. die $@;
  1341. }
  1342.  
  1343. if (!defined($res->{path})) {
  1344. return undef if $noerr;
  1345. die "Path has to be defined\n";
  1346. }
  1347.  
  1348. return $res;
  1349. }
  1350.  
  1351. sub print_volume {
  1352. my ($class, $key, $volume) = @_;
  1353.  
  1354. return $class->print_ct_unused($volume) if $key =~ m/^unused(\d+)$/;
  1355.  
  1356. return $class->print_ct_mountpoint($volume, $key eq 'rootfs');
  1357. }
  1358.  
  1359. sub volid_key {
  1360. my ($class) = @_;
  1361.  
  1362. return 'volume';
  1363. }
  1364.  
  1365. sub print_lxc_network {
  1366. my ($class, $net) = @_;
  1367. return PVE::JSONSchema::print_property_string($net, $netconf_desc);
  1368. }
  1369.  
  1370. sub parse_lxc_network {
  1371. my ($class, $data) = @_;
  1372.  
  1373. return {} if !$data;
  1374.  
  1375. my $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
  1376.  
  1377. $res->{type} = 'veth';
  1378. if (!$res->{hwaddr}) {
  1379. my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
  1380. $res->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
  1381. }
  1382.  
  1383. return $res;
  1384. }
  1385.  
  1386. sub parse_features {
  1387. my ($class, $data) = @_;
  1388. return {} if !$data;
  1389. return PVE::JSONSchema::parse_property_string($features_desc, $data);
  1390. }
  1391.  
  1392. sub option_exists {
  1393. my ($class, $name) = @_;
  1394.  
  1395. return defined($confdesc->{$name});
  1396. }
  1397. # END JSON config code
  1398.  
  1399. # takes a max memory value as KiB and returns an tuple with max and high values
  1400. sub calculate_memory_constraints {
  1401. my ($memory) = @_;
  1402.  
  1403. return if !defined($memory);
  1404.  
  1405. # cgroup memory usage is limited by the hard 'max' limit (OOM-killer enforced) and the soft
  1406. # 'high' limit (cgroup processes get throttled and put under heavy reclaim pressure).
  1407. my $memory_max = int($memory * 1024 * 1024);
  1408. # Set the high to 1016/1024 (~99.2%) of the 'max' hard limit clamped to 128 MiB max, to scale
  1409. # it for the lower range while having a decent 2^x based rest for 2^y memory configs.
  1410. my $memory_high = $memory >= 16 * 1024 ? int(($memory - 128) * 1024 * 1024) : int($memory * 1024 * 1016);
  1411.  
  1412. return ($memory_max, $memory_high);
  1413. }
  1414.  
  1415. my $LXC_FASTPLUG_OPTIONS= {
  1416. 'description' => 1,
  1417. 'onboot' => 1,
  1418. 'startup' => 1,
  1419. 'protection' => 1,
  1420. 'hostname' => 1,
  1421. 'hookscript' => 1,
  1422. 'cores' => 1,
  1423. 'tags' => 1,
  1424. 'lock' => 1,
  1425. };
  1426.  
  1427. sub vmconfig_hotplug_pending {
  1428. my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
  1429.  
  1430. my $pid = PVE::LXC::find_lxc_pid($vmid);
  1431. my $rootdir = "/proc/$pid/root";
  1432.  
  1433. my $add_hotplug_error = sub {
  1434. my ($opt, $msg) = @_;
  1435. $errors->{$opt} = "unable to hotplug $opt: $msg";
  1436. };
  1437.  
  1438. foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
  1439. next if $selection && !$selection->{$opt};
  1440. if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
  1441. $conf->{$opt} = delete $conf->{pending}->{$opt};
  1442. }
  1443. }
  1444.  
  1445. my $cgroup = PVE::LXC::CGroup->new($vmid);
  1446.  
  1447. # There's no separate swap size to configure, there's memory and "total"
  1448. # memory (iow. memory+swap). This means we have to change them together.
  1449. my $hotplug_memory_done;
  1450. my $hotplug_memory = sub {
  1451. my ($new_memory, $new_swap) = @_;
  1452.  
  1453. ($new_memory, my $new_memory_high) = calculate_memory_constraints($new_memory);
  1454. $new_swap = int($new_swap * 1024 * 1024) if defined($new_swap);
  1455. $cgroup->change_memory_limit($new_memory, $new_swap, $new_memory_high);
  1456.  
  1457. $hotplug_memory_done = 1;
  1458. };
  1459.  
  1460. my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
  1461. # FIXME: $force deletion is not implemented for CTs
  1462. foreach my $opt (sort keys %$pending_delete_hash) {
  1463. next if $selection && !$selection->{$opt};
  1464. eval {
  1465. if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
  1466. # pass
  1467. } elsif ($opt =~ m/^unused(\d+)$/) {
  1468. PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
  1469. if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
  1470. } elsif ($opt eq 'swap') {
  1471. $hotplug_memory->(undef, 0);
  1472. } elsif ($opt eq 'cpulimit') {
  1473. $cgroup->change_cpu_quota(undef, undef); # reset, cgroup module can better decide values
  1474. } elsif ($opt eq 'cpuunits') {
  1475. $cgroup->change_cpu_shares(undef);
  1476. } elsif ($opt =~ m/^net(\d)$/) {
  1477. my $netid = $1;
  1478. PVE::Network::veth_delete("veth${vmid}i$netid");
  1479. if ($have_sdn) {
  1480. my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
  1481. print "delete ips from $opt\n";
  1482. eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
  1483. warn $@ if $@;
  1484. }
  1485. } else {
  1486. die "skip\n"; # skip non-hotpluggable opts
  1487. }
  1488. };
  1489. if (my $err = $@) {
  1490. $add_hotplug_error->($opt, $err) if $err ne "skip\n";
  1491. } else {
  1492. delete $conf->{$opt};
  1493. $class->remove_from_pending_delete($conf, $opt);
  1494. }
  1495. }
  1496.  
  1497. foreach my $opt (sort keys %{$conf->{pending}}) {
  1498. next if $opt eq 'delete'; # just to be sure
  1499. next if $selection && !$selection->{$opt};
  1500. my $value = $conf->{pending}->{$opt};
  1501. eval {
  1502. if ($opt eq 'cpulimit') {
  1503. my $quota = 100000 * $value;
  1504. $cgroup->change_cpu_quota(int(100000 * $value), 100000);
  1505. } elsif ($opt eq 'cpuunits') {
  1506. $cgroup->change_cpu_shares($value);
  1507. } elsif ($opt =~ m/^net(\d+)$/) {
  1508. my $netid = $1;
  1509. my $net = $class->parse_lxc_network($value);
  1510. $value = $class->print_lxc_network($net);
  1511. PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
  1512. } elsif ($opt eq 'memory' || $opt eq 'swap') {
  1513. if (!$hotplug_memory_done) { # don't call twice if both opts are passed
  1514. $hotplug_memory->($conf->{pending}->{memory}, $conf->{pending}->{swap});
  1515. }
  1516. } elsif ($opt =~ m/^mp(\d+)$/) {
  1517. if (exists($conf->{$opt})) {
  1518. die "skip\n"; # don't try to hotplug over existing mp
  1519. }
  1520.  
  1521. $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 1);
  1522. # apply_pending_mountpoint modifies the value if it creates a new disk
  1523. $value = $conf->{pending}->{$opt};
  1524. } else {
  1525. die "skip\n"; # skip non-hotpluggable
  1526. }
  1527. };
  1528. if (my $err = $@) {
  1529. $add_hotplug_error->($opt, $err) if $err ne "skip\n";
  1530. } else {
  1531. $conf->{$opt} = $value;
  1532. delete $conf->{pending}->{$opt};
  1533. }
  1534. }
  1535. }
  1536.  
  1537. sub vmconfig_apply_pending {
  1538. my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
  1539.  
  1540. my $add_apply_error = sub {
  1541. my ($opt, $msg) = @_;
  1542. my $err_msg = "unable to apply pending change $opt : $msg";
  1543. $errors->{$opt} = $err_msg;
  1544. warn $err_msg;
  1545. };
  1546.  
  1547. my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
  1548. # FIXME: $force deletion is not implemented for CTs
  1549. foreach my $opt (sort keys %$pending_delete_hash) {
  1550. next if $selection && !$selection->{$opt};
  1551. eval {
  1552. if ($opt =~ m/^mp(\d+)$/) {
  1553. my $mp = $class->parse_volume($opt, $conf->{$opt});
  1554. if ($mp->{type} eq 'volume') {
  1555. $class->add_unused_volume($conf, $mp->{volume})
  1556. if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
  1557. }
  1558. } elsif ($opt =~ m/^unused(\d+)$/) {
  1559. PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
  1560. if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
  1561. } elsif ($opt =~ m/^net(\d+)$/) {
  1562. if ($have_sdn) {
  1563. my $net = $class->parse_lxc_network($conf->{$opt});
  1564. eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
  1565. warn $@ if $@;
  1566. }
  1567. }
  1568. };
  1569. if (my $err = $@) {
  1570. $add_apply_error->($opt, $err);
  1571. } else {
  1572. delete $conf->{$opt};
  1573. $class->remove_from_pending_delete($conf, $opt);
  1574. }
  1575. }
  1576.  
  1577. $class->cleanup_pending($conf);
  1578.  
  1579. foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
  1580. next if $opt eq 'delete'; # just to be sure
  1581. next if $selection && !$selection->{$opt};
  1582. eval {
  1583. if ($opt =~ m/^mp(\d+)$/) {
  1584. $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 0);
  1585. } elsif ($opt =~ m/^net(\d+)$/) {
  1586. my $netid = $1;
  1587. my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
  1588. $conf->{pending}->{$opt} = $class->print_lxc_network($net);
  1589. if ($have_sdn) {
  1590. if ($conf->{$opt}) {
  1591. my $old_net = $class->parse_lxc_network($conf->{$opt});
  1592. if ($old_net->{bridge} ne $net->{bridge} || $old_net->{hwaddr} ne $net->{hwaddr}) {
  1593. PVE::Network::SDN::Vnets::del_ips_from_mac($old_net->{bridge}, $old_net->{hwaddr}, $conf->{name});
  1594. PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
  1595. }
  1596. } else {
  1597. PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
  1598. }
  1599. }
  1600. }
  1601. };
  1602. if (my $err = $@) {
  1603. $add_apply_error->($opt, $err);
  1604. } else {
  1605. $conf->{$opt} = delete $conf->{pending}->{$opt};
  1606. }
  1607. }
  1608. }
  1609.  
  1610. my $rescan_volume = sub {
  1611. my ($storecfg, $mp) = @_;
  1612. eval {
  1613. $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5);
  1614. };
  1615. warn "Could not rescan volume size - $@\n" if $@;
  1616. };
  1617.  
  1618. sub apply_pending_mountpoint {
  1619. my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
  1620.  
  1621. my $mp = $class->parse_volume($opt, $conf->{pending}->{$opt});
  1622. my $old = $conf->{$opt};
  1623. if ($mp->{type} eq 'volume' && $mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
  1624. my $original_value = $conf->{pending}->{$opt};
  1625. my $vollist = PVE::LXC::create_disks(
  1626. $storecfg,
  1627. $vmid,
  1628. { $opt => $original_value },
  1629. $conf,
  1630. 1,
  1631. );
  1632. if ($running) {
  1633. # Re-parse mount point:
  1634. my $mp = $class->parse_volume($opt, $conf->{pending}->{$opt});
  1635. eval {
  1636. PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
  1637. };
  1638. my $err = $@;
  1639. if ($err) {
  1640. PVE::LXC::destroy_disks($storecfg, $vollist);
  1641. # The pending-changes code collects errors but keeps on looping through further
  1642. # pending changes, so unroll the change in $conf as well if destroy_disks()
  1643. # didn't die().
  1644. $conf->{pending}->{$opt} = $original_value;
  1645. die $err;
  1646. }
  1647. }
  1648. } else {
  1649. die "skip\n" if $running && defined($old); # TODO: "changing" mount points?
  1650. $rescan_volume->($storecfg, $mp) if $mp->{type} eq 'volume';
  1651. if ($running) {
  1652. PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
  1653. }
  1654. $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
  1655. }
  1656.  
  1657. if (defined($old)) {
  1658. my $mp = $class->parse_volume($opt, $old);
  1659. if ($mp->{type} eq 'volume') {
  1660. $class->add_unused_volume($conf, $mp->{volume})
  1661. if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
  1662. }
  1663. }
  1664. }
  1665.  
  1666. sub classify_mountpoint {
  1667. my ($class, $vol) = @_;
  1668. if ($vol =~ m!^/!) {
  1669. return 'device' if $vol =~ m!^/dev/!;
  1670. return 'bind';
  1671. }
  1672. return 'volume';
  1673. }
  1674.  
  1675. my $__is_volume_in_use = sub {
  1676. my ($class, $config, $volid) = @_;
  1677. my $used = 0;
  1678.  
  1679. $class->foreach_volume($config, sub {
  1680. my ($ms, $mountpoint) = @_;
  1681. return if $used;
  1682. $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
  1683. });
  1684.  
  1685. return $used;
  1686. };
  1687.  
  1688. sub is_volume_in_use_by_snapshots {
  1689. my ($class, $config, $volid) = @_;
  1690.  
  1691. if (my $snapshots = $config->{snapshots}) {
  1692. foreach my $snap (keys %$snapshots) {
  1693. return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
  1694. }
  1695. }
  1696.  
  1697. return 0;
  1698. }
  1699.  
  1700. sub is_volume_in_use {
  1701. my ($class, $config, $volid, $include_snapshots, $include_pending) = @_;
  1702. return 1 if $__is_volume_in_use->($class, $config, $volid);
  1703. return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
  1704. return 1 if $include_pending && $__is_volume_in_use->($class, $config->{pending}, $volid);
  1705. return 0;
  1706. }
  1707.  
  1708. sub has_dev_console {
  1709. my ($class, $conf) = @_;
  1710.  
  1711. return !(defined($conf->{console}) && !$conf->{console});
  1712. }
  1713.  
  1714. sub has_lxc_entry {
  1715. my ($class, $conf, $keyname) = @_;
  1716.  
  1717. if (my $lxcconf = $conf->{lxc}) {
  1718. foreach my $entry (@$lxcconf) {
  1719. my ($key, undef) = @$entry;
  1720. return 1 if $key eq $keyname;
  1721. }
  1722. }
  1723.  
  1724. return 0;
  1725. }
  1726.  
  1727. sub get_tty_count {
  1728. my ($class, $conf) = @_;
  1729.  
  1730. return $conf->{tty} // $confdesc->{tty}->{default};
  1731. }
  1732.  
  1733. sub get_cmode {
  1734. my ($class, $conf) = @_;
  1735.  
  1736. return $conf->{cmode} // $confdesc->{cmode}->{default};
  1737. }
  1738.  
  1739. sub valid_volume_keys {
  1740. my ($class, $reverse) = @_;
  1741.  
  1742. my @names = ('rootfs');
  1743.  
  1744. for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
  1745. push @names, "mp$i";
  1746. }
  1747.  
  1748. return $reverse ? reverse @names : @names;
  1749. }
  1750.  
  1751. sub valid_volume_keys_with_unused {
  1752. my ($class, $reverse) = @_;
  1753. my @names = $class->valid_volume_keys();
  1754. for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
  1755. push @names, "unused$i";
  1756. }
  1757. return $reverse ? reverse @names : @names;
  1758. }
  1759.  
  1760. sub get_vm_volumes {
  1761. my ($class, $conf, $excludes) = @_;
  1762.  
  1763. my $vollist = [];
  1764.  
  1765. $class->foreach_volume($conf, sub {
  1766. my ($ms, $mountpoint) = @_;
  1767.  
  1768. return if $excludes && $ms eq $excludes;
  1769.  
  1770. my $volid = $mountpoint->{volume};
  1771. return if !$volid || $mountpoint->{type} ne 'volume';
  1772.  
  1773. my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
  1774. return if !$sid;
  1775.  
  1776. push @$vollist, $volid;
  1777. });
  1778.  
  1779. return $vollist;
  1780. }
  1781.  
  1782. sub get_replicatable_volumes {
  1783. my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
  1784.  
  1785. my $volhash = {};
  1786.  
  1787. my $test_volid = sub {
  1788. my ($volid, $mountpoint) = @_;
  1789.  
  1790. return if !$volid;
  1791.  
  1792. my $mptype = $mountpoint->{type};
  1793. my $replicate = $mountpoint->{replicate} // 1;
  1794.  
  1795. if ($mptype ne 'volume') {
  1796. # skip bindmounts if replicate = 0 even for cleanup,
  1797. # since bind mounts could not have been replicated ever
  1798. return if !$replicate;
  1799. die "unable to replicate mountpoint type '$mptype'\n";
  1800. }
  1801.  
  1802. my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
  1803. return if !$storeid;
  1804.  
  1805. my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
  1806. return if $scfg->{shared};
  1807.  
  1808. my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
  1809. return if !$owner || ($owner != $vmid);
  1810.  
  1811. die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
  1812.  
  1813. return if !$cleanup && !$replicate;
  1814.  
  1815. if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
  1816. return if $cleanup || $noerr;
  1817. die "missing replicate feature on volume '$volid'\n";
  1818. }
  1819.  
  1820. $volhash->{$volid} = 1;
  1821. };
  1822.  
  1823. $class->foreach_volume($conf, sub {
  1824. my ($ms, $mountpoint) = @_;
  1825. $test_volid->($mountpoint->{volume}, $mountpoint);
  1826. });
  1827.  
  1828. foreach my $snapname (keys %{$conf->{snapshots}}) {
  1829. my $snap = $conf->{snapshots}->{$snapname};
  1830. $class->foreach_volume($snap, sub {
  1831. my ($ms, $mountpoint) = @_;
  1832. $test_volid->($mountpoint->{volume}, $mountpoint);
  1833. });
  1834. }
  1835.  
  1836. # add 'unusedX' volumes to volhash
  1837. foreach my $key (keys %$conf) {
  1838. if ($key =~ m/^unused/) {
  1839. $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
  1840. }
  1841. }
  1842.  
  1843. return $volhash;
  1844. }
  1845.  
  1846. sub get_backup_volumes {
  1847. my ($class, $conf) = @_;
  1848.  
  1849. my $return_volumes = [];
  1850.  
  1851. my $test_mountpoint = sub {
  1852. my ($key, $volume) = @_;
  1853.  
  1854. my ($included, $reason) = $class->mountpoint_backup_enabled($key, $volume);
  1855.  
  1856. push @$return_volumes, {
  1857. key => $key,
  1858. included => $included,
  1859. reason => $reason,
  1860. volume_config => $volume,
  1861. };
  1862. };
  1863.  
  1864. PVE::LXC::Config->foreach_volume($conf, $test_mountpoint);
  1865.  
  1866. return $return_volumes;
  1867. }
  1868.  
  1869. sub get_derived_property {
  1870. my ($class, $conf, $name) = @_;
  1871.  
  1872. if ($name eq 'max-cpu') {
  1873. return $conf->{cpulimit} || $conf->{cores} || 0;
  1874. } elsif ($name eq 'max-memory') {
  1875. return ($conf->{memory} || 512) * 1024 * 1024;
  1876. } else {
  1877. die "unknown derived property - $name\n";
  1878. }
  1879. }
  1880.  
  1881. sub foreach_passthrough_device {
  1882. my ($class, $conf, $func, @param) = @_;
  1883.  
  1884. for my $key (keys %$conf) {
  1885. next if $key !~ m/^dev(\d+)$/;
  1886.  
  1887. my $device = $class->parse_device($conf->{$key});
  1888.  
  1889. $func->($key, $device, @param);
  1890. }
  1891. }
  1892.  
  1893. 1;

In der Datei /usr/share/perl5/PVE/LXC/Setup.pm muss eisfair auch noch an einigen Stellen eingefügt werden.

usr/share/perl5/PVE/LXC/Setup.pm einblenden

ausblenden

Die entsprechenden Stellen sind farblich hervorgehoben, (Zeilen 16, 31, 74-75)

/usr/share/perl5/PVE/LXC/Setup.pm
  1. package PVE::LXC::Setup;
  2.  
  3. use strict;
  4. use warnings;
  5.  
  6. use POSIX;
  7. use Cwd 'abs_path';
  8.  
  9. use PVE::Tools;
  10.  
  11. use PVE::LXC::Setup::Alpine;
  12. use PVE::LXC::Setup::ArchLinux;
  13. use PVE::LXC::Setup::CentOS;
  14. use PVE::LXC::Setup::Debian;
  15. use PVE::LXC::Setup::Devuan;
  16. use PVE::LXC::Setup::Eisfair;
  17. use PVE::LXC::Setup::Fedora;
  18. use PVE::LXC::Setup::Gentoo;
  19. use PVE::LXC::Setup::SUSE;
  20. use PVE::LXC::Setup::Ubuntu;
  21. use PVE::LXC::Setup::NixOS;
  22. use PVE::LXC::Setup::OpenEuler;
  23. use PVE::LXC::Setup::Unmanaged;
  24.  
  25. my $plugins = {
  26. alpine => 'PVE::LXC::Setup::Alpine',
  27. archlinux => 'PVE::LXC::Setup::ArchLinux',
  28. centos => 'PVE::LXC::Setup::CentOS',
  29. debian => 'PVE::LXC::Setup::Debian',
  30. devuan => 'PVE::LXC::Setup::Devuan',
  31. eisfair => 'PVE::LXC::Setup::Eisfair',
  32. fedora => 'PVE::LXC::Setup::Fedora',
  33. gentoo => 'PVE::LXC::Setup::Gentoo',
  34. openeuler => 'PVE::LXC::Setup::OpenEuler',
  35. opensuse => 'PVE::LXC::Setup::SUSE',
  36. ubuntu => 'PVE::LXC::Setup::Ubuntu',
  37. nixos => 'PVE::LXC::Setup::NixOS',
  38. unmanaged => 'PVE::LXC::Setup::Unmanaged',
  39. };
  40.  
  41. # a map to allow supporting related distro flavours
  42. my $plugin_alias = {
  43. 'opensuse-leap' => 'opensuse',
  44. 'opensuse-tumbleweed' => 'opensuse',
  45. 'opensuse-slowroll' => 'opensuse',
  46. 'openEuler' => 'openeuler',
  47. arch => 'archlinux',
  48. sles => 'opensuse',
  49. };
  50.  
  51. my $autodetect_type = sub {
  52. my ($self, $rootdir, $os_release) = @_;
  53.  
  54. if (my $id = $os_release->{ID}) {
  55. return $id if $plugins->{$id};
  56. return $plugin_alias->{$id} if $plugin_alias->{$id};
  57. warn "unknown ID '$id' in /etc/os-release file, trying fallback detection\n";
  58. }
  59.  
  60. # fallback compatibility checks
  61.  
  62. my $lsb_fn = "$rootdir/etc/lsb-release";
  63. if (-f $lsb_fn) {
  64. my $data = PVE::Tools::file_get_contents($lsb_fn);
  65. if ($data =~ m/^DISTRIB_ID=Ubuntu$/im) {
  66. return 'ubuntu';
  67. }
  68. }
  69.  
  70. if (-f "$rootdir/etc/debian_version") {
  71. return "debian";
  72. } elsif (-f "$rootdir/etc/devuan_version") {
  73. return "devuan";
  74. } elsif (-f "$rootdir/etc/version") {
  75. return "eisfair";
  76. } elsif (-f "$rootdir/etc/SuSE-brand" || -f "$rootdir/etc/SuSE-release") {
  77. return "opensuse";
  78. } elsif (-f "$rootdir/etc/fedora-release") {
  79. return "fedora";
  80. } elsif (-f "$rootdir/etc/centos-release" || -f "$rootdir/etc/redhat-release") {
  81. return "centos";
  82. } elsif (-f "$rootdir/etc/arch-release") {
  83. return "archlinux";
  84. } elsif (-f "$rootdir/etc/alpine-release") {
  85. return "alpine";
  86. } elsif (-f "$rootdir/etc/gentoo-release") {
  87. return "gentoo";
  88. } elsif (-d "$rootdir/nix/store") {
  89. return "nixos";
  90. } elsif (-f "$rootdir/etc/openEuler-release") {
  91. return "openeuler";
  92. } elsif (-f "$rootdir/etc/os-release") {
  93. die "unable to detect OS distribution\n";
  94. } else {
  95. warn "/etc/os-release file not found and autodetection failed, falling back to 'unmanaged'\n";
  96. return "unmanaged";
  97. }
  98. };
  99.  
  100. sub new {
  101. my ($class, $conf, $rootdir, $type) = @_;
  102.  
  103. die "no root directory\n" if !$rootdir || $rootdir eq '/';
  104.  
  105. my $self = bless { conf => $conf, rootdir => $rootdir}, $class;
  106.  
  107. my $os_release = $self->get_ct_os_release();
  108.  
  109. if ($conf->{ostype} && $conf->{ostype} eq 'unmanaged') {
  110. $type = 'unmanaged';
  111. } elsif (!defined($type)) {
  112. # try to autodetect type
  113. $type = &$autodetect_type($self, $rootdir, $os_release);
  114. my $expected_type = $conf->{ostype} || $type;
  115.  
  116. if ($type ne $expected_type) {
  117. warn "WARNING: /etc not present in CT, is the rootfs mounted?\n"
  118. if ! -e "$rootdir/etc";
  119. warn "got unexpected ostype ($type != $expected_type)\n"
  120. }
  121. }
  122.  
  123. my $plugin_class = $plugins->{$type} || die "no such OS type '$type'\n";
  124.  
  125. my $plugin = $plugin_class->new($conf, $rootdir, $os_release);
  126. $self->{plugin} = $plugin;
  127. $self->{in_chroot} = 0;
  128.  
  129. # Cache some host files we need access to:
  130. $plugin->{host_resolv_conf} = PVE::INotify::read_file('resolvconf');
  131. $plugin->{host_timezone} = PVE::INotify::read_file('timezone');
  132.  
  133. abs_path('/etc/localtime') =~ m|^(/.+)| or die "invalid /etc/localtime\n"; # untaint
  134. $plugin->{host_localtime} = $1;
  135.  
  136. # pass on user namespace information:
  137. my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
  138. if (@$id_map) {
  139. $plugin->{id_map} = $id_map;
  140. $plugin->{root_uid} = $root_uid;
  141. $plugin->{root_gid} = $root_gid;
  142. }
  143.  
  144. # if arch is unset, we try to autodetect it
  145. if (!defined($conf->{arch})) {
  146. my $arch = eval { $self->protected_call(sub { $plugin->detect_architecture() }) };
  147.  
  148. if (my $err = $@) {
  149. warn "Architecture detection failed: $err" if $err;
  150. }
  151.  
  152. if (!defined($arch)) {
  153. $arch = 'amd64';
  154. print "Falling back to $arch.\nUse `pct set VMID --arch ARCH` to change.\n";
  155. } else {
  156. print "Detected container architecture: $arch\n";
  157. }
  158.  
  159. $conf->{arch} = $arch;
  160. }
  161.  
  162. return $self;
  163. }
  164.  
  165. # Forks into a chroot and executes $sub
  166. sub protected_call {
  167. my ($self, $sub) = @_;
  168.  
  169. # avoid recursion:
  170. return $sub->() if $self->{in_chroot};
  171.  
  172. pipe(my $res_in, my $res_out) or die "pipe failed: $!\n";
  173.  
  174. my $child = fork();
  175. die "fork failed: $!\n" if !defined($child);
  176.  
  177. if (!$child) {
  178. close($res_in);
  179. # avoid recursive forks
  180. $self->{in_chroot} = 1;
  181. eval {
  182. my $rootdir = $self->{rootdir};
  183. chroot($rootdir) or die "failed to change root to: $rootdir: $!\n";
  184. chdir('/') or die "failed to change to root directory\n";
  185. my $res = $sub->();
  186. if (defined($res)) {
  187. print {$res_out} "$res";
  188. $res_out->flush();
  189. }
  190. };
  191. if (my $err = $@) {
  192. warn $err;
  193. POSIX::_exit(1);
  194. }
  195. POSIX::_exit(0);
  196. }
  197. close($res_out);
  198. my $result = do { local $/ = undef; <$res_in> };
  199. while (waitpid($child, 0) != $child) {}
  200. if ($? != 0) {
  201. my $method = (caller(1))[3];
  202. die "error in setup task $method\n";
  203. }
  204. return $result;
  205. }
  206.  
  207. sub template_fixup {
  208. my ($self) = @_;
  209. $self->protected_call(sub { $self->{plugin}->template_fixup($self->{conf}) });
  210. }
  211.  
  212. sub setup_network {
  213. my ($self) = @_;
  214. $self->protected_call(sub { $self->{plugin}->setup_network($self->{conf}) });
  215. }
  216.  
  217. sub set_hostname {
  218. my ($self) = @_;
  219. $self->protected_call(sub { $self->{plugin}->set_hostname($self->{conf}) });
  220. }
  221.  
  222. sub set_dns {
  223. my ($self) = @_;
  224. $self->protected_call(sub { $self->{plugin}->set_dns($self->{conf}) });
  225. }
  226.  
  227. sub set_timezone {
  228. my ($self) = @_;
  229. $self->protected_call(sub { $self->{plugin}->set_timezone($self->{conf}) });
  230. }
  231.  
  232. sub setup_init {
  233. my ($self) = @_;
  234. $self->protected_call(sub { $self->{plugin}->setup_init($self->{conf}) });
  235. }
  236.  
  237. sub set_user_password {
  238. my ($self, $user, $pw) = @_;
  239. $self->protected_call(sub { $self->{plugin}->set_user_password($self->{conf}, $user, $pw) });
  240. }
  241.  
  242. my sub generate_ssh_key { # create temporary key in hosts' /run, then read and unlink
  243. my ($type, $comment) = @_;
  244.  
  245. my $key_id = '';
  246. my $keygen_outfunc = sub {
  247. if ($_[0] =~ m/^((?:[0-9a-f]{2}:)+[0-9a-f]{2}|SHA256:[0-9a-z+\/]{43})\s+\Q$comment\E$/i) {
  248. $key_id = $_[0];
  249. }
  250. };
  251. my $file = "/run/pve/.tmp$$.$type";
  252. PVE::Tools::run_command(
  253. ['ssh-keygen', '-f', $file, '-t', $type, '-N', '', '-E', 'sha256', '-C', $comment],
  254. outfunc => $keygen_outfunc,
  255. );
  256. my ($private) = (PVE::Tools::file_get_contents($file) =~ /^(.*)$/sg); # untaint
  257. my ($public) = (PVE::Tools::file_get_contents("$file.pub") =~ /^(.*)$/sg); # untaint
  258. unlink $file, "$file.pub";
  259.  
  260. return ($key_id, $private, $public);
  261. }
  262.  
  263. sub rewrite_ssh_host_keys {
  264. my ($self) = @_;
  265.  
  266. my $plugin = $self->{plugin};
  267.  
  268. my $keynames = $plugin->ssh_host_key_types_to_generate();
  269.  
  270. return if ! -d "$self->{rootdir}/etc/ssh" || !$keynames || !scalar(keys $keynames->%*);
  271.  
  272. my $hostname = $self->{conf}->{hostname} || 'localhost';
  273. $hostname =~ s/\..*$//;
  274.  
  275. my $keyfiles = [];
  276. for my $keytype (keys $keynames->%*) {
  277. my $basename = $keynames->{$keytype};
  278. print "Creating SSH host key '$basename' - this may take some time ...\n";
  279. my ($id, $private, $public) = generate_ssh_key($keytype, "root\@$hostname");
  280. print "done: $id\n";
  281.  
  282. push $keyfiles->@*, ["/etc/ssh/$basename", $private, 0600], ["/etc/ssh/$basename.pub", $public, 0644];
  283. }
  284.  
  285. $self->protected_call(sub { # write them now all to the CTs rootfs at once
  286. for my $file ($keyfiles->@*) {
  287. $plugin->ct_file_set_contents($file->@*);
  288. }
  289. });
  290. }
  291.  
  292. sub pre_start_hook {
  293. my ($self) = @_;
  294.  
  295. $self->protected_call(sub { $self->{plugin}->pre_start_hook($self->{conf}) });
  296. }
  297.  
  298. sub post_clone_hook {
  299. my ($self, $conf) = @_;
  300.  
  301. $self->protected_call(sub { $self->{plugin}->post_clone_hook($conf) });
  302. }
  303.  
  304. sub post_create_hook {
  305. my ($self, $root_password, $ssh_keys) = @_;
  306.  
  307. $self->protected_call(sub {
  308. $self->{plugin}->post_create_hook($self->{conf}, $root_password, $ssh_keys);
  309. });
  310. $self->rewrite_ssh_host_keys();
  311. }
  312.  
  313. sub unified_cgroupv2_support {
  314. my ($self) = @_;
  315.  
  316. return $self->{plugin}->unified_cgroupv2_support($self->get_ct_init_path());
  317. }
  318.  
  319. # os-release(5):
  320. # (...) a newline-separated list of environment-like shell-compatible
  321. # variable assignments. (...) beyond mere variable assignments, no shell
  322. # features are supported (this means variable expansion is explicitly not
  323. # supported) (...). Variable assignment values must be enclosed in double or
  324. # single quotes *if* they include spaces, semicolons or other special
  325. # characters outside of A-Z, a-z, 0-9. Shell special characters ("$", quotes,
  326. # backslash, backtick) must be escaped with backslashes (...). All strings
  327. # should be in UTF-8 format, and non-printable characters should not be used.
  328. # It is not supported to concatenate multiple individually quoted strings.
  329. # Lines beginning with "#" shall be ignored as comments.
  330. my $parse_os_release = sub {
  331. my ($data) = @_;
  332. my $variables = {};
  333. while (defined($data) && $data =~ /^(.+)$/gm) {
  334. next if $1 !~ /^\s*([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
  335. my ($var, $content) = ($1, $2);
  336. chomp $content;
  337.  
  338. if ($content =~ /^'([^']*)'/) {
  339. $variables->{$var} = $1;
  340. } elsif ($content =~ /^"((?:[^"\\]|\\.)*)"/) {
  341. my $s = $1;
  342. $s =~ s/(\\["'`nt\$\\])/"\"$1\""/eeg;
  343. $variables->{$var} = $s;
  344. } elsif ($content =~ /^([A-Za-z0-9]*)/) {
  345. $variables->{$var} = $1;
  346. }
  347. }
  348. return $variables;
  349. };
  350.  
  351. sub get_ct_os_release {
  352. my ($self) = @_;
  353.  
  354. my $data = $self->protected_call(sub {
  355. if (-f '/etc/os-release') {
  356. return PVE::Tools::file_get_contents('/etc/os-release');
  357. } elsif (-f '/usr/lib/os-release') {
  358. return PVE::Tools::file_get_contents('/usr/lib/os-release');
  359. }
  360. });
  361.  
  362. return &$parse_os_release($data);
  363. }
  364.  
  365. # Checks whether /sbin/init is a symlink, and if it is, resolves it to the actual binary
  366. sub get_ct_init_path {
  367. my ($self) = @_;
  368.  
  369. my $init = $self->protected_call(sub {
  370. return $self->{plugin}->get_ct_init_path();
  371. });
  372.  
  373. return $init;
  374. }
  375.  
  376. 1;

Fügt nach Zeile 15 (use PVE::LXC::Setup::Devuan;) bitte noch

use PVE::LXC::Setup::Eisfair;

etwas weiter unten bei my $plugins = { dann noch

eisfair => ‚PVE::LXC::SETUP::Eisfair‘;

und nochmals weiter unten nach return „devuan“; suchen und darunter dann

} elsif (-f „$rootdir/etc/version“) {
    return „eisfair“;

einfügen.

Jetzt noch das file /usr/share/perl5/PVR/LXC/Setup/Eisfair.pm anlegen und mit diesem Inhalt füllen:

/usr/share/perl5/PVR/LXC/Setup/Eisfair.pm
package PVE::LXC::Setup::Eisfair;
 
use strict;
use warnings;
 
use PVE::LXC;
use PVE::Network;
 
use base qw(PVE::LXC::Setup::Base);
 
sub new {
    my ($class, $conf, $rootdir) = @_;
 
    my $self = { conf => $conf, rootdir => $rootdir };
 
    $conf->{ostype} = "eisfair";
 
    return bless $self, $class;
}
 
# Eisfair verwendet systemd, daher benötigen wir kein spezielles /dev/lxc-Verzeichnis.
sub devttydir {
    return '';
}
 
sub setup_init {
    my ($self, $conf) = @_;
 
    # Überprüfen, ob systemd verwendet wird
    my $systemd = $self->ct_readlink('/sbin/init');
    if (defined($systemd) && $systemd =~ m@/systemd$@) {
        $self->setup_container_getty_service($conf);
 
        # Vorkonfiguration für systemd-basierte Dienste
        $self->setup_systemd_preset({
            'NetworkManager.service' => 0,
        });
    }
}
 
sub setup_network {
    my ($self, $conf) = @_;
 
    my $base_filename = "/etc/config.d/base";
    my $dhcpc_filename = "/etc/config.d/dhcpc";
 
    # Alte Base-Konfigurationsdatei einlesen
    open(my $base_fh, '<', $base_filename) or die "Kann $base_filename nicht öffnen: $!";
    my @base_lines = <$base_fh>;
    close($base_fh);
 
    # Initialisiere Standard-Netzwerk-Konfiguration
    my $network_settings = {
        address => '',
        netmask => '',
        gateway => $conf->{ip_default_gateway} || '',
        macaddr => $conf->{hwaddr} || '',
        network => '',
        eth_name => 'eth0',
        dhcp    => 0,
    };
 
    # Netzwerkschnittstellen verarbeiten
    foreach my $k (keys %$conf) {
        next if $k !~ m/^net(\d+)$/;  # Nur Schnittstellen mit "netX" bearbeiten
        my $ind = $1;                 # Netzwerkindex extrahieren
        my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k});
 
        # DHCP prüfen
        if ($d->{ip} && $d->{ip} eq 'dhcp') {
            $network_settings->{dhcp} = 1;
            $network_settings->{eth_name} = "eth$ind";
            next;
        }
 
        # Statische IP und Netzmaske extrahieren
        if (defined($d->{ip})) {
            my $ipinfo = PVE::LXC::parse_ipv4_cidr($d->{ip});
            $network_settings->{address} = $ipinfo->{address};
            $network_settings->{netmask} = $ipinfo->{netmask};
            $network_settings->{network} = $self->get_network_address($d->{ip});
        }
 
        # Gateway und MAC-Adresse
        $network_settings->{gateway} = $d->{gw} if $d->{gw};
        $network_settings->{macaddr} = $d->{hwaddr} || '';
        $network_settings->{eth_name} = "eth$ind";
    }
 
    # Hostname aus der Konfiguration holen
    my $hostname = $conf->{hostname} || 'eisfair';
 
    # DNS- und Domain-Einstellungen abrufen
    my ($searchdomain, $nameserver) = $self->lookup_dns_conf($conf);
 
    if ($network_settings->{dhcp}) {
        # DHCP-Konfigurationsdatei bearbeiten
        open(my $dhcpc_fh, '<', $dhcpc_filename) or die "Kann $dhcpc_filename nicht öffnen: $!";
        my @dhcpc_lines = <$dhcpc_fh>;
        close($dhcpc_fh);
 
        foreach my $line (@dhcpc_lines) {
            if ($line =~ /^START_DHCPC=/) {
                $line = "START_DHCPC='yes'\n";
            } elsif ($line =~ /^DHCP_HOSTNAME=/) {
                $line = "DHCP_HOSTNAME='$hostname'\n";
            }
        }
 
        # DHCP-Konfiguration speichern
        open($dhcpc_fh, '>', $dhcpc_filename) or die "Kann $dhcpc_filename nicht schreiben: $!";
        print $dhcpc_fh @dhcpc_lines;
        close($dhcpc_fh);
 
        # Ausführen des dhcpc.sh-Skripts
        $self->start_meinscript('dhcpc.sh');
 
        # Dienste starten
        $self->start_service('resolver');
        $self->start_service('dhcpc');
    } else {
        # Statische Netzwerkkonfiguration
        foreach my $line (@base_lines) {
            if ($line =~ /^HOSTNAME=/) {
                $line = "HOSTNAME='$hostname'\n";
            } elsif ($line =~ /^IP_ETH_1_IPADDR=/) {
                $line = "IP_ETH_1_IPADDR='$network_settings->{address}'\n";
            } elsif ($line =~ /^IP_ETH_1_NETMASK=/) {
                $line = "IP_ETH_1_NETMASK='$network_settings->{netmask}'\n";
            } elsif ($line =~ /^IP_ETH_1_MACADDR=/) {
                $line = "IP_ETH_1_MACADDR='$network_settings->{macaddr}'\n";
            } elsif ($line =~ /^IP_DEFAULT_GATEWAY=/) {
                $line = "IP_DEFAULT_GATEWAY='$network_settings->{gateway}'\n";
            } elsif ($line =~ /^IP_ETH_1_NAME=/) {
                $line = "IP_ETH_1_NAME='$network_settings->{eth_name}'\n";
            } elsif ($line =~ /^IP_ETH_1_NETWORK=/) {
                $line = "IP_ETH_1_NETWORK='$network_settings->{network}'\n";
            } elsif ($line =~ /^DOMAIN_NAME=/) {
                $line = "DOMAIN_NAME='$searchdomain'\n";
            } elsif ($line =~ /^DNS_SERVER=/) {
                $line = "DNS_SERVER='$nameserver'\n";
            }
        }
 
        # Base-Konfiguration speichern
        open($base_fh, '>', $base_filename) or die "Kann $base_filename nicht schreiben: $!";
        print $base_fh @base_lines;
        close($base_fh);
 
        # DHCP-Konfigurationsdatei bearbeiten
        open(my $dhcpc_fh, '<', $dhcpc_filename) or die "Kann $dhcpc_filename nicht öffnen: $!";
        my @dhcpc_lines = <$dhcpc_fh>;
        close($dhcpc_fh);
 
        foreach my $line (@dhcpc_lines) {
            if ($line =~ /^START_DHCPC=/) {
                $line = "START_DHCPC='no'\n";
            }
        }
 
        # DHCP-Konfiguration speichern
        open($dhcpc_fh, '>', $dhcpc_filename) or die "Kann $dhcpc_filename nicht schreiben: $!";
        print $dhcpc_fh @dhcpc_lines;
        close($dhcpc_fh);
 
        # Ausführen des dhcpc.sh-Skripts
        $self->start_meinscript('dhcpc.sh');
 
 
        # Dienste verwalten
        $self->stop_service('dhcpc');
        $self->restart_service('resolver');
    }
 
    print "Netzwerk-, Hostname-, Domain- und DNS-Konfiguration für Eisfair wurde aktualisiert.\n";
}
 
# Script ausführen
sub start_meinscript {
    my ($self, $scriptname) = @_;
    system("/bin/sh /var/install/config.d/$scriptname") == 0 or die "Konnte Script $scriptname nicht starten: $!";
}
 
# Dienstverwaltungsfunktionen bleiben unverändert
sub start_service {
    my ($self, $service) = @_;
    system("/usr/bin/systemctl start $service") == 0 or die "Konnte Dienst $service nicht starten: $!";
}
 
sub restart_service {
    my ($self, $service) = @_;
    system("/usr/bin/systemctl restart $service") == 0 or die "Konnte Dienst $service nicht neu starten: $!";
}
 
sub stop_service {
    my ($self, $service) = @_;
    system("/usr/bin/systemctl stop $service") == 0 or die "Konnte Dienst $service nicht stoppen: $!";
}
 
# Funktionalität für Netzwerkadressberechnungen bleibt unverändert
sub get_network_address {
    my ($self, $ip_cidr) = @_;
 
    my $ipinfo = PVE::LXC::parse_ipv4_cidr($ip_cidr);
    my $ip_bin = $self->ip_to_binary($ipinfo->{address});
    my $netmask_bin = $self->ip_to_binary($ipinfo->{netmask});
    my $network_bin = $self->bitwise_and($ip_bin, $netmask_bin);
    return $self->binary_to_ip($network_bin);
}
 
sub ip_to_binary {
    my ($self, $ip) = @_;
    return join('', map { sprintf("%08b", $_) } split(/\./, $ip));
}
 
sub binary_to_ip {
    my ($self, $binary) = @_;
    return join('.', map { oct("0b$_") } ($binary =~ /(.{8})/g));
}
 
sub bitwise_and {
    my ($self, $bin1, $bin2) = @_;
    return join('', map { substr($bin1, $_, 1) & substr($bin2, $_, 1) } 0 .. length($bin1) - 1);
}
 
1;

Danach am besten einmal den Proxmox Host neustarten oder zumindest die PVE Dienste:

systemctl restart pveproxy
systemctl restart pvedaemon

Jetzt sollte eigentlich ein Eisfair LXC Container erstellt werden können. :)

Viel Spaß
Christian Richter, richterch@gmx.de

Zuletzt geändert: 2024/12/13 11:32