diff options
Diffstat (limited to 'weed/admin/dash/handler_admin.go')
| -rw-r--r-- | weed/admin/dash/handler_admin.go | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/weed/admin/dash/handler_admin.go b/weed/admin/dash/handler_admin.go new file mode 100644 index 000000000..53eb54ec9 --- /dev/null +++ b/weed/admin/dash/handler_admin.go @@ -0,0 +1,373 @@ +package dash + +import ( + "context" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/cluster" + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" +) + +type AdminData struct { + Username string `json:"username"` + ClusterStatus string `json:"cluster_status"` + TotalVolumes int `json:"total_volumes"` + TotalFiles int64 `json:"total_files"` + TotalSize int64 `json:"total_size"` + MasterNodes []MasterNode `json:"master_nodes"` + VolumeServers []VolumeServer `json:"volume_servers"` + FilerNodes []FilerNode `json:"filer_nodes"` + DataCenters []DataCenter `json:"datacenters"` + LastUpdated time.Time `json:"last_updated"` + SystemHealth string `json:"system_health"` +} + +// S3 Bucket management data structures for templates +type S3BucketsData struct { + Username string `json:"username"` + Buckets []S3Bucket `json:"buckets"` + TotalBuckets int `json:"total_buckets"` + TotalSize int64 `json:"total_size"` + LastUpdated time.Time `json:"last_updated"` +} + +type CreateBucketRequest struct { + Name string `json:"name" binding:"required"` + Region string `json:"region"` +} + +// Object Store Users management structures +type ObjectStoreUser struct { + Username string `json:"username"` + Email string `json:"email"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + LastLogin time.Time `json:"last_login"` + Permissions []string `json:"permissions"` +} + +type ObjectStoreUsersData struct { + Username string `json:"username"` + Users []ObjectStoreUser `json:"users"` + TotalUsers int `json:"total_users"` + LastUpdated time.Time `json:"last_updated"` +} + +type FilerNode struct { + Address string `json:"address"` + DataCenter string `json:"datacenter"` + Rack string `json:"rack"` + Status string `json:"status"` + LastUpdated time.Time `json:"last_updated"` +} + +// GetAdminData retrieves admin data as a struct (for reuse by both JSON and HTML handlers) +func (s *AdminServer) GetAdminData(username string) (AdminData, error) { + if username == "" { + username = "admin" + } + + // Get cluster topology + topology, err := s.GetClusterTopology() + if err != nil { + glog.Errorf("Failed to get cluster topology: %v", err) + return AdminData{}, err + } + + // Get master nodes status + masterNodes := s.getMasterNodesStatus() + + // Get filer nodes status + filerNodes := s.getFilerNodesStatus() + + // Prepare admin data + adminData := AdminData{ + Username: username, + ClusterStatus: s.determineClusterStatus(topology, masterNodes), + TotalVolumes: topology.TotalVolumes, + TotalFiles: topology.TotalFiles, + TotalSize: topology.TotalSize, + MasterNodes: masterNodes, + VolumeServers: topology.VolumeServers, + FilerNodes: filerNodes, + DataCenters: topology.DataCenters, + LastUpdated: topology.UpdatedAt, + SystemHealth: s.determineSystemHealth(topology, masterNodes), + } + + return adminData, nil +} + +// ShowAdmin displays the main admin page (now uses GetAdminData) +func (s *AdminServer) ShowAdmin(c *gin.Context) { + username := c.GetString("username") + + adminData, err := s.GetAdminData(username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get admin data: " + err.Error()}) + return + } + + // Return JSON for API calls + c.JSON(http.StatusOK, adminData) +} + +// ShowOverview displays cluster overview +func (s *AdminServer) ShowOverview(c *gin.Context) { + topology, err := s.GetClusterTopology() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, topology) +} + +// S3 Bucket Management Handlers + +// ShowS3Buckets displays the S3 buckets management page +func (s *AdminServer) ShowS3Buckets(c *gin.Context) { + username := c.GetString("username") + + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 buckets: " + err.Error()}) + return + } + + // Calculate totals + var totalSize int64 + for _, bucket := range buckets { + totalSize += bucket.Size + } + + data := S3BucketsData{ + Username: username, + Buckets: buckets, + TotalBuckets: len(buckets), + TotalSize: totalSize, + LastUpdated: time.Now(), + } + + c.JSON(http.StatusOK, data) +} + +// ShowBucketDetails displays detailed information about a specific bucket +func (s *AdminServer) ShowBucketDetails(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + details, err := s.GetBucketDetails(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, details) +} + +// CreateBucket creates a new S3 bucket +func (s *AdminServer) CreateBucket(c *gin.Context) { + var req CreateBucketRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Validate bucket name (basic validation) + if len(req.Name) < 3 || len(req.Name) > 63 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) + return + } + + err := s.CreateS3Bucket(req.Name) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "Bucket created successfully", + "bucket": req.Name, + }) +} + +// DeleteBucket deletes an S3 bucket +func (s *AdminServer) DeleteBucket(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + err := s.DeleteS3Bucket(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket deleted successfully", + "bucket": bucketName, + }) +} + +// ListBucketsAPI returns buckets as JSON API +func (s *AdminServer) ListBucketsAPI(c *gin.Context) { + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "buckets": buckets, + "count": len(buckets), + }) +} + +// getMasterNodesStatus checks status of all master nodes +func (s *AdminServer) getMasterNodesStatus() []MasterNode { + var masterNodes []MasterNode + + // Since we have a single master address, create one entry + var isLeader bool = true // Assume leader since it's the only master we know about + var status string + + // Try to get leader info from this master + err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { + _, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{}) + if err != nil { + return err + } + // For now, assume this master is the leader since we can connect to it + isLeader = true + return nil + }) + + if err != nil { + status = "unreachable" + isLeader = false + } else { + status = "active" + } + + masterNodes = append(masterNodes, MasterNode{ + Address: s.masterAddress, + IsLeader: isLeader, + Status: status, + }) + + return masterNodes +} + +// getFilerNodesStatus checks status of all filer nodes using master's ListClusterNodes +func (s *AdminServer) getFilerNodesStatus() []FilerNode { + var filerNodes []FilerNode + + // Get filer nodes from master using ListClusterNodes + err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { + resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{ + ClientType: cluster.FilerType, + }) + if err != nil { + return err + } + + // Process each filer node + for _, node := range resp.ClusterNodes { + filerNodes = append(filerNodes, FilerNode{ + Address: node.Address, + DataCenter: node.DataCenter, + Rack: node.Rack, + Status: "active", // If it's in the cluster list, it's considered active + LastUpdated: time.Now(), + }) + } + + return nil + }) + + if err != nil { + glog.Errorf("Failed to get filer nodes from master %s: %v", s.masterAddress, err) + // Return empty list if we can't get filer info from master + return []FilerNode{} + } + + return filerNodes +} + +// determineClusterStatus analyzes cluster health +func (s *AdminServer) determineClusterStatus(topology *ClusterTopology, masters []MasterNode) string { + // Check if we have an active leader + hasActiveLeader := false + for _, master := range masters { + if master.IsLeader && master.Status == "active" { + hasActiveLeader = true + break + } + } + + if !hasActiveLeader { + return "critical" + } + + // Check volume server health + activeServers := 0 + for _, vs := range topology.VolumeServers { + if vs.Status == "active" { + activeServers++ + } + } + + if activeServers == 0 { + return "critical" + } else if activeServers < len(topology.VolumeServers) { + return "warning" + } + + return "healthy" +} + +// determineSystemHealth provides overall system health assessment +func (s *AdminServer) determineSystemHealth(topology *ClusterTopology, masters []MasterNode) string { + // Simple health calculation based on active components + totalComponents := len(masters) + len(topology.VolumeServers) + activeComponents := 0 + + for _, master := range masters { + if master.Status == "active" { + activeComponents++ + } + } + + for _, vs := range topology.VolumeServers { + if vs.Status == "active" { + activeComponents++ + } + } + + if totalComponents == 0 { + return "unknown" + } + + healthPercent := float64(activeComponents) / float64(totalComponents) * 100 + + if healthPercent >= 95 { + return "excellent" + } else if healthPercent >= 80 { + return "good" + } else if healthPercent >= 60 { + return "fair" + } else { + return "poor" + } +} |
