You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
10 KiB
351 lines
10 KiB
package command
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/admin"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/handlers"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
var (
|
|
a AdminOptions
|
|
)
|
|
|
|
type AdminOptions struct {
|
|
port *int
|
|
masters *string
|
|
adminUser *string
|
|
adminPassword *string
|
|
dataDir *string
|
|
}
|
|
|
|
func init() {
|
|
cmdAdmin.Run = runAdmin // break init cycle
|
|
a.port = cmdAdmin.Flag.Int("port", 23646, "admin server port")
|
|
a.masters = cmdAdmin.Flag.String("masters", "localhost:9333", "comma-separated master servers")
|
|
a.dataDir = cmdAdmin.Flag.String("dataDir", "", "directory to store admin configuration and data files")
|
|
|
|
a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username")
|
|
a.adminPassword = cmdAdmin.Flag.String("adminPassword", "", "admin interface password (if empty, auth is disabled)")
|
|
}
|
|
|
|
var cmdAdmin = &Command{
|
|
UsageLine: "admin -port=23646 -masters=localhost:9333 [-dataDir=/path/to/data]",
|
|
Short: "start SeaweedFS web admin interface",
|
|
Long: `Start a web admin interface for SeaweedFS cluster management.
|
|
|
|
The admin interface provides a modern web interface for:
|
|
- Cluster topology visualization and monitoring
|
|
- Volume management and operations
|
|
- File browser and management
|
|
- System metrics and performance monitoring
|
|
- Configuration management
|
|
- Maintenance operations
|
|
|
|
The admin interface automatically discovers filers from the master servers.
|
|
A gRPC server for worker connections runs on HTTP port + 10000.
|
|
|
|
Example Usage:
|
|
weed admin -port=23646 -masters="master1:9333,master2:9333"
|
|
weed admin -port=23646 -masters="localhost:9333" -dataDir="/var/lib/seaweedfs-admin"
|
|
weed admin -port=23646 -masters="localhost:9333" -dataDir="~/seaweedfs-admin"
|
|
|
|
Data Directory:
|
|
- If dataDir is specified, admin configuration and maintenance data is persisted
|
|
- The directory will be created if it doesn't exist
|
|
- Configuration files are stored in JSON format for easy editing
|
|
- Without dataDir, all configuration is kept in memory only
|
|
|
|
Authentication:
|
|
- If adminPassword is not set, the admin interface runs without authentication
|
|
- If adminPassword is set, users must login with adminUser/adminPassword
|
|
- Sessions are secured with auto-generated session keys
|
|
|
|
Security Configuration:
|
|
- The admin server reads TLS configuration from security.toml
|
|
- Configure [https.admin] section in security.toml for HTTPS support
|
|
- If https.admin.key is set, the server will start in TLS mode
|
|
- If https.admin.ca is set, mutual TLS authentication is enabled
|
|
- Set strong adminPassword for production deployments
|
|
- Configure firewall rules to restrict admin interface access
|
|
|
|
security.toml Example:
|
|
[https.admin]
|
|
cert = "/etc/ssl/admin.crt"
|
|
key = "/etc/ssl/admin.key"
|
|
ca = "/etc/ssl/ca.crt" # optional, for mutual TLS
|
|
|
|
Worker Communication:
|
|
- Workers connect via gRPC on HTTP port + 10000
|
|
- Workers use [grpc.admin] configuration from security.toml
|
|
- TLS is automatically used if certificates are configured
|
|
- Workers fall back to insecure connections if TLS is unavailable
|
|
|
|
Configuration File:
|
|
- The security.toml file is read from ".", "$HOME/.seaweedfs/",
|
|
"/usr/local/etc/seaweedfs/", or "/etc/seaweedfs/", in that order
|
|
- Generate example security.toml: weed scaffold -config=security
|
|
|
|
`,
|
|
}
|
|
|
|
func runAdmin(cmd *Command, args []string) bool {
|
|
// Load security configuration
|
|
util.LoadSecurityConfiguration()
|
|
|
|
// Validate required parameters
|
|
if *a.masters == "" {
|
|
fmt.Println("Error: masters parameter is required")
|
|
fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333")
|
|
return false
|
|
}
|
|
|
|
// Security warnings
|
|
if *a.adminPassword == "" {
|
|
fmt.Println("WARNING: Admin interface is running without authentication!")
|
|
fmt.Println(" Set -adminPassword for production use")
|
|
}
|
|
|
|
fmt.Printf("Starting SeaweedFS Admin Interface on port %d\n", *a.port)
|
|
fmt.Printf("Masters: %s\n", *a.masters)
|
|
fmt.Printf("Filers will be discovered automatically from masters\n")
|
|
if *a.dataDir != "" {
|
|
fmt.Printf("Data Directory: %s\n", *a.dataDir)
|
|
} else {
|
|
fmt.Printf("Data Directory: Not specified (configuration will be in-memory only)\n")
|
|
}
|
|
if *a.adminPassword != "" {
|
|
fmt.Printf("Authentication: Enabled (user: %s)\n", *a.adminUser)
|
|
} else {
|
|
fmt.Printf("Authentication: Disabled\n")
|
|
}
|
|
|
|
// Set up graceful shutdown
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Handle interrupt signals
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
sig := <-sigChan
|
|
fmt.Printf("\nReceived signal %v, shutting down gracefully...\n", sig)
|
|
cancel()
|
|
}()
|
|
|
|
// Start the admin server
|
|
err := startAdminServer(ctx, a)
|
|
if err != nil {
|
|
fmt.Printf("Admin server error: %v\n", err)
|
|
return false
|
|
}
|
|
|
|
fmt.Println("Admin server stopped")
|
|
return true
|
|
}
|
|
|
|
// startAdminServer starts the actual admin server
|
|
func startAdminServer(ctx context.Context, options AdminOptions) error {
|
|
// Set Gin mode
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
// Create router
|
|
r := gin.New()
|
|
r.Use(gin.Logger(), gin.Recovery())
|
|
|
|
// Session store - always auto-generate session key
|
|
sessionKeyBytes := make([]byte, 32)
|
|
_, err := rand.Read(sessionKeyBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate session key: %v", err)
|
|
}
|
|
store := cookie.NewStore(sessionKeyBytes)
|
|
r.Use(sessions.Sessions("admin-session", store))
|
|
|
|
// Static files - serve from embedded filesystem
|
|
staticFS, err := admin.GetStaticFS()
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to load embedded static files: %v", err)
|
|
} else {
|
|
r.StaticFS("/static", http.FS(staticFS))
|
|
}
|
|
|
|
// Create data directory if specified
|
|
var dataDir string
|
|
if *options.dataDir != "" {
|
|
// Expand tilde (~) to home directory
|
|
expandedDir, err := expandHomeDir(*options.dataDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to expand dataDir path %s: %v", *options.dataDir, err)
|
|
}
|
|
dataDir = expandedDir
|
|
|
|
// Show path expansion if it occurred
|
|
if dataDir != *options.dataDir {
|
|
fmt.Printf("Expanded dataDir: %s -> %s\n", *options.dataDir, dataDir)
|
|
}
|
|
|
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create data directory %s: %v", dataDir, err)
|
|
}
|
|
fmt.Printf("Data directory created/verified: %s\n", dataDir)
|
|
}
|
|
|
|
// Create admin server
|
|
adminServer := dash.NewAdminServer(*options.masters, nil, dataDir)
|
|
|
|
// Show discovered filers
|
|
filers := adminServer.GetAllFilers()
|
|
if len(filers) > 0 {
|
|
fmt.Printf("Discovered filers: %s\n", strings.Join(filers, ", "))
|
|
} else {
|
|
fmt.Printf("No filers discovered from masters\n")
|
|
}
|
|
|
|
// Start worker gRPC server for worker connections
|
|
err = adminServer.StartWorkerGrpcServer(*options.port)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start worker gRPC server: %v", err)
|
|
}
|
|
|
|
// Set up cleanup for gRPC server
|
|
defer func() {
|
|
if stopErr := adminServer.StopWorkerGrpcServer(); stopErr != nil {
|
|
log.Printf("Error stopping worker gRPC server: %v", stopErr)
|
|
}
|
|
}()
|
|
|
|
// Create handlers and setup routes
|
|
adminHandlers := handlers.NewAdminHandlers(adminServer)
|
|
adminHandlers.SetupRoutes(r, *options.adminPassword != "", *options.adminUser, *options.adminPassword)
|
|
|
|
// Server configuration
|
|
addr := fmt.Sprintf(":%d", *options.port)
|
|
server := &http.Server{
|
|
Addr: addr,
|
|
Handler: r,
|
|
}
|
|
|
|
// Start server
|
|
go func() {
|
|
log.Printf("Starting SeaweedFS Admin Server on port %d", *options.port)
|
|
|
|
// start http or https server with security.toml
|
|
var (
|
|
clientCertFile,
|
|
certFile,
|
|
keyFile string
|
|
)
|
|
useTLS := false
|
|
useMTLS := false
|
|
|
|
if viper.GetString("https.admin.key") != "" {
|
|
useTLS = true
|
|
certFile = viper.GetString("https.admin.cert")
|
|
keyFile = viper.GetString("https.admin.key")
|
|
}
|
|
|
|
if viper.GetString("https.admin.ca") != "" {
|
|
useMTLS = true
|
|
clientCertFile = viper.GetString("https.admin.ca")
|
|
}
|
|
|
|
if useMTLS {
|
|
server.TLSConfig = security.LoadClientTLSHTTP(clientCertFile)
|
|
}
|
|
|
|
if useTLS {
|
|
log.Printf("Starting SeaweedFS Admin Server with TLS on port %d", *options.port)
|
|
err = server.ListenAndServeTLS(certFile, keyFile)
|
|
} else {
|
|
err = server.ListenAndServe()
|
|
}
|
|
|
|
if err != nil && err != http.ErrServerClosed {
|
|
log.Printf("Failed to start server: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Wait for context cancellation
|
|
<-ctx.Done()
|
|
|
|
// Graceful shutdown
|
|
log.Println("Shutting down admin server...")
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
return fmt.Errorf("admin server forced to shutdown: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAdminOptions returns the admin command options for testing
|
|
func GetAdminOptions() *AdminOptions {
|
|
return &AdminOptions{}
|
|
}
|
|
|
|
// expandHomeDir expands the tilde (~) in a path to the user's home directory
|
|
func expandHomeDir(path string) (string, error) {
|
|
if path == "" {
|
|
return path, nil
|
|
}
|
|
|
|
if !strings.HasPrefix(path, "~") {
|
|
return path, nil
|
|
}
|
|
|
|
// Get current user
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get current user: %v", err)
|
|
}
|
|
|
|
// Handle different tilde patterns
|
|
if path == "~" {
|
|
return currentUser.HomeDir, nil
|
|
}
|
|
|
|
if strings.HasPrefix(path, "~/") {
|
|
return filepath.Join(currentUser.HomeDir, path[2:]), nil
|
|
}
|
|
|
|
// Handle ~username/ patterns
|
|
if strings.HasPrefix(path, "~") {
|
|
parts := strings.SplitN(path[1:], "/", 2)
|
|
username := parts[0]
|
|
|
|
targetUser, err := user.Lookup(username)
|
|
if err != nil {
|
|
return "", fmt.Errorf("user %s not found: %v", username, err)
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
return targetUser.HomeDir, nil
|
|
}
|
|
return filepath.Join(targetUser.HomeDir, parts[1]), nil
|
|
}
|
|
|
|
return path, nil
|
|
}
|