The smallest watchdog on earth. Tiny, monitoring-plugins compatible monitoring with a status page. https://cloud.docker.com/repository/docker/momar/chihuahua/general
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.

173 lines
4.3KB

  1. package chihuahua
  2. //go:generate pkger
  3. import (
  4. "github.com/rs/zerolog/log"
  5. "io/ioutil"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "github.com/gin-gonic/gin"
  14. "github.com/markbates/pkger"
  15. )
  16. var keyList string
  17. var keyListAge time.Time
  18. func UpdateKeys() {
  19. if time.Now().Sub(keyListAge) < 500*time.Millisecond {
  20. return
  21. }
  22. keyFiles, _ := filepath.Glob(filepath.Join(os.Getenv("HOME"), ".ssh/id_*"))
  23. keyList = ""
  24. if keyFiles != nil {
  25. for _, file := range keyFiles {
  26. if !strings.HasSuffix(file, ".pub") {
  27. continue // it's a private key, and we don't want those to be reachable unauthenticated via HTTP :)
  28. }
  29. key, _ := ioutil.ReadFile(file)
  30. keyList += strings.TrimSpace(string(key)) + "\n"
  31. }
  32. }
  33. keyListAge = time.Now()
  34. }
  35. func GenerateKeys() {
  36. // Get SSH keys for the setup script
  37. // chown if someone already created a .ssh directory; mostly relevant for Docker
  38. filepath.Walk(filepath.Join(os.Getenv("HOME"), ".ssh/"), func(path string, info os.FileInfo, err error) error {
  39. os.Chown(path, os.Getuid(), os.Getgid())
  40. return nil
  41. })
  42. UpdateKeys()
  43. if keyList == "" {
  44. if info, err := os.Stat(filepath.Join(os.Getenv("HOME"), ".ssh")); err != nil || !info.IsDir() {
  45. err := os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".ssh"), 755)
  46. if err != nil {
  47. log.Warn().Str("caller", "api").Err(err).Msg("couldn't generate an SSH key")
  48. }
  49. }
  50. cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", "-C", "Chihuahua Monitoring", "-f", filepath.Join(os.Getenv("HOME"), ".ssh/id_ed25519"))
  51. cmd.Stderr = os.Stderr
  52. cmd.Stdout = os.Stdout
  53. log.Info().Str("caller", "api").Msgf("no SSH key found, generating one at %s", cmd.Args[len(cmd.Args)-1])
  54. err := cmd.Run()
  55. if err != nil {
  56. log.Warn().Str("caller", "api").Err(err).Msg("couldn't generate an SSH key")
  57. }
  58. }
  59. }
  60. func Api(cfg *Config) {
  61. // getCheck returns the check results
  62. getCheck := func(c *gin.Context) {
  63. path := c.Param("id")
  64. if path == "" {
  65. // All servers
  66. c.JSON(200, cfg.Servers)
  67. return
  68. }
  69. id := strings.Split(strings.Trim(path, "/"), "/")
  70. parent := &ServerOrGroup{Children: cfg.Servers}
  71. for i := 0; i < len(id); i++ {
  72. idPart, err := url.QueryUnescape(id[i])
  73. if err != nil {
  74. idPart = id[i]
  75. }
  76. found := false
  77. if parent.Children != nil {
  78. for _, child := range parent.Children {
  79. if child.ID == idPart {
  80. parent = child
  81. found = true
  82. break
  83. }
  84. }
  85. }
  86. if !found {
  87. // allow single checks
  88. if parent.Checks != nil {
  89. for _, check := range parent.Checks {
  90. if check.ID == idPart {
  91. if i+1 >= len(id) {
  92. c.JSON(200, check)
  93. return
  94. }
  95. }
  96. }
  97. }
  98. c.JSON(404, map[string]struct{}{})
  99. return
  100. }
  101. }
  102. c.JSON(200, parent)
  103. return
  104. }
  105. // putMessage adds a message to a check
  106. //putMessage := func(req *air.Request, res *air.Response) error {
  107. // // TODO
  108. // return nil
  109. //}
  110. // deleteMessage deletes the message from a check
  111. //deleteMessage := func(req *air.Request, res *air.Response) error {
  112. // // TODO:
  113. // return nil
  114. //}
  115. // Get the setup script
  116. setupFile, err := pkger.Open("codeberg.org/momar/chihuahua:/resources/setup.sh")
  117. if err != nil {
  118. log.Error().Str("caller", "api").Err(err).Msg("couldn't open setup.sh from web resources")
  119. os.Exit(1)
  120. }
  121. setupScript, err := ioutil.ReadAll(setupFile)
  122. if err != nil {
  123. log.Error().Str("caller", "api").Err(err).Msg("couldn't read setup.sh from web resources")
  124. os.Exit(1)
  125. }
  126. getSetupScript := func(c *gin.Context) {
  127. UpdateKeys()
  128. c.String(200, strings.ReplaceAll(string(setupScript), "[CHIHUAHUA_PUBLIC_KEYS]", keyList))
  129. }
  130. app := gin.New()
  131. app.Use(gin.Recovery())
  132. app.GET("/setup.sh", getSetupScript)
  133. app.GET("/checks", getCheck)
  134. app.GET("/checks/*id", getCheck)
  135. //app.PUT("/checks/*", putMessage)
  136. //app.DELETE("/checks/:server/:check", deleteMessage)
  137. app.NoRoute(gin.WrapH(http.FileServer(pkger.Dir("/web"))))
  138. addr := ":"
  139. if host := os.Getenv("HOST"); host != "" {
  140. addr = host + addr
  141. }
  142. if port := os.Getenv("PORT"); port != "" {
  143. addr += port
  144. } else {
  145. addr += "8080"
  146. }
  147. log.Info().Str("caller", "api").Str("address", addr).Msg("starting API web server")
  148. err = app.Run(addr)
  149. if err != nil {
  150. log.Error().Str("caller", "api").Err(err).Msg("web server threw an error")
  151. os.Exit(1)
  152. }
  153. }
  154. func SendUpdate(cfg *Config) {
  155. // TODO: send current check state to browsers via SSE
  156. }