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, }) }