Files
upstream-seaweedfs/weed/admin/dash/admin_data.go
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

456 lines
15 KiB
Go

package dash
import (
"context"
"net"
"net/http"
"sort"
"time"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
// Access key status constants
const (
AccessKeyStatusActive = iam.AccessKeyStatusActive
AccessKeyStatusInactive = iam.AccessKeyStatusInactive
)
type AdminData struct {
Username string `json:"username"`
TotalVolumes int `json:"total_volumes"`
TotalChunks int64 `json:"total_chunks"`
TotalSize int64 `json:"total_size"`
VolumeSizeLimitMB uint64 `json:"volume_size_limit_mb"`
MasterNodes []MasterNode `json:"master_nodes"`
VolumeServers []VolumeServer `json:"volume_servers"`
FilerNodes []FilerNode `json:"filer_nodes"`
MessageBrokers []MessageBrokerNode `json:"message_brokers"`
S3Nodes []S3Node `json:"s3_nodes"`
DataCenters []DataCenter `json:"datacenters"`
LastUpdated time.Time `json:"last_updated"`
// EC shard totals for dashboard
TotalEcVolumes int `json:"total_ec_volumes"` // Total number of EC volumes across all servers
TotalEcShards int `json:"total_ec_shards"` // Total number of EC shards across all servers
// TotalMountClients is the number of connected FUSE/VFS mount clients across filers.
TotalMountClients int `json:"total_mount_clients"`
// Trends holds at-a-glance sparklines built from the admin's own recent
// cluster snapshots (no Prometheus required).
Trends DashboardTrends `json:"trends"`
// TierStats breaks volumes and EC shards down by storage tier: local
// disk types plus one entry per remote storage holding tiered volumes.
TierStats []TierStats `json:"tier_stats"`
}
// 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"`
Permissions []string `json:"permissions"`
PolicyNames []string `json:"policy_names"`
IsStatic bool `json:"is_static"` // loaded from static config file, not editable
}
type ObjectStoreUsersData struct {
Username string `json:"username"`
Users []ObjectStoreUser `json:"users"`
TotalUsers int `json:"total_users"`
HasAnonymousUser bool `json:"has_anonymous_user"`
LastUpdated time.Time `json:"last_updated"`
}
// User management request structures
type CreateUserRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email"`
Actions []string `json:"actions"`
GenerateKey bool `json:"generate_key"`
PolicyNames []string `json:"policy_names"`
}
type UpdateUserRequest struct {
Email string `json:"email"`
Actions []string `json:"actions"`
PolicyNames []string `json:"policy_names"`
}
type UpdateUserPoliciesRequest struct {
Actions []string `json:"actions" binding:"required"`
}
type AccessKeyInfo struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type CreateAccessKeyRequest struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
}
type UpdateAccessKeyStatusRequest struct {
Status string `json:"status" binding:"required"`
}
type UserDetails struct {
Username string `json:"username"`
Email string `json:"email"`
Actions []string `json:"actions"`
PolicyNames []string `json:"policy_names"`
AccessKeys []AccessKeyInfo `json:"access_keys"`
Groups []string `json:"groups"`
}
// RoleReadOnly is the session role assigned to read-only (view-only) admin
// accounts. It matches the value stored by HandleLogin when the read-only
// credentials are used.
const RoleReadOnly = "readonly"
// IsReadOnlyRole reports whether the given admin session role grants only
// view-only access. Any other role (admin, or the empty role used when auth
// is disabled) is treated as non-read-only.
func IsReadOnlyRole(role string) bool {
return role == RoleReadOnly
}
// RedactSecretKey clears the plaintext S3 secret key from an object-store
// user record. The access key (a public identifier) is retained so the
// identity can still be listed; only the reusable secret is removed.
func (u *ObjectStoreUser) RedactSecretKey() {
u.SecretKey = ""
}
// RedactSecretKeys clears the plaintext S3 secret keys from a user's access
// key records. Access key identifiers are retained so the set of keys remains
// visible; only the reusable secrets are removed.
func (d *UserDetails) RedactSecretKeys() {
for i := range d.AccessKeys {
d.AccessKeys[i].SecretKey = ""
}
}
type FilerNode struct {
Address string `json:"address"`
DataCenter string `json:"datacenter"`
Rack string `json:"rack"`
LastUpdated time.Time `json:"last_updated"`
}
type MessageBrokerNode struct {
Address string `json:"address"`
DataCenter string `json:"datacenter"`
Rack string `json:"rack"`
LastUpdated time.Time `json:"last_updated"`
}
type S3Node struct {
Address string `json:"address"`
DataCenter string `json:"datacenter"`
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 volume servers data with EC shard information
volumeServersData, err := s.GetClusterVolumeServers()
if err != nil {
glog.Errorf("Failed to get cluster volume servers: %v", err)
return AdminData{}, err
}
// Get master nodes status
masterNodes := s.getMasterNodesStatus()
// Get filer nodes status
filerNodes := s.getFilerNodesStatus()
// Get message broker nodes status
messageBrokers := s.getMessageBrokerNodesStatus()
// Get S3 nodes status
s3Nodes := s.getS3NodesStatus()
// Get volume size limit from master configuration
var volumeSizeLimitMB uint64 = 30000 // Default to 30GB
err = s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{})
if err != nil {
return err
}
volumeSizeLimitMB = uint64(resp.VolumeSizeLimitMB)
return nil
})
if err != nil {
glog.Warningf("Failed to get volume size limit from master: %v", err)
// Keep default value on error
}
// Calculate EC shard totals
var totalEcVolumes, totalEcShards int
ecVolumeSet := make(map[uint32]bool) // To avoid counting the same EC volume multiple times
for _, vs := range volumeServersData.VolumeServers {
totalEcShards += vs.EcShards
// Count unique EC volumes across all servers
for _, ecInfo := range vs.EcShardDetails {
ecVolumeSet[ecInfo.VolumeID] = true
}
}
totalEcVolumes = len(ecVolumeSet)
// Count connected FUSE/VFS mount clients (best-effort; don't fail the dashboard)
totalMountClients := 0
if mountData, mountErr := s.GetMountClients(); mountErr == nil {
totalMountClients = mountData.TotalMountClients
}
// Prepare admin data
adminData := AdminData{
Username: username,
TotalVolumes: topology.TotalVolumes,
TotalChunks: topology.TotalChunks,
TotalSize: topology.TotalSize,
VolumeSizeLimitMB: volumeSizeLimitMB,
MasterNodes: masterNodes,
VolumeServers: volumeServersData.VolumeServers,
FilerNodes: filerNodes,
MessageBrokers: messageBrokers,
S3Nodes: s3Nodes,
DataCenters: topology.DataCenters,
LastUpdated: topology.UpdatedAt,
TotalEcVolumes: totalEcVolumes,
TotalEcShards: totalEcShards,
TotalMountClients: totalMountClients,
Trends: s.GetDashboardTrends(),
TierStats: topology.TierStats,
}
return adminData, nil
}
// ShowAdmin displays the main admin page (now uses GetAdminData)
func (s *AdminServer) ShowAdmin(w http.ResponseWriter, r *http.Request) {
username := UsernameFromContext(r.Context())
adminData, err := s.GetAdminData(username)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to get admin data: "+err.Error())
return
}
// Return JSON for API calls
writeJSON(w, http.StatusOK, adminData)
}
// ShowOverview displays cluster overview
func (s *AdminServer) ShowOverview(w http.ResponseWriter, r *http.Request) {
topology, err := s.GetClusterTopology()
if err != nil {
writeJSONError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, topology)
}
// getMasterNodesStatus returns the full set of master nodes in the cluster.
// It prefers the authoritative raft membership (RaftListClusterServers) and
// falls back to the currently-connected master if the raft call fails, so the
// dashboard never shows an empty list.
func (s *AdminServer) getMasterNodesStatus() []MasterNode {
masterMap := make(map[string]MasterNode)
raftCallSucceeded := false
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.RaftListClusterServers(ctx, &master_pb.RaftListClusterServersRequest{})
if err != nil {
return err
}
raftCallSucceeded = true
for _, server := range resp.ClusterServers {
// pb.GrpcAddressToServerAddress calls glog.Fatalf on a parse
// error, so pre-validate the raft address with net.SplitHostPort
// and skip malformed entries instead of taking the process down.
if _, _, splitErr := net.SplitHostPort(server.Address); splitErr != nil {
glog.Warningf("skip master with invalid raft address %q: %v", server.Address, splitErr)
continue
}
httpAddress := pb.GrpcAddressToServerAddress(server.Address)
masterMap[httpAddress] = MasterNode{
Address: httpAddress,
IsLeader: server.IsLeader,
}
}
return nil
})
if err != nil {
currentMaster := s.masterClient.GetMaster(context.Background())
glog.Errorf("Failed to list raft cluster masters from %s: %v", currentMaster, err)
}
if len(masterMap) == 0 {
currentMaster := s.masterClient.GetMaster(context.Background())
if currentMaster != "" {
addr := pb.ServerAddress(currentMaster).ToHttpAddress()
// A successful empty raft response means raft is not initialized
// (standalone/non-raft cluster); the only master IS the leader.
// A failed RPC means connectivity issue; do not claim leadership.
masterMap[addr] = MasterNode{
Address: addr,
IsLeader: raftCallSucceeded,
}
}
}
masterNodes := make([]MasterNode, 0, len(masterMap))
for _, m := range masterMap {
masterNodes = append(masterNodes, m)
}
sort.Slice(masterNodes, func(i, j int) bool {
return masterNodes[i].Address < masterNodes[j].Address
})
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(), s.listClusterNodesRequest(cluster.FilerType))
if err != nil {
return err
}
// Process each filer node
for _, node := range resp.ClusterNodes {
filerNodes = append(filerNodes, FilerNode{
Address: pb.ServerAddress(node.Address).ToHttpAddress(),
DataCenter: node.DataCenter,
Rack: node.Rack,
LastUpdated: time.Now(),
})
}
return nil
})
if err != nil {
currentMaster := s.masterClient.GetMaster(context.Background())
glog.Errorf("Failed to get filer nodes from master %s: %v", currentMaster, err)
// Return empty list if we can't get filer info from master
return []FilerNode{}
}
// Sort filer nodes by address for consistent ordering on page refresh
sort.Slice(filerNodes, func(i, j int) bool {
return filerNodes[i].Address < filerNodes[j].Address
})
return filerNodes
}
// getMessageBrokerNodesStatus checks status of all message broker nodes using master's ListClusterNodes
func (s *AdminServer) getMessageBrokerNodesStatus() []MessageBrokerNode {
var messageBrokers []MessageBrokerNode
// Get message broker nodes from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.BrokerType))
if err != nil {
return err
}
// Process each message broker node
for _, node := range resp.ClusterNodes {
messageBrokers = append(messageBrokers, MessageBrokerNode{
Address: node.Address,
DataCenter: node.DataCenter,
Rack: node.Rack,
LastUpdated: time.Now(),
})
}
return nil
})
if err != nil {
currentMaster := s.masterClient.GetMaster(context.Background())
glog.Errorf("Failed to get message broker nodes from master %s: %v", currentMaster, err)
// Return empty list if we can't get broker info from master
return []MessageBrokerNode{}
}
// Sort message broker nodes by address for consistent ordering on page refresh
sort.Slice(messageBrokers, func(i, j int) bool {
return messageBrokers[i].Address < messageBrokers[j].Address
})
return messageBrokers
}
// getS3NodesStatus checks status of all S3 nodes using master's ListClusterNodes
func (s *AdminServer) getS3NodesStatus() []S3Node {
var s3Nodes []S3Node
// Get S3 nodes from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.S3Type))
if err != nil {
return err
}
// Process each S3 node
for _, node := range resp.ClusterNodes {
s3Nodes = append(s3Nodes, S3Node{
Address: pb.ServerAddress(node.Address).ToHttpAddress(),
DataCenter: node.DataCenter,
LastUpdated: time.Now(),
})
}
return nil
})
if err != nil {
currentMaster := s.masterClient.GetMaster(context.Background())
glog.Errorf("Failed to get S3 nodes from master %s: %v", currentMaster, err)
// Return empty list if we can't get S3 info from master
return []S3Node{}
}
// Sort S3 nodes by address for consistent ordering on page refresh
sort.Slice(s3Nodes, func(i, j int) bool {
return s3Nodes[i].Address < s3Nodes[j].Address
})
return s3Nodes
}