Select Page

NETWAYS Blog

NETWAYS Support Collector Roadmap

Den Support Collector konnte ich bereits in meinem letzten Blogpost vorstellen. Für alle die den Beitrag verpasst haben, hier kurz umrissen was es ist:
Bei dem Tool handelt es sich um einen von uns geschriebenen Datensammler, welche alle möglichen Support relevanten Daten von einem System sammelt und als ZIP verpackt. Das ZIP kann in Support Fällen an uns geschickt werden, damit wir uns einen Überblick über das System machen können.

Letzte Woche konnte mit Verzögerungen die Version 0.7.0 veröffentlich werden, welche nun auch Daten über die IcingaDB und Redis sammelt. Von Versionen bis hin zur Konfiguration und Service Status wird alles mit gesammelt.

Im Rahmen dieses Blogposts möchte ich euch einen kleinen Ausblick geben, welche möglichen Erweiterungen wir mit dem Support Collector noch abbilden möchten.

Systemweiter Datensammler

Zum aktuellen Stand sammelt der Support Collector Daten ein, speichert sie in eine Datei und verpackt dass alles zu einem großem ZIP. Das ganze passiert aber nur auf dem System auf welchen das Tool ausgeführt wird. Jetzt stehen wir natürlich vor dem “Problem” dass Icinga 2 Umgebungen über mehrere Systeme verteilt sein können. So kann es sein dass einfach nur die Datenbank auf einem anderen Host läuft oder dass sich irgendwo noch ein zweiter Master bzw. Satelliten befindet. Aus Sicht des Supports wäre es natürlich schön auch diese Daten mit abzufragen.
Die Umsetzung des eben beschriebenen Vorhabens ist noch nicht ganz klar, da es hier neben vielen Kleinigkeiten vor allem die Security zu beachten gilt. Da wir uns auch vorstellen können, dass nicht ein jeder es gut findet, wenn wir komplette Systeme scannen, wird diese Funktion auch nur optional. Unser Augenmerk liegt darauf, dass der Benutzer frei entscheiden kann, was er gesammelt haben möchte.

Statistiken

Mit den gesammelten Daten lassen Sich natürlich auch aussagekräftige Statistiken erstellen. Anhand von diesen Daten könnten wir von den einfachsten Statistiken wie “Welche Versionen werden wie oft genutzt”, bis hin zu komplexen Themen wie “Durchschnittliche Größe eines Systems” oder “Welche Hardware Specs für welche Icinga 2 Größe” erstellen. Allerdings ist auch hier noch nicht zu eindeutig wie die Umsetzung aussehen soll, da hier ebenfalls die Security und Anonymität eine große und wichtige Rolle spielen.

Mit den zwei Punkten welche ich hier angesprochen habe, konnte ich euch nur einen kleinen Einblick gegeben, was an Feature Ideen noch in Planung sind. Sollte euch etwas einfallen, was aus eurer Sicht sinnvoll wäre umzusetzen, könnt ihr gerne ein Feature Request im Git Repository eröffnen.

Tobias Bauriedel
Tobias Bauriedel
Assistant Manager Operations

Tobias ist ein offener und gelassener Mensch, dem vor allem der Spaß an der Arbeit wichtig ist. Bei uns hat er seine Ausbildung zum Fachinformatiker für Systemintegration abgeschlossen und arbeitet nun im NETWAYS Professional Services - Team Operations und entwickelt nebenbei Projekte für die NPS. In seiner Freizeit engagiert er sich ehrenamtlich aktiv bei der Freiwilligen Feuerwehr als Atemschutzgerätetrager und Maschinist, bereist die Welt und unternimmt gerne etwas mit Freunden.

Der NETWAYS Support Collector

Dem ein oder anderen unserer Support Kunden ist unser neuer Support Collector vielleicht schon über den Weg gelaufen. Aber was ist das überhaupt? Und was bringt er?

Der NETWAYS Support Collector ist eines unserer neuesten Kreationen. Inspiriert von, dem mehr verbreiteten, icinga2-diagnostics ist die Aufgabe des Support Collectors, Daten über laufende Systeme und deren Komponenten zu sammeln.
So ist es möglich mittels eines einzelnen Aufrufes alle essentiellen Daten über das System zu sammeln.

Anhand dieser Daten können beispielsweise Support Abläufe effizienter gemacht werden oder sogar aussagekräftige Statistiken erstellt werden.

Der Support Collector kann neben den vorstellbar gängigen Daten wie Icinga 2 und Icinga Web 2 weit aus mehr.
Der aktuelle Rahmen, welcher durch das Tool abgedeckt wird, ist folgender:

  • Allgemeine System Informationen
  • Icinga 2
  • Icinga Web 2
  • Icinga Director
  • Mysql / MariaDB
  • PostgreSQL
  • Ansible
  • Puppet
  • InfluxDB
  • Grafana
  • Graphite

Für den User ist es selber wählbar, welche “Module” durch den Support Collector alle gesammelt werden sollen. Standartmäßig werden alle “Module” gesammelt, welche auf dem System gefunden werden.

Um den Sicherheitsaspekt zu beachten, werden alle Passwörter / IP Adressen / Token innerhalb der gesammelten Daten entfernt, bevor diese zu einen ZIP verpackt werden.
Die generierte ZIP Datei kann dann durch Support Kunden an unseren Support weitergeleitet werden, sobald ein Support Fall eintrifft.

Wer selber einen Blick auf den Support Collector werfen möchte, kann dies in dem GitHub Repository machen oder sich das Tool mit den durch uns bereit gestellten Paketen auf packages.netways.de/extra installieren.
Die –help Übersicht liefert einige Konfigurations Möglichkeiten, welche optional mitgegeben werden können.

Tobias Bauriedel
Tobias Bauriedel
Assistant Manager Operations

Tobias ist ein offener und gelassener Mensch, dem vor allem der Spaß an der Arbeit wichtig ist. Bei uns hat er seine Ausbildung zum Fachinformatiker für Systemintegration abgeschlossen und arbeitet nun im NETWAYS Professional Services - Team Operations und entwickelt nebenbei Projekte für die NPS. In seiner Freizeit engagiert er sich ehrenamtlich aktiv bei der Freiwilligen Feuerwehr als Atemschutzgerätetrager und Maschinist, bereist die Welt und unternimmt gerne etwas mit Freunden.

Icinga Plugins in Golang

Golang ist an sich noch eine relativ junge Programmiersprache, ist jedoch bei vielen Entwicklern und Firmen gut angekommen und ist die Basis von vielen modernen Software Tools, von Docker bis Kubernetes.

Für die Entwicklung von Icinga Plugins bringt die Sprache einige hilfreiche Konzepte mit. Golang baut fertige Binaries, Plugins können also zentral kompiliert und ohne große Abhängigkeiten verteilt werden. Alle Abhängigkeiten werden im Rahmen vom Bauprozess abgedeckt, die einzige Unterscheidung liegt in der Ziel Architektur, also Linux, Windows, Mac oder ähnliches, sowie ob 32 oder 64 bit.

Viele Funktionen und Packages (vergleichbar mit Libraries) kommen entweder direkt mit Golang mit oder können leicht aus der Open Source Community verwendet werden. Mit dem Package go-check von uns haben wir eine kleine Basis geschaffen, um leichter Plugins schreiben zu können, ohne sich zu sehr im Code wiederholen zu müssen.

Als ganz einfaches Go Plugin hier ein Beispiel eine “main.go” Datei:

package main

import (
	"github.com/NETWAYS/go-check"
)

func main() {
	config := check.NewConfig()
	config.Name = "check_test"
	config.Readme = `Test Plugin`
	config.Version = "1.0.0"

	_ = config.FlagSet.StringP("hostname", "H", "localhost", "Hostname to check")

	config.ParseArguments()

	// Some checking should be done here, when --help is not passed

	check.Exitf(check.OK, "Everything is fine - answer=%d", 42)
}

Alles was man noch tun muss, ist das Plugin zu kompilieren oder direkt auszuführen:

go build -o check_test main.go && ./check_test --help
go run main.go

Ein guter Einstieg in Go findet man über die Dokumentation, die Tour und vor allem in dem man sich umschaut, was die Community an Packages zu bieten hat.

Natürlich bleibt die Frage, wie überwache ich das Ding was mir wichtig ist, wofür es aber noch kein Plugin gibt. Gerade dort helfen wir von NETWAYS mit unseren Consulting und Entwicklungsleistungen.  Beispiele unserer Go Plugins findet man auf GitHub unter der NETWAYS Organisation.

 

C++Go. Halb C++ – halb Go.

Scherz beiseite, die Go-Entwickler bieten noch keine Möglichkeit, C++-Bibliotheken ohne weiteres anzusprechen. Aber es geht ja auch mit weiteres. Das weitere besteht darin, dass C++-Funktionen mittels C-Bibliotheken gewrapped werden können. Und in meinem letzten Blogpost zu diesem Thema habe ich bereits erklärt, wie C-Bibliotheken in Go angesprochen werden können. Sprich, es braucht nur eine hauchdünne C-Schicht zwischen C++ und Go.

Multilingual++ in der Praxis

Wie auch letztes mal habe ich schon mal was vorbereitet – eine Schnittstelle für die Boost.Regex-Bibliothek. Diese findet sich in diesem GitHub-Repo und besteht aus folgenden Komponenten:

  • Ein Struct, das boost::basic_regex<char> wrapped
  • Eine C-Bibliothek, die den C++-Teil wrapped
  • Die Go-Bibliothek, die die C-Bibliothek verwendet

Wrapper-Struct

Dieses Struct ist Notwendig, da die Größe von boost::basic_regex<char> zwar C++ bekannt ist, aber nicht Go. Das Wrapper-Struct hingegen hat eine feste Größe (ein Zeiger).

libcxx/regex.hpp

[c language=”++”]
#pragma once

#include <boost/regex.hpp>
// boost::basic_regex
// boost::match_results
// boost::regex_search

#include <utility>
// std::forward
// std::move

template<class Char>
struct Regex
{
template<class… Args>
inline
Regex(Args&&… args) : Rgx(new boost::basic_regex<Char>(std::forward<Args>(args)…))
{
}

Regex(const Regex& origin) : Rgx(new boost::basic_regex<Char>(*origin.Rgx))
{
}

Regex& operator=(const Regex& origin)
{
Regex copy (origin);
return *this = std::move(copy);
}

inline
Regex(Regex&& origin) noexcept : Rgx(origin.Rgx)
{
origin.Rgx = nullptr;
}

inline
Regex& operator=(Regex&& origin) noexcept
{
this->~Regex();
new(this) Regex(std::move(origin));
return *this;
}

inline
~Regex()
{
delete this->Rgx;
}

template<class Iterator>
bool MatchesSomewhere(Iterator first, Iterator last) const
{
boost::match_results<Iterator> m;
return boost::regex_search(first, last, m, *(const boost::basic_regex<Char>*)this->Rgx);
}

boost::basic_regex<Char>* Rgx;
};
[/c]

Eine C-Bibliothek, die den C++-Teil wrapped

Die folgenden Funktionen sind zwar waschechte C++-Funktionen, aber dank dem extern "C" werden sind sie in der Bibliothek als C-Funktionen sichtbar und können von Go angesprochen werden.

libcxx/regex.cpp

[c language=”++”]
#include "regex.hpp"
// Regex

#include <boost/regex.hpp>
using boost::bad_expression;

#include <stdint.h>
// uint64_t

#include <utility>
using std::move;

extern "C" unsigned char CompileRegex(uint64_t pattern_start, uint64_t pattern_end, uint64_t out)
{
try {
*(Regex<char>*)out = Regex<char>((const char*)pattern_start, (const char*)pattern_end);
} catch (const boost::bad_expression&) {
return 2;
} catch (…) {
return 1;
}

return 0;
}

extern "C" void FreeRegex(uint64_t rgx)
{
try {
Regex<char> r (move(*(Regex<char>*)rgx));
} catch (…) {
}
}

extern "C" signed char MatchesSomewhere(uint64_t rgx, uint64_t subject_start, uint64_t subject_end)
{
try {
return ((const Regex<char>*)rgx)->MatchesSomewhere((const char*)subject_start, (const char*)subject_end);
} catch (…) {
return -1;
}
}
[/c]

libcxx/regex.h

[c language=”++”]
#pragma once

#include <stdint.h>
// uint64_t

unsigned char CompileRegex(uint64_t pattern_start, uint64_t pattern_end, uint64_t out);

void FreeRegex(uint64_t rgx);

signed char MatchesSomewhere(uint64_t rgx, uint64_t subject_start, uint64_t subject_end);
[/c]

Go-Bibliothek

Diese spricht letztendlich die C-Funktionen an. Dabei übergibt sie die Zeiger als Ganzzahlen, um gewisse Sicherheitsmaßnahmen von CGo zu umgehen. Das Regex-Struct entspricht dem Regex-Struct aus dem C++-Teil.

regex.go

[c language=”go”]
package boostregex2go

import (
"io"
"reflect"
"runtime"
"unsafe"
)

/*
#include "libcxx/regex.h"
// CompileRegex
// FreeRegex
// MatchesSomewhere
*/
import "C"

type OOM struct {
}

var _ error = OOM{}

func (OOM) Error() string {
return "out of memory"
}

type BadPattern struct {
}

var _ error = BadPattern{}

func (BadPattern) Error() string {
return "bad pattern"
}

type Regex struct {
rgx unsafe.Pointer
}

var _ io.Closer = (*Regex)(nil)

func (r *Regex) Close() error {
C.FreeRegex(rgxPtr64(r))
return nil
}

func (r *Regex) MatchesSomewhere(subject []byte) (bool, error) {
defer runtime.KeepAlive(subject)
start, end := bytesToCharRange(subject)

switch C.MatchesSomewhere(rgxPtr64(r), start, end) {
case 0:
return false, nil
case 1:
return true, nil
default:
return false, OOM{}
}
}

func NewRegex(pattern []byte) (*Regex, error) {
rgx := &Regex{}

defer runtime.KeepAlive(pattern)
start, end := bytesToCharRange(pattern)

switch C.CompileRegex(start, end, rgxPtr64(rgx)) {
case 0:
return rgx, nil
case 2:
return nil, BadPattern{}
default:
return nil, OOM{}
}
}

func bytesToCharRange(b []byte) (C.uint64_t, C.uint64_t) {
sh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
return C.uint64_t(sh.Data), C.uint64_t(sh.Data + uintptr(sh.Len))
}

func rgxPtr64(p *Regex) C.uint64_t {
return C.uint64_t(uintptr(unsafe.Pointer(p)))
}
[/c]

Fazit++

Wenn etwas abgedrehtes mal nicht zu gehen scheint, dann gebe ich doch nicht auf, sondern ich mache es einfach noch abgedrehter. Impossible is nothing.

Wenn Du auch lernen willst, wie man unmögliches möglich macht, komm auf unsere Seite der Macht.

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

Generics waren gestern. Lang lebe Golangs Reflection!

Vor einiger Zeit habe ich die Programmiersprache Golang und all ihren Nutzen für die Entwickler vorgestellt. Zugegeben, eine in der Konkurrenz sehr verbreitete Funktionalität besitzt Go nicht: Generics. Und diese Funktionalität is noch dazu sehr gefragt. Allein das Gesamtbild der Reaktionen (Smileys) auf den Vorschlag, Generics in Go v2 zu integrieren, sagt mehr als 1000 Worte. Aber es geht auch anders…

Problem

Aktuell arbeite ich an einem (streng geheimen) Programm, das u.a. mit SQL-Datenbanken kommunizieren soll. Die grundsätzliche Infrastruktur dafür bringt Go von Haus aus mit. Jedoch kann es etwas mühselig sein, bei jeder Abfrage dieselbe Routine (samt Fehlerbehandlung) durchzukauen:

[code]
package blog

import (
"database/sql"
)

type Employee struct {
GivenName, FamilyName string
}

func GetEmployees(db *sql.DB) ([]Employee, error) {
rows, errQuery := db.Query("SELECT given_name, family_name FROM employee")
if errQuery != nil {
return nil, errQuery
}

defer rows.Close()

employees := []Employee{}

for {
if rows.Next() {
row := Employee{}

if errScan := rows.Scan(&row.GivenName, &row.FamilyName); errScan != nil {
return nil, errScan
}

employees = append(employees, row)
} else if errNext := rows.Err(); errNext == nil {
break
} else {
return nil, errNext
}
}

return employees, nil
}
[/code]

Außerdem hat sich in einem vergangenen Projekt herausgestellt, dass (zumindest in Transaktionen) erst nach rows.Close() die nächste Datenbank-Operation beginnen kann. Dies verpflichtete fast schon dazu, den Code ab defer db.Close() bei jeder Abfrage so oder so ähnlich zu schreiben. Letztendlich löste das Team das Problem mit folgender Funktion:

[code]
func FetchAll(db *sql.DB, query string, args …interface{}) ([][]interface{}, error)
[/code]

Diese erledigte die oben gezeigte Routinearbeit und verringerte damit den Aufwand pro Abfrage deutlich:

[code]
func GetEmployees(db *sql.DB) ([]Employee, error) {
rows, errFetchAll := FetchAll(db, "SELECT given_name, family_name FROM employee")
if errFetchAll != nil {
return nil, errFetchAll
}

employees := []Employee{}

for _, row := range rows {
employees = append(employees, Employee{row[0].(string), row[1].(string)})
}

return employees, nil
}
[/code]

Jedoch war nun jede Spalte jeder Zeile des Ergebnisses ein interface{}, das erstmal in den richtigen Datentyp umgewandelt werden musste. Dafür wiederum musste die neue Funktion zusätzlich den Spaltentyp beim Datenbanktreiber erfragen, um immer die (hinter dem interface{} versteckten) Datentypen zurückzugeben, die die konkrete Abfrage erwartet. Andernfalls hätten wir uns auf die Standard-Datentypen der Datenbanktreiber verlassen müssen.

Lösung

Nun übernehme ich also den Code Schritt für Schritt in das neue Projekt und frage mich: Geht das nicht auch einfacher? Ja, mit sog. Reflection:

[code]
package blog

import (
"database/sql"
"reflect"
)

func FetchAll(db *sql.DB, rowType interface{}, query string, args …interface{}) (interface{}, error) {
rows, errQuery := db.Query(query, args…)

if errQuery != nil {
return nil, errQuery
}

defer rows.Close()

blankRow := reflect.ValueOf(rowType)
res := reflect.MakeSlice(reflect.SliceOf(blankRow.Type()), 0, 0)
idx := -1
scanDest := make([]interface{}, blankRow.NumField())

for {
if rows.Next() {
res = reflect.Append(res, blankRow)
idx++

row := res.Index(idx)

for i := range scanDest {
scanDest[i] = row.Field(i).Addr().Interface()
}

if errScan := rows.Scan(scanDest…); errScan != nil {
return nil, errScan
}
} else if errNext := rows.Err(); errNext == nil {
break
} else {
return nil, errNext
}
}

return res.Interface(), nil
}
[/code]

Diese Funktion erwartet einen zusätzlichen Parameter, rowType. Dessen eigentlicher Typ hinter interface{} (Employee) bestimmt den Typ einer Zeile des Abfrage-Ergebnisses. Das komplette Ergebnis ist logischerweise eine Slice aus Zeilen ([]Employee). Mit Hilfe von Funktionen aus dem reflect-Paket arbeitet FetchAll() zur Laufzeit mit dem konkreten Datentyp Employee, fast so als wäre er mittels Generics zur Kompilierzeit bekannt:

  • reflect.ValueOf(rowType) analysiert rowType und kapselt ihn als Wert vom Typ Employee
  • reflect.ValueOf(rowType).Type() steht für Employee
  • reflect.SliceOf(Employee) steht für []Employee
  • reflect.MakeSlice([]Employee, 0, 0) steht für make([]Employee, 0, 0)
  • reflect.ValueOf(rowType).NumField() zählt die Felder des Structs Employee

Ja, richtig, rowType muss ein Struct sein, sonst stürzt das Programm spätestens bei reflect.ValueOf(rowType).NumField() ab. Jedes Feld des Structs steht nämlich für eine Spalte des Abfrage-Ergebnisses. Genau das wird in der darauf folgenden Schleife wie folgt bewerkstelligt:

  • res = reflect.Append(res, reflect.ValueOf(rowType)) steht für res = append(res, Employee{})
  • res.Index(idx) steht für res[idx]
  • res[idx].Field(0) steht für res[idx].GivenName
  • res[idx].GivenName.Addr() steht für &res[idx].GivenName

Und .Interface() holt letztendlich den Zeiger auf das Struct-Feld aus der Reflection-Versenkung, damit rows.Scan() die entsprechende Spalte des Abfrage-Ergebnisses darin speichert. Am Ende verbirgt sich hinter res tatsächlich ein []Employee, das mit res.Interface() in ein interface{} gekapselt, um es zurückzugeben. Damit bestimmt GetEmployees() den Zeilen-Typ im voraus und schrumpft auf ein vernünftiges Minimum:

[code]
func GetEmployees(db *sql.DB) ([]Employee, error) {
rows, errFetchAll := FetchAll(db, Employee{}, "SELECT given_name, family_name FROM employee")
if errFetchAll != nil {
return nil, errFetchAll
}

return rows.([]Employee), nil
}
[/code]

Fazit

Nachdem ich zuletzt schon eine C-Bibliothek in Go wiederverwendet habe, spare ich schon zum zweiten mal in Folge Code und damit Zeit. Sprich, wir arbeiten jetzt noch ein bisschen effizienter (als sowieso schon) an euren Projekten. Bestelle noch heute!

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.