Direktes Drucken mit Linux-Clients

Aus dem IServ-Wiki
Zur Navigation springen Zur Suche springen

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, müssen 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 das Drucken 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.


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 sind, 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 (Icon Wikipedia De.gifInternet Printing Protocol) innerhalb des LANs freigegeben werden. Das ist notwendig, da Linux-Clients wie oben erwähnt, nicht die Möglichkeit haben, die Icon Wikipedia De.gifSamba-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 installieren.

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 werden die JSON-Dateien mit der Druckerkonfiguration (default.json, default.local.json, room/Raumname.json, [...]) geladen, und anschließend aus ihnen die Druckerinformationen zusammengetragen (eingebundene Drucker, Standarddrucker) und im gleichen Zug Drucker, die dem Rechner nicht (mehr) zugewiesen sind, entfernt.

SSH-Schlüssel

Falls die Linux-Clients nicht über die Softwareverteilung eingerichtet worden sind, muss man den SSH-Key für den Fernzugriff von IServ aus, dort einrichten. Dazu müssen folgende Befehle als root auf dem Client ausgeführt werden:

root
mkdir ~/.ssh
root
touch ~/.ssh/authorized_keys
<rootpre>
wget -O - http://iserv.local/id_rsa.pub >> .ssh/authorized_keys

Dokumentation des Clientprogramms

Bei Ubuntu, Linux Mint oder Debian muss das Clientprogramm nicht manuell eingerichtet werden, das erledigt das oben genannte IServ-Modul automatisch.

Nachfolgend sich die Dokumentation des Clientprogramms, um es zum Beispiel auch auf nicht unterstützten Linux-Betriebssystemen verwenden zu können.

Information Alle im folgenden genannten Terminal-Befehle sind als root auf dem Linux-Client und nicht auf dem IServ auszuführen! Falls für root auf dem Client kein Passwort gesetzt sein sollte, kann mit sudo -i eine root-Login-Shell emuliert werden. Das Modul stsbl-iserv-linux-support muss vorher auf dem Portalserver installiert sein, die Funktionalität des Clients hängt davon ab.

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:

root
vim /etc/security/pam_mount.conf.xml

Dort muss hinter dem <!-- Volume definitions --> angefügt werden:

datei
<!-- 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 -->
Information 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):

root
vim /usr/lib/iserv/setup_printer

Das Skript sieht wie folgt aus (es wird einfach in den Editor eingefügt):

datei
#!/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;
Information Das Skript benötigt die Perl-Bibliotheken libnet-address-ip-local-perl, libjson-perl und libnet-cups-perl, diese muss man genau jetzt über den Paketmanager der eigenen Distribution installieren.

Nach dem Speichern, muss das Skript noch ausführbar gemacht werden, das erledigt man so:

root
chmod +x /usr/lib/iserv/setup_printer

Sudo konfigurieren

Das Beispiel am besten genau kopieren und nicht manuell abtippen, Fehler in der sudo-Konfiguration können dazu führen, dass man sich auf dem Rechner vom root-Account aussperrt.

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:

datei
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:

datei
#!/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:

root
chmod +x /etc/X11/Xession.d/61iserv_print
Information 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

  1. 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 ansonsten der entsprechend nötige SSH-Key, der für den Fernzugriff vom IServ auf die Rechner benötigt wird, auf den Rechnern nicht eingerichtet wird.
  2. Anleitung dazu findet sich auf unserer Website.
  3. Es wird wohl auch bei Windows-Clients versucht, dort wird es allein wegen des fehlenden SSH-Servers wahrscheinlich sowieso nicht funktionieren.