From 545c4ddfecd365fb4fdf7acc41d6547781a998ef Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Wed, 24 Sep 2025 00:23:04 -0700 Subject: [PATCH 01/14] Began implementing SSE for cloning status/progress --- internal/api/handlers/cloning_handler.go | 25 ++++++++++++++++++++++- internal/cloning/types.go | 7 +++++++ internal/tools/sse/sse.go | 26 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 internal/tools/sse/sse.go diff --git a/internal/api/handlers/cloning_handler.go b/internal/api/handlers/cloning_handler.go index 30d8a43..e81e738 100644 --- a/internal/api/handlers/cloning_handler.go +++ b/internal/api/handlers/cloning_handler.go @@ -11,6 +11,7 @@ import ( "github.com/cpp-cyber/proclone/internal/ldap" "github.com/cpp-cyber/proclone/internal/proxmox" "github.com/cpp-cyber/proclone/internal/tools" + "github.com/cpp-cyber/proclone/internal/tools/sse" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) @@ -88,6 +89,16 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) { return } + // Create new sse object for streaming + sseWriter, err := sse.NewWriter(c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to initialize SSE", + "details": err.Error(), + }) + return + } + // Create the cloning request using the new format cloneReq := cloning.CloneRequest{ Template: req.Template, @@ -98,6 +109,7 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) { IsGroup: false, }, }, + SSE: sseWriter, } if err := ch.Service.CloneTemplate(cloneReq); err != nil { @@ -144,16 +156,27 @@ func (ch *CloningHandler) AdminCloneTemplateHandler(c *gin.Context) { }) } + // Create new sse object for streaming + sseWriter, err := sse.NewWriter(c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to initialize SSE", + "details": err.Error(), + }) + return + } + // Create clone request cloneReq := cloning.CloneRequest{ Template: req.Template, Targets: targets, CheckExistingDeployments: false, StartingVMID: req.StartingVMID, + SSE: sseWriter, } // Perform clone operation - err := ch.Service.CloneTemplate(cloneReq) + err = ch.Service.CloneTemplate(cloneReq) if err != nil { log.Printf("Admin %s encountered error while bulk cloning template: %v", username, err) c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/internal/cloning/types.go b/internal/cloning/types.go index 50a2a45..cb1000b 100644 --- a/internal/cloning/types.go +++ b/internal/cloning/types.go @@ -7,6 +7,7 @@ import ( "github.com/cpp-cyber/proclone/internal/ldap" "github.com/cpp-cyber/proclone/internal/proxmox" + "github.com/cpp-cyber/proclone/internal/tools/sse" "github.com/gin-gonic/gin" ) @@ -116,6 +117,7 @@ type CloneRequest struct { Targets []CloneTarget CheckExistingDeployments bool // Whether to check if templates are already deployed StartingVMID int // Optional starting VMID for admin clones + SSE *sse.Writer } type RouterInfo struct { @@ -124,3 +126,8 @@ type RouterInfo struct { Node string VMID int } + +type ProgressMessage struct { + Message string `json:"message"` + Progress int `json:"progress"` +} diff --git a/internal/tools/sse/sse.go b/internal/tools/sse/sse.go new file mode 100644 index 0000000..324f1b8 --- /dev/null +++ b/internal/tools/sse/sse.go @@ -0,0 +1,26 @@ +package sse + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type Writer struct { + w http.ResponseWriter + f http.Flusher +} + +func NewWriter(w http.ResponseWriter) (*Writer, error) { + f, ok := w.(http.Flusher) + if !ok { + return nil, fmt.Errorf("streaming unsupported") + } + return &Writer{w: w, f: f}, nil +} + +func (s *Writer) Send(message any) { + b, _ := json.Marshal(message) + fmt.Fprintf(s.w, "data: %s\n", b) + s.f.Flush() +} From 5e0acdfb26e336675e7bd2c0ec52faf4de892982 Mon Sep 17 00:00:00 2001 From: HGWJ Date: Thu, 25 Sep 2025 21:21:22 -0700 Subject: [PATCH 02/14] update for vyos router --- internal/cloning/cloning_service.go | 11 ++- internal/cloning/networking.go | 111 ++++++++++++++++++++-------- internal/cloning/types.go | 4 +- 3 files changed, 91 insertions(+), 35 deletions(-) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index 31c3d5d..b6c8187 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -89,7 +89,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { for _, vm := range templatePool { // Check to see if this VM is the router lowerVMName := strings.ToLower(vm.Name) - if strings.Contains(lowerVMName, "router") || strings.Contains(lowerVMName, "pfsense") { + if strings.Contains(lowerVMName, "router") || strings.Contains(lowerVMName, "pfsense") || strings.Contains(lowerVMName, "vyos") { router = &proxmox.VM{ Name: vm.Name, Node: vm.NodeName, @@ -186,9 +186,16 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { if err != nil { errors = append(errors, fmt.Sprintf("failed to clone router VM for %s: %v", target.Name, err)) } else { + // Determine router type + routerType, err := cs.getRouterType(*router) + if err != nil { + errors = append(errors, fmt.Sprintf("failed to get router type for %s: %v", target.Name, err)) + } + // Store router info for later operations clonedRouters = append(clonedRouters, RouterInfo{ TargetName: target.Name, + RouterType: routerType, PodNumber: target.PodNumber, Node: bestNode, VMID: target.VMIDs[0], @@ -288,7 +295,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } log.Printf("Configuring pod router for %s (Pod: %d, VMID: %d)", routerInfo.TargetName, routerInfo.PodNumber, routerInfo.VMID) - err = cs.configurePodRouter(routerInfo.PodNumber, routerInfo.Node, routerInfo.VMID) + err = cs.configurePodRouter(routerInfo.PodNumber, routerInfo.Node, routerInfo.VMID, routerInfo.RouterType) if err != nil { errors = append(errors, fmt.Sprintf("failed to configure pod router for %s: %v", routerInfo.TargetName, err)) } diff --git a/internal/cloning/networking.go b/internal/cloning/networking.go index 0cc56f3..6e2fd6e 100644 --- a/internal/cloning/networking.go +++ b/internal/cloning/networking.go @@ -5,13 +5,35 @@ import ( "log" "math" "regexp" + "strings" "time" + "github.com/cpp-cyber/proclone/internal/proxmox" "github.com/cpp-cyber/proclone/internal/tools" ) +func (cs *CloningService) getRouterType(router proxmox.VM) (string, error) { + infoReq := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/config", router.Node, router.VMID), + } + + infoRsp, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(infoReq) + if err != nil { + return "", fmt.Errorf("request for router type failed: %v", err) + } + switch { + case strings.Contains(string(infoRsp), "pfsense"): + return "pfsense", nil + case strings.Contains(string(infoRsp), "vyos"): + return "vyos", nil + default: + return "", fmt.Errorf("router type not defined") + } +} + // configurePodRouter configures the pod router with proper networking settings -func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid int) error { +func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid int, routerType string) error { // Wait for router agent to be pingable statusReq := tools.ProxmoxAPIRequest{ Method: "POST", @@ -37,42 +59,67 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in backoff = time.Duration(math.Min(float64(backoff*2), float64(maxBackoff))) } - // Configure router WAN IP to have correct third octet using qemu agent API call - reqBody := map[string]any{ - "command": []string{ - cs.Config.WANScriptPath, - fmt.Sprintf("%s%d.1", cs.Config.WANIPBase, podNumber), - }, - } + // Clone depending on router type + switch routerType { + case "pfsense": + // Configure router WAN IP to have correct third octet using qemu agent API call + reqBody := map[string]any{ + "command": []string{ + cs.Config.WANScriptPath, + fmt.Sprintf("%s%d.1", cs.Config.WANIPBase, podNumber), + }, + } - execReq := tools.ProxmoxAPIRequest{ - Method: "POST", - Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", node, vmid), - RequestBody: reqBody, - } + execReq := tools.ProxmoxAPIRequest{ + Method: "POST", + Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", node, vmid), + RequestBody: reqBody, + } - _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(execReq) - if err != nil { - return fmt.Errorf("failed to make IP change request: %v", err) - } + _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(execReq) + if err != nil { + return fmt.Errorf("failed to make IP change request: %v", err) + } - // Send agent exec request to change VIP subnet - vipReqBody := map[string]any{ - "command": []string{ - cs.Config.VIPScriptPath, - fmt.Sprintf("%s%d.0", cs.Config.WANIPBase, podNumber), - }, - } + // Send agent exec request to change VIP subnet + vipReqBody := map[string]any{ + "command": []string{ + cs.Config.VIPScriptPath, + fmt.Sprintf("%s%d.0", cs.Config.WANIPBase, podNumber), + }, + } - vipExecReq := tools.ProxmoxAPIRequest{ - Method: "POST", - Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", node, vmid), - RequestBody: vipReqBody, - } + vipExecReq := tools.ProxmoxAPIRequest{ + Method: "POST", + Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", node, vmid), + RequestBody: vipReqBody, + } - _, err = cs.ProxmoxService.GetRequestHelper().MakeRequest(vipExecReq) - if err != nil { - return fmt.Errorf("failed to make VIP change request: %v", err) + _, err = cs.ProxmoxService.GetRequestHelper().MakeRequest(vipExecReq) + if err != nil { + return fmt.Errorf("failed to make VIP change request: %v", err) + } + case "vyos": + reqBody := map[string]any{ + "command": []string{ + cs.Config.VYOSScriptPath, + fmt.Sprintf("%s%d.1", cs.Config.WANIPBase, podNumber), + }, + } + + execReq := tools.ProxmoxAPIRequest{ + Method: "POST", + Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", node, vmid), + RequestBody: reqBody, + } + + _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(execReq) + if err != nil { + return fmt.Errorf("failed to make IP change request: %v", err) + } + + default: + return fmt.Errorf("router type invalid") } return nil diff --git a/internal/cloning/types.go b/internal/cloning/types.go index cb1000b..8cbdd14 100644 --- a/internal/cloning/types.go +++ b/internal/cloning/types.go @@ -13,7 +13,7 @@ import ( // Config holds the configuration for cloning operations type Config struct { - RouterName string `envconfig:"ROUTER_NAME" default:"1-1NAT-pfsense"` + RouterName string `envconfig:"ROUTER_NAME" default:"1-1NAT-vyos"` RouterVMID int `envconfig:"ROUTER_VMID"` RouterNode string `envconfig:"ROUTER_NODE"` MinPodID int `envconfig:"MIN_POD_ID" default:"1001"` @@ -23,6 +23,7 @@ type Config struct { SDNApplyTimeout time.Duration `envconfig:"SDN_APPLY_TIMEOUT" default:"30s"` WANScriptPath string `envconfig:"WAN_SCRIPT_PATH" default:"/home/update-wan-ip.sh"` VIPScriptPath string `envconfig:"VIP_SCRIPT_PATH" default:"/home/update-wan-vip.sh"` + VYOSScriptPath string `envconfig:"VYOS_SCRIPT_PATH" default:"vbash /config/scripts/setup.sh"` WANIPBase string `envconfig:"WAN_IP_BASE" default:"172.16."` } @@ -122,6 +123,7 @@ type CloneRequest struct { type RouterInfo struct { TargetName string + RouterType string PodNumber int Node string VMID int From 97b586ff54a7419dbf57610cc879c4dc569ad8e2 Mon Sep 17 00:00:00 2001 From: HGWJ Date: Fri, 26 Sep 2025 13:43:45 -0700 Subject: [PATCH 03/14] consistency and fix commands --- internal/cloning/networking.go | 4 +++- internal/cloning/types.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/cloning/networking.go b/internal/cloning/networking.go index 6e2fd6e..f19bee6 100644 --- a/internal/cloning/networking.go +++ b/internal/cloning/networking.go @@ -102,8 +102,10 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in case "vyos": reqBody := map[string]any{ "command": []string{ + "vbash", cs.Config.VYOSScriptPath, - fmt.Sprintf("%s%d.1", cs.Config.WANIPBase, podNumber), + fmt.Sprintf("%d", podNumber), + cs.Config.WANIPBase, }, } diff --git a/internal/cloning/types.go b/internal/cloning/types.go index 8cbdd14..bb6dc51 100644 --- a/internal/cloning/types.go +++ b/internal/cloning/types.go @@ -23,7 +23,7 @@ type Config struct { SDNApplyTimeout time.Duration `envconfig:"SDN_APPLY_TIMEOUT" default:"30s"` WANScriptPath string `envconfig:"WAN_SCRIPT_PATH" default:"/home/update-wan-ip.sh"` VIPScriptPath string `envconfig:"VIP_SCRIPT_PATH" default:"/home/update-wan-vip.sh"` - VYOSScriptPath string `envconfig:"VYOS_SCRIPT_PATH" default:"vbash /config/scripts/setup.sh"` + VYOSScriptPath string `envconfig:"VYOS_SCRIPT_PATH" default:"/config/scripts/setup.sh"` WANIPBase string `envconfig:"WAN_IP_BASE" default:"172.16."` } From 84fb707e03e98b1b5d2ff88b6c82d9571a5f4a3a Mon Sep 17 00:00:00 2001 From: HGWJ Date: Fri, 26 Sep 2025 14:35:28 -0700 Subject: [PATCH 04/14] change different script to prevent changing config during boot issue --- internal/cloning/networking.go | 7 ++++--- internal/cloning/types.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/cloning/networking.go b/internal/cloning/networking.go index f19bee6..ab8011a 100644 --- a/internal/cloning/networking.go +++ b/internal/cloning/networking.go @@ -102,10 +102,11 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in case "vyos": reqBody := map[string]any{ "command": []string{ - "vbash", + "sed", + "-i", + "-e", + fmt.Sprintf("'s/{{THIRD_OCTET}}/%d/g;s/{{NETWORK_PREFIX}}/%s/g'", podNumber, cs.Config.WANIPBase), cs.Config.VYOSScriptPath, - fmt.Sprintf("%d", podNumber), - cs.Config.WANIPBase, }, } diff --git a/internal/cloning/types.go b/internal/cloning/types.go index bb6dc51..abefd34 100644 --- a/internal/cloning/types.go +++ b/internal/cloning/types.go @@ -23,7 +23,7 @@ type Config struct { SDNApplyTimeout time.Duration `envconfig:"SDN_APPLY_TIMEOUT" default:"30s"` WANScriptPath string `envconfig:"WAN_SCRIPT_PATH" default:"/home/update-wan-ip.sh"` VIPScriptPath string `envconfig:"VIP_SCRIPT_PATH" default:"/home/update-wan-vip.sh"` - VYOSScriptPath string `envconfig:"VYOS_SCRIPT_PATH" default:"/config/scripts/setup.sh"` + VYOSScriptPath string `envconfig:"VYOS_SCRIPT_PATH" default:"/config/scripts/vyos-postconfig-bootup.script"` WANIPBase string `envconfig:"WAN_IP_BASE" default:"172.16."` } From f93a9286ee6ab8fc6e7a1466708a2f3db36b8b68 Mon Sep 17 00:00:00 2001 From: HGWJ Date: Fri, 26 Sep 2025 15:08:55 -0700 Subject: [PATCH 05/14] new command --- internal/cloning/networking.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/cloning/networking.go b/internal/cloning/networking.go index ab8011a..194629c 100644 --- a/internal/cloning/networking.go +++ b/internal/cloning/networking.go @@ -102,11 +102,9 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in case "vyos": reqBody := map[string]any{ "command": []string{ - "sed", - "-i", - "-e", - fmt.Sprintf("'s/{{THIRD_OCTET}}/%d/g;s/{{NETWORK_PREFIX}}/%s/g'", podNumber, cs.Config.WANIPBase), - cs.Config.VYOSScriptPath, + "sh", + "-c", + fmt.Sprintf("sed -i -e 's/{{THIRD_OCTET}}/%d/g;s/{{NETWORK_PREFIX}}/%s/g' %s", podNumber, cs.Config.WANIPBase, cs.Config.VYOSScriptPath), }, } From 43dd92be58042397ec866f94ae34be591d2cdb01 Mon Sep 17 00:00:00 2001 From: HGWJ Date: Fri, 26 Sep 2025 15:22:38 -0700 Subject: [PATCH 06/14] missed something --- internal/cloning/networking.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cloning/networking.go b/internal/cloning/networking.go index 194629c..c7475fa 100644 --- a/internal/cloning/networking.go +++ b/internal/cloning/networking.go @@ -139,7 +139,7 @@ func (cs *CloningService) SetPodVnet(poolName string, vnetName string) error { log.Printf("Setting VNet %s for %d VMs in pool %s", vnetName, len(vms), poolName) - routerRegex := regexp.MustCompile(`(?i).*(router|pfsense).*`) + routerRegex := regexp.MustCompile(`(?i).*(router|pfsense|vyos).*`) var errors []string for _, vm := range vms { From 20e577109a40581a8bbb75a6e92feb5eefba3de8 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Sat, 27 Sep 2025 14:44:05 -0700 Subject: [PATCH 07/14] Standardized pod names to lowercase for pod getting because active directory --- internal/api/handlers/cloning_handler.go | 2 +- internal/api/handlers/dashboard_handler.go | 2 +- internal/cloning/pods.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/api/handlers/cloning_handler.go b/internal/api/handlers/cloning_handler.go index e81e738..1c262a7 100644 --- a/internal/api/handlers/cloning_handler.go +++ b/internal/api/handlers/cloning_handler.go @@ -287,7 +287,7 @@ func (ch *CloningHandler) GetPodsHandler(c *gin.Context) { // Loop through the user's deployed pods and add template information for i := range pods { - templateName := strings.Replace(pods[i].Name[5:], fmt.Sprintf("_%s", username), "", 1) + templateName := strings.Replace(strings.ToLower(pods[i].Name[5:]), fmt.Sprintf("_%s", strings.ToLower(username)), "", 1) templateInfo, err := ch.Service.DatabaseService.GetTemplateInfo(templateName) if err != nil { log.Printf("Error retrieving template info for pod %s: %v", pods[i].Name, err) diff --git a/internal/api/handlers/dashboard_handler.go b/internal/api/handlers/dashboard_handler.go index 8571840..62321b5 100644 --- a/internal/api/handlers/dashboard_handler.go +++ b/internal/api/handlers/dashboard_handler.go @@ -91,7 +91,7 @@ func (dh *DashboardHandler) GetUserDashboardStatsHandler(c *gin.Context) { // Loop through the user's deployed pods and add template information for i := range pods { - templateName := strings.Replace(pods[i].Name[5:], fmt.Sprintf("_%s", username), "", 1) + templateName := strings.Replace(strings.ToLower(pods[i].Name[5:]), fmt.Sprintf("_%s", strings.ToLower(username)), "", 1) templateInfo, err := dh.cloningHandler.Service.DatabaseService.GetTemplateInfo(templateName) if err != nil { log.Printf("Error retrieving template info for pod %s: %v", pods[i].Name, err) diff --git a/internal/cloning/pods.go b/internal/cloning/pods.go index dad1518..0ef5880 100644 --- a/internal/cloning/pods.go +++ b/internal/cloning/pods.go @@ -23,7 +23,7 @@ func (cs *CloningService) GetPods(username string) ([]Pod, error) { // Build regex pattern to match username or any of their group names groupsWithUser := append(groups, username) - regexPattern := fmt.Sprintf(`1[0-9]{3}_.*_(%s)`, strings.Join(groupsWithUser, "|")) + regexPattern := fmt.Sprintf(`(?i)1[0-9]{3}_.*_(%s)`, strings.Join(groupsWithUser, "|")) // Get pods based on regex pattern pods, err := cs.MapVirtualResourcesToPods(regexPattern) @@ -87,11 +87,11 @@ func (cs *CloningService) ValidateCloneRequest(templateName string, username str for _, pod := range podPools { // Remove the Pod ID number and _ to compare - if !alreadyDeployed && pod.Name[5:] == templateName { + if !alreadyDeployed && strings.EqualFold(pod.Name[5:], templateName) { alreadyDeployed = true } - if strings.Contains(pod.Name, username) { + if strings.Contains(strings.ToLower(pod.Name), strings.ToLower(username)) { numDeployments++ } } From 343e2b84611eaf73517d69794e2045bf310acec6 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Tue, 30 Sep 2025 13:42:14 -0700 Subject: [PATCH 08/14] Setup progress info for user clones --- internal/api/handlers/cloning_handler.go | 7 ++ internal/api/middleware/authorization.go | 4 +- internal/cloning/cloning_service.go | 108 ++++++++++++++++++++++- internal/tools/sse/sse.go | 2 +- 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/internal/api/handlers/cloning_handler.go b/internal/api/handlers/cloning_handler.go index 1c262a7..932041f 100644 --- a/internal/api/handlers/cloning_handler.go +++ b/internal/api/handlers/cloning_handler.go @@ -99,6 +99,13 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) { return } + sseWriter.Send( + cloning.ProgressMessage{ + Message: "Starting cloning process...", + Progress: 0, + }, + ) + // Create the cloning request using the new format cloneReq := cloning.CloneRequest{ Template: req.Template, diff --git a/internal/api/middleware/authorization.go b/internal/api/middleware/authorization.go index e173d39..6fb069f 100644 --- a/internal/api/middleware/authorization.go +++ b/internal/api/middleware/authorization.go @@ -63,12 +63,14 @@ func Logout(c *gin.Context) { func CORSMiddleware(fqdn string) gin.HandlerFunc { return func(c *gin.Context) { - c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.Header().Set("Content-Type", "application/json; text/event-stream") c.Writer.Header().Set("Access-Control-Allow-Origin", fqdn) c.Writer.Header().Set("Access-Control-Max-Age", "86400") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Origin") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(200) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index b6c8187..8998ef6 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -63,6 +63,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { var clonedRouters []RouterInfo // 1. Get the template pool and its VMs + req.SSE.Send( + ProgressMessage{ + Message: "Retrieving template information...", + Progress: 2, + }, + ) + templatePool, err := cs.ProxmoxService.GetPoolVMs("kamino_template_" + req.Template) if err != nil { return fmt.Errorf("failed to get template pool: %w", err) @@ -70,6 +77,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // 2. Check if any template is already deployed (if requested) if req.CheckExistingDeployments { + req.SSE.Send( + ProgressMessage{ + Message: "Checking existing deployments...", + Progress: 4, + }, + ) + for _, target := range req.Targets { targetPoolName := fmt.Sprintf("%s_%s", req.Template, target.Name) isValid, err := cs.ValidateCloneRequest(targetPoolName, target.Name) @@ -83,6 +97,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 3. Identify router and other VMs + req.SSE.Send( + ProgressMessage{ + Message: "Identifying template VMs...", + Progress: 6, + }, + ) + var router *proxmox.VM var templateVMs []proxmox.VM @@ -119,6 +140,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 5. Get pod IDs, Numbers, and VMIDs and assign them to targets + req.SSE.Send( + ProgressMessage{ + Message: "Allocating resources...", + Progress: 8, + }, + ) + numVMsPerTarget := len(templateVMs) + 1 // +1 for router log.Printf("Number of VMs per target (including router): %d", numVMsPerTarget) @@ -156,6 +184,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 6. Create new pool for each target + req.SSE.Send( + ProgressMessage{ + Message: "Creating proxmox pool...", + Progress: 10, + }, + ) + for _, target := range req.Targets { err = cs.ProxmoxService.CreateNewPool(target.PoolName) if err != nil { @@ -175,6 +210,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // Clone router + req.SSE.Send( + ProgressMessage{ + Message: "Cloning router VM...", + Progress: 15, + }, + ) + routerCloneReq := proxmox.VMCloneRequest{ SourceVM: *router, PoolName: target.PoolName, @@ -204,6 +246,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // Clone each VM to new pool for i, vm := range templateVMs { + req.SSE.Send( + ProgressMessage{ + Message: fmt.Sprintf("Cloning VM (%d/%d): %s", i+1, len(templateVMs), vm.Name), + Progress: 20 + int(float64(i+1)/float64(len(templateVMs))*20), + }, + ) + vmCloneReq := proxmox.VMCloneRequest{ SourceVM: vm, PoolName: target.PoolName, @@ -219,6 +268,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 8. Wait for all VM clone operations to complete before configuring VNets + req.SSE.Send( + ProgressMessage{ + Message: "Waiting for clone operations to complete...", + Progress: 42, + }, + ) + log.Printf("Waiting for clone operations to complete for %d targets", len(req.Targets)) for _, target := range req.Targets { // Wait for all VMs in the pool to be properly cloned @@ -247,6 +303,12 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { cs.vmidMutex.Unlock() // 9. Configure VNet of all VMs + req.SSE.Send( + ProgressMessage{ + Message: "Configuring VNets...", + Progress: 45, + }, + ) log.Printf("Configuring VNets for %d targets", len(req.Targets)) for _, target := range req.Targets { vnetName := fmt.Sprintf("kamino%d", target.PodNumber) @@ -258,6 +320,12 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 10. Start all routers and wait for them to be running + req.SSE.Send( + ProgressMessage{ + Message: "Starting routers...", + Progress: 50, + }, + ) log.Printf("Starting %d routers", len(clonedRouters)) for _, routerInfo := range clonedRouters { // Wait for router disk to be available @@ -285,8 +353,24 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 11. Configure all pod routers (separate step after all routers are running) + req.SSE.Send( + ProgressMessage{ + Message: "Configuring pod routers (This may take a few minutes)...", + Progress: 60, + }, + ) + log.Printf("Configuring %d pod routers", len(clonedRouters)) - for _, routerInfo := range clonedRouters { + for i, routerInfo := range clonedRouters { + // Send progress update for each router being configured + routerProgress := 60 + int(float64(i)/float64(len(clonedRouters))*30) + req.SSE.Send( + ProgressMessage{ + Message: fmt.Sprintf("Configuring router (%d/%d)...", i+1, len(clonedRouters)), + Progress: routerProgress, + }, + ) + // Double-check that router is still running before configuration err = cs.ProxmoxService.WaitForRunning(routerInfo.Node, routerInfo.VMID) if err != nil { @@ -301,7 +385,21 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } } + // Router configuration complete - update progress + req.SSE.Send( + ProgressMessage{ + Message: "Router configuration complete. Finalizing deployment...", + Progress: 90, + }, + ) + // 12. Set permissions on the pool to the user/group + req.SSE.Send( + ProgressMessage{ + Message: "Setting pool permissions...", + Progress: 95, + }, + ) for _, target := range req.Targets { err = cs.ProxmoxService.SetPoolPermission(target.PoolName, target.Name, target.IsGroup) if err != nil { @@ -321,6 +419,14 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { return fmt.Errorf("bulk clone operation completed with errors: %v", errors) } + // Final completion message + req.SSE.Send( + ProgressMessage{ + Message: "Template cloning completed successfully!", + Progress: 100, + }, + ) + return nil } diff --git a/internal/tools/sse/sse.go b/internal/tools/sse/sse.go index 324f1b8..18a7e0b 100644 --- a/internal/tools/sse/sse.go +++ b/internal/tools/sse/sse.go @@ -21,6 +21,6 @@ func NewWriter(w http.ResponseWriter) (*Writer, error) { func (s *Writer) Send(message any) { b, _ := json.Marshal(message) - fmt.Fprintf(s.w, "data: %s\n", b) + fmt.Fprintf(s.w, "data: %s\n\n", b) s.f.Flush() } From 12b629c2d7ef874ae2138ac165f0785530bfc5b0 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Tue, 30 Sep 2025 15:11:59 -0700 Subject: [PATCH 09/14] Moved validity check and also reduced the amount of progress updates --- internal/api/handlers/cloning_handler.go | 24 ++++++- internal/cloning/cloning_service.go | 90 +++--------------------- 2 files changed, 32 insertions(+), 82 deletions(-) diff --git a/internal/api/handlers/cloning_handler.go b/internal/api/handlers/cloning_handler.go index 932041f..b3d5bf6 100644 --- a/internal/api/handlers/cloning_handler.go +++ b/internal/api/handlers/cloning_handler.go @@ -89,6 +89,26 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) { return } + // Check for existing deployments before starting SSE + targetPoolName := fmt.Sprintf("%s_%s", req.Template, username) + isValid, err := ch.Service.ValidateCloneRequest(targetPoolName, username) + if err != nil { + log.Printf("Error validating deployment for user %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to validate existing deployments", + "details": err.Error(), + }) + return + } + if !isValid { + log.Printf("Template %s is already deployed for user %s or they have exceeded deployment limits", req.Template, username) + c.JSON(http.StatusConflict, gin.H{ + "error": "Deployment not allowed", + "details": fmt.Sprintf("Template %s is already deployed for %s or they have exceeded the maximum of 5 deployed pods", req.Template, username), + }) + return + } + // Create new sse object for streaming sseWriter, err := sse.NewWriter(c.Writer) if err != nil { @@ -101,7 +121,7 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) { sseWriter.Send( cloning.ProgressMessage{ - Message: "Starting cloning process...", + Message: "Retrieving template information", Progress: 0, }, ) @@ -109,7 +129,7 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) { // Create the cloning request using the new format cloneReq := cloning.CloneRequest{ Template: req.Template, - CheckExistingDeployments: true, // Check for existing deployments for single user clones + CheckExistingDeployments: false, // Already checked above Targets: []cloning.CloneTarget{ { Name: username, diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index 8998ef6..15a7113 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -63,13 +63,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { var clonedRouters []RouterInfo // 1. Get the template pool and its VMs - req.SSE.Send( - ProgressMessage{ - Message: "Retrieving template information...", - Progress: 2, - }, - ) - templatePool, err := cs.ProxmoxService.GetPoolVMs("kamino_template_" + req.Template) if err != nil { return fmt.Errorf("failed to get template pool: %w", err) @@ -77,13 +70,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // 2. Check if any template is already deployed (if requested) if req.CheckExistingDeployments { - req.SSE.Send( - ProgressMessage{ - Message: "Checking existing deployments...", - Progress: 4, - }, - ) - for _, target := range req.Targets { targetPoolName := fmt.Sprintf("%s_%s", req.Template, target.Name) isValid, err := cs.ValidateCloneRequest(targetPoolName, target.Name) @@ -97,13 +83,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 3. Identify router and other VMs - req.SSE.Send( - ProgressMessage{ - Message: "Identifying template VMs...", - Progress: 6, - }, - ) - var router *proxmox.VM var templateVMs []proxmox.VM @@ -140,13 +119,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 5. Get pod IDs, Numbers, and VMIDs and assign them to targets - req.SSE.Send( - ProgressMessage{ - Message: "Allocating resources...", - Progress: 8, - }, - ) - numVMsPerTarget := len(templateVMs) + 1 // +1 for router log.Printf("Number of VMs per target (including router): %d", numVMsPerTarget) @@ -184,13 +156,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 6. Create new pool for each target - req.SSE.Send( - ProgressMessage{ - Message: "Creating proxmox pool...", - Progress: 10, - }, - ) - for _, target := range req.Targets { err = cs.ProxmoxService.CreateNewPool(target.PoolName) if err != nil { @@ -201,6 +166,13 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 7. Clone targets to proxmox + req.SSE.Send( + ProgressMessage{ + Message: "Cloning VMs", + Progress: 10, + }, + ) + for _, target := range req.Targets { // Find best node per target bestNode, err := cs.ProxmoxService.FindBestNode() @@ -210,13 +182,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // Clone router - req.SSE.Send( - ProgressMessage{ - Message: "Cloning router VM...", - Progress: 15, - }, - ) - routerCloneReq := proxmox.VMCloneRequest{ SourceVM: *router, PoolName: target.PoolName, @@ -246,13 +211,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // Clone each VM to new pool for i, vm := range templateVMs { - req.SSE.Send( - ProgressMessage{ - Message: fmt.Sprintf("Cloning VM (%d/%d): %s", i+1, len(templateVMs), vm.Name), - Progress: 20 + int(float64(i+1)/float64(len(templateVMs))*20), - }, - ) - vmCloneReq := proxmox.VMCloneRequest{ SourceVM: vm, PoolName: target.PoolName, @@ -268,13 +226,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // 8. Wait for all VM clone operations to complete before configuring VNets - req.SSE.Send( - ProgressMessage{ - Message: "Waiting for clone operations to complete...", - Progress: 42, - }, - ) - log.Printf("Waiting for clone operations to complete for %d targets", len(req.Targets)) for _, target := range req.Targets { // Wait for all VMs in the pool to be properly cloned @@ -303,12 +254,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { cs.vmidMutex.Unlock() // 9. Configure VNet of all VMs - req.SSE.Send( - ProgressMessage{ - Message: "Configuring VNets...", - Progress: 45, - }, - ) log.Printf("Configuring VNets for %d targets", len(req.Targets)) for _, target := range req.Targets { vnetName := fmt.Sprintf("kamino%d", target.PodNumber) @@ -323,7 +268,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { req.SSE.Send( ProgressMessage{ Message: "Starting routers...", - Progress: 50, + Progress: 25, }, ) log.Printf("Starting %d routers", len(clonedRouters)) @@ -356,21 +301,12 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { req.SSE.Send( ProgressMessage{ Message: "Configuring pod routers (This may take a few minutes)...", - Progress: 60, + Progress: 33, }, ) log.Printf("Configuring %d pod routers", len(clonedRouters)) - for i, routerInfo := range clonedRouters { - // Send progress update for each router being configured - routerProgress := 60 + int(float64(i)/float64(len(clonedRouters))*30) - req.SSE.Send( - ProgressMessage{ - Message: fmt.Sprintf("Configuring router (%d/%d)...", i+1, len(clonedRouters)), - Progress: routerProgress, - }, - ) - + for _, routerInfo := range clonedRouters { // Double-check that router is still running before configuration err = cs.ProxmoxService.WaitForRunning(routerInfo.Node, routerInfo.VMID) if err != nil { @@ -394,12 +330,6 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { ) // 12. Set permissions on the pool to the user/group - req.SSE.Send( - ProgressMessage{ - Message: "Setting pool permissions...", - Progress: 95, - }, - ) for _, target := range req.Targets { err = cs.ProxmoxService.SetPoolPermission(target.PoolName, target.Name, target.IsGroup) if err != nil { From 028410d8d2baabbdf465c7207a7a4121862bef64 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Wed, 1 Oct 2025 12:23:20 -0700 Subject: [PATCH 10/14] Fixed progress not completing if small error occured. Also general cleanup from old logging --- internal/cloning/cloning_service.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index 15a7113..b418e3b 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -324,7 +324,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // Router configuration complete - update progress req.SSE.Send( ProgressMessage{ - Message: "Router configuration complete. Finalizing deployment...", + Message: "Finalizing deployment...", Progress: 90, }, ) @@ -343,20 +343,20 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { errors = append(errors, fmt.Sprintf("failed to increment template deployments for %s: %v", req.Template, err)) } + // Final completion message + // req.SSE.Send( + // ProgressMessage{ + // Message: "Template cloning completed!", + // Progress: 100, + // }, + // ) + // Handle errors and cleanup if necessary if len(errors) > 0 { cs.cleanupFailedClones(createdPools) return fmt.Errorf("bulk clone operation completed with errors: %v", errors) } - // Final completion message - req.SSE.Send( - ProgressMessage{ - Message: "Template cloning completed successfully!", - Progress: 100, - }, - ) - return nil } @@ -400,9 +400,7 @@ func (cs *CloningService) DeletePod(pod string) error { VMID: vm.VmId, }) stoppedCount++ - } else { } - } else { } } From 19295c51fc33cb9d05cba1c2ded9a38faae4e3bd Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Wed, 1 Oct 2025 13:11:27 -0700 Subject: [PATCH 11/14] Fixed api not sending final progress message --- internal/cloning/cloning_service.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index b418e3b..b52f0c5 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -344,12 +344,12 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } // Final completion message - // req.SSE.Send( - // ProgressMessage{ - // Message: "Template cloning completed!", - // Progress: 100, - // }, - // ) + req.SSE.Send( + ProgressMessage{ + Message: "Template cloning completed!", + Progress: 100, + }, + ) // Handle errors and cleanup if necessary if len(errors) > 0 { From ffd8355847f262858eef18bd2ae89054d3fa8831 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Wed, 1 Oct 2025 13:57:17 -0700 Subject: [PATCH 12/14] Fixed duplicated ellipsis since they are now in the frontend --- internal/cloning/cloning_service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index b52f0c5..dd1b378 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -267,7 +267,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // 10. Start all routers and wait for them to be running req.SSE.Send( ProgressMessage{ - Message: "Starting routers...", + Message: "Starting routers", Progress: 25, }, ) @@ -300,7 +300,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // 11. Configure all pod routers (separate step after all routers are running) req.SSE.Send( ProgressMessage{ - Message: "Configuring pod routers (This may take a few minutes)...", + Message: "Configuring pod routers", Progress: 33, }, ) @@ -324,7 +324,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // Router configuration complete - update progress req.SSE.Send( ProgressMessage{ - Message: "Finalizing deployment...", + Message: "Finalizing deployment", Progress: 90, }, ) From 90df91cc788d7dc0a6a66e789c1c2e4081e49dc8 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Wed, 1 Oct 2025 13:57:59 -0700 Subject: [PATCH 13/14] Changed disk check API route to use gonk statically since this node seems to have the most reliable response time --- internal/proxmox/vms.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/proxmox/vms.go b/internal/proxmox/vms.go index 6b2f3be..18f817a 100644 --- a/internal/proxmox/vms.go +++ b/internal/proxmox/vms.go @@ -140,9 +140,10 @@ func (s *ProxmoxService) WaitForDisk(node string, vmID int, maxWait time.Duratio } if configResp.HardDisk != "" { + //TODO/NOTE: Using static node "gonk" here because it seems to be the most reliable pendingReq := tools.ProxmoxAPIRequest{ Method: "GET", - Endpoint: fmt.Sprintf("/nodes/%s/storage/%s/content?vmid=%d", node, s.Config.StorageID, vmID), + Endpoint: fmt.Sprintf("/nodes/gonk/storage/%s/content?vmid=%d", s.Config.StorageID, vmID), } var diskResponse []PendingDiskResponse From a98faac8cfe17fa899dc5593faa876c4ca80c9e1 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Thu, 2 Oct 2025 13:01:49 -0700 Subject: [PATCH 14/14] Converted router check to a regex --- internal/cloning/cloning_service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index dd1b378..346fd48 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -5,7 +5,7 @@ import ( "fmt" "log" "os" - "strings" + "regexp" "time" "github.com/cpp-cyber/proclone/internal/ldap" @@ -85,11 +85,11 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { // 3. Identify router and other VMs var router *proxmox.VM var templateVMs []proxmox.VM + routerPattern := regexp.MustCompile(`(?i)(router|pfsense|vyos)`) for _, vm := range templatePool { // Check to see if this VM is the router - lowerVMName := strings.ToLower(vm.Name) - if strings.Contains(lowerVMName, "router") || strings.Contains(lowerVMName, "pfsense") || strings.Contains(lowerVMName, "vyos") { + if routerPattern.MatchString(vm.Name) { router = &proxmox.VM{ Name: vm.Name, Node: vm.NodeName,