|
@@ -0,0 +1,137 @@
|
|
|
|
|
+// udeServer: 启用服务(状态开关拨向"启用") —— 逐保护目录调外挂程序 /api/fuseMount 挂载。
|
|
|
|
|
+// 成功后 mount_status=mounted、status=启用;任一目录挂载失败则 mount_status=failed,
|
|
|
|
|
+// 失败信息聚合进 mountWarning 由前端弹黄色警告(软处理,不阻塞状态变更)。
|
|
|
|
|
+
|
|
|
|
|
+package logic
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "bytes"
|
|
|
|
|
+ "context"
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "path"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+
|
|
|
|
|
+ "udeServer/internal/audit"
|
|
|
|
|
+ "udeServer/internal/svc"
|
|
|
|
|
+ "udeServer/internal/types"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/zeromicro/go-zero/core/logx"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type MountServiceLogic struct {
|
|
|
|
|
+ logx.Logger
|
|
|
|
|
+ ctx context.Context
|
|
|
|
|
+ svcCtx *svc.ServiceContext
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func NewMountServiceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MountServiceLogic {
|
|
|
|
|
+ return &MountServiceLogic{
|
|
|
|
|
+ Logger: logx.WithContext(ctx),
|
|
|
|
|
+ ctx: ctx,
|
|
|
|
|
+ svcCtx: svcCtx,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// MountService 请求体与手动补刷同款:路径参数 id(types.ServiceIdReq)。
|
|
|
|
|
+// 挂载是幂等的:外挂注册表里"已挂且同密钥"的目录直接跳过,重复点启用零成本。
|
|
|
|
|
+func (l *MountServiceLogic) MountService(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
|
|
|
|
|
+ }
|
|
|
|
|
+ ip, port := svc.Ip.String, svc.Port // port 已是 int64
|
|
|
|
|
+ if ip == "" || port == 0 {
|
|
|
|
|
+ return nil, fmt.Errorf("外挂服务 IP/端口为空,无法下发挂载")
|
|
|
|
|
+ }
|
|
|
|
|
+ // 密钥内容:按服务配置的密钥标识(keyIden)查 t_key,与创建流程 notifyFuseMount 同源
|
|
|
|
|
+ keyIden := svc.KeyIden.String
|
|
|
|
|
+ if keyIden == "" {
|
|
|
|
|
+ return nil, fmt.Errorf("服务未配置密钥标识(keyIden),无法下发挂载")
|
|
|
|
|
+ }
|
|
|
|
|
+ key, err := l.svcCtx.KeyModel.FindOneByIden(l.ctx, keyIden)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("查询密钥(%s)失败: %w", keyIden, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ ikey := key.Content.String
|
|
|
|
|
+ if ikey == "" {
|
|
|
|
|
+ return nil, fmt.Errorf("密钥(%s)内容为空,无法下发挂载", keyIden)
|
|
|
|
|
+ }
|
|
|
|
|
+ dirs, err := l.svcCtx.DirectoryModel.FindDirsByServiceId(l.ctx, req.Id)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("查询保护目录失败: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 逐目录挂载:audit.DoSimple 记 type=2 审计日志,
|
|
|
|
|
+ // doFuseRequest 注入 X-TOKEN=iden(外挂侧与环境变量 TOKEN 恒定时间比对)
|
|
|
|
|
+ var msgs []string
|
|
|
|
|
+ for _, mount := range dirs {
|
|
|
|
|
+ // path.Clean(非 filepath!):发给远程 Linux 外挂必须保持 / 分隔符
|
|
|
|
|
+ dirLinuxPath := path.Clean(mount)
|
|
|
|
|
+ payload := map[string]interface{}{
|
|
|
|
|
+ "backing": dirLinuxPath + "-cipher", // 密文目录 = 保护目录 + "-cipher" 后缀
|
|
|
|
|
+ "mount": dirLinuxPath, // 挂载点 = 保护目录本身
|
|
|
|
|
+ "ikey": ikey,
|
|
|
|
|
+ }
|
|
|
|
|
+ body, err := json.Marshal(payload)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ msgs = append(msgs, fmt.Sprintf("序列化挂载请求失败: %v", err))
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ baseURL := fmt.Sprintf("http://%s:%d/api/fuseMount", ip, port) // %d:port 是 int64
|
|
|
|
|
+ if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
|
|
|
|
|
+ func() (map[string]interface{}, int64, error) {
|
|
|
|
|
+ resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
|
|
|
|
|
+ if rerr != nil {
|
|
|
|
|
+ return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, 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", dirLinuxPath, 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 mount ok: %s -> %s", dirLinuxPath, dirLinuxPath+"-cipher")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 挂载结果落库:全成功=mounted;任一失败=failed;没配保护目录=unmounted(无挂载可言)。
|
|
|
|
|
+ // 启用状态与挂载结果挂钩:挂载成功才启用;任一失败(整体 failed)则保持停用,
|
|
|
|
|
+ // 前端按响应里的 mountStatus/status 把开关弹回"停用"位。落库失败只记日志,不影响主流程。
|
|
|
|
|
+ mountStatus := "mounted"
|
|
|
|
|
+ if len(dirs) == 0 {
|
|
|
|
|
+ mountStatus = "unmounted"
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(msgs) > 0 {
|
|
|
|
|
+ mountStatus = "failed"
|
|
|
|
|
+ }
|
|
|
|
|
+ finalStatus := "启用"
|
|
|
|
|
+ if mountStatus == "failed" {
|
|
|
|
|
+ finalStatus = "停用"
|
|
|
|
|
+ }
|
|
|
|
|
+ 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, finalStatus); uerr != nil {
|
|
|
|
|
+ l.Errorf("update status=%s failed(id=%d): %v", finalStatus, 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
|
|
|
|
|
+}
|