1
0

maintenance.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. package site
  2. import (
  3. "fmt"
  4. "net/http"
  5. "os"
  6. "runtime"
  7. "strings"
  8. "sync"
  9. "github.com/0xJacky/Nginx-UI/internal/helper"
  10. "github.com/0xJacky/Nginx-UI/internal/nginx"
  11. "github.com/0xJacky/Nginx-UI/internal/notification"
  12. "github.com/0xJacky/Nginx-UI/model"
  13. "github.com/go-resty/resty/v2"
  14. "github.com/tufanbarisyildirim/gonginx/config"
  15. "github.com/tufanbarisyildirim/gonginx/parser"
  16. "github.com/uozi-tech/cosy/logger"
  17. "github.com/uozi-tech/cosy/settings"
  18. )
  19. const MaintenanceSuffix = "_nginx_ui_maintenance"
  20. // EnableMaintenance enables maintenance mode for a site
  21. func EnableMaintenance(name string) (err error) {
  22. // Check if the site exists in sites-available
  23. configFilePath := nginx.GetConfPath("sites-available", name)
  24. _, err = os.Stat(configFilePath)
  25. if err != nil {
  26. return
  27. }
  28. // Path for the maintenance configuration file
  29. maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix)
  30. // Path for original configuration in sites-enabled
  31. originalEnabledPath := nginx.GetConfPath("sites-enabled", name)
  32. // Check if the site is already in maintenance mode
  33. if helper.FileExists(maintenanceConfigPath) {
  34. return
  35. }
  36. // Read the original configuration file
  37. content, err := os.ReadFile(configFilePath)
  38. if err != nil {
  39. return
  40. }
  41. // Parse the nginx configuration
  42. p := parser.NewStringParser(string(content), parser.WithSkipValidDirectivesErr())
  43. conf, err := p.Parse()
  44. if err != nil {
  45. return fmt.Errorf("failed to parse nginx configuration: %s", err)
  46. }
  47. // Create new maintenance configuration
  48. maintenanceConfig := createMaintenanceConfig(conf)
  49. // Write maintenance configuration to file
  50. err = os.WriteFile(maintenanceConfigPath, []byte(maintenanceConfig), 0644)
  51. if err != nil {
  52. return
  53. }
  54. // Remove the original symlink from sites-enabled if it exists
  55. if helper.FileExists(originalEnabledPath) {
  56. err = os.Remove(originalEnabledPath)
  57. if err != nil {
  58. // If we couldn't remove the original, remove the maintenance file and return the error
  59. _ = os.Remove(maintenanceConfigPath)
  60. return
  61. }
  62. }
  63. // Test nginx config, if not pass, then restore original configuration
  64. output := nginx.TestConf()
  65. if nginx.GetLogLevel(output) > nginx.Warn {
  66. // Configuration error, cleanup and revert
  67. _ = os.Remove(maintenanceConfigPath)
  68. if helper.FileExists(originalEnabledPath + "_backup") {
  69. _ = os.Rename(originalEnabledPath+"_backup", originalEnabledPath)
  70. }
  71. return fmt.Errorf("%s", output)
  72. }
  73. // Reload nginx
  74. output = nginx.Reload()
  75. if nginx.GetLogLevel(output) > nginx.Warn {
  76. return fmt.Errorf("%s", output)
  77. }
  78. // Synchronize with other nodes
  79. go syncEnableMaintenance(name)
  80. return nil
  81. }
  82. // DisableMaintenance disables maintenance mode for a site
  83. func DisableMaintenance(name string) (err error) {
  84. // Check if the site is in maintenance mode
  85. maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix)
  86. _, err = os.Stat(maintenanceConfigPath)
  87. if err != nil {
  88. return
  89. }
  90. // Original configuration paths
  91. configFilePath := nginx.GetConfPath("sites-available", name)
  92. enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
  93. // Check if the original configuration exists
  94. _, err = os.Stat(configFilePath)
  95. if err != nil {
  96. return
  97. }
  98. // Create symlink to original configuration
  99. err = os.Symlink(configFilePath, enabledConfigFilePath)
  100. if err != nil {
  101. return
  102. }
  103. // Remove maintenance configuration
  104. err = os.Remove(maintenanceConfigPath)
  105. if err != nil {
  106. // If we couldn't remove the maintenance file, remove the new symlink and return the error
  107. _ = os.Remove(enabledConfigFilePath)
  108. return
  109. }
  110. // Test nginx config, if not pass, then revert
  111. output := nginx.TestConf()
  112. if nginx.GetLogLevel(output) > nginx.Warn {
  113. // Configuration error, cleanup and revert
  114. _ = os.Remove(enabledConfigFilePath)
  115. _ = os.Symlink(configFilePath, maintenanceConfigPath)
  116. return fmt.Errorf("%s", output)
  117. }
  118. // Reload nginx
  119. output = nginx.Reload()
  120. if nginx.GetLogLevel(output) > nginx.Warn {
  121. return fmt.Errorf("%s", output)
  122. }
  123. // Synchronize with other nodes
  124. go syncDisableMaintenance(name)
  125. return nil
  126. }
  127. // createMaintenanceConfig creates a maintenance configuration based on the original config
  128. func createMaintenanceConfig(conf *config.Config) string {
  129. nginxUIPort := settings.ServerSettings.Port
  130. schema := "http"
  131. if settings.ServerSettings.EnableHTTPS {
  132. schema = "https"
  133. }
  134. // Create new configuration
  135. ngxConfig := nginx.NewNgxConfig("")
  136. // Find all server blocks in the original configuration
  137. serverBlocks := findServerBlocks(conf.Block)
  138. // Create maintenance mode configuration for each server block
  139. for _, server := range serverBlocks {
  140. ngxServer := nginx.NewNgxServer()
  141. // Copy listen directives
  142. listenDirectives := extractDirectives(server, "listen")
  143. for _, directive := range listenDirectives {
  144. ngxDirective := &nginx.NgxDirective{
  145. Directive: directive.GetName(),
  146. Params: strings.Join(extractParams(directive), " "),
  147. }
  148. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  149. }
  150. // Copy server_name directives
  151. serverNameDirectives := extractDirectives(server, "server_name")
  152. for _, directive := range serverNameDirectives {
  153. ngxDirective := &nginx.NgxDirective{
  154. Directive: directive.GetName(),
  155. Params: strings.Join(extractParams(directive), " "),
  156. }
  157. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  158. }
  159. // Copy SSL certificate directives
  160. sslCertDirectives := extractDirectives(server, "ssl_certificate")
  161. for _, directive := range sslCertDirectives {
  162. ngxDirective := &nginx.NgxDirective{
  163. Directive: directive.GetName(),
  164. Params: strings.Join(extractParams(directive), " "),
  165. }
  166. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  167. }
  168. // Copy SSL certificate key directives
  169. sslKeyDirectives := extractDirectives(server, "ssl_certificate_key")
  170. for _, directive := range sslKeyDirectives {
  171. ngxDirective := &nginx.NgxDirective{
  172. Directive: directive.GetName(),
  173. Params: strings.Join(extractParams(directive), " "),
  174. }
  175. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  176. }
  177. // Copy http2 directives
  178. http2Directives := extractDirectives(server, "http2")
  179. for _, directive := range http2Directives {
  180. ngxDirective := &nginx.NgxDirective{
  181. Directive: directive.GetName(),
  182. Params: strings.Join(extractParams(directive), " "),
  183. }
  184. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  185. }
  186. // Add maintenance mode location
  187. location := &nginx.NgxLocation{
  188. Path: "~ .*",
  189. }
  190. // Build location content using string builder
  191. var locationContent strings.Builder
  192. locationContent.WriteString("proxy_set_header Host $host;\n")
  193. locationContent.WriteString("proxy_set_header X-Real-IP $remote_addr;\n")
  194. locationContent.WriteString("proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
  195. locationContent.WriteString("proxy_set_header X-Forwarded-Proto $scheme;\n")
  196. locationContent.WriteString(fmt.Sprintf("rewrite ^ /pages/maintenance break;\n"))
  197. locationContent.WriteString(fmt.Sprintf("proxy_pass %s://127.0.0.1:%d;\n", schema, nginxUIPort))
  198. location.Content = locationContent.String()
  199. ngxServer.Locations = append(ngxServer.Locations, location)
  200. // Add to configuration
  201. ngxConfig.Servers = append(ngxConfig.Servers, ngxServer)
  202. }
  203. // Generate configuration file content
  204. content, err := ngxConfig.BuildConfig()
  205. if err != nil {
  206. logger.Error("Failed to build maintenance config", err)
  207. return ""
  208. }
  209. return content
  210. }
  211. // findServerBlocks finds all server blocks in a configuration
  212. func findServerBlocks(block config.IBlock) []config.IDirective {
  213. var servers []config.IDirective
  214. if block == nil {
  215. return servers
  216. }
  217. for _, directive := range block.GetDirectives() {
  218. if directive.GetName() == "server" {
  219. servers = append(servers, directive)
  220. }
  221. }
  222. return servers
  223. }
  224. // extractDirectives extracts all directives with a specific name from a server block
  225. func extractDirectives(server config.IDirective, name string) []config.IDirective {
  226. var directives []config.IDirective
  227. if server.GetBlock() == nil {
  228. return directives
  229. }
  230. for _, directive := range server.GetBlock().GetDirectives() {
  231. if directive.GetName() == name {
  232. directives = append(directives, directive)
  233. }
  234. }
  235. return directives
  236. }
  237. // extractParams extracts all parameters from a directive
  238. func extractParams(directive config.IDirective) []string {
  239. var params []string
  240. for _, param := range directive.GetParameters() {
  241. params = append(params, param.Value)
  242. }
  243. return params
  244. }
  245. // syncEnableMaintenance synchronizes enabling maintenance mode with other nodes
  246. func syncEnableMaintenance(name string) {
  247. nodes := getSyncNodes(name)
  248. wg := &sync.WaitGroup{}
  249. wg.Add(len(nodes))
  250. for _, node := range nodes {
  251. go func(node *model.Environment) {
  252. defer func() {
  253. if err := recover(); err != nil {
  254. buf := make([]byte, 1024)
  255. runtime.Stack(buf, false)
  256. logger.Error(err)
  257. }
  258. }()
  259. defer wg.Done()
  260. client := resty.New()
  261. client.SetBaseURL(node.URL)
  262. resp, err := client.R().
  263. SetHeader("X-Node-Secret", node.Token).
  264. Post(fmt.Sprintf("/api/sites/%s/maintenance/enable", name))
  265. if err != nil {
  266. notification.Error("Enable Remote Site Maintenance Error", err.Error(), nil)
  267. return
  268. }
  269. if resp.StatusCode() != http.StatusOK {
  270. notification.Error("Enable Remote Site Maintenance Error", "Enable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp))
  271. return
  272. }
  273. notification.Success("Enable Remote Site Maintenance Success", "Enable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp))
  274. }(node)
  275. }
  276. wg.Wait()
  277. }
  278. // syncDisableMaintenance synchronizes disabling maintenance mode with other nodes
  279. func syncDisableMaintenance(name string) {
  280. nodes := getSyncNodes(name)
  281. wg := &sync.WaitGroup{}
  282. wg.Add(len(nodes))
  283. for _, node := range nodes {
  284. go func(node *model.Environment) {
  285. defer func() {
  286. if err := recover(); err != nil {
  287. buf := make([]byte, 1024)
  288. runtime.Stack(buf, false)
  289. logger.Error(err)
  290. }
  291. }()
  292. defer wg.Done()
  293. client := resty.New()
  294. client.SetBaseURL(node.URL)
  295. resp, err := client.R().
  296. SetHeader("X-Node-Secret", node.Token).
  297. Post(fmt.Sprintf("/api/sites/%s/maintenance/disable", name))
  298. if err != nil {
  299. notification.Error("Disable Remote Site Maintenance Error", err.Error(), nil)
  300. return
  301. }
  302. if resp.StatusCode() != http.StatusOK {
  303. notification.Error("Disable Remote Site Maintenance Error", "Disable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp))
  304. return
  305. }
  306. notification.Success("Disable Remote Site Maintenance Success", "Disable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp))
  307. }(node)
  308. }
  309. wg.Wait()
  310. }