pixel
Select Page

NETWAYS Blog

Ein Ausblick – Traefik Provider OpenStack

Bei unseren Online-Trainings von NETWAYS, werden jedem einzelnen Teilnehmer eine oder mehrere explizite VMs zur Verfügung gestellt, welche in der OpenStack-Umgebung von NWS gehostet werden. Hierbei benötigen die VMs jeweils öffentliche IPs, welche leider nicht unendlich zu Verfügung stehen. Aus dieser Prämisse heraus entstand die Idee, einen sog. reverse Proxy zu benutzen, welcher Anfragen von Clients an Webserver weiterleitet – in diesem Fall an die entsprechende VM.

Das Problem an dieser Stelle ist, dass es für Traefik noch keinen Provider für OpenStack gibt. Daher wurde ein experimenteller openstack-provider entwickelt, welchen ich hier kurz vorstelle.

 

Funktionsweise des openstack-providers

Der openstack-traefik-provider benutzt den http-provider von Traefik um die VMs/Instanzen innerhalb eines OpenStack-Projektes zu suchen/finden. Damit Traefik die Informationen über die Instanzen erhält, wird ein OpenStack-Go-Client innerhalb eines Docker-Containers gestartet, welcher anschließend über die URL http://openstack:8080/traefik (diese URL kann im Code selbst angepasst werden), alle Informationen des Projektes über ein HTTP-Response zur Verfügung stellt. Traefik selbst pollt alle 10 sek. von dieser Adresse, um die Routen, Middlewares, etc. zu aktualisieren.

 

Beispielhafte Konfiguration

Jeglicher HTTPS-Traffic soll am Proxy terminiert werden, sodass innerhalb des internen Netzwerks HTTP benutzt wird:

main.go
[...]
settings := discovery.DefaultSettings
	settings.DefaultRule = "Host(`{{ .Name }}.trainig.netways.de`)"
	settings.DefaultEnable = true
	settings.AddressType = "floating"
	settings.DefaultLabels = map[string]string{
		// HTTP
		"traefik.http.routers.{{ .Name }}-http.service":                   "{{ .Name }}-http",
		"traefik.http.routers.{{ .Name }}-http.rule":                      "Host(`{{ .Name }}.trainig.netways.de`)",
		"traefik.http.routers.{{ .Name }}-http.entrypoints":               "http",
		"traefik.http.services.{{ .Name }}-http.loadBalancer.server.port": "80",
		"traefik.http.routers.{{ .Name }}-http.middlewares":               "http-to-https",

		// HTTPS
		"traefik.http.routers.{{ .Name }}-https.service":                   "{{ .Name }}-https",
		"traefik.http.routers.{{ .Name }}-https.rule":                      "Host(`{{ .Name }}.trainig.netways.de`)",
		"traefik.http.routers.{{ .Name }}-https.entrypoints":               "https",
		"traefik.http.services.{{ .Name }}-https.loadBalancer.server.port": "80",
		"traefik.http.routers.{{ .Name }}-https.tls":                       "true",

		// Redirect HTTP to HTTPS
		"traefik.http.middlewares.http-to-https.redirectscheme.scheme":    "https",
		"traefik.http.middlewares.http-to-https.redirectscheme.permanent": "true",
	}
[...]

 

Anschließend muss über den Befehl docker-compose up der Docker-Container mit der neuen Konfiguration gestartet werden und die Routen + Middleware sollten im Frontend zu sehen sein:

 

Da das Projekt sich derzeit noch in Entwicklung befindet, würde ich mich über Vorschläge, PullRequests oder Kritik von der Open Source Community sehr freuen!

Sobald das Projekt abgeschlossen ist, werde ich auch davon berichten!

 

Philipp Dorschner
Philipp Dorschner
Technical Service Manager

Nach seiner Ausbildung zum Fachinformatiker bei der NETWAYS Professional Services GmbH wuchs das Interesse an Development und organisatorischen Themen. Heute unterstützt Philipp die Kollegen aus PS-Services als hybrider Mitarbeiter. Fünfzig Prozent seiner Zeit als Technical Service Manager und die anderen fünfzig Prozent als interner Entwickler. Als Ausgleich zu seiner Arbeit im Büro verbringt er seine Freizeit meistens beim Sport oder trifft sich mit Freunden.

Python – Generator

Wer schon einmal eine etwas größere Datei, bspw. 1Gb, mit dem Editor VIM geöffnet hat, der weis, wie lange dies dauern kann. Das kommt daher, dass diese Datei zunächst komplett in den Arbeitsspeicher geladen werden muss. So ähnlich verhält es sich, wenn in Python eine Variable “befüllt” wird, welche anschließend Speicherplatz im Arbeitsspeicher belegt. Bei der heutigen Hardware stellen 1 Gb große Variablen kein Problem dar, aber was passiert wenn diese deutlich größer sind und zusätzlich eine gute Performance benötigt wird? In diesem Fall empfiehlt es sich, auf einen Generator zurückzugreifen. Ein Generator liefert lapidar gesagt die Ergebnisse einer Funktion “häppchenweise” und nicht als riesiges Stück zurück. Syntaktisch unterscheidet sich ein Generator von einer “normalen” Funktion nicht großartig, wie hier zu sehen:

def a_function():
  yield x
  yield y
  yield z

Bei der Funktionsdefinition wird lediglich das Schlüsselwort return durch yield ersetzt. Der große Unterschied zu einer “normalen” Funktion dabei ist, dass sich die Funktion mit yield nicht sofort beendet. Somit erhält man, vereinfacht gesagt, ein Objekt (Iterator) als Rückgabewert, über das man iterieren kann.

Zur besseren Veranschaulichung eines Generators werde ich eine exemplarische Datenbankabfrage aufzeigen:

Hinweis: Ich habe eine Beispieldatenbank genommen.

“normale” Funktion
SELECT-Statement und anschließende Rückgabe des gesamten Ergebnisses:

def select_list():
    result = []

    db = mysql.connector.connect(host='127.0.0.1',
                                 user='thomas',
                                 password="4nD3r5on",
                                 db='employees')

    cursor = db.cursor()

    cursor.execute('select * from employees')

    for row in cursor:
        result.append(row)

    return result

Die Funktion “select_list()” liefert eine Liste zurück:

print(select_list())

[...]
(10016,'1961-05-02','Kazuhito','Cappelletti','M','1995-01-27'),
(10017,'1958-07-06','Cristinel','Bouloucos','F','1993-08-03'),
(10018,'1954-06-19','Kazuhide','Peha','F','1987-04-03'),
[...]

“generator” Funktion
Im Gegensatz zum obigen Beispiel wird keine Liste mit den Werten befüllt, sondern jeder Wert einzeln durch yield zurückgegeben:

def select_generator():
    db = mysql.connector.connect(host='127.0.0.1',
                                 user='thomas',
                                 password="4nD3r5on",
                                 db='employees')

    cursor = db.cursor()

    cursor.execute('select * from employees')

    for row in cursor.fetchmany(1000):
        yield row

Die Funktion “select_generator()” liefert keine Liste zurück, sondern einen Generator:

print(select_generator())

<generator object select_generator at 0x10ba165d0>

Durch anschließendes Iterieren des Generatorsobjektes werden die gewünschten Werte ausgegeben:

for row in select_generator():
        print(row)

[...]
(10016,'1961-05-02','Kazuhito','Cappelletti','M','1995-01-27'),
(10017,'1958-07-06','Cristinel','Bouloucos','F','1993-08-03'),
(10018,'1954-06-19','Kazuhide','Peha','F','1987-04-03'),
[...]

Wo liegt nun der Vorteil eines Generators? Das zeigt sich erst bei Benchmark-Tests, welche im folgenden Beispiel die Performance der jeweiligen Funktion aufzeigt. Die SELECT-Statements sind bei beiden Beispielen dieselbigen, werden aber nicht ausgegeben, es wird nur eine Variable initialisiert:
select_list()

print('Memory (Before): {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))
    
t1_start = perf_counter()
test_list = select_list()
t1_stop = perf_counter()
    
print('Memory (After) : {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

print("Elapsed time: {0} seconds".format(t1_stop-t1_start))

select_generator()

print('Memory (Before): {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

t1_start = perf_counter()
test_generator = select_generator()
t1_stop = perf_counter()

print('Memory (After) : {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

print("Elapsed time: {0} seconds".format(t1_stop-t1_start))

select_list():

Memory (Before): 11.776 Mb
Memory (After) : 124.960768 Mb
Elapsed time: 8.311030800000001 seconds

 

select_generator():

Memory (Before): 11.698176 Mb
Memory (After) : 11.718656 Mb
Elapsed time: 4.869999999934649e-07 seconds

Die Zahlen sprechen für sich. Beim Generator bleibt der Verbrauch des Arbeitsspeichers so gut wie unverändert, da dieser beim Ausführen des Codes nicht “alle Werte speichert”, sondern jeden einzelnen Wert ab dem Schlüsselwort yield zurückgibt. Dadurch ergibt sich auch die immense Geschwindigkeit.
An dieser Stelle ist noch zu erwähnen, dass alle Vorteile eines Generators verloren gehen, wenn man diesen in ein Liste umwandelt.

Quelle: https://media.giphy.com/media/yUrUb9fYz6x7a/giphy.gif

Philipp Dorschner
Philipp Dorschner
Technical Service Manager

Nach seiner Ausbildung zum Fachinformatiker bei der NETWAYS Professional Services GmbH wuchs das Interesse an Development und organisatorischen Themen. Heute unterstützt Philipp die Kollegen aus PS-Services als hybrider Mitarbeiter. Fünfzig Prozent seiner Zeit als Technical Service Manager und die anderen fünfzig Prozent als interner Entwickler. Als Ausgleich zu seiner Arbeit im Büro verbringt er seine Freizeit meistens beim Sport oder trifft sich mit Freunden.

Go – Automatische Package Updates

Durch die Einführung des GO-Module, welches ein eigenes Dependency Management für das GO-Ökosystem ist, erleichtert es dem Programmierer, die importieren Go-Packages im Projekt aktuell zu halten. Dabei muss der Programmierer diese Updates immer manuell auslösen, was über folgende Befehle gemacht werden kann:

go get -u ./...
go mod tidy

Dabei wird der aktuelle Stand aller importierten Packages heruntergeladen. Das Problem hierbei ist – wie oben erwähnt – dass der Programmierer dies manuell auslösen muss. Bei einem Projekt ist das kein Problem – sind es aber mehrere Projekte, können womöglich manche Updates vergessen werden oder es ist dem Programmierer nicht bewusst, dass Updates anstehen. Aus diesem Grund bietet sich eine Automatisierung an, genauer gesagt handelt es sich hierbei um den Dependabot.

Was kann der Dependabot?

Der Dependabot selbst ist auch ein Dependency Management Tool, welches in GitHub integriert werden kann. Dependabot sucht automatisch nach Manifestdateien und prüft dort die eingetragenen Versionen der Package-Abhängigkeiten. Wurde ein Update gefunden, erstellt Dependabot automatisch einen Pull Request, um die Packages zu aktualisieren.

Um eine Integration in GitHub zu erhalten, muss lediglich eine dependabot.yml im Ordner .github des Repositories angelegt werden:

.github/dependabot.yml

In dieser Datei kann nun das Verhalten des Bots konfiguriert werden:

version: 2
  updates:
  - package-ecosystem: gomod
    directory: "/"
  schedule:
    interval: weekly
    time: '10:00'
  open-pull-requests-limit: 10
  • package-ecosystem: In diesem Fall gomod. Kann je nach Programmiersprache angepasst werden
  • directory: Der Ort der Manifestdatei. In diesem Fall die go.mod
  • schedule.interval: Der Zyklus, wie oft auf Updates geprüft werden soll
  • open-pull-requests-limit: Limitiert die Anzahl der Pull Requests, die Dependabot für das jeweilige Repository erstellt

Die komplette Liste an Konfigurationseinstellungen kann auf der Projektseite von Dependabot gefunden werden.

Anschließend überprüft Dependabot alle Abhängigkeiten und erstellt selbstständig Pull Request mit den aktuellen Packages:

Der Pull Request kann im Anschluss gemerged werden und der aktuelle Stand der Packages ist gesichert.

Du möchtest mehr dazu wissen?

Wenn ich mit diesem Blog das Interesse an Versionsverwaltungssoftware geweckt habe, kann ich nur die NETWAYS-Trainings empfehlen. Dort werden die Grundlagen einer Versionskontrolle für Softwareprojekte auf Basis von Git beigebracht.

 

Bildquelle: Dependabot-Core
Philipp Dorschner
Philipp Dorschner
Technical Service Manager

Nach seiner Ausbildung zum Fachinformatiker bei der NETWAYS Professional Services GmbH wuchs das Interesse an Development und organisatorischen Themen. Heute unterstützt Philipp die Kollegen aus PS-Services als hybrider Mitarbeiter. Fünfzig Prozent seiner Zeit als Technical Service Manager und die anderen fünfzig Prozent als interner Entwickler. Als Ausgleich zu seiner Arbeit im Büro verbringt er seine Freizeit meistens beim Sport oder trifft sich mit Freunden.

Dynamische Formulare in Icinga Web 2

In meinem letzten internen Projekt der NETWAYS durfte ich ein Icinga Web 2 Module bauen, welches ein dynamisches Formular verwenden soll. Konkret sollte es mehrere Dropdown-Menüs geben, welche den Wert des vorherigen Dropdown-Menü evaluiert und anhand dessen, ein neues Dropdown befüllt. Dabei kann man zwei Herangehensweisen benutzen, welche ich im Folgenden erläutere.

Die klassische (schlechte) Herangehensweise

Die erste Möglichkeit, die mir in den Sinn kommt, ist, dass man sich über AJAX verschiedene Actions in Icinga Web 2 aufruft. Diese wiederum besitzen einen Code, der den Wert des zuvor ausgewählten Wertes evaluiert und das weitere Dropdown befüllt. Da Icinga Web 2 einem MVC (Modell, View, Controller) Muster folgt, wird ein Controller- für die Steuerung und ein View-Script für die Darstellung benötigt. Des Weiteren wird für den AJAX Request eine eigene module.js Datei benötigt, in der der JavaScript Code hinterlegt ist. Darüber hinaus müsste man noch CSS einbinden, damit das Formular auch noch schön dargestellt wird.

Alles im Allem, eine sehr sehr aufwendige Herangehensweise!

Die richtige Herangehensweise

Wie oben erwähnt, handelt es sich bei Icinga Web 2 um ein MVC, somit ist es auch möglich, bestimmte “ConfigForm-Views” zu erstellen, die bereits alle benötigten Sachen zur Verfügung stellen: CSS, JavaScript und HTML. Das beste daran ist, dass dies super einfach zu implementieren ist und das Framework die ganze Arbeit von der “schlechten Herangehensweise” übernimmt.
Man kann einer Configform die Klasse autosubmit mitgeben, dadurch erhält man ein Verhalten wie ein AJAX-Request. Im Folgenden ein kleiner Ausschnitt eines solchen ConfigControllers:

<?php
    namespace Icinga\Module\Testmodule\Forms\Test;
    use Icinga\Web\Form;
 
    class TestForm extends Form 
    {
      public function init()
      {
        $this->setName('form_test_form');
        $this->setSubmitLabel('Save Changes');
      }

      public function createElements(array $formData)
      {
          // Load cars
          $this->addElement(
              'select',
              'car',
              array(
                  'label' => $this->translate('Car'),
                  'multiOptions' => [
                      '' => '(select option)',
                      'audi' => 'Audi',
                      'vw' => 'VW'
                  ],
                  // 'autosubmit' acts like an AJAX-Request
                  'class' => 'autosubmit'
              )
          );

          // Select model
          if (isset($formData['car']) && $formData['car'] === 'audi') {
              $this->addElement(
                  'select',
                  'model',
                  array(
                      'label' => $this->translate('Model'),
                      'multiOptions' => [
                          '' => '(select model)',
                          'A1',
                          'A6'
                      ],
                      'required' => true,
                  )
              );
          } elseif (isset($formData['car']) && $formData['car'] === 'vw') {
              $this->addElement(
                  'select',
                  'model',
                  array(
                      'label' => $this->translate('Model'),
                      'multiOptions' => [
                          '' => '(select model)',
                          'Golf',
                          'Polo'
                      ],
                      'required' => true,
                  )
              );
          }
      }  
    }

Wenn man nun noch das View-Script einbindet erhält man eine Form, die wie folgt aussieht:



Durch das Framework von Icinga Web 2 ist es möglich, sehr schnell ein Modul nach eigenen Wünschen zu erstellen. Wem ich damit nun Lust auf ein bisschen Programmierung gemacht habe, dem kann ich die bereits vorhandenen Trainingsunterlagen auf Github empfehlen! Oder Ihr besucht eine unserer verschiedenen Konferenzen, bei der man auch mal detailliertere Einblicke wie z.B. Icinga Web 2 Modul Programmierung erhält.

Philipp Dorschner
Philipp Dorschner
Technical Service Manager

Nach seiner Ausbildung zum Fachinformatiker bei der NETWAYS Professional Services GmbH wuchs das Interesse an Development und organisatorischen Themen. Heute unterstützt Philipp die Kollegen aus PS-Services als hybrider Mitarbeiter. Fünfzig Prozent seiner Zeit als Technical Service Manager und die anderen fünfzig Prozent als interner Entwickler. Als Ausgleich zu seiner Arbeit im Büro verbringt er seine Freizeit meistens beim Sport oder trifft sich mit Freunden.

Request Tracker – Dark Theme und SearchResult

Bei der neuen Version des Request Tracker 5 wurde – meiner Meinung nach – ein tolles neues Feature implementiert und zwar das sog. Dark ThemeDabei wurde die Hintergrundfarbe verdunkelt und die Textschrift aufgehellt, wodurch eine bessere Lesbarkeit in dunklen Umgebungen gewährleistet wird. In diesem Zuge musste die RT-Extension SearchResult auf das neue Dark Theme angepasst werden. SearchResult ist eine RT-Extension die es möglich macht, Tickets nach bestimmten Kriterien farblich hervorzuheben, wie z.B. bei Ablauf des Due Date, um somit möglichst schnell einen Überblick über den aktuellen Status eines oder mehrerer Tickets zu erhalten.

Hinweis: Im folgenden Blogpost gehe ich davon aus, dass die Grundinstallation des Request Tracker bereits vorhanden ist. Falls dies nicht der Fall ist und Probleme oder anderweitige Fragen zum Request Tracker auftauchen, einfach bei NETWAYS nachfragen. Wir helfen gerne 🙂

 

An dieser Stelle ist zu erwähnen, dass die Aktualisierung netterweise von RedBridge gesponsert wurde, vielen Dank hierfür 🙂

 

Um dies zu verdeutlichen, oben das Light Theme und unten das Dark Theme:

Im Folgenden ein kleiner Exkurs wie das Dark Theme sowie SearchResult installiert bzw. aktiviert werden:

 

Dark Theme:

Durch Hinzufügen der Option innerhalb der RT_SiteConfig.pm  und anschließendem Neustart des Webservers, wird das Dark Theme aktiviert:

# cat /opt/rt5/etc/RT_SiteConfig.pm
[...]
Set($WebDefaultStylesheet, "elevator-dark");
[...]

 

Neustart des Webservers (in diesem Fall Apache):

systemctl restart apache2

Kommt keine Fehlermeldung, wurde das Dark Theme erfolgreich aktiviert.

 

SearchResult:

Um die Extension SearchResult Extension zu aktivieren wird diese zunächst aus folgendem Repository geklont und anschließend installiert:

Klonen des Repository

# git clone https://github.com/NETWAYS/rt-extension-searchresult
# cd rt-extension-searchresult/

 

Installation von SearchResult

# perl Makefile.PL
# make
# make install

 

Konfiguration von SearchResult

# cat /opt/rt5/etc/RT_SiteConfig.pm
[...]
Plugin('RT::Extension::SearchResult');
Set($SearchResult_HighlightOnLastUpdatedByCondition,[
{
"conditions" => { "groups" => [ "admins" ] },
"color" => "purple",
"icon" => "fa-exclamation-triangle",
"tooltip" => "Customer replied. Action required."}]);
Set($SearchResult_HighlightOnDueDate,[
{
"conditions" => { "due" => 0 },
"color" => "red",
"icon" => "fa-question-circle",
"tooltip" => "Due date reached. Action required."},
{
"conditions" => { "due" => 3 },
"color" => "yellow",
"icon" => "fa-question-circle",
"tooltip" => "Due date soon. Plan ahead."},]);
[...]

Damit die Änderungen übernommen und aktiviert werden muss wie im obigen Beispiel der Webserver neugestartet werden. War dies erfolgreich, wird die Oberfläche des Request Tracker wie folgt dargestellt:

Philipp Dorschner
Philipp Dorschner
Technical Service Manager

Nach seiner Ausbildung zum Fachinformatiker bei der NETWAYS Professional Services GmbH wuchs das Interesse an Development und organisatorischen Themen. Heute unterstützt Philipp die Kollegen aus PS-Services als hybrider Mitarbeiter. Fünfzig Prozent seiner Zeit als Technical Service Manager und die anderen fünfzig Prozent als interner Entwickler. Als Ausgleich zu seiner Arbeit im Büro verbringt er seine Freizeit meistens beim Sport oder trifft sich mit Freunden.