admin.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package command
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "os"
  9. "os/signal"
  10. "os/user"
  11. "path/filepath"
  12. "strings"
  13. "syscall"
  14. "time"
  15. "github.com/gin-contrib/sessions"
  16. "github.com/gin-contrib/sessions/cookie"
  17. "github.com/gin-gonic/gin"
  18. "github.com/spf13/viper"
  19. "github.com/seaweedfs/seaweedfs/weed/admin"
  20. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  21. "github.com/seaweedfs/seaweedfs/weed/admin/handlers"
  22. "github.com/seaweedfs/seaweedfs/weed/pb"
  23. "github.com/seaweedfs/seaweedfs/weed/security"
  24. "github.com/seaweedfs/seaweedfs/weed/util"
  25. )
  26. var (
  27. a AdminOptions
  28. )
  29. type AdminOptions struct {
  30. port *int
  31. grpcPort *int
  32. masters *string
  33. adminUser *string
  34. adminPassword *string
  35. dataDir *string
  36. }
  37. func init() {
  38. cmdAdmin.Run = runAdmin // break init cycle
  39. a.port = cmdAdmin.Flag.Int("port", 23646, "admin server port")
  40. a.grpcPort = cmdAdmin.Flag.Int("port.grpc", 0, "gRPC server port for worker connections (default: http port + 10000)")
  41. a.masters = cmdAdmin.Flag.String("masters", "localhost:9333", "comma-separated master servers")
  42. a.dataDir = cmdAdmin.Flag.String("dataDir", "", "directory to store admin configuration and data files")
  43. a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username")
  44. a.adminPassword = cmdAdmin.Flag.String("adminPassword", "", "admin interface password (if empty, auth is disabled)")
  45. }
  46. var cmdAdmin = &Command{
  47. UsageLine: "admin -port=23646 -masters=localhost:9333 [-port.grpc=33646] [-dataDir=/path/to/data]",
  48. Short: "start SeaweedFS web admin interface",
  49. Long: `Start a web admin interface for SeaweedFS cluster management.
  50. The admin interface provides a modern web interface for:
  51. - Cluster topology visualization and monitoring
  52. - Volume management and operations
  53. - File browser and management
  54. - System metrics and performance monitoring
  55. - Configuration management
  56. - Maintenance operations
  57. The admin interface automatically discovers filers from the master servers.
  58. A gRPC server for worker connections runs on the configured gRPC port (default: HTTP port + 10000).
  59. Example Usage:
  60. weed admin -port=23646 -masters="master1:9333,master2:9333"
  61. weed admin -port=23646 -masters="localhost:9333" -dataDir="/var/lib/seaweedfs-admin"
  62. weed admin -port=23646 -port.grpc=33646 -masters="localhost:9333" -dataDir="~/seaweedfs-admin"
  63. weed admin -port=9900 -port.grpc=19900 -masters="localhost:9333"
  64. Data Directory:
  65. - If dataDir is specified, admin configuration and maintenance data is persisted
  66. - The directory will be created if it doesn't exist
  67. - Configuration files are stored in JSON format for easy editing
  68. - Without dataDir, all configuration is kept in memory only
  69. Authentication:
  70. - If adminPassword is not set, the admin interface runs without authentication
  71. - If adminPassword is set, users must login with adminUser/adminPassword
  72. - Sessions are secured with auto-generated session keys
  73. Security Configuration:
  74. - The admin server reads TLS configuration from security.toml
  75. - Configure [https.admin] section in security.toml for HTTPS support
  76. - If https.admin.key is set, the server will start in TLS mode
  77. - If https.admin.ca is set, mutual TLS authentication is enabled
  78. - Set strong adminPassword for production deployments
  79. - Configure firewall rules to restrict admin interface access
  80. security.toml Example:
  81. [https.admin]
  82. cert = "/etc/ssl/admin.crt"
  83. key = "/etc/ssl/admin.key"
  84. ca = "/etc/ssl/ca.crt" # optional, for mutual TLS
  85. Worker Communication:
  86. - Workers connect via gRPC on HTTP port + 10000
  87. - Workers use [grpc.admin] configuration from security.toml
  88. - TLS is automatically used if certificates are configured
  89. - Workers fall back to insecure connections if TLS is unavailable
  90. Configuration File:
  91. - The security.toml file is read from ".", "$HOME/.seaweedfs/",
  92. "/usr/local/etc/seaweedfs/", or "/etc/seaweedfs/", in that order
  93. - Generate example security.toml: weed scaffold -config=security
  94. `,
  95. }
  96. func runAdmin(cmd *Command, args []string) bool {
  97. // Load security configuration
  98. util.LoadSecurityConfiguration()
  99. // Validate required parameters
  100. if *a.masters == "" {
  101. fmt.Println("Error: masters parameter is required")
  102. fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333")
  103. return false
  104. }
  105. // Validate that masters string can be parsed
  106. masterAddresses := pb.ServerAddresses(*a.masters).ToAddresses()
  107. if len(masterAddresses) == 0 {
  108. fmt.Println("Error: no valid master addresses found")
  109. fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333")
  110. return false
  111. }
  112. // Set default gRPC port if not specified
  113. if *a.grpcPort == 0 {
  114. *a.grpcPort = *a.port + 10000
  115. }
  116. // Security warnings
  117. if *a.adminPassword == "" {
  118. fmt.Println("WARNING: Admin interface is running without authentication!")
  119. fmt.Println(" Set -adminPassword for production use")
  120. }
  121. fmt.Printf("Starting SeaweedFS Admin Interface on port %d\n", *a.port)
  122. fmt.Printf("Worker gRPC server will run on port %d\n", *a.grpcPort)
  123. fmt.Printf("Masters: %s\n", *a.masters)
  124. fmt.Printf("Filers will be discovered automatically from masters\n")
  125. if *a.dataDir != "" {
  126. fmt.Printf("Data Directory: %s\n", *a.dataDir)
  127. } else {
  128. fmt.Printf("Data Directory: Not specified (configuration will be in-memory only)\n")
  129. }
  130. if *a.adminPassword != "" {
  131. fmt.Printf("Authentication: Enabled (user: %s)\n", *a.adminUser)
  132. } else {
  133. fmt.Printf("Authentication: Disabled\n")
  134. }
  135. // Set up graceful shutdown
  136. ctx, cancel := context.WithCancel(context.Background())
  137. defer cancel()
  138. // Handle interrupt signals
  139. sigChan := make(chan os.Signal, 1)
  140. signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  141. go func() {
  142. sig := <-sigChan
  143. fmt.Printf("\nReceived signal %v, shutting down gracefully...\n", sig)
  144. cancel()
  145. }()
  146. // Start the admin server with all masters
  147. err := startAdminServer(ctx, a)
  148. if err != nil {
  149. fmt.Printf("Admin server error: %v\n", err)
  150. return false
  151. }
  152. fmt.Println("Admin server stopped")
  153. return true
  154. }
  155. // startAdminServer starts the actual admin server
  156. func startAdminServer(ctx context.Context, options AdminOptions) error {
  157. // Set Gin mode
  158. gin.SetMode(gin.ReleaseMode)
  159. // Create router
  160. r := gin.New()
  161. r.Use(gin.Logger(), gin.Recovery())
  162. // Session store - always auto-generate session key
  163. sessionKeyBytes := make([]byte, 32)
  164. _, err := rand.Read(sessionKeyBytes)
  165. if err != nil {
  166. return fmt.Errorf("failed to generate session key: %w", err)
  167. }
  168. store := cookie.NewStore(sessionKeyBytes)
  169. // Configure session options to ensure cookies are properly saved
  170. store.Options(sessions.Options{
  171. Path: "/",
  172. MaxAge: 3600 * 24, // 24 hours
  173. })
  174. r.Use(sessions.Sessions("admin-session", store))
  175. // Static files - serve from embedded filesystem
  176. staticFS, err := admin.GetStaticFS()
  177. if err != nil {
  178. log.Printf("Warning: Failed to load embedded static files: %v", err)
  179. } else {
  180. r.StaticFS("/static", http.FS(staticFS))
  181. }
  182. // Create data directory if specified
  183. var dataDir string
  184. if *options.dataDir != "" {
  185. // Expand tilde (~) to home directory
  186. expandedDir, err := expandHomeDir(*options.dataDir)
  187. if err != nil {
  188. return fmt.Errorf("failed to expand dataDir path %s: %v", *options.dataDir, err)
  189. }
  190. dataDir = expandedDir
  191. // Show path expansion if it occurred
  192. if dataDir != *options.dataDir {
  193. fmt.Printf("Expanded dataDir: %s -> %s\n", *options.dataDir, dataDir)
  194. }
  195. if err := os.MkdirAll(dataDir, 0755); err != nil {
  196. return fmt.Errorf("failed to create data directory %s: %v", dataDir, err)
  197. }
  198. fmt.Printf("Data directory created/verified: %s\n", dataDir)
  199. }
  200. // Create admin server
  201. adminServer := dash.NewAdminServer(*options.masters, nil, dataDir)
  202. // Show discovered filers
  203. filers := adminServer.GetAllFilers()
  204. if len(filers) > 0 {
  205. fmt.Printf("Discovered filers: %s\n", strings.Join(filers, ", "))
  206. } else {
  207. fmt.Printf("No filers discovered from masters\n")
  208. }
  209. // Start worker gRPC server for worker connections
  210. err = adminServer.StartWorkerGrpcServer(*options.grpcPort)
  211. if err != nil {
  212. return fmt.Errorf("failed to start worker gRPC server: %w", err)
  213. }
  214. // Set up cleanup for gRPC server
  215. defer func() {
  216. if stopErr := adminServer.StopWorkerGrpcServer(); stopErr != nil {
  217. log.Printf("Error stopping worker gRPC server: %v", stopErr)
  218. }
  219. }()
  220. // Create handlers and setup routes
  221. adminHandlers := handlers.NewAdminHandlers(adminServer)
  222. adminHandlers.SetupRoutes(r, *options.adminPassword != "", *options.adminUser, *options.adminPassword)
  223. // Server configuration
  224. addr := fmt.Sprintf(":%d", *options.port)
  225. server := &http.Server{
  226. Addr: addr,
  227. Handler: r,
  228. }
  229. // Start server
  230. go func() {
  231. log.Printf("Starting SeaweedFS Admin Server on port %d", *options.port)
  232. // start http or https server with security.toml
  233. var (
  234. clientCertFile,
  235. certFile,
  236. keyFile string
  237. )
  238. useTLS := false
  239. useMTLS := false
  240. if viper.GetString("https.admin.key") != "" {
  241. useTLS = true
  242. certFile = viper.GetString("https.admin.cert")
  243. keyFile = viper.GetString("https.admin.key")
  244. }
  245. if viper.GetString("https.admin.ca") != "" {
  246. useMTLS = true
  247. clientCertFile = viper.GetString("https.admin.ca")
  248. }
  249. if useMTLS {
  250. server.TLSConfig = security.LoadClientTLSHTTP(clientCertFile)
  251. }
  252. if useTLS {
  253. log.Printf("Starting SeaweedFS Admin Server with TLS on port %d", *options.port)
  254. err = server.ListenAndServeTLS(certFile, keyFile)
  255. } else {
  256. err = server.ListenAndServe()
  257. }
  258. if err != nil && err != http.ErrServerClosed {
  259. log.Printf("Failed to start server: %v", err)
  260. }
  261. }()
  262. // Wait for context cancellation
  263. <-ctx.Done()
  264. // Graceful shutdown
  265. log.Println("Shutting down admin server...")
  266. shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  267. defer cancel()
  268. if err := server.Shutdown(shutdownCtx); err != nil {
  269. return fmt.Errorf("admin server forced to shutdown: %w", err)
  270. }
  271. return nil
  272. }
  273. // GetAdminOptions returns the admin command options for testing
  274. func GetAdminOptions() *AdminOptions {
  275. return &AdminOptions{}
  276. }
  277. // expandHomeDir expands the tilde (~) in a path to the user's home directory
  278. func expandHomeDir(path string) (string, error) {
  279. if path == "" {
  280. return path, nil
  281. }
  282. if !strings.HasPrefix(path, "~") {
  283. return path, nil
  284. }
  285. // Get current user
  286. currentUser, err := user.Current()
  287. if err != nil {
  288. return "", fmt.Errorf("failed to get current user: %w", err)
  289. }
  290. // Handle different tilde patterns
  291. if path == "~" {
  292. return currentUser.HomeDir, nil
  293. }
  294. if strings.HasPrefix(path, "~/") {
  295. return filepath.Join(currentUser.HomeDir, path[2:]), nil
  296. }
  297. // Handle ~username/ patterns
  298. if strings.HasPrefix(path, "~") {
  299. parts := strings.SplitN(path[1:], "/", 2)
  300. username := parts[0]
  301. targetUser, err := user.Lookup(username)
  302. if err != nil {
  303. return "", fmt.Errorf("user %s not found: %v", username, err)
  304. }
  305. if len(parts) == 1 {
  306. return targetUser.HomeDir, nil
  307. }
  308. return filepath.Join(targetUser.HomeDir, parts[1]), nil
  309. }
  310. return path, nil
  311. }