Hardware | ALT | NEU |
---|---|---|
CPU | Intel Core I5 6600K | AMD Ryzen 9 7900X |
Mainboard | Asrock Z170 Pro 4S | ASUS TUF Gaming X670E-Plus |
CPU Kühler | EKL Alpenföhn Brocken ECO Tower | Be Quiet Dark Rock Elite |
RAM | 16GB (4x 4GB) Crucial DDR4-2133 | 32 GB (2x 16GB) AData Lancer XMP DDR5-5200 |
Zusätzlich noch eine M.2 SSD WD Black SN850X mit 4TB. Ich hatte zwar vorher auch nur noch SSDs in Betrieb und werde die auch übernehmen, aber als Haupt-Laufwerk hab ich mir das eingeblidet.
Da will ich jetzt gar nicht soviel drüber schreiben. Ich glaub, es gibt im Internet schon genügend Diskussionen darüber, ob man die Wärmeleitpaste (da lag beim Kühler übrigens auch eine dabei) per Tröpfchen-Technik aufträgt oder gleichmäßig mit irgendwas verteilt.
Ich möchte lediglich ein Bild zeigen:
Ja, das ist mein alter Lüfter. Links hab ich bereits leicht abgesaugt, rechts ist noch original… Ich hab in meinem Gehäuse eigentlich Staubfilter drin, aber wenn man das jetzt so sieht, sollte man doch gelegentlich mal den Rechner aussaugen.
Ansonsten hier ein paar Bilder, wie das Teil fertig aussieht
Ein paar kleine Anekdoten kann ich hier noch schreiben:
Meine ursprüngliche Isolierung musste ich zum Großteil entfernen, weil der Kühler viel zu groß ist. Er passt jetzt wirklich auf den Millimeter hinein. Da der Lüfter aber ohnehin so leise ist, bereue ich das auch nicht.
Des Weiteren war es eine lustige Sache: Alles leuchtet und blinkt (das Schlimmste ist der Kühler übrigens) und ich denk mir dabei: “Wenn ich fertig bin, mach ich die Klappe zu und man sieht nichts mehr davon. Ja… ich hab die ganzen LEDs dann im Bios deaktiviert, weil ich die wirklich nicht benötige.
Der viel interessantere Teil ist jetzt vermutlich wie sich die Performance verändert hat.
Also zum Einen hab ich mit Cinebench einen Benchmark ausgeführt. Mit dem alten System kam ich auf folgende Werte:
CPU (Multi Core) | 128 pts |
CPU (Single Core) | 57 pts |
Also Vergleich dazu hier die neuen Zahlen:
CPU (Multi Core) | 28726 pts |
CPU (Single Core) | 1980 pts |
Wenn man diese Zahlen jetzt sieht, könnte man echt meinen, dass mein alter Intel gar nichts mehr geleistet hat. Wenn ich da irgendwas gestartet hätte, hätte ich den Jakobsweg komplett pilgern können und meine Frau hätte ein zweites Kind ausgetragen bevor da irgendwas weiter geht. Aber ganz ehrlich war der Rechner gar nicht so langsam.
Sicherlich spielt auch noch mit rein, dass bekanntlich Windows nach ein paar Jahren Einsatz einfach zugemüllt ist. Das hat sich in meinen Augen immer noch nicht verbessert und verfälscht sicherlich auch das Ergebnis, weil der neue Rechner natürlich komplett neu aufgesetzt ist.
Ich hab daher beschlossen, die Performance mit anderen - für mich praktischeren Zahlen zu vergleichen.
Ich hab die Software, an der ich beruflich arbeite genommen und die Compile-Zeit verglichen. Es handelt sich dabei um ein größeres Angular-Projekt, das zum Compilieren schon etwas aufwändiger ist. Nodetypisch hängt da die Zeit aber nicht nur von der CPU sondern auch von der Festplatte ab. Da ist natürlich die M.2 auch um einiges schneller als eine normale SSD, aber ich denke trotzdem, dass man so besser vergleichen kann.
Da ich diesen Vergleich ohnehin interessant finde, hab ich das gleiche auch mit mehreren Rechnern durchgeführt:
AMD 7900X | Intel Core I5 6600K | Intel Core I7 4800MQ | Intel Core I7 5820K | Apple Pro M2 | |
---|---|---|---|---|---|
NEU | ALT | Thinkpad T540P | Dell Alienware Area 51 | Mac meines Kollegen | |
real | 1m03,250s | 4m52,329s | 6m10,482s | 4m44,658s | 1m38,54s |
usr | 1m57,134s | 6m23,232s | 7m46,321s | 0m0,153s | 204,52s |
sys | 0m9,978s | 0m2,332s | 0m18,052s | 0m0,276s | 16,08s |
Also bei der Compilezeit (die erste Zeile ist das Interessanteste für mich), sieht man, dass der Unterschied definitiv da ist, aber weitem nicht so schlimm, wie das von Cinebench aussieht. Der I7 aus der Arbeit würde von der Zeit sicherlich auch noch viel mehr schaffen, aber wir haben im Büro alle das Gefühl, dass die Domäne und der installierte Virenscanner erheblich für Einbußen in der Performance verantwortlich ist.
Zudem sollte man auch erwähnen, dass ich bei meinen Systemen zu Hause unter Linux compiliert habe, während der I7 in der Arbeit unter Windows 10 läuft.
Ich bin jedenfalls froh um den neuen Rechner. Mittlerweile ist auch alles wieder einigermaßen eingerichtet und es macht einfach Spaß mit ein bisschen mehr Performance an einem Rechner zu sitzen. Spiele hab ich ehrlich gesagt bis jetzt noch nicht ausprobiert, aber da muss ich noch schauen, was geht. Natürlich wird da die Grafikkarte dann der limitierende Faktor sein, aber ich denke trotzdem, dass ein bisschen mehr gehen wird, als früher.
]]>Fangen wir damit an, wie man die WSL installiert. Dafür ist eigentlich nur ein aktuelles Windows 10 oder Windows 11 notwendig und die notwendigen Komponenten sind in der Regel auch schon installiert.
Wir öffnen also eine Kommandozeile mit Administratorrechten (ich verwende mittlerweile immer Windows Terminal mit Powershell) und gibt folgendes ein:
$ wsl --install
Und dann legt das System schon los
Installation: VM-Plattform
VM-Plattform wurde installiert.
Installation: Windows-Subsystem für Linux
Windows-Subsystem für Linux wurde installiert.
Herunterladen: WSL-Kernel
Installation: WSL-Kernel
WSL-Kernel wurde installiert.
Herunterladen: Ubuntu
Der angeforderte Vorgang wurde erfolgreich abgeschlossen. Änderungen werden erst nach einem Neustart des Systems wirksam
Nach dem geforderten Neustart - der kann eine weile dauern, weil jetzt die Windows Features dafür installiert und konfiguriert werden - öffnet sich ein Konsolenfenster mit dem Titel “Ubuntu”. Ubuntu ist das Standardsystem, das für die WSL verwendet wird. Man kann natürlich auch andere Linux-Systeme nachträglich installieren, aber ich konzentriere mich hier jetzt auf den Standard.
In diesem Fenster wird folgendes Angezeigt:
Installing, this may take a few minutes...
Please create a default UNIX user account. The username does not need to match your Windows username.
For more information visit: https://aka.ms/wslusers
Enter new UNIX username:
Hier gibt man dann die Accountdaten ein, die man künftig für das nun installierte Linuxsystem verwenden will. Ich empfehle, dass man hier den gleichen Benutzernamen verwendet, den man auch unter Windows verwendet. Das minimiert die Verwechselungsgefahr.
Dann sehen wir auch schon den Prompt für das neue Linux-System, das man gerade installiert hat.
Ich habe immer die Angewohntheit, dass ich erst einmal auch gleich das Linuxsystem auf den aktuellen Stand bringe:
$ sudo apt update
$ sudo apt upgrade -y
$ sudo apt autoremove -y
Dann können wir mal beginnen, dass wir Docker installieren.
An sich kann man Docker auch direkt aus dem Ubuntu-Repository installieren. Aber selbst auf den Docker-Hilfeseiten wird empfohlen, dass man stattdessen das offizielle Docker-Repository einbindet und Docker von da installiert. Damit kann man eher sicherstellen, dass man die aktuellste Version verwendet.
Dazu gibt man folgende Statements - auf die ich jetzt nicht näher eingehe - ein:
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Damit ist dann das Repository ins System eingebunden und mit den folgenden Statements kann man dann Docker installieren:
$ sudo apt update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Damit ist die Installation an sich auch abgeschlossen.
Auf einem normalen Linuxsystem wäre jetzt die Installation erledigt und man könnte Docker auch schon verwenden. Wenn man hier aber jetzt versucht Docker zu verwenden, wird es leider noch nicht funktionieren:
$ docker run hello-world
Man erhält dann folgenden Fehler:
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.
See 'docker run --help'.
Das liegt einfach daran, dass der Docker-Daemon auf einem normalen Linuxsystem im Systemd konfiguriert wird, aber in der WSL ist dieser Sitzungsmanager leider nicht vorhanden (in einer kommenden Version soll das auch in der WSL verfügbar sein).
Man muss also den Docker Daemon manuell starten.
Diesen startet man an sich als root mit dem Kommando dockerd. Das ist allerdings ein bisschen umständlich, wenn man das jedes mal manuell machen muss. Ein geeigneter Ort dafür wäre das Startscript .bashrc.
Daher fügen wir in der .bashrc folgendes am Ende ein:
# Start Docker daemon automatically when logging in if not running.
RUNNING=`ps aux | grep dockerd | grep -v grep`
if [ -z "$RUNNING" ]; then
echo "docker daemon is launching"
DOCKER_DIR=/mnt/wsl/shared-docker
DOCKER_SOCK="$DOCKER_DIR/docker.sock"
export DOCKER_HOST="unix://$DOCKER_SOCK"
if [ ! -S "$DOCKER_SOCK" ]; then
mkdir -pm o=,ug=rwx "$DOCKER_DIR"
chgrp docker "$DOCKER_DIR"
fi
sudo dockerd > /dev/null 2>&1 &
disown
fi
Jetzt hat man allerdings das Problem, dass dockerd als root ausgeführt werden muss. Durch den sudo Befehl würde man immer, wenn man sich in die WSL einloggt nochmal das Passwort eingeben müssen. Abgesehen davon kann diese zusätzliche Passwortaufforderung dafür sorgen, dass die Anzeige im Terminal fehlerhaft sein kann.
Fügen wir also eine Freigabe in der Sudo-Konfiguration ein, um dieses Problem zu umgehen:
$ sudo visudo
Hier am Ende dann folgendes eingeben:
%sudo ALL(ALL) NOPASSWD: /usr/bin/dockerd
Statt %sudo kann man direkt den Benutzernamen verwenden oder eine andere Gruppe.
Anschließen kann man sich aus dem Linux-System mit exit abmelden und öffnet die WSL nochmal (dazu einfach in der Kommandozeile wsl eingeben).
Jetzt sollte der Docker-Daemon gestartet sein und man kann Docker nocheinmal ausprobieren:
docker run hello-world
Aber auch jetzt bekommt man einen Fehler.
docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create": dial unix /var/run/docker.sock: connect: permission denied.
See 'docker run --help'.
An sich ist das korrekt, weil man auf einem Server docker aus Sicherheitsgründen mit sudo verwenden soll, aber für unsere Zwecke ist das eher hinderlich. Daher wollen wir den aktuellen Benutzer noch der Gruppe docker hinzufügen, damit man docker ohne sudo direkt verwenden kann.
$ sudo usermod -aG docker $USER
Und jetzt sollte endlich alles so funktionieren, wie es soll.
Wir haben jetzt Docker in der WSL installiert. Damit man jetzt einen Container starten kann, muss man immer erst in die WSL wechseln und kann dann loslegen. Wem das reicht, der ist jetzt fertig. ich finde es allerdings bequemer, wenn ich direkt auf der Kommandozeile unter Windows gleich meine Docker-Kommandos absetzen kann.
Dazu muss man sich ein bisschen Gedanken machen, wie man ein Kommando in der WSL ausführt:
$ wsl -d Ubuntu ps aux
Damit kann ich zum Beispiel die Prozesse ausgeben, die in der WSL ausgeführt werden.
Man schreibt also nur das Kommando wsl, gefolgt mit dem Parameter -d, der angibt, in welcher Umgebung das Kommando ausgeführt werden soll (man kann ja auch mehrere Linux-Systeme in der WSL installieren) und dann das jeweilige Statement, das man ausführen will.
Das Kommando für docker würde also so aussehen:
$ wsl -d Ubuntu docker -H unix:///mnt/wsl/shared-docker/docker.sock run hello-world
das Socket haben wir im Startscript so definiert und man sollte das entsprechend angeben, damit man voll auf die Docker-Engine zugreifen kann.
Wenn man aber immer das gesamte Statement so eingeben muss, wäre das nicht wirklich eine Erleichterung. Besser wäre es, wenn man auch unter Windows einfach mit dem Befehl docker die entsprechenden Funktionen zur Verfügung hätte.
Das kann man mit Hilfe einer Funktion bewerkstelligen. Eine solche Funktion definiert man am besten im Profile-Skript der Powershell und dieses findet man hier:
C:\Users\<username>\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
hier schreibt man jetzt also folgendes rein:
function docker {
wsl -d Ubuntu docker -H unix:///mnt/wsl/shared-docker/docker.sock @Args
}
Das war’s jetzt endlich. Nur noch die Kommandozeile neu öffnen, damit das Profile neu geladen wird und es sollte alles funktionieren.
Wenn wir das Ganze jetzt nochmal unter der Windows Kommandozeile (Powershell) testen, erhalten wir folgende Ausgabe:
$ docker run hello-world
Die Ausgabe sieht dann so aus:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:18a657d0cc1c7d0678a3fbea8b7eb4918bba25968d3e1b0adebfa71caddbc346
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Damit sind wir fertig und können Docker jetzt endlich ganz normal unter Windows verwenden.
]]>Die Technik funktioniert folgendermaßen:
In der WebAPI gibt es einen offenen Endpunkt, über den sich ein Client authentifizieren kann. Zum Beispiel mit Benutzername und Passwort. Der Endpunkt liefert
dem Client dann einen codierten Token zurück in dem dann unter Umständen auch noch weiter nützliche Informationen zum angemeldeten Benutzer enthalten sind.
Diesen Token muss dann der Client bei jedem Request im Auth-Header mit dem Schlüsselwort Bearer an den Server schicken, so dass der Server erkennen kann, dass dieser Request auch den
entsprechenden Endpunkt verwenden darf. Wenn der Token fehlt, ungültig ist oder abgelaufen ist, dann liefert der Server einen entsprechenden Fehler zurück.
Ein Token besteht immer aus drei Teilen:
Header
Hier stehen einige allgemeine Informationen zum Token, also um welchen Typ es sich handelt und welcher Algorhytmus in diesem Token verwenden wird. Das Ganze ist ein einfaches JSON Objekt, das mit base64 codiert ist. Um diesen Teil muss man sich meist keine oder nur wenig Gedanken machen, da dieser Teil in er Regel von den Frameworks automatisch verwaltet wird.
Payload
Der zweite Teil ist schon interessanter: Hier stehen die Claims, die man im Server festlegen kann. Also die - oben erwähnten - weiteren nützlichen Informationen.
Auch hier handelt es sich lediglich um ein JSON-Objekt, das nur mit base64 codiert wurde und damit auch jeder Zeit vom Client wieder gelesen werden kann.
Daher ganz wichtig: Niemals vertrauliche Daten in den Claims ablegen.
Zusätzlich stehen hier auch ein paar Standard-Informationen zum Token, wie zum Beispiel der Aussteller (Issuer), wie lange der Token gültig ist und so weiter.
Signature
Der dritte und letzte Teil ist dann eigentlich das Kernstück des Ganzen. Hier findet man die digitale Signatur, die aus den ersten beiden Teilen und einem SecretKey, den nur der Server kennt, erstellt wird. Das heißt, dass damit geprüft wird, ob der Inhalt der ersten beiden Teile valide ist und allgemein der Token erlaubt ist.
Hier haben wir ein kleines Beispiel, wie ein Token aussehen kann:
Zum Decodieren von base64 zu einem lesbaren String verwende ich folgende Methode:
static string decodeBase64(string value)
{
Byte[] bytes = Convert.FromBase64String(value);
return Encoding.UTF8.GetString(bytes);
}
Die einzelnen Teile sind immer durch einen Punkt getrennt. Man kann also den ersten Teil nehmen und dekodieren:
{"alg":"HS256","typ":"JWT"}
Der zweite Teil sieht so aus:
{"exp":1614972736,"iss":"Dashboard","aud":"Dashboard"}
Da das Decodieren der Tokens recht einfach ist, gibt es auch im Internet eine große Auswahl an Seiten, auf denen man einen Token
decodieren und prüfen kann.
Hier ein Beispiel: https://www.jsonwebtoken.io
In der WebAPI unterstützt uns das Baukasten-System des Frameworks sehr stark bei der Implementierung der JWT-Authentifizierung.
Im Grunde muss man nur die notwendige Middleware in der startup.cs konfigurieren und aktivieren.
Wie sieht das aus:
Zuerst benötigen wir das folgende Nuget Package: Microsoft.AspNetCore.Authentication.JwtBearer
Falls es noch nicht installiert ist, müssen wir das über den Nuget-Paketmanager installieren.
Dann fügen wir in ConfigureServices() die Middleware für die Authentifizierung dazu:
var authBuilder = services.AddAuthentication(opt => {
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
});
Damit ist schonmal klar, dass für die Authentifizierung der JwtBearer verwendet werden soll. Diesen muss man aber jetzt noch konfigurieren und festlegen, welche Bestandteile davon für eine Gültigkeit geprüft werden sollen.
authBuilder.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://authserver.mydomain.com",
ValidAudience = "https://application.mydomain.com",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@345"))
};
});
in diesem Fall ist das ganz einfach gehalten mit einem super sicheren SecretKey. Der gehört natürlich in einem echten System nicht hier her, sondern in einen verschlüsselten KeyStore oder etwas Ähnlichem. Auch die Werte für Issuer und Audience sind hier nur dummy Beispiel, damit man sieht, was es geben würde. Normalerweise steht im Issuer die Anwendung, die den Token ausstellt. Also der Login-Server zum Beispiel, wenn das System getrennt ist. In Audience dagegen steht die Anwendung, die den Token verwendet. In einfachen Anwendungsfällen haben diese Felder nur informativen Character. Aber auf großen geteilten Systemen kann man hier noch eine zusätzliche Sicherheitsebene damit aufbauen.
Die einzelnen Properties sind im Grunde selbsterklärend bzw. weiterführenden Informationen zu den Möglichkeiten kann man der Microsoft-Hilfe entnehmen.
Zu guter Letzt muss man die Middleware noch in der Methode configure() aktivieren:
...
app.UseRouting();
app.UseAuthentication(); // <--- hier
app.UseAuthorization();
...
Jetzt muss nur noch die Endpunkte bzw. Controll mit einem Attribut versehen, damit das System weiß, dass für diese Endpunkte eine Authentifizierung notwendig ist. Dazu fügt man einfach bei den Controller-Klasse oder bei den Endpunkten das Attribut [Authorize] dazu.
Beispiel:
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
...
}
Das werden wir später noch weg rationalisieren, so dass standardmäßig alle Endpunkte eine Authentifizierung benötigen.
Jetzt brauchen wir noch einen Endpunkt, über den man sich überhaupt authentifizieren kann, damit man dann auch einen JWT-Token bekommt, den man für die anderen Endpunkte dann verwenden kann.
Wir legen uns dazu einen neuen Controller mit dem Namen AuthController an und in diesem einen Endpunkt Login
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
[HttpPost]
[Route("login")]
public IActionResult Login([FromBody] LoginModel user)
{
}
}
Ich verwende hier ein ViewModel für die Login-Daten. Das Model ist ganz einfach aufgebaut:
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
Was muss jetzt eigentlich hier passieren? Eigentlich ganz einfach: Es muss geprüft werden, ob die Login-Daten korrekt sind und falls ja,
muss ein Token erstellt werden, der die notwendigen Informationen enthält.
Das Ganze kann dann zum Beispiel so aussehen:
[HttpPost]
[Route("login")]
public IActionResult Login([FromBody] LoginModel user)
{
if (user == null)
return BadRequest("Invalid client request");
if (user.Username == "username" && user.Password == "password")
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("supersecretkeyyy"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokenOptions = new JwtSecurityToken(
issuer: "Dashboard",
audience: "Dashboard",
claims: new List<Claim>(),
expires: DateTime.Now.AddMinutes(5),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return Ok(new { Token = tokenString });
}
else
return Unauthorized();
}
Wichtig: Es gibt hier nicht das Attribut [Authorize], weil man sich ja mit diesem Endpunkt ohne Token authentifizieren will.
Wir verwenden also unseren super sicheren SecretKey und erstellen daraus ein Credentials-Objekt. Dann werden die Properties
des Tokens mit unseren Daten befüllt, die wir benötigen. Unter anderem eben auch die oben erwähnten Claims, also weitere nützliche Informationen, die der
Client auch auslesen kann und natürlich die Gültigkeit, wie lange der Token gültig sein soll. Hier nur 5 Minuten.
Über die Methode WriteToken wird letztendlich der Token dann erstellt und kann im Result an den Client zurück gegeben werden.
Wenn man in seinem Projekt Swagger verwendet (wenn man das Projekt über die Konsole mit dotnet new webapi erstellt hat, ist Swagger automatisch vorhanden),
wird man feststellen, dass man jetzt die Endpunkte in Swagger nicht mehr testen kann.
Dazu muss man Swagger erst erklären, wie man sich authentifizieren kann und das entsprechend in der startup.cs konfigurieren. Dazu fügt man in ConfigureServices eine SecurityDefinition und ein SecurityRequirement zu Swagger hinzu:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "DashboardApi", Version = "v1" });
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Authorization",
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new List<string>()
}
});
});
Jetzt gibt es auf der Swagger-Seite einen neuen Button Authorize über den man dann den Bearer Token eintragen kann.
Man führt zuerst den Login-Endpunkt mit seinen Benutzerdaten aus und kopiert sich das Result in den Zwischenspeicher. Dann clickt man den Button
Authorize und trägt hier bei Value folgendes ein:
Bearer xyz
Wobei xyz natürlich der davor erstellte Token sein soll.
Eigentlich funktioniert jetzt alles, wie es soll. Aber es gibt einen kleinen Nachteil, der durch eine kleine Unachtsamkeit gleich eine Sicherheitslücke öffnen kann. Man muss jetzt nämlich jeden Controller bzw. jeden Endpunkt mit dem AutorizeAttribute versehen, damit die Authentifizierung überhaupt greift.
Das ist relativ unschön, weil man ja eigentlich davon ausgeht, dass möglichst alles gesperrt sein sollte und nur die Endpunkte zum Anmelden öffentlich zugänglich sein sollten.
In früheren Versionen von .Net Core musste man dafür einen globalen AuthorizationFilter registrieren und darin das entsprechende Attribut quasi automatisch setzen. Ausgenommen wurden von diesem Filter lediglich die Endpunkte/Controller, die man explizit mit einem AllowAnonymousAttribute versehen hat.
Seit .Net Core 3.1 geht das zum Glück auch einfacher. Man muss jetzt lediglich in Configure() bei der Endpoint-Configuration sagen, dass die Endpunkte eine Authentifizierung benötigen sollen.
Das sieht dann so aus:
public void Configure(...)
{
...
app.UseEndpoints(endpoints =>
{
endpoints
.MapControllers()
.RequireAuthorization(); // <--- hier
});
...
}
Damit man jetzt den Login-Endpunkt allerdings verwenden kann, muss man diesen mit dem Attribut AllowAnonymous versehen:
[HttpPost]
[Route("login")]
[AllowAnonymous]
public IActionResult Login([FromBody] LoginModel user)
...
Hier mal die Sachen, die ich dieses Jahr rein gesteckt habe:
Also irgendwie alles, was man gerade so findet und auf was man Lust hat. Natürlich muss man bei bestimmten Sachen - wie zum Beispiel Zitronenmelisse - ein bisschen aufpassen, da diese gerne zu intensiv wird und es soll ja ein Salbei-Likör werden. Aber da muss man eben ein bisschen rumprobieren.
Ich schneide meine Kräuter einfach, putze sie, falls nötig und dann reiße ich sie einfach ein bisschen kleiner und geb sie in ein Einweck-Glas. Reißen deswegen, weil ich das Gefühl habe, dass damit die ätherischen Öle besser austreten können. Das kann auch ein Irrtum sein, aber ganz ehrlich… man muss da wirklich keine Kunstwerke ausschneiden, weil die Blätter in ein paar Monaten dann eh weggeworfen werden.
Das Ganze kommt dann wie gesagt in ein Einweck-Glas und dann gibt man noch etwas Zucker dazu. Ich hab auf mein großes Einweck-Glas dieses mal 200g braunen Rohrzucker gegeben. Gegebenenfalls muss man dann am Ende nochmal nachzuckern, aber da komm ich dann noch drauf.
Zu guter Letzt wird das Ganze mit einem Doppelkorn aufgefüllt bis das Glas voll ist. Sollte es sich herausstellen, dass noch Kräuter reinpassen (die fallen beim aufgießen ein bisschen zusammen), dann nimmt man einfach noch mehr Salbei und drück den hinein. Das Einweck-Glas soll am Ende wirklich bis oben hin voll sein, damit möglichst wenig Luft im Glas verbleibt.
Dann verschließen, gut durchschütteln, damit sich der Zucker auflöst und ab ins Regal damit, wo der Likör dann einige Monate (2-3 Monate) stehen darf. Gelegentlich sollte man ihn wieder schütteln.
Wenn man ihn dann später abfüllt, kann man ihn abschmecken. Falls ich nachträglich noch Zucker nachgeben will, mach ich das immer so, dass ich eine Schöpfkelle voll Likör in einen Topf gebe und erwärme. Dann geb ich auch noch den Zucker in den warmen Likör und löse ihn unter rühren auf. Aufpassen, dass der Likör nicht zu heiß wird. Nur ein bisschen erwärmen, damit sich der Zucker besser auflöst. Dann wieder zurück zum restlichen Likör und wieder probieren. Solange, bis man entweder soviel probiert hat, dass man nicht mehr stehen kann oder bis einem der Geschmack passt.
Grundsätzlich ist der Likör sehr einfach zu machen. Man nehme zwei verschiedene Sorten von Beeren und etwas Zucker und fülle das Ganze mit Alkohol auf. Das wäre die Anleitung ganz allgemein.
Hier aber jetzt das Ganze ein bisschen im Detail:
300g rote Johannisbeeren 300g Stachelbeeren 300g brauner Zucker
Ich habe mich für Johannisbeeren und Stachelbeeren entschieden, weil die in meinem Garten waschen und auch vom Geschmack her gut zusammenpassen. Ich muss dazu sagen, dass es sich bei meinen Stachelbeeren nicht um reine Stachelbeeren handelt, sondern um eine Kreuzung mit Johannisbeeren. Genau kann ich das nicht sagen, weil ich den Strauch von meinen Eltern geschenkt bekommen habe. So quasi als eine der esten Nutzpflanzen im eigenen Garten vor einiger Zeit. Diese Stachelbeeren sind jedenfalls ein bisschen sauerer als normale Stachelbeeren und passen daher hervorragend zu den ohnehin sauren Johannisbeeren.
Man könnte aber auch andere Beerenfrüchte verwenden. Wichti ist lediglich, dass die Beeren vom Geschmack her zusammen passen.
So. Das Ganze füllt man dann nur noch mit einem Obstler auf. Der Obstler bringt auch noch etwas Eigengeschmack mit, so dass letztendlich ein richtig fruchtiger, beeriger Likör entsteht.
Ich hab das Ganze in ein großes Einmachglas (1,8l) gefüllt und wirklich bis oben hin mit Obstler aufgefüllt.
Das Ganze muss ich jetzt noch für ca. 3 Monate stehen lassen und dabei gelegentlich schütteln. In ein paar Tagen nimmt die Flüssigkeit dann schon eine schöne, rote Farbe an.
Wenn die 3 Monate rum sind, kann man die Beeren einfach durch ein feines Sieb abseien und den Likör in Flaschen füllen. Ich habe die Beeren nicht nur abgeseit, sondern auch noch ausgedrückt, dmait ja nichts verloren geht.
Hier ein paar Bilder, wie es im Anfangsstadium aussieht:
Fangen wir mal an mit der Planung eines kleinen Dockerfiles.
Was benötigen wir?
Ich glaub, mit dieser Basis kann man schon anfangen und das würde dann ungefähr so aussehen:
FROM python:2.7-alpine
MAINTAINER mossi <mail@hofmann-robert.info>
RUN apk add less \
&& pip install markdown \
&& pip install pygments \
&& pip install pyyaml \
&& pip install docopt \
&& pip install tabulate \
&& pip install setuptools
Was jetzt noch fehlt, ist das Github-Projekt. Ich möchte allerdings vermeiden, dass ich Git und alles was dazugehört in diesem Image mitinstallieren muss. Daher habe ich beschlossen, dass ich stattdessen mit einem Build-Script arbeite, das den Quellcode holt und diesen füge ich dann einfach in das Image ein und installiere ihn.
Das Dockerfile geht also folgendermaßen weiter:
ADD terminal_markdown_viewer /build
WORKDIR /build
RUN /build/setup.py install \
&& rm -rf /build
Wie man sieht, lösche ich den Ordner /build gleich wieder nachdem ich mit der Installation fertig bin. Ich brauche die Dateien ja nicht mehr und kann mir damit auch den Speicherplatz dafür sparen.
Damit die Anzeige so ist, wie im alten Post, will ich das Ganze mit less anzeigen und darin auch entsprechend farbig ausgeben lassen. Da ich dafür eine pipe benötige, kann ich das nicht direkt als Command oder Entrypoint angeben (bzw. ich hab zumindest selbst nicht rausgefunden, wie ich das umsetzen könnte). Daher mache ich stattdessen ein kleines Script, dass ich ebenfalls hinzufüge.
Hier erst einmal das kleine Script:
#!/bin/sh
mdv "$1" | less -R
Dieses wird jetzt über das Dockerfile hinzugefügt und außerdem auch gleich noch das Arbeitsverzeichnis entsprechend gesetzt.
Hier also weiter mit dem Dockerfile:
ADD view.sh /app/view.sh
WORKDIR /data
RUN chmod 777 /app/view.sh
ENTRYPOINT ["/app/view.sh"]
Damit ist das Dockerfile quasi komplett.
Oben habe ich schon erwähnt, dass ich ein Build-Script verwenden will, dass mir die Daten aus Github holt und zudem auch noch das Erstellen des Images anstößt.
Hier das kleine Build-Script:
#!/bin/bash
git clone https://github.com/axiros/terminal_markdown_viewer
docker build -t mossi/mdv --rm .
rm -rf terminal_markdown_viewer
Auch hier mach ich es so, dass das Projekt am Ende wieder gelöscht wird. Falls ich nochmal kompilieren muss, wird es ja ohnehin wieder neu geholt und so kann ich auch sicher stellen, dass das Programm immer aktuell ist.
So. Wenn ich jetzt das Build-Script ausführe, hab ich mein Image und kann daraus schon einen Container erstellen und ausführen.
Zum Ausführen kann ich folgendes Kommando verwenden:
docker run -ti --rm mossi/mdv "sample.md"
Man wird aber jetzt feststellen, dass das so nicht klappt. Der Hintergrund ist ganz einfach. Selbst wenn wir die Datei sample.md lokal in unserem Verzeichnis liegen haben, so ist diese Datei dennoch nicht im Container vorhanden und kann daher auch nicht angezeigt werden. Man muss also die Datei irgendwie zur Laufzeit in den Container bringen.
Ich hab mir dabei gedacht, dass ich einfach das aktuelle Verzeichnis in den Container mounte und damit sollte die Datei dann ja auch verfügbar sein.
docker run -ti --rm -v $PWD:/data mossi/mdv "sample.md"
Super, das funktioniert schonmal.
Jetzt integrieren wir das wieder in die Linux-Shell, damit es leichter aufzurufen geht und man sich nicht immer das ganze Statement merken muss. Dazu war mein erster Gedanke, dass ich einfach einen Eintrag in der .bash_aliases erzeuge und das war’s dann auch schon:
alias view='docker run -ti --rm -v $PWD:/data mossi/mdv'
Einmal neu anmelden und schon kann ich meine Markdown-Dateien schön formatiert anzeigen indem ich mit dem Kommand view einfach die Datei öffne. Natürlich sollte man sich einen anderen Kommandonamen einfallen lassen, falls man view schon für was anderes verwendet wie zum Beispiel die alte Anleitung.
Leider gibt es doch noch ein kleines Problem. Ich habe meine Notizen in einem speziellen Ordner. Wenn ich mich jetzt im Ordner /etc befinde und von dort mit view ~/notes/note.md meine Notiz anzeigen lassen will, steht ich wieder vor dem Problem, dass die Datei im Comtainer nicht vorhanden ist. Eigentlich ist das auch klar, weil ich PWD in den Container mounte und in diesem Fall ist das leider der falsche Pfad.
Abhilfe zu diesem Problem kann man schaffen, indem man doch wiede eine function erzeugt.
Wie im alten Artikel öffne ich also die Datei .bash_functions und erweitere sie um folgenden Code:
function view {
path="$(realpath "$1")"
directory="$(dirname "$path")"
filename="$(basename "$path")"
docker run -ti --rm -u $(id -u):$(id -g) -v "$directory":/data mossi/mdv "$filename"
}
export -f view
Was passiert hier? Ich nehme die übergebene Datei und zerlege deren absoluten Pfad in Pfad und Dateiname. Dann mounte ich statt PWD diesen hier ermittelten Pfad und rufe über Docker die entsprechende Datei auf.
Den Eintrag in der .bash_aliases sollte man natürlich wieder entsprechend entfernen, da der ohnehin nicht korrekt funktioniert.
Aber jetzt sollte alles so klappen, wie man es sich vorstellt.
ssh phpmyadmin.hofmann.lan
Verdammt viel zu tippen, wenn man so tipfaul ist, wie ich.
Bisher hab ich dazu immer Aliases angelegt, so dass ich irgendein Kürzel definiert habe, um schnell auf irgendwas zuzugreifen. Aber das ist weder dynamisch noch besonders einfallsreich.
Ich habe mal vor einiger Zeit einen Webservice gebastelt, der von meinem Nameserver das Zone-File ausliest und die darin enthaltenen Einträge als JSON-Liste zurück gibt. Die Liste sieht ungefähr so aus:
[
{
"ipaddress":"192.168.0.5",
"subdomain":"odroid.hofmann.lan"
},
{
"ipaddress":"192.168.0.11",
"subdomain":"desktop.hofmann.lan"
},
{
"ipaddress":"192.168.0.12",
"subdomain":"laptop.hofmann.lan"
}
]
Diese Liste kann man dafür hervorragend verwenden. Ich will jetzt aber gar nicht darauf eingehen, wie ich den Webservice aufgebaut habe, sondern ich will zeigen, wie man diese Daten verwenden kann.
Mein erster Gedanke war, wie ich die Daten in ein Format bringen kann, das ich verwenden kann. Zuerst braucht man natürlich einen Zugriff auf den Webservice. Das kann man in Bash ganz leicht mit curl machen:
curl -s dns-sniffer.hofmann.lan/api/dns/get/hofmann.lan
Dieses Kommando liefert mir schonmal das gesamte JSON, das ich benötige. Ich brauche aber davon lediglich die Werte im Feld subdomain. Hier musste ich dann schon ein bisschen suchen, weil JSON nicht so einfach in der Bash verarbeitet werden kann. Ich könnte natürlich den Webservice entsprechend anpassen, dass mir nur noch die Subdomains als plain text geliefert werden, aber es gibt zum Glück auch andere Möglichkeiten.
Das Kommando (oder Progrämmchen) zum Verarbeiten von JSON nennt sich jq (json query). Die Syntax für meinen Anwendungsfall schaut so aus:
jq -r '.[] | .subdomain'
Ganz grob übersetzt bedeutet das, dass ich aus jedem Element des Arrays das Feld subdomain haben will. Der Parameter -r gibt an, dass ich das Ganze raw haben will, also ohne Quotes um die einzelnen Ergebnisse.
Diese beiden Kommandos kann man jetzt noch kombinieren und das Ergebnis sieht dann so aus:
curl -s dns-sniffer.hofmann.lan/api/dns/get/hofmann.lan|jq -r '.[] | .subdomain'
Und es wird jetzt folgende Liste ausgegeben:
odroid.hofmann.lan
desktop.hofmann.lan
laptop.hofmann.lan
Die Scripts für die Vervollständigung findet man im Verzeichnis /etc/bash_completion.d. Hier legt man sich ein neues Script mit dem Namen ssh an und kopiert folgenden Inhalt hinein:
function _ssh
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=$(curl -s dns-sniffer.hofmann.lan/api/dns/get/hofmann.lan|jq -r '.[] | .subdomain')
COMPREPLY=( $(compgen -W "$opts" -- ${cur}) )
return 0
}
complete -F _ssh ssh
Ich gehe jetzt nicht auf jede Zeile ein, da man das alles aus der Doku rauslesen kann. Im Grunde sehen die Files ohnehin immer gleich aus. Lediglich die Zeile opts=… enthält die für uns wichtigen Informationen. Hier schreibt man nämlich das Kommando rein, das wir vorher erarbeitet haben. Das Ganze soll in einer Variablen angelegt sein.
Die letzte Zeile legt dann fest, dass beim Kommando ssh die Funktion _ssh für die Parameter verwendet werden soll und diese liefert die entsprechenden Parameter die in opts festgelegt wurden.
Anschließend einmal neu am System anmelden bzw. falls man auf einem Desktop arbeitet, reicht es auch aus, wenn man das Terminal neu öffnet und das war es dann auch schon.
Jetzt muss ich nur immer einen Eintrag im Nameserver erzeugen, wenn ich einen neuen Dienst im Netzwerk bereitstelle. Eine weitere Konfiguration auf den Clients ist nicht mehr notwendig und ich kann immer auf alle meine Hosts mit wenig Tippen zugreifen.
ssh o<tab>
Das reicht, um den Text auf ssh odroid.hofmann.lan zu vervollständigen.
]]>Jetzt aber zu meinem aktuellen Anliegen:
Ich schreibe wie gesagt nahezu alles mit Markdown. Auch meine Notizen, auf meinen Linux-Servern. Jetzt kann es aber vorkommen, dass ich die Notizen gerne mal sauber formatiert ausgeben will. Und zwar auf der Konsole, da man per ssh nunmal in der Regel nur eine Konsole zur Verfügung hat.
Das ist dann schon nicht mehr so einfach, aber auch machbar, da es mittlerweile einige netten Tools gibt, die Markdown entsprechend für die Konsole aufbereiten.
Eines davon ist zum Beispiel der Terminal Markdown Viewer, den man auf Guthub unter folgender Adresse findet: https://github.com/axiros/terminal_markdown_viewer
Die Installation ist an sich auf der github-Seite recht gut beschrieben. Dennoch will ich sie hier nochmal zusammenfassen.
Zuerst benötigen wir python und python pip. Python ist ja in den meisten Fällen schon installiert, aber falls nicht, kann man hier einfach eine Standard-Installation durchführen:
sudo apt install python python-pip
Anschließend braucht man verschiedene Python-Module, die man dann per pip installieren kann:
sudo pip install markdown
sudo pip install pygments
sudo pip install pyyaml
sudo pip install docopt
sudo pip install tabulate
Dann holt man sich mal das Paket aus Github:
git clone https://github.com/axiros/terminal_markdown_viewer
und wechsel ist das entsprechende Verzeichnis, das damit angelegt wurde. Hier kann man dann das Setup starten:
sudo ./setup.py install
Jetzt kann man das Tool schon verwenden. Legen wir dazu einfach mal eine Sample-Markdown-Datei an:
# header 1
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
## header 2
* list1
* list2
* list3
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Wenn man jetzt auf der Kommandozeile das folgende Kommando ausführt, wird der Markdown-Text aufbereitet ausgegeben:
mdv sample.md
Das ist jetzt schon alles recht schön. Aber mich stört hier jetzt noch eine Kleinigkeit. Der Text wird nämlich einfach ausgegeben, aber man kann nicht wirklich scrollen (lediglich das herkömmliche Scrollen mit Bild auf/ab) und man kann auch nicht gezielt an eine Stelle springen.
In der Regel verwende ich zum Anzeigen von Textdateien immer less. Damit kann man dann schön vor- und zurückblätter und man kann auch durch die Suche an bestimmte Stellen innerhalb der Datei springen.
Durch das Command Piping kann man das alles ja recht schön verbinden:
mdv sample.md | less
Allerdings wird man jetzt feststellen, dass die Ausgabe jetzt nicht mehr farbig ist, sondern seltsame Steuerzeichen stattdessen angezeigt werden. Das hängt damit zusammen, dass less standardmäßig keine Farbcodes ausgibt, sondern stattdessen die Steuerzeichen ausgibt. Mit dem Parameter -R kann man das ändern. Das Kommando würde also jetzt so aussehen:
mdv sample.md | less -R
Und siehe da… Jetzt sieht das auch alles nach was aus.
Das Einzige, was mir immer noch nicht gefällt ist, dass ich jetzt relativ viel eingeben muss, um eine Datei anzuzeigen (ja, ich bin schreibfaul). Das heißt, ich würde mir für dieses Kommando ein alias anlegen. Allerdings wüsste ich jetzt nicht, wie man ein alias mit einem Parameter dazwischen drin anlegt.
Es gibt aber auch noch eine andere Möglichkeit: Die Bash-Functions.
Dazu lege ich mir eine Datei in meinem Homeverzeichnis an:
nano ~/.bash_functions
Der Inhalt sieht folgendermaßen aus:
function view { mdv "$1" | less -R; }
export -f view
Ich erzeuge also eine Funktion mit dem Namen view. Die Funktion selbst enthält genau das Kommando, das ich mir vorher erarbeitet habe.
Anschließen wird diese Funktion noch exportiert, wie man es bei Systen-Variablen auch macht. Der Parameter -f gibt an, dass es sich dabei um eine Funktion handelt.
Jetzt muss man diese Funktion nur noch bekannt machen. Dazu muss man in der .bashrc einen kleinen Eintrag machen, der die Datei beim Start der Shell in den Speicher lädt, so dass sie künftig ausgeführt werden kann. Ich hab es dabei so gemacht, dass ich einfach den Block kopiert habe, mit dem die .bash_aliases geladen wird. Im Grunde ist es nämlich genau das gleiche und nur eine andere Datei.
if [ -f ~/.bash_functions ]; then
. ~/.bash_functions
fi
Kurz beschrieben: Wenn die Datei existiert, soll sie geladen werden.
Wenn man jetzt eine neue Konsole öffnet, wird die Datei geladen und es steht absofort das Kommando view zur Verfügung und stellt dabei genau das erarbeitete Kommando zur Verfügung.
view sample.md
Das Quellcode-Verzeichnis, das wir aus Github geladen haben, kann man übrigens auch wieder löschen, da das Programm selbst ja mittlerweile installiert ist.
]]>Da ich aber gerade dabei bin, das ich für mein Heimnetzwerk einen neuen Server einrichte - Ich hab mir einen Intel Nuk J5005 geleistet - will ich auch den Nameserver darauf umziehen. Aber… und das ist der Clou daran: Ich möchte alles möglichst kapseln und daher will ich den Nameserver in einem Docker Container laufen lassen.
Dazu steht auch schon ein schönes Docker-Image zur Verfügung: https://hub.docker.com/r/cytopia/bind
Das Image scheint ziemlich weit verbreitet zu sein und ist auch leicht zu konfigurieren.
Da es sich nicht um einen öffentlichen Nameserver handelt, sind die Anforderungen an den Nameserver eher überschaubar.
Das sollte sich eigentlich alles ganz leicht abbilden lassen.
Ich bin dabei streng nach Anleitung vorgegangen und hab nur den Container mit folgendem Kommando gestartet:
docker run -i \
-p 53:53/tcp \
-p 53:53/udp \
-e WILDCARD_DNS='\
nuk.hofmann.lan=192.168.0.39,\
docker.hofmann.lan=192.168.0.39, \
odroid.hofmann.lan=192.168.0.5, \
loki.hofmann.lan=192.168.0.46 \
' \
-e DNS_FORWARDER='192.168.0.1' \
-t cytopia/bind
Das ist jetzt natürlich nicht vollständig, aber an Hand der Beispiele kann man sehr gut sehen, wie die einfache Konfiguration funktioniert. Mit der Umgebungsvariable WILDCARD_DNS legt man die einzelnen Subdomains an. Mit DNS_FORWARDER leite ich den Rest an meine Fritzbox weiter.
Die Ernüchterung kam dann allerdings, als ich das Kommando ausführen wollte:
docker: Error response from daemon: driver failed programming external connectivity on endpoint
priceless_bohr 7f5ef34f4325d7c3581c75e20301af7ab99df45263510462da424aac32507c80): Error
starting userland proxy: listen tcp 0.0.0.0:53: bind: address already in use.
Irgendwie scheint auf dem frisch installierten Ubuntu 18.04 bereits irgendwas an diesem Port zu laufen. Nur was? ich hab da nichts installiert und es wäre mir neu, dass Ubuntu standardmäßig einen Nameserver installiert.
Nach einigem Suchen hab ich den “Übeltäter” auch gefunden. Es handelt sich dabei wirklich um einen Nameserver, nämlich um systemd-resolv, einem lokalen Nameserver. Ich muss zugeben, dass ich davon noch gar nichts wusste. Da ich aber Bind verwenden will, stört mich in diesem Fall dieser Nameserver nur und ich müsste mich da auch erst einmal einlesen, ob ich damit meine Anforderungen nicht vielleicht auch abbilden könnte. Daher wird das Ding jetzt einfach abgeschaltet.
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
Zusätzlich muss man auch noch wissen, dass man auch noch einen Eintrag in der Datei /etc/resolv.conf entfernen muss. Hier wird nämlich auf den lokalen Nameserver verwiesen:
# auskommentiert für bind nameserver
# nameserver 127.0.0.53
search fritz.box
Und jetzt kann es losgehen und wir führen das obige Statement einfach nochmal aus. Das Ergebnis sieht gleich viel besser aus.
[INFO] Debug level: 1
[INFO] Using default DNS TTL time: 3600 sec
[INFO] Using default DNS Refresh time: 1200 sec
[INFO] Using default DNS Retry time: 180 sec
[INFO] Using default DNS Expiry time: 1209600 sec
[INFO] Using default DNS Max Cache time: 10800 sec
[INFO] Adding wildcard DNS: *.nuk.hofmann.lan -> 192.168.0.39
[INFO] Adding wildcard DNS: *.docker.hofmann.lan -> 192.168.0.39
[INFO] Adding wildcard DNS: *.odroid.hofmann.lan -> 192.168.0.5
[INFO] Adding wildcard DNS: *.loki.hofmann.lan -> 192.168.0.8
[INFO] Not adding any extra hosts
[INFO] DNSSEC Validation: no
[INFO] Adding custom DNS forwarder: 192.168.0.1
[INFO] Starting BIND 9.11.4
Testen wir noch kurz, ob das alles auch so funktioniert:
~$ ping odroid.hofmann.lan
PING odroid.hofmann.lan (192.168.178.5) 56(84) bytes of data.
64 bytes from 192.168.178.5 (192.168.178.5): icmp_seq=1 ttl=64 time=0.593 ms
~$ ping google.de
PING google.de(fra15s29-in-x03.1e100.net (2a00:1450:4001:806::2003)) 56 data bytes
64 bytes from fra15s29-in-x03.1e100.net (2a00:1450:4001:806::2003): icmp_seq=1 ttl=56 time=20.8 ms
Also sowohl im lokalen Netzwerk also auch in Internet funktioniert die Namensauflösung. Daraus folgt: Test erfolgreich
Jetzt brauchen wir nur noch ein paar Schönheitskorrekturen. Aktuell ist es so, dass der Container manuell gestartet werden muss und durch den Parameter -i auch noch eine offene Konsole benötigt.
Dazu ändern wir den Parameter -i (interaktiv) zu -d (daemon).
Für den Neustart verwendet man die Docker Restart Policy mit dem Parameter –restart always.
Eine weitere Sache will ich auch noch geändert haben: Der Container soll einen vernünftigen Namen haben. Standardmäßig erzeugt Docker immer zufällige Namen, wenn ein Container gestartet wird. Ich persönlich finde das aber etwas unübersichtlich, daher möchte ich haben, dass der Container künftig auf den Namen bind hört. Dazu gibt es den Parameter –name mit dem man dann einen namen angeben kann.
Das geänderte Statement zum Anlegen/Starten des Containers sieht jetzt folgendermaßen aus:
docker run -d \
--restart always \
--name bind \
-p 53:53/tcp \
-p 53:53/udp \
-e WILDCARD_DNS='\
nuk.hofmann.lan=192.168.0.39,\
docker.hofmann.lan=192.168.0.39, \
odroid.hofmann.lan=192.168.0.5, \
loki.hofmann.lan=192.168.0.46 \
' \
-e DNS_FORWARDER='192.168.0.1' \
-t cytopia/bind
Fangen wir mit dem Angular-Projekt an.
Einigen ist sicherlich schon aufgefallen, dass beim Anlegen eines Angular-Projekts auch eine Datei .gitignore angelegt wird. Darin wird vom Framework her automatisch alles eingetragen, das nicht an git gepusht werden soll. Da muss man sich also nicht weiter darum kümmern.
Letztendlich muss man also das lokale Verzeichnis lediglich mit dem Server-Repository verbinden und das geht folgendermaßen:
(ich schreib jetzt zwecks Nachvollziehbarkeit auch das Erstellen des Angular-Projekts dazu)
ng new angular-test
cd angular-test
git remote add origin git@<server-ip>:angular-test
Damit ist die Verbindung erst einmal hergestellt. Ob das auch wirklich stimmt, kann man mit folgendem Kommando ausprobieren:
git remote -v
Damit bekommt man folgende Ausgabe:
origin git@<server-ip>:angular-test (fetch)
origin git@<server-ip>:angular-test (push)
Letztendlich muss man das Ganze dann nur noch auf den Server pushen:
git push origin master
Zur Erklärung der Parameter erlaube ich mir jetzt mal einen Spaß, den man vielleicht auch mal produktiv brauchen kann. Wir legen einfach ein zweites Repository auf dem Server an und fügen zum aktuellen Projekt einen zweiten Remote hinzu. Und dann pushen wir das Ganze in ein anderen Repository. Dazu verwenden wir folgende Kommandos:
git remote add origin2 git@<server-ip>:angular-test2
git push origin2 master
Was haben wir jetzt hier gemacht? Wir haben die ganze Arbeitskopie mit einem zweiten Repository verknüpft. Die beiden Repositories kann ich mit den Namen origin bzw origin2 ansprechen. Das kann man auch überprüfen, auf welche Repositories mit welchem Namen zugegriffen wird:
git remote -v
-->
origin git@<server-ip>:angular-test (fetch)
origin git@<server-ip>:angular-test (push)
origin2 git@<server-ip>:angular-test2 (fetch)
origin2 git@<server-ip>:angular-test2 (push)
So… Und was heißt master? Einfach erklärt kann man sagen, dass das der Name des Branches ist, den man einchecken will. Bei einem neu angelegten Projekt arbeitet man logischerweise im Master-Branch, da es ja sonst noch keinen Branch gibt. Wenn man später einen Branch angelegt hat und diesen einchecken will, sollte man den hier entsprechend verwenden.
Probieren wir das Ganze jetzt gleich mal mit unserem dotnetr-Projekt aus. Dummerweise werden wir bereits beim ersten Kommando (git remote add…) feststellen, dass es nicht so funktioniert. Man bekommt nämlich die folgende Fehlermeldung:
fatal: Kein Git-Repository (oder irgendeines der Elternverzeichnisse): .git
Man muss also erst noch irgendwas machen, um dem System zu sagen, dass das Ganze ein git-Arbeitsverzeichnis werden soll. Dazu gibt es folgendes Kommando:
git init
Aber auch damit alleine klappt es noch nicht. Man muss nämlich auch noch die Dateien hinzufügen und committen. Erst dann kann man sie zum Server pushen.
git add --all
git commit -m 'initial version'
git push origin master
Jetzt wird alles zum git-Server gesendet… Und zwar wirklich alles. Auch die Ordner bin und obj und alles andere, was sich irgendwie in diesem Verzeichnis befindet und was nicht unbedingt was in der Versionsverwaltung zu suchen hat.
Jetzt kommt das, was ich oben schon geschrieben habe: Ein Angular-Projekt hat von Haus aus ein gitignore-File. Das brauchen wir jetzt auch bei unserem dotnet-Projekt, aber hier müssen wir es uns selbst anlegen. Es gibt einige Vorschläge, wie ein solchen File auszusehen hat. Ich habe hier folgendes Beispiel:
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
# Visual Studio Code
.vscode
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio
.vs/
Diese Datei liegen wir also jetzt manuell an. Zusätzlich löschen wir die bereits falsch eingecheckten Verzeichnisse und Dateien. In meinem Fall sind das die Ordner bin und obj, da ich lediglich ein leeres Consolen-Projekt erstellt habe. Und dann committen und pushen wir das Ganze wieder zum Server:
git add --all
git commit -m 'added .gitignore and removed bin and obj folder'
git push origin master
Jetzt haben wir alles fertig und können das Projekt auch an einem anderen Rechner (oder Ordner) wieder auschecken und daran weiter arbeiten.
Ein kleiner Tipp noch:
Damit man nicht immer origin master mitangeben muss, kann man den upstream auch entsprechend setzen, damit immer diese Vorgabe verwendet wird:
git push --set-upstream origin master
Damit spart man sich ein bisschen Tiparbeit und muss sich nicht ganz soviel merken.
]]>