unmountservicelogic.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. // udeServer: 停用服务(状态开关拨向"停用") —— 逐保护目录调外挂程序 /api/fuseUnMount 卸载。
  2. // 成功后 mount_status=unmounted、status=停用;任一目录卸载失败则 mount_status=failed,
  3. // 失败信息聚合进 mountWarning 由前端弹黄色警告(软处理,不阻塞状态变更)。
  4. package logic
  5. import (
  6. "context"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "path"
  12. "strings"
  13. "udeServer/internal/audit"
  14. "udeServer/internal/svc"
  15. "udeServer/internal/types"
  16. "github.com/zeromicro/go-zero/core/logx"
  17. )
  18. type UnmountServiceLogic struct {
  19. logx.Logger
  20. ctx context.Context
  21. svcCtx *svc.ServiceContext
  22. }
  23. func NewUnmountServiceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnmountServiceLogic {
  24. return &UnmountServiceLogic{
  25. Logger: logx.WithContext(ctx),
  26. ctx: ctx,
  27. svcCtx: svcCtx,
  28. }
  29. }
  30. // UnmountService 请求体与手动补刷同款:路径参数 id(types.ServiceIdReq)。
  31. // 卸载接口幂等:挂载点不在外挂注册表中也返回成功,重复点停用零成本。
  32. func (l *UnmountServiceLogic) UnmountService(req *types.ServiceIdReq) (*types.ServiceResp, error) {
  33. if err := checkId(req.Id); err != nil {
  34. return nil, err
  35. }
  36. svc, err := findService(l.ctx, l.svcCtx.ServiceModel, req.Id)
  37. if err != nil {
  38. return nil, err
  39. }
  40. dirs, err := l.svcCtx.DirectoryModel.FindDirsByServiceId(l.ctx, req.Id)
  41. if err != nil {
  42. return nil, fmt.Errorf("查询保护目录失败: %w", err)
  43. }
  44. // 只有挂载状态是 mounted 才真正调外挂卸载(与删除服务的前置判断同款):
  45. // unmounted/failed 直接跳过,不去外挂机器白跑一趟。
  46. var msgs []string
  47. if svc.MountStatus.String == "mounted" {
  48. ip, port := svc.Ip.String, svc.Port // port 已是 int64
  49. if ip == "" || port == 0 {
  50. msgs = append(msgs, "外挂服务 IP/端口为空,无法联动卸载,目录可能仍处于挂载状态")
  51. } else {
  52. for _, mount := range dirs {
  53. // path.Clean(非 filepath!):发给远程 Linux 外挂必须保持 / 分隔符
  54. cleanMount := path.Clean(mount)
  55. baseURL := fmt.Sprintf("http://%s:%d/api/fuseUnMount", ip, port) // %d:port 是 int64
  56. reqURL, uerr := url.Parse(baseURL)
  57. if uerr != nil {
  58. msgs = append(msgs, fmt.Sprintf("解析 URL 失败: %v", uerr))
  59. continue
  60. }
  61. // q.Encode() 自动做 URL 编码(/ → %2F),外挂侧 Query().Get 自动解码还原
  62. q := reqURL.Query()
  63. q.Set("mount", cleanMount)
  64. reqURL.RawQuery = q.Encode()
  65. finalURL := reqURL.String()
  66. // DoSimple 记 type=2 审计日志(GET 用 url 描述请求原文),
  67. // doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret,外挂侧与环境变量 TOKEN 恒定时间比对)
  68. if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
  69. audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
  70. func() (map[string]interface{}, int64, error) {
  71. resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, svc.Iden.String, http.MethodGet, finalURL, nil, "")
  72. if rerr != nil {
  73. return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
  74. }
  75. raw, _ := io.ReadAll(resp.Body) // 读到 EOF 保证 TCP 连接复用 Keep-Alive
  76. resp.Body.Close()
  77. status := int64(resp.StatusCode)
  78. if status >= http.StatusBadRequest {
  79. return nil, status, fmt.Errorf("卸载目录 %s 失败,外挂程序返回 HTTP %d: %s", cleanMount, status, string(raw))
  80. }
  81. return map[string]interface{}{"status": status, "body": string(raw)}, status, nil
  82. }); callErr != nil {
  83. msgs = append(msgs, callErr.Error())
  84. continue
  85. }
  86. l.Infof("fuse unmount ok: %s", cleanMount)
  87. }
  88. }
  89. }
  90. // 卸载结果落库:全成功(或本来就没挂载)=unmounted;任一失败=failed。
  91. // 同时把服务启用状态置"停用"。落库失败只记日志,不影响主流程。
  92. mountStatus := "unmounted"
  93. if len(msgs) > 0 {
  94. mountStatus = "failed"
  95. }
  96. if uerr := l.svcCtx.ServiceModel.UpdateMountStatus(l.ctx, req.Id, mountStatus); uerr != nil {
  97. l.Errorf("update mount_status=%s failed(id=%d): %v", mountStatus, req.Id, uerr)
  98. }
  99. if uerr := l.svcCtx.ServiceModel.UpdateStatus(l.ctx, req.Id, "停用"); uerr != nil {
  100. l.Errorf("update status=停用 failed(id=%d): %v", req.Id, uerr)
  101. }
  102. updated, err := findService(l.ctx, l.svcCtx.ServiceModel, req.Id)
  103. if err != nil {
  104. return nil, err
  105. }
  106. resp := toServiceResp(updated)
  107. if len(msgs) > 0 {
  108. resp.MountWarning = strings.Join(msgs, "; ")
  109. }
  110. return resp, nil
  111. }