feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ : - API REST + WebSocket (chi, gorilla/websocket) - Authentification PAM via SSH + JWT RS256 - Chiffrement AES-256-GCM pour secrets SQLite - Pool SSH, client Proxmox REST, hub WebSocket pub/sub - Système de modules compilés à initialisation conditionnelle - Audit log, migrations SQLite versionnées Frontend Vue 3 + Vite + TypeScript : - Thème Neumorphism sombre/clair (CSS custom properties) - Wizard d'installation, Dashboard drag-drop, Terminal xterm.js - Toutes les vues CORE + stubs modules optionnels - i18n EN/FR (vue-i18n v11) Infrastructure : - Docker multi-stage (Go → alpine, Node → nginx) - docker-compose.yml, .gitattributes, LICENSE MIT, README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
5dbcb1df07
66 changed files with 10370 additions and 0 deletions
216
backend/internal/ssh/pool.go
Normal file
216
backend/internal/ssh/pool.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Package ssh gère un pool de connexions SSH réutilisables vers le host Proxmox et les LXC.
|
||||
// Les connexions inactives depuis plus de 5 minutes sont automatiquement fermées.
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
idleTimeout = 5 * time.Minute
|
||||
connTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// poolEntry représente une connexion SSH dans le pool avec sa date de dernier usage.
|
||||
type poolEntry struct {
|
||||
client *gossh.Client
|
||||
lastUsed time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Pool est un pool thread-safe de connexions SSH.
|
||||
type Pool struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]*poolEntry
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewPool crée un pool SSH et démarre le nettoyage automatique des connexions inactives.
|
||||
func NewPool() *Pool {
|
||||
p := &Pool{
|
||||
entries: make(map[string]*poolEntry),
|
||||
ticker: time.NewTicker(1 * time.Minute),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go p.cleanup()
|
||||
return p
|
||||
}
|
||||
|
||||
// Close arrête le pool et ferme toutes les connexions.
|
||||
func (p *Pool) Close() {
|
||||
close(p.done)
|
||||
p.ticker.Stop()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
for _, entry := range p.entries {
|
||||
entry.client.Close()
|
||||
}
|
||||
p.entries = make(map[string]*poolEntry)
|
||||
}
|
||||
|
||||
// getOrCreate retourne une connexion existante ou en crée une nouvelle.
|
||||
func (p *Pool) getOrCreate(key, host, user, password string) (*poolEntry, error) {
|
||||
p.mu.Lock()
|
||||
entry, exists := p.entries[key]
|
||||
p.mu.Unlock()
|
||||
|
||||
if exists {
|
||||
// Vérifier que la connexion est toujours active
|
||||
entry.mu.Lock()
|
||||
_, _, err := entry.client.SendRequest("keepalive@openssh.com", true, nil)
|
||||
if err == nil {
|
||||
entry.lastUsed = time.Now()
|
||||
entry.mu.Unlock()
|
||||
return entry, nil
|
||||
}
|
||||
entry.mu.Unlock()
|
||||
// Connexion morte — on la supprime et en crée une nouvelle
|
||||
p.mu.Lock()
|
||||
delete(p.entries, key)
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// Créer une nouvelle connexion
|
||||
config := &gossh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []gossh.AuthMethod{
|
||||
gossh.Password(password),
|
||||
},
|
||||
Timeout: connTimeout,
|
||||
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
client, err := gossh.Dial("tcp", host, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connexion SSH vers %s : %w", host, err)
|
||||
}
|
||||
|
||||
newEntry := &poolEntry{
|
||||
client: client,
|
||||
lastUsed: time.Now(),
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.entries[key] = newEntry
|
||||
p.mu.Unlock()
|
||||
|
||||
return newEntry, nil
|
||||
}
|
||||
|
||||
// RunCommand exécute une commande sur l'hôte distant et retourne la sortie combinée.
|
||||
func (p *Pool) RunCommand(host, user, password, command string) (string, error) {
|
||||
key := fmt.Sprintf("%s@%s", user, host)
|
||||
entry, err := p.getOrCreate(key, host, user, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
|
||||
session, err := entry.client.NewSession()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ouverture session : %w", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
output, err := session.CombinedOutput(command)
|
||||
entry.lastUsed = time.Now()
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
|
||||
// StreamCommand exécute une commande et envoie sa sortie ligne par ligne dans le channel.
|
||||
// Le channel est fermé à la fin de la commande.
|
||||
func (p *Pool) StreamCommand(host, user, password, command string, output chan<- string) error {
|
||||
key := fmt.Sprintf("%s@%s", user, host)
|
||||
entry, err := p.getOrCreate(key, host, user, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.mu.Lock()
|
||||
session, err := entry.client.NewSession()
|
||||
entry.mu.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ouverture session : %w", err)
|
||||
}
|
||||
|
||||
// Utiliser un pipe pour lire la sortie en streaming
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return fmt.Errorf("pipe stdout : %w", err)
|
||||
}
|
||||
stderr, err := session.StderrPipe()
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return fmt.Errorf("pipe stderr : %w", err)
|
||||
}
|
||||
|
||||
if err := session.Start(command); err != nil {
|
||||
session.Close()
|
||||
return fmt.Errorf("démarrage commande : %w", err)
|
||||
}
|
||||
|
||||
// Lire stdout et stderr en goroutines et envoyer dans le channel
|
||||
var wg sync.WaitGroup
|
||||
readStream := func(r io.Reader) {
|
||||
defer wg.Done()
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
if n > 0 {
|
||||
output <- string(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(2)
|
||||
go readStream(stdout)
|
||||
go readStream(stderr)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
session.Wait()
|
||||
session.Close()
|
||||
close(output)
|
||||
p.mu.Lock()
|
||||
if e, ok := p.entries[key]; ok {
|
||||
e.lastUsed = time.Now()
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanup supprime périodiquement les connexions inactives depuis plus de idleTimeout.
|
||||
func (p *Pool) cleanup() {
|
||||
for {
|
||||
select {
|
||||
case <-p.done:
|
||||
return
|
||||
case <-p.ticker.C:
|
||||
p.mu.Lock()
|
||||
for key, entry := range p.entries {
|
||||
entry.mu.Lock()
|
||||
if time.Since(entry.lastUsed) > idleTimeout {
|
||||
entry.client.Close()
|
||||
delete(p.entries, key)
|
||||
}
|
||||
entry.mu.Unlock()
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue