| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- 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/pb"
- "github.com/seaweedfs/seaweedfs/weed/security"
- "github.com/seaweedfs/seaweedfs/weed/util"
- )
- var (
- a AdminOptions
- )
- type AdminOptions struct {
- port *int
- grpcPort *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.grpcPort = cmdAdmin.Flag.Int("port.grpc", 0, "gRPC server port for worker connections (default: http port + 10000)")
- 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 [-port.grpc=33646] [-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 the configured gRPC port (default: 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 -port.grpc=33646 -masters="localhost:9333" -dataDir="~/seaweedfs-admin"
- weed admin -port=9900 -port.grpc=19900 -masters="localhost:9333"
- 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
- }
- // Validate that masters string can be parsed
- masterAddresses := pb.ServerAddresses(*a.masters).ToAddresses()
- if len(masterAddresses) == 0 {
- fmt.Println("Error: no valid master addresses found")
- fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333")
- return false
- }
- // Set default gRPC port if not specified
- if *a.grpcPort == 0 {
- *a.grpcPort = *a.port + 10000
- }
- // 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("Worker gRPC server will run on port %d\n", *a.grpcPort)
- 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 with all masters
- 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: %w", err)
- }
- store := cookie.NewStore(sessionKeyBytes)
- // Configure session options to ensure cookies are properly saved
- store.Options(sessions.Options{
- Path: "/",
- MaxAge: 3600 * 24, // 24 hours
- })
- 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.grpcPort)
- if err != nil {
- return fmt.Errorf("failed to start worker gRPC server: %w", 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: %w", 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: %w", 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
- }
|