Compare commits
No commits in common. "main" and "v0.1.1" have entirely different histories.
100 changed files with 612 additions and 5642 deletions
|
@ -1,11 +1,11 @@
|
||||||
root = "."
|
root = "./src"
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "go build -o ./tmp/main ./src/."
|
cmd = "go build -o ./tmp/main ./src/."
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["tmp", "vendor", "ui/dist", "ui/node_modules", "ui/src"]
|
exclude_dir = ["assets", "tmp", "vendor"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = []
|
exclude_regex = []
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
||||||
build
|
build
|
||||||
tmp
|
tmp
|
||||||
/sebrauc.toml
|
|
||||||
/htpasswd
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
linters:
|
linters:
|
||||||
presets:
|
presets:
|
||||||
- bugs
|
- bugs
|
||||||
|
- unused
|
||||||
- import
|
- import
|
||||||
- module
|
- module
|
||||||
|
|
||||||
|
@ -13,17 +14,11 @@ linters:
|
||||||
|
|
||||||
disable:
|
disable:
|
||||||
- scopelint
|
- scopelint
|
||||||
- noctx
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
lll:
|
lll:
|
||||||
line-length: 88
|
line-length: 88
|
||||||
tab-width: 4
|
|
||||||
gocognit:
|
gocognit:
|
||||||
min-complexity: 10
|
min-complexity: 10
|
||||||
nestif:
|
nestif:
|
||||||
min-complexity: 3
|
min-complexity: 3
|
||||||
errcheck:
|
|
||||||
exclude-functions:
|
|
||||||
- "(*github.com/gin-gonic/gin.Context).Error"
|
|
||||||
- "(*github.com/gin-gonic/gin.Context).AbortWithError"
|
|
||||||
|
|
|
@ -6,7 +6,10 @@ repos:
|
||||||
name: GolangCI Lint
|
name: GolangCI Lint
|
||||||
- id: go-test-repo-mod
|
- id: go-test-repo-mod
|
||||||
name: Backend tests
|
name: Backend tests
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v2.4.1
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: tsc
|
- id: tsc
|
||||||
|
@ -17,8 +20,3 @@ repos:
|
||||||
args: ["-p", "./ui/tsconfig.json"]
|
args: ["-p", "./ui/tsconfig.json"]
|
||||||
additional_dependencies: ["typescript@4.5.2"]
|
additional_dependencies: ["typescript@4.5.2"]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
||||||
rev: v2.4.1
|
|
||||||
hooks:
|
|
||||||
- id: prettier
|
|
||||||
|
|
|
@ -1,4 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
tmp
|
|
||||||
.tmp
|
|
|
@ -1,14 +0,0 @@
|
||||||
pipeline:
|
|
||||||
frontend:
|
|
||||||
image: node:16-alpine
|
|
||||||
commands:
|
|
||||||
- cd ui
|
|
||||||
- npm install -g pnpm
|
|
||||||
- pnpm install
|
|
||||||
- pnpm run build
|
|
||||||
backend:
|
|
||||||
image: golangci/golangci-lint:latest
|
|
||||||
commands:
|
|
||||||
- go get -t ./src/...
|
|
||||||
- golangci-lint run --timeout 5m
|
|
||||||
- go test -v ./src/...
|
|
24
Makefile
24
Makefile
|
@ -1,35 +1,19 @@
|
||||||
SRC_DIR=./src
|
SRC_DIR=./src
|
||||||
UI_DIR=./ui
|
UI_DIR=./ui
|
||||||
|
|
||||||
APIDOC_FILE=./src/server/swagger/swagger.yaml
|
VERSION=$(shell git tag --sort=-version:refname | head -n 1)
|
||||||
|
|
||||||
VER=$(or ${VERSION},$(shell git tag --sort=-version:refname | head -n 1))
|
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
|
go get -t ./src/...
|
||||||
cd ${UI_DIR} && pnpm install
|
cd ${UI_DIR} && pnpm install
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -v ./src/...
|
go test -v ./src/...
|
||||||
|
|
||||||
lint:
|
|
||||||
golangci-lint run
|
|
||||||
cd ${UI_DIR} && npm run format && npm run lint
|
|
||||||
|
|
||||||
build-ui:
|
build-ui:
|
||||||
cd ${UI_DIR} && pnpm run build
|
cd ${UI_DIR} && VITE_VERSION=${VERSION} pnpm run build
|
||||||
|
|
||||||
build-server:
|
build-server:
|
||||||
go build -tags prod -ldflags "-s -w -X code.thetadev.de/TSGRain/SEBRAUC/src/util.version=${VER}" -o build/sebrauc ./src/.
|
go build -tags prod -ldflags "-s -w -X code.thetadev.de/TSGRain/SEBRAUC/src/util.version=${VERSION}" -o build/sebrauc ./src/.
|
||||||
|
|
||||||
build: build-ui build-server
|
build: build-ui build-server
|
||||||
|
|
||||||
generate-apidoc:
|
|
||||||
SWAGGER_GENERATE_EXTENSION=false swagger generate spec --scan-models -o ${APIDOC_FILE}
|
|
||||||
|
|
||||||
generate-apiclient:
|
|
||||||
openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true"
|
|
||||||
cd ${UI_DIR} && npm run format
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f build/*
|
|
||||||
rm -rf ${UI_DIR}/dist/**
|
|
||||||
|
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|

|
460
README.rst
460
README.rst
|
@ -1,460 +0,0 @@
|
||||||
.. image:: ui/src/assets/logo_border.svg
|
|
||||||
|
|
||||||
SEBRAUC ist eine einfach zu bedienende Weboberfläche für den
|
|
||||||
`RAUC <https://github.com/rauc/rauc/>`__ Firmware-Updater, die es
|
|
||||||
erlaubt, Softwareupdates auf Embedded-Systemen wie dem Raspberry Pi zu
|
|
||||||
installieren.
|
|
||||||
|
|
||||||
Ich habe die Anwendung für die TSGRain-Bewässerungssteuerung entwickelt,
|
|
||||||
da es noch keine einfache Möglichkeit gab, RAUC-Updates über eine
|
|
||||||
Weboberfläche zu installieren.
|
|
||||||
|
|
||||||
Der Updatevorgang funktioniert ähnlich wie bei älteren
|
|
||||||
WLAN-Routern. Das ``*.raucb``-Firmwarepaket kann über den Browser an das
|
|
||||||
Gerät übertragen und installiert werden. Im Gegensatz zu OTA-basierten
|
|
||||||
Lösungen wie `hawkBit <https://github.com/rauc/rauc-hawkbit-updater>`__
|
|
||||||
wird keine Internetverbindung, sondern nur ein lokaler WLAN-Access-Point
|
|
||||||
für die Datenübertragung benötigt. Das macht SEBRAUC ideal für fest
|
|
||||||
installierte Geräte an abgelegenen Orten wie unser
|
|
||||||
TSGRain-Bewässerungssystem.
|
|
||||||
|
|
||||||
Funktion
|
|
||||||
########
|
|
||||||
|
|
||||||
Startseite
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. image:: _screenshots/update0.png
|
|
||||||
:width: 500
|
|
||||||
|
|
||||||
Auf der Startseite findet der Nutzer einen großen Upload-Button, mit dem
|
|
||||||
er ein ``*.raucb``-Image auswählen und hochladen kann.
|
|
||||||
|
|
||||||
Update hochladen
|
|
||||||
================
|
|
||||||
|
|
||||||
.. image:: _screenshots/update1.png
|
|
||||||
:width: 500
|
|
||||||
|
|
||||||
Wurde eine Update-Datei ausgewählt, wird sie auf den Server
|
|
||||||
hochgeladen und zunächst temporär im Ordner ``/tmp/sebrauc`` gespeichert.
|
|
||||||
Dabei wird der Uploadfortschritt mit einer kreisförmigen Skala
|
|
||||||
angezeigt.
|
|
||||||
|
|
||||||
Update installieren
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. image:: _screenshots/update2.png
|
|
||||||
:width: 500
|
|
||||||
|
|
||||||
Ist der Uplodad der Datei abgeschlossen, beginnt die Installation.
|
|
||||||
SEBRAUC startet den RAUC-Updater mit dem entsprechenden
|
|
||||||
Kommandozeilenbefehl.
|
|
||||||
|
|
||||||
Während des Updatevorgangs liefert RAUC folgende Konsolenausgabe:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
installing
|
|
||||||
0% Installing
|
|
||||||
0% Determining slot states
|
|
||||||
20% Determining slot states done.
|
|
||||||
20% Checking bundle
|
|
||||||
20% Verifying signature
|
|
||||||
40% Verifying signature done.
|
|
||||||
40% Checking bundle done.
|
|
||||||
40% Checking manifest contents
|
|
||||||
60% Checking manifest contents done.
|
|
||||||
60% Determining target install group
|
|
||||||
80% Determining target install group done.
|
|
||||||
80% Updating slots
|
|
||||||
80% Checking slot rootfs.0
|
|
||||||
90% Checking slot rootfs.0 done.
|
|
||||||
90% Copying image to rootfs.0
|
|
||||||
100% Copying image to rootfs.0 done.
|
|
||||||
100% Updating slots done.
|
|
||||||
100% Installing done.
|
|
||||||
idle
|
|
||||||
Installing `/app/tsgrain-update-raspberrypi3.raucb` succeeded
|
|
||||||
|
|
||||||
Aus dieser Konsolenausgabe lässt sich der aktuelle Fortschritt in
|
|
||||||
Prozent und der Status ermitteln. SEBRAUC leitet diese Daten
|
|
||||||
in Echtzeit an die Weboberfläche weiter. So kann der Nutzer live
|
|
||||||
den Updatevorgang mitverfolgen.
|
|
||||||
|
|
||||||
Wenn RAUC eine Fehlermeldung ausgibt, wird diese mit einem roten
|
|
||||||
Warndreieck am unteren Bildschirmrand angezeigt.
|
|
||||||
|
|
||||||
|
|
||||||
Installation abgeschlossen
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. image:: _screenshots/update3.png
|
|
||||||
:width: 500
|
|
||||||
|
|
||||||
Ist die Installation abgeschlossen, kehrt SEBRAUC zum Startbildschirm
|
|
||||||
zurück. Am unteren Rand erhält der Nutzer den Hinweis, dass das System
|
|
||||||
neu gestartet werden muss, um die neue Software in Betrieb zu nehmen.
|
|
||||||
Drückt man auf den :guilabel:`Restart`-Button, startet das System mit
|
|
||||||
einer Verzögerung von 3 Sekunden neu.
|
|
||||||
|
|
||||||
Systeminformationen
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. image:: _screenshots/sysinfo.png
|
|
||||||
:width: 500
|
|
||||||
|
|
||||||
Mit der Schaltfläche oben rechts lässt sich ein Informationsbildschirm
|
|
||||||
einblenden, der einige Statusinformationen über das System und den
|
|
||||||
RAUC-Updater anzeigt.
|
|
||||||
So lässt sich in Erfahrung bringen, welches Betriebssystem in welcher Version
|
|
||||||
gerade ausgeführt wird und welche der 2 Root-Partitionen gerade aktiv ist.
|
|
||||||
|
|
||||||
Der RAUC-Updater hat eine Konfigurationsoption, mit der festgelegt werden
|
|
||||||
kann, welche Firmware und Firmware-Variante akzeptiert wird. Diese Werte
|
|
||||||
werden ebenfalls auf dem Systeminformationsbildschirm angezeigt,
|
|
||||||
damit der Nutzer sehen kann, welche Firmware-Images mit seinem Gerät
|
|
||||||
kompatibel sind.
|
|
||||||
|
|
||||||
|
|
||||||
Technische Details
|
|
||||||
##################
|
|
||||||
|
|
||||||
SEBRAUC besteht aus zwei Komponenten: dem in Go geschriebenen Backend
|
|
||||||
und einer mit Preact und Typescript umgesetzten Single-Page-Weboberfläche.
|
|
||||||
|
|
||||||
Das Backend steuert den RAUC-Updater und stellt die Web-API bereit, die vom
|
|
||||||
Frontend angesprochen wird. Ich habe für das Backend das
|
|
||||||
`Gin-Webframework <https://github.com/gin-gonic/gin>`_ verwendet. Hierbei
|
|
||||||
handelt es sich um ein minimales und performantes Framework in der Programmiersprache
|
|
||||||
Go, vergleichbar mit Flask für Python oder Express für
|
|
||||||
Javascript. Es erlaubt die einfache Erstellung von Routen und Handlern
|
|
||||||
für Webanfragen, bietet aber im Gegensatz zu größeren Frameworks
|
|
||||||
wie Django keine eingebaute Datenbankintegration oder Administrationsoberfläche.
|
|
||||||
|
|
||||||
Eine asynchrone Kommunikation vom Server zum Client mittels Websockets
|
|
||||||
kann mit dem Modul ``github.com/gorilla/websocket`` leicht in eine Gin-Anwendung
|
|
||||||
integriert werden.
|
|
||||||
|
|
||||||
SEBRAUC verwendet eine solche Websocket-Verbindung, um den aktuellen Zustand
|
|
||||||
des Systems an das Frontend zu streamen. Dadurch erhält der Nutzer immer die aktuellen
|
|
||||||
Informationen, ohne dass die Seite neu aufgerufen werden oder die Daten
|
|
||||||
durch das Frontend in bestimmten Zeitabständen aktualisiert werden müssen.
|
|
||||||
|
|
||||||
Beim Frontend kam das Javascript-Framework `Preact <https://preactjs.com/>`_
|
|
||||||
zum Einsatz, was eine kleinere und schnellere Alternative zu React darstellt.
|
|
||||||
|
|
||||||
Da Preact in komprimierter Form nur 3kB groß ist, eignet es sich perfekt
|
|
||||||
für kleine Webanwendungen, die schnell laden sollen. So ist die fertige
|
|
||||||
SEBRAUC-Webanwendung inklusive Styles und Icons nur 22kB groß. Hätte ich hierfür
|
|
||||||
React oder Vue verwendet, würde die Webseite mindestens doppelt so groß sein.
|
|
||||||
|
|
||||||
Der Autor Ryan Carniato hat in
|
|
||||||
`diesem Blogpost <https://dev.to/this-is-learning/javascript-framework-todomvc-size-comparison-504f>`_
|
|
||||||
verschiedene Webframeworks, darunter React, Vue und Preact miteinander verglichen.
|
|
||||||
Dafür hat er eine simple Todo-Liste mit allen Frameworks gebaut und
|
|
||||||
die Größe des generierten Javascript-Codes verglichen. Laut diesem Vergleich ist
|
|
||||||
die React-Anwendung mit 37.5kB fast siebenmal so groß wie die nur 5.6kB große Preact-Anwendung.
|
|
||||||
|
|
||||||
Um auch das Stylesheet der Anwendung kompakt zu halten, wurde auf fertige
|
|
||||||
CSS-Komponentenbibliotheken wie Bootstrap verzichtet und stattdessen
|
|
||||||
selbstgeschriebenes scss verwendet.
|
|
||||||
|
|
||||||
|
|
||||||
REST-API
|
|
||||||
========
|
|
||||||
|
|
||||||
Die Weboberfläche kommuniziert über REST-API-Anfragen mit dem Server.
|
|
||||||
|
|
||||||
Die API ist im OpenAPI 2.0-Format dokumentiert (die yaml-Dokumentation
|
|
||||||
befindet sich unter ``src/server/swagger/swagger.yaml``).
|
|
||||||
|
|
||||||
OpenAPI ist ein Standard für maschinenlesbare API-Dokumentationen,
|
|
||||||
aus denen mit geeigneten Tools menschenlesbare Dokumentationsseiten
|
|
||||||
sowie fertige API-Clients in verschiedenen Programmiersprachen
|
|
||||||
erzeugen lassen.
|
|
||||||
|
|
||||||
In diesem Projekt verwende ich ein Tool (``go-swagger``),
|
|
||||||
das Kommentare und Structs aus dem Programmcode einlesen und daraus die
|
|
||||||
OpenAPI-Dokumentation generieren kann. Daraus wird dann der Code für den
|
|
||||||
Typescript-Client für die Weboberfläche generiert.
|
|
||||||
|
|
||||||
|
|
||||||
Konfiguration
|
|
||||||
#############
|
|
||||||
|
|
||||||
Die Konfiguration erfolgt über eine Datei im ``*.json``, ``*.yaml`` oder
|
|
||||||
``*.toml``-Format. Eine Beispielkonfiguration findet sich
|
|
||||||
`hier <https://code.thetadev.de/TSGRain/SEBRAUC/src/branch/main/sebrauc.example.toml>`__.
|
|
||||||
|
|
||||||
Alternativ kann die Konfiguration auch mittels Umgebungsvariablen angepasst
|
|
||||||
werden, wobei verschachtelte Datenstrukturen mit Unterstrichen
|
|
||||||
addressiert werden. Beispiel:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
SERVER_ADDRESS=80 SERVER_COMPRESSION_BROTLI=false ./sebrauc
|
|
||||||
|
|
||||||
SEBRAUC sucht zuerst nach einer Konfigurationsdatei mit dem Namen
|
|
||||||
``sebrauc.{json,yml,yaml,toml}`` im aktuellen Arbeitsverzeichnis und
|
|
||||||
anschließend im Ordner ``/etc/sebrauc``. Der Pfad zur
|
|
||||||
Konfigurationsdatei kann auch manuell mit dem Flag ``-c`` angegeben
|
|
||||||
werden.
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:widths: 35 50 35
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - Option
|
|
||||||
- Beschreibung
|
|
||||||
- Standard
|
|
||||||
* - ``Tmpdir``
|
|
||||||
- Temporäres Verzeichnis
|
|
||||||
- ``"/tmp/sebrauc"``
|
|
||||||
* - ``Server.Address``
|
|
||||||
- IP-Adresse, von der der Server Verbindungen
|
|
||||||
|
|
||||||
akzeptiert. Leer lassen, um Verbindungen von
|
|
||||||
|
|
||||||
allen Adressen zu akzeptieren.
|
|
||||||
- ``""``
|
|
||||||
* - ``Server.Port``
|
|
||||||
- Server-Port
|
|
||||||
- ``80``
|
|
||||||
* - ``Server.Websocket.Ping``
|
|
||||||
- Zeitabstand, in denen ein Ping vom
|
|
||||||
|
|
||||||
Websocket-Server an den Client erfolgt.
|
|
||||||
- ``45``
|
|
||||||
* - ``Server.Websocket.Timeout``
|
|
||||||
- Timeout in Sekunden, nachdem der Server
|
|
||||||
|
|
||||||
eine Websocket-Verbindung ohne Antwort trennt.
|
|
||||||
- ``45``
|
|
||||||
* - ``Server.Compression.Gzip``
|
|
||||||
- Webseiteninhalte mit Gzip komprimieren,
|
|
||||||
|
|
||||||
wenn vom Browser unterstützt.
|
|
||||||
|
|
||||||
Optionen: ``"false"``, ``"default"``,
|
|
||||||
|
|
||||||
``"min"``, ``"max"``, ``"1"``-``"9"``
|
|
||||||
- ``"default"``
|
|
||||||
* - ``Server.Compression.Brotli``
|
|
||||||
- Webseiteninhalte mit Brotli komprimieren,
|
|
||||||
|
|
||||||
wenn vom Browser unterstützt.
|
|
||||||
|
|
||||||
Optionen: ``"false"``, ``"default"``,
|
|
||||||
|
|
||||||
``"min"``, ``"max"``, ``"1"``-``"11"``
|
|
||||||
- ``"default"``
|
|
||||||
* - ``Authentication.Enable``
|
|
||||||
- HTTP Basic-Authentifizierung
|
|
||||||
|
|
||||||
(Passwortabfrage im Browser) aktivieren.
|
|
||||||
- ``false``
|
|
||||||
* - ``Authentication.PasswdFile``
|
|
||||||
- Apache-kompatible Passwortdatei.
|
|
||||||
|
|
||||||
Suche nach einer Passwortdatei
|
|
||||||
|
|
||||||
unter `./htpasswd` oder `/etc/sebrauc/htpasswd`,
|
|
||||||
|
|
||||||
wenn leergelassen.
|
|
||||||
- ``""``
|
|
||||||
* - ``Sysinfo.ReleaseFile``
|
|
||||||
- Datei, aus der OS-Informationen gelesen werden.
|
|
||||||
- ``"/etc/os-release"``
|
|
||||||
* - ``Sysinfo.NameKey``
|
|
||||||
- Schlüssel für den Namen des Betriebssystems
|
|
||||||
|
|
||||||
aus der ReleaseFile.
|
|
||||||
- ``"NAME"``
|
|
||||||
* - ``Sysinfo.VersionKey``
|
|
||||||
- Schlüssel für die Version des Betriebssystems
|
|
||||||
|
|
||||||
aus der ReleaseFile.
|
|
||||||
- ``"VERSION"``
|
|
||||||
* - ``Sysinfo.HostnameFile``
|
|
||||||
- Datei, aus der der Hostname gelesen wird.
|
|
||||||
- ``"/etc/hostname"``
|
|
||||||
* - ``Sysinfo.UptimeFile``
|
|
||||||
- Datei, aus der die Betriebszeit des Systems
|
|
||||||
|
|
||||||
gelesen wird.
|
|
||||||
- ``"/proc/uptime"``
|
|
||||||
* - ``Commands.RaucStatus``
|
|
||||||
- RAUC-Befehl, um den Status im JSON-Format
|
|
||||||
|
|
||||||
auszulesen.
|
|
||||||
- ``"rauc status``
|
|
||||||
|
|
||||||
``--output-format=json"``
|
|
||||||
* - ``Commands.RaucInstall``
|
|
||||||
- RAUC-Befehl, um ein Update-Paket zu installieren.
|
|
||||||
- ``"rauc install"``
|
|
||||||
* - ``Commands.Reboot``
|
|
||||||
- Befehl, um das System neu zu starten.
|
|
||||||
- ``"shutdown -r 0"``
|
|
||||||
|
|
||||||
|
|
||||||
Passwortdatei
|
|
||||||
=============
|
|
||||||
|
|
||||||
Ist die Authentifizierung aktiviert, liest SEBRAUC Nutzernamen und
|
|
||||||
Passwörter aus Apache-kompatiblen Passwortdateien (standardmäßig unter
|
|
||||||
``./htpasswd`` oder ``/etc/sebrauc/htpasswd``).
|
|
||||||
|
|
||||||
Die Passwörter können im Klartext oder als Hashes vorliegen.
|
|
||||||
Gehashte Passwörter lassen sich mit mit dem Befehl ``htpasswd -nB username``
|
|
||||||
erzeugen. Hierfür wird unter Linux das Paket ``apache-tools`` benötigt.
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
# Klartextpasswort (nur zum Testen verwenden)
|
|
||||||
username:password
|
|
||||||
|
|
||||||
# Gehashtes Passwort
|
|
||||||
test:$2y$05$EzzPOZprUhPE.1ru1gM8du0ZNpmsU40EFDZ1PmzZtBzkMHsJVK1au
|
|
||||||
|
|
||||||
|
|
||||||
Entwicklung und Build
|
|
||||||
#####################
|
|
||||||
|
|
||||||
Systemvoraussetzungen
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Um SEBRAUC zu kompilieren werden folgende Programme benötigt:
|
|
||||||
|
|
||||||
- Golang (Version 1.16 oder neuer)
|
|
||||||
- NodeJS
|
|
||||||
- PNPM-Paketmanager (installieren mit ``npm i -g pnpm``)
|
|
||||||
|
|
||||||
Zur Entwicklung werden zusätzlich folgende Tools verwendet:
|
|
||||||
|
|
||||||
- `golangci-lint <https://golangci-lint.run/>`_ (Überprüft Go-Code auf
|
|
||||||
Formatierung, Code Style und bestimmete Fehler)
|
|
||||||
- `air <https://github.com/cosmtrek/air>`_ (Kompiliert und startet
|
|
||||||
Go-Anwendungen bei Codeänderungen neu)
|
|
||||||
- `go-swagger <https://github.com/go-swagger/go-swagger>`_
|
|
||||||
(Generierung einer OpenAPI-Dokumentation aus Go-Servercode, der nach
|
|
||||||
einem bestimmten Schema kommentiert wurde)
|
|
||||||
- `openapi-generator <https://openapi-generator.tech/docs/installation>`_
|
|
||||||
(Erzeugt fertige API-Clients aus OpenAPI-Dokumentationen)
|
|
||||||
|
|
||||||
Befehle
|
|
||||||
=======
|
|
||||||
|
|
||||||
Das Projekt enthät eine Makefile, mit der sich oft benötigte Befehle
|
|
||||||
schnell ausführen lassen.
|
|
||||||
|
|
||||||
``make setup``
|
|
||||||
Dieser Befehl muss vor dem Arbeiten an dem Projekt ausgeführt werden.
|
|
||||||
Hiermit werden sämtliche Abhängigkeiten für das Frontend
|
|
||||||
|
|
||||||
``make lint``
|
|
||||||
Überprüfe den Backend-Code mit ``golangci-lint`` auf Fehler und schlechte
|
|
||||||
Programmierpraktiken.
|
|
||||||
Zudem wird der Typescript-Code im Frontend mit dem Typescript-Compiler
|
|
||||||
auf Typfehler geprüft.
|
|
||||||
|
|
||||||
``make build``
|
|
||||||
Baue die Produktionsversion der Software.
|
|
||||||
Die Softwareversion, die in die Software einkompiliert wird,
|
|
||||||
wird aus dem aktuellsten Git-Tag ermittelt.
|
|
||||||
|
|
||||||
``make test``
|
|
||||||
Führt die Tests für das Backend aus.
|
|
||||||
|
|
||||||
``make generate-apidoc``
|
|
||||||
Liest die API-Dokumentation aus den Kommentaren im Quellcode ein
|
|
||||||
und produziert daraus eine Swagger-Dokumentationsdatei im yaml-Format.
|
|
||||||
|
|
||||||
``make generate-apiclient``
|
|
||||||
Erzeugt den Typescript-API-Client für das Frontend aus der Swagger
|
|
||||||
API-Dokumentation. Um die API-Dokumentation zu aktualisieren, muss
|
|
||||||
vorher ``make generate-apidoc`` ausgeführt werden.
|
|
||||||
|
|
||||||
|
|
||||||
Um die SEBRAUC-Software zu bauen, müssen nach dem Herunterladen des Repositorys
|
|
||||||
folgende Befehle ausgeführt werden:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
# Abhängigkeiten installieren
|
|
||||||
make setup
|
|
||||||
|
|
||||||
# Build für die CPU-Architektur des laufenden Systems
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Build für andere CPU-Architekturen (Cross-Compile)
|
|
||||||
GOARCH=arm make build
|
|
||||||
|
|
||||||
Die kompilierte Anwendung befindet wird im Ordner ``build``
|
|
||||||
abgelegt.
|
|
||||||
|
|
||||||
|
|
||||||
RAUC-Mock
|
|
||||||
=========
|
|
||||||
|
|
||||||
Um SEBRAUC testweise auf einem Entwicklerrechner ohne den RAUC-Updater
|
|
||||||
laufen zu lassen, habe ich ein kleines Go-Programm geschrieben, das
|
|
||||||
die Konsolenausgabe von RAUC simuliert.
|
|
||||||
|
|
||||||
Man kann dieses Programm anstelle der echten RAUC-Befehle in die
|
|
||||||
Konfigurationsdatei eintragen.
|
|
||||||
|
|
||||||
.. code-block:: toml
|
|
||||||
|
|
||||||
# Commands to be run by SEBRAUC
|
|
||||||
[Commands]
|
|
||||||
# RAUC status command (outputs updater status in json format)
|
|
||||||
RaucStatus = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json"
|
|
||||||
# RAUC install command (installs FW image passed as argument)
|
|
||||||
RaucInstall = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install"
|
|
||||||
|
|
||||||
Wird SEBRAUC im Testmodus (d.h. ohne das Build-Flag ``-tags prod``) gebaut,
|
|
||||||
führt das Programm stets den Mock-RAUC-Befehl aus und ignoriert die konfigurierten
|
|
||||||
Befehle. Ob sich SEBRAUC im Testmodus befindet, lässt sich an der Hinweisnachricht
|
|
||||||
beim Start erkennen.
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
Test mode active - no update operations are executed.
|
|
||||||
Build with -tags prod to enable live mode.
|
|
||||||
|
|
||||||
Das Verhalten von ``rauc_mock`` kann mittels Umgebungsvariablen angepasst werden.
|
|
||||||
Ist die Variable ``RAUC_MOCK_FAIL`` gesetzt, wird eine fehlerhafte Installation
|
|
||||||
simuliert. Mit dem Setzen der Variable ``RAUC_MOCK_TEST`` reduziert man die Verzögerung,
|
|
||||||
mit der die Nachrichten angezeigt werden, von 500 auf 10 Millisekunden. Diese Option
|
|
||||||
dient dazu, automatische Tests zu beschleunigen, während die Wartezeit bei manuellen
|
|
||||||
Tests lang genug ist, um sämtliche Statusnachrichten lesen zu können.
|
|
||||||
|
|
||||||
|
|
||||||
Auto-Reload des Backends
|
|
||||||
========================
|
|
||||||
|
|
||||||
Wenn man am Backend-Code arbeitet und die Webseite oder API dabei gleichzeitig
|
|
||||||
testen möchte, hat man mit kompilierten Sprachen wie Go das Problem, dass sich
|
|
||||||
Codeänderungen nicht sofort auf das Verhalten der Anwendung auswirken.
|
|
||||||
Erst nach einer Rekompilierung und einem Neustart des Servers ist eine
|
|
||||||
Änderung wirksam.
|
|
||||||
|
|
||||||
Glücklicherweise gibt es Tools wie ``air``, die die Quellcodedateien überwachen
|
|
||||||
und die Anwendung bei Änderung automatisch kompilieren und neu starten.
|
|
||||||
|
|
||||||
Um den SEBRAUC-Server mit Auto-Reload zu starten,
|
|
||||||
muss man einfach den Befehl ``air`` im Stammverzeichnis des Projekts ausführen
|
|
||||||
|
|
||||||
|
|
||||||
Frontend
|
|
||||||
========
|
|
||||||
|
|
||||||
Um den Entwicklungsserver für das Frontend zu starten, navigiert man in das
|
|
||||||
``ui``-Verzeichnis und startet den Server mit dem Befehl ``npm run dev``.
|
|
||||||
|
|
||||||
Im Entwicklungsmodus kommuniziert das Frontend mit dem Backend unter der
|
|
||||||
Adresse ``localhost:8080``. Man kann also parallel SEBRAUC mit entsprechender
|
|
||||||
Konfiguration (Port: 8080) starten, um das Frontend mit einem funktionsfähigen
|
|
||||||
Server zu testen.
|
|
Binary file not shown.
Before Width: | Height: | Size: 155 KiB |
Binary file not shown.
Before Width: | Height: | Size: 101 KiB |
Binary file not shown.
Before Width: | Height: | Size: 127 KiB |
Binary file not shown.
Before Width: | Height: | Size: 126 KiB |
Binary file not shown.
Before Width: | Height: | Size: 119 KiB |
25
go.mod
25
go.mod
|
@ -3,29 +3,10 @@ module code.thetadev.de/TSGRain/SEBRAUC
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.thetadev.de/TSGRain/ginzip v0.1.1
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db
|
github.com/gofiber/fiber/v2 v2.21.0
|
||||||
github.com/fortytw2/leaktest v1.3.0
|
github.com/gofiber/websocket/v2 v2.0.12
|
||||||
github.com/gin-contrib/cors v1.3.1
|
|
||||||
github.com/gin-gonic/gin v1.7.7
|
|
||||||
github.com/go-errors/errors v1.4.1 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
|
||||||
github.com/google/go-cmp v0.5.6 // indirect
|
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/websocket v1.4.1
|
|
||||||
github.com/jinzhu/configor v1.2.1
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/tg123/go-htpasswd v1.2.0
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
|
138
go.sum
138
go.sum
|
@ -1,128 +1,48 @@
|
||||||
code.thetadev.de/TSGRain/ginzip v0.1.1 h1:+X0L6qumEZiKYSLmM+Q0LqKVHsKvdcg4CVzsEpvM7fk=
|
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
|
||||||
code.thetadev.de/TSGRain/ginzip v0.1.1/go.mod h1:BH7VkvpP83vPRyMQ8rLIjKycQwGzF+/mFV0BKzg+BuA=
|
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db h1:oZ4U9IqO8NS+61OmGTBi8vopzqTRxwQeogyBHdrhjbc=
|
github.com/fasthttp/websocket v1.4.3-rc.9 h1:CWJH0vONrOatdKXZgkgbFKWllijD9aY50C5KfbSDcWk=
|
||||||
github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db/go.mod h1:Pk7/9x6tyChFTkahDvLBQMlvdsWvfC+yU8HTT5VD314=
|
github.com/fasthttp/websocket v1.4.3-rc.9/go.mod h1:eXL2zqDbexYJxaCw8/PQlm7VcMK6uoGvwbYbTdt4dFo=
|
||||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q=
|
||||||
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
|
github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ=
|
||||||
github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
|
github.com/gofiber/websocket/v2 v2.0.12 h1:jKwTrXiOut9UGOGEzFTAD6gq+/78mM3NcrI05VbxjAU=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gofiber/websocket/v2 v2.0.12/go.mod h1:lQRy0u5ACJfiez/e/bhGeYvM0/M940Y3NFw14U3/otI=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
|
||||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
|
||||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
|
||||||
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
|
|
||||||
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
|
||||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
|
||||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
|
|
||||||
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
github.com/valyala/fasthttp v1.29.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
|
||||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
107
openapi.yml
Normal file
107
openapi.yml
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
openapi: "3.0.3"
|
||||||
|
info:
|
||||||
|
title: SEBRAUC
|
||||||
|
version: "0.0.1"
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8080/api
|
||||||
|
paths:
|
||||||
|
/status:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
"application/json":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RaucStatus"
|
||||||
|
default:
|
||||||
|
description: "Server error"
|
||||||
|
content:
|
||||||
|
"application/json":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusMessage"
|
||||||
|
/update:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
updateFile:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "OK"
|
||||||
|
content:
|
||||||
|
"application/json":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusMessage"
|
||||||
|
default:
|
||||||
|
description: "Server error"
|
||||||
|
content:
|
||||||
|
"application/json":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusMessage"
|
||||||
|
|
||||||
|
/reboot:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "OK"
|
||||||
|
content:
|
||||||
|
"application/json":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusMessage"
|
||||||
|
default:
|
||||||
|
description: "Server error"
|
||||||
|
content:
|
||||||
|
"application/json":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusMessage"
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
RaucStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
installing:
|
||||||
|
description: "True if the installer is running"
|
||||||
|
type: boolean
|
||||||
|
percent:
|
||||||
|
description: "Installation progress"
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
message:
|
||||||
|
description: "Current installation step"
|
||||||
|
type: string
|
||||||
|
example: "Copying image to rootfs.0"
|
||||||
|
last_error:
|
||||||
|
description: "Installation error message"
|
||||||
|
type: string
|
||||||
|
example: "Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?"
|
||||||
|
log:
|
||||||
|
description: "Full command line output of the current installation"
|
||||||
|
type: string
|
||||||
|
example: "0% Installing\n0% Determining slot states\n20% Determining slot states done.\n"
|
||||||
|
required:
|
||||||
|
- installing
|
||||||
|
- percent
|
||||||
|
- message
|
||||||
|
- last_error
|
||||||
|
- log
|
||||||
|
|
||||||
|
StatusMessage:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
description: "Is operation successful"
|
||||||
|
type: boolean
|
||||||
|
msg:
|
||||||
|
description: "Success message"
|
||||||
|
type: string
|
||||||
|
example: "Update started"
|
||||||
|
required:
|
||||||
|
- msg
|
|
@ -1,44 +0,0 @@
|
||||||
# SEBRAUC config file example
|
|
||||||
|
|
||||||
# Temporary directory
|
|
||||||
# Update packages are stored temporarily under Tmpdir/update-<UUID>.raucb
|
|
||||||
Tmpdir = "/tmp/sebrauc"
|
|
||||||
|
|
||||||
# Webserver options
|
|
||||||
[Server]
|
|
||||||
# IP address to accept connections from
|
|
||||||
Address = ""
|
|
||||||
# Port to listen at
|
|
||||||
Port = 8080
|
|
||||||
|
|
||||||
# Websocket connection settings: Ping interval/Timeout in seconds
|
|
||||||
[Server.Websocket]
|
|
||||||
Ping = 45
|
|
||||||
Timeout = 15
|
|
||||||
|
|
||||||
# Compression settings. Refer to code.thetadev.de/TSGRain/ginzip for details.
|
|
||||||
[Server.Compression]
|
|
||||||
Gzip = "default"
|
|
||||||
Brotli = "default"
|
|
||||||
|
|
||||||
[Authentication]
|
|
||||||
Enable = true
|
|
||||||
PasswdFile = "htpasswd"
|
|
||||||
|
|
||||||
# Where to obtain system info from
|
|
||||||
[Sysinfo]
|
|
||||||
ReleaseFile = "/etc/os-release"
|
|
||||||
# Keys to look for in the OS release file
|
|
||||||
NameKey = "NAME"
|
|
||||||
VersionKey = "VERSION"
|
|
||||||
HostnameFile = "/etc/hostname"
|
|
||||||
UptimeFile = "/proc/uptime"
|
|
||||||
|
|
||||||
# Commands to be run by SEBRAUC
|
|
||||||
[Commands]
|
|
||||||
# RAUC status command (outputs updater status in json format)
|
|
||||||
RaucStatus = "rauc status --output-format=json"
|
|
||||||
# RAUC install command (installs FW image passed as argument)
|
|
||||||
RaucInstall = "rauc install"
|
|
||||||
# System reboot command
|
|
||||||
Reboot = "shutdown -r 0"
|
|
8
src/assets/assets.go
Normal file
8
src/assets/assets.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed files/**
|
||||||
|
var Assets embed.FS
|
88
src/assets/files/index.html
Normal file
88
src/assets/files/index.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<!-- See https://github.com/gorilla/websocket/blob/master/examples/chat/home.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Chat Example</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.onload = function () {
|
||||||
|
var conn
|
||||||
|
var msg = document.getElementById("msg")
|
||||||
|
var log = document.getElementById("log")
|
||||||
|
|
||||||
|
function appendLog(item) {
|
||||||
|
var doScroll =
|
||||||
|
log.scrollTop > log.scrollHeight - log.clientHeight - 1
|
||||||
|
log.appendChild(item)
|
||||||
|
if (doScroll) {
|
||||||
|
log.scrollTop = log.scrollHeight - log.clientHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window["WebSocket"]) {
|
||||||
|
conn = new WebSocket("ws://" + document.location.host + "/api/ws")
|
||||||
|
conn.onclose = function (evt) {
|
||||||
|
var item = document.createElement("div")
|
||||||
|
item.innerHTML = "<b>Connection closed.</b>"
|
||||||
|
appendLog(item)
|
||||||
|
}
|
||||||
|
conn.onmessage = function (evt) {
|
||||||
|
var messages = evt.data.split("\n")
|
||||||
|
for (var i = 0; i < messages.length; i++) {
|
||||||
|
var item = document.createElement("div")
|
||||||
|
item.innerText = messages[i]
|
||||||
|
appendLog(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var item = document.createElement("div")
|
||||||
|
item.innerHTML = "<b>Your browser does not support WebSockets.</b>"
|
||||||
|
appendLog(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
background: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||||
|
position: absolute;
|
||||||
|
top: 4em;
|
||||||
|
left: 0.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
bottom: 3em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#form {
|
||||||
|
padding: 0 0.5em 0 0.5em;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1em;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="log"></div>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="updateFile" id="file" />
|
||||||
|
<input type="submit" name="submit" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,130 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
|
|
||||||
"github.com/jinzhu/configor"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfgFilePaths = []string{"sebrauc", "/etc/sebrauc/sebrauc"}
|
|
||||||
cfgFileTypes = []string{"toml", "yaml", "yml", "json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
// SEBRAUC config object
|
|
||||||
type Config struct {
|
|
||||||
// Temporary directory
|
|
||||||
// Update packages are stored temporarily under Tmpdir/update-<UUID>.raucb
|
|
||||||
Tmpdir string
|
|
||||||
|
|
||||||
// Webserver options
|
|
||||||
Server struct {
|
|
||||||
// IP address to accept connections from
|
|
||||||
Address string
|
|
||||||
// Port to listen at
|
|
||||||
Port int `default:"80"`
|
|
||||||
|
|
||||||
// Websocket connection settings: Ping interval/Timeout in seconds
|
|
||||||
Websocket struct {
|
|
||||||
Ping int `default:"45"`
|
|
||||||
Timeout int `default:"15"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compression settings. Refer to code.thetadev.de/TSGRain/ginzip for details.
|
|
||||||
Compression struct {
|
|
||||||
Gzip string
|
|
||||||
Brotli string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Authentication struct {
|
|
||||||
Enable bool `default:"false"`
|
|
||||||
PasswdFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Where to obtain system info from
|
|
||||||
Sysinfo struct {
|
|
||||||
ReleaseFile string `default:"/etc/os-release"`
|
|
||||||
// Keys to look for in the OS release file
|
|
||||||
NameKey string `default:"NAME"`
|
|
||||||
VersionKey string `default:"VERSION"`
|
|
||||||
HostnameFile string `default:"/etc/hostname"`
|
|
||||||
UptimeFile string `default:"/proc/uptime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commands to be run by SEBRAUC
|
|
||||||
// Note that these are overriden when running in development mode
|
|
||||||
Commands struct {
|
|
||||||
// RAUC status command (outputs updater status in json format)
|
|
||||||
RaucStatus string `default:"rauc status --output-format=json"`
|
|
||||||
// RAUC install command (installs FW image passed as argument)
|
|
||||||
RaucInstall string `default:"rauc install"`
|
|
||||||
// System reboot command
|
|
||||||
Reboot string `default:"shutdown -r 0"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findConfigFile(pathIn string) string {
|
|
||||||
fpath, err := util.FindFile(pathIn, cfgFilePaths, cfgFileTypes)
|
|
||||||
if err != nil {
|
|
||||||
if pathIn != "" {
|
|
||||||
panic("cfg file not found: " + err.Error())
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return fpath
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripTrailingSlashes(pathIn string) string {
|
|
||||||
return strings.TrimRight(pathIn, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig(cfgFile string) *Config {
|
|
||||||
cfg := new(Config)
|
|
||||||
cfgor := configor.New(&configor.Config{
|
|
||||||
// Debug: true,
|
|
||||||
ENVPrefix: "SEBRAUC",
|
|
||||||
})
|
|
||||||
|
|
||||||
err := cfgor.Load(cfg, cfgFile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override commands with testing options
|
|
||||||
if mode.IsDev() {
|
|
||||||
cfg.Commands.RaucStatus = testcmd.RaucStatus
|
|
||||||
cfg.Commands.RaucInstall = testcmd.RaucInstall
|
|
||||||
cfg.Commands.Reboot = testcmd.Reboot
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Tmpdir = stripTrailingSlashes(cfg.Tmpdir)
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWithFlags returns the configuration extracted from cmdline args,
|
|
||||||
// env variables or config file.
|
|
||||||
func GetWithFlags(pathIn string, portIn int) *Config {
|
|
||||||
cfg := loadConfig(findConfigFile(pathIn))
|
|
||||||
|
|
||||||
// Override port with cmdline flag if set
|
|
||||||
if portIn > 0 {
|
|
||||||
cfg.Server.Port = portIn
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the configuration extracted from env variables or config file.
|
|
||||||
func Get() *Config {
|
|
||||||
return loadConfig(findConfigFile(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDefault() *Config {
|
|
||||||
return loadConfig("")
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefault(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
cfg := Get()
|
|
||||||
|
|
||||||
assert.Equal(t, "", cfg.Server.Address)
|
|
||||||
assert.Equal(t, 80, cfg.Server.Port)
|
|
||||||
|
|
||||||
assert.Equal(t, 45, cfg.Server.Websocket.Ping)
|
|
||||||
assert.Equal(t, 15, cfg.Server.Websocket.Timeout)
|
|
||||||
|
|
||||||
assert.Equal(t, "", cfg.Server.Compression.Gzip)
|
|
||||||
assert.Equal(t, "", cfg.Server.Compression.Brotli)
|
|
||||||
|
|
||||||
assert.Equal(t, "", cfg.Tmpdir)
|
|
||||||
|
|
||||||
assert.Equal(t, false, cfg.Authentication.Enable)
|
|
||||||
assert.Equal(t, "", cfg.Authentication.PasswdFile)
|
|
||||||
|
|
||||||
assert.Equal(t, "/etc/os-release", cfg.Sysinfo.ReleaseFile)
|
|
||||||
assert.Equal(t, "NAME", cfg.Sysinfo.NameKey)
|
|
||||||
assert.Equal(t, "VERSION", cfg.Sysinfo.VersionKey)
|
|
||||||
assert.Equal(t, "/etc/hostname", cfg.Sysinfo.HostnameFile)
|
|
||||||
assert.Equal(t, "/proc/uptime", cfg.Sysinfo.UptimeFile)
|
|
||||||
|
|
||||||
assert.Equal(t, "rauc status --output-format=json", cfg.Commands.RaucStatus)
|
|
||||||
assert.Equal(t, "rauc install", cfg.Commands.RaucInstall)
|
|
||||||
assert.Equal(t, "shutdown -r 0", cfg.Commands.Reboot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigFile(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
cfile := filepath.Join(fixtures.GetTestfilesDir(), "sebrauc.toml")
|
|
||||||
cfg := GetWithFlags(cfile, 0)
|
|
||||||
|
|
||||||
assert.Equal(t, "127.0.0.1", cfg.Server.Address)
|
|
||||||
assert.Equal(t, 8001, cfg.Server.Port)
|
|
||||||
|
|
||||||
assert.Equal(t, 30, cfg.Server.Websocket.Ping)
|
|
||||||
assert.Equal(t, 10, cfg.Server.Websocket.Timeout)
|
|
||||||
|
|
||||||
assert.Equal(t, "max", cfg.Server.Compression.Gzip)
|
|
||||||
assert.Equal(t, "false", cfg.Server.Compression.Brotli)
|
|
||||||
|
|
||||||
assert.Equal(t, "/var/tmp", cfg.Tmpdir)
|
|
||||||
|
|
||||||
assert.Equal(t, true, cfg.Authentication.Enable)
|
|
||||||
assert.Equal(t, "/etc/htpasswd", cfg.Authentication.PasswdFile)
|
|
||||||
|
|
||||||
assert.Equal(t, "/etc/release", cfg.Sysinfo.ReleaseFile)
|
|
||||||
assert.Equal(t, "PRETTY_NAME", cfg.Sysinfo.NameKey)
|
|
||||||
assert.Equal(t, "VER", cfg.Sysinfo.VersionKey)
|
|
||||||
assert.Equal(t, "/etc/hn", cfg.Sysinfo.HostnameFile)
|
|
||||||
assert.Equal(t, "/proc/up", cfg.Sysinfo.UptimeFile)
|
|
||||||
|
|
||||||
assert.Equal(t, "myrauc status --output-format=json", cfg.Commands.RaucStatus)
|
|
||||||
assert.Equal(t, "myrauc install", cfg.Commands.RaucInstall)
|
|
||||||
assert.Equal(t, "reboot", cfg.Commands.Reboot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvvar(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
fixtures.ResetEnv()
|
|
||||||
defer fixtures.ResetEnv()
|
|
||||||
|
|
||||||
os.Setenv("SEBRAUC_TMPDIR", "/var/tmp")
|
|
||||||
os.Setenv("SEBRAUC_SERVER_ADDRESS", "127.0.0.1")
|
|
||||||
os.Setenv("SEBRAUC_SERVER_PORT", "8001")
|
|
||||||
os.Setenv("SEBRAUC_SERVER_WEBSOCKET_PING", "30")
|
|
||||||
os.Setenv("SEBRAUC_SERVER_WEBSOCKET_TIMEOUT", "10")
|
|
||||||
os.Setenv("SEBRAUC_SERVER_COMPRESSION_GZIP", "max")
|
|
||||||
os.Setenv("SEBRAUC_SERVER_COMPRESSION_BROTLI", "false")
|
|
||||||
os.Setenv("SEBRAUC_AUTHENTICATION_ENABLE", "true")
|
|
||||||
os.Setenv("SEBRAUC_AUTHENTICATION_PASSWDFILE", "/etc/htpasswd")
|
|
||||||
os.Setenv("SEBRAUC_SYSINFO_RELEASEFILE", "/etc/release")
|
|
||||||
os.Setenv("SEBRAUC_SYSINFO_NAMEKEY", "PRETTY_NAME")
|
|
||||||
os.Setenv("SEBRAUC_SYSINFO_VERSIONKEY", "VER")
|
|
||||||
os.Setenv("SEBRAUC_SYSINFO_HOSTNAMEFILE", "/etc/hn")
|
|
||||||
os.Setenv("SEBRAUC_SYSINFO_UPTIMEFILE", "/proc/up")
|
|
||||||
os.Setenv("SEBRAUC_COMMANDS_RAUCSTATUS", "myrauc status --output-format=json")
|
|
||||||
os.Setenv("SEBRAUC_COMMANDS_RAUCINSTALL", "myrauc install")
|
|
||||||
os.Setenv("SEBRAUC_COMMANDS_REBOOT", "reboot")
|
|
||||||
|
|
||||||
cfg := Get()
|
|
||||||
|
|
||||||
assert.Equal(t, "127.0.0.1", cfg.Server.Address)
|
|
||||||
assert.Equal(t, 8001, cfg.Server.Port)
|
|
||||||
|
|
||||||
assert.Equal(t, 30, cfg.Server.Websocket.Ping)
|
|
||||||
assert.Equal(t, 10, cfg.Server.Websocket.Timeout)
|
|
||||||
|
|
||||||
assert.Equal(t, "max", cfg.Server.Compression.Gzip)
|
|
||||||
assert.Equal(t, "false", cfg.Server.Compression.Brotli)
|
|
||||||
|
|
||||||
assert.Equal(t, "/var/tmp", cfg.Tmpdir)
|
|
||||||
|
|
||||||
assert.Equal(t, true, cfg.Authentication.Enable)
|
|
||||||
assert.Equal(t, "/etc/htpasswd", cfg.Authentication.PasswdFile)
|
|
||||||
|
|
||||||
assert.Equal(t, "/etc/release", cfg.Sysinfo.ReleaseFile)
|
|
||||||
assert.Equal(t, "PRETTY_NAME", cfg.Sysinfo.NameKey)
|
|
||||||
assert.Equal(t, "VER", cfg.Sysinfo.VersionKey)
|
|
||||||
assert.Equal(t, "/etc/hn", cfg.Sysinfo.HostnameFile)
|
|
||||||
assert.Equal(t, "/proc/up", cfg.Sysinfo.UptimeFile)
|
|
||||||
|
|
||||||
assert.Equal(t, "myrauc status --output-format=json", cfg.Commands.RaucStatus)
|
|
||||||
assert.Equal(t, "myrauc install", cfg.Commands.RaucInstall)
|
|
||||||
assert.Equal(t, "reboot", cfg.Commands.Reboot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDevMode(t *testing.T) {
|
|
||||||
cfg := Get()
|
|
||||||
|
|
||||||
//nolint:lll
|
|
||||||
assert.Equal(t, "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json", cfg.Commands.RaucStatus)
|
|
||||||
//nolint:lll
|
|
||||||
assert.Equal(t, "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install", cfg.Commands.RaucInstall)
|
|
||||||
assert.Equal(t, "touch /tmp/sebrauc_reboot_test", cfg.Commands.Reboot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFlags(t *testing.T) {
|
|
||||||
cfg := GetWithFlags("", 8001)
|
|
||||||
|
|
||||||
assert.Equal(t, 8001, cfg.Server.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStripTrailingSlashes(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
in string
|
|
||||||
out string
|
|
||||||
}{
|
|
||||||
{in: "/tmp", out: "/tmp"},
|
|
||||||
{in: "/tmp/", out: "/tmp"},
|
|
||||||
{in: "/tmp///", out: "/tmp"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
|
||||||
res := stripTrailingSlashes(tt.in)
|
|
||||||
assert.Equal(t, tt.out, res)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -41,54 +41,22 @@ LastError: Failed to check bundle identifier: Invalid identifier. ` +
|
||||||
idle
|
idle
|
||||||
Installing ` + "/app/demo` failed"
|
Installing ` + "/app/demo` failed"
|
||||||
|
|
||||||
const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` +
|
func main() {
|
||||||
`"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` +
|
arg := ""
|
||||||
`"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` +
|
if len(os.Args) > 1 {
|
||||||
`"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` +
|
arg = os.Args[1]
|
||||||
`{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` +
|
}
|
||||||
`"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}`
|
|
||||||
|
var lines string
|
||||||
|
switch arg {
|
||||||
|
case "fail":
|
||||||
|
lines = outputFailure
|
||||||
|
default:
|
||||||
|
lines = outputSuccess
|
||||||
|
}
|
||||||
|
|
||||||
func printLinesWithDelay(lines string, delay time.Duration) {
|
|
||||||
for _, line := range strings.Split(lines, "\n") {
|
for _, line := range strings.Split(lines, "\n") {
|
||||||
fmt.Println(line)
|
fmt.Println(line)
|
||||||
time.Sleep(delay)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBoolEnvvar(name string) bool {
|
|
||||||
val := strings.ToLower(os.Getenv(name))
|
|
||||||
return val != "" && val != "false" && val != "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
method := ""
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
method = os.Args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
test := getBoolEnvvar("RAUC_MOCK_TEST")
|
|
||||||
failure := getBoolEnvvar("RAUC_MOCK_FAIL")
|
|
||||||
|
|
||||||
delay := 500 * time.Millisecond
|
|
||||||
if test {
|
|
||||||
delay = 10 * time.Millisecond
|
|
||||||
}
|
|
||||||
|
|
||||||
switch method {
|
|
||||||
case "install":
|
|
||||||
if failure {
|
|
||||||
printLinesWithDelay(outputFailure, delay)
|
|
||||||
} else {
|
|
||||||
printLinesWithDelay(outputSuccess, delay)
|
|
||||||
}
|
|
||||||
case "status":
|
|
||||||
if os.Args[2] != "--output-format=json" {
|
|
||||||
fmt.Println("output format must be json")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println(statusJson)
|
|
||||||
default:
|
|
||||||
fmt.Println("invalid method")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package testcmd
|
|
||||||
|
|
||||||
//nolint:lll
|
|
||||||
const (
|
|
||||||
RaucStatus = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json"
|
|
||||||
RaucInstall = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install"
|
|
||||||
Reboot = "touch /tmp/sebrauc_reboot_test"
|
|
||||||
)
|
|
|
@ -1,3 +0,0 @@
|
||||||
plain:1234
|
|
||||||
md5:$apr1$V2wxHBfb$gBU2yIYjTIeciKapglql6/
|
|
||||||
bcrypt:$2y$05$f9rV6uTQEEnNR1saPksExOR31LauUZzpLDhpCrodAvxX3zZ6nLy12
|
|
|
@ -1,5 +0,0 @@
|
||||||
ID=tsgrain
|
|
||||||
NAME="TSGRain distro"
|
|
||||||
VERSION="0.0.1"
|
|
||||||
VERSION_ID=0.0.1
|
|
||||||
PRETTY_NAME="TSGRain distro 0.0.1"
|
|
|
@ -1,32 +0,0 @@
|
||||||
# SEBRAUC config file for testing
|
|
||||||
# Dont use for real, the commands and paths are not correct
|
|
||||||
|
|
||||||
Tmpdir = "/var/tmp/"
|
|
||||||
|
|
||||||
[Server]
|
|
||||||
Address = "127.0.0.1"
|
|
||||||
Port = 8001
|
|
||||||
|
|
||||||
[Server.Websocket]
|
|
||||||
Ping = 30
|
|
||||||
Timeout = 10
|
|
||||||
|
|
||||||
[Server.Compression]
|
|
||||||
Gzip = "max"
|
|
||||||
Brotli = "false"
|
|
||||||
|
|
||||||
[Authentication]
|
|
||||||
Enable = true
|
|
||||||
PasswdFile = "/etc/htpasswd"
|
|
||||||
|
|
||||||
[Sysinfo]
|
|
||||||
ReleaseFile = "/etc/release"
|
|
||||||
NameKey = "PRETTY_NAME"
|
|
||||||
VersionKey = "VER"
|
|
||||||
HostnameFile = "/etc/hn"
|
|
||||||
UptimeFile = "/proc/up"
|
|
||||||
|
|
||||||
[Commands]
|
|
||||||
RaucStatus = "myrauc status --output-format=json"
|
|
||||||
RaucInstall = "myrauc install"
|
|
||||||
Reboot = "reboot"
|
|
|
@ -3,11 +3,8 @@ package fixtures
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var envPrefixes = []string{"SEBRAUC", "RAUC_MOCK"}
|
|
||||||
|
|
||||||
func doesFileExist(filepath string) bool {
|
func doesFileExist(filepath string) bool {
|
||||||
_, err := os.Stat(filepath)
|
_, err := os.Stat(filepath)
|
||||||
return !os.IsNotExist(err)
|
return !os.IsNotExist(err)
|
||||||
|
@ -41,20 +38,3 @@ func GetTestfilesDir() string {
|
||||||
CdProjectRoot()
|
CdProjectRoot()
|
||||||
return filepath.Join("src", "fixtures", "testfiles")
|
return filepath.Join("src", "fixtures", "testfiles")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetEnv() {
|
|
||||||
for _, envvar := range os.Environ() {
|
|
||||||
split := strings.SplitN(envvar, "=", 2)
|
|
||||||
if len(split) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := split[0]
|
|
||||||
|
|
||||||
for _, prefix := range envPrefixes {
|
|
||||||
if strings.HasPrefix(key, prefix) {
|
|
||||||
_ = os.Unsetenv(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -35,15 +35,3 @@ func TestCdProjectRoot(t *testing.T) {
|
||||||
CdProjectRoot()
|
CdProjectRoot()
|
||||||
assert.True(t, doesFileExist("go.sum"))
|
assert.True(t, doesFileExist("go.sum"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResetEnv(t *testing.T) {
|
|
||||||
os.Setenv("RAUC_MOCK_TEST", "1")
|
|
||||||
os.Setenv("SEBRAUC_PORT", "8001")
|
|
||||||
|
|
||||||
ResetEnv()
|
|
||||||
|
|
||||||
_, exists := os.LookupEnv("RAUC_MOCK_TEST")
|
|
||||||
assert.False(t, exists)
|
|
||||||
_, exists = os.LookupEnv("SEBRAUC_PORT")
|
|
||||||
assert.False(t, exists)
|
|
||||||
}
|
|
||||||
|
|
29
src/main.go
29
src/main.go
|
@ -1,45 +1,22 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/config"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/server"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/server"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const titleArt = ` _____ __________ ____ ___ __ ________
|
|
||||||
/ ___// ____/ __ )/ __ \/ | / / / / ____/
|
|
||||||
\__ \/ __/ / __ / /_/ / /| |/ / / / /
|
|
||||||
___/ / /___/ /_/ / _, _/ ___ / /_/ / /___
|
|
||||||
/____/_____/_____/_/ |_/_/ |_\____/\____/ `
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
run(os.Args[1:])
|
fmt.Println("SEBRAUC " + util.Version())
|
||||||
}
|
|
||||||
|
|
||||||
func run(args []string) {
|
if util.TestMode {
|
||||||
fmt.Println(titleArt + util.Version() + "\n")
|
|
||||||
|
|
||||||
cmdFlags := flag.NewFlagSet("sebrauc", flag.ExitOnError)
|
|
||||||
port := cmdFlags.Int("p", 0, "HTTP port")
|
|
||||||
cfgPath := cmdFlags.String("c", "", "Config file path")
|
|
||||||
_ = cmdFlags.Parse(args)
|
|
||||||
|
|
||||||
if mode.IsDev() {
|
|
||||||
fmt.Println("Test mode active - no update operations are executed.")
|
fmt.Println("Test mode active - no update operations are executed.")
|
||||||
fmt.Println("Build with -tags prod to enable live mode.")
|
fmt.Println("Build with -tags prod to enable live mode.")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.GetWithFlags(*cfgPath, *port)
|
srv := server.NewServer(":8080")
|
||||||
|
|
||||||
fmt.Printf("Starting server at %s:%d\n", cfg.Server.Address, cfg.Server.Port)
|
|
||||||
|
|
||||||
srv := server.NewServer(cfg)
|
|
||||||
err := srv.Run()
|
err := srv.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
// Error model
|
|
||||||
//
|
|
||||||
// The Error contains error relevant information.
|
|
||||||
//
|
|
||||||
//swagger:model Error
|
|
||||||
type Error struct {
|
|
||||||
// The general error message according to HTTP specification.
|
|
||||||
//
|
|
||||||
// required: true
|
|
||||||
// example: Unauthorized
|
|
||||||
Error string `json:"error"`
|
|
||||||
// The http error code.
|
|
||||||
//
|
|
||||||
// required: true
|
|
||||||
// example: 500
|
|
||||||
StatusCode int `json:"status_code"`
|
|
||||||
// Concrete error message.
|
|
||||||
//
|
|
||||||
// required: true
|
|
||||||
// example: already running
|
|
||||||
Message string `json:"msg"`
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
// RaucStatus model
|
|
||||||
//
|
|
||||||
// RaucStatus contains information about the current RAUC updater status.
|
|
||||||
//
|
|
||||||
//swagger:model RaucStatus
|
|
||||||
//nolint:lll
|
|
||||||
type RaucStatus struct {
|
|
||||||
// True if the installer is running
|
|
||||||
// required: true
|
|
||||||
Installing bool `json:"installing"`
|
|
||||||
|
|
||||||
// Installation progress
|
|
||||||
// required: true
|
|
||||||
// minimum: 0
|
|
||||||
// maximum: 100
|
|
||||||
Percent int `json:"percent"`
|
|
||||||
|
|
||||||
// Current installation step
|
|
||||||
// required: true
|
|
||||||
// example: Copying image to rootfs.0
|
|
||||||
Message string `json:"message"`
|
|
||||||
|
|
||||||
// Installation error message
|
|
||||||
// required: true
|
|
||||||
// example: Failed to check bundle identifier: Invalid identifier.
|
|
||||||
LastError string `json:"last_error"`
|
|
||||||
|
|
||||||
// Full command line output of the current installation
|
|
||||||
// required: true
|
|
||||||
// example: 0% Installing 0% Determining slot states 20% Determining slot states done
|
|
||||||
Log string `json:"log"`
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
// StatusMessage model
|
|
||||||
//
|
|
||||||
// StatusMessage contains the status of an operation.
|
|
||||||
//
|
|
||||||
//swagger:model StatusMessage
|
|
||||||
type StatusMessage struct {
|
|
||||||
// Is operation successful?
|
|
||||||
// required: true
|
|
||||||
Success bool `json:"success"`
|
|
||||||
|
|
||||||
// Status message text
|
|
||||||
// required: true
|
|
||||||
// example: Update started
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
// SystemInfo model
|
|
||||||
//
|
|
||||||
// SystemInfo contains information about the running system.
|
|
||||||
//
|
|
||||||
//swagger:model SystemInfo
|
|
||||||
type SystemInfo struct {
|
|
||||||
// Hostname of the system
|
|
||||||
// required: true
|
|
||||||
// example: raspberrypi3
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
|
|
||||||
// Name of the os distribution
|
|
||||||
// required: true
|
|
||||||
// example: Poky
|
|
||||||
OsName string `json:"os_name"`
|
|
||||||
|
|
||||||
// Operating system version
|
|
||||||
// required: true
|
|
||||||
// example: 1.0.2
|
|
||||||
OsVersion string `json:"os_version"`
|
|
||||||
|
|
||||||
// System uptime in seconds
|
|
||||||
// required: true
|
|
||||||
// example: 5832
|
|
||||||
Uptime int `json:"uptime"`
|
|
||||||
|
|
||||||
// Compatible firmware name
|
|
||||||
// required: true
|
|
||||||
// example: Poky
|
|
||||||
RaucCompatible string `json:"rauc_compatible"`
|
|
||||||
|
|
||||||
// Compatible firmware variant
|
|
||||||
// required: true
|
|
||||||
// example: rpi-prod
|
|
||||||
RaucVariant string `json:"rauc_variant"`
|
|
||||||
|
|
||||||
// List of RAUC root filesystems
|
|
||||||
// required: true
|
|
||||||
RaucRootfs map[string]Rootfs `json:"rauc_rootfs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
//swagger:model Rootfs
|
|
||||||
type Rootfs struct {
|
|
||||||
// Block device
|
|
||||||
// required: true
|
|
||||||
// example: /dev/mmcblk0p2
|
|
||||||
Device string `json:"device"`
|
|
||||||
|
|
||||||
// Filesystem
|
|
||||||
// required: true
|
|
||||||
// example: ext4
|
|
||||||
Type string `json:"type"`
|
|
||||||
|
|
||||||
// Mount path (null when not mounted)
|
|
||||||
// required: true
|
|
||||||
// nullable: true
|
|
||||||
// example: /
|
|
||||||
Mountpoint *string `json:"mountpoint"`
|
|
||||||
|
|
||||||
// Is the filesystem bootable?
|
|
||||||
// required: true
|
|
||||||
Bootable bool `json:"bootable"`
|
|
||||||
|
|
||||||
// Is the filesystem booted?
|
|
||||||
// required: true
|
|
||||||
Booted bool `json:"booted"`
|
|
||||||
|
|
||||||
// Is the filesystem the next boot target?
|
|
||||||
// required: true
|
|
||||||
Primary bool `json:"primary"`
|
|
||||||
|
|
||||||
Bootname string `json:"-"`
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,26 +18,32 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Rauc struct {
|
type Rauc struct {
|
||||||
cmdRaucInstall string
|
broadcast chan string
|
||||||
bc util.Broadcaster
|
status RaucStatus
|
||||||
status model.RaucStatus
|
runningMtx sync.Mutex
|
||||||
runningMtx sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cmdRaucInstall string) *Rauc {
|
type RaucStatus struct {
|
||||||
return &Rauc{
|
Installing bool `json:"installing"`
|
||||||
cmdRaucInstall: cmdRaucInstall,
|
Percent int `json:"percent"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
LastError string `json:"last_error"`
|
||||||
|
Log string `json:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRauc(broadcast chan string) *Rauc {
|
||||||
|
r := &Rauc{
|
||||||
|
broadcast: broadcast,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rauc) SetBroadcaster(bc util.Broadcaster) {
|
r.broadcast <- r.GetStatusJson()
|
||||||
r.bc = bc
|
|
||||||
r.bcStatus()
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rauc) completed(updateFile string) {
|
func (r *Rauc) completed(updateFile string) {
|
||||||
r.status.Installing = false
|
r.status.Installing = false
|
||||||
r.bcStatus()
|
r.broadcast <- r.GetStatusJson()
|
||||||
|
|
||||||
_ = os.Remove(updateFile)
|
_ = os.Remove(updateFile)
|
||||||
}
|
}
|
||||||
|
@ -60,12 +65,12 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset installer
|
// Reset installer
|
||||||
r.status = model.RaucStatus{
|
r.status = RaucStatus{
|
||||||
Installing: true,
|
Installing: true,
|
||||||
}
|
}
|
||||||
r.bcStatus()
|
r.broadcast <- r.GetStatusJson()
|
||||||
|
|
||||||
cmd := util.CommandFromString(r.cmdRaucInstall + " " + updateFile)
|
cmd := util.CommandFromString(fmt.Sprintf("%s %s", util.UpdateCmd, updateFile))
|
||||||
|
|
||||||
readPipe, _ := cmd.StdoutPipe()
|
readPipe, _ := cmd.StdoutPipe()
|
||||||
cmd.Stderr = cmd.Stdout
|
cmd.Stderr = cmd.Stdout
|
||||||
|
@ -95,7 +100,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasUpdate {
|
if hasUpdate {
|
||||||
r.bcStatus()
|
r.broadcast <- r.GetStatusJson()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -117,19 +122,11 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rauc) GetStatus() model.RaucStatus {
|
func (r *Rauc) GetStatus() RaucStatus {
|
||||||
return r.status
|
return r.status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rauc) GetStatusJson() []byte {
|
func (r *Rauc) GetStatusJson() string {
|
||||||
statusJson, err := json.Marshal(r.status)
|
statusJson, _ := json.Marshal(r.status)
|
||||||
if err != nil {
|
return string(statusJson)
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusJson
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rauc) bcStatus() {
|
|
||||||
r.bc.Broadcast(r.GetStatusJson())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
package rauc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type broadcasterMock struct {
|
|
||||||
messages []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *broadcasterMock) Broadcast(msg []byte) {
|
|
||||||
b.messages = append(b.messages, string(msg))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRauc(t *testing.T) {
|
|
||||||
//nolint:lll
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fail string
|
|
||||||
messages []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "ok",
|
|
||||||
fail: "",
|
|
||||||
messages: []string{
|
|
||||||
"{\"installing\":false,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}",
|
|
||||||
"{\"installing\":true,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}",
|
|
||||||
"{\"installing\":true,\"percent\":0,\"message\":\"Installing\",\"last_error\":\"\",\"log\":\"0% Installing\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":0,\"message\":\"Determining slot states\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":20,\"message\":\"Determining slot states done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":20,\"message\":\"Checking bundle\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":20,\"message\":\"Verifying signature\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":40,\"message\":\"Verifying signature done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":40,\"message\":\"Checking bundle done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":40,\"message\":\"Checking manifest contents\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":60,\"message\":\"Checking manifest contents done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":60,\"message\":\"Determining target install group\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":80,\"message\":\"Determining target install group done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":80,\"message\":\"Updating slots\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":80,\"message\":\"Checking slot rootfs.0\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":90,\"message\":\"Checking slot rootfs.0 done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":90,\"message\":\"Copying image to rootfs.0\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":100,\"message\":\"Copying image to rootfs.0 done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":100,\"message\":\"Updating slots done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n100% Updating slots done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":100,\"message\":\"Installing done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n100% Updating slots done.\\n100% Installing done.\\n\"}",
|
|
||||||
"{\"installing\":false,\"percent\":100,\"message\":\"Installing done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n100% Updating slots done.\\n100% Installing done.\\nInstalling `/app/tsgrain-update-raspberrypi3.raucb` succeeded\\n\"}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fail",
|
|
||||||
fail: "1",
|
|
||||||
messages: []string{
|
|
||||||
"{\"installing\":false,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}",
|
|
||||||
"{\"installing\":true,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}",
|
|
||||||
"{\"installing\":true,\"percent\":0,\"message\":\"Installing\",\"last_error\":\"\",\"log\":\"0% Installing\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":0,\"message\":\"Determining slot states\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":20,\"message\":\"Determining slot states done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":20,\"message\":\"Checking bundle\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":40,\"message\":\"Checking bundle failed.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":100,\"message\":\"Installing failed.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n100% Installing failed.\\n\"}",
|
|
||||||
"{\"installing\":true,\"percent\":100,\"message\":\"Installing failed.\",\"last_error\":\"Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n100% Installing failed.\\nLastError: Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\\n\"}",
|
|
||||||
"{\"installing\":false,\"percent\":100,\"message\":\"Installing failed.\",\"last_error\":\"Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n100% Installing failed.\\nLastError: Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\\nInstalling /app/demo` failed\\n\"}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
fixtures.ResetEnv()
|
|
||||||
defer fixtures.ResetEnv()
|
|
||||||
|
|
||||||
os.Setenv("RAUC_MOCK_TEST", "1")
|
|
||||||
os.Setenv("RAUC_MOCK_FAIL", tt.fail)
|
|
||||||
|
|
||||||
updater := New(testcmd.RaucInstall)
|
|
||||||
bc := &broadcasterMock{}
|
|
||||||
updater.SetBroadcaster(bc)
|
|
||||||
|
|
||||||
testfile := createTmpfile()
|
|
||||||
|
|
||||||
err := updater.RunRauc(testfile)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Dont run multiple updates concurrently
|
|
||||||
err = updater.RunRauc(testfile)
|
|
||||||
assert.ErrorIs(t, err, util.ErrAlreadyRunning)
|
|
||||||
|
|
||||||
// Wait for updater to finish
|
|
||||||
for updater.GetStatus().Installing {
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.False(t, util.DoesFileExist(testfile), "update file was not deleted")
|
|
||||||
|
|
||||||
assert.Equal(t, tt.messages, bc.messages)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTmpfile() string {
|
|
||||||
tmpdir := util.GetTmpdir("")
|
|
||||||
|
|
||||||
tmpfile := filepath.Join(tmpdir, "test.raucb")
|
|
||||||
_, err := os.Create(tmpfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmpfile
|
|
||||||
}
|
|
98
src/server/hub.go
Normal file
98
src/server/hub.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hubClient struct{}
|
||||||
|
|
||||||
|
type MessageHub struct {
|
||||||
|
Broadcast chan string
|
||||||
|
|
||||||
|
clients map[*websocket.Conn]hubClient
|
||||||
|
register chan *websocket.Conn
|
||||||
|
unregister chan *websocket.Conn
|
||||||
|
lastMessage string
|
||||||
|
|
||||||
|
running bool
|
||||||
|
runningMtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub() *MessageHub {
|
||||||
|
return &MessageHub{
|
||||||
|
clients: make(map[*websocket.Conn]hubClient),
|
||||||
|
register: make(chan *websocket.Conn),
|
||||||
|
Broadcast: make(chan string, 5),
|
||||||
|
unregister: make(chan *websocket.Conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hub *MessageHub) sendMessage(conn *websocket.Conn, message string) {
|
||||||
|
if err := conn.WriteMessage(
|
||||||
|
websocket.TextMessage, []byte(message)); err != nil {
|
||||||
|
log.Println("write error:", err)
|
||||||
|
|
||||||
|
_ = conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
_ = conn.Close()
|
||||||
|
delete(hub.clients, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hub *MessageHub) Run() {
|
||||||
|
hub.runningMtx.Lock()
|
||||||
|
isRunning := hub.running
|
||||||
|
hub.running = true
|
||||||
|
hub.runningMtx.Unlock()
|
||||||
|
|
||||||
|
if isRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case conn := <-hub.register:
|
||||||
|
hub.clients[conn] = hubClient{}
|
||||||
|
log.Println("connection registered")
|
||||||
|
|
||||||
|
case message := <-hub.Broadcast:
|
||||||
|
log.Println("message received:", message)
|
||||||
|
hub.lastMessage = message
|
||||||
|
|
||||||
|
// Send the message to all clients
|
||||||
|
for conn := range hub.clients {
|
||||||
|
hub.sendMessage(conn, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
case conn := <-hub.unregister:
|
||||||
|
// Remove the client from the hub
|
||||||
|
delete(hub.clients, conn)
|
||||||
|
|
||||||
|
log.Println("connection unregistered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hub *MessageHub) Handler(conn *websocket.Conn) {
|
||||||
|
// When the function returns, unregister the client and close the connection
|
||||||
|
defer func() {
|
||||||
|
hub.unregister <- conn
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Register the client
|
||||||
|
hub.register <- conn
|
||||||
|
|
||||||
|
if hub.lastMessage != "" {
|
||||||
|
hub.sendMessage(conn, hub.lastMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, _, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return // Calls the deferred function, i.e. closes the connection on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/tg123/go-htpasswd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var pwdFilePaths = []string{"htpasswd", "/etc/sebrauc/htpasswd"}
|
|
||||||
|
|
||||||
// Authentication requires HTTP basic auth or an active session
|
|
||||||
func Authentication(pwdFile string) gin.HandlerFunc {
|
|
||||||
fpath, err := util.FindFile(pwdFile, pwdFilePaths, nil)
|
|
||||||
if err != nil {
|
|
||||||
panic("passwd file not found: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
myauth, err := htpasswd.New(fpath, htpasswd.DefaultSystems, nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
if user, pass, ok := c.Request.BasicAuth(); ok {
|
|
||||||
if myauth.Match(user, pass) {
|
|
||||||
c.Set(gin.AuthUserKey, user)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Header("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAuthentication(t *testing.T) {
|
|
||||||
testfiles := fixtures.GetTestfilesDir()
|
|
||||||
pwdfile := filepath.Join(testfiles, "htpasswd")
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.Use(Authentication(pwdfile))
|
|
||||||
router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "HelloWorld") })
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{name: "plain"},
|
|
||||||
{name: "md5"},
|
|
||||||
{name: "bcrypt"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name+"_ok", func(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
req.SetBasicAuth(tt.name, "1234")
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
assert.Equal(t, "HelloWorld", w.Body.String())
|
|
||||||
assert.Empty(t, w.Header().Get("WWW-Authenticate"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("fail", func(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
req.SetBasicAuth("plain", "asdf")
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
||||||
assert.Empty(t, w.Body.String())
|
|
||||||
assert.Equal(t, "Basic realm=\"Authorization Required\"",
|
|
||||||
w.Header().Get("WWW-Authenticate"))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
func Cache(c *gin.Context) {
|
|
||||||
c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCache(t *testing.T) {
|
|
||||||
router := gin.New()
|
|
||||||
router.Use(Cache)
|
|
||||||
router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "HelloWorld") })
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
assert.Equal(t, "HelloWorld", w.Body.String())
|
|
||||||
assert.Equal(t, "public, max-age=604800, immutable",
|
|
||||||
w.Header().Get("Cache-Control"))
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"code.thetadev.de/TSGRain/ginzip"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Compression(gzip, brotli string) gin.HandlerFunc {
|
|
||||||
opts := ginzip.DefaultOptions()
|
|
||||||
opts.GzipLevel = gzip
|
|
||||||
opts.BrotliLevel = brotli
|
|
||||||
|
|
||||||
return ginzip.New(opts)
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
nice "github.com/ekyoung/gin-nice-recovery"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrorHandler creates a gin middleware for handling errors.
|
|
||||||
func ErrorHandler(isApi bool) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
c.Next()
|
|
||||||
|
|
||||||
if len(c.Errors) > 0 {
|
|
||||||
for _, e := range c.Errors {
|
|
||||||
writeError(c, e.Err, isApi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PanicHandler(isApi bool) gin.HandlerFunc {
|
|
||||||
return nice.Recovery(func(c *gin.Context, err interface{}) {
|
|
||||||
writeError(c, fmt.Errorf("[PANIC] %s", err), isApi)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeError(c *gin.Context, err error, isApi bool) {
|
|
||||||
status := http.StatusInternalServerError
|
|
||||||
|
|
||||||
var httpErr util.HttpError
|
|
||||||
if errors.As(err, &httpErr) {
|
|
||||||
status = httpErr.StatusCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// only write error message if there is no content
|
|
||||||
if c.Writer.Size() != -1 {
|
|
||||||
c.Status(status)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isApi {
|
|
||||||
// Machine-readable JSON error message
|
|
||||||
c.JSON(status, &model.Error{
|
|
||||||
Error: http.StatusText(status),
|
|
||||||
StatusCode: status,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Human-readable error message
|
|
||||||
c.String(status, "%d %s: %s", status, http.StatusText(status), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestErrorHandler(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
controller gin.HandlerFunc
|
|
||||||
isApi bool
|
|
||||||
expectResponse string
|
|
||||||
expectStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
controller: controllerError,
|
|
||||||
isApi: false,
|
|
||||||
expectResponse: "400 Bad Request: error test",
|
|
||||||
expectStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error_api",
|
|
||||||
controller: controllerError,
|
|
||||||
isApi: true,
|
|
||||||
//nolint:lll
|
|
||||||
expectResponse: `{"error":"Bad Request","status_code":400,"msg":"error test"}`,
|
|
||||||
expectStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "generic_error",
|
|
||||||
controller: controllerErrorGeneric,
|
|
||||||
isApi: false,
|
|
||||||
expectResponse: "500 Internal Server Error: generic error",
|
|
||||||
expectStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "generic_error_api",
|
|
||||||
controller: controllerErrorGeneric,
|
|
||||||
isApi: true,
|
|
||||||
//nolint:lll
|
|
||||||
expectResponse: `{"error":"Internal Server Error","status_code":500,"msg":"generic error"}`,
|
|
||||||
expectStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
router := gin.New()
|
|
||||||
router.Use(ErrorHandler(tt.isApi))
|
|
||||||
router.GET("/", tt.controller)
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectStatus, w.Code)
|
|
||||||
assert.Equal(t, tt.expectResponse, w.Body.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPanicHandler(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
controller gin.HandlerFunc
|
|
||||||
isApi bool
|
|
||||||
expectResponse string
|
|
||||||
expectStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "panic",
|
|
||||||
controller: controllerPanic,
|
|
||||||
isApi: false,
|
|
||||||
expectResponse: "500 Internal Server Error: [PANIC] panic message",
|
|
||||||
expectStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "panic_api",
|
|
||||||
controller: controllerPanic,
|
|
||||||
isApi: true,
|
|
||||||
//nolint:lll
|
|
||||||
expectResponse: `{"error":"Internal Server Error","status_code":500,"msg":"[PANIC] panic message"}`,
|
|
||||||
expectStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "panic_w_error",
|
|
||||||
controller: controllerPanicErr,
|
|
||||||
isApi: false,
|
|
||||||
expectResponse: "500 Internal Server Error: [PANIC] panic message in error",
|
|
||||||
expectStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "panic_w_error_api",
|
|
||||||
controller: controllerPanicErr,
|
|
||||||
isApi: true,
|
|
||||||
//nolint:lll
|
|
||||||
expectResponse: `{"error":"Internal Server Error","status_code":500,"msg":"[PANIC] panic message in error"}`,
|
|
||||||
expectStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
router := gin.New()
|
|
||||||
router.Use(PanicHandler(tt.isApi))
|
|
||||||
router.GET("/", tt.controller)
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectStatus, w.Code)
|
|
||||||
assert.Equal(t, tt.expectResponse, w.Body.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func controllerError(c *gin.Context) {
|
|
||||||
c.Error(util.HttpErrNew("error test", http.StatusBadRequest))
|
|
||||||
}
|
|
||||||
|
|
||||||
func controllerErrorGeneric(c *gin.Context) {
|
|
||||||
c.Error(errors.New("generic error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func controllerPanic(c *gin.Context) {
|
|
||||||
panic("panic message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func controllerPanicErr(c *gin.Context) {
|
|
||||||
panic(errors.New("panic message in error"))
|
|
||||||
}
|
|
|
@ -1,266 +1,158 @@
|
||||||
// SEBRAUC
|
|
||||||
//
|
|
||||||
// # REST API for the SEBRAUC firmware updater
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// Schemes: http, https
|
|
||||||
// Version: 0.2.0
|
|
||||||
// License: MIT
|
|
||||||
//
|
|
||||||
// swagger:meta
|
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/config"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/server/stream"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/server/swagger"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/ui"
|
"code.thetadev.de/TSGRain/SEBRAUC/ui"
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SEBRAUCServer struct {
|
type SEBRAUCServer struct {
|
||||||
config *config.Config
|
address string
|
||||||
streamer *stream.API
|
raucUpdater *rauc.Rauc
|
||||||
updater *rauc.Rauc
|
hub *MessageHub
|
||||||
sysinfo *sysinfo.Sysinfo
|
tmpdir string
|
||||||
tmpdir string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(config *config.Config) *SEBRAUCServer {
|
type statusMessage struct {
|
||||||
updater := rauc.New(config.Commands.RaucInstall)
|
Success bool `json:"success"`
|
||||||
streamer := stream.New(
|
Msg string `json:"msg"`
|
||||||
time.Duration(config.Server.Websocket.Ping)*time.Second,
|
}
|
||||||
time.Duration(config.Server.Websocket.Timeout)*time.Second,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
sysinfo := sysinfo.New(
|
|
||||||
config.Commands.RaucStatus,
|
|
||||||
config.Sysinfo.ReleaseFile,
|
|
||||||
config.Sysinfo.NameKey,
|
|
||||||
config.Sysinfo.VersionKey,
|
|
||||||
config.Sysinfo.HostnameFile,
|
|
||||||
config.Sysinfo.UptimeFile,
|
|
||||||
)
|
|
||||||
|
|
||||||
updater.SetBroadcaster(streamer)
|
func NewServer(address string) *SEBRAUCServer {
|
||||||
|
hub := NewHub()
|
||||||
|
|
||||||
tmpdir := util.GetTmpdir(config.Tmpdir)
|
raucUpdater := rauc.NewRauc(hub.Broadcast)
|
||||||
|
|
||||||
|
tmpdir, err := util.GetTmpdir()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
return &SEBRAUCServer{
|
return &SEBRAUCServer{
|
||||||
config: config,
|
address: address,
|
||||||
updater: updater,
|
raucUpdater: raucUpdater,
|
||||||
streamer: streamer,
|
hub: hub,
|
||||||
sysinfo: sysinfo,
|
tmpdir: tmpdir,
|
||||||
tmpdir: tmpdir,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) getRouter() *gin.Engine {
|
|
||||||
router := gin.New()
|
|
||||||
router.Use(gin.Logger())
|
|
||||||
_ = router.SetTrustedProxies(nil)
|
|
||||||
|
|
||||||
if mode.IsDev() {
|
|
||||||
router.Use(cors.Default())
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Use(middleware.ErrorHandler(false), middleware.PanicHandler(false))
|
|
||||||
router.NoRoute(func(c *gin.Context) { c.Error(util.ErrPageNotFound) })
|
|
||||||
|
|
||||||
if srv.config.Authentication.Enable {
|
|
||||||
router.Use(middleware.Authentication(srv.config.Authentication.PasswdFile))
|
|
||||||
}
|
|
||||||
|
|
||||||
api := router.Group("/api",
|
|
||||||
middleware.ErrorHandler(true), middleware.PanicHandler(true))
|
|
||||||
|
|
||||||
// API ROUTES
|
|
||||||
api.GET("/ws", srv.streamer.Handle)
|
|
||||||
api.GET("/status", srv.controllerStatus)
|
|
||||||
api.GET("/info", srv.controllerInfo)
|
|
||||||
|
|
||||||
api.POST("/update", srv.controllerUpdate)
|
|
||||||
api.POST("/reboot", srv.controllerReboot)
|
|
||||||
|
|
||||||
// Error routes for testing
|
|
||||||
if mode.IsDev() {
|
|
||||||
router.GET("/error", srv.controllerError)
|
|
||||||
router.GET("/panic", srv.controllerPanic)
|
|
||||||
|
|
||||||
api.GET("/error", srv.controllerError)
|
|
||||||
api.GET("/panic", srv.controllerPanic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI
|
|
||||||
uiGroup := router.Group("/", middleware.Compression(
|
|
||||||
srv.config.Server.Compression.Gzip,
|
|
||||||
srv.config.Server.Compression.Brotli),
|
|
||||||
)
|
|
||||||
ui.Register(uiGroup)
|
|
||||||
swagger.Register(uiGroup)
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) Run() error {
|
func (srv *SEBRAUCServer) Run() error {
|
||||||
router := srv.getRouter()
|
app := fiber.New(fiber.Config{
|
||||||
|
AppName: "SEBRAUC",
|
||||||
|
BodyLimit: 1024 * 1024 * 1024,
|
||||||
|
ErrorHandler: errorHandler,
|
||||||
|
DisableStartupMessage: true,
|
||||||
|
})
|
||||||
|
|
||||||
return router.Run(fmt.Sprintf("%s:%d",
|
app.Use(logger.New())
|
||||||
srv.config.Server.Address, srv.config.Server.Port))
|
|
||||||
|
app.Use(compress.New(compress.Config{
|
||||||
|
Next: func(c *fiber.Ctx) bool {
|
||||||
|
return strings.HasPrefix(c.Path(), "/api")
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// just for testing
|
||||||
|
app.Use("/api", cors.New())
|
||||||
|
|
||||||
|
app.Use("/api/ws", func(c *fiber.Ctx) error {
|
||||||
|
// IsWebSocketUpgrade returns true if the client
|
||||||
|
// requested upgrade to the WebSocket protocol.
|
||||||
|
if websocket.IsWebSocketUpgrade(c) {
|
||||||
|
c.Locals("allowed", true)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
return fiber.ErrUpgradeRequired
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Use("/", filesystem.New(filesystem.Config{
|
||||||
|
Root: http.FS(ui.Assets),
|
||||||
|
PathPrefix: ui.AssetsDir,
|
||||||
|
MaxAge: 7200,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ROUTES
|
||||||
|
app.Get("/api/ws", websocket.New(srv.hub.Handler))
|
||||||
|
app.Post("/api/update", srv.controllerUpdate)
|
||||||
|
app.Get("/api/status", srv.controllerStatus)
|
||||||
|
app.Post("/api/reboot", srv.controllerReboot)
|
||||||
|
|
||||||
|
// Start messaging hub
|
||||||
|
go srv.hub.Run()
|
||||||
|
|
||||||
|
return app.Listen(srv.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:operation POST /update startUpdate
|
func (srv *SEBRAUCServer) controllerUpdate(c *fiber.Ctx) error {
|
||||||
//
|
|
||||||
// # Start the update process
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// consumes:
|
|
||||||
// - multipart/form-data
|
|
||||||
// produces: [application/json]
|
|
||||||
// parameters:
|
|
||||||
// - name: updateFile
|
|
||||||
// in: formData
|
|
||||||
// description: RAUC firmware image file (*.raucb)
|
|
||||||
// required: true
|
|
||||||
// type: file
|
|
||||||
//
|
|
||||||
// responses:
|
|
||||||
//
|
|
||||||
// 200:
|
|
||||||
// description: Ok
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/StatusMessage"
|
|
||||||
// 409:
|
|
||||||
// description: already running
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/Error"
|
|
||||||
// 500:
|
|
||||||
// description: Server Error
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/Error"
|
|
||||||
func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) {
|
|
||||||
file, err := c.FormFile("updateFile")
|
file, err := c.FormFile("updateFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uid, err := uuid.NewRandom()
|
uid, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFile := fmt.Sprintf("%s/update_%s.raucb", srv.tmpdir, uid.String())
|
updateFile := fmt.Sprintf("%s/update_%s.raucb", srv.tmpdir, uid.String())
|
||||||
|
|
||||||
err = c.SaveUploadedFile(file, updateFile)
|
err = c.SaveFile(file, updateFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = srv.updater.RunRauc(updateFile)
|
err = srv.raucUpdater.RunRauc(updateFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
writeStatus(c, true, "Update started")
|
writeStatus(c, true, "Update started")
|
||||||
|
} else if errors.Is(err, util.ErrAlreadyRunning) {
|
||||||
|
return fiber.NewError(fiber.StatusConflict, "already running")
|
||||||
} else {
|
} else {
|
||||||
c.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:operation GET /status getStatus
|
func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error {
|
||||||
//
|
c.Context().SetStatusCode(200)
|
||||||
// # Get the current status of the RAUC updater
|
_ = c.JSON(srv.raucUpdater.GetStatus())
|
||||||
//
|
return nil
|
||||||
// ---
|
|
||||||
// produces: [application/json]
|
|
||||||
// responses:
|
|
||||||
//
|
|
||||||
// 200:
|
|
||||||
// description: Ok
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/RaucStatus"
|
|
||||||
// 500:
|
|
||||||
// description: Server Error
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/Error"
|
|
||||||
func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, srv.updater.GetStatus())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:operation GET /info getInfo
|
func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error {
|
||||||
//
|
go util.Reboot(5 * time.Second)
|
||||||
// # Get the current system info
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// produces: [application/json]
|
|
||||||
// responses:
|
|
||||||
//
|
|
||||||
// 200:
|
|
||||||
// description: Ok
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/SystemInfo"
|
|
||||||
// 500:
|
|
||||||
// description: Server Error
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/Error"
|
|
||||||
func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) {
|
|
||||||
info, err := srv.sysinfo.GetSysinfo()
|
|
||||||
if err != nil {
|
|
||||||
c.Error(err)
|
|
||||||
} else {
|
|
||||||
c.JSON(http.StatusOK, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:operation POST /reboot startReboot
|
|
||||||
//
|
|
||||||
// # Reboot the system
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// produces: [application/json]
|
|
||||||
// responses:
|
|
||||||
//
|
|
||||||
// 200:
|
|
||||||
// description: Ok
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/StatusMessage"
|
|
||||||
// 500:
|
|
||||||
// description: Server Error
|
|
||||||
// schema:
|
|
||||||
// $ref: "#/definitions/Error"
|
|
||||||
func (srv *SEBRAUCServer) controllerReboot(c *gin.Context) {
|
|
||||||
go util.Reboot(srv.config.Commands.Reboot, 5*time.Second)
|
|
||||||
|
|
||||||
writeStatus(c, true, "System is rebooting")
|
writeStatus(c, true, "System is rebooting")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// controllerError throws an error for testing
|
func errorHandler(c *fiber.Ctx, err error) error {
|
||||||
func (srv *SEBRAUCServer) controllerError(c *gin.Context) {
|
// API error handling
|
||||||
c.Error(util.HttpErrNew("error test", http.StatusBadRequest))
|
if strings.HasPrefix(c.Path(), "/api") {
|
||||||
|
writeStatus(c, false, err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// controllerPanic panics for testing
|
func writeStatus(c *fiber.Ctx, success bool, msg string) {
|
||||||
func (srv *SEBRAUCServer) controllerPanic(c *gin.Context) {
|
_ = c.JSON(statusMessage{
|
||||||
panic(errors.New("panic message"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeStatus(c *gin.Context, success bool, msg string) {
|
|
||||||
c.JSON(http.StatusOK, model.StatusMessage{
|
|
||||||
Success: success,
|
Success: success,
|
||||||
Msg: msg,
|
Msg: msg,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if success {
|
||||||
|
c.Context().SetStatusCode(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/config"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testServer struct {
|
|
||||||
srv *SEBRAUCServer
|
|
||||||
router *gin.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestServer() *testServer {
|
|
||||||
return newTestServerCfg(config.GetDefault())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestServerCfg(cfg *config.Config) *testServer {
|
|
||||||
sebraucServer := NewServer(cfg)
|
|
||||||
router := sebraucServer.getRouter()
|
|
||||||
|
|
||||||
return &testServer{
|
|
||||||
srv: sebraucServer,
|
|
||||||
router: router,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *testServer) testRequest(t assert.TestingT, method string, url string,
|
|
||||||
body io.Reader, contentType string,
|
|
||||||
) *httptest.ResponseRecorder {
|
|
||||||
req, err := http.NewRequest(method, url, body)
|
|
||||||
req.Header.Set("Content-Type", contentType)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
srv.router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdate(t *testing.T) {
|
|
||||||
fixtures.ResetEnv()
|
|
||||||
defer fixtures.ResetEnv()
|
|
||||||
util.RemoveTmpdir("")
|
|
||||||
|
|
||||||
os.Setenv("RAUC_MOCK_TEST", "1")
|
|
||||||
|
|
||||||
srv := newTestServer()
|
|
||||||
|
|
||||||
updateContent := []byte("mock update file")
|
|
||||||
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
part, err := writer.CreateFormFile("updateFile", "update.raucb")
|
|
||||||
assert.Nil(t, err)
|
|
||||||
_, err = part.Write(updateContent)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
err = writer.Close()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
w := srv.testRequest(t, "POST", "/api/update", body, writer.FormDataContentType())
|
|
||||||
|
|
||||||
assert.Equal(t, 200, w.Code)
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
assert.Equal(t,
|
|
||||||
`{"success":true,"msg":"Update started"}`,
|
|
||||||
w.Body.String(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Find update file
|
|
||||||
tmpdir := util.GetTmpdir("")
|
|
||||||
//nolint:lll
|
|
||||||
updateFileExp := regexp.MustCompile(
|
|
||||||
`update_[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\.raucb`)
|
|
||||||
updateFile := ""
|
|
||||||
|
|
||||||
tmpdirObj, err := os.Open(tmpdir)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
list, err := tmpdirObj.ReadDir(-1)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
for _, f := range list {
|
|
||||||
if updateFileExp.MatchString(f.Name()) {
|
|
||||||
updateFile = filepath.Join(tmpdir, f.Name())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.NotEmpty(t, updateFile, "update file not found")
|
|
||||||
|
|
||||||
// Check update file
|
|
||||||
content, err := os.ReadFile(updateFile)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, updateContent, content)
|
|
||||||
|
|
||||||
// Wait for update to complete
|
|
||||||
time.Sleep(1000 * time.Millisecond)
|
|
||||||
|
|
||||||
// Update file should be removed when update is completed
|
|
||||||
assert.NoFileExists(t, updateFile)
|
|
||||||
|
|
||||||
// Get final status
|
|
||||||
w = srv.testRequest(t, "GET", "/api/status", nil, "")
|
|
||||||
|
|
||||||
assert.Equal(t, 200, w.Code)
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
//nolint:lll
|
|
||||||
assert.Equal(t,
|
|
||||||
`{"installing":false,"percent":100,"message":"Installing done.","last_error":"","log":"0% Installing\n0% Determining slot states\n20% Determining slot states done.\n20% Checking bundle\n20% Verifying signature\n40% Verifying signature done.\n40% Checking bundle done.\n40% Checking manifest contents\n60% Checking manifest contents done.\n60% Determining target install group\n80% Determining target install group done.\n80% Updating slots\n80% Checking slot rootfs.0\n90% Checking slot rootfs.0 done.\n90% Copying image to rootfs.0\n100% Copying image to rootfs.0 done.\n100% Updating slots done.\n100% Installing done.\nInstalling `+"`/app/tsgrain-update-raspberrypi3.raucb`"+` succeeded\n"}`,
|
|
||||||
w.Body.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatus(t *testing.T) {
|
|
||||||
srv := newTestServer()
|
|
||||||
w := srv.testRequest(t, "GET", "/api/status", nil, "")
|
|
||||||
|
|
||||||
assert.Equal(t, 200, w.Code)
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
assert.Equal(t,
|
|
||||||
`{"installing":false,"percent":0,"message":"","last_error":"","log":""}`,
|
|
||||||
w.Body.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInfo(t *testing.T) {
|
|
||||||
srv := newTestServer()
|
|
||||||
w := srv.testRequest(t, "GET", "/api/info", nil, "")
|
|
||||||
|
|
||||||
assert.Equal(t, 200, w.Code)
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
|
|
||||||
var info model.SystemInfo
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &info)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "TSGRain", info.RaucCompatible)
|
|
||||||
assert.Equal(t, "dev", info.RaucVariant)
|
|
||||||
assert.Len(t, info.RaucRootfs, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReboot(t *testing.T) {
|
|
||||||
srv := newTestServer()
|
|
||||||
testfile := "/tmp/sebrauc_reboot_test"
|
|
||||||
_ = os.Remove(testfile)
|
|
||||||
|
|
||||||
w := srv.testRequest(t, "POST", "/api/reboot", nil, "")
|
|
||||||
|
|
||||||
assert.Equal(t, 200, w.Code)
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
assert.Equal(t,
|
|
||||||
`{"success":true,"msg":"System is rebooting"}`,
|
|
||||||
w.Body.String(),
|
|
||||||
)
|
|
||||||
|
|
||||||
time.Sleep(5100 * time.Millisecond)
|
|
||||||
|
|
||||||
assert.FileExists(t, testfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuth(t *testing.T) {
|
|
||||||
testfiles := fixtures.GetTestfilesDir()
|
|
||||||
|
|
||||||
cfg := config.GetDefault()
|
|
||||||
cfg.Authentication.Enable = true
|
|
||||||
cfg.Authentication.PasswdFile = filepath.Join(testfiles, "htpasswd")
|
|
||||||
|
|
||||||
srv := newTestServerCfg(cfg)
|
|
||||||
|
|
||||||
t.Run("fail", func(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
req.SetBasicAuth("plain", "asdf")
|
|
||||||
srv.router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
||||||
assert.Empty(t, w.Body.String())
|
|
||||||
assert.Equal(t, "Basic realm=\"Authorization Required\"",
|
|
||||||
w.Header().Get("WWW-Authenticate"))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ok", func(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
req.SetBasicAuth("plain", "1234")
|
|
||||||
srv.router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
assert.NotEmpty(t, w.Body.String())
|
|
||||||
assert.Empty(t, w.Header().Get("WWW-Authenticate"))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
package stream
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
writeWait = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var ping = func(conn *websocket.Conn) error {
|
|
||||||
return conn.WriteMessage(websocket.PingMessage, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeBytes = func(conn *websocket.Conn, data []byte) error {
|
|
||||||
return conn.WriteMessage(websocket.TextMessage, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type client struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
onClose func(*client)
|
|
||||||
write chan []byte
|
|
||||||
id uint
|
|
||||||
once once
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClient(conn *websocket.Conn, id uint, onClose func(*client)) *client {
|
|
||||||
return &client{
|
|
||||||
conn: conn,
|
|
||||||
write: make(chan []byte, 1),
|
|
||||||
id: id,
|
|
||||||
onClose: onClose,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection.
|
|
||||||
func (c *client) Close() {
|
|
||||||
c.once.Do(func() {
|
|
||||||
c.conn.Close()
|
|
||||||
close(c.write)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifyClose closes the connection and notifies that the connection was closed.
|
|
||||||
func (c *client) NotifyClose() {
|
|
||||||
c.once.Do(func() {
|
|
||||||
c.conn.Close()
|
|
||||||
close(c.write)
|
|
||||||
c.onClose(c)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// startWriteHandler starts listening on the client connection.
|
|
||||||
// As we do not need anything from the client,
|
|
||||||
// we ignore incoming messages. Leaves the loop on errors.
|
|
||||||
func (c *client) startReading(pongWait time.Duration) {
|
|
||||||
defer c.NotifyClose()
|
|
||||||
c.conn.SetReadLimit(64)
|
|
||||||
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
|
||||||
c.conn.SetPongHandler(func(appData string) error {
|
|
||||||
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
for {
|
|
||||||
if _, _, err := c.conn.NextReader(); err != nil {
|
|
||||||
printWebSocketError("ReadError", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startWriteHandler starts the write loop. The method has the following tasks:
|
|
||||||
// * ping the client in the interval provided as parameter
|
|
||||||
// * write messages send by the channel to the client
|
|
||||||
// * on errors exit the loop.
|
|
||||||
func (c *client) startWriteHandler(pingPeriod time.Duration) {
|
|
||||||
pingTicker := time.NewTicker(pingPeriod)
|
|
||||||
defer func() {
|
|
||||||
c.NotifyClose()
|
|
||||||
pingTicker.Stop()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case message, ok := <-c.write:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err := writeBytes(c.conn, message); err != nil {
|
|
||||||
printWebSocketError("WriteError", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-pingTicker.C:
|
|
||||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err := ping(c.conn); err != nil {
|
|
||||||
printWebSocketError("PingError", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printWebSocketError(prefix string, err error) {
|
|
||||||
var closeError *websocket.CloseError
|
|
||||||
ok := errors.As(err, &closeError)
|
|
||||||
|
|
||||||
if ok && closeError != nil && (closeError.Code == 1000 || closeError.Code == 1001) {
|
|
||||||
// normal closure
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("WebSocket:", prefix, err)
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package stream
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Modified version of sync.Once
|
|
||||||
// (https://github.com/golang/go/blob/master/src/sync/once.go)
|
|
||||||
// This version unlocks the mutex early and therefore doesn't
|
|
||||||
// hold the lock while executing func f().
|
|
||||||
type once struct {
|
|
||||||
m sync.Mutex
|
|
||||||
done uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *once) Do(f func()) {
|
|
||||||
if atomic.LoadUint32(&o.done) == 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if o.mayExecute() {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *once) mayExecute() bool {
|
|
||||||
o.m.Lock()
|
|
||||||
defer o.m.Unlock()
|
|
||||||
if o.done == 0 {
|
|
||||||
atomic.StoreUint32(&o.done, 1)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package stream
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Execute(t *testing.T) {
|
|
||||||
executeOnce := once{}
|
|
||||||
execution := make(chan struct{})
|
|
||||||
fExecute := func() {
|
|
||||||
execution <- struct{}{}
|
|
||||||
}
|
|
||||||
go executeOnce.Do(fExecute)
|
|
||||||
go executeOnce.Do(fExecute)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-execution:
|
|
||||||
// expected
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
t.Fatal("Execute should be executed once")
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-execution:
|
|
||||||
t.Fatal("should only execute once")
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
// expected
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.False(t, executeOnce.mayExecute())
|
|
||||||
|
|
||||||
go executeOnce.Do(fExecute)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-execution:
|
|
||||||
t.Fatal("should only execute once")
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
// expected
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
package stream
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The API provides a handler for a WebSocket stream API.
|
|
||||||
type API struct {
|
|
||||||
clients map[uint]*client
|
|
||||||
lock sync.RWMutex
|
|
||||||
pingPeriod time.Duration
|
|
||||||
pongTimeout time.Duration
|
|
||||||
upgrader *websocket.Upgrader
|
|
||||||
counter *util.Counter
|
|
||||||
lastBroadcast []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new instance of API.
|
|
||||||
// pingPeriod: is the interval, in which is server sends the a ping to the client.
|
|
||||||
// pongTimeout: is the duration after the connection will be terminated,
|
|
||||||
// when the client does not respond with the pong command.
|
|
||||||
func New(pingPeriod, pongTimeout time.Duration, allowedWebSocketOrigins []string) *API {
|
|
||||||
return &API{
|
|
||||||
clients: make(map[uint]*client),
|
|
||||||
pingPeriod: pingPeriod,
|
|
||||||
pongTimeout: pingPeriod + pongTimeout,
|
|
||||||
upgrader: newUpgrader(allowedWebSocketOrigins),
|
|
||||||
counter: &util.Counter{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifyDeletedUser closes existing connections for the given user.
|
|
||||||
func (a *API) NotifyDeletedClient(userID uint) error {
|
|
||||||
a.lock.Lock()
|
|
||||||
defer a.lock.Unlock()
|
|
||||||
if client, ok := a.clients[userID]; ok {
|
|
||||||
client.Close()
|
|
||||||
delete(a.clients, userID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify notifies the clients with the given userID that a new messages was created.
|
|
||||||
func (a *API) Notify(userID uint, msg []byte) {
|
|
||||||
a.lock.RLock()
|
|
||||||
defer a.lock.RUnlock()
|
|
||||||
if client, ok := a.clients[userID]; ok {
|
|
||||||
client.write <- msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *API) Broadcast(msg []byte) {
|
|
||||||
a.lock.RLock()
|
|
||||||
defer a.lock.RUnlock()
|
|
||||||
for _, client := range a.clients {
|
|
||||||
client.write <- msg
|
|
||||||
}
|
|
||||||
a.lastBroadcast = msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *API) remove(remove *client) {
|
|
||||||
a.lock.Lock()
|
|
||||||
defer a.lock.Unlock()
|
|
||||||
delete(a.clients, remove.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *API) register(client *client) {
|
|
||||||
a.lock.Lock()
|
|
||||||
defer a.lock.Unlock()
|
|
||||||
a.clients[client.id] = client
|
|
||||||
|
|
||||||
// Send new clients the last broadcast so they get the current state
|
|
||||||
if a.lastBroadcast != nil {
|
|
||||||
client.write <- a.lastBroadcast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *API) Handle(ctx *gin.Context) {
|
|
||||||
conn, err := a.upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := newClient(conn, a.counter.Increment(), a.remove)
|
|
||||||
a.register(client)
|
|
||||||
go client.startReading(a.pongTimeout)
|
|
||||||
go client.startWriteHandler(a.pingPeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes all client connections and stops answering new connections.
|
|
||||||
func (a *API) Close() {
|
|
||||||
a.lock.Lock()
|
|
||||||
defer a.lock.Unlock()
|
|
||||||
|
|
||||||
for _, client := range a.clients {
|
|
||||||
client.Close()
|
|
||||||
}
|
|
||||||
for k := range a.clients {
|
|
||||||
delete(a.clients, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAllowedOrigin(r *http.Request, allowedOrigins []*regexp.Regexp) bool {
|
|
||||||
origin := r.Header.Get("origin")
|
|
||||||
if origin == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(origin)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(u.Host, r.Host) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, allowedOrigin := range allowedOrigins {
|
|
||||||
if allowedOrigin.Match([]byte(strings.ToLower(u.Hostname()))) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUpgrader(allowedWebSocketOrigins []string) *websocket.Upgrader {
|
|
||||||
compiledAllowedOrigins := compileAllowedWebSocketOrigins(allowedWebSocketOrigins)
|
|
||||||
|
|
||||||
return &websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
if mode.IsDev() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return isAllowedOrigin(r, compiledAllowedOrigins)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func compileAllowedWebSocketOrigins(allowedOrigins []string) []*regexp.Regexp {
|
|
||||||
var compiledAllowedOrigins []*regexp.Regexp
|
|
||||||
for _, origin := range allowedOrigins {
|
|
||||||
compiledAllowedOrigins = append(compiledAllowedOrigins,
|
|
||||||
regexp.MustCompile(origin))
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiledAllowedOrigins
|
|
||||||
}
|
|
|
@ -1,472 +0,0 @@
|
||||||
package stream
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
|
|
||||||
"github.com/fortytw2/leaktest"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFailureOnNormalHttpRequest(t *testing.T) {
|
|
||||||
mode.Set(mode.TestDev)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
defer api.Close()
|
|
||||||
|
|
||||||
resp, err := http.Get(server.URL)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteMessageFails(t *testing.T) {
|
|
||||||
mode.Set(mode.TestDev)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
oldWrite := writeBytes
|
|
||||||
// try emulate an write error, mostly this should kill the ReadMessage
|
|
||||||
// goroutine first but you'll never know.
|
|
||||||
writeBytes = func(conn *websocket.Conn, data []byte) error {
|
|
||||||
return errors.New("asd")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
writeBytes = oldWrite
|
|
||||||
}()
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
defer api.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
user := testClient(t, wsURL)
|
|
||||||
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
client := getClient(api, 1)
|
|
||||||
assert.NotNil(t, client)
|
|
||||||
|
|
||||||
api.Notify(1, []byte("HI"))
|
|
||||||
user.expectNoMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWritePingFails(t *testing.T) {
|
|
||||||
mode.Set(mode.TestDev)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
oldPing := ping
|
|
||||||
// try emulate an write error, mostly this should kill the ReadMessage
|
|
||||||
// gorouting first but you'll never know.
|
|
||||||
ping = func(conn *websocket.Conn) error {
|
|
||||||
return errors.New("asd")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
ping = oldPing
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer leaktest.CheckTimeout(t, 10*time.Second)()
|
|
||||||
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer api.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
user := testClient(t, wsURL)
|
|
||||||
defer user.conn.Close()
|
|
||||||
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
client := getClient(api, 1)
|
|
||||||
|
|
||||||
assert.NotNil(t, client)
|
|
||||||
|
|
||||||
time.Sleep(api.pingPeriod) // waiting for ping
|
|
||||||
|
|
||||||
api.Notify(1, []byte("HI"))
|
|
||||||
user.expectNoMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
|
||||||
mode.Set(mode.TestDev)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
defer api.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
user := createClient(t, wsURL)
|
|
||||||
defer user.conn.Close()
|
|
||||||
|
|
||||||
ping := make(chan bool)
|
|
||||||
oldPingHandler := user.conn.PingHandler()
|
|
||||||
user.conn.SetPingHandler(func(appData string) error {
|
|
||||||
err := oldPingHandler(appData)
|
|
||||||
ping <- true
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
|
|
||||||
startReading(user)
|
|
||||||
|
|
||||||
expectNoMessage(user)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
assert.Fail(t, "Expected ping but there was one :(")
|
|
||||||
case <-ping:
|
|
||||||
// expected
|
|
||||||
}
|
|
||||||
|
|
||||||
expectNoMessage(user)
|
|
||||||
api.Notify(1, []byte("HI"))
|
|
||||||
user.expectMessage([]byte("HI"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloseClientOnNotReading(t *testing.T) {
|
|
||||||
mode.Set(mode.TestDev)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
defer api.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
ws, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
resp.Body.Close()
|
|
||||||
defer ws.Close()
|
|
||||||
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
assert.NotNil(t, getClient(api, 1))
|
|
||||||
|
|
||||||
time.Sleep(api.pingPeriod + api.pongTimeout)
|
|
||||||
|
|
||||||
assert.Nil(t, getClient(api, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageDirectlyAfterConnect(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
defer api.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
user := testClient(t, wsURL)
|
|
||||||
defer user.conn.Close()
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
api.Notify(1, []byte("msg"))
|
|
||||||
user.expectMessage([]byte("msg"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteClientShouldCloseConnection(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
defer api.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
user := testClient(t, wsURL)
|
|
||||||
defer user.conn.Close()
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
api.Notify(1, []byte("HI"))
|
|
||||||
user.expectMessage([]byte("HI"))
|
|
||||||
|
|
||||||
assert.Nil(t, api.NotifyDeletedClient(1))
|
|
||||||
|
|
||||||
api.Notify(1, []byte("HI"))
|
|
||||||
user.expectNoMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotify(t *testing.T) {
|
|
||||||
mode.Set(mode.TestDev)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
client1 := testClient(t, wsURL)
|
|
||||||
defer client1.conn.Close()
|
|
||||||
|
|
||||||
client2 := testClient(t, wsURL)
|
|
||||||
defer client2.conn.Close()
|
|
||||||
|
|
||||||
client3 := testClient(t, wsURL)
|
|
||||||
defer client3.conn.Close()
|
|
||||||
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
api.Notify(1, []byte("msg"))
|
|
||||||
expectMessage([]byte("msg"), client1)
|
|
||||||
expectNoMessage(client2)
|
|
||||||
expectNoMessage(client3)
|
|
||||||
|
|
||||||
assert.Nil(t, api.NotifyDeletedClient(1))
|
|
||||||
|
|
||||||
api.Notify(1, []byte("msg"))
|
|
||||||
expectNoMessage(client1)
|
|
||||||
expectNoMessage(client2)
|
|
||||||
expectNoMessage(client3)
|
|
||||||
|
|
||||||
api.Notify(2, []byte("msg"))
|
|
||||||
expectNoMessage(client1)
|
|
||||||
expectMessage([]byte("msg"), client2)
|
|
||||||
expectNoMessage(client3)
|
|
||||||
|
|
||||||
api.Notify(3, []byte("msg"))
|
|
||||||
expectNoMessage(client1)
|
|
||||||
expectNoMessage(client2)
|
|
||||||
expectMessage([]byte("msg"), client3)
|
|
||||||
|
|
||||||
api.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBroadcast(t *testing.T) {
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
client1 := testClient(t, wsURL)
|
|
||||||
defer client1.conn.Close()
|
|
||||||
|
|
||||||
client2 := testClient(t, wsURL)
|
|
||||||
defer client2.conn.Close()
|
|
||||||
|
|
||||||
client3 := testClient(t, wsURL)
|
|
||||||
defer client3.conn.Close()
|
|
||||||
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
testMsg1 := []byte("hello1")
|
|
||||||
api.Broadcast(testMsg1)
|
|
||||||
expectMessage(testMsg1, client1, client2, client3)
|
|
||||||
|
|
||||||
assert.Nil(t, api.NotifyDeletedClient(1))
|
|
||||||
|
|
||||||
testMsg2 := []byte("hello2")
|
|
||||||
api.Broadcast(testMsg2)
|
|
||||||
expectNoMessage(client1)
|
|
||||||
expectMessage(testMsg2, client2, client3)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLastBroadcast(t *testing.T) {
|
|
||||||
defer leaktest.Check(t)()
|
|
||||||
server, api := bootTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
wsURL := wsURL(server.URL)
|
|
||||||
|
|
||||||
testMsg1 := []byte("hello1")
|
|
||||||
api.Broadcast(testMsg1)
|
|
||||||
|
|
||||||
client1 := testClient(t, wsURL)
|
|
||||||
defer client1.conn.Close()
|
|
||||||
|
|
||||||
client2 := testClient(t, wsURL)
|
|
||||||
defer client2.conn.Close()
|
|
||||||
|
|
||||||
// the server may take some time to register the client
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
expectMessage(testMsg1, client1, client2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_sameOrigin_returnsTrue(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
|
||||||
req.Header.Set("Origin", "http://example.com")
|
|
||||||
actual := isAllowedOrigin(req, nil)
|
|
||||||
assert.True(t, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_sameOrigin_returnsTrue_withCustomPort(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com:8080/stream", nil)
|
|
||||||
req.Header.Set("Origin", "http://example.com:8080")
|
|
||||||
actual := isAllowedOrigin(req, nil)
|
|
||||||
assert.True(t, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_isAllowedOrigin_withoutAllowedOrigins_failsWhenNotSameOrigin(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
|
||||||
req.Header.Set("Origin", "http://gorify.example.com")
|
|
||||||
actual := isAllowedOrigin(req, nil)
|
|
||||||
assert.False(t, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_isAllowedOriginMatching(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
compiledAllowedOrigins := compileAllowedWebSocketOrigins(
|
|
||||||
[]string{"go.{4}\\.example\\.com", "go\\.example\\.com"},
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.me/stream", nil)
|
|
||||||
req.Header.Set("Origin", "http://gorify.example.com")
|
|
||||||
assert.True(t, isAllowedOrigin(req, compiledAllowedOrigins))
|
|
||||||
|
|
||||||
req.Header.Set("Origin", "http://go.example.com")
|
|
||||||
assert.True(t, isAllowedOrigin(req, compiledAllowedOrigins))
|
|
||||||
|
|
||||||
req.Header.Set("Origin", "http://hello.example.com")
|
|
||||||
assert.False(t, isAllowedOrigin(req, compiledAllowedOrigins))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_emptyOrigin_returnsTrue(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
|
||||||
actual := isAllowedOrigin(req, nil)
|
|
||||||
assert.True(t, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_otherOrigin_returnsFalse(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
|
||||||
req.Header.Set("Origin", "http://otherexample.de")
|
|
||||||
actual := isAllowedOrigin(req, nil)
|
|
||||||
assert.False(t, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_invalidOrigin_returnsFalse(t *testing.T) {
|
|
||||||
mode.Set(mode.Prod)
|
|
||||||
defer mode.Set(mode.Dev)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
|
||||||
req.Header.Set("Origin", "http\\://otherexample.de")
|
|
||||||
actual := isAllowedOrigin(req, nil)
|
|
||||||
assert.False(t, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_compileAllowedWebSocketOrigins(t *testing.T) {
|
|
||||||
assert.Equal(t, 0, len(compileAllowedWebSocketOrigins([]string{})))
|
|
||||||
assert.Equal(t, 3, len(compileAllowedWebSocketOrigins([]string{"^.*$", "", "abc"})))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClient(api *API, user uint) *client {
|
|
||||||
api.lock.RLock()
|
|
||||||
defer api.lock.RUnlock()
|
|
||||||
|
|
||||||
return api.clients[user]
|
|
||||||
}
|
|
||||||
|
|
||||||
func testClient(t *testing.T, url string) *testingClient {
|
|
||||||
client := createClient(t, url)
|
|
||||||
startReading(client)
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func startReading(client *testingClient) {
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
_, payload, err := client.conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.readMessage <- payload
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClient(t *testing.T, url string) *testingClient {
|
|
||||||
ws, resp, err := websocket.DefaultDialer.Dial(url, nil)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
readMessages := make(chan []byte)
|
|
||||||
|
|
||||||
return &testingClient{conn: ws, readMessage: readMessages, t: t}
|
|
||||||
}
|
|
||||||
|
|
||||||
type testingClient struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
readMessage chan []byte
|
|
||||||
t *testing.T
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testingClient) expectMessage(expected []byte) {
|
|
||||||
select {
|
|
||||||
case <-time.After(50 * time.Millisecond):
|
|
||||||
assert.Fail(c.t, "Expected message but none was send :(")
|
|
||||||
case actual := <-c.readMessage:
|
|
||||||
assert.Equal(c.t, expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func expectMessage(expected []byte, clients ...*testingClient) {
|
|
||||||
for _, client := range clients {
|
|
||||||
client.expectMessage(expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func expectNoMessage(clients ...*testingClient) {
|
|
||||||
for _, client := range clients {
|
|
||||||
client.expectNoMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testingClient) expectNoMessage() {
|
|
||||||
select {
|
|
||||||
case <-time.After(50 * time.Millisecond):
|
|
||||||
// no message == as expected
|
|
||||||
case msg := <-c.readMessage:
|
|
||||||
assert.Fail(c.t, "Expected NO message but there was one :(", fmt.Sprint(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bootTestServer() (*httptest.Server, *API) {
|
|
||||||
r := gin.New()
|
|
||||||
// ping every 500 ms, and the client has 500 ms to respond
|
|
||||||
api := New(500*time.Millisecond, 500*time.Millisecond, []string{})
|
|
||||||
|
|
||||||
r.GET("/", api.Handle)
|
|
||||||
server := httptest.NewServer(r)
|
|
||||||
return server, api
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsURL(httpURL string) string {
|
|
||||||
return "ws" + strings.TrimPrefix(httpURL, "http")
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package swagger
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed swagger.html
|
|
||||||
var swaggerHtml []byte
|
|
||||||
|
|
||||||
//go:embed swagger.yaml
|
|
||||||
var swaggerYaml []byte
|
|
||||||
|
|
||||||
func Register(r gin.IRouter) {
|
|
||||||
swg := r.Group("/api/swagger", middleware.Cache)
|
|
||||||
|
|
||||||
swg.GET("/", func(c *gin.Context) {
|
|
||||||
c.Data(200, "text/html", swaggerHtml)
|
|
||||||
})
|
|
||||||
swg.GET("/swagger.yaml", func(c *gin.Context) {
|
|
||||||
c.Data(200, "text/yaml", swaggerYaml)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>SEBRAUC API documentation</title>
|
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<redoc spec-url="swagger.yaml"></redoc>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,228 +0,0 @@
|
||||||
definitions:
|
|
||||||
Error:
|
|
||||||
description: The Error contains error relevant information.
|
|
||||||
properties:
|
|
||||||
error:
|
|
||||||
description: The general error message according to HTTP specification.
|
|
||||||
example: Unauthorized
|
|
||||||
type: string
|
|
||||||
msg:
|
|
||||||
description: Concrete error message.
|
|
||||||
example: already running
|
|
||||||
type: string
|
|
||||||
status_code:
|
|
||||||
description: The http error code.
|
|
||||||
example: 500
|
|
||||||
format: int64
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- error
|
|
||||||
- status_code
|
|
||||||
- msg
|
|
||||||
title: Error model
|
|
||||||
type: object
|
|
||||||
RaucStatus:
|
|
||||||
description: RaucStatus contains information about the current RAUC updater status.
|
|
||||||
properties:
|
|
||||||
installing:
|
|
||||||
description: True if the installer is running
|
|
||||||
type: boolean
|
|
||||||
last_error:
|
|
||||||
description: Installation error message
|
|
||||||
example: "Failed to check bundle identifier: Invalid identifier."
|
|
||||||
type: string
|
|
||||||
log:
|
|
||||||
description: Full command line output of the current installation
|
|
||||||
example: 0% Installing 0% Determining slot states 20% Determining slot states
|
|
||||||
done
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
description: Current installation step
|
|
||||||
example: Copying image to rootfs.0
|
|
||||||
type: string
|
|
||||||
percent:
|
|
||||||
description: Installation progress
|
|
||||||
format: int64
|
|
||||||
maximum: 100
|
|
||||||
minimum: 0
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- installing
|
|
||||||
- percent
|
|
||||||
- message
|
|
||||||
- last_error
|
|
||||||
- log
|
|
||||||
title: RaucStatus model
|
|
||||||
type: object
|
|
||||||
Rootfs:
|
|
||||||
properties:
|
|
||||||
bootable:
|
|
||||||
description: Is the filesystem bootable?
|
|
||||||
type: boolean
|
|
||||||
booted:
|
|
||||||
description: Is the filesystem booted?
|
|
||||||
type: boolean
|
|
||||||
device:
|
|
||||||
description: Block device
|
|
||||||
example: /dev/mmcblk0p2
|
|
||||||
type: string
|
|
||||||
mountpoint:
|
|
||||||
description: Mount path (null when not mounted)
|
|
||||||
example: /
|
|
||||||
type: string
|
|
||||||
primary:
|
|
||||||
description: Is the filesystem the next boot target?
|
|
||||||
type: boolean
|
|
||||||
type:
|
|
||||||
description: Filesystem
|
|
||||||
example: ext4
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- device
|
|
||||||
- type
|
|
||||||
- mountpoint
|
|
||||||
- bootable
|
|
||||||
- booted
|
|
||||||
- primary
|
|
||||||
type: object
|
|
||||||
StatusMessage:
|
|
||||||
description: StatusMessage contains the status of an operation.
|
|
||||||
properties:
|
|
||||||
msg:
|
|
||||||
description: Status message text
|
|
||||||
example: Update started
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
description: Is operation successful?
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- success
|
|
||||||
- msg
|
|
||||||
title: StatusMessage model
|
|
||||||
type: object
|
|
||||||
SystemInfo:
|
|
||||||
description: SystemInfo contains information about the running system.
|
|
||||||
properties:
|
|
||||||
hostname:
|
|
||||||
description: Hostname of the system
|
|
||||||
example: raspberrypi3
|
|
||||||
type: string
|
|
||||||
os_name:
|
|
||||||
description: Name of the os distribution
|
|
||||||
example: Poky
|
|
||||||
type: string
|
|
||||||
os_version:
|
|
||||||
description: Operating system version
|
|
||||||
example: 1.0.2
|
|
||||||
type: string
|
|
||||||
rauc_compatible:
|
|
||||||
description: Compatible firmware name
|
|
||||||
example: Poky
|
|
||||||
type: string
|
|
||||||
rauc_rootfs:
|
|
||||||
additionalProperties:
|
|
||||||
$ref: "#/definitions/Rootfs"
|
|
||||||
description: List of RAUC root filesystems
|
|
||||||
type: object
|
|
||||||
rauc_variant:
|
|
||||||
description: Compatible firmware variant
|
|
||||||
example: rpi-prod
|
|
||||||
type: string
|
|
||||||
uptime:
|
|
||||||
description: System uptime in seconds
|
|
||||||
example: 5832
|
|
||||||
format: int64
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- hostname
|
|
||||||
- os_name
|
|
||||||
- os_version
|
|
||||||
- uptime
|
|
||||||
- rauc_compatible
|
|
||||||
- rauc_variant
|
|
||||||
- rauc_rootfs
|
|
||||||
title: SystemInfo model
|
|
||||||
type: object
|
|
||||||
info:
|
|
||||||
description: REST API for the SEBRAUC firmware updater
|
|
||||||
license:
|
|
||||||
name: MIT
|
|
||||||
title: SEBRAUC
|
|
||||||
version: 0.2.0
|
|
||||||
paths:
|
|
||||||
/info:
|
|
||||||
get:
|
|
||||||
description: Get the current system info
|
|
||||||
operationId: getInfo
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Ok
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/SystemInfo"
|
|
||||||
"500":
|
|
||||||
description: Server Error
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Error"
|
|
||||||
/reboot:
|
|
||||||
post:
|
|
||||||
description: Reboot the system
|
|
||||||
operationId: startReboot
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Ok
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/StatusMessage"
|
|
||||||
"500":
|
|
||||||
description: Server Error
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Error"
|
|
||||||
/status:
|
|
||||||
get:
|
|
||||||
description: Get the current status of the RAUC updater
|
|
||||||
operationId: getStatus
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Ok
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/RaucStatus"
|
|
||||||
"500":
|
|
||||||
description: Server Error
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Error"
|
|
||||||
/update:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- multipart/form-data
|
|
||||||
description: Start the update process
|
|
||||||
operationId: startUpdate
|
|
||||||
parameters:
|
|
||||||
- description: RAUC firmware image file (*.raucb)
|
|
||||||
in: formData
|
|
||||||
name: updateFile
|
|
||||||
required: true
|
|
||||||
type: file
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Ok
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/StatusMessage"
|
|
||||||
"409":
|
|
||||||
description: already running
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Error"
|
|
||||||
"500":
|
|
||||||
description: Server Error
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Error"
|
|
||||||
schemes:
|
|
||||||
- http
|
|
||||||
- https
|
|
||||||
swagger: "2.0"
|
|
|
@ -1,44 +0,0 @@
|
||||||
package swagger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSwagger(t *testing.T) {
|
|
||||||
router := gin.New()
|
|
||||||
Register(router)
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/api/swagger/", nil)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
assert.Equal(t, swaggerHtml, w.Body.Bytes())
|
|
||||||
assert.NotEmpty(t, w.Header().Get("Cache-Control"))
|
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
req, _ = http.NewRequest("GET", "/api/swagger/swagger.yaml", nil)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
assert.Equal(t, swaggerYaml, w.Body.Bytes())
|
|
||||||
assert.NotEmpty(t, w.Header().Get("Cache-Control"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSwaggerData(t *testing.T) {
|
|
||||||
assert.True(t, bytes.Contains(swaggerHtml,
|
|
||||||
[]byte("https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js")),
|
|
||||||
"HTML data missing",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, bytes.Contains(swaggerYaml,
|
|
||||||
[]byte("REST API for the SEBRAUC firmware updater")),
|
|
||||||
"YAML data missing",
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
package sysinfo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Sysinfo struct {
|
|
||||||
cmdRaucStatus string
|
|
||||||
releaseFile string
|
|
||||||
hostnameFile string
|
|
||||||
uptimeFile string
|
|
||||||
rexpName *regexp.Regexp
|
|
||||||
rexpVersion *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
type raucInfo struct {
|
|
||||||
Compatible string `json:"compatible"`
|
|
||||||
Variant string `json:"variant"`
|
|
||||||
Booted string `json:"booted"`
|
|
||||||
BootPrimary string `json:"boot_primary"`
|
|
||||||
Slots []map[string]raucFS `json:"slots"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type raucFS struct {
|
|
||||||
Class string `json:"class"`
|
|
||||||
Device string `json:"device"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Bootname string `json:"bootname"`
|
|
||||||
State string `json:"state"`
|
|
||||||
Mountpoint *string `json:"mountpoint"`
|
|
||||||
BootStatus string `json:"boot_status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type osRelease struct {
|
|
||||||
OsName string `json:"os_name"`
|
|
||||||
OsVersion string `json:"os_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var rexpUptime = regexp.MustCompile(`^\d+`)
|
|
||||||
|
|
||||||
func New(cmdRaucStatus string, releaseFile string, nameKey string, versionKey string,
|
|
||||||
hostnameFile string, uptimeFile string,
|
|
||||||
) *Sysinfo {
|
|
||||||
return &Sysinfo{
|
|
||||||
cmdRaucStatus: cmdRaucStatus,
|
|
||||||
releaseFile: releaseFile,
|
|
||||||
hostnameFile: hostnameFile,
|
|
||||||
uptimeFile: uptimeFile,
|
|
||||||
rexpName: regexp.MustCompile(
|
|
||||||
`(?m)^` + regexp.QuoteMeta(nameKey) + `="(.+)"`),
|
|
||||||
rexpVersion: regexp.MustCompile(
|
|
||||||
`(?m)^` + regexp.QuoteMeta(versionKey) + `="(.+)"`),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Default(cmdRaucStatus string) *Sysinfo {
|
|
||||||
return New(cmdRaucStatus, "/etc/os-release", "NAME", "VERSION",
|
|
||||||
"/etc/hostname", "/proc/uptime")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRaucInfo(raucInfoJson []byte) (raucInfo, error) {
|
|
||||||
res := raucInfo{}
|
|
||||||
err := json.Unmarshal(raucInfoJson, &res)
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sysinfo) parseOsRelease() (osRelease, error) {
|
|
||||||
osReleaseTxt, err := os.ReadFile(s.releaseFile)
|
|
||||||
if err != nil {
|
|
||||||
return osRelease{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nameMatch := s.rexpName.FindSubmatch(osReleaseTxt)
|
|
||||||
versionMatch := s.rexpVersion.FindSubmatch(osReleaseTxt)
|
|
||||||
|
|
||||||
name := ""
|
|
||||||
if nameMatch != nil {
|
|
||||||
name = string(nameMatch[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
version := ""
|
|
||||||
if versionMatch != nil {
|
|
||||||
version = string(versionMatch[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return osRelease{
|
|
||||||
OsName: name,
|
|
||||||
OsVersion: version,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapRootfs(rinf raucInfo) map[string]model.Rootfs {
|
|
||||||
res := make(map[string]model.Rootfs)
|
|
||||||
|
|
||||||
for _, slot := range rinf.Slots {
|
|
||||||
for name, fs := range slot {
|
|
||||||
if fs.Class == "rootfs" {
|
|
||||||
res[name] = model.Rootfs{
|
|
||||||
Device: fs.Device,
|
|
||||||
Type: fs.Type,
|
|
||||||
Bootname: fs.Bootname,
|
|
||||||
Mountpoint: fs.Mountpoint,
|
|
||||||
Bootable: fs.BootStatus == "good",
|
|
||||||
Booted: fs.State == "booted",
|
|
||||||
Primary: rinf.BootPrimary == name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFSNameFromBootname(rfslist map[string]model.Rootfs, bootname string) string {
|
|
||||||
for name, rfs := range rfslist {
|
|
||||||
if rfs.Bootname == bootname {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "n/a"
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapSysinfo(rinf raucInfo, osr osRelease, uptime int,
|
|
||||||
hostname string,
|
|
||||||
) model.SystemInfo {
|
|
||||||
rfslist := mapRootfs(rinf)
|
|
||||||
|
|
||||||
return model.SystemInfo{
|
|
||||||
Hostname: hostname,
|
|
||||||
OsName: osr.OsName,
|
|
||||||
OsVersion: osr.OsVersion,
|
|
||||||
Uptime: uptime,
|
|
||||||
RaucCompatible: rinf.Compatible,
|
|
||||||
RaucVariant: rinf.Variant,
|
|
||||||
RaucRootfs: rfslist,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sysinfo) getUptime() (int, error) {
|
|
||||||
uptimeRaw, err := os.ReadFile(s.uptimeFile)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uptimeChars := rexpUptime.Find(uptimeRaw)
|
|
||||||
return strconv.Atoi(string(uptimeChars))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sysinfo) getHostname() string {
|
|
||||||
hostname, err := os.ReadFile(s.hostnameFile)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(hostname))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sysinfo) GetSysinfo() (model.SystemInfo, error) {
|
|
||||||
cmd := util.CommandFromString(s.cmdRaucStatus)
|
|
||||||
rinfJson, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return model.SystemInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rinf, err := parseRaucInfo(rinfJson)
|
|
||||||
if err != nil {
|
|
||||||
return model.SystemInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
osinf, err := s.parseOsRelease()
|
|
||||||
if err != nil {
|
|
||||||
return model.SystemInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uptime, err := s.getUptime()
|
|
||||||
if err != nil {
|
|
||||||
return model.SystemInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := s.getHostname()
|
|
||||||
|
|
||||||
return mapSysinfo(rinf, osinf, uptime, hostname), nil
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
package sysinfo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` +
|
|
||||||
`"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` +
|
|
||||||
`"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` +
|
|
||||||
`"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` +
|
|
||||||
`{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` +
|
|
||||||
`"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}`
|
|
||||||
|
|
||||||
var mountRoot = "/"
|
|
||||||
|
|
||||||
var expectedRaucInfo = raucInfo{
|
|
||||||
Compatible: "TSGRain",
|
|
||||||
Variant: "dev",
|
|
||||||
Booted: "A",
|
|
||||||
BootPrimary: "rootfs.0",
|
|
||||||
Slots: []map[string]raucFS{
|
|
||||||
{
|
|
||||||
"rootfs.1": {
|
|
||||||
Class: "rootfs",
|
|
||||||
Device: "/dev/mmcblk0p3",
|
|
||||||
Type: "ext4",
|
|
||||||
Bootname: "B",
|
|
||||||
State: "inactive",
|
|
||||||
Mountpoint: nil,
|
|
||||||
BootStatus: "good",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rootfs.0": {
|
|
||||||
Class: "rootfs",
|
|
||||||
Device: "/dev/mmcblk0p2",
|
|
||||||
Type: "ext4",
|
|
||||||
Bootname: "A",
|
|
||||||
State: "booted",
|
|
||||||
Mountpoint: &mountRoot,
|
|
||||||
BootStatus: "good",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedRootfsList = map[string]model.Rootfs{
|
|
||||||
"rootfs.0": {
|
|
||||||
Device: "/dev/mmcblk0p2",
|
|
||||||
Type: "ext4",
|
|
||||||
Bootname: "A",
|
|
||||||
Mountpoint: &mountRoot,
|
|
||||||
Bootable: true,
|
|
||||||
Booted: true,
|
|
||||||
Primary: true,
|
|
||||||
},
|
|
||||||
"rootfs.1": {
|
|
||||||
Device: "/dev/mmcblk0p3",
|
|
||||||
Type: "ext4",
|
|
||||||
Bootname: "B",
|
|
||||||
Mountpoint: nil,
|
|
||||||
Bootable: true,
|
|
||||||
Booted: false,
|
|
||||||
Primary: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseRaucInfo(t *testing.T) {
|
|
||||||
info, err := parseRaucInfo([]byte(statusJson))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, expectedRaucInfo, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseOsRelease(t *testing.T) {
|
|
||||||
testfiles := fixtures.GetTestfilesDir()
|
|
||||||
osReleaseFile := filepath.Join(testfiles, "os-release")
|
|
||||||
|
|
||||||
si := New(testcmd.RaucStatus, osReleaseFile, "NAME", "VERSION",
|
|
||||||
"/etc/hostname", "/proc/uptime")
|
|
||||||
|
|
||||||
osRel, err := si.parseOsRelease()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := osRelease{
|
|
||||||
OsName: "TSGRain distro",
|
|
||||||
OsVersion: "0.0.1",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, expected, osRel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapRootfsList(t *testing.T) {
|
|
||||||
rootfsList := mapRootfs(expectedRaucInfo)
|
|
||||||
|
|
||||||
assert.Equal(t, expectedRootfsList, rootfsList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetFSNameFromBootname(t *testing.T) {
|
|
||||||
rootfsList := mapRootfs(expectedRaucInfo)
|
|
||||||
|
|
||||||
assert.Equal(t, "rootfs.0", getFSNameFromBootname(rootfsList, "A"))
|
|
||||||
assert.Equal(t, "rootfs.1", getFSNameFromBootname(rootfsList, "B"))
|
|
||||||
assert.Equal(t, "n/a", getFSNameFromBootname(rootfsList, "C"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSysinfo(t *testing.T) {
|
|
||||||
si := Default(testcmd.RaucStatus)
|
|
||||||
|
|
||||||
sysinfo, err := si.GetSysinfo()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Greater(t, sysinfo.Uptime, 0)
|
|
||||||
assert.Equal(t, "TSGRain", sysinfo.RaucCompatible)
|
|
||||||
assert.Equal(t, "dev", sysinfo.RaucVariant)
|
|
||||||
assert.Equal(t, expectedRootfsList, sysinfo.RaucRootfs)
|
|
||||||
}
|
|
11
src/util/commands.go
Normal file
11
src/util/commands.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
//go:build prod
|
||||||
|
// +build prod
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
const (
|
||||||
|
RebootCmd = "shutdown -r 0"
|
||||||
|
UpdateCmd = "rauc install"
|
||||||
|
|
||||||
|
TestMode = false
|
||||||
|
)
|
11
src/util/commands_mock.go
Normal file
11
src/util/commands_mock.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
//go:build !prod
|
||||||
|
// +build !prod
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
const (
|
||||||
|
RebootCmd = "touch /tmp/sebrauc_reboot_test"
|
||||||
|
UpdateCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock"
|
||||||
|
|
||||||
|
TestMode = true
|
||||||
|
)
|
|
@ -1,30 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
type Counter struct {
|
|
||||||
count uint
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Counter) Get() uint {
|
|
||||||
c.mutex.RLock()
|
|
||||||
defer c.mutex.RUnlock()
|
|
||||||
|
|
||||||
return c.count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Counter) Reset() {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
c.count = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Counter) Increment() uint {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
c.count++
|
|
||||||
return c.count
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCounter(t *testing.T) {
|
|
||||||
counter := Counter{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
incrementer := func() {
|
|
||||||
for i := 0; i < 50; i++ {
|
|
||||||
counter.Increment()
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go incrementer()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
assert.EqualValues(t, 5000, counter.Get())
|
|
||||||
}
|
|
|
@ -1,13 +1,8 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import "errors"
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAlreadyRunning = HttpErrNew("rauc already running", http.StatusConflict)
|
ErrAlreadyRunning = errors.New("rauc already running")
|
||||||
ErrFileDoesNotExist = errors.New("file does not exist")
|
ErrFileDoesNotExist = errors.New("file does not exist")
|
||||||
ErrPageNotFound = HttpErrNew("page not found", http.StatusNotFound)
|
|
||||||
ErrUnauthorized = HttpErrNew("unauthorized", http.StatusUnauthorized)
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
type HttpError interface {
|
|
||||||
error
|
|
||||||
StatusCode() int
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpErr struct {
|
|
||||||
err error
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func HttpErrWrap(e error, statusCode int) HttpError {
|
|
||||||
return &httpErr{
|
|
||||||
err: e,
|
|
||||||
statusCode: statusCode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HttpErrNew(msg string, statusCode int) HttpError {
|
|
||||||
return HttpErrWrap(errors.New(msg), statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *httpErr) Error() string {
|
|
||||||
if e.err == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *httpErr) Unwrap() error {
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *httpErr) StatusCode() int {
|
|
||||||
return e.statusCode
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package mode
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Dev for development mode.
|
|
||||||
Dev = "dev"
|
|
||||||
// Prod for production mode.
|
|
||||||
Prod = "prod"
|
|
||||||
// TestDev used for tests.
|
|
||||||
TestDev = "testdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
var currentMode = Dev
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Set(appmode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(newMode string) {
|
|
||||||
currentMode = newMode
|
|
||||||
updateGinMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the current mode.
|
|
||||||
func Get() string {
|
|
||||||
return currentMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDev returns true if the current mode is dev mode.
|
|
||||||
func IsDev() bool {
|
|
||||||
return Get() == Dev || Get() == TestDev
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateGinMode() {
|
|
||||||
switch Get() {
|
|
||||||
case Dev:
|
|
||||||
gin.SetMode(gin.DebugMode)
|
|
||||||
case TestDev:
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
case Prod:
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
default:
|
|
||||||
panic("unknown mode")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
//go:build !prod
|
|
||||||
// +build !prod
|
|
||||||
|
|
||||||
package mode
|
|
||||||
|
|
||||||
const appmode = Dev
|
|
|
@ -1,6 +0,0 @@
|
||||||
//go:build prod
|
|
||||||
// +build prod
|
|
||||||
|
|
||||||
package mode
|
|
||||||
|
|
||||||
const appmode = Prod
|
|
|
@ -1,35 +0,0 @@
|
||||||
package mode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDevMode(t *testing.T) {
|
|
||||||
Set(Dev)
|
|
||||||
assert.Equal(t, Get(), Dev)
|
|
||||||
assert.True(t, IsDev())
|
|
||||||
assert.Equal(t, gin.Mode(), gin.DebugMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTestDevMode(t *testing.T) {
|
|
||||||
Set(TestDev)
|
|
||||||
assert.Equal(t, Get(), TestDev)
|
|
||||||
assert.True(t, IsDev())
|
|
||||||
assert.Equal(t, gin.Mode(), gin.TestMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProdMode(t *testing.T) {
|
|
||||||
Set(Prod)
|
|
||||||
assert.Equal(t, Get(), Prod)
|
|
||||||
assert.False(t, IsDev())
|
|
||||||
assert.Equal(t, gin.Mode(), gin.ReleaseMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidMode(t *testing.T) {
|
|
||||||
assert.Panics(t, func() {
|
|
||||||
Set("asdasda")
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
type Broadcaster interface {
|
|
||||||
Broadcast(msg []byte)
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -26,24 +25,10 @@ func CreateDirIfNotExists(dirpath string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTmpdir(tmpdirPath string) string {
|
func GetTmpdir() (string, error) {
|
||||||
tmpdir := tmpdirPath
|
tmpdir := filepath.Join(os.TempDir(), tmpdirName)
|
||||||
// Default temporary directory
|
|
||||||
if tmpdirPath == "" {
|
|
||||||
tmpdir = filepath.Join(os.TempDir(), tmpdirName)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := CreateDirIfNotExists(tmpdir)
|
err := CreateDirIfNotExists(tmpdir)
|
||||||
if err != nil {
|
return tmpdir, err
|
||||||
panic(fmt.Sprintf("could not create tmpdir %s: %s", tmpdir, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmpdir
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveTmpdir(tmpdirPath string) {
|
|
||||||
tmpdir := GetTmpdir(tmpdirPath)
|
|
||||||
_ = os.RemoveAll(tmpdir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommandFromString(cmdString string) *exec.Cmd {
|
func CommandFromString(cmdString string) *exec.Cmd {
|
||||||
|
@ -56,41 +41,8 @@ func CommandFromString(cmdString string) *exec.Cmd {
|
||||||
return exec.Command(parts[0], parts[1:]...)
|
return exec.Command(parts[0], parts[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Reboot(rebootCmd string, t time.Duration) {
|
func Reboot(t time.Duration) {
|
||||||
time.Sleep(t)
|
time.Sleep(t)
|
||||||
cmd := CommandFromString(rebootCmd)
|
cmd := CommandFromString(RebootCmd)
|
||||||
_ = cmd.Run()
|
_ = cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindFile(explicitPath string, locations, endings []string) (string, error) {
|
|
||||||
if explicitPath != "" {
|
|
||||||
if !DoesFileExist(explicitPath) {
|
|
||||||
return "", fmt.Errorf("file %s not found", explicitPath)
|
|
||||||
}
|
|
||||||
return explicitPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
notFound := []string{}
|
|
||||||
|
|
||||||
for _, f := range locations {
|
|
||||||
if endings != nil {
|
|
||||||
for _, t := range endings {
|
|
||||||
fpath := f + "." + t
|
|
||||||
|
|
||||||
if DoesFileExist(fpath) {
|
|
||||||
return fpath, nil
|
|
||||||
} else {
|
|
||||||
notFound = append(notFound, fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if DoesFileExist(f) {
|
|
||||||
return f, nil
|
|
||||||
} else {
|
|
||||||
notFound = append(notFound, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("none of the following files found: %s",
|
|
||||||
strings.Join(notFound, "; "))
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,46 +41,27 @@ func TestDoesFileExist(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTmpdir(t *testing.T) {
|
func TestTmpdir(t *testing.T) {
|
||||||
tests := []struct {
|
td, err := GetTmpdir()
|
||||||
name string
|
if err != nil {
|
||||||
path string
|
panic(err)
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "default",
|
|
||||||
path: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom",
|
|
||||||
path: filepath.Join(os.TempDir(), "customTmpdir"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
tfile := filepath.Join(td, "test.txt")
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
f, err := os.Create(tfile)
|
||||||
td := GetTmpdir(tt.path)
|
if err != nil {
|
||||||
assert.DirExists(t, td)
|
panic(err)
|
||||||
|
|
||||||
tfile := filepath.Join(td, "test.txt")
|
|
||||||
f, err := os.Create(tfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.WriteString("Hello")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = f.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.FileExists(t, tfile)
|
|
||||||
|
|
||||||
RemoveTmpdir(tt.path)
|
|
||||||
assert.NoDirExists(t, td)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString("Hello")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.FileExists(t, tfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommandFromString(t *testing.T) {
|
func TestCommandFromString(t *testing.T) {
|
||||||
|
@ -124,78 +103,7 @@ func TestReboot(t *testing.T) {
|
||||||
testfile := "/tmp/sebrauc_reboot_test"
|
testfile := "/tmp/sebrauc_reboot_test"
|
||||||
_ = os.Remove(testfile)
|
_ = os.Remove(testfile)
|
||||||
|
|
||||||
Reboot(testcmd.Reboot, 0)
|
Reboot(0)
|
||||||
|
|
||||||
assert.FileExists(t, testfile)
|
assert.FileExists(t, testfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindFile(t *testing.T) {
|
|
||||||
testfiles := fixtures.GetTestfilesDir()
|
|
||||||
|
|
||||||
//nolint:lll
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
explicitPath string
|
|
||||||
locations []string
|
|
||||||
endings []string
|
|
||||||
expect string
|
|
||||||
expectErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "locations",
|
|
||||||
explicitPath: "",
|
|
||||||
locations: []string{filepath.Join(testfiles, "sebrauc")},
|
|
||||||
endings: []string{"yml", "toml"},
|
|
||||||
expect: filepath.Join(testfiles, "sebrauc.toml"),
|
|
||||||
expectErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "locations_nf",
|
|
||||||
explicitPath: "",
|
|
||||||
locations: []string{filepath.Join(testfiles, "banana")},
|
|
||||||
endings: []string{"yml", "toml"},
|
|
||||||
expect: "",
|
|
||||||
expectErr: errors.New("none of the following files found: src/fixtures/testfiles/banana.yml; src/fixtures/testfiles/banana.toml"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no_endings",
|
|
||||||
explicitPath: "",
|
|
||||||
locations: []string{filepath.Join(testfiles, "banana"), filepath.Join(testfiles, "os-release")},
|
|
||||||
endings: nil,
|
|
||||||
expect: filepath.Join(testfiles, "os-release"),
|
|
||||||
expectErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no_endings_nf",
|
|
||||||
explicitPath: "",
|
|
||||||
locations: []string{filepath.Join(testfiles, "banana"), filepath.Join(testfiles, "apple")},
|
|
||||||
endings: nil,
|
|
||||||
expect: "",
|
|
||||||
expectErr: errors.New("none of the following files found: src/fixtures/testfiles/banana; src/fixtures/testfiles/apple"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit",
|
|
||||||
explicitPath: filepath.Join(testfiles, "sebrauc.toml"),
|
|
||||||
locations: []string{filepath.Join(testfiles, "banana")},
|
|
||||||
endings: []string{"yml", "toml"},
|
|
||||||
expect: filepath.Join(testfiles, "sebrauc.toml"),
|
|
||||||
expectErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit_nf",
|
|
||||||
explicitPath: filepath.Join(testfiles, "banana.toml"),
|
|
||||||
locations: []string{filepath.Join(testfiles, "banana")},
|
|
||||||
endings: []string{"yml", "toml"},
|
|
||||||
expect: "",
|
|
||||||
expectErr: errors.New("file src/fixtures/testfiles/banana.toml not found"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
fpath, err := FindFile(tt.explicitPath, tt.locations, tt.endings)
|
|
||||||
assert.Equal(t, tt.expectErr, err)
|
|
||||||
assert.Equal(t, tt.expect, fpath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
VITE_VERSION=dev
|
||||||
VITE_API_HOST=127.0.0.1:8080
|
VITE_API_HOST=127.0.0.1:8080
|
||||||
|
|
|
@ -7,11 +7,7 @@
|
||||||
<title>SEBRAUC</title>
|
<title>SEBRAUC</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You have to enable JavaScript to use SEBRAUC.</noscript>
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script>
|
|
||||||
window.config = "%CONFIG%"
|
|
||||||
</script>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -4,9 +4,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview"
|
||||||
"lint": "tsc",
|
|
||||||
"format": "prettier --write ../"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^6.5.95",
|
"@mdi/js": "^6.5.95",
|
||||||
|
|
4
ui/pnpm-lock.yaml
generated
4
ui/pnpm-lock.yaml
generated
|
@ -1,4 +1,4 @@
|
||||||
lockfileVersion: 5.4
|
lockfileVersion: 5.3
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
"@mdi/js": ^6.5.95
|
"@mdi/js": ^6.5.95
|
||||||
|
@ -282,8 +282,6 @@ packages:
|
||||||
}
|
}
|
||||||
engines: {node: ">=6.0.0"}
|
engines: {node: ">=6.0.0"}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
|
||||||
"@babel/types": 7.16.0
|
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@babel/plugin-syntax-jsx/7.16.0:
|
/@babel/plugin-syntax-jsx/7.16.0:
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
import {Component} from "preact"
|
|
||||||
import {SystemInfo} from "../../sebrauc-client"
|
|
||||||
import {sebraucApi} from "../../util/apiUrls"
|
|
||||||
import {secondsToString} from "../../util/functions"
|
|
||||||
import Icon from "../Icon/Icon"
|
|
||||||
import {
|
|
||||||
mdiAlphaVCircleOutline,
|
|
||||||
mdiCheckCircleOutline,
|
|
||||||
mdiCircleOutline,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCloseCircleOutline,
|
|
||||||
mdiMonitor,
|
|
||||||
mdiPenguin,
|
|
||||||
mdiTagMultipleOutline,
|
|
||||||
mdiTagOutline,
|
|
||||||
} from "@mdi/js"
|
|
||||||
import colors from "../../util/colors"
|
|
||||||
|
|
||||||
type Props = {}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
sysinfo: SystemInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SysinfoCard extends Component<Props, State> {
|
|
||||||
private fetchTimeout: number | undefined
|
|
||||||
|
|
||||||
constructor(props?: Props | undefined, context?: any) {
|
|
||||||
super(props, context)
|
|
||||||
this.fetchInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchInfo = () => {
|
|
||||||
sebraucApi
|
|
||||||
.getInfo()
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status == 200) {
|
|
||||||
this.setState({sysinfo: response.data})
|
|
||||||
} else {
|
|
||||||
console.log("error fetching info", response.data)
|
|
||||||
this.fetchTimeout = window.setTimeout(this.fetchInfo, 3000)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
console.log("error fetching info", reason)
|
|
||||||
this.fetchTimeout = window.setTimeout(this.fetchInfo, 3000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSysinfo() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="card">
|
|
||||||
<p class="top">System information</p>
|
|
||||||
<table class="table no-bottom-border">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Icon icon={mdiMonitor} /> Hostname
|
|
||||||
</td>
|
|
||||||
<td>{this.state.sysinfo.hostname}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Icon icon={mdiPenguin} /> Operating system
|
|
||||||
</td>
|
|
||||||
<td>{this.state.sysinfo.os_name}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Icon icon={mdiAlphaVCircleOutline} /> OS version
|
|
||||||
</td>
|
|
||||||
<td>{this.state.sysinfo.os_version}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Icon icon={mdiClockOutline} /> Uptime
|
|
||||||
</td>
|
|
||||||
<td>{secondsToString(this.state.sysinfo.uptime)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Icon icon={mdiTagOutline} /> Compatible FW
|
|
||||||
</td>
|
|
||||||
<td>{this.state.sysinfo.rauc_compatible}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Icon icon={mdiTagMultipleOutline} /> Compatible FW
|
|
||||||
variant
|
|
||||||
</td>
|
|
||||||
<td>{this.state.sysinfo.rauc_variant}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card">
|
|
||||||
<p class="top">Rootfs slots</p>
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="table no-bottom-border">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Device</th>
|
|
||||||
<th>Mountpoint</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.keys(this.state.sysinfo.rauc_rootfs).map(
|
|
||||||
(k, i) => {
|
|
||||||
const rfs = this.state.sysinfo.rauc_rootfs[k]
|
|
||||||
let icon = mdiCircleOutline
|
|
||||||
let iconColor = colors.BLUE
|
|
||||||
|
|
||||||
if (!rfs.bootable) {
|
|
||||||
icon = mdiCloseCircleOutline
|
|
||||||
iconColor = colors.RED
|
|
||||||
} else if (rfs.primary) {
|
|
||||||
icon = mdiCheckCircleOutline
|
|
||||||
iconColor = colors.GREEN
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>
|
|
||||||
<Icon
|
|
||||||
icon={icon}
|
|
||||||
color={iconColor}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>{k}</td>
|
|
||||||
<td>{rfs.device}</td>
|
|
||||||
<td>{rfs.mountpoint}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderLoadingAnimation() {
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<p>loading sysinfo...</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.fetchTimeout !== undefined) {
|
|
||||||
window.clearTimeout(this.fetchTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.sysinfo) {
|
|
||||||
return this.renderSysinfo()
|
|
||||||
}
|
|
||||||
return this.renderLoadingAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import {mdiInformation, mdiUpload} from "@mdi/js"
|
|
||||||
import {Component} from "preact"
|
|
||||||
import Icon from "../Icon/Icon"
|
|
||||||
import SysinfoCard from "./SysinfoCard"
|
|
||||||
import UpdaterCard from "./UpdaterCard"
|
|
||||||
import "./Updater.scss"
|
|
||||||
|
|
||||||
type Props = {}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
flipped: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UpdaterView extends Component<Props, State> {
|
|
||||||
constructor(props?: Props | undefined, context?: any) {
|
|
||||||
super(props, context)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
flipped: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private flipCard = () => {
|
|
||||||
this.setState({flipped: !this.state.flipped})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="iconButton button-top-right"
|
|
||||||
onClick={this.flipCard}
|
|
||||||
aria-label={
|
|
||||||
this.state.flipped
|
|
||||||
? "Switch to updater"
|
|
||||||
: "Switch to system info"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon icon={this.state.flipped ? mdiUpload : mdiInformation} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="updater-view">
|
|
||||||
{!this.state.flipped ? <UpdaterCard /> : <SysinfoCard />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {Component} from "preact"
|
import {Component} from "preact"
|
||||||
import {mdiTriangleOutline} from "@mdi/js"
|
import {mdiTriangleOutline} from "@mdi/js"
|
||||||
import Icon from "../Icon/Icon"
|
import Icon from "../Icon/Icon"
|
||||||
import colors from "../../util/colors"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
source?: string
|
source?: string
|
||||||
|
@ -21,7 +20,7 @@ export default class Alert extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
<span>
|
<span>
|
||||||
<Icon icon={mdiTriangleOutline} color={colors.RED} />
|
<Icon icon={mdiTriangleOutline} color="#FF0039" />
|
||||||
{msg}
|
{msg}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
|
@ -1,6 +1,7 @@
|
||||||
import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js"
|
import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js"
|
||||||
|
import axios, {AxiosError, AxiosResponse} from "axios"
|
||||||
import {Component} from "preact"
|
import {Component} from "preact"
|
||||||
import {sebraucApi} from "../../util/apiUrls"
|
import {apiUrl} from "../../util/apiUrls"
|
||||||
import Icon from "../Icon/Icon"
|
import Icon from "../Icon/Icon"
|
||||||
|
|
||||||
export default class Reboot extends Component {
|
export default class Reboot extends Component {
|
||||||
|
@ -8,9 +9,9 @@ export default class Reboot extends Component {
|
||||||
const res = confirm("Reboot the system?")
|
const res = confirm("Reboot the system?")
|
||||||
if (!res) return
|
if (!res) return
|
||||||
|
|
||||||
sebraucApi
|
axios
|
||||||
.startReboot()
|
.post(apiUrl + "/reboot")
|
||||||
.then((response) => {
|
.then((response: AxiosResponse) => {
|
||||||
const msg = response.data.msg
|
const msg = response.data.msg
|
||||||
|
|
||||||
if (msg !== undefined) {
|
if (msg !== undefined) {
|
||||||
|
@ -19,7 +20,7 @@ export default class Reboot extends Component {
|
||||||
alert("No response")
|
alert("No response")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error: AxiosError) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const msg = error.response.data.msg
|
const msg = error.response.data.msg
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
.updater-view {
|
.uploader {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 600px;
|
max-width: 500px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
@ -18,8 +18,8 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin-top: 25px;
|
padding: 15px 8px;
|
||||||
margin-bottom: 8px;
|
margin: 8px 0;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@ -29,14 +29,6 @@
|
||||||
.top {
|
.top {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pad {
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
|
@ -50,9 +42,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-top-right {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
}
|
|
|
@ -5,11 +5,10 @@ import Dropzone from "../Dropzone/Dropzone"
|
||||||
import ProgressCircle from "../ProgressCircle/ProgressCircle"
|
import ProgressCircle from "../ProgressCircle/ProgressCircle"
|
||||||
import Icon from "../Icon/Icon"
|
import Icon from "../Icon/Icon"
|
||||||
import "./Updater.scss"
|
import "./Updater.scss"
|
||||||
|
import axios from "axios"
|
||||||
import Alert from "./Alert"
|
import Alert from "./Alert"
|
||||||
import Reboot from "./Reboot"
|
import Reboot from "./Reboot"
|
||||||
import {sebraucApi} from "../../util/apiUrls"
|
import {apiUrl, wsUrl} from "../../util/apiUrls"
|
||||||
import colors from "../../util/colors"
|
|
||||||
import WebsocketClient from "../../util/websocket"
|
|
||||||
|
|
||||||
class UploadStatus {
|
class UploadStatus {
|
||||||
uploading = false
|
uploading = false
|
||||||
|
@ -51,21 +50,21 @@ type State = {
|
||||||
wsConnected: boolean
|
wsConnected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UpdaterCard extends Component<Props, State> {
|
export default class Updater extends Component<Props, State> {
|
||||||
private dropzoneRef = createRef<Dropzone>()
|
private dropzoneRef = createRef<Dropzone>()
|
||||||
private ws: WebsocketClient
|
private conn: WebSocket | undefined
|
||||||
|
|
||||||
constructor(props?: Props | undefined, context?: any) {
|
constructor(props?: Props | undefined, context?: any) {
|
||||||
super(props, context)
|
super(props, context)
|
||||||
|
|
||||||
this.ws = new WebsocketClient(this.onWsStatusUpdate, this.onWsMessage)
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
uploadStatus: new UploadStatus(false),
|
uploadStatus: new UploadStatus(false),
|
||||||
uploadFilename: "",
|
uploadFilename: "",
|
||||||
raucStatus: new RaucStatus(),
|
raucStatus: new RaucStatus(),
|
||||||
wsConnected: this.ws.api().isConnected(),
|
wsConnected: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.connectWebsocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
private buttonClick = () => {
|
private buttonClick = () => {
|
||||||
|
@ -78,13 +77,19 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
const newFile = files[0]
|
const newFile = files[0]
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("updateFile", newFile)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadStatus: new UploadStatus(true, newFile.size, 0),
|
uploadStatus: new UploadStatus(true, newFile.size, 0),
|
||||||
uploadFilename: newFile.name,
|
uploadFilename: newFile.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
sebraucApi
|
axios
|
||||||
.startUpdate(newFile, {
|
.post(apiUrl + "/update", formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
onUploadProgress: (progressEvent: {loaded: number; total: number}) => {
|
onUploadProgress: (progressEvent: {loaded: number; total: number}) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadStatus: UploadStatus.fromProgressEvent(progressEvent),
|
uploadStatus: UploadStatus.fromProgressEvent(progressEvent),
|
||||||
|
@ -106,16 +111,33 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
this.dropzoneRef.current?.reset()
|
this.dropzoneRef.current?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWsStatusUpdate = (wsConnected: boolean) => {
|
private connectWebsocket = () => {
|
||||||
this.setState({wsConnected: wsConnected})
|
if (window.WebSocket) {
|
||||||
}
|
this.conn = new WebSocket(wsUrl)
|
||||||
|
this.conn.onopen = () => {
|
||||||
|
this.setState({wsConnected: true})
|
||||||
|
console.log("WS connected")
|
||||||
|
}
|
||||||
|
this.conn.onclose = () => {
|
||||||
|
this.setState({wsConnected: false})
|
||||||
|
console.log("WS connection closed")
|
||||||
|
window.setTimeout(this.connectWebsocket, 3000)
|
||||||
|
}
|
||||||
|
this.conn.onmessage = (evt) => {
|
||||||
|
var messages = evt.data.split("\n")
|
||||||
|
for (var i = 0; i < messages.length; i++) {
|
||||||
|
this.setState({
|
||||||
|
raucStatus: Object.assign(
|
||||||
|
new RaucStatus(),
|
||||||
|
JSON.parse(messages[i])
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
private onWsMessage = (evt: MessageEvent) => {
|
console.log(this.state.raucStatus)
|
||||||
var messages = evt.data.split("\n")
|
}
|
||||||
for (var i = 0; i < messages.length; i++) {
|
}
|
||||||
this.setState({
|
} else {
|
||||||
raucStatus: Object.assign(new RaucStatus(), JSON.parse(messages[i])),
|
console.log("Your browser does not support WebSockets")
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,9 +163,9 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private circleColor(): string {
|
private circleColor(): string {
|
||||||
if (this.state.raucStatus.installing) return colors.RED
|
if (this.state.raucStatus.installing) return "#FF0039"
|
||||||
if (this.state.uploadStatus.uploading) return colors.GREEN
|
if (this.state.uploadStatus.uploading) return "#148420"
|
||||||
return colors.BLUE
|
return "#1f85de"
|
||||||
}
|
}
|
||||||
|
|
||||||
private circlePercentage(): number {
|
private circlePercentage(): number {
|
||||||
|
@ -153,10 +175,6 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.ws.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const acceptUploads = this.acceptUploads()
|
const acceptUploads = this.acceptUploads()
|
||||||
const circleColor = this.circleColor()
|
const circleColor = this.circleColor()
|
||||||
|
@ -177,13 +195,12 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
topText = "Updating firmware"
|
topText = "Updating firmware"
|
||||||
bottomText = this.state.raucStatus.message
|
bottomText = this.state.raucStatus.message
|
||||||
} else {
|
} else {
|
||||||
topText = "Firmware update"
|
topText = "Upload firmware package"
|
||||||
bottomText = "Upload *.raucb FW package"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="uploader">
|
||||||
<div class="card pad">
|
<div class="card upload">
|
||||||
<div>
|
<div>
|
||||||
<p class="top">{topText}</p>
|
<p class="top">{topText}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -198,7 +215,7 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
progress={circlePercentage}
|
progress={circlePercentage}
|
||||||
color={circleColor}
|
color={circleColor}
|
||||||
>
|
>
|
||||||
<button onClick={this.buttonClick} aria-label="Upload">
|
<button onClick={this.buttonClick}>
|
||||||
<Icon icon={mdiUpload} size={50} />
|
<Icon icon={mdiUpload} size={50} />
|
||||||
</button>
|
</button>
|
||||||
</ProgressCircle>
|
</ProgressCircle>
|
|
@ -1,15 +1,15 @@
|
||||||
import {Component} from "preact"
|
import {Component} from "preact"
|
||||||
import UpdaterView from "./Updater/UpdaterView"
|
import Updater from "./Upload/Updater"
|
||||||
import logo from "../assets/logo.svg"
|
import logo from "../assets/logo.svg"
|
||||||
import {getConfig} from "../util/config"
|
import {version} from "../util/version"
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<img src={logo} alt="SEBRAUC" height="64" />
|
<img src={logo} height="64" />
|
||||||
{getConfig().version}
|
{version}
|
||||||
<UpdaterView />
|
<Updater />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
# OpenAPI Generator Ignore
|
|
||||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
|
||||||
|
|
||||||
# Use this file to prevent files from being overwritten by the generator.
|
|
||||||
# The patterns follow closely to .gitignore or .dockerignore.
|
|
||||||
|
|
||||||
# As an example, the C# client generator defines ApiClient.cs.
|
|
||||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
|
||||||
#ApiClient.cs
|
|
||||||
|
|
||||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
|
||||||
#foo/*/qux
|
|
||||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
|
||||||
|
|
||||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
|
||||||
#foo/**/qux
|
|
||||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
|
||||||
|
|
||||||
# You can also negate patterns with an exclamation (!).
|
|
||||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
|
||||||
#docs/*.md
|
|
||||||
# Then explicitly reverse the ignore rule for a single file:
|
|
||||||
#!docs/README.md
|
|
||||||
|
|
||||||
/.gitignore
|
|
||||||
/.npmignore
|
|
||||||
/git_push.sh
|
|
|
@ -1,5 +0,0 @@
|
||||||
api.ts
|
|
||||||
base.ts
|
|
||||||
common.ts
|
|
||||||
configuration.ts
|
|
||||||
index.ts
|
|
|
@ -1 +0,0 @@
|
||||||
5.3.0
|
|
|
@ -1,566 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* SEBRAUC
|
|
||||||
* REST API for the SEBRAUC firmware updater
|
|
||||||
*
|
|
||||||
* The version of the OpenAPI document: 0.2.0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
||||||
* https://openapi-generator.tech
|
|
||||||
* Do not edit the class manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Configuration} from "./configuration"
|
|
||||||
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
|
|
||||||
// Some imports not used depending on template conditions
|
|
||||||
// @ts-ignore
|
|
||||||
import {
|
|
||||||
DUMMY_BASE_URL,
|
|
||||||
assertParamExists,
|
|
||||||
setApiKeyToObject,
|
|
||||||
setBasicAuthToObject,
|
|
||||||
setBearerAuthToObject,
|
|
||||||
setOAuthToObject,
|
|
||||||
setSearchParams,
|
|
||||||
serializeDataIfNeeded,
|
|
||||||
toPathString,
|
|
||||||
createRequestFunction,
|
|
||||||
} from "./common"
|
|
||||||
// @ts-ignore
|
|
||||||
import {
|
|
||||||
BASE_PATH,
|
|
||||||
COLLECTION_FORMATS,
|
|
||||||
RequestArgs,
|
|
||||||
BaseAPI,
|
|
||||||
RequiredError,
|
|
||||||
} from "./base"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Error contains error relevant information.
|
|
||||||
* @export
|
|
||||||
* @interface ModelError
|
|
||||||
*/
|
|
||||||
export interface ModelError {
|
|
||||||
/**
|
|
||||||
* The general error message according to HTTP specification.
|
|
||||||
* @type {string}
|
|
||||||
* @memberof ModelError
|
|
||||||
*/
|
|
||||||
error: string
|
|
||||||
/**
|
|
||||||
* Concrete error message.
|
|
||||||
* @type {string}
|
|
||||||
* @memberof ModelError
|
|
||||||
*/
|
|
||||||
msg: string
|
|
||||||
/**
|
|
||||||
* The http error code.
|
|
||||||
* @type {number}
|
|
||||||
* @memberof ModelError
|
|
||||||
*/
|
|
||||||
status_code: number
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* RaucStatus contains information about the current RAUC updater status.
|
|
||||||
* @export
|
|
||||||
* @interface RaucStatus
|
|
||||||
*/
|
|
||||||
export interface RaucStatus {
|
|
||||||
/**
|
|
||||||
* True if the installer is running
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof RaucStatus
|
|
||||||
*/
|
|
||||||
installing: boolean
|
|
||||||
/**
|
|
||||||
* Installation error message
|
|
||||||
* @type {string}
|
|
||||||
* @memberof RaucStatus
|
|
||||||
*/
|
|
||||||
last_error: string
|
|
||||||
/**
|
|
||||||
* Full command line output of the current installation
|
|
||||||
* @type {string}
|
|
||||||
* @memberof RaucStatus
|
|
||||||
*/
|
|
||||||
log: string
|
|
||||||
/**
|
|
||||||
* Current installation step
|
|
||||||
* @type {string}
|
|
||||||
* @memberof RaucStatus
|
|
||||||
*/
|
|
||||||
message: string
|
|
||||||
/**
|
|
||||||
* Installation progress
|
|
||||||
* @type {number}
|
|
||||||
* @memberof RaucStatus
|
|
||||||
*/
|
|
||||||
percent: number
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface Rootfs
|
|
||||||
*/
|
|
||||||
export interface Rootfs {
|
|
||||||
/**
|
|
||||||
* Is the filesystem bootable?
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof Rootfs
|
|
||||||
*/
|
|
||||||
bootable: boolean
|
|
||||||
/**
|
|
||||||
* Is the filesystem booted?
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof Rootfs
|
|
||||||
*/
|
|
||||||
booted: boolean
|
|
||||||
/**
|
|
||||||
* Block device
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Rootfs
|
|
||||||
*/
|
|
||||||
device: string
|
|
||||||
/**
|
|
||||||
* Mount path (null when not mounted)
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Rootfs
|
|
||||||
*/
|
|
||||||
mountpoint: string
|
|
||||||
/**
|
|
||||||
* Is the filesystem the next boot target?
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof Rootfs
|
|
||||||
*/
|
|
||||||
primary: boolean
|
|
||||||
/**
|
|
||||||
* Filesystem
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Rootfs
|
|
||||||
*/
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* StatusMessage contains the status of an operation.
|
|
||||||
* @export
|
|
||||||
* @interface StatusMessage
|
|
||||||
*/
|
|
||||||
export interface StatusMessage {
|
|
||||||
/**
|
|
||||||
* Status message text
|
|
||||||
* @type {string}
|
|
||||||
* @memberof StatusMessage
|
|
||||||
*/
|
|
||||||
msg: string
|
|
||||||
/**
|
|
||||||
* Is operation successful?
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof StatusMessage
|
|
||||||
*/
|
|
||||||
success: boolean
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* SystemInfo contains information about the running system.
|
|
||||||
* @export
|
|
||||||
* @interface SystemInfo
|
|
||||||
*/
|
|
||||||
export interface SystemInfo {
|
|
||||||
/**
|
|
||||||
* Hostname of the system
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
hostname: string
|
|
||||||
/**
|
|
||||||
* Name of the os distribution
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
os_name: string
|
|
||||||
/**
|
|
||||||
* Operating system version
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
os_version: string
|
|
||||||
/**
|
|
||||||
* Compatible firmware name
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
rauc_compatible: string
|
|
||||||
/**
|
|
||||||
* List of RAUC root filesystems
|
|
||||||
* @type {{ [key: string]: Rootfs; }}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
rauc_rootfs: {[key: string]: Rootfs}
|
|
||||||
/**
|
|
||||||
* Compatible firmware variant
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
rauc_variant: string
|
|
||||||
/**
|
|
||||||
* System uptime in seconds
|
|
||||||
* @type {number}
|
|
||||||
* @memberof SystemInfo
|
|
||||||
*/
|
|
||||||
uptime: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DefaultApi - axios parameter creator
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Get the current system info
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
getInfo: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
|
||||||
const localVarPath = `/info`
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
|
||||||
let baseOptions
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
|
|
||||||
const localVarHeaderParameter = {} as any
|
|
||||||
const localVarQueryParameter = {} as any
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
|
||||||
let headersFromBaseOptions =
|
|
||||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
|
||||||
localVarRequestOptions.headers = {
|
|
||||||
...localVarHeaderParameter,
|
|
||||||
...headersFromBaseOptions,
|
|
||||||
...options.headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Get the current status of the RAUC updater
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
getStatus: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
|
||||||
const localVarPath = `/status`
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
|
||||||
let baseOptions
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
|
|
||||||
const localVarHeaderParameter = {} as any
|
|
||||||
const localVarQueryParameter = {} as any
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
|
||||||
let headersFromBaseOptions =
|
|
||||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
|
||||||
localVarRequestOptions.headers = {
|
|
||||||
...localVarHeaderParameter,
|
|
||||||
...headersFromBaseOptions,
|
|
||||||
...options.headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Reboot the system
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
startReboot: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
|
||||||
const localVarPath = `/reboot`
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
|
||||||
let baseOptions
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
|
|
||||||
const localVarHeaderParameter = {} as any
|
|
||||||
const localVarQueryParameter = {} as any
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
|
||||||
let headersFromBaseOptions =
|
|
||||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
|
||||||
localVarRequestOptions.headers = {
|
|
||||||
...localVarHeaderParameter,
|
|
||||||
...headersFromBaseOptions,
|
|
||||||
...options.headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Start the update process
|
|
||||||
* @param {any} updateFile RAUC firmware image file (*.raucb)
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
startUpdate: async (
|
|
||||||
updateFile: any,
|
|
||||||
options: AxiosRequestConfig = {}
|
|
||||||
): Promise<RequestArgs> => {
|
|
||||||
// verify required parameter 'updateFile' is not null or undefined
|
|
||||||
assertParamExists("startUpdate", "updateFile", updateFile)
|
|
||||||
const localVarPath = `/update`
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
|
||||||
let baseOptions
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
|
|
||||||
const localVarHeaderParameter = {} as any
|
|
||||||
const localVarQueryParameter = {} as any
|
|
||||||
const localVarFormParams = new ((configuration &&
|
|
||||||
configuration.formDataCtor) ||
|
|
||||||
FormData)()
|
|
||||||
|
|
||||||
if (updateFile !== undefined) {
|
|
||||||
localVarFormParams.append("updateFile", updateFile as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
localVarHeaderParameter["Content-Type"] = "multipart/form-data"
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
|
||||||
let headersFromBaseOptions =
|
|
||||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
|
||||||
localVarRequestOptions.headers = {
|
|
||||||
...localVarHeaderParameter,
|
|
||||||
...headersFromBaseOptions,
|
|
||||||
...options.headers,
|
|
||||||
}
|
|
||||||
localVarRequestOptions.data = localVarFormParams
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DefaultApi - functional programming interface
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const DefaultApiFp = function (configuration?: Configuration) {
|
|
||||||
const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Get the current system info
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async getInfo(
|
|
||||||
options?: AxiosRequestConfig
|
|
||||||
): Promise<
|
|
||||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemInfo>
|
|
||||||
> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getInfo(options)
|
|
||||||
return createRequestFunction(
|
|
||||||
localVarAxiosArgs,
|
|
||||||
globalAxios,
|
|
||||||
BASE_PATH,
|
|
||||||
configuration
|
|
||||||
)
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Get the current status of the RAUC updater
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async getStatus(
|
|
||||||
options?: AxiosRequestConfig
|
|
||||||
): Promise<
|
|
||||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RaucStatus>
|
|
||||||
> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getStatus(options)
|
|
||||||
return createRequestFunction(
|
|
||||||
localVarAxiosArgs,
|
|
||||||
globalAxios,
|
|
||||||
BASE_PATH,
|
|
||||||
configuration
|
|
||||||
)
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Reboot the system
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async startReboot(
|
|
||||||
options?: AxiosRequestConfig
|
|
||||||
): Promise<
|
|
||||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusMessage>
|
|
||||||
> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.startReboot(
|
|
||||||
options
|
|
||||||
)
|
|
||||||
return createRequestFunction(
|
|
||||||
localVarAxiosArgs,
|
|
||||||
globalAxios,
|
|
||||||
BASE_PATH,
|
|
||||||
configuration
|
|
||||||
)
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Start the update process
|
|
||||||
* @param {any} updateFile RAUC firmware image file (*.raucb)
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async startUpdate(
|
|
||||||
updateFile: any,
|
|
||||||
options?: AxiosRequestConfig
|
|
||||||
): Promise<
|
|
||||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusMessage>
|
|
||||||
> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.startUpdate(
|
|
||||||
updateFile,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
return createRequestFunction(
|
|
||||||
localVarAxiosArgs,
|
|
||||||
globalAxios,
|
|
||||||
BASE_PATH,
|
|
||||||
configuration
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DefaultApi - factory interface
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const DefaultApiFactory = function (
|
|
||||||
configuration?: Configuration,
|
|
||||||
basePath?: string,
|
|
||||||
axios?: AxiosInstance
|
|
||||||
) {
|
|
||||||
const localVarFp = DefaultApiFp(configuration)
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Get the current system info
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
getInfo(options?: any): AxiosPromise<SystemInfo> {
|
|
||||||
return localVarFp
|
|
||||||
.getInfo(options)
|
|
||||||
.then((request) => request(axios, basePath))
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Get the current status of the RAUC updater
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
getStatus(options?: any): AxiosPromise<RaucStatus> {
|
|
||||||
return localVarFp
|
|
||||||
.getStatus(options)
|
|
||||||
.then((request) => request(axios, basePath))
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Reboot the system
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
startReboot(options?: any): AxiosPromise<StatusMessage> {
|
|
||||||
return localVarFp
|
|
||||||
.startReboot(options)
|
|
||||||
.then((request) => request(axios, basePath))
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Start the update process
|
|
||||||
* @param {any} updateFile RAUC firmware image file (*.raucb)
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
startUpdate(updateFile: any, options?: any): AxiosPromise<StatusMessage> {
|
|
||||||
return localVarFp
|
|
||||||
.startUpdate(updateFile, options)
|
|
||||||
.then((request) => request(axios, basePath))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DefaultApi - object-oriented interface
|
|
||||||
* @export
|
|
||||||
* @class DefaultApi
|
|
||||||
* @extends {BaseAPI}
|
|
||||||
*/
|
|
||||||
export class DefaultApi extends BaseAPI {
|
|
||||||
/**
|
|
||||||
* Get the current system info
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof DefaultApi
|
|
||||||
*/
|
|
||||||
public getInfo(options?: AxiosRequestConfig) {
|
|
||||||
return DefaultApiFp(this.configuration)
|
|
||||||
.getInfo(options)
|
|
||||||
.then((request) => request(this.axios, this.basePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current status of the RAUC updater
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof DefaultApi
|
|
||||||
*/
|
|
||||||
public getStatus(options?: AxiosRequestConfig) {
|
|
||||||
return DefaultApiFp(this.configuration)
|
|
||||||
.getStatus(options)
|
|
||||||
.then((request) => request(this.axios, this.basePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reboot the system
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof DefaultApi
|
|
||||||
*/
|
|
||||||
public startReboot(options?: AxiosRequestConfig) {
|
|
||||||
return DefaultApiFp(this.configuration)
|
|
||||||
.startReboot(options)
|
|
||||||
.then((request) => request(this.axios, this.basePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the update process
|
|
||||||
* @param {any} updateFile RAUC firmware image file (*.raucb)
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof DefaultApi
|
|
||||||
*/
|
|
||||||
public startUpdate(updateFile: any, options?: AxiosRequestConfig) {
|
|
||||||
return DefaultApiFp(this.configuration)
|
|
||||||
.startUpdate(updateFile, options)
|
|
||||||
.then((request) => request(this.axios, this.basePath))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* SEBRAUC
|
|
||||||
* REST API for the SEBRAUC firmware updater
|
|
||||||
*
|
|
||||||
* The version of the OpenAPI document: 0.2.0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
||||||
* https://openapi-generator.tech
|
|
||||||
* Do not edit the class manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Configuration} from "./configuration"
|
|
||||||
// Some imports not used depending on template conditions
|
|
||||||
// @ts-ignore
|
|
||||||
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
|
|
||||||
|
|
||||||
export const BASE_PATH = "http://localhost".replace(/\/+$/, "")
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const COLLECTION_FORMATS = {
|
|
||||||
csv: ",",
|
|
||||||
ssv: " ",
|
|
||||||
tsv: "\t",
|
|
||||||
pipes: "|",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface RequestArgs
|
|
||||||
*/
|
|
||||||
export interface RequestArgs {
|
|
||||||
url: string
|
|
||||||
options: AxiosRequestConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class BaseAPI
|
|
||||||
*/
|
|
||||||
export class BaseAPI {
|
|
||||||
protected configuration: Configuration | undefined
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
configuration?: Configuration,
|
|
||||||
protected basePath: string = BASE_PATH,
|
|
||||||
protected axios: AxiosInstance = globalAxios
|
|
||||||
) {
|
|
||||||
if (configuration) {
|
|
||||||
this.configuration = configuration
|
|
||||||
this.basePath = configuration.basePath || this.basePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class RequiredError
|
|
||||||
* @extends {Error}
|
|
||||||
*/
|
|
||||||
export class RequiredError extends Error {
|
|
||||||
name: "RequiredError" = "RequiredError"
|
|
||||||
constructor(public field: string, msg?: string) {
|
|
||||||
super(msg)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,181 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* SEBRAUC
|
|
||||||
* REST API for the SEBRAUC firmware updater
|
|
||||||
*
|
|
||||||
* The version of the OpenAPI document: 0.2.0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
||||||
* https://openapi-generator.tech
|
|
||||||
* Do not edit the class manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Configuration} from "./configuration"
|
|
||||||
import {RequiredError, RequestArgs} from "./base"
|
|
||||||
import {AxiosInstance, AxiosResponse} from "axios"
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const DUMMY_BASE_URL = "https://example.com"
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const assertParamExists = function (
|
|
||||||
functionName: string,
|
|
||||||
paramName: string,
|
|
||||||
paramValue: unknown
|
|
||||||
) {
|
|
||||||
if (paramValue === null || paramValue === undefined) {
|
|
||||||
throw new RequiredError(
|
|
||||||
paramName,
|
|
||||||
`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const setApiKeyToObject = async function (
|
|
||||||
object: any,
|
|
||||||
keyParamName: string,
|
|
||||||
configuration?: Configuration
|
|
||||||
) {
|
|
||||||
if (configuration && configuration.apiKey) {
|
|
||||||
const localVarApiKeyValue =
|
|
||||||
typeof configuration.apiKey === "function"
|
|
||||||
? await configuration.apiKey(keyParamName)
|
|
||||||
: await configuration.apiKey
|
|
||||||
object[keyParamName] = localVarApiKeyValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const setBasicAuthToObject = function (
|
|
||||||
object: any,
|
|
||||||
configuration?: Configuration
|
|
||||||
) {
|
|
||||||
if (configuration && (configuration.username || configuration.password)) {
|
|
||||||
object["auth"] = {
|
|
||||||
username: configuration.username,
|
|
||||||
password: configuration.password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const setBearerAuthToObject = async function (
|
|
||||||
object: any,
|
|
||||||
configuration?: Configuration
|
|
||||||
) {
|
|
||||||
if (configuration && configuration.accessToken) {
|
|
||||||
const accessToken =
|
|
||||||
typeof configuration.accessToken === "function"
|
|
||||||
? await configuration.accessToken()
|
|
||||||
: await configuration.accessToken
|
|
||||||
object["Authorization"] = "Bearer " + accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const setOAuthToObject = async function (
|
|
||||||
object: any,
|
|
||||||
name: string,
|
|
||||||
scopes: string[],
|
|
||||||
configuration?: Configuration
|
|
||||||
) {
|
|
||||||
if (configuration && configuration.accessToken) {
|
|
||||||
const localVarAccessTokenValue =
|
|
||||||
typeof configuration.accessToken === "function"
|
|
||||||
? await configuration.accessToken(name, scopes)
|
|
||||||
: await configuration.accessToken
|
|
||||||
object["Authorization"] = "Bearer " + localVarAccessTokenValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const setSearchParams = function (url: URL, ...objects: any[]) {
|
|
||||||
const searchParams = new URLSearchParams(url.search)
|
|
||||||
for (const object of objects) {
|
|
||||||
for (const key in object) {
|
|
||||||
if (Array.isArray(object[key])) {
|
|
||||||
searchParams.delete(key)
|
|
||||||
for (const item of object[key]) {
|
|
||||||
searchParams.append(key, item)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
searchParams.set(key, object[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url.search = searchParams.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const serializeDataIfNeeded = function (
|
|
||||||
value: any,
|
|
||||||
requestOptions: any,
|
|
||||||
configuration?: Configuration
|
|
||||||
) {
|
|
||||||
const nonString = typeof value !== "string"
|
|
||||||
const needsSerialization =
|
|
||||||
nonString && configuration && configuration.isJsonMime
|
|
||||||
? configuration.isJsonMime(requestOptions.headers["Content-Type"])
|
|
||||||
: nonString
|
|
||||||
return needsSerialization
|
|
||||||
? JSON.stringify(value !== undefined ? value : {})
|
|
||||||
: value || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const toPathString = function (url: URL) {
|
|
||||||
return url.pathname + url.search + url.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export const createRequestFunction = function (
|
|
||||||
axiosArgs: RequestArgs,
|
|
||||||
globalAxios: AxiosInstance,
|
|
||||||
BASE_PATH: string,
|
|
||||||
configuration?: Configuration
|
|
||||||
) {
|
|
||||||
return <T = unknown, R = AxiosResponse<T>>(
|
|
||||||
axios: AxiosInstance = globalAxios,
|
|
||||||
basePath: string = BASE_PATH
|
|
||||||
) => {
|
|
||||||
const axiosRequestArgs = {
|
|
||||||
...axiosArgs.options,
|
|
||||||
url: (configuration?.basePath || basePath) + axiosArgs.url,
|
|
||||||
}
|
|
||||||
return axios.request<T, R>(axiosRequestArgs)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* SEBRAUC
|
|
||||||
* REST API for the SEBRAUC firmware updater
|
|
||||||
*
|
|
||||||
* The version of the OpenAPI document: 0.2.0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
||||||
* https://openapi-generator.tech
|
|
||||||
* Do not edit the class manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ConfigurationParameters {
|
|
||||||
apiKey?:
|
|
||||||
| string
|
|
||||||
| Promise<string>
|
|
||||||
| ((name: string) => string)
|
|
||||||
| ((name: string) => Promise<string>)
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
accessToken?:
|
|
||||||
| string
|
|
||||||
| Promise<string>
|
|
||||||
| ((name?: string, scopes?: string[]) => string)
|
|
||||||
| ((name?: string, scopes?: string[]) => Promise<string>)
|
|
||||||
basePath?: string
|
|
||||||
baseOptions?: any
|
|
||||||
formDataCtor?: new () => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Configuration {
|
|
||||||
/**
|
|
||||||
* parameter for apiKey security
|
|
||||||
* @param name security name
|
|
||||||
* @memberof Configuration
|
|
||||||
*/
|
|
||||||
apiKey?:
|
|
||||||
| string
|
|
||||||
| Promise<string>
|
|
||||||
| ((name: string) => string)
|
|
||||||
| ((name: string) => Promise<string>)
|
|
||||||
/**
|
|
||||||
* parameter for basic security
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Configuration
|
|
||||||
*/
|
|
||||||
username?: string
|
|
||||||
/**
|
|
||||||
* parameter for basic security
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Configuration
|
|
||||||
*/
|
|
||||||
password?: string
|
|
||||||
/**
|
|
||||||
* parameter for oauth2 security
|
|
||||||
* @param name security name
|
|
||||||
* @param scopes oauth2 scope
|
|
||||||
* @memberof Configuration
|
|
||||||
*/
|
|
||||||
accessToken?:
|
|
||||||
| string
|
|
||||||
| Promise<string>
|
|
||||||
| ((name?: string, scopes?: string[]) => string)
|
|
||||||
| ((name?: string, scopes?: string[]) => Promise<string>)
|
|
||||||
/**
|
|
||||||
* override base path
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Configuration
|
|
||||||
*/
|
|
||||||
basePath?: string
|
|
||||||
/**
|
|
||||||
* base options for axios calls
|
|
||||||
*
|
|
||||||
* @type {any}
|
|
||||||
* @memberof Configuration
|
|
||||||
*/
|
|
||||||
baseOptions?: any
|
|
||||||
/**
|
|
||||||
* The FormData constructor that will be used to create multipart form data
|
|
||||||
* requests. You can inject this here so that execution environments that
|
|
||||||
* do not support the FormData class can still run the generated client.
|
|
||||||
*
|
|
||||||
* @type {new () => FormData}
|
|
||||||
*/
|
|
||||||
formDataCtor?: new () => any
|
|
||||||
|
|
||||||
constructor(param: ConfigurationParameters = {}) {
|
|
||||||
this.apiKey = param.apiKey
|
|
||||||
this.username = param.username
|
|
||||||
this.password = param.password
|
|
||||||
this.accessToken = param.accessToken
|
|
||||||
this.basePath = param.basePath
|
|
||||||
this.baseOptions = param.baseOptions
|
|
||||||
this.formDataCtor = param.formDataCtor
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given MIME is a JSON MIME.
|
|
||||||
* JSON MIME examples:
|
|
||||||
* application/json
|
|
||||||
* application/json; charset=UTF8
|
|
||||||
* APPLICATION/JSON
|
|
||||||
* application/vnd.company+json
|
|
||||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
|
||||||
* @return True if the given MIME is JSON, false otherwise.
|
|
||||||
*/
|
|
||||||
public isJsonMime(mime: string): boolean {
|
|
||||||
const jsonMime: RegExp = new RegExp(
|
|
||||||
"^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$",
|
|
||||||
"i"
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
mime !== null &&
|
|
||||||
(jsonMime.test(mime) ||
|
|
||||||
mime.toLowerCase() === "application/json-patch+json")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* SEBRAUC
|
|
||||||
* REST API for the SEBRAUC firmware updater
|
|
||||||
*
|
|
||||||
* The version of the OpenAPI document: 0.2.0
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
||||||
* https://openapi-generator.tech
|
|
||||||
* Do not edit the class manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./api"
|
|
||||||
export * from "./configuration"
|
|
|
@ -1,5 +1,3 @@
|
||||||
@use "table";
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
.table-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.no-bottom-border {
|
|
||||||
&,
|
|
||||||
> tr:last-child,
|
|
||||||
:not(thead) tr:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table caption {
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: 0.5em 0 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tr {
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
padding: 0.35em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th,
|
|
||||||
.table td {
|
|
||||||
padding: 0.625em;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: #1f85de;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
font-size: 0.85em;
|
|
||||||
letter-spacing: 0.085em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
|
@ -1,5 +1,3 @@
|
||||||
import {Configuration, DefaultApi} from "../sebrauc-client"
|
|
||||||
|
|
||||||
let apiHost = document.location.host
|
let apiHost = document.location.host
|
||||||
const httpProto = document.location.protocol
|
const httpProto = document.location.protocol
|
||||||
const wsProto = httpProto === "https:" ? "wss:" : "ws:"
|
const wsProto = httpProto === "https:" ? "wss:" : "ws:"
|
||||||
|
@ -11,10 +9,4 @@ if (import.meta.env.VITE_API_HOST !== undefined) {
|
||||||
const apiUrl = `${httpProto}//${apiHost}/api`
|
const apiUrl = `${httpProto}//${apiHost}/api`
|
||||||
const wsUrl = `${wsProto}//${apiHost}/api/ws`
|
const wsUrl = `${wsProto}//${apiHost}/api/ws`
|
||||||
|
|
||||||
let apicfg = new Configuration({
|
export {apiUrl, wsUrl}
|
||||||
basePath: apiUrl,
|
|
||||||
})
|
|
||||||
|
|
||||||
const sebraucApi = new DefaultApi(apicfg)
|
|
||||||
|
|
||||||
export {apiUrl, wsUrl, sebraucApi}
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
class colors {
|
|
||||||
static readonly RED = "#FF0039"
|
|
||||||
static readonly GREEN = "#148420"
|
|
||||||
static readonly BLUE = "#1f85de"
|
|
||||||
}
|
|
||||||
|
|
||||||
export default colors
|
|
|
@ -1,23 +0,0 @@
|
||||||
export interface Config {
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
config?: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isConfig(object: any): object is Config {
|
|
||||||
return typeof object === "object" && "version" in object
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfig(): Config {
|
|
||||||
if (isConfig(window.config)) {
|
|
||||||
return window.config
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
version: "dev",
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
function secondsToString(seconds: number): string {
|
|
||||||
const numyears = Math.floor(seconds / 31536000)
|
|
||||||
const numdays = Math.floor((seconds % 31536000) / 86400)
|
|
||||||
const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600)
|
|
||||||
const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60)
|
|
||||||
const numseconds = (((seconds % 31536000) % 86400) % 3600) % 60
|
|
||||||
|
|
||||||
let res = []
|
|
||||||
if (numyears > 0) res.push(numyears + "yr")
|
|
||||||
if (numdays > 0) res.push(numdays + "d")
|
|
||||||
if (numhours > 0) res.push(numhours + "h")
|
|
||||||
if (numminutes > 0) res.push(numminutes + "m")
|
|
||||||
if (seconds < 60) res.push(numseconds + "s")
|
|
||||||
|
|
||||||
return res.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
export {secondsToString}
|
|
7
ui/src/util/version.ts
Normal file
7
ui/src/util/version.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
let version = import.meta.env.VITE_VERSION
|
||||||
|
|
||||||
|
if (version === undefined) {
|
||||||
|
version = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
export {version}
|
|
@ -1,92 +0,0 @@
|
||||||
import {wsUrl} from "./apiUrls"
|
|
||||||
|
|
||||||
class WebsocketAPI {
|
|
||||||
private static ws: WebsocketAPI | undefined
|
|
||||||
|
|
||||||
private conn: WebSocket | undefined
|
|
||||||
private wsConnected: boolean
|
|
||||||
|
|
||||||
private clients: Set<WebsocketClient>
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.clients = new Set()
|
|
||||||
this.wsConnected = false
|
|
||||||
|
|
||||||
if (window.WebSocket) {
|
|
||||||
this.connect()
|
|
||||||
} else {
|
|
||||||
console.log("Your browser does not support WebSockets")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setStatus(wsConnected: boolean) {
|
|
||||||
if (wsConnected !== this.wsConnected) {
|
|
||||||
this.wsConnected = wsConnected
|
|
||||||
this.clients.forEach((client) => {
|
|
||||||
client.statusCallback(this.wsConnected)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private connect() {
|
|
||||||
this.conn = new WebSocket(wsUrl)
|
|
||||||
this.conn.onopen = () => {
|
|
||||||
this.setStatus(true)
|
|
||||||
console.log("WS connected")
|
|
||||||
}
|
|
||||||
this.conn.onclose = () => {
|
|
||||||
this.setStatus(false)
|
|
||||||
console.log("WS connection closed")
|
|
||||||
window.setTimeout(() => this.connect(), 3000)
|
|
||||||
}
|
|
||||||
this.conn.onmessage = (evt) => {
|
|
||||||
this.clients.forEach((client) => {
|
|
||||||
client.msgCallback(evt)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Get(): WebsocketAPI {
|
|
||||||
if (this.ws === undefined) {
|
|
||||||
this.ws = new WebsocketAPI()
|
|
||||||
}
|
|
||||||
return this.ws
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected(): boolean {
|
|
||||||
return this.wsConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
addClient(client: WebsocketClient) {
|
|
||||||
console.log("added client", client)
|
|
||||||
this.clients.add(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeClient(client: WebsocketClient) {
|
|
||||||
console.log("removed client", client)
|
|
||||||
this.clients.delete(client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class WebsocketClient {
|
|
||||||
statusCallback: (wsConnected: boolean) => void
|
|
||||||
msgCallback: (evt: MessageEvent) => void
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
statusCallback: (wsConnected: boolean) => void,
|
|
||||||
msgCallback: (evt: MessageEvent) => void
|
|
||||||
) {
|
|
||||||
this.statusCallback = statusCallback
|
|
||||||
this.msgCallback = msgCallback
|
|
||||||
|
|
||||||
this.api().addClient(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
api(): WebsocketAPI {
|
|
||||||
return WebsocketAPI.Get()
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.api().removeClient(this)
|
|
||||||
}
|
|
||||||
}
|
|
58
ui/ui.go
58
ui/ui.go
|
@ -1,64 +1,10 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware"
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const distDir = "dist"
|
const AssetsDir = "dist"
|
||||||
|
|
||||||
//go:embed dist/**
|
//go:embed dist/**
|
||||||
var assets embed.FS
|
var Assets embed.FS
|
||||||
|
|
||||||
type uiConfig struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func subFS(fsys fs.FS, dir string) fs.FS {
|
|
||||||
sub, err := fs.Sub(fsys, dir)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
func distFS() fs.FS {
|
|
||||||
return subFS(assets, distDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Register(r gin.IRouter) {
|
|
||||||
indexHandler := getIndexHandler()
|
|
||||||
|
|
||||||
uiAssets := r.Group("/assets", middleware.Cache)
|
|
||||||
|
|
||||||
r.GET("/", indexHandler)
|
|
||||||
r.GET("/index.html", indexHandler)
|
|
||||||
|
|
||||||
uiAssets.StaticFS("/", http.FS(subFS(distFS(), "assets")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIndexHandler() gin.HandlerFunc {
|
|
||||||
content, err := fs.ReadFile(distFS(), "index.html")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiConfigBytes, err := json.Marshal(uiConfig{
|
|
||||||
Version: util.Version(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
content = bytes.ReplaceAll(content, []byte("\"%CONFIG%\""), uiConfigBytes)
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
c.Data(200, "text/html", content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUI(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
contains string
|
|
||||||
cached bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "index_html",
|
|
||||||
path: "/",
|
|
||||||
contains: "SEBRAUC",
|
|
||||||
cached: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "index_html2",
|
|
||||||
path: "/index.html",
|
|
||||||
contains: "SEBRAUC",
|
|
||||||
cached: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "index_js",
|
|
||||||
path: path.Join("/assets", getIndexJS()),
|
|
||||||
contains: "SEBRAUC",
|
|
||||||
cached: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
Register(router)
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", tt.path, nil)
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
assert.Contains(t, w.Body.String(), tt.contains)
|
|
||||||
|
|
||||||
ccHeader := w.Header().Get("Cache-Control")
|
|
||||||
|
|
||||||
if tt.cached {
|
|
||||||
assert.Equal(t, "public, max-age=604800, immutable", ccHeader)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, "", ccHeader)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIndexJS() string {
|
|
||||||
baseDir := "ui/dist/assets"
|
|
||||||
indexExp := regexp.MustCompile(`index\.[0-9a-f]{8}\.js`)
|
|
||||||
|
|
||||||
fixtures.CdProjectRoot()
|
|
||||||
distDir, err := os.Open(baseDir)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
list, err := distDir.Readdir(-1)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range list {
|
|
||||||
if indexExp.MatchString(f.Name()) {
|
|
||||||
return f.Name()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic("no index.js found")
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
pipeline:
|
|
||||||
Frontend build:
|
|
||||||
image: node:16-alpine
|
|
||||||
commands:
|
|
||||||
- cd ui
|
|
||||||
- npm install -g pnpm
|
|
||||||
- pnpm install
|
|
||||||
- pnpm run build
|
|
||||||
Backend test:
|
|
||||||
image: golangci/golangci-lint:latest
|
|
||||||
commands:
|
|
||||||
- go get -t ./src/...
|
|
||||||
- golangci-lint run --timeout 5m
|
|
||||||
- go test -v ./src/...
|
|
Loading…
Add table
Reference in a new issue