Direktes Drucken mit Linux-Clients
Im Prinzip ist die Unterstützung von Linux-Clients bei IServ schon sehr weit fortgeschritten, ein für viele wichtiges Feature fehlt aber noch: Direktes Drucken - bedeutet, dass man die Weboberfläche des Druckmoduls umgeht und stattdessen über den Druckserver von IServ die Aufträge an den Drucker schickt.
Um direktes Drucken mit Windows-Clients nutzen zu können, muss soweit nur in iservcfg ein bis zwei Einstellungen geändert werden und anschließend werden die Drucker auf den Windows-Rechnern automatisch eingebunden, sogar der Standarddrucker wird dabei automatisch (alphabetisch) bestimmt. Unter Linux war die Nutzung des Features bislang nicht möglich, da der CUPS-Client (kommt unter anderem in Ubuntu für Ubuntu zum Einsatz) sich nicht mit den Druckerfreigaben, die für Windows im Rahmen des direkten Drucken angelegt werden, versteht und diese so auch nicht verwendet werden können.
Daher musste an dieser Stelle ein neues Konzept erdacht werden, zum einem deswegen und zum anderen, weil Linux-Betriebssysteme in der Regel keine Unterstützung für Anmeldeskripte haben, die zentral ausgeführt werden und über die man die Drucker konfigurieren könnte.
Inhaltsverzeichnis
Auf Portalserver-Seite
Das direkte Drucken mit Linux ist nun mittlerweile von mir (Felix Jacobi) inoffiziell (bedeutet, das ist kein offizielles IServ-Feature und gehört auch nicht zum Umfang des Portalservers von IServ), zunächst für den schulinternen Gebrauch umgesetzt worden, ich veröffentliche es jetzt an dieser Stelle, da es vielen anderen bestimmt weiterhilft. In Verbindung mit Ubuntu und anderen Debian-artigen Distributionen muss nichts manuell eingerichtet werden, hier gibt es alles fertig gebacken.
Auf die möglichst einfache Verwendung wurde prinzipiell geachtet, die einzelnen Linux-Clients müssen nicht angefasst werden, zum Beispiel um dort ein Programm zu installieren. Dies wird alles zentral vom IServ mit einem Zusatzmodul gesteuert, dass die nötige Software auf die Linux-Clients, die Mitglied der Domäne vollautomatisch installiert, sobald sie sich im Netzwerk melden.[1]
Um dies zu ermöglichen, wird das Modul stsbl-iserv-linux-support auf dem Portalserver installiert, welches über das Repository der Stadtteilschule Blankenese zu beziehen ist, sobald dieses auf dem Portalserver eingerichtet ist.[2] Das Modul erledigt folgendes, um das direkte Drucken von Linux-Clients zu ermöglichen:
- Es wird ein serverseitiges Skript hinterlegt, das versucht, auf Domänenrechnern[3], sobald sich ein Benutzer anmeldet, das Client-Programm für das direkte Drucken zu installieren. Das ist in dem Fall dann der Ersatz für das Login-Skript, dass dies bei Windows ausführt.
- Der Druckerserver (CUPS) des IServs wird im Falle, dass man die Einstellung "Direktes Drucken" in iservcfg aktiviert hat, so konfiguriert, dass er alle in die Druckerverwaltung eingetragen Drucker per ipp (Internet Printing Protocol innerhalb der LANs freigegeben werden. Das ist notwendig, da ein Linux-Client wie oben erwähnt, nicht die Möglichkeit haben, die Samba-Freigaben zu verwenden, die Windows verwendet. Das bringt kein Sicherheitsrisiko mit sich, die Stellen von CUPS, die administrativen Zugriff ermöglichen, sind weiterhin nur über die Verwaltungsoberfläche des Druckmoduls zugänglich und damit gesichert.
- Wichtig ist zu beachten, dass die automatische Installation der Clientsoftware nur mit Debian-Abkömmlingen funktioniert (dazu gehören unter anderem Ubuntu und auch Linux Mint sowie natürlich Debian selber), hat man eine andere Linux-Distribution im Einsatz, muss man den Client manuell aufspielen.
Auf Client-Seite
Hier bleibt simpel festzuhalten, dass nach der Installation des Clientprogramms durch den IServ (stsbl-iserv-client-print) bei jedem Benutzerlogin eine Verbindung mit der netlogon-Freigabe des IServs hergestellt wird, von dort die JSON-Dateien Druckerkonfiguration (default.json,default.local.json, room/Raumname.json, [...]) geladen, anschließend aus ihnen die Druckerinformationen zusammengetragen werden (eingebundene Drucker, Standarddrucker) und im gleichen Zug Drucker, die dem Rechner nicht (mehr) zugewiesen sind, entfernt werden.
Dokumentation des Clientprogramms
Nachfolgend sich die Dokumentation des Clientprogramms, um es zum Beispiel auch auf nicht unterstützten Linux-Betriebssystemen verwenden zu können.
Netlogon-Freigabe einbinden
Zunächst muss dafür gesorgt werden, dass die netlogon-Freigabe bei der Benutzeranmeldung automatisch eingebunden wird, sie muss zum Zeitpunkt der Anmeldung vorhanden sein, da der Druckclient von dort die Druckerkonfiguration ließt. Dazu öffnet man die Datei /etc/security/pam_mount.conf.xml mit einem Editor seiner Wahl, zum Beispiel:
vim /etc/security/pam_mount.conf.xml
Dort muss hinter dem <!-- Volume definitions --> angefügt werden:
<!-- IServ Print BEGIN --> <volume uid="10000-100000" fstype="cifs" server="10.0.0.1" path="netlogon" mountpoint="/var/lib/iserv/netlogon/%(USER)" options="iocharset=utf8,dir_mode=0700" /> <!-- IServ Print END -->
Die 10.0.0.1 ist an dieser Stelle eine Beispiel-IP, sie muss natürlich durch die eigene Portalserver-IP ersetzt werden. |
Dies weist das System an, beim Login eines Domänenbenutzers automatisch die netlogon-Freigabe des IServ beim Anmelden mit dessen Zugangsdaten einbindet.
Clientprogramm
Als nächstes muss das Skript, was die Drucker einrichtet, angelegt werden, dazu benutzt man wieder seinen Lieblingseditor (das Verzeichnis /usr/lib/iserv/ kann eventuell fehlen und muss dann manuell angelegt werden):
vim /usr/lib/iserv/setup_printer
Das Skript sieht wie folgt aus (es wird einfach in den Editor eingefügt):
#!/usr/bin/perl -CSDAL use strict; use warnings; use utf8; use JSON qw(decode_json); use Net::Address::IP::Local; use Net::CUPS; if (@ARGV < 1) { print STDERR "Usage: iserv_setup_printer [act]"; exit 1; } my ($act) = @ARGV; my ($server, $room, %printers, %exclude, %server_printers, $default_printer); # prevent illegal account names die "Invalid account $act!" if not $act =~ /^[a-z][a-z0-9._-]*$/; # initialize CUPS client my $cups = Net::CUPS->new(); sub info($) { my ($info) = @_; print "[INFO] $info\n"; } sub warning(@) { my ($warning) = @_; print "[WARNING] $warning\n"; } sub decode_json_file($) { my $fn = shift; my $fp; local $SIG{__WARN__} = sub { warn(((caller 1)[3]), ": ", $fn, ": ", $_[0]) }; open $fp, "<", $fn or warn "Cannot read from $fn: $!"; my @res = <$fp>; close $fp; my $content = join "", @res; return decode_json $content; } sub add_printers(@) { foreach my $p (@_) { info "Ignoring printer (it is locally available as IServ)." and next if $p eq "printer"; info "Adding printer $p."; $printers{$p} = 1; } } sub add_excludes(@) { foreach my $p (@_) { info "Ignoring printer (it is locally available as IServ)." and next if $p eq "printer"; info "Adding printer $p to exclude."; $exclude{$p} = 1; } } sub set_default_printer($) { my ($default) = @_; my $old = $default_printer; my $output; # workaround for IServ printer if ($default eq "printer") { $default_printer = "IServ"; $output = "IServ (printer) is now the default printer"; if (defined $old) { $output .= " (replacing $old)."; } else { $output .= "."; } info $output; return; } if (not defined $printers{$default}) { undef $default_printer; warning "Ignoring $default as default printer: Currently not in printer list!" and return; } if (defined $exclude{$default}) { undef $default_printer; warning "Ignoring $default as default printer: Printer is in exclude list!" and return; } $default_printer = $default; $output = "$default is now the default printer"; if (defined $old) { $output .= " (replacing $old)."; } else { $output .= "."; } info $output; } sub merge_json_file($) { my ($file) = @_; my $hash = decode_json_file $file; if (defined $hash->{'printers'}) { # result isn't always an array :/ if (ref $hash->{'printers'} eq "ARRAY") { add_printers @{$hash->{'printers'}}; } else { my @single_printer = ($hash->{'printers'}); add_printers @single_printer; } } if (defined $hash->{'def'}) { set_default_printer $hash->{'def'}; } if (defined $hash->{'exclude'}) { # result isn't always an array :/ if (ref $hash->{'exclude'} eq "ARRAY") { add_excludes @{$hash->{'exclude'}}; } else { my @single_exclude = ($hash->{'exclude'}); add_printers @single_exclude; } } } sub parse_server_printers { return if not -f "/var/lib/iserv/netlogon/$act/Printer Configuration/printers.json"; info "Parsing printers.json ..."; my $server_printer_info = decode_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/printers.json"; if (defined $server_printer_info->{'printers'}) { info "List of all server-side printers: $server_printer_info->{'printers'}"; # result isn't always an array :/ if (ref $server_printer_info->{'printers'} eq "ARRAY") { foreach my $server_printer (@{$server_printer_info->{'printers'}}) { $server_printers{$server_printer} = 1; } } else { $server_printers{$server_printer_info->{'printers'}} = 1; } } } sub parse_default { # read default json if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/default.json") { info "Parsing default.json ..."; my $json = "/var/lib/iserv/netlogon/$act/Printer Configuration/default.json"; my $default = decode_json_file $json; # determine print server if (defined $default->{'server'}) { $server = $default->{'server'}; info "Printer server is $server."; } else { # hardcoded $server = "iserv"; } merge_json_file $json; } else { die "/var/lib/iserv/netlogon/$act/Printer Configuration/default.json is missing!"; } # read local default json if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/default.local.json") { info "Parsing default.local.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/default.local.json"; } } sub parse_room { return if not -f "/var/lib/iserv/netlogon/$act/Printer Configuration/rooms.json"; # get the room where we are my $room_name; my $ip = Net::Address::IP::Local->public_ipv4; info "Current IP is $ip."; my $room_info = decode_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/rooms.json"; if (defined $room_info->{"i$ip"}{'room'}) { $room_name = $room_info->{"i$ip"}{'room'}; info "Current room is $room_name."; } else { $room_name = ""; warning "Couldn't determine room name!"; } if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/room/$room_name.json") { info "Parsing room/$room_name.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/room/$room_name.json"; } if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/room.local/$room_name.json") { info "Parsing room.local/$room_name.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/room.local/$room_name.json"; } if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/host/$ip.json") { info "Parsing host/$ip.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/host/$ip.json"; } if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/host.local/$ip.json") { info "Parsing host.local/$ip.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/host.local/$ip.json"; } } sub parse_group { return if not -f "/var/lib/iserv/netlogon/$act/Printer Configuration/groups.json"; # get groups of current user my $group_info = decode_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/groups.json"; if (defined $group_info->{"$act"}->{'groups'}) { # result isn't always an array :/ if (ref $group_info->{"$act"}->{'groups'} eq "ARRAY") { my @groups = @{$group_info->{"$act"}->{'groups'}}; info "User $act has groups @groups."; foreach my $group (@groups) { next if not -f "/var/lib/iserv/netlogon/$act/Printer Configuration/group/$group.json"; info "Parsing group/$group.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/group/$group.json"; } foreach my $group (@groups) { next if not -f "/var/lib/iserv/netlogon/$act/Printer Configuration/group.local/$group.json"; info "Parsing group.local/$group.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/group.local/$group.json"; } } else { my $group = $group_info->{"$act"}->{'groups'}; info "user $act has group $group"; if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/group/$group.json") { info "Parsing group/$group.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/group/$group.json"; } if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/group.local/$group.json") { info "Parsing group.local/$group.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/group.local/$group.json"; } } } } sub parse_user { if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/user/$act.json") { info "Parsing user/$act.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/user/$act.json"; } if (-f "/var/lib/iserv/netlogon/$act/Printer Configuration/user.local/$act.json") { info "Parsing user.local/$act.json ..."; merge_json_file "/var/lib/iserv/netlogon/$act/Printer Configuration/user.local/$act.json"; } } sub finalize { # remove excluded printers foreach my $printer (keys %printers) { (info "Deleting printer $printer, because it is in exclude list." and delete $printers{$printer}) if defined $exclude{$printer} } if (defined $default_printer) { # unset default printer it is not still there if (not $default_printer eq "IServ") { (warning "Unsetting default printer, because $default_printer seems to be gone!" and undef $default_printer) if not defined $printers{$default_printer} } } info "Final result:"; my @final_printers = keys %printers; info "Printers: @final_printers"; my @final_exclude = keys %exclude; info "Exclude: @final_exclude"; if (defined $default_printer) { info "Default printer: $default_printer"; } } sub configure_cups { info "Re-configuring CUPS printer."; my @cups_printers = $cups->getDestinations(); my $current_printers; foreach my $cups_printer (@cups_printers) { $current_printers .= $cups_printer->getName()." "; } info "Printers in CUPS: $current_printers"; my $printer; my $printer_location; open(IN, 'LC_ALL=C lpstat -l -p|'); while(<IN>) { $printer = { 'name'=>'', 'location'=>'' } if /^\w/; if (/^printer\s(.+?) /) { next if $1 eq 'iserv'; $printer->{name} = $1; } elsif (/^\s+Location:\s(.*)$/) { $printer->{location} = $1; } next unless $printer->{name}; $printer_location->{$printer->{name}} = $printer->{location}; } close IN; # delete old printers foreach my $cups_printer (@cups_printers) { # don't delete local iserv printer next if $cups_printer->getName() eq "IServ"; # ignore printers without location next if not defined $printer_location->{$cups_printer->getName()}; # if location is not 'iserv', ignore printer next if not $printer_location->{$cups_printer->getName()} eq "iserv"; # don't delete printers which do not have an equivalence on server side # TODO: not safe, could create printer "zombies" on clients, better add # printer with specific mark and select them by mark. #next if not defined $server_printers{$cups_printer->getName()}; info "Deleting printer ".$cups_printer->getName()." from CUPS."; $cups->deleteDestination($cups_printer->getName()); } # add new printers foreach my $printer (keys %printers) { info "Adding printer $printer to CUPS."; # Net::CUPS sucks in adding printers, so we have to use low-level lpadmin :/ #$cups->addDestination($printer, "IServ", $printer, "Generic PostScript Printer", "ipp://$server/printers/$printer"); system "lpadmin", "-p", $printer, "-D", "$printer @ $server", "-P", "/var/lib/iserv/netlogon/$act/ppd/generic-postscript-printer.ppd", "-v", "ipp://$server/printers/$printer", "-L", "$server"; # start printer system "lpadmin", "-p", $printer, "-E"; # set default printer if (defined $default_printer) { info "Setting $default_printer as server default."; system "lpadmin", "-d", $default_printer; } } } parse_server_printers; parse_default; parse_room; parse_group; parse_user; finalize; configure_cups;
Das Skript benötigt die Perl-Bibliotheken libnet-address-ip-local-perl, libjson-perl und libnet-cups-perl, diese muss genau jetzt über den Paketmanager der eigenen Distribution installieren. |
Nach dem Speichern, muss das Skript noch ausführbar gemacht werden, das erledigt man so:
chmod +x /usr/lib/iserv/setup_printer
Sudo konfigurieren
Das Skript zum Einrichten der Drucker braucht selbstverständlich Admin-Rechte, die normale Benutzer natürlich nicht haben, daher muss man ihnen das Ausführen des Skriptes mit Adminrechten explizit erlauben (sudo), dafür legt man die Datei /etc/sudoers.d/iserv-print mit folgenden Inhalt an:
ALL ALL = (root) NOPASSWD: /usr/lib/iserv/setup_printer
Anmeldeskript
Damit die Druckerkonfiguration bei der Anmeldung ausgeführt wird, legt man zuletzt noch die Datei /etc/X11/Xession.d/61iserv_print mit dem folgenden Inhalt an:
#!/bin/sh if [ $(id -u) -ge 10000 -a $(id -u) -le 100000 ] then echo "IServ - Setting up printers ..." if [ ! -d "$HOME/.iserv" ] then mkdir -pv "$HOME/.iserv" fi sudo /usr/lib/iserv/setup_printer "$USER" &> "$HOME/.iserv/printer-setup.log" fi
Auch dieses Skript muss ausführbar sein:
chmod +x /etc/X11/Xession.d/61iserv_print
Ein Log der Skriptausführung wird im Homeverzeichnis des Benutzers, der sich angemeldet hat, im Unterordner .iserv gespeichert und ist sicher nützlich für die Fehlersuche. |
Weitere Einsichtsmöglichkeiten
Sowohl der Quellcode des IServ-Moduls als auch die Clientprogramms sind auf GitHub einsehbar.
Fußnoten
- ↑ Außerdem ist es nötig, dass die Rechner mit dem Linux Softwareverteilungsmodul installiert wurden und nicht per DVD/USB-Stick oder anderen Medien, da sonst der entsprechend nötige SSH-Key für den Fernzugriff vom IServ auf die Rechner, auf diesen nicht vorhanden ist.
- ↑ Anleitung dazu findet sich auf unserer Website.
- ↑ Es wird wohl auch bei Windows-Clients versucht, dort wird es allein wegen des fehlenden SSH-Servers wahrscheinlich sowieso nicht funktionieren.