Client install roles added
This commit is contained in:
parent
5e8410eac6
commit
990114e28b
47 changed files with 4181 additions and 0 deletions
112
roles/lmn_kde/files/fvs-config.js
Normal file
112
roles/lmn_kde/files/fvs-config.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
// configure plasma defaults
|
||||
|
||||
function forEachWidgetInContainmentList(containmentList, callback) {
|
||||
for (var containmentIndex = 0; containmentIndex < containmentList.length; containmentIndex++) {
|
||||
var containment = containmentList[containmentIndex];
|
||||
|
||||
var widgets = containment.widgets();
|
||||
for (var widgetIndex = 0; widgetIndex < widgets.length; widgetIndex++) {
|
||||
var widget = widgets[widgetIndex];
|
||||
callback(widget, containment);
|
||||
if (widget.type === "org.kde.plasma.systemtray") {
|
||||
systemtrayId = widget.readConfig("SystrayContainmentId");
|
||||
if (systemtrayId) {
|
||||
forEachWidgetInContainmentList([desktopById(systemtrayId)], callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forEachWidget(callback) {
|
||||
forEachWidgetInContainmentList(desktops(), callback);
|
||||
forEachWidgetInContainmentList(panels(), callback);
|
||||
}
|
||||
|
||||
function forEachWidgetByType(type, callback) {
|
||||
forEachWidget(function(widget, containment) {
|
||||
if (widget.type == type) {
|
||||
callback(widget, containment);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function widgetSetProperty(args) {
|
||||
if (!(args.widgetType && args.configGroup && args.configKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEachWidgetByType(args.widgetType, function(widget){
|
||||
widget.currentConfigGroup = [args.configGroup];
|
||||
/*
|
||||
//--- Delete when done debugging
|
||||
const oldValue = widget.readConfig(args.configKey);
|
||||
print("" + widget.type + " (id: " + widget.id + "):");
|
||||
print("\t[" + args.configGroup + "] " + args.configKey + ": " +
|
||||
oldValue + " => " + args.configValue + "\n");
|
||||
//--- End Debug
|
||||
*/
|
||||
widget.writeConfig(args.configKey, args.configValue);
|
||||
});
|
||||
}
|
||||
|
||||
// configure task bar starters:
|
||||
widgetSetProperty({
|
||||
widgetType: "org.kde.plasma.icontasks",
|
||||
configGroup: "General",
|
||||
configKey: "launchers",
|
||||
configValue: [
|
||||
"applications:systemsettings.desktop",
|
||||
"preferred://browser",
|
||||
"applications:thunderbird.desktop",
|
||||
"applications:libreoffice-startcenter.desktop",
|
||||
"preferred://filemanager"
|
||||
//"applications:org.kde.konsole.desktop",
|
||||
//"applications:org.kde.discover.desktop"
|
||||
],
|
||||
|
||||
});
|
||||
|
||||
// kickoff is the default menu:
|
||||
/* this does not work (anymore?)
|
||||
widgetSetProperty({
|
||||
widgetType: "org.kde.plasma.kickoff",
|
||||
configGroup: "General",
|
||||
configKey: "favorites",
|
||||
configValue: ["applications:libreoffice-startcenter.desktop",],
|
||||
});
|
||||
*/
|
||||
|
||||
widgetSetProperty({
|
||||
widgetType: "org.kde.plasma.kickoff",
|
||||
configGroup: "General",
|
||||
configKey: "systemFavorites",
|
||||
configValue: ["reboot", "shutdown", "logout"],
|
||||
});
|
||||
|
||||
|
||||
// prepare a folder view on the desktop:
|
||||
/* 20230917 disabled for now
|
||||
var allDesktops = desktops();
|
||||
for (var desktopIndex = 0; desktopIndex < allDesktops.length; desktopIndex++) {
|
||||
var d = allDesktops[desktopIndex];
|
||||
d.addWidget("org.kde.plasma.folder", 50, 50, 456, 600)
|
||||
print("Folder app generated!\n")
|
||||
}
|
||||
|
||||
widgetSetProperty({
|
||||
widgetType: "org.kde.plasma.folder",
|
||||
configGroup: "General",
|
||||
configKey: "url",
|
||||
configValue: "/lmn/media/",
|
||||
});
|
||||
|
||||
widgetSetProperty({
|
||||
widgetType: "org.kde.plasma.folder",
|
||||
configGroup: "General",
|
||||
configKey: "labelMode",
|
||||
configValue: "0",
|
||||
});
|
||||
*/
|
||||
|
||||
// /usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fvs-config.js
|
222
roles/lmn_kde/files/lmn-reset-dolphin.sh
Executable file
222
roles/lmn_kde/files/lmn-reset-dolphin.sh
Executable file
|
@ -0,0 +1,222 @@
|
|||
#!/bin/bash
|
||||
|
||||
sed -e "s|HOME|/${HOME##/srv/samba/schools/default-school/}|g" -e "s|USER|${USER}|g" > ~/.local/share/user-places.xbel <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xbel>
|
||||
<xbel xmlns:mime="http://www.freedesktop.org/standards/shared-mime-info" xmlns:bookmark="http://www.freedesktop.org/standards/desktop-bookmarks" xmlns:kdepriv="http://www.kde.org/kdepriv">
|
||||
<info>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<kde_places_version>4</kde_places_version>
|
||||
<GroupState-Places-IsHidden>false</GroupState-Places-IsHidden>
|
||||
<GroupState-Remote-IsHidden>false</GroupState-Remote-IsHidden>
|
||||
<GroupState-Devices-IsHidden>false</GroupState-Devices-IsHidden>
|
||||
<GroupState-RemovableDevices-IsHidden>false</GroupState-RemovableDevices-IsHidden>
|
||||
<GroupState-Tags-IsHidden>false</GroupState-Tags-IsHidden>
|
||||
<withRecentlyUsed>true</withRecentlyUsed>
|
||||
<GroupState-RecentlySaved-IsHidden>false</GroupState-RecentlySaved-IsHidden>
|
||||
<withBaloo>true</withBaloo>
|
||||
<GroupState-SearchFor-IsHidden>false</GroupState-SearchFor-IsHidden>
|
||||
</metadata>
|
||||
</info>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME">
|
||||
<title>Home</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="user-home"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/0</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME/Schreibtisch">
|
||||
<title>Desktop</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="user-desktop"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/1</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME/Dokumente">
|
||||
<title>Documents</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-documents"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/2</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME/Downloads">
|
||||
<title>Downloads</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-downloads"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/3</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME/Musik">
|
||||
<title>Music</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-music"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/6</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME/Bilder">
|
||||
<title>Pictures</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-pictures"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/7</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-schoolHOME/Videos">
|
||||
<title>Videos</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-videos"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/8</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///srv/samba/schools/default-school/share">
|
||||
<title>Tausch</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-publicshare"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/9</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="file:///lmn/media/USER/nextcloud">
|
||||
<title>Nextcloud</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-cloud"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/10</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="remote:/">
|
||||
<title>Network</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-network"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/4</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="trash:/">
|
||||
<title>Trash</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="user-trash"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/5</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="recentlyused:/files">
|
||||
<title>Recent Files</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="document-open-recent"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/9</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<bookmark href="recentlyused:/locations">
|
||||
<title>Recent Locations</title>
|
||||
<info>
|
||||
<metadata owner="http://freedesktop.org">
|
||||
<bookmark:icon name="folder-open-recent"/>
|
||||
</metadata>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<ID>1682498425/10</ID>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</bookmark>
|
||||
<separator>
|
||||
<info>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<UDI>/org/kde/fstab///server/default-school/:/srv/samba/schools/default-school</UDI>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
<IsHidden>true</IsHidden>
|
||||
</metadata>
|
||||
</info>
|
||||
</separator>
|
||||
<separator>
|
||||
<info>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<UDI>/org/kde/fstab///server/default-school/:/lmn/media/USER/home</UDI>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
<IsHidden>true</IsHidden>
|
||||
</metadata>
|
||||
</info>
|
||||
</separator>
|
||||
<separator>
|
||||
<info>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<UDI>/org/kde/fstab///server/sysvol/:/srv/samba/USER/sysvol</UDI>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
<IsHidden>true</IsHidden>
|
||||
</metadata>
|
||||
</info>
|
||||
</separator>
|
||||
<separator>
|
||||
<info>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<UDI>/org/kde/fstab///server/default-school/:/lmn/media/USER/share</UDI>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
<IsHidden>true</IsHidden>
|
||||
</metadata>
|
||||
</info>
|
||||
</separator>
|
||||
<separator>
|
||||
<info>
|
||||
<metadata owner="http://www.kde.org">
|
||||
<UDI>/org/freedesktop/UDisks2/block_devices/sda2</UDI>
|
||||
<isSystemItem>true</isSystemItem>
|
||||
</metadata>
|
||||
</info>
|
||||
</separator>
|
||||
</xbel>
|
||||
EOF
|
74
roles/lmn_kde/files/policies.json
Normal file
74
roles/lmn_kde/files/policies.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"policies": {
|
||||
"Proxy": {
|
||||
"Mode": "system"
|
||||
},
|
||||
"OverrideFirstRunPage": "https://www.steinbeisschule-reutlingen.de",
|
||||
"Homepage": {
|
||||
"URL": "https://www.debian.org",
|
||||
"Locked": false,
|
||||
"StartPage": "previous-session"
|
||||
},
|
||||
"DisplayBookmarksToolbar": true,
|
||||
"ManagedBookmarks": [
|
||||
{
|
||||
"toplevel_name": "FvS-Reutlingen"
|
||||
},
|
||||
{
|
||||
"url": "https://server.pn.steinbeis.schule",
|
||||
"name": "Passwort ändern"
|
||||
},
|
||||
{
|
||||
"url": "https://dw.steinbeis.schule",
|
||||
"name": "FvS-Hilfesystem"
|
||||
},
|
||||
{
|
||||
"url": "https://nextcloud.steinbeisschule-reutlingen.de",
|
||||
"name": "FvS-Nextcloud"
|
||||
},
|
||||
{
|
||||
"url": "https://moodle.steinbeisschule-reutlingen.de",
|
||||
"name": "FvS-Moodle"
|
||||
},
|
||||
{
|
||||
"name": "Debian",
|
||||
"children": [
|
||||
{
|
||||
"url": "https://www.debian.org",
|
||||
"name": "Debian Homepage"
|
||||
},
|
||||
{
|
||||
"url": "https://wiki.debian.org",
|
||||
"name": "Debian Wiki"
|
||||
},
|
||||
{
|
||||
"name": "Debian LAN/Live",
|
||||
"children": [
|
||||
{
|
||||
"url": "https://salsa.debian.org/andi/debian-lan-ansible",
|
||||
"name": "Debian LAN Ansible"
|
||||
},
|
||||
{
|
||||
"url": "https://wiki.debian.org/DebianLive",
|
||||
"name": "Debian Live"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SearchEngines": {
|
||||
"Add": [
|
||||
{
|
||||
"Name": "Startpage",
|
||||
"URLTemplate": "https://www.startpage.com/sp/search?query={searchTerms}",
|
||||
"Method": "GET",
|
||||
"IconURL": "https://www.startpage.com/sp/cdn/favicons/favicon--default.ico",
|
||||
"Alias": "sp",
|
||||
"Description": "Startpage Search Engine"
|
||||
}
|
||||
],
|
||||
"Default": "Startpage"
|
||||
}
|
||||
}
|
||||
}
|
47
roles/lmn_kde/files/pwroff
Executable file
47
roles/lmn_kde/files/pwroff
Executable file
|
@ -0,0 +1,47 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# logout idle users and shutdown machine
|
||||
#
|
||||
set -eu
|
||||
|
||||
action="systemctl poweroff"
|
||||
uptime=$(cat /proc/uptime | cut -f1 -d.)
|
||||
maxidle=3600
|
||||
|
||||
u=($(loginctl list-users --no-legend | sort -hr | head -1))
|
||||
una=${u[1]:-''}
|
||||
uid=${u[0]:-''}
|
||||
|
||||
talk2dbus() {
|
||||
local display=":$(ls /tmp/.X11-unix/* | sed 's#/tmp/.X11-unix/X##' | head -n 1)"
|
||||
sudo -u $una DISPLAY=$display \
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
########
|
||||
|
||||
## shutdown if nobody is loged in:
|
||||
if [[ -z "$una" ]] || [[ $uid -lt 1000 ]] ; then
|
||||
exec $action
|
||||
fi
|
||||
|
||||
# FIXME: find idle time independent of running screensaver
|
||||
if ! t=$(talk2dbus qdbus org.kde.screensaver /ScreenSaver GetActiveTime) ; then
|
||||
echo "No graphical logins found."
|
||||
else
|
||||
idle=$(( t / 1000 ))
|
||||
if [[ $idle -gt $maxidle ]] ; then
|
||||
talk2dbus notify-send -i system-shutdown -u critical -a 'Important System Information' \
|
||||
'Please log out, the system will shut down soon!' \
|
||||
'There has been no activity for too long.'
|
||||
## shutdown:
|
||||
#talk2dbus qdbus org.kde.ksmserver /KSMServer logout 1 2 0
|
||||
## logout:
|
||||
talk2dbus qdbus org.kde.ksmserver /KSMServer logout 1 0 0
|
||||
echo "Log-out user $una after being idle for $idle seconds."
|
||||
else
|
||||
echo "The user $una has been idle for $idle seconds."
|
||||
fi
|
||||
fi
|
||||
|
||||
#w -s | grep tty | sed "s/[[:space:]]\+/ /g" | cut -f4 -d ' '
|
6
roles/lmn_kde/files/pwroff.service
Normal file
6
roles/lmn_kde/files/pwroff.service
Normal file
|
@ -0,0 +1,6 @@
|
|||
[Unit]
|
||||
Description=Run pwroff script
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/sbin/pwroff
|
9
roles/lmn_kde/files/pwroff.timer
Normal file
9
roles/lmn_kde/files/pwroff.timer
Normal file
|
@ -0,0 +1,9 @@
|
|||
[Unit]
|
||||
Description=Run pwroff script every 10 min after 60 min uptime
|
||||
|
||||
[Timer]
|
||||
OnBootSec=60min
|
||||
OnUnitActiveSec=10min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
14
roles/lmn_kde/handlers/main.yml
Normal file
14
roles/lmn_kde/handlers/main.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
- name: Run update-grub
|
||||
command: update-grub
|
||||
|
||||
- name: Enable tmp.mount
|
||||
systemd:
|
||||
daemon_reload: yes
|
||||
name: tmp.mount
|
||||
enabled: yes
|
||||
listen: enable tmp.mount
|
||||
|
||||
- name: enable pwroff.timer
|
||||
systemd:
|
||||
name: pwroff.timer
|
||||
enabled: true
|
214
roles/lmn_kde/tasks/main.yml
Normal file
214
roles/lmn_kde/tasks/main.yml
Normal file
|
@ -0,0 +1,214 @@
|
|||
---
|
||||
- name: Preseed ttf-mscorefonts-installer
|
||||
ansible.builtin.debconf:
|
||||
name: ttf-mscorefonts-installer
|
||||
question: msttcorefonts/dlurl
|
||||
value: http://netboot.qgm.lan/mscorefonts/
|
||||
vtype: string
|
||||
|
||||
- name: Install desktop EDU packages and some more
|
||||
apt:
|
||||
name:
|
||||
- task-kde-desktop
|
||||
- task-german-kde-desktop
|
||||
- task-german-desktop
|
||||
- xdg-desktop-portal-kde
|
||||
- xdg-desktop-portal-wlr # share screen in browser
|
||||
- kde-full
|
||||
- akonadi-backend-sqlite
|
||||
- thunderbird-l10n-de
|
||||
- webext-privacy-badger
|
||||
- webext-ublock-origin-firefox
|
||||
- webext-ublock-origin-chromium
|
||||
- vlc
|
||||
- gimp
|
||||
- inkscape
|
||||
- flameshot
|
||||
- bluefish
|
||||
- git
|
||||
- gitk
|
||||
- gitg
|
||||
- nmap
|
||||
- net-tools
|
||||
- ghex
|
||||
- thonny
|
||||
- spyder
|
||||
- mu-editor
|
||||
- dia
|
||||
- vym
|
||||
- tree
|
||||
- sqlite3
|
||||
- sqlitebrowser
|
||||
- neovim
|
||||
- qtcreator
|
||||
- freecad
|
||||
- librecad
|
||||
- arduino
|
||||
- keepassxc
|
||||
- tmux
|
||||
- curl
|
||||
- pulseview
|
||||
- sigrok
|
||||
- sigrok-cli
|
||||
- codeblocks
|
||||
- ttf-mscorefonts-installer
|
||||
autoremove: true
|
||||
state: latest
|
||||
environment:
|
||||
http_proxy: '' # this is needed to avoid ttf-mscorefonts-installer picking up aptcacher
|
||||
|
||||
- name: Add {{ ansible_distribution_release }}-backports
|
||||
apt_repository:
|
||||
repo: deb http://deb.debian.org/debian/ {{ ansible_distribution_release }}-backports main non-free-firmware
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Install extra packages from backports
|
||||
apt:
|
||||
name:
|
||||
- libreoffice
|
||||
- libreoffice-l10n-de
|
||||
- kicad
|
||||
- kicad-doc-de
|
||||
state: latest # noqa package-latest
|
||||
autoremove: true
|
||||
default_release: "{{ ansible_distribution_release }}-backports"
|
||||
|
||||
|
||||
- name: Enable splash screen
|
||||
replace:
|
||||
dest: "/etc/default/grub"
|
||||
regexp: '"quiet"$'
|
||||
replace: '"quiet splash"'
|
||||
notify: Run update-grub
|
||||
|
||||
|
||||
- name: Create akonadi config dir
|
||||
ansible.builtin.file:
|
||||
path: /etc/xdg/akonadi/
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Use sqlite in akonadi
|
||||
blockinfile:
|
||||
path: /etc/xdg/akonadi/akonadiserverrc
|
||||
create: true
|
||||
block: |
|
||||
[%General]
|
||||
Driver=QSQLITE3
|
||||
|
||||
|
||||
- name: Add home dirs to apparmor
|
||||
lineinfile:
|
||||
dest: /etc/apparmor.d/tunables/home.d/ubuntu
|
||||
line: >-
|
||||
@{HOMEDIRS}+=/srv/samba/schools/default-school/teachers/
|
||||
/srv/samba/schools/default-school/students/*/
|
||||
|
||||
|
||||
- name: Create firefox policies directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/firefox-esr/policies
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Create a symbolic link firefox to firefox-esr
|
||||
ansible.builtin.file:
|
||||
src: /etc/firefox-esr
|
||||
dest: /etc/firefox
|
||||
state: link
|
||||
|
||||
- name: copy policy
|
||||
ansible.builtin.copy:
|
||||
src: policies.json
|
||||
dest: /etc/firefox-esr/policies/
|
||||
|
||||
|
||||
- name: tune SDDM login
|
||||
blockinfile:
|
||||
path: /etc/sddm.conf
|
||||
create: true
|
||||
block: |
|
||||
[Users]
|
||||
MaximumUid=999
|
||||
RememberLastUser=false
|
||||
RememberLastSession=false
|
||||
|
||||
|
||||
- name: Create directory to avoid suspend
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/sleep.conf.d/
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Avoid suspending
|
||||
blockinfile:
|
||||
path: /etc/systemd/sleep.conf.d/nosuspend.conf
|
||||
create: true
|
||||
block: |
|
||||
[Sleep]
|
||||
AllowSuspend=no
|
||||
AllowHibernation=no
|
||||
AllowSuspendThenHibernate=no
|
||||
AllowHybridSleep=no
|
||||
|
||||
|
||||
- name: Copy pwroff script
|
||||
copy:
|
||||
src: pwroff
|
||||
dest: /usr/local/sbin/
|
||||
mode: 0755
|
||||
|
||||
- name: Provide service and timer for pwroff script
|
||||
copy:
|
||||
src: "{{ item }}"
|
||||
dest: "/etc/systemd/system/{{ item }}"
|
||||
mode: 0644
|
||||
with_items:
|
||||
- pwroff.service
|
||||
- pwroff.timer
|
||||
notify: enable pwroff.timer
|
||||
|
||||
- name: copy lmn-reset-dolphin.sh
|
||||
ansible.builtin.copy:
|
||||
src: lmn-reset-dolphin.sh
|
||||
dest: /usr/local/bin/
|
||||
mode: 0755
|
||||
|
||||
- name: Copy fvs-config.js to configure plasma
|
||||
ansible.builtin.copy:
|
||||
src: fvs-config.js
|
||||
dest: /usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fvs-config.js
|
||||
mode: 0644
|
||||
|
||||
################# general settings ##################
|
||||
- name: Protect grub menu entries
|
||||
blockinfile:
|
||||
path: /etc/grub.d/40_custom
|
||||
block: |
|
||||
set superusers='root'
|
||||
password_pbkdf2 root {{ grub_pwd }}
|
||||
notify: Run update-grub
|
||||
|
||||
- name: Allow booting default entry
|
||||
lineinfile:
|
||||
dest: /etc/grub.d/10_linux
|
||||
line: CLASS="${CLASS} --unrestricted"
|
||||
insertafter: '^CLASS=.*'
|
||||
firstmatch: true
|
||||
notify: Run update-grub
|
||||
|
||||
- name: Grub timeout
|
||||
lineinfile:
|
||||
dest: /etc/default/grub
|
||||
regexp: '^(GRUB_TIMEOUT=).*'
|
||||
line: '\g<1>3'
|
||||
backrefs: yes
|
||||
notify: Run update-grub
|
||||
|
||||
- name: Keyboard compose key
|
||||
lineinfile:
|
||||
dest: /etc/default/keyboard
|
||||
regexp: '^(XKBOPTIONS=).*'
|
||||
line: '\1"compose:caps"'
|
||||
backrefs: yes
|
2
roles/lmn_mount/defaults/main.yml
Normal file
2
roles/lmn_mount/defaults/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
smb_server: "server"
|
||||
smb_share: "default-school/"
|
4
roles/lmn_mount/files/lmn-linkhome.sh
Normal file
4
roles/lmn_mount/files/lmn-linkhome.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
if [[ "${UID}" -gt 60000 ]]; then
|
||||
[[ -L "/lmn/media/${USER}/share" ]] || ln -s .default-school/share "/lmn/media/${USER}/share"
|
||||
[[ -L "/lmn/media/${USER}/home" ]] || ln -s ".default-school/${HOME##/srv/samba/schools/default-school/}" "/lmn/media/${USER}/home"
|
||||
fi
|
3
roles/lmn_mount/files/lmn-mounthome.sh
Normal file
3
roles/lmn_mount/files/lmn-mounthome.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
if [[ "${UID}" -gt 60000 ]]; then
|
||||
sudo /usr/local/bin/mounthome.sh &
|
||||
fi
|
82
roles/lmn_mount/tasks/main.yml
Normal file
82
roles/lmn_mount/tasks/main.yml
Normal file
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
- name: Install needed packages
|
||||
apt:
|
||||
name:
|
||||
- libpam-mount
|
||||
- cifs-utils
|
||||
- nfs-common
|
||||
- hxtools
|
||||
- davfs2
|
||||
state: latest
|
||||
|
||||
- name: Configure pam_mount for LMN homes
|
||||
blockinfile:
|
||||
dest: /etc/security/pam_mount.conf.xml
|
||||
marker: "<!-- {mark} ANSIBLE MANAGED BLOCK (mount LMN homes) -->"
|
||||
block: |
|
||||
<!-- mounts for home, share and nextcloud -->
|
||||
<volume
|
||||
fstype="cifs"
|
||||
server="{{ smb_server }}"
|
||||
path="{{ smb_share }}"
|
||||
mountpoint="/srv/samba/schools/default-school"
|
||||
options="sec=krb5i,cruid=%(USERUID),user=%(USER),gid=1010,file_mode=0770,dir_mode=0770,mfsymlinks,nobrl"
|
||||
><not><or><user>root</user><user>ansible</user><user>Debian-gdm</user><user>sddm</user><user>virti</user></or></not>
|
||||
</volume>
|
||||
insertafter: "<!-- Volume definitions -->"
|
||||
|
||||
|
||||
- name: Prepare persistent user cache base directory
|
||||
ansible.builtin.file:
|
||||
path: /var/cache/user/
|
||||
state: directory
|
||||
mode: '1777'
|
||||
|
||||
- name: Create user-environment-generator directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/user-environment-generators/
|
||||
state: directory
|
||||
|
||||
- name: Prepare generator for persistent user cache directory
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/user-environment-generators/50-xdg-cache-home.sh
|
||||
content: |
|
||||
#!/usr/bin/bash
|
||||
set -eu
|
||||
## local users do not need the extra cache dir:
|
||||
[[ "$UID" -le 60000 ]] && exit 0
|
||||
cp -r -n /etc/skel/.* "$HOME"
|
||||
DIR="/var/cache/user/${UID}/"
|
||||
[[ -d "$DIR" ]] || mkdir -m 0700 "$DIR"
|
||||
echo XDG_CACHE_HOME="$DIR"
|
||||
mode: "0755"
|
||||
|
||||
|
||||
- name: Clean up all user processes after logout
|
||||
ansible.builtin.replace:
|
||||
path: /etc/security/pam_mount.conf.xml
|
||||
regexp: '^(<logout wait="0" hup="no" term="no" kill="no" />)$'
|
||||
replace: '<!-- \1 -->\n<logout wait="1000" hup="yes" term="yes" kill="yes" />'
|
||||
|
||||
- name: Kill all user processes on logout
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/systemd/logind.conf
|
||||
line: KillUserProcesses=yes
|
||||
insertafter: '#KillUserProcesses=no'
|
||||
|
||||
- name: Bind mount lmn/media with nosuid directory
|
||||
ansible.posix.mount:
|
||||
src: /lmn/media
|
||||
path: /lmn/media
|
||||
opts: nosuid,bind
|
||||
state: present
|
||||
fstype: none
|
||||
|
||||
#- name: Mount NFSv4 home directory
|
||||
# ansible.posix.mount:
|
||||
# src: server:/default-school
|
||||
# path: /srv/samba/schools/default-school
|
||||
# opts: sec=krb5p,_netdev,x-systemd.automount,x-systemd.idle-timeout=60
|
||||
# state: present
|
||||
# fstype: nfs4
|
||||
# when: nfs4
|
13
roles/lmn_network/tasks/main.yml
Normal file
13
roles/lmn_network/tasks/main.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
|
||||
- name: set aptcache
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apt/apt.conf
|
||||
content: >
|
||||
Acquire::http::Proxy "http://10.16.1.2:3142/";
|
||||
|
||||
- name: set ntp Server
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/systemd/timesyncd.conf
|
||||
insertafter: '^#NTP='
|
||||
line: NTP=server.qgm.lan
|
2
roles/lmn_printer/defaults/main.yml
Normal file
2
roles/lmn_printer/defaults/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
smb_server: "server"
|
||||
smb_share: "default-school/"
|
4
roles/lmn_printer/files/90-lmn-sudotools
Normal file
4
roles/lmn_printer/files/90-lmn-sudotools
Normal file
|
@ -0,0 +1,4 @@
|
|||
%examusers ALL=(root) NOPASSWD: /usr/share/linuxmuster-linuxclient7/scripts/sudoTools
|
||||
%role-student ALL=(root) NOPASSWD: /usr/share/linuxmuster-linuxclient7/scripts/sudoTools
|
||||
%role-teacher ALL=(root) NOPASSWD: /usr/share/linuxmuster-linuxclient7/scripts/sudoTools
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#
|
||||
# linuxmuster-linuxclient7 is a library for use with Linuxmuster.net
|
||||
#
|
57
roles/lmn_printer/files/linuxmusterLinuxclient7/computer.py
Normal file
57
roles/lmn_printer/files/linuxmusterLinuxclient7/computer.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
import socket
|
||||
from linuxmusterLinuxclient7 import logging, ldapHelper, realm, localUserHelper
|
||||
|
||||
def hostname():
|
||||
"""
|
||||
Get the hostname of the computer
|
||||
|
||||
:return: The hostname
|
||||
:rtype: str
|
||||
"""
|
||||
return socket.gethostname().split('.', 1)[0]
|
||||
|
||||
def krbHostName():
|
||||
"""
|
||||
Get the krb hostname, eg. `COMPUTER01$`
|
||||
|
||||
:return: The krb hostname
|
||||
:rtype: str
|
||||
"""
|
||||
return hostname().upper() + "$"
|
||||
|
||||
def readAttributes():
|
||||
"""
|
||||
Read all ldap attributes of the cumputer
|
||||
|
||||
:return: Tuple (success, dict of attributes)
|
||||
:rtype: tuple
|
||||
"""
|
||||
return ldapHelper.searchOne("(sAMAccountName={}$)".format(hostname()))
|
||||
|
||||
def isInGroup(groupName):
|
||||
"""
|
||||
Check if the computer is part of an ldap group
|
||||
|
||||
:param groupName: The name of the group to check
|
||||
:type grouName: str
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
rc, groups = localUserHelper.getGroupsOfLocalUser(krbHostName())
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
return groupName in groups
|
||||
|
||||
def isInAD():
|
||||
"""
|
||||
Check if the computer is joined to an AD
|
||||
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
rc, groups = localUserHelper.getGroupsOfLocalUser(krbHostName())
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
return "domain computers" in groups
|
136
roles/lmn_printer/files/linuxmusterLinuxclient7/config.py
Normal file
136
roles/lmn_printer/files/linuxmusterLinuxclient7/config.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
import configparser, re
|
||||
from linuxmusterLinuxclient7 import logging, constants
|
||||
|
||||
def network():
|
||||
"""
|
||||
Get the network configuration in `/etc/linuxmuster-linuxclient7/network.conf`
|
||||
|
||||
:return: Tuple (success, dict of keys)
|
||||
:rtype: tuple
|
||||
"""
|
||||
rc, rawNetworkConfig = _readNetworkConfig()
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
if not _checkNetworkConfigVersion(rawNetworkConfig)[0]:
|
||||
return False, None
|
||||
|
||||
networkConfig = {}
|
||||
|
||||
try:
|
||||
networkConfig["serverHostname"] = rawNetworkConfig["serverHostname"]
|
||||
networkConfig["domain"] = rawNetworkConfig["domain"]
|
||||
networkConfig["realm"] = rawNetworkConfig["realm"]
|
||||
except KeyError as e:
|
||||
logging.error("Error when reading network.conf (2)")
|
||||
logging.exception(e)
|
||||
return False, None
|
||||
|
||||
return True, networkConfig
|
||||
|
||||
def writeNetworkConfig(newNetworkConfig):
|
||||
"""
|
||||
Write the network configuration in `/etc/linuxmuster-linuxclient7/network.conf`
|
||||
|
||||
:param newNetworkConfig: The new config
|
||||
:type newNetworkConfig: dict
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
networkConfig = configparser.ConfigParser(interpolation=None)
|
||||
|
||||
try:
|
||||
networkConfig["network"] = {}
|
||||
networkConfig["network"]["version"] = str(_networkConfigVersion())
|
||||
networkConfig["network"]["serverHostname"] = newNetworkConfig["serverHostname"]
|
||||
networkConfig["network"]["domain"] = newNetworkConfig["domain"]
|
||||
networkConfig["network"]["realm"] = newNetworkConfig["realm"]
|
||||
except Exception as e:
|
||||
logging.error("Error when preprocessing new network configuration!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
try:
|
||||
logging.info("Writing new network Configuration")
|
||||
with open(constants.networkConfigFilePath, 'w') as networkConfigFile:
|
||||
networkConfig.write(networkConfigFile)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Failed!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def upgrade():
|
||||
"""
|
||||
Upgrade the format of the network configuration in `/etc/linuxmuster-linuxclient7/network.conf`
|
||||
This is done automatically on package upgrades.
|
||||
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
return _upgradeNetworkConfig()
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _readNetworkConfig():
|
||||
configParser = configparser.ConfigParser()
|
||||
configParser.read(constants.networkConfigFilePath)
|
||||
try:
|
||||
rawNetworkConfig = configParser["network"]
|
||||
return True, rawNetworkConfig
|
||||
except KeyError as e:
|
||||
logging.error("Error when reading network.conf (1)")
|
||||
logging.exception(e)
|
||||
return False, None
|
||||
return configParser
|
||||
|
||||
def _networkConfigVersion():
|
||||
return 1
|
||||
|
||||
def _checkNetworkConfigVersion(rawNetworkConfig):
|
||||
try:
|
||||
networkConfigVersion = int(rawNetworkConfig["version"])
|
||||
except KeyError as e:
|
||||
logging.warning("The network.conf version could not be identified, assuming 0")
|
||||
networkConfigVersion = 0
|
||||
|
||||
if networkConfigVersion != _networkConfigVersion():
|
||||
logging.warning("The network.conf Version is a mismatch!")
|
||||
return False, networkConfigVersion
|
||||
|
||||
return True, networkConfigVersion
|
||||
|
||||
def _upgradeNetworkConfig():
|
||||
logging.info("Upgrading network config.")
|
||||
rc, rawNetworkConfig = _readNetworkConfig()
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
rc, networkConfigVersion = _checkNetworkConfigVersion(rawNetworkConfig)
|
||||
if rc:
|
||||
logging.info("No need to upgrade, already up-to-date.")
|
||||
return True
|
||||
|
||||
logging.info("Upgrading network config from {0} to {1}".format(networkConfigVersion, _networkConfigVersion()))
|
||||
|
||||
if networkConfigVersion > _networkConfigVersion():
|
||||
logging.error("Cannot upgrade from a newer version to an older one!")
|
||||
return False
|
||||
|
||||
try:
|
||||
if networkConfigVersion == 0:
|
||||
newNetworkConfig = {}
|
||||
newNetworkConfig["serverHostname"] = rawNetworkConfig["serverHostname"] + "." + rawNetworkConfig["domain"]
|
||||
newNetworkConfig["domain"] = rawNetworkConfig["domain"]
|
||||
newNetworkConfig["realm"] = rawNetworkConfig["domain"].upper()
|
||||
return writeNetworkConfig(newNetworkConfig)
|
||||
except Exception as e:
|
||||
logging.error("Error when upgrading network config!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
return True
|
46
roles/lmn_printer/files/linuxmusterLinuxclient7/constants.py
Normal file
46
roles/lmn_printer/files/linuxmusterLinuxclient7/constants.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
templateUser = "linuxadmin"
|
||||
userTemplateDir = "/home/" + templateUser
|
||||
defaultDomainAdminUser = "global-admin"
|
||||
|
||||
# {} will be substituted for the username
|
||||
shareMountBasepath = "/home/{}/media"
|
||||
hiddenShareMountBasepath = "/srv/samba/{}"
|
||||
machineAccountSysvolMountPath = "/var/lib/samba/sysvol"
|
||||
|
||||
etcBaseDir = "/etc/linuxmuster-linuxclient7"
|
||||
shareBaseDir = "/usr/share/linuxmuster-linuxclient7"
|
||||
configFileTemplateDir = shareBaseDir + "/templates"
|
||||
scriptDir = shareBaseDir + "/scripts"
|
||||
|
||||
networkConfigFilePath = etcBaseDir + "/network.conf"
|
||||
# {} will be substituted for the username
|
||||
tmpEnvironmentFilePath = "/home/{}/.linuxmuster-linuxclient7-environment.sh"
|
||||
|
||||
notTemplatableFiles = ["/etc/sssd/sssd.conf", "/etc/linuxmuster-linuxclient7/network.conf"]
|
||||
|
||||
# cleanup
|
||||
obsoleteFiles = [
|
||||
"/etc/profile.d/99-linuxmuster.sh",
|
||||
"/etc/sudoers.d/linuxmuster",
|
||||
"/etc/profile.d/linuxmuster-proxy.sh",
|
||||
"/etc/bash_completion.d/99-linuxmuster-client-adsso.sh",
|
||||
"/etc/profile.d/99-linuxmuster-client-adsso.sh",
|
||||
"/etc/sudoers.d/linuxmuster-client-adsso",
|
||||
"/usr/sbin/linuxmuster-client-adsso",
|
||||
"/usr/sbin/linuxmuster-client-adsso-print-logs",
|
||||
"/etc/systemd/system/linuxmuster-client-adsso.service",
|
||||
"{}/.config/autostart/linuxmuster-client-adsso-autostart.desktop".format(userTemplateDir),
|
||||
"/etc/cups/client.conf",
|
||||
"/usr/share/linuxmuster-linuxclient7/templates/linuxmuster-client-adsso.service",
|
||||
"/usr/share/linuxmuster-linuxclient7/templates/linuxmuster-client-adsso-autostart.desktop",
|
||||
"/etc/security/pam_mount.conf.xml",
|
||||
"{}/pam_mount.conf.xml".format(configFileTemplateDir)
|
||||
]
|
||||
|
||||
obsoleteDirectories = [
|
||||
"/etc/linuxmuster-client",
|
||||
"/etc/linuxmuster-client-adsso",
|
||||
"/usr/share/linuxmuster-client-adsso"
|
||||
]
|
|
@ -0,0 +1,60 @@
|
|||
import os
|
||||
from linuxmusterLinuxclient7 import constants, user, logging
|
||||
|
||||
def export(keyValuePair):
|
||||
"""
|
||||
Export an environment variable
|
||||
|
||||
:param keyValuePair: Key value pair in format `key=value`
|
||||
:type keyValuePait: str
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.debug("Saving export '{}' to tmp file".format(keyValuePair))
|
||||
|
||||
envList = keyValuePair.split("=")
|
||||
if len(envList) == 2:
|
||||
os.putenv(envList[0], envList[1])
|
||||
|
||||
return _appendToTmpEnvFile("export", keyValuePair)
|
||||
|
||||
def unset(key):
|
||||
"""
|
||||
Unset a previously exported environment variable
|
||||
|
||||
:param key: The key to unset
|
||||
:type key: str
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.debug("Saving unset '{}' to tmp file".format(key))
|
||||
return _appendToTmpEnvFile("unset", key)
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _isApplicable():
|
||||
if not user.isInAD():
|
||||
logging.error("Modifying environment variables of non-AD users is not supported by lmn-export and lmn-unset!")
|
||||
return False
|
||||
elif "LinuxmusterLinuxclient7EnvFixActive" not in os.environ or os.environ["LinuxmusterLinuxclient7EnvFixActive"] != "1":
|
||||
logging.error("lmn-export and lmn-unset may only be used inside of linuxmuster-linuxclient7 hooks!")
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _appendToTmpEnvFile(mode, keyValuePair):
|
||||
if not _isApplicable():
|
||||
return False
|
||||
|
||||
tmpEnvironmentFilePath = constants.tmpEnvironmentFilePath.format(user.username())
|
||||
fileOpenMode = "a" if os.path.exists(tmpEnvironmentFilePath) else "w"
|
||||
|
||||
try:
|
||||
with open(tmpEnvironmentFilePath, fileOpenMode) as tmpEnvironmentFile:
|
||||
tmpEnvironmentFile.write("\n{0} '{1}'".format(mode, keyValuePair))
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return False
|
133
roles/lmn_printer/files/linuxmusterLinuxclient7/fileHelper.py
Normal file
133
roles/lmn_printer/files/linuxmusterLinuxclient7/fileHelper.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
import os, shutil
|
||||
from linuxmusterLinuxclient7 import logging
|
||||
|
||||
def removeLinesInFileContainingString(filePath, forbiddenStrings):
|
||||
"""
|
||||
Remove all lines containing a given string form a file.
|
||||
|
||||
:param filePath: The path to the file
|
||||
:type filePath: str
|
||||
:param forbiddenStrings: The string to search for
|
||||
:type forbiddenStrings: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isinstance(forbiddenStrings, list):
|
||||
forbiddenStrings = [forbiddenStrings]
|
||||
|
||||
try:
|
||||
with open(filePath, "r") as originalFile:
|
||||
originalContents = originalFile.read()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.warning("Could not read contents of original file")
|
||||
return False
|
||||
|
||||
newContents = ""
|
||||
for line in originalContents.split("\n"):
|
||||
lineIsClean = True
|
||||
for forbiddenString in forbiddenStrings:
|
||||
lineIsClean = lineIsClean and not forbiddenString in line
|
||||
|
||||
if lineIsClean :
|
||||
newContents += line + "\n"
|
||||
|
||||
try:
|
||||
with open(filePath, "w") as originalFile:
|
||||
originalFile.write(newContents)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.warning("Could not write new contents to original file")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def deleteFile(filePath):
|
||||
"""
|
||||
Delete a file
|
||||
|
||||
:param filePath: The path of the file
|
||||
:type filePath: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(filePath):
|
||||
os.unlink(filePath)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error("Failed!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
def deleteFilesWithExtension(directory, extension):
|
||||
"""
|
||||
Delete all files with a given extension in a given directory.
|
||||
|
||||
:param directory: The path of the directory
|
||||
:type directory: str
|
||||
:param extension: The file extension
|
||||
:type extension: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if directory.endswith("/"):
|
||||
directory = directory[:-1]
|
||||
|
||||
if not os.path.exists(directory):
|
||||
return True
|
||||
|
||||
existingFiles=os.listdir(directory)
|
||||
|
||||
for file in existingFiles:
|
||||
if file.endswith(extension):
|
||||
logging.info("* Deleting {}".format(file))
|
||||
if not deleteFile("{}/{}".format(directory, file)):
|
||||
logging.error("Failed!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def deleteDirectory(directory):
|
||||
"""
|
||||
Recoursively delete a directory.
|
||||
|
||||
:param directory: The path of the directory
|
||||
:type directory: bool
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
shutil.rmtree(directory)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def deleteAllInDirectory(directory):
|
||||
"""
|
||||
Delete all files in a given directory
|
||||
|
||||
:param directory: The path of the directory
|
||||
:type directory: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
if directory.endswith("/"):
|
||||
directory = directory[:-1]
|
||||
|
||||
if not os.path.exists(directory):
|
||||
return True
|
||||
|
||||
existingFiles=os.listdir(directory)
|
||||
for file in existingFiles:
|
||||
fullFilePath = "{}/{}".format(directory, file)
|
||||
if os.path.isdir(fullFilePath):
|
||||
rc = deleteDirectory(fullFilePath)
|
||||
else:
|
||||
rc = deleteFile(fullFilePath)
|
||||
if not rc:
|
||||
logging.error("Failed!")
|
||||
return False
|
||||
|
||||
return True
|
291
roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py
Normal file
291
roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py
Normal file
|
@ -0,0 +1,291 @@
|
|||
# Order of parsing: (overwriting each other)
|
||||
# 1. Local (does not apply)
|
||||
# 2. Site (does not apply)
|
||||
# 3. Domain
|
||||
# 4. OUs from top to bottom
|
||||
import ldap, ldap.sasl, re, os.path
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
from linuxmusterLinuxclient7 import logging, constants, config, user, ldapHelper, shares, computer, printers
|
||||
|
||||
def processAllPolicies():
|
||||
"""
|
||||
Process all applicable policies (equivalent to gpupdate on windows)
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
rc, policyDnList = _findApplicablePolicies()
|
||||
if not rc:
|
||||
logging.fatal("* Error when loading applicable GPOs! Shares and printers will not work.")
|
||||
return False
|
||||
|
||||
for policyDn in policyDnList:
|
||||
_parsePolicy(policyDn)
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _parseGplinkSring(string):
|
||||
# a gPLink strink looks like this:
|
||||
# [LDAP://<link>;<status>][LDAP://<link>;<status>][...]
|
||||
# The ragex matches <link> and <status> in two separate groups
|
||||
# Note: "LDAP://" is matched as .*:// to prevent issues when the capitalization changes
|
||||
pattern = re.compile("\\[.*:\\/\\/([^\\]]+)\\;([0-9]+)\\]")
|
||||
|
||||
return pattern.findall(string)
|
||||
|
||||
def _extractOUsFromDN(dn):
|
||||
# NOT finished!
|
||||
pattern = re.compile("OU=([^,]+),")
|
||||
|
||||
ouList = pattern.findall(dn)
|
||||
# We need to parse from top to bottom
|
||||
ouList.reverse()
|
||||
return ouList
|
||||
|
||||
def _findApplicablePolicies():
|
||||
|
||||
policyDnList = []
|
||||
|
||||
""" Do this later!
|
||||
# 1. Domain
|
||||
rc, domainAdObject = ldapHelper.searchOne("(distinguishedName={})".format(ldapHelper.baseDn()))
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
policyDNs.extend(_parseGplinkSring(domainAdObject["gPLink"]))
|
||||
|
||||
# 2. OU policies from top to bottom
|
||||
rc, userAdObject = ldapHelper.searchOne("(sAMAccountName={})".format(user.username()))
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
print(userAdObject["distinguishedName"])
|
||||
"""
|
||||
|
||||
# For now, just parse policy sophomorix:school:<school name>
|
||||
rc, schoolName = user.school()
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
policyName = "sophomorix:school:{}".format(schoolName)
|
||||
|
||||
# find policy
|
||||
rc, policyAdObject = ldapHelper.searchOne("(displayName={})".format(policyName))
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
policyDnList.append((policyAdObject["distinguishedName"], 0))
|
||||
|
||||
return True, policyDnList
|
||||
|
||||
def _parsePolicy(policyDn):
|
||||
logging.info("=== Parsing policy [{0};{1}] ===".format(policyDn[0], policyDn[1]))
|
||||
|
||||
# Check if the policy is disabled
|
||||
if policyDn[1] == 1:
|
||||
logging.info("===> Policy is disabled! ===")
|
||||
return True
|
||||
|
||||
# Find policy in AD
|
||||
rc, policyAdObject = ldapHelper.searchOne("(distinguishedName={})".format(policyDn[0]))
|
||||
if not rc:
|
||||
logging.error("===> Could not find poilcy in AD! ===")
|
||||
return False, None
|
||||
|
||||
# mount the share the policy is on (probaply already mounted, just to be sure)
|
||||
rc, localPolicyPath = shares.getMountpointOfRemotePath(policyAdObject["gPCFileSysPath"], hiddenShare = True, autoMount = True)
|
||||
if not rc:
|
||||
logging.error("===> Could not mount path of poilcy! ===")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
# parse drives
|
||||
# Skip Drive Policys (fvs change)
|
||||
#_processDrivesPolicy(localPolicyPath)
|
||||
# parse printers
|
||||
_processPrintersPolicy(localPolicyPath)
|
||||
except Exception as e:
|
||||
logging.error("An error occured when parsing policy!")
|
||||
logging.exception(e)
|
||||
|
||||
logging.info("===> Parsed policy [{0};{1}] ===".format(policyDn[0], policyDn[1]))
|
||||
|
||||
def _parseXmlFilters(filtersXmlNode):
|
||||
if not filtersXmlNode.tag == "Filters":
|
||||
logging.warning("Tried to parse a non-filter node as a filter!")
|
||||
return []
|
||||
|
||||
filters = []
|
||||
|
||||
for xmlFilter in filtersXmlNode:
|
||||
if xmlFilter.tag == "FilterGroup":
|
||||
filters.append({
|
||||
"name": xmlFilter.attrib["name"].split("\\")[1],
|
||||
"bool": xmlFilter.attrib["bool"],
|
||||
"userContext": xmlFilter.attrib["userContext"],
|
||||
# userContext defines if the filter applies in user or computer context
|
||||
"type": xmlFilter.tag
|
||||
})
|
||||
|
||||
return filters
|
||||
|
||||
def _processFilters(policies):
|
||||
filteredPolicies = []
|
||||
|
||||
for policy in policies:
|
||||
if not len(policy["filters"]) > 0:
|
||||
filteredPolicies.append(policy)
|
||||
else:
|
||||
filtersPassed = True
|
||||
for filter in policy["filters"]:
|
||||
logging.debug("Testing filter: {}".format(filter))
|
||||
# TODO: check for AND and OR
|
||||
if filter["bool"] == "AND":
|
||||
filtersPassed = filtersPassed and _processFilter(filter)
|
||||
elif filter["bool"] == "OR":
|
||||
filtersPassed = filtersPassed or _processFilter(filter)
|
||||
else:
|
||||
logging.warning("Unknown boolean operation: {}! Cannot process filter!".format(filter["bool"]))
|
||||
|
||||
if filtersPassed:
|
||||
filteredPolicies.append(policy)
|
||||
|
||||
return filteredPolicies
|
||||
|
||||
def _processFilter(filter):
|
||||
if filter["type"] == "FilterGroup":
|
||||
if filter["userContext"] == "1":
|
||||
return user.isInGroup(filter["name"])
|
||||
elif filter["userContext"] == "0":
|
||||
return computer.isInGroup(filter["name"])
|
||||
|
||||
return False
|
||||
|
||||
def _parseXmlPolicy(policyFile):
|
||||
if not os.path.isfile(policyFile):
|
||||
logging.warning("==> XML policy file not found! ==")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
tree = ElementTree.parse(policyFile)
|
||||
return True, tree
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("==> Error while reading XML policy file! ==")
|
||||
return False, None
|
||||
|
||||
def _processDrivesPolicy(policyBasepath):
|
||||
logging.info("== Parsing a drive policy! ==")
|
||||
policyFile = "{}/User/Preferences/Drives/Drives.xml".format(policyBasepath)
|
||||
shareList = []
|
||||
|
||||
rc, tree = _parseXmlPolicy(policyFile)
|
||||
|
||||
if not rc:
|
||||
logging.error("==> Error while reading Drives policy file, skipping! ==")
|
||||
return
|
||||
|
||||
xmlDrives = tree.getroot()
|
||||
|
||||
if not xmlDrives.tag == "Drives":
|
||||
logging.warning("==> Drive policy xml File is of invalid format, skipping! ==")
|
||||
return
|
||||
|
||||
for xmlDrive in xmlDrives:
|
||||
|
||||
if xmlDrive.tag != "Drive" or ("disabled" in xmlDrive.attrib and xmlDrive.attrib["disabled"] == "1"):
|
||||
continue
|
||||
|
||||
drive = {}
|
||||
drive["filters"] = []
|
||||
for xmlDriveProperty in xmlDrive:
|
||||
if xmlDriveProperty.tag == "Properties":
|
||||
try:
|
||||
drive["label"] = xmlDriveProperty.attrib["label"]
|
||||
drive["letter"] = xmlDriveProperty.attrib["letter"]
|
||||
drive["path"] = xmlDriveProperty.attrib["path"]
|
||||
drive["useLetter"] = xmlDriveProperty.attrib["useLetter"]
|
||||
except Exception as e:
|
||||
logging.warning("Exception when parsing a drive policy XML file")
|
||||
logging.exception(e)
|
||||
continue
|
||||
|
||||
if xmlDriveProperty.tag == "Filters":
|
||||
drive["filters"] = _parseXmlFilters(xmlDriveProperty)
|
||||
|
||||
shareList.append(drive)
|
||||
|
||||
shareList = _processFilters(shareList)
|
||||
|
||||
logging.info("Found shares:")
|
||||
for drive in shareList:
|
||||
logging.info("* {:10}| {:5}| {:40}| {:5}".format(drive["label"], drive["letter"], drive["path"], drive["useLetter"]))
|
||||
|
||||
for drive in shareList:
|
||||
if drive["useLetter"] == "1":
|
||||
shareName = f"{drive['label']} ({drive['letter']}:)"
|
||||
else:
|
||||
shareName = drive["label"]
|
||||
shares.mountShare(drive["path"], shareName=shareName)
|
||||
|
||||
logging.info("==> Successfully parsed a drive policy! ==")
|
||||
|
||||
def _processPrintersPolicy(policyBasepath):
|
||||
logging.info("== Parsing a printer policy! ==")
|
||||
policyFile = "{}/User/Preferences/Printers/Printers.xml".format(policyBasepath)
|
||||
printerList = []
|
||||
# test
|
||||
rc, tree = _parseXmlPolicy(policyFile)
|
||||
|
||||
if not rc:
|
||||
logging.error("==> Error while reading Printer policy file, skipping! ==")
|
||||
return
|
||||
|
||||
xmlPrinters = tree.getroot()
|
||||
|
||||
if not xmlPrinters.tag == "Printers":
|
||||
logging.warning("==> Printer policy xml File is of invalid format, skipping! ==")
|
||||
return
|
||||
|
||||
for xmlPrinter in xmlPrinters:
|
||||
|
||||
if xmlPrinter.tag != "SharedPrinter" or ("disabled" in xmlPrinter.attrib and xmlPrinter.attrib["disabled"] == "1"):
|
||||
continue
|
||||
|
||||
printer = {}
|
||||
printer["filters"] = []
|
||||
|
||||
try:
|
||||
printer["name"] = xmlPrinter.attrib["name"]
|
||||
except Exception as e:
|
||||
logging.warning("Exception when reading a printer name from a printer policy XML file")
|
||||
logging.exception(e)
|
||||
|
||||
for xmlPrinterProperty in xmlPrinter:
|
||||
if xmlPrinterProperty.tag == "Properties":
|
||||
try:
|
||||
rc, printerUrl = printers.translateSambaToIpp(xmlPrinterProperty.attrib["path"])
|
||||
if rc:
|
||||
printer["path"] = printerUrl
|
||||
except Exception as e:
|
||||
logging.warning("Exception when parsing a printer policy XML file")
|
||||
logging.exception(e)
|
||||
continue
|
||||
|
||||
if xmlPrinterProperty.tag == "Filters":
|
||||
printer["filters"] = _parseXmlFilters(xmlPrinterProperty)
|
||||
|
||||
printerList.append(printer)
|
||||
|
||||
printerList = _processFilters(printerList)
|
||||
|
||||
logging.info("Found printers:")
|
||||
for printer in printerList:
|
||||
logging.info("* {0}\t\t| {1}\t| {2}".format(printer["name"], printer["path"], printer["filters"]))
|
||||
printers.installPrinter(printer["path"], printer["name"])
|
||||
|
||||
logging.info("==> Successfully parsed a printer policy! ==")
|
290
roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py.orig
Normal file
290
roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py.orig
Normal file
|
@ -0,0 +1,290 @@
|
|||
# Order of parsing: (overwriting each other)
|
||||
# 1. Local (does not apply)
|
||||
# 2. Site (does not apply)
|
||||
# 3. Domain
|
||||
# 4. OUs from top to bottom
|
||||
import ldap, ldap.sasl, re, os.path
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
from linuxmusterLinuxclient7 import logging, constants, config, user, ldapHelper, shares, computer, printers
|
||||
|
||||
def processAllPolicies():
|
||||
"""
|
||||
Process all applicable policies (equivalent to gpupdate on windows)
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
rc, policyDnList = _findApplicablePolicies()
|
||||
if not rc:
|
||||
logging.fatal("* Error when loading applicable GPOs! Shares and printers will not work.")
|
||||
return False
|
||||
|
||||
for policyDn in policyDnList:
|
||||
_parsePolicy(policyDn)
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _parseGplinkSring(string):
|
||||
# a gPLink strink looks like this:
|
||||
# [LDAP://<link>;<status>][LDAP://<link>;<status>][...]
|
||||
# The ragex matches <link> and <status> in two separate groups
|
||||
# Note: "LDAP://" is matched as .*:// to prevent issues when the capitalization changes
|
||||
pattern = re.compile("\\[.*:\\/\\/([^\\]]+)\\;([0-9]+)\\]")
|
||||
|
||||
return pattern.findall(string)
|
||||
|
||||
def _extractOUsFromDN(dn):
|
||||
# NOT finished!
|
||||
pattern = re.compile("OU=([^,]+),")
|
||||
|
||||
ouList = pattern.findall(dn)
|
||||
# We need to parse from top to bottom
|
||||
ouList.reverse()
|
||||
return ouList
|
||||
|
||||
def _findApplicablePolicies():
|
||||
|
||||
policyDnList = []
|
||||
|
||||
""" Do this later!
|
||||
# 1. Domain
|
||||
rc, domainAdObject = ldapHelper.searchOne("(distinguishedName={})".format(ldapHelper.baseDn()))
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
policyDNs.extend(_parseGplinkSring(domainAdObject["gPLink"]))
|
||||
|
||||
# 2. OU policies from top to bottom
|
||||
rc, userAdObject = ldapHelper.searchOne("(sAMAccountName={})".format(user.username()))
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
print(userAdObject["distinguishedName"])
|
||||
"""
|
||||
|
||||
# For now, just parse policy sophomorix:school:<school name>
|
||||
rc, schoolName = user.school()
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
policyName = "sophomorix:school:{}".format(schoolName)
|
||||
|
||||
# find policy
|
||||
rc, policyAdObject = ldapHelper.searchOne("(displayName={})".format(policyName))
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
policyDnList.append((policyAdObject["distinguishedName"], 0))
|
||||
|
||||
return True, policyDnList
|
||||
|
||||
def _parsePolicy(policyDn):
|
||||
logging.info("=== Parsing policy [{0};{1}] ===".format(policyDn[0], policyDn[1]))
|
||||
|
||||
# Check if the policy is disabled
|
||||
if policyDn[1] == 1:
|
||||
logging.info("===> Policy is disabled! ===")
|
||||
return True
|
||||
|
||||
# Find policy in AD
|
||||
rc, policyAdObject = ldapHelper.searchOne("(distinguishedName={})".format(policyDn[0]))
|
||||
if not rc:
|
||||
logging.error("===> Could not find poilcy in AD! ===")
|
||||
return False, None
|
||||
|
||||
# mount the share the policy is on (probaply already mounted, just to be sure)
|
||||
rc, localPolicyPath = shares.getMountpointOfRemotePath(policyAdObject["gPCFileSysPath"], hiddenShare = True, autoMount = True)
|
||||
if not rc:
|
||||
logging.error("===> Could not mount path of poilcy! ===")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
# parse drives
|
||||
_processDrivesPolicy(localPolicyPath)
|
||||
# parse printers
|
||||
_processPrintersPolicy(localPolicyPath)
|
||||
except Exception as e:
|
||||
logging.error("An error occured when parsing policy!")
|
||||
logging.exception(e)
|
||||
|
||||
logging.info("===> Parsed policy [{0};{1}] ===".format(policyDn[0], policyDn[1]))
|
||||
|
||||
def _parseXmlFilters(filtersXmlNode):
|
||||
if not filtersXmlNode.tag == "Filters":
|
||||
logging.warning("Tried to parse a non-filter node as a filter!")
|
||||
return []
|
||||
|
||||
filters = []
|
||||
|
||||
for xmlFilter in filtersXmlNode:
|
||||
if xmlFilter.tag == "FilterGroup":
|
||||
filters.append({
|
||||
"name": xmlFilter.attrib["name"].split("\\")[1],
|
||||
"bool": xmlFilter.attrib["bool"],
|
||||
"userContext": xmlFilter.attrib["userContext"],
|
||||
# userContext defines if the filter applies in user or computer context
|
||||
"type": xmlFilter.tag
|
||||
})
|
||||
|
||||
return filters
|
||||
|
||||
def _processFilters(policies):
|
||||
filteredPolicies = []
|
||||
|
||||
for policy in policies:
|
||||
if not len(policy["filters"]) > 0:
|
||||
filteredPolicies.append(policy)
|
||||
else:
|
||||
filtersPassed = True
|
||||
for filter in policy["filters"]:
|
||||
logging.debug("Testing filter: {}".format(filter))
|
||||
# TODO: check for AND and OR
|
||||
if filter["bool"] == "AND":
|
||||
filtersPassed = filtersPassed and _processFilter(filter)
|
||||
elif filter["bool"] == "OR":
|
||||
filtersPassed = filtersPassed or _processFilter(filter)
|
||||
else:
|
||||
logging.warning("Unknown boolean operation: {}! Cannot process filter!".format(filter["bool"]))
|
||||
|
||||
if filtersPassed:
|
||||
filteredPolicies.append(policy)
|
||||
|
||||
return filteredPolicies
|
||||
|
||||
def _processFilter(filter):
|
||||
if filter["type"] == "FilterGroup":
|
||||
if filter["userContext"] == "1":
|
||||
return user.isInGroup(filter["name"])
|
||||
elif filter["userContext"] == "0":
|
||||
return computer.isInGroup(filter["name"])
|
||||
|
||||
return False
|
||||
|
||||
def _parseXmlPolicy(policyFile):
|
||||
if not os.path.isfile(policyFile):
|
||||
logging.warning("==> XML policy file not found! ==")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
tree = ElementTree.parse(policyFile)
|
||||
return True, tree
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("==> Error while reading XML policy file! ==")
|
||||
return False, None
|
||||
|
||||
def _processDrivesPolicy(policyBasepath):
|
||||
logging.info("== Parsing a drive policy! ==")
|
||||
policyFile = "{}/User/Preferences/Drives/Drives.xml".format(policyBasepath)
|
||||
shareList = []
|
||||
|
||||
rc, tree = _parseXmlPolicy(policyFile)
|
||||
|
||||
if not rc:
|
||||
logging.error("==> Error while reading Drives policy file, skipping! ==")
|
||||
return
|
||||
|
||||
xmlDrives = tree.getroot()
|
||||
|
||||
if not xmlDrives.tag == "Drives":
|
||||
logging.warning("==> Drive policy xml File is of invalid format, skipping! ==")
|
||||
return
|
||||
|
||||
for xmlDrive in xmlDrives:
|
||||
|
||||
if xmlDrive.tag != "Drive" or ("disabled" in xmlDrive.attrib and xmlDrive.attrib["disabled"] == "1"):
|
||||
continue
|
||||
|
||||
drive = {}
|
||||
drive["filters"] = []
|
||||
for xmlDriveProperty in xmlDrive:
|
||||
if xmlDriveProperty.tag == "Properties":
|
||||
try:
|
||||
drive["label"] = xmlDriveProperty.attrib["label"]
|
||||
drive["letter"] = xmlDriveProperty.attrib["letter"]
|
||||
drive["path"] = xmlDriveProperty.attrib["path"]
|
||||
drive["useLetter"] = xmlDriveProperty.attrib["useLetter"]
|
||||
except Exception as e:
|
||||
logging.warning("Exception when parsing a drive policy XML file")
|
||||
logging.exception(e)
|
||||
continue
|
||||
|
||||
if xmlDriveProperty.tag == "Filters":
|
||||
drive["filters"] = _parseXmlFilters(xmlDriveProperty)
|
||||
|
||||
shareList.append(drive)
|
||||
|
||||
shareList = _processFilters(shareList)
|
||||
|
||||
logging.info("Found shares:")
|
||||
for drive in shareList:
|
||||
logging.info("* {:10}| {:5}| {:40}| {:5}".format(drive["label"], drive["letter"], drive["path"], drive["useLetter"]))
|
||||
|
||||
for drive in shareList:
|
||||
if drive["useLetter"] == "1":
|
||||
shareName = f"{drive['label']} ({drive['letter']}:)"
|
||||
else:
|
||||
shareName = drive["label"]
|
||||
shares.mountShare(drive["path"], shareName=shareName)
|
||||
|
||||
logging.info("==> Successfully parsed a drive policy! ==")
|
||||
|
||||
def _processPrintersPolicy(policyBasepath):
|
||||
logging.info("== Parsing a printer policy! ==")
|
||||
policyFile = "{}/User/Preferences/Printers/Printers.xml".format(policyBasepath)
|
||||
printerList = []
|
||||
# test
|
||||
rc, tree = _parseXmlPolicy(policyFile)
|
||||
|
||||
if not rc:
|
||||
logging.error("==> Error while reading Printer policy file, skipping! ==")
|
||||
return
|
||||
|
||||
xmlPrinters = tree.getroot()
|
||||
|
||||
if not xmlPrinters.tag == "Printers":
|
||||
logging.warning("==> Printer policy xml File is of invalid format, skipping! ==")
|
||||
return
|
||||
|
||||
for xmlPrinter in xmlPrinters:
|
||||
|
||||
if xmlPrinter.tag != "SharedPrinter" or ("disabled" in xmlPrinter.attrib and xmlPrinter.attrib["disabled"] == "1"):
|
||||
continue
|
||||
|
||||
printer = {}
|
||||
printer["filters"] = []
|
||||
|
||||
try:
|
||||
printer["name"] = xmlPrinter.attrib["name"]
|
||||
except Exception as e:
|
||||
logging.warning("Exception when reading a printer name from a printer policy XML file")
|
||||
logging.exception(e)
|
||||
|
||||
for xmlPrinterProperty in xmlPrinter:
|
||||
if xmlPrinterProperty.tag == "Properties":
|
||||
try:
|
||||
rc, printerUrl = printers.translateSambaToIpp(xmlPrinterProperty.attrib["path"])
|
||||
if rc:
|
||||
printer["path"] = printerUrl
|
||||
except Exception as e:
|
||||
logging.warning("Exception when parsing a printer policy XML file")
|
||||
logging.exception(e)
|
||||
continue
|
||||
|
||||
if xmlPrinterProperty.tag == "Filters":
|
||||
printer["filters"] = _parseXmlFilters(xmlPrinterProperty)
|
||||
|
||||
printerList.append(printer)
|
||||
|
||||
printerList = _processFilters(printerList)
|
||||
|
||||
logging.info("Found printers:")
|
||||
for printer in printerList:
|
||||
logging.info("* {0}\t\t| {1}\t| {2}".format(printer["name"], printer["path"], printer["filters"]))
|
||||
printers.installPrinter(printer["path"], printer["name"])
|
||||
|
||||
logging.info("==> Successfully parsed a printer policy! ==")
|
219
roles/lmn_printer/files/linuxmusterLinuxclient7/hooks.py
Normal file
219
roles/lmn_printer/files/linuxmusterLinuxclient7/hooks.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
#
|
||||
# This is used to run hooks
|
||||
#
|
||||
|
||||
from enum import Enum
|
||||
import os, subprocess
|
||||
from linuxmusterLinuxclient7 import logging, constants, user, config, computer, environment, setup, shares
|
||||
|
||||
class Type(Enum):
|
||||
"""
|
||||
Enum containing all hook types
|
||||
"""
|
||||
|
||||
Boot = 0
|
||||
"""The onBoot hook
|
||||
"""
|
||||
Shutdown = 1
|
||||
"""The on Shutdown hook
|
||||
"""
|
||||
LoginAsRoot = 2
|
||||
"""The onLoginAsRoot hook
|
||||
"""
|
||||
Login = 3
|
||||
"""The onLogin hook
|
||||
"""
|
||||
SessionStarted = 4
|
||||
"""The onSession started hook
|
||||
"""
|
||||
LogoutAsRoot = 5
|
||||
LoginLogoutAsRoot = 6
|
||||
|
||||
remoteScriptNames = {
|
||||
Type.Boot: "sysstart.sh",
|
||||
Type.Login: "logon.sh",
|
||||
Type.SessionStarted: "sessionstart.sh",
|
||||
Type.Shutdown: "sysstop.sh"
|
||||
}
|
||||
|
||||
_remoteScriptInUserContext = {
|
||||
Type.Boot: False,
|
||||
Type.Login: True,
|
||||
Type.SessionStarted: True,
|
||||
Type.Shutdown: False
|
||||
}
|
||||
|
||||
def runLocalHook(hookType):
|
||||
"""
|
||||
Run all scripts in a local hookdir
|
||||
|
||||
:param hookType: The type of hook to run
|
||||
:type hookType: hooks.Type
|
||||
"""
|
||||
logging.info("=== Running local hook on{0} ===".format(hookType.name))
|
||||
hookDir = _getLocalHookDir(hookType)
|
||||
if os.path.exists(hookDir):
|
||||
_prepareEnvironment()
|
||||
for fileName in sorted(os.listdir(hookDir)):
|
||||
filePath = hookDir + "/" + fileName
|
||||
_runHookScript(filePath)
|
||||
logging.info("===> Finished running local hook on{0} ===".format(hookType.name))
|
||||
|
||||
|
||||
def runRemoteHook(hookType):
|
||||
"""
|
||||
Run hookscript from sysvol
|
||||
|
||||
:param hookType: The type of hook to run
|
||||
:type hookType: hooks.Type
|
||||
"""
|
||||
logging.info("=== Running remote hook on{0} ===".format(hookType.name))
|
||||
rc, hookScripts = _getRemoteHookScripts(hookType)
|
||||
|
||||
if rc:
|
||||
_prepareEnvironment()
|
||||
_runHookScript(hookScripts[0])
|
||||
_runHookScript(hookScripts[1])
|
||||
|
||||
logging.info("===> Finished running remote hook on{0} ===".format(hookType.name))
|
||||
|
||||
def runHook(hookType):
|
||||
"""
|
||||
Executes hooks.runLocalHook() and hooks.runRemoteHook()
|
||||
|
||||
:param hookType: The type of hook to run
|
||||
:type hookType: hooks.Type
|
||||
"""
|
||||
runLocalHook(hookType)
|
||||
runRemoteHook(hookType)
|
||||
|
||||
def getLocalHookScript(hookType):
|
||||
"""Get the path of a local hookscript
|
||||
|
||||
:param hookType: The type of hook script to get the path for
|
||||
:type hookType: hooks.Type
|
||||
:return: The path
|
||||
:rtype: str
|
||||
"""
|
||||
return "{0}/on{1}".format(constants.scriptDir,hookType.name)
|
||||
|
||||
def shouldHooksBeExecuted(overrideUsername=None):
|
||||
"""Check if hooks should be executed
|
||||
|
||||
:param overrideUsername: Override the username to check, defaults to None
|
||||
:type overrideUsername: str, optional
|
||||
:return: True if hooks should be executed, fale otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
# check if linuxmuster-linuxclient7 is setup
|
||||
if not setup.isSetup():
|
||||
logging.info("==== Linuxmuster-linuxclient7 is not setup, exiting ====")
|
||||
return False
|
||||
|
||||
# check if the computer is joined
|
||||
if not computer.isInAD():
|
||||
logging.info("==== This Client is not joined to any domain, exiting ====")
|
||||
return False
|
||||
|
||||
# Check if the user is an AD user
|
||||
if overrideUsername == None:
|
||||
overrideUsername = user.username()
|
||||
|
||||
if not user.isUserInAD(overrideUsername):
|
||||
logging.info("==== {0} is not an AD user, exiting ====".format(user.username()))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _prepareEnvironment():
|
||||
dictsAndPrefixes = {}
|
||||
|
||||
rc, networkConfig = config.network()
|
||||
if rc:
|
||||
dictsAndPrefixes["Network"] = networkConfig
|
||||
|
||||
rc, userConfig = user.readAttributes()
|
||||
if rc:
|
||||
dictsAndPrefixes["User"] = userConfig
|
||||
|
||||
rc, computerConfig = computer.readAttributes()
|
||||
if rc:
|
||||
dictsAndPrefixes["Computer"] = computerConfig
|
||||
|
||||
environment = _dictsToEnv(dictsAndPrefixes)
|
||||
_writeEnvironment(environment)
|
||||
|
||||
def _getLocalHookDir(hookType):
|
||||
return "{0}/on{1}.d".format(constants.etcBaseDir,hookType.name)
|
||||
|
||||
def _getRemoteHookScripts(hookType):
|
||||
if not hookType in remoteScriptNames:
|
||||
return False, None
|
||||
|
||||
rc, networkConfig = config.network()
|
||||
|
||||
if not rc:
|
||||
logging.error("Could not execute server hooks because the network config could not be read")
|
||||
return False, None
|
||||
|
||||
if _remoteScriptInUserContext[hookType]:
|
||||
rc, attributes = user.readAttributes()
|
||||
if not rc:
|
||||
logging.error("Could not execute server hooks because the user config could not be read")
|
||||
return False, None
|
||||
else:
|
||||
rc, attributes = computer.readAttributes()
|
||||
if not rc:
|
||||
logging.error("Could not execute server hooks because the computer config could not be read")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
domain = networkConfig["domain"]
|
||||
school = attributes["sophomorixSchoolname"]
|
||||
scriptName = remoteScriptNames[hookType]
|
||||
except:
|
||||
logging.error("Could not execute server hooks because the computer/user config is missing attributes")
|
||||
return False, None
|
||||
|
||||
rc, sysvolPath = shares.getLocalSysvolPath()
|
||||
if not rc:
|
||||
logging.error("Could not execute server hook {} because the sysvol could not be mounted!\n")
|
||||
return False, None
|
||||
|
||||
hookScriptPathTemplate = "{0}/{1}/scripts/{2}/{3}/linux/{4}".format(sysvolPath, domain, school, "{}", scriptName)
|
||||
|
||||
return True, [hookScriptPathTemplate.format("lmn"), hookScriptPathTemplate.format("custom")]
|
||||
|
||||
# parameter must be a dict of {"prefix": dict}
|
||||
def _dictsToEnv(dictsAndPrefixes):
|
||||
environmentDict = {}
|
||||
for prefix in dictsAndPrefixes:
|
||||
for key in dictsAndPrefixes[prefix]:
|
||||
if type(dictsAndPrefixes[prefix][key]) is list:
|
||||
environmentDict[prefix + "_" + key] = "\n".join(dictsAndPrefixes[prefix][key])
|
||||
else:
|
||||
environmentDict[prefix + "_" + key] = dictsAndPrefixes[prefix][key]
|
||||
|
||||
return environmentDict
|
||||
|
||||
def _runHookScript(filePath):
|
||||
if not os.path.isfile(filePath):
|
||||
logging.warning("* File {0} should be executed as hook but does not exist!".format(filePath))
|
||||
return
|
||||
if not os.access(filePath, os.X_OK):
|
||||
logging.warning("* File {0} is in hook dir but not executable!".format(filePath))
|
||||
return
|
||||
|
||||
logging.info("== Executing script {0} ==".format(filePath))
|
||||
|
||||
result = subprocess.call([filePath])
|
||||
|
||||
logging.info("==> Script {0} finished with exit code {1} ==".format(filePath, result))
|
||||
|
||||
def _writeEnvironment(environment):
|
||||
for key in environment:
|
||||
os.putenv(key, environment[key])
|
220
roles/lmn_printer/files/linuxmusterLinuxclient7/imageHelper.py
Normal file
220
roles/lmn_printer/files/linuxmusterLinuxclient7/imageHelper.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
import os, subprocess, shutil
|
||||
from linuxmusterLinuxclient7 import logging, setup, realm, user, constants, printers, fileHelper
|
||||
|
||||
def prepareForImage(unattended=False):
|
||||
"""Prepare the computer for creating an image
|
||||
|
||||
:param unattended: If set to True, all questions will be answered with yes, defaults to False
|
||||
:type unattended: bool, optional
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info("#### Image preparation ####")
|
||||
|
||||
try:
|
||||
if not _testDomainJoin(unattended):
|
||||
return False
|
||||
|
||||
if not _upgradeSystem(unattended):
|
||||
return False
|
||||
|
||||
if not _clearCaches(unattended):
|
||||
return False
|
||||
|
||||
if not _clearUserHomes(unattended):
|
||||
return False
|
||||
|
||||
if not _clearUserCache(unattended):
|
||||
return False
|
||||
|
||||
if not _clearPrinters(unattended):
|
||||
return False
|
||||
|
||||
if not _clearLogs(unattended):
|
||||
return False
|
||||
|
||||
if not _emptyTrash(unattended):
|
||||
return False
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
logging.info("Cancelled.")
|
||||
return False
|
||||
|
||||
print()
|
||||
logging.info("#### Image preparation done ####")
|
||||
logging.info("#### You may create an Image now :) ####")
|
||||
print()
|
||||
return True
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _askStep(step, printPlaceholder=True):
|
||||
if printPlaceholder:
|
||||
print()
|
||||
response = input("Do you want to {}? (y/n): ".format(step))
|
||||
result = response in ["y", "Y", "j", "J"]
|
||||
if result:
|
||||
print()
|
||||
return result
|
||||
|
||||
def _testDomainJoin(unattended=False):
|
||||
if not unattended and not _askStep("test if the domain join works"):
|
||||
return True
|
||||
|
||||
return setup.status()
|
||||
|
||||
def _upgradeSystem(unattended=False):
|
||||
if not unattended and not _askStep("update this computer now"):
|
||||
return True
|
||||
|
||||
# Perform an update
|
||||
logging.info("Updating this computer now...")
|
||||
|
||||
if subprocess.call(["apt", "update"]) != 0:
|
||||
logging.error("apt update failed!")
|
||||
return False
|
||||
|
||||
if subprocess.call(["apt", "dist-upgrade", "-y"]) != 0:
|
||||
logging.error("apt dist-upgrade failed!")
|
||||
return False
|
||||
|
||||
if subprocess.call(["apt", "autoremove", "-y"]) != 0:
|
||||
logging.error("apt autoremove failed!")
|
||||
return False
|
||||
|
||||
if subprocess.call(["apt", "clean", "-y"]) != 0:
|
||||
logging.error("apt clean failed!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _clearCaches(unattended=False):
|
||||
if not unattended and not _askStep("clear journalctl and apt caches now"):
|
||||
return True
|
||||
|
||||
logging.info("Cleaning caches..")
|
||||
logging.info("* apt")
|
||||
fileHelper.deleteAllInDirectory("/var/lib/apt/lists/")
|
||||
logging.info("* journalctl")
|
||||
subprocess.call(["journalctl", "--flush", "--rotate"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.call(["journalctl", "--vacuum-time=1s"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
logging.info("Done.")
|
||||
return True
|
||||
|
||||
def _checkLoggedInUsers():
|
||||
result = subprocess.run("who -s | awk '{ print $1 }'", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
logging.error("Failed to get logged in users!")
|
||||
return False, None
|
||||
|
||||
loggedInUsers = list(filter(None, result.stdout.split("\n")))
|
||||
|
||||
for loggedInUser in loggedInUsers:
|
||||
if user.isUserInAD(loggedInUser):
|
||||
logging.error("User {} is still logged in, please log out first! Aborting!".format(loggedInUser))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _clearUserCache(unattended=False):
|
||||
if not unattended and not _askStep("clear all cached users now"):
|
||||
return True
|
||||
|
||||
if not _checkLoggedInUsers():
|
||||
return False
|
||||
|
||||
realm.clearUserCache()
|
||||
|
||||
logging.info("Done.")
|
||||
|
||||
return realm.clearUserCache()
|
||||
|
||||
def _unmountAllCifsMounts():
|
||||
logging.info("Unmounting all CIFS mounts!")
|
||||
if subprocess.call(["umount", "-a", "-t", "cifs", "-l"]) != 0:
|
||||
logging.info("Failed!")
|
||||
return False
|
||||
|
||||
# double check (just to be sure)
|
||||
result = subprocess.run("mount", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
logging.error("Failed to get mounts!")
|
||||
return False
|
||||
|
||||
if ("cifs" in result.stdout) or ("CIFS" in result.stdout):
|
||||
logging.error("There are still shares mounted!")
|
||||
logging.info("Use \"mount | grep cifs\" to view them.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _clearUserHomes(unattended=False):
|
||||
print("\nCAUTION! This will delete all userhomes of AD users!")
|
||||
if not unattended and not _askStep("clear all user homes now", False):
|
||||
return True
|
||||
|
||||
if not _checkLoggedInUsers():
|
||||
return False
|
||||
|
||||
if not _unmountAllCifsMounts():
|
||||
logging.info("Aborting deletion of user homes to prevent deleting data on the server.")
|
||||
return False
|
||||
|
||||
userHomes = os.listdir("/home")
|
||||
|
||||
logging.info("Deleting all user homes now!")
|
||||
for userHome in userHomes:
|
||||
if not user.isUserInAD(userHome):
|
||||
logging.info("* {} [SKIPPED]".format(userHome))
|
||||
continue
|
||||
|
||||
logging.info("* {}".format(userHome))
|
||||
try:
|
||||
shutil.rmtree("/home/{}".format(userHome))
|
||||
except Exception as e:
|
||||
logging.error("* FAILED!")
|
||||
logging.exception(e)
|
||||
|
||||
try:
|
||||
shutil.rmtree(constants.hiddenShareMountBasepath.format(userHome))
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.info("Done.")
|
||||
return True
|
||||
|
||||
def _clearPrinters(unattended=False):
|
||||
print("\nCAUTION! This will delete all printers of {}!".format(constants.templateUser))
|
||||
print("This makes sure that local printers do not conflict with remote printers defined by GPOs.")
|
||||
if not unattended and not _askStep("remove all local printers of {}".format(constants.templateUser), False):
|
||||
return True
|
||||
|
||||
if not printers.uninstallAllPrintersOfUser(constants.templateUser):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _clearLogs(unattended=False):
|
||||
if not unattended and not _askStep("clear the syslog"):
|
||||
return True
|
||||
|
||||
if not fileHelper.deleteFile("/var/log/syslog"):
|
||||
return False
|
||||
|
||||
subprocess.call(["sudo", "service", "rsyslog", "restart"])
|
||||
|
||||
return True
|
||||
|
||||
def _emptyTrash(unattended=False):
|
||||
if not unattended and not _askStep("clear the Trash of linuxadmin"):
|
||||
return True
|
||||
|
||||
if not fileHelper.deleteAllInDirectory("/home/{}/.local/share/Trash".format(constants.templateUser)):
|
||||
return False
|
||||
|
||||
return True
|
52
roles/lmn_printer/files/linuxmusterLinuxclient7/keytab.py
Normal file
52
roles/lmn_printer/files/linuxmusterLinuxclient7/keytab.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from krb5KeytabUtil import Krb5KeytabUtil
|
||||
from linuxmusterLinuxclient7 import computer, config, logging
|
||||
|
||||
def patchKeytab():
|
||||
"""
|
||||
Patches the `/etc/krb5.keytab` file. It inserts the correct hostname of the current computer.
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
krb5KeytabFilePath = "/etc/krb5.keytab"
|
||||
logging.info("Patching {}".format(krb5KeytabFilePath))
|
||||
krb5KeytabUtil = Krb5KeytabUtil(krb5KeytabFilePath)
|
||||
|
||||
try:
|
||||
krb5KeytabUtil.read()
|
||||
except:
|
||||
logging.error("Error reading {}".format(krb5KeytabFilePath))
|
||||
return False
|
||||
|
||||
for entry in krb5KeytabUtil.keytab.entries:
|
||||
oldData = entry.principal.components[-1].data
|
||||
if len(entry.principal.components) == 1:
|
||||
newData = computer.hostname().upper() + "$"
|
||||
entry.principal.components[0].data = newData
|
||||
|
||||
elif len(entry.principal.components) == 2 and (entry.principal.components[0].data == "host" or entry.principal.components[0].data == "RestrictedKrbHost"):
|
||||
rc, networkConfig = config.network()
|
||||
if not rc:
|
||||
continue
|
||||
|
||||
newData = ""
|
||||
domain = networkConfig["domain"]
|
||||
if domain in entry.principal.components[1].data:
|
||||
newData = computer.hostname().lower() + "." + domain
|
||||
else:
|
||||
newData = computer.hostname().upper()
|
||||
|
||||
entry.principal.components[1].data = newData
|
||||
|
||||
logging.debug("{} was changed to {}".format(oldData, entry.principal.components[-1].data))
|
||||
|
||||
logging.info("Trying to overwrite {}".format(krb5KeytabFilePath))
|
||||
try:
|
||||
result = krb5KeytabUtil.write()
|
||||
except:
|
||||
result = False
|
||||
|
||||
if not result:
|
||||
logging.error("Error overwriting {}".format(krb5KeytabFilePath))
|
||||
|
||||
return result
|
148
roles/lmn_printer/files/linuxmusterLinuxclient7/ldapHelper.py
Normal file
148
roles/lmn_printer/files/linuxmusterLinuxclient7/ldapHelper.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
import ldap, ldap.sasl, sys, getpass, subprocess
|
||||
from linuxmusterLinuxclient7 import logging, constants, config, user, computer
|
||||
|
||||
_currentLdapConnection = None
|
||||
|
||||
def serverUrl():
|
||||
"""
|
||||
Returns the server URL
|
||||
|
||||
:return: The server URL
|
||||
:rtype: str
|
||||
"""
|
||||
rc, networkConfig = config.network()
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
serverHostname = networkConfig["serverHostname"]
|
||||
return 'ldap://{0}'.format(serverHostname)
|
||||
|
||||
def baseDn():
|
||||
"""
|
||||
Returns the base DN
|
||||
|
||||
:return: The baseDN
|
||||
:rtype: str
|
||||
"""
|
||||
rc, networkConfig = config.network()
|
||||
|
||||
if not rc:
|
||||
return None
|
||||
|
||||
domain = networkConfig["domain"]
|
||||
return "dc=" + domain.replace(".", ",dc=")
|
||||
|
||||
def conn():
|
||||
"""
|
||||
Returns the ldap connection object
|
||||
|
||||
:return: The ldap connection object
|
||||
:rtype: ldap.ldapobject.LDAPObject
|
||||
"""
|
||||
global _currentLdapConnection
|
||||
|
||||
if _connect():
|
||||
return _currentLdapConnection
|
||||
|
||||
return None
|
||||
|
||||
def searchOne(filter):
|
||||
"""Searches the LDAP with a filter and returns the first found object
|
||||
|
||||
:param filter: A valid ldap filter
|
||||
:type filter: str
|
||||
:return: Tuple (success, ldap object as dict)
|
||||
:rtype: tuple
|
||||
"""
|
||||
if conn() == None:
|
||||
logging.error("Cannot talk to LDAP")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
rawResult = conn().search_s(
|
||||
baseDn(),
|
||||
ldap.SCOPE_SUBTREE,
|
||||
filter
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error("Error executing LDAP search!")
|
||||
logging.exception(e)
|
||||
return False, None
|
||||
|
||||
try:
|
||||
result = {}
|
||||
|
||||
if len(rawResult) <= 0 or rawResult[0][0] == None:
|
||||
logging.debug(f"Search \"{filter}\" did not return any objects")
|
||||
return False, None
|
||||
|
||||
for k in rawResult[0][1]:
|
||||
if rawResult[0][1][k] != None:
|
||||
rawAttribute = rawResult[0][1][k]
|
||||
try:
|
||||
if len(rawAttribute) == 1:
|
||||
result[k] = str(rawAttribute[0].decode())
|
||||
elif len(rawAttribute) > 0:
|
||||
result[k] = []
|
||||
for rawItem in rawAttribute:
|
||||
result[k].append(str(rawItem.decode()))
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Error while reading ldap search results!")
|
||||
logging.exception(e)
|
||||
return False, None
|
||||
|
||||
def isObjectInGroup(objectDn, groupName):
|
||||
"""
|
||||
Check if a given object is in a given group
|
||||
|
||||
:param objectDn: The DN of the object
|
||||
:type objectDn: str
|
||||
:param groupName: The name of the group
|
||||
:type groupName: str
|
||||
:return: True if it is a member, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.debug("= Testing if object {0} is a member of group {1} =".format(objectDn, groupName))
|
||||
rc, groupAdObject = searchOne("(&(member:1.2.840.113556.1.4.1941:={0})(sAMAccountName={1}))".format(objectDn, groupName))
|
||||
logging.debug("=> Result: {} =".format(rc))
|
||||
return rc
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _connect():
|
||||
global _currentLdapConnection
|
||||
|
||||
if not user.isInAD() and not (user.isRoot() or not computer.isInAD()):
|
||||
logging.warning("Cannot perform LDAP search: User is not in AD!")
|
||||
_currentLdapConnection = None
|
||||
return False
|
||||
|
||||
if not _currentLdapConnection == None:
|
||||
return True
|
||||
|
||||
try:
|
||||
sasl_auth = ldap.sasl.sasl({} ,'GSSAPI')
|
||||
_currentLdapConnection = ldap.initialize(serverUrl(), trace_level=0)
|
||||
# TODO:
|
||||
# conn.set_option(ldap.OPT_X_TLS_CACERTFILE, '/path/to/ca.pem')
|
||||
# conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
|
||||
# conn.start_tls_s()
|
||||
_currentLdapConnection.set_option(ldap.OPT_REFERRALS,0)
|
||||
_currentLdapConnection.protocol_version = ldap.VERSION3
|
||||
|
||||
_currentLdapConnection.sasl_interactive_bind_s("", sasl_auth)
|
||||
except Exception as e:
|
||||
_currentLdapConnection = None
|
||||
logging.error("Cloud not bind to ldap!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
return True
|
|
@ -0,0 +1,19 @@
|
|||
import subprocess
|
||||
from linuxmusterLinuxclient7 import logging
|
||||
|
||||
def getGroupsOfLocalUser(username):
|
||||
"""
|
||||
Get all groups of a local user
|
||||
|
||||
:param username: The username of the user
|
||||
:type username: str
|
||||
:return: Tuple (success, list of groups)
|
||||
:rtype: tuple
|
||||
"""
|
||||
try:
|
||||
groups = subprocess.check_output(["id", "-Gnz", username])
|
||||
stringList=[x.decode('utf-8') for x in groups.split(b"\x00")]
|
||||
return True, stringList
|
||||
except Exception as e:
|
||||
logging.warning("Exception when querying groups of user {}, it probaply does not exist".format(username))
|
||||
return False, None
|
130
roles/lmn_printer/files/linuxmusterLinuxclient7/logging.py
Normal file
130
roles/lmn_printer/files/linuxmusterLinuxclient7/logging.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
import logging, os, traceback, re, sys, subprocess
|
||||
from enum import Enum
|
||||
from linuxmusterLinuxclient7 import user, config
|
||||
|
||||
class Level(Enum):
|
||||
DEBUG = 0
|
||||
INFO = 1
|
||||
WARNING = 2
|
||||
ERROR = 3
|
||||
FATAL = 4
|
||||
|
||||
def debug(message):
|
||||
"""
|
||||
Do a debug log.
|
||||
|
||||
:param message: The message to log
|
||||
:type message: str
|
||||
"""
|
||||
_log(Level.DEBUG, message)
|
||||
|
||||
def info(message):
|
||||
"""
|
||||
Do an info log.
|
||||
|
||||
:param message: The message to log
|
||||
:type message: str
|
||||
"""
|
||||
_log(Level.INFO, message)
|
||||
|
||||
def warning(message):
|
||||
"""
|
||||
Do a warning log.
|
||||
|
||||
:param message: The message to log
|
||||
:type message: str
|
||||
"""
|
||||
_log(Level.WARNING, message)
|
||||
|
||||
def error(message):
|
||||
"""
|
||||
Do an error log.
|
||||
|
||||
:param message: The message to log
|
||||
:type message: str
|
||||
"""
|
||||
_log(Level.ERROR, message)
|
||||
|
||||
def fatal(message):
|
||||
"""
|
||||
Do a fatal log. If used in onLogin hook, this will create a dialog containing the message.
|
||||
|
||||
:param message: The message to log
|
||||
:type message: str
|
||||
"""
|
||||
_log(Level.FATAL, message)
|
||||
|
||||
def exception(exception):
|
||||
"""
|
||||
Log an exception
|
||||
|
||||
:param exception: The exception to log
|
||||
:type exception: Exception
|
||||
"""
|
||||
error("=== An exception occurred ===")
|
||||
error(str(exception))
|
||||
# Only use for debugging! This will cause ugly error dialogs in X11
|
||||
#traceback.print_tb(exception.__traceback__)
|
||||
error("=== end exception ===")
|
||||
|
||||
def printLogs(compact=False,anonymize=False):
|
||||
"""
|
||||
Print logs of linuxmuster-linuxclient7 from `/var/log/syslog`.
|
||||
|
||||
:param compact: If set to True, some stuff like time and date will be removed. Defaults to False
|
||||
:type compact: bool, optional
|
||||
:param anonymize: If set to True, domain/realm/serverHostname will be replaced by linuxmuster.lan. Defaults to False
|
||||
:type anonymize: bool, optional
|
||||
"""
|
||||
print("===========================================")
|
||||
print("=== Linuxmuster-linuxclient7 logs begin ===")
|
||||
|
||||
(rc, networkConfig) = config.network()
|
||||
if rc:
|
||||
domain = networkConfig["domain"]
|
||||
serverHostname = networkConfig["serverHostname"]
|
||||
realm= networkConfig["realm"]
|
||||
|
||||
with open("/var/log/syslog") as logfile:
|
||||
startPattern = re.compile("^.*linuxmuster-linuxclient7[^>]+======$")
|
||||
endPattern = re.compile("^.*linuxmuster-linuxclient7.*======>.*$")
|
||||
|
||||
currentlyInsideOfLinuxmusterLinuxclient7Log = False
|
||||
|
||||
for line in logfile:
|
||||
line = line.replace("\n", "")
|
||||
if startPattern.fullmatch(line):
|
||||
currentlyInsideOfLinuxmusterLinuxclient7Log = True
|
||||
print("\n")
|
||||
|
||||
if currentlyInsideOfLinuxmusterLinuxclient7Log:
|
||||
if compact:
|
||||
# "^([^ ]+[ ]+){4}" matches "Apr 6 14:39:23 somehostname"
|
||||
line = re.sub("^([^ ]+[ ]+){4}", "", line)
|
||||
if anonymize and rc:
|
||||
line = re.sub(serverHostname, "server.linuxmuster.lan", line)
|
||||
line = re.sub(domain, "linuxmuster.lan", line)
|
||||
line = re.sub(realm, "LINUXMUSTER.LAN", line)
|
||||
|
||||
print(line)
|
||||
|
||||
if endPattern.fullmatch(line):
|
||||
currentlyInsideOfLinuxmusterLinuxclient7Log = False
|
||||
print("\n")
|
||||
|
||||
print("=== Linuxmuster-linuxclient7 logs end ===")
|
||||
print("=========================================")
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _log(level, message):
|
||||
#if level == Level.DEBUG:
|
||||
# return
|
||||
if level == Level.FATAL:
|
||||
sys.stderr.write(message)
|
||||
|
||||
print("[{0}] {1}".format(level.name, message))
|
||||
message = message.replace("'", "")
|
||||
subprocess.call(["logger", "-t", "linuxmuster-linuxclient7", f"[{level.name}] {message}"])
|
129
roles/lmn_printer/files/linuxmusterLinuxclient7/printers.py
Normal file
129
roles/lmn_printer/files/linuxmusterLinuxclient7/printers.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
import os, subprocess, re
|
||||
from linuxmusterLinuxclient7 import logging, user
|
||||
|
||||
def installPrinter(networkPath, name=None, username=None):
|
||||
"""
|
||||
Installs a networked printer for a user
|
||||
|
||||
:param networkPath: The network path of the printer
|
||||
:type networkPath: str
|
||||
:param name: The name for the printer, defaults to None
|
||||
:type name: str, optional
|
||||
:param username: The username of the user whom the is installed printer for. Defaults to the executing user
|
||||
:type username: str, optional
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if username == None:
|
||||
username = user.username()
|
||||
|
||||
if user.isRoot():
|
||||
return _installPrinter(username, networkPath, name)
|
||||
else:
|
||||
# This will call installPrinter() again with root privileges
|
||||
return _installPrinterWithoutRoot(networkPath, name)
|
||||
|
||||
pass
|
||||
|
||||
def uninstallAllPrintersOfUser(username):
|
||||
"""
|
||||
Uninstalls all printers of a given user
|
||||
|
||||
:param username: The username of the user
|
||||
:type username: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info("Uninstalling all printers of {}".format(username))
|
||||
rc, installedPrinters = _getInstalledPrintersOfUser(username)
|
||||
|
||||
if not rc:
|
||||
logging.error("Error getting printers!")
|
||||
return False
|
||||
|
||||
for installedPrinter in installedPrinters:
|
||||
if not _uninstallPrinter(installedPrinter):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def translateSambaToIpp(networkPath):
|
||||
"""
|
||||
Translates a samba url, like `\\server\PRINTER-01`, to an ipp url like `ipp://server/printers/PRINTER-01`.
|
||||
|
||||
:param networkPath: The samba url
|
||||
:type networkPath: str
|
||||
:return: An ipp url
|
||||
:rtype: str
|
||||
"""
|
||||
networkPath = networkPath.replace("\\", "/")
|
||||
# path has to be translated: \\server\EW-FARBLASER -> ipp://server/printers/EW-farblaser
|
||||
pattern = re.compile("\\/\\/([^/]+)\\/(.*)")
|
||||
|
||||
result = pattern.findall(networkPath)
|
||||
if len(result) != 1 or len(result[0]) != 2:
|
||||
logging.error("Cannot convert printer network path from samba to ipp, as it is invalid: {}".format(networkPath))
|
||||
return False, None
|
||||
|
||||
ippNetworkPath = "ipp://{0}/printers/{1}".format(result[0][0], result[0][1])
|
||||
return True, ippNetworkPath
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _installPrinter(username, networkPath, name):
|
||||
logging.info("Install Printer {0} on {1}".format(name, networkPath))
|
||||
installCommand = ["timeout", "10", "lpadmin", "-p", name, "-E", "-v", networkPath, "-m", "everywhere", "-u", f"allow:{username}"]
|
||||
logging.debug("* running '{}'".format(" ".join(installCommand)))
|
||||
p = subprocess.call(installCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if p == 0:
|
||||
logging.debug("* Success Install Printer!")
|
||||
return True
|
||||
elif p == 124:
|
||||
logging.fatal(f"* Timeout error while installing printer {name} on {networkPath}")
|
||||
else:
|
||||
logging.fatal(f"* Error installing printer {name} on {networkPath}!")
|
||||
return False
|
||||
|
||||
def _installPrinterWithoutRoot(networkPath, name):
|
||||
return subprocess.call(["sudo", "/usr/share/linuxmuster-linuxclient7/scripts/sudoTools", "install-printer", "--path", networkPath, "--name", name]) == 0
|
||||
|
||||
def _getInstalledPrintersOfUser(username):
|
||||
logging.info(f"Getting installed printers of {username}")
|
||||
command = f"lpstat -U {username} -p"
|
||||
#logging.debug("running '{}'".format(command))
|
||||
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
|
||||
if not result.returncode == 0:
|
||||
logging.info("No Printers installed.")
|
||||
return True, []
|
||||
|
||||
rawInstalledPrinters = list(filter(None, result.stdout.split("\n")))
|
||||
installedPrinters = []
|
||||
|
||||
for rawInstalledPrinter in rawInstalledPrinters:
|
||||
rawInstalledPrinterList = list(filter(None, rawInstalledPrinter.split(" ")))
|
||||
|
||||
if len(rawInstalledPrinterList) < 2:
|
||||
continue
|
||||
|
||||
installedPrinter = rawInstalledPrinterList[1]
|
||||
installedPrinters.append(installedPrinter)
|
||||
|
||||
return True, installedPrinters
|
||||
|
||||
def _uninstallPrinter(name):
|
||||
logging.info("Uninstall Printer {}".format(name))
|
||||
uninstallCommand = ["timeout", "10", "lpadmin", "-x", name]
|
||||
logging.debug("* running '{}'".format(" ".join(uninstallCommand)))
|
||||
p = subprocess.call(uninstallCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if p == 0:
|
||||
logging.debug("* Success Uninstall Printer!")
|
||||
return True
|
||||
elif p == 124:
|
||||
logging.fatal(f"* Timeout error while installing printer {name}")
|
||||
else:
|
||||
logging.fatal(f"* Error Uninstalling Printer {name}!")
|
||||
return False
|
195
roles/lmn_printer/files/linuxmusterLinuxclient7/realm.py
Normal file
195
roles/lmn_printer/files/linuxmusterLinuxclient7/realm.py
Normal file
|
@ -0,0 +1,195 @@
|
|||
import os, sys, subprocess, configparser
|
||||
from linuxmusterLinuxclient7 import logging, computer
|
||||
|
||||
def join(domain, user):
|
||||
"""
|
||||
Joins the computer to an AD
|
||||
|
||||
:param domain: The domain to join
|
||||
:type domain: str
|
||||
:param user: The admin user used for joining
|
||||
:type user: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
# join the domain using the kerberos ticket
|
||||
joinCommand = ["realm", "join", "-v", domain, "-U", user]
|
||||
if subprocess.call(joinCommand) != 0:
|
||||
print()
|
||||
logging.error('Failed! Did you enter the correct password?')
|
||||
return False
|
||||
|
||||
logging.info("It looks like the domain was joined successfully.")
|
||||
return True
|
||||
|
||||
def leave(domain):
|
||||
"""
|
||||
Leave a domain
|
||||
|
||||
:param domain: The domain to leave
|
||||
:type domain: str
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
leaveCommand = ["realm", "leave", domain]
|
||||
return subprocess.call(leaveCommand) == 0
|
||||
|
||||
def leaveAll():
|
||||
"""
|
||||
Leaves all joined domains
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info("Cleaning / leaving all domain joins")
|
||||
|
||||
rc, joinedDomains = getJoinedDomains()
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
for joinedDomain in joinedDomains:
|
||||
logging.info("* {}".format(joinedDomain))
|
||||
if not leave(joinedDomain):
|
||||
logging.error("-> Failed! Aborting!")
|
||||
return False
|
||||
|
||||
logging.info("-> Done!")
|
||||
return True
|
||||
|
||||
def isJoined():
|
||||
"""
|
||||
Checks if the computer is joined to a domain
|
||||
|
||||
:return: True if it is joined to one or more domains, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
rc, joinedDomains = getJoinedDomains()
|
||||
if not rc:
|
||||
return False
|
||||
else:
|
||||
return len(joinedDomains) > 0
|
||||
|
||||
def pullKerberosTicketForComputerAccount():
|
||||
"""
|
||||
Pulls a kerberos ticket using the computer account from `/etc/krb5.keytab`
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return subprocess.call(["kinit", "-k", computer.krbHostName()]) == 0
|
||||
|
||||
def verifyDomainJoin():
|
||||
"""
|
||||
Checks if the domain join actually works.
|
||||
|
||||
:return: True if it does, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info("Testing if the domain join actually works")
|
||||
if not isJoined():
|
||||
logging.error("No domain is joined!")
|
||||
return False
|
||||
|
||||
logging.info("* Checking if the group \"domain users\" exists")
|
||||
if subprocess.call(["getent", "group", "domain users"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) != 0:
|
||||
logging.error("The \"domain users\" group does not exists! Users wont be able to log in!")
|
||||
logging.error("This is sometimes related to /etc/nsswitch.conf.")
|
||||
return False
|
||||
|
||||
# Try to get a kerberos ticket for the computer account
|
||||
logging.info("* Trying to get a kerberos ticket for the Computer Account")
|
||||
if not pullKerberosTicketForComputerAccount():
|
||||
logging.error("Could not get a kerberos ticket for the Computer Account!")
|
||||
logging.error("Logins of non-cached users WILL NOT WORK!")
|
||||
logging.error("Please try to re-join the Domain.")
|
||||
return False
|
||||
|
||||
|
||||
logging.info("The domain join is working!")
|
||||
return True
|
||||
|
||||
def getJoinedDomains():
|
||||
"""
|
||||
Returns all joined domains
|
||||
|
||||
:return: Tuple (success, list of joined domians)
|
||||
:rtype: tuple
|
||||
"""
|
||||
result = subprocess.run("realm list --name-only", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
logging.error("Failed to read domain joins!")
|
||||
return False, None
|
||||
|
||||
return True, list(filter(None, result.stdout.split("\n")))
|
||||
|
||||
def discoverDomains():
|
||||
"""
|
||||
Searches for avialable domains on the current network
|
||||
|
||||
:return: Tuple (success, list of available domains)
|
||||
:rtype: tuple
|
||||
"""
|
||||
result = subprocess.run("realm discover --name-only", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
logging.error("Failed to discover available domains!")
|
||||
return False, None
|
||||
|
||||
return True, list(filter(None, result.stdout.split("\n")))
|
||||
|
||||
def getDomainConfig(domain):
|
||||
"""
|
||||
Looks up all relevant properties of a domain:
|
||||
- domain controller IP
|
||||
- domain name
|
||||
|
||||
:param domain: The domain to check
|
||||
:type domain: str
|
||||
:return: Tuple (success, dict with domain config)
|
||||
:rtype: tuple
|
||||
"""
|
||||
result = subprocess.run("adcli info '{}'".format(domain), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
logging.error("Failed to get details of domain {}!".format(domain))
|
||||
return False, None
|
||||
|
||||
rawConfig = _readConfigFromString(result.stdout)
|
||||
try:
|
||||
rawDomainConfig = rawConfig["domain"]
|
||||
except KeyError:
|
||||
logging.error("Error when reading domain details")
|
||||
return False, None
|
||||
|
||||
domainConfig = {}
|
||||
|
||||
try:
|
||||
domainConfig["domain-controller"] = rawDomainConfig["domain-controller"]
|
||||
domainConfig["domain-name"] = rawDomainConfig["domain-name"]
|
||||
except KeyError:
|
||||
logging.error("Error when reading domain details (2)")
|
||||
return False, None
|
||||
|
||||
return True, domainConfig
|
||||
|
||||
def clearUserCache():
|
||||
"""
|
||||
Clears the local user cache
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
# clean sssd cache
|
||||
logging.info("Cleaning sssd cache.")
|
||||
subprocess.call(["sssctl", "cache-remove", "--stop", "--start", "--override"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return True
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _readConfigFromString(string):
|
||||
configParser = configparser.ConfigParser()
|
||||
configParser.read_string(string)
|
||||
return configParser
|
364
roles/lmn_printer/files/linuxmusterLinuxclient7/setup.py
Normal file
364
roles/lmn_printer/files/linuxmusterLinuxclient7/setup.py
Normal file
|
@ -0,0 +1,364 @@
|
|||
import os, re, sys, configparser, subprocess, shutil
|
||||
from pathlib import Path
|
||||
from linuxmusterLinuxclient7 import logging, constants, hooks, shares, config, user, templates, realm, fileHelper, printers, computer
|
||||
|
||||
def setup(domain=None, user=None):
|
||||
"""
|
||||
Sets up the client to be able to act in a linuxmuster environment
|
||||
|
||||
:param domain: The domain to join, defaults to the first discovered domain
|
||||
:type domain: str, optional
|
||||
:param user: The admin user for the join, defaults to global-admin
|
||||
:type user: str, optional
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info('#### linuxmuster-linuxclient7 setup ####')
|
||||
|
||||
if not realm.clearUserCache():
|
||||
return False
|
||||
|
||||
if not _cleanOldDomainJoins():
|
||||
return False
|
||||
|
||||
rc, domain = _findDomain(domain)
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
if user == None:
|
||||
user = constants.defaultDomainAdminUser
|
||||
|
||||
if not _prepareNetworkConfiguration(domain):
|
||||
return False
|
||||
|
||||
if not _deleteObsoleteFiles():
|
||||
return False
|
||||
|
||||
if not templates.applyAll():
|
||||
return False
|
||||
|
||||
if not _preparePam():
|
||||
return False
|
||||
|
||||
if not _prepareServices():
|
||||
return False
|
||||
|
||||
# Actually join domain!
|
||||
print()
|
||||
logging.info(f"#### Joining domain {domain} ####")
|
||||
|
||||
if not realm.join(domain, user):
|
||||
return False
|
||||
|
||||
# copy server ca certificate in place
|
||||
# This will also make sure that the domain join actually worked;
|
||||
# mounting the sysvol will fail otherwise.
|
||||
if not _installCaCertificate(domain, user):
|
||||
return False
|
||||
|
||||
if not _adjustSssdConfiguration(domain):
|
||||
return False
|
||||
|
||||
# run a final test
|
||||
if not realm.verifyDomainJoin():
|
||||
return False
|
||||
|
||||
print("\n\n")
|
||||
|
||||
logging.info(f"#### SUCCESSFULLY joined domain {domain} ####")
|
||||
|
||||
return True
|
||||
|
||||
def status():
|
||||
"""
|
||||
Checks the status of the client
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info('#### linuxmuster-linuxclient7 status ####')
|
||||
|
||||
if not isSetup():
|
||||
logging.info("Not setup!")
|
||||
return False
|
||||
|
||||
logging.info("Linuxmuster-linuxclient7 is setup!")
|
||||
logging.info("Testing if domain is joined...")
|
||||
|
||||
logging.info("Checking joined domains")
|
||||
rc, joinedDomains = realm.getJoinedDomains()
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
print()
|
||||
logging.info("Joined domains:")
|
||||
for joinedDomain in joinedDomains:
|
||||
logging.info(f"* {joinedDomain}")
|
||||
print()
|
||||
|
||||
if len(joinedDomains) > 0 and not realm.verifyDomainJoin():
|
||||
print()
|
||||
# Give a little explination to our users :)
|
||||
print("\n===============================================================================================")
|
||||
print("This Computer is joined to a domain, but it was not possible to authenticate")
|
||||
print("to the domain controller. There is an error with your domain join! The login WILL NOT WORK!")
|
||||
print("Please try to re-join the domain using 'linuxmuster-linuxclient7 setup' and create a new image.")
|
||||
print("===============================================================================================\n")
|
||||
return False
|
||||
elif len(joinedDomains) <= 0:
|
||||
print()
|
||||
logging.info('#### This client is not joined to any domain. ####')
|
||||
print("#### To join a domain, run \"linuxmuster-linuxclient7 setup\" ####")
|
||||
|
||||
print()
|
||||
|
||||
logging.info('#### linuxmuster-linuxclient7 is fully setup and working! ####')
|
||||
|
||||
return True
|
||||
|
||||
def upgrade():
|
||||
"""
|
||||
Performs an upgrade of the linuxmuster-linuxclient7. This is executed after the package is updated.
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isSetup():
|
||||
logging.info("linuxmuster-linuxclient7 does not seem to be setup -> no upgrade is needed")
|
||||
return True
|
||||
|
||||
logging.info('#### linuxmuster-linuxclient7 upgrade ####')
|
||||
if not config.upgrade():
|
||||
return False
|
||||
|
||||
if not _deleteObsoleteFiles():
|
||||
return False
|
||||
|
||||
if not templates.applyAll():
|
||||
return False
|
||||
|
||||
if not _prepareServices():
|
||||
return False
|
||||
|
||||
rc, joinedDomains = realm.getJoinedDomains()
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
for domain in joinedDomains:
|
||||
_adjustSssdConfiguration(domain)
|
||||
|
||||
logging.info('#### linuxmuster-linuxclient7 upgrade SUCCESSFULL ####')
|
||||
return True
|
||||
|
||||
def clean():
|
||||
"""Removes all sensitive files like keys and leaves all domain joins.
|
||||
"""
|
||||
logging.info("#### linuxmuster-linuxclient7 clean ####")
|
||||
|
||||
realm.clearUserCache()
|
||||
_cleanOldDomainJoins()
|
||||
|
||||
# clean /etc/pam.d/common-session
|
||||
logging.info("Cleaning /etc/pam.d/common-session to prevent logon brick")
|
||||
fileHelper.removeLinesInFileContainingString("/etc/pam.d/common-session", ["pam_mkhomedir.so", "pam_exec.so", "pam_mount.so", "linuxmuster.net", "linuxmuster-linuxclient7", "linuxmuster-client-adsso"])
|
||||
|
||||
logging.info('#### linuxmuster-linuxclient7 clean SUCCESSFULL ####')
|
||||
|
||||
def isSetup():
|
||||
"""
|
||||
Checks if the client is setup.
|
||||
|
||||
:return: True if setup, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return os.path.isfile(constants.networkConfigFilePath)
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _cleanOldDomainJoins():
|
||||
# stop sssd
|
||||
logging.info("Stopping sssd")
|
||||
if subprocess.call(["service", "sssd", "stop"]) != 0:
|
||||
logging.error("Failed!")
|
||||
return False
|
||||
|
||||
# Clean old domain join data
|
||||
logging.info("Deleting old kerberos tickets.")
|
||||
subprocess.call(["kdestroy"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if not realm.leaveAll():
|
||||
return False
|
||||
|
||||
# delete krb5.keytab file, if existent
|
||||
logging.info('Deleting krb5.keytab if it exists ... ')
|
||||
if not fileHelper.deleteFile("/etc/krb5.keytab"):
|
||||
return False
|
||||
|
||||
# delete old CA Certificate
|
||||
logging.info('Deleting old CA certificate if it exists ... ')
|
||||
if not fileHelper.deleteFilesWithExtension("/var/lib/samba/private/tls", ".pem"):
|
||||
return False
|
||||
|
||||
# remove network.conf
|
||||
logging.info(f"Deleting {constants.networkConfigFilePath} if exists ...")
|
||||
if not fileHelper.deleteFile(constants.networkConfigFilePath):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _findDomain(domain=None):
|
||||
logging.info("Trying to discover available domains...")
|
||||
rc, availableDomains = realm.discoverDomains()
|
||||
if not rc or len(availableDomains) < 1:
|
||||
logging.error("Could not discover any domain!")
|
||||
return False, None
|
||||
|
||||
if domain == None:
|
||||
domain = availableDomains[0]
|
||||
logging.info(f"Using first discovered domain {domain}")
|
||||
elif domain in availableDomains:
|
||||
logging.info(f"Using domain {domain}")
|
||||
else:
|
||||
print("\n")
|
||||
logging.error(f"Could not find domain {domain}!")
|
||||
return False, None
|
||||
|
||||
return True, domain
|
||||
|
||||
def _prepareNetworkConfiguration(domain):
|
||||
logging.info("Preparing network configuration")
|
||||
rc, domainConfig = realm.getDomainConfig(domain)
|
||||
if not rc:
|
||||
logging.error("Could not read domain configuration")
|
||||
return False
|
||||
|
||||
newNetworkConfig = {}
|
||||
newNetworkConfig["serverHostname"] = domainConfig["domain-controller"]
|
||||
newNetworkConfig["domain"] = domainConfig["domain-name"]
|
||||
newNetworkConfig["realm"] = domainConfig["domain-name"].upper()
|
||||
|
||||
config.writeNetworkConfig(newNetworkConfig)
|
||||
|
||||
return True
|
||||
|
||||
def _preparePam():
|
||||
# enable necessary pam modules
|
||||
logging.info('Updating pam configuration ... ')
|
||||
subprocess.call(['pam-auth-update', '--package', '--enable', 'libpam-mount', 'pwquality', 'sss', '--force'])
|
||||
## mkhomedir was injected in template not using pam-auth-update
|
||||
subprocess.call(['pam-auth-update', '--package', '--remove', 'krb5', 'mkhomedir', '--force'])
|
||||
|
||||
return True
|
||||
|
||||
def _prepareServices():
|
||||
logging.info("Raloading systctl daemon")
|
||||
subprocess.call(["systemctl", "daemon-reload"])
|
||||
|
||||
logging.info('Enabling services:')
|
||||
services = ['linuxmuster-linuxclient7', 'smbd', 'nmbd', 'sssd']
|
||||
for service in services:
|
||||
logging.info('* %s' % service)
|
||||
subprocess.call(['systemctl','enable', service + '.service'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
logging.info('Restarting services:')
|
||||
services = ['smbd', 'nmbd', 'systemd-timesyncd']
|
||||
for service in services:
|
||||
logging.info('* %s' % service)
|
||||
subprocess.call(['systemctl', 'restart' , service + '.service'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
return True
|
||||
|
||||
def _installCaCertificate(domain, user):
|
||||
logging.info('Installing server ca certificate ... ')
|
||||
|
||||
# try to mount the share
|
||||
rc, sysvolMountpoint = shares.getLocalSysvolPath()
|
||||
if not rc:
|
||||
logging.error("Failed to mount sysvol!")
|
||||
return False
|
||||
|
||||
cacertPath = f"{sysvolMountpoint}/{domain}/tls/cacert.pem"
|
||||
cacertTargetPath = f"/var/lib/samba/private/tls/{domain}.pem"
|
||||
|
||||
logging.info("Copying CA certificate from server to client!")
|
||||
try:
|
||||
Path(Path(cacertTargetPath).parent.absolute()).mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(cacertPath, cacertTargetPath)
|
||||
except Exception as e:
|
||||
logging.error("Failed!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
# make sure the file was successfully copied
|
||||
if not os.path.isfile(cacertTargetPath):
|
||||
logging.error('Failed to copy over CA certificate!')
|
||||
return False
|
||||
|
||||
# unmount sysvol
|
||||
shares.unmountAllSharesOfUser(computer.krbHostName())
|
||||
|
||||
return True
|
||||
|
||||
def _adjustSssdConfiguration(domain):
|
||||
logging.info("Adjusting sssd.conf")
|
||||
|
||||
sssdConfigFilePath = '/etc/sssd/sssd.conf'
|
||||
sssdConfig = configparser.ConfigParser(interpolation=None)
|
||||
|
||||
sssdConfig.read(sssdConfigFilePath)
|
||||
# accept usernames without domain
|
||||
sssdConfig[f"domain/{domain}"]["use_fully_qualified_names"] = "False"
|
||||
|
||||
# override homedir
|
||||
sssdConfig[f"domain/{domain}"]["override_homedir"] = "/home/%u"
|
||||
|
||||
# Don't validate KVNO! Otherwise the Login will fail when the KVNO stored
|
||||
# in /etc/krb5.keytab does not match the one in the AD (msDS-KeyVersionNumber)
|
||||
sssdConfig[f"domain/{domain}"]["krb5_validate"] = "False"
|
||||
|
||||
sssdConfig[f"domain/{domain}"]["ad_gpo_access_control"] = "permissive"
|
||||
sssdConfig[f"domain/{domain}"]["ad_gpo_ignore_unreadable"] = "True"
|
||||
|
||||
# Don't renew the machine password, as this will break the domain join
|
||||
# See: https://github.com/linuxmuster/linuxmuster-linuxclient7/issues/27
|
||||
sssdConfig[f"domain/{domain}"]["ad_maximum_machine_account_password_age"] = "0"
|
||||
|
||||
# Make sure usernames are not case sensitive
|
||||
sssdConfig[f"domain/{domain}"]["case_sensitive"] = "False"
|
||||
|
||||
try:
|
||||
logging.info("Writing new Configuration")
|
||||
with open(sssdConfigFilePath, 'w') as sssdConfigFile:
|
||||
sssdConfig.write(sssdConfigFile)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Failed!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
logging.info("Restarting sssd")
|
||||
if subprocess.call(["service", "sssd", "restart"]) != 0:
|
||||
logging.error("Failed!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _deleteObsoleteFiles():
|
||||
|
||||
# files
|
||||
logging.info("Deleting obsolete files")
|
||||
|
||||
for obsoleteFile in constants.obsoleteFiles:
|
||||
logging.info(f"* {obsoleteFile}")
|
||||
fileHelper.deleteFile(obsoleteFile)
|
||||
|
||||
# directories
|
||||
logging.info("Deleting obsolete directories")
|
||||
|
||||
for obsoleteDirectory in constants.obsoleteDirectories:
|
||||
logging.info(f"* {obsoleteDirectory}")
|
||||
fileHelper.deleteDirectory(obsoleteDirectory)
|
||||
|
||||
return True
|
256
roles/lmn_printer/files/linuxmusterLinuxclient7/shares.py
Normal file
256
roles/lmn_printer/files/linuxmusterLinuxclient7/shares.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
import os, pwd, sys, shutil, re, subprocess, shutil
|
||||
from linuxmusterLinuxclient7 import logging, constants, user, config, computer
|
||||
from pathlib import Path
|
||||
|
||||
def mountShare(networkPath, shareName = None, hiddenShare = False, username = None):
|
||||
"""
|
||||
Mount a given path of a samba share
|
||||
|
||||
:param networkPath: Network path of the share
|
||||
:type networkPath: str
|
||||
:param shareName: The name of the share (name of the folder the share is being mounted to)
|
||||
:type shareName: str
|
||||
:param hiddenShare: If the share sould be visible in Nautilus
|
||||
:type hiddenShare: bool
|
||||
:param username: The user in whoms context the share should be mounted
|
||||
:type username: str
|
||||
:return: Tuple: (success, mountpoint)
|
||||
:rtype: tuple
|
||||
"""
|
||||
networkPath = networkPath.replace("\\", "/")
|
||||
username = _getDefaultUsername(username)
|
||||
shareName = _getDefaultShareName(networkPath, shareName)
|
||||
|
||||
if user.isRoot():
|
||||
return _mountShare(username, networkPath, shareName, hiddenShare, True)
|
||||
else:
|
||||
mountpoint = _getShareMountpoint(networkPath, username, hiddenShare, shareName)
|
||||
# This will call _mountShare() directly with root privileges
|
||||
return _mountShareWithoutRoot(networkPath, shareName, hiddenShare), mountpoint
|
||||
|
||||
def getMountpointOfRemotePath(remoteFilePath, hiddenShare = False, username = None, autoMount = True):
|
||||
"""
|
||||
Get the local path of a remote samba share path.
|
||||
This function automatically checks if the shares is already mounted.
|
||||
It optionally automatically mounts the top path of the remote share:
|
||||
If the remote path is `//server/sysvol/linuxmuster.lan/Policies` it mounts `//server/sysvol`
|
||||
|
||||
:param remoteFilePath: Remote path
|
||||
:type remoteFilePath: str
|
||||
:param hiddenShare: If the share sould be visible in Nautilus
|
||||
:type hiddenShare: bool
|
||||
:param username: The user in whoms context the share should be mounted
|
||||
:type username: str
|
||||
:parama autoMount: If the share should be mouted automatically if it is not already mounted
|
||||
:type autoMount: bool
|
||||
:return: Tuple: (success, mountpoint)
|
||||
:rtype: tuple
|
||||
"""
|
||||
remoteFilePath = remoteFilePath.replace("\\", "/")
|
||||
username = _getDefaultUsername(username)
|
||||
|
||||
# get basepath fo remote file path
|
||||
# this turns //server/sysvol/linuxmuster.lan/Policies into //server/sysvol
|
||||
pattern = re.compile("(^\\/\\/[^\\/]+\\/[^\\/]+)")
|
||||
match = pattern.search(remoteFilePath)
|
||||
|
||||
if match is None:
|
||||
logging.error("Cannot get local file path of {} beacuse it is not a valid path!".format(remoteFilePath))
|
||||
return False, None
|
||||
|
||||
shareBasepath = match.group(0)
|
||||
|
||||
if autoMount:
|
||||
rc, mointpoint = mountShare(shareBasepath, hiddenShare=hiddenShare, username=username)
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
# calculate local path
|
||||
shareMountpoint = _getShareMountpoint(shareBasepath, username, hiddenShare, shareName=None)
|
||||
localFilePath = remoteFilePath.replace(shareBasepath, shareMountpoint)
|
||||
|
||||
return True, localFilePath
|
||||
|
||||
def unmountAllSharesOfUser(username):
|
||||
"""
|
||||
Unmount all shares of a given user and safely delete the mountpoints and the parent directory.
|
||||
|
||||
:param username: The username of the user
|
||||
:type username: str
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info("=== Trying to unmount all shares of user {0} ===".format(username))
|
||||
for basedir in [constants.shareMountBasepath, constants.hiddenShareMountBasepath]:
|
||||
shareMountBasedir = basedir.format(username)
|
||||
|
||||
try:
|
||||
mountedShares = os.listdir(shareMountBasedir)
|
||||
except FileNotFoundError:
|
||||
logging.info("Mount basedir {} does not exist -> nothing to unmount".format(shareMountBasedir))
|
||||
continue
|
||||
|
||||
for share in mountedShares:
|
||||
_unmountShare("{0}/{1}".format(shareMountBasedir, share))
|
||||
|
||||
if len(os.listdir(shareMountBasedir)) > 0:
|
||||
logging.warning("* Mount basedir {} is not empty so not removed!".format(shareMountBasedir))
|
||||
return False
|
||||
else:
|
||||
# Delete the directory
|
||||
logging.info("Deleting {0}...".format(shareMountBasedir))
|
||||
try:
|
||||
os.rmdir(shareMountBasedir)
|
||||
except Exception as e:
|
||||
logging.error("FAILED!")
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
logging.info("===> Finished unmounting all shares of user {0} ===".format(username))
|
||||
return True
|
||||
|
||||
def getLocalSysvolPath():
|
||||
"""
|
||||
Get the local mountpoint of the sysvol
|
||||
|
||||
:return: Full path of the mountpoint
|
||||
:rtype: str
|
||||
"""
|
||||
rc, networkConfig = config.network()
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
networkPath = f"//{networkConfig['serverHostname']}/sysvol"
|
||||
return getMountpointOfRemotePath(networkPath, True)
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
# useCruidOfExecutingUser:
|
||||
# defines if the ticket cache of the user executing the mount command should be used.
|
||||
# If set to False, the cache of the user with the given username will be used.
|
||||
# This parameter influences the `cruid` mount option.
|
||||
def _mountShare(username, networkPath, shareName, hiddenShare, useCruidOfExecutingUser=False):
|
||||
|
||||
mountpoint = _getShareMountpoint(networkPath, username, hiddenShare, shareName)
|
||||
|
||||
mountCommandOptions = f"file_mode=0700,dir_mode=0700,sec=krb5,nodev,nosuid,mfsymlinks,nobrl,vers=3.0,user={username}"
|
||||
rc, networkConfig = config.network()
|
||||
domain = None
|
||||
|
||||
if rc:
|
||||
domain = networkConfig["domain"]
|
||||
mountCommandOptions += f",domain={domain.upper()}"
|
||||
|
||||
try:
|
||||
pwdInfo = pwd.getpwnam(username)
|
||||
uid = pwdInfo.pw_uid
|
||||
gid = pwdInfo.pw_gid
|
||||
mountCommandOptions += f",gid={gid},uid={uid}"
|
||||
|
||||
if not useCruidOfExecutingUser:
|
||||
mountCommandOptions += f",cruid={uid}"
|
||||
|
||||
except KeyError:
|
||||
uid = -1
|
||||
gid = -1
|
||||
logging.warning("Uid could not be found! Continuing anyway!")
|
||||
|
||||
mountCommand = [shutil.which("mount.cifs"), "-o", mountCommandOptions, networkPath, mountpoint]
|
||||
|
||||
logging.debug(f"Trying to mount '{networkPath}' to '{mountpoint}'")
|
||||
logging.debug("* Creating directory...")
|
||||
|
||||
try:
|
||||
Path(mountpoint).mkdir(parents=True, exist_ok=False)
|
||||
except FileExistsError:
|
||||
# Test if a share is already mounted there
|
||||
if _directoryIsMountpoint(mountpoint):
|
||||
logging.debug("* The mountpoint is already mounted.")
|
||||
return True, mountpoint
|
||||
else:
|
||||
logging.warning("* The target directory already exists, proceeding anyway!")
|
||||
|
||||
logging.debug("* Executing '{}' ".format(" ".join(mountCommand)))
|
||||
logging.debug("* Trying to mount...")
|
||||
if not subprocess.call(mountCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
|
||||
logging.fatal(f"* Error mounting share {networkPath} to {mountpoint}!\n")
|
||||
return False, None
|
||||
|
||||
logging.debug("* Success!")
|
||||
|
||||
# hide the shares parent dir (/home/%user/media) in case it is not a hidden share
|
||||
if not hiddenShare:
|
||||
try:
|
||||
hiddenFilePath = f"{mountpoint}/../../.hidden"
|
||||
logging.debug(f"* hiding parent dir {hiddenFilePath}")
|
||||
hiddenFile = open(hiddenFilePath, "w+")
|
||||
hiddenFile.write(mountpoint.split("/")[-2])
|
||||
hiddenFile.close()
|
||||
except:
|
||||
logging.warning(f"Could not hide parent dir of share {mountpoint}")
|
||||
|
||||
return True, mountpoint
|
||||
|
||||
def _unmountShare(mountpoint):
|
||||
# check if mountpoint exists
|
||||
if (not os.path.exists(mountpoint)) or (not os.path.isdir(mountpoint)):
|
||||
logging.warning(f"* Could not unmount {mountpoint}, it does not exist.")
|
||||
|
||||
# Try to unmount share
|
||||
logging.info("* Trying to unmount {0}...".format(mountpoint))
|
||||
if not subprocess.call(["umount", mountpoint]) == 0:
|
||||
logging.warning("* Failed!")
|
||||
if _directoryIsMountpoint(mountpoint):
|
||||
logging.warning("* It is still mounted! Exiting!")
|
||||
# Do not delete in this case! We might delete userdata!
|
||||
return
|
||||
logging.info("* It is not mounted! Continuing!")
|
||||
|
||||
# check if the mountpoint is empty
|
||||
if len(os.listdir(mountpoint)) > 0:
|
||||
logging.warning("* mountpoint {} is not empty so not removed!".format(mountpoint))
|
||||
return
|
||||
|
||||
# Delete the directory
|
||||
logging.info("* Deleting {0}...".format(mountpoint))
|
||||
try:
|
||||
os.rmdir(mountpoint)
|
||||
except Exception as e:
|
||||
logging.error("* FAILED!")
|
||||
logging.exception(e)
|
||||
|
||||
def _getDefaultUsername(username=None):
|
||||
if username == None:
|
||||
if user.isRoot():
|
||||
username = computer.hostname().upper() + "$"
|
||||
else:
|
||||
username = user.username()
|
||||
return username
|
||||
|
||||
def _getDefaultShareName(networkPath, shareName=None):
|
||||
if shareName is None:
|
||||
shareName = networkPath.split("/")[-1]
|
||||
return shareName
|
||||
|
||||
def _mountShareWithoutRoot(networkPath, name, hidden):
|
||||
mountCommand = ["sudo", "/usr/share/linuxmuster-linuxclient7/scripts/sudoTools", "mount-share", "--path", networkPath, "--name", name]
|
||||
|
||||
if hidden:
|
||||
mountCommand.append("--hidden")
|
||||
|
||||
return subprocess.call(mountCommand) == 0
|
||||
|
||||
def _getShareMountpoint(networkPath, username, hidden, shareName = None):
|
||||
logging.debug(f"Calculating mountpoint of {networkPath}")
|
||||
|
||||
shareName = _getDefaultShareName(networkPath, shareName)
|
||||
|
||||
if hidden:
|
||||
return "{0}/{1}".format(constants.hiddenShareMountBasepath.format(username), shareName)
|
||||
else:
|
||||
return "{0}/{1}".format(constants.shareMountBasepath.format(username), shareName)
|
||||
|
||||
def _directoryIsMountpoint(dir):
|
||||
return subprocess.call(["mountpoint", "-q", dir]) == 0
|
128
roles/lmn_printer/files/linuxmusterLinuxclient7/templates.py
Normal file
128
roles/lmn_printer/files/linuxmusterLinuxclient7/templates.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
import os, codecs, sys, shutil, subprocess
|
||||
from pathlib import Path
|
||||
from linuxmusterLinuxclient7 import logging, constants, hooks, config
|
||||
|
||||
|
||||
def applyAll():
|
||||
"""
|
||||
Applies all templates from `/usr/share/linuxmuster-linuxclient7/templates`
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info('Applying all configuration templates:')
|
||||
|
||||
templateDir = constants.configFileTemplateDir
|
||||
for templateFile in os.listdir(templateDir):
|
||||
templatePath = templateDir + '/' + templateFile
|
||||
logging.info('* ' + templateFile + ' ...')
|
||||
if not _apply(templatePath):
|
||||
logging.error("Aborting!")
|
||||
return False
|
||||
|
||||
# reload sctemctl
|
||||
logging.info('Reloading systemctl ... ')
|
||||
if not subprocess.call(["systemctl", "daemon-reload"]) == 0:
|
||||
logging.error("Failed!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
|
||||
def _apply(templatePath):
|
||||
try:
|
||||
# read template file
|
||||
rc, fileData = _readTextfile(templatePath)
|
||||
|
||||
if not rc:
|
||||
logging.error('Failed!')
|
||||
return False
|
||||
|
||||
fileData = _resolveVariables(fileData)
|
||||
|
||||
# get target path
|
||||
firstLine = fileData.split('\n')[0]
|
||||
targetFilePath = firstLine.partition(' ')[2]
|
||||
|
||||
# remove first line (the target file path)
|
||||
fileData = fileData[fileData.find('\n'):]
|
||||
|
||||
# never ever overwrite sssd.conf, this will lead to issues!
|
||||
# sssd.conf is written by `realm join`!
|
||||
if targetFilePath in constants.notTemplatableFiles:
|
||||
logging.warning("Skipping forbidden file {}".format(targetFilePath))
|
||||
return True
|
||||
|
||||
# create target directory
|
||||
Path(Path(targetFilePath).parent.absolute()).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# remove comment lines beginning with # from .xml files
|
||||
if targetFilePath.endswith('.xml'):
|
||||
fileData = _stripComment(fileData)
|
||||
|
||||
# write config file
|
||||
logging.debug("-> to {}".format(targetFilePath))
|
||||
with open(targetFilePath, 'w') as targetFile:
|
||||
targetFile.write(fileData)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.error('Failed!')
|
||||
logging.exception(e)
|
||||
return False
|
||||
|
||||
def _resolveVariables(fileData):
|
||||
# replace placeholders with values
|
||||
rc, networkConfig = config.network()
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
# network
|
||||
fileData = fileData.replace('@@serverHostname@@', networkConfig["serverHostname"])
|
||||
fileData = fileData.replace('@@domain@@', networkConfig["domain"])
|
||||
fileData = fileData.replace('@@realm@@', networkConfig["realm"])
|
||||
|
||||
# constants
|
||||
fileData = fileData.replace('@@userTemplateDir@@', constants.userTemplateDir)
|
||||
fileData = fileData.replace('@@hiddenShareMountBasepath@@', constants.hiddenShareMountBasepath.format("%(USER)"))
|
||||
|
||||
# hooks
|
||||
fileData = fileData.replace('@@hookScriptBoot@@', hooks.getLocalHookScript(hooks.Type.Boot))
|
||||
fileData = fileData.replace('@@hookScriptShutdown@@', hooks.getLocalHookScript(hooks.Type.Shutdown))
|
||||
fileData = fileData.replace('@@hookScriptLoginLogoutAsRoot@@', hooks.getLocalHookScript(hooks.Type.LoginLogoutAsRoot))
|
||||
fileData = fileData.replace('@@hookScriptSessionStarted@@', hooks.getLocalHookScript(hooks.Type.SessionStarted))
|
||||
|
||||
return fileData
|
||||
|
||||
# read textfile in variable
|
||||
def _readTextfile(filePath):
|
||||
if not os.path.isfile(filePath):
|
||||
return False, None
|
||||
try:
|
||||
infile = codecs.open(filePath ,'r', encoding='utf-8', errors='ignore')
|
||||
content = infile.read()
|
||||
infile.close()
|
||||
return True, content
|
||||
except Exception as e:
|
||||
logging.info('Cannot read ' + filePath + '!')
|
||||
logging.exception(e)
|
||||
return False, None
|
||||
|
||||
# remove lines beginning with #
|
||||
def _stripComment(fileData):
|
||||
filedata_stripped = ''
|
||||
for line in fileData.split('\n'):
|
||||
if line[:1] == '#':
|
||||
continue
|
||||
else:
|
||||
if filedata_stripped == '':
|
||||
filedata_stripped = line
|
||||
else:
|
||||
filedata_stripped = filedata_stripped + '\n' + line
|
||||
return filedata_stripped
|
158
roles/lmn_printer/files/linuxmusterLinuxclient7/user.py
Normal file
158
roles/lmn_printer/files/linuxmusterLinuxclient7/user.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
import ldap, ldap.sasl, sys, getpass, subprocess, pwd, os, os.path
|
||||
from pathlib import Path
|
||||
from linuxmusterLinuxclient7 import logging, constants, config, user, ldapHelper, shares, fileHelper, computer, localUserHelper
|
||||
|
||||
def readAttributes():
|
||||
"""
|
||||
Reads all attributes of the current user from ldap
|
||||
|
||||
:return: Tuple (success, dict of user attributes)
|
||||
:rtype: tuple
|
||||
"""
|
||||
if not user.isInAD():
|
||||
return False, None
|
||||
|
||||
return ldapHelper.searchOne(f"(sAMAccountName={user.username()})")
|
||||
|
||||
def school():
|
||||
"""
|
||||
Gets the school of the current user from the AD
|
||||
|
||||
:return: The short name of the school
|
||||
:rtype: str
|
||||
"""
|
||||
rc, userdata = readAttributes()
|
||||
|
||||
if not rc:
|
||||
return False, None
|
||||
|
||||
return True, userdata["sophomorixSchoolname"]
|
||||
|
||||
def username():
|
||||
"""
|
||||
Returns the user of the current user
|
||||
|
||||
:return: The username of the current user
|
||||
:rtype: str
|
||||
"""
|
||||
return getpass.getuser().lower()
|
||||
|
||||
def isUserInAD(user):
|
||||
"""
|
||||
Checks if a given user is an AD user.
|
||||
|
||||
:param user: The username of the user to check
|
||||
:type user: str
|
||||
:return: True if the user is in the AD, False if it is a local user
|
||||
:rtype: bool
|
||||
"""
|
||||
if not computer.isInAD():
|
||||
return False
|
||||
|
||||
rc, groups = localUserHelper.getGroupsOfLocalUser(user)
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
return "domain users" in groups
|
||||
|
||||
def isInAD():
|
||||
"""Checks if the current user is an AD user.
|
||||
|
||||
:return: True if the user is in the AD, False if it is a local user
|
||||
:rtype: bool
|
||||
"""
|
||||
return isUserInAD(username())
|
||||
|
||||
def isRoot():
|
||||
"""
|
||||
Checks if the current user is root
|
||||
|
||||
:return: True if the current user is root, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return os.geteuid() == 0
|
||||
|
||||
def isInGroup(groupName):
|
||||
"""
|
||||
Checks if the current user is part of a given group
|
||||
|
||||
:param groupName: The name of the group
|
||||
:type groupName: str
|
||||
:return: True if the user is part of the group, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
rc, groups = localUserHelper.getGroupsOfLocalUser(username())
|
||||
if not rc:
|
||||
return False
|
||||
|
||||
return groupName in groups
|
||||
|
||||
def cleanTemplateUserGtkBookmarks():
|
||||
"""Remove gtk bookmarks of the template user from the current users `~/.config/gtk-3.0/bookmarks` file.
|
||||
"""
|
||||
logging.info("Cleaning {} gtk bookmarks".format(constants.templateUser))
|
||||
gtkBookmarksFile = "/home/{0}/.config/gtk-3.0/bookmarks".format(user.username())
|
||||
|
||||
if not os.path.isfile(gtkBookmarksFile):
|
||||
logging.warning("Gtk bookmarks file not found, skipping!")
|
||||
return
|
||||
|
||||
fileHelper.removeLinesInFileContainingString(gtkBookmarksFile, constants.templateUser)
|
||||
|
||||
def getHomeShareMountpoint():
|
||||
"""
|
||||
Returns the mountpoint of the users serverhome.
|
||||
|
||||
:return: The monutpoint of the users serverhome
|
||||
:rtype: str
|
||||
"""
|
||||
rc, homeShareName = _getHomeShareName()
|
||||
|
||||
if rc:
|
||||
basePath = constants.shareMountBasepath.format(username())
|
||||
return True, f"{basePath}/{homeShareName}"
|
||||
|
||||
return False, None
|
||||
|
||||
def mountHomeShare():
|
||||
"""
|
||||
Mounts the serverhome of the current user
|
||||
|
||||
:return: True on success, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
rc1, userAttributes = readAttributes()
|
||||
rc2, shareName = _getHomeShareName(userAttributes)
|
||||
if rc1 and rc2:
|
||||
try:
|
||||
homeShareServerPath = userAttributes["homeDirectory"]
|
||||
res = shares.mountShare(homeShareServerPath, shareName=shareName, hiddenShare=False, username=username())
|
||||
return res
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Could not mount home dir of user")
|
||||
logging.exception(e)
|
||||
|
||||
return False, None
|
||||
|
||||
# --------------------
|
||||
# - Helper functions -
|
||||
# --------------------
|
||||
|
||||
def _getHomeShareName(userAttributes=None):
|
||||
if userAttributes is None:
|
||||
rc, userAttributes = readAttributes()
|
||||
else:
|
||||
rc = True
|
||||
|
||||
if rc:
|
||||
try:
|
||||
usernameString = username()
|
||||
shareName = f"{usernameString} ({userAttributes['homeDrive']})"
|
||||
return True, shareName
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Could not mount home dir of user")
|
||||
logging.exception(e)
|
||||
|
||||
return False, None
|
1
roles/lmn_printer/files/lmn-printer.sh
Normal file
1
roles/lmn_printer/files/lmn-printer.sh
Normal file
|
@ -0,0 +1 @@
|
|||
[[ "${UID}" -gt 10000 ]] && /usr/local/bin/onLogin
|
33
roles/lmn_printer/files/onLogin
Normal file
33
roles/lmn_printer/files/onLogin
Normal file
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# DO NOT MODIFY THIS SCRIPT!
|
||||
# For custom scripts use the hookdir /etc/linuxmuster-linuxclient7/onLogin.d
|
||||
|
||||
# This schript is called in user context when a user logs in
|
||||
try:
|
||||
import os, sys
|
||||
#import traceback
|
||||
from linuxmusterLinuxclient7 import logging, hooks, shares, user, constants, gpo, computer, environment
|
||||
|
||||
logging.info("====== onLogin started ======")
|
||||
|
||||
# mount sysvol
|
||||
rc, sysvolPath = shares.getLocalSysvolPath()
|
||||
if rc:
|
||||
environment.export(f"SYSVOL={sysvolPath}")
|
||||
|
||||
# process GPOs
|
||||
gpo.processAllPolicies()
|
||||
|
||||
logging.info("======> onLogin end ======")
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
#traceback.print_exc()
|
||||
logging.exception(e)
|
||||
except:
|
||||
print("A fatal error occured!")
|
||||
|
||||
# We need to catch all exceptions and return 0 in any case!
|
||||
# If we do not return 0, login will FAIL FOR EVERYONE!
|
||||
sys.exit(0)
|
38
roles/lmn_printer/files/onLogout
Normal file
38
roles/lmn_printer/files/onLogout
Normal file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# DO NOT MODIFY THIS SCRIPT!
|
||||
# For custom scripts use the hookdirs
|
||||
# /etc/linuxmuster-linuxclient7/onLoginAsRoot.d
|
||||
# and /etc/linuxmuster-linuxclient7/onLogoutAsRoot.d
|
||||
|
||||
# This schript is called in root context when a user logs in or out
|
||||
try:
|
||||
import os, sys
|
||||
from linuxmusterLinuxclient7 import logging, hooks, constants, user, shares, printers, computer, realm
|
||||
|
||||
pamType = os.getenv("PAM_TYPE")
|
||||
pamUser = os.getenv("PAM_USER")
|
||||
#PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER and PAM_TYPE
|
||||
logging.info("====== onLoginLogoutAsRoot started with PAM_TYPE={0} PAM_RHOST={1} PAM_RUSER={2} PAM_SERVICE={3} PAM_TTY={4} PAM_USER={5} ======".format(pamType, os.getenv("PAM_RHOST"), os.getenv("PAM_RUSER"), os.getenv("PAM_SERVICE"), os.getenv("PAM_TTY"), pamUser))
|
||||
|
||||
# check if whe should execute
|
||||
if not hooks.shouldHooksBeExecuted(pamUser):
|
||||
logging.info("======> onLoginLogoutAsRoot end ====")
|
||||
sys.exit(0)
|
||||
|
||||
elif pamType == "close_session":
|
||||
# cleanup
|
||||
printers.uninstallAllPrintersOfUser(pamUser)
|
||||
|
||||
logging.info("======> onLoginLogoutAsRoot end ======")
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
logging.exception(e)
|
||||
except:
|
||||
print("A fatal error occured!")
|
||||
|
||||
# We need to catch all exceptions and return 0 in any case!
|
||||
# If we do not return 0, login will FAIL FOR EVERYONE!
|
||||
sys.exit(0)
|
||||
|
6
roles/lmn_printer/files/rmlpr.service
Normal file
6
roles/lmn_printer/files/rmlpr.service
Normal file
|
@ -0,0 +1,6 @@
|
|||
[Unit]
|
||||
Description=Remove all printers
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=sh -c 'lpstat -p && for printer in $(lpstat -p | cut -f 2 -d " "); do lpadmin -x $printer; done || echo no printer found.'
|
8
roles/lmn_printer/files/rmlpr.timer
Normal file
8
roles/lmn_printer/files/rmlpr.timer
Normal file
|
@ -0,0 +1,8 @@
|
|||
[Unit]
|
||||
Description=Remove all printers on boot
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
50
roles/lmn_printer/files/scripts/sudoTools
Executable file
50
roles/lmn_printer/files/scripts/sudoTools
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/python3
|
||||
#
|
||||
# Script to do some things that require root permissions as a normal user
|
||||
# Currently used for:
|
||||
# - mounting shares
|
||||
# - installing printers
|
||||
#
|
||||
|
||||
import os, sys, argparse
|
||||
from linuxmusterLinuxclient7 import shares, printers, constants
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Script to do some things that require root permissions as a normal user")
|
||||
|
||||
subparsers = parser.add_subparsers(title='Tasks', metavar="<task>", help="The task to execute", dest="task", required=True)
|
||||
|
||||
subparserCache = subparsers.add_parser('install-printer', help='install a printer')
|
||||
requiredGroupCache = subparserCache.add_argument_group('required arguments')
|
||||
requiredGroupCache.add_argument("--path", help="The network path of the printer", required=True)
|
||||
requiredGroupCache.add_argument("--name", help="The name of the printer", required=True)
|
||||
|
||||
subparserCache = subparsers.add_parser('mount-share', help='mount a network share')
|
||||
subparserCache.add_argument("--hidden", help="Hide this share", action='store_true')
|
||||
requiredGroupCache = subparserCache.add_argument_group('required arguments')
|
||||
requiredGroupCache.add_argument("--path", help="The network path of the share", required=True)
|
||||
requiredGroupCache.add_argument("--name", help="The name of the share", required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
task = args.task
|
||||
|
||||
if not os.geteuid() == 0:
|
||||
print("This script has to be run using sudo!")
|
||||
exit(1)
|
||||
|
||||
username = os.getenv("SUDO_USER")
|
||||
|
||||
if task == "install-printer":
|
||||
if printers.installPrinter(args.path, name=args.name, username=username):
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
pass
|
||||
elif task == "mount-share":
|
||||
if shares._mountShare(username, args.path, args.name, args.hidden, False):
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
exit(0)
|
102
roles/lmn_printer/tasks/main.yml
Normal file
102
roles/lmn_printer/tasks/main.yml
Normal file
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
- name: Install cups and python libs
|
||||
apt:
|
||||
name:
|
||||
- cups
|
||||
- python3-ldap
|
||||
state: latest
|
||||
|
||||
- name: Disable cups printer browsing
|
||||
lineinfile:
|
||||
dest: /etc/cups/cupsd.conf
|
||||
regexp: '^(Browsing ).*'
|
||||
line: '\1No'
|
||||
backrefs: yes
|
||||
|
||||
- name: Disable cups-browsed
|
||||
ansible.builtin.systemd:
|
||||
name: cups-browsed.service
|
||||
state: stopped
|
||||
enabled: no
|
||||
|
||||
- name: Configure pam_mount sysvol mount
|
||||
blockinfile:
|
||||
dest: /etc/security/pam_mount.conf.xml
|
||||
marker: "<!-- {mark} ANSIBLE MANAGED BLOCK (SysVol) -->"
|
||||
block: |
|
||||
<volume
|
||||
fstype="cifs"
|
||||
server="{{ smb_server }}"
|
||||
path="sysvol/"
|
||||
mountpoint="/srv/samba/%(USER)/sysvol"
|
||||
options="sec=krb5i,cruid=%(USERUID),user=%(USER),gid=1010,file_mode=0770,dir_mode=0770,mfsymlinks"
|
||||
><not><or><user>root</user><user>ansible</user><user>Debian-gdm</user><user>sddm</user><user>virti</user></or></not></volume>
|
||||
insertafter: "<!-- Volume definitions -->"
|
||||
|
||||
- name: Create /etc/linuxmuster-linuxclient7 Directory
|
||||
file:
|
||||
path: /etc/linuxmuster-linuxclient7
|
||||
state: directory
|
||||
mode: 0755
|
||||
|
||||
- name: install linuxmuster-linuxclient network.conf
|
||||
template:
|
||||
src: network.conf.j2
|
||||
dest: /etc/linuxmuster-linuxclient7/network.conf
|
||||
mode: 0644
|
||||
|
||||
- name: install linuxmuster-linuxclient python libs
|
||||
copy :
|
||||
src: linuxmusterLinuxclient7
|
||||
dest: /usr/lib/python3/dist-packages
|
||||
|
||||
- name: Create /usr/share/linuxmuster-linuxclient7/scripts Directory
|
||||
file:
|
||||
path: /usr/share/linuxmuster-linuxclient7/scripts
|
||||
state: directory
|
||||
mode: 0755
|
||||
|
||||
- name: install linuxmuster-scripts
|
||||
copy:
|
||||
src: scripts/sudoTools
|
||||
dest: /usr/share/linuxmuster-linuxclient7/scripts/
|
||||
mode: 0755
|
||||
|
||||
- name: install lmn-sudotools
|
||||
copy:
|
||||
src: 90-lmn-sudotools
|
||||
dest: /etc/sudoers.d/
|
||||
mode: 0660
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: install onLogin script
|
||||
copy :
|
||||
src: onLogin
|
||||
dest: /usr/local/bin/
|
||||
mode: 0755
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: install lmn-printer.sh in /etc/profile.d/
|
||||
copy:
|
||||
src: lmn-printer.sh
|
||||
dest: /etc/profile.d/
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Provide service and timer for remove all printers on boot
|
||||
copy:
|
||||
src: "{{ item }}"
|
||||
dest: "/etc/systemd/system/{{ item }}"
|
||||
mode: 0644
|
||||
with_items:
|
||||
- rmlpr.service
|
||||
- rmlpr.timer
|
||||
|
||||
- name: enable rmlpr.timer
|
||||
systemd:
|
||||
name: rmlpr.timer
|
||||
enabled: true
|
||||
|
5
roles/lmn_printer/templates/network.conf.j2
Normal file
5
roles/lmn_printer/templates/network.conf.j2
Normal file
|
@ -0,0 +1,5 @@
|
|||
[network]
|
||||
serverHostname = server
|
||||
domain = pn.steinbeis.schule
|
||||
realm = PN.STEINBEIS.SCHULE
|
||||
version = 1
|
3
roles/lmn_sssd/handlers/main.yml
Normal file
3
roles/lmn_sssd/handlers/main.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- name: restart sssd
|
||||
service: name=sssd state=restarted enabled=yes
|
||||
listen: "restart sssd"
|
25
roles/lmn_sssd/tasks/main.yml
Normal file
25
roles/lmn_sssd/tasks/main.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
- name: install needed packages
|
||||
apt:
|
||||
name:
|
||||
- sssd-ad
|
||||
- sssd-tools
|
||||
- adcli
|
||||
state: latest
|
||||
|
||||
- name: provide identities from directory
|
||||
template:
|
||||
src: sssd.conf.j2
|
||||
dest: /etc/sssd/sssd.conf
|
||||
mode: 0600
|
||||
notify: restart sssd
|
||||
|
||||
## Either one of the variables is defined:
|
||||
- name: join the domain
|
||||
shell:
|
||||
cmd: >
|
||||
echo "{{ ansible_cmdline.adpw | default('') + adpw.user_input | default('') }}" |
|
||||
adcli join --stdin-password -U global-admin {{ domain | upper }}
|
||||
when: >
|
||||
ansible_cmdline.adpw | default('') | length > 0 or
|
||||
adpw.user_input | default('') | length > 0
|
17
roles/lmn_sssd/templates/sssd.conf.j2
Normal file
17
roles/lmn_sssd/templates/sssd.conf.j2
Normal file
|
@ -0,0 +1,17 @@
|
|||
[sssd]
|
||||
domains = {{ domain }}
|
||||
config_file_version = 2
|
||||
|
||||
[domain/{{ domain }}]
|
||||
krb5_realm = {{ domain | upper }}
|
||||
ad_domain = {{ domain }}
|
||||
id_provider = ad
|
||||
access_provider = ad
|
||||
use_fully_qualified_names = False
|
||||
cache_credentials = True
|
||||
krb5_store_password_if_offline = True
|
||||
default_shell = /usr/bin/bash
|
||||
# default: # ldap_id_mapping = True
|
||||
ad_gpo_access_control = disabled
|
||||
ad_gpo_ignore_unreadable = True
|
||||
ad_maximum_machine_account_password_age = 0
|
Loading…
Add table
Add a link
Reference in a new issue