From 51b4d810a5150392e049c7027a5391cac699c447 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Fri, 7 Nov 2025 11:24:06 -0800 Subject: [PATCH 1/6] Moved users and groups management to use proxmox instead of ldap --- internal/api/auth/auth_service.go | 40 +-- internal/api/auth/types.go | 8 +- internal/api/handlers/auth_handler.go | 284 +---------------- internal/api/handlers/dashboard_handler.go | 6 +- internal/api/handlers/proxmox_handler.go | 216 +++++++++++++ internal/api/handlers/types.go | 12 +- internal/api/routes/admin_routes.go | 22 +- internal/cloning/cloning_service.go | 27 +- internal/cloning/pods.go | 8 +- internal/cloning/types.go | 14 +- internal/ldap/groups.go | 326 -------------------- internal/ldap/types.go | 33 -- internal/ldap/users.go | 309 ------------------- internal/proxmox/cluster.go | 4 - internal/proxmox/groups.go | 231 ++++++++++++++ internal/{cloning => proxmox}/networking.go | 67 ++-- internal/proxmox/pools.go | 186 +++++++++++ internal/proxmox/types.go | 96 +++++- internal/proxmox/users.go | 98 ++++++ internal/proxmox/vms.go | 19 +- 20 files changed, 962 insertions(+), 1044 deletions(-) delete mode 100644 internal/ldap/groups.go create mode 100644 internal/proxmox/groups.go rename internal/{cloning => proxmox}/networking.go (70%) create mode 100644 internal/proxmox/users.go diff --git a/internal/api/auth/auth_service.go b/internal/api/auth/auth_service.go index 6d93b28..96c8db1 100644 --- a/internal/api/auth/auth_service.go +++ b/internal/api/auth/auth_service.go @@ -2,19 +2,21 @@ package auth import ( "fmt" - "strings" + "slices" "github.com/cpp-cyber/proclone/internal/ldap" + "github.com/cpp-cyber/proclone/internal/proxmox" ) -func NewAuthService() (*AuthService, error) { +func NewAuthService(proxmoxService *proxmox.ProxmoxService) (*AuthService, error) { ldapService, err := ldap.NewLDAPService() if err != nil { return nil, fmt.Errorf("failed to create LDAP service: %w", err) } return &AuthService{ - ldapService: ldapService, + ldapService: ldapService, + proxmoxService: proxmoxService, }, nil } @@ -51,38 +53,18 @@ func (s *AuthService) Authenticate(username string, password string) (bool, erro } func (s *AuthService) IsAdmin(username string) (bool, error) { - // Input validation - if username == "" { - return false, fmt.Errorf("username cannot be empty") - } - - // Get user DN - userDN, err := s.ldapService.GetUserDN(username) - if err != nil { - return false, fmt.Errorf("failed to get user DN: %w", err) - } - - // Get user's groups - userGroups, err := s.ldapService.GetUserGroups(userDN) + // Get user's groups from Proxmox + userGroups, err := s.proxmoxService.GetUserGroups(username) if err != nil { return false, fmt.Errorf("failed to get user groups: %w", err) } - // Load LDAP config to get admin group DN - config, err := ldap.LoadConfig() - if err != nil { - return false, fmt.Errorf("failed to load LDAP config: %w", err) - } - - if config.AdminGroupDN == "" { - return false, fmt.Errorf("admin group DN not configured") - } + // Get the admin group name from config + adminGroupName := s.proxmoxService.Config.AdminGroupName // Check if user is in the admin group - for _, groupDN := range userGroups { - if strings.EqualFold(groupDN, "Proxmox-Admins") { - return true, nil - } + if slices.Contains(userGroups, adminGroupName) { + return true, nil } return false, nil diff --git a/internal/api/auth/types.go b/internal/api/auth/types.go index 34420fa..d2c77cd 100644 --- a/internal/api/auth/types.go +++ b/internal/api/auth/types.go @@ -2,6 +2,7 @@ package auth import ( "github.com/cpp-cyber/proclone/internal/ldap" + "github.com/cpp-cyber/proclone/internal/proxmox" ) // ================================================= @@ -19,13 +20,14 @@ type Service interface { } type AuthService struct { - ldapService ldap.Service + ldapService ldap.Service + proxmoxService *proxmox.ProxmoxService } // ================================================= // Types for Auth Service (re-exported from ldap) // ================================================= -type User = ldap.User -type Group = ldap.Group +type User = proxmox.User +type Group = proxmox.Group type UserRegistrationInfo = ldap.UserRegistrationInfo diff --git a/internal/api/handlers/auth_handler.go b/internal/api/handlers/auth_handler.go index 3a15003..ed60269 100644 --- a/internal/api/handlers/auth_handler.go +++ b/internal/api/handlers/auth_handler.go @@ -18,19 +18,25 @@ import ( // NewAuthHandler creates a new authentication handler func NewAuthHandler() (*AuthHandler, error) { - authService, err := auth.NewAuthService() + proxmoxServiceInterface, err := proxmox.NewService() if err != nil { - return nil, fmt.Errorf("failed to create auth service: %w", err) + return nil, fmt.Errorf("failed to create proxmox service: %w", err) } - ldapService, err := ldap.NewLDAPService() + // Type assert to get concrete type for auth service + proxmoxService, ok := proxmoxServiceInterface.(*proxmox.ProxmoxService) + if !ok { + return nil, fmt.Errorf("failed to convert proxmox service to concrete type") + } + + authService, err := auth.NewAuthService(proxmoxService) if err != nil { - return nil, fmt.Errorf("failed to create LDAP service: %w", err) + return nil, fmt.Errorf("failed to create auth service: %w", err) } - proxmoxService, err := proxmox.NewService() + ldapService, err := ldap.NewLDAPService() if err != nil { - return nil, fmt.Errorf("failed to create proxmox service: %w", err) + return nil, fmt.Errorf("failed to create LDAP service: %w", err) } log.Println("Auth handler initialized") @@ -38,7 +44,7 @@ func NewAuthHandler() (*AuthHandler, error) { return &AuthHandler{ authService: authService, ldapService: ldapService, - proxmoxService: proxmoxService, + proxmoxService: proxmoxServiceInterface, }, nil } @@ -149,38 +155,6 @@ func (h *AuthHandler) RegisterHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "User registered successfully"}) } -// ================================================= -// User Handlers -// ================================================= - -// ADMIN: GetUsersHandler returns a list of all users -func (h *AuthHandler) GetUsersHandler(c *gin.Context) { - users, err := h.ldapService.GetUsers() - if err != nil { - log.Printf("Failed to retrieve users: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve users"}) - return - } - - var adminCount = 0 - var disabledCount = 0 - for _, user := range users { - if user.IsAdmin { - adminCount++ - } - if !user.Enabled { - disabledCount++ - } - } - - c.JSON(http.StatusOK, gin.H{ - "users": users, - "count": len(users), - "disabled_count": disabledCount, - "admin_count": adminCount, - }) -} - // ADMIN: CreateUsersHandler creates new user(s) func (h *AuthHandler) CreateUsersHandler(c *gin.Context) { var req AdminCreateUserRequest @@ -244,235 +218,3 @@ func (h *AuthHandler) DeleteUsersHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Users deleted successfully"}) } - -// ADMIN: EnableUsersHandler enables existing user(s) -func (h *AuthHandler) EnableUsersHandler(c *gin.Context) { - var req UsersRequest - if !validateAndBind(c, &req) { - return - } - - var errors []error - - for _, username := range req.Usernames { - if err := h.ldapService.EnableUserAccount(username); err != nil { - errors = append(errors, fmt.Errorf("failed to enable user %s: %v", username, err)) - } - } - - if len(errors) > 0 { - log.Printf("Failed to enable users: %v", errors) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to enable users", "details": errors}) - return - } - - // Sync users to Proxmox - if err := h.proxmoxService.SyncUsers(); err != nil { - log.Printf("Failed to sync users with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync users with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Users enabled successfully"}) -} - -// ADMIN: DisableUsersHandler disables existing user(s) -func (h *AuthHandler) DisableUsersHandler(c *gin.Context) { - var req UsersRequest - if !validateAndBind(c, &req) { - return - } - - var errors []error - - for _, username := range req.Usernames { - if err := h.ldapService.DisableUserAccount(username); err != nil { - errors = append(errors, fmt.Errorf("failed to disable user %s: %v", username, err)) - } - } - - if len(errors) > 0 { - log.Printf("Failed to disable users: %v", errors) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable users", "details": errors}) - return - } - - // Sync users to Proxmox - if err := h.proxmoxService.SyncUsers(); err != nil { - log.Printf("Failed to sync users with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync users with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Users disabled successfully"}) -} - -// ================================================= -// Group Handlers -// ================================================= - -// ADMIN: SetUserGroupsHandler sets the groups for an existing user -func (h *AuthHandler) SetUserGroupsHandler(c *gin.Context) { - var req SetUserGroupsRequest - if !validateAndBind(c, &req) { - return - } - - if err := h.ldapService.SetUserGroups(req.Username, req.Groups); err != nil { - log.Printf("Failed to set groups for user %s: %v", req.Username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set user groups", "details": err.Error()}) - return - } - - // Sync groups to Proxmox - if err := h.proxmoxService.SyncGroups(); err != nil { - log.Printf("Failed to sync groups with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "User groups updated successfully"}) -} - -func (h *AuthHandler) GetGroupsHandler(c *gin.Context) { - groups, err := h.ldapService.GetGroups() - if err != nil { - log.Printf("Failed to retrieve groups: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve groups"}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "groups": groups, - "count": len(groups), - }) -} - -// ADMIN: CreateGroupsHandler creates new group(s) -func (h *AuthHandler) CreateGroupsHandler(c *gin.Context) { - var req GroupsRequest - if !validateAndBind(c, &req) { - return - } - - var errors []error - - // Create groups in AD - for _, group := range req.Groups { - if err := h.ldapService.CreateGroup(group); err != nil { - errors = append(errors, fmt.Errorf("failed to create group %s: %v", group, err)) - } - } - - if len(errors) > 0 { - log.Printf("Failed to create groups: %v", errors) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create groups", "details": errors}) - return - } - - // Sync groups to Proxmox - if err := h.proxmoxService.SyncGroups(); err != nil { - log.Printf("Failed to sync groups with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{"message": "Groups created successfully"}) -} - -func (h *AuthHandler) RenameGroupHandler(c *gin.Context) { - var req RenameGroupRequest - if !validateAndBind(c, &req) { - return - } - - if err := h.ldapService.RenameGroup(req.OldName, req.NewName); err != nil { - log.Printf("Failed to rename group %s to %s: %v", req.OldName, req.NewName, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to rename group", "details": err.Error()}) - return - } - - // Sync groups to Proxmox - if err := h.proxmoxService.SyncGroups(); err != nil { - log.Printf("Failed to sync groups with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Group renamed successfully"}) -} - -func (h *AuthHandler) DeleteGroupsHandler(c *gin.Context) { - var req GroupsRequest - if !validateAndBind(c, &req) { - return - } - - var errors []error - - // Delete groups in AD - for _, group := range req.Groups { - if err := h.ldapService.DeleteGroup(group); err != nil { - errors = append(errors, fmt.Errorf("failed to delete group %s: %v", group, err)) - } - } - - if len(errors) > 0 { - log.Printf("Failed to delete groups: %v", errors) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete groups", "details": errors}) - return - } - - // Sync groups to Proxmox - if err := h.proxmoxService.SyncGroups(); err != nil { - log.Printf("Failed to sync groups with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Groups deleted successfully"}) -} - -func (h *AuthHandler) AddUsersHandler(c *gin.Context) { - var req ModifyGroupMembersRequest - if !validateAndBind(c, &req) { - return - } - - if err := h.ldapService.AddUsersToGroup(req.Group, req.Usernames); err != nil { - log.Printf("Failed to add users to group %s: %v", req.Group, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add users to group", "details": err.Error()}) - return - } - - // Sync groups to Proxmox - if err := h.proxmoxService.SyncGroups(); err != nil { - log.Printf("Failed to sync groups with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Users added to group successfully"}) -} - -func (h *AuthHandler) RemoveUsersHandler(c *gin.Context) { - var req ModifyGroupMembersRequest - if !validateAndBind(c, &req) { - return - } - - if err := h.ldapService.RemoveUsersFromGroup(req.Group, req.Usernames); err != nil { - log.Printf("Failed to remove users from group %s: %v", req.Group, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove users from group", "details": err.Error()}) - return - } - - // Sync groups to Proxmox - if err := h.proxmoxService.SyncGroups(); err != nil { - log.Printf("Failed to sync groups with Proxmox: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Users removed from group successfully"}) -} diff --git a/internal/api/handlers/dashboard_handler.go b/internal/api/handlers/dashboard_handler.go index 62321b5..f80939b 100644 --- a/internal/api/handlers/dashboard_handler.go +++ b/internal/api/handlers/dashboard_handler.go @@ -24,7 +24,7 @@ func (dh *DashboardHandler) GetAdminDashboardStatsHandler(c *gin.Context) { stats := DashboardStats{} // Get user count - users, err := dh.authHandler.ldapService.GetUsers() + users, err := dh.proxmoxHandler.service.GetUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve user count", "details": err.Error()}) return @@ -32,7 +32,7 @@ func (dh *DashboardHandler) GetAdminDashboardStatsHandler(c *gin.Context) { stats.UserCount = len(users) // Get group count - groups, err := dh.authHandler.ldapService.GetGroups() + groups, err := dh.proxmoxHandler.service.GetGroups() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve group count", "details": err.Error()}) return @@ -102,7 +102,7 @@ func (dh *DashboardHandler) GetUserDashboardStatsHandler(c *gin.Context) { } // Get user's information - userInfo, err := dh.authHandler.ldapService.GetUser(username) + userInfo, err := dh.authHandler.proxmoxService.GetUser(username) if err != nil { log.Printf("Error retrieving user info for %s: %v", username, err) c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/internal/api/handlers/proxmox_handler.go b/internal/api/handlers/proxmox_handler.go index 0bc8676..0c3e581 100644 --- a/internal/api/handlers/proxmox_handler.go +++ b/internal/api/handlers/proxmox_handler.go @@ -4,8 +4,10 @@ import ( "fmt" "log" "net/http" + "strings" "github.com/cpp-cyber/proclone/internal/proxmox" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) @@ -96,3 +98,217 @@ func (ph *ProxmoxHandler) RebootVMHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "VM rebooted"}) } + +// ADMIN: GetUsersHandler handles GET requests for retrieving all users from Proxmox +func (ph *ProxmoxHandler) GetUsersHandler(c *gin.Context) { + users, err := ph.service.GetUsers() + if err != nil { + log.Printf("Error retrieving users: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve users", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "count": len(users), + "users": users, + }) +} + +// ADMIN: GetGroupsHandler handles GET requests for retrieving all groups from Proxmox +func (ph *ProxmoxHandler) GetGroupsHandler(c *gin.Context) { + groups, err := ph.service.GetGroups() + if err != nil { + log.Printf("Error retrieving groups: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve groups", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "count": len(groups), + "groups": groups, + }) +} + +// ADMIN: SetUserGroupsHandler handles POST requests for setting user groups in Proxmox +func (ph *ProxmoxHandler) SetUserGroupsHandler(c *gin.Context) { + var req SetUserGroupsRequest + if !validateAndBind(c, &req) { + return + } + + if err := ph.service.SetUserGroups(req.Username, req.Groups); err != nil { + log.Printf("Error setting groups for user %s: %v", req.Username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set user groups", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "User groups updated"}) +} + +// ADMIN: CreateGroupsHandler handles POST requests for creating groups in Proxmox +func (ph *ProxmoxHandler) CreateGroupsHandler(c *gin.Context) { + var req GroupsRequest + if !validateAndBind(c, &req) { + return + } + + var errors []string + var created []string + + for _, groupName := range req.Groups { + if err := ph.service.CreateGroup(groupName, ""); err != nil { + log.Printf("Error creating group %s: %v", groupName, err) + errors = append(errors, fmt.Sprintf("Failed to create group %s: %v", groupName, err)) + } else { + created = append(created, groupName) + } + } + + if len(errors) > 0 { + c.JSON(http.StatusPartialContent, gin.H{ + "status": "Partial success", + "created": created, + "errors": errors, + }) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "Groups created", "created": created}) +} + +// ADMIN: AddUsersHandler handles POST requests for adding users to a group in Proxmox +func (ph *ProxmoxHandler) AddUsersHandler(c *gin.Context) { + var req ModifyGroupMembersRequest + if !validateAndBind(c, &req) { + return + } + + if err := ph.service.AddUsersToGroup(req.Group, req.Usernames); err != nil { + log.Printf("Error adding users to group %s: %v", req.Group, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add users to group", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "Users added to group"}) +} + +// ADMIN: RemoveUsersHandler handles POST requests for removing users from a group in Proxmox +func (ph *ProxmoxHandler) RemoveUsersHandler(c *gin.Context) { + var req ModifyGroupMembersRequest + if !validateAndBind(c, &req) { + return + } + + if err := ph.service.RemoveUsersFromGroup(req.Group, req.Usernames); err != nil { + log.Printf("Error removing users from group %s: %v", req.Group, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove users from group", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "Users removed from group"}) +} + +// ADMIN: DeleteGroupsHandler handles POST requests for deleting groups in Proxmox +func (ph *ProxmoxHandler) DeleteGroupsHandler(c *gin.Context) { + var req GroupsRequest + if !validateAndBind(c, &req) { + return + } + + var errors []string + var deleted []string + + for _, groupName := range req.Groups { + if err := ph.service.DeleteGroup(groupName); err != nil { + log.Printf("Error deleting group %s: %v", groupName, err) + errors = append(errors, fmt.Sprintf("Failed to delete group %s: %v", groupName, err)) + } else { + deleted = append(deleted, groupName) + } + } + + if len(errors) > 0 { + c.JSON(http.StatusPartialContent, gin.H{ + "status": "Partial success", + "deleted": deleted, + "errors": errors, + }) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "Groups deleted", "deleted": deleted}) +} + +func (ph *ProxmoxHandler) EditGroupHandler(c *gin.Context) { + var req EditGroupRequest + if !validateAndBind(c, &req) { + return + } + + if err := ph.service.EditGroup(req.Group, req.Comment); err != nil { + log.Printf("Error editing group %s: %v", req.Group, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to edit group", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "Group edited"}) +} + +func (ph *ProxmoxHandler) GetVMTemplatesHandler(c *gin.Context) { + vmTemplates, err := ph.service.GetVMTemplates() + if err != nil { + log.Printf("Error getting VM templates: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get VM templates", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "VM templates retrieved", "vm_templates": vmTemplates}) +} + +func (ph *ProxmoxHandler) GetProxmoxTemplatePoolsHandler(c *gin.Context) { + templatePools, err := ph.service.GetTemplatePools() + if err != nil { + log.Printf("Error getting template pools: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get template pools", "details": err.Error()}) + return + } + + trimmedTemplatePools := make([]string, 0, len(templatePools)) + for _, pool := range templatePools { + trimmedTemplatePools = append(trimmedTemplatePools, strings.Replace(pool, "kamino_template_", "", 1)) + } + + c.JSON(http.StatusOK, gin.H{"status": "Template pools retrieved", "template_pools": trimmedTemplatePools}) +} + +func (ph *ProxmoxHandler) GetUsedVNetsHandler(c *gin.Context) { + vnets, err := ph.service.GetUsedVNets() + if err != nil { + log.Printf("Error getting VNets: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get VNets", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "VNets retrieved", "vnets": vnets}) +} + +func (ph *ProxmoxHandler) CreateTemplateHandler(c *gin.Context) { + session := sessions.Default(c) + username := session.Get("id").(string) + + var request CreateTemplateRequest + if err := c.ShouldBindJSON(&request); err != nil { + log.Printf("Error binding JSON: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()}) + return + } + + err := ph.service.CreateTemplatePool(username, request.Name, request.Router, request.VMs) + if err != nil { + log.Printf("Error creating template: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create template", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "Template created"}) +} diff --git a/internal/api/handlers/types.go b/internal/api/handlers/types.go index 7f8a5e0..fc7048c 100644 --- a/internal/api/handlers/types.go +++ b/internal/api/handlers/types.go @@ -103,9 +103,9 @@ type SetUserGroupsRequest struct { Groups []string `json:"groups" binding:"required,min=1,dive,min=1,max=100" validate:"dive,alphanum,ascii"` } -type RenameGroupRequest struct { - OldName string `json:"old_name" binding:"required,min=1,max=100" validate:"alphanum,ascii"` - NewName string `json:"new_name" binding:"required,min=1,max=100" validate:"alphanum,ascii"` +type EditGroupRequest struct { + Group string `json:"group"` + Comment string `json:"comment"` } type DashboardStats struct { @@ -117,6 +117,12 @@ type DashboardStats struct { ClusterResourceUsage any `json:"cluster"` } +type CreateTemplateRequest struct { + Name string `json:"name"` + Router bool `json:"add_router"` + VMs []proxmox.VM `json:"vms"` +} + // ================================================= // Private Functions // ================================================= diff --git a/internal/api/routes/admin_routes.go b/internal/api/routes/admin_routes.go index a97629d..d1816e7 100644 --- a/internal/api/routes/admin_routes.go +++ b/internal/api/routes/admin_routes.go @@ -10,29 +10,31 @@ func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, // GET Requests g.GET("/dashboard", dashboardHandler.GetAdminDashboardStatsHandler) g.GET("/cluster", proxmoxHandler.GetClusterResourceUsageHandler) - g.GET("/users", authHandler.GetUsersHandler) - g.GET("/groups", authHandler.GetGroupsHandler) + g.GET("/users", proxmoxHandler.GetUsersHandler) + g.GET("/groups", proxmoxHandler.GetGroupsHandler) g.GET("/vms", proxmoxHandler.GetVMsHandler) + g.GET("/vnets", proxmoxHandler.GetUsedVNetsHandler) g.GET("/pods", cloningHandler.AdminGetPodsHandler) g.GET("/templates", cloningHandler.AdminGetTemplatesHandler) g.GET("/templates/unpublished", cloningHandler.GetUnpublishedTemplatesHandler) + g.GET("/templates/vms", proxmoxHandler.GetVMTemplatesHandler) + g.GET("/templates/proxmox", proxmoxHandler.GetProxmoxTemplatePoolsHandler) // POST Requests g.POST("/users/create", authHandler.CreateUsersHandler) g.POST("/users/delete", authHandler.DeleteUsersHandler) - g.POST("/users/enable", authHandler.EnableUsersHandler) - g.POST("/users/disable", authHandler.DisableUsersHandler) - g.POST("/user/groups", authHandler.SetUserGroupsHandler) - g.POST("/groups/create", authHandler.CreateGroupsHandler) - g.POST("/group/members/add", authHandler.AddUsersHandler) - g.POST("/group/members/remove", authHandler.RemoveUsersHandler) - g.POST("/group/rename", authHandler.RenameGroupHandler) - g.POST("/groups/delete", authHandler.DeleteGroupsHandler) + g.POST("/user/groups", proxmoxHandler.SetUserGroupsHandler) + g.POST("/groups/create", proxmoxHandler.CreateGroupsHandler) + g.POST("/group/members/add", proxmoxHandler.AddUsersHandler) + g.POST("/group/members/remove", proxmoxHandler.RemoveUsersHandler) + g.POST("/group/edit", proxmoxHandler.EditGroupHandler) + g.POST("/groups/delete", proxmoxHandler.DeleteGroupsHandler) g.POST("/vm/start", proxmoxHandler.StartVMHandler) g.POST("/vm/shutdown", proxmoxHandler.ShutdownVMHandler) g.POST("/vm/reboot", proxmoxHandler.RebootVMHandler) g.POST("/pods/delete", cloningHandler.AdminDeletePodHandler) g.POST("/template/publish", cloningHandler.PublishTemplateHandler) + g.POST("/template/create", proxmoxHandler.CreateTemplateHandler) g.POST("/template/edit", cloningHandler.EditTemplateHandler) g.POST("/template/delete", cloningHandler.DeleteTemplateHandler) g.POST("/template/visibility", cloningHandler.ToggleTemplateVisibilityHandler) diff --git a/internal/cloning/cloning_service.go b/internal/cloning/cloning_service.go index 346fd48..23b1342 100644 --- a/internal/cloning/cloning_service.go +++ b/internal/cloning/cloning_service.go @@ -194,7 +194,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { 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) + routerType, err := cs.ProxmoxService.GetRouterType(*router) if err != nil { errors = append(errors, fmt.Sprintf("failed to get router type for %s: %v", target.Name, err)) } @@ -232,7 +232,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { log.Printf("Waiting for VMs in pool %s to be available", target.PoolName) time.Sleep(2 * time.Second) - // Check if pool has the expected number of VMs + // First wait for all VMs to appear in the pool for retries := range 30 { poolVMs, err := cs.ProxmoxService.GetPoolVMs(target.PoolName) if err != nil { @@ -241,13 +241,30 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { } if len(poolVMs) >= numVMsPerTarget { - log.Printf("Pool %s has %d VMs (expected %d) - clone operations complete", target.PoolName, len(poolVMs), numVMsPerTarget) + log.Printf("Pool %s has %d VMs (expected %d) - all VMs present", target.PoolName, len(poolVMs), numVMsPerTarget) break } log.Printf("Pool %s has %d VMs, waiting for %d (retry %d/30)", target.PoolName, len(poolVMs), numVMsPerTarget, retries+1) time.Sleep(2 * time.Second) } + + // Wait for all VM locks to be released + log.Printf("Waiting for all VM clone operations to complete for pool %s (checking locks)", target.PoolName) + poolVMs, err := cs.ProxmoxService.GetPoolVMs(target.PoolName) + if err != nil { + errors = append(errors, fmt.Sprintf("failed to get pool VMs after waiting for %s: %v", target.Name, err)) + continue + } + + for _, vm := range poolVMs { + log.Printf("Waiting for VM %d (%s) lock to be released", vm.VmId, vm.Name) + if err := cs.ProxmoxService.WaitForLock(vm.NodeName, vm.VmId); err != nil { + log.Printf("Warning: timeout waiting for VM %d lock, continuing anyway: %v", vm.VmId, err) + } + } + + log.Printf("All clone operations complete for pool %s", target.PoolName) } // Release the vmid allocation mutex now that all of the VMs are cloned on proxmox @@ -258,7 +275,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error { for _, target := range req.Targets { vnetName := fmt.Sprintf("kamino%d", target.PodNumber) log.Printf("Setting VNet %s for pool %s (target: %s)", vnetName, target.PoolName, target.Name) - err = cs.SetPodVnet(target.PoolName, vnetName) + err = cs.ProxmoxService.SetPodVnet(target.PoolName, vnetName) if err != nil { errors = append(errors, fmt.Sprintf("failed to update pod vnet for %s: %v", target.Name, err)) } @@ -315,7 +332,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, routerInfo.RouterType) + err = cs.ProxmoxService.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/pods.go b/internal/cloning/pods.go index 0ef5880..369b84a 100644 --- a/internal/cloning/pods.go +++ b/internal/cloning/pods.go @@ -9,14 +9,8 @@ import ( ) func (cs *CloningService) GetPods(username string) ([]Pod, error) { - // Get User DN - userDN, err := cs.LDAPService.GetUserDN(username) - if err != nil { - return nil, fmt.Errorf("failed to get user DN: %w", err) - } - // Get user's groups - groups, err := cs.LDAPService.GetUserGroups(userDN) + groups, err := cs.ProxmoxService.GetUserGroups(username) if err != nil { return nil, fmt.Errorf("failed to get user groups: %w", err) } diff --git a/internal/cloning/types.go b/internal/cloning/types.go index abefd34..4d9481c 100644 --- a/internal/cloning/types.go +++ b/internal/cloning/types.go @@ -13,18 +13,14 @@ import ( // Config holds the configuration for cloning operations type Config struct { - RouterName string `envconfig:"ROUTER_NAME" default:"1-1NAT-vyos"` - RouterVMID int `envconfig:"ROUTER_VMID"` - RouterNode string `envconfig:"ROUTER_NODE"` + RouterName string `envconfig:"PROXMOX_ROUTER_NAME" default:"1-1NAT-vyos"` + RouterVMID int `envconfig:"PROXMOX_ROUTER_VMID"` + RouterNode string `envconfig:"PROXMOX_ROUTER_NODE"` MinPodID int `envconfig:"MIN_POD_ID" default:"1001"` MaxPodID int `envconfig:"MAX_POD_ID" default:"1250"` CloneTimeout time.Duration `envconfig:"CLONE_TIMEOUT" default:"3m"` - RouterWaitTimeout time.Duration `envconfig:"ROUTER_WAIT_TIMEOUT" default:"120s"` 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/vyos-postconfig-bootup.script"` - WANIPBase string `envconfig:"WAN_IP_BASE" default:"172.16."` + RouterWaitTimeout time.Duration `envconfig:"ROUTER_WAIT_TIMEOUT" default:"120s"` } // KaminoTemplate represents a template in the system @@ -95,7 +91,7 @@ type PodResponse struct { type Pod struct { Name string `json:"name"` VMs []proxmox.VirtualResource `json:"vms"` - Template KaminoTemplate `json:"template,omitempty"` + Template KaminoTemplate `json:"template"` } var allowedMIMEs = map[string]struct{}{ diff --git a/internal/ldap/groups.go b/internal/ldap/groups.go deleted file mode 100644 index 5f7359d..0000000 --- a/internal/ldap/groups.go +++ /dev/null @@ -1,326 +0,0 @@ -package ldap - -import ( - "fmt" - "regexp" - "strings" - "time" - - ldapv3 "github.com/go-ldap/ldap/v3" -) - -// ================================================= -// Public Functions -// ================================================= - -func (s *LDAPService) GetGroups() ([]Group, error) { - // Search for all groups in the KaminoGroups OU - kaminoGroupsOU := "OU=KaminoGroups," + s.client.config.BaseDN - req := ldapv3.NewSearchRequest( - kaminoGroupsOU, - ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 0, 0, false, - "(objectClass=group)", - []string{"cn", "whenCreated", "member"}, - nil, - ) - - searchResult, err := s.client.Search(req) - if err != nil { - return nil, fmt.Errorf("failed to search for groups: %v", err) - } - - var groups []Group - for _, entry := range searchResult.Entries { - cn := entry.GetAttributeValue("cn") - - // Check if the group is protected - protectedGroup, err := isProtectedGroup(cn) - if err != nil { - return nil, fmt.Errorf("failed to determine if the group %s is protected: %v", cn, err) - } - - group := Group{ - Name: cn, - CanModify: !protectedGroup, - UserCount: len(entry.GetAttributeValues("member")), - } - - // Add creation date if available and convert it - whenCreated := entry.GetAttributeValue("whenCreated") - if whenCreated != "" { - // AD stores dates in GeneralizedTime format: YYYYMMDDHHMMSS.0Z - if parsedTime, err := time.Parse("20060102150405.0Z", whenCreated); err == nil { - group.CreatedAt = parsedTime.Format("2006-01-02 15:04:05") - } - } - - groups = append(groups, group) - } - - return groups, nil -} - -func (s *LDAPService) CreateGroup(groupName string) error { - // Validate group name - if err := validateGroupName(groupName); err != nil { - return fmt.Errorf("invalid group name: %v", err) - } - - // Check if group already exists - _, err := s.getGroupDN(groupName) - if err == nil { - return fmt.Errorf("group already exists: %s", groupName) - } - - // Construct the DN for the new group - groupDN := fmt.Sprintf("CN=%s,OU=KaminoGroups,%s", groupName, s.client.config.BaseDN) - - // Create the add request - addReq := ldapv3.NewAddRequest(groupDN, nil) - addReq.Attribute("objectClass", []string{"top", "group"}) - addReq.Attribute("cn", []string{groupName}) - addReq.Attribute("sAMAccountName", []string{groupName}) - addReq.Attribute("groupType", []string{"-2147483646"}) - - // Execute the add request - err = s.client.Add(addReq) - if err != nil { - return fmt.Errorf("failed to create group: %v", err) - } - - return nil -} - -func (s *LDAPService) RenameGroup(oldGroupName string, newGroupName string) error { - // Validate new group name - if err := validateGroupName(newGroupName); err != nil { - return fmt.Errorf("invalid new group name: %v", err) - } - - // Check if old group exists - oldGroupDN, err := s.getGroupDN(oldGroupName) - if err != nil { - return fmt.Errorf("old group not found: %v", err) - } - - // Check if new group already exists - _, err = s.getGroupDN(newGroupName) - if err == nil { - return fmt.Errorf("new group name already exists: %s", newGroupName) - } - - // Create modify DN request - newRDN := fmt.Sprintf("CN=%s", newGroupName) - modifyDNReq := ldapv3.NewModifyDNRequest(oldGroupDN, newRDN, true, "") - - // Execute the modify DN request - err = s.client.ModifyDN(modifyDNReq) - if err != nil { - return fmt.Errorf("failed to rename group: %v", err) - } - - return nil -} - -func (s *LDAPService) DeleteGroup(groupName string) error { - // Check if group is protected - protected, err := isProtectedGroup(groupName) - if err != nil { - return fmt.Errorf("failed to check if group is protected: %v", err) - } - if protected { - return fmt.Errorf("cannot delete protected group: %s", groupName) - } - - // Get group DN - groupDN, err := s.getGroupDN(groupName) - if err != nil { - return fmt.Errorf("group not found: %v", err) - } - - // Create delete request - delReq := ldapv3.NewDelRequest(groupDN, nil) - - // Execute the delete request - err = s.client.Del(delReq) - if err != nil { - return fmt.Errorf("failed to delete group: %v", err) - } - - return nil -} - -func (s *LDAPService) GetGroupMembers(groupName string) ([]User, error) { - groupDN, err := s.getGroupDN(groupName) - if err != nil { - return nil, fmt.Errorf("group not found: %v", err) - } - - // Search for the group and get its members - req := ldapv3.NewSearchRequest( - groupDN, - ldapv3.ScopeBaseObject, ldapv3.NeverDerefAliases, 0, 0, false, - "(objectClass=group)", - []string{"member"}, - nil, - ) - - searchResult, err := s.client.Search(req) - if err != nil { - return nil, fmt.Errorf("failed to search for group: %v", err) - } - - if len(searchResult.Entries) == 0 { - return []User{}, nil - } - - memberDNs := searchResult.Entries[0].GetAttributeValues("member") - var users []User - - for _, memberDN := range memberDNs { - // Get user details from DN - userReq := ldapv3.NewSearchRequest( - memberDN, - ldapv3.ScopeBaseObject, ldapv3.NeverDerefAliases, 0, 0, false, - "(objectClass=user)", - []string{"sAMAccountName", "cn", "whenCreated", "userAccountControl"}, - nil, - ) - - userResult, err := s.client.Search(userReq) - if err != nil { - continue // Skip this user if there's an error - } - - if len(userResult.Entries) > 0 { - entry := userResult.Entries[0] - user := User{ - Name: entry.GetAttributeValue("sAMAccountName"), - CreatedAt: entry.GetAttributeValue("whenCreated"), - Enabled: true, // Default, will be updated based on userAccountControl - } - - // Check if user is enabled - userAccountControl := entry.GetAttributeValue("userAccountControl") - if userAccountControl != "" { - // Parse userAccountControl to determine if account is enabled - // UF_ACCOUNTDISABLE = 0x02 - if strings.Contains(userAccountControl, "2") { - user.Enabled = false - } - } - - users = append(users, user) - } - } - - return users, nil -} - -func (s *LDAPService) AddUsersToGroup(groupName string, usernames []string) error { - groupDN, err := s.getGroupDN(groupName) - if err != nil { - return fmt.Errorf("group not found: %v", err) - } - - // Add users one by one to handle cases where some users might already be in the group - for _, username := range usernames { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("user %s not found: %v", username, err) - } - - if err := s.AddToGroup(userDN, groupDN); err != nil { - return fmt.Errorf("failed to add user %s to group: %v", username, err) - } - } - - return nil -} - -func (s *LDAPService) RemoveUsersFromGroup(groupName string, usernames []string) error { - groupDN, err := s.getGroupDN(groupName) - if err != nil { - return fmt.Errorf("group not found: %v", err) - } - - // Remove users one by one to handle cases where some users might not be in the group - for _, username := range usernames { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("user %s not found: %v", username, err) - } - - if err := s.RemoveFromGroup(userDN, groupDN); err != nil { - return fmt.Errorf("failed to remove user %s from group: %v", username, err) - } - } - - return nil -} - -// ================================================= -// Private Functions -// ================================================= - -func (s *LDAPService) getGroupDN(groupName string) (string, error) { - kaminoGroupsOU := "OU=KaminoGroups," + s.client.config.BaseDN - req := ldapv3.NewSearchRequest( - kaminoGroupsOU, - ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 1, 30, false, - fmt.Sprintf("(&(objectClass=group)(cn=%s))", ldapv3.EscapeFilter(groupName)), - []string{"dn"}, - nil, - ) - - searchResult, err := s.client.Search(req) - if err != nil { - return "", fmt.Errorf("failed to search for group: %v", err) - } - - if len(searchResult.Entries) == 0 { - return "", fmt.Errorf("group %s not found", groupName) - } - - return searchResult.Entries[0].DN, nil -} - -func validateGroupName(groupName string) error { - if groupName == "" { - return fmt.Errorf("group name cannot be empty") - } - - if len(groupName) >= 64 { - return fmt.Errorf("group name must be less than 64 characters") - } - - regex := regexp.MustCompile("^[a-zA-Z0-9-_]*$") - if !regex.MatchString(groupName) { - return fmt.Errorf("group name must only contain letters, numbers, hyphens, and underscores") - } - - return nil -} - -func isProtectedGroup(groupName string) (bool, error) { - protectedGroups := []string{ - "Domain Admins", - "Domain Users", - "Domain Guests", - "Schema Admins", - "Enterprise Admins", - "Administrators", - "Users", - "Guests", - "Proxmox-Admins", - "KaminoUsers", - } - - for _, protectedGroup := range protectedGroups { - if strings.EqualFold(groupName, protectedGroup) { - return true, nil - } - } - - return false, nil -} diff --git a/internal/ldap/types.go b/internal/ldap/types.go index 79d447d..93e07ad 100644 --- a/internal/ldap/types.go +++ b/internal/ldap/types.go @@ -12,27 +12,10 @@ import ( type Service interface { // User Management - GetUsers() ([]User, error) - GetUser(username string) (*User, error) CreateAndRegisterUser(userInfo UserRegistrationInfo) error DeleteUser(username string) error - AddUserToGroup(username string, groupName string) error - SetUserGroups(username string, groups []string) error - EnableUserAccount(username string) error - DisableUserAccount(username string) error - GetUserGroups(userDN string) ([]string, error) GetUserDN(username string) (string, error) - // Group Management - CreateGroup(groupName string) error - GetGroups() ([]Group, error) - RenameGroup(oldGroupName string, newGroupName string) error - DeleteGroup(groupName string) error - GetGroupMembers(groupName string) ([]User, error) - RemoveUserFromGroup(username string, groupName string) error - AddUsersToGroup(groupName string, usernames []string) error - RemoveUsersFromGroup(groupName string, usernames []string) error - // Connection Management HealthCheck() error Reconnect() error @@ -52,7 +35,6 @@ type Config struct { BindUser string `envconfig:"LDAP_BIND_USER"` BindPassword string `envconfig:"LDAP_BIND_PASSWORD"` SkipTLSVerify bool `envconfig:"LDAP_SKIP_TLS_VERIFY" default:"false"` - AdminGroupDN string `envconfig:"LDAP_ADMIN_GROUP_DN"` BaseDN string `envconfig:"LDAP_BASE_DN"` } @@ -71,25 +53,10 @@ type CreateRequest struct { Group string `json:"group"` } -type Group struct { - Name string `json:"name"` - CanModify bool `json:"can_modify"` - CreatedAt string `json:"created_at,omitempty"` - UserCount int `json:"user_count,omitempty"` -} - // ================================================= // Users // ================================================= -type User struct { - Name string `json:"name"` - CreatedAt string `json:"created_at"` - Enabled bool `json:"enabled"` - IsAdmin bool `json:"is_admin"` - Groups []Group `json:"groups"` -} - type UserRegistrationInfo struct { Username string `json:"username" validate:"required,min=1,max=20"` Password string `json:"password" validate:"required,min=8,max=128"` diff --git a/internal/ldap/users.go b/internal/ldap/users.go index f38d781..2ee9f5e 100644 --- a/internal/ldap/users.go +++ b/internal/ldap/users.go @@ -4,9 +4,7 @@ import ( "encoding/binary" "fmt" "regexp" - "strconv" "strings" - "time" "unicode/utf16" ldapv3 "github.com/go-ldap/ldap/v3" @@ -16,124 +14,6 @@ import ( // Public Functions // ================================================= -func (s *LDAPService) GetUsers() ([]User, error) { - kaminoUsersGroupDN := "CN=KaminoUsers,OU=KaminoGroups," + s.client.config.BaseDN - searchRequest := ldapv3.NewSearchRequest( - s.client.config.BaseDN, ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 0, 0, false, - fmt.Sprintf("(&(objectClass=user)(sAMAccountName=*)(memberOf=%s))", kaminoUsersGroupDN), // Filter for users in KaminoUsers group - []string{"sAMAccountName", "dn", "whenCreated", "memberOf", "userAccountControl"}, // Attributes to retrieve - nil, - ) - - searchResult, err := s.client.Search(searchRequest) - if err != nil { - return nil, fmt.Errorf("failed to search for users: %v", err) - } - - var users = []User{} - for _, entry := range searchResult.Entries { - user := User{ - Name: entry.GetAttributeValue("sAMAccountName"), - } - - whenCreated := entry.GetAttributeValue("whenCreated") - if whenCreated != "" { - // AD stores dates in GeneralizedTime format: YYYYMMDDHHMMSS.0Z - if parsedTime, err := time.Parse("20060102150405.0Z", whenCreated); err == nil { - user.CreatedAt = parsedTime.Format("2006-01-02 15:04:05") - } - } - - // Check if user is enabled - userAccountControl := entry.GetAttributeValue("userAccountControl") - if userAccountControl != "" { - uac, err := strconv.Atoi(userAccountControl) - if err == nil { - // UF_ACCOUNTDISABLE = 0x02 - user.Enabled = (uac & 0x02) == 0 - } - } - - // Check if user is admin - memberOfValues := entry.GetAttributeValues("memberOf") - for _, memberOf := range memberOfValues { - if strings.Contains(memberOf, s.client.config.AdminGroupDN) { - user.IsAdmin = true - break - } - } - - // Get user groups - groups, err := getUserGroupsFromMemberOf(memberOfValues) - if err == nil { - user.Groups = groups - } - - users = append(users, user) - } - - return users, nil -} - -func (s *LDAPService) GetUser(username string) (*User, error) { - kaminoUsersGroupDN := "CN=KaminoUsers,OU=KaminoGroups," + s.client.config.BaseDN - searchRequest := ldapv3.NewSearchRequest( - s.client.config.BaseDN, ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 0, 0, false, - fmt.Sprintf("(&(objectClass=user)(sAMAccountName=%s)(memberOf=%s))", username, kaminoUsersGroupDN), // Filter for specific user in KaminoUsers group - []string{"sAMAccountName", "dn", "whenCreated", "memberOf", "userAccountControl"}, // Attributes to retrieve - nil, - ) - - searchResult, err := s.client.Search(searchRequest) - if err != nil { - return nil, fmt.Errorf("failed to search for user: %v", err) - } - - if len(searchResult.Entries) == 0 { - return nil, fmt.Errorf("user '%s' not found", username) - } - - entry := searchResult.Entries[0] - user := User{ - Name: entry.GetAttributeValue("sAMAccountName"), - } - - whenCreated := entry.GetAttributeValue("whenCreated") - if whenCreated != "" { - // AD stores dates in GeneralizedTime format: YYYYMMDDHHMMSS.0Z - if parsedTime, err := time.Parse("20060102150405.0Z", whenCreated); err == nil { - user.CreatedAt = parsedTime.Format("2006-01-02 15:04:05") - } - } - - // Check if user is enabled - userAccountControl := entry.GetAttributeValue("userAccountControl") - if userAccountControl != "" { - uac, err := strconv.Atoi(userAccountControl) - if err == nil { - // UF_ACCOUNTDISABLE = 0x02 - user.Enabled = (uac & 0x02) == 0 - } - } - - // Check if user is admin - memberOfValues := entry.GetAttributeValues("memberOf") - for _, memberOf := range memberOfValues { - if strings.Contains(memberOf, s.client.config.AdminGroupDN) { - user.IsAdmin = true - break - } - } - - // Get user groups - groups, err := getUserGroupsFromMemberOf(memberOfValues) - if err == nil { - user.Groups = groups - } - - return &user, nil -} - func (s *LDAPService) CreateUser(userInfo UserRegistrationInfo) (string, error) { // Create DN for new user in Users container // TODO: Static @@ -191,54 +71,6 @@ func (s *LDAPService) EnableUserAccountByDN(userDN string) error { return nil } -// DisableUserAccountByDN disables a user account by DN -func (s *LDAPService) DisableUserAccountByDN(userDN string) error { - modifyRequest := ldapv3.NewModifyRequest(userDN, nil) - modifyRequest.Replace("userAccountControl", []string{"514"}) // Disabled account - - err := s.client.Modify(modifyRequest) - if err != nil { - return fmt.Errorf("failed to disable user account: %v", err) - } - - return nil -} - -func (s *LDAPService) AddToGroup(userDN string, groupDN string) error { - modifyRequest := ldapv3.NewModifyRequest(groupDN, nil) - modifyRequest.Add("member", []string{userDN}) - - err := s.client.Modify(modifyRequest) - if err != nil { - // Check if the error is because the user is already in the group - if strings.Contains(strings.ToLower(err.Error()), "already exists") || - strings.Contains(strings.ToLower(err.Error()), "attribute or value exists") { - return nil // Not an error if user is already in group - } - return fmt.Errorf("failed to add user to group: %v", err) - } - - return nil -} - -func (s *LDAPService) RemoveFromGroup(userDN string, groupDN string) error { - modifyRequest := ldapv3.NewModifyRequest(groupDN, nil) - modifyRequest.Delete("member", []string{userDN}) - - err := s.client.Modify(modifyRequest) - if err != nil { - // Check if the error is because the user is not in the group - if strings.Contains(strings.ToLower(err.Error()), "no such attribute") || - strings.Contains(strings.ToLower(err.Error()), "unwilling to perform") || - strings.Contains(strings.ToLower(err.Error()), "no such object") { - return nil // Not an error if user is not in group - } - return fmt.Errorf("failed to remove user from group: %v", err) - } - - return nil -} - func (s *LDAPService) CreateAndRegisterUser(userInfo UserRegistrationInfo) error { // Validate username if !isValidUsername(userInfo.Username) { @@ -270,49 +102,6 @@ func (s *LDAPService) CreateAndRegisterUser(userInfo UserRegistrationInfo) error return fmt.Errorf("failed to enable user account: %v", err) } - // Add user to KaminoUsers group - kaminoUsersGroupDN := "CN=KaminoUsers,OU=KaminoGroups," + s.client.config.BaseDN - err = s.AddToGroup(userDN, kaminoUsersGroupDN) - if err != nil { - return fmt.Errorf("failed to add user to KaminoUsers group: %v", err) - } - - return nil -} - -func (s *LDAPService) AddUserToGroup(username string, groupName string) error { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("failed to get user DN: %v", err) - } - - groupDN, err := s.getGroupDN(groupName) - if err != nil { - return fmt.Errorf("failed to get group DN: %v", err) - } - - return s.AddToGroup(userDN, groupDN) -} - -func (s *LDAPService) RemoveUserFromGroup(username string, groupName string) error { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("failed to get user DN: %v", err) - } - - groupDN, err := s.getGroupDN(groupName) - if err != nil { - return fmt.Errorf("failed to get group DN: %v", err) - } - - modifyRequest := ldapv3.NewModifyRequest(groupDN, nil) - modifyRequest.Delete("member", []string{userDN}) - - err = s.client.Modify(modifyRequest) - if err != nil { - return fmt.Errorf("failed to remove user from group: %v", err) - } - return nil } @@ -342,108 +131,10 @@ func (s *LDAPService) DeleteUsers(usernames []string) []error { return errors } -func (s *LDAPService) GetUserGroups(userDN string) ([]string, error) { - searchRequest := ldapv3.NewSearchRequest( - userDN, - ldapv3.ScopeBaseObject, - ldapv3.NeverDerefAliases, - 1, - 30, - false, - "(objectClass=*)", - []string{"memberOf"}, - nil, - ) - - searchResult, err := s.client.Search(searchRequest) - if err != nil { - return nil, fmt.Errorf("failed to search for user groups: %v", err) - } - - if len(searchResult.Entries) == 0 { - return []string{}, nil - } - - memberOfValues := searchResult.Entries[0].GetAttributeValues("memberOf") - var groups []string - for _, memberOf := range memberOfValues { - // Extract CN from DN - parts := strings.Split(memberOf, ",") - if len(parts) > 0 && strings.HasPrefix(parts[0], "CN=") { - groupName := strings.TrimPrefix(parts[0], "CN=") - groups = append(groups, groupName) - } - } - - return groups, nil -} - -func (s *LDAPService) EnableUserAccount(username string) error { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("failed to get user DN: %v", err) - } - - return s.EnableUserAccountByDN(userDN) -} - -func (s *LDAPService) DisableUserAccount(username string) error { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("failed to get user DN: %v", err) - } - - return s.DisableUserAccountByDN(userDN) -} - -func (s *LDAPService) SetUserGroups(username string, groups []string) error { - userDN, err := s.GetUserDN(username) - if err != nil { - return fmt.Errorf("failed to get user DN: %v", err) - } - - // Get current groups - currentGroups, err := s.GetUserGroups(userDN) - if err != nil { - return fmt.Errorf("failed to get current user groups: %v", err) - } - - // Remove from current groups - for _, group := range currentGroups { - err = s.RemoveUserFromGroup(username, group) - if err != nil { - return fmt.Errorf("failed to remove user from group %s: %v", group, err) - } - } - - // Add to new groups - for _, group := range groups { - err = s.AddUserToGroup(username, group) - if err != nil { - return fmt.Errorf("failed to add user to group %s: %v", group, err) - } - } - - return nil -} - // ================================================= // Private Functions // ================================================= -func getUserGroupsFromMemberOf(memberOfValues []string) ([]Group, error) { - var groups []Group - for _, memberOf := range memberOfValues { - // Extract CN from DN - parts := strings.Split(memberOf, ",") - if len(parts) > 0 && strings.HasPrefix(parts[0], "CN=") { - groupName := strings.TrimPrefix(parts[0], "CN=") - groups = append(groups, Group{Name: groupName}) - } - } - return groups, nil -} - func isValidUsername(username string) bool { if len(username) < 1 || len(username) > 20 { return false diff --git a/internal/proxmox/cluster.go b/internal/proxmox/cluster.go index 9de7983..d58514c 100644 --- a/internal/proxmox/cluster.go +++ b/internal/proxmox/cluster.go @@ -113,10 +113,6 @@ func (s *ProxmoxService) SyncUsers() error { return s.syncRealm("users") } -func (s *ProxmoxService) SyncGroups() error { - return s.syncRealm("groups") -} - // ================================================= // Private Functions // ================================================= diff --git a/internal/proxmox/groups.go b/internal/proxmox/groups.go new file mode 100644 index 0000000..7d3e446 --- /dev/null +++ b/internal/proxmox/groups.go @@ -0,0 +1,231 @@ +package proxmox + +import ( + "fmt" + "regexp" + "slices" + "strings" + + "github.com/cpp-cyber/proclone/internal/tools" +) + +// ================================================= +// Public Functions +// ================================================= + +func (s *ProxmoxService) GetGroups() ([]Group, error) { + groups := []Group{} + + req := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: "/access/groups", + } + + var groupsResponse []GroupsResponse + if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &groupsResponse); err != nil { + return nil, fmt.Errorf("failed to get groups: %w", err) + } + + for _, group := range groupsResponse { + groups = append(groups, Group{ + Name: group.Name, + UserCount: len(strings.Split(group.Users, ",")), + Comment: group.Comment, + }) + } + + return groups, nil +} + +func (s *ProxmoxService) CreateGroup(groupName string, comment string) error { + // Validate group name + if err := validateGroupName(groupName); err != nil { + return fmt.Errorf("invalid group name: %v", err) + } + + req := tools.ProxmoxAPIRequest{ + Method: "POST", + Endpoint: "/access/groups", + RequestBody: map[string]string{ + "groupid": groupName, + "comment": comment, + }, + } + + _, err := s.RequestHelper.MakeRequest(req) + if err != nil { + return fmt.Errorf("failed to create group: %w", err) + } + + return nil +} + +func (s *ProxmoxService) DeleteGroup(groupName string) error { + req := tools.ProxmoxAPIRequest{ + Method: "DELETE", + Endpoint: fmt.Sprintf("/access/groups/%s", groupName), + } + + _, err := s.RequestHelper.MakeRequest(req) + if err != nil { + return fmt.Errorf("failed to delete group: %w", err) + } + + return nil +} + +func (s *ProxmoxService) GetGroupMembers(groupName string) ([]string, error) { + // Search for the group and get its members + req := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: fmt.Sprintf("/access/groups/%s", groupName), + } + + var groupMemebersResponse GroupMembersResponse + if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &groupMemebersResponse); err != nil { + return nil, fmt.Errorf("failed to get groups members of group: %w", err) + } + + return groupMemebersResponse.Members, nil +} + +func (s *ProxmoxService) AddUsersToGroup(groupName string, usernames []string) error { + // Add users one by one to handle cases where some users might already be in the group + for _, username := range usernames { + // Check user's current groups + userGroups, err := s.getUserGroups(username) + if err != nil { + return fmt.Errorf("failed to get user %s groups: %w", username, err) + } + + // Check if user is already in the group, if not add them + if !contains(userGroups, groupName) { + userGroups = append(userGroups, groupName) + } + + userID := s.getUserID(username) + + req := tools.ProxmoxAPIRequest{ + Method: "PUT", + Endpoint: fmt.Sprintf("/access/users/%s", userID), + RequestBody: map[string]string{ + "groups": strings.Join(userGroups, ","), + }, + } + + _, err = s.RequestHelper.MakeRequest(req) + if err != nil { + return fmt.Errorf("failed to add user %s to group: %v", username, err) + } + } + + return nil +} + +func (s *ProxmoxService) RemoveUsersFromGroup(groupName string, usernames []string) error { + // Remove users one by one to handle cases where some users might not be in the group + for _, username := range usernames { + // Check user's current groups + userGroups, err := s.getUserGroups(username) + if err != nil { + return fmt.Errorf("failed to get user %s groups: %w", username, err) + } + + // Check if user is already in the group, if so, remove them + if contains(userGroups, groupName) { + userGroups = remove(userGroups, groupName) + } + + userID := s.getUserID(username) + + req := tools.ProxmoxAPIRequest{ + Method: "PUT", + Endpoint: fmt.Sprintf("/access/users/%s", userID), + RequestBody: map[string]string{ + "groups": strings.Join(userGroups, ","), + }, + } + + _, err = s.RequestHelper.MakeRequest(req) + if err != nil { + return fmt.Errorf("failed to remove user %s from group %s: %v", username, groupName, err) + } + } + + return nil +} + +func (s *ProxmoxService) EditGroup(groupName string, comment string) error { + if err := validateGroupName(groupName); err != nil { + return err + } + + req := tools.ProxmoxAPIRequest{ + Method: "PUT", + Endpoint: fmt.Sprintf("/access/groups/%s", groupName), + RequestBody: map[string]string{ + "comment": comment, + }, + } + + _, err := s.RequestHelper.MakeRequest(req) + if err != nil { + return fmt.Errorf("failed to edit group %s: %v", groupName, err) + } + + return nil +} + +// ================================================= +// Private Functions +// ================================================= + +func (s *ProxmoxService) getUserGroups(username string) ([]string, error) { + userID := s.getUserID(username) + + req := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: fmt.Sprintf("/access/users/%s", userID), + } + + var userResponse ProxmoxUserIDResponse + if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &userResponse); err != nil { + return nil, fmt.Errorf("failed to get user information: %v", err) + } + + return userResponse.Groups, nil +} + +func validateGroupName(groupName string) error { + if groupName == "" { + return fmt.Errorf("group name cannot be empty") + } + + if len(groupName) >= 64 { + return fmt.Errorf("group name must be less than 64 characters") + } + + regex := regexp.MustCompile("^[a-zA-Z0-9-_]*$") + if !regex.MatchString(groupName) { + return fmt.Errorf("group name must only contain letters, numbers, hyphens, and underscores") + } + + return nil +} + +func contains(slice []string, item string) bool { + if slices.Contains(slice, item) { + return true + } + return false +} + +func remove(slice []string, item string) []string { + result := []string{} + for _, s := range slice { + if s != item { + result = append(result, s) + } + } + return result +} diff --git a/internal/cloning/networking.go b/internal/proxmox/networking.go similarity index 70% rename from internal/cloning/networking.go rename to internal/proxmox/networking.go index c7475fa..e30f0cb 100644 --- a/internal/cloning/networking.go +++ b/internal/proxmox/networking.go @@ -1,4 +1,4 @@ -package cloning +package proxmox import ( "fmt" @@ -8,17 +8,25 @@ import ( "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) { +// RouterConfig holds configuration needed for router operations +type RouterConfig struct { + RouterWaitTimeout time.Duration + WANScriptPath string + VIPScriptPath string + VYOSScriptPath string + WANIPBase string +} + +func (s *ProxmoxService) GetRouterType(router 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) + infoRsp, err := s.RequestHelper.MakeRequest(infoReq) if err != nil { return "", fmt.Errorf("request for router type failed: %v", err) } @@ -32,8 +40,16 @@ func (cs *CloningService) getRouterType(router proxmox.VM) (string, error) { } } -// configurePodRouter configures the pod router with proper networking settings -func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid int, routerType string) error { +// ConfigurePodRouter configures the pod router with proper networking settings +func (s *ProxmoxService) ConfigurePodRouter(podNumber int, node string, vmid int, routerType string) error { + config := RouterConfig{ + RouterWaitTimeout: s.Config.RouterWaitTimeout, + WANScriptPath: s.Config.WANScriptPath, + VIPScriptPath: s.Config.VIPScriptPath, + VYOSScriptPath: s.Config.VYOSScriptPath, + WANIPBase: s.Config.WANIPBase, + } + // Wait for router agent to be pingable statusReq := tools.ProxmoxAPIRequest{ Method: "POST", @@ -50,7 +66,7 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in return fmt.Errorf("router qemu agent timed out") } - _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(statusReq) + _, err := s.RequestHelper.MakeRequest(statusReq) if err == nil { break // Agent is responding } @@ -65,8 +81,8 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in // 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), + config.WANScriptPath, + fmt.Sprintf("%s%d.1", config.WANIPBase, podNumber), }, } @@ -76,7 +92,7 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in RequestBody: reqBody, } - _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(execReq) + _, err := s.RequestHelper.MakeRequest(execReq) if err != nil { return fmt.Errorf("failed to make IP change request: %v", err) } @@ -84,8 +100,8 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in // 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), + config.VIPScriptPath, + fmt.Sprintf("%s%d.0", config.WANIPBase, podNumber), }, } @@ -95,7 +111,7 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in RequestBody: vipReqBody, } - _, err = cs.ProxmoxService.GetRequestHelper().MakeRequest(vipExecReq) + _, err = s.RequestHelper.MakeRequest(vipExecReq) if err != nil { return fmt.Errorf("failed to make VIP change request: %v", err) } @@ -104,7 +120,7 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in "command": []string{ "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), + fmt.Sprintf("sed -i -e 's/{{THIRD_OCTET}}/%d/g;s/{{NETWORK_PREFIX}}/%s/g' %s", podNumber, config.WANIPBase, config.VYOSScriptPath), }, } @@ -114,7 +130,7 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in RequestBody: reqBody, } - _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(execReq) + _, err := s.RequestHelper.MakeRequest(execReq) if err != nil { return fmt.Errorf("failed to make IP change request: %v", err) } @@ -126,9 +142,9 @@ func (cs *CloningService) configurePodRouter(podNumber int, node string, vmid in return nil } -func (cs *CloningService) SetPodVnet(poolName string, vnetName string) error { +func (s *ProxmoxService) SetPodVnet(poolName string, vnetName string) error { // Get all VMs in the pool - vms, err := cs.ProxmoxService.GetPoolVMs(poolName) + vms, err := s.GetPoolVMs(poolName) if err != nil { return fmt.Errorf("failed to get pool VMs for pool %s: %w", poolName, err) } @@ -164,7 +180,7 @@ func (cs *CloningService) SetPodVnet(poolName string, vnetName string) error { RequestBody: reqBody, } - _, err := cs.ProxmoxService.GetRequestHelper().MakeRequest(req) + _, err := s.RequestHelper.MakeRequest(req) if err != nil { errorMsg := fmt.Sprintf("failed to update network for VM %s (VMID: %d): %v", vm.Name, vm.VmId, err) log.Printf("ERROR: %s", errorMsg) @@ -181,3 +197,18 @@ func (cs *CloningService) SetPodVnet(poolName string, vnetName string) error { log.Printf("Successfully configured VNet %s for all %d VMs in pool %s", vnetName, len(vms), poolName) return nil } + +func (s *ProxmoxService) GetUsedVNets() ([]VNet, error) { + vnets := []VNet{} + + req := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: "/cluster/sdn/vnets", + } + + if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &vnets); err != nil { + return nil, fmt.Errorf("failed to get vnets: %w", err) + } + + return vnets, nil +} diff --git a/internal/proxmox/pools.go b/internal/proxmox/pools.go index dee9ffc..f2ae548 100644 --- a/internal/proxmox/pools.go +++ b/internal/proxmox/pools.go @@ -249,3 +249,189 @@ func (s *ProxmoxService) GetNextPodIDs(minPodID int, maxPodID int, num int) ([]s return podIDs, adjustedIDs, nil } + +func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRouter bool, vms []VM) error { + // 1. Create pool in proxmox with specific name and "kamino_template_" prefix + poolName := fmt.Sprintf("kamino_template_%s", name) + if err := s.CreateNewPool(poolName); err != nil { + return err + } + + if err := s.SetPoolPermission(poolName, creator, false); err != nil { + return err + } + + if addRouter == false && len(vms) == 0 { + return nil + } + + // 2. Get VMIDs + numVMs := len(vms) + if addRouter { + numVMs++ + } + + vmIDs, err := s.GetNextVMIDs(numVMs) + if err != nil { + return err + } + + // 3. Find best node to clone to + bestNode, err := s.FindBestNode() + if err != nil { + return err + } + + // 4. If addRouter is true, clone router from Config + var router VM + var routerCloneReq VMCloneRequest + var routerVMID int + + if addRouter { + router = VM{ + Name: s.Config.RouterName, + Node: s.Config.RouterNode, + VMID: s.Config.RouterVMID, + } + + // Save router VMID before removing it from the list + routerVMID = vmIDs[0] + + routerCloneReq = VMCloneRequest{ + SourceVM: router, + PoolName: poolName, + NewVMID: routerVMID, + TargetNode: bestNode, + } + + // Remove the first VMID from the list + vmIDs = vmIDs[1:] + + if err := s.CloneVM(routerCloneReq); err != nil { + return err + } + } + + // 5. Clone specified templates to newly created pool with the specified names + for i, vm := range vms { + vmCloneReq := VMCloneRequest{ + SourceVM: vm, + PoolName: poolName, + NewVMID: vmIDs[i], + TargetNode: bestNode, + } + + if err := s.CloneVM(vmCloneReq); err != nil { + return err + } + } + + // Return with no error if addRouter is false since all other operations below have to do with routing + if !addRouter { + return nil + } + + // 8. Wait for all VM clone operations to complete before configuring VNets + log.Printf("Waiting for VMs in pool %s to be available", poolName) + time.Sleep(2 * time.Second) + + // First wait for all VMs to appear in the pool + for retries := range 30 { + poolVMs, err := s.GetPoolVMs(poolName) + if err != nil { + time.Sleep(2 * time.Second) + continue + } + + if len(poolVMs) >= numVMs { + log.Printf("Pool %s has %d VMs (expected %d) - all VMs present", poolName, len(poolVMs), numVMs) + break + } + + log.Printf("Pool %s has %d VMs, waiting for %d (retry %d/30)", poolName, len(poolVMs), numVMs, retries+1) + time.Sleep(2 * time.Second) + } + + // Wait for all VM locks to be released + log.Printf("Waiting for all VM clone operations to complete (checking locks)") + poolVMs, err := s.GetPoolVMs(poolName) + if err != nil { + return fmt.Errorf("failed to get pool VMs after waiting: %w", err) + } + + for _, vm := range poolVMs { + log.Printf("Waiting for VM %d (%s) lock to be released", vm.VmId, vm.Name) + if err := s.WaitForLock(vm.NodeName, vm.VmId); err != nil { + log.Printf("Warning: timeout waiting for VM %d lock, continuing anyway: %v", vm.VmId, err) + } + } + + log.Printf("All clone operations complete for pool %s", poolName) + + // 9. Configure VNet for all VMs + // Get number of template pools + templatePools, err := s.GetTemplatePools() + if err != nil { + return fmt.Errorf("failed to get template pools: %w", err) + } + + // Calculate the template ID to get the VNet name + templateID := len(templatePools) % 10 + vnet := fmt.Sprintf("templ%d", templateID) + + log.Printf("Configuring VNet %s for pool %s", vnet, poolName) + err = s.SetPodVnet(poolName, vnet) + if err != nil { + return fmt.Errorf("failed to set VNet for pool %s: %w", poolName, err) + } + + // 10. Start router and wait for it to be available + if addRouter { + log.Printf("Starting router VM (VMID: %d) on node %s", routerVMID, bestNode) + + // Wait for router disk to be available + log.Printf("Waiting for router disk to be available") + err = s.WaitForDisk(bestNode, routerVMID, 2*time.Minute) + if err != nil { + return fmt.Errorf("router disk unavailable: %w", err) + } + + // Start the router + log.Printf("Starting router VM") + err = s.StartVM(bestNode, routerVMID) + if err != nil { + return fmt.Errorf("failed to start router VM: %w", err) + } + + // Wait for router to be running + log.Printf("Waiting for router VM to be running") + err = s.WaitForRunning(bestNode, routerVMID) + if err != nil { + return fmt.Errorf("router VM failed to start: %w", err) + } + + log.Printf("Router VM is now running") + } + + // 11. Run config scripts on router + // Determine router type + routerType, err := s.GetRouterType(router) + if err != nil { + return fmt.Errorf("failed to get router type: %v", err) + } + + // Calculate the third octect + octect := 254 - templateID + + err = s.ConfigurePodRouter(octect, bestNode, router.VMID, routerType) + if err != nil { + return fmt.Errorf("failed to configure router for %s: %v", routerType, err) + } + + log.Printf("Successfully created template pool %s with %d VMs", poolName, len(vms)) + if addRouter { + log.Printf("Router VM included and started") + } + + return nil +} diff --git a/internal/proxmox/types.go b/internal/proxmox/types.go index 4600ea7..3a3ae12 100644 --- a/internal/proxmox/types.go +++ b/internal/proxmox/types.go @@ -9,17 +9,27 @@ import ( // ProxmoxConfig holds the configuration for Proxmox API type ProxmoxConfig struct { - Host string `envconfig:"PROXMOX_HOST" required:"true"` - Port string `envconfig:"PROXMOX_PORT" default:"8006"` - TokenID string `envconfig:"PROXMOX_TOKEN_ID" required:"true"` - TokenSecret string `envconfig:"PROXMOX_TOKEN_SECRET" required:"true"` - VerifySSL bool `envconfig:"PROXMOX_VERIFY_SSL" default:"false"` - CriticalPool string `envconfig:"PROXMOX_CRITICAL_POOL"` - Realm string `envconfig:"REALM"` - NodesStr string `envconfig:"PROXMOX_NODES"` - StorageID string `envconfig:"STORAGE_ID" default:"local-lvm"` - Nodes []string // Parsed from NodesStr - APIToken string // Computed from TokenID and TokenSecret + Host string `envconfig:"PROXMOX_HOST" required:"true"` + Port string `envconfig:"PROXMOX_PORT" default:"8006"` + TokenID string `envconfig:"PROXMOX_TOKEN_ID" required:"true"` + TokenSecret string `envconfig:"PROXMOX_TOKEN_SECRET" required:"true"` + VerifySSL bool `envconfig:"PROXMOX_VERIFY_SSL" default:"false"` + CriticalPool string `envconfig:"PROXMOX_CRITICAL_POOL"` + Realm string `envconfig:"PROXMOX_REALM"` + NodesStr string `envconfig:"PROXMOX_NODES"` + StorageID string `envconfig:"PROXMOX_STORAGE_ID" default:"local-lvm"` + AdminGroupName string `envconfig:"PROXMOX_ADMIN_GROUP_NAME" default:"admin"` + VMTemplatePool string `envconfig:"PROXMOX_VM_TEMPLATE_POOL" default:"Templates"` + RouterName string `envconfig:"PROXMOX_ROUTER_NAME" default:"1-1NAT-vyos"` + RouterNode string `envconfig:"PROXMOX_ROUTER_NODE"` + RouterVMID int `envconfig:"PROXMOX_ROUTER_VMID"` + RouterWaitTimeout time.Duration `envconfig:"ROUTER_WAIT_TIMEOUT" default:"120s"` + 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/vyos-postconfig-bootup.script"` + WANIPBase string `envconfig:"WAN_IP_BASE" default:"172.16."` + Nodes []string // Parsed from NodesStr + APIToken string // Computed from TokenID and TokenSecret } // Service interface defines the methods for Proxmox operations @@ -30,13 +40,13 @@ type Service interface { GetNodeStatus(nodeName string) (*ProxmoxNodeStatus, error) FindBestNode() (string, error) SyncUsers() error - SyncGroups() error // Pod Management GetNextPodIDs(minPodID int, maxPodID int, num int) ([]string, []int, error) // VM Management GetVMs() ([]VirtualResource, error) + GetVMTemplates() ([]VirtualResource, error) GetNextVMIDs(num int) ([]int, error) StartVM(node string, vmID int) error ShutdownVM(node string, vmID int) error @@ -63,6 +73,28 @@ type Service interface { // Template Management GetTemplatePools() ([]string, error) + // Networking + GetRouterType(router VM) (string, error) + ConfigurePodRouter(podNumber int, node string, vmid int, routerType string) error + SetPodVnet(poolName string, vnetName string) error + GetUsedVNets() ([]VNet, error) + CreateTemplatePool(creator string, name string, addRouter bool, vms []VM) error + + // User Management + GetUsers() ([]User, error) + GetUser(username string) (*User, error) + SetUserGroups(username string, groups []string) error + GetUserGroups(username string) ([]string, error) + + // Group Management + GetGroups() ([]Group, error) + CreateGroup(groupName string, comment string) error + DeleteGroup(groupName string) error + EditGroup(groupName string, comment string) error + GetGroupMembers(groupName string) ([]string, error) + AddUsersToGroup(groupName string, usernames []string) error + RemoveUsersFromGroup(groupName string, usernames []string) error + // Internal access for router functionality GetRequestHelper() *tools.ProxmoxRequestHelper } @@ -165,3 +197,43 @@ type PendingDiskResponse struct { Used int64 `json:"used"` Size int64 `json:"size"` } + +type GroupsResponse struct { + Name string `json:"groupid"` + Users string `json:"users"` + Comment string `json:"comment"` +} + +type Group struct { + Name string `json:"name"` + UserCount int `json:"user_count"` + Comment string `json:"comment"` +} + +type GroupMembersResponse struct { + Members []string `json:"members"` +} + +type ProxmoxUserResponse struct { + ID string `json:"userid"` + Comment string `json:"comment"` + Expire int64 `json:"expire"` + Groups string `json:"groups"` +} + +type ProxmoxUserIDResponse struct { + ID string `json:"userid"` + Comment string `json:"comment"` + Expire int64 `json:"expire"` + Groups []string `json:"groups"` +} + +type User struct { + Name string `json:"name"` + Groups []string `json:"groups"` +} + +type VNet struct { + Name string `json:"vnet"` + Tag int `json:"tag"` +} diff --git a/internal/proxmox/users.go b/internal/proxmox/users.go new file mode 100644 index 0000000..8909362 --- /dev/null +++ b/internal/proxmox/users.go @@ -0,0 +1,98 @@ +package proxmox + +import ( + "fmt" + "strings" + + "github.com/cpp-cyber/proclone/internal/tools" +) + +// ================================================= +// Public Functions +// ================================================= + +// GetUsers retrieves all users from Proxmox +func (s *ProxmoxService) GetUsers() ([]User, error) { + req := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: "/access/users?full=1", + } + + var usersResponse []ProxmoxUserResponse + if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &usersResponse); err != nil { + return nil, fmt.Errorf("failed to get users: %w", err) + } + + users := []User{} + for _, userResp := range usersResponse { + // Extract username without realm suffix + username := strings.TrimSuffix(userResp.ID, "@"+s.Config.Realm) + + user := User{ + Name: username, + Groups: strings.Split(userResp.Groups, ","), + } + + users = append(users, user) + } + + return users, nil +} + +// GetUser retrieves a specific user from Proxmox +func (s *ProxmoxService) GetUser(username string) (*User, error) { + userID := s.getUserID(username) + + req := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: fmt.Sprintf("/access/users/%s", userID), + } + + var userResp ProxmoxUserIDResponse + if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &userResp); err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + user := User{ + Name: username, + Groups: userResp.Groups, + } + + return &user, nil +} + +// SetUserGroups sets the groups for a user in Proxmox +func (s *ProxmoxService) SetUserGroups(username string, groups []string) error { + userID := s.getUserID(username) + + req := tools.ProxmoxAPIRequest{ + Method: "PUT", + Endpoint: fmt.Sprintf("/access/users/%s", userID), + RequestBody: map[string]any{ + "groups": strings.Join(groups, ","), + }, + } + + _, err := s.RequestHelper.MakeRequest(req) + if err != nil { + return fmt.Errorf("failed to set user groups: %w", err) + } + + return nil +} + +// GetUserGroups retrieves the groups a user belongs to +func (s *ProxmoxService) GetUserGroups(username string) ([]string, error) { + return s.getUserGroups(username) +} + +// ================================================= +// Private Functions +// ================================================= + +func (s *ProxmoxService) getUserID(username string) string { + if strings.Contains(username, "@") { + return username + } + return fmt.Sprintf("%s@%s", username, s.Config.Realm) +} diff --git a/internal/proxmox/vms.go b/internal/proxmox/vms.go index 18f817a..159ff08 100644 --- a/internal/proxmox/vms.go +++ b/internal/proxmox/vms.go @@ -22,6 +22,22 @@ func (s *ProxmoxService) GetVMs() ([]VirtualResource, error) { return vms, nil } +func (s *ProxmoxService) GetVMTemplates() ([]VirtualResource, error) { + vms, err := s.GetClusterResources("type=vm") + if err != nil { + return []VirtualResource{}, err + } + + var templates []VirtualResource + for _, vm := range vms { + if vm.ResourcePool == s.Config.VMTemplatePool { + templates = append(templates, vm) + } + } + + return templates, nil +} + func (s *ProxmoxService) StartVM(node string, vmID int) error { return s.vmAction("start", node, vmID) } @@ -140,10 +156,9 @@ 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/gonk/storage/%s/content?vmid=%d", s.Config.StorageID, vmID), + Endpoint: fmt.Sprintf("/nodes/%s/storage/%s/content?vmid=%d", s.Config.Nodes[0], s.Config.StorageID, vmID), } var diskResponse []PendingDiskResponse From e4b56bce07c96a53eafc1dc1de03ff640519d382 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Fri, 7 Nov 2025 12:19:26 -0800 Subject: [PATCH 2/6] Improved logging and fixed silent error for checking disk size --- internal/proxmox/vms.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/proxmox/vms.go b/internal/proxmox/vms.go index 159ff08..ccf9ada 100644 --- a/internal/proxmox/vms.go +++ b/internal/proxmox/vms.go @@ -155,12 +155,18 @@ func (s *ProxmoxService) WaitForDisk(node string, vmID int, maxWait time.Duratio continue } + log.Printf("%+v", configResp) + if configResp.HardDisk != "" { + log.Printf("/nodes/%s/storage/%s/content?vmid=%d", s.Config.Nodes[0], s.Config.StorageID, vmID) + pendingReq := tools.ProxmoxAPIRequest{ Method: "GET", Endpoint: fmt.Sprintf("/nodes/%s/storage/%s/content?vmid=%d", s.Config.Nodes[0], s.Config.StorageID, vmID), } + log.Printf("%+v", pendingReq) + var diskResponse []PendingDiskResponse err := s.RequestHelper.MakeRequestAndUnmarshal(pendingReq, &diskResponse) if err != nil || len(diskResponse) == 0 { @@ -168,10 +174,12 @@ func (s *ProxmoxService) WaitForDisk(node string, vmID int, maxWait time.Duratio continue } + log.Printf("%+v", diskResponse) + // Iterate through all disks, if all have valid Used and Size (not 0) consider available allAvailable := true for _, disk := range diskResponse { - if disk.Used == 0 || disk.Size == 0 { + if disk.Size == 0 { allAvailable = false break } From bf0d885f920ddbdc7832e3c2e3f6fe570921d204 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Fri, 7 Nov 2025 15:02:09 -0800 Subject: [PATCH 3/6] Fixed an issue where the kamino template creation portal was using the incorrect VMID for the router --- internal/proxmox/networking.go | 9 ++++----- internal/proxmox/pools.go | 6 +++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/proxmox/networking.go b/internal/proxmox/networking.go index e30f0cb..67aa6ba 100644 --- a/internal/proxmox/networking.go +++ b/internal/proxmox/networking.go @@ -43,11 +43,10 @@ func (s *ProxmoxService) GetRouterType(router VM) (string, error) { // ConfigurePodRouter configures the pod router with proper networking settings func (s *ProxmoxService) ConfigurePodRouter(podNumber int, node string, vmid int, routerType string) error { config := RouterConfig{ - RouterWaitTimeout: s.Config.RouterWaitTimeout, - WANScriptPath: s.Config.WANScriptPath, - VIPScriptPath: s.Config.VIPScriptPath, - VYOSScriptPath: s.Config.VYOSScriptPath, - WANIPBase: s.Config.WANIPBase, + WANScriptPath: s.Config.WANScriptPath, + VIPScriptPath: s.Config.VIPScriptPath, + VYOSScriptPath: s.Config.VYOSScriptPath, + WANIPBase: s.Config.WANIPBase, } // Wait for router agent to be pingable diff --git a/internal/proxmox/pools.go b/internal/proxmox/pools.go index f2ae548..e52e252 100644 --- a/internal/proxmox/pools.go +++ b/internal/proxmox/pools.go @@ -415,15 +415,19 @@ func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRout // 11. Run config scripts on router // Determine router type + log.Printf("Determining router type") routerType, err := s.GetRouterType(router) if err != nil { return fmt.Errorf("failed to get router type: %v", err) } + log.Printf("Router type is %s", routerType) // Calculate the third octect octect := 254 - templateID + log.Printf("Third octect is %d", octect) - err = s.ConfigurePodRouter(octect, bestNode, router.VMID, routerType) + log.Printf("Configuring router") + err = s.ConfigurePodRouter(octect, bestNode, routerVMID, routerType) if err != nil { return fmt.Errorf("failed to configure router for %s: %v", routerType, err) } From 0b1d873fa34ff51ba116d7a3cc92a83916a046b4 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Mon, 10 Nov 2025 12:23:51 -0800 Subject: [PATCH 4/6] Adjusted template pool creation timing and status checks using static sleep calls and proxmox tasks --- internal/proxmox/networking.go | 3 +- internal/proxmox/pools.go | 123 ++++++++++++++++++--------------- internal/proxmox/tasks.go | 20 ++++++ internal/proxmox/types.go | 15 ++++ internal/proxmox/vms.go | 24 +++++++ 5 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 internal/proxmox/tasks.go diff --git a/internal/proxmox/networking.go b/internal/proxmox/networking.go index 67aa6ba..ff0947b 100644 --- a/internal/proxmox/networking.go +++ b/internal/proxmox/networking.go @@ -65,8 +65,7 @@ func (s *ProxmoxService) ConfigurePodRouter(podNumber int, node string, vmid int return fmt.Errorf("router qemu agent timed out") } - _, err := s.RequestHelper.MakeRequest(statusReq) - if err == nil { + if _, err := s.RequestHelper.MakeRequest(statusReq); err == nil { break // Agent is responding } diff --git a/internal/proxmox/pools.go b/internal/proxmox/pools.go index e52e252..1fb0482 100644 --- a/internal/proxmox/pools.go +++ b/internal/proxmox/pools.go @@ -253,15 +253,18 @@ func (s *ProxmoxService) GetNextPodIDs(minPodID int, maxPodID int, num int) ([]s func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRouter bool, vms []VM) error { // 1. Create pool in proxmox with specific name and "kamino_template_" prefix poolName := fmt.Sprintf("kamino_template_%s", name) + log.Printf("Creating template pool %s", poolName) if err := s.CreateNewPool(poolName); err != nil { return err } + log.Printf("Setting pool permissions for %s", poolName) if err := s.SetPoolPermission(poolName, creator, false); err != nil { return err } if addRouter == false && len(vms) == 0 { + log.Printf("No VMs to clone") return nil } @@ -282,12 +285,16 @@ func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRout return err } + // Track cloning UPIDs directly in a map for O(1) lookup + pendingUPIDs := make(map[string]bool) + // 4. If addRouter is true, clone router from Config var router VM var routerCloneReq VMCloneRequest var routerVMID int if addRouter { + log.Printf("Cloning router VM") router = VM{ Name: s.Config.RouterName, Node: s.Config.RouterNode, @@ -307,13 +314,16 @@ func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRout // Remove the first VMID from the list vmIDs = vmIDs[1:] - if err := s.CloneVM(routerCloneReq); err != nil { + upid, err := s.cloneVMWithUPID(routerCloneReq) + if err != nil { return err } + pendingUPIDs[upid] = true } // 5. Clone specified templates to newly created pool with the specified names for i, vm := range vms { + log.Printf("Cloning VM %s", vm.Name) vmCloneReq := VMCloneRequest{ SourceVM: vm, PoolName: poolName, @@ -321,54 +331,64 @@ func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRout TargetNode: bestNode, } - if err := s.CloneVM(vmCloneReq); err != nil { + upid, err := s.cloneVMWithUPID(vmCloneReq) + if err != nil { return err } + pendingUPIDs[upid] = true } - // Return with no error if addRouter is false since all other operations below have to do with routing - if !addRouter { + if len(pendingUPIDs) == 0 { + log.Printf("No VM clone operations to complete") return nil } - // 8. Wait for all VM clone operations to complete before configuring VNets - log.Printf("Waiting for VMs in pool %s to be available", poolName) - time.Sleep(2 * time.Second) - - // First wait for all VMs to appear in the pool - for retries := range 30 { - poolVMs, err := s.GetPoolVMs(poolName) + log.Printf("Waiting for %d VM clone operation(s) to complete", len(pendingUPIDs)) + for retries := range 20 { + activeTasks, err := s.getActiveCloningTasks(bestNode) if err != nil { - time.Sleep(2 * time.Second) - continue + return fmt.Errorf("failed to get active tasks: %w", err) + } + + // Build set of currently active UPIDs + activeUPIDs := make(map[string]bool) + for _, task := range activeTasks { + activeUPIDs[task.UPID] = true } - if len(poolVMs) >= numVMs { - log.Printf("Pool %s has %d VMs (expected %d) - all VMs present", poolName, len(poolVMs), numVMs) + // Check if any of our clone operations are still running + stillRunning := false + for upid := range pendingUPIDs { + if activeUPIDs[upid] { + stillRunning = true + break + } + } + + if !stillRunning { + log.Printf("All VM clone operations completed") break } - log.Printf("Pool %s has %d VMs, waiting for %d (retry %d/30)", poolName, len(poolVMs), numVMs, retries+1) - time.Sleep(2 * time.Second) - } + if retries == 19 { + return fmt.Errorf("timed out waiting for VM clone operations to complete") + } - // Wait for all VM locks to be released - log.Printf("Waiting for all VM clone operations to complete (checking locks)") - poolVMs, err := s.GetPoolVMs(poolName) - if err != nil { - return fmt.Errorf("failed to get pool VMs after waiting: %w", err) + log.Printf("Waiting for %d VM clone operation(s) to complete (retry %d/30)", len(pendingUPIDs), retries+1) + time.Sleep(5 * time.Second) } - for _, vm := range poolVMs { - log.Printf("Waiting for VM %d (%s) lock to be released", vm.VmId, vm.Name) - if err := s.WaitForLock(vm.NodeName, vm.VmId); err != nil { - log.Printf("Warning: timeout waiting for VM %d lock, continuing anyway: %v", vm.VmId, err) - } + // Return with no error if addRouter is false since all other operations below have to do with routing + if !addRouter { + log.Printf("No router VM to configure") + return nil } - log.Printf("All clone operations complete for pool %s", poolName) + // Additional sleep call after all VM clone operations are completed just in case + log.Printf("Sleeping for 10 seconds after VM clone operations") + time.Sleep(10 * time.Second) - // 9. Configure VNet for all VMs + // 6. Configure VNet for all VMs // Get number of template pools templatePools, err := s.GetTemplatePools() if err != nil { @@ -385,35 +405,28 @@ func (s *ProxmoxService) CreateTemplatePool(creator string, name string, addRout return fmt.Errorf("failed to set VNet for pool %s: %w", poolName, err) } - // 10. Start router and wait for it to be available - if addRouter { - log.Printf("Starting router VM (VMID: %d) on node %s", routerVMID, bestNode) + // 7. Start router and wait for it to be available + log.Printf("Starting router VM (VMID: %d) on node %s", routerVMID, bestNode) - // Wait for router disk to be available - log.Printf("Waiting for router disk to be available") - err = s.WaitForDisk(bestNode, routerVMID, 2*time.Minute) - if err != nil { - return fmt.Errorf("router disk unavailable: %w", err) - } - - // Start the router - log.Printf("Starting router VM") - err = s.StartVM(bestNode, routerVMID) - if err != nil { - return fmt.Errorf("failed to start router VM: %w", err) - } - - // Wait for router to be running - log.Printf("Waiting for router VM to be running") - err = s.WaitForRunning(bestNode, routerVMID) - if err != nil { - return fmt.Errorf("router VM failed to start: %w", err) - } + // Start the router + log.Printf("Starting router VM") + err = s.StartVM(bestNode, routerVMID) + if err != nil { + return fmt.Errorf("failed to start router VM: %w", err) + } - log.Printf("Router VM is now running") + // Wait for router to be running + log.Printf("Waiting for router VM to be running") + err = s.WaitForRunning(bestNode, routerVMID) + if err != nil { + return fmt.Errorf("router VM failed to start: %w", err) } - // 11. Run config scripts on router + // Additional sleep call after router starts just in case + log.Printf("Router VM is now running, sleeping for 10 seconds") + time.Sleep(10 * time.Second) + + // 8. Run config scripts on router // Determine router type log.Printf("Determining router type") routerType, err := s.GetRouterType(router) diff --git a/internal/proxmox/tasks.go b/internal/proxmox/tasks.go new file mode 100644 index 0000000..a981c46 --- /dev/null +++ b/internal/proxmox/tasks.go @@ -0,0 +1,20 @@ +package proxmox + +import ( + "fmt" + + "github.com/cpp-cyber/proclone/internal/tools" +) + +func (s *ProxmoxService) getActiveCloningTasks(node string) ([]Task, error) { + activeCloningReq := tools.ProxmoxAPIRequest{ + Method: "GET", + Endpoint: fmt.Sprintf("/nodes/%s/tasks?source=active&typefilter=qmclone", node), + } + + var activeCloningTasks []Task + if err := s.RequestHelper.MakeRequestAndUnmarshal(activeCloningReq, &activeCloningTasks); err != nil { + return nil, err + } + return activeCloningTasks, nil +} diff --git a/internal/proxmox/types.go b/internal/proxmox/types.go index 3a3ae12..d81d92f 100644 --- a/internal/proxmox/types.go +++ b/internal/proxmox/types.go @@ -61,6 +61,7 @@ type Service interface { WaitForLock(node string, vmID int) error WaitForRunning(node string, vmID int) error WaitForStopped(node string, vmID int) error + UPIDTest() error // Pool Management GetPoolVMs(poolName string) ([]VirtualResource, error) @@ -237,3 +238,17 @@ type VNet struct { Name string `json:"vnet"` Tag int `json:"tag"` } + +type Task struct { + ID string `json:"id"` + Node string `json:"node"` + PID int `json:"pid"` + PStart int `json:"pstart"` + StartTime int64 `json:"starttime"` + Type string `json:"type"` + UPID string `json:"upid"` + User string `json:"user"` + EndTime int64 `json:"endtime"` + Status string `json:"status"` + ExitStatus string `json:"exitstatus"` +} diff --git a/internal/proxmox/vms.go b/internal/proxmox/vms.go index ccf9ada..18fb991 100644 --- a/internal/proxmox/vms.go +++ b/internal/proxmox/vms.go @@ -144,6 +144,30 @@ func (s *ProxmoxService) CloneVM(req VMCloneRequest) error { return nil } +func (s *ProxmoxService) cloneVMWithUPID(req VMCloneRequest) (string, error) { + // Clone VM + cloneBody := map[string]any{ + "newid": req.NewVMID, + "name": req.SourceVM.Name, + "pool": req.PoolName, + "full": req.Full, + "target": req.TargetNode, + } + + cloneReq := tools.ProxmoxAPIRequest{ + Method: "POST", + Endpoint: fmt.Sprintf("/nodes/%s/qemu/%d/clone", req.SourceVM.Node, req.SourceVM.VMID), + RequestBody: cloneBody, + } + + var upid string + if err := s.RequestHelper.MakeRequestAndUnmarshal(cloneReq, &upid); err != nil { + return "", fmt.Errorf("failed to initiate VM clone: %w", err) + } + + return upid, nil +} + func (s *ProxmoxService) WaitForDisk(node string, vmID int, maxWait time.Duration) error { start := time.Now() From 570a5d764caeebd7c8dd39c16af52bc10a51f7f4 Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Mon, 10 Nov 2025 13:25:04 -0800 Subject: [PATCH 5/6] Removed test function --- internal/proxmox/types.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/proxmox/types.go b/internal/proxmox/types.go index d81d92f..e04299e 100644 --- a/internal/proxmox/types.go +++ b/internal/proxmox/types.go @@ -61,7 +61,6 @@ type Service interface { WaitForLock(node string, vmID int) error WaitForRunning(node string, vmID int) error WaitForStopped(node string, vmID int) error - UPIDTest() error // Pool Management GetPoolVMs(poolName string) ([]VirtualResource, error) From 375316f2949ed570fc400dc2e8a9b5adf28362ab Mon Sep 17 00:00:00 2001 From: MaxwellCaron Date: Mon, 17 Nov 2025 14:09:10 -0800 Subject: [PATCH 6/6] Seperated template management and creation from Admin only. Created new panel for "Creators" --- internal/api/auth/auth_service.go | 18 ++++++ internal/api/auth/types.go | 1 + internal/api/handlers/auth_handler.go | 27 +++++++- internal/api/middleware/authorization.go | 82 ++++++++++++++++++++---- internal/api/routes/admin_routes.go | 31 +++++---- internal/api/routes/creator_routes.go | 23 +++++++ internal/api/routes/routes.go | 12 +++- internal/ldap/users.go | 4 +- internal/proxmox/types.go | 3 +- 9 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 internal/api/routes/creator_routes.go diff --git a/internal/api/auth/auth_service.go b/internal/api/auth/auth_service.go index 96c8db1..846e980 100644 --- a/internal/api/auth/auth_service.go +++ b/internal/api/auth/auth_service.go @@ -70,6 +70,24 @@ func (s *AuthService) IsAdmin(username string) (bool, error) { return false, nil } +func (s *AuthService) IsCreator(username string) (bool, error) { + // Get user's groups from Proxmox + userGroups, err := s.proxmoxService.GetUserGroups(username) + if err != nil { + return false, fmt.Errorf("failed to get user groups: %w", err) + } + + // Get the creator group name from config + creatorGroupName := s.proxmoxService.Config.CreatorGroupName + + // Check if user is in the creator group + if slices.Contains(userGroups, creatorGroupName) { + return true, nil + } + + return false, nil +} + func (s *AuthService) HealthCheck() error { return s.ldapService.HealthCheck() } diff --git a/internal/api/auth/types.go b/internal/api/auth/types.go index d2c77cd..1ab6569 100644 --- a/internal/api/auth/types.go +++ b/internal/api/auth/types.go @@ -13,6 +13,7 @@ type Service interface { // Authentication Authenticate(username, password string) (bool, error) IsAdmin(username string) (bool, error) + IsCreator(username string) (bool, error) // Health and Connection HealthCheck() error diff --git a/internal/api/handlers/auth_handler.go b/internal/api/handlers/auth_handler.go index ed60269..813d601 100644 --- a/internal/api/handlers/auth_handler.go +++ b/internal/api/handlers/auth_handler.go @@ -48,6 +48,11 @@ func NewAuthHandler() (*AuthHandler, error) { }, nil } +// GetAuthService returns the auth service for use in middleware +func (h *AuthHandler) GetAuthService() auth.Service { + return h.authService +} + // LoginHandler handles the login POST request func (h *AuthHandler) LoginHandler(c *gin.Context) { var req UsernamePasswordRequest @@ -80,6 +85,14 @@ func (h *AuthHandler) LoginHandler(c *gin.Context) { } session.Set("isAdmin", isAdmin) + // Check if user is creator + isCreator, err := h.authService.IsCreator(req.Username) + if err != nil { + log.Printf("Error checking creator status for user %s: %v", req.Username, err) + isCreator = false + } + session.Set("isCreator", isCreator) + if err := session.Save(); err != nil { log.Printf("Failed to save session for user %s: %v", req.Username, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"}) @@ -87,8 +100,9 @@ func (h *AuthHandler) LoginHandler(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{ - "message": "Login successful", - "isAdmin": isAdmin, + "message": "Login successful", + "isAdmin": isAdmin, + "isCreator": isCreator, }) } @@ -113,17 +127,24 @@ func (h *AuthHandler) SessionHandler(c *gin.Context) { // Since this is under private routes, AuthRequired middleware ensures session exists id := session.Get("id") isAdmin := session.Get("isAdmin") + isCreator := session.Get("isCreator") - // Convert isAdmin to bool, defaulting to false if not set + // Convert to bool, defaulting to false if not set adminStatus := false if isAdmin != nil { adminStatus = isAdmin.(bool) } + creatorStatus := false + if isCreator != nil { + creatorStatus = isCreator.(bool) + } + c.JSON(http.StatusOK, gin.H{ "authenticated": true, "username": id.(string), "isAdmin": adminStatus, + "isCreator": creatorStatus, }) } diff --git a/internal/api/middleware/authorization.go b/internal/api/middleware/authorization.go index 6fb069f..bdcbbce 100644 --- a/internal/api/middleware/authorization.go +++ b/internal/api/middleware/authorization.go @@ -1,8 +1,10 @@ package middleware import ( + "log" "net/http" + "github.com/cpp-cyber/proclone/internal/api/auth" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) @@ -19,23 +21,75 @@ func AuthRequired(c *gin.Context) { c.Next() } -func AdminRequired(c *gin.Context) { - session := sessions.Default(c) - id := session.Get("id") - if id == nil { - c.String(http.StatusUnauthorized, "Unauthorized") - c.Abort() - return - } +func AdminRequired(authService auth.Service) gin.HandlerFunc { + return func(c *gin.Context) { + session := sessions.Default(c) + id := session.Get("id") + if id == nil { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } - isAdmin := session.Get("isAdmin") - if isAdmin == nil || !isAdmin.(bool) { - c.String(http.StatusForbidden, "Admin access required") - c.Abort() - return + username := id.(string) + + // Verify with Proxmox + isAdmin, err := authService.IsAdmin(username) + if err != nil { + log.Printf("Error verifying admin status for user %s: %v", username, err) + c.String(http.StatusInternalServerError, "Failed to verify permissions") + c.Abort() + return + } + + if !isAdmin { + c.String(http.StatusForbidden, "Admin access required") + c.Abort() + return + } + + c.Next() } +} - c.Next() +// CreatorOrAdminRequired provides authorization middleware for template operations +func CreatorOrAdminRequired(authService auth.Service) gin.HandlerFunc { + return func(c *gin.Context) { + session := sessions.Default(c) + id := session.Get("id") + if id == nil { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + + username := id.(string) + + // Verify with Proxmox for template operations + isAdmin, err := authService.IsAdmin(username) + if err != nil { + log.Printf("Error verifying admin status for user %s: %v", username, err) + c.String(http.StatusInternalServerError, "Failed to verify permissions") + c.Abort() + return + } + + isCreator, err := authService.IsCreator(username) + if err != nil { + log.Printf("Error verifying creator status for user %s: %v", username, err) + c.String(http.StatusInternalServerError, "Failed to verify permissions") + c.Abort() + return + } + + if !isAdmin && !isCreator { + c.String(http.StatusForbidden, "Creator or Admin access required") + c.Abort() + return + } + + c.Next() + } } func GetUser(c *gin.Context) string { diff --git a/internal/api/routes/admin_routes.go b/internal/api/routes/admin_routes.go index d1816e7..07bf126 100644 --- a/internal/api/routes/admin_routes.go +++ b/internal/api/routes/admin_routes.go @@ -5,39 +5,38 @@ import ( "github.com/gin-gonic/gin" ) -// registerAdminRoutes defines all routes accessible to admin users +// registerAdminRoutes defines all routes accessible ONLY to admin users +// Template operations have been moved to creator routes (accessible by both admins and creators) func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler, dashboardHandler *handlers.DashboardHandler) { - // GET Requests + // Admin dashboard and cluster management g.GET("/dashboard", dashboardHandler.GetAdminDashboardStatsHandler) g.GET("/cluster", proxmoxHandler.GetClusterResourceUsageHandler) - g.GET("/users", proxmoxHandler.GetUsersHandler) - g.GET("/groups", proxmoxHandler.GetGroupsHandler) - g.GET("/vms", proxmoxHandler.GetVMsHandler) g.GET("/vnets", proxmoxHandler.GetUsedVNetsHandler) + g.GET("/vms", proxmoxHandler.GetVMsHandler) g.GET("/pods", cloningHandler.AdminGetPodsHandler) - g.GET("/templates", cloningHandler.AdminGetTemplatesHandler) - g.GET("/templates/unpublished", cloningHandler.GetUnpublishedTemplatesHandler) - g.GET("/templates/vms", proxmoxHandler.GetVMTemplatesHandler) - g.GET("/templates/proxmox", proxmoxHandler.GetProxmoxTemplatePoolsHandler) - // POST Requests + // User management (admin only) + g.GET("/users", proxmoxHandler.GetUsersHandler) g.POST("/users/create", authHandler.CreateUsersHandler) g.POST("/users/delete", authHandler.DeleteUsersHandler) g.POST("/user/groups", proxmoxHandler.SetUserGroupsHandler) + + // Group management (admin only) + g.GET("/groups", proxmoxHandler.GetGroupsHandler) g.POST("/groups/create", proxmoxHandler.CreateGroupsHandler) g.POST("/group/members/add", proxmoxHandler.AddUsersHandler) g.POST("/group/members/remove", proxmoxHandler.RemoveUsersHandler) g.POST("/group/edit", proxmoxHandler.EditGroupHandler) g.POST("/groups/delete", proxmoxHandler.DeleteGroupsHandler) + + // VM management (admin only) g.POST("/vm/start", proxmoxHandler.StartVMHandler) g.POST("/vm/shutdown", proxmoxHandler.ShutdownVMHandler) g.POST("/vm/reboot", proxmoxHandler.RebootVMHandler) + + // Pod management (admin only) g.POST("/pods/delete", cloningHandler.AdminDeletePodHandler) - g.POST("/template/publish", cloningHandler.PublishTemplateHandler) - g.POST("/template/create", proxmoxHandler.CreateTemplateHandler) - g.POST("/template/edit", cloningHandler.EditTemplateHandler) - g.POST("/template/delete", cloningHandler.DeleteTemplateHandler) - g.POST("/template/visibility", cloningHandler.ToggleTemplateVisibilityHandler) - g.POST("/template/image/upload", cloningHandler.UploadTemplateImageHandler) + + // Bulk template deployment (admin only) g.POST("/templates/clone", cloningHandler.AdminCloneTemplateHandler) } diff --git a/internal/api/routes/creator_routes.go b/internal/api/routes/creator_routes.go new file mode 100644 index 0000000..243dbd7 --- /dev/null +++ b/internal/api/routes/creator_routes.go @@ -0,0 +1,23 @@ +package routes + +import ( + "github.com/cpp-cyber/proclone/internal/api/handlers" + "github.com/gin-gonic/gin" +) + +// registerCreatorRoutes defines all routes accessible to both creators and admins +func registerCreatorRoutes(g *gin.RouterGroup, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) { + // Template management operations (create, publish, edit, delete) + g.POST("/template/publish", cloningHandler.PublishTemplateHandler) + g.POST("/template/create", proxmoxHandler.CreateTemplateHandler) + g.POST("/template/edit", cloningHandler.EditTemplateHandler) + g.POST("/template/delete", cloningHandler.DeleteTemplateHandler) + g.POST("/template/visibility", cloningHandler.ToggleTemplateVisibilityHandler) + g.POST("/template/image/upload", cloningHandler.UploadTemplateImageHandler) + + // Template viewing operations + g.GET("/templates", cloningHandler.AdminGetTemplatesHandler) + g.GET("/templates/unpublished", cloningHandler.GetUnpublishedTemplatesHandler) + g.GET("/templates/vms", proxmoxHandler.GetVMTemplatesHandler) + g.GET("/templates/proxmox", proxmoxHandler.GetProxmoxTemplatePoolsHandler) +} diff --git a/internal/api/routes/routes.go b/internal/api/routes/routes.go index 28b7631..133bd6e 100644 --- a/internal/api/routes/routes.go +++ b/internal/api/routes/routes.go @@ -11,6 +11,9 @@ func RegisterRoutes(r *gin.Engine, authHandler *handlers.AuthHandler, proxmoxHan // Create centralized dashboard handler dashboardHandler := handlers.NewDashboardHandler(authHandler, proxmoxHandler, cloningHandler) + // Get auth service from handler for middleware + authService := authHandler.GetAuthService() + // Public routes (no authentication required) public := r.Group("/api/v1") registerPublicRoutes(public, authHandler, cloningHandler) @@ -20,8 +23,15 @@ func RegisterRoutes(r *gin.Engine, authHandler *handlers.AuthHandler, proxmoxHan private.Use(middleware.AuthRequired) registerPrivateRoutes(private, authHandler, cloningHandler, dashboardHandler) + // Creator routes (authentication + creator OR admin privileges required) + // Template management operations accessible to both creators and admins + creator := r.Group("/api/v1/creator") + creator.Use(middleware.CreatorOrAdminRequired(authService)) + registerCreatorRoutes(creator, proxmoxHandler, cloningHandler) + // Admin routes (authentication + admin privileges required) + // User/group management and system operations admin := r.Group("/api/v1/admin") - admin.Use(middleware.AdminRequired) + admin.Use(middleware.AdminRequired(authService)) registerAdminRoutes(admin, authHandler, proxmoxHandler, cloningHandler, dashboardHandler) } diff --git a/internal/ldap/users.go b/internal/ldap/users.go index 2ee9f5e..16722d6 100644 --- a/internal/ldap/users.go +++ b/internal/ldap/users.go @@ -150,8 +150,8 @@ func extractDomainFromDN(dn string) string { for _, part := range parts { part = strings.TrimSpace(part) - if strings.HasPrefix(part, "dc=") { - domainParts = append(domainParts, strings.TrimPrefix(part, "dc=")) + if domain, found := strings.CutPrefix(part, "dc="); found { + domainParts = append(domainParts, domain) } } diff --git a/internal/proxmox/types.go b/internal/proxmox/types.go index e04299e..fed3fc8 100644 --- a/internal/proxmox/types.go +++ b/internal/proxmox/types.go @@ -18,7 +18,8 @@ type ProxmoxConfig struct { Realm string `envconfig:"PROXMOX_REALM"` NodesStr string `envconfig:"PROXMOX_NODES"` StorageID string `envconfig:"PROXMOX_STORAGE_ID" default:"local-lvm"` - AdminGroupName string `envconfig:"PROXMOX_ADMIN_GROUP_NAME" default:"admin"` + AdminGroupName string `envconfig:"PROXMOX_ADMIN_GROUP_NAME" default:"Admin"` + CreatorGroupName string `envconfig:"PROXMOX_CREATOR_GROUP_NAME" default:"Creator"` VMTemplatePool string `envconfig:"PROXMOX_VM_TEMPLATE_POOL" default:"Templates"` RouterName string `envconfig:"PROXMOX_ROUTER_NAME" default:"1-1NAT-vyos"` RouterNode string `envconfig:"PROXMOX_ROUTER_NODE"`