| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- // udeServer: 停用服务(状态开关拨向"停用") —— 逐保护目录调外挂程序 /api/fuseUnMount 卸载。
- // 成功后 mount_status=unmounted、status=停用;任一目录卸载失败则 mount_status=failed,
- // 失败信息聚合进 mountWarning 由前端弹黄色警告(软处理,不阻塞状态变更)。
- package logic
- import (
- "context"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "path"
- "strings"
- "udeServer/internal/audit"
- "udeServer/internal/svc"
- "udeServer/internal/types"
- "github.com/zeromicro/go-zero/core/logx"
- )
- type UnmountServiceLogic struct {
- logx.Logger
- ctx context.Context
- svcCtx *svc.ServiceContext
- }
- func NewUnmountServiceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnmountServiceLogic {
- return &UnmountServiceLogic{
- Logger: logx.WithContext(ctx),
- ctx: ctx,
- svcCtx: svcCtx,
- }
- }
- // UnmountService 请求体与手动补刷同款:路径参数 id(types.ServiceIdReq)。
- // 卸载接口幂等:挂载点不在外挂注册表中也返回成功,重复点停用零成本。
- func (l *UnmountServiceLogic) UnmountService(req *types.ServiceIdReq) (*types.ServiceResp, error) {
- if err := checkId(req.Id); err != nil {
- return nil, err
- }
- svc, err := findService(l.ctx, l.svcCtx.ServiceModel, req.Id)
- if err != nil {
- return nil, err
- }
- dirs, err := l.svcCtx.DirectoryModel.FindDirsByServiceId(l.ctx, req.Id)
- if err != nil {
- return nil, fmt.Errorf("查询保护目录失败: %w", err)
- }
- // 只有挂载状态是 mounted 才真正调外挂卸载(与删除服务的前置判断同款):
- // unmounted/failed 直接跳过,不去外挂机器白跑一趟。
- var msgs []string
- if svc.MountStatus.String == "mounted" {
- ip, port := svc.Ip.String, svc.Port // port 已是 int64
- if ip == "" || port == 0 {
- msgs = append(msgs, "外挂服务 IP/端口为空,无法联动卸载,目录可能仍处于挂载状态")
- } else {
- for _, mount := range dirs {
- // path.Clean(非 filepath!):发给远程 Linux 外挂必须保持 / 分隔符
- cleanMount := path.Clean(mount)
- baseURL := fmt.Sprintf("http://%s:%d/api/fuseUnMount", ip, port) // %d:port 是 int64
- reqURL, uerr := url.Parse(baseURL)
- if uerr != nil {
- msgs = append(msgs, fmt.Sprintf("解析 URL 失败: %v", uerr))
- continue
- }
- // q.Encode() 自动做 URL 编码(/ → %2F),外挂侧 Query().Get 自动解码还原
- q := reqURL.Query()
- q.Set("mount", cleanMount)
- reqURL.RawQuery = q.Encode()
- finalURL := reqURL.String()
- // DoSimple 记 type=2 审计日志(GET 用 url 描述请求原文),
- // doFuseRequest 注入 X-TOKEN=iden(外挂侧与环境变量 TOKEN 恒定时间比对)
- if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
- audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
- func() (map[string]interface{}, int64, error) {
- resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodGet, finalURL, nil, "")
- if rerr != nil {
- return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
- }
- raw, _ := io.ReadAll(resp.Body) // 读到 EOF 保证 TCP 连接复用 Keep-Alive
- resp.Body.Close()
- status := int64(resp.StatusCode)
- if status >= http.StatusBadRequest {
- return nil, status, fmt.Errorf("卸载目录 %s 失败,外挂程序返回 HTTP %d: %s", cleanMount, status, string(raw))
- }
- return map[string]interface{}{"status": status, "body": string(raw)}, status, nil
- }); callErr != nil {
- msgs = append(msgs, callErr.Error())
- continue
- }
- l.Infof("fuse unmount ok: %s", cleanMount)
- }
- }
- }
- // 卸载结果落库:全成功(或本来就没挂载)=unmounted;任一失败=failed。
- // 同时把服务启用状态置"停用"。落库失败只记日志,不影响主流程。
- mountStatus := "unmounted"
- if len(msgs) > 0 {
- mountStatus = "failed"
- }
- if uerr := l.svcCtx.ServiceModel.UpdateMountStatus(l.ctx, req.Id, mountStatus); uerr != nil {
- l.Errorf("update mount_status=%s failed(id=%d): %v", mountStatus, req.Id, uerr)
- }
- if uerr := l.svcCtx.ServiceModel.UpdateStatus(l.ctx, req.Id, "停用"); uerr != nil {
- l.Errorf("update status=停用 failed(id=%d): %v", req.Id, uerr)
- }
- updated, err := findService(l.ctx, l.svcCtx.ServiceModel, req.Id)
- if err != nil {
- return nil, err
- }
- resp := toServiceResp(updated)
- if len(msgs) > 0 {
- resp.MountWarning = strings.Join(msgs, "; ")
- }
- return resp, nil
- }
|