├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── Neu73.md ├── README.md ├── Renew_certs.md ├── buildpackage.sh ├── debian ├── README.Debian ├── changelog ├── compat ├── control ├── copyright ├── devices.csv.5 ├── dirs ├── install ├── postinst └── rules ├── etc ├── linuxmuster │ └── codename └── profile.d │ └── Z99-linuxmuster.sh ├── get-depends.sh ├── lib ├── dhcpd-update-samba-dns.py ├── functions.py └── setup.d │ ├── a_ini.py │ ├── c_general-dialog.py │ ├── d_templates.py │ ├── e_fstab.py │ ├── g_ssl.py │ ├── h_ssh.py │ ├── i_linbo.py │ ├── j_samba-provisioning.py │ ├── k_samba-users.py │ ├── l_add-server.py │ ├── m_firewall.py │ └── z_final.py ├── sbin ├── linuxmuster-holiday ├── linuxmuster-holiday-generate ├── linuxmuster-import-devices ├── linuxmuster-import-subnets ├── linuxmuster-modini ├── linuxmuster-opnsense-reset ├── linuxmuster-renew-certs └── linuxmuster-setup └── share ├── examples ├── create-testusers.py ├── students.csv └── teachers.csv ├── firewall └── opnsense │ ├── config.xml.tpl │ ├── create-auth-config.py │ ├── create-keytab.py │ ├── fwsetup.sh │ └── pre-auth.conf ├── fix-ntp_signd-dir.sh ├── setupdefaults.ini └── templates ├── cupsd.conf ├── devices.csv ├── dhcpd.apparmor.d ├── dhcpd.conf ├── dhcpd.custom.conf ├── dhcpd.devices.conf ├── dhcpd.events.conf ├── dhcpd.subnets.conf ├── firewall_cert_ext.cnf ├── nsswitch.conf ├── ntp.conf ├── ntpd.apparmor.d ├── server_cert_ext.cnf ├── smb.conf ├── smb.conf.admin ├── subnets.csv └── webui-sudoers /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | workflow_dispatch: 8 | 9 | name: Build Release 10 | 11 | jobs: 12 | deb-package: 13 | name: build DEB-Package 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install dependencies 20 | run: ./get-depends.sh 21 | 22 | - name: Build 23 | run: ./buildpackage.sh 24 | 25 | - name: Copy artifacts 26 | run: mkdir package && cp ../linuxmuster-base7_* ./package 27 | 28 | - name: Upload artifact 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: deb-package 32 | path: package/* 33 | 34 | github-release: 35 | needs: deb-package 36 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 37 | name: GitHub Release 38 | runs-on: ubuntu-24.04 39 | steps: 40 | - name: Download artifact 41 | uses: actions/download-artifact@v4.1.7 42 | with: 43 | name: deb-package 44 | 45 | - name: Extract current changes 46 | run: cat *.changes | sed '0,/^Changes:$/d' | sed '/Checksums.*/Q' | sed '1,2d' | tail >> ./current-changes 47 | 48 | - name: Define distribution variables 49 | run: | 50 | export DISTRIBUTION=$(grep -i ^Distribution *.changes | awk -F\: '{ print $2 }' | awk '{ print $1 }') 51 | echo "DISTRIBUTION=$DISTRIBUTION" >> $GITHUB_ENV 52 | 53 | export VERSION=$(grep -i ^Version *.changes | awk -F\: '{ print $2 }' | awk '{ print $1 }') 54 | echo "VERSION=$VERSION" >> $GITHUB_ENV 55 | 56 | - name: Test if it's a testing prerelease 57 | id: check_prerelease 58 | uses: haya14busa/action-cond@v1 59 | with: 60 | cond: ${{ env.DISTRIBUTION == 'lmn73' }} 61 | if_true: "false" 62 | if_false: "true" 63 | 64 | - name: Create Release 65 | id: create_release 66 | uses: actions/create-release@v1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | tag_name: ${{ github.ref }} 71 | release_name: Release ${{ env.VERSION }} (${{ env.DISTRIBUTION }}) 72 | draft: false 73 | prerelease: ${{ steps.check_prerelease.outputs.value }} 74 | body_path: ./current-changes 75 | 76 | - name: Delete current changes file 77 | run: rm ./current-changes 78 | 79 | - name: Upload Release Assets 80 | id: upload-release-assets 81 | uses: dwenegar/upload-release-assets@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | release_id: ${{ steps.create_release.outputs.id }} 86 | assets_path: . 87 | 88 | publish: 89 | needs: deb-package 90 | name: Push latest release to archive repo 91 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') && github.repository == 'linuxmuster/linuxmuster-base7' 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Setup SSH Key 95 | env: 96 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 97 | run: | 98 | ssh-agent -a $SSH_AUTH_SOCK > /dev/null 99 | ssh-add - <<< "${{ secrets.REPO_SSH_KEY }}" 100 | 101 | - name: Clone archive repo 102 | uses: actions/checkout@v2 103 | with: 104 | repository: "linuxmuster/deb" 105 | ssh-key: ${{ secrets.REPO_SSH_KEY }} 106 | path: "./deb" 107 | 108 | - name: Prepare download 109 | run: mkdir "package" 110 | 111 | - name: Download artifact 112 | uses: actions/download-artifact@v4.1.7 113 | with: 114 | name: deb-package 115 | path: "./package" 116 | 117 | - name: Prepare environment 118 | run: | 119 | export PACKAGE=$(grep -i ^Source package/*.changes | awk -F\: '{ print $2 }' | awk '{ print $1 }') 120 | echo "PACKAGE=$PACKAGE" >> $GITHUB_ENV 121 | 122 | export DISTRIBUTION=$(grep -i ^Distribution package/*.changes | awk -F\: '{ print $2 }' | awk '{ print $1 }') 123 | echo "DISTRIBUTION=$DISTRIBUTION" >> $GITHUB_ENV 124 | 125 | export VERSION=$(grep -i ^Version package/*.changes | awk -F\: '{ print $2 }' | awk '{ print $1 }') 126 | echo "VERSION=$VERSION" >> $GITHUB_ENV 127 | 128 | export BRANCH_NAME=$(echo "$PACKAGE-$DISTRIBUTION-$VERSION" | sed 's/~/tilde/') 129 | echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV 130 | 131 | - name: Copy packages 132 | run: | 133 | mkdir -p deb/packages/$PACKAGE/$DISTRIBUTION 134 | rm -rf deb/packages/$PACKAGE/$DISTRIBUTION/* 135 | 136 | cp package/* deb/packages/$PACKAGE/$DISTRIBUTION 137 | 138 | - name: Push to repo 139 | env: 140 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 141 | run: | 142 | cd deb 143 | git config user.name github-actions 144 | git config user.email github-actions@github.com 145 | git checkout -b update/$BRANCH_NAME 146 | git add * 147 | git commit -a -m "Update $PACKAGE/$DISTRIBUTION to $VERSION (By GitHub actions)" 148 | git push --set-upstream origin update/$BRANCH_NAME 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .directory 2 | share/firewall/opnsense/opnsense* 3 | mkpkg 4 | /syncToTestServer.sh 5 | -------------------------------------------------------------------------------- /Neu73.md: -------------------------------------------------------------------------------- 1 | # Neuerungen in linuxmuster.net 7.3 2 | 3 | ## Upgrade 7.2 -> 7.3 4 | 5 | Wird in einem Schritt von dem Skript 6 | `linuxmuster-release-upgrade` 7 | erledigt. Es muss eine Sicherheitsabfrage bestätigt werden. Anschließend ist ein Reboot fällig. 8 | 9 | Die Skript-Parameter 10 | `--force --reboot` 11 | überspringen die Sicherheitsabfrage und führen den abschließenden Reboot automatisch aus. 12 | 13 | ### Links 14 | 15 | - https://ask.linuxmuster.net/t/testing-lmn-7-3/11547 16 | 17 | ## Installation "from scratch" 18 | 19 | Erfolgt analog zu linuxmuster.net 7.2 (siehe https://github.com/linuxmuster/linuxmuster-prepare#linuxmuster-prepare). 20 | 21 | ## Zertifikatserneuerung 22 | 23 | Zur Erneuerung der beim Setup erstellten selbstsignierten Zertifikate gibt es das Skript 24 | `linuxmuster-renew-certs`: 25 | 26 | ``` 27 | Usage: linuxmuster-renew-certs [options] 28 | 29 | [options] may be: 30 | 31 | -c , --certs= : Comma separated list of certificates to be renewed ("ca", "server" and/or "firewall" or "all"). 32 | -d <#>, --days=<#> : Set number of days (default: 7305). 33 | -f, --force : Skip security prompt. 34 | -n, --dry-run : Test only if the firewall certs can be renewed. 35 | -r, --reboot : Reboot server and firewall finally. 36 | -h, --help : Print this help. 37 | ``` 38 | 39 | - Es wird empfohlen, vor der Erneuerung des Firewallzertifikats zu überprüfen, ob die ursprünglich beim Setup erzeugte Zertifikatskette noch gültig ist und das Zertifikat erneuert werden kann (Option `-n`). 40 | 41 | - Nach erfolgter Zertifikatserneuerung müssen Server und/oder Firewall neu gestartet werden, damit Änderungen wirksam werden. 42 | 43 | - CA-, Server- und Firewallzertifikate können unabhängig voneinander mit unterschiedlicher Gültigkeitsdauer erneuert werden (Option `-c`). 44 | 45 | - Wenn das CA-Zertifikat erneuert wird, müssen zwingend auch Server- und Firewallzertifikate erneuert werden, da diese auf der CA basieren. 46 | 47 | ### Gültigkeitsdauer überprüfen 48 | 49 | - auf dem Server: 50 | 51 | - `openssl x509 -in -noout -text` 52 | 53 | - auf der OPNsense-Firewall: 54 | 55 | - System: Sicherheit: Zertifikate 56 | - System: Zugang: Tester 57 | - Dienste: Squid: Einmalige Anmeldung: Kerberos-Authentifizierung 58 | 59 | ### Links 60 | 61 | - https://github.com/linuxmuster/linuxmuster-base7/issues/158 62 | - https://github.com/linuxmuster/linuxmuster-base7/blob/master/Renew_certs.md 63 | 64 | ## Automatisches Image seeding 65 | 66 | Während des Linbo-Bootvorganges werden automatisch Torrent-Seeder-Prozesse für alle im Cache abgelegten Images gestartet. 67 | 68 | ### Links 69 | 70 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/127 71 | 72 | ## Linbo-Konfiguration (start.conf) 73 | 74 | Syntax der Linbo-start.conf wurde konsolidiert und obsolete Optionen entfernt: `Server` | `SystemType` | `Version` | `Image` | `Boot` | `Hidden` 75 | 76 | Aktuelle Beispiel-start.conf: 77 | 78 | ``` 79 | [LINBO] 80 | Group = ubuntu 81 | Cache = /dev/disk0p3 82 | RootTimeout = 600 83 | AutoPartition = no 84 | AutoFormat = no 85 | AutoInitCache = no 86 | DownloadType = torrent 87 | BackgroundFontColor = white 88 | ConsoleFontColorStdout = lightgreen 89 | ConsoleFontColorStderr = orange 90 | KernelOptions = quiet splash 91 | Locale = de-DE 92 | 93 | [Partition] 94 | Dev = /dev/disk0p1 95 | Label = EFI 96 | Size = 200M 97 | Id = ef 98 | FSType = vfat 99 | Bootable = yes 100 | 101 | [Partition] 102 | Dev = /dev/disk0p2 103 | Label = UBUNTU 104 | Size = 30G 105 | Id = 83 106 | FSType = ext4 107 | 108 | [Partition] 109 | Dev = /dev/disk0p3 110 | Label = CACHE 111 | Size = 112 | Id = 83 113 | FSType = ext4 114 | 115 | [OS] 116 | Name = Ubuntu 117 | Description = Ubuntu 24.04 118 | IconName = ubuntu.svg 119 | BaseImage = noble.qcow2 120 | Root = /dev/disk0p1 121 | Kernel = vmlinuz 122 | Initrd = initrd.img 123 | Append = ro splash 124 | StartEnabled = yes 125 | SyncEnabled = yes 126 | NewEnabled = yes 127 | Autostart = no 128 | AutostartTimeout = 5 129 | DefaultAction = sync 130 | ``` 131 | 132 | ### Links 133 | 134 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/132 135 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/131 136 | 137 | ## Hardware-Info / verbessertes Logging 138 | 139 | Mit dem Tool *hwinfo* kann die Hardware-Information des Clients ausgelesen werden. Linbo erstellt pro Client einmalig eine gzippte hwinfo-Datei und lädt sie nach `/srv/linbo/log/_hwinfo.gz` auf den Server. 140 | 141 | Die Konsolen-Ausgaben des Linbo-Clients werden jetzt übersichtlicher in eine Datei geloggt: `/srv/linbo/log/_linbo.log`. 142 | 143 | ### Links 144 | 145 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/117 146 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/123 147 | 148 | ## Einheitliche Partitionsnamen 149 | 150 | Unabhängig vom verbauten Festplattentyp (SATA, NVME etc.) können Partitionen jetzt mit einheitlichen Namen angesprochen werden. 151 | 152 | ### Namensschema: 153 | 154 | - Platte 1: `/dev/disk0` 155 | - Platte 2: `/dev/disk1` 156 | - … 157 | - Partition 1: `/dev/disk0p1` 158 | - Partition 2: `/dev/disk0p2` 159 | - … 160 | 161 | ### Vorgaben 162 | 163 | - Linbo legt beim Bootvorgang entsprechende Symlinks zu den tatsächlichen Devices an. 164 | 165 | - Eine NVME-Disk wird immer als erste Platte (`disk0`) definiert. 166 | 167 | - Eine USB-Platte wird immer als letzte Platte definiert. 168 | 169 | - Aktuelles start.conf-Beispiel siehe [oben](https://github.com/linuxmuster/linuxmuster-base7/blob/master/Neu73.md#linbo-konfiguration-startconf). 170 | 171 | ### Links 172 | 173 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/126 174 | 175 | ## VNC-Server 176 | 177 | - Bei Verwendung des Linbo-Kernel-Parameters `vncserver` wird während des Bootvorgangs ein xvnc-Dienst gestartet. Der Dienst akzeptiert nur Verbindungen von der Server-IP eingehend auf Port 9999. 178 | 179 | - Legt man von seinem PC ausgehend einen SSH-Tunnel durch den Server an `ssh -L 9999::9999 root@` kann man danach direkt per `vncviewer localhost:9999` auf die Linbo-Clientoberfläche zugreifen. 180 | 181 | ### Links 182 | 183 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/104 184 | - https://github.com/linuxmuster/linuxmuster-linbo7#linbo-kernel-parameters 185 | 186 | ## Live-System von ISO booten 187 | 188 | Stellt man eine ISO-Datei von einem Linux-Live-System als Imagedatei bereit, kann diese bei entsprechender Konfiguration von der Linbo-Clientoberfläche aus gestartet werden. 189 | 190 | ### Vorgehensweise 191 | 192 | - ISO-Datei unter `/srv/linbo/images` bereitstellen. 193 | 194 | - Beispiel: `ubuntu-24.04.2-desktop-amd64.iso` 195 | - auf dem Server unter 196 | `/srv/linbo/images/ubuntu-24.04.2-desktop-amd64/ubuntu-24.04.2-desktop-amd64.iso` 197 | ablegen. 198 | - Torrent- und Info-Datei erzeugen mit 199 | `linbo-torrent create ubuntu-24.04.2-desktop-amd64.iso` 200 | 201 | - ISO-Datei auf dem PC mounten und 202 | 203 | - Pfade von Kernel und Initrd herausfinden (liegen in der obigen 204 | Beispiel-ISO im Verzeichnis `casper`). 205 | - Kernel-Append-Parameter herausfinden (unter `boot/grub/grub.cfg` oder `isolinux.cfg`). Die Parameter `splash`, `quiet`, `findiso` und `iso-scan` können weggelassen werden, da sie automatisch erzeugt werden. 206 | 207 | - OS-Abschnitt in die start.conf eintragen, bei `Root` die Cachepartition verwenden: 208 | 209 | ``` 210 | [OS] 211 | Name = Ubuntu (Live) 212 | Description = Ubuntu 24.04.2 Desktop Live 213 | IconName = ubuntu.svg 214 | BaseImage = ubuntu-24.04.2-desktop-amd64.iso 215 | Root = /dev/disk0p3 216 | Kernel = casper/vmlinuz 217 | Initrd = casper/initrd 218 | Append = locales=de_DE.UTF-8 219 | StartEnabled = yes 220 | SyncEnabled = no 221 | NewEnabled = no 222 | Autostart = no 223 | AutostartTimeout = 5 224 | DefaultAction = start 225 | ``` 226 | 227 | - Abschließend auf dem Server `linuxmuster-import-devices` aufrufen. 228 | 229 | ## Sonstiges 230 | 231 | ### Firmware 232 | 233 | Firmware ist in Ubuntu 24.04 zst-komprimiert. Firmware-Dateien können in `/etc/linuxmuster/linbo/firmware` aber wie bisher ohne .zst-Extension angegeben werden. 234 | 235 | ### Kernel 236 | 237 | Verwendete Linbo-Kernelversionen: 238 | 239 | - legacy: 6.1.* (aktuell 6.1.138) 240 | - longterm: 6.12.* (aktuell 6.12.28) 241 | - stable: 6.14.* (aktuell 6.14.6) 242 | - https://github.com/linuxmuster/linuxmuster-linbo7/issues/134 243 | 244 | ### OPNsense-Firewall 245 | 246 | - Version 25 ist jetzt Installationsvoraussetzung. 247 | - Nach dem Setup sind auf der OPNsense unter `Dienste: Unbound DNS: Query Forwarding` die Quad9-DNS-Server eingetragen (9.9.9.9 und 9.9.9.10). Das kann geändert werden, falls andere DNS-Server als Forwarder verwendet werden sollen. 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Linuxmuster-base7 3 |

4 | 5 |

6 | 7 | GitHub release 8 | 9 | 10 | 11 | 12 | 13 | Badge License 14 | 15 | 16 | 17 | 18 | 19 | Community Forum 20 | 21 |

22 | 23 | # Features 24 | Management scripts for linuxmuster.net V7 25 | - Setup of a linuxmuster.net server 26 | - Device import and boot preparation for [Linbo](https://github.com/linuxmuster/linuxmuster-linbo7) 27 | - Subnet import and setup in OPNSense 28 | 29 | Further information is available in the [Docs](https://docs.linuxmuster.net/de/latest/). 30 | 31 | ## Maintainance Details 32 | 33 | Linuxmuster.net official | ![#c5f015](https://via.placeholder.com/15/c5f015/000000?text=+) YES 34 | :---: | :---: 35 | [Community support](https://ask.linuxmuster.net) | ![#c5f015](https://via.placeholder.com/15/c5f015/000000?text=+) YES** 36 | Actively developed | ![#c5f015](https://via.placeholder.com/15/c5f015/000000?text=+) YES 37 | Maintainer organisation | Linuxmuster.net e.V. 38 | Primary maintainer | thomas@linuxmuster.net 39 | 40 | ** The linuxmuster community consits of people who are nice and happy to help. They are not directly involved in the development though, and might not be able to help in any case. 41 | -------------------------------------------------------------------------------- /Renew_certs.md: -------------------------------------------------------------------------------- 1 | # Renew self signed server and firewall certs 2 | 3 | To renew ca, server and firewall certs invoke `linuxmuster-renew-certs` on the server: 4 | ``` 5 | ------------------------------------------------------------------------------ 6 | #### linuxmuster-renew-certs started at 2025-04-09 18:05:42 #### 7 | ------------------------------------------------------------------------------ 8 | Usage: linuxmuster-renew-certs [options] 9 | [options] may be: 10 | -c , --certs= : Comma separated list of certificates to be renewed 11 | ("ca", "server" and/or "firewall" or "all"). 12 | -d <#>, --days=<#> : Set number of days (default: 7305). 13 | -f, --force : Skip security prompt. 14 | -n, --dry-run : Test only if the firewall certs can be renewed. 15 | -r, --reboot : Reboot server and firewall finally. 16 | -h, --help : Print this help. 17 | ``` 18 | 19 | Note: 20 | - It is recommended to check beforehand whether the current firewall certificates were originally created by linuxmuster-setup and are therefore renewable. To do this, use the option `-n`. 21 | - You need to restart both server and firewall to apply the renewed certificates. 22 | - You may renew all certificates seperately with different validity periods (see option `-c`). 23 | - Note: If you renew the ca certificate, you have to renew also all other certificates, which depend on it. 24 | - After the firewall has rebooted login to the OPNsense Web-UI and navigate to 25 | - `System: Trust: Authorities` and `System: Trust: Certificates` to see if the certificates have been renewed correctly, 26 | - `System: Access: Tester` to test authentication against the linuxmuster server and 27 | - `Services: Squid Web Proxy: Single Sign-On: Kerberos Authentication` to test the "Kerberos login". 28 | -------------------------------------------------------------------------------- /buildpackage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | fakeroot dpkg-buildpackage \ 4 | -tc -sn -us -uc \ 5 | -I".git" \ 6 | -I".github" \ 7 | -I".directory" 8 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | This package provides configuration scripts for linuxmuster.net. 2 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: linuxmuster-base7 2 | Section: linuxmuster 3 | Priority: optional 4 | Maintainer: Thomas Schmitt 5 | Build-Depends: debhelper (>= 5.0.0) 6 | Standards-Version: 5.0.0 7 | 8 | Package: linuxmuster-base7 9 | Architecture: all 10 | Conflicts: dnsmasq-base 11 | Pre-Depends: python3, linuxmuster-common 12 | Depends: coreutils, cups, printer-driver-cups-pdf, isc-dhcp-server, python3-bcrypt, python3-bs4, 13 | python3-lxml, python3-ipy, python3-apt, python3-netifaces, python3-dialog, python3-ldap3, 14 | python3-netaddr, python3-pip, python3-requests, openssl, ntp, ntpdate, samba, samba-dsdb-modules, 15 | smbclient, ldb-tools, krb5-user, linuxmuster-linbo7, linuxmuster-webui7, 16 | sophomorix-samba, tmux 17 | Description: linuxmuster.net configuration scripts 18 | This package provides configuration scripts for linuxmuster.net 19 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: linuxmuster-base7 3 | Source: https://github.com/linuxmuster/linuxmuster-base7 4 | 5 | Files: * 6 | Copyright: 2018 Thomas Schmitt 7 | License: GPL-3+ 8 | 9 | License: GPL-3+ 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | . 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | . 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | . 23 | On Debian systems, the complete text of the GNU General Public 24 | License version 3 can be found in "/usr/share/common-licenses/GPL-3". 25 | 26 | License: LGPL-3+ 27 | This library is free software: you can redistribute it and/or 28 | modify it under the terms of the GNU Lesser General Public 29 | License as published by the Free Software Foundation; either 30 | version 3 of the License, or (at your option) any later version. 31 | . 32 | This library is distributed in the hope that it will be useful, 33 | but WITHOUT ANY WARRANTY; without even the implied warranty of 34 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 35 | Lesser General Public License for more details. 36 | . 37 | You should have received a copy of the GNU Lesser General Public 38 | License along with this library. If not, see 39 | . 40 | . 41 | On Debian systems, the complete text of the GNU Lesser General 42 | Public License version 3 can be found in 43 | "/usr/share/common-licenses/LGPL-3". 44 | -------------------------------------------------------------------------------- /debian/devices.csv.5: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" First parameter, NAME, should be all caps 3 | .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection 4 | .\" other parameters are allowed: see man(7), man(1) 5 | .TH DEVICES.CSV 5 "May 08, 2018" 6 | .\" Please adjust this date whenever revising the manpage. 7 | .\" 8 | .\" Some roff macros, for reference: 9 | .\" .nh disable hyphenation 10 | .\" .hy enable hyphenation 11 | .\" .ad l left justify 12 | .\" .ad b justify to both left and right margins 13 | .\" .nf disable filling 14 | .\" .fi enable filling 15 | .\" .br insert line break 16 | .\" .sp insert n+1 empty lines 17 | .\" for manpage-specific macros, see man(7) 18 | .SH NAME 19 | 20 | Configuration files for network devices: 21 | 22 | default-school: 23 | .br 24 | /etc/linuxmuster/sophomorix/default-school/devices.csv 25 | 26 | Other : 27 | .br 28 | /etc/linuxmuster/sophomorix//.devices.csv 29 | 30 | .br 31 | .SH DESCRIPTION 32 | This data is used to configure various services (DHCP, DNS, Linbo etc.) by 33 | processing them through the linuxmuster-import-devices script. 34 | 35 | As part of this processing, sophomorix-device takes care of the entries in the 36 | Active Directory of Samba 4 (DNS, computer accounts, groups etc.) 37 | 38 | .PP 39 | .SH FIELDS 40 | 41 | The following 15 fields have to be on one row separated by semicolons: 42 | .TP 43 | .B Field 1: room name 44 | .br 45 | creates a group with sophomorixType 'room' 46 | .TP 47 | .B Field 2: hostname 48 | .br 49 | creates a dnsNode and (depending on sophomorixRole) a computer 50 | account and a printer group (if sophomorixRole is printer). 51 | .TP 52 | .B Field 3: device group (formerly known as hardwareclass) 53 | .br 54 | created as sophomorixType 'hardwareclass' 55 | .TP 56 | .B Field 4: mac address 57 | .TP 58 | .B Field 5: client ip (IPv4) 59 | .TP 60 | .B Field 6: ms office key 61 | .TP 62 | .B Field 7: ms windows key 63 | .TP 64 | .B Field 8: supplemental dhcp options, comma separated 65 | .TP 66 | .B Field 9: sophomorixRole 67 | .br 68 | Only certain rolenames are valid. 69 | .TP 70 | .B Field 10: reserved by linuxmuster.net 71 | .TP 72 | .B Field 11: pxe flag 73 | .br 74 | Possible values 0, 1: 75 | .br 76 | 0: no pxe, 1: linbo pxe 77 | .TP 78 | .B Field 12: reserved by linuxmuster.net 79 | .TP 80 | .B Field 13: reserved by linuxmuster.net 81 | .TP 82 | .B Field 14: reserved by linuxmuster.net 83 | .TP 84 | .B Field 15: sophomorixComment 85 | .br 86 | comment field sophomorixComment for computer account. 87 | 88 | .SH Notes: 89 | 90 | .B room name 91 | .br 92 | Apart from the 'default-school', room names are given a prefix: '-'. 93 | 94 | .B hostname 95 | .br 96 | Apart from the 'default-school', dnsNode names and computer accounts are given 97 | a prefix: '-'. 98 | 99 | Computer account names consist of capital letters followed by $: 100 | hostname 'a102pc1' will be dnsNode 'a102pc1' and computer 101 | account 'A102PC1$'. 102 | 103 | .B device groups (hardwareclass) 104 | .br 105 | do NOT get a prefix. They are globally valid. 106 | 107 | .B sophomorixRole 108 | .br 109 | There are only predefined values, see 110 | 111 | sophomorix-samba --show-roletype 112 | 113 | sophomorixRole determines, whether a computer account will be created or not. 114 | 115 | .B Other fields ... 116 | .br 117 | Further fields after position 16 are allowed and can be synchronized to AD by 118 | configuration in the future. 119 | 120 | 121 | .SH SEE ALSO 122 | .BR sophomorix-device (8), 123 | 124 | .\".BR baz (1). 125 | .\".br 126 | .\"You can see the full options of the programs by calling for example 127 | .\".IR "sophomrix-useradd -h" , 128 | . 129 | .SH AUTHOR 130 | Written by , translated and amended by . 131 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | etc/linuxmuster/ssl 2 | etc/linuxmuster/.secret 3 | usr/share/linuxmuster/templates 4 | var/cache/linuxmuster 5 | var/lib/linuxmuster/hooks/device-import.post.d 6 | var/log/linuxmuster 7 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | etc/* etc/ 2 | lib/* usr/lib/linuxmuster/ 3 | sbin/* usr/sbin/ 4 | share/* usr/share/linuxmuster/ 5 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # postinst script for linuxmuster-base 4 | # thomas@linuxmuster.net 5 | # 20250407 6 | # GPL v3 7 | # 8 | 9 | # see: dh_installdeb(1) 10 | 11 | set -e 12 | 13 | # summary of how this script can be called: 14 | # * `configure' 15 | # * `abort-upgrade' 16 | # * `abort-remove' `in-favour' 17 | # 18 | # * `abort-deconfigure' `in-favour' 19 | # `removing' 20 | # 21 | # for details, see http://www.debian.org/doc/debian-policy/ or 22 | # the debian-policy package 23 | # 24 | 25 | # get environment 26 | source /etc/os-release || exit 1 27 | source /usr/share/linuxmuster/environment.sh || exit 1 28 | datestr="$(date +%Y%m%d%H%M)" 29 | 30 | # os version specific 31 | case "$VERSION_ID" in 32 | 24.04) ntpgrp="ntpsec" ;; 33 | 22.04) ntpgrp="ntp" ;; 34 | *) echo "Wrong os version $VERSION_ID." ; exit 1 ;; 35 | esac 36 | 37 | 38 | case "$1" in 39 | 40 | configure) 41 | 42 | # ensure python3 paramiko and reconfigure are installed 43 | /usr/bin/python3 -m pip install paramiko reconfigure --break-system-packages 44 | 45 | # add lmn paths to python environment 46 | usersite="$(python3 -c 'import site; site._script()' --user-site)" 47 | userpth="$usersite/lmn.pth" 48 | mkdir -p "$usersite" 49 | echo "$LIBDIR" > "$userpth" 50 | echo "$SETUPDIR" >> "$userpth" 51 | 52 | # clean and bytecompile modules 53 | find "$LIBDIR" -name \*.pyc -exec rm '{}' \; 54 | find "$LIBDIR" -type d -name __pycache__ -exec rm -r '{}' \; 2> /dev/null || true 55 | python3 -m compileall "$LIBDIR" 56 | 57 | # create ssl-cert group 58 | groupadd --force --system ssl-cert 59 | 60 | # set permissions 61 | # linuxmuster ssl certs 62 | chgrp ssl-cert "$SSLDIR" -R 63 | chmod 750 "$SSLDIR" 64 | # linuxmuster secrets directory 65 | for i in "$SECRETDIR" "$BINDUSERSECRET" "$DNSADMINSECRET"; do 66 | [ -e "$i" ] && chgrp dhcpd "$i" 67 | if [ -d "$i" ]; then 68 | chmod 750 "$i" 69 | else 70 | [ -e "$i" ] && chmod 440 "$i" 71 | fi 72 | done 73 | # samba sysvol directory 74 | sysvol="/var/lib/samba/sysvol" 75 | [ -d "$sysvol" ] && find "$sysvol" -type d -exec chmod 775 '{}' \; 76 | # samba ntp socket directory 77 | mkdir -p "$NTPSOCKDIR" 78 | chgrp "$ntpgrp" "$NTPSOCKDIR" 79 | chmod 750 "$NTPSOCKDIR" 80 | 81 | 82 | # skip subsequent actions on configured systems 83 | [ -s "$SETUPINI" ] || exit 0 84 | 85 | # provide cacert.pem for clients if not present 86 | if [ -n "$domainname" -a -s "$CACERT" ]; then 87 | sysvoltlsdir="$(echo "$SYSVOLTLSDIR" | sed -e 's|@@domainname@@|'"$domainname"'|')" 88 | sysvolpemfile="$sysvoltlsdir/$(basename "$CACERT")" 89 | [ -d "$sysvoltlsdir" ] || mkdir -p "$sysvoltlsdir" 90 | if [ -d "$sysvoltlsdir" -a ! -e "$sysvolpemfile" ]; then 91 | echo "Providing $sysvolpemfile." 92 | cp "$CACERT" "$sysvolpemfile" 93 | fi 94 | fi 95 | 96 | # update certificate creation configs 97 | for tpl in "$TPLDIR"/*_cert_ext.cnf; do 98 | conf="$(head -1 "$tpl" | cut -d' ' -f2)" 99 | if [ ! -e "$conf" ] || ! grep -q subjectAltName "$conf"; then 100 | echo "Updating $conf." 101 | sed -e "s|@@serverip@@|$serverip| 102 | s|@@firewallip@@|$firewallip| 103 | s|@@servername@@|$servername|g 104 | s|@@domainname@@|$domainname|" "$tpl" > "$conf" 105 | fi 106 | done 107 | 108 | # update apparmor profiles 109 | for tpl in "$TPLDIR"/*.apparmor.d; do 110 | conf="$(head -1 "$tpl" | cut -d' ' -f2)" 111 | [ -s "$conf" ] || continue 112 | tpl_ver="$(grep -P '(?&2 151 | exit 1 152 | ;; 153 | 154 | esac 155 | 156 | # dh_installdeb will replace this with shell code automatically 157 | # generated by other debhelper scripts. 158 | 159 | #DEBHELPER# 160 | 161 | exit 0 162 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # Sample debian/rules that uses debhelper. 3 | # GNU copyright 1997 by Joey Hess. 4 | # 5 | # This version is for a hypothetical package that builds an 6 | # architecture-dependant package, as well as an architecture-independent 7 | # package. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | # This is the debhelper compatibility version to use. 13 | export DH_COMPAT=10 14 | 15 | #ifneq (,$(findstring debug,$(DEB_BUILD_OPTIONS))) 16 | # CFLAGS += -g 17 | #endif 18 | #ifeq (,$(findstring nostrip,$(DEB_BUILD_OPTIONS))) 19 | # INSTALL_PROGRAM += -s 20 | #endif 21 | 22 | configure: configure-stamp 23 | configure-stamp: 24 | dh_testdir 25 | # Add here commands to configure the package. 26 | 27 | touch configure-stamp 28 | 29 | 30 | build-arch: configure-stamp build-arch-stamp 31 | build-arch-stamp: 32 | dh_testdir 33 | 34 | # Add here command to compile/build the package. 35 | #$(MAKE) 36 | 37 | touch build-arch-stamp 38 | 39 | build-indep: configure-stamp build-indep-stamp 40 | build-indep-stamp: 41 | dh_testdir 42 | 43 | # Add here command to compile/build the arch indep package. 44 | # It's ok not to do anything here, if you don't need to build 45 | # anything for this package. 46 | 47 | touch build-indep-stamp 48 | 49 | build: build-arch build-indep 50 | 51 | clean: clean1 52 | clean1: 53 | dh_testdir 54 | dh_testroot 55 | rm -f build-indep-stamp build-arch-stamp configure-stamp 56 | 57 | # Add here commands to clean up after the build process. 58 | #-$(MAKE) clean 59 | 60 | dh_clean 61 | 62 | 63 | install: DH_OPTIONS= 64 | install: build 65 | dh_testdir 66 | dh_testroot 67 | dh_prep 68 | dh_installdirs 69 | 70 | # Add here commands to install the package. 71 | dh_install 72 | # remove unnecessary dirs 73 | rm -rf `find debian/linuxmuster-base7 -name .svn` 74 | rm -f `find debian/linuxmuster-base7 -name .directory` 75 | 76 | # Build architecture-independent files here. 77 | # Pass -i to all debhelper commands in this target to reduce clutter. 78 | binary-indep: build install 79 | dh_testdir 80 | dh_testroot 81 | dh_installdirs 82 | dh_installdebconf 83 | dh_installdocs 84 | # dh_installexamples -i 85 | # dh_installmenu -i 86 | dh_installlogrotate -i 87 | # dh_installemacsen -i 88 | # dh_installpam -i 89 | # dh_installmime -i 90 | dh_installinit -i --no-start 91 | dh_installcron -i 92 | dh_installman -i debian/devices.csv.5 93 | # dh_installinfo -i 94 | # dh_undocumented -i 95 | dh_installchangelogs 96 | dh_link 97 | dh_compress 98 | dh_fixperms 99 | dh_installdeb 100 | # dh_perl -i 101 | dh_gencontrol 102 | dh_md5sums 103 | dh_builddeb 104 | 105 | # Build architecture-dependent files here. 106 | binary-arch: install 107 | # We have nothing to do by default. 108 | 109 | 110 | binary: binary-indep binary-arch 111 | .PHONY: build clean binary-indep binary-arch binary install configure clean1 112 | -------------------------------------------------------------------------------- /etc/linuxmuster/codename: -------------------------------------------------------------------------------- 1 | beta 2 | -------------------------------------------------------------------------------- /etc/profile.d/Z99-linuxmuster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # /etc/profile.d/Z99-linuxmuster.sh 3 | # 4 | # thomas@linuxmuster.net 5 | # credits to joanna 6 | # 20220530 7 | # 8 | 9 | upSeconds="$(/usr/bin/cut -d. -f1 /proc/uptime)" 10 | secs=$((${upSeconds}%60)) 11 | mins=$((${upSeconds}/60%60)) 12 | hours=$((${upSeconds}/3600%24)) 13 | days=$((${upSeconds}/86400)) 14 | UPTIME=`printf "%d days, %02dh%02dm%02ds" "$days" "$hours" "$mins" "$sec"` 15 | MEM=`free -m | awk 'NR==2{printf "%s/%sMB (%.2f", $3,$2,$3*100/$2 }'` 16 | IPint=`ip a | grep glo | awk '{print $2}' | head -1 | cut -f1 -d/` 17 | IPext=`wget -q -O - http://ipinfo.io/ip | tail` 18 | VERSIONbase7=`dpkg --status linuxmuster-base7 | grep ^Version | awk '{print $2}'` 19 | VERSIONlmn=`echo $VERSIONbase7 | awk -F\. '{print $1 "." $2}'` 20 | CODEname=`cat /etc/linuxmuster/codename` 21 | VERSIONlinbo7=`dpkg --status linuxmuster-linbo7 | grep ^Version | awk '{print $2}'` 22 | VERSIONwebui7=`dpkg --status linuxmuster-webui7 | grep ^Version | awk '{print $2}'` 23 | VERSIONsophomorix=`dpkg --status sophomorix-samba | grep ^Version | awk '{print $2}'` 24 | 25 | 26 | printf "\e[38;5;208m 27 | ███ ███ \e[4m\e[38;5;15mWELCOME TO LINUXMUSTER.NET ${VERSIONlmn} - ${CODEname}\e[38;5;208m\e[24m 28 | █████ █████ \e[38;5;15m `date +"%A, %e %B %Y, %T"`\e[38;5;208m 29 | ███ ███ 30 | ███ ███ \e[38;5;15m Uptime..........: \e[38;5;208m${UPTIME}\e[38;5;208m 31 | █████ █████ \e[38;5;15m Memory..........: \e[38;5;208m${MEM}"%%\)"\e[38;5;208m 32 | ███ ███ \e[38;5;15m IP Internal.....: \e[38;5;208m${IPint}\e[38;5;24m 33 | ███ \e[38;5;15m IP External.....: \e[38;5;208m${IPext}\e[38;5;24m 34 | █████ 35 | ███ \e[38;5;208m 36 | ███ ███ \e[38;5;15m linuxmuster.net packages:\e[38;5;208m 37 | █████ █████ \e[38;5;15m -Base...........: \e[38;5;208m${VERSIONbase7} 38 | ███ ███ \e[38;5;15m -Linbo..........: \e[38;5;208m${VERSIONlinbo7}\e[38;5;208m 39 | ███ ███ \e[38;5;15m -WebUI..........: \e[38;5;208m${VERSIONwebui7}\e[38;5;208m 40 | █████ █████ \e[38;5;15m -Sophomorix.....: \e[38;5;208m${VERSIONsophomorix}\e[38;5;208m 41 | ███ ███ 42 | \e[0m 43 | " 44 | -------------------------------------------------------------------------------- /get-depends.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # thomas@linuxmuster.net 4 | # 20250327 5 | # 6 | 7 | set -e 8 | 9 | SUDO="$(which sudo)" 10 | if [ -z "$SUDO" ]; then 11 | echo "Please install sudo!" 12 | exit 1 13 | fi 14 | 15 | PKGNAME="linuxmuster-base7" 16 | CONTROL_URL="https://raw.githubusercontent.com/linuxmuster/$PKGNAME/main/debian/control" 17 | 18 | echo "###############################################" 19 | echo "# Installing $PKGNAME build depends #" 20 | echo "###############################################" 21 | echo 22 | 23 | if [ ! -e debian/control ]; then 24 | echo "debian/control not found!" 25 | exit 26 | fi 27 | 28 | if ! grep -q "Source: $PKGNAME" debian/control; then 29 | echo "This is no $PKGNAME source tree!" 30 | exit 31 | fi 32 | 33 | # install prerequisites 34 | $SUDO apt-get update 35 | $SUDO apt-get -y install bash bash-completion build-essential curl debhelper dpkg-dev || exit 1 36 | 37 | # install build depends 38 | BUILDDEPENDS="$(curl -s $CONTROL_URL | grep -v ^Standards-Version | sed -n '/Build-Depends:/,/Package:/p' | grep -v ^Package | sed -e 's|^Build-Depends: ||' | sed -e 's| (.*)||g' | sed -e 's|,||g')" 39 | $SUDO apt-get -y install $BUILDDEPENDS || exit 1 40 | -------------------------------------------------------------------------------- /lib/dhcpd-update-samba-dns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # adds/updates/removes A DNS records 4 | # thomas@linuxmuster.net 5 | # 20220119 6 | # 7 | # usage: dhcpd-update-samba-dns.py 8 | # 9 | 10 | import socket 11 | import sys 12 | 13 | from functions import isDynamicIpDevice 14 | from functions import isValidHostname 15 | from functions import isValidHostIpv4 16 | from functions import sambaTool 17 | 18 | cmd = '' 19 | ip = '' 20 | hostname = '' 21 | skipad = '' 22 | 23 | # get arguments 24 | cmd, ip, hostname, skipad = sys.argv[1:] 25 | 26 | # check arguments 27 | if cmd not in ['add', 'delete']: 28 | sys.exit(1) 29 | if not isValidHostIpv4(ip): 30 | sys.exit(1) 31 | if not isValidHostname(hostname): 32 | sys.exit(1) 33 | 34 | # no action for pxclient 35 | if hostname.lower() == 'pxeclient': 36 | sys.exit(0) 37 | 38 | # check if it is a dynamic ip device, skipped if skipad is set to yes 39 | # (see /etc/dhcp/events.conf) 40 | if skipad != 'yes': 41 | if not isDynamicIpDevice(hostname): 42 | sys.exit(0) 43 | 44 | # test if there are already valid dns records for this host 45 | try: 46 | ip_resolved = socket.gethostbyname(hostname) 47 | except: 48 | ip_resolved = '' 49 | try: 50 | name_resolved = socket.gethostbyaddr(ip)[0].split('.')[0] 51 | except: 52 | name_resolved = '' 53 | if cmd == 'add' and ip == ip_resolved and hostname == name_resolved: 54 | print('DNS records for host ' + hostname 55 | + ' with ip ' + ip + ' are already up-to-date.') 56 | sys.exit(0) 57 | 58 | # delete existing dns records if there are any 59 | domainname = socket.getfqdn().split('.', 1)[1] 60 | fqdn = hostname + '.' + domainname 61 | for item in ip_resolved, ip: 62 | if item == '': 63 | continue 64 | if sambaTool('dns delete localhost ' + domainname + ' ' + hostname + ' A ' + item): 65 | print('Deleted A record for ' + fqdn + ' -> ' + item + '.') 66 | oc1, oc2, oc3, oc4 = item.split('.') 67 | zone = oc3 + '.' + oc2 + '.' + oc1 + '.in-addr.arpa' 68 | if sambaTool('dns delete localhost ' + zone + ' ' + oc4 + ' PTR ' + fqdn): 69 | print('Deleted PTR record for ' + item + ' -> ' + fqdn + '.') 70 | 71 | # in case of deletion job is already done 72 | if cmd == 'delete': 73 | sys.exit(0) 74 | 75 | # add dns A record 76 | try: 77 | sambaTool('dns add localhost ' + domainname + ' ' + hostname + ' A ' + ip) 78 | print('Added A record for ' + fqdn + '.') 79 | except: 80 | print('Failed to add A record for ' + fqdn + '.') 81 | sys.exit(1) 82 | 83 | # add dns zone if necessary 84 | if not sambaTool('dns zoneinfo localhost ' + zone): 85 | try: 86 | sambaTool('dns zonecreate localhost ' + zone) 87 | print('Created dns zone ' + zone + '.') 88 | except: 89 | print('Failed to create zone ' + zone + '.') 90 | sys.exit(1) 91 | 92 | # add dns PTR record 93 | try: 94 | sambaTool('dns add localhost ' + zone + ' ' + oc4 + ' PTR ' + fqdn) 95 | print('Added PTR record for ' + ip + '.') 96 | except: 97 | print('Failed to add PTR record for ' + ip + '.') 98 | sys.exit(1) 99 | -------------------------------------------------------------------------------- /lib/setup.d/a_ini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # process setup ini files 4 | # thomas@linuxmuster.net 5 | # 20220105 6 | # 7 | 8 | import configparser 9 | import environment 10 | import os 11 | import sys 12 | 13 | from functions import isValidHostname, isValidDomainname, isValidHostIpv4 14 | from functions import mySetupLogfile, printScript, randomPassword, subProc 15 | from IPy import IP 16 | 17 | logfile = mySetupLogfile(__file__) 18 | 19 | # read ini files 20 | setup = configparser.RawConfigParser( 21 | delimiters=('='), inline_comment_prefixes=('#', ';')) 22 | for item in [environment.DEFAULTSINI, environment.PREPINI, environment.SETUPINI, environment.CUSTOMINI]: 23 | # skip non existant file 24 | if not os.path.isfile(item): 25 | continue 26 | # reading setup values 27 | msg = 'Reading ' + item + ' ' 28 | printScript(msg, '', False, False, True) 29 | try: 30 | setup.read(item) 31 | printScript(' Success!', '', True, True, False, len(msg)) 32 | except: 33 | printScript(' Failed!', '', True, True, False, len(msg)) 34 | sys.exit(1) 35 | 36 | # compute missing values 37 | # from domainname 38 | msg = '* Domainname ' 39 | printScript(msg, '', False, False, True) 40 | try: 41 | domainname = setup.get('setup', 'domainname') 42 | if not isValidDomainname(domainname): 43 | printScript(' ' + domainname + ' is not valid!', 44 | '', True, True, False, len(msg)) 45 | sys.exit(1) 46 | printScript(' ' + domainname, '', True, True, False, len(msg)) 47 | except: 48 | printScript(' not set!', '', True, True, False, len(msg)) 49 | sys.exit(1) 50 | 51 | # derive values from domainname 52 | # realm 53 | setup.set('setup', 'realm', domainname.upper()) 54 | # sambadomain 55 | setup.set('setup', 'sambadomain', domainname.split('.')[0].upper()) 56 | # basedn 57 | basedn = '' 58 | for item in domainname.split('.'): 59 | basedn = basedn + 'DC=' + item + ',' 60 | setup.set('setup', 'basedn', basedn[:-1]) 61 | 62 | # servername 63 | msg = '* Servername ' 64 | printScript(msg, '', False, False, True) 65 | servername = '_' 66 | if 'servername' in setup['setup']: 67 | servername = setup.get('setup', 'servername') 68 | elif 'hostname' in setup['setup']: 69 | servername = setup.get('setup', 'hostname') 70 | if not isValidHostname(servername): 71 | printScript(' servername ' + servername + ' is not valid!', 72 | '', True, True, False, len(msg)) 73 | sys.exit(1) 74 | printScript(' ' + servername, '', True, True, False, len(msg)) 75 | setup.set('setup', 'servername', servername) 76 | 77 | # derive values from servername 78 | # netbiosname 79 | setup.set('setup', 'netbiosname', servername.upper()) 80 | 81 | # serverip 82 | msg = '* Server-IP ' 83 | printScript(msg, '', False, False, True) 84 | try: 85 | serverip = setup.get('setup', 'serverip') 86 | if not isValidHostIpv4(serverip): 87 | printScript(' ' + serverip + ' is not valid!', 88 | '', True, True, False, len(msg)) 89 | sys.exit(1) 90 | printScript(' ' + serverip, '', True, True, False, len(msg)) 91 | except: 92 | printScript(' not set!', '', True, True, False, len(msg)) 93 | sys.exit(1) 94 | 95 | # netmask 96 | msg = '* Bitmask ' 97 | printScript(msg, '', False, False, True) 98 | try: 99 | bitmask = setup.get('setup', 'bitmask') 100 | ip = IP(serverip + '/' + bitmask, make_net=True) 101 | except: 102 | printScript(' ' + bitmask + ' is not valid!', 103 | '', True, True, False, len(msg)) 104 | sys.exit(1) 105 | printScript(' ' + bitmask, '', True, True, False, len(msg)) 106 | 107 | # derive values from bitmask 108 | # netmask 109 | setup.set('setup', 'netmask', ip.netmask().strNormal(0)) 110 | # network address 111 | setup.set('setup', 'network', IP(ip).strNormal(0)) 112 | # broadcast address 113 | setup.set('setup', 'broadcast', ip.broadcast().strNormal(0)) 114 | 115 | # dhcprange 116 | msg = '* DHCP range ' 117 | printScript(msg, '', False, False, True) 118 | try: 119 | dhcprange = setup.get('setup', 'dhcprange') 120 | dhcprange1 = dhcprange.split(' ')[0] 121 | dhcprange2 = dhcprange.split(' ')[1] 122 | if not isValidHostIpv4(dhcprange1) and not isValidHostIpv4(dhcprange2): 123 | dhcprange = '' 124 | except: 125 | dhcprange = '' 126 | if dhcprange == '': 127 | try: 128 | if int(bitmask) <= 16: 129 | dhcprange1 = serverip.split( 130 | '.')[0] + '.' + serverip.split('.')[1] + '.255.1' 131 | dhcprange2 = serverip.split( 132 | '.')[0] + '.' + serverip.split('.')[1] + '.255.254' 133 | else: 134 | dhcprange1 = serverip.split('.')[ 135 | 0] + '.' + serverip.split('.')[1] + '.' + serverip.split('.')[2] + '.' + '201' 136 | dhcprange2 = serverip.split('.')[ 137 | 0] + '.' + serverip.split('.')[1] + '.' + serverip.split('.')[2] + '.' + '250' 138 | dhcprange = dhcprange1 + ' ' + dhcprange2 139 | setup.set('setup', 'dhcprange', dhcprange) 140 | except: 141 | printScript(' failed to set!', '', True, True, False, len(msg)) 142 | sys.exit(1) 143 | printScript(' ' + dhcprange1 + '-' + dhcprange2, 144 | '', True, True, False, len(msg)) 145 | 146 | # firewallip 147 | msg = '* Firewall IP ' 148 | printScript(msg, '', False, False, True) 149 | try: 150 | firewallip = setup.get('setup', 'firewallip') 151 | if not isValidHostIpv4(firewallip): 152 | printScript(' ' + firewallip + ' is not valid!', 153 | '', True, True, False, len(msg)) 154 | sys.exit(1) 155 | printScript(' ' + firewallip, '', True, True, False, len(msg)) 156 | except: 157 | printScript(' not set!', '', True, True, False, len(msg)) 158 | sys.exit(1) 159 | 160 | # create global binduser password 161 | msg = 'Creating global binduser secret ' 162 | printScript(msg, '', False, False, True) 163 | try: 164 | binduserpw = randomPassword(16) 165 | with open(environment.BINDUSERSECRET, 'w') as secret: 166 | secret.write(binduserpw) 167 | subProc('chmod 440 ' + environment.BINDUSERSECRET, logfile) 168 | subProc('chgrp dhcpd ' + environment.BINDUSERSECRET, logfile) 169 | printScript(' Success!', '', True, True, False, len(msg)) 170 | except: 171 | printScript(' Failed!', '', True, True, False, len(msg)) 172 | sys.exit(1) 173 | 174 | # write setup.ini finally 175 | msg = 'Writing setup ini file ' 176 | printScript(msg, '', False, False, True) 177 | try: 178 | with open(environment.SETUPINI, 'w') as outfile: 179 | setup.write(outfile) 180 | subProc('chmod 600 ' + environment.SETUPINI, logfile) 181 | # temporary setup.ini for transfering it later to additional vms 182 | setup.set('setup', 'binduserpw', binduserpw) 183 | setup.set('setup', 'adminpw', '') 184 | with open('/tmp/setup.ini', 'w') as outfile: 185 | setup.write(outfile) 186 | subProc('chmod 600 /tmp/setup.ini', logfile) 187 | printScript(' Success!', '', True, True, False, len(msg)) 188 | except: 189 | printScript(' Failed!', '', True, True, False, len(msg)) 190 | sys.exit(1) 191 | 192 | # delete obsolete ini files 193 | for item in [environment.CUSTOMINI, environment.PREPINI]: 194 | if os.path.isfile(item): 195 | os.unlink(item) 196 | -------------------------------------------------------------------------------- /lib/setup.d/c_general-dialog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # general setup 4 | # thomas@linuxmuster.net 5 | # 20240515 6 | # 7 | 8 | import environment 9 | import os 10 | import sys 11 | import configparser 12 | 13 | from dialog import Dialog 14 | from functions import detectedInterfaces, isValidHostname, isValidDomainname 15 | from functions import isValidHostIpv4, isValidPassword, mySetupLogfile 16 | from functions import printScript, subProc 17 | from IPy import IP 18 | 19 | logfile = mySetupLogfile(__file__) 20 | 21 | # read setup ini 22 | msg = 'Reading setup data ' 23 | printScript(msg, '', False, False, True) 24 | setupini = environment.SETUPINI 25 | try: 26 | setup = configparser.RawConfigParser( 27 | delimiters=('='), inline_comment_prefixes=('#', ';')) 28 | setup.read(setupini) 29 | serverip = setup.get('setup', 'serverip') 30 | servername = setup.get('setup', 'servername') 31 | domainname = setup.get('setup', 'domainname') 32 | dhcprange = setup.get('setup', 'dhcprange') 33 | printScript(' Success!', '', True, True, False, len(msg)) 34 | except: 35 | printScript(' Failed!', '', True, True, False, len(msg)) 36 | sys.exit(1) 37 | 38 | # get network interfaces 39 | # iface_list, iface_default = detectedInterfaces() 40 | 41 | # begin dialog 42 | title = 'linuxmuster.net 7.2: Setup for ' + \ 43 | servername + '.' + domainname + '\n\n' 44 | dialog = Dialog(dialog="dialog") 45 | dialog.set_background_title(title) 46 | button_names = {dialog.OK: "OK", 47 | dialog.CANCEL: "Cancel"} 48 | 49 | 50 | # servername 51 | ititle = title + ': Servername' 52 | while True: 53 | rc, servername = dialog.inputbox('Enter the hostname of the main server:', 54 | title=ititle, height=16, width=64, init=setup.get('setup', 'servername')) 55 | if rc == 'cancel': 56 | sys.exit(1) 57 | if isValidHostname(servername): 58 | break 59 | 60 | print('Server hostname: ' + servername) 61 | setup.set('setup', 'servername', servername) 62 | setup.set('setup', 'hostname', servername) 63 | netbiosname = servername.upper() 64 | print('Netbios name: ' + netbiosname) 65 | setup.set('setup', 'netbiosname', netbiosname) 66 | 67 | 68 | # domainname 69 | ititle = title + ': Domainname' 70 | while True: 71 | rc, domainname = dialog.inputbox( 72 | 'Note that the first part of the domain name is used automatically as samba domain (maximal 15 characters using a-z and "-"). Use a prepending "linuxmuster" if your domain has more characters. Enter the internet domain name:', title=ititle, height=16, width=64, init=domainname) 73 | if rc == 'cancel': 74 | sys.exit(1) 75 | if isValidDomainname(domainname): 76 | break 77 | 78 | print('Domain name: ' + domainname) 79 | setup.set('setup', 'domainname', domainname) 80 | basedn = 'DC=' + domainname.replace('.', ',DC=') 81 | print('BaseDN: ' + basedn) 82 | setup.set('setup', 'basedn', basedn) 83 | realm = domainname.upper() 84 | print('REALM: ' + realm) 85 | setup.set('setup', 'realm', realm) 86 | sambadomain = realm.split('.')[0] 87 | print('Sambadomain: ' + sambadomain) 88 | setup.set('setup', 'sambadomain', sambadomain) 89 | 90 | 91 | # dhcprange 92 | ititle = title + ': DHCP Range' 93 | dhcprange1 = dhcprange.split(' ')[0] 94 | dhcprange2 = dhcprange.split(' ')[1] 95 | if dhcprange1 == '': 96 | dhcprange1 = serverip.split('.')[ 97 | 0] + '.' + serverip.split('.')[1] + '.' + serverip.split('.')[2] + '.' + '100' 98 | if dhcprange2 == '': 99 | dhcprange2 = serverip.split('.')[ 100 | 0] + '.' + serverip.split('.')[1] + '.' + serverip.split('.')[2] + '.' + '200' 101 | dhcprange = dhcprange1 + ' ' + dhcprange2 102 | while True: 103 | rc, dhcprange = dialog.inputbox( 104 | 'Enter the two ip addresses for the free dhcp range (space separated):', title=ititle, height=16, width=64, init=dhcprange) 105 | if rc == 'cancel': 106 | sys.exit(1) 107 | dhcprange1 = dhcprange.split(' ')[0] 108 | dhcprange2 = dhcprange.split(' ')[1] 109 | if isValidHostIpv4(dhcprange1) and isValidHostIpv4(dhcprange2): 110 | break 111 | print('DHCP range: ' + dhcprange) 112 | setup.set('setup', 'dhcprange', dhcprange) 113 | 114 | 115 | # global admin password 116 | ititle = title + ': Administrator password' 117 | adminpw = '' 118 | adminpw_repeated = '' 119 | while True: 120 | rc, adminpw = dialog.passwordbox( 121 | 'Enter the Administrator password (Note: Input will be unvisible!). Minimal length is 7 characters. Use upper and lower and special characters or numbers (e.g. mUster!):', title=ititle, insecure=True) 122 | if rc == 'cancel': 123 | sys.exit(1) 124 | if isValidPassword(adminpw): 125 | while True: 126 | rc, adminpw_repeated = dialog.passwordbox( 127 | 'Re-enter the Administrator password:', title=ititle, insecure=True) 128 | if rc == 'cancel': 129 | sys.exit(1) 130 | if isValidPassword(adminpw_repeated): 131 | break 132 | if adminpw == adminpw_repeated: 133 | break 134 | 135 | # print('Administrator password: ' + adminpw) 136 | setup.set('setup', 'adminpw', adminpw) 137 | 138 | 139 | # write INIFILE 140 | msg = 'Writing input to setup ini file ' 141 | printScript(msg, '', False, False, True) 142 | try: 143 | with open(setupini, 'w') as INIFILE: 144 | setup.write(INIFILE) 145 | printScript(' Success!', '', True, True, False, len(msg)) 146 | except: 147 | printScript(' Failed!', '', True, True, False, len(msg)) 148 | sys.exit(1) 149 | 150 | 151 | # set root password 152 | msg = 'Setting root password ' 153 | printScript(msg, '', False, False, True) 154 | try: 155 | subProc('echo "root:' + adminpw + '" | chpasswd', logfile) 156 | if os.path.isdir('/home/linuxmuster'): 157 | subProc('echo "linuxmuster:' + adminpw + '" | chpasswd', logfile) 158 | printScript(' Success!', '', True, True, False, len(msg)) 159 | except: 160 | printScript(' Failed!', '', True, True, False, len(msg)) 161 | sys.exit(1) 162 | -------------------------------------------------------------------------------- /lib/setup.d/d_templates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # process config templates 4 | # thomas@linuxmuster.net 5 | # 20220920 6 | # 7 | 8 | import configparser 9 | import environment 10 | import datetime 11 | import os 12 | import sys 13 | 14 | from functions import backupCfg, mySetupLogfile, printScript, readTextfile 15 | from functions import replaceInFile, setupComment, subProc 16 | 17 | logfile = mySetupLogfile(__file__) 18 | 19 | # read setup data 20 | msg = 'Reading setup data ' 21 | printScript(msg, '', False, False, True) 22 | setupini = environment.SETUPINI 23 | try: 24 | # setupdefaults.ini 25 | defaults = configparser.ConfigParser( 26 | delimiters=('='), inline_comment_prefixes=('#', ';')) 27 | defaults.read(environment.DEFAULTSINI) 28 | # setup.ini 29 | setup = configparser.RawConfigParser( 30 | delimiters=('='), inline_comment_prefixes=('#', ';')) 31 | setup.read(setupini) 32 | adminpw = setup.get('setup', 'adminpw') 33 | bitmask = setup.get('setup', 'bitmask') 34 | broadcast = setup.get('setup', 'broadcast') 35 | dhcprange = setup.get('setup', 'dhcprange') 36 | dhcprange1 = dhcprange.split(' ')[0] 37 | dhcprange2 = dhcprange.split(' ')[1] 38 | domainname = setup.get('setup', 'domainname') 39 | firewallip = setup.get('setup', 'firewallip') 40 | linbodir = environment.LINBODIR 41 | netbiosname = setup.get('setup', 'netbiosname') 42 | netmask = setup.get('setup', 'netmask') 43 | network = setup.get('setup', 'network') 44 | realm = setup.get('setup', 'realm') 45 | sambadomain = setup.get('setup', 'sambadomain') 46 | servername = setup.get('setup', 'servername') 47 | serverip = setup.get('setup', 'serverip') 48 | printScript(' Success!', '', True, True, False, len(msg)) 49 | except: 50 | printScript(' Failed!', '', True, True, False, len(msg)) 51 | sys.exit(1) 52 | 53 | # templates, whose corresponding configfiles must not be overwritten 54 | do_not_overwrite = 'dhcpd.custom.conf' 55 | # templates, whose corresponding configfiles must not be backed up 56 | do_not_backup = ['interfaces.linuxmuster', 57 | 'dovecot.linuxmuster.conf', 'smb.conf'] 58 | 59 | printScript('Processing config templates:') 60 | for f in os.listdir(environment.TPLDIR): 61 | source = environment.TPLDIR + '/' + f 62 | msg = '* ' + f + ' ' 63 | printScript(msg, '', False, False, True) 64 | try: 65 | # read template file 66 | rc, filedata = readTextfile(source) 67 | # replace placeholders with values 68 | filedata = filedata.replace('@@bitmask@@', bitmask) 69 | filedata = filedata.replace('@@broadcast@@', broadcast) 70 | filedata = filedata.replace('@@dhcprange@@', dhcprange) 71 | filedata = filedata.replace('@@dhcprange1@@', dhcprange1) 72 | filedata = filedata.replace('@@dhcprange2@@', dhcprange2) 73 | filedata = filedata.replace('@@domainname@@', domainname) 74 | filedata = filedata.replace('@@firewallip@@', firewallip) 75 | filedata = filedata.replace('@@linbodir@@', linbodir) 76 | filedata = filedata.replace('@@netbiosname@@', netbiosname) 77 | filedata = filedata.replace('@@netmask@@', netmask) 78 | filedata = filedata.replace('@@network@@', network) 79 | filedata = filedata.replace('@@realm@@', realm) 80 | filedata = filedata.replace('@@sambadomain@@', sambadomain) 81 | filedata = filedata.replace('@@servername@@', servername) 82 | filedata = filedata.replace('@@serverip@@', serverip) 83 | filedata = filedata.replace('@@ntpsockdir@@', environment.NTPSOCKDIR) 84 | # get target path 85 | firstline = filedata.split('\n')[0] 86 | target = firstline.partition(' ')[2] 87 | # remove target path from shebang line, define target file permissions 88 | if '#!/bin/sh' in firstline or '#!/bin/bash' in firstline: 89 | filedata = filedata.replace(' ' + target, '\n# ' + target) 90 | operms = '755' 91 | elif 'sudoers.d' in target: 92 | operms = '400' 93 | else: 94 | operms = '644' 95 | # do not overwrite specified configfiles if they exist 96 | if (f in do_not_overwrite and os.path.isfile(target)): 97 | printScript(' Success!', '', True, True, False, len(msg)) 98 | continue 99 | # create target directory 100 | subProc('mkdir -p ' + os.path.dirname(target), logfile) 101 | # backup file 102 | if f not in do_not_backup: 103 | backupCfg(target) 104 | with open(target, 'w') as outfile: 105 | outfile.write(setupComment()) 106 | outfile.write(filedata) 107 | os.system('chmod ' + operms + ' ' + target) 108 | printScript(' Success!', '', True, True, False, len(msg)) 109 | except: 110 | printScript(' Failed!', '', True, True, False, len(msg)) 111 | sys.exit(1) 112 | 113 | # server prepare update 114 | msg = 'Server prepare update ' 115 | printScript(msg, '', False, False, True) 116 | try: 117 | subProc('/usr/sbin/lmn-prepare -x -s -u -p server -f ' + firewallip 118 | + ' -n ' + serverip + '/' 119 | + bitmask + ' -d ' + domainname + ' -t ' + servername + ' -r ' 120 | + serverip + ' -a "' + adminpw + '"', logfile) 121 | # remove adminpw from logfile 122 | replaceInFile(logfile, adminpw, '******') 123 | printScript(' Success!', '', True, True, False, len(msg)) 124 | except: 125 | printScript(' Failed!', '', True, True, False, len(msg)) 126 | sys.exit(1) 127 | 128 | # set server time 129 | msg = 'Adjusting server time ' 130 | printScript(msg, '', False, False, True) 131 | subProc('mkdir -p ' + environment.NTPSOCKDIR, logfile) 132 | subProc('chgrp ntp ' + environment.NTPSOCKDIR, logfile) 133 | subProc('chmod 750 ' + environment.NTPSOCKDIR, logfile) 134 | subProc('timedatectl set-ntp false', logfile) 135 | subProc('systemctl stop ntp', logfile) 136 | subProc('ntpdate pool.ntp.org', logfile) 137 | subProc('systemctl enable ntp', logfile) 138 | subProc('systemctl start ntp', logfile) 139 | now = str(datetime.datetime.now()).split('.')[0] 140 | printScript(' ' + now, '', True, True, False, len(msg)) 141 | -------------------------------------------------------------------------------- /lib/setup.d/e_fstab.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # patch fstab with mount options 4 | # thomas@linuxmuster.net 5 | # 20220105 6 | # 7 | 8 | import environment 9 | import os 10 | import reconfigure 11 | import sys 12 | 13 | from functions import mySetupLogfile, printScript, subProc 14 | from reconfigure.configs import FSTabConfig 15 | from reconfigure.items.fstab import FilesystemData 16 | 17 | logfile = mySetupLogfile(__file__) 18 | 19 | # patch fstab with mount options 20 | config = FSTabConfig(path='/etc/fstab') 21 | config.load() 22 | c = 0 23 | mountpoints = ['/', '/srv'] 24 | while True: 25 | # try all fstab entries 26 | try: 27 | for i in mountpoints: 28 | # if mountpoint matches change mount options 29 | if config.tree.filesystems[c].mountpoint == i: 30 | msg = 'Modifying mount options for ' + i + ' ' 31 | printScript(msg, '', False, False, True) 32 | try: 33 | # get mount options from environment 34 | config.tree.filesystems[c].options = environment.ROOTMNTOPTS 35 | # save fstab 36 | config.save() 37 | printScript(' Success!', '', True, True, False, len(msg)) 38 | except: 39 | printScript(' Failed!', '', True, True, False, len(msg)) 40 | sys.exit(1) 41 | msg = 'Remounting ' + i + ' ' 42 | printScript(msg, '', False, False, True) 43 | # try to remount filesystem with new options 44 | try: 45 | subProc('mount -o remount ' + i, logfile) 46 | printScript(' Success!', '', True, True, False, len(msg)) 47 | except: 48 | printScript(' Failed!', '', True, True, False, len(msg)) 49 | sys.exit(1) 50 | # next entry 51 | c += 1 52 | # break if entries ran out 53 | except: 54 | break 55 | -------------------------------------------------------------------------------- /lib/setup.d/g_ssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # create ssl certificates 4 | # thomas@linuxmuster.net 5 | # 20250416 6 | # 7 | 8 | from __future__ import print_function 9 | 10 | import configparser 11 | import environment 12 | import glob 13 | import os 14 | import subprocess 15 | import sys 16 | 17 | from functions import createServerCert, mySetupLogfile, randomPassword, \ 18 | printScript, subProc, writeTextfile 19 | 20 | logfile = mySetupLogfile(__file__) 21 | 22 | # read setup ini 23 | msg = 'Reading setup data ' 24 | printScript(msg, '', False, False, True) 25 | setupini = environment.SETUPINI 26 | try: 27 | setup = configparser.RawConfigParser( 28 | delimiters=('='), inline_comment_prefixes=('#', ';')) 29 | setup.read(setupini) 30 | schoolname = setup.get('setup', 'schoolname') 31 | servername = setup.get('setup', 'servername') 32 | domainname = setup.get('setup', 'domainname') 33 | sambadomain = setup.get('setup', 'sambadomain') 34 | skipfw = setup.getboolean('setup', 'skipfw') 35 | realm = setup.get('setup', 'realm') 36 | printScript(' Success!', '', True, True, False, len(msg)) 37 | except: 38 | printScript(' Failed!', '', True, True, False, len(msg)) 39 | sys.exit(1) 40 | 41 | # basic subject string 42 | subjbase = '-subj /O="' + schoolname + '"/OU=' + sambadomain + '/CN=' 43 | 44 | # substring with sha and validation duration 45 | days = '3650' 46 | shadays = ' -sha256 -days ' + days 47 | 48 | # ca key password & string 49 | cakeypw = randomPassword(16) 50 | passin = ' -passin pass:' + cakeypw 51 | 52 | # create ca stuff 53 | msg = 'Creating private CA key & certificate ' 54 | subj = subjbase + realm + '/subjectAltName=' + realm + '/' 55 | printScript(msg, '', False, False, True) 56 | try: 57 | writeTextfile(environment.CAKEYSECRET, cakeypw, 'w') 58 | os.chmod(environment.CAKEYSECRET, 0o400) 59 | subProc('openssl genrsa -out ' + environment.CAKEY 60 | + ' -aes128 -passout pass:' + cakeypw + ' 2048', logfile) 61 | subProc('openssl req -batch -x509 ' + subj + ' -new -nodes ' + passin 62 | + ' -key ' + environment.CAKEY + shadays + ' -out ' + environment.CACERT, logfile) 63 | subProc('openssl x509 -in ' + environment.CACERT 64 | + ' -inform PEM -out ' + environment.CACERTCRT, logfile) 65 | # install crt 66 | subProc('ln -sf ' + environment.CACERTCRT 67 | + ' /usr/local/share/ca-certificates/linuxmuster_cacert.crt', logfile) 68 | subProc('update-ca-certificates', logfile) 69 | # create base64 encoded version for opnsense's config.xml 70 | cacertb64 = subprocess.check_output(['base64', '-w0', environment.CACERT]).decode('utf-8') 71 | writeTextfile(environment.CACERTB64, cacertb64, 'w') 72 | if not os.path.isfile(environment.CACERTB64): 73 | printScript(' Failed!', '', True, True, False, len(msg)) 74 | sys.exit(1) 75 | printScript(' Success!', '', True, True, False, len(msg)) 76 | except: 77 | printScript(' Failed!', '', True, True, False, len(msg)) 78 | sys.exit(1) 79 | 80 | # create server and firewall certificates 81 | for item in [servername, 'firewall']: 82 | if skipfw and item == 'firewall': 83 | # no cert for firewall if skipped by setup option 84 | continue 85 | createServerCert(item, days, logfile) 86 | 87 | 88 | # copy cacert.pem to sysvol for clients 89 | sysvoltlsdir = environment.SYSVOLTLSDIR.replace('@@domainname@@', domainname) 90 | sysvolpemfile = sysvoltlsdir + '/' + os.path.basename(environment.CACERT) 91 | subProc('mkdir -p ' + sysvoltlsdir, logfile) 92 | subProc('cp ' + environment.CACERT + ' ' + sysvolpemfile, logfile) 93 | 94 | # permissions 95 | msg = 'Ensure key and certificate permissions ' 96 | printScript(msg, '', False, False, True) 97 | try: 98 | subProc('chgrp -R ssl-cert ' + environment.SSLDIR, logfile) 99 | os.chmod(environment.SSLDIR, 0o750) 100 | for file in glob.glob(environment.SSLDIR + '/*'): 101 | os.chmod(file, 0o640) 102 | for file in glob.glob(environment.SSLDIR + '/*key*'): 103 | os.chmod(file, 0o600) 104 | printScript(' Success!', '', True, True, False, len(msg)) 105 | except: 106 | printScript(' Failed!', '', True, True, False, len(msg)) 107 | sys.exit(1) -------------------------------------------------------------------------------- /lib/setup.d/h_ssh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # setup ssh host keys 4 | # thomas@linuxmuster.net 5 | # 20240219 6 | # 7 | 8 | import configparser 9 | import environment 10 | import glob 11 | import os 12 | import re 13 | import subprocess 14 | import sys 15 | 16 | from functions import backupCfg, checkSocket, isValidHostIpv4, modIni 17 | from functions import mySetupLogfile, printScript, replaceInFile 18 | from functions import setupComment, subProc, writeTextfile 19 | 20 | logfile = mySetupLogfile(__file__) 21 | 22 | # read setup ini 23 | msg = 'Reading setup data ' 24 | printScript(msg, '', False, False, True) 25 | setupini = environment.SETUPINI 26 | try: 27 | setup = configparser.RawConfigParser( 28 | delimiters=('='), inline_comment_prefixes=('#', ';')) 29 | setup.read(setupini) 30 | # get ip addresses 31 | serverip = setup.get('setup', 'serverip') 32 | printScript(' Success!', '', True, True, False, len(msg)) 33 | except: 34 | printScript(' Failed!', '', True, True, False, len(msg)) 35 | sys.exit(1) 36 | 37 | # variables 38 | hostkey_prefix = '/etc/ssh/ssh_host_' 39 | crypto_list = ['dsa', 'ecdsa', 'ed25519', 'rsa'] 40 | sshdir = '/root/.ssh' 41 | rootkey_prefix = sshdir + '/id_' 42 | known_hosts = sshdir + '/known_hosts' 43 | 44 | # stop ssh service 45 | msg = 'Stopping ssh service ' 46 | printScript(msg, '', False, False, True) 47 | try: 48 | subProc('service ssh stop', logfile) 49 | printScript(' Success!', '', True, True, False, len(msg)) 50 | except: 51 | printScript(' Failed!', '', True, True, False, len(msg)) 52 | sys.exit(1) 53 | 54 | # delete old ssh keys 55 | for file in glob.glob('/etc/ssh/*key*'): 56 | os.unlink(file) 57 | for file in glob.glob(sshdir + '/id*'): 58 | os.unlink(file) 59 | 60 | # create ssh keys 61 | msg = "Creating ssh host keys " 62 | printScript(msg, '', False, False, True) 63 | try: 64 | subProc('ssh-keygen -A', logfile) 65 | printScript(' Success!', '', True, True, False, len(msg)) 66 | except: 67 | printScript(' Failed!', '', True, True, False, len(msg)) 68 | sys.exit(1) 69 | printScript('Creating ssh root keys:') 70 | for a in crypto_list: 71 | msg = '* ' + a + ' key ' 72 | printScript(msg, '', False, False, True) 73 | try: 74 | subProc('ssh-keygen -t ' + a + ' -f ' 75 | + rootkey_prefix + a + ' -N ""', logfile) 76 | if a == 'rsa': 77 | keyfile = rootkey_prefix + a + '.pub' 78 | b64sshkey = subprocess.check_output(['base64', keyfile]).decode('utf-8').replace('\n', '') 79 | writeTextfile(environment.SSHPUBKEYB64, b64sshkey, 'w') 80 | printScript(' Success!', '', True, True, False, len(msg)) 81 | except: 82 | printScript(' Failed!', '', True, True, False, len(msg)) 83 | sys.exit(1) 84 | 85 | # start ssh service 86 | msg = 'starting ssh service ' 87 | printScript(msg, '', False, False, True) 88 | try: 89 | subProc('service ssh start', logfile) 90 | printScript(' Success!', '', True, True, False, len(msg)) 91 | except: 92 | printScript(' Failed!', '', True, True, False, len(msg)) 93 | sys.exit(1) 94 | -------------------------------------------------------------------------------- /lib/setup.d/i_linbo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # linbo setup 4 | # thomas@linuxmuster.net 5 | # 20230511 6 | # 7 | 8 | import configparser 9 | import environment 10 | import os 11 | import re 12 | import subprocess 13 | import sys 14 | 15 | from functions import backupCfg, enterPassword, isValidPassword, mySetupLogfile 16 | from functions import modIni, printScript, readTextfile, setupComment 17 | from functions import subProc, writeTextfile 18 | 19 | logfile = mySetupLogfile(__file__) 20 | 21 | # read INIFILE, get schoolname 22 | msg = 'Reading setup data ' 23 | printScript(msg, '', False, False, True) 24 | setupini = environment.SETUPINI 25 | try: 26 | setup = configparser.RawConfigParser( 27 | delimiters=('='), inline_comment_prefixes=('#', ';')) 28 | setup.read(setupini) 29 | serverip = setup.get('setup', 'serverip') 30 | printScript(' Success!', '', True, True, False, len(msg)) 31 | except: 32 | printScript(' Failed!', '', True, True, False, len(msg)) 33 | sys.exit(1) 34 | 35 | # test adminpw 36 | try: 37 | adminpw = setup.get('setup', 'adminpw') 38 | except: 39 | adminpw = '' 40 | if not isValidPassword(adminpw): 41 | printScript('There is no admin password!') 42 | adminpw = enterPassword('admin', True) 43 | if not isValidPassword(adminpw): 44 | printScript('No valid admin password! Aborting!') 45 | sys.exit(1) 46 | else: 47 | msg = 'Saving admin password to setup.ini ' 48 | printScript(msg, '', False, False, True) 49 | rc = modIni(environment.SETUPINI, 'setup', 'adminpw', adminpw) 50 | if rc == True: 51 | printScript(' Success!', '', True, True, False, len(msg)) 52 | else: 53 | printScript(' Failed!', '', True, True, False, len(msg)) 54 | sys.exit(1) 55 | 56 | # write linbo auth data to rsyncd.secrets 57 | msg = 'Creating rsync secrets file ' 58 | printScript(msg, '', False, False, True) 59 | configfile = '/etc/rsyncd.secrets' 60 | filedata = setupComment() + '\n' + 'linbo:' + adminpw + '\n' 61 | try: 62 | with open(configfile, 'w') as outfile: 63 | outfile.write(filedata) 64 | # set permissions 65 | subProc('chmod 600 ' + configfile, logfile) 66 | # enable rsync service 67 | subProc('systemctl -q enable rsync.service', logfile) 68 | # restart rsync service 69 | subProc('service rsync stop', logfile) 70 | subProc('service rsync start', logfile) 71 | printScript(' Success!', '', True, True, False, len(msg)) 72 | except: 73 | printScript(' Failed!', '', True, True, False, len(msg)) 74 | sys.exit(1) 75 | 76 | # set serverip in default start.conf 77 | msg = 'Providing server ip to linbo start.conf files ' 78 | # default start.conf 79 | conffiles = [environment.LINBODIR + '/start.conf'] 80 | # collect example start.conf files 81 | for item in os.listdir(environment.LINBODIR + '/examples'): 82 | if not item.startswith('start.conf.'): 83 | continue 84 | conffiles.append(environment.LINBODIR + '/examples/' + item) 85 | printScript(msg, '', False, False, True) 86 | try: 87 | for startconf in conffiles: 88 | rc, content = readTextfile(startconf) 89 | rc = writeTextfile(startconf, content.replace( 90 | '10.16.1.1', serverip), 'w') 91 | printScript(' Success!', '', True, True, False, len(msg)) 92 | except: 93 | printScript(' Failed!', '', True, True, False, len(msg)) 94 | sys.exit(1) 95 | 96 | # linbo-torrent service 97 | msg = 'Activating linbo-torrent service ' 98 | printScript(msg, '', False, False, True) 99 | try: 100 | subprocess.call('systemctl -q enable opentracker 2>&1', shell=True) 101 | subprocess.call('systemctl -q enable linbo-torrent 2>&1', shell=True) 102 | printScript(' Success!', '', True, True, False, len(msg)) 103 | except: 104 | printScript(' Failed!', '', True, True, False, len(msg)) 105 | sys.exit(1) 106 | 107 | # linbofs update 108 | msg = 'Reconfiguring linbo (forking to background) ' 109 | printScript(msg, '', False, False, True) 110 | try: 111 | subProc('rm -f ' + environment.SYSDIR + '/linbo/*key*', logfile) 112 | subprocess.call('dpkg-reconfigure linuxmuster-linbo7 >> ' 113 | + logfile + ' 2>&1 &', shell=True) 114 | printScript(' Success!', '', True, True, False, len(msg)) 115 | except: 116 | printScript(' Failed!', '', True, True, False, len(msg)) 117 | sys.exit(1) 118 | -------------------------------------------------------------------------------- /lib/setup.d/j_samba-provisioning.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # samba provisioning 4 | # thomas@linuxmuster.net 5 | # 20220622 6 | # 7 | 8 | import configparser 9 | import environment 10 | import datetime 11 | import os 12 | import sys 13 | from functions import mySetupLogfile, printScript, randomPassword 14 | from functions import readTextfile, subProc, writeTextfile 15 | 16 | logfile = mySetupLogfile(__file__) 17 | 18 | # stop services 19 | msg = 'Stopping samba services ' 20 | printScript(msg, '', False, False, True) 21 | 22 | services = ['winbind', 'samba-ad-dc', 'smbd', 'nmbd', 'systemd-resolved', 'samba-ad-dc'] 23 | try: 24 | for service in services: 25 | subProc('systemctl stop ' + service + '.service', logfile) 26 | if service == 'samba-ad-dc': 27 | continue 28 | # disabling not needed samba services 29 | subProc('systemctl disable ' + service + '.service', logfile) 30 | printScript(' Success!', '', True, True, False, len(msg)) 31 | except Exception as error: 32 | printScript(error, '', True, True, False, len(msg)) 33 | sys.exit(1) 34 | 35 | # read setup ini 36 | msg = 'Reading setup data ' 37 | printScript(msg, '', False, False, True) 38 | setupini = environment.SETUPINI 39 | try: 40 | setup = configparser.RawConfigParser(delimiters=('='), inline_comment_prefixes=('#', ';')) 41 | setup.read(setupini) 42 | realm = setup.get('setup', 'domainname').upper() 43 | sambadomain = setup.get('setup', 'sambadomain') 44 | serverip = setup.get('setup', 'serverip') 45 | servername = setup.get('setup', 'servername') 46 | domainname = setup.get('setup', 'domainname') 47 | basedn = setup.get('setup', 'basedn') 48 | printScript(' Success!', '', True, True, False, len(msg)) 49 | except Exception as error: 50 | printScript(error, '', True, True, False, len(msg)) 51 | sys.exit(1) 52 | 53 | # generate ad admin password 54 | msg = 'Generating AD admin password ' 55 | printScript(msg, '', False, False, True) 56 | try: 57 | adadminpw = randomPassword(16) 58 | with open(environment.ADADMINSECRET, 'w') as secret: 59 | secret.write(adadminpw) 60 | subProc('chmod 400 ' + environment.ADADMINSECRET, logfile) 61 | # symlink for sophomorix 62 | subProc('ln -sf ' + environment.ADADMINSECRET + ' ' + environment.SOPHOSYSDIR + '/sophomorix-samba.secret', logfile) 63 | printScript(' Success!', '', True, True, False, len(msg)) 64 | except Exception as error: 65 | printScript(error, '', True, True, False, len(msg)) 66 | sys.exit(1) 67 | 68 | # alte smb.conf löschen 69 | smbconf = '/etc/samba/smb.conf' 70 | if os.path.isfile(smbconf): 71 | os.unlink(smbconf) 72 | 73 | # provisioning samba 74 | msg = 'Provisioning samba ' 75 | printScript(msg, '', False, False, True) 76 | try: 77 | subProc('samba-tool domain provision --use-rfc2307 --server-role=dc --domain=' + sambadomain + ' --realm=' + realm + ' --adminpass=' + adadminpw, logfile) 78 | printScript(' Success!', '', True, True, False, len(msg)) 79 | except Exception as error: 80 | printScript(error, '', True, True, False, len(msg)) 81 | sys.exit(1) 82 | 83 | # create krb5.conf symlink 84 | krb5conf_src = '/var/lib/samba/private/krb5.conf' 85 | if os.path.isfile(krb5conf_src): 86 | msg = 'Provisioning krb5 ' 87 | printScript(msg, '', False, False, True) 88 | try: 89 | krb5conf_dst = '/etc/krb5.conf' 90 | if os.path.isfile(krb5conf_dst): 91 | os.remove(krb5conf_dst) 92 | os.symlink(krb5conf_src, krb5conf_dst) 93 | rc, filedata = readTextfile(krb5conf_dst) 94 | filedata = filedata.replace('dns_lookup_realm = false', 'dns_lookup_realm = true') 95 | rc = writeTextfile(krb5conf_dst, filedata, 'w') 96 | printScript(' Success!', '', True, True, False, len(msg)) 97 | except Exception as error: 98 | printScript(error, '', True, True, False, len(msg)) 99 | sys.exit(1) 100 | 101 | # restart services 102 | msg = 'Enabling samba services ' 103 | printScript(msg, '', False, False, True) 104 | try: 105 | subProc('systemctl daemon-reload', logfile) 106 | for service in services: 107 | subProc('systemctl stop ' + service, logfile) 108 | subProc('systemctl mask smbd.service', logfile) 109 | subProc('systemctl mask nmbd.service', logfile) 110 | # start only samba-ad-dc service 111 | subProc('systemctl unmask samba-ad-dc.service', logfile) 112 | subProc('systemctl enable samba-ad-dc.service', logfile) 113 | printScript(' Success!', '', True, True, False, len(msg)) 114 | except Exception as error: 115 | printScript(error, '', True, True, False, len(msg)) 116 | sys.exit(1) 117 | 118 | # backup samba before sophomorix modifies anything 119 | msg = 'Backing up samba ' 120 | printScript(msg, '', False, False, True) 121 | try: 122 | subProc('sophomorix-samba --backup-samba without-sophomorix-schema', logfile) 123 | printScript(' Success!', '', True, True, False, len(msg)) 124 | except Exception as error: 125 | printScript(error, '', True, True, False, len(msg)) 126 | sys.exit(1) 127 | 128 | # loading sophomorix samba schema 129 | msg = 'Provisioning sophomorix samba schema ' 130 | printScript(msg, '', False, False, True) 131 | try: 132 | subProc('cd /usr/share/sophomorix/schema ; ./sophomorix_schema_add.sh ' + basedn + ' . -H /var/lib/samba/private/sam.ldb -writechanges', logfile) 133 | printScript(' Success!', '', True, True, False, len(msg)) 134 | except Exception as error: 135 | printScript(error, '', True, True, False, len(msg)) 136 | sys.exit(1) 137 | 138 | # fixing resolv.conf 139 | msg = 'Fixing resolv.conf ' 140 | printScript(msg, '', False, False, True) 141 | try: 142 | resconf = '/etc/resolv.conf' 143 | now = str(datetime.datetime.now()).split('.')[0] 144 | header = '# created by linuxmuster-setup ' + now + '\n' 145 | search = 'search ' + domainname + '\n' 146 | ns = 'nameserver ' + serverip + '\n' 147 | filedata = header + search + ns 148 | os.unlink(resconf) 149 | rc = writeTextfile(resconf, filedata, 'w') 150 | printScript(' Success!', '', True, True, False, len(msg)) 151 | except Exception as error: 152 | printScript(error, '', True, True, False, len(msg)) 153 | sys.exit(1) 154 | 155 | # exchange smb.conf 156 | msg = 'Exchanging smb.conf ' 157 | printScript(msg, '', False, False, True) 158 | try: 159 | os.system('mv ' + smbconf + ' ' + smbconf + '.orig') 160 | os.system('mv ' + smbconf + '.setup ' + smbconf) 161 | printScript(' Success!', '', True, True, False, len(msg)) 162 | except Exception as error: 163 | printScript(error, '', True, True, False, len(msg)) 164 | sys.exit(1) 165 | 166 | # starting samba service again 167 | msg = 'Starting samba ad dc service ' 168 | printScript(msg, '', False, False, True) 169 | try: 170 | subProc('systemctl start samba-ad-dc.service', logfile) 171 | subProc('sleep 5', logfile) 172 | printScript(' Success!', '', True, True, False, len(msg)) 173 | except Exception as error: 174 | printScript(error, '', True, True, False, len(msg)) 175 | sys.exit(1) 176 | -------------------------------------------------------------------------------- /lib/setup.d/k_samba-users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # create samba users & shares 4 | # thomas@linuxmuster.net 5 | # 20220105 6 | # 7 | 8 | import configparser 9 | import environment 10 | import os 11 | import sys 12 | 13 | from functions import mySetupLogfile, printScript, randomPassword, readTextfile 14 | from functions import replaceInFile, sambaTool, subProc, writeTextfile 15 | 16 | logfile = mySetupLogfile(__file__) 17 | 18 | # read setup ini 19 | msg = 'Reading setup data ' 20 | printScript(msg, '', False, False, True) 21 | setupini = environment.SETUPINI 22 | try: 23 | setup = configparser.RawConfigParser( 24 | delimiters=('='), inline_comment_prefixes=('#', ';')) 25 | setup.read(setupini) 26 | adminpw = setup.get('setup', 'adminpw') 27 | domainname = setup.get('setup', 'domainname') 28 | sambadomain = setup.get('setup', 'sambadomain') 29 | firewallip = setup.get('setup', 'firewallip') 30 | # get binduser password 31 | rc, binduserpw = readTextfile(environment.BINDUSERSECRET) 32 | printScript(' Success!', '', True, True, False, len(msg)) 33 | except Exception as error: 34 | printScript(error, '', True, True, False, len(msg)) 35 | sys.exit(1) 36 | 37 | # samba backup 38 | msg = 'Backing up samba ' 39 | printScript(msg, '', False, False, True) 40 | try: 41 | subProc('sophomorix-samba --backup-samba without-users', logfile) 42 | printScript(' Success!', '', True, True, False, len(msg)) 43 | except Exception as error: 44 | printScript(error, '', True, True, False, len(msg)) 45 | sys.exit(1) 46 | 47 | # renew sophomorix configs 48 | os.system('rm -f ' + environment.SCHOOLCONF) 49 | os.system('rm -f ' + environment.SOPHOSYSDIR + '/sophomorix.conf') 50 | subProc('sophomorix-postinst', logfile) 51 | 52 | # create default-school share 53 | schoolname = os.path.basename(environment.DEFAULTSCHOOL) 54 | defaultpath = environment.SCHOOLSSHARE + '/' + schoolname 55 | shareopts = 'writeable=y guest_ok=n' 56 | shareoptsex = ['comment "Share for default-school"', '"hide unreadable" yes', '"msdfs root" no', 57 | '"strict allocate" yes', '"valid users" "' + sambadomain + '\\administrator, @' + sambadomain + '\\SCHOOLS"'] 58 | msg = 'Creating share for ' + schoolname + ' ' 59 | printScript(msg, '', False, False, True) 60 | try: 61 | subProc('net conf addshare ' + schoolname + ' ' 62 | + defaultpath + ' ' + shareopts, logfile) 63 | for item in shareoptsex: 64 | subProc('net conf setparm ' + schoolname + ' ' + item) 65 | printScript(' Success!', '', True, True, False, len(msg)) 66 | except Exception as error: 67 | printScript(error, '', True, True, False, len(msg)) 68 | sys.exit(1) 69 | 70 | # create global-admin 71 | sophomorix_comment = "created by linuxmuster-setup" 72 | msg = 'Creating samba account for global-admin ' 73 | printScript(msg, '', False, False, True) 74 | try: 75 | subProc('sophomorix-admin --create-global-admin global-admin --password "' 76 | + adminpw + '"', logfile) 77 | subProc('sophomorix-user --user global-admin --comment "' 78 | + sophomorix_comment + '"', logfile) 79 | printScript(' Success!', '', True, True, False, len(msg)) 80 | except Exception as error: 81 | printScript(error, '', True, True, False, len(msg)) 82 | sys.exit(1) 83 | 84 | # create global bind user 85 | msg = 'Creating samba account for global-binduser ' 86 | printScript(msg, '', False, False, True) 87 | try: 88 | subProc('sophomorix-admin --create-global-binduser global-binduser --password "' 89 | + binduserpw + '"', logfile) 90 | subProc('sophomorix-user --user global-binduser --comment "' 91 | + sophomorix_comment + '"', logfile) 92 | printScript(' Success!', '', True, True, False, len(msg)) 93 | except Exception as error: 94 | printScript(error, '', True, True, False, len(msg)) 95 | sys.exit(1) 96 | 97 | # no expiry for Administrator password 98 | msg = 'No expiry for administrative passwords ' 99 | printScript(msg, '', False, False, True) 100 | try: 101 | for i in ['Administrator', 'global-admin', 'sophomorix-admin', 'global-binduser']: 102 | sambaTool('user setexpiry ' + i + ' --noexpiry', logfile) 103 | printScript(' Success!', '', True, True, False, len(msg)) 104 | except Exception as error: 105 | printScript(error, '', True, True, False, len(msg)) 106 | sys.exit(1) 107 | 108 | # create default-school, no connection to ad 109 | msg = 'Creating ou for ' + schoolname + ' ' 110 | printScript(msg, '', False, False, True) 111 | try: 112 | subProc('sophomorix-school --create --school ' + schoolname, logfile) 113 | subProc('sophomorix-school --gpo-create ' + schoolname, logfile) 114 | printScript(' Success!', '', True, True, False, len(msg)) 115 | except Exception as error: 116 | printScript(error, '', True, True, False, len(msg)) 117 | sys.exit(1) 118 | 119 | # create pgmadmin for default-school 120 | msg = 'Creating samba account for pgmadmin ' 121 | printScript(msg, '', False, False, True) 122 | try: 123 | subProc('sophomorix-admin --create-school-admin pgmadmin --school ' 124 | + schoolname + ' --password "' + adminpw + '"', logfile) 125 | subProc('sophomorix-user --user pgmadmin --comment "' 126 | + sophomorix_comment + '"', logfile) 127 | printScript(' Success!', '', True, True, False, len(msg)) 128 | except Exception as error: 129 | printScript(error, '', True, True, False, len(msg)) 130 | sys.exit(1) 131 | 132 | # create dns-admin account 133 | msg = 'Creating samba account for dns-admin ' 134 | printScript(msg, '', False, False, True) 135 | try: 136 | dnspw = randomPassword(16) 137 | desc = 'Unprivileged user for DNS updates via DHCP server' 138 | sambaTool('user create dns-admin ' + dnspw 139 | + ' --description="' + desc + '"', logfile) 140 | sambaTool('user setexpiry dns-admin --noexpiry', logfile) 141 | sambaTool('group addmembers DnsAdmins dns-admin', logfile) 142 | rc, writeTextfile(environment.DNSADMINSECRET, dnspw, 'w') 143 | os.system('chgrp dhcpd ' + environment.DNSADMINSECRET) 144 | os.system('chmod 440 ' + environment.DNSADMINSECRET) 145 | printScript(' Success!', '', True, True, False, len(msg)) 146 | except Exception as error: 147 | printScript(error, '', True, True, False, len(msg)) 148 | sys.exit(1) 149 | 150 | # mask passwords in logfile 151 | msg = 'Masking passwords in logfile ' 152 | printScript(msg, '', False, False, True) 153 | try: 154 | for item in [adminpw, binduserpw, dnspw]: 155 | replaceInFile(logfile, item, '******') 156 | printScript(' Success!', '', True, True, False, len(msg)) 157 | except Exception as error: 158 | printScript(error, '', True, True, False, len(msg)) 159 | sys.exit(1) 160 | -------------------------------------------------------------------------------- /lib/setup.d/l_add-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # add additional servers to devices.csv 4 | # thomas@linuxmuster.net 5 | # 20220105 6 | # 7 | 8 | import configparser 9 | import environment 10 | import os 11 | import random 12 | import re 13 | import sys 14 | 15 | from functions import isValidHostIpv4, isValidMac, mySetupLogfile, printScript 16 | from functions import readTextfile, subProc, writeTextfile 17 | from subprocess import Popen, PIPE 18 | from uuid import getnode 19 | 20 | logfile = mySetupLogfile(__file__) 21 | 22 | # read setup.ini 23 | msg = 'Reading setup data ' 24 | printScript(msg, '', False, False, True) 25 | setupini = environment.SETUPINI 26 | try: 27 | setup = configparser.RawConfigParser( 28 | delimiters=('='), inline_comment_prefixes=('#', ';')) 29 | setup.read(setupini) 30 | firewallip = setup.get('setup', 'firewallip') 31 | servername = setup.get('setup', 'servername') 32 | serverip = setup.get('setup', 'serverip') 33 | rc, devices = readTextfile(environment.WIMPORTDATA) 34 | printScript(' Success!', '', True, True, False, len(msg)) 35 | except: 36 | printScript(' Failed!', '', True, True, False, len(msg)) 37 | sys.exit(1) 38 | 39 | # get random mac address 40 | 41 | 42 | def getRandomMac(devices): 43 | while True: 44 | mac = "00:00:00:%02x:%02x:%02x" % ( 45 | random.randint(0, 255), 46 | random.randint(0, 255), 47 | random.randint(0, 255) 48 | ) 49 | if not ';' + mac.upper() + ';' in devices: 50 | break 51 | return mac.upper() 52 | 53 | # get mac address from arp cache 54 | 55 | 56 | def getMacFromArp(ip): 57 | mac = '' 58 | c = 0 59 | max = 10 60 | while not isValidMac(mac): 61 | if c > 0: 62 | os.system('sleep 15') 63 | subProc('ping -c2 ' + ip, logfile) 64 | pid = Popen(["arp", "-n", ip], stdout=PIPE) 65 | arpout = pid.communicate()[0] 66 | try: 67 | mac = re.search( 68 | r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})", str(arpout)).groups()[0] 69 | if isValidMac(mac): 70 | return mac.upper() 71 | except: 72 | mac = '' 73 | c = c + 1 74 | if c > max: 75 | break 76 | return mac 77 | 78 | # add devices entry 79 | 80 | 81 | def addServerDevice(hostname, mac, ip, devices): 82 | if mac == '': 83 | return devices 84 | # server is type addc 85 | if ip == serverip: 86 | type = 'addc' 87 | else: 88 | type = 'server' 89 | line = 'server;' + hostname + ';nopxe;' + mac + \ 90 | ';' + ip + ';;;;' + type + ';;0;;;;SETUP;' 91 | if ';' + hostname + ';' in devices: 92 | devices = '\n' + devices + '\n' 93 | devices = re.sub(r'\n.+?;' + hostname + ';.+?\n', 94 | '\n' + line + '\n', devices) 95 | devices = devices[1:-1] 96 | else: 97 | if devices[-1] != '\n': 98 | line = '\n' + line 99 | else: 100 | line = line + '\n' 101 | devices = devices + line 102 | return devices 103 | 104 | 105 | # collect array 106 | device_array = [] 107 | 108 | # server 109 | device_array.append((servername, serverip)) 110 | # firewall 111 | device_array.append(('firewall', firewallip)) 112 | 113 | # iterate 114 | printScript('Creating device entries for:') 115 | for item in device_array: 116 | hostname = item[0] 117 | ip = item[1] 118 | msg = '* ' + hostname + ' ' 119 | printScript(msg, '', False, False, True) 120 | # get mac address 121 | if ip == serverip: 122 | h = iter(hex(getnode())[2:].zfill(12)) 123 | mac = ":".join(i + next(h) for i in h) 124 | else: 125 | mac = getMacFromArp(ip) 126 | if mac == '': 127 | mac = getRandomMac(devices) 128 | # create devices.csv entry 129 | devices = addServerDevice(hostname, mac, ip, devices) 130 | if rc == False: 131 | printScript(' Failed!', '', True, True, False, len(msg)) 132 | sys.exit(1) 133 | else: 134 | printScript(' ' + ip + ' ' + mac, '', True, True, False, len(msg)) 135 | 136 | # finally write devices.csv 137 | if not writeTextfile(environment.WIMPORTDATA, devices, 'w'): 138 | sys.exit(1) 139 | -------------------------------------------------------------------------------- /lib/setup.d/m_firewall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # firewall setup 4 | # thomas@linuxmuster.net 5 | # 20250417 6 | # 7 | 8 | import bcrypt 9 | import environment 10 | import datetime 11 | import os 12 | import shutil 13 | import sys 14 | 15 | from bs4 import BeautifulSoup 16 | from functions import getFwConfig, getSetupValue, isValidHostIpv4, mySetupLogfile 17 | from functions import modIni, printScript, putFwConfig, putSftp, randomPassword 18 | from functions import readTextfile, sshExec, subProc, writeTextfile 19 | 20 | logfile = mySetupLogfile(__file__) 21 | 22 | 23 | # main routine 24 | def main(): 25 | # get various setup values 26 | msg = 'Reading setup data ' 27 | printScript(msg, '', False, False, True) 28 | try: 29 | serverip = getSetupValue('serverip') 30 | bitmask = getSetupValue('bitmask') 31 | firewallip = getSetupValue('firewallip') 32 | servername = getSetupValue('servername') 33 | domainname = getSetupValue('domainname') 34 | basedn = getSetupValue('basedn') 35 | network = getSetupValue('network') 36 | adminpw = getSetupValue('adminpw') 37 | printScript(' Success!', '', True, True, False, len(msg)) 38 | except: 39 | printScript(' Failed!', '', True, True, False, len(msg)) 40 | sys.exit(1) 41 | 42 | # get timezone 43 | rc, timezone = readTextfile('/etc/timezone') 44 | timezone = timezone.replace('\n', '') 45 | 46 | # get binduser password 47 | rc, binduserpw = readTextfile(environment.BINDUSERSECRET) 48 | 49 | # get firewall root password provided by linuxmuster-opnsense-reset 50 | pwfile = '/tmp/linuxmuster-opnsense-reset' 51 | if os.path.isfile(pwfile): 52 | # firewall reset after setup, given password is current password 53 | rc, rolloutpw = readTextfile(pwfile) 54 | productionpw = rolloutpw 55 | os.unlink(pwfile) 56 | else: 57 | # initial setup, rollout root password is standardized 58 | rolloutpw = environment.ROOTPW 59 | # new root production password provided by setup 60 | productionpw = adminpw 61 | 62 | # create and save radius secret 63 | msg = 'Calculating radius secret ' 64 | printScript(msg, '', False, False, True) 65 | try: 66 | radiussecret = randomPassword(16) 67 | with open(environment.RADIUSSECRET, 'w') as secret: 68 | secret.write(radiussecret) 69 | subProc('chmod 400 ' + environment.RADIUSSECRET, logfile) 70 | printScript(' Success!', '', True, True, False, len(msg)) 71 | except: 72 | printScript(' Failed!', '', True, True, False, len(msg)) 73 | sys.exit(1) 74 | 75 | # firewall config files 76 | now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') 77 | fwconftmp = environment.FWCONFLOCAL 78 | fwconfbak = fwconftmp.replace('.xml', '-' + now + '.xml') 79 | fwconftpl = environment.FWOSCONFTPL 80 | 81 | # get current config 82 | rc = getFwConfig(firewallip, rolloutpw) 83 | if not rc: 84 | sys.exit(1) 85 | 86 | # backup config 87 | msg = '* Backing up ' 88 | printScript(msg, '', False, False, True) 89 | try: 90 | shutil.copy(fwconftmp, fwconfbak) 91 | printScript(' Success!', '', True, True, False, len(msg)) 92 | except: 93 | printScript(' Failed!', '', True, True, False, len(msg)) 94 | sys.exit(1) 95 | 96 | # get root password hash 97 | msg = '* Reading current config ' 98 | printScript(msg, '', False, False, True) 99 | try: 100 | rc, content = readTextfile(fwconftmp) 101 | soup = BeautifulSoup(content, features='xml') 102 | # save certain configuration values for later use 103 | firmware = str(soup.find('firmware')) 104 | sysctl = str(soup.find('sysctl')) 105 | # get already configured interfaces 106 | for item in soup.findAll('interfaces'): 107 | if '' in str(item): 108 | interfaces = str(item) 109 | # save language information 110 | try: 111 | language = str(soup.findAll('language')[0]) 112 | except: 113 | language = '' 114 | # second try get language from locale settings 115 | if language == '': 116 | try: 117 | lang = os.environ['LANG'].split('.')[0] 118 | except: 119 | lang = 'en_US' 120 | language = '' + lang + '' 121 | # save gateway configuration 122 | try: 123 | gwconfig = str(soup.find('gateways').content) 124 | except: 125 | gwconfig = '' 126 | # save opt1 configuration if present 127 | try: 128 | opt1config = str(soup.findAll('opt1')[0]) 129 | except: 130 | opt1config = '' 131 | printScript(' Success!', '', True, True, False, len(msg)) 132 | except: 133 | printScript(' Failed!', '', True, True, False, len(msg)) 134 | sys.exit(1) 135 | 136 | # get base64 encoded certs 137 | msg = '* Reading certificates & ssh key ' 138 | printScript(msg, '', False, False, True) 139 | try: 140 | rc, cacertb64 = readTextfile(environment.CACERTB64) 141 | rc, fwcertb64 = readTextfile( 142 | environment.SSLDIR + '/firewall.cert.pem.b64') 143 | rc, fwkeyb64 = readTextfile(environment.SSLDIR + '/firewall.key.pem.b64') 144 | rc, authorizedkey = readTextfile(environment.SSHPUBKEYB64) 145 | printScript(' Success!', '', True, True, False, len(msg)) 146 | except: 147 | printScript(' Failed!', '', True, True, False, len(msg)) 148 | sys.exit(1) 149 | 150 | # create list of first ten network ips for aliascontent (NoProxy group in firewall) 151 | aliascontent = '' 152 | netpre = network.split( 153 | '.')[0] + '.' + network.split('.')[1] + '.' + network.split('.')[2] + '.' 154 | c = 0 155 | max = 10 156 | while c < max: 157 | c = c + 1 158 | aliasip = netpre + str(c) 159 | if aliascontent == '': 160 | aliascontent = aliasip 161 | else: 162 | aliascontent = aliascontent + ' ' + aliasip 163 | # add server ip if not already collected 164 | if not serverip in aliascontent: 165 | aliascontent = aliascontent + '\n' + serverip 166 | 167 | # create new firewall configuration 168 | msg = '* Creating xml configuration file ' 169 | printScript(msg, '', False, False, True) 170 | try: 171 | # create password hash for new firewall password 172 | hashedpw = bcrypt.hashpw(str.encode(productionpw), bcrypt.gensalt(10)) 173 | fwrootpw_hashed = hashedpw.decode() 174 | apikey = randomPassword(80) 175 | apisecret = randomPassword(80) 176 | hashedpw = bcrypt.hashpw(str.encode(apisecret), bcrypt.gensalt(10)) 177 | apisecret_hashed = hashedpw.decode() 178 | # read template 179 | rc, content = readTextfile(fwconftpl) 180 | # replace placeholders with values 181 | content = content.replace('@@firmware@@', firmware) 182 | content = content.replace('@@sysctl@@', sysctl) 183 | content = content.replace('@@servername@@', servername) 184 | content = content.replace('@@domainname@@', domainname) 185 | content = content.replace('@@basedn@@', basedn) 186 | content = content.replace('@@interfaces@@', interfaces) 187 | content = content.replace('@@gwconfig@@', gwconfig) 188 | content = content.replace('@@serverip@@', serverip) 189 | content = content.replace('@@firewallip@@', firewallip) 190 | content = content.replace('@@network@@', network) 191 | content = content.replace('@@bitmask@@', bitmask) 192 | content = content.replace('@@aliascontent@@', aliascontent) 193 | content = content.replace('@@gw_lan@@', environment.GW_LAN) 194 | content = content.replace('@@fwrootpw_hashed@@', fwrootpw_hashed) 195 | content = content.replace('@@authorizedkey@@', authorizedkey) 196 | content = content.replace('@@apikey@@', apikey) 197 | content = content.replace('@@apisecret_hashed@@', apisecret_hashed) 198 | content = content.replace('@@binduserpw@@', binduserpw) 199 | content = content.replace('@@radiussecret@@', radiussecret) 200 | content = content.replace('@@language@@', language) 201 | content = content.replace('@@timezone@@', timezone) 202 | content = content.replace('@@cacertb64@@', cacertb64) 203 | content = content.replace('@@fwcertb64@@', fwcertb64) 204 | content = content.replace('@@fwkeyb64@@', fwkeyb64) 205 | # write new configfile 206 | rc = writeTextfile(fwconftmp, content, 'w') 207 | printScript(' Success!', '', True, True, False, len(msg)) 208 | except: 209 | printScript(' Failed!', '', True, True, False, len(msg)) 210 | sys.exit(1) 211 | 212 | # create api credentials ini file 213 | msg = '* Saving api credentials ' 214 | printScript(msg, '', False, False, True) 215 | try: 216 | rc = modIni(environment.FWAPIKEYS, 'api', 'key', apikey) 217 | rc = modIni(environment.FWAPIKEYS, 'api', 'secret', apisecret) 218 | os.system('chmod 400 ' + environment.FWAPIKEYS) 219 | printScript(' Success!', '', True, True, False, len(msg)) 220 | except: 221 | printScript(' Failed!', '', True, True, False, len(msg)) 222 | sys.exit(1) 223 | 224 | # upload config files 225 | # upload modified main config.xml 226 | rc = putFwConfig(firewallip, '/tmp/opnsense.xml', rolloutpw) 227 | if not rc: 228 | sys.exit(1) 229 | 230 | # upload modified auth config file for web-proxy sso (#83) 231 | printScript('Creating web proxy sso auth config file') 232 | subProc(environment.FWSHAREDIR + '/create-auth-config.py', logfile) 233 | conftmp = '/tmp/' + os.path.basename(environment.FWAUTHCFG) 234 | if not os.path.isfile(conftmp): 235 | sys.exit(1) 236 | rc = putSftp(firewallip, conftmp, conftmp, rolloutpw) 237 | if not rc: 238 | sys.exit(1) 239 | 240 | # remove temporary files 241 | os.unlink(conftmp) 242 | 243 | # reboot firewall 244 | printScript('Installing extensions and rebooting firewall') 245 | fwsetup_local = environment.FWSHAREDIR + '/fwsetup.sh' 246 | fwsetup_remote = '/tmp/fwsetup.sh' 247 | rc = putSftp(firewallip, fwsetup_local, fwsetup_remote, rolloutpw) 248 | rc = sshExec(firewallip, 'chmod +x ' + fwsetup_remote, rolloutpw) 249 | rc = sshExec(firewallip, fwsetup_remote, rolloutpw) 250 | if not rc: 251 | sys.exit(1) 252 | 253 | 254 | # quit if firewall setup shall be skipped 255 | skipfw = getSetupValue('skipfw') 256 | if skipfw: 257 | msg = 'Skipping firewall setup as requested' 258 | printScript(msg, '', True, False, False) 259 | else: 260 | main() 261 | -------------------------------------------------------------------------------- /lib/setup.d/z_final.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # final tasks 4 | # thomas@linuxmuster.net 5 | # 20250422 6 | 7 | import configparser 8 | import environment 9 | import glob 10 | import os 11 | import re 12 | import sys 13 | 14 | from functions import getSetupValue, mySetupLogfile, printScript, readTextfile, \ 15 | subProc, waitForFw, writeTextfile 16 | 17 | logfile = mySetupLogfile(__file__) 18 | 19 | # remove temporary files 20 | if os.path.isfile('/tmp/setup.ini'): 21 | os.unlink('/tmp/setup.ini') 22 | 23 | # get various setup values 24 | msg = 'Reading setup data ' 25 | printScript(msg, '', False, False, True) 26 | try: 27 | adminpw = getSetupValue('adminpw') 28 | printScript(' Success!', '', True, True, False, len(msg)) 29 | except: 30 | printScript(' Failed!', '', True, True, False, len(msg)) 31 | sys.exit(1) 32 | 33 | # fix netplan file permissions 34 | for file in glob.glob('/etc/netplan/*.yaml*'): 35 | os.chmod(file, 0o600) 36 | 37 | # restart apparmor service 38 | msg = 'Restarting apparmor service ' 39 | printScript(msg, '', False, False, True) 40 | try: 41 | subProc('systemctl restart apparmor.service', logfile) 42 | printScript(' Success!', '', True, True, False, len(msg)) 43 | except Exception as error: 44 | printScript(error, '', True, True, False, len(msg)) 45 | sys.exit(1) 46 | 47 | # write schoolname to sophomorix school.conf 48 | msg = 'Writing school name to school.conf ' 49 | printScript(msg, '', False, False, True) 50 | try: 51 | schoolname = getSetupValue('schoolname') 52 | rc, content = readTextfile(environment.SCHOOLCONF) 53 | # need to use regex because sophomorix config files do not do not comply with the ini file standard 54 | content = re.sub(r'SCHOOL_LONGNAME=.*\n', 55 | 'SCHOOL_LONGNAME=' + schoolname + '\n', content) 56 | rc = writeTextfile(environment.SCHOOLCONF, content, 'w') 57 | printScript(' Success!', '', True, True, False, len(msg)) 58 | except Exception as error: 59 | printScript(error, '', True, True, False, len(msg)) 60 | sys.exit(1) 61 | 62 | # import devices 63 | msg = 'Starting device import ' 64 | printScript(msg, '', False, False, True) 65 | try: 66 | subProc('linuxmuster-import-devices', logfile) 67 | printScript(' Success!', '', True, True, False, len(msg)) 68 | except Exception as error: 69 | printScript(error, '', True, True, False, len(msg)) 70 | sys.exit(1) 71 | 72 | # wait for fw 73 | skipfw = getSetupValue('skipfw') 74 | if not skipfw: 75 | try: 76 | waitForFw(wait=30) 77 | except Exception as error: 78 | print(error) 79 | sys.exit(1) 80 | 81 | # import subnets 82 | msg = 'Starting subnets import ' 83 | printScript(msg, '', False, False, True) 84 | try: 85 | subProc('linuxmuster-import-subnets', logfile) 86 | printScript(' Success!', '', True, True, False, len(msg)) 87 | except Exception as error: 88 | printScript(error, '', True, True, False, len(msg)) 89 | sys.exit(1) 90 | 91 | # create web proxy sso keytab 92 | msg = 'Creating web proxy sso keytab ' 93 | printScript(msg, '', False, False, True) 94 | try: 95 | subProc(environment.FWSHAREDIR + "/create-keytab.py -v -a '" + adminpw + "'", logfile, True) 96 | printScript(' Success!', '', True, True, False, len(msg)) 97 | except Exception as error: 98 | printScript(error, '', True, True, False, len(msg)) 99 | sys.exit(1) 100 | 101 | # admin password not more needed in setup.ini 102 | msg = 'Removing admin password from setup.ini ' 103 | printScript(msg, '', False, False, True) 104 | setupini = environment.SETUPINI 105 | try: 106 | setup = configparser.RawConfigParser( 107 | delimiters=('='), inline_comment_prefixes=('#', ';')) 108 | setup.read(setupini) 109 | setup.set('setup', 'adminpw', '') 110 | with open(setupini, 'w') as INIFILE: 111 | setup.write(INIFILE) 112 | printScript(' Success!', '', True, True, False, len(msg)) 113 | except: 114 | printScript(' Failed!', '', True, True, False, len(msg)) 115 | sys.exit(1) 116 | -------------------------------------------------------------------------------- /sbin/linuxmuster-holiday: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """ 4 | Simple script to test if today is holiday, based on the configuration file 5 | /etc/linuxmuster/sophomorix/SCHOOL/holidays.yml. 6 | 7 | Structure of the configuration file (YAML): 8 | holiday_name1: 9 | start: "dd.mm.yyyy" 10 | end: "dd.mm.yyyy" 11 | holiday_name2: 12 | start: "dd.mm.yyyy" 13 | end: "dd.mm.yyyy" 14 | """ 15 | 16 | import yaml 17 | import sys 18 | import getopt 19 | 20 | from datetime import date 21 | 22 | 23 | class Holiday: 24 | def __init__(self, name, start, end): 25 | """ 26 | Holiday object. 27 | :param name: Name of holiday 28 | :type name: string 29 | :param start: Start date as dd.mm.yyyy 30 | :type start: string 31 | :param end: End as dd.mm.yyyy 32 | :type end: string 33 | """ 34 | 35 | self.name = name 36 | self.start = date(*map(int, start.split('.')[::-1])) 37 | self.end = date(*map(int, end.split('.')[::-1])) 38 | 39 | def __str__(self): 40 | return f"{self.name} ({self.start} - {self.end})" 41 | 42 | def TestToday(config): 43 | """ 44 | Method to get all holidays stored in configuration file and test if the current day is in holiday. 45 | """ 46 | 47 | today = date.today() 48 | 49 | try: 50 | with open(config, 'r') as f: 51 | holidays_dict = yaml.load(f.read(), Loader=yaml.SafeLoader) 52 | except FileNotFoundError as e: 53 | print(f"The file {config} does not seem to exists, you need to configure it first with the Webui.") 54 | return None 55 | except yaml.scanner.ScannerError as e: 56 | print(f"The file {config} does not seem to respect yaml standards, you need to verify it.") 57 | return None 58 | 59 | holidays = [] 60 | try: 61 | for name, dates in holidays_dict.items(): 62 | holidays.append(Holiday(name, dates['start'], dates['end'])) 63 | 64 | for holiday in holidays: 65 | if holiday.start <= today <= holiday.end: 66 | return holiday 67 | except AttributeError: 68 | print(f"The file {config} seems to be empty, you need to configure it first with the Webui.") 69 | 70 | return None 71 | 72 | def usage(): 73 | print(""" 74 | Usage: linuxmuster-holiday [options] 75 | Options: 76 | -s, --school - Specify school to process. Process 'default-school' if nothing is specified. 77 | -h, --help - Show this help 78 | """) 79 | 80 | if __name__ == "__main__": 81 | 82 | school = 'default-school' 83 | 84 | try: 85 | opts, args = getopt.getopt( 86 | sys.argv[1:], 87 | 'hs:', 88 | ['help', 'school='] 89 | ) 90 | except getopt.GetoptError as err: 91 | print(err) 92 | usage() 93 | sys.exit(2) 94 | 95 | for o, a in opts: 96 | if o in ("-h", "--help"): 97 | usage() 98 | sys.exit() 99 | elif o in ("-s", "--school"): 100 | school = a 101 | 102 | 103 | config = f"/etc/linuxmuster/sophomorix/{school}/holidays.yml" 104 | 105 | isTodayInHoliday = TestToday(config) 106 | 107 | if isTodayInHoliday is None: 108 | print("Today is not holiday") 109 | sys.exit() 110 | else: 111 | print(f"Today is holiday in: {isTodayInHoliday}") 112 | sys.exit(-1) 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /sbin/linuxmuster-holiday-generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime, date 4 | import requests 5 | from requests.structures import CaseInsensitiveDict 6 | import argparse 7 | import yaml 8 | import sys 9 | 10 | """ 11 | Simple script to generate yaml file for holidays based on ferien-api.de 12 | 13 | The user has to take care of the output, maybe pipe it into /etc/linuxmuster/sophomorix/SCHOOL/holidays.yml 14 | 15 | Structure of the configuration file (YAML): 16 | holiday_name1: 17 | start: "dd.mm.yyyy" 18 | end: "dd.mm.yyyy" 19 | holiday_name2: 20 | start: "dd.mm.yyyy" 21 | end: "dd.mm.yyyy" 22 | """ 23 | 24 | def get_holidays(year,state) -> list: 25 | url = "https://ferien-api.de/api/v1/holidays/" + state 26 | headers = CaseInsensitiveDict() 27 | headers["Content-Type"] = "application/json" 28 | resp = requests.get(url, headers=headers) 29 | content = resp.json() 30 | holidays = [] 31 | 32 | for entry in content: 33 | if entry['year'] == int(year) or entry['year'] == int(year)+1: 34 | start = datetime.strptime(entry['start'], '%Y-%m-%dT%H:%MZ') 35 | end = datetime.strptime(entry['end'], '%Y-%m-%dT%H:%MZ') 36 | holidays.append({entry['name']:{'start': start.strftime('%d.%m.%Y'), 'end': end.strftime('%d.%m.%Y')}}) 37 | return holidays 38 | 39 | def print_holiday_yaml(holidays): 40 | for holiday in holidays: 41 | yaml.dump(holiday, sys.stdout, default_flow_style=False) 42 | 43 | 44 | def main(): 45 | possible_states = ["BW", "BY", "BE", "BB", "HB", "HH", "HE", "MV", "NI", "NW", "RP", "SL", "SN", "ST", "SH", "TH"] 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument("-y", "--year", required = False, help = "Define which year too look for") 48 | parser.add_argument("-s", "--state", required = True, help = "Define state, possible values are: " + ','.join(possible_states)) 49 | 50 | args = parser.parse_args() 51 | 52 | args.state = args.state.upper() 53 | if args.state not in possible_states: 54 | print ("Provided state is not supported.\nSupported states are: "+ ','.join(possible_states)) 55 | quit(1) 56 | 57 | if not args.year: 58 | args.year = date.today().year 59 | 60 | holidays = get_holidays(args.year,args.state) 61 | print_holiday_yaml(holidays) 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /sbin/linuxmuster-import-devices: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # linuxmuster-import-devices 4 | # thomas@linuxmuster.net 5 | # 20250503 6 | # 7 | 8 | import configparser 9 | import environment 10 | import fnmatch 11 | import os 12 | import subprocess 13 | import sys 14 | import getopt 15 | import csv 16 | 17 | from os import listdir 18 | from os.path import isfile, join 19 | from pathlib import Path 20 | 21 | from functions import getDevicesArray, getGrubOstype, getGrubPart, getStartconfOsValues, \ 22 | getStartconfOption, getStartconfPartnr, getStartconfPartlabel, getSubnetArray, \ 23 | getLinboVersion, printScript, readTextfile, subProc, writeTextfile 24 | 25 | 26 | def usage(): 27 | print('Usage: linuxmuster-import-devices [options]') 28 | print(' [options] may be:') 29 | print(' -s , --school= : Select a school other than default-school.') 30 | 31 | 32 | # read commandline arguments 33 | # get cli args 34 | try: 35 | opts, args = getopt.getopt(sys.argv[1:], "s:", ["school="]) 36 | except getopt.GetoptError as err: 37 | # print help information and exit: 38 | print(err) # will print something like "option -a not recognized" 39 | usage() 40 | sys.exit(2) 41 | 42 | # default valued 43 | school = 'default-school' 44 | 45 | # evaluate options 46 | for o, a in opts: 47 | if o in ("-s", "--school"): 48 | school = a 49 | 50 | # default school's devices.csv 51 | devices = environment.WIMPORTDATA 52 | 53 | # read INIFILE 54 | setup = configparser.ConfigParser(delimiters=( 55 | '='), inline_comment_prefixes=('#', ';')) 56 | setup.read(environment.SETUPINI) 57 | serverip = setup.get('setup', 'serverip') 58 | domainname = setup.get('setup', 'domainname') 59 | 60 | # start message 61 | printScript(os.path.basename(__file__), 'begin') 62 | 63 | # do sophomorix-devices first 64 | msg = 'Starting sophomorix-device syntax check:' 65 | printScript(msg) 66 | try: 67 | msg = 'sophomorix-device finished ' 68 | subProc('sophomorix-device --sync') 69 | printScript(msg + ' OK!') 70 | except Exception as err: 71 | printScript(msg + ' errors detected!') 72 | print(err) 73 | sys.exit(1) 74 | 75 | 76 | # functions begin 77 | 78 | 79 | # delete symlinks 80 | def delSymlinksByPattern(directory, pattern): 81 | # check if dir exists 82 | if not os.path.exists(directory): 83 | #print(f"Directory '{directory}' does not exist.") # debug 84 | return 85 | # iterate through dir 86 | for root, dirs, files in os.walk(directory): 87 | for name in files + dirs: 88 | path = os.path.join(root, name) 89 | # test for symlink 90 | if os.path.islink(path) and fnmatch.fnmatch(name, pattern): 91 | try: 92 | os.unlink(path) # delete symlink 93 | #print(f"Symlink deleted: {path}") # debug 94 | except Exception as err: 95 | continue 96 | #print(f"Deletion of {path} failed: {err}") # debug 97 | 98 | 99 | # write grub cfgs 100 | def doGrubCfg(startconf, group, kopts): 101 | grubcfg = environment.LINBOGRUBDIR + '/' + group + '.cfg' 102 | rc, content = readTextfile(grubcfg) 103 | if rc and environment.MANAGEDSTR not in content: 104 | return 'present' 105 | # get grub partition name of cache 106 | cache = getStartconfOption(startconf, 'LINBO', 'Cache') 107 | partnr = getStartconfPartnr(startconf, cache) 108 | cacheroot = getGrubPart(cache) 109 | cachelabel = getStartconfPartlabel(startconf, partnr) 110 | # if cache is not defined provide a forced netboot cfg 111 | if cacheroot is None: 112 | netboottpl = environment.LINBOTPLDIR + '/grub.cfg.forced_netboot' 113 | subProc('cp ' + netboottpl + ' ' + grubcfg) 114 | return 'not yet configured!' 115 | # create return message 116 | if os.path.isfile(grubcfg): 117 | msg = 'replaced' 118 | else: 119 | msg = 'created' 120 | # create gobal part for group cfg 121 | globaltpl = environment.LINBOTPLDIR + '/grub.cfg.global' 122 | rc, content = readTextfile(globaltpl) 123 | if not rc: 124 | return 'error!' 125 | replace_list = [('@@group@@', group), ('@@cachelabel@@', cachelabel), 126 | ('@@cacheroot@@', cacheroot), ('@@kopts@@', kopts)] 127 | for item in replace_list: 128 | content = content.replace(item[0], item[1]) 129 | rc = writeTextfile(grubcfg, content, 'w') 130 | # get os infos from group's start.conf 131 | oslists = getStartconfOsValues(startconf) 132 | if oslists is None: 133 | return 'error!' 134 | # write os parts to grub cfg 135 | ostpl_pre = environment.LINBOTPLDIR + '/grub.cfg.os' 136 | for oslist in oslists: 137 | osname, baseimage, partition, kernel, initrd, kappend, osnr = oslist 138 | osroot = getGrubPart(partition) 139 | ostype = getGrubOstype(osname) 140 | partnr = getStartconfPartnr(startconf, partition) 141 | oslabel = getStartconfPartlabel(startconf, partnr) 142 | # different grub.cfg template for os. which is booted from live iso 143 | imagename, ext = os.path.splitext(baseimage) 144 | if ext == '.iso': 145 | ostpl = ostpl_pre + '-iso' 146 | else: 147 | ostpl = ostpl_pre 148 | # add root to kernel append 149 | if 'root=' not in kappend and ext != '.iso': 150 | try: 151 | kappend = kappend + ' root=LABEL=' + oslabel 152 | except: 153 | kappend = kappend + ' root=' + partition 154 | rc, content = readTextfile(ostpl) 155 | if not rc: 156 | return 'error!' 157 | replace_list = [('@@group@@', group), ('@@cachelabel@@', cachelabel), 158 | ('@@baseimage@@', baseimage), ('@@cacheroot@@', cacheroot), 159 | ('@@osname@@', osname), ('@@osnr@@', osnr), 160 | ('@@ostype@@', ostype), ('@@oslabel@@', oslabel), 161 | ('@@osroot@@', osroot), ('@@partnr@@', partnr), 162 | ('@@kernel@@', kernel), ('@@initrd@@', initrd), 163 | ('@@kopts@@', kopts), ('@@append@@', kappend)] 164 | for item in replace_list: 165 | content = content.replace(item[0], str(item[1])) 166 | rc = writeTextfile(grubcfg, content, 'a') 167 | if not rc: 168 | return 'error!' 169 | return msg 170 | 171 | 172 | # write linbo start configuration file 173 | def doLinboStartconf(group): 174 | startconf = environment.LINBODIR + '/start.conf.' + group 175 | # provide unconfigured start.conf if there is none for this group 176 | if os.path.isfile(startconf): 177 | if getStartconfOption(startconf, 'LINBO', 'Cache') is None: 178 | msg1 = 'not yet configured!' 179 | else: 180 | msg1 = 'present' 181 | else: 182 | msg1 = 'not yet configured!' 183 | subProc('cp ' + environment.LINBODIR + '/start.conf ' + startconf) 184 | # read kernel options from start.conf 185 | kopts = getStartconfOption(startconf, 'LINBO', 'KernelOptions') 186 | # process grub cfgs 187 | msg2 = doGrubCfg(startconf, group, kopts) 188 | # format row in columns for output 189 | row = [group, msg1, msg2] 190 | printScript(" {: <15} | {: <20} | {: <20}".format(*row)) 191 | 192 | 193 | # write dhcp subnet devices config 194 | def writeDhcpDevicesConfig(school='default-school'): 195 | printScript('', 'begin') 196 | printScript('Working on dhcp configuration for devices') 197 | host_decl_tpl = """host @@hostname@@ { 198 | option host-name "@@hostname@@"; 199 | hardware ethernet @@mac@@; 200 | """ 201 | baseConfigFilePath = environment.DHCPDEVCONF 202 | devicesConfigBasedir = "/etc/dhcp/devices" 203 | Path(devicesConfigBasedir).mkdir(parents=True, exist_ok=True) 204 | 205 | cfgfile = devicesConfigBasedir + "/" + school + ".conf" 206 | if os.path.isfile(cfgfile): 207 | os.unlink(cfgfile) 208 | if os.path.isfile(baseConfigFilePath): 209 | os.unlink(baseConfigFilePath) 210 | try: 211 | # open devices/.conf for append 212 | with open(cfgfile, 'a') as outfile: 213 | # iterate over the defined subnets 214 | subnets = getSubnetArray('0') 215 | subnets.append(['DHCP']) 216 | for item in subnets: 217 | subnet = item[0] 218 | # iterate over devices per subnet 219 | headline = False 220 | for device_array in getDevicesArray(fieldnrs='1,2,3,4,7,8,10', subnet=subnet, school=school): 221 | if not headline: 222 | # write corresponding subnet as a comment 223 | if subnet == 'DHCP': 224 | outfile.write('# dynamic ip hosts\n') 225 | printScript('* dynamic ip hosts:') 226 | else: 227 | outfile.write('# subnet ' + subnet + '\n') 228 | printScript('* in subnet ' + subnet + ':') 229 | headline = True 230 | hostname, group, mac, ip, dhcpopts, computertype, pxeflag = device_array 231 | if len(computertype) > 15: 232 | computertype = computertype[0:15] 233 | # format row in columns for output 234 | row = [hostname, ip, computertype, pxeflag] 235 | printScript( 236 | " {: <15} | {: <15} | {: <15} | {: <1}".format(*row)) 237 | # begin host declaration 238 | host_decl = host_decl_tpl.replace( 239 | '@@mac@@', mac).replace('@@hostname@@', hostname) 240 | # fixed ip 241 | if ip != 'DHCP': 242 | host_decl = host_decl + ' fixed-address ' + ip + ';\n' 243 | # only for pxe clients 244 | if int(pxeflag) != 0: 245 | host_decl = host_decl + ' option extensions-path "' + group + '";\n option nis-domain "' + group + '";\n' 246 | # dhcp options have to be 5 chars minimum to get processed 247 | if len(dhcpopts) > 4: 248 | for opt in dhcpopts.split(','): 249 | host_decl = host_decl + ' ' + opt + ';\n' 250 | # finish host declaration 251 | host_decl = host_decl + '}\n' 252 | # finally write host declaration 253 | outfile.write(host_decl) 254 | 255 | # open devices.conf for append 256 | with open(baseConfigFilePath, 'a') as outfile: 257 | for devicesConf in listdir(devicesConfigBasedir): 258 | outfile.write( 259 | "include \"{0}/{1}\";\n".format(devicesConfigBasedir, devicesConf)) 260 | 261 | except Exception as error: 262 | print(error) 263 | return False 264 | 265 | 266 | # Create necessary host-based symlinks 267 | def doSchoolSpecificGroupLinksAndGetPxeGroups(school='default-school'): 268 | pxe_groups = [] 269 | 270 | # clean up 271 | linksFileBasepath = environment.LINBODIR + "/boot/links" 272 | Path(linksFileBasepath).mkdir(parents=True, exist_ok=True) 273 | linksFile = linksFileBasepath + "/" + school + ".csv" 274 | if os.path.isfile(linksFile): 275 | os.unlink(linksFile) 276 | 277 | with open(linksFile, "w+") as csvfile: 278 | csvWriter = csv.writer(csvfile, delimiter=';', 279 | quotechar='"', quoting=csv.QUOTE_MINIMAL) 280 | 281 | for device_array in getDevicesArray(fieldnrs='1,2,3,4,10', subnet='all', pxeflag='1,2,3', school=school): 282 | host, group, mac, ip, pxeflag = device_array 283 | # collect groups with pxe for later use 284 | if group not in pxe_groups: 285 | pxe_groups.append(group) 286 | 287 | # format row in columns for output 288 | printScript(" {: <15} | {: <15}".format(host, group)) 289 | 290 | # start.conf 291 | linkSource = 'start.conf.' + group 292 | linkTarget = environment.LINBODIR + '/start.conf-' 293 | if ip == 'DHCP': 294 | linkTarget += mac.lower() 295 | else: 296 | linkTarget += ip 297 | csvWriter.writerow([linkSource, linkTarget]) 298 | 299 | # Grub.cfg 300 | linkSource = '../' + group + '.cfg' 301 | linkTarget = environment.LINBOGRUBDIR + '/hostcfg/' + host + '.cfg' 302 | csvWriter.writerow([linkSource, linkTarget]) 303 | 304 | return pxe_groups 305 | 306 | 307 | # look up all links for all schools and place them in the correct place 308 | def doAllGroupLinks(): 309 | # delete old config links 310 | delSymlinksByPattern(environment.LINBODIR, "start.conf-*") 311 | delSymlinksByPattern(environment.LINBOGRUBDIR + "/hostcfg", "*.cfg") 312 | 313 | linksConfBasedir = environment.LINBODIR + "/boot/links" 314 | for schoolLinksConf in listdir(linksConfBasedir): 315 | schoolLinksConfPath = linksConfBasedir + "/" + schoolLinksConf 316 | if not os.path.isfile(schoolLinksConfPath) or not schoolLinksConf.endswith(".csv"): 317 | continue 318 | 319 | with open(schoolLinksConfPath, newline='') as csvfile: 320 | csvReader = csv.reader(csvfile, delimiter=';', quotechar='"') 321 | for row in csvReader: 322 | os.symlink(row[0], row[1]) 323 | 324 | # functions end 325 | 326 | 327 | # write dhcp devices.conf 328 | writeDhcpDevicesConfig(school=school) 329 | 330 | 331 | # linbo stuff 332 | linbo_version = int(getLinboVersion().split('.')[0]) 333 | printScript('', 'begin') 334 | printScript('Working on linbo/grub configuration for devices:') 335 | 336 | pxe_groups = doSchoolSpecificGroupLinksAndGetPxeGroups(school=school) 337 | 338 | # resolve all links and place them 339 | doAllGroupLinks() 340 | 341 | # write pxe configs for collected groups 342 | printScript('', 'begin') 343 | printScript('Working on linbo/grub configuration for groups:') 344 | printScript(" {: <15} | {: <20} | {: <20}".format( 345 | *[' ', 'linbo start.conf', 'grub cfg'])) 346 | printScript(" {: <15}+{: <20}+{: <20}".format(*['-'*16, '-'*22, '-'*21])) 347 | for group in pxe_groups: 348 | doLinboStartconf(group) 349 | 350 | 351 | # execute post hooks 352 | hookpath = environment.POSTDEVIMPORT 353 | hookscripts = [f for f in listdir(hookpath) if isfile( 354 | join(hookpath, f)) and os.access(join(hookpath, f), os.X_OK)] 355 | if len(hookscripts) > 0: 356 | printScript('', 'begin') 357 | printScript('Executing post hooks:') 358 | for h in hookscripts: 359 | hookscript = hookpath + '/' + h 360 | msg = '* ' + h + ' ' 361 | printScript(msg, '', False, False, True) 362 | output = subprocess.check_output([hookscript, "-s", school]).decode('utf-8') 363 | if output != '': 364 | print(output) 365 | 366 | # restart services 367 | printScript('', 'begin') 368 | printScript('Finally restarting dhcp service.') 369 | subProc('service isc-dhcp-server restart') 370 | 371 | # end message 372 | printScript(os.path.basename(__file__), 'end') 373 | -------------------------------------------------------------------------------- /sbin/linuxmuster-import-subnets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # linuxmuster-import-subnets 4 | # thomas@linuxmuster.net 5 | # 20230801 6 | # 7 | 8 | import ast 9 | import environment 10 | import datetime 11 | import os 12 | import re 13 | import subprocess 14 | import time 15 | import yaml 16 | 17 | from bs4 import BeautifulSoup 18 | from functions import firewallApi, getFwConfig, getSetupValue, getSftp 19 | from functions import getSubnetArray, ipMatchSubnet, isValidHostIpv4 20 | from functions import printScript, putFwConfig, putSftp, readTextfile 21 | from functions import sshExec, writeTextfile 22 | from IPy import IP 23 | 24 | # read necessary values from setup.ini and other sources 25 | serverip = getSetupValue('serverip') 26 | domainname = getSetupValue('domainname') 27 | gateway = getSetupValue('gateway') 28 | firewallip = getSetupValue('firewallip') 29 | # get boolean value 30 | skipfw = ast.literal_eval(getSetupValue('skipfw')) 31 | bitmask_setup = getSetupValue('bitmask') 32 | network_setup = getSetupValue('network') 33 | ipnet_setup = network_setup + '/' + bitmask_setup 34 | 35 | 36 | # template variables 37 | 38 | # lan gateway 39 | gw_lan_descr = 'Interface LAN Gateway' 40 | gw_lan_xml = """ 41 | 42 | lan 43 | @@gw_ip@@ 44 | @@gw_lan@@ 45 | 1 46 | inet 47 | 48 | @@gw_lan_descr@@ 49 | 50 | 51 | 52 | 1 53 | 54 | """ 55 | gw_lan_xml = gw_lan_xml.replace('@@gw_lan@@', environment.GW_LAN).replace( 56 | '@@gw_lan_descr@@', gw_lan_descr) 57 | 58 | # outbound nat rules 59 | nat_rule_descr = 'Outbound NAT rule for subnet' 60 | nat_rule_xml = """ 61 | 62 | 63 | @@subnet@@ 64 | 65 | 66 | 1 67 | 68 | @@nat_rule_descr@@ @@subnet@@ 69 | wan 70 | 71 | 72 | 73 | inet 74 | 75 | root@@@serverip@@ 76 | 77 | linuxmuster-import-subnet made changes 78 | 79 | 80 | 0 81 | 82 | 83 | """ 84 | nat_rule_xml = nat_rule_xml.replace( 85 | '@@nat_rule_descr@@', nat_rule_descr).replace('@@serverip@@', serverip) 86 | 87 | 88 | # functions begin 89 | # update static routes in netplan configuration 90 | def updateNetplan(subnets): 91 | printScript('Processing netplan configuration:') 92 | cfgfile = environment.NETCFG 93 | # create backup of current configuration 94 | timestamp = str(datetime.datetime.now()).replace('-', '').replace(' ', '').replace(':', '').split('.')[0] 95 | bakfile = cfgfile + '-' + timestamp 96 | rc = subprocess.call('cp ' + cfgfile + ' ' + bakfile, shell=True) 97 | if rc != 0: 98 | printScript('* Failed to backup ' + cfgfile + '!') 99 | return False 100 | # read netplan config file 101 | with open(cfgfile) as config: 102 | netcfg = yaml.safe_load(config) 103 | iface = str(netcfg['network']['ethernets']).split('\'')[1] 104 | ifcfg = netcfg['network']['ethernets'][iface] 105 | # remove deprecated gateway4 106 | try: 107 | del ifcfg['gateway4'] 108 | printScript('* Removed deprecated gateway4 statement.') 109 | except: 110 | None 111 | # first delete the old routes if there are any 112 | try: 113 | del ifcfg['routes'] 114 | printScript('* Removed old routes.') 115 | except: 116 | None 117 | # set default route 118 | ifcfg['routes'] = [] 119 | subroute = eval('{"to": \'default\', "via": \'' + gateway + '\'}') 120 | ifcfg['routes'].append(subroute) 121 | # add subnet routes if there are any beside server network 122 | if len(subnets) > 0: 123 | for item in subnets: 124 | # skip if subnet gateway is the default 125 | if servernet_router == gateway: 126 | continue 127 | subnet = item.split(':')[0] 128 | # tricky: concenate dict object for yaml using eval 129 | subroute = eval('{"to": \'' + subnet + '\', "via": \'' + servernet_router + '\'}') 130 | ifcfg['routes'].append(subroute) 131 | printScript('* Added new routes for all subnets.') 132 | # save netcfg 133 | with open(cfgfile, 'w') as config: 134 | config.write(yaml.dump(netcfg, default_flow_style=False)) 135 | rc = subprocess.call('netplan apply', shell=True) 136 | if rc == 0: 137 | printScript('* Applied new netplan configuration.') 138 | else: 139 | printScript('* Failed to apply new netplan configuration. Rolling back to previous status.') 140 | subprocess.call('cp ' + bakfile + ' ' + cfgfile, shell=True) 141 | subprocess.call('netplan apply', shell=True) 142 | return False 143 | 144 | 145 | # update vlan gateway on firewall 146 | def updateFwGw(servernet_router, content): 147 | soup = BeautifulSoup(content, 'lxml') 148 | # get all gateways 149 | gateways = soup.findAll('gateways')[0] 150 | soup = BeautifulSoup(str(gateways), 'lxml') 151 | # remove old lan gateway from gateways 152 | gw_array = [] 153 | for gw_item in soup.findAll('gateway_item'): 154 | if gw_lan_descr not in str(gw_item): 155 | gw_array.append(gw_item) 156 | # append new lan gateway 157 | gw_array.append(gw_lan_xml.replace('@@gw_ip@@', servernet_router)) 158 | # create gateways xml code 159 | gateways_xml = '' 160 | for gw_item in gw_array: 161 | gateways_xml = gateways_xml + str(gw_item) 162 | gateways_xml = gateways_xml + '\n' + '' 163 | content = re.sub(r'.*?', 164 | gateways_xml, content, flags=re.S) 165 | return True, content 166 | 167 | 168 | # update subnet nat rules on firewall 169 | def updateFwNat(subnets, ipnet_setup, serverip, content): 170 | # create array with all nat rules 171 | soup = BeautifulSoup(content, 'lxml') 172 | out_nat = soup.findAll('outbound')[0] 173 | soup = BeautifulSoup(str(out_nat), 'lxml') 174 | # remove old subnet rules from array 175 | nat_rules = [] 176 | for item in soup.findAll('rule'): 177 | if nat_rule_descr not in str(item): 178 | nat_rules.append(item) 179 | # add new subnet rules to array 180 | for item in subnets: 181 | subnet = item.split(':')[0] 182 | # skip servernet 183 | if subnet == ipnet_setup: 184 | continue 185 | timestamp = str(datetime.datetime.now(datetime.timezone.utc).timestamp()) 186 | nat_rule = nat_rule_xml.replace('@@subnet@@', subnet) 187 | nat_rule = nat_rule.replace('@@timestamp@@', timestamp) 188 | nat_rules.append(nat_rule) 189 | # create nat rules xml code 190 | nat_xml = '\n\nhybrid\n' 191 | for nat_rule in nat_rules: 192 | nat_xml = nat_xml + str(nat_rule) 193 | nat_xml = nat_xml + '\n' 194 | # replace code in config content 195 | content = re.sub(r'.*?', nat_xml, content, flags=re.S) 196 | return True, content 197 | 198 | 199 | # download, modify and upload firewall config 200 | def updateFw(subnets, firewallip, ipnet_setup, serverip, servernet_router, gw_lan_xml): 201 | # first get config.xml 202 | if not getFwConfig(firewallip): 203 | return False 204 | # load configfile 205 | rc, content = readTextfile(environment.FWCONFLOCAL) 206 | if not rc: 207 | return rc 208 | changed = False 209 | # add vlan gateway to firewall 210 | rc, content = updateFwGw(servernet_router, content) 211 | if rc: 212 | changed = rc 213 | # add subnet nat rules to firewall 214 | rc, content = updateFwNat(subnets, ipnet_setup, serverip, content) 215 | if rc: 216 | changed = rc 217 | if changed: 218 | # write changed config 219 | if writeTextfile(environment.FWCONFLOCAL, content, 'w'): 220 | printScript('* Saved changed config.') 221 | else: 222 | printScript('* Unable to save configfile!') 223 | return False 224 | if not putFwConfig(firewallip): 225 | return False 226 | return changed 227 | 228 | 229 | # add single route 230 | def addFwRoute(subnet): 231 | try: 232 | payload = '{"route": {"network": "' + subnet + '", "gateway": "' + \ 233 | environment.GW_LAN + '", "descr": "Route for subnet ' + \ 234 | subnet + '", "disabled": "0"}}' 235 | res = firewallApi('post', '/routes/routes/addroute', payload) 236 | printScript('* Added route for subnet ' + subnet + '.') 237 | return True 238 | except: 239 | printScript('* Unable to add route for subnet ' + subnet + '!') 240 | return False 241 | 242 | 243 | # delete route on firewall by uuid 244 | def delFwRoute(uuid, subnet): 245 | try: 246 | rc = firewallApi('post', '/routes/routes/delroute/' + uuid) 247 | printScript('* Route ' + uuid + ' - ' + subnet + ' deleted.') 248 | return True 249 | except: 250 | printScript('* Unable to delete route ' + uuid + ' - ' + subnet + '!') 251 | return False 252 | 253 | 254 | # update firewall routes 255 | def updateFwRoutes(subnets, ipnet_setup, servernet_router): 256 | printScript('Updating subnet routing on firewall:') 257 | try: 258 | routes = firewallApi('get', '/routes/routes/searchroute') 259 | staticroutes_nr = len(routes['rows']) 260 | printScript('* Got ' + str(staticroutes_nr) + ' routes.') 261 | except: 262 | printScript('* Unable to get routes.') 263 | return False 264 | # iterate through firewall routes and delete them if necessary 265 | changed = False 266 | gateway_orig = environment.GW_LAN + ' - ' + servernet_router 267 | if staticroutes_nr > 0: 268 | count = 0 269 | while (count < staticroutes_nr): 270 | uuid = routes['rows'][count]['uuid'] 271 | subnet = routes['rows'][count]['network'] 272 | gateway = routes['rows'][count]['gateway'] 273 | # delete not compliant routes 274 | if (subnet not in str(subnets) and gateway == gateway_orig) or (subnet in str(subnets) and gateway != gateway_orig): 275 | delFwRoute(uuid, subnet) 276 | printScript('* Route ' + subnet + ' deleted.') 277 | changed = True 278 | count += 1 279 | # get changed routes 280 | if changed: 281 | routes = firewallApi('get', '/routes/routes/searchroute') 282 | # find and collect routes to be added 283 | for subnet in subnets: 284 | # extract subnet from string 285 | s = subnet.split(':')[0] 286 | # skip server network 287 | if s == ipnet_setup: 288 | continue 289 | if s not in str(routes): 290 | rc = addFwRoute(s) 291 | if rc: 292 | changed = rc 293 | return changed 294 | # functions end 295 | 296 | 297 | # iterate over subnets 298 | printScript('linuxmuster-import-subnets') 299 | printScript('', 'begin') 300 | printScript('Reading setup data:') 301 | printScript('* Server address: ' + serverip) 302 | printScript('* Server network: ' + ipnet_setup) 303 | printScript('Processing dhcp subnets:') 304 | servernet_router = firewallip 305 | subnets = [] 306 | # collect subnet data and write dhcpd's subnet.conf 307 | subnetconf = open(environment.DHCPSUBCONF, 'w') 308 | for row in getSubnetArray(): 309 | try: 310 | ipnet = row[0] 311 | router = row[1] 312 | range1 = row[2] 313 | range2 = row[3] 314 | nameserver = row[4] 315 | except: 316 | continue 317 | try: 318 | nextserver = row[5] 319 | except: 320 | nextserver = '' 321 | if ipnet[:1] == '#' or ipnet[:1] == ';' or not isValidHostIpv4(router): 322 | continue 323 | if not isValidHostIpv4(range1) or not isValidHostIpv4(range2): 324 | range1 = '' 325 | range2 = '' 326 | if not isValidHostIpv4(nameserver): 327 | nameserver = '' 328 | if not isValidHostIpv4(nextserver): 329 | nextserver = '' 330 | 331 | # compute network data 332 | try: 333 | n = IP(ipnet, make_net=True) 334 | network = IP(n).strNormal(0) 335 | netmask = IP(n).strNormal(2).split('/')[1] 336 | broadcast = IP(n).strNormal(3).split('-')[1] 337 | except: 338 | continue 339 | # save servernet router address for later use 340 | if ipnet == ipnet_setup: 341 | servernet_router = router 342 | supp_info = 'server network' 343 | else: 344 | supp_info = '' 345 | subnets.append(ipnet + ':' + router) 346 | # write subnets.conf 347 | printScript('* ' + ipnet) 348 | subnetconf.write('# Subnet ' + ipnet + ' ' + supp_info + '\n') 349 | subnetconf.write('subnet ' + network + ' netmask ' + netmask + ' {\n') 350 | subnetconf.write(' option routers ' + router + ';\n') 351 | subnetconf.write(' option subnet-mask ' + netmask + ';\n') 352 | subnetconf.write(' option broadcast-address ' + broadcast + ';\n') 353 | if nameserver != '': 354 | subnetconf.write(' option domain-name-servers ' + nameserver + ';\n') 355 | nameserver = '' 356 | else: 357 | subnetconf.write(' option netbios-name-servers ' + serverip + ';\n') 358 | if nextserver != '': 359 | subnetconf.write(' next-server ' + nextserver + ';\n') 360 | if range1 != '': 361 | subnetconf.write(' range ' + range1 + ' ' + range2 + ';\n') 362 | subnetconf.write(' option host-name pxeclient;\n') 363 | subnetconf.write('}\n') 364 | 365 | subnetconf.close() 366 | 367 | # restart dhcp service 368 | service = 'isc-dhcp-server' 369 | msg = 'Restarting ' + service + ' ' 370 | printScript(msg, '', False, False, True) 371 | os.system('service ' + service + ' stop') 372 | os.system('service ' + service + ' start') 373 | # wait one second before service check 374 | time.sleep(1) 375 | rc = os.system('systemctl is-active --quiet ' + service) 376 | if rc == 0: 377 | printScript(' OK!', '', True, True, False, len(msg)) 378 | else: 379 | printScript(' Failed!', '', True, True, False, len(msg)) 380 | 381 | os.system('systemctl restart isc-dhcp-server.service') 382 | 383 | # update netplan config with new routes for server (localhost) 384 | changed = updateNetplan(subnets) 385 | 386 | # update firewall 387 | if not skipfw: 388 | changed = updateFw(subnets, firewallip, ipnet_setup, 389 | serverip, servernet_router, gw_lan_xml) 390 | if changed: 391 | changed = firewallApi('post', '/routes/routes/reconfigure') 392 | if changed: 393 | printScript('Applied new gateway.') 394 | changed = updateFwRoutes(subnets, ipnet_setup, servernet_router) 395 | if changed: 396 | changed = firewallApi('post', '/routes/routes/reconfigure') 397 | if changed: 398 | printScript('Applied new routes.') 399 | -------------------------------------------------------------------------------- /sbin/linuxmuster-modini: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # linuxmuster-modini 4 | # thomas@linuxmuster.net 5 | # 20191204 6 | # 7 | 8 | import getopt 9 | import os 10 | import sys 11 | from functions import modIni 12 | from functions import printLf 13 | 14 | 15 | def usage(): 16 | print('Modify ini files on command line. Usage: linuxmuster-modini [options]') 17 | print(' [options] may be:') 18 | print(' -i , --inifile= : Path to inifile (mandatory).') 19 | print(' -s , --section= : Name of section to work on (mandatory).') 20 | print(' -o , --option= : Name of option (mandatory).') 21 | print(' -v , --value= : value of option (mandatory).') 22 | print(' -r , --service= : Name of service to restart (optional).') 23 | print(' -h, --help : print this help') 24 | print("Example: linuxmuster-modini -i /etc/samba/smb.conf -s global -o 'time server' -v Yes -r samba-ad-dc") 25 | 26 | # get cli args 27 | try: 28 | opts, args = getopt.getopt(sys.argv[1:], "hi:o:r:s:v:", ["help", "inifile=", "section=", "option=", "value=", "service="]) 29 | except getopt.GetoptError as err: 30 | # print help information and exit: 31 | print(err) # will print something like "option -a not recognized" 32 | usage() 33 | sys.exit(2) 34 | 35 | 36 | # evaluate options 37 | inifile = None 38 | section = None 39 | option = None 40 | value = None 41 | service = None 42 | for o, a in opts: 43 | if o in ("-i", "--inifile"): 44 | inifile = a 45 | elif o in ("-s", "--section"): 46 | section = a 47 | elif o in ("-o", "--option"): 48 | option = a 49 | elif o in ("-v", "--value"): 50 | value = a 51 | elif o in ("-r", "--service"): 52 | service = a 53 | elif o in ("-h", "--help"): 54 | usage() 55 | sys.exit() 56 | else: 57 | assert False, "unhandled option" 58 | 59 | 60 | # is inifile there? 61 | if inifile is not None and not os.path.isfile(inifile): 62 | print('File not found!') 63 | usage() 64 | sys.exit() 65 | 66 | # check parameter values 67 | if section is None or option is None or value is None: 68 | print('Parameter error!') 69 | usage() 70 | sys.exit() 71 | 72 | 73 | # modify inifile 74 | printLf('Modifying ' + inifile + ' ... ', False) 75 | rc = modIni(inifile, section, option, value) 76 | if rc is True: 77 | rc = 0 78 | print('OK!') 79 | else: 80 | rc = 1 81 | print('Failed!') 82 | 83 | 84 | # restart service 85 | if service is not None and rc == 0: 86 | printLf('Restarting ' + service + ' ... ', False) 87 | rc = os.system('service ' + service + ' restart') 88 | if rc == 0: 89 | print('OK!') 90 | else: 91 | print('Failed!') 92 | 93 | sys.exit(rc) 94 | -------------------------------------------------------------------------------- /sbin/linuxmuster-opnsense-reset: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # reset opnsense configuration to setup state 4 | # thomas@linuxmuster.net 5 | # 20250422 6 | # 7 | 8 | import environment 9 | import getopt 10 | import os 11 | import sys 12 | import time 13 | 14 | from functions import createServerCert, datetime, enterPassword, firewallApi, \ 15 | getSetupValue, printScript, sshExec, subProc, writeTextfile, waitForFw 16 | 17 | 18 | # check first if firewall is skipped by setup 19 | skipfw = getSetupValue('skipfw') 20 | if skipfw: 21 | printScript('Firewall is skipped by setup!') 22 | sys.exit(0) 23 | 24 | 25 | infotxt = 'Sets the firewall to the state after setup.\n\ 26 | Custom adjustments made since then are lost.\n\ 27 | Note: The firewall will be restartet during the process.' 28 | 29 | 30 | def usage(): 31 | print('Usage: linuxmuster-opnsense-reset [options]') 32 | print(infotxt) 33 | print(' [options] may be:') 34 | print(' -f, --force : Force execution without asking for consent.') 35 | print(' -p, --pw= : Current firewall root password,') 36 | print(' if it is omitted script will ask for it.') 37 | print(' -s, --sleep=<#> : Sleep time in secs after firewall restart and before') 38 | print(' keytab creation (default 10).') 39 | print(' -h, --help : Print this help.') 40 | 41 | 42 | # get cli args 43 | try: 44 | opts, args = getopt.getopt(sys.argv[1:], "fhp:s:", ["force", "help", "pw=", "sleep="]) 45 | except getopt.GetoptError as err: 46 | # print help information and exit: 47 | print(err) # will print something like "option -a not recognized" 48 | usage() 49 | sys.exit(2) 50 | 51 | 52 | # evaluate options 53 | force = False 54 | adminpw = None 55 | sleep = 10 56 | for o, a in opts: 57 | if o in ("-f", "--force"): 58 | force = True 59 | elif o in ("-p", "--pw"): 60 | adminpw = a 61 | elif o in ("-s", "--sleep"): 62 | sleep = int(a) 63 | elif o in ("-h", "--help"): 64 | usage() 65 | sys.exit() 66 | else: 67 | assert False, "unhandled option" 68 | 69 | 70 | logfile = environment.LOGDIR + '/opnsense-reset.log' 71 | now = str(datetime.datetime.now()).split('.')[0] 72 | printScript('linuxmuster-opnsense-reset ' + now) 73 | 74 | 75 | # security prompt 76 | if not force: 77 | print(infotxt) 78 | answer = input('Do you want to continue (YES)? ') 79 | if answer != 'YES': 80 | sys.exit(0) 81 | 82 | 83 | # ask for password 84 | if adminpw is None: 85 | adminpw = enterPassword('the current firewall root', validate=False) 86 | 87 | 88 | # test ssh connection with provided password 89 | firewallip = getSetupValue('firewallip') 90 | if not sshExec(firewallip, 'exit', adminpw): 91 | sys.exit(1) 92 | 93 | 94 | # write password to temporary file 95 | if not writeTextfile('/tmp/linuxmuster-opnsense-reset', adminpw, 'w'): 96 | sys.exit(1) 97 | 98 | # create firewall cert if not there 99 | if not os.path.isfile(environment.SSLDIR + '/firewall.cert.pem'): 100 | if not createServerCert('firewall', logfile): 101 | sys.exit(1) 102 | 103 | # invoke setup script 104 | rc = subProc('python3 ' + environment.SETUPDIR + '/m_firewall.py', logfile) 105 | 106 | 107 | # wait for firewall 108 | try: 109 | waitForFw(wait=30) 110 | except Exception as error: 111 | print(error) 112 | sys.exit(1) 113 | 114 | printScript('Waiting ' + str(sleep) + ' seconds.') 115 | time.sleep(sleep) 116 | 117 | 118 | # delete old keytable 119 | rc = subProc(environment.FWSHAREDIR + '/create-keytab.py -c', logfile) 120 | if rc: 121 | printScript('Deleting old keytab.') 122 | apipath = '/proxysso/service/deletekeytab' 123 | res = firewallApi('get', apipath) 124 | print(res) 125 | 126 | printScript('Waiting ' + str(sleep) + ' seconds.') 127 | time.sleep(sleep) 128 | 129 | 130 | # create new keytab 131 | rc = subProc(environment.FWSHAREDIR + '/create-keytab.py', logfile) 132 | if rc: 133 | printScript('New kerberos key table has been successfully created.') 134 | else: 135 | printScript('Failed to create new kerberos key table. See opnsense-reset.log for details.') 136 | 137 | sys.exit(rc) 138 | -------------------------------------------------------------------------------- /sbin/linuxmuster-renew-certs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # renew self-signed server certs 4 | # thomas@linuxmuster.net 5 | # 20250425 6 | # 7 | 8 | import datetime 9 | import environment 10 | import getopt 11 | import os 12 | import shutil 13 | import sys 14 | 15 | from functions import catFiles, checkFwMajorVer, getFwConfig, getSetupValue, printScript, putFwConfig, \ 16 | readTextfile, replaceInFile, sshExec, subProc, tee 17 | 18 | 19 | # print usage info 20 | def usage(): 21 | print('Usage: linuxmuster-renew-certs [options]') 22 | print(' [options] may be:') 23 | print(' -c , --certs= : Comma separated list of certificates to be renewed') 24 | print(' ("ca", "server" and/or "firewall" or "all"). Mandatory.') 25 | print(' -d <#>, --days=<#> : Set number of days (default: 7305).') 26 | print(' -f, --force : Skip security prompt.') 27 | print(' -n, --dry-run : Test only if the firewall certs can be renewed.') 28 | print(' -r, --reboot : Reboot server and firewall finally.') 29 | print(' -h, --help : Print this help.') 30 | 31 | 32 | # get cli args 33 | try: 34 | opts, args = getopt.getopt(sys.argv[1:], "c:d:fhnr", ["certs=", "days=", "dry-run", "force", "help", "reboot"]) 35 | except getopt.GetoptError as err: 36 | # print help information and exit: 37 | print(err) # will print something like "option -a not recognized" 38 | usage() 39 | sys.exit(2) 40 | 41 | 42 | # default values 43 | dry = False 44 | force = False 45 | reboot = False 46 | days = '7305' 47 | all_list = ['ca', 'server', 'firewall'] 48 | cert_list = [] 49 | 50 | 51 | # open logfile 52 | logfile = environment.LOGDIR + '/renew-certs.log' 53 | try: 54 | l = open(logfile, 'a') 55 | orig_out = sys.stdout 56 | sys.stdout = tee(sys.stdout, l) 57 | sys.stderr = tee(sys.stderr, l) 58 | except Exception as err: 59 | printScript('Cannot open logfile ' + logfile + ' !') 60 | printScript(err) 61 | sys.exit() 62 | 63 | 64 | # start message 65 | printScript(os.path.basename(__file__), 'begin') 66 | 67 | 68 | # evaluate options 69 | for o, a in opts: 70 | if o in ("-c", "--certs"): 71 | if a == 'all': 72 | cert_list = all_list 73 | else: 74 | cert_list = a.split(',') 75 | elif o in ("-d", "--days"): 76 | days = str(a) 77 | elif o in ("-f", "--force"): 78 | force = True 79 | elif o in ("-n", "--dry-run"): 80 | dry = True 81 | elif o in ("-r", "--reboot"): 82 | reboot = True 83 | elif o in ("-h", "--help"): 84 | usage() 85 | sys.exit() 86 | else: 87 | assert False, "unhandled option" 88 | usage() 89 | sys.exit(1) 90 | if len(cert_list) == 0: 91 | printScript('No certs to renew given (-c)!') 92 | usage() 93 | sys.exit(1) 94 | 95 | 96 | # get setup values 97 | msg = 'Reading setup data.' 98 | printScript(msg) 99 | try: 100 | schoolname = getSetupValue('schoolname') 101 | servername = getSetupValue('servername') 102 | domainname = getSetupValue('domainname') 103 | sambadomain = getSetupValue('sambadomain') 104 | skipfw = getSetupValue('skipfw') 105 | realm = getSetupValue('realm') 106 | firewallip = getSetupValue('firewallip') 107 | except Exception as err: 108 | printScript(msg + ' errors detected!') 109 | print(err) 110 | sys.exit(1) 111 | 112 | 113 | # check options 114 | if skipfw and dry: 115 | printScript('Dry mode runs only with standard OPNsense firewall.') 116 | usage 117 | sys.exit(1) 118 | if skipfw and 'firewall' in cert_list: 119 | printScript('Renewing the firewall certificate works only with standard OPNsense firewall.') 120 | usage 121 | sys.exit(1) 122 | if dry: 123 | force = True 124 | cert_list = ['ca', 'firewall'] 125 | 126 | 127 | # security prompt 128 | if not force: 129 | msg = 'Attention! Please confirm the renewing of the server certificates.' 130 | printScript(msg) 131 | answer = input("Answer \"YES\" to proceed: ") 132 | if answer != "YES": 133 | sys.exit(1) 134 | 135 | 136 | # certificate environment 137 | ssldir = environment.SSLDIR 138 | cacert = environment.CACERT 139 | cacert_crt = environment.CACERTCRT 140 | cacert_subject = '-subj /O="' + schoolname + '"/OU=' + sambadomain + '/CN=' + realm + '/subjectAltName=' + realm + '/' 141 | cakey = environment.CAKEY 142 | rc, cakeypw = readTextfile(environment.CAKEYSECRET) 143 | cakey_passin = '-passin pass:' + cakeypw 144 | fwconftmp = environment.FWCONFLOCAL 145 | now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') 146 | fwconfbak = fwconftmp.replace('.xml', '-' + now + '.xml') 147 | 148 | 149 | # functions 150 | 151 | # test firewall if cert is renewable 152 | def testFw(item, b64): 153 | if skipfw: 154 | return 155 | msg = 'Test if ' + item + ' cert can be renewed:' 156 | printScript(msg) 157 | try: 158 | rc, b64_test = readTextfile(b64) 159 | with open(fwconftmp) as fwconf: 160 | if b64_test in fwconf.read(): 161 | printScript('* Success!') 162 | else: 163 | printScript('* Failed, certificate is unknown!') 164 | sys.exit(1) 165 | except Exception as err: 166 | printScript('Failed!') 167 | print(err) 168 | sys.exit(1) 169 | 170 | 171 | # patch firewall config with new cert 172 | def patchFwCert(new, old): 173 | msg = 'Patching firewall config with ' + os.path.basename(new) + '.' 174 | printScript(msg) 175 | try: 176 | rc, cert_old = readTextfile(old) 177 | rc, cert_new = readTextfile(new) 178 | replaceInFile(fwconftmp, cert_old, cert_new) 179 | except Exception as err: 180 | printScript('* Failed!') 181 | print(err) 182 | return False 183 | 184 | 185 | # check firewall version and download config 186 | def checkFw(): 187 | if skipfw: 188 | return 189 | try: 190 | checkFwMajorVer() 191 | getFwConfig(firewallip) 192 | shutil.copyfile(fwconftmp, fwconfbak) 193 | except Exception as err: 194 | printScript('Failed!') 195 | print(err) 196 | sys.exit(1) 197 | 198 | 199 | # apply firewall changes 200 | def applyFw(): 201 | if skipfw: 202 | return 203 | try: 204 | putFwConfig(firewallip) 205 | if reboot: 206 | sshExec(firewallip, '/sbin/reboot') 207 | except Exception as err: 208 | printScript('Failed!') 209 | print(err) 210 | sys.exit(1) 211 | 212 | 213 | # renew certificate 214 | def renewCert(item): 215 | if item == servername and servername != 'server': 216 | name = 'server' 217 | else: 218 | name = item 219 | if item == 'ca': 220 | pem = cacert 221 | else: 222 | key = ssldir + '/' + name + '.key.pem' 223 | pem = ssldir + '/' + name + '.cert.pem' 224 | csr = ssldir + '/' + name + '.csr' 225 | cnf = ssldir + '/' + name + '_cert_ext.cnf' 226 | chn = ssldir + '/' + name + '.fullchain.pem' 227 | bdl = ssldir + '/' + name + '.cert.bundle.pem' 228 | b64 = pem + '.b64' 229 | b64_old = b64 + '_old' 230 | if name == 'firewall' or name == 'ca': 231 | testFw(item, b64) 232 | if dry: return 233 | msg = 'Renewing ' + name + ' certificate.' 234 | printScript(msg) 235 | try: 236 | if name == 'ca': 237 | printScript('Note that you have to renew also all certs which depend on cacert.') 238 | subProc('openssl req -batch -x509 ' + cacert_subject + ' -new -nodes ' + cakey_passin 239 | + ' -key ' + cakey + ' -sha256 -days ' + days + ' -out ' + cacert, logfile) 240 | subProc('openssl x509 -in ' + cacert + ' -inform PEM -out ' + cacert_crt, logfile) 241 | else: 242 | subProc('openssl x509 -req -in ' + csr + ' -CA ' + cacert + ' ' + cakey_passin + ' -CAkey ' 243 | + cakey + ' -CAcreateserial -out ' + pem + ' -days ' + days + ' -sha256 -extfile ' + cnf, logfile) 244 | catFiles([pem, cacert], chn) 245 | catFiles([key, pem], bdl) 246 | if name == 'firewall' or name == 'ca': 247 | shutil.copyfile(b64, b64_old) 248 | subProc('base64 -w0 ' + pem + ' > ' + b64, logfile) 249 | patchFwCert(b64, b64_old) 250 | except Exception as err: 251 | printScript('Failed!') 252 | print(err) 253 | sys.exit(1) 254 | 255 | 256 | # reorder certlist to ensure ca is the first item 257 | def reorderCertlist(cert_list): 258 | cert_list.remove('ca') 259 | ordered_list = ['ca'] 260 | for item in cert_list: 261 | ordered_list.append(item) 262 | return ordered_list 263 | 264 | 265 | # main 266 | 267 | # reorder certlist to ensure ca is the first item 268 | if 'ca' in cert_list and len(cert_list) > 1 and cert_list[0] != 'ca': 269 | cert_list = reorderCertlist(cert_list) 270 | 271 | 272 | # check firewall version and download config 273 | if 'firewall' in cert_list or 'ca' in cert_list: 274 | checkFw() 275 | 276 | 277 | # iterate certificate items 278 | for item in cert_list: 279 | # process only valid items 280 | if item not in all_list: 281 | continue 282 | # renew cert 283 | renewCert(item) 284 | 285 | 286 | # dry mode 287 | if dry: 288 | printScript("Dry run finished successfully.") 289 | 290 | else: 291 | # apply firewall changes 292 | if 'firewall' in cert_list or 'ca' in cert_list: 293 | applyFw() 294 | 295 | # reboot server if requested 296 | if reboot: 297 | printScript("Rebooting server.") 298 | subProc('/sbin/reboot', logfile) 299 | 300 | 301 | # end message 302 | printScript(os.path.basename(__file__), 'end') 303 | -------------------------------------------------------------------------------- /sbin/linuxmuster-setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # linuxmuster-setup 4 | # thomas@linuxmuster.net 5 | # 20240219 6 | # 7 | 8 | import environment 9 | import getopt 10 | import importlib 11 | import os 12 | import sys 13 | from functions import checkFwMajorVer, modIni, printScript, subProc, tee 14 | 15 | 16 | def usage(): 17 | print('Usage: linuxmuster-setup [options]') 18 | print(' [options] may be:') 19 | print(' -n , --servername= : Set server hostname.') 20 | print(' -d , --domainname= : Set domainname.') 21 | print(' -r , --dhcprange= : Set dhcp range.') 22 | print(' -a , --adminpw= : Set admin password.') 23 | print(' -e , --schoolname= : Set school name.') 24 | print(' -l , --location= : Set school location.') 25 | print(' -z , --country= : Set school country.') 26 | print(' -v , --state= : Set school state.') 27 | print(' -c , --config= : path to ini file with setup values') 28 | print(' -u, --unattended : unattended mode, do not ask questions') 29 | print(' -s, --skip-fw : skip firewall setup per ssh') 30 | print(' -h, --help : print this help') 31 | 32 | 33 | # get cli args 34 | try: 35 | opts, args = getopt.getopt(sys.argv[1:], "a:c:d:e:hl:n:r:suv:z:", 36 | ["adminpw=", "config=", "domainname=", "schoolname=", "help", 37 | "location=", "servername=", "dhcprange=", "skip-fw", "unattended", "state=", "country="]) 38 | except getopt.GetoptError as err: 39 | # print help information and exit: 40 | print(err) # will print something like "option -a not recognized" 41 | usage() 42 | sys.exit(2) 43 | 44 | # default values 45 | unattended = False 46 | skipfw = False 47 | servername = '' 48 | domainname = '' 49 | dhcprange = '' 50 | adminpw = '' 51 | schoolname = '' 52 | location = '' 53 | country = '' 54 | state = '' 55 | cli_customini = '' 56 | 57 | # open logfile 58 | global logfile 59 | logfile = environment.SETUPLOG 60 | subProc('touch ' + logfile) 61 | subProc('chmod 600 ' + logfile) 62 | try: 63 | l = open(logfile, 'w') 64 | orig_out = sys.stdout 65 | sys.stdout = tee(sys.stdout, l) 66 | sys.stderr = tee(sys.stderr, l) 67 | except: 68 | print('Cannot open logfile ' + logfile + ' !') 69 | sys.exit() 70 | 71 | # evaluate options 72 | for o, a in opts: 73 | if o in ("-u", "--unattended"): 74 | unattended = True 75 | elif o in ("-v", "--state"): 76 | state = a 77 | elif o in ("-z", "--country"): 78 | country = a 79 | elif o in ("-l", "--location"): 80 | location = a 81 | elif o in ("-e", "--schoolname"): 82 | schoolname = a 83 | elif o in ("-a", "--adminpw"): 84 | adminpw = a 85 | elif o in ("-n", "--servername"): 86 | servername = a 87 | elif o in ("-d", "--domainname"): 88 | domainname = a 89 | elif o in ("-r", "--dhcprange"): 90 | dhcprange = a 91 | elif o in ("-s", "--skip-fw"): 92 | skipfw = True 93 | elif o in ("-c", "--config"): 94 | if os.path.isfile(a): 95 | cli_customini = a 96 | else: 97 | usage() 98 | sys.exit() 99 | elif o in ("-h", "--help"): 100 | usage() 101 | sys.exit() 102 | else: 103 | assert False, "unhandled option" 104 | 105 | # start message 106 | printScript(os.path.basename(__file__), 'begin') 107 | 108 | # custom ini file given on cli 109 | if cli_customini != '': 110 | print('Custom inifile ' + cli_customini 111 | + ' given on cli, ignoring other arguments!') 112 | subProc('cp ' + cli_customini + ' ' + environment.CUSTOMINI) 113 | subProc('chmod 600 ' + environment.CUSTOMINI) 114 | else: 115 | # check params 116 | print('Processing commandline arguments.') 117 | if servername != '': 118 | rc = modIni(environment.CUSTOMINI, 'setup', 'servername', servername) 119 | if domainname != '': 120 | rc = modIni(environment.CUSTOMINI, 'setup', 'domainname', domainname) 121 | if dhcprange != '': 122 | rc = modIni(environment.CUSTOMINI, 'setup', 'dhcprange', dhcprange) 123 | if adminpw != '': 124 | rc = modIni(environment.CUSTOMINI, 'setup', 'adminpw', adminpw) 125 | if schoolname != '': 126 | rc = modIni(environment.CUSTOMINI, 'setup', 'schoolname', schoolname) 127 | if location != '': 128 | rc = modIni(environment.CUSTOMINI, 'setup', 'location', location) 129 | if country != '': 130 | rc = modIni(environment.CUSTOMINI, 'setup', 'country', country) 131 | if state != '': 132 | rc = modIni(environment.CUSTOMINI, 'setup', 'state', state) 133 | rc = modIni(environment.CUSTOMINI, 'setup', 'skipfw', str(skipfw)) 134 | 135 | 136 | # work off setup modules 137 | setup_modules = os.listdir(environment.SETUPDIR) 138 | setup_modules.remove('__pycache__') 139 | setup_modules.sort() 140 | for module_file in setup_modules: 141 | # skip dialog in unattended mode 142 | if (unattended and 'dialog.py' in module_file): 143 | continue 144 | # check firewall major version 145 | if (not skipfw and 'templates.py' in module_file): 146 | if not checkFwMajorVer(): 147 | sys.exit(1) 148 | # print module name 149 | module_name = os.path.basename(os.path.splitext(module_file)[0]).split('_')[1] 150 | printScript('', 'begin') 151 | printScript(module_name) 152 | # execute module 153 | importlib.import_module(module_file.replace('.py', '')) 154 | 155 | printScript(os.path.basename(__file__), 'end') 156 | -------------------------------------------------------------------------------- /share/examples/create-testusers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # create a bunch of testusers 4 | # thomas@linuxmuster.net 5 | # 20211218 6 | # 7 | 8 | import configparser 9 | import environment 10 | import getopt 11 | import os 12 | import sys 13 | 14 | from functions import printScript 15 | from functions import sambaTool 16 | from functions import subProc 17 | from functions import replaceInFile 18 | from shutil import copyfile 19 | from subprocess import Popen, PIPE, STDOUT 20 | 21 | starget = environment.DEFAULTSCHOOL + '/students.csv' 22 | ttarget = environment.DEFAULTSCHOOL + '/teachers.csv' 23 | 24 | 25 | def usage(): 26 | print('Usage: create-testusers.py [options]') 27 | print(' [options] may be:') 28 | print(' -f, --force : Ignore existing users.') 29 | print(' -h, --help : Print this help.') 30 | 31 | 32 | # get cli args 33 | force = False 34 | try: 35 | opts, args = getopt.getopt(sys.argv[1:], "fh", ["force", "help"]) 36 | except getopt.GetoptError as err: 37 | # print help information and exit: 38 | print(err) # will print something like "option -a not recognized" 39 | usage() 40 | sys.exit(2) 41 | for o, a in opts: 42 | if o in ("-f", "--force"): 43 | force = True 44 | elif o in ("-h", "--help"): 45 | usage() 46 | sys.exit() 47 | else: 48 | assert False, "unhandled option" 49 | 50 | # do not overwrite existing user files 51 | if not force: 52 | if os.path.isfile(starget) or os.path.isfile(ttarget): 53 | print('There are already users on the system!') 54 | usage() 55 | sys.exit(1) 56 | 57 | # copy example user files 58 | ssource = environment.EXAMPLEDIR + '/students.csv' 59 | tsource = environment.EXAMPLEDIR + '/teachers.csv' 60 | copyfile(ssource, starget) 61 | copyfile(tsource, ttarget) 62 | 63 | # script header 64 | filename = os.path.basename(__file__).replace('.py', '') 65 | logfile = environment.LOGDIR + '/' + filename + '.log' 66 | 67 | title = 'Creating test users for default-school' 68 | printScript('', 'begin') 69 | printScript(title) 70 | 71 | msg = 'Logging to ' + logfile 72 | printScript(msg) 73 | 74 | # set password policy 75 | msg = 'Password policy setup ' 76 | printScript(msg, '', False, False, True) 77 | try: 78 | replaceInFile(environment.SCHOOLCONF, 'RANDOM_PWD=yes', 'RANDOM_PWD=no') 79 | printScript(' Success!', '', True, True, False, len(msg)) 80 | except: 81 | printScript(' Failed!', '', True, True, False, len(msg)) 82 | sys.exit(1) 83 | 84 | # check 85 | msg = 'Running sophomorix-check ' 86 | printScript(msg, '', False, False, True) 87 | try: 88 | subProc('sophomorix-check', logfile) 89 | printScript(' Success!', '', True, True, False, len(msg)) 90 | except: 91 | printScript(' Failed!', '', True, True, False, len(msg)) 92 | sys.exit(1) 93 | 94 | # add 95 | msg = 'Running sophomorix-add ' 96 | printScript(msg, '', False, False, True) 97 | try: 98 | subProc('sophomorix-add', logfile) 99 | printScript(' Success!', '', True, True, False, len(msg)) 100 | except: 101 | printScript(' Failed!', '', True, True, False, len(msg)) 102 | sys.exit(1) 103 | 104 | # quota 105 | msg = 'Running sophomorix-quota ' 106 | printScript(msg, '', False, False, True) 107 | try: 108 | subProc('sophomorix-quota', logfile) 109 | printScript(' Success!', '', True, True, False, len(msg)) 110 | except: 111 | printScript(' Failed!', '', True, True, False, len(msg)) 112 | sys.exit(1) 113 | 114 | # get usernames 115 | msg = 'Get usernames ' 116 | printScript(msg, '', False, False, True) 117 | try: 118 | students = os.popen( 119 | "sophomorix-query --schoolbase default-school --student --user-minimal | grep [1-9]: | awk '{ print $2 }'").read().split('\n') 120 | teachers = os.popen( 121 | "sophomorix-query --schoolbase default-school --teacher --user-minimal | grep [1-9]: | awk '{ print $2 }'").read().split('\n') 122 | printScript(' Success!', '', True, True, False, len(msg)) 123 | except: 124 | printScript(' Failed!', '', True, True, False, len(msg)) 125 | sys.exit(1) 126 | 127 | # change password to Muster! 128 | pw = environment.ROOTPW 129 | msg = 'Setting user passwords to "' + pw + '" ' 130 | printScript(msg) 131 | for user in students + teachers: 132 | if user == '': 133 | continue 134 | msg = ' * ' + user + ' ' 135 | printScript(msg, '', False, False, True) 136 | try: 137 | subProc('sophomorix-passwd --user ' + user 138 | + ' --pass "' + pw + '"', logfile) 139 | printScript(' Success!', '', True, True, False, len(msg)) 140 | except: 141 | printScript(' Failed!', '', True, True, False, len(msg)) 142 | 143 | msg = 'done! ' 144 | printScript(msg) 145 | printScript('', 'end') 146 | -------------------------------------------------------------------------------- /share/examples/students.csv: -------------------------------------------------------------------------------- 1 | 10A;Fray;Katrin;11.05.1986; 2 | 10A;Gengler;Felix;07.07.1985; 3 | 10A;Ilkes;Judith;28.04.1985; 4 | 10A;Imbrogiana;Henriette;16.06.1986; 5 | 10A;Krüger;Richard;30.06.1986; 6 | 13A;Gaißinger;Jochen;21.07.1981; 7 | 13A;Gengler;Achim;02.11.1981; 8 | 13A;Gelhaar;Tanja;13.09.1983; 9 | 5A;Schirra;Moritz;22.03.1990; 10 | 5A;Serdarevicic;Jelena;16.09.1990; 11 | 5A;Hartmann;Peter;30.01.1991; 12 | -------------------------------------------------------------------------------- /share/examples/teachers.csv: -------------------------------------------------------------------------------- 1 | lehrer ;Bader ;Hans ;01.01.2001 ;ba ; 2 | lehrer ;Bech ;Manfred ;01.03.1970 ;bz ; 3 | lehrer ;Bo ;Hans ;01.01.2001 ;bo ; 4 | lehrer ;Denzer ;Andrea ;01.01.2001 ;de ; 5 | lehrer ;Dornstett ;Marianne ;01.01.2001 ;dorn ; 6 | lehrer ;Müller ;Dörthe ;01.01.2001 ;do ; 7 | lehrer ;Schöninger ;Hans-Peter ;01.01.2001 ;schoen ; 8 | lehrer ;Zell ;Klaus ;01.01.2001 ;zell ; 9 | lehrer ;Zembowski ;Klaus ;01.01.2001 ;zem ; 10 | -------------------------------------------------------------------------------- /share/firewall/opnsense/create-auth-config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # create web proxy sso authentication config 4 | # thomas@linuxmuster.net 5 | # 20200311 6 | # 7 | 8 | import environment 9 | import os 10 | import sys 11 | 12 | from functions import datetime 13 | from functions import getSetupValue 14 | from functions import printScript 15 | from functions import readTextfile 16 | from functions import writeTextfile 17 | 18 | 19 | now = str(datetime.datetime.now()).split('.')[0] 20 | printScript('create-auth-config.py ' + now) 21 | 22 | 23 | # get setup values 24 | printScript('Reading setup values.') 25 | servername = getSetupValue('servername') 26 | domainname = getSetupValue('domainname') 27 | realm = getSetupValue('realm') 28 | rc, bindpw = readTextfile(environment.BINDUSERSECRET) 29 | if not rc: 30 | sys.exit(1) 31 | 32 | # read config template 33 | printScript('Reading config template.') 34 | rc, content = readTextfile(environment.FWAUTHCFG) 35 | if not rc: 36 | sys.exit(1) 37 | 38 | # replace placeholders 39 | content = content.replace('@@servername@@', servername) 40 | content = content.replace('@@domainname@@', domainname) 41 | content = content.replace('@@realm@@', realm) 42 | content = content.replace('@@bindpw@@', bindpw) 43 | 44 | # write outfile 45 | outfile = '/tmp/' + os.path.basename(environment.FWAUTHCFG) 46 | printScript('Writing ' + outfile + '.') 47 | rc = writeTextfile(outfile, content, 'w') 48 | if not rc: 49 | printScript('Error writing file.') 50 | sys.exit(1) 51 | else: 52 | printScript('Finished successfully.') 53 | -------------------------------------------------------------------------------- /share/firewall/opnsense/create-keytab.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # create web proxy sso keytab 4 | # thomas@linuxmuster.net 5 | # 20250422 6 | # 7 | 8 | import environment 9 | import getopt 10 | import os 11 | import subprocess 12 | import sys 13 | 14 | from functions import datetime, firewallApi, getSetupValue, printScript, readTextfile 15 | 16 | 17 | # check first if firewall is skipped by setup 18 | skipfw = getSetupValue('skipfw') 19 | if skipfw: 20 | printScript('Firewall is skipped by setup!') 21 | sys.exit(0) 22 | 23 | 24 | def usage(): 25 | print('Usage: create-keytab.py [options]') 26 | print('Creates opnsense web proxy sso keytable.') 27 | print('If adminpw is omitted saved administrator credentials are used.') 28 | print(' [options] may be:') 29 | print(' -a , --adminpw=: global-admin password (optional)') 30 | print(' -c, --check : check only the presence of keytable file') 31 | print(' -v, --verbose : be more verbose') 32 | print(' -h, --help : print this help') 33 | 34 | 35 | # get cli args 36 | try: 37 | opts, args = getopt.getopt(sys.argv[1:], "a:chv", ["adminpw=", "check", "help", "verbose"]) 38 | except getopt.GetoptError as err: 39 | # print help information and exit: 40 | print(err) # will print something like "option -a not recognized" 41 | usage() 42 | sys.exit(2) 43 | 44 | verbose = False 45 | adminpw = None 46 | adminlogin = 'global-admin' 47 | check = False 48 | 49 | # evaluate options 50 | for o, a in opts: 51 | if o in ("-v", "--verbose"): 52 | verbose = True 53 | elif o in ("-a", "--adminpw"): 54 | adminpw = a 55 | elif o in ("-c", "--check"): 56 | check = True 57 | elif o in ("-h", "--help"): 58 | usage() 59 | sys.exit() 60 | else: 61 | assert False, "unhandled option" 62 | 63 | 64 | now = str(datetime.datetime.now()).split('.')[0] 65 | printScript('create-keytab.py ' + now) 66 | 67 | 68 | if not check: 69 | # get firewall ip from setupini 70 | firewallip = getSetupValue('firewallip') 71 | 72 | # get administrator credentials if global-admin password was not provided 73 | if adminpw is None: 74 | rc, adminpw = readTextfile(environment.ADADMINSECRET) 75 | adminlogin = 'administrator' 76 | 77 | # reload relevant services 78 | sshconnect = 'ssh -q -oBatchmode=yes -oStrictHostKeyChecking=accept-new ' + firewallip 79 | for item in ['unbound', 'squid']: 80 | printScript('Restarting ' + item) 81 | sshcmd = sshconnect + ' pluginctl -s ' + item + ' restart' 82 | rc = os.system(sshcmd) 83 | if rc != 0: 84 | sys.exit(1) 85 | 86 | # create keytab 87 | payload = '{"admin_login": "' + adminlogin + '", "admin_password": "' + adminpw + '"}' 88 | apipath = '/proxysso/service/createkeytab' 89 | res = firewallApi('post', apipath, payload) 90 | if verbose: 91 | print(res) 92 | 93 | # set firewall spn if it does not exist yet 94 | entry = 'HTTP/firewall\n' 95 | output = subprocess.check_output(['samba-tool', 'spn', 'list', 'FIREWALL-K$']).decode('utf-8') 96 | if entry not in output: 97 | entry = entry.replace('\n', '') 98 | printScript('Adding servicePrincipalName ' + entry + ' for FIREWALL-K$') 99 | subprocess.run(['samba-tool', 'spn', 'add', entry, 'FIREWALL-K$']) 100 | 101 | 102 | # check success 103 | keytabtest = 'No keytab' 104 | apipath = '/proxysso/service/showkeytab' 105 | res = firewallApi('get', apipath) 106 | if verbose: 107 | print(res) 108 | if keytabtest in str(res): 109 | rc = 1 110 | printScript('Keytab is not present :-(') 111 | else: 112 | rc = 0 113 | printScript('Keytab is present :-)') 114 | 115 | 116 | sys.exit(rc) 117 | -------------------------------------------------------------------------------- /share/firewall/opnsense/fwsetup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # install extensions and reboot 4 | # thomas@linuxmuster.net 5 | # 20240212 6 | # 7 | 8 | # test if necessary files are present 9 | [ -s /tmp/opnsense.xml -a -s /tmp/pre-auth.conf ] || exit 1 10 | 11 | # install necessary extensions 12 | pkg install -y os-squid os-web-proxy-sso os-freeradius || exit 1 13 | 14 | # copy squid's pre-auth.conf in place 15 | paconf="$(head -1 /tmp/pre-auth.conf | awk '{print $2}')" 16 | padir="$(dirname "$paconf")" 17 | [ -d "$padir" ] || mkdir -p "$padir" 18 | cp /tmp/pre-auth.conf "$paconf" || exit 1 19 | 20 | # copy setup config 21 | cp /tmp/opnsense.xml /conf/config.xml || exit 1 22 | 23 | # reboot finally 24 | reboot 25 | -------------------------------------------------------------------------------- /share/firewall/opnsense/pre-auth.conf: -------------------------------------------------------------------------------- 1 | # /usr/local/etc/squid/pre-auth/50-linuxmuster.pre-auth.conf 2 | # 3 | # thomas@linuxmuster.net 4 | # 20240212 5 | # 6 | # web proxy sso, allow only internet group 7 | # 8 | 9 | external_acl_type InternetAllowed ttl=60 negative_ttl=60 %LOGIN /usr/local/libexec/squid/ext_kerberos_ldap_group_acl -a -l ldaps://@@servername@@.@@domainname@@:636 -u global-binduser@@@realm@@ -p @@bindpw@@ -g internet@@@realm@@ -D @@realm@@ 10 | 11 | acl InternetAllowed external InternetAllowed 12 | 13 | http_access deny !InternetAllowed 14 | -------------------------------------------------------------------------------- /share/fix-ntp_signd-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # fix ntp_signd directory 4 | # use this at your own risk 5 | # 6 | # thomas@linuxmuster.net 7 | # 20250326 8 | # 9 | 10 | # get environment 11 | source /usr/share/linuxmuster/environment.sh 12 | 13 | # additional variables 14 | NTPSOCKDIR_OLD="${NTPSOCKDIR/\/var\/lib\//\/run\/}" 15 | conffiles="/etc/ntp.conf /etc/samba/smb.conf" 16 | services="apparmor ntp samba-ad-dc" 17 | timestamp="$(date +%Y%m%d%H%M)" 18 | 19 | # provide ntpd apparmor override file 20 | ntpd_template="$TPLDIR/ntpd.apparmor.d" 21 | ntpd_target="$(head -1 $ntpd_template | awk '{print $2}')" 22 | ntpd_dir="$(dirname $ntpd_target)" 23 | if [ ! -d "$ntpd_dir" ]; then 24 | echo "$ntpd_dir does not exist!" 25 | exit 1 26 | fi 27 | echo "Providing $ntpd_target ..." 28 | cp "$ntpd_template" "$ntpd_target" 29 | 30 | # create socket directory 31 | echo "Creating $NTPSOCKDIR ..." 32 | mkdir -p "$NTPSOCKDIR" 33 | chgrp ntp "$NTPSOCKDIR" 34 | chmod 750 "$NTPSOCKDIR" 35 | 36 | # patch config files 37 | for item in $conffiles; do 38 | echo "Patching $item ..." 39 | cp "$item" "$item.$timestamp" 40 | sed -i "s|$NTPSOCKDIR_OLD|$NTPSOCKDIR|g" "$item" 41 | done 42 | 43 | # restart services 44 | for item in $services; do 45 | echo "Restarting $item.service ..." 46 | systemctl restart "$item.service" 47 | done 48 | 49 | echo "Done!" -------------------------------------------------------------------------------- /share/setupdefaults.ini: -------------------------------------------------------------------------------- 1 | # setupdefaults.ini 2 | # 3 | # thomas@linuxmuster.net 4 | # 20211219 5 | # 6 | # Don't change this file! 7 | # 8 | 9 | [setup] 10 | servername = server 11 | domainname = linuxmuster.lan 12 | dhcprange = 13 | adminpw = Muster! 14 | schoolname = Linuxmuster 15 | location = Schönau 16 | country = DE 17 | state = BW 18 | skipfw = False 19 | -------------------------------------------------------------------------------- /share/templates/cupsd.conf: -------------------------------------------------------------------------------- 1 | # /etc/cups/cupsd.conf 2 | # 3 | # thomas@linuxmuster.net 4 | # 20190325 5 | # 6 | LogLevel warn 7 | PageLogFormat 8 | MaxLogSize 0 9 | # Allow remote access 10 | Port 631 11 | Listen /run/cups/cups.sock 12 | # Share local printers on the local network. 13 | Browsing On 14 | BrowseLocalProtocols dnssd 15 | DefaultAuthType Basic 16 | WebInterface Yes 17 | 18 | # Allow shared printing and remote administration... 19 | Order allow,deny 20 | Allow all 21 | 22 | 23 | AuthType Default 24 | Require user @SYSTEM 25 | # Allow remote administration... 26 | Order allow,deny 27 | Allow all 28 | 29 | 30 | # Allow remote access to the configuration files... 31 | Order allow,deny 32 | Allow all 33 | 34 | 35 | AuthType Default 36 | Require user @SYSTEM 37 | Order allow,deny 38 | # Allow remote access to the log files... 39 | Order allow,deny 40 | Allow all 41 | 42 | 43 | JobPrivateAccess default 44 | JobPrivateValues default 45 | SubscriptionPrivateAccess default 46 | SubscriptionPrivateValues default 47 | 48 | Order deny,allow 49 | 50 | 51 | Require user @OWNER @SYSTEM 52 | Order deny,allow 53 | 54 | 55 | AuthType Default 56 | Require user @SYSTEM 57 | Order deny,allow 58 | 59 | 60 | AuthType Default 61 | Require user @SYSTEM 62 | Order deny,allow 63 | 64 | 65 | Require user @OWNER @SYSTEM 66 | Order deny,allow 67 | 68 | 69 | Order deny,allow 70 | 71 | 72 | 73 | JobPrivateAccess default 74 | JobPrivateValues default 75 | SubscriptionPrivateAccess default 76 | SubscriptionPrivateValues default 77 | 78 | AuthType Default 79 | Order deny,allow 80 | 81 | 82 | AuthType Default 83 | Require user @OWNER @SYSTEM 84 | Order deny,allow 85 | 86 | 87 | AuthType Default 88 | Require user @SYSTEM 89 | Order deny,allow 90 | 91 | 92 | AuthType Default 93 | Require user @SYSTEM 94 | Order deny,allow 95 | 96 | 97 | AuthType Default 98 | Require user @OWNER @SYSTEM 99 | Order deny,allow 100 | 101 | 102 | Order deny,allow 103 | 104 | 105 | 106 | JobPrivateAccess default 107 | JobPrivateValues default 108 | SubscriptionPrivateAccess default 109 | SubscriptionPrivateValues default 110 | 111 | AuthType Negotiate 112 | Order deny,allow 113 | 114 | 115 | AuthType Negotiate 116 | Require user @OWNER @SYSTEM 117 | Order deny,allow 118 | 119 | 120 | AuthType Default 121 | Require user @SYSTEM 122 | Order deny,allow 123 | 124 | 125 | AuthType Default 126 | Require user @SYSTEM 127 | Order deny,allow 128 | 129 | 130 | AuthType Negotiate 131 | Require user @OWNER @SYSTEM 132 | Order deny,allow 133 | 134 | 135 | Order deny,allow 136 | 137 | 138 | -------------------------------------------------------------------------------- /share/templates/devices.csv: -------------------------------------------------------------------------------- 1 | # /etc/linuxmuster/sophomorix/default-school/devices.csv 2 | # 3 | # thomas@linuxmuster.net 4 | # 20190323 5 | # 6 | # Example: 7 | #r100;r100-pc01;group1;00:11:22:33:44:55;10.16.100.1;;;;classroom-studentcomputer;;1 8 | # 9 | # For Details see devices.csv.5 10 | # 11 | -------------------------------------------------------------------------------- /share/templates/dhcpd.apparmor.d: -------------------------------------------------------------------------------- 1 | # /etc/apparmor.d/local/usr.sbin.dhcpd 2 | # 3 | # thomas@linuxmuster.net 4 | # 20250328 5 | # 6 | /usr/lib/linuxmuster/dhcpd-update-samba-dns.py Ux, 7 | -------------------------------------------------------------------------------- /share/templates/dhcpd.conf: -------------------------------------------------------------------------------- 1 | # /etc/dhcp/dhcpd.conf 2 | # 3 | # Sample configuration file for ISC dhcpd for Debian 4 | # 5 | # thomas@linuxmuster.net 6 | # 20200414 7 | # 8 | 9 | # The ddns-updates-style parameter controls whether or not the server will 10 | # attempt to do a DNS update when a lease is confirmed. We default to the 11 | # behavior of the version 2 packages ('none', since DHCP v2 didn't 12 | # have support for DDNS.) 13 | server-identifier @@servername@@; 14 | update-static-leases true; 15 | ddns-update-style none; 16 | 17 | # option definitions common to all supported networks... 18 | server-name "@@servername@@.@@domainname@@"; 19 | option domain-name "@@domainname@@"; 20 | option domain-name-servers @@serverip@@; 21 | option netbios-name-servers @@serverip@@; 22 | option ntp-servers @@serverip@@; 23 | option font-servers @@serverip@@; 24 | use-host-decl-names on; 25 | 26 | default-lease-time 600; 27 | max-lease-time 7200; 28 | 29 | # If this DHCP server is the official DHCP server for the local 30 | # network, the authoritative directive should be uncommented. 31 | authoritative; 32 | 33 | # Use this to send dhcp log messages to a different log file (you also 34 | # have to hack syslog.conf to complete the redirection). 35 | log-facility local7; 36 | 37 | allow booting; 38 | allow bootp; 39 | 40 | # Define option 150 for the grub menu 41 | option grubmenu code 150 = text; 42 | option arch code 93 = unsigned integer 16; #RFC4578 43 | 44 | # arch specific boot images 45 | if option arch = 00:06 { 46 | filename "boot/grub/i386-efi/core.efi"; 47 | } else if option arch = 00:07 { 48 | filename "boot/grub/x86_64-efi/core.efi"; 49 | } else { 50 | filename "boot/grub/i386-pc/core.0"; 51 | } 52 | 53 | next-server @@serverip@@; 54 | 55 | # subnet definitions 56 | include "/etc/dhcp/subnets.conf"; 57 | 58 | # dhcp events 59 | include "/etc/dhcp/events.conf"; 60 | 61 | group { 62 | 63 | # groessere lease time fuer feste ips 64 | default-lease-time 172800; 65 | max-lease-time 172800; 66 | 67 | # dynamically created stuff by linuxmuster-import-devices is included 68 | include "/etc/dhcp/devices.conf"; 69 | 70 | # put your custom stuff in this included file 71 | include "/etc/dhcp/custom.conf"; 72 | 73 | } 74 | -------------------------------------------------------------------------------- /share/templates/dhcpd.custom.conf: -------------------------------------------------------------------------------- 1 | # /etc/dhcp/custom.conf 2 | # 3 | # put your custom dhcpd stuff configuration in this file 4 | # 5 | # thomas@linuxmuster.net 6 | # 20160913 7 | # 8 | -------------------------------------------------------------------------------- /share/templates/dhcpd.devices.conf: -------------------------------------------------------------------------------- 1 | # /etc/dhcp/devices.conf 2 | # 3 | # configfile dummy, valid file will be created by linuxmuster-import-devices 4 | # 5 | # thomas@linuxmuster.net 6 | # 20170210 7 | # 8 | -------------------------------------------------------------------------------- /share/templates/dhcpd.events.conf: -------------------------------------------------------------------------------- 1 | # /etc/dhcp/events.conf 2 | # 3 | # thomas@linuxmuster.net 4 | # 20220119 5 | # 6 | 7 | if not static { 8 | on commit { 9 | set clip = binary-to-ascii(10, 8, ".", leased-address); 10 | execute("/usr/lib/linuxmuster/dhcpd-update-samba-dns.py", "add", clip, host-decl-name, "yes"); 11 | } 12 | on release { 13 | set clip = binary-to-ascii(10, 8, ".", leased-address); 14 | execute("/usr/lib/linuxmuster/dhcpd-update-samba-dns.py", "delete", clip, host-decl-name, "yes"); 15 | } 16 | on expiry { 17 | set clip = binary-to-ascii(10, 8, ".", leased-address); 18 | execute("/usr/lib/linuxmuster/dhcpd-update-samba-dns.py", "delete", clip, host-decl-name, "yes"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /share/templates/dhcpd.subnets.conf: -------------------------------------------------------------------------------- 1 | # /etc/dhcp/subnets.conf 2 | # 3 | # configfile dummy, valid file will be created by linuxmuster-import-subnets 4 | # 5 | # thomas@linuxmuster.net 6 | # 20180514 7 | # 8 | -------------------------------------------------------------------------------- /share/templates/firewall_cert_ext.cnf: -------------------------------------------------------------------------------- 1 | # /etc/linuxmuster/ssl/firewall_cert_ext.cnf 2 | 3 | subjectAltName = IP:@@firewallip@@, DNS:firewall.@@domainname@@, DNS:firewall 4 | basicConstraints = CA:FALSE 5 | nsCertType = server 6 | nsComment = "OpenSSL Generated Firewall Certificate" 7 | subjectKeyIdentifier = hash 8 | authorityKeyIdentifier = keyid,issuer:always 9 | keyUsage = critical, digitalSignature, keyEncipherment 10 | extendedKeyUsage = serverAuth 11 | -------------------------------------------------------------------------------- /share/templates/nsswitch.conf: -------------------------------------------------------------------------------- 1 | # /etc/nsswitch.conf 2 | # 3 | # thomas@linuxmuster.net 4 | # 20160902 5 | # 6 | # Example configuration of GNU Name Service Switch functionality. 7 | # If you have the `glibc-doc-reference' and `info' packages installed, try: 8 | # `info libc "Name Service Switch"' for information about this file. 9 | 10 | passwd: compat winbind 11 | group: compat winbind 12 | shadow: compat winbind 13 | gshadow: files 14 | 15 | hosts: files dns 16 | networks: files 17 | 18 | protocols: db files 19 | services: db files 20 | ethers: db files 21 | rpc: db files 22 | 23 | netgroup: nis 24 | -------------------------------------------------------------------------------- /share/templates/ntp.conf: -------------------------------------------------------------------------------- 1 | # /etc/ntp.conf 2 | # 3 | # thomas@linuxmuster.net 4 | # 20230919 5 | # 6 | # configuration for ntpd; see ntp.conf(5) for help 7 | 8 | driftfile /var/lib/ntp/ntp.drift 9 | statistics loopstats peerstats clockstats 10 | filegen loopstats file loopstats type day enable 11 | filegen peerstats file peerstats type day enable 12 | filegen clockstats file clockstats type day enable 13 | server @@firewallip@@ iburst prefer 14 | restrict -4 default kod notrap nomodify nopeer noquery limited mssntp 15 | restrict -6 default kod notrap nomodify nopeer noquery limited mssntp 16 | restrict 127.0.0.1 17 | restrict ::1 18 | restrict source notrap nomodify noquery 19 | ntpsigndsocket @@ntpsockdir@@ 20 | -------------------------------------------------------------------------------- /share/templates/ntpd.apparmor.d: -------------------------------------------------------------------------------- 1 | # /etc/apparmor.d/local/usr.sbin.ntpd 2 | # 3 | # thomas@linuxmuster.net 4 | # 20250328 5 | # 6 | 7 | # samba4 ntp signing socket 8 | /var/lib/samba/ntp_signd/socket rw, 9 | -------------------------------------------------------------------------------- /share/templates/server_cert_ext.cnf: -------------------------------------------------------------------------------- 1 | # /etc/linuxmuster/ssl/server_cert_ext.cnf 2 | 3 | subjectAltName = IP:@@serverip@@, DNS:@@servername@@.@@domainname@@, DNS:@@servername@@ 4 | basicConstraints = CA:FALSE 5 | nsCertType = server 6 | nsComment = "OpenSSL Generated Server Certificate" 7 | subjectKeyIdentifier = hash 8 | authorityKeyIdentifier = keyid,issuer:always 9 | keyUsage = critical, digitalSignature, keyEncipherment 10 | extendedKeyUsage = serverAuth 11 | -------------------------------------------------------------------------------- /share/templates/smb.conf: -------------------------------------------------------------------------------- 1 | # /etc/samba/smb.conf.setup 2 | # 3 | # Don't edit this file!!! 4 | # Add your stuff in /etc/samba/smb.conf.admin. 5 | # 6 | # thomas@linuxmuster.net 7 | # 20220910 8 | # 9 | 10 | [global] 11 | workgroup = @@sambadomain@@ 12 | realm = @@realm@@ 13 | netbios name = @@netbiosname@@ 14 | server role = active directory domain controller 15 | dns forwarder = @@firewallip@@ 16 | registry shares = yes 17 | host msdfs = yes 18 | tls enabled = yes 19 | tls keyfile = /etc/linuxmuster/ssl/@@servername@@.key.pem 20 | tls certfile = /etc/linuxmuster/ssl/@@servername@@.cert.pem 21 | tls cafile = /etc/linuxmuster/ssl/cacert.pem 22 | tls verify peer = ca_and_name 23 | ldap server require strong auth = no 24 | rpc_server:spoolss = external 25 | rpc_daemon:spoolssd = fork 26 | spoolss:architecture = Windows x64 27 | printing = cups 28 | printcap name = cups 29 | time server = yes 30 | ntp signd socket directory = @@ntpsockdir@@ 31 | ntlm auth = mschapv2-and-ntlmv2-only 32 | 33 | [netlogon] 34 | path = /var/lib/samba/sysvol/@@domainname@@/scripts 35 | read only = No 36 | acl allow execute always = yes 37 | 38 | [sysvol] 39 | path = /var/lib/samba/sysvol 40 | read only = No 41 | 42 | [printers] 43 | browseable = No 44 | path = /var/spool/samba 45 | printable = Yes 46 | read only = No 47 | 48 | [print$] 49 | path = /var/lib/samba/printers 50 | read only = No 51 | 52 | # including custom admin stuff 53 | include = /etc/samba/smb.conf.admin 54 | -------------------------------------------------------------------------------- /share/templates/smb.conf.admin: -------------------------------------------------------------------------------- 1 | # /etc/samba/smb.conf.admin 2 | # 3 | # thomas@linuxmuster.net 4 | # 20180713 5 | # 6 | # add here your custom admin stuff 7 | # 8 | -------------------------------------------------------------------------------- /share/templates/subnets.csv: -------------------------------------------------------------------------------- 1 | # /etc/linuxmuster/subnets.csv 2 | # 3 | # thomas@linuxmuster.net 4 | # 20200917 5 | # 6 | # Network/Prefix ; Router-IP (last available IP in network) ; 1. Range-IP ; Last-Range-IP ;Namserver(use lmn server if empty);nextserver (tftp server leave empty for default; SETUP-Flag 7 | # 8 | # server subnet definition 9 | @@network@@/@@bitmask@@;@@firewallip@@;@@dhcprange1@@;@@dhcprange2@@;;;SETUP 10 | 11 | # add your subnets below 12 | # 13 | -------------------------------------------------------------------------------- /share/templates/webui-sudoers: -------------------------------------------------------------------------------- 1 | # /etc/sudoers.d/linuxmuster 2 | # 3 | # thomas@linuxmuster.net 4 | # 20220730 5 | # 6 | # sudoer groups for webui 7 | # 8 | 9 | %@@sambadomain@@\\role-student ALL=(ALL:ALL) NOPASSWD: /usr/sbin/sophomorix-passwd,/usr/sbin/sophomorix-query,/usr/sbin/sophomorix-session,/usr/sbin/sophomorix-user-custom 10 | %@@sambadomain@@\\role-teacher ALL=(ALL:ALL) NOPASSWD:ALL 11 | %@@sambadomain@@\\role-globaladministrator ALL=(ALL:ALL) NOPASSWD:ALL 12 | %@@sambadomain@@\\role-schooladministrator ALL=(ALL:ALL) NOPASSWD:ALL 13 | 14 | Cmnd_Alias SOPHPASS = /usr/sbin/sophomorix-passwd 15 | Defaults!SOPHPASS !syslog 16 | --------------------------------------------------------------------------------