235 lines
6.7 KiB
Go
235 lines
6.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/ansible"
|
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type AgentDeployGroup struct {
|
|
*Handlers
|
|
executor *ansible.Executor
|
|
}
|
|
|
|
func NewAgentDeployGroup(h *Handlers) *AgentDeployGroup {
|
|
workDir := os.Getenv("ANSIBLE_WORK_DIR")
|
|
if workDir == "" {
|
|
workDir = "/tmp/hellreign/ansible"
|
|
}
|
|
|
|
grpcPort := os.Getenv("GRPC_PORT")
|
|
if grpcPort == "" {
|
|
grpcPort = "9001"
|
|
}
|
|
|
|
grpcHost := os.Getenv("GRPC_SERVER_HOST")
|
|
if grpcHost == "" {
|
|
grpcHost = "0.0.0.0"
|
|
}
|
|
|
|
backendURL := os.Getenv("BACKEND_URL")
|
|
if backendURL == "" {
|
|
backendURL = "http://localhost:8080"
|
|
}
|
|
|
|
giteaURL := os.Getenv("GITEA_RELEASES_URL")
|
|
if giteaURL == "" {
|
|
giteaURL = "https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download"
|
|
}
|
|
|
|
exec := ansible.NewExecutor(ansible.ExecutorConfig{
|
|
WorkDir: workDir,
|
|
GRPCServerHost: grpcHost,
|
|
GRPCServerPort: grpcPort,
|
|
BackendURL: backendURL,
|
|
GiteaReleasesURL: giteaURL,
|
|
})
|
|
|
|
// Write playbooks on init
|
|
if err := exec.WriteAllPlaybooks(); err != nil {
|
|
// Log the error - deployment will fail later if playbooks can't be written
|
|
fmt.Fprintf(os.Stderr, "WARNING: failed to write Ansible playbooks: %v\n", err)
|
|
}
|
|
|
|
return &AgentDeployGroup{
|
|
Handlers: h,
|
|
executor: exec,
|
|
}
|
|
}
|
|
|
|
// DeployAgents deploys agents to multiple servers
|
|
// @Summary Deploy agents to multiple servers via Ansible
|
|
// @Description Deploy HellreigN agents to multiple servers using Ansible playbooks. Supports Docker and Binary deployment types.
|
|
// @Tags agents
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body repository.DeployAgentsRequest true "Deployment configuration for servers"
|
|
// @Success 200 {object} repository.DeployResponse "Deployment results with tokens for each server"
|
|
// @Failure 400 {object} map[string]string "Invalid request"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Security Bearer
|
|
// @Router /agents/deploy [post]
|
|
func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
|
|
var req repository.DeployAgentsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate auth credentials for each server
|
|
for i, server := range req.Servers {
|
|
switch server.AuthMethod {
|
|
case repository.AuthMethodKey:
|
|
if server.SSHKey == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("server %d (%s): sshKey is required when authMethod is 'key'", i, server.IP),
|
|
})
|
|
return
|
|
}
|
|
case repository.AuthMethodPassword:
|
|
if server.Password == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("server %d (%s): password is required when authMethod is 'password'", i, server.IP),
|
|
})
|
|
return
|
|
}
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("server %d (%s): invalid authMethod %q, expected 'key' or 'password'", i, server.IP, server.AuthMethod),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Pre-flight check: verify community.docker collection is available for docker deployments
|
|
needsDockerCollection := false
|
|
for _, server := range req.Servers {
|
|
if server.DeployType == repository.DeployTypeDocker {
|
|
needsDockerCollection = true
|
|
break
|
|
}
|
|
}
|
|
if needsDockerCollection {
|
|
if err := adg.executor.CheckDockerCollection(); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("Docker deployment requires 'community.docker' Ansible collection: %v", err),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Create work directory
|
|
workDir := adg.executor.WorkDir()
|
|
if err := os.MkdirAll(workDir, 0755); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create work directory"})
|
|
return
|
|
}
|
|
|
|
// Generate registration tokens for each server
|
|
results := make([]repository.DeployResult, 0, len(req.Servers))
|
|
timestamp := time.Now().UnixMilli()
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute)
|
|
defer cancel()
|
|
|
|
for i, server := range req.Servers {
|
|
// Create registration token
|
|
token, err := adg.Repo.CreateRegistrationToken(server.AgentLabel)
|
|
if err != nil {
|
|
results = append(results, repository.DeployResult{
|
|
IP: server.IP,
|
|
AgentLabel: server.AgentLabel,
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to create token: %v", err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Set default port
|
|
port := server.Port
|
|
if port == 0 {
|
|
port = 22
|
|
}
|
|
|
|
// Generate inventory for this single server
|
|
inventoryHosts := []ansible.InventoryHost{
|
|
{
|
|
Name: server.AgentLabel,
|
|
IP: server.IP,
|
|
Port: port,
|
|
User: server.User,
|
|
AuthMethod: string(server.AuthMethod),
|
|
SSHKey: server.SSHKey,
|
|
Password: server.Password,
|
|
DeployType: string(server.DeployType),
|
|
Token: token,
|
|
GRPCURL: adg.executor.GRPCURL(),
|
|
},
|
|
}
|
|
|
|
inventoryPath := filepath.Join(workDir, fmt.Sprintf("inventory_%d_%d", timestamp, i))
|
|
if err := ansible.GenerateInventory(inventoryHosts, inventoryPath); err != nil {
|
|
// Rollback: delete the token we just created
|
|
_ = adg.Repo.DeleteRegistrationToken(token)
|
|
results = append(results, repository.DeployResult{
|
|
IP: server.IP,
|
|
AgentLabel: server.AgentLabel,
|
|
Token: token,
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to generate inventory: %v", err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Run Ansible playbook for this server
|
|
deployResults, err := adg.executor.Deploy(ctx, inventoryPath, string(server.DeployType))
|
|
|
|
// Clean up inventory file (log error but don't fail deployment)
|
|
if cleanupErr := os.Remove(inventoryPath); cleanupErr != nil {
|
|
fmt.Fprintf(os.Stderr, "WARNING: failed to remove inventory file %s: %v\n", inventoryPath, cleanupErr)
|
|
}
|
|
|
|
if err != nil {
|
|
// Rollback: delete the token since deployment failed
|
|
_ = adg.Repo.DeleteRegistrationToken(token)
|
|
results = append(results, repository.DeployResult{
|
|
IP: server.IP,
|
|
AgentLabel: server.AgentLabel,
|
|
Token: token,
|
|
Success: false,
|
|
Error: fmt.Sprintf("deployment failed: %v", err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
success := true
|
|
errMsg := ""
|
|
if len(deployResults) > 0 && !deployResults[0].Success {
|
|
success = false
|
|
errMsg = deployResults[0].Stderr
|
|
// Rollback: delete the token since ansible playbook reported failure
|
|
_ = adg.Repo.DeleteRegistrationToken(token)
|
|
}
|
|
|
|
results = append(results, repository.DeployResult{
|
|
IP: server.IP,
|
|
AgentLabel: server.AgentLabel,
|
|
Token: token,
|
|
Success: success,
|
|
Error: errMsg,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, repository.DeployResponse{
|
|
Message: "Deployment completed",
|
|
Results: results,
|
|
})
|
|
}
|