Bcc pro Queue in OTRS

| No Comments | No TrackBacks

Die Antworten auf Tickets einer bestimmten Queue im OTRS sollen per Bcc an eine weitere Adresse gesendet werden. Das sieht OTRS bisher nicht vor; die einzige Option ist ein generelles Bcc für alle Queues.

Um hier Abhilfe zu schaffen, habe ich Kernel/Modules/AgentTicketCompose.pm ein wenig erweitert. Etwa bei Zeile 400 findet sich folgende Methode, bei der die mit Bcc beginnende Zeile wie folgt geändert wird.

-Bcc            => $GetParam{Bcc},
+Bcc            => $Ticket{QueueID} == 83 ? ('bcc@example.com') : $GetParam{Bcc},

Die QueueID (im Beispiel 83) und die Zieladresse müssen selbstverständlich angepaßt werden.

# send email
    my $ArticleID = $Self->{TicketObject}->ArticleSend(
        ArticleType    => 'email-external',
        SenderType     => 'agent',
        TicketID       => $Self->{TicketID},
        HistoryType    => 'SendAnswer',
        HistoryComment => "\%\%$Recipients",
        From           => $GetParam{From},
        To             => $GetParam{To},
        Cc             => $GetParam{Cc},
        # Dirty hack to Bcc (forward) answers of tickets from queue id=83.
        #Bcc           => $GetParam{Bcc},
        Bcc            => $Ticket{QueueID} == 83 ? ('bcc@example.com') : $GetParam{Bcc},
        Subject        => $GetParam{Subject},
        UserID         => $Self->{UserID},
        Body           => $GetParam{Body},
        InReplyTo      => $GetParam{InReplyTo},
        Charset        => $Self->{LayoutObject}->{UserCharset},
        Type           => 'text/plain',
        Attachment     => \@AttachmentData,
        %ArticleParam,
    );

Mit ein bisschen mehr Aufwand ließe sich das auch auf mehrere Queues ausweiten sowie die Definition der QueueIDs und Bcc-Adressen in die Konfigurationsdatei auslagern. Das überlasse ich dem geneigten Leser als Hausaufgabe.

Einen einfachen und schnellen Weg, die Datenkonsistenz einer Replikation in MySQL zu überprüfen, bietet mk-table-checksum aus Maatkit. Das Perl-Skript erstellt Prüfsummen der Tabellen und schreibt diese in eine eigene Tabelle. Durch die Replikation wird diese Tabelle auf den Slave übertragen und dort ebenfalls die Prüfsummen ermittelt. Hinterher lassen sich auf dem Slave die Prüfsummen vergleichen und eventuelle Veränderungen der Datenbestände feststellen. Diese lassen sich dann beispielsweise mit mk-table-sync aus der selben Tool-Sammlung wieder synchronisieren.

Bevor es losgeht, muß auf dem Master eine neue Datenbank (hier test) und die folgende Tabelle erstellt werden.

CREATE TABLE checksum (
    db         char(64)     NOT NULL,
    tbl        char(64)     NOT NULL,
    chunk      int          NOT NULL,
    boundaries char(100)    NOT NULL,
    this_crc   char(40)     NOT NULL,
    this_cnt   int          NOT NULL,
    master_crc char(40)         NULL,
    master_cnt int              NULL,
    ts         timestamp    NOT NULL,
    PRIMARY KEY (db, tbl, chunk)
);

Danach wird das Skript ausgeführt.

mk-table-checksum --replicate=test.checksum h=localhost,u=username,p=password,S=/data/db/mysql.sock

Ist das Programm durchgelaufen, kann man hinterher auf dem Slave prüfen, ob die Prüfsummen von Master und Slave abweichend sind.

SELECT db, tbl, chunk, this_cnt-master_cnt AS cnt_diff,
    this_crc <> master_crc OR ISNULL(master_crc) <> ISNULL(this_crc)
        AS crc_diff
FROM checksum
WHERE master_cnt <> this_cnt OR master_crc <> this_crc
    OR ISNULL(master_crc) <> ISNULL(this_crc);

Wer im Internet unterwegs ist, hinterläßt dabei seine Spuren unter anderem in Form der IP-Adresse des verwendeten Zugangs. Diese Adressen werden von den Providern zum Zweck der Strafverfolgung gespeichert (siehe Vorratsdatenspeicherung). Außerdem kann die IP-Adresse vom Kommunikationspartner, also Webservern, Chatkontakten, Teilnehmern von P2P-Netzwerken usw. gespeichert werden.

Am Wochenende las ich einen haarsträubenden Bericht über den Umgang mit IP-Adressen in straf- und zivilrechtlichen Prozessen, wodurch Unschuldige wegen unbeweisbarer Behauptungen und Übertragungsfehlern zu Unrecht unschöner Dinge verdächtigt wurden. Dabei kam mir die Idee, die Vorratsdatenspeicherung umzudrehen und die von mir benutzten IP-Adressen selbst zu speichern. Sollte ich einmal in die unangenehme Situation kommen, solchen Verdächtigungen ausgesetzt zu sein, kann ich wenigstens für mich selbst überprüfen, ob es wirklich von meinem Anschluß geschah. Ob das juristisch von Bedeutung und bei der eigenen Verteidigung verwertbar ist, weiß ich nicht. Aber es würde wenigstens die eigenen Zweifel ausräumen.

Daher habe ich also eine kleine Lösung entwickelt. Benötigt wird in diesem Fall ein Linux-System mit Perl und MySQL sowie ein Webserver, der CGIs (ebenfalls Perl) ausführen kann.

Zunächst wird ein Datenbankschema mit einer Tabelle angelegt. Die Tabelle wird später zur Speicherung der IP-Adressen und Start- sowie Enddatum deren Nutzung gebraucht.

CREATE DATABASE `remote_addr_logger` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `remote_addr_logger`;
CREATE TABLE IF NOT EXISTS `remote_addr` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `remote_addr` int(10) unsigned NOT NULL COMMENT 'Remote ip address. Use INET_NTOA() to convert.',
  `date_start` datetime NOT NULL COMMENT 'Address used between ...',
  `date_end` datetime NOT NULL COMMENT 'and ...',
  PRIMARY KEY (`id`),
  KEY `remote_addr` (`remote_addr`),
  KEY `date_start` (`date_start`),
  KEY `date_end` (`date_end`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

Für den Zugriff auf die Datenbank wird noch ein Benutzer angelegt.

GRANT SELECT , INSERT , UPDATE ON `remote_addr_logger` . * TO 'remote_addr'@'localhost' IDENTIFIED BY '***';

Als Nächstes braucht es eine Möglichkeit, die IP-Adresse seines Gateways (DSL-Router etc.) zu ermitteln. Hier gibt es viele Möglichkeiten mit verschiedenen Vor- und Nachteilen. Ich entschied mich kurzerhand für ein winziges CGI-Skript, das später vom Hauptprogramm via HTTP request aufgerufen werden soll und die eigene remote address in einem kleinen XML output ausgibt.

#!/usr/bin/perl

my $remote_addr = $ENV{'REMOTE_ADDR'};

print "Content-type: text/html\n\n";
print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
print "<remote_addr>$remote_addr</remote_addr>\n";

Nun fehlt nur noch das Hauptprogramm, das das CGI-Skript aufruft, die XML-Datei empfängt und auswertet und die IP-Adresse samt Datum in die Datenbank schreibt. Hier sind zu Beginn die URL des CGI-Skripts sowie die Zugangsdaten zur Datenbank einzutragen.

#!/usr/bin/perl

use strict;
use warnings;
use LWP::Simple;
use DBI;

my $get_remote_addr_cgi_url = 'http://example.com/cgi-bin/get_remote_addr.cgi';

my $db_name = 'remote_addr_logger';
my $db_host = 'localhost';
my $db_user = 'remote_addr';
my $db_pass = 'yoursecretpassword';

sub parse_xml_data {
    my $xml_data = shift;
    my $tag = 'remote_addr';
    my $inet_addr = '(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
    my $pattern = "<$tag>($inet_addr)</$tag>";
    if ( $xml_data =~ /$pattern/ ) {
        my $remote_addr = $1;
        return $remote_addr;
    } else {
        return -1;
    }
}

sub get_remote_addr {
    my $xml_data = get($get_remote_addr_cgi_url);
    if ( defined( $xml_data )) {
        my $remote_addr = parse_xml_data( $xml_data );
        return $remote_addr;
    } else {
        return -1;
    }
}

my $remote_addr = get_remote_addr();
unless ( $remote_addr eq -1 ) {
    my $dsn = "dbi:mysql:$db_name:$db_host";
    my $dbh = DBI->connect($dsn, $db_user, $db_pass);
    my ($id) = $dbh->selectrow_array("SELECT `id` FROM `remote_addr` WHERE `remote_addr` = INET_ATON( '$remote_addr' ) AND `id` IN ( SELECT MAX(`id`) FROM `remote_addr`);");
    if ( defined($id) and $id gt 0 ) {
        $dbh->do("UPDATE `remote_addr` SET `date_end` = NOW() WHERE `id`=$id;");
    } else {
        $dbh->do("INSERT INTO `remote_addr` ( `remote_addr`, `date_start`, `date_end` ) VALUES ( INET_ATON( '$remote_addr'), NOW(), NOW() );");
    }
    $dbh->disconnect;
}

Die Funktionsweise ist recht simpel. Die IP-Adresse wird mit der des letzten Eintrags in der Tabelle verglichen. Hat sie sich nicht geändert, wird das Enddatum mit der aktuellen Zeit aktualisiert. Ist es eine andere Adresse, wird ein neuer Eintrag eingefügt.

Jetzt muß das Skript nur noch regelmäßig aufgerufen werden. Sinnvollerweise erledigt das cron.

*/1 * * * * /usr/local/bin/remote_addr_logger

Die Einträge können wie folgt aus der Datenbank ausgelesen werden:

SELECT INET_NTOA(`remote_addr`), `date_start`, `date_end` FROM `remote_addr`;

Und jetzt bin ich mal gespannt…

OpenX mit APC beschleunigen

| No Comments | No TrackBacks

Die Auslastung eines von mir betreuten Webserver, der hauptsächlich den Adserver OpenX ausführt, ließ mich nach Optimierungsmöglichkeiten suchen. Schnell wurde ich in diesem Artikel fündig. Dort wird dringend geraten einen PHP accelerator wie APC (Alternative PHP Cache) zu verwenden. Ein solcher Opcode-Cache speichert den kompilierten PHP-Quelltext zwischen und kann dadurch bei wiederholter Ausführung das zeitaufwändige Kompilieren nahezu vollständig vermeiden.

Die versprochene Beschleunigung klingt traumhaft und kann tatsächlich erreicht werden, wie folgende Graphen aus dem Monitoring des Servers zeigen. APC wurde an Tag 8 gestartet.

cpu-week.png

load-week.png

Ähnliche Ergebnisse sollten sich mit anderen PHP-Anwendungen und einem Opcode-Cache ebenfalls erzielen lassen.

Im folgenden Text soll die Installation von Sun JDK 6 und Tomcat6 auf einem System mit RHEL5 oder Centos5 beschrieben werden. Da die Pakete nicht in den Repositories der Distribution verfügbar sind, müssen sie von extern bezogen bzw. selbst gebaut werden.

JPackage Repository nutzen

Das JPackage Projekt bietet eine umfangreiche Sammlung von Java-Software-Paketen für Linux an, die wir nun nutzen möchten. Zuerst wird das neue Repository eingebunden und der GPG-Schlüssel importiert.

wget -O /etc/yum.repos.d/jpackage50.repo http://www.jpackage.org/jpackage50.repo
rpm --import http://www.jpackage.org/jpackage.asc

Sun JDK 6 installieren

Aus lizenzrechtlichen Gründen kann kein fertiges RPM-Paket für das Java6 JDK angeboten werden. Es gibt aber ein Source-Paket, mit dessen Hilfe aus dem auf der Java-Seite erhältlichen Software ein ordentliches RPM gebaut werden kann. Sofern nicht schon vorhanden, müssen hierfür erforderliche Tools installiert werden. Das Sun JDK6 muß separat heruntergeladen werden. Sollte für die aktuelle Version noch kein Paket verfügbar sein, findet man im Archiv die passende ältere Version.

yum install rpm-build jpackage-utils libXtst
wget -O /tmp/java-1.6.0-sun-1.6.0.14-1jpp.nosrc.rpm http://mirrors.dotsrc.org/jpackage/6.0/generic/non-free/SRPMS/java-1.6.0-sun-1.6.0.14-1jpp.nosrc.rpm
rpm -ihv /tmp/java-1.6.0-sun-1.6.0.14-1jpp.nosrc.rpm
mv /tmp/jdk-6u14-linux-x64.bin /usr/src/redhat/SOURCES/
rpmbuild -bb /usr/src/redhat/SPECS/java-1.6.0-sun.spec
rpm -ihv /usr/src/redhat/RPMS/x86_64/java-1.6.0-sun-1.6.0.14-1jpp.x86_64.rpm
rpm -ihv /usr/src/redhat/RPMS/x86_64/java-1.6.0-sun-devel-1.6.0.14-1jpp.x86_64.rpm

Tomcat 6 installieren

Tomcat6 sollte sich nun einfach über die Paketverwaltung installieren lassen. Leider gibt es einen Abhängigkeitenkonflikt zwischen benötigten Paketen.

Error: Missing Dependency: /usr/bin/rebuild-security-providers is needed by package java-1.4.2-gcj-compat-1.4.2.0-40jpp.115.x86_64 (base)

Eine Lösung ist der Bau und die Installation eines Kompatibilitätspakets wie es in einem Posting auf der Mailinglist JPackage-discuss beschrieben ist. Danach klappt auch die Installation von Tomcat6 ohne Probleme.

rpmbuild -bb /usr/src/redhat/SPECS/jpackage-utils-compat.spec
rpm -ihv /usr/src/redhat/RPMS/noarch/jpackage-utils-compat-el5-0.0.1-1.noarch.rpm
yum install tomcat6

Die Installation weiterer Java-Software aus dem JPackage-Softwareverzeichnis sollte nun einfach über yum möglich sein.

Papierkorb für Samba

| 1 Comment | No TrackBacks

Der Gedanke, dass Dateien, die auf dem Fileserver liegen, von den Benutzern versehentlich gelöscht werden könnten und diese dann nur aus den Backups wiederherzugestellen seien, ließ dem Vorgesetzten zwanzig graue Haare wachsen und mich nach einer Lösung suchen. Die besteht aus dem VFS-Modul recycle für Samba, dass die Papierkorb-Funktion von Windows nachahmt. Wird es benutzt, werden UNLINK-Befehle abgefangen, und die Datei in den recycle-Ordner verschoben, anstatt gelöscht zu werden.

Bei Verfügbarkeit des Moduls sollte im Bibliotheksverzeichnis von Samba unter vfs die Datei recycle.so zu finden sein, üblicherweise /usr/lib/samba/vfs/recycle.so auf Linux oder /usr/local/lib/samba/vfs/recycle.so auf BSD.

Am Beispiel einer für alle Benutzer zugänglichen Freigabe sei die Konfiguration gezeigt. Mit der Definition von vfs object = recycle wird das Modul geladen und danach weitere Parameter übergeben. Eine Erklärung zu den Optionen ist der Dokumentation zu entnehmen.

[public]
        path = /var/fileserver/public
        writeable = Yes
        browseable = Yes
        create mask = 0666
        directory mask = 0777
        vfs object = recycle
        recycle:repository = .recycle
        recycle:keeptree = Yes
        recycle:touch = Yes
        recycle:versions = Yes
        recycle:maxsize = 0
        recycle:directory_mode = 0777

Das Verzeichnis .recycle wird automatisch in oberster Ebene der Freigabe angelegt und eine gelöschte Dateien und Verzeichnisse nun dort hin verschoben.

Wenn das Zugangspasswort für ein Benutzerkonto im Windows unbekannt ist und zurückgesetzt werden soll, kann auch der Linux-Admin schnell Abhilfe leisten. Hierzu bedarf es lediglich eines Bootmediums mit Linux und dem Tool chntpw. In meinem Fall ist das ein USB-Stick mit GRML, das chntpw bereits mitbringt.

Ist das System gestartet, wird die Windows-Partition gemountet und in das Verzeichnis \WINDOWS\System32\config gewechselt. Dort liegt die Datei SAM, die die Accountdaten, also auch die Passwort-Hashes, beinhaltet.

# mkdir /mnt/windows
# ntfs-3g /dev/sda1 /mnt/windows
# cd /mnt/windows/WINDOWS/system32/config

Mit dem folgenden Kommando kann man sich die einzelnen Konten anzeigen lassen…

# chntpw -l SAM

…und dann zur Tat schreiten und das Passwort löschen oder neu setzen:

# chntpw -u username SAM

Im letzten Kommando ist ‘username’ dabei selbstverständlich durch den Benutzernamen des Windows-Kontos zu ersetzen.

Auf einen Hilfssystem, das wir von Hetzner hosten lassen, stellte ich gestern einen Hardwaredefekt bei einer der beiden Festplatten des RAID-1-Verbunds fest. Das RAID war degraded und in der dmesg waren die typischen Ausgaben zu finden.

ata2: EH complete
SCSI device sdb: 976773168 512-byte hdwr sectors (500108 MB)
sdb: Write Protect is off
sdb: Mode Sense: 00 3a 00 00
SCSI device sdb: drive cache: write back
ata2.00: exception Emask 0x0 SAct 0x1 SErr 0x0 action 0x0
ata2.00: (irq_stat 0x40000008)
ata2.00: tag 0 cmd 0x60 Emask 0x9 stat 0x41 err 0x40 (media error)
sd 1:0:0:0: SCSI error: return code = 0x08000002
sdb: Current: sense key: Medium Error
    Additional sense: Unrecovered read error - auto reallocate failed
end_request: I/O error, dev sdb, sector 976767110
ata2: EH complete
SCSI device sdb: 976773168 512-byte hdwr sectors (500108 MB)
sdb: Write Protect is off
sdb: Mode Sense: 00 3a 00 00
SCSI device sdb: drive cache: write back
ata2.00: WARNING: zero len r/w req
ata2.00: WARNING: zero len r/w req
ata2.00: WARNING: zero len r/w req
ata2.00: WARNING: zero len r/w req> ata2.00: WARNING: zero len r/w req
ata2.00: WARNING: zero len r/w req
raid1: Disk failure on sdb2, disabling device.
    Operation continuing on 1 devices
RAID1 conf printout:
 --- wd:1 rd:2
 disk 0, wo:0, o:1, dev:sda2
 disk 1, wo:1, o:0, dev:sdb2
RAID1 conf printout:
 --- wd:1 rd:2
  disk 0, wo:0, o:1, dev:sda2

Kurz den Support bei Hetzner angeschrieben und den Sachverhalt geschildert, bekam ich nach zehn Minuten die erste Antwort und zwei weitere Mails (insgesamt etwa eine halbe Stunde) später hatte ich einen Termin am nächsten Morgen für den Austausch der Platte. Versprochene Downtime war etwa zehn Minuten.

Den Termin und die Downtime hat Hetzner eingehalten. Die neue Platte mußte ich nur noch partitionieren und ins RAID einbinden. Fertig.

sfdisk -d /dev/sda | sfdisk /dev/sdb
mdadm --add /dev/md0 /dev/sdb1
mdadm --add /dev/md1 /dev/sdb2
grub-install /dev/sdb

Ich bin sehr zufrieden mit Hetzner. Von manch anderem Massenhoster kenne ich deutlich unprofessionelleres Vorgehen bei Hardwaredefekten dieser Art.

Smarthost signiert Mails mit DKIM

| No Comments | No TrackBacks

Die Produktionsserver relayen alle Mails an einen zentralen Mailserver, der dann den Versand übernimmt. Zusätzlich soll der Smarthost nun alle Mails mit DKIM signieren. Ich habe mich für den dkim-milter entschieden und die Umsetzung ging dank seiner Helferlein wie dkim-genkey recht schnell und einfach.

Damit der dkim-filter für mehrere Domains signierend tätig werden kann, muss man die Schlüsselinformationen in einer Datei hinterlegen und deren Pfad mit KeyList übergeben. Die Paramater Domain, KeyFile und Selector können ausschliesslich für Ein-Domain-Lösungen verwendet werden.

Die an KeyList übergebene Datei könnte beispielsweise wie folgt aussehen. Durch Doppelpunkte getrennt werden die für die Signierung zugelassenen E-Mail-Adressen, der Domainname (die Redundanz an dieser Stelle ist auffällig häßlich und historisch bedingt) sowie der Pfad zum keyfile angebenen. Die Angabe der Endungen .private und .pem ist optional.

relay:~# cat /etc/dkim-keys.conf
*@example.de:example.de:/etc/dkim/example.de/2009
*@example.com:example.com:/etc/dkim/example.com/2009
*@example.es:example.es:/etc/dkim/example.es/2009
*@example.fr:example.fr:/etc/dkim/example.fr/2009
*@example.nl:example.nl:/etc/dkim/example.nl/2009
*@example.pl:example.pl:/etc/dkim/example.pl/2009
*@example.se:example.se:/etc/dkim/example.se/2009

Nach den notwendigen DNS-Änderungen (TXT für z.B. 2009._domainkey.example.com) funktionierte der lokale Versand wie gewünscht und mit gültiger Signatur. Allein die (wichtigen) externen Hosts wurden ignoriert. Im Logfile sieht das dann so aus:

(unknown-jobid) external host www.example.com attempted to send as example.de

Ein Blick in die man-Page konnte auch dieses Problem lösen: Die Grundeinstellung sieht nur eine Signierung von Mails von 127.0.0.1 vor. Mit dem Aufrufparameter -i kann man den Pfad einer Datei übergeben, die pro Zeile eine IP-Adresse weiterer zustellender Systeme enthält.

relay:~# cat /etc/default/dkim-filter
# Command-line options specified here will override the contents of
# /etc/dkim-filter.conf. See dkim-filter(8) for a complete list of options.
DAEMON_OPTS="-i /etc/dkim-peers.conf"
[...]

relay:~# cat /etc/dkim-peers.conf
127.0.0.1
10.17.133.217

NFS: Permission denied wegen Group

| No Comments | No TrackBacks

RPC, das von NFS für die Identifikation von Benutzern verwendet wird, kann nicht mehr als 16 Gruppen, deren Mitglied ein Benutzer ist, übermitteln. Hier hatten wir den Fall, dass eine Benutzerin plötzlich nicht mehr auf Verzeichnisse zugreifen konnte, für die sie aufgrund der Gruppenrechte Zugriff haben sollte (Permission denied). Selbstverständlich waren die GID auf NFS-Server und Client-System identisch. Sie war kürzlich einer 17. Gruppe zugeordnet worden und die für den Zugriff relevante Gruppe (sie hatte die höchste Group-ID) wurde nicht mehr an den NFS-Server übertragen.

Das war extrem schwer zu debuggen. Die Ursache wurde erst nach Stunden gefunden. Der Sachverhalt wird hier ausführlich erläutert. Die Lösung war einfach: Es wurde die Option —manage-gids beim Start von rpc.mountd hinzugefügt.

Recent Comments

  • sw: Der Vorteil von sed ist in diesem Fall, dass man read more
  • sw: Es hat sich als sinnvoll herausgestellt, recycle:touch_mtime = Yes zu read more
  • sw: Die ausgetauschte Platte hatte gestern nach nur etwa einem halben read more
  • Makkus: Allerbeste Grüße aus Münster. Hatte heute morgen einen Ausfall meines read more
  • Hannes: Alda Hacker du! read more
  • Stefan: Hallo! tolle Anleitung, hat bei mir einwandfrei funktioniert :-) Danke read more
  • sw: Ich habe den Sachverhalt leider nicht abschließend klären können, da read more
  • vps: Hast du schließlich noch herausgefunden, woran genau es lag bzw. read more
  • uwe: Hi Das ganze geht auch relativ einfach mit sed (dürfte read more
  • Sven: Das Auskommentieren einer Zeile funktioniert - in Anlehnung an das read more