Now Loading ...
-
KI-Anrufe
Roboteranrufe
Ich will einen elektronischen Mönch haben, der diese Anrufe für mich annimmt.
– Roland Lichti, 2025
In den letzten Tagen habe ich mehrmals Anrufe von KI-Systemen erhalten. Mal haben sie Facebook, mal für Google gearbeitet. Beziehungsweise für Firmen, die mir Werbung dort verkaufen wollten.
Das Skript ist immer das gleiche: sie wollen die Einwilligung haben, dass ein “Berater” mich anrufen kann.
Ich habe nach der beauftragenden Firma gefragt, aber diese Antwort wird immer abgelehnt. Nach einem Hinweis auf unerlaubte Telefonwerbung wird dann der Anruf schnell und ohne weitere Informationen beendet.
Daher habe ich bei der Bundesnetzagentur Online-Beschwerde gegen diese Anrufe eingereicht. Bitte macht das auch alle, wenn Ihr solche Anrufe erhaltet. Wir haben bei Email erlebt, wie Spammer dieses schöne System zerstört haben.
Bis jetzt waren es die folgenden Nummern:
+49 231 22613175
+49 241 98090247
Warum kann es nicht so sein, dass alle Nummern, die für ausgehnde Werbeanrufe genutzt werden, bei der Bundesnetzagentur gemeldet und öffentlich mit ihren Betreibern gelistet werden müssen? Und wer seine Nummern nicht meldet, wird mit Bußgeldern in Höhe der 300.000 € belegt, die ja auch für unerwünschte Werbeanrufe verlangt werden könnten?
Und Telefongesellschaften, die solche Nummern trotz Beschwerde geschaltet lassen sollten ebenfalls dafür haftbar gemacht werden. Man könnte Firmen, die diese Werbeanruf-Regeln brechen, z.B. listen, dass diese einfach keine Telefonleitungen mehr bekommen (bzw. max. 1 Leitung für Notrufe).
-
-
-
UPS, Du hast es geschafft. Du bist für mich der mieseste Paketdienst, den ich kenne
Hallo UPS, wir müssen reden.
Ich habe am letzten Wochenende ein neues Tablett bestellt. Leider hat der Verkäufer sich entschieden, Dich mit der Lieferung zu beauftragen. Eventuell kannst Du ja etwas. Aber weder die Lieferung von Paketen noch das Einhalten von Versprechen seitens des Kundenservices scheinen dazu zu gehören.
Beim Anruf bei der Hotline unter 06966405060 um 15:02 hast Du mir einen Anruf der Zustellbasis spätestens in einer Stunde zugesagt. Mir wurde gesagt, der Fahrer habe versucht, das Paket in Heddesheim, Benzstraße 2 zuzustellen. Komisch, das UPS Customer Center in Großsachen-Heddesheim hat die Adresse Benzstraße 2. Ich wohne ja in Bensheim. Armer Fahrer, der versucht, ein Paket 2 Häuser von der Zustellbasis entfernt bei mir abzuliefern. Und das angeblich jeden Tag.
Ich war eher nicht überrascht, dass kein Anruf kam.
Gerade um 16:32 habe ich wieder angerufen und es wurde mir wieder ein Anruf oder gar eine direkte Auslieferung innerhalb von einer Stunde zugesagt. Ich werde überrascht sein, wenn ich das Paket noch vor dem nächsten Montag in den Händen halten werde.
Denn um 19:00 hört die Hotline auf, zu arbeiten. Und bis dahin werden sie pflichtbewusst Emails an die Zustellbasis schicken, die ihre Email pflichtbewusst erst wieder am Montag lesen wird. Weil das Büro
Update 18:43: Nach einem letzten Anruf um 17:38, bei dem ich vom Kundenservice ohne Gespräch an den technischen Service weitergeleitet wurde, habe ich diesem mitgeteilt, dass ich die ca. 25 km (ein Weg, also 50 km hin- und zurück) jetzt fahren werde um das Paket selbst abzuholen.
Natürlich war dies nicht weitergegeben worden und das Paket in einem Container, aus dem es nicht geholt werden könne.Mir wurde generös angeboten, ich könne am Montag das Paket abholen kommen (wieder 50 km für einen Job, für den UPS bereits bezahlt wurde vom Versender).
Ein Anruf bei der Hotline nach meiner Rückkehr ergab, dass man nichts tun könne, da inzwischen Feierabend sei. Mein erster Anruf war um 15:02, also weit vor dem Feierabend. Aber jetzt müsse ich - so schwer könne es doch nicht sein - einfach auf Montag warten. Das sei doch einfach.
Damit am Montag dann da "um einen Arbeitstag verzögert" wieder weitergeht, wie schon diese Woche seit Dienstag?
Auch sei ganz UPS im Feierabend und kein Supervisor mehr in ganz UPS, mit dem ich reden könne. Mir wurde höflich ein schönes Wochenende gewünscht und dann aufgelegt.
Und ja, nachdem ich heute mehrfach von der Hotline belogen wurde (versprochene Rückrufe und eine Lieferung noch heute Abend wie beim 16:32-Anruf sind Lügen, wenn sie nicht eingehalten werden) habe ich mal nach Bewertungen im Internet gesucht. Es ist kein Einzelfall ist ein System-UPS ...
Weitere Links zu diesem Thema:
</p>
https://de.trustpilot.com/review/www.ups.de
https://www.trustedshops.de/bewertung/ups-de
Google-Rezensionen
</ul>
</blockquote>
-
Liebe Post, wir müssen reden ...
Hallo Post!
Es gäbe viele Gründe, mit Dir zu reden, aber heute geht es um den Post-Ident.
Denn nach ca. 9 Minuten in der Warteschleife macht man alles, was euer Agent will. Leider musste ich noch ein zusätzliches Licht einschalten (USB-Kabel anschließen und die Lampe einschalten - ungefähr 15 Sekunden. Damit die Dame auch alle Hologramme sehen kann.
Und nach 14 Sekunden sagte die nette Dame "wenn es nicht geht, dann laden Sie bitte die Mobile App und versuchen Sie es damit" . Und 1 Sekunde später war das benötigte Licht da.
Also nochmal anrufen und warten. Hier ging es dann. Komischerweise musste ich hier die Karte dann genau in der Mitte mit zwei Fingern halten und Daten abdecken (aber natürlich nicht meine PIN, um die Online-Dienste zu sperren/entsperren - da war die Bundesrepublik ja so nett, diese PIN direkt auf den Ausweis zu drucken).
Wenn Du, liebe Post, deine Callcenter-Agents nach Zeit anstatt nach erfolgreich abgefertigten "Kunden" (Zahlvieh wäre hier wohl der Ausdruck, der zu Deinem Service passt) bezahlen würdest, dann könnten die Agents auch einen Service bieten, wie er einem guten seriösen Unternehmen anstehen würde.
Aber von diesem Image hast Du Dich ja leider schon länger getrennt - wie man am Paketdienst und den entsprechenden Beschwerden, den teilweise horrenden Brieflaufzeiten inklusive dem "wir liefern nur noch zweimal die Woche Post aus" und der Unmöglichkeit, hierzu einen Ansprechpartner zu finden, damit ihr nicht mehr behaupten könnt, ihr wüsstet von nichts, sehen kann. Aber das sind ja garnicht die Themen von heute.
Hochachtungsvoll,
Roland Lichti
-
Das negative spezielle antroposophische Prinzip
Da ich manchmal das negative spezielle antroposophische Prinzip anwende oder darüber rede, nutze ich diese Blogeintrag mal dafür, es zu erläutern. Es ist eigentlich ganz einfach.
Gemäß dem Allgemeinen Antroposophischen Prinzip
.. was wir zu beobachten erwarten können, muss eingeschränkt sein durch die Bedingungen, welche für unsere Gegenwart als Beobachter notwendig sind.
Brandon Carter, 1973, zitiert gemäß Wikipedia
lässt sich das auf das Spezielle Antroposophische Prinzip einengen:
... was wir zu beobachten erwarten können, muss eingeschränkt sein durch die Bedingungen, welche für meine Gegenwart als Beobachter notwendig sind.
Roland Lichti, nach 2005
Und das negative Antroposophische Prinzip spezifiert die erwarteten Beobachtungen und lässt sich einfach formulieren als:
Das Universum ist dazu geschaffen, mich zu ärgern.
Roland Lichti, nach 2005
Mit der Anwendung dieses Prinzips ist es möglich, die meisten Erlebnisse zu erklären und damit ein zufriedenes Leben zu leben.
-
OKD Cluster bei Hetzner
OKD4 wird langsam beliebt. Wer kein Geld für einen vollwertigen OpenShift Cluster hat, hat hier eine Chance auf den Service. Für VMs auf Basis von oVirt gibt es auf GitHub einige Projekte, die Installationen durchführen. Wie man aber einen Cluster auf echten Maschinen bei Hetzner aufbaut, muss man sich zusammensuchen.
Ich habe bis for ein paar Tagen einen OKD 3.11 Cluster als Single-Node-Cluster auf einem Node laufen gehabt. 64GB Hauptspeicher, 16 Cores. Da ich sowieso auf OKD 4 wechseln wollte und die Leistung am Ende war, habe ich mir jetzt drei Server mit jeweils 12 Cores und 64 GB Hauptspeicher geholt. Platten sind jeweils 500 GB enthalten. Als Loadblancer habe ich mir auf der Serverbörse zwei kleine Maschinen geschossen, die jeweils per haproxy auf den Cluster loadblancen. Außerdem werde ich dort ein paar weitere Dienste installieren, die nicht in Kubernetes laufen oder ich nicht dort haben will.
Außerdem habe ich mir temporär eine weitere Maschine als bootstrap-Maschine geholt - ich wollte eigentlich eine der kleineren Maschinen dazu nutzen, aber leider gab es da Netzwerkprobleme, da die entsprechende Netzwerkkarte von Fedora CoreOS nicht unterstützt wurde.
Ich hatte zwei Probleme:
Die Maschinen kamen alle als "localhost" hoch. Ich habe das dann so gelöst, dass ich per Ignition-File den einzelnen Hosts ihren Namen vorgegeben habe. Hat nur nichts gebracht. Also habe ich - sobald der Host hochkam - mit "hostnamectl" den Namen gesetzt und neu gestartet. Nachdem ich mit allen 3 Nodes durch war, musste ich nur noch per "oc delete node localhost" den blöden Zusatzeintrag loswerden.
Ich habe zwei Rechner im gleichen Subnetz bekommen. Dummerweise denken die dann, sie wären direkt über das Netz erreichbar, Hetzner verbietet aber natürlich direkte Kommunikation. Da ich mich mit Fedora CoreOS nicht ganz so gut auskenne und es nicht hinbekommen habe, diese Route zu ersetzen, habe ich mittels systemd-Timer einen Service aufgesetzt, der auf den Nodes alle 2 Sekunden die per DHCP gesetzte Route lösche und eine direkte Route zum Gateway setze.
Der Netzwerk-Workaround ist doof, aber die übliche dokumentierte Lösung über <interface>.connection-Datei hat bei mir nicht gegriffen.
-
MTBF und MTTR - Hä?
Nachdem ich diese Diskussion in der letzten Zeit mehrfach geführt habe, gehe ich im Rahmen eines Block-Posts auf diese strukturelle Änderung vom Rechenzentrums-IT-Betrieb auf Cloud-IT-Betrieb ein. Ich mache es an Rechenzentrum und Cloud fest, obwohl es eigentlich ein Paradigmenwechsel ist, der durch die erhöhte Automatisierung im Betrieb von Anwendungen und Systemen bedingt ist.
Aber zuerst einmal: was ist MTBF und MTTR?
Unter MTBF versteht man die Mean Time Between Failure - also die durchschnittliche Zeit zwischen zwei Ausfällen eines Systems.
Die MTTR oder Mean Time To Repair ist die durchschnittliche Zeit, die benötigt wird, um ein System bei einem Ausfall wieder zu reparieren (also verfügbar zu machen).
Nur damit wir komplett sind gibt es auch nocht die MTTF, die Mean Time to Failure. Also die Durchnittszeit bis zum Versagen. Als Formel ließe ich das Verhältnis der drei Betriffe als MTBF = MTTF + MTTR beschreiben. Aber ich werde die MTTF hier nicht weiter betrachten.
IT-Bertrieb in der Vergangenheit
Früher wurde der IT-Betrieb rein kostenbasiert betrachtet. Er sollte eine definierte Leistung bei minimalen Kosten liefern. Und die definierte Leistung war der störungsfreie Betrieb der Systeme. Damit war die Optimierung der IT klar: die Vergrößerung der MTBF, Denn jede einzelne Störung hat Kosten verursacht. Die Idee ist ja auch nicht schlecht, aber jeder erinnert sich noch an das Rennen zu den 5x9 (also 99,999% Verfügbarkeit). Jede 9 nach dem Komma vervielfacht die Kosten.
Trotzdem war es das Ziel, die MTBF zu optimieren. Hierzu dienten die allzeit bekannten Lessons learned und Root Cause Analyse bei jedem einzelnen Vorfall.
Was hat sich geändert?
Die IT ist schneller geworden. Sie ist inzwischen ein zentraler Bestandteil vieler Unternehmen, seien es Serviceunternehmen, die ihre Dienste in Software abbilden (wie z.B. Versicherungen oder Banken bei Vertragsabschlüssen oder Schadensabwicklungen, die oft genug ohne menschliche Intervention seitens des Unternehmens erfolgen) oder um die Firmware von Geräten oder Backendservices der Handy-Apps.
Software ändert sich nicht mehr im Quartals- oder Monatsrhythmus. Oft wird wöchentlich, täglich oder gar mehrfach am Tag eine neue Softwareversion eingespielt (ok, Firmware ist hier noch nicht ganz so weit, aber IoT ebnet dem schnellen Wechsel auch hier den Weg).
Und wo kommt hier die MTTR ins Spiel?
Um dem schnellen Wandel zu managen, gehen viele IT-Betreiber "in die Cloud" oder bauen eine eigene "private Cloud" auf. Aber in der Cloud ändert sich etwas: wurden vorher noch die Rechenzentren (angefangen von den Netzwerkkomponenten, den Servern, Betriebssystem bis hin zur Laufzeitumgebung) an die Anwendungen angepasst, so diktieren nun die Clouds die komplette Laufzeitumgebung und die Anwendungen werden entsprechend umgebaut oder neu entwickelt. Containerisierung ist hier das Zauberwort und Mittel der Stunde. Aber darüber soll es hier nicht gehen.
Kontrollverlust
Es ist etwas anderes passiert, dass viele übersehen haben: Einflussverlust. Die Anwendungen haben die Kontrolle über die Infrastruktur verloren. Bei einer "Public Cloud", also das vollständig virtuelle Rechenzentrum bei einem Cloud-Anbieter wie AWS, Azure oder Google, hat der Kunde nur noch extrem wenig Einfluss, was im Netzwerk, der RZ-Technik oder den Servern passiert. Während im eigenen Rechenzentrum meist schon vor dem Ausfall einer wichtigen Infrastrukturkomponente die Warnsignale angehen, bekommt man davon "in der Cloud" nichts mit. Aber "die Cloud" heißt nur "Computer eines anderen Unternehmens", sie ist keine abstrakte Konstruktion im Nirvana. Auch dort gibt es Ausfälle. Für den Nutzer kommen sie nur unvorhergesehen. Natürlich sind die Cloudbetreiber sehr gut im Betrieb der Komponenten, auch sie mögen keine Ausfälle. Aber zählt mal die beteiligten Komponenten bei einer cloudbasierten Anwendung und bei einer altertümlichen RZ-basierten Anwendungen. Man wird sehen, dass die Cloud als verteiltes System in fast allen Fällen komplexer ist als die alten "on premise"-Systeme.
Der Winter.....Fehler kommt!
Es wird also zu vorher nicht absehbaren Fehlern kommen. Fehler, deren Auftreten man nicht beeinflussen kann. Man hat die Kontrolle über die MTBF verloren. Was bleibt? Man muss die MTTR optimieren. Wenn man weiß, dass Fehler passieren und man keine Chance hat, sie wegzuoptimieren oder zumindest zu reduzieren, dann muss man lernen, diese Fehler möglichst schnell in den Griff zu bekommen.
Lessons Learned und die Root Cause Analyse sind übrigens nicht mitgestorben. Aber sie werden nicht jedes Mal gemacht. Erst wenn die gleiche Störung mehrfach auftaucht, oder der Eindruck entsteht, dass verschiedene Störungen einen gemeinsamen Grund haben, kommen sie zum Einsatz.
Die Königin MTBF ist tot, hoch lebe die neue Königin MTTR.
-
Entschlackung des Eingangsbereichs
Da ja die Chancen steigen, in der nächsten Zeit (lies: vielleicht noch dieses Jahr) das Paladins Inn wirklich eröffnen zu können, muss ich auch unten etwas klar Schiff schaffen. Da stapelten sich noch einige Kartons, meine neue Klimaanlage für das Büro (laut, aber im Sommer hier notwendig) sowie ein paar Werkzeuge.
Außerdem war da noch das 19"-Rack, das ich extra gekauft habe, um den "IT-Haufen" in meinem Hochschrank loszuwerden. Heute war der Zeitpunkt, es einzubauen. Und nach nur 30 min war es geschafft. Es ist zwar nur ein 19"-Gerät drinnen, aber wenigstens stapeln sich die anderen Geräte jetzt nicht mehr. Man glaubt ja nicht, was man alleine für ein schönes Netzwerk braucht.
Außerdem habe ich mir die Zeit genommen, endlich einige Kartons zu zerschneiden und in den Papiermüll zu schmeißen. Jetzt muss nor noch die Schneeschaufel und das andere Werkzeug in die Garage und der Eingangsbereich ist in Ordnung.
Als nächstes muss ich die letzten verbliebenen Umzugskartons in den Keller schaffen und das zweite Warenregal im Studio aufbauen, damit die Sachen etwas ordentlicher verstaut werden können. Außerdem würde ich gerne noch ein 3. Regal in Reihe stellen, da passt aber nur noch ein halbes ...
Aber vorher kommt am Samstag noch das neue Gästebett, dass natürlich dann auch aufgebaut werden muss.
Persönliches
· 2021-06-03
-
-
Logging in Java II
Vor über 4 Jahren habe ich bereits im Beitrag Logging in Java ein paar grundlegende Gedanken zum Thema Logging geäußert. Dabei bin ich recht unspezifisch geblieben und habe nur ein paar grundlegende Ideen geäußert. Diesmal will ich etwas konkreter werden.
Erstmal wie man Logifles schreibt. Hier nutze ich inzwischen immer Slf4j. Mit dieser Fassade kann das eigentliche Logging über alle Logframeworks geleitet werden, ohne dass Änderungen am Quellcode notwendig werden. Es ist ein No-Brainer. Alles Logging über Slf4j, keine Diskussion darüber. Jede Klasse bekommt entweder einen statischen Logger (z.B über die Templating-Funktion der genutzten IDE bei der Erstellung des Files) oder - deutlich eleganter - mittels Der Annotation @Slf4j via lombok. Eigentlich ist es egal, aber ich sage wieder: nutzt lombok. Punkt.
Nachdem das geklärt ist, wird es jetzt etwas diffiziler. Hier kommt jetzt das Fingerspitzengefühl. Ich orientiere mich an den üblichen Logleveln, die via Slf4j addressiert werden können und dann auf die entsprechenden Loglevel der Frameworks verteilt werden:
Fehlerlevel
Beschreibung
Auswirkung auf 24/7 Betrieb
Error
Ein technischer Fehler ist passiert. Das System könnte dauerhaft gestört sein und ein Administrator sollte darauf reagieren.
Hier ist ein Bereitschaftsfall gegeben.
Warn
Ein technischer Fehler ist passiert. Ein einzelner Aufruf oder Batch wurde gestört und ein Administrator sollte sich das einmal anschauen.
Hier ist keine sofortige Reaktion notwendig. Es reicht, dass sich ein Admin das zu normalen Arbeitszeiten anschaut.
Info
Ein einzelner Aufrag oder Batch wurde bearbeitet. Auch fachliche Fehler werden auf diesem Level protokolliert. Wenn die Aufruffrequenz es zulässt, kann z.B. der Start und das Ende jedes Systemaufrufs protokolliert werden, sollte das Logvolumen dadurch zu hoch sein, wird nur das Ende quitiert.
Kein Fehler. Das Logvolumen sollte möglichst gering sein und nur die eigentlichen Transaktionen sichtbar sein.
Debug
Der Ablauf eines Auftrags oder Batches wird sichtbar. Mindestens der Aufruf eines Services wird zusätzlich zum Ende protokolliert (wenn es nicht sowieso schon auf Info-Level passiert).
Wenn ein System auf Fehlverhalten geprüft wird, sollten die Debug-Meldungen dem 2nd-Level-Support helfen, den Systemstatus zu erfassen und möglicht den Fehler zu analysieren und zu beheben. Das Logvolumen ist deutlich erhöht.
Trace
Der Ablauf eines Auftrags oder Batches wird detailiert protokolliert.
Hier kann der komplette Ablauf detailiert verfolgt werden. Es handelt sich um massives Logging, das dem 3rd-Level oder Lieferanten des Systems eine vollständige Analyse ermöglicht.
Natürlich sind diese "Regeln" abhängig von der Erfahrung, man muss lernen, zu antizipieren, was dem Betrieb hilft. Im Gegensatz zu meinem Artikel von 2017 bin ich inzwischen nicht mehr der Meinung, dass der Softwareentwickler selbst ein Addressat des Loggings ist. Als Entwickler nutze ich den Debugger bei der Entwicklung, das Logfile gehört dem Betrieb.
Sollte ein fachliches Logging gebraucht werden, kann man einen zusätzlichen Logger zusätlich definieren (z.B. als "bussiness" oder "bus" - dort können dann die fachlichen Logfiles geschrieben werden. Hier fällt normalerweise deutlich weniger Logfile an und es werden meist nur die Level Error, Info und Debug benötigt.
-
-
Arbeiten auf Treibsand
In der IT werden seit etwa 20 Jahren Organisationsformen ausprobiert. Einige haben sich bewährt, viele sind jedoch gescheitert. Aber sie werden meistens nur als Methoden der Softwareentwicklung gesehen. In anderen Bereichen haben sich Trends wie NEW WORK in die gleiche Lücke geschoben. Aber allen ist gemein, dass sie versuchen, ein Problem zu lösen, das unlösbar scheint.
Spätestens im letzten Jahr hat sich für viele Menschen die Arbeitswelt deutlich geändert. Während einige Arbeitsbereiche sich nicht ändern können (LKW-Fahrer und Feuerwehr können nicht im Homeoffice arbeiten) ist in den Verwaltungen des Unternehmens ein jahrzentelanger Mythos gebrochen worden. Viele Chefs waren davon überzeugt, dass die Mitarbeiter nur auf der Couch schlafen würden, wenn sie nicht im Büro sitzen würden. Sie hatten Angst vor Kontrollverlust. Durch die SARS-Covid-Pandemie blieb vielen Chefs nichts anderes übrig, als sich darauf einzulassen.
Aber die Welt hat sich geändert. Stand man früher noch auf festen Boden und konnte agieren, weiß man heute nicht, wann sich als ewig geglaubte Grundsätze als alt erweisen. Es ist so, als würde sich der stabile Fels von gestern in Treibsand verwandelt haben. Nichts ist mehr sicher.
Die meisten Verwaltungen funktionieren weiter. Es gab (und gibt) noch Reibungsverluste, liebgewonnene Gewohnheiten und Arbeitsweisen funktionieren so nicht mehr und man muss neue Wege finden. Aber es finden sich Wege. Die Situation hat eines gezeigt: die oft beschworene "Business Resilienz" greift zu kurz. Das haben wir in der IT schon die letzten Jahre vor allem durch das Aufkommen der Cloud lernen müssen und viele Firmen haben diese Umstellung noch nicht abgeschlossen. Aber gehen wir ein paar Schritte zurück. Früher hatten IT-Abteilungen oft eine Zahl, an der sie gemessen wurden: die MTBF ("Mean Time Between Failure", auf Deutsch: "durchschnittliche Zeit zwischen Fehlern"). Die musste so hoch wie möglich sein. Fehler sollten immer ausgeschlossen sein. Ich muss sagen, in vielen (vor allem Manager-Köpfen) ist dies auch heute noch die wichtigste Kennzahl: "Die IT muss funktionieren, wir können uns keine Fehler erlauben."
Dabei wird eines total vergessen: wir haben nicht alles unter Kontrolle - und in einer Cloud-Welt noch viel weniger als früher. Und selbst wenn wir es unter Kontrolle hätten: Fehler passieren. Jemand ist auf dem falschen Server angemeldet und fährt ihn herunter, eine Sicherung im Rechenzentrum brennt durch, der inzwischen sprichwörtliche Bagger trennt das Daten- oder Stromkabel. Es ist eine Tatsache, dass niemand vollständig ausschließen kann, dass ein Fehler passiert. Soll man verzweifeln? Eher mal schauen, was man sonst machen kann. Zusammen mit der Cloud kam in der IT eine weitere (schon alte) Kennzahl mehr ins Zentrum: MTTR ("Mean Time To Repair", auf Deutsch: "durchschnittlicher Fehlerbehebungszeitraum"). Man akzeptiert also, dass Fehler und Störungen auftreten werden. Wichtig ist, die Technik und Organisation so aufzubauen, dass sie Fehler schnell korrigiert und damit die Störungen beheben kann. Je kleiner diese Zahl ist, desto besser ist die Organisation. Da man Störungen niemals ausschließen kann, muss man mit ihnen leben und sie schnell ausschalten. Wer darin so gut ist, greift sogar zu Methoden wie Chaos-Engineering oder Monkey-Testing. Dabei wird in der Produktion wild von einem unbeteiligten Techniker irgendeine Technikkomponente (oder mehrere) deaktiviert oder gestört und damit das System für die "natürlich" auftretenden Fehler optimiert - denn ohne diese Fehler weiß man nicht, worauf man sich vorbereiten muss.
Wenn wir aus der IT ausbrechen und uns in die Firmenorganisation begeben, wäre das so als würde der Chef einfach mal ins Büro einer wichtigen Mitarbeiterin gehen und sie für 4 Wochen in den Urlaub schicken. Am besten die Projektleiterin eines Projektes, dass in 3 Wochen geliefert werden muss. Ein Unternehmen, dass damit umgehen kann, wird auch den Autounfall einer Schlüsselperson überstehen.
Viele belassen es dann dabei. Aber damit ist nur der halbe Weg beschritten. Hier wird nur mit Fehlern umgegangen. Aber wenn man es weiterbetrachtet, sollte man hier nicht stehen bleiben, sondern die Organisation so gestalten, dass sie mit dem Treibsand von oben umgehen können. Damit kann man meist unerkannte Potentiale heben. Denn der Treibsand fordert Vernetzung. Konnte auf dem Stabilen Grund jeder vor sich hinarbeiten und man kam dann doch am Ziel an, versinkt man im Treibsand, wenn man sich kein Netz geschaffen hat. Dieses Netz hilft einem, nicht im Treibsand zu versinken. Je mehr "Seilschaften" in verschiedenste Bereiche einer Organisation man sich geschaffen hat, desto sicherer ist man, nicht im Sand zu versinken. Was früher einen faden Beigeschmack hatte und nach Vetternwirtschaft klang, ist heute notwendig und dient der Organisation. Jeder muss auf Informationen reagieren und immer mitdenken, sonst bemerkt man nicht einmal, dass die Organisation schon fast im Sand versunken ist. Aber wenn jeder wie eine Spinne im Netz sitzt und das Zittern spürt, kann man reagieren und schnell genug die Kollegen aus dem Sand ziehen, bevor alle feststecken.
Aber bevor ich jetzt zu weit abdrifte, belasse ich es heute dabei. Der nächste Gedankensplitter kommt bestimmt ...
-
GitHub Runner in OpenShift/kubernetes
Nachdem wir uns im letzten Teil das quay.io-Container-Repository aufgesetzt haben Das Aufsetzen der OpenShift basierten GitHub Action Runner ist der letzte Schritt, den wir noch brauchen, um die CI-Pipeline fertig zu haben. Und darum kümmern wir uns in diesem Artikel.
Ich gebe zu, ich habe es mir eigentlich recht bequem gemacht, denn hier haben schon meine Kollegen von Red Hat sehr viel vorbereitet. Es gibt ein Helm-Chart und selbst einige Runner haben sie schon im Angebot. Nur beim Java-Runner musste ich etwas nacharbeiten, da ich für Vaadin neben dem Java 11 auch noch node.js im gleichen Runner gebraucht habe (Vaadin kompiliert den Frontendteil zusammen mit dem Java-Anteil). Aber dazu kommen wir später.
Als erstes brauche ich ein Projekt in meiner OpenShift-Umgebung (nunja, ich nutze hier meine OKD-Installation, die noch immer in Version 3.11 läuft, aber wenn Ihr ein OpenShift 4 habt, sollte es nicht viel anders sein).
$ oc new-project github-runner
Now using project "github-runner" on server "https://console.das.wuesstet.ihr.wohl.gerne:8443".
You can add applications to this project with the 'new-app' command. For example, try:
oc new-app django-psql-example
to build a new example application in Python. Or use kubectl to deploy a simple Kubernetes application:
kubectl create deployment hello-node --image=gcr.io/hello-minikube-zero-install/hello-node
$
Natürlich muss man dazu eingeloggt sein. Aber wie das geht, wisst Ihr bei eurem Cluster selbst.
Da builda-sa mit erhöhten Rechten laufen muss, erledige ich das hier gleich. Immer daran denken, dass dies natürlich ein Bruch der Security-Sandbox ist. Ob ihr dazu bereit seid, müsst Ihr selbst abwägen.
$ oc create -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: buildah-sa
EOF
$ oc adm policy add-scc-to-user privileged -z buildah-sa
securitycontextconstraints.security.openshift.io/privileged added to: ["system:serviceaccount:github-runner:buildah-sa"]
$
Jetzt haben wir den privilegierten Benutzer, damit buildah seine Arbeit verrichten kann. Allerdings brauchen wir noch ein GitHub PAT (personal access token) - ich verweise aus Faulheit auf die recht gute Dokumentation seitens GitHub mit der Anmerkung, dass wir mindestens die Berechtigung repo benötigt. Dieses Token und den Eigentümer des GitHub-Repositories schreiben wir uns mal in Umgebungsvariablen, denn wir werden sie öfter benötigen:
$ export GITHUB_PAT=<Personal Access Token>
$ export GITHUB_OWNER=<Eigentümer eures Repositories>
$ export GITHUB_REPO=<Repository>
Den letzten Eintrag braucht Ihr nur, wenn ihr den Runner an ein bestimmtes Repository binden wollt. Wenn Ihr es euch für mehrere Projekte einfach machen wollt, legt bei GitHub eine Organisation an und bindet die Runner an die Organisation - dann könnt ihr die Repos innerhalb der Organisation mit den gleichen Runnern glücklich machen und müsst nicht jedes Repository bei GitHub einzeln mit Runnern bespaßen.
Wir brauchen natürlich das Helm-Chart, um die Runner zu installieren. Das bekommen wir auch von GitHub:
$ helm repo add openshift-actions-runner https://redhat-actions.github.io/openshift-actions-runner-chart
$
Jetzt kann der Spaß losgehen:
$ helm upgrade --install buildah-runner openshift-actions-runner/actions-runner --set-string githubPat=$GITHUB_PAT --set-string githubOwner=$GITHUB_OWNER --set-string runnerImage=quay.io/redhat-github-actions/buildah-runner --set-string privileged=true --set runnerLabels="{podman,buildah}"
$ helm upgrade --install java-runner-11 openshift-actions-runner/actions-runner --set-string githubPat=$GITHUB_PAT --set-string githubOwner=$GITHUB_OWNER --set-string runnerImage=quay.io/klenkes74/java-runner-with-maven --set-string runnerTag=latest --set runnerLabels="{java,java-11,maven,gradle,ant,ivy}"
$
Ihr könnt auch gerne noch die anderen Runner der Red Hat Community of Practice installieren - es gibt noch einen allgemeinen Runner, einen Runner für node.js und einen k8s-tools-runner, damit Ihr per GitHub-Action auch direkt euren Cluster konfigurieren könnt. Aber für unsere Anwendung benötigen wir nur den Java- und den buildah/podman-Runner.
Dem aufmerksamen Leser wird auch noch etwas am java-runner-11 aufgefallen sein. Er lädt kein Image von quay.io/redhat-github-actions (wo es auch einen java-11-runner gibt), sondern einen von mir bereitgestellten Runner von quay.io/klenkes74/java-runner-with-maven. Das liegt daran, dass der normale Runner kein Maven beinhaltet. Und weil ich gerade dabei war, habe ich noch gradle, ant und ivy und - wie oben schon erwähnt - node.js mit reingeworfen. Wider erwarten findet er auch bereits bei mir unbekannten Projekten innerhalb von AWS Anwendung - wie ich durch die quay.io-Statistiken erfahren durfte. Wer sich für den Runner und seinen Bau interessiert, kann sich das auf https://github.com/klenkes74/java-runner-with-maven anschauen. Vielleicht schreibe ich da auch einen Blog drüber - aber eigentlich ist das nur ein Dockerfile, dass diverse Sachen nachinstalliert.
Und jetzt sollten die Pods laufen und sich zu Github verbinden. Auf OpenShift-Seite sieht es ungefähr so aus:
$ oc get pod
NAME READY STATUS RESTARTS AGE
buildah-runner-774f989c86-sc96s 1/1 Running 0 13d
java-runner-11-79f5c4f49d-rlmjt 1/1 Running 0 20h
$
Und auf Github findet man es unter den Settings (bei mir natürlich bei der Organisation und nicht im Repository):
Voilá, die Github-Runner laufen und verrichten ihre Arbeit. Ihr könnt euch auch über die OKD-Console den Output anschauen:
Jetzt haben wir alles zusammen. Ein Push in die konfigurierten Branches (bei mir development) wird den Workflow CI auslösen und am Schluss liegt dann - wenn alles funktionierte - das Container Image im quay.io-Repository.
Inzwischen habe ich im Projekt noch CodeQL-Checks hinzugefügt und einen zweiten Workflow für Releases auf den main-Branch gesetzt. Aber das ist dann nochmal ein neues Thema.
Ich wünsche allen viel Spaß mit Github-Actions ohne dabei auf die Minuten schauen zu müssen.
-
Aufsetzen des quay.io-Repositories
In diesem Teil der Artikelserie befassen wir uns, nachdem das GitHub-Source-Repository existert, mit dem quay.io-Repository und dem Einrichten eines Robot-Benutzers für die Nutzung durch GitHub Actions.
Als erstes braucht man natürlich einen Account auf quay.io. Wie das geht, findet Ihr ganz leicht selbst heraus. Im April ändert sich auch etwas, quay.io wird in das SSO-System von Red Hat eingebunden. Daher macht es keinen Sinn, die notwendigen Schritte hier zu erklären.
Das Anlegen eines neuen Repositories ist recht einfach:
Anlegen eines neuen Repositories in quay.io
Entweder man klickt einfach im Bildschirm auf das "+ Create New Repository" oder öffnet mit dem "+" oben das Menü und wählt dann "New Repository".
Im folgenden Screen kann man dann den Namen des Repositories wählen Den typ lässt man unverändert, wir wollen ja ein Container Image Repository. Außerdem ist es extrem wichtig, es auf öffentlich zu schalten!
Der Größte Teil ist damit schon erledigt. Jetzt brauchen wir noch einen Benutzer für GitHub und müssen ihm Zugriff auf dieses Repository erteilen. Dazu klicken wir auf unseren Usernamen:
Und in den sich öffenen Settings legen wir in den "Robot Accounts" durch das "+ Create Robot Account" einen neuen Account an.
Es öffnet sich ein Requestor, der nach Accountnamen und einer Beschreibung fragt:
Anlegen eines neuen Roboter-Accounts
Sobald man auf "Create robot account" klickt, öffnet sich die Seite, auf der man diesem Account die Berechtigung auf Repositories erteilen kann:
Einfach die gewünschten Repositories mit "Write"-Rechten ausstatten und der Account wird funktionieren. Auf den neuen Button "Add permissions" klicken (der Button erscheint erst, wenn man auch eine entsprechende Permission ausgewählt hat). Und der Account wurde angelegt und taucht in der Liste der Robot Accounts auf. Den Token kann man einsehen, in dem man in der Liste das Zahnrad ("Settings") und dann dort "View Credentials" auswählt:
Es öffnet sich wieder ein Requester und man sieht gleich den notwendigen Username und das Token für die GitHub-Secrets (siehe den letzten Artikel dieser Serie, wie er als Secret in GitHub hinterlegt werden kann).
Damit ist quay.io vorbereitet.
-
-
Aufsetzen des GitHub-Repositories
In diesem Teil der Artikelserie befassen wir uns mit dem GitHub-Repository und natürlich dem Workflow für die GitHub Actions.
Außerdem verliere ich ein paar Worte über den Maven-Build und die Integration des Helmcharts in diesen Build.
Als erstes braucht man natürlich einen Account auf GitHub. Ich gehe aber mal davon aus, dass diese Hürde bereits genommen ist und in der Softwareindustrie eigentlich jeder schon über einen GitHub-Account verfügt. Aber selbst wenn dies nicht der Fall sein sollte, ist es ganz leicht, dazu braucht ihr meine Hilfe nicht.
Auch das Anlegen eines neuen Repositories ist ganz einfach.
Anlegen eines neuen Repositories. Auf das "+" klicken und dann "New repository" anwählen.
Danach öffnet sich eine neue Seite und man kann den gewünschten Namen eingaben. Hier kann man auch seine Lizenz aussuchen oder es auch sein lassen.
Der Software-Build mit Maven
Danach hat man sein Repository und kann die Software dorthin installieren. Ich benutze mein Repository unter https://github.com/Paladins-Inn/delphi-council als Beispiel und gehe damit von einem Maven-Build (gesteuert durch die Datei pom.xml im Hauptverzeichnis) aus. Es handelt sich um eine Spring Boot Anwendung. Außerdem nutze ich Maven, um ein paar Parameter im Helm-Chart zu ersetzen. Die Sourcen des Helm-Charts leben unterhalb von ./src/main/helm und werden durch die Maven-Konfiguration nach ./target/helm installiert:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
...
<resources>
...
<resource>
<directory>src/main/helm</directory>
<targetPath>../helm</targetPath>
<filtering>true</filtering>
</resource>
...
</resources>
...
</build>
...
</project>
Da es sich um ein Spring-Boot-Projekt handelt, muss man aufpassen. Normalerweise ersetzt Maven Variablen im Format "${VARIABLENNAME}" - Spring-Boot Starter definieren es um und man muss die Notation "@VARIABLENNAME@" nutzen. Der targetPath oben ist wichtig, da für das Maven Resource-Plugin der Standard-Ausgabepfad ./target/classes ist. Mit dem <targetPath>../helm</targetPath> wird daraus ./target/helm.
Ich nutze die Ersetzung vor allem, um die aktuelle Versionsnummer in die Chart.yaml zu bekommen. Das gleiche Spielchen mache ich übrigens auch mit dem Dockerfile, dass hier unter ./src/main/docker liegt:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
...
<resources>
...
<resource>
<directory>src/main/docker</directory>
<filtering>true</filtering>
</resource>
...
</resources>
...
</build>
...
</project>
Hier akzeptiere ich jedoch, dass das Dockerfile nach ./target/classes kommt. Damit habe ich das Dockerfile auch im jar-Archiv und damit weiß jeder, wie die Software gebaut wurde. Das gefällt mir persönlich. Aber Ihr könnt gerne auch einen anderen targetPath konfigurieren. Bleibt aber unterhalb von ./target, damit mvn clean alles automatisch wegräumt.
Der Rest des Maven Buildfiles befasst sich mit dem Softwarebuild, der neben dem Javabuild auch einen npm-basierten Build für das Frontend umfasst. Das kommt aber so mit dem von mir verwendeten Framework Vaadin und eine nähere Besprechung würde endgültig den Rahmen dieser Artikelserie sprengen. Ihr könnt aber die Entwicklung der Software in meinen Live-Codings auf Twitch (und später auf Youtube) gerne verfolgen.
Auch die Software-Tests sollen hier während des Maven-Builds stattfinden, sodass wir uns beim Workflow nicht weiter darum kümmern müssen.
Der GitHub Workflow
Und zum Workflow kommen wir jetzt. Er versteckt sich in der Datei ./github/workflows/ci.yml. Natürlich kann man mehrere Workflows haben. Einfach eine weitere Datei daneben legen und es sind schon zwei Workflows. Aber wir werden uns die vorhande Datei mal von Oben nach unten anschauen.
## This is basic continuous integration build for your Quarkus application.
name: CI
on:
push:
branches: [ main ]
jobs:
Der Kopf der Datei. Hier definieren wir den Namen des Workflows, wie er uns später auch von GitHub angezeigt wird.
Außerdem definieren wir, wann der Workflow ausgeführt werden soll. Hier soll er bei jedem Push im Branch main ausgeführt werden. Damit ist dies eigentlich kein CI-Workflow mehr sondern eher ein Release-Workflow. Aber wenn man unter branches noch development eintragen würde, wäre es wieder ein CI-Workflow, ignorieren wir diese Information also erstmal. Denn jetzt kommen die Definitionen der jobs. Jobs sind die Schritte im Workflow, die jeweils einem Runner zugewiesen werden können. Dazu nutzt man den Parameter runs-on und definiert dann die einzelnen Schritte (steps), die auf diesem Runner ausgeführt werden sollen:
jobs:
java-build:
runs-on: [ java ]
steps:
...
container-build:
runs-on: [ podman ]
needs: java-build
steps:
...
Dieser Workflow hat also zwei Schritte (java-build und container-build). Diese habe ich so gewählt, da ich einen runner für java (mit maven, gradle und wie oben beschrieben auch node.js) und einen Runner für buildah und podman (für Containerbuilds und Management) habe und diese so den verschiedenen Runnern zuweisen kann.
Und mittels des needs-Eintrags sorge ich dafür, dass cer container-build nicht parallel startet sondern erst nach dem java-build, da der Container natürlich die Software benötigt, die dort gebaut wird.
Der Java-Build-Teil des Workflows
Schauen wir uns den Java-Build an, zerfällt er wieder in drei Teile. Zuerst wird der Build vorbereitet, indem die Sourcen aus git ausgecheckt werden (mit der Aktion actions/checkout@v2) und die Java-Umgebung wird vorbereitet (Aktion actions/setup-java@v1). Die einzelnen Steps haben Namen und Ids. Welche Aktion genutzt wird, wird per uses definiert und mittels with werden die Aktionen mit Parametern versehen. So wähle ich per java-version hier Java11 aus.
jobs:
java-build:
runs-on: [ java ]
steps:
- name: Checkout sources
id: checkout-sources
uses: actions/checkout@v2
- name: Set up JDK 11
id: setup-java
uses: actions/setup-java@v1
with:
java-version: 11
Nachdem die Umgebung vorbereitet ist, können wir jetzt den Java-Build starten. Hierzu rufen wir Maven auf:
- name: Build
id: build-java
run: mvn package -B -Pproduction
Damit ist der Maven-Build gelaufen und unsere Artifakte liegen alle unterhalb des Verzeichnisses ./target. Ja, durch die oben beschriebenen Änderungen am pom.xml, nutzen wir drei Artifakte:
Das jar-File mit der Anwendung ./target/delphi-council-is-@project.version@.jar (um in der Spring-Schreibweise zu bleiben)
Das Dockerfile ./target/classes/Dockerfile
Das Helmchart ./target/helm/delphi-council-is
Die Versionsnummer will ich nur an einer Stelle pflegen: im Maven-Buildfile unter project->version. Daher habe ich ja die Resourcen definiert, die diese Version überall hin ersetzen. Und die Version brauche ich mindestens für das Sichern der Artefakte auf GitHub. Daher extrahiere ich den APP_NAME und die APP_VERSION aus dem Dockerfile und merke mir die Variablen als IMAGE und VERSION in den $GITHUB_ENV (diese werden bei jedem step ins Environment geschrieben und machen diese als ${{ env.IMAGE }} und ${{ env.VERSION }} für die steps verfügbar. Ich gebe diese Variablen als Information aus und nutze sie zum Upload nach GitHub. Hierzu nutze ich die Aktion actions/upload-artifact@v2 mit der Liste der Dateien, die zu sichern sind. Damit wird ein Archiv mit dem konfigurierten Namen (dci) erstellt. Die Besonderheit ist, dass der gemeinsame Pfad der Dateien möglichst weit gekürzt wird. Im Archiv stehen also nicht die Dateien ./target/delphi-council-is-${{ env.VERSION }}.jar, ./target/classes/Dockerfile und ./target/helm/... - nein, im Archiv sind ./delphi-council-is-${{ env.VERSION }}.jar, ./classes/Dockerfile und ./helm/...; daran müssen wir uns erinnern, wenn wir im zweiten Teil des Flows auf diese Dateien zugreiffen wollen.
- name: Set Image name and version
run: |
echo "IMAGE=$(cat target/classes/Dockerfile | grep APP_NAME= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV
echo "VERSION=$(cat target/classes/Dockerfile | grep APP_VERSION= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV
- name: Image name and version
run: echo "Working on image '${{ env.IMAGE }}:${{ env.VERSION }}'."
- name: Upload Artifact
id: upload-jar
uses: actions/upload-artifact@v2
with:
name: dci
path: |
target/delphi-council-is-${{ env.VERSION }}.jar
target/classes/Dockerfile
target/helm
retention-days: 1
Damit ist der eigentliche Build der Software abgeschlossen und die Ergebnisse liegen im Archiv dci bei den GitHub Actions.
Hier findet man bei den Workflow-Ausführungen die erzeugte Datei. Da ich sie nur für 1 Tag aufbewahre, ist sie hier als expired markiert.
Der Container-Build-Teil des Workflows
Jetzt kommen wir zum 2. Teil des Workflows, dem Container-Build. Hier wird erstmal definiert, dass er einen Runner mit dem Tag podman nutzen soll (da ich nur dort die notwendige Software installiert habe. Außerdem wird definiert, dass dieser Schritt erst nach erfolgreichem java-build laufen darf. Das git-Repository wird wieder ausgecheckt und das Archiv dci aus dem Java-Build heruntergelanden sowie wieder die Umgebungsvariablen erzeugt (es ist ein anderer Runner, damit sind diese natürlich nicht verfügbar).
container-build:
runs-on: [ podman ]
needs: java-build
steps:
- name: checkout sources
id: checkout-sources
uses: actions/checkout@v2
- name: retrieve jar and dockerfile
id: retrieve-jar
uses: actions/download-artifact@v2
with:
name: dci
- name: Set Image name and version
run: |
echo "IMAGE=$(cat classes/Dockerfile | grep APP_NAME= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV
echo "VERSION=$(cat classes/Dockerfile | grep APP_VERSION= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV
Als erstes nutzen wir buildah über die Aktion redhat-actions/buildah-build@v2, um einen Dockerfile-basierten container-Build anzustoßen. ./classes/Dockerfile ist die Datei aus dem Archiv dci und der Build soll das aktuelle Verzeichnis als Basis nutzen. Außerdem braucht das Image einen Namen (image) und tags (ich tagge das Image mit drei tags): ${{ env.VERSION }} (die Version aus dem Maven-Buildfile), latest und die Git Commit-Id (${{ github.sha }}).
- name: Buildah
id: build-container
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE }}
tags: ${{ env.VERSION }} latest ${{ github.sha }}
dockerfiles: |
./classes/Dockerfile
context: ./
Nach dem Build muss der erzeugte Container natürlich noch in die Container-Registry geschoben werden. Dies erledigt die Aktion redhat-actions/push-to-registry@v2, die dazu diverse Parameter braucht. Unter anderem auch Credentials für die Registry und natürlich das eigentliche Repository auf der Registry, in das das Image geschoben werden soll.
- name: Push To quay
id: push-to-quay
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ env.IMAGE }}
tags: ${{ env.VERSION }} latest ${{ github.sha }}
registry: ${{ secrets.QUAY_REPO }}
username: ${{ secrets.QUAY_USER }}
password: ${{ secrets.QUAY_TOKEN }}
Damit man nicht seinen persönlichen Account nutzen muss, legt man auf quay.io einen Robot-Account an. Dazu kommen wir im nächsten Teil. Ein solcher Account hat einen Benutzernamen und ein Token, dass hier in der Aktion als username bzw. password gesetzt werden müssen. Außerdem muss man den Pfad zum Repository angeben. Die Secrets werden im Repository definiert:
In einem normalen Repository würden QUAY_TOKEN und QUAY_USER auch unter "Repository secrets" stehen. Da ich diese aber für eine Organisation angelegt habe, werden sie hier zwar gelistet aber bei der Organisation gepflegt. Für den Workflow macht dies keinen Unterschied, Ihr könnt sie auch direkt im Repository anlegen. Ich habe mehrere Projekte und will nur einen Quay-Account pflegen und habe daher den Weg über die Organisation gewählt.
In das QUAY_REPO müsst Ihr den kompletten Pfad angeben, bei mir ist das quay.io/klenkes74. Der QUAY_USER beinhalten den Benutzernamen für das quay-Repository, der QUAY_TOKEN das generierte Token für den Benutzer. Aber um quay.io kümmern wir uns im nächsten Teil der Artikelserie.
Sichere und unsichere Github Actions
GitHub weißt darauf hin, dass beim Einsatz eigener Runner natürlich die Sicherheit nicht vernachlässigt werden darf. Immerhin können Fremde einen Pullrequest stellen und wenn man da nicht aufpasst, kann natürlich auch der Workflow verändert werden. Daher bietet GitHub eine Konfiguration an, welche Runner erlaubt sind und welche nicht. Hier müsst Ihr auswählen, wem Ihr vertraut. Ich vertraue allen GitHub-Actions und denen von verifizierten Erstellern:
Sicherheitseinstellungen für Actions auf github.com
Damit sind wir am Ende der Github-Repository-Konfiguration angekommen. Als nächstes schauen wir uns an, wie wir an ein Quay-Repository kommen und dort einen eigenen Robot-Account für Github Actions anlegen.
-
CI/CD mit Maven, GitHub Actions, quay.io und OpenShift
In der Softwareentwicklung gehören CI/CD-Pipelines inzwischen zum guten Ton. Allerdings braucht man hierfür einiges an Infrastruktur, um den Buildprozess so weit zu automatisieren. In dieser kurzen Artikelserie will ich eine mögliche Pipeline auf Basis von GitHub, GitHub Actions, quay.io und OpenShift-basierten Runnern für GitHub Actions betrachten. Ich nutze hier OpenShift, da ich einen OKD-Cluster zu Verfügung habe, aber die Runner lassen sich auch 1:1 für Kubernetes-Cluster nutzen.
</amp-fit-text>
Meine Software ist eine Java spring-boot-Anwendung, die per Maven gebaut wird. Aber dies betrifft nur den kurzen build-Teil der Pipeline und man kann die gleiche Methode auch für Gradle-Builds oder auch für node.js nutzen - man muss gegebenenfalls einen anderen Build-Runner aussuchen.
</amp-fit-text>
Dieser Teil der Artikelserie verdeutlicht das Zusammenspiel der Komponenten und bietet so eine Übersicht. Die einzelnen Bestandteile werden dann in den folgenden Artikeln besprochen.
Überblick über die Komponenten, die wir hier betrachten werden.
Wir nutzen hier die Services von Github (als git-Repository und als Steuerung für unsere Pipeline, bei Github "Workflow" genannt), quay.io (als Container-Repository, das App-Repository ist nur als zukünftige Erweiterung der Pipeline eingetragen) und natürlich unseren eigenen OCP-Cluster (bei mir noch OKD 3.11).
Die Applikation kommt aus einem weiteren Projekt von mir, aber ist in diesem Kontext Nebensache. Sie wird per Maven gebaut und dann per podman-Build in einen Container gegossen. Den java-Runner werden wir hier im Rahmen der Artikel selbst erweitern, um den Node-Part der Anwendung ebenfalls hier bauen zu können. Und weil wir gerade dabei sind, packen wir auch noch gradle als Buildsystem in den Container. Aber dazu mehr im entsprechenden Artikel dieser Serie.
Das mag jetzt nach einem großen Haufen an Komponenten aussehen, aber neben dem Runner-Projekt auf OCP und den Repositories auf Github (source) und quay.io (Container) sind es noch zwei Dateien im Source-Repository (.github/workflow/ci.yaml und src/main/docker/Dockerfile), die unsere Pipeline vervollständigen werden.
Schauen wir uns den Ablauf der Pipeline mal systematisch an:
Ablauf eines Github Workflows mit Github Actions
So ein Workflow funktioniert ganz einfach. Nach dem Auslöser (normalerweise ein commit) führt er einfach der Reihe nach alle definierten Aktionen aus. Wenn ein Fehler auftritt, bricht er ab. Wurden alle Aktionen ausgeführt, beendet sich der Workflow. Nichts besonderes also.
Wir werden einen einfachen Workflow haben, der die Software
mittels Maven auf dem java-runner baut,
den Container mittels buildah baut und
ihn dann per podman nach quay.io hochlädt.
Es kommen neben diesen grundsätzlichen Arbeiten noch ein paar technische Aktionen (auschecken des Codes, aufsetzen der Build-Umgebung, ...) Also nichts, wovor man sich fürchten müsste.
Damit haben wir unseren Überblick. Im nächsten Teil werden wir uns um das Aufsetzen des Github-Repositories (und die Definition des Workflows dort) kümmern.
-
Heyho, DHL, wir müssen reden ...
Am Mittwoch habe ich zwei Pakete verschickt. Ich habe zuhause die Paketmarken ausgedruckt und wollte bei der Post dann die Päckchen kaufen. Leider hatte die Post aber wegen Betriebsversammlung geschlossen. Da habe ich kein Problem mit, die findet ja regelmäßig statt und dann ist das so.
Also habe ich im Papiergeschäft gegenüber zwei passende Umschläge für meine Pakete gekauft (ein Buch und ein kleiner Karton). Dann im Kofferraum meines Autos die Sachen eingepackt und in die Paketbox der Post geworfen. Das war Mittwoch 14.04.2021 um ca. 12:45. Ich war glücklich die Pakete auf dem Weg.
Allerdings hat sich bis heute Morgen am Status des Versands nichts geändert "elektronisch angekündigt". Also rufe ich bei der Reklamationsstelle der Post an. Der nette Herr auf der Gegenseite sagte mir, ja, die Pakete seien elektronisch angekündigt. Das weiß ich - ich habe die Marken ja im Internet gekauft. Ich könne erst am nächsten Montag eine Reklamation wegen der Pakete machen. Also habe ich nicht die Pakete reklamiert, sondern das Nicht-leeren der Paketbox. Er versprach mir, die Reklamation aufzunehmen.
Und es ist etwas passiert. Das Paket mit dem Buch ist auf einmal im Status "abgeholt":
Sendungsverfolgung von DHL für das Paket mit einem Buch
Ich frage mich: würde die Paketbox noch immer rumgammeln, wenn ich als Kunde nicht angerufen hätte? Aber was mich eigentlich noch mehr stört, ist das hier:
Sendungsverfolgung von DHL für das Paket mit kleiner Box (Fritz!PoE-Adaptern)
Ich hoffe, der Status ändert sich hier auch noch, anderenfalls muss ich davon ausgehen, dass dieses Paket in der Paketbox der DHL Beine bekommen hat und vor den netten Leuten von DHL weggelaufen ist ...
Update 18:08: Während ich den Artikel schreibe, hat sich der Status für das verschollene Pakete geändert:
Neue Sendungsverfolgung für das Paket mit den PoE-Adaptern
Das Paket hat sich also gefunden und ist magisch ohne Abholung in das lokale Verteilzentrum gelangt. Soll mir recht sein, alles ist nun auf dem Weg. Ich hätte mal am Mittwoch anrufen sollen, dass sie meine Pakete aus der Box holen sollen ...
-
-
-
-
-
-
-
-
-
-
quarkus live coding
Heute ist es wieder soweit! Um 20:30 werde ich auf https://twitch.tv/klenkes74 wieder live programmieren. Ich habe mich freiwillig gemeldet, an einem Community-Projekt zu helfen, bei dem wir eine spezialisierte Suchmaschine bauen. Hier bekommen wir ASCIIDOC Quelltexte geliefert, die wir einerseits verschlagworten und dann auch noch einer Volltextdatenbank übergeben.
Bis jetzt ist der Code in Python geschrieben, aber da ich kein Python beherrsche, besteht meine erste Aufgabe, den Code zu verstehen und nach Java zu transponieren.
Als Basis habe ich mir - natürlich - Quarkus gewählt, da mir hier schon viele Integrationen mundgerecht geliefert werden. Ein leeres Scaffolding habe ich auch schon, ich starte damit, die Datenbank via Liquibase einzurichten, um dann den Code in die Datenbank schreiben zu lassen.
Den Quellcode gibt es erstmal nicht öffentlich, aber das könnte sich eventuell auch ändern - ich bin aber nicht der Projektlead und kann das daher nicht entscheiden.
Titelbild: old phonebook (lusi@rgbstock, RGBStock Lizenz)
-
Lebenslauf
Berufstätigkeiten
10/2023 bis heute - Software Entwickler bei der SEW-EURODRIVE GmbH & Co KG in Bruchsal
Planung und Coaching einer agilen Transformation eines kleines Softwareentwicklerteams ohne Führungsverantwortung.
Durch laterale Führung soll ein sehr traditionell arbeitendes Team an moderne Entwicklungsmethoden und auch Softwarchitekturen herangeführt werden.
Zur Untersüttzung muss das dazu notwendige Platform Engineering durch den Aufbau einer kompletten Internal Developer Platform und Kubernetes-basierten Laufzeitumgebung durchgeführt werden.
01/2016 bis 09/2023 - Principal Consultant bei der Red Hat GmbH in Grasbrunn
05/2011 bis 12/2025 - Senior Consultant bei der ITConcepts Professional GmbH in Bonn
02/2001 bis 05/2011 - Telefónica O2 Germany GmbH & Co. OHG und Rechtsvorgänger in Verl**
04/2009 - 05/2011 IT Application Manager
Aufbau und fachliche Führung des Betriebsteams für die Fulfillmentsysteme des Wholesale-Geschäftsbereiches. Die von mir vereinbarten Schnittstellen und Prozeduren beschleunigten die durchschnittliche Ticketlaufzeit von ca. 45 Tage auf 3 Tage.
Das Team von Junior-Administratoren wurde von mir motiviert, Aufgaben selbstmotiviert anzugehen und damit viele Probleme nicht erst entstehen zu lassen. Dabei habe ich ihnen durch ein langsames Heranführen das notwendige Selbstvertrauen vermittelt, um sich sicher in den fordernden Umgebungen unserer Systemlandschaft zu bewegen.
06/2005 - 03/2009 IT System Analyst
JEE Entwickler im Wholesale-Geschäftsbereich. Ich habe die technologische Basis des Rufnummerprovisionierungssystems von einer Eigenentwicklung auf ein Open Source Off-the-shelf-Produkt ersetzt und dadurch die Anpaßbarkeit an neue Anforderungen sichergestellt.
Ich war außerdem federführend bei der Vereinheitlichung unserer Softwareentwicklungslandschaft auf Eclipse mit Maven als Buildsystem bei allen Java-Projekten, sodass neue Entwickler sich schneller in bestehende Projekte einarbeiten konnten.
02/2001 - 05/2005 Unix Systemplaner
Planung und Aufbau von Rechenzentren auf Basis von Kolokationsflächen. Die von mir designte Infrastruktur ist mit leichten Anpassungen bis heute Basis für das Hosting und Housing reichweitenstarker Webfarmen.
Die RZ-Verwaltungssoftware wurde von mir auf Basis einer Open Source-Software komplett implementiert und bildet die Rechenzentren und Server bis hinunter auf Systemebene vollständig ab.
04/2000 bis 01/2001 - Leiter Systemadministration bei der Cyland AG, Karlsruhe
Betriebsleiter eines Internet-Startups. Die Aufgabe war, die Anwendung durch operative Planung hochskalierbar zu betreiben.
01/1999 bis 03/2000 - Systemadministrator bei der Schlund + Partner AG (heute 1&1 Internet AG), Karlsruhe
Aufgabe war der eigenständige Betrieb der Internet-Server des Hostingunternehmens und dadurch die Trennung von Systementwicklung und Systembetrieb. Dadurch wurde die Entwicklungsleistung der Systementwickler wieder für die Neu- oder Weiterentwicklung der Produkte des Unternehmens verfügbar.
01/1998 bis 12/1998 - Systemadministrator bei der DI Delta Internet GmbH, Arnsberg
Ich habe als Administrator den technischen Betrieb des Internetproviders durchgeführt. Außerdem habe ich an Kundenprojekten zur Migration der Arbeitsplatzrechner von Windows- auf Linuxsystemen mitgewirkt.
Schule und Studium
Oktober 2000 - Fachliche Anerkennung der Eignung als Ausbilder für “Fachinformatiker (Systemintegration)”, Regierungspräsidium Stuttgart.
11/1996 bis 12/1998 - FH Karlsruhe, Studium der Wirtschaftsinformatik, kein Abschluss
10/1994 bis 09/1996 - Universität Mannheim, Studium der Rechtswissenschaft, kein Abschluss
08/1991 bis 05/1994 - Friedrich-List-Schule (Wirtschaftsgymnasium) Karlsruhe, Allgemeine Hochschulreife
07/1980 bis 07/1991 - Grundschule Raeren (Belgien), Grundschule Friedrichstal und Thomas-Mann-Gymnasium Stutensee
Sonstiges
Februar 2023 - altMBA, Akimbo
Oktober 2012 - Professional Scrum Master I
März 2012 - ITIL v3 Foundation Certification
November 2011 - ProAMT/ARIS, Volkswagen Coaching
04/1992 - 04/2010 - Gewerbe als EDV-Berater mit Spezialisierung in DFÜ und Internet für Gewerbetreibende und freie Berufe
Sprachen und Kenntnisse
Deutsch als Muttersprache.
Englsich fließend in Wort und Schrift.
Führerscheine: A, B
None
· 2020-10-18
-
-
-
Schattenjagen
Heute will ich etwas über eine beliebte Beschäftigung von Softwareentwicklern schreiben: dem Schattenjagen.
Schon früh lernt man als Softwareentwickler, Fehler zuerst bei sich zu suchen, bevor man Probleme bei anderen sucht. Oft genug ist es Lernen-durch-Schmerzen. Man behauptet, eine Library oder Code eines Kollegen hat einen Bug und bekommt dann nachgewiesen, dass es der eigene Code war. Man lernt also, erstmal auszuschließen, dass es sich bei dem Problem um den eigenen Code handelt, bevor man ihn woanders sucht.
Aber manchmal (oder öfter, je nach der einen Fähigkeit) ist es halt doch ein Problem, das man nicht selbst verursacht hat. Aber bis man dahin kommt, habt man oft schon lange Zeit bei der Fehlersuche verbracht.
Mein aktuelles Beispiel war z.B. ein Fehler in Liquibase, der bei der Nutzung von mariadb eine seltsame Exception geworfen hat (https://liquibase.jira.com/browse/CORE-3457). Und ausgerechnet bei einem neuen Pet-Project hatte ich mich entschieden, eine MariaDB zu nutzen. Und lief auf den Fehler. Ich habe an meinen Changelogs für Liquibase herumgeschraubt und sie solange umgeschrieben, bis ich einfach mal ein leeres Changelog nutzte und den gleichen Fehler bekam.
Und diesmal brachte mich Google dann auf die Jira-Issue CORE-3457. Ich habe also nun frohen Mutes einen Github-Issue für Quarkus 1.7.1.Final geöffnet - der derzeit aktuellen Version. Und dann wollte ich diesen Issue auch gleich lösen und einen Pullrequest dafür öffnen - man will ja auch etwas für die OpenSource tun und nicht immer nur geiern.
Also quarkus geforkt, in die IDE geladen und dann erst gesehen, dass im Master bereits liquibase 4.0.0 genutzt wird - und der Fehler wurde bereits vor ein paar Tagen gefixt.
Und so lebte mein Bug-Issue bei Quarkus ganze 10 Minuten, bevor ich ihn wieder kleinlaut geschlossen habe. Im Moment baue ich gerade das Snapshot-Quarkus, da ich keine 20 Tage warten will, bis ich weitermachen kann mit meinem Projekt. Aber alles in allem, habe ich sicherlich 5 Stunden wieder mal einen Schatten gejagt ...
Titelbild: Meme von G.L Solaria auf dev.to
-
-
-
-
-
-
Providing documentation the OpenShift way - Part 1
Documentation is one of the most hated part of the life of a developer. So the documentation is often the most neglected part of a project. At work I use Asciidoctor to write my customer documentation and it is quite acceptable. I loved LaTeX and Asciidoctor is an acceptable replacement for technical documentation - especially with the alternatives being google doc or word.
Problem statement
Lets define the problem to solve first:
A service/software needs the current documentation available on the internet.
The documentation source should be saved in the same git repository as the software/service source code itself, so it is managed in the same lifecycle as the software itself.
A static documentation should be generated and provided in form of HTML via HTTP (commonly known as "web server").
It should be deployed within an OpenShift or OKD cluster.
Asciidoctor as markup language for documentation
I don't want to advocate Asciidoctor, well in fact I do want to advocate for it. Asciidoctor is a nice way to write technical documentation as pure ascii (well, UTF-8) text and let compile it to nice output like PDF or HTML. But check other resources like the home page of Asciidoctor for the syntax and semantic of this text processing language. Believe me, it's really easy to write and read (even in unprocessed form).
The nice thing, the whole documentation may be checked in on the source code revisioning. Since we work with s2i-bilder I assume you use git. If that's a gitlab or public or private github or something like gogs, doesn't matter. If it is able to provide a remote git repo, that's fine.
Writing documentation
Let's put the documentation in the directory /documentation of the git repo. There is an index.adoc file (the future landing page as index.html on the webserver). Perhaps you put the whole documentation into one single file (I talk about the output, the input may be split into more than one file included in the generation process). Or you have an hierarchy of documents.
How you structure your documentation is completely up to you. It only matters that there is a single index.adoc in the base directory of your documentation.
Generating documentation
And that's it. I want to have a build configuration where I point to that git repository (the URI, the branch and the contextDir like /documentation) and the rest is done by software.
And how I solve this, is described in the upcoming articles:
Part 2: Creating the s2i builder with ASCIIDOC generation software included
Part 3: Creating the documentation site
Part 4: Bundling the components for OpenShift
-
Nach 15 Jahren ...
... bin ich wieder Eigentümer eines Autos. Es ist jetzt nicht so, dass ich kein Auto besessen hätte. Aber das Familienauto war eigentlich immer auf meine Exfrau zugelassen. Und dann hatte ich von 2011 bis 2015 noch einen Dienstwagen meines Arbeitgebers.
Und seitdem hatte ich oft Mietwagen oder Carsharing-Autos, die ich nutze. Aber ich habe gemerkt, dass selbst in einer deutschen Stadt mit grundsätzlich guter Verkehrsanbindung wie Bensheim (OK, KLEINstadt mit guter Verkehrsanbindung) Situationen entstehen, in denen ein Auto Seelenfrieden stiftet.
So musste ich mit meiner Tochter nachts um 2:00 nach Heidelberg in die Kinderklinik. Zum Glück war meine Exfrau auch in die Notaufnahme des Heppenheimer Krankenhauses gekommen, sodass wir ein Auto hatten - sonst wären die ca. 50 km schon ein teurer Spaß mit Taxi geworden.
Außerdem kann man auch größere Sachen gebraucht kaufen und sie dann einfach abholen. Es ist halt bequem. Aber ausschlaggebend war es, nachts auch zum Arzt fahren zu können oder demnächst die Chance zu haben, die Tochter vom Club (früher nannten wir das Disco) abzuholen, wenn es sein muss.
Jetzt muss ich nur noch den TÜV-Bericht abwarten und dann das Auto angemeldet bekommen. In Heppenheim ist es nicht so einfach, kurzfristig einen Termin dafür zu bekommen ...
Persönliches
· 2019-05-05
-
Das 1. Zimmer ist fertig
Was man immer vergisst, wenn man länger nicht umgezogen ist, ist dass es lange dauert, bis man wieder in der neuen Wohnung "angekommen" ist. Nach dem Umzugstag stehen meistens noch längere Zeit einige Umzugskartons herum. Wenn man einen Speicher oder einen Keller hat, ist es auch nicht unüblich, dass einige wenige Kartons garnicht ausgepackt werden.
Aber auch wenn alles ausgepackt ist, braucht es einige Zeit, bis es wieder die eigene Wohnung ist. Dinge wandern erstmal, bis sie ihren Platz finden. Einige Dinge werden aussortiert, weil man sie nicht mehr braucht oder es einfach keinen Platz gibt (heutzutage wäre wohl der Ausdruck "sie machen keine Freude mehr").
Und wenn man neben dem Umziehen und Einrichten noch arbeiten muss, kann es sein, dass man auch längere Zeit "in einer Baustelle" wohnt. Einiges wird schneller erledigt, andere Dinge (wie bei mir einige Lampen, die wohl noch längere Zeit einfache Lampenfassungen sein werden) brauchen dafür länger.
Aber irgendwann kommt der Zeitpunkt, an dem ein Zimmer fertig ist. Zumindest für einige Zeit (das Leben besteht aus Veränderung, also ändert sich auch unsere Wohnungen ständig). Und so etwas ist heute passiert. Ein kleines aber wichtiges Zimmer ist fertiggeworden: mein Badezimmer. Es ist so ziemlich das kleinste Zimmer der Wohnung. Aber es ist meines und es ist fertig. OK, wir haben noch das große Badezimmer im 2. Stock - aber das ist das Badezimmer meiner Tochter, das ist schon gut nutzbar, wir müssen aber noch die Handtücher usw. in die Wandschränke einsortieren.
Aber das Badezimmer im 1. Stock ist nun fertig. Alles erledigt, was erledigt werden sollte. Ich bin stolz.
Oder man könnte auch einfach sagen: ich habe den Waschbeckenunterschrank aufgebaut und den Spiegelschrank aufgehängt. Aber das klingt so banal ...
Persönliches
· 2019-04-20
-
Einkäufe, Einkäufe und Einkäufe
Langsam gerate ich in einen Kaufrausch. Diverse Anschaffungen macht so ein Sesshaftwerden ja notwendig. So braucht man z.B. einen Kühlschrank, diverse Kochgerätschaften, Lebensmittel, Lampen, Teppiche, Fahrradpumpen, Ersatzbänder für das Beschriftungsgerät, technisches Equipment, um auch das letzte Eck des Hauses mit W-Lan und Internet auszustatten und und und.
Und das alles kommt entweder von Amazon (wie ein komplett neues Geschirrset, da das alte die letzten Umzüge nicht so ganz überstanden hat) und Besteck (Warum hat das alte Set 6 Messer, 4 Gabeln, 5 große und 3 kleine Löffel? Das ist eine komische Mischung).
Und zusammen mit den IKEA-Einkäufen der Kinderzimmer und des eigenen neuen Bettes und Ergänzungen sammelt sich Massen von Kartons an. Berge, ja ganze Gebirge. Und auch die Nahrungsmitteleinkäufe reißen nicht ab. Nudeln, Reis, Mehl, Zucker, Milch, ja auch Salz und andere Gewürze wollen wieder beschafft sein.
Da ich ja früher die ganze Woche im Hotel lebte und mir anfangs immer die Lebensmittel schlecht wurden, habe ich auch an den Wochenenden nicht gekocht (nunja, ich kann es sowieso nicht besonders gut). Aber nun wird wieder gekocht werden und all die kleinen Dinge, die sich sonst über längere Zeiten ansammeln, müssen wieder her. Und alles will ins Haus und in den 1. OG geschafft werden. Sowas schlaucht. Wo ich die letzten Jahre nach der Arbeit entspannt aufs Hotelzimmer bin, reicht jetzt ein Tag "leben" ohne Arbeit (ich habe ja diese Woche wegen des Umzugs freigenommen) um mich komplett zu schlauchen.
Wie erwartet, strengt die Umgewöhnung ziemlich an. Aber bald kommen auch die positiven Seiten des häuslichen Lebens dazu. Sich unter der Woche mit Freunden treffen, die man nicht von der Arbeit kennt sondern in diesem seltsamen Umfeld von "Nachbarn" zu finden sind z.B. - da gibt es viele neue Welten zu entdecken. To boldly go where lot of people have gone before ...
Persönliches
· 2019-02-15
-
Anmeldung und Wartezeit auf Behörden
Heute morgen musste ich sowieso auf eine Lieferung warten, also plante ich, mich in Bensheim anzumelden. Da ich sowieso wegen eines Diebstahls einen neuen Personalausweis und Führerschein brauchte, sollte das dann alles auf einem Schlag erledigt werden. Doch ich hatte nicht mit deutschen Behörden gerechnet ...
Aber zurück zum Anfang. Ich war gestern am Sonntag schon in Bensheim, da Wohnungsübergabe mit dem Vermieter war. Allerdings hatte ich keine Passfotos. In Bensheim konnte ich nichts finden, was zu meinem Zeitplan passen würde, also fuhr ich um 7:28 nach Darmstadt, um dort im Hauptbahnhof im Fotoautomaten die biometrischen Fotos zu machen. Kurz vor 9:00 war ich dann wieder in Bensheim zurück. Außerdem habe ich in Darmstadt meine neue Bahncard gekauft. So konnte ich zwei Fliegen auf einen Schlag erledigen.
Also dann vom Bahnhof zum Bürgerbüro, um mich anzumelden und einen neuen Personalausweis wie auch Führerschein zu beantragen (wie ich laut Webseite der Stadt Bensheim dort könnte). Nach einer kurzen Wartezeit kam ich auch dran und erfuhr, dass ich mich nur anmelden könne. Für den Ausweis müsste erst die Rückmeldung aus Wuppertal kommen. Und für den Führerschein müsste ich nach Heppenheim.
Ok, das waren jetzt 1 von 3. Nicht so schön. Aber nächste Woche kann ich dann den Personalausweis beantragen (da bin ich eh die ganze Woche in Bensheim, da dann meine Möbel kommen). Dann werde ich auch mal nach Heppenheim fahren wegen des Führerscheins.
Dafür kam die Lieferung (angekündigt zwischen 10:00 und 14:00) schon um 10:30, sodass ich den Zug um 11:01 nach Frankfurt nahm, um dort wieder zu arbeiten.
Allerdings ging das dann doch zu schnell, sodass ich die neue Bahncard (die ich ab übermorgen brauche) in Bensheim vergaß. Also werde ich heute oder morgen nach der Arbeit wieder nach Bensheim fahren, um dort meine neue Fahrtkarte für das kommende Jahr aus der Wohnung zu holen. Wie sagte mein Vater immer? Was man nicht im Kopf hat, muss man in den Beinen haben ...
Titelbild: rgbstock.de (© Lusi - ein anders 1)
Persönliches
· 2019-02-04
-
-
-
Danke Bahn!
Hallo Bahn!
ich schreibe Dir auf meinem Blog, da dies den gleichen Effekt haben wird wie ein Telefonat mit Deiner Hotline oder einen Brief an irgendeine Deiner Adressen. Aber ich finde, es muss mal gesagt werden, daher wähle ich diesen Weg.
Ich fahre grundsätzlich gerne mit der Bahn. Und meistens funktioniert es ja auch. Über die selten eingehaltenen Fahrpläne mecker ich nicht. Ein paar Minuten hin oder her stören mich nicht. Ich habe mich darauf eingestellt, bei Umsteigeverbindungen mindestens 20 Minuten zu reservieren. Bei Bahnhöfen wie Kassel Willhelmshöhe, bei denen ich meist von Gleis 1 auf Gleis 10 umsteigen muss mit den dortigen langen Wegen dazwischen mache ich halt 30 Minuten. Das gibt Luft für die üblichen Ungenauigkeiten der "Fahrplanutopie", wie ein Freund den Fahrplan der DB tituliert.
Was mich stört ist die mieserable Kommunikation zum Kunden und zu den Schaffnern im Zug. Ich habe heute um ca. 10:00 eine Fahrt von Wuppertal-Oberbarmen nach Erfurt gebucht. Cool war, dass ich nur zweimal umsteigen musste. Einmal in Wuppertal (das würde ich noch nichtmal als Umstieg zählen, immerhin entspricht das der Straßenbahnfahrt zum Hauptbahnhof). Und einmal im Fernbahnhof des Flughafens. Echt bequem. Vor allem mit meinem Gepäck.
Aber dann verzögerte sich die Fahrt und wir haben offiziell meinen Aschluss in Frankfurt nicht mehr geschafft. Da der aber auch leicht verzögert war und einen menschlichen Zugchef hatte, konnte ich meine Platzreservierung sogar nutzen. Nunja, zumindest kurz. Denn dieser Zug hat den Halt in Erfurt umfahren. Als ich das im Zug las, machte ich mich auf die Suche und fand, dass eigentlich nur noch ein Zug mich rechtzeitig in mein Hotel nach Erfurt bringen würde. Dazu müsste ich in Frankfurt Süd austeigen, mit der Regionalbahn zum Hauptbahnhof fahren und dort dann in den ICE nach Berlin umsteigen. Bei der Durchsage der Schaffnerin wurde auf den Ausfall des Halts in Erfurt hingewiesen und dass man NACH der Abfahrt in Frankfurt Süd bescheid geben würde, wie man noch nach Erfurt kommen kann. Leute, die sich daran gehalten haben, haben nach den mir vorliegenden Fahrplandaten Probleme, überhaupt heute noch nach Erfurt zu kommen.
Nunja, dafür ist mein ICE jetzt schön leer. Hat aber auch 90 min angekündigte Verspätung. Und ich habe eine Deutschlandrundtour: Wuppertal-Oberbarmen -> Köln -> Frankfurt -> Kassel -> Erfurt. Ich fahre gefühlt im Zickzack von West nach Ost.
Grund für die Verzögerung und die Ausfälle sind übrigens BAUSTELLEN. Kein Sturm, kein Personenschaden oder Vandalismus. Und das war heute um 10:00 also noch nicht absehbar. Sind das Überfall-Baustellen, die die Bahn überfallen und die Bahn kann sich nicht dagegen wehren? Arbeitet mal an eurer Kommunikation:
zu den IT-Datenhalden (damit diese um 10:00 morgens die Baustellen von 16:00 nachmittags auch kennen),
zu euren Schaffnern (damit diese rechtzeitig wissen, wohin sie ihre Kunden schicken müssen),
zu euren Kunden, damit diese sich nicht von euch alleine - und an Tagen wie heute auch wortwörtlich im Regen gestehen lassen fühlen.
-
Openshift and GroupSync from LDAP
OpenShift offers a variety of possible integrations into security providers. The integration is divided into authentication and authorization. Authentication is handled by one of the configurable IdentityProviders of OpenShift. While authorization is handled by importing groups into OpenShift. For importing groups the most used method is reading from an LDAP (or an Active Directory via its LDAP interface). OpenShift already has a synchronization tool for this type of synchronization. And as long as that tool is sufficient, there are more reasons to stay with that tool than to replace it. But there are some situations where you need to replace it. And here the base software I written and published to github project klenkes74/openshift-ldapsync.
Reasons for using this software
Possible reasons (among others) not to use the official LDAP sync are:
The SSL certificate of the LDAP could not be checked.
The group structure is not supported. Mainly nested groups pose a (performance) problem.
The linking element between user and groups (the username) is not exposed as single attribute on the LDAP.
You don't have an LDAP server to sync with but other means of providing group authorization data.
Well, the last reason will result in a little bit more work since you have to replace the LDAP reading part of the software, but it's still doable and easy forward. The current software as provided on github will take care of the first three problems. But since your LDAP structure may vary, you will propably need to change the code.
Integration into openShift
The LDAP sync will run as single pod within OpenShift. By using this approach we don't need to take care of system cron jobs or how to handle the case that the server this job is running on fails. OpenShift is good at managing that a pod is running, so it was a natural decision to give that job to OpenShift. In very restrictive environments you may have to define an egress router to be able to connect to the LDAP directory (or even the OpenShift API). But you will know that and there is quite good documentation for adding this egress router.
The README of the github project describes how to install the software and which parameter you need to set.
Spring Boot
The software is a default spring-boot application. It leveraged the easy startup and scheduling functions of spring-boot to start and run the synchronization. The application class is very straight forward and starts only the injected runner SyncGroupsController.
Doing the work
The SyncGroupsController reads the groups from the LDAP and the OpenShift API into standardized maps (taking the OpenShift name as key and the Group as value). And then it runs all defined group executors and passing them the two maps. Currently only one executor is defined but you could add additional ones matching your needs (e.g. you could enrich already existing users with their email addresses or names if your authentication module does not deliver that data). It is really up to you.
But looking at the SyncGroupEecutor (this is the class, the real work takes place) you see, it is really structured. It first creates selector sets for synched groups (groups that exist in LDAP and OpenShift), groups to add (groups that only exist in LDAP) and groups to delete (groups that only exist in OpenShift). As long as we have only one source for groups, this handling is simple and easy.
After having selected the different action items, the code creates a set of commands to run (these could be either Create, Update or Delete). Every command executes the changes for exactly one group. And this set of course is then executed. And that's it.
Well. But almost every LDAP looks different and one of the requirements was to handle it different. And you are right. That magic is hidden in the readers for OpenShift and LDAP (the things creating the maps with the Group). There are the readers mapping the data read from OpenShift or LDAP and providing the normalized Group object. Within these readers the converts convert from OpenShift (since the OpenShift data does not change, the converter is within the same class) or LDAP (one for the groups and one for the users). And here are the places to change the mappings. Feel free to adapt them to your needs.
And the ugly rest ...
Well, and then there is the SSL part. I'm not proud of it. But especially the type of companies with big datacenters using OpenShift often have problems providing corect SSL certificates to their AD servers. So there is a small part of code in LdapServer (lines 79 to 97), that copes with that problem:
[code lang="java" toolbar="true" title="Removing checks for SSL in java" firstline="79"]
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);
SSLContext.setDefault(ctx);
} catch (Exception ex) {
ex.printStackTrace();
}
[/code]
Of course there is some boilerplate code to read configuration from the system environment to be able to configure it the "OpenShift way". But you also could give every needed parameter via command line (to run it "adhoc" as a first import for example).
Summary
Of course you should not use this software (or a derivate of it) when the good tested and easy configurable default LDAP sync mechanism works for you. Never add any security related component to your system if you don't have to. But If you need, then you have a nice basis to work on. Drop me an email or comment. And I'd love to receive pull requests to improve the software. Especially the tests are missing ...
-
Logging in Java
Der Artikel "Is Standard Java Logging Dead?" auf dzone.com hat mich animiert, wieder einmal ein paar Gedanken zu Papier zu bringen. Logging ist immer ein nettes Thema in den Projekten. Meistens wird wenig dazu gesagt oder vereinbart. Jeder Programmierer zieht sein Ding durch. Doch wenn Logging wirklich helfen soll, dann muss man sich ein paar Gedanken machen.
Zu allererst muss man sich klarmachen, für wen das Logging ist und wie es ihn unterstützen soll. Sofort kommen zwei Adressaten in den Sinn. Natürlich der Entwickler. Er braucht Informationen während der Entwicklung und die Einführungsphase der Software. Und dann braucht natürlich der Admin in der Produktion Logmeldungen, die ihn in das System sehen lassen.
Aber es gibt noch weitere Adressaten. So kann zum Beispiel ein Abrechnungssystem auf entsprechende Daten angewiesen sein. Oder die Governance-Abteilung. Moderne Frameworks erlauben, Logging-Events nach diversen Regeln zu interpretieren.
Aber jetzt beginnt das Problem. Der Programmierer wird das Logging so bauen, dass er mit dem Wissen, was er programmiert hat, schnell Informationen bekommt. Dummerweise hat sein Nachfolger im Projekt oder der Administrator dieses Wissen nicht mehr. Und fragt sich, warum hier geloggt wird und da nicht. Mit diesem Entwicklerlogging kann der Admin und erst recht das Billing nichts anfangen. Daher müssen Logginganforderungen auch als nicht-funktionale Anforderungen erfasst werden. Wenn man ein System aus Micro-Services zusammensetzt wird Operations sehr genau definieren müssen, wie es Logging-Events erhalten will, damit diese zentral verarbeitet und aufbereitet werden können - sonst müssen die Admins hunderte Logfiles auswerten und die einzelnen Geschäftsvorfälle durch die Systeme verfolgen. Eine nicht zu leistende Aufgabe.
-
Buildsystem via Vagrant-Box
In manchen Java-Projekten hat man das Problem, dass Entwickler unter Windows, Linux und MacOS arbeiten. Damit stellt schon das Build-Tooling ein Problem dar. Obwohl moderne Build-Systeme meistens in der JVM laufen und damit eigentlich betriebssystemunabhängig sein sollten, stößt man oft an das Problem, dass Pfadangaben leider nicht so unabhängig sind.
Um trotzdem lokal bauen zu können, wäre ein System schön, das wie ein zentraler Build-Server fungiert. Aber halt lokal. Und hier kommt Vagrant ins Spiel ...
In den meisten Fällen dürfte der Buildserver auf einem Linuxsystem laufen, die sind billig und schnell aufgesetzt. Meistens ist das die Qualifikation dafür, da das Management für solchen "Overhead" kein Geld ausgeben will. Daher betrachte ich hier nur diesen Fall[ref name="dislike-windows"]Außerdem mag ich Windows und MacOS als Server nicht.[/ref].
Was braucht man alles?
Eine Virtualisierungsumgebung (z.B. VMWare oder VirtualBox).
Vagrant zum Erzeugen der Virtual Machine.
Zeit, Lust und Durchhaltevermögen zum Erstellen des Vagrantfiles für die lokale Box.
Ich benutze hier VirtualBox auf einem MacBook Pro. Zusammen mit der VirtualBox ist das eine nette Kombination. Vagrant gibt es auf als Download auf https://www.vagrantup.com/downloads.html[ref name="vagrant-version"]Die aktuelle Version 1.8.7 für den Mac sind ca. 70 MB, je nach Netzwerk ist es also sofort da oder dauert etwas.[/ref]. In Version 1.8.7 ist wohl das gelieferte curl defekt und muss gelöscht werden:
[code lang="bash" toolbar="true" title="Entfernen des defekten curl"]
hermes:vagrant klenkes$ rm /opt/vagrant/embedded/bin/curl
hermes:vagrant klenkes$
[/code]
Damit funktionieren die Box-Downloads[ref]siehe https://github.com/twobitcircus/rpi-build-and-boot/issues/25[/ref]. Danach kann man per
[code lang="bash" toolbar="true" title="Herunterladen der Box mit Centos/7"]
hermes:vagrant klenkes$ vagrant box add centos/7
==> box: Loading metadata for box 'centos/7'
box: URL: https://atlas.hashicorp.com/centos/7
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.
1) libvirt
2) virtualbox
3) vmware_desktop
4) vmware_fusion
Enter your choice: 2
==> box: Adding box 'centos/7' (v1610.01) for provider: virtualbox
box: Downloading: https://atlas.hashicorp.com/centos/boxes/7/versions/1610.01/providers/virtualbox.box
==> box: Successfully added box 'centos/7' (v1610.01) for 'virtualbox'!
hermes:vagrant klenkes$
[/code]
eine Boxdefinition herunterladen. Ein Update könnte man mit
[code lang="bash" toolbar="true" title="Updaten einer Box-Definition"]
hermes:vagrant klenkes$ vagrant box update --box centos/7
Checking for updates to 'centos/7'
Latest installed version: 1610.01
Version constraints: > 1610.01
Provider: virtualbox
Box 'centos/7' (v1610.01) is running the latest version.
hermes:vagrant klenkes$
[/code]
ausgeführt werden.
Und mit
[code lang="bash" toolbar="true" title="Ausführen einer Vagrant-Box"]
hermes:vagrant klenkes$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
default: Adapter 2: hostonly
==> default: Forwarding ports...
default: 80 (guest) => 8080 (host) (adapter 1)
default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default: Warning: Remote connection disconnect. Retrying...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
default: No guest additions were detected on the base box for this VM! Guest
default: additions are required for forwarded ports, shared folders, host only
default: networking, and more. If SSH fails on this machine, please install
default: the guest additions and repackage the box to continue.
default:
default: This is not an error message; everything may continue to work properly,
default: in which case you may ignore this message.
==> default: Configuring and enabling network interfaces...
==> default: Rsyncing folder: /Users/klenkes/Documents/demo/vagrant/ => /vagrant
hermes:vagrant klenkes$
[/code]
kann man die Box starten. Wie man aber sieht, können keine Verzeichnisse per Virtualbox gemountet werden, da die guest additions nicht installiert sind. Darum müssen wir uns kümmern:
Wie kommen die Virtualbox Guest-Addons ins System?
Hier gibt es ein Vagrant-Plugin namens "vagrant-vbguest". Und das installieren wir erstmal:
[code lang="bash" toolbar="true" title="Installieren von vagrant-vbguest"]
hermes:vagrant klenkes$ vagrant plugin install vagrant-vbguest
Installing the 'vagrant-vbguest' plugin. This can take a few minutes...
Installed the plugin 'vagrant-vbguest (0.13.0)'!
hermes:vagrant klenkes$
[/code]
Die Meldung "this can take a few minutes..." ist ernst gemeint. Es kann etwas dauern. Ist das Plugin installiert, muss es noch konfiguriert werden - natürlich im Vagrantfile:
[code lang="text" toolbox="true" title="Virtualbox Guest-Addons konfigurieren im Vagrantfile"]
...
if Vagrant.has_plugin?("vagrant-proxyconf")
config.vbguest.auto_update = true
config.vbguest.no_remote = false
end
...
[/code]
Beim nächste start sollte es jetzt da sein. Das Plugin wird erstmal den GCC und alles was CentOS noch so braucht, um Kernelmodule zu kompilieren, installieren und dann auch gleich die Addons installieren. Sehr bequem.
Pack den Proxy in den Tank
Meistens befindet man sich ja als Consultant in einem abgeschlossenen Netz und braucht einen Proxy, um auf das Internet zugreifen zu könne. Hier hilft das Plugin "vagrant-proxyconf". Mittels
[code lang="bash" toolbar="true" title="Installieren von vagrant-proxyconf"]
hermes:vagrant klenkes$ vagrant plugin install vagrant-proxyconf
Installing the 'vagrant-proxyconf' plugin. This can take a few minutes...
Installed the plugin 'vagrant-proxyconf (1.5.2)'!
hermes:vagrant klenkes$
[/code]
lässt sich das benötigte Modul installieren. Nun kann man den Proxy in das Vagrantfile eintragen und die Einstellungen aus dem Host-System in die Box übernehmen:
[code lang="text" toolbox="true" title="Proxy-Konfiguration im Vagrantfile"]
...
if Vagrant.has_plugin?("vagrant-proxyconf")
puts "Configuring proxy!"
if ENV["http_proxy"]
puts "http_proxy: " + ENV["http_proxy"]
config.proxy.http = ENV["http_proxy"]
end
if ENV["https_proxy"]
puts "https_proxy: " + ENV["https_proxy"]
config.proxy.https = ENV["https_proxy"]
end
if ENV["no_proxy"]
puts "no_proxy: " + ENV["no_proxy"] + ",127.0.0.1,localhost"
config.proxy.no_proxy = ENV["no_proxy"] + ",127.0.0.1,localhost"
end
...
[/code]
Mount des Verzeichnisses /vagrant
Laut Dokumentation wird dieses Verzeichnis bei virtualbox per Virtualbox gemounted. Leider wurde es bei mir immer nur per rsync synchronisiert. Aber ein Eintrag für den Mount mit dem richtigen Typ hat dies behoben:
[code lang="text" toolbox="true" title="Mountpoint korrigieren"]
...
config.vm.synced_folder "./", "/vagrant", type: "virtualbox"
...
[/code]
Damit steht einem das Host-Verzeichnis auch in der Gast-Box zu Verfügung.
Build-System
Jetzt muss man noch das gewünschte Build-System per Provision installieren. Ich installiere git und java gleich mit. und habe es in einem Aufwasch hinter mir. In das Script kann man noch alle notwendigen Änderungen einfügen. Es handelt sich um ein Shell-Script, dass mit den Rechten des Users "vagrant" auf der Box ausgeführt wird. Root-Aktionen müssen also per "sudo" eingeleitet werden ...
Die Sourcen legen wir in unserem Host-Directory neben das Vagrantfile und nennen den Ordner src. Damit steht er sowohl auf dem Host wie auch in der Guest-Box zu Verfügung.
[code lang="text" toolbox="true" title="Build-System aufsetzen"]
...
config.vm.provision "shell", inline: <<-SHELL
sudo yum install -y git unzip zip vim java-1.8.0-openjdk-headless java-1.8.0-openjdk-devel-debug maven
cd /vagrant
mkdir -p /vagrant/src
SHELL
...
[/code]
Und voila, wir haben eine Vagrant-Box, die ein definiertes Build-System beinhaltet.
Und alles zusammen sieht das Vagrantfile nun so aus:
[code lang="text" toolbox="true" title="Vollständiges Vagrantfile für ein Build-System"]
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
config.vm.box = "centos/7"
# accessing "localhost:8080" will access port 80 on the guest machine.
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "private_network", ip: "192.168.33.10"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
config.vm.synced_folder "./", "/vagrant", type: "virtualbox"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.provider "virtualbox" do |vb|
# Display the VirtualBox GUI when booting the machine
vb.gui = false
# Customize the amount of memory on the VM:
vb.memory = "1024"
end
if Vagrant.has_plugin?("vagrant-proxyconf")
puts "Configuring proxy!"
if ENV["http_proxy"]
puts "http_proxy: " + ENV["http_proxy"]
config.proxy.http = ENV["http_proxy"]
end
if ENV["https_proxy"]
puts "https_proxy: " + ENV["https_proxy"]
config.proxy.https = ENV["https_proxy"]
end
if ENV["no_proxy"]
puts "no_proxy: " + ENV["no_proxy"] + ",127.0.0.1,localhost"
config.proxy.no_proxy = ENV["no_proxy"] + ",127.0.0.1,localhost"
end
end
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "shell", inline: <<-SHELL
sudo yum install -y git unzip zip vim java-1.8.0-openjdk-headless java-1.8.0-openjdk-devel-debug maven
cd /vagrant
mkdir -p /vagrant/src
SHELL
end
[/code]
Viel Spaß damit!
-
Blick in das JNDI ...
Manchmal muss man einfach in einen vorhanden JNDI reinschauen, um herauszufinden, was dort eigentlich auf Entdeckung schlummert. Die folgende Klasse liefert einen formatierten Baum aus den JNDI-Eintragungen.
[java autolinks="false" collapse="false" firstline="1" gutter="true" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
package de.kaiserpfalzedv.office.common.jndi;
import javax.naming.InitialContext;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A small utlitiy class to print the JNDI tree.
*
* @author klenkes {@literal <rlichti@kaiserpfalz-edv.de>}
* @version 1.0.0
* @since 2016-09-29
*/
public class JndiWalker {
private static final Logger LOG = LoggerFactory.getLogger(JndiWalker.class);
/**
* Walks the {@link InitialContext} with the entry point given and returns a string containing all sub entries.
*
* @param context The initial context to be printed.
* @param entryPoint The node to start with.
* @return A string containing the tree of the initial context (nodes including the leaves).
* @throws NamingException If the lookup failes for some unknown reason.
*/
public String walk(final InitialContext context, final String entryPoint) throws NamingException {
StringBuffer sb = new StringBuffer(" JNDI tree for '").append(entryPoint).append("': ");
LOG.trace("JNDI-Walker activated for entry point '{}' on: {}", entryPoint, context);
walk(sb, context, entryPoint, 0);
return sb.toString();
}
private void walk(
StringBuffer sb,
final InitialContext context,
final String name,
final int level
) throws NamingException {
NamingEnumeration ne;
try {
ne = context.list(name);
} catch (NamingException e) {
LOG.trace("Reached leaf of JNDI: {}", name);
return;
}
while (ne.hasMoreElements()) {
NameClassPair ncp = (NameClassPair) ne.nextElement();
printLevel(sb, level);
sb
.append(ncp.getName())
.append(" (").append(ncp.getClassName())
.append(", ").append(getNameInNamespace(ncp))
.append(", ").append(getRelativeFlag(ncp))
.append(")");
walk(sb, context, name + "/" + ncp.getName(), level + 4);
}
}
private void printLevel(StringBuffer sb, int level) {
sb.append("\n");
for (int i=0; i < level; i++) {
sb.append(" ");
}
}
private String getNameInNamespace(NameClassPair ncp) {
String nameInNamespace;
try {
nameInNamespace = ncp.getNameInNamespace();
} catch (UnsupportedOperationException e) {
nameInNamespace = "not-supported";
}
return nameInNamespace;
}
private String getRelativeFlag(NameClassPair ncp) {
String isRelative;
try {
isRelative = ncp.isRelative() ? "relative" : "fixed";
} catch (UnsupportedOperationException e) {
isRelative = "not-supported";
}
return isRelative;
}
}
[/java]
Die Basis für diese kleine Hilfsklasse war ein Blogeintrag von Anders Rudklaer Norgaard. Ich habe sie nur Servlet-unabhängig gemacht.
-
H2-Datenbankserver für Integrationstests mit Maven starten und stoppen
Der Datenbankserver H2 ist ein beliebter Server während der Entwicklungsphase einer Software. Die Fähigkeit zu In-Memory-Datenbanken ist für viele Tests geradezu ideal. Wer jedoch die Datenbankstrukturen analysieren will, kommt um eine Datenbank mit Persistierung nicht herum. H2 kann natürlich das auch. Allerdings gestaltet sich das Starten/Stoppen für die Integrationstests etwas komplex. Natürlich gibt es entsprechende Plugins, aber auf einem CI-Server kommen so leicht Portkonflikte zustanden, wenn mehrere Builds gleichzeitig laufen. Ich beschreibe hier ein Setting, das den Maven-Build-Helper nutzt, um dies zu verhindern.
Das eigentliche Starten und Stoppen geht per Plugin ganz einfach. Allerdings mögen es CI-Systeme wie z.B. Jenkins nicht, wenn z.B. der Datenbankport hart vorgegeben ist. Da sind Konflikte einfach vorprogrammiert. Aber Mojohaus (Nachfolger des Mojo-Projekts bei Codehaus) bietet auch hier Abhilfe mit dem build-helper-maven-plugin.
[xml autolinks="false" collapse="false" firstline="1" gutter="true" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
...
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.12</version>
<executions>
<execution>
<id>reserve-ports</id>
<phase>generate-sources</phase>
<goals>
<goal>reserve-network-port</goal>
</goals>
<configuration>
<portNames>
<portName>h2port</portName>
</portNames>
</configuration>
</execution>
</executions>
</plugin>
...
</plugins>
...
</build>
...
[/xml]
Mit diesem Port (der jetzt als Maven-Property ${h2port} verfügbar ist) kann man nun das eigentliche Plugin konfigurieren. Wichtig ist, dass das Plugin die gleiche Version der H2-Datenbank nutzt, wie die Anwendung. Sonst kommen unschöne Fehler vor, die sich schwer debuggen lassen. Daher habe ich die Versionen wieder einmal als Properties ausgelagert:
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="3-4,14,17,43" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
...
<properties>
<h2.version>1.3.176</h2.version>
<h2-maven-plugin.version>1.0</h2-maven-plugin.version>
</properties>
...
<build>
...
<plugins>
...
<plugin>
<groupId>com.edugility</groupId>
<artifactId>h2-maven-plugin</artifactId>
<version>${h2-maven-plugin.version}</version>
<configuration>
<!--suppress MavenModelInspection --><!-- Will be set by the build-helper -->
<port>${h2port}</port>
<allowOthers>true</allowOthers>
<baseDirectory>${project.basedir}/target/data</baseDirectory>
<shutdownAllServers>true</shutdownAllServers>
<trace>true</trace>
</configuration>
<executions>
<execution>
<id>Spawn a new H2 TCP server</id>
<phase>pre-integration-test</phase>
<goals>
<goal>spawn</goal>
</goals>
</execution>
<execution>
<id>Stop a spawned H2 TCP server</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
</dependencies>
</plugin>
...
</plugins>
...
</build>
...
[/xml]
Damit wird nun der H2-Server vor den Integrationstests gestartet und danach gestoppt. Wichtig ist, dass die Integrationstests per failsafe ausgeführt werden, da der Stop durch die dynamische Zuweisung des Ports nicht mehr automatisiert durchgeführt werden kann, sobald der Mavenprozess abgebrochen ist. Ein Build-Abbruch während der Integrationstests führt also zu einem hängendem Datenbankserver auf einem automatisch generierten Port.
Im Zusammenspiel mit einem Datenbankversionierungssystem wie z.B. Liquibase lässt sich nun viel in Maven automatisieren.
-
Datenbankversionierung mit Liquibase und Maven
SQL-Datenbanken gehören trotz der großen Aufmerksamkeit für NOSQL zum Brot-und-Butter-Handwerkzeug eines Softwareentwicklers. Und zumindest im Unternehmensumfeld wird des wohl noch lange so bleiben, denn dort setzen sich neue Konzepte nur langsam durch. Doch schon während der Softwareentwicklung stößt man im Team auf das Problem, dass Änderungen an den Datenstrukturen verwaltet werden müssen. Die Kollegen brauchen bei Softwareänderungen auch geänderte Testdatenbanken, man will auch nachhalten, welcher Commit welche Datenbankänderung nach sich zog.
Hier kommen dann Datenbankversionierungstools wie Flyway oder eben Liquibase ins Spiel. Flyway habe ich uns vor langer Zeit nur kurz angeschaut, aber eine persönliche Präferenz des für das damalige Projekt verantwortlichen Softwarearchitekten für XML-basierte Tools hat mich dann zu Liquibase geführt. Wie es in ein Mavenprojekt integriert werden kann, damit Integrationstests immer auf eine aktuelle Datenbank zugreifen können, will ich im folgenden beschreiben.
Ich werde hier nicht näher darauf eingehen, wie man zum Beispiel einen H2-Server in einen Maven-Integrationstest einbauen kann. Ich gehe von einem laufenden Server aus, um dann die Datenbankstruktur validieren und updaten zu können.
Die Verwaltung der Datenbankänderungen
Man braucht natürlich die Dateien, die die Datenbank und ihre Änderungen beschreiben. Ich habe mir Angewöhnt, nicht erst beim 1. Update in der Produktion mit Liquibase zu arbeiten sondern bereits in der Entwicklung damit zu starten. Im Team kann man so auch leicht Strukturänderungen an den Datenbanken übernehmen und damit kommunizieren. Außerdem werden sie so im Sourcecodemanagement-Tool verwaltet und können (und sollten!) Bestandteil des Code-Review im Team werden. Ich fange meist mit einer zentralen Changelog-Datei an, die dann weitere Dateien einbindet. Je nach Komplexität werden in den eingebundenen Dateien direkt Datenbankänderungen konfiguriert oder weitere Dateien eingebunden. Es lässt sich so eine Gruppierung der Änderungen vornehmen:
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="6" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<include file="topic-file.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
[/xml]
Natürlich können hier auch mehrere Dateien eingebunden werden. Doch schauen wir uns das topic-file.xml mal genauer an:
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="6" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<include file="topic-1.0.0.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>[/xml]
Auch hier sind noch keine Änderungen direkt erfasst. Ich nutze diese Ebene, um die verschiedenen Versionen zu strukturieren. Damit lässt sich sehr einfach nachhalten, zu welchem Release welche Datenbankänderungen eingeflossen sind. Und zu guter letzt kommt die eigentliche Verwaltungsdatei topic-1.0.0.xml (die ersten Nutzungen von Maven-Properties, die durch die Filterung von Maven dann ersetzt werden, habe ich noch hervorgehoben):
[xml autolinks="false" collapse="false" firstline="1" gutter="true" hightlight="13,16,23" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<changeSet id="topic-schema-create-h2" author="klenkes74">
<preConditions>
<dbms type="H2"/>
<preConditions>
<comment>Creates the schema needed for the database.</comment>
<sql>CREATE SCHEMA IF NOT EXISTS ${db.schema};</sql>
...
</changeSet>
<changeSet id="topic-initial" author="klenkes74">
<comment>The initial data base tables.</comment>
...
</changeSet>
</databaseChangeLog>
[/xml]
Über die eigentliche Syntax der Datei möchte ich mich nicht auslassen, sie ist auf der Webseite von Liquibase gut beschrieben und recht eingängig. Wichtig ist, dass ein Changeset nicht mehr verändert werden darf, sobald es einmal im zentralen SCM (git oder svn oder was immer ihr auch einsetzen mögt) eingecheckt wurde. Alle späteren Änderungen müssen als weitere Changesets veröffentlicht werden.
Diese Dateien lege ich meistens im Ordner src/main/resources/ddl ab. Sollte ich Daten via Liquibase importieren (mit dieser Funktion kann man sich oft ein gesondertes Aufsetzen von z.B. dbunit sparen), dann liegen diese Dateien meist unter src/main/resources/data für Daten, die auch in Produktion gebraucht werden (z.B. einen Admin-User in den Benutzertabellen) oder unter src/test/resources/data für Testdaten. Apropos Testdaten. Hier hilft meine Changelog-Datei, die unter src/test/resources/ddl liegt:
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="7" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<!-- Import the real changelog -->
<include file="${project.build.outputDirectory}/ddl/changelog-master.xml" relativeToChangelogFile="false" />
</databaseChangeLog>
[/xml]
Wie man schön sieht, binde ich hier die Änderungsdatei aus der Produktion ein. Außerdem könnte ich hier jetzt noch Testdaten-Importe durchführen.
Die JPA-persistence.xml Konfigurationsdatei
Ich nutze unter src/test/resources/META-INF/persistence.xml eine besondere persistence.xml für Integrationstests. Man sieht hier gut die Nutzung der Maven-Properties für die eigentliche Konfiguration. Natürlich muss Maven so aufgesetzt sein, dass die Resourcen auch gefiltert werden.
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="13-16,19" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="tenant">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
... meine Klassen sind hier nicht besonders interessant, daher weg damit ...
<properties>
<property name="javax.persistence.jdbc.url" value="${jdbc.url}"/>
<property name="javax.persistence.jdbc.user" value="${jdbc.user}"/>
<property name="javax.persistence.jdbc.password" value="${jdbc.password}"/>
<property name="javax.persistence.jdbc.driver" value="${jdbc.driver}"/>
<property name="hibernate.show_sql" value="false"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.dialect" value="${jdbc.dialect}"/>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<!-- Configuring Connection Pool -->
<property name="hibernate.c3p0.min_size" value="5"/>
<property name="hibernate.c3p0.max_size" value="20"/>
<property name="hibernate.c3p0.timeout" value="500"/>
<property name="hibernate.c3p0.max_statements" value="50"/>
<property name="hibernate.c3p0.idle_test_period" value="2000"/>
</properties>
</persistence-unit>
</persistence>
[/xml]
Einbinden des Liquibase-Plugins
Nachdem ich die Daten vorbereitet habe, kommt jetzt das Zusammenfügen per Maven. zuerst brauchen wir ja die Properties, die oben ersetzt werden und einige, die uns das Arbeiten mit dem Plugin erleichtern. Zu beachten ist die Zeile 6, in der die benötigte URL steht. Hier nutze ich die Maven-Property ${h2port}. Diese wird bei mir vom Maven-Build-Helper gesetzt. Wer diesen nicht einsetzen will, sollte hier einen entsprechenden Port eintragen, sonst wird das nicht funktionieren:
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="8" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
...
<properties>
...
<skipDatabaseSetup>${skipTests}</skipDatabaseSetup>
<db.schema>TENANT</db.schema>
<database.changelog>${project.build.testOutputDirectory}/ddl/changelog-test.xml</database.changelog>
<jdbc.url>jdbc:h2:tcp://localhost:${h2port}/testdb;AUTO_RECONNECT=TRUE;DB_CLOSE_DELAY=-1</jdbc.url>
<jdbc.driver>org.h2.Driver</jdbc.driver>
<jdbc.user>sa</jdbc.user>
<jdbc.password>password</jdbc.password>
<jdbc.driver>org.h2.Driver</jdbc.driver>
<jdbc.dialect>org.hibernate.dialect.H2Dialect</jdbc.dialect>
<liquibase-maven-plugin.version>3.5.2</liquibase-maven-plugin.version>
<h2.version>1.3.176</h2.version>
...
</properties>
...
[/xml]
Und nun die Plugin-Definition innerhalb der Builddefinition des pom-Files:
[xml autolinks="false" collapse="false" firstline="1" gutter="true" highlight="12-16,20,27,35-39,43" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
...
<build>
...
<plugins>
...
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase-maven-plugin.version}</version>
<configuration>
<changeLogFile>${database.changelog}</changeLogFile>
<driver>${jdbc.driver}</driver>
<url>${jdbc.url}</url>
<username>${jdbc.user}</username>
<password>${jdbc.password}</password>
<verbose>true</verbose>
<logging>warning</logging>
<promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
<skip>${skipDatabaseSetup}</skip>
<dropFirst>false</dropFirst>
</configuration>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>update-db</id>
<phase>pre-integration-test</phase>
<configuration>
<changeLogFile>${database.changelog}</changeLogFile>
<driver>${jdbc.driver}</driver>
<url>${jdbc.url}</url>
<username>${jdbc.user}</username>
<password>${jdbc.password}</password>
<verbose>true</verbose>
<logging>warning</logging>
<promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
<skip>${skipDatabaseSetup}</skip>
<dropFirst>false</dropFirst>
</configuration>
<goals>
<goal>update</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
...
</build>
...
[/xml]
Und das war es eigentlich auch schon. Hat man alles richtig gemacht, wird nun in der Maven-Build-Phase pre-integration-test die Datenbank via Liquibase upgedated. Wer sich das ganze im Zusammenspiel anschauen möchte, ist gerne eingeladen, mal in mein Projekt https://github.com/klenkes74/kp-office zu schauen. Dort habe ich ein Parent für JPA-Module (kp-office-parent-adapter-jpa) mit den globalen Definitionen (wie dem Plugin) und dann Modulen, die dies nutzen (z.B. kp-office-base-adapter-jpa), in denen dann die Changelog-Dateien und Testdaten für die eigentlichen Datenbanken liegen.
-
Aufruf!
Nichts ist eines Kulturvolkes unwürdiger, als sich ohne Widerstand von einer verantwortungslosen und dunklen Trieben ergebenen Herrscherclique "regieren" zu lassen. Ist es nicht so, daß sich jeder ehrliche Deutsche heute seiner Regierung schämt, und wer von uns ahnt das Ausmaß der Schmach, die über uns und unsere Kinder kommen wird, wenn einst der Schleier von unseren Augen gefallen ist und die grauenvollsten und jegliches Maß unendlich überschreitenden Verbrechen ans Tageslicht treten?
Wenn das deutsche Volk schon so in seinem tiefsten Wesen korrumpiert und zerfallen ist, daß es, ohne eine Hand zu regen, im leichtsinnigen Vertrauen auf eine fragwürdige Gesetzmäßigkeit der Geschichte das Höchste, das ein Mensch besitzt und das ihn über jede andere Kreatur erhöht, nämlich den freien Willen, preisgibt, die Freiheit des Menschen preisgibt, selbst mit einzugreifen in das Rad der Geschichte und es seiner vernünftigen Entscheidung unterzuordnen - wenn die Deutschen, so jeder Individualität bar, schon so sehr zur geistlosen und feigen Masse geworden sind, dann, ja dann verdienen sie den Untergang.
Goethe spricht von den Deutschen als einem tragischen Volke, gleich dem der Juden und Griechen, aber heute hat es eher den Anschein, als sei es eine seichte, willenlose Herde von Mitläufern, denen das Mark aus dem Innersten gesogen und die nun ihres Kerns beraubt, bereit sind, sich in den Untergang hetzen zu lassen. Es scheint so - aber es ist nicht so; vielmehr hat man in langsamer, trügerischer, systematischer Vergewaltigung jeden einzelnen in ein geistiges Gefängnis gesteckt, und erst als er darin gefesselt lag, wurde er sich des Verhängnisses bewußt. Wenige nur erkannten das drohende Verderben, und der Lohn für ihr heroisches Mahnen war der Tod. Über das Schicksal dieser Menschen wird noch zu reden sein.
Wenn jeder wartet, bis der andere anfängt, werden die Boten der rächenden Nemesis unaufhaltsam näher und näher rücken, dann wird auch das letzte Opfer sinnlos in den Rachen des unersättlichen Dämons geworfen sein. Daher muß jeder einzelne seiner Verantwortung als Mitglied der christlichen und abendländischen Kultur bewußt in dieser letzten Stunde sich wehren, soviel er kann, arbeiten wider die Geißel der Menschheit, wider den Faschismus und jedes ihm ähnliche System des absoluten Staates. Leistet passiven Widerstand - Widerstand -, wo immer Ihr auch seid, verhindert das Weiterlaufen dieser atheistischen Kriegsmaschine, ehe es zu spät ist, ehe die letzten Städte ein Trümmerhaufen sind, gleich Köln, und ehe die letzte Jugend des Volkes irgendwo für die Hybris eines Untermenschen verblutet ist. Vergeßt nicht, daß ein jedes Volk diejenige Regierung verdient, die es erträgt!
Aus Friedrich Schiller, "Die Gesetzgebung des Lykurgus und Solon":
".... Gegen seinen eigenen Zweck gehalten, ist die Gesetzgebung des Lykurgus ein Meisterstück der Staats- und Menschenkunde. Er wollte einen mächtigen, in sich selbst gegründeten, unzerstörbaren Staat; politische Stärke und Dauerhaftigkeit waren das Ziel, wonach er strebte, und dieses Ziel hat er so weit erreicht, als unter seinen Umständen möglich war. Aber hält man den Zweck, welchen Lykurgus sich vorsetzte, gegen den Zweck der Menschheit, so muß eine tiefe Mißbilligung an die Stelle der Bewunderung treten, die uns der erste flüchtige Blick abgewonnen hat. Alles darf dem Besten des Staats zum Opfer gebracht werden, nur dasjenige nicht, dem der Staat selbst nur als ein Mittel dient. Der Staat selbst ist niemals Zweck, er ist nur wichtig als eine Bedingung, unter welcher der Zweck der Menschheit erfüllt werden kann, und dieser Zweck der Menschheit ist kein anderer, als Ausbildung aller Kräfte des Menschen, Fortschreitung. Hindert eine Staatsverfassung, daß alle Kräfte, die im Menschen liegen, sich entwickeln; hindert sie die Fortschreitung des Geistes, so ist sie verwerflich und schädlich, sie mag übrigens noch so durchdacht und in ihrer Art noch so vollkommen sein. Ihre Dauerhaftigkeit selbst gereicht ihr alsdann viel mehr zum Vorwurf als zum Ruhme - sie ist dann nur ein verlängertes Übel; je länger sie Bestand hat, um so schädlicher ist sie.
... Auf Unkosten aller sittlichen Gefühle wurde das politische Verdienst errungen und die Fähigkeit dazu ausgebildet. In Sparta gab es keine eheliche Liebe, keine Mutterliebe, keine kindliche Liebe, keine Freundschaft es gab nichts als Bürger, nichts als bürgerliche Tugend.
... Ein Staatsgesetz machte den Spartanern die Unmenschlichkeit gegen ihre Sklaven zur Pflicht; in diesen unglücklichen Schlachtopfern wurde die Menschheit beschimpft und mißhandelt. In dem spartanischen Gesetzbuche selbst wurde der gefährliche Grundsatz gepredigt, Menschen als Mittel und nicht als Zwecke zu betrachten dadurch wurden die Grundfesten des Naturrechts und der Sittlichkeit gesetzmäßig eingerissen.
... Welch schöneres Schauspiel gibt der rauhe Krieger Gaius Marcius in seinem Lager vor Rom, der Rache und Sieg aufopfert, weil er die Tränen der Mutter nicht fließen sehen kann!
... Der Staat (des Lykurgus) könnte nur unter der einzigen Bedingung fortdauern, wenn der Geist des Volks stillstünde; er könnte sich also nur dadurch erhalten, daß er den höchsten und einzigen Zweck eines Staates verfehlte."
Aus Goethes "Des Epimenides Erwachen", zweiter Aufzug, vierter Auftritt:
Genien:
Doch was dem Abgrund kühn entstiegen,
Kann durch ein ehernes Geschick
Den halben Weltkreis übersiegen,
Zum Abgrund muß es doch zurück.
Schon droht ein ungeheures Bangen,
Vergebens wird er widerstehn!
Und alle, die noch an ihm bangen,
Sie müssen mit zu Grunde gehn.
Hoffnung:
Nun begegn' ich meinen Braven,
Die sich in der Nacht versammelt,
Um zu schweigen, nicht zu schlafen,
Und das schöne Wort der Freiheit
Wird gelispelt und gestammelt,
Bis in ungewohnter Neuheit
Wir an unsrer Tempel Stufen
Wieder neu entzückt es rufen:
(Mit Überzeugung, laut:)
Freiheit!
(gemässigter:)
Freiheit!
(von allen Seiten und Enden Echo:)
Freiheit!
-
Spaß mit der Telekom.... äh DHL
Eventuell erinnert sich noch jemand an die Sendug "Wie bitte?" mit der Dauerrubrik "Spaß mit der Telekom". Nunja, ich hätte hier etwas für ein Unternehmen aus dem gleichen Hause: DHL (ja, früher war die Telekom ein Bestandteil der Post).
Es begann an einem Dienstag Vormittag ...
Ich musste Unterlagen dringend an meine Anwaltskanzlei nach Darmstadt schicken. Also habe ich mir gedacht, dass DHL Express mit Auslieferung bis 12:00 eine gute Wahl wäre. Denn damit sollte sich die Post auskennen. Ich druckte mir also die im Internet gekaufte Marke aus und klebte sie auf den Umschlag mit für mich recht wichtigen Dokumenten. Auf dem Ausdruck stand, dass man 29000 Annahmestellen im Netz findet. Filialen, Packstationen, ...
Und da bei mir eine Packstation direkt um die Ecke steht, ging ich dorthin und diese nahm die Sendung auch gerne an. Ich hatte einen grundlegenden Fehler gemacht. Der Fehler bestand darin, im Bestellformular nicht auf eines der vielen Fragezeichen zu clicken. Denn dann hätte ich erfahren, dass ich Express-Sendungen nicht in einer Packstation einliefern dürfe. Aber die Sendung wurde angenommen und auch schon 15 Minuten später vom Fahrer abgeholt. Das war perferkt.
Allerdings änderte sich dann am Sendungsstatus nichts mehr. Ich rief also am Mittwochmorgen bei DHL an und erfuhr dann von meinem großen Fehler. Ich hätte die Sendung einem komplett anderem Unternehmen übergeben, DHL Express und DHL hätten nichts miteinander zu tun und der Brief würde jetzt per Standard-Paket-Versand transportiert. Es gäbe auch keinerlei Kommunikationsmöglichkeit zwischen den beiden Unternehmen, um die Sendung wieder in die Expresslieferung zu bekommen. Mir wurde auch gesagt, dass es rechtlich keinen Unterschied gemacht hätte, wenn ich die Sendung bei einem Hermes-Shop abgeliefert hätte.
Nach einigem Geschimpfe meinerseits wurde ich an einen "Team-Experten" weitergereicht. Dieser erklärte mir wiederrum, dass ich die Sendung bei einem anderen Unternehmen eingereicht hätte und das mein Problem sei. Komischerweise war auf dem Einlieferungsbeleg des Packstation ein expliziter Verweis auf die AGB der DHL EXPRESS vorhanden. Und auf dem Auftragsschein stand: "Fachtführer Deutsche Post AG". Und die ist Mutter aller beteiligten Unternehmen.
Aber nach einer weiteren Rückfrage konnte mir der Team-Experte die glückliche Nachricht überbringen, dass DHL auf eigene Rechnung eine DHL DOMESTIC EPRESS 9:00-Lieferung durchführen würde und der Fahrer die Sendung bei mir abholen würde. Was auch passiert ist. Ich war glücklich, dass die Unterlagen noch am Donnerstag Morgen bei meiner Anwältin sein sollten.
Um kurz vor 9:00 heute Morgen (Donnerstag) kontrollierte ich die Sendungsverfolgung im Internet - der letzte Eintrag war von ca. halb acht. Die Sendung sei in der Verteilstation in Frankfurt angekommen. Nunja, das würde knapp werden, das noch nach Darmstadt zu bekommen. Ich rief also an und erfuhr, dass die Sendung im Kurrierwagen verladen sei. Ab ca. 9:30 wurde mir das auch im Internet so angezeigt, dass die Sendung seit ca. 1 Stunde auf dem Weg sei.
Ich lehnte mich also zurück und war erstmal glücklich, dass die Sendung zwar nicht um 9:00 aber doch noch am Vormittag ankommen würde.
Und dann wurde es 12:00 und ich rief bei der Kanzlei an, ob die Sendung angekommen sei. Und erfuhr, dass sie keinen Brief von mir erhalten hätten.
WTF?
Und wieder einmal ein Anruf bei DHL ... ja, die Sendung würde heute im Laufe des Tages ausgeliefert. Nach meinem Hinweis auf "DHL DOMESTIC EXPRESS 9:00" wurde wieder eine Beschwerde aufgenommen und mir ein Rückruf im Laufe des Tages versprochen.
Mal sehen, ob die Sendung noch rechtzeitig eingeht, damit die gerichtlich festgesetzte Frist nicht verstreicht. Eines habe ich bereits gelernt: DHL kann kein Express. Weder bis 12:00 noch bis 09:00. Und EDV hat die Deutsche Post AG auch nicht im Griff, da deren Automaten eindeutig als Express gekennzeichnete Sendungen annimmt - und diese dann irgendwo im System verschwinden läßt.
Nachtrag 14:03
Bis jetzt hat es die DHL Express Kundenservice-Beschwerdeabteilung nicht hinbekommen, einen Kontakt zum Depot in Frankfurt herzustellen, um herauszufinden, was mit meiner Sendung passiert ist. Mir wurde wieder einmal ein Rückruf versprochen. Kann DHL eigentlich irgendetwas, was mit Logistik zu tun hat? Kommunikation können sie schonmal nicht. Jedenfalls nicht miteinander. Die Spannung steigt, ob ich heute noch nach Darmstadt fahren darf ...
Nachtrag 14:20
Laut DHL ist von 9:00 bei der Auslieferung nichts bekannt und der Kundenservice hätte Druck gemacht, dass vor 15:00 ausgeliefert wird. Mal sehen, was der "Team-Experte" mir erzählt, auf den ich diesmal wieder verwiesen habe.
Nachtrag 14:59
Ich wage es nicht zu glauben. Laut DHL Kundenservice ist das Schreiben angekommen und ich erhalte eine Email, mit der meine Kontoverbindung erfragt wird, um mir das ursprüngliche Beförderungsentgelt zu ersetzen.
Auch ein Anruf bei meiner Anwältin bestätigt, dass das Schreiben da ist. Ich erwarte den Igor, der mit quietschiger Stimme "Es lebt!" ruft ...
Nachtrag 27.11.2015, ca 13:20
Heute kam wieder ein Anruf von DHL Express. Eine nette Dame wollte wissen, auf welche Kundenummer die Sendung vom 25. November gebucht werden dürfte. Ich wies sie dann amüsiert und unter Hinweis auf den "Team-Experten" darauf hin, dass es die Kundennummer von DHL Express sein dürfte. Ich bin gespannt, wie es weiter geht ...
-
-
-
-
-
Eine zentrale Meldestelle für staatliche Willkür
Nach den sich immer wiederholenden Berichten rund um Polizeiwillkür (bedauerliche Einzelfälle), die oft sehr fragwürdigen Methoden der ARGEN und natürlich den großen Themen Voratsdatenspeicherung, Totalüberwachung, Geheimdienste, kam mir eine Idee: wenn der Staat zentrale Datenbestände hat, in denen er "Verdächtige" speichert, warum sollen nicht auch Bürger einen zentralen Datenbestand anlegen, in denen sie Übergriffe sammeln. Ich werde hier ein paar Gedanken veröffentlichen, um weitere Mitstreiter zu finden, die hier unterstützen können.
Geplant ist eine zentrale Datenbank, in der Protokolle von Willkürakten seitens des Staates in Verbindung mit der jeweiligen Behörde gesammelt werden. Es ist nicht geplant, einzelne Beamte bloßzustellen, daher müssen in den veröffentlichten Dokumenten alle Hinweise auf Einzelpersonen (nicht Gremien) entfernt werden.
Nach einer Meldung sind die Vorgänge erst einmal komplett ungeprüft und werden auch als solche deutlich gekennzeichnet. Sonst wäre es zu einfach, Falschberichte einzureichen. Die Prüfung ist noch nicht ganz durchdacht, denn mir wird keine Behörde Auskünfte geben. Ich habe daran gedacht, die Piratenpartei zu nutzen, zumindest dort, wo sie schon in Parlamenten sitzt: regelmäßig könnten die anonymisierten Dokumente und Statistiken nach Behörde den Volksvertretern zugeschickt werden, damit diese über Anfragen in den Parlamenten die Vorgänge verifizieren können.
Damit ließe sich eine dreistufige Statistik erstellen:
Verifizierte Fälle (die unangenehmste Kategorie für den Staat)
Starke Verdachtsfälle (nicht vollständig verifiziert, aber mit starken Hinweisen - Kriterien müssen definiert werden)
Ungeprüfte Fälle
Diese Daten könnten in regelmäßigen Berichten veröffentlicht werden und so die staatliche Behauptung, dass es sich um Einzelfälle handelt, entweder bestätigen oder entkräften.
Zunächst gibt es zwei Dinge zu tun:
Rechtliche Prüfung, inwieweit die entsprechenden Dokumente veröffentlicht werden dürfen (eventuell ab wann sie veröffentlich werden dürfen).
Design einer Systematik, um die Anonymität der einzelnen handelnden Beamten sowie auch der Betroffenen (und besonders der Zeugen) zu schützen. Das System soll kein Pranger für Beamte sein, sollte es sich um einzelne fehlhandelnde Beamte handeln, so ist es Aufgabe der beschuldigten Behörde, diese zu identifizieren und entsprechende Maßnahmen zu treffen.
Bitte kommentiert diesen Post oder schreibt mir direkt Emails, wenn ihr helfen wollt, oder Bedenken oder Vorschläge habt. Ich werde versuchen dieses Projekt notfalls auch alleine auf die Beine zu stellen, aber in Gruppen geht es schneller und man verrennt sich nicht in Sackgassen.
Ihr erreicht mich unter roland@lichti.de.
-
Konstruktoren, Factory oder Builder? Wie entstehen Objekte?
Im Netz wurde schon sehr viel geschrieben, wie man Objekte erschaffen kann. Es ist eines der Grundprobleme bei der Softwareentwicklung und wie bei vielen wichtigen Entscheidungen gibt es eine klare Antwort: Es kommt auf die Situation an.
Die einfachste Fassung ist es, den Konstruktor direkt aufzurufen. Das funktioniert immer, wird aber ab einer bestimmten Anzahl von Paramtern leicht unübersichtlich, vor allem wenn viele diese Parameter vom gleichen Typ sind. Das Typenproblem kann aber auch schon bei sehr wenigen Parametern auftreten:
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="11,20-27" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
package de.kaiserpfalzEdv.blog.creation;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ConstructorDemo {
...
public demo() {
...
ValueObject object = new ValueObject("Meier", "Hermann");
...
}
}
class ValueObject {
private String familyName;
private String givenName;
public ValueObject(final String familyName, final String givenName) {
checkArgument(isNotBlank(familyName));
checkArgument(isNotBlank(givenName));
this.familyName = familyName;
this.givenName = givenName;
}
public String getFamilyName() {
return familyName;
}
public String getGivenName() {
return givenName;
}
}
[/java]
Bei diesem Bespiel könnte es nach einiger Zeit (oder bei einem anderen Entwickler) fraglich sein, ob man zuerst den Nachnamen oder den Vornamen übergeben muss. Noch schlimmer wird es, wenn es vier oder fünf Parameter werden und diese eventuell sogar den gleichen Typ haben - da kann selbst die beste IDE nicht mehr helfen und es wird fast automatisch irgendwann zu Verdrehern kommen.
Das Builder Pattern (deutsch: Erbauer) hilft es, diese Klippe zu umschiffen. Hier hilft eine weitere Klasse, der sogenannte Builder, das Objekt zu erschaffen:
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="12,35-57" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
package de.kaiserpfalzEdv.blog.creation;
import java.lang.IllegalStateException;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class BuilderDemo {
...
public demo() {
...
ValueObject object = new ValueObject.Builder().withFamilyName("Meier").withGivenName("Hermann").build();
...
}
}
class ValueObject {
private String familyName;
private String givenName;
private ValueObject() {}
private boolean isValid() {
return isNotBlank(familyName) && isNotBlank(givenName);
}
public String getFamilyName() {
return familyName;
}
public String getGivenName() {
return givenName;
}
public static class Builder() {
private ValueObject buildObject = new DataClass();
public ValueObject build() {
if (buildObject.isValid()) {
return buildObject;
} else {
throw new IllegalStateException("Sorry, DataObject can not be created with the given data.");
}
}
public Builder withFamilyName(final String name) {
buildObject.familyName = name;
return this;
}
public Builder withGivenName(final String name) {
buildObject.givenName = name;
return this;
}
}
}
[/java]
Jetzt können die Paramter nur noch verwechselt werden, wenn der Entwickler nicht weiß, was "familyName" und was "givenName" bedeutet. Aber dann hat man ein ganz anderes Problem. Wie man aber am Code sieht, muss man sich ein paar Gedanken machen: der eigentliche Konstruktor ist nun private, kann also nicht mehr direkt aufgerufen werden.
Das funktioniert, wenn der Builder wie hier eine innere Klasse zum ValueObject darstellt. Bei so kleinen Objekten wie diesem hier geht das auch noch, kann aber unübersichtlich werden.
Bleibt einem so also nicht die Wahl und man muss den Builder als eigene Klasse bauen, dann könnte man das eigentliche Value-Objekt package-local definieren (also ohne "public") und den Builder ins gleiche Package stecken. Der Konstruktor des Value-Objektes müsste demnach dann auch package-lokal sein, was die enge Kapselung leider wieder etwas öffnet. Auch müsste man die Variablen des Value-Objektes package-lokal definieren (oder entsprechende Setter, die wie man oben sieht hier vollständig fehlen).
Im Builder kann man aber auch noch etwas anderes verstecken: Werden oft Objekte mit den gleichen Daten benötigt, so könnten diese in einen Cache gelegt werden und dann diese wieder ausgegeben werden. Da es sich um ValueObjekte handelt, deren Status (hier: Vor- und Nachname) sich nicht mehr ändern kann, kann auch die Software in Wirklichkeit mit einem einzelnen Objekt arbeiten und braucht nicht immer ein neues Objekt. Es kann also dadurch optimiert werden.
Was mache ich aber, wenn die die Möglichkeit des Wiederverwendens eines Objekts nutzen will, aber ein Builder mit Kanonen auf Spatzen geschossen wäre (zum Beispiel, wenn ich nur einen Parameter habe)?
Dann kann ich eine Factory nutzen.
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="11,34-39" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
package de.kaiserpfalzEdv.blog.creation;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ConstructorDemo {
...
public demo() {
...
ValueObject object = ValueObject.Factory.getInstance("Meier", "Hermann");
...
}
}
class ValueObject {
private String familyName;
private String givenName;
private ValueObject(final String familyName, final String givenName) {
this.familyName = familyName;
this.givenName = givenName;
}
public String getFamilyName() {
return familyName;
}
public String getGivenName() {
return givenName;
}
public static class Factory {
public static ValueObject getInstance(final String familyName, final String givenName) {
checkArgument(isNotBlank(familyName));
checkArgument(isNotBlank(givenName));
return new ValueObject(familyName, givenName);
}
}
}
[/java]
Die Factory unterscheidet sich von der Factory-Methode dadurch, dass sie eine eigene Klasse ist, die eine oder mehrere Factory-Methoden in sich vereint, während die Factory-Methode eine statische Methode in der eigentlichen Klasse darstellt:
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="11,25-30" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="2" toolbar="true"]
package de.kaiserpfalzEdv.blog.creation;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ConstructorDemo {
...
public demo() {
...
ValueObject object = ValueObject.getInstance("Meier", "Hermann");
...
}
}
class ValueObject {
private String familyName;
private String givenName;
private ValueObject(final String familyName, final String givenName) {
this.familyName = familyName;
this.givenName = givenName;
}
public static ValueObject getInstance(final String familyName, final String givenName) {
checkArgument(isNotBlank(familyName));
checkArgument(isNotBlank(givenName));
return new ValueObject(familyName, givenName);
}
public String getFamilyName() {
return familyName;
}
public String getGivenName() {
return givenName;
}
}
[/java]
Damit haben wir nun alle vier Möglichkeiten gesehen. Welche passt am besten? Und die Antwort hatten wir schon zu Anfang: Es kommt auf die Situation an. Eventuell kann die folgende Kurzliste helfen ...
Das Objekt hat nur wenige (1-3) Parameter, die auch nicht leicht zu verwechseln sind. Außerdem soll es jedesmal neu geschaffen werden. Ein typischer Fall für den Konstruktor-Aufruf per "new Object(...)".
Das Objekt hat nur wenige (1-3) Parameter, die auch nicht leicht zu verwechseln sind. Es soll eventuell gecacht werden. Ein typischer Fall für die Factory-Method innerhalb des Objekts.
Das Objekt hat nur wenige (1-3) Parameter, die auch nicht leicht zu wechseln sind. Es soll eventuell gecacht werden oder gar eine Subklasse generiert werden (abhängig von den Parametern). Ein typischer Fall für die Factory.
Das Objekt hat viele Paramter oder wenige Paramter, die leicht zu verwechseln sind. Hier kann der Builder echt helfen ...
-
Lesbarer Quellcode - Heute: Wie validiere ich Methodenparameter?
Fast alle Softwareentwickler sind sich einig, dass Quellcode lesbar sein soll und für andere Entwickler möglichst lesbar sein soll.
Was allerdings als lesbar und leicht verständlich ist, ist nicht immer so eindeutig. Neben den vereinbarten Styleguides und den eigenen Vorlieben gibt es dann noch andere Kriterien.
Ein Beispiel ist die Validierung von Parametern. Viele generische Bibliotheken beinhalten entsprechende Funktionen. Von den von mir benutzten Bibliotheken dürften zwei der bekanntesten Google Guava und die schon zu den Altmeistern gehörende apache-commons Bibliothek sein. Beide beinhalten Methoden zur Validierung von Parametern. Bei Google handelt es sich um die Klasse Preconditions mit den checkArgument()-Funktionen, bei Apache Commons sind es die Methoden der Klasse Validate (für Java bis 1.4) bzw. Validate (für Java 5+). Egal für welche Variante man sich entscheidet, beide Methoden werfen eine IllegalArgumentException.
Jetzt habe ich ein Projekt, in dem beide Klassen (Guava und apache-commons) genutzt werden. Ob das sinnvoll ist, zwei sich soweit überdeckende allgemeine Bibliotheken zu nutzen, wäre einen eigenen Blogpost wert, daher will ich hier nicht weiter darauf eingehen. Allerdings stellt sich dann die Frage, ob man jetzt zu den Precondition-Klassen oder die Validate-Klassen bei der Argumentvalidierung greift:
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="3,7" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="4" toolbar="true"]
package de.kaiserpfalzEdv.blog.style;
import static com.google.common.base.Preconditions.checkArgument;
public class ValidationStyle {
public void checkWithPrecondition(String argument) {
checkArgument(argument != null && !argument.isEmpty(), "You have to give a not-null and not-empty argument!");
...
}
...
}
[/java]
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="3,7" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="4" toolbar="true"]
package de.kaiserpfalzEdv.blog.style;
import org.apache.commons.lang3.Validate.notEmpty;
public class ValidationStyle {
public void checkWithValidate(String argument) {
notEmpty(argument, "You have to give a not-null and not-empty argument!");
...
}
...
}
[/java]
Auf den ersten Blick sieht die apache-commons-Variante unten eleganter aus. Das ist sie auch, da die Google-Variante immer einen kompletten boolschen Ausdruck erfordert. Allerdings kann man - wenn man sowieso schon Guava und apache-commons eingebunden hat, auch zu einer dritten Variante greifen:
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="3-4,8" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="4" toolbar="true"]
package de.kaiserpfalzEdv.blog.style;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
public class ValidationStyle {
public void checkWithPreconditionAndStringUtils(String argument) {
checkArgument(isNotEmpty(argument), "You have to give a not-null and not-empty argument!");
...
}
...
}
[/java]
In dieser Variante kann mn sofort erkennen, dass ein Argument überprüft wird ("checkArgument") und was denn geprüft wird ("isNotEmpty"). Im Moment ist diese Variante meine Lieblingsvariante. Aber ich bin für Anregungen offen. Die letzte Variante will ich nur noch der Vollständigkeit halber darstellen: alles selbst machen ...
[java autolinks="false" collapse="false" firstline="1" gutter="true" highlight="3,7-9" htmlscript="false" light="false" padlinenumbers="false" smarttabs="true" tabsize="4" toolbar="true"]
package de.kaiserpfalzEdv.blog.style;
import java.langIllegalArgumentException;
public class ValidationStyle {
public void checkWithPreconditionAndStringUtils(String argument) {
if (argument == null || argument.isEmpty) {
throw new IllegalArgumentException("You have to give a not-null and not-empty argument!");
}
...
}
...
}
[/java]
-
-
Das Leben ist Veränderung
Seit dem letzten Update hat sich bei uns einiges geändert.
Telefónica hat den Standort Verl geschlossen und wollte mich mit nach München nehmen. Allerdings habe ich mich entschlossen, stattdessen eine neue Stelle anzunehmen. Nach über 10 Jahren war dies wohl auch notwendig geworden.
Also arbeite ich jetzt als Externer Consultant für Volkswagen. Damit ergibt es sich, daß ich in Wolfsburg arbeite und der Arbeitsweg Bielefeld-Wolfsburg wäre doch etwas weit.
Daher sind wir im August von Bielefeld nach Lahstedt (bei Peine) gezogen. Wer die A2 nicht häufiger fährt, kennt Peine nicht. Peine liegt ziemlich genau in der Mitte zwischen Hannover und Braunschweig.
Alex ist auch Mitte August in die Schule gekommen und ist damit in die Reihen der i-Dötze aufgenommen. Wenn man sie fragt, sind Deutsch und Mathe ihre Lieblingsfächer - das ist praktisch, denn sie hat jeden Tag Deutsch und jeden Tag Mathe.
Marvin hat sich in ihrem neuen Kindergarten auch gut eingelebt und genießt es, mit den anderen Kindern auch Mittag essen zu dürfen, was Alex und Marvin ja beide in Bielefeld vermißten.
Nici ist dabei, die Kartons zu beseitigen und das Haus langsam aber sicher in einem bewohnbaren Zustand zu überführen. Leider läuft es langsamer als erwartet, da ich wegen Nierensteinen jetzt zum dritten Mal in zwei Monaten im Krankenhaus bin und Nici sich einen Bandscheibenvorfall zugezogen hat. Aber das Haus wird langsam ...
Fragen beantworten wir natürlich gerne. Und wer mal die A2 zwischen Hannover und Braunschweig befährt und Lust hat, ist eingeladen, uns zu besuchen. Ist nur ca. 10 km nach Süden von der Autobahnabfahrt Peine ab. Wer kein Navi hat: Richtung Ilsede und nach Groß Ilsede dann rechts ab. :-)
Persönliches
· 2011-09-07
-
Deligieren und Vertrauen ...
Chefs können nicht alles selbst machen. Sollen sie ja auch nicht. Dafür gibt es ja die Mitarbeiter, die nicht für Führung sondern für Arbeit bezahlt werden.
Bei einem Chef, der sich die Leute selbst einstellen durfte, hat er meistens genügend Vertrauen - er hat sich die Leute ja ausgesucht und damit genau jenen Typ, den er selbst bevorzugt und den er meint, einschätzen zu können.
Dieses Privileg haben aber nur die wenigsten Chefs - die meisten Chefs übernehmen eine Gruppe oder bekommen die Leute zugeteilt.
Da der Chef aber von seinem Chef für die Leistung verantwortlich gemacht wird, steckt er in einem Dilemma: entweder er weiß nicht, wem er etwas zutrauen kann oder er weiß, daß er es niemandem zutrauen kann, oder?
Drehen wir die Situation um. Wir haben ein eingespieltes Team. Es haben sich Arbeitswege etabliert und jedem ist klar, wer vom Chef mit welchen Aufgaben betraut wurde. Oder es gibt sogar "Stellvertreter" oder Leute, die normalerweise mit Führungsaufgaben oder Teilen davon betraut werden. Seien sie jetzt offiziell oder nur informell dazu geworden, ist hier egal. Jetzt kommt ein neuer Chef. Der kennt erstmal niemanden.
Leider ist es in dieser Situation teilweise üblich, erstmal alles selbst zu machen, da man den Leuten ja noch nicht vertrauen kann. Die Leute sehen aber, daß der Chef ihnen alles aus der Hand nimmt und ihnen keinerlei Verantwortung zutraut. Und dann kommt etwas zum Tragen, was immer vergessen wird: nicht nur die Chefs müssen ihren Mitarbeitern vertrauen sondern auch die Mitarbeiter ihren Chefs. Der Chef stellt nämlich das Gesicht der Gruppe nach außen dar.
Und Vertrauen ist ein Gut auf Gegenseitigkeit. Leider verlangen Chefs immer, daß man ihnen bedingungslos vertraut - sie selbst wollen aber, daß man sich das Vertrauen verdient. Das ist eine Schieflage.
Warum vertrauen sie nicht ihren Vorgängern? Nunja, wenn der Vorgänger "gegangen wurde", kann ich ja noch verstehen, wenn sie es nicht machen - aber sonst? Der Vorgänger war ja kein Idiot und normalerweise wird er schon einigermaßen passende Leute ausgewählt haben. Diese Entscheidung in Frage zu stellen, destabilisert die Gruppe. Und damit ihre Ergebnisse. Und damit steht der Chef schlecht da, da er schlechte Ergebnisse liefert.
Das nenne ich eine Lose-Lose-Situation.
Doch was kann er machen? Als allererstes mit den Leuten reden. Eine Führungskraft soll doch führen - und dazu muß er wissen, wie sich die Leute führen lassen und was überhaupt ihre Ideen sind. Denn auch Mitarbeiter haben Ziele, die sich meistens (oder zumindest oft) mit den Zielen der Gruppe in Einklang bringen lassen. Und jemand, der seine eigenen Ziele verfolgt, wird wahrscheinlich effektiver arbeiten als jemand, der gegen seine Ziele arbeiten muß.
Als zweites sollte er seinen Leuten das gleiche Vertrauen entgegenbringen, das er von ihnen einfordert. Denn ob er es sich eingesteht oder nicht: er hält so oder so den Kopf dafür hin, was seine Leute anstellen. Und wenn er ihnen Freiräume läßt, geht er die Gefahr ein, enttäuscht zu werden. Wenn er ihnen aber keine Freiräume läßt, dann wird er enttäuscht werden, denn er könnte genausogut die Gruppe entlassen. Wenn nur seine Entscheidungen zählen, braucht man die restlichen Gruppenmitglieder nicht - aber er wird mit der Arbeitslast nicht klarkommen. Also wird die Gruppe scheitern und damit er selbst.
Deligieren heißt nicht, jeden in dessen eigene Richtung rennen zu lassen. Natürlich gibt der Chef die Richtung vor, aber wie das Ziel erreicht wird oder ob man leichte Abweichungen in Kauf nehmen muß (zum Beispiel, weil andere Teams im Spiel mitmischen), das kann und sollte der Chef seinen Mitarbeitern schon zur Entscheidung zutrauen. Wenn er bestimmte Punkte auf jeden Fall erreichen will, kann er ja bei der Auftragserteilung darauf hinweisen. Dann muß er aber damit leben, daß der Auftrag zu ihm zurückkommt, wenn er dadurch für seinen Mitarbeiter unlösbar wird ...
-
Chefs sind nicht zum Kuscheln da ...
Die Unternehmenskultur unterscheidet sich von Unternehmen zu Unternehmen. Bei Unternehmen, die von ihrem Inhaber geführt werden, entspricht die Führungskultur oft dem Naturell des Unternehmers.
Bei größeren Unternehmen (besonders in Konzernen) entwickelt sich über die Jahre eine eigene Kultur, die nur sehr schwer zu ändern ist. In einigen Unternehmen wird "gekuschelt" (alle versuchen, dem jeweils anderen zu helfen), bei anderen Unternehmen wird scharf geschossen und Gewinner ist, wer den Schwarzen Gürtel im Verwaltungsdreikampf hat.
Eine ganz scheußliche Situation ist, wenn sich ein Verwaltungsdreikämpfer in eine Riege von Kuschlern erhoben hat. Dort kann er ungestört und ungehemmt wildern. Seine Abteilung wird wunderbar "funktionieren" während alle anderen Abteilungen immer weiter an den Rand gedrängt werden. In einer solchen Situation zeigt sich: Chefs werden nicht dafür bezahlt, bei anderen Chefs beliebt zu sein.
Der eine "Haifisch" wird den "Goldfischen" seinen Willen und seine Vorstellungen aufdrängen. Wenn er nicht durch die nächsthörere Ebene gebremst wird, entwickelt sich ein äußerst ungünstiges Ungleichgewicht zwischen den Abteilungen. Vereinbarungen müssen nur noch von den "Goldfischen" gehalten werden, der Hai darf sie nach belieben ignorieren oder verändern. Eine größere Organisation funktioniert aber zum guten Teil, daß Vereinbarungen eingehalten werden und sich jeder darauf verlassen kann.
Man könnte jetzt meinen, ich will nur Goldfische als Führungsebene. Nein. Ich will nur Haie. Führungskräfte sollen nicht kuscheln und lieb miteinander Ringelreihen spielen. Führungskräfte in Unternehmen müssen die Grenzen ihrer Kompetenzbereiche abstecken und die Schnittstellen zwischen den Bereichen sauberhalten und am Funktionieren halten. Mit Kuscheln kommt man da meist nicht weiter. Solange es keine Probleme gibt, braucht man keine Führungsmannschaft. Diese wird erst benötigt, wenn "der Staff", also die "arbeitende Masse" (oft "Fabrik" oder neudenglisch "Factory" genannt) nicht weiterkommt oder wenn es darum geht, Änderungen durchzusetzen.
-
Das Wort zum Sonntag
Es gibt ein paar Menschen, mit denen ich mich gerne unterhalte. Und oft ist es reiner Unsinn, den wir dann zusammenspinnen. Aber manchmal kommen echte Perlen zum Vorschein.
Und gestern schlug ein Kollege vor, daß man diese Perlen eigentlich publizieren müßte - zum Beispiel als Blog. Da ich sehr viel von ihm und seinen Ideen halte, dachte ich mir: Roland, das machst Du.
Ich werde also in loser Folge Gedanken zu Organisation von Firmen, dem Berufsleben und dem Universum, dem Leben und dem ganzen Rest hier veröffentlichen. Keine großgeistigen Dinge, aber Weisheiten und Binsenweisheiten, bei denen ich mich frage, ob ich ein solcher Spinner bin oder ob das viele andere Menschen einfach nicht wahrnehmen.
Vielleicht interessiert es ja jemanden.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Touch background to close