pixel
Select Page

NETWAYS Blog

Automatic PDF Generation with Google Chrome

Many developers get, sooner or later, the task to generate PDF documents automatically. Though, less developers put their experiences and insights then into a blog-post to save others some hassle. So let me do you a favor by explaining how we utilize Google Chrome in headless mode to generate pretty PDF files from HTML.

I won’t go into the details why Google Chrome. If you found this blog entry I suppose you already tried other options without success or satisfying results. We sure have tried several other options (e.g. wkhtmltopdf, dompdf, tcpdf) but only Google Chrome provided us with the results we wanted.

Another advantage of Google Chrome is the possibility to instrument a remotely running instance. And that’s exactly how we instruct it to generate a PDF file for us. Though, not with Puppeteer but with plain chrome devtools protocol (CDP) communication over a websocket.

I’ll avoid any programming language specific examples. You can take a look here at our implementation in PHP. So, let’s start with it.

The Process to Print HTML to PDF

First you’ll need to connect with the browser by use of a websocket connection at: ws://10.11.12.13:9222/devtools/browser/79744167-25f0-41a8-9226-382fa2eb4d66

This is printed on stderr right at launch of the process and also available on the JSON api: 10.11.12.13:9222/json/version

The important bit is the browser id (79744167-25f0-41a8-9226-382fa2eb4d66) at the end of the URI. This changes every time the process is launched and can’t be configured.

Communicating with the browser now takes place over this socket by transmitting and receiving JSON messages. They are divided into four types:

Calls

These primarily originate from ourselves. They contain an id, a method and parameters:

{ “id”: <mixed>, “method”: <string>, “params”: <object> }

The id is chosen by us and should be different for each call. It’s sent back with the response so it’s possible to later associate it with the call we made. Though this is mostly relevant if you are not communicating synchronously, which we do. So this may just as well be an integer incremented by 1 each time.

Results

These are sent by the browser in response to an API call.

{ “id”: <mixed>, “result”: <mixed> }

Errors

If it’s not a response, it’s an error.

{ “id”: <mixed>, “error”: { “code”: <int>, “message”: <string> } }

Events

These may be sent by the browser at any time.

{ “method”: <string>, “params”: <object> }

Some of these are of interest to us. Some of them are not and can be ignored.

Operating the Browser As Usual

In order to let the browser print us a web page (or HTML) to PDF we need to instrument it the same as when we do it manually on the desktop.

First we need to open a new tab:

Call: { “id”: 1, “method”: “Target.createTarget”, “params”: { “url”: “about:blank” } }

Result: { “id”: 1, “result”: { “targetId”: “418546565-d4f9-4d9f-8569-9ad8f9db7f9de” } }

Now we have to communicate with the tab, which requires a new websocket connection to: ws://10.11.12.13:9222/devtools/page/418546565-d4f9-4d9f-8569-9ad8f9db7f9de

Before we can print anything we have to tell the browser where to load the content to print from. This may either be a URI (which then needs to be accessible for the browser) or raw HTML. I’ll stick to raw HTML here, since it’s the most flexible option anyway:

Call: { “id”: 2, “method”: “Page.setDocumentContent”, “params”: { “frameId”: “418546565-d4f9-4d9f-8569-9ad8f9db7f9de”, “html”: <html> } }

Result: { “id”: 2, “result”: { } }

The next step is the instruction to print the page’s content to PDF:

Call: { “id”: 3, “method”: “Page.printToPDF”, “params”: <object> }

Result: { “id: 3, “result”: { “data”: <string> } }

What parameters you can use to influence the generation (e.g. the layout) are outlined in the official documentation.

The string you will get back is probably encoded in Base64, so decode it and store it where you want. The PDF file has been successfully generated.

If you are planning to use a single process to generate multiple PDFs with, remember to clean up afterwards. (i.e. closing the tab) Otherwise you will soon have a memory usage issue.

Call: { “id”: 4, “method”: “Target.closeTarget”, “params”: { “targetId”: “418546565-d4f9-4d9f-8569-9ad8f9db7f9de” } }

Result: { “id”: 4, “result”: { “success”: <bool> } }

I hope this proves useful to you or convinces you to give Google Chrome a try to generate pretty PDFs. ūüôā

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer f√ľr Icinga Web 2, Icinga DB Web sowie alle m√∂glichen anderen Module und Bibliotheken im Web Bereich zust√§ndig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gem√ľtlich. Passt das Wetter, geht's auch mal auf eines seiner Zweir√§der. Motorisiert oder nicht.

Icinga Web 2 – More Goodies for Developers

Tuesday version 2.7 of Icinga Web 2 has been released and introduced some interesting new functionality for module developers. Now I’d like to tell you, my fellow colleague, some more details about this.

 

jQuery v3 – Migration Required

First a friendly warning. We’ve upgraded the jQuery version we ship to v3.4.1. This has previously been v2.1.0 so now with this major upgrade some deprecated methods and interfaces are gone.

Though, don’t worry, you don’t need to hurry to avoid everyone complaining your module is incompatible with v2.7. We also ship jQuery migrate now which ensures that the usage of removed methods/interfaces still works. It also emits console warnings if it detects such a usage. The warnings are not active by default. They only appear when using the non-minimized javascript code. Put _dev in your address bar to instruct Icinga Web 2 to serve the non-minimized version. (e.g. /icingaweb2/dashboard?_dev)

Then start using the front-end as usual. Interact with all widgets you’ve written your own Javascript for and look for console entries starting with JQMIGRATE. Any of these messages will only appear once, repeated usages are not reported. If you’ve got a warning then, consult jQuery migrate’s warnings.md in order to get hints how to solve it.

jQuery migrate will be removed with Icinga Web 2 version 2.8.0. While this is still some time ahead, this (and the note in the upgrade documentation) is probably the only warning.

 

Persistence and Collapsible Containers

While we’re at it, let’s stay with the topic of Javascript. If you don’t already know about the localStorage and sessionStorage, it’s now time to inform yourself. (That’s an entire blogpost if described thoroughly)

There’s now an abstraction layer for this shipped with Icinga Web 2. It hides all the handling of complex datatypes and conflicts with other apps using the storage from you. That’s the object Icinga.Storage which utilizes the localStorage by default but also supports the sessionStorage. Take a look here to see how this is used for Icinga Web 2’s sidebar.

Though, this is only the basic stuff. If you need to store more complex data and want to benefit from a storage’s event processing, take a look at the object Icinga.Storage.StorageAwareMap. This is a proxy for Map and allows to subscribe to change events of particular keys in the map. It also keeps track of a key’s age and removes it automatically if it hasn’t been accessed for 90 days.

Another new addition are collapsible HTML containers. This is provided by a behavior which makes use of the StorageAwareMap, a perfect example use-case.

Making a container collapsible is as easy as possible. Just apply the CSS class collapsible and you’re done. If you’re not satisfied with the default height, apply the data attribute data-visible-height and give it the desired height in pixels. (For table‘s and ul‘s or ol‘s there is also data-visible-rows.) Then, if you fancy a custom control by which users expand or collapse the container you can pass a CSS selector to data-toggle-element which (if a direct descendant of the container) then acts as the toggle.

 

Custom XHR Without Dirty Hacks

Have you ever wanted/tried to process link clicks or form submissions by yourself? Well, I have and it was a nightmare every single time. Most of Icinga Web 2’s processing is fine. But of course there ever is this single behavior or side-effect which keeps getting in the way. This has now come to an end.

Meet data attribute data-no-icinga-ajax which does exactly what it’s name suggests. Applied to an element it causes Icinga Web 2 to ignore click and submit events triggered by the element itself and all descendants.

Couldn’t be more simple, can it?

 

Hooks For Everyone

Previously hooks were only processed for logged-in users with the permission to access the module providing the hook. This for example prevented the audit module to register logins from users without the permission module/audit and also didn’t allow to log failed logins.

When providing a hook it is now possible to have it run always. ($this->provideHook($name, $implementation = null, $alwaysRun = false);)

Another case of hooks not being processed was the issue that, unlike in the web front-end, enabled modules were not loaded automatically on the CLI. Thus also their hooks were not registered. Now this has changed and also on the CLI all enabled modules are automatically loaded. If you’ve previously loaded the modules explicitly this is not required anymore. If you don’t need any other modules and want to avoid the overhead of loading them, you can disable this of course.

If you still don’t have enough of this, there’s also an entirely new hook available: ConfigFormEventsHook This hook enables you to influence every configuration form of Icinga Web 2. Extending a form’s validation or doing additional work once submission succeeded is now on the table.

 

That’s it. I hope these things are as useful to you as they were to us. And remember, we don’t mind any suggestions to further improve the integration of modules. You are the developer, you’ll know best what’s… best.

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer f√ľr Icinga Web 2, Icinga DB Web sowie alle m√∂glichen anderen Module und Bibliotheken im Web Bereich zust√§ndig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gem√ľtlich. Passt das Wetter, geht's auch mal auf eines seiner Zweir√§der. Motorisiert oder nicht.

Verschachtelte Listen mit Sortable.js

Nachdem ich vor einiger Zeit die Ehre hatte Drag & Drop im Business Process Modul f√ľr Icinga Web 2 zu integrieren, dachte ich mir ich plaudere heute mal ein wenig aus dem N√§hk√§stchen welche Stolpersteine ich dabei √ľberwinden musste.

Aber erst einmal eine kleine Vorstellung der Bibliothek die eingesetzt wurde: Sortable.js 

Die Entscheidung hierf√ľr fiel erst nicht leicht, da das Angebot jener Bibliotheken die sich Drag & Drop verschrieben haben, nicht besonders klein ist. Aber wo wir gerade von klein reden, ist das bereits der wichtigste Punkt der Sortable.js von anderen abhebt. Weil sie nur das n√∂tigste an Funktionalit√§t abdeckt und keine fancy Animationen nutzt die nicht nur Leistung fressen sondern auch Dateigr√∂√üe ist diese Bibliothek mit 25kB (minimized) eines der Leichtgewichte unter ihren Vertretern. Au√üerdem existiert keine Abh√§ngigkeit zu jQueryUI, das alleine hat bereits √ľberzeugt.

Nun aber zum eigentlichen Thema. W√§hrend Sortable.js wunderbar mit einfachen Listen arbeiten kann, wird es etwas anspruchsvoller wenn es um verschachtelte Listen geht. Gl√ľcklicherweise wurde in diesem Segment in den letzen Wochen einiges getan und die Unterst√ľtzung erheblich verbessert. Dennoch gibt es etwas das immer noch existiert und mir einige ruhelose N√§chte bereitet hat. Gut, so schlimm war es nun auch wieder nicht, dennoch, es k√∂nnte ja einigen genauso wie mir nicht sofort wie Schuppen von den Augen fallen.

Aber erst einmal die Demo die das Problem darstellt. Versucht einmal alles aus der roten Liste zu entfernen und dann wieder Elemente aus der blauen hineinzuschieben. Demo

Klappt nicht so ganz? Tja, das liegt daran, dass die rote Liste zu klein ist sobald keine Elemente mehr enthalten sind. Der findige Leser denkt jetzt eventuell daran der Liste eine Mindesthöhe zu geben. Gar nicht so falsch. Demo

Klappt dennoch nicht? Hehe, willkommen im Club. Ein Blick in die README.md von Sortable.js und man findet emptyInsertThreshold. Leider f√ľhrt der Name dieser Option eher in die Irre, denn die L√∂sung ist sie nicht. Euer Blick sollte eher auf invertSwap fallen. Demo

Warum das nun funktioniert? Arr, das geht etwas √ľber das Ziel des Beitrags hinaus. Ich kann euch aber folgendes ans Herz legen.

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer f√ľr Icinga Web 2, Icinga DB Web sowie alle m√∂glichen anderen Module und Bibliotheken im Web Bereich zust√§ndig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gem√ľtlich. Passt das Wetter, geht's auch mal auf eines seiner Zweir√§der. Motorisiert oder nicht.

Was mein ist, bleibt mein

Auch wenn mich das als Egoist darstellt, sollte das jeder in Betracht ziehen. Jedenfalls wenn es um potentiell sensible Daten geht.

Ein Beispiel

Jeder der häufig im Internet unterwegs ist, sei es nun privat oder geschäftlich, kennt das. Eine Anmelde-Maske.

Wer auf Sicherheit wert legt, nutzt hoffentlich √ľberall ein anderes Passwort. Selbstverst√§ndlich sind die auch in einem Passwort-Manager wie Keepass oder Enpass¬†hinterlegt und mit einem sicheren Master-Passwort gesichert.

Aber mal ganz ehrlich, wer klickt im Browser bei der Frage “Soll dieses Passwort gespeichert werden?” nicht gerne auf Ja? Nun, ich hab es eine Zeit lang vermieden war aber jedes Mal traurig nicht auf Ja geklickt zu haben.

Warum? Weil ich eine √ľble Angewohnheit, wie viele andere wohl auch, habe.

Bequemlichkeit

Ich nutze ein Passwort niemals ein zweites Mal. Ich habe alle meine Passwörter in Keepass gespeichert. Diese Datenbank ist mit einem relativ komplexen jedoch noch leicht zu merkendem Master-Passwort versehen und mit meinem Yubikey gekoppelt. 2-Faktor Authentifizierung wie aus dem Buche.

Aber ich bin bequemlich. Jedes Mal Keepass aufzumachen und diese Prozedur durchzuf√ľhren, f√ľr jede Anmelde-Maske die ich im Laufe des Tages benutzen muss? Ein Graus. Selbstverst√§ndlich kann ich Keepass im Hintergrund offen lassen, aber das w√ľrde dem Gedanken der 2-Faktor Authenfizierung widersprechen. Sicher, Keepass sch√ľtzt die Passw√∂rter vor unerlaubten Speicherzugriffen und derlei Sp√§√üen, jedoch hab ich dennoch kein gutes Gef√ľhl dabei.

Aber warum speichere ich dann nicht einfach die Passwörter mit Hilfe des Browsers? Werden die dort nicht auch sicher gespeichert? Na, selbstverständlich werden sie das. Doch das Problem ist ein ganz anderes.

Das schwache Glied

Die Frage ist nämlich nicht wie die Passwörter vom Browser gespeichert werden, sondern wie erneut auf sie zugegriffen wird.

Ich setze Ubuntu 18 ein. Hier werden derart gespeicherte Passw√∂rter im GnuPG-Schl√ľsselbund hinterlegt. Dieser wird bei jedem Login auf dem System entsperrt. (Oder beim entsperren des Systems.)

Nun, der aufmerksame Leser wird sich nun denken k√∂nnen weshalb ich das als schwaches Glied in der Kette ansehe. Ich bin bequemlich, welch √úberraschung. Wenn ich das System entsperren muss, m√∂chte ich nicht erst ein super sicheres Passwort eintippen m√ľssen. Erst recht nicht w√§hrend jemand daneben steht/sitzt. Je komplexer das Passwort n√§mlich, desto langsamer tippe ich es. Je langsamer ich tippe, desto eher steigt die Gefahr mir schaut jemand dabei zu. (Kollegen vertraue ich selbstverst√§ndlich, aber man wei√ü ja nie wo man sonst ist.) Deshalb: Es ist ein einfaches Passwort das super schnell getippt ist.

Falls aber doch jemand, oder etwas, Kenntnis von diesem Passwort erlangt war alles f√ľr die Katz. Sofort sind alle Passw√∂rter aus dem¬†GnuPG-Schl√ľsselbund gef√§hrdet. Da hilft nur eines.

Die L√ľcke schlie√üen

Zum Gl√ľck hat meine Bequemlichkeit doch ihre Grenzen. Denn ich fahre mein DELL XPS 13 grunds√§tzlich immer herunter wenn ich es l√§nger aus den Augen lasse.

Somit ist diese L√ľcke auf einen Schlag verschlossen sobald der gesamte Festplatten-Inhalt verschl√ľsselt ist. Und das ist er inzwischen. LUKS sei dank.

Auch hier kommt es auf die Qualit√§t der gew√§hlten Passw√∂rter an, schlie√ülich muss vor dem Start des Systems erst einmal alles entschl√ľsselt werden k√∂nnen. Aber Achtung: Ein zu schwaches Passwort ist erneut das schwache Glied.

Hier habe ich einen Kompromiss mit meiner Bequemlichkeit geschlossen. Ich habe zwei M√∂glichkeiten meine Festplatte zu entschl√ľsseln. Zum einen ein super sicheres Passwort (das nicht im W√∂rterbuch zu finden ist), zum anderen aber auch ein leicht zu merkendes. (Das aber auch nicht im W√∂rterbuch zu finden ist, jedenfalls nicht 1:1) Der Clou jedoch ist, das zweite (einfache) Passwort ist mit dem Yubikey gekoppelt.

Ein Hoch auf die 2-Faktor Authentifizerung

Man merkt es vielleicht. Ich bin ein Fan meines Yubikeys. Besser gesagt, meiner zwei Yubikeys. (Es k√∂nnte ja einer abhanden kommen) Auf die technischen Details gehe ich jetzt nicht mehr ein, das macht zum Teil bereits¬†Marius. Doch kurz auflisten wof√ľr ich ihn noch einsetze m√∂chte ich:

  • GPG (Private subkeys auf dem Yubikey)
  • SSH (Dank GPG, Gunnar hatte hierzu bereits etwas geschrieben)
  • Github (FIDO U2F)

Außerdem ist mein zweiter (privater) Yubikey NFC fähig, ich kann ihn also super easy mit der Keepass App auf dem Smartphone nutzen.

Was mein ist, bleibt mein!

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer f√ľr Icinga Web 2, Icinga DB Web sowie alle m√∂glichen anderen Module und Bibliotheken im Web Bereich zust√§ndig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gem√ľtlich. Passt das Wetter, geht's auch mal auf eines seiner Zweir√§der. Motorisiert oder nicht.

DELL XPS13 РEin anständiges Fliegengewicht mit kleinerer Klappe

Dies ist die Fortsetzung zum zickigen Leichtgewicht mit großer Klappe.
Nun war es mal wieder soweit. Nach Jahren als einiger der wenigen die bei Meetings kein Macbook vor sich stehen hatten, habe ich nun erneut ein DELL XPS13 erhalten. Diesmal ist es das Modell 9370 (vormals 9343) in der FHD Ausf√ľhrung. Oh ja, was vorher ein QHD+ war ist nun kleiner. Aber daf√ľr viel angenehmer. Seit Ubuntu 14.04 hat sich zwar einiges getan hinsichtlich HiDPI Unterst√ľtzung, jedoch scheitert es immer noch meist an einzelnen Applikationen. Aus diesem Grund habe ich seit langem schon nicht die native Aufl√∂sung von 3200×1600 Bildpunkten betrieben, sondern wie auch mein zweiter Bildschirm mit 1920×1080 Bildpunkten. Allerdings war eine gewisse Unsch√§rfe nicht zu verhindern.
Nunja, das neue Modell hat nun FHD als native Auflösung und jegliche Probleme mit Unschärfe, zu kleiner Schrift oder schrägen Skalierungs-Artefakten sind nun Geschichte. Geschichte ist außerdem der Touchscreen, aber den hab ich eh nie gebraucht. Was hingegen vollkommen neu ist:

  • Es wirkt leichter. Ich habs nicht nachgewogen, aber es wirkt eindeutig leichter.
  • 3 (!) USB-C Ports (Das waren vorher 2 USB-A Ports)
  • 4 statt 2 CPU-Kerne. Power satt. (Aber auch Hitze, dazu sp√§ter mehr)
  • Ganze 16 GB RAM. (Vorher mit 8GB kam ich schon hin und wieder an meine Grenzen)
  • Mit 512 GB SSD doppelter Speicherplatz als vorher. (Jetzt werd ich wohl weniger oft VMs l√∂schen)
  • Eine Infrarot Kamera. (Ist wohl ein √úberbleibsel aus der Windows Variante, k√∂nnte noch n√ľtzlich werden)
  • Oh, und das Tastatur Layout. Ich nutze gerne Home, End, PageUp und PageDown. Jetzt muss ich daf√ľr keine akrobatischen Kunstst√ľcke mit dem Function-key mehr vollziehen!

Wieder einmal war auch Ubuntu vorinstalliert. Da ich allerdings diesmal FDE (Full Disk Encryption) einsetzen wollte musste das runter. Zuerst hatte ich versucht mit Dell Recovery neu zu installieren. Schlie√ülich hat Dell einen eigenen Kernel mit Plattform spezifischen Verbesserungen entwickelt. Dummerweise jedoch ist scheinbar genau jener Kernel (oder irgendwas anderes in diesem Paket) inkompatibel mit LUKS (Quelle), denn egal welches Passwort ich gew√§hlt hatte (zuletzt “test”), nach abgeschlossener Installation wurde keines von LUKS als richtig erkannt.
Gut, also hie√ü es nun das normale Ubuntu 18.04 mit dem generic Kernel zu installieren. Und siehe da, es lief perfekt. Und so l√§uft es auch jetzt noch. Kaum zu glauben, ist aber wahr. Okay, vielleicht nicht perfekt, aber immerhin gut genug f√ľr mich. Bisher sind mir keine Fehler aufgefallen. All die Probleme die ich initial mit dem vorherigen Modell (9343) hatte, traten nicht auf. Kein Tastatur-Lag. Kein Touchpad-Ghosting. Sound ging sofort. Nichts. Nicht einmal mit dmesg sind grobe Fehler oder Warnungen zu entdecken. Ja sogar der Philips Monitor mit USB-C Dock-Funktionalit√§t wird mitsamt der an ihn angeschlossenen Peripherie anstandslos erkannt.
Der einzige Wermutstropfen, wie eingangs schon erw√§hnt, ist die Hitze-Entwicklung. Ich habe noch nicht nachgesehen ob ich im UEFI die L√ľfter konfigurieren kann, aber im Werkszustand drehen die leider bereits bei knapp √ľber 55¬į lautstark auf. Wie laut kann ich nicht messen, aber es √ľbert√∂nt die sonst √ľblichen Ger√§usche im B√ľro. (Tastatur Klackern, knarrende St√ľhle, etc) Und hab ich mal PhpStorm und eine Centos-7 VM mit Icinga 2 und Icinga Web 2 laufen, werden die 55¬į schon recht oft √ľberschritten. Dann blasen die L√ľfter erst einmal f√ľr einige Minuten, bis ~43¬į erreicht sind.
Zu guter letzt habe ich heute mal nachgesehen was ich mit dieser omin√∂sen Infrarot Kamera machen kann. Dabei erfahre ich, h√§tte ich Windows k√∂nnte ich diese mit Windows Hello koppeln. Hm, hab ich aber nicht, ich habe Ubuntu. Gut, gibt es Windows Hello Alternativen f√ľr Linux? Ja! Howdy! Auch ich musste schmunzeln bei diesem Namen. Erste Versuche f√ľhrten auch recht schnell zum Erfolg. Jetzt kann ich einfach in die Kamera grinsen wenn ich im Login-Screen oder Lock-Screen bin. Oder mit sudo Kommandos ausf√ľhre. Oder im Ubuntu Software-Center etwas installiere. Kurz, dank PAM geht das einfach √ľberall.

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer f√ľr Icinga Web 2, Icinga DB Web sowie alle m√∂glichen anderen Module und Bibliotheken im Web Bereich zust√§ndig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gem√ľtlich. Passt das Wetter, geht's auch mal auf eines seiner Zweir√§der. Motorisiert oder nicht.