186 lines
5.5 KiB
Go
186 lines
5.5 KiB
Go
|
|
package deployer
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"runtime"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"nexusrmm.local/agent/internal/executor"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Payload wird vom Backend als JSON im cmd.Payload-Feld gesendet.
|
||
|
|
type Payload struct {
|
||
|
|
PackageName string `json:"packageName"`
|
||
|
|
PackageManager string `json:"packageManager"` // "choco", "apt", "dnf", "direct"
|
||
|
|
InstallerUrl string `json:"installerUrl"`
|
||
|
|
Checksum string `json:"checksum"` // SHA256 (hex, optional)
|
||
|
|
SilentArgs string `json:"silentArgs"`
|
||
|
|
DisplayName string `json:"displayName"`
|
||
|
|
Version string `json:"version"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Install installiert ein Software-Paket auf dem aktuellen System.
|
||
|
|
func Install(ctx context.Context, payloadJSON string) *executor.Result {
|
||
|
|
payload, err := parsePayload(payloadJSON)
|
||
|
|
if err != nil {
|
||
|
|
return errorResult(fmt.Sprintf("Ungültiges Payload-JSON: %v", err))
|
||
|
|
}
|
||
|
|
|
||
|
|
switch payload.PackageManager {
|
||
|
|
case "choco":
|
||
|
|
return installWithChoco(ctx, payload.PackageName)
|
||
|
|
case "apt":
|
||
|
|
return installWithApt(ctx, payload.PackageName)
|
||
|
|
case "dnf":
|
||
|
|
return installWithDnf(ctx, payload.PackageName)
|
||
|
|
case "direct":
|
||
|
|
return installDirect(ctx, *payload)
|
||
|
|
default:
|
||
|
|
// Auto-detect
|
||
|
|
return installAutoDetect(ctx, *payload)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Uninstall deinstalliert ein Software-Paket.
|
||
|
|
func Uninstall(ctx context.Context, payloadJSON string) *executor.Result {
|
||
|
|
payload, err := parsePayload(payloadJSON)
|
||
|
|
if err != nil {
|
||
|
|
return errorResult(fmt.Sprintf("Ungültiges Payload-JSON: %v", err))
|
||
|
|
}
|
||
|
|
|
||
|
|
switch payload.PackageManager {
|
||
|
|
case "choco":
|
||
|
|
return executor.Execute(ctx, fmt.Sprintf("choco uninstall %s -y", payload.PackageName), 600)
|
||
|
|
case "apt":
|
||
|
|
return executor.Execute(ctx, fmt.Sprintf("apt-get remove -y %s", payload.PackageName), 600)
|
||
|
|
case "dnf":
|
||
|
|
return executor.Execute(ctx, fmt.Sprintf("dnf remove -y %s", payload.PackageName), 600)
|
||
|
|
default:
|
||
|
|
return installAutoDetect(ctx, *payload)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func installWithChoco(ctx context.Context, packageName string) *executor.Result {
|
||
|
|
return executor.Execute(ctx,
|
||
|
|
fmt.Sprintf("choco install %s -y --no-progress", packageName), 600)
|
||
|
|
}
|
||
|
|
|
||
|
|
func installWithApt(ctx context.Context, packageName string) *executor.Result {
|
||
|
|
return executor.Execute(ctx,
|
||
|
|
fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y %s", packageName), 600)
|
||
|
|
}
|
||
|
|
|
||
|
|
func installWithDnf(ctx context.Context, packageName string) *executor.Result {
|
||
|
|
return executor.Execute(ctx,
|
||
|
|
fmt.Sprintf("dnf install -y %s", packageName), 600)
|
||
|
|
}
|
||
|
|
|
||
|
|
func installAutoDetect(ctx context.Context, payload Payload) *executor.Result {
|
||
|
|
if runtime.GOOS == "windows" {
|
||
|
|
// Versuche choco, dann winget als Fallback
|
||
|
|
if isCommandAvailable("choco") {
|
||
|
|
return installWithChoco(ctx, payload.PackageName)
|
||
|
|
}
|
||
|
|
if payload.InstallerUrl != "" {
|
||
|
|
return installDirect(ctx, payload)
|
||
|
|
}
|
||
|
|
return errorResult("Kein unterstützter Paketmanager auf Windows verfügbar (choco nicht gefunden)")
|
||
|
|
}
|
||
|
|
// Linux
|
||
|
|
if isCommandAvailable("apt-get") {
|
||
|
|
return installWithApt(ctx, payload.PackageName)
|
||
|
|
}
|
||
|
|
if isCommandAvailable("dnf") {
|
||
|
|
return installWithDnf(ctx, payload.PackageName)
|
||
|
|
}
|
||
|
|
if payload.InstallerUrl != "" {
|
||
|
|
return installDirect(ctx, payload)
|
||
|
|
}
|
||
|
|
return errorResult("Kein unterstützter Paketmanager auf Linux verfügbar")
|
||
|
|
}
|
||
|
|
|
||
|
|
func installDirect(ctx context.Context, payload Payload) *executor.Result {
|
||
|
|
if payload.InstallerUrl == "" {
|
||
|
|
return errorResult("Direct-Install: Keine InstallerUrl angegeben")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Temp-Datei herunterladen
|
||
|
|
tmpFile, err := os.CreateTemp("", "nexusrmm-install-*")
|
||
|
|
if err != nil {
|
||
|
|
return errorResult(fmt.Sprintf("Temp-Datei konnte nicht erstellt werden: %v", err))
|
||
|
|
}
|
||
|
|
tmpPath := tmpFile.Name()
|
||
|
|
defer os.Remove(tmpPath)
|
||
|
|
|
||
|
|
resp, err := http.Get(payload.InstallerUrl)
|
||
|
|
if err != nil {
|
||
|
|
tmpFile.Close()
|
||
|
|
return errorResult(fmt.Sprintf("Download fehlgeschlagen: %v", err))
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
hasher := sha256.New()
|
||
|
|
writer := io.MultiWriter(tmpFile, hasher)
|
||
|
|
if _, err := io.Copy(writer, resp.Body); err != nil {
|
||
|
|
tmpFile.Close()
|
||
|
|
return errorResult(fmt.Sprintf("Download-Fehler beim Schreiben: %v", err))
|
||
|
|
}
|
||
|
|
tmpFile.Close()
|
||
|
|
|
||
|
|
// Checksum prüfen
|
||
|
|
if payload.Checksum != "" {
|
||
|
|
actualHash := hex.EncodeToString(hasher.Sum(nil))
|
||
|
|
expected := strings.ToLower(payload.Checksum)
|
||
|
|
if actualHash != expected {
|
||
|
|
return errorResult(fmt.Sprintf("Prüfsummen-Fehler: erwartet %s, erhalten %s", expected, actualHash))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Installer ausführen
|
||
|
|
ext := strings.ToLower(filepath.Ext(payload.InstallerUrl))
|
||
|
|
var installCmd string
|
||
|
|
switch {
|
||
|
|
case runtime.GOOS == "windows" && ext == ".msi":
|
||
|
|
installCmd = fmt.Sprintf("msiexec /i \"%s\" /quiet /norestart %s", tmpPath, payload.SilentArgs)
|
||
|
|
case runtime.GOOS == "windows" && (ext == ".exe"):
|
||
|
|
installCmd = fmt.Sprintf("\"%s\" %s", tmpPath, payload.SilentArgs)
|
||
|
|
case runtime.GOOS == "linux" && (ext == ".deb"):
|
||
|
|
installCmd = fmt.Sprintf("dpkg -i \"%s\"", tmpPath)
|
||
|
|
case runtime.GOOS == "linux" && (ext == ".rpm"):
|
||
|
|
installCmd = fmt.Sprintf("rpm -i \"%s\"", tmpPath)
|
||
|
|
default:
|
||
|
|
installCmd = fmt.Sprintf("\"%s\" %s", tmpPath, payload.SilentArgs)
|
||
|
|
}
|
||
|
|
|
||
|
|
return executor.Execute(ctx, installCmd, 1200)
|
||
|
|
}
|
||
|
|
|
||
|
|
func isCommandAvailable(name string) bool {
|
||
|
|
result := executor.Execute(context.Background(), "which "+name+" || where "+name, 5)
|
||
|
|
return result.Success || result.ExitCode == 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func parsePayload(payloadJSON string) (*Payload, error) {
|
||
|
|
var p Payload
|
||
|
|
if err := json.Unmarshal([]byte(payloadJSON), &p); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return &p, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func errorResult(msg string) *executor.Result {
|
||
|
|
return &executor.Result{
|
||
|
|
ExitCode: -1,
|
||
|
|
Stderr: msg,
|
||
|
|
Success: false,
|
||
|
|
}
|
||
|
|
}
|