Phase 17g - Double Authentification: - TOTP avec Google Authenticator/Authy - QR code pour enrôlement - Codes de backup (10 codes usage unique) - Page /admin/security pour gestion 2FA - Page /admin/users avec Reset 2FA (super_admin) - 2FA obligatoire pour rôles configurés Phase 21 - Infrastructure Management: - SQLite pour données infra (/data/infra.db) - SSH Pool avec reconnexion auto - Gestion Incus (list, start, stop, restart, sync) - Gestion Nginx (test, reload, deploy, sync, certbot) - Interface admin /admin/infra - Formulaire ajout serveur - Page détail serveur avec containers et sites Fichiers créés: - internal/infra/ (db, models, migrations, repository, ssh, incus, nginx) - cmd/sogoms/admin/totp.go - cmd/sogoms/admin/handlers_2fa.go - cmd/sogoms/admin/handlers_infra.go - Templates: 2fa_*, security, users, infra, server_* 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
228 lines
6.9 KiB
Go
228 lines
6.9 KiB
Go
// Package infra gère l'infrastructure (serveurs, containers, nginx).
|
|
package infra
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// IncusContainer représente un container Incus retourné par incus list.
|
|
type IncusContainer struct {
|
|
Name string `json:"name"`
|
|
State string `json:"state"`
|
|
IPv4 []string `json:"ipv4"`
|
|
IPv6 []string `json:"ipv6"`
|
|
Type string `json:"type"`
|
|
Snapshots int `json:"snapshots"`
|
|
Location string `json:"location"`
|
|
Image string `json:"-"` // Rempli séparément
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// incusListJSON représente le format JSON de incus list.
|
|
type incusListJSON struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Type string `json:"type"`
|
|
Snapshots int `json:"snapshots"`
|
|
Location string `json:"location"`
|
|
IPv4 string `json:"ipv4"`
|
|
IPv6 string `json:"ipv6"`
|
|
}
|
|
|
|
// ListIncusContainers liste les containers Incus sur le serveur.
|
|
func (c *SSHClient) ListIncusContainers(ctx context.Context) ([]IncusContainer, error) {
|
|
// Récupérer la liste en format JSON
|
|
result, err := c.Exec(ctx, "incus list --format json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("incus list: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return nil, fmt.Errorf("incus list failed: %s", result.Stderr)
|
|
}
|
|
|
|
// Parser le JSON
|
|
var rawContainers []struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
StatusCode int `json:"status_code"`
|
|
Type string `json:"type"`
|
|
Config struct {
|
|
Image string `json:"image.description"`
|
|
} `json:"config"`
|
|
State struct {
|
|
Network map[string]struct {
|
|
Addresses []struct {
|
|
Address string `json:"address"`
|
|
Family string `json:"family"`
|
|
Scope string `json:"scope"`
|
|
} `json:"addresses"`
|
|
} `json:"network"`
|
|
} `json:"state"`
|
|
Snapshots []interface{} `json:"snapshots"`
|
|
Location string `json:"location"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(result.Stdout), &rawContainers); err != nil {
|
|
return nil, fmt.Errorf("parse incus json: %w", err)
|
|
}
|
|
|
|
// Convertir en notre format
|
|
containers := make([]IncusContainer, 0, len(rawContainers))
|
|
for _, rc := range rawContainers {
|
|
container := IncusContainer{
|
|
Name: rc.Name,
|
|
State: strings.ToLower(rc.Status),
|
|
Type: rc.Type,
|
|
Snapshots: len(rc.Snapshots),
|
|
Location: rc.Location,
|
|
Image: rc.Config.Image,
|
|
CreatedAt: rc.CreatedAt,
|
|
}
|
|
|
|
// Extraire les IPs
|
|
for _, net := range rc.State.Network {
|
|
for _, addr := range net.Addresses {
|
|
if addr.Scope == "global" {
|
|
if addr.Family == "inet" {
|
|
container.IPv4 = append(container.IPv4, addr.Address)
|
|
} else if addr.Family == "inet6" {
|
|
container.IPv6 = append(container.IPv6, addr.Address)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
containers = append(containers, container)
|
|
}
|
|
|
|
return containers, nil
|
|
}
|
|
|
|
// GetIncusContainer récupère les infos d'un container spécifique.
|
|
func (c *SSHClient) GetIncusContainer(ctx context.Context, name string) (*IncusContainer, error) {
|
|
containers, err := c.ListIncusContainers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, container := range containers {
|
|
if container.Name == name {
|
|
return &container, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("container %s not found", name)
|
|
}
|
|
|
|
// StartIncusContainer démarre un container.
|
|
func (c *SSHClient) StartIncusContainer(ctx context.Context, name string) error {
|
|
result, err := c.Exec(ctx, fmt.Sprintf("incus start %s", name))
|
|
if err != nil {
|
|
return fmt.Errorf("incus start: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return fmt.Errorf("incus start %s failed: %s", name, result.Stderr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StopIncusContainer arrête un container.
|
|
func (c *SSHClient) StopIncusContainer(ctx context.Context, name string) error {
|
|
result, err := c.Exec(ctx, fmt.Sprintf("incus stop %s", name))
|
|
if err != nil {
|
|
return fmt.Errorf("incus stop: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return fmt.Errorf("incus stop %s failed: %s", name, result.Stderr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RestartIncusContainer redémarre un container.
|
|
func (c *SSHClient) RestartIncusContainer(ctx context.Context, name string) error {
|
|
result, err := c.Exec(ctx, fmt.Sprintf("incus restart %s", name))
|
|
if err != nil {
|
|
return fmt.Errorf("incus restart: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return fmt.Errorf("incus restart %s failed: %s", name, result.Stderr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ExecInContainer exécute une commande dans un container.
|
|
func (c *SSHClient) ExecInContainer(ctx context.Context, containerName, cmd string) (*SSHResult, error) {
|
|
fullCmd := fmt.Sprintf("incus exec %s -- %s", containerName, cmd)
|
|
return c.Exec(ctx, fullCmd)
|
|
}
|
|
|
|
// ExecInContainerSimple exécute une commande et retourne stdout.
|
|
func (c *SSHClient) ExecInContainerSimple(ctx context.Context, containerName, cmd string) (string, error) {
|
|
result, err := c.ExecInContainer(ctx, containerName, cmd)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return "", fmt.Errorf("command failed in %s (exit %d): %s", containerName, result.ExitCode, result.Stderr)
|
|
}
|
|
return strings.TrimSpace(result.Stdout), nil
|
|
}
|
|
|
|
// PushFileToContainer envoie un fichier vers un container.
|
|
func (c *SSHClient) PushFileToContainer(ctx context.Context, containerName, localPath, remotePath string) error {
|
|
// D'abord copier vers le serveur hôte
|
|
tempPath := fmt.Sprintf("/tmp/incus-push-%d", ctx.Value("request_id"))
|
|
if err := c.CopyFile(ctx, localPath, tempPath); err != nil {
|
|
return fmt.Errorf("copy to host: %w", err)
|
|
}
|
|
|
|
// Puis push vers le container
|
|
result, err := c.Exec(ctx, fmt.Sprintf("incus file push %s %s%s && rm %s",
|
|
tempPath, containerName, remotePath, tempPath))
|
|
if err != nil {
|
|
return fmt.Errorf("incus file push: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return fmt.Errorf("incus file push failed: %s", result.Stderr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PullFileFromContainer récupère un fichier depuis un container.
|
|
func (c *SSHClient) PullFileFromContainer(ctx context.Context, containerName, remotePath, localPath string) error {
|
|
tempPath := fmt.Sprintf("/tmp/incus-pull-%d", ctx.Value("request_id"))
|
|
|
|
// Pull depuis le container vers l'hôte
|
|
result, err := c.Exec(ctx, fmt.Sprintf("incus file pull %s%s %s",
|
|
containerName, remotePath, tempPath))
|
|
if err != nil {
|
|
return fmt.Errorf("incus file pull: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return fmt.Errorf("incus file pull failed: %s", result.Stderr)
|
|
}
|
|
|
|
// Puis copier vers local
|
|
if err := c.CopyFrom(ctx, tempPath, localPath); err != nil {
|
|
return fmt.Errorf("copy from host: %w", err)
|
|
}
|
|
|
|
// Nettoyer
|
|
c.Exec(ctx, fmt.Sprintf("rm %s", tempPath))
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetContainerLogs récupère les logs d'un container.
|
|
func (c *SSHClient) GetContainerLogs(ctx context.Context, containerName string, lines int) (string, error) {
|
|
if lines <= 0 {
|
|
lines = 100
|
|
}
|
|
return c.ExecInContainerSimple(ctx, containerName, fmt.Sprintf("journalctl -n %d --no-pager", lines))
|
|
}
|