package dash import ( "context" "encoding/json" "fmt" "io" "net/http" "time" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" util_http "github.com/seaweedfs/seaweedfs/weed/util/http" ) const dirStatusTimeout = 5 * time.Second // GetClusterTopology returns the current cluster topology with caching func (s *AdminServer) GetClusterTopology() (*ClusterTopology, error) { now := time.Now() if s.cachedTopology != nil && now.Sub(s.lastCacheUpdate) < s.cacheExpiration { return s.cachedTopology, nil } topology := &ClusterTopology{ UpdatedAt: now, } // Use gRPC only err := s.getTopologyViaGRPC(topology) if err != nil { currentMaster := s.masterClient.GetMaster(context.Background()) glog.Errorf("Failed to connect to master server %s: %v", currentMaster, err) return nil, fmt.Errorf("gRPC topology request failed: %w", err) } // Cache the result s.cachedTopology = topology s.lastCacheUpdate = now return topology, nil } // fetchPublicUrlMap queries the master's /dir/status HTTP endpoint and returns // a map from data node ID (ip:port) to its PublicUrl. func (s *AdminServer) fetchPublicUrlMap() map[string]string { currentMaster := s.masterClient.GetMaster(context.Background()) if currentMaster == "" { return nil } ctx, cancel := context.WithTimeout(context.Background(), dirStatusTimeout) defer cancel() url := fmt.Sprintf("http://%s/dir/status", currentMaster.ToHttpAddress()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { glog.V(1).Infof("Failed to build /dir/status request for %s: %v", currentMaster, err) return nil } resp, err := util_http.GetGlobalHttpClient().Do(req) if err != nil { glog.V(1).Infof("Failed to fetch /dir/status from %s: %v", currentMaster, err) return nil } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { glog.V(1).Infof("Non-OK response from /dir/status: %d", resp.StatusCode) return nil } body, err := io.ReadAll(resp.Body) if err != nil { glog.V(1).Infof("Failed to read /dir/status response body: %v", err) return nil } // Parse the JSON response to extract PublicUrl for each data node var status struct { Topology struct { DataCenters []struct { Racks []struct { DataNodes []struct { Url string `json:"Url"` PublicUrl string `json:"PublicUrl"` } `json:"DataNodes"` } `json:"Racks"` } `json:"DataCenters"` } `json:"Topology"` } if err := json.Unmarshal(body, &status); err != nil { glog.V(1).Infof("Failed to parse /dir/status response: %v", err) return nil } publicUrls := make(map[string]string) for _, dc := range status.Topology.DataCenters { for _, rack := range dc.Racks { for _, dn := range rack.DataNodes { if dn.PublicUrl != "" { publicUrls[dn.Url] = dn.PublicUrl } } } } return publicUrls } // getTopologyViaGRPC gets topology using gRPC (original method) func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error { // Fetch public URL mapping from master HTTP API // The gRPC DataNodeInfo does not include PublicUrl, so we supplement it. publicUrls := s.fetchPublicUrlMap() // Get cluster status from master err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { resp, err := pb.CollectVolumeList(context.Background(), client, &master_pb.VolumeListRequest{}) if err != nil { currentMaster := s.masterClient.GetMaster(context.Background()) glog.Errorf("Failed to get volume list from master %s: %v", currentMaster, err) return err } if resp.TopologyInfo != nil { // Get volume size limit from response, default to 30GB if not set volumeSizeLimitMb := resp.VolumeSizeLimitMb if volumeSizeLimitMb == 0 { volumeSizeLimitMb = 30000 } // Process gRPC response for _, dc := range resp.TopologyInfo.DataCenterInfos { dataCenter := DataCenter{ ID: dc.Id, Racks: []Rack{}, } for _, rack := range dc.RackInfos { rackObj := Rack{ ID: rack.Id, Nodes: []VolumeServer{}, } for _, node := range rack.DataNodeInfos { // Calculate totals from disk infos var totalVolumes int64 var totalMaxVolumes int64 var totalSize int64 var remoteSize int64 // Prefer the real physical disk capacity the volume server // reports per disk; the slot-based estimate overstates capacity // when maxVolumeCount is configured higher than the disk holds. var diskCapacity int64 for _, diskInfo := range node.DiskInfos { totalVolumes += diskInfo.VolumeCount totalMaxVolumes += diskInfo.MaxVolumeCount if diskInfo.DiskTotalBytes > 0 { diskCapacity += int64(diskInfo.DiskTotalBytes) } else { diskCapacity += diskInfo.MaxVolumeCount * int64(volumeSizeLimitMb) * 1024 * 1024 } // A remote-tiered volume reports its cloud object's // size; keep those bytes out of the local disk usage // that is compared against diskCapacity. for _, volInfo := range diskInfo.VolumeInfos { if volInfo.RemoteStorageName != "" { remoteSize += int64(volInfo.Size) } else { totalSize += int64(volInfo.Size) } } // ShardSizes is local to this node, so summing // across nodes gives the physical footprint. for _, ecShardInfo := range diskInfo.EcShardInfos { for _, shardSize := range ecShardInfo.ShardSizes { totalSize += shardSize } } } // Look up PublicUrl from master HTTP API // Use node.Address (ip:port) as the key, matching the Url field in /dir/status nodeAddr := node.Address if nodeAddr == "" { nodeAddr = node.Id } publicUrl := publicUrls[nodeAddr] if publicUrl == "" { publicUrl = nodeAddr } vs := VolumeServer{ ID: node.Id, Address: node.Id, DataCenter: dc.Id, Rack: rack.Id, PublicURL: publicUrl, Volumes: int(totalVolumes), MaxVolumes: int(totalMaxVolumes), DiskUsage: totalSize, DiskCapacity: diskCapacity, LastHeartbeat: time.Now(), RemoteSize: remoteSize, } rackObj.Nodes = append(rackObj.Nodes, vs) topology.VolumeServers = append(topology.VolumeServers, vs) topology.TotalVolumes += vs.Volumes // TotalSize is the logical data size, wherever the // bytes live, so remote-tiered volumes still count. topology.TotalSize += totalSize + remoteSize } dataCenter.Racks = append(dataCenter.Racks, rackObj) } topology.DataCenters = append(topology.DataCenters, dataCenter) } // Chunk counts come from the shared collection aggregation, which // nets out tombstones and counts a chunk once no matter how many // volume replicas or EC shard holders report it. topology.TotalChunks = totalCollectionFileCount(resp.TopologyInfo) topology.TierStats = CollectTierStats(resp.TopologyInfo, volumeSizeLimitMb) } return nil }) return err } // InvalidateCache forces a refresh of cached data func (s *AdminServer) InvalidateCache() { s.lastCacheUpdate = time.Now().Add(-s.cacheExpiration) s.cachedTopology = nil s.lastFilerUpdate = time.Now().Add(-s.filerCacheExpiration) s.cachedFilers = nil s.lastCollectionStatsUpdate = time.Now().Add(-s.collectionStatsCacheThreshold) s.collectionStatsCache = nil }