Vom einzeiligen Kommando über maßgeschneiderte Skripte bis zum ausgewachsenen Konfigurationsmanager: Die freie Programmiersprache Ruby erweist sich als praktisches Schmuckstück für die Systemadministration und erledigt vieles in erstaunlich wenigen Zeilen.
Ein Sysadmin hat stets alle Hände voll zu tun: Die DNS-Konfiguration automatisieren, Ressourcen zuteilen oder einen Software-Rollout planen. Was er auch macht, immer hat es mit Integration zu tun, mit der Kombination geeigneter Tools. Aufgabe einer Skriptsprache ist es dabei, die Komponenten zusammenzuhalten oder sie in einen praktischen Wrapper zu verpacken.
Praktische Einzeiler
Die Programmiersprache Ruby [1] eignet sich gut für diesem Zweck. Das beginnt bei schlichten Einzeilern, die Ruby mit der Standardshell des Systems verquicken. Die Zeile
ruby -e 'puts "hello world"' > /tmp/hello
schreibt die Zeichenkette in die angegebene Datei. Das »-e« teilt dem Interpreter mit, dass nun Ruby-Code folgt. Die Skriptsprache lässt sich auch als Filter in Pipelines nutzen:
ls -l | ruby -e 'Kommando' | grep Muster
Wie Perl besitzt Ruby einige reservierte globale Variablen, die eine fest definierte Bedeutung tragen. Sie sind besonders praktisch für Einzeiler. Etwa die Variable »$_« : Sie enthält die aktuelle Zeile, wenn man mit »ruby -n« Textdateien zeilenweise ausliest:
ruby -ne 'puts $_ if $_ =~ /foot/'/usr/share/dict/words
Dieser Codeschnipsel gibt alle Einträge aus dem Systemwörterbuch aus, die das Suchmuster »foot« enthalten – eine Art Grep für Arme. Der Option »-n« lassen sich übrigens mehrere Dateien in einer Befehlszeile übergeben.
Noch praktischer ist es, die Option »-p« zu verwenden, die das Gleiche wie »-n« tut, aber zusätzlich das Ergebnis mit »puts $_« ausgibt. Das folgende Kommando kommentiert alle Zeilen eines Ruby-Skripts aus:
ruby -pe '$_ = "#" + $_' ruby_script.rb
Die Ruby-Dokumentation [2] beschreibt weitere globale Variablen wie »$<« , »$.« und »$F« sowie Optionen wie »-i« und »-a« . Als Faustregel lässt sich festhalten, dass Ruby sich dabei an Perl orientiert und einige Verbesserungen hinzufügt.
Prozesse
Ruby kann auch Unterprozesse erzeugen, beispielsweise indem es andere Programme aufruft. So lässt sich mit wenig Code das Unix-Kommando »watch« nachbauen, das im Sekundentakt einen Befehl ausführt:
ruby -e 'system "clear; ls -lahG"while sleep 1'
Daneben bietet die Sprache weitere Funktionen zum Umgang mit Prozessen. Die Methoden »Process.pid()« , »Process.uid()« und »Process.gid()« beispielsweise liefern Prozess-, User- und Gruppen-ID des aktuellen Prozesses. Auch Signale lassen sich mit Ruby aussenden:
Process.kill("HUP", 1465)
So wartet ein Elternprozess darauf, dass sich ein Kindprozess beendet:
pid = Process.wait
Die folgenden Zeilen erzeugen einen Kindprozess und fragen dessen Erfolg und Prozess-ID ab:
system "echo foo" $?.success? # => true $?.pid
Unter Linux und Unix gilt: “Alles ist eine Datei.” Wie gut, dass Ruby eine einfache, sichere und skalierbare Schnittstelle für die Arbeit mit Dateien mitbringt.
Zeilenweise einlesen
Listing 1 erledigt eine typische Aufgabe im Zusamenhang mit Textdateien. Dabei geschieht die meiste Arbeit in der letzten Zeile: Sie liest eine Datei ein und zerteilt sie an den Newline-Zeichen – aber nur, falls die angegebene Datei überhaupt existiert. Der Code lässt sich zeilenweise an der interaktiven Ruby-Shell eingeben, die der Befehl »irb« aufruft.
Listing 1
Zeilen einlesen
01 people_path = "/tmp/people.txt"
02
03 people = []
04 people = File.read(people_path).split("\n") if File.exists?(people_path)
Der Code wird plattformneutral, wenn man Rubys eingebaute Variable »$/« für das Zeilenende verwendet. Diese dient auch als Trennzeichen der Methode »File.readlines()« , die eine Datei gleichzeitig liest und in Zeilen aufteilt:
File.readlines(people_path)# => ["line1\n", "line2\n", ...]
Offenbar belässt »readlines()« das Zeichen für das Zeilenende im Text, wie Abbildung 1 zeigt. So lässt sich das Zeichen entfernen:
File.readlines(people_path).map{ |line| line.chomp }
Der obige Code verwendet eine Map, um jede Zeile durch einen Codeblock zu schicken. Da Ruby Koroutinen kennt und Funktionen als Bürger erster Klasse behandelt, erinnern die Programme oft an funktionale Programmierung.
Der Einzeiler lässt sich noch weiter kürzen, denn Ruby kennt eine Schreibweise, um eine benannte Funktion auf jedes Element einer Menge anzuwenden:
File.readlines(people_path).map(&:chomp)
Der bisherige Ansatz birgt allerdings ein Problem: Er setzt voraus, dass mehr Speicher zu Verfügung steht, als Daten eingelesen werden. Für Ad-hoc-Skripte mag es angehen, mit kompletten Dateien zu arbeiten, für ernsthafte Admin-Aufgaben ist es zu riskant. Besser ist es, die Daten Zeile für Zeile zu lesen und zu verarbeiten. Dazu eignet sich eine Koroutine:
File.foreach(people_path) do |line| # Zeile verarbeiten end
Die Methode »File.foreach()« gibt einen so genannten Enumerator zurück, aus dem sich Element für Element verarbeiten lässt. Angenommen die verarbeitete Datei enthält einige sehr lange Zeilen, deren Zeichen der Ruby-Anwender zählen möchte. Dazu schreibt er:
character_counts = File.foreach(people_path).map(&:size)
Die Map-Methode entnimmt dem von »foreach()« erzeugten Enumerator nur eine Zeile auf einmal, daher sollte sich auch nur jeweils eine Zeile im Speicher befinden. Liebhaber der funktionalen Programmierung kennen das als Lazy Evaluation. Das Programm wertet nur so viel aus, wie es gerade für den nächsten Schritt benötigt. In einer der nächsten Releases soll Ruby Unterstützung für lange Kommandoverkettungen mit mehreren Map- und Filter-Stufen bieten. Listing 2 zeigt die Verwendung von »File.open()« . Es erzeugt ein Filehandle, ein Konzept, das Programmierer aus anderen Sprachen kennen. Der Modestring »”w”« hat dabei dieselbe Bedeutung wie in C und öffnet die Datei zum Schreiben. Standardmäßig öffnet Ruby Dateien nur zum Lesen.
Listing 2
Filehandle
01 File.open(people_path, "w") do |f| 02 f.puts "john" # eine Zeile schreiben 03 f.puts "james", "george" # zwei Zeilen schreiben 04 f.print "anna" # 4 Zeichen ohne Newline 05 f.print "bel\n" # explizit Newline anfügen 06 end
Filehandles, Blocks und Verzeichnisse
Die Besonderheit von Listing 2 besteht darin, dass es das Filehandle an einen Codeblock übergibt. Das heißt, alles was im Block steht, passiert nur, wenn die Datei erfolgreich zum Schreiben geöffnet wurde. Am Ende das Blocks schließt Ruby die Datei automatisch.
Das »Dir« -Modul dient dazu, Verzeichnisse zu lesen, anzulegen und so weiter. Listing 3 erstellt eine Liste aller C-Quelltextdateien im Arbeitsverzeichnis und sortiert sie nach dem »modified« -Zeitstempel. Dabei kommt das von der Bash vertraute Globbing-Muster mit Asterisk zum Einsatz.
Listing 3
Dir
01 source_files = Dir["*.c"].select{|f| File.size(f) > 1024}.sort_by{|f| File.mtime(f)}
Der beschriebene Umgang mit Dateien lässt sich in der Ruby-Dokumentation unter den Klassennamen »File« , »Dir« , »IO« und »Enumerator« nachlesen. Daneben besitzt Ruby ein eingebautes Informationssystem, das sich auf der Kommandozeile mit »ri Schlagwort« aufrufen lässt (Abbildung 2).
Für die aktuelle Version 1.9.x haben die Ruby-Entwickler dem Encoding eine Runderneuerung verpasst. Dieses Thema hat mehrere Aspekte: Die Kodierung der Skriptdateien selbst darf nun auch UTF-8 sein, was der Programmierer zu Beginn der Datei angeben muss:
#!/usr/bin/env ruby -w # encoding: UTF-8 utf8_string_literal = "André"
Außerdem geht es um den Zeichensatz der Ein- und Ausgaben. Hier ist es in der Regel empfehlenswert, die Standardkodierung des Betriebssystems zu verwenden:
ruby -e 'puts Encoding.default_external.name'
Daneben spielt noch das Encoding eine Rolle, das Ruby intern beim Verarbeiten der eingelesenen Zeichenketten einsetzt. Ein Blogeintrag des Ruby-Programmierers James Edward Gray behandelt alle Aspekte ausführlich [3].
Regular Expressions
Wer Admin-Skripte programmiert, kommt kaum um reguläre Ausdrücke herum. Dabei muss er eine Art Reverse Engineering der verarbeiteten Daten durchführen. Es gilt, Muster in den Eingabedateien zu erkennen, zu verstehen und in Regeln zu fassen. Mehrmaliges Nachjustieren bleibt nicht aus.
Eine Skriptsprache für den Administrator muss also gute Unterstützung für Regular Expressions bieten – und das tut Ruby. Wie in Perl lassen sich einfache Zeichenketten finden (Listing 4). Auch Gruppen, sogar verschachtelte, beherrscht die Sprache, wie Listing 5 zeigt. Und wie das Listing 6 demonstriert, eignen sich reguläre Ausdrücke gut als Prüfinstrumente, ob Zeilen in einer Eingabedatei auskommentiert oder unlesbar sind.
Listing 6
Textzeilen prüfen
01 # auskommentierte Zeilen auslassen
02 File.foreach("users.txt") do |line|
03 next if line =~ /^#/
04 # nicht verarbeitbare Zeilen auslassen
05 next unless line =~ /^([a-z]+)\s+(\w+)$/
06 # sonstige Zeilen verarbeiten
07 login, name = $1, $2
08 [...]
09 end
Listing 5
Regex mit Gruppen
01 corpus =~ /fo(..)s/ # => 0 02 $1 # => "xe" 03 04 corpus =~ /fo(.(.))s/ # => 0 05 [$1, $2] # => ["xe", "e"]
Listing 4
Reguläre Ausdrücke
01 corpus = "foxes and goats" 02 corpus =~ /foxes/ # => 0 03 corpus =~ /and/ # => 6 04 corpus =~ /and$/ # => nil 05 corpus =~ /.es\s+/ # => 2
Aufs Netzwerk zugreifen
Zum umfangreichen Repertoire von Ruby gehört außerdem die Netzwerk-Programmierung. Es beherrscht TCP-, UDP- und Unix-Domain-Sockets. So lässt sich bereits ein einfacher Webzugriff realisieren (Abbildung 3). Packt der Programmierer das Öffnen in einen Block, schließt Ruby den Socket am Ende automatisch:

Abbildung 3: Mit Rubys Sockets ist in nur wenigen Zeilen ein Skript geschrieben, das eine Webseite aufruft.

Abbildung 4: Mit Ruby kann der Administrator eigene Tools zur Systemverwaltung programmieren. Hier hat das Skript Benutzerkonten angelegt, die es aus einer CSV-Datei gelesen hat.
require "socket"
TCPSocket.open("www.ard.de", 80) do |socket|
socket.puts "GET / HTTP/1.0\n\n"
puts socket.read
end
Der Einsatz von Sockets eignet sich gut, um Rubys Fehlerbehandlung mittels Exceptions zu demonstrieren. Listing 7 zeigt ein Codeschnipsel, das dreimal versucht eine Verbindung mit einem Webserver aufzunehmen, ehe es schließlich mit einer Fehlermeldung aufgibt.
Listing 7
Exceptions
01 require "socket"
02
03 attempts = 0
04
05 begin
06 attempts += 1
07 TCPSocket.open("www.example.com", 80) { |socket| [...] }
08 rescue Exception => e
09 if attempts < 3 then sleep 5 and retry
10 else puts "Es reicht! #{e}"
11 end
12 end
In vielen Situationen braucht sich der Programmierer aber gar nicht auf die Ebene der Sockets herabzulassen. Er kann beispielsweise zur HTTP-Bibliothek greifen und mit wenigen Zeichen die Slashdot-Startseite aufrufen:
require "net/http"
body = Net::HTTP.get("slashdot.org", "/")
Listing 8 verfeinert den HTTP-Zugriff, indem es das strukturierte »response« -Objekt verwendet, in dem sich der HTTP-Status und die Header finden. Ruby bringt in seiner Standardbibliothek sogar einen kompletten HTTP-Server namens Webrick mit.
Listing 8
HTTP-Header
01 require "net/http"
02
03 response = Net::HTTP.get("slashdot.org", "/")
04 puts response.body if response.code == 200
05 puts response.each_header.select { |key, value| key =~ /^content/}
Für den ernsthaften Einsatz bietet sich aber eher Eventmachine [4] an, ein skalierbarer Ruby-Webserver nach asynchronem Modell. Anhand seiner Dokumentation ist in wenigen Codezeilen etwa ein einfacher Echo-Server umgesetzt. Der Autor dieses Artikels hat Eventmachine erfolgreich in Produktivinstallationen als Loadbalancer oder als SSL-Proxy für andere Rechner eingesetzt.
Äußerst nützlich für den Sysadmin ist auch die Pcap-Bibliothek [5], ein Ruby-Wrapper um die Packet-Capture-Library Libpcap. Mit ihrer Hilfe lesen Ruby-Skripte rohen Netzwerktraffic, und dabei ist die Bibliothek einfach zu benutzen.
Ruby als Schnüffler
Ein Skript wie in Listing 9 genügt Root, um einen kleinen Traffic-Monitor zu betreiben. Es definiert die zu überwachende IP-Adresse und erzeugt ein Sniffer-Objekt, das die angegebene Netzwerkkarte beschnüffelt. Für jedes Paket, das der Sniffer mitbekommt, prüft das Programm, ob der Absender oder der Empfänger mit der angegebenen IP-Adresse übereinstimmt, und zählt die übertragenen Bytes zum Akkumulator »sent_bytes« beziehungsweise »received_bytes« hinzu. Die Schleife »sniffer.each_packet« kann der Anwender auf der Kommandozeile mit der Tastenkombination [Strg]+[C] abbrechen. Dann läuft der Rest des Skripts zu Ende ab, schließt den Sniffer und gibt eine Zusammenfassung der gesendeten und empfangenen Pakete aus.
Listing 9
Pcap-Bibliothek
01 require "pcaplet"
02
03 HOST_IP = Pcap::IPAddress.new("10.0.0.1")
04
05 received_bytes = 0
06 sent_bytes = 0
07
08 sniffer = Pcap::Pcaplet.new("-i eth1")
09 sniffer.each_packet do |pkt|
10 next unless pkt.ip?
11
12 src, dst, = pkt.ip_src, pkt.ip_dst
13
14 if src == HOST_IP
15 sent_bytes += pkt.size
16 elsif dst == HOST_IP
17 received_bytes += pkt.size
18 end
19 end
20 sniffer.close
21
22 puts "#{HOST_IP} hat #{sent_bytes} Bytes gesandt und #{received_bytes} empfangen."
Fit bei Dateiformaten
Damit der Programmierer Dateien nicht stets von Hand auseinandernehmen oder zusammenstückeln muss, bringt Ruby Bibliotheken für gebräuchliche Dateiformate wie Json oder Base64 (für E-Mail-Attachments) mit. Auch CSV-Unterstützung ist mittlerweile eingebaut, früher musste man sie noch gesondert über das Faster-CSV-Gem installieren. Listing 10 liest und schreibt zeilenweise.
Listing 10
CSV-Verarbeitung
01 require "csv"
02
03 # Zeilen als Arrays einlesen
04 CSV.foreach("users.csv") do |(login, name)|
05 puts login if name =~ /And/
06 end
07
08 # Zeilen als Hashes einlesen
09 CSV.foreach("users.csv", :headers => true) do |row|
10 puts row["login"] if row["name"] =~ /And/
11 end
12
13 # Array in Zeilen schreiben
14 CSV.open("users.csv", "wb") do |csv|
15 csv << ["george", "Georgey Porgey"]
16 csv << %w(gertrude Gertrude)
17 end
Wer mit Ruby in Zip-Archive sehen oder sie verändern möchte, greift zur Zip-Gem [6]. Sie kann Dateien auflisten, extrahieren und hinzufügen. Zum Arbeiten mit XML gibt es die mitgelieferte Bibliothek Rexmal, die vollständig in Ruby implementiert ist. Sie eignet sich, um einfache Dokumente zu erzeugen oder Elemente und Werte per Xpath auszulesen.
Wer Gnomes native XML-Bibliothek Libxml2 installiert hat, kann alternativ Libxml-ruby [7] als praktischen Wrapper benutzen, der wesentlich bessere Performance bietet. Eine besonders schöne Programmierschnittstelle zeichnet Nokogiri [8] aus, das sowohl Xpath- als auch CSS-3-Selektoren unterstützt.
Anwendungsbeispiel
Viele in diesem Artikel gezeigte Ruby-Features finden sich in Listing 11 wieder, das die Aufgabenstellung aus der Einführung zu diesem Magazin-Schwerpunkt umsetzt: Es liest eine CSV-Datei zeilenweise ein, prüft Login-Namen gegen einen regulären Ausdruck und schreibt die gültigen Daten in alle relevanten Linux-Systemdateien, um die Benutzerkonten anzulegen (Abbildung 4). Selbst für Ruby-Neulinge liest es sich leicht und verrät, was es in welcher Zeile tut.
Listing 11
User aus CVS-Liste anlegen
01 # erfordert Ruby 1.9
02
03 require "csv"
04 require "fileutils"
05 require "set"
06
07 csv_file = ARGV.first
08 raise "specify a user CSV" if csv_file.nil?
09
10 raise "must be run as root" unless Process.uid == 0
11
12 def invalid_login?(login)
13 return false if login =~ /^[a-z0-9]+$/
14 warn "invalid login: #{login.inspect}"
15 true
16 end
17
18 $existing_logins = CSV.readlines("/etc/passwd", :col_sep => ":").map(&:first).to_set
19 def existing_login?(login)
20 return false unless $existing_logins.include?(login)
21 warn "existing login: #{login.inspect}"
22 true
23 end
24
25 new_users = CSV.readlines(csv_file).reject { |(login, _, _)| invalid_login?(login) or existing_login?(login) }
26
27 exit 0 if new_users.empty?
28
29 lines_by_file_name = {"passwd" => [], "group" => [], "shadow" => []}
30
31 new_users.each_with_index do |(login, lastname, firstname), i|
32 uid = i + 1001
33 lines_by_file_name["passwd"] << [login, "x", uid, uid, "#{lastname}, #{firstname}", "/bin/bash"].join(":")
34 lines_by_file_name["group"] << [login, "x", uid, login].join(":")
35 lines_by_file_name["shadow"] << [login, "!", "", "", "", "", "", "", ""].join(":")
36 end
37
38 lines_by_file_name.each do |file_name, lines|
39 File.open("/etc/#{file_name}", "a") { |f| f.puts lines }
40 end
41
42 new_users.each do |(login, _, _)|
43 home_dir = "/home/#{login}"
44 FileUtils.mkdir_p(home_dir, :mode => 0700)
45 FileUtils.cp_r("/etc/skel/.", home_dir)
46 end
Überall Juwelen
Ruby eignet sich nicht nur für selbst geschrieben Skripte. Die Programmiersprache findet sich in den letzten Jahren oft in leistungsstarker Software zur Verwaltung größerer Systeme. Dazu gehören das Deployment-Tool Capistrano [9] und Puppet [10], ein System zum Management von Serverkonfigurationen. Dem gleichen Zweck dient das Framework Chef [11]. Das Buildsystem Rake [12] ist eine Offenbarung für alle, die bisher an GNU Make gewöhnt waren.
Wer auf dem Laufenden in Sachen Ruby-Programmierung bleiben möchte, sollte die Website Ruby Inside [13] besuchen. Dort sind auch hervorragende Tutorials zu finden. (mhu)
Infos
- Ruby: http://www.ruby-lang.org
- Ruby-Dokumentation http://www.ruby-doc.org
- “Ruby 1.9’s Three Default Encodings”, James Edward Gray II: http://blog.grayproductions.net/articles/ruby_19s_three_default_encodings
- Eventmachine: http://rubyeventmachine.com
- Pcap-Bibliothek: http://sourceforge.net/apps/trac/rubypcap
- Zip-Bibliothek: http://rubygems.org/gems/zip
- Libxml-ruby: http://rubygems.org/gems/libxml-ruby
- Nokogiri: http://rubygems.org/gems/nokogiri
- Capistrano: https://github.com/capistrano/capistrano/wiki
- Puppet: http://puppetlabs.com/puppet/puppet-open-source
- Chef: http://wiki.opscode.com/display/chef/Chef+Basics
- Rake: http://rake.rubyforge.org
- Ruby Inside: http://www.rubyinside.com
- Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2012/06/ruby








