From 60cda668edaf09f1d3578836dac5d206f0d107e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Wed, 14 Jan 2026 18:59:48 +0100 Subject: [PATCH 01/22] Retrieve timezone field in NodeByNodeId function --- cdb/db_nodes.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cdb/db_nodes.go b/cdb/db_nodes.go index 6c3f85c..3d53260 100644 --- a/cdb/db_nodes.go +++ b/cdb/db_nodes.go @@ -28,6 +28,7 @@ type ( EnclosureSlot string Enclosure string Hv string + Tz string } ) @@ -43,19 +44,19 @@ func (oDb *DB) NodeByNodeID(ctx context.Context, nodeID string) (*DBNode, error) var ( query = `SELECT nodename, cluster_id, node_env, app, hv, node_frozen, loc_country, loc_city, loc_addr, loc_building, loc_floor, loc_room, - loc_rack, loc_zip, enclosure, enclosureslot + loc_rack, loc_zip, enclosure, enclosureslot, tz FROM nodes WHERE node_id = ? LIMIT 1` nodename, clusterID, nodeEnv, app, hv, frozen, locCountry sql.NullString locCity, locAddr, locBuilding, locFloor, locRoom, locRack sql.NullString - locZip, enclosure, enclosureSlot sql.NullString + locZip, enclosure, enclosureSlot, tz sql.NullString ) err := oDb.DB. QueryRowContext(ctx, query, nodeID). Scan( &nodename, &clusterID, &nodeEnv, &app, &hv, &frozen, &locCountry, &locCity, &locAddr, &locBuilding, &locFloor, &locRoom, - &locRack, &locZip, &enclosure, &enclosureSlot) + &locRack, &locZip, &enclosure, &enclosureSlot, &tz) switch { case errors.Is(err, sql.ErrNoRows): return nil, nil @@ -80,6 +81,7 @@ func (oDb *DB) NodeByNodeID(ctx context.Context, nodeID string) (*DBNode, error) Enclosure: enclosure.String, EnclosureSlot: enclosureSlot.String, Hv: hv.String, + Tz: tz.String, } return &node, nil } From 28206c66bd17a7d9d2237c34efaae54e0e49f3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Thu, 15 Jan 2026 09:41:24 +0100 Subject: [PATCH 02/22] Add /feed/action endpoint - POST /feed/action for "begin_action" - Updated API version to 1.0.9. --- api/api.yaml | 61 ++++++++- api/codegen_server_gen.go | 81 +++++++----- api/codegen_type_gen.go | 16 +++ apihandlers/post_feed_action_begin.go | 81 ++++++++++++ cachekeys/main.go | 4 + cdb/db_actions.go | 24 ++++ worker/job_feed_action_begin.go | 175 ++++++++++++++++++++++++++ worker/worker.go | 9 ++ 8 files changed, 419 insertions(+), 32 deletions(-) create mode 100644 apihandlers/post_feed_action_begin.go create mode 100644 worker/job_feed_action_begin.go diff --git a/api/api.yaml b/api/api.yaml index e934272..bdbcb88 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -5,7 +5,7 @@ servers: info: title: opensvc collector api - version: 1.0.8 + version: 1.0.9 paths: /feed/daemon/ping: @@ -197,6 +197,34 @@ paths: tags: - agent + /feed/action: + post: + description: | + Begin an action for a given object path + operationId: PostFeedActionBegin + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ActionBegin' + responses: + 202: + description: action begin accepted + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 500: + $ref: '#/components/responses/500' + security: + - basicAuth: [ ] + - bearerAuth: [ ] + tags: + - agent + /version: get: operationId: GetVersion @@ -292,6 +320,35 @@ components: type: string description: the opensvc client data version + ActionBegin: + type: object + required: + - path + - action + - version + - begin + - cron + - session_uuid + - argv + properties: + path: + type: string + action: + type: string + version: + type: string + description: the opensvc client data version + begin: + type: string + cron: + type: boolean + session_uuid: + type: string + argv: + type: array + items: + type: string + NodeDisks: type: object properties: @@ -558,3 +615,5 @@ components: items: type: string description: object name + + \ No newline at end of file diff --git a/api/codegen_server_gen.go b/api/codegen_server_gen.go index ab80781..3610030 100644 --- a/api/codegen_server_gen.go +++ b/api/codegen_server_gen.go @@ -24,6 +24,9 @@ type ServerInterface interface { // (GET /docs/openapi) GetSwagger(ctx echo.Context) error + // (POST /feed/action) + PostFeedActionBegin(ctx echo.Context) error + // (POST /feed/daemon/ping) PostFeedDaemonPing(ctx echo.Context) error @@ -63,6 +66,19 @@ func (w *ServerInterfaceWrapper) GetSwagger(ctx echo.Context) error { return err } +// PostFeedActionBegin converts echo context to params. +func (w *ServerInterfaceWrapper) PostFeedActionBegin(ctx echo.Context) error { + var err error + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostFeedActionBegin(ctx) + return err +} + // PostFeedDaemonPing converts echo context to params. func (w *ServerInterfaceWrapper) PostFeedDaemonPing(ctx echo.Context) error { var err error @@ -201,6 +217,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.GET(baseURL+"/docs/openapi", wrapper.GetSwagger) + router.POST(baseURL+"/feed/action", wrapper.PostFeedActionBegin) router.POST(baseURL+"/feed/daemon/ping", wrapper.PostFeedDaemonPing) router.POST(baseURL+"/feed/daemon/status", wrapper.PostFeedDaemonStatus) router.POST(baseURL+"/feed/instance/resource_info", wrapper.PostFeedInstanceResourceInfo) @@ -215,37 +232,39 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "KHRK53Sq+AUt78v/BgAA//89OreI0iMAAA==", + "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", + "x4Je4O399r8BAAD//zo8TfdvJwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/codegen_type_gen.go b/api/codegen_type_gen.go index 6453c5f..4d89ebc 100644 --- a/api/codegen_type_gen.go +++ b/api/codegen_type_gen.go @@ -12,6 +12,19 @@ const ( BearerAuthScopes = "bearerAuth.Scopes" ) +// ActionBegin defines model for ActionBegin. +type ActionBegin struct { + Action string `json:"action"` + Argv []string `json:"argv"` + Begin string `json:"begin"` + Cron bool `json:"cron"` + Path string `json:"path"` + SessionUuid string `json:"session_uuid"` + + // Version the opensvc client data version + Version string `json:"version"` +} + // Disk defines model for Disk. type Disk struct { Dg string `json:"dg"` @@ -161,6 +174,9 @@ type PostFeedInstanceStatusParams struct { Sync *InQuerySync `form:"sync,omitempty" json:"sync,omitempty"` } +// PostFeedActionBeginJSONRequestBody defines body for PostFeedActionBegin for application/json ContentType. +type PostFeedActionBeginJSONRequestBody = ActionBegin + // PostFeedDaemonPingJSONRequestBody defines body for PostFeedDaemonPing for application/json ContentType. type PostFeedDaemonPingJSONRequestBody = PostFeedDaemonPing diff --git a/apihandlers/post_feed_action_begin.go b/apihandlers/post_feed_action_begin.go new file mode 100644 index 0000000..9838fc2 --- /dev/null +++ b/apihandlers/post_feed_action_begin.go @@ -0,0 +1,81 @@ +package apihandlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/labstack/echo/v4" + + "github.com/opensvc/oc3/api" + "github.com/opensvc/oc3/cachekeys" +) + +// { +// "action": "thaw", +// "argv": [ +// "foo", +// "thaw", +// "--local" +// ], +// "begin": "2026-01-12 10:57:12", +// "cron": false, +// "path": "foo", +// "session_uuid": "b9d795bc-498e-4c20-aada-9feec2eaa947", +// "version": "2.1-1977" +// } + +// PostFeedActionBegin handles POST /action/begin +func (a *Api) PostFeedActionBegin(c echo.Context) error { + keyH := cachekeys.FeedActionBeginH + keyQ := cachekeys.FeedActionBeginQ + keyPendingH := cachekeys.FeedActionBeginPendingH + + log := getLog(c) + + nodeID := nodeIDFromContext(c) + if nodeID == "" { + log.Debug("node auth problem") + return JSONNodeAuthProblem(c) + } + + ClusterID := clusterIDFromContext(c) + if ClusterID == "" { + return JSONProblemf(c, http.StatusConflict, "Refused", "authenticated node doesn't define cluster id") + } + + var payload api.PostFeedActionBeginJSONRequestBody + if err := c.Bind(&payload); err != nil { + return JSONProblem(c, http.StatusBadRequest, "Failed to json decode request body", err.Error()) + } + + if !strings.HasPrefix(payload.Version, "2.") && !strings.HasPrefix(payload.Version, "3.") { + log.Error(fmt.Sprintf("unexpected version %s", payload.Version)) + return JSONProblemf(c, http.StatusBadRequest, "BadRequest", "unsupported data client version: %s", payload.Version) + } + + b, err := json.Marshal(payload) + if err != nil { + return JSONProblem(c, http.StatusInternalServerError, "Failed to re-encode config", err.Error()) + } + + reqCtx := c.Request().Context() + + idx := fmt.Sprintf("%s@%s@%s", payload.Path, nodeID, ClusterID) // todo : add action + + s := fmt.Sprintf("HSET %s %s", keyH, idx) + if _, err := a.Redis.HSet(reqCtx, keyH, idx, b).Result(); err != nil { + s = fmt.Sprintf("%s: %s", s, err) + log.Error(s) + return JSONProblem(c, http.StatusInternalServerError, "", s) + } + + if err := a.pushNotPending(reqCtx, keyPendingH, keyQ, idx); err != nil { + log.Error(fmt.Sprintf("can't push %s %s: %s", keyQ, idx, err)) + return JSONProblemf(c, http.StatusInternalServerError, "redis operation", "can't push %s %s: %s", keyQ, idx, err) + } + + log.Debug("action begin accepted") + return c.NoContent(http.StatusAccepted) +} diff --git a/cachekeys/main.go b/cachekeys/main.go index 897f709..3fa342c 100644 --- a/cachekeys/main.go +++ b/cachekeys/main.go @@ -34,4 +34,8 @@ const ( FeedInstanceStatusQ = "oc3:q:feed_instance_status" FeedInstanceStatusP = "oc3:p:feed_instance_status" FeedInstanceStatusPendingH = "oc3:h:feed_instance_status_pending" + + FeedActionBeginH = "oc3:h:feed_action_begin" + FeedActionBeginQ = "oc3:q:feed_action_begin" + FeedActionBeginPendingH = "oc3:h:feed_action_begin_pending" ) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index 1986bdf..dee4869 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "time" "github.com/google/uuid" ) @@ -209,3 +210,26 @@ func (oDb *DB) GetUnfinishedActions(ctx context.Context) (lines []SvcAction, err return } + +func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, action string, begin time.Time, status_log string, sid string) (int64, error) { + query := `INSERT INTO svcactions (svc_id, node_id, action, begin, status_log, sid) + VALUES (?, ?, ?, ?, ?, ?)` + + result, err := oDb.DB.ExecContext(ctx, query, svcID, nodeID, action, begin, status_log, sid) + if err != nil { + return 0, err + } + + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + + if rowsAffected, err := result.RowsAffected(); err != nil { + return id, err + } else if rowsAffected > 0 { + oDb.SetChange("svcactions") + } + + return id, nil +} diff --git a/worker/job_feed_action_begin.go b/worker/job_feed_action_begin.go new file mode 100644 index 0000000..b6aa67d --- /dev/null +++ b/worker/job_feed_action_begin.go @@ -0,0 +1,175 @@ +package worker + +import ( + "encoding/json" + "fmt" + "log/slog" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/opensvc/oc3/api" + "github.com/opensvc/oc3/cachekeys" + "github.com/opensvc/oc3/cdb" +) + +type jobFeedActionBegin struct { + *BaseJob + + nodeID string + clusterID string + node *cdb.DBNode + + // idX is the id of the posted action begin with the pattern: @ + idX string + + objectName string + objectID string + + // data is the posted action begin payload + data *api.PostFeedActionBeginJSONRequestBody + + rawData []byte // necessaire ? +} + +func newActionBegin(objectName, nodeID, clusterID string) *jobFeedActionBegin { + idX := fmt.Sprintf("%s@%s@%s", objectName, nodeID, clusterID) + return &jobFeedActionBegin{ + BaseJob: &BaseJob{ + name: "actionBegin", + detail: "ID: " + idX, + cachePendingH: cachekeys.FeedActionBeginPendingH, + cachePendingIDX: idX, + }, + idX: idX, + nodeID: nodeID, + clusterID: clusterID, + objectName: objectName, + } +} + +func (d *jobFeedActionBegin) Operations() []operation { + return []operation{ + {desc: "actionBegin/dropPending", do: d.dropPending}, + {desc: "actionBegin/findNodeFromDb", do: d.findNodeFromDb}, + {desc: "actionBegin/getData", do: d.getData}, + {desc: "actionBegin/findObjectFromDb", do: d.findObjectFromDb}, + {desc: "actionBegin/processAction", do: d.updateDB}, + {desc: "actionBegin/pushFromTableChanges", do: d.pushFromTableChanges}, + } +} + +func (d *jobFeedActionBegin) getData() error { + var ( + data api.PostFeedActionBeginJSONRequestBody + ) + if b, err := d.redis.HGet(d.ctx, cachekeys.FeedActionBeginH, d.idX).Bytes(); err != nil { + return fmt.Errorf("getData: HGET %s %s: %w", cachekeys.FeedActionBeginH, d.idX, err) + } else if err = json.Unmarshal(b, &data); err != nil { + return fmt.Errorf("getData: unexpected data from %s %s: %w", cachekeys.FeedActionBeginH, d.idX, err) + } else { + d.rawData = b + d.data = &data + } + + slog.Info(fmt.Sprintf("got action begin data for node %s:%#v", d.nodeID, d.data)) + return nil +} + +func (d *jobFeedActionBegin) findNodeFromDb() error { + if n, err := d.oDb.NodeByNodeID(d.ctx, d.nodeID); err != nil { + return fmt.Errorf("findNodeFromDb: node %s: %w", d.nodeID, err) + } else { + d.node = n + } + slog.Info(fmt.Sprintf("jobFeedActionBegin found node %s for id %s", d.node.Nodename, d.nodeID)) + return nil +} + +func (d *jobFeedActionBegin) findObjectFromDb() error { + if isNew, objId, err := d.oDb.ObjectIDFindOrCreate(d.ctx, d.objectName, d.clusterID); err != nil { + return fmt.Errorf("find or create object ID failed for %s: %w", d.objectName, err) + } else if isNew { + slog.Info(fmt.Sprintf("jobFeedActionBegin has created new object id %s@%s %s", d.objectName, d.clusterID, objId)) + } else { + d.objectID = objId + slog.Info(fmt.Sprintf("jobFeedActionBegin found object id %s@%s %s", d.objectName, d.clusterID, objId)) + } + + return nil +} + +func (d *jobFeedActionBegin) updateDB() error { + // Log the action begin for audit/tracking purposes + if d.data == nil || d.data.Path == "" { + return fmt.Errorf("invalid action data: missing path") + } + + // slog.Info(fmt.Sprintf("====> action begin on node %s: path=%s action=%s begin=%s node_id=%s object_id=%s", + // d.node.Nodename, + // d.data.Path, + // d.data.Action, + // d.data.Begin, + // d.nodeID, + // d.objectID, + // )) + + // slog.Info(fmt.Sprintf("Object ID : %s", d.objectID)) + objectUUID, err := uuid.Parse(d.objectID) + if err != nil { + return fmt.Errorf("invalid object ID UUID: %w", err) + } + nodeUUID, err := uuid.Parse(d.nodeID) + if err != nil { + return fmt.Errorf("invalid node ID UUID: %w", err) + } + beginTime, err := parseTimeWithTimezone(d.data.Begin, d.node.Tz) + if err != nil { + return fmt.Errorf("invalid begin time format: %w", err) + } + + status_log := "" + if len(d.data.Argv) > 0 { + status_log = fmt.Sprintf("%s", d.data.Argv[0]) + for i := 1; i < len(d.data.Argv); i++ { + status_log += " " + d.data.Argv[i] + } + } + + d.oDb.InsertSvcAction(d.ctx, objectUUID, nodeUUID, d.data.Action, beginTime, status_log, d.data.SessionUuid) + + return nil +} + +// Todo : to move elsewhere... +// Tz can be either a timezone name (ex: "Europe/Paris") or a UTC offset (ex: "+01:00") +func parseTimeWithTimezone(dateStr, tzStr string) (time.Time, error) { + const timeFormat = "2006-01-02 15:04:05" + + // Named Tz + if loc, err := time.LoadLocation(tzStr); err == nil { + return time.ParseInLocation(timeFormat, dateStr, loc) + } + + // Offset Tz + tzStr = strings.TrimSpace(tzStr) + if tzStr != "" && (tzStr[0] == '+' || tzStr[0] == '-') { + parts := strings.Split(tzStr[1:], ":") + if len(parts) >= 2 { + hours, errH := strconv.Atoi(parts[0]) + minutes, errM := strconv.Atoi(parts[1]) + if errH == nil && errM == nil { + offset := hours*3600 + minutes*60 + if tzStr[0] == '-' { + offset = -offset + } + loc := time.FixedZone(tzStr, offset) + return time.ParseInLocation(timeFormat, dateStr, loc) + } + } + } + + // Fallback + return time.Parse(timeFormat, dateStr) +} diff --git a/worker/worker.go b/worker/worker.go index 8b38349..e8b0cff 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -164,6 +164,15 @@ func (w *Worker) runJob(unqueuedJob []string) error { return err } j = newInstanceStatus(objectName, nodeID, clusterID) + case cachekeys.FeedActionBeginQ: + objectName, nodeID, ClusterID, err := w.jobToInstanceAndClusterID(unqueuedJob[1]) + if err != nil { + err := fmt.Errorf("invalid feed begin action index: %s", unqueuedJob[1]) + slog.Warn(err.Error()) + return err + } + j = newActionBegin(objectName, nodeID, ClusterID) + default: slog.Debug(fmt.Sprintf("ignore queue '%s'", unqueuedJob[0])) return nil From 19790ff9b6e9e94183a7a88caf328094e58d307e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Thu, 15 Jan 2026 10:27:24 +0100 Subject: [PATCH 03/22] Rename post_feed_action_begin.go to post_feed_action.go --- apihandlers/{post_feed_action_begin.go => post_feed_action.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apihandlers/{post_feed_action_begin.go => post_feed_action.go} (100%) diff --git a/apihandlers/post_feed_action_begin.go b/apihandlers/post_feed_action.go similarity index 100% rename from apihandlers/post_feed_action_begin.go rename to apihandlers/post_feed_action.go From f7f9def040b70a472100defb791c1e1454e6d70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Thu, 15 Jan 2026 10:36:46 +0100 Subject: [PATCH 04/22] Add PUT /feed/action endpoint --- api/api.yaml | 55 ++++++++++++++++++++++ api/codegen_server_gen.go | 84 +++++++++++++++++++++------------- api/codegen_type_gen.go | 15 ++++++ apihandlers/put_feed_action.go | 61 ++++++++++++++++++++++++ cachekeys/main.go | 4 ++ 5 files changed, 186 insertions(+), 33 deletions(-) create mode 100644 apihandlers/put_feed_action.go diff --git a/api/api.yaml b/api/api.yaml index bdbcb88..3470531 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -198,6 +198,32 @@ paths: - agent /feed/action: + put: + description: | + End an action for a given object path + operationId: PutFeedActionEnd + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ActionEnd' + responses: + 202: + description: action end accepted + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 500: + $ref: '#/components/responses/500' + security: + - basicAuth: [ ] + - bearerAuth: [ ] + tags: + - agent post: description: | Begin an action for a given object path @@ -349,6 +375,35 @@ components: items: type: string + ActionEnd: + type: object + required: + - path + - action + - begin + - end + - cron + - sid + - actionlogfile + - err + properties: + path: + type: string + action: + type: string + begin: + type: string + end: + type: string + cron: + type: boolean + sid: + type: string + actionlogfile: + type: string + err: + type: integer + NodeDisks: type: object properties: diff --git a/api/codegen_server_gen.go b/api/codegen_server_gen.go index 3610030..3b8f75b 100644 --- a/api/codegen_server_gen.go +++ b/api/codegen_server_gen.go @@ -27,6 +27,9 @@ type ServerInterface interface { // (POST /feed/action) PostFeedActionBegin(ctx echo.Context) error + // (PUT /feed/action) + PutFeedActionEnd(ctx echo.Context) error + // (POST /feed/daemon/ping) PostFeedDaemonPing(ctx echo.Context) error @@ -79,6 +82,19 @@ func (w *ServerInterfaceWrapper) PostFeedActionBegin(ctx echo.Context) error { return err } +// PutFeedActionEnd converts echo context to params. +func (w *ServerInterfaceWrapper) PutFeedActionEnd(ctx echo.Context) error { + var err error + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PutFeedActionEnd(ctx) + return err +} + // PostFeedDaemonPing converts echo context to params. func (w *ServerInterfaceWrapper) PostFeedDaemonPing(ctx echo.Context) error { var err error @@ -218,6 +234,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/docs/openapi", wrapper.GetSwagger) router.POST(baseURL+"/feed/action", wrapper.PostFeedActionBegin) + router.PUT(baseURL+"/feed/action", wrapper.PutFeedActionEnd) router.POST(baseURL+"/feed/daemon/ping", wrapper.PostFeedDaemonPing) router.POST(baseURL+"/feed/daemon/status", wrapper.PostFeedDaemonStatus) router.POST(baseURL+"/feed/instance/resource_info", wrapper.PostFeedInstanceResourceInfo) @@ -232,39 +249,40 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "x4Je4O399r8BAAD//zo8TfdvJwAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/codegen_type_gen.go b/api/codegen_type_gen.go index 4d89ebc..0cc2754 100644 --- a/api/codegen_type_gen.go +++ b/api/codegen_type_gen.go @@ -25,6 +25,18 @@ type ActionBegin struct { Version string `json:"version"` } +// ActionEnd defines model for ActionEnd. +type ActionEnd struct { + Action string `json:"action"` + Actionlogfile string `json:"actionlogfile"` + Begin string `json:"begin"` + Cron bool `json:"cron"` + End string `json:"end"` + Err int `json:"err"` + Path string `json:"path"` + Sid string `json:"sid"` +} + // Disk defines model for Disk. type Disk struct { Dg string `json:"dg"` @@ -177,6 +189,9 @@ type PostFeedInstanceStatusParams struct { // PostFeedActionBeginJSONRequestBody defines body for PostFeedActionBegin for application/json ContentType. type PostFeedActionBeginJSONRequestBody = ActionBegin +// PutFeedActionEndJSONRequestBody defines body for PutFeedActionEnd for application/json ContentType. +type PutFeedActionEndJSONRequestBody = ActionEnd + // PostFeedDaemonPingJSONRequestBody defines body for PostFeedDaemonPing for application/json ContentType. type PostFeedDaemonPingJSONRequestBody = PostFeedDaemonPing diff --git a/apihandlers/put_feed_action.go b/apihandlers/put_feed_action.go new file mode 100644 index 0000000..40d1d5c --- /dev/null +++ b/apihandlers/put_feed_action.go @@ -0,0 +1,61 @@ +package apihandlers + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + + "github.com/opensvc/oc3/api" + "github.com/opensvc/oc3/cachekeys" +) + +// PutFeedActionEnd handles PUT /feed/action +func (a *Api) PutFeedActionEnd(c echo.Context) error { + keyH := cachekeys.FeedActionEndH + keyQ := cachekeys.FeedActionEndQ + keyPendingH := cachekeys.FeedActionEndPendingH + + log := getLog(c) + + nodeID := nodeIDFromContext(c) + if nodeID == "" { + log.Debug("node auth problem") + return JSONNodeAuthProblem(c) + } + + ClusterID := clusterIDFromContext(c) + if ClusterID == "" { + return JSONProblemf(c, http.StatusConflict, "Refused", "authenticated node doesn't define cluster id") + } + + var payload api.PutFeedActionEndJSONRequestBody + if err := c.Bind(&payload); err != nil { + return JSONProblem(c, http.StatusBadRequest, "Failed to json decode request body", err.Error()) + } + + b, err := json.Marshal(payload) + if err != nil { + return JSONProblem(c, http.StatusInternalServerError, "Failed to re-encode config", err.Error()) + } + + reqCtx := c.Request().Context() + + idx := fmt.Sprintf("%s@%s@%s", payload.Path, nodeID, ClusterID) + + s := fmt.Sprintf("HSET %s %s", keyH, idx) + if _, err := a.Redis.HSet(reqCtx, keyH, idx, b).Result(); err != nil { + s = fmt.Sprintf("%s: %s", s, err) + log.Error(s) + return JSONProblem(c, http.StatusInternalServerError, "", s) + } + + if err := a.pushNotPending(reqCtx, keyPendingH, keyQ, idx); err != nil { + log.Error(fmt.Sprintf("can't push %s %s: %s", keyQ, idx, err)) + return JSONProblemf(c, http.StatusInternalServerError, "redis operation", "can't push %s %s: %s", keyQ, idx, err) + } + + log.Debug("action end accepted") + return c.NoContent(http.StatusAccepted) +} diff --git a/cachekeys/main.go b/cachekeys/main.go index 3fa342c..79209e8 100644 --- a/cachekeys/main.go +++ b/cachekeys/main.go @@ -38,4 +38,8 @@ const ( FeedActionBeginH = "oc3:h:feed_action_begin" FeedActionBeginQ = "oc3:q:feed_action_begin" FeedActionBeginPendingH = "oc3:h:feed_action_begin_pending" + + FeedActionEndH = "oc3:h:feed_action_end" + FeedActionEndQ = "oc3:q:feed_action_end" + FeedActionEndPendingH = "oc3:h:feed_action_end_pending" ) From 26290ddcd03793bc3b6c35cbccb5de60bb6c0e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Thu, 15 Jan 2026 15:19:27 +0100 Subject: [PATCH 05/22] Add worker for end_action --- api/api.yaml | 6 +- api/codegen_server_gen.go | 68 +++++++++--------- api/codegen_type_gen.go | 2 +- cdb/db_actions.go | 25 +++++++ cdb/util.go | 34 +++++++++ worker/job_feed_action_begin.go | 37 +--------- worker/job_feed_action_end.go | 124 ++++++++++++++++++++++++++++++++ worker/worker.go | 8 +++ 8 files changed, 230 insertions(+), 74 deletions(-) create mode 100644 worker/job_feed_action_end.go diff --git a/api/api.yaml b/api/api.yaml index 3470531..4c9a387 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -385,7 +385,7 @@ components: - cron - sid - actionlogfile - - err + - status properties: path: type: string @@ -401,8 +401,8 @@ components: type: string actionlogfile: type: string - err: - type: integer + status: + type: string NodeDisks: type: object diff --git a/api/codegen_server_gen.go b/api/codegen_server_gen.go index 3b8f75b..15047fa 100644 --- a/api/codegen_server_gen.go +++ b/api/codegen_server_gen.go @@ -249,40 +249,40 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/codegen_type_gen.go b/api/codegen_type_gen.go index 0cc2754..334c0e7 100644 --- a/api/codegen_type_gen.go +++ b/api/codegen_type_gen.go @@ -32,9 +32,9 @@ type ActionEnd struct { Begin string `json:"begin"` Cron bool `json:"cron"` End string `json:"end"` - Err int `json:"err"` Path string `json:"path"` Sid string `json:"sid"` + Status string `json:"status"` } // Disk defines model for Disk. diff --git a/cdb/db_actions.go b/cdb/db_actions.go index dee4869..97a2fbf 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -233,3 +233,28 @@ func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, act return id, nil } + +func (oDb *DB) EndSvcAction(ctx context.Context, svcActionID int64, end time.Time, status string) error { + const query = `UPDATE svcactions SET end = ?, status = ?, time = TIMESTAMPDIFF(SECOND, begin, ?) WHERE id = ?` + result, err := oDb.DB.ExecContext(ctx, query, end, status, end, svcActionID) + if err != nil { + return err + } + if rowsAffected, err := result.RowsAffected(); err != nil { + return err + } else if rowsAffected > 0 { + oDb.SetChange("svcactions") + } + return nil +} + +// FindActionID finds the action ID for the given parameters. +func (oDb *DB) FindActionID(ctx context.Context, nodeID string, svcID string, begin time.Time) (int64, error) { + // todo : check if there is only one result + const query = "SELECT id FROM svcactions WHERE node_id = ? AND svc_id = ? AND begin = ? AND pid IS NULL" + var id int64 + if err := oDb.DB.QueryRowContext(ctx, query, nodeID, svcID, begin).Scan(&id); err != nil { + return 0, err + } + return id, nil +} diff --git a/cdb/util.go b/cdb/util.go index 9038bcf..9de5ba7 100644 --- a/cdb/util.go +++ b/cdb/util.go @@ -3,6 +3,8 @@ package cdb import ( "fmt" "log/slog" + "strconv" + "strings" "time" ) @@ -13,3 +15,35 @@ func logDuration(s string, begin time.Time) { func logDurationInfo(s string, begin time.Time) { slog.Info(fmt.Sprintf("STAT: %s elapse: %s", s, time.Since(begin))) } + +// Todo : to move elsewhere... +// Tz can be either a timezone name (ex: "Europe/Paris") or a UTC offset (ex: "+01:00") +func ParseTimeWithTimezone(dateStr, tzStr string) (time.Time, error) { + const timeFormat = "2006-01-02 15:04:05" + + // Named Tz + if loc, err := time.LoadLocation(tzStr); err == nil { + return time.ParseInLocation(timeFormat, dateStr, loc) + } + + // Offset Tz + tzStr = strings.TrimSpace(tzStr) + if tzStr != "" && (tzStr[0] == '+' || tzStr[0] == '-') { + parts := strings.Split(tzStr[1:], ":") + if len(parts) >= 2 { + hours, errH := strconv.Atoi(parts[0]) + minutes, errM := strconv.Atoi(parts[1]) + if errH == nil && errM == nil { + offset := hours*3600 + minutes*60 + if tzStr[0] == '-' { + offset = -offset + } + loc := time.FixedZone(tzStr, offset) + return time.ParseInLocation(timeFormat, dateStr, loc) + } + } + } + + // Fallback + return time.Parse(timeFormat, dateStr) +} diff --git a/worker/job_feed_action_begin.go b/worker/job_feed_action_begin.go index b6aa67d..173276b 100644 --- a/worker/job_feed_action_begin.go +++ b/worker/job_feed_action_begin.go @@ -4,9 +4,6 @@ import ( "encoding/json" "fmt" "log/slog" - "strconv" - "strings" - "time" "github.com/google/uuid" "github.com/opensvc/oc3/api" @@ -124,7 +121,7 @@ func (d *jobFeedActionBegin) updateDB() error { if err != nil { return fmt.Errorf("invalid node ID UUID: %w", err) } - beginTime, err := parseTimeWithTimezone(d.data.Begin, d.node.Tz) + beginTime, err := cdb.ParseTimeWithTimezone(d.data.Begin, d.node.Tz) if err != nil { return fmt.Errorf("invalid begin time format: %w", err) } @@ -141,35 +138,3 @@ func (d *jobFeedActionBegin) updateDB() error { return nil } - -// Todo : to move elsewhere... -// Tz can be either a timezone name (ex: "Europe/Paris") or a UTC offset (ex: "+01:00") -func parseTimeWithTimezone(dateStr, tzStr string) (time.Time, error) { - const timeFormat = "2006-01-02 15:04:05" - - // Named Tz - if loc, err := time.LoadLocation(tzStr); err == nil { - return time.ParseInLocation(timeFormat, dateStr, loc) - } - - // Offset Tz - tzStr = strings.TrimSpace(tzStr) - if tzStr != "" && (tzStr[0] == '+' || tzStr[0] == '-') { - parts := strings.Split(tzStr[1:], ":") - if len(parts) >= 2 { - hours, errH := strconv.Atoi(parts[0]) - minutes, errM := strconv.Atoi(parts[1]) - if errH == nil && errM == nil { - offset := hours*3600 + minutes*60 - if tzStr[0] == '-' { - offset = -offset - } - loc := time.FixedZone(tzStr, offset) - return time.ParseInLocation(timeFormat, dateStr, loc) - } - } - } - - // Fallback - return time.Parse(timeFormat, dateStr) -} diff --git a/worker/job_feed_action_end.go b/worker/job_feed_action_end.go new file mode 100644 index 0000000..735defd --- /dev/null +++ b/worker/job_feed_action_end.go @@ -0,0 +1,124 @@ +package worker + +import ( + "encoding/json" + "fmt" + "log/slog" + + "github.com/opensvc/oc3/api" + "github.com/opensvc/oc3/cachekeys" + "github.com/opensvc/oc3/cdb" +) + +type jobFeedActionEnd struct { + *BaseJob + + nodeID string + clusterID string + node *cdb.DBNode + + // idX is the id of the posted action end with the pattern: @ + idX string + + objectName string + objectID string + + // data is the posted action end payload + data *api.PutFeedActionEndJSONRequestBody + + rawData []byte +} + +func newActionEnd(objectName, nodeID, clusterID string) *jobFeedActionEnd { + idX := fmt.Sprintf("%s@%s@%s", objectName, nodeID, clusterID) + return &jobFeedActionEnd{ + BaseJob: &BaseJob{ + name: "actionEnd", + detail: "ID: " + idX, + cachePendingH: cachekeys.FeedActionEndPendingH, + cachePendingIDX: idX, + }, + idX: idX, + nodeID: nodeID, + clusterID: clusterID, + objectName: objectName, + } +} + +func (d *jobFeedActionEnd) Operations() []operation { + return []operation{ + {desc: "actionEnd/dropPending", do: d.dropPending}, + {desc: "actionEnd/findNodeFromDb", do: d.findNodeFromDb}, + {desc: "actionEnd/getData", do: d.getData}, + {desc: "actionEnd/findObjectFromDb", do: d.findObjectFromDb}, + {desc: "actionEnd/processAction", do: d.updateDb}, + {desc: "actionEnd/pushFromTableChanges", do: d.pushFromTableChanges}, + } +} + +func (d *jobFeedActionEnd) getData() error { + var ( + data api.PutFeedActionEndJSONRequestBody + ) + if b, err := d.redis.HGet(d.ctx, cachekeys.FeedActionEndH, d.idX).Bytes(); err != nil { + return fmt.Errorf("getData: HGET %s %s: %w", cachekeys.FeedActionEndH, d.idX, err) + } else if err = json.Unmarshal(b, &data); err != nil { + return fmt.Errorf("getData: unexpected data from %s %s: %w", cachekeys.FeedActionEndH, d.idX, err) + } else { + d.rawData = b + d.data = &data + } + + slog.Info(fmt.Sprintf("got action end data for node %s:%#v", d.nodeID, d.data)) + return nil +} + +func (d *jobFeedActionEnd) findNodeFromDb() error { + if n, err := d.oDb.NodeByNodeID(d.ctx, d.nodeID); err != nil { + return fmt.Errorf("findNodeFromDb: node %s: %w", d.nodeID, err) + } else { + d.node = n + } + slog.Info(fmt.Sprintf("jobFeedActionEnd found node %s for id %s", d.node.Nodename, d.nodeID)) + return nil +} + +func (d *jobFeedActionEnd) findObjectFromDb() error { + if isNew, objId, err := d.oDb.ObjectIDFindOrCreate(d.ctx, d.objectName, d.clusterID); err != nil { + return fmt.Errorf("find or create object ID failed for %s: %w", d.objectName, err) + } else if isNew { + slog.Info(fmt.Sprintf("jobFeedActionEnd has created new object id %s@%s %s", d.objectName, d.clusterID, objId)) + } else { + d.objectID = objId + slog.Info(fmt.Sprintf("jobFeedActionEnd found object id %s@%s %s", d.objectName, d.clusterID, objId)) + } + + return nil +} + +func (d *jobFeedActionEnd) updateDb() error { + if d.data == nil || d.data.Path == "" { + return fmt.Errorf("invalid action data: missing path") + } + + beginTime, err := cdb.ParseTimeWithTimezone(d.data.Begin, d.node.Tz) + if err != nil { + return fmt.Errorf("invalid begin time format: %w", err) + } + + endTime, err := cdb.ParseTimeWithTimezone(d.data.End, d.node.Tz) + if err != nil { + return fmt.Errorf("invalid end time format: %w", err) + } + + actionId, err := d.oDb.FindActionID(d.ctx, d.nodeID, d.objectID, beginTime) + if err != nil { + return fmt.Errorf("find action ID failed: %w", err) + } + + if err := d.oDb.EndSvcAction(d.ctx, actionId, endTime, d.data.Status); err != nil { + return fmt.Errorf("end svc action failed: %w", err) + } + + return nil +} diff --git a/worker/worker.go b/worker/worker.go index e8b0cff..39ca091 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -172,6 +172,14 @@ func (w *Worker) runJob(unqueuedJob []string) error { return err } j = newActionBegin(objectName, nodeID, ClusterID) + case cachekeys.FeedActionEndQ: + objectName, nodeID, ClusterID, err := w.jobToInstanceAndClusterID(unqueuedJob[1]) + if err != nil { + err := fmt.Errorf("invalid feed end action index: %s", unqueuedJob[1]) + slog.Warn(err.Error()) + return err + } + j = newActionEnd(objectName, nodeID, ClusterID) default: slog.Debug(fmt.Sprintf("ignore queue '%s'", unqueuedJob[0])) From 73491e18bab4e360f7334a89e01f73b9f06caeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Thu, 15 Jan 2026 18:39:57 +0100 Subject: [PATCH 06/22] Update FindActionID to include action parameter in query --- cdb/db_actions.go | 6 +++--- worker/job_feed_action_end.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index 97a2fbf..3b83028 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -249,11 +249,11 @@ func (oDb *DB) EndSvcAction(ctx context.Context, svcActionID int64, end time.Tim } // FindActionID finds the action ID for the given parameters. -func (oDb *DB) FindActionID(ctx context.Context, nodeID string, svcID string, begin time.Time) (int64, error) { +func (oDb *DB) FindActionID(ctx context.Context, nodeID string, svcID string, begin time.Time, action string) (int64, error) { // todo : check if there is only one result - const query = "SELECT id FROM svcactions WHERE node_id = ? AND svc_id = ? AND begin = ? AND pid IS NULL" + const query = "SELECT id FROM svcactions WHERE node_id = ? AND svc_id = ? AND begin = ? AND action = ? AND pid IS NULL" var id int64 - if err := oDb.DB.QueryRowContext(ctx, query, nodeID, svcID, begin).Scan(&id); err != nil { + if err := oDb.DB.QueryRowContext(ctx, query, nodeID, svcID, begin, action).Scan(&id); err != nil { return 0, err } return id, nil diff --git a/worker/job_feed_action_end.go b/worker/job_feed_action_end.go index 735defd..6434c50 100644 --- a/worker/job_feed_action_end.go +++ b/worker/job_feed_action_end.go @@ -111,7 +111,7 @@ func (d *jobFeedActionEnd) updateDb() error { return fmt.Errorf("invalid end time format: %w", err) } - actionId, err := d.oDb.FindActionID(d.ctx, d.nodeID, d.objectID, beginTime) + actionId, err := d.oDb.FindActionID(d.ctx, d.nodeID, d.objectID, beginTime, d.data.Action) if err != nil { return fmt.Errorf("find action ID failed: %w", err) } From 8f98266c88ea4401742fe660d6cf1dc8c6a9a3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Thu, 15 Jan 2026 18:42:15 +0100 Subject: [PATCH 07/22] Add UpdateActionErrors and UpdateDashActionErrors functions --- cdb/db_actions.go | 98 +++++++++++++++++++++++++++++++++++ worker/job_feed_action_end.go | 9 ++++ 2 files changed, 107 insertions(+) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index 3b83028..7c0aaf5 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -258,3 +258,101 @@ func (oDb *DB) FindActionID(ctx context.Context, nodeID string, svcID string, be } return id, nil } + +// UpdateActionErrors updates the action errors in the database. +func (oDb *DB) UpdateActionErrors(ctx context.Context, svcID, nodeID string) error { + const queryCount = `SELECT count(id) FROM svcactions a + WHERE + a.svc_id = ? AND + a.node_id = ? AND + a.status = "err" AND + ((a.ack <> 1) OR (a.ack IS NULL)) AND + a.begin > DATE_SUB(NOW(), INTERVAL 2 DAY)` + + var errCount int + if err := oDb.DB.QueryRowContext(ctx, queryCount, svcID, nodeID).Scan(&errCount); err != nil { + return fmt.Errorf("UpdateActionErrors: count failed: %w", err) + } + + if errCount == 0 { + const queryDelete = `DELETE FROM b_action_errors + WHERE + svc_id = ? AND + node_id = ?` + if _, err := oDb.DB.ExecContext(ctx, queryDelete, svcID, nodeID); err != nil { + return fmt.Errorf("UpdateActionErrors: delete failed: %w", err) + } + } else { + const queryInsert = `INSERT INTO b_action_errors + SET + svc_id= ?, + node_id= ?, + err= ? + ON DUPLICATE KEY UPDATE + err= ?` + if _, err := oDb.DB.ExecContext(ctx, queryInsert, svcID, nodeID, errCount, errCount); err != nil { + return fmt.Errorf("UpdateActionErrors: insert/update failed: %w", err) + } + } + return nil +} + +// UpdateDashActionErrors updates the dashboard with action errors. +func (oDb *DB) UpdateDashActionErrors(ctx context.Context, svcID, nodeID string) error { + const query = `SELECT e.err, s.svc_env FROM b_action_errors e + JOIN services s ON e.svc_id=s.svc_id + WHERE + e.svc_id = ? AND + e.node_id = ?` + + var errCount int + var svcEnv string + var err error + + err = oDb.DB.QueryRowContext(ctx, query, svcID, nodeID).Scan(&errCount, &svcEnv) + if err != nil && err != sql.ErrNoRows { + return fmt.Errorf("UpdateDashActionErrors: select failed: %w", err) + } + + if err == nil { + sev := 3 + if svcEnv == "PRD" { + sev = 4 + } + const queryInsert = `INSERT INTO dashboard + SET + dash_type="action errors", + svc_id = ?, + node_id = ?, + dash_severity = ?, + dash_fmt = "%(err)s action errors", + dash_dict = ?, + dash_created = NOW(), + dash_updated = NOW(), + dash_env = ? + ON DUPLICATE KEY UPDATE + dash_severity = ?, + dash_fmt = "%(err)s action errors", + dash_dict = ?, + dash_updated = NOW(), + dash_env = ?` + + dashDict := fmt.Sprintf(`{"err": "%d"}`, errCount) + + if _, err := oDb.DB.ExecContext(ctx, queryInsert, svcID, nodeID, sev, dashDict, svcEnv, sev, dashDict, svcEnv); err != nil { + return fmt.Errorf("UpdateDashActionErrors: insert/update failed: %w", err) + } + // TODO: WebSocket notification + } else { + // TODO: WebSocket notification + const queryDelete = `DELETE FROM dashboard + WHERE + dash_type="action errors" AND + svc_id = ? AND + node_id = ?` + if _, err := oDb.DB.ExecContext(ctx, queryDelete, svcID, nodeID); err != nil { + return fmt.Errorf("UpdateDashActionErrors: delete failed: %w", err) + } + } + return nil +} diff --git a/worker/job_feed_action_end.go b/worker/job_feed_action_end.go index 6434c50..19f04e3 100644 --- a/worker/job_feed_action_end.go +++ b/worker/job_feed_action_end.go @@ -120,5 +120,14 @@ func (d *jobFeedActionEnd) updateDb() error { return fmt.Errorf("end svc action failed: %w", err) } + if d.data.Status == "err" { + if err := d.oDb.UpdateActionErrors(d.ctx, d.objectID, d.nodeID); err != nil { + return fmt.Errorf("update action errors failed: %w", err) + } + if err := d.oDb.UpdateDashActionErrors(d.ctx, d.objectID, d.nodeID); err != nil { + return fmt.Errorf("update dash action errors failed: %w", err) + } + } + return nil } From 34e7d1d4a4c72be924f1463e3a257e372067259c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Fri, 16 Jan 2026 09:37:59 +0100 Subject: [PATCH 08/22] Fix parameter in UpdateActionErrors function --- cdb/db_actions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index 7c0aaf5..e8dc7ab 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -260,7 +260,7 @@ func (oDb *DB) FindActionID(ctx context.Context, nodeID string, svcID string, be } // UpdateActionErrors updates the action errors in the database. -func (oDb *DB) UpdateActionErrors(ctx context.Context, svcID, nodeID string) error { +func (oDb *DB) UpdateActionErrors(ctx context.Context, svcID string, nodeID string) error { const queryCount = `SELECT count(id) FROM svcactions a WHERE a.svc_id = ? AND From 17607aef61e9fe5132420909d1e0f08b9b721073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Fri, 16 Jan 2026 09:54:17 +0100 Subject: [PATCH 09/22] Fix type for svcEnv (sql.NullString) --- cdb/db_actions.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index e8dc7ab..e5aea89 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -298,7 +298,7 @@ func (oDb *DB) UpdateActionErrors(ctx context.Context, svcID string, nodeID stri } // UpdateDashActionErrors updates the dashboard with action errors. -func (oDb *DB) UpdateDashActionErrors(ctx context.Context, svcID, nodeID string) error { +func (oDb *DB) UpdateDashActionErrors(ctx context.Context, svcID string, nodeID string) error { const query = `SELECT e.err, s.svc_env FROM b_action_errors e JOIN services s ON e.svc_id=s.svc_id WHERE @@ -306,7 +306,7 @@ func (oDb *DB) UpdateDashActionErrors(ctx context.Context, svcID, nodeID string) e.node_id = ?` var errCount int - var svcEnv string + var svcEnv sql.NullString var err error err = oDb.DB.QueryRowContext(ctx, query, svcID, nodeID).Scan(&errCount, &svcEnv) @@ -316,7 +316,7 @@ func (oDb *DB) UpdateDashActionErrors(ctx context.Context, svcID, nodeID string) if err == nil { sev := 3 - if svcEnv == "PRD" { + if svcEnv.Valid && svcEnv.String == "PRD" { sev = 4 } const queryInsert = `INSERT INTO dashboard From 1ac4aace402444269d3399b2afbfce7fed574eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Fri, 16 Jan 2026 10:37:42 +0100 Subject: [PATCH 10/22] Add "action" in index --- apihandlers/post_feed_action.go | 2 +- apihandlers/put_feed_action.go | 2 +- worker/job_feed_action_begin.go | 6 +++--- worker/job_feed_action_end.go | 6 +++--- worker/worker.go | 23 +++++++++++++++++++---- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apihandlers/post_feed_action.go b/apihandlers/post_feed_action.go index 9838fc2..d7c1f17 100644 --- a/apihandlers/post_feed_action.go +++ b/apihandlers/post_feed_action.go @@ -62,7 +62,7 @@ func (a *Api) PostFeedActionBegin(c echo.Context) error { reqCtx := c.Request().Context() - idx := fmt.Sprintf("%s@%s@%s", payload.Path, nodeID, ClusterID) // todo : add action + idx := fmt.Sprintf("%s@%s@%s:%s", payload.Path, nodeID, ClusterID, payload.Action) s := fmt.Sprintf("HSET %s %s", keyH, idx) if _, err := a.Redis.HSet(reqCtx, keyH, idx, b).Result(); err != nil { diff --git a/apihandlers/put_feed_action.go b/apihandlers/put_feed_action.go index 40d1d5c..4003264 100644 --- a/apihandlers/put_feed_action.go +++ b/apihandlers/put_feed_action.go @@ -42,7 +42,7 @@ func (a *Api) PutFeedActionEnd(c echo.Context) error { reqCtx := c.Request().Context() - idx := fmt.Sprintf("%s@%s@%s", payload.Path, nodeID, ClusterID) + idx := fmt.Sprintf("%s@%s@%s:%s", payload.Path, nodeID, ClusterID, payload.Action) s := fmt.Sprintf("HSET %s %s", keyH, idx) if _, err := a.Redis.HSet(reqCtx, keyH, idx, b).Result(); err != nil { diff --git a/worker/job_feed_action_begin.go b/worker/job_feed_action_begin.go index 173276b..3c5cf03 100644 --- a/worker/job_feed_action_begin.go +++ b/worker/job_feed_action_begin.go @@ -18,7 +18,7 @@ type jobFeedActionBegin struct { clusterID string node *cdb.DBNode - // idX is the id of the posted action begin with the pattern: @ + // idX is the id of the posted action begin with the pattern: @@: idX string objectName string @@ -30,8 +30,8 @@ type jobFeedActionBegin struct { rawData []byte // necessaire ? } -func newActionBegin(objectName, nodeID, clusterID string) *jobFeedActionBegin { - idX := fmt.Sprintf("%s@%s@%s", objectName, nodeID, clusterID) +func newActionBegin(objectName, nodeID, clusterID, action string) *jobFeedActionBegin { + idX := fmt.Sprintf("%s@%s@%s:%s", objectName, nodeID, clusterID, action) return &jobFeedActionBegin{ BaseJob: &BaseJob{ name: "actionBegin", diff --git a/worker/job_feed_action_end.go b/worker/job_feed_action_end.go index 19f04e3..6faef90 100644 --- a/worker/job_feed_action_end.go +++ b/worker/job_feed_action_end.go @@ -17,7 +17,7 @@ type jobFeedActionEnd struct { clusterID string node *cdb.DBNode - // idX is the id of the posted action end with the pattern: @ + // idX is the id of the posted action end with the pattern: @@: idX string objectName string @@ -29,8 +29,8 @@ type jobFeedActionEnd struct { rawData []byte } -func newActionEnd(objectName, nodeID, clusterID string) *jobFeedActionEnd { - idX := fmt.Sprintf("%s@%s@%s", objectName, nodeID, clusterID) +func newActionEnd(objectName, nodeID, clusterID, action string) *jobFeedActionEnd { + idX := fmt.Sprintf("%s@%s@%s:%s", objectName, nodeID, clusterID, action) return &jobFeedActionEnd{ BaseJob: &BaseJob{ name: "actionEnd", diff --git a/worker/worker.go b/worker/worker.go index 39ca091..46637f8 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -164,22 +164,23 @@ func (w *Worker) runJob(unqueuedJob []string) error { return err } j = newInstanceStatus(objectName, nodeID, clusterID) + case cachekeys.FeedActionBeginQ: - objectName, nodeID, ClusterID, err := w.jobToInstanceAndClusterID(unqueuedJob[1]) + objectName, nodeID, ClusterID, action, err := w.jobToInstanceClusterIdAndAction(unqueuedJob[1]) if err != nil { err := fmt.Errorf("invalid feed begin action index: %s", unqueuedJob[1]) slog.Warn(err.Error()) return err } - j = newActionBegin(objectName, nodeID, ClusterID) + j = newActionBegin(objectName, nodeID, ClusterID, action) case cachekeys.FeedActionEndQ: - objectName, nodeID, ClusterID, err := w.jobToInstanceAndClusterID(unqueuedJob[1]) + objectName, nodeID, ClusterID, action, err := w.jobToInstanceClusterIdAndAction(unqueuedJob[1]) if err != nil { err := fmt.Errorf("invalid feed end action index: %s", unqueuedJob[1]) slog.Warn(err.Error()) return err } - j = newActionEnd(objectName, nodeID, ClusterID) + j = newActionEnd(objectName, nodeID, ClusterID, action) default: slog.Debug(fmt.Sprintf("ignore queue '%s'", unqueuedJob[0])) @@ -227,3 +228,17 @@ func (w *Worker) jobToInstanceAndClusterID(jobName string) (path, nodeID, cluste } return } + +func (w *Worker) jobToInstanceClusterIdAndAction(jobName string) (path, nodeID, clusterID, action string, err error) { + l := strings.Split(jobName, ":") + if len(l) != 2 { + err = fmt.Errorf("unexpected job name: %s", jobName) + return + } + if path, nodeID, clusterID, err = w.jobToInstanceAndClusterID(l[0]); err != nil { + err = fmt.Errorf("unexpected job name: %s", jobName) + return + } + action = l[1] + return +} From 08a114a6be7b957ee064c5cc3573cc3de9b99e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Fri, 16 Jan 2026 10:51:53 +0100 Subject: [PATCH 11/22] Add cron parameter to InsertSvcAction --- cdb/db_actions.go | 8 ++++---- worker/job_feed_action_begin.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index e5aea89..3a6fdc8 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -211,11 +211,11 @@ func (oDb *DB) GetUnfinishedActions(ctx context.Context) (lines []SvcAction, err } -func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, action string, begin time.Time, status_log string, sid string) (int64, error) { - query := `INSERT INTO svcactions (svc_id, node_id, action, begin, status_log, sid) - VALUES (?, ?, ?, ?, ?, ?)` +func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, action string, begin time.Time, status_log string, sid string, cron bool) (int64, error) { + query := `INSERT INTO svcactions (svc_id, node_id, action, begin, status_log, sid, cron) + VALUES (?, ?, ?, ?, ?, ?, ?)` - result, err := oDb.DB.ExecContext(ctx, query, svcID, nodeID, action, begin, status_log, sid) + result, err := oDb.DB.ExecContext(ctx, query, svcID, nodeID, action, begin, status_log, sid, cron) if err != nil { return 0, err } diff --git a/worker/job_feed_action_begin.go b/worker/job_feed_action_begin.go index 3c5cf03..207d2a0 100644 --- a/worker/job_feed_action_begin.go +++ b/worker/job_feed_action_begin.go @@ -134,7 +134,7 @@ func (d *jobFeedActionBegin) updateDB() error { } } - d.oDb.InsertSvcAction(d.ctx, objectUUID, nodeUUID, d.data.Action, beginTime, status_log, d.data.SessionUuid) + d.oDb.InsertSvcAction(d.ctx, objectUUID, nodeUUID, d.data.Action, beginTime, status_log, d.data.SessionUuid, d.data.Cron) return nil } From c953ccf25946f4e40af8a342b79ff63c37028dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Fri, 16 Jan 2026 15:30:57 +0100 Subject: [PATCH 12/22] Refactor /feed/action handling - Use a single queue for both begin and end actions - Update the queue if the begin action has not been processed yet, to avoid having both begin and end actions --- api/api.yaml | 47 +++---- api/codegen_server_gen.go | 78 +++++----- api/codegen_type_gen.go | 39 +++-- apihandlers/post_feed_action.go | 18 +-- apihandlers/put_feed_action.go | 36 ++++- cachekeys/main.go | 10 +- cdb/db_actions.go | 23 ++- ...eed_action_begin.go => job_feed_action.go} | 91 ++++++++---- worker/job_feed_action_end.go | 133 ------------------ worker/worker.go | 18 +-- 10 files changed, 206 insertions(+), 287 deletions(-) rename worker/{job_feed_action_begin.go => job_feed_action.go} (54%) delete mode 100644 worker/job_feed_action_end.go diff --git a/api/api.yaml b/api/api.yaml index 4c9a387..842964f 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -207,7 +207,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ActionEnd' + $ref: '#/components/schemas/Action' responses: 202: description: action end accepted @@ -227,16 +227,20 @@ paths: post: description: | Begin an action for a given object path - operationId: PostFeedActionBegin + operationId: PostFeedAction requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/ActionBegin' + $ref: '#/components/schemas/Action' responses: 202: description: action begin accepted + content: + application/json: + schema: + $ref: '#/components/schemas/ActionRequestAccepted' 400: $ref: '#/components/responses/400' 401: @@ -346,7 +350,15 @@ components: type: string description: the opensvc client data version - ActionBegin: + ActionRequestAccepted: + type: object + required: + - uuid + properties: + uuid: + type: string + + Action: type: object required: - path @@ -356,6 +368,10 @@ components: - cron - session_uuid - argv + - uuid + - end + - actionlogfile + - status properties: path: type: string @@ -374,31 +390,10 @@ components: type: array items: type: string - - ActionEnd: - type: object - required: - - path - - action - - begin - - end - - cron - - sid - - actionlogfile - - status - properties: - path: - type: string - action: - type: string - begin: + uuid: type: string end: type: string - cron: - type: boolean - sid: - type: string actionlogfile: type: string status: diff --git a/api/codegen_server_gen.go b/api/codegen_server_gen.go index 15047fa..fbbc68a 100644 --- a/api/codegen_server_gen.go +++ b/api/codegen_server_gen.go @@ -25,7 +25,7 @@ type ServerInterface interface { GetSwagger(ctx echo.Context) error // (POST /feed/action) - PostFeedActionBegin(ctx echo.Context) error + PostFeedAction(ctx echo.Context) error // (PUT /feed/action) PutFeedActionEnd(ctx echo.Context) error @@ -69,8 +69,8 @@ func (w *ServerInterfaceWrapper) GetSwagger(ctx echo.Context) error { return err } -// PostFeedActionBegin converts echo context to params. -func (w *ServerInterfaceWrapper) PostFeedActionBegin(ctx echo.Context) error { +// PostFeedAction converts echo context to params. +func (w *ServerInterfaceWrapper) PostFeedAction(ctx echo.Context) error { var err error ctx.Set(BasicAuthScopes, []string{}) @@ -78,7 +78,7 @@ func (w *ServerInterfaceWrapper) PostFeedActionBegin(ctx echo.Context) error { ctx.Set(BearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.PostFeedActionBegin(ctx) + err = w.Handler.PostFeedAction(ctx) return err } @@ -233,7 +233,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.GET(baseURL+"/docs/openapi", wrapper.GetSwagger) - router.POST(baseURL+"/feed/action", wrapper.PostFeedActionBegin) + router.POST(baseURL+"/feed/action", wrapper.PostFeedAction) router.PUT(baseURL+"/feed/action", wrapper.PutFeedActionEnd) router.POST(baseURL+"/feed/daemon/ping", wrapper.PostFeedDaemonPing) router.POST(baseURL+"/feed/daemon/status", wrapper.PostFeedDaemonStatus) @@ -249,40 +249,40 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/codegen_type_gen.go b/api/codegen_type_gen.go index 334c0e7..98a2636 100644 --- a/api/codegen_type_gen.go +++ b/api/codegen_type_gen.go @@ -12,29 +12,26 @@ const ( BearerAuthScopes = "bearerAuth.Scopes" ) -// ActionBegin defines model for ActionBegin. -type ActionBegin struct { - Action string `json:"action"` - Argv []string `json:"argv"` - Begin string `json:"begin"` - Cron bool `json:"cron"` - Path string `json:"path"` - SessionUuid string `json:"session_uuid"` +// Action defines model for Action. +type Action struct { + Action string `json:"action"` + Actionlogfile string `json:"actionlogfile"` + Argv []string `json:"argv"` + Begin string `json:"begin"` + Cron bool `json:"cron"` + End string `json:"end"` + Path string `json:"path"` + SessionUuid string `json:"session_uuid"` + Status string `json:"status"` + Uuid string `json:"uuid"` // Version the opensvc client data version Version string `json:"version"` } -// ActionEnd defines model for ActionEnd. -type ActionEnd struct { - Action string `json:"action"` - Actionlogfile string `json:"actionlogfile"` - Begin string `json:"begin"` - Cron bool `json:"cron"` - End string `json:"end"` - Path string `json:"path"` - Sid string `json:"sid"` - Status string `json:"status"` +// ActionRequestAccepted defines model for ActionRequestAccepted. +type ActionRequestAccepted struct { + Uuid string `json:"uuid"` } // Disk defines model for Disk. @@ -186,11 +183,11 @@ type PostFeedInstanceStatusParams struct { Sync *InQuerySync `form:"sync,omitempty" json:"sync,omitempty"` } -// PostFeedActionBeginJSONRequestBody defines body for PostFeedActionBegin for application/json ContentType. -type PostFeedActionBeginJSONRequestBody = ActionBegin +// PostFeedActionJSONRequestBody defines body for PostFeedAction for application/json ContentType. +type PostFeedActionJSONRequestBody = Action // PutFeedActionEndJSONRequestBody defines body for PutFeedActionEnd for application/json ContentType. -type PutFeedActionEndJSONRequestBody = ActionEnd +type PutFeedActionEndJSONRequestBody = Action // PostFeedDaemonPingJSONRequestBody defines body for PostFeedDaemonPing for application/json ContentType. type PostFeedDaemonPingJSONRequestBody = PostFeedDaemonPing diff --git a/apihandlers/post_feed_action.go b/apihandlers/post_feed_action.go index d7c1f17..f1792ce 100644 --- a/apihandlers/post_feed_action.go +++ b/apihandlers/post_feed_action.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/opensvc/oc3/api" @@ -26,11 +27,11 @@ import ( // "version": "2.1-1977" // } -// PostFeedActionBegin handles POST /action/begin -func (a *Api) PostFeedActionBegin(c echo.Context) error { - keyH := cachekeys.FeedActionBeginH - keyQ := cachekeys.FeedActionBeginQ - keyPendingH := cachekeys.FeedActionBeginPendingH +// PostFeedAction handles POST /action/begin +func (a *Api) PostFeedAction(c echo.Context) error { + keyH := cachekeys.FeedActionH + keyQ := cachekeys.FeedActionQ + keyPendingH := cachekeys.FeedActionPendingH log := getLog(c) @@ -45,7 +46,7 @@ func (a *Api) PostFeedActionBegin(c echo.Context) error { return JSONProblemf(c, http.StatusConflict, "Refused", "authenticated node doesn't define cluster id") } - var payload api.PostFeedActionBeginJSONRequestBody + var payload api.PostFeedActionJSONRequestBody if err := c.Bind(&payload); err != nil { return JSONProblem(c, http.StatusBadRequest, "Failed to json decode request body", err.Error()) } @@ -62,7 +63,8 @@ func (a *Api) PostFeedActionBegin(c echo.Context) error { reqCtx := c.Request().Context() - idx := fmt.Sprintf("%s@%s@%s:%s", payload.Path, nodeID, ClusterID, payload.Action) + uuid := uuid.New().String() + idx := fmt.Sprintf("%s@%s@%s:%s", payload.Path, nodeID, ClusterID, uuid) s := fmt.Sprintf("HSET %s %s", keyH, idx) if _, err := a.Redis.HSet(reqCtx, keyH, idx, b).Result(); err != nil { @@ -77,5 +79,5 @@ func (a *Api) PostFeedActionBegin(c echo.Context) error { } log.Debug("action begin accepted") - return c.NoContent(http.StatusAccepted) + return c.JSON(http.StatusAccepted, api.ActionRequestAccepted{Uuid: uuid}) } diff --git a/apihandlers/put_feed_action.go b/apihandlers/put_feed_action.go index 4003264..a305a39 100644 --- a/apihandlers/put_feed_action.go +++ b/apihandlers/put_feed_action.go @@ -11,11 +11,23 @@ import ( "github.com/opensvc/oc3/cachekeys" ) +// { +// "uuid": "ea9a8373-3dda-4fe7-8c4b-08f5290c6c8b", +// "path": "foo", +// "action": "thaw", +// "begin": "2026-01-16 15:05:00", +// "end": "2026-01-16 15:06:00", +// "cron": false, +// "session_uuid": "7f2df7b2-8a4a-4bc1-9a8b-03acffaacd45", +// "actionlogfile": "/var/tmp/opensvc/foo.freezeupdfl98l", +// "status": "ok" +// } + // PutFeedActionEnd handles PUT /feed/action func (a *Api) PutFeedActionEnd(c echo.Context) error { - keyH := cachekeys.FeedActionEndH - keyQ := cachekeys.FeedActionEndQ - keyPendingH := cachekeys.FeedActionEndPendingH + keyH := cachekeys.FeedActionH + keyQ := cachekeys.FeedActionQ + keyPendingH := cachekeys.FeedActionPendingH log := getLog(c) @@ -42,7 +54,23 @@ func (a *Api) PutFeedActionEnd(c echo.Context) error { reqCtx := c.Request().Context() - idx := fmt.Sprintf("%s@%s@%s:%s", payload.Path, nodeID, ClusterID, payload.Action) + idx := fmt.Sprintf("%s@%s@%s:%s", payload.Path, nodeID, ClusterID, payload.Uuid) + + // if action is pending, update the stored begin action with end info to avoid begin and end processing + if n, err := a.Redis.HExists(reqCtx, keyPendingH, idx).Result(); err == nil && n { + if currentBytes, err := a.Redis.HGet(reqCtx, keyH, idx).Bytes(); err == nil { + var currentAction api.Action + if err := json.Unmarshal(currentBytes, ¤tAction); err == nil { + currentAction.End = payload.End + currentAction.Status = payload.Status + currentAction.Actionlogfile = payload.Actionlogfile + + if updatedBytes, err := json.Marshal(currentAction); err == nil { + b = updatedBytes + } + } + } + } s := fmt.Sprintf("HSET %s %s", keyH, idx) if _, err := a.Redis.HSet(reqCtx, keyH, idx, b).Result(); err != nil { diff --git a/cachekeys/main.go b/cachekeys/main.go index 79209e8..17b582e 100644 --- a/cachekeys/main.go +++ b/cachekeys/main.go @@ -35,11 +35,7 @@ const ( FeedInstanceStatusP = "oc3:p:feed_instance_status" FeedInstanceStatusPendingH = "oc3:h:feed_instance_status_pending" - FeedActionBeginH = "oc3:h:feed_action_begin" - FeedActionBeginQ = "oc3:q:feed_action_begin" - FeedActionBeginPendingH = "oc3:h:feed_action_begin_pending" - - FeedActionEndH = "oc3:h:feed_action_end" - FeedActionEndQ = "oc3:q:feed_action_end" - FeedActionEndPendingH = "oc3:h:feed_action_end_pending" + FeedActionH = "oc3:h:feed_action" + FeedActionQ = "oc3:q:feed_action" + FeedActionPendingH = "oc3:h:feed_action_pending" ) diff --git a/cdb/db_actions.go b/cdb/db_actions.go index 3a6fdc8..feca3bd 100644 --- a/cdb/db_actions.go +++ b/cdb/db_actions.go @@ -211,11 +211,24 @@ func (oDb *DB) GetUnfinishedActions(ctx context.Context) (lines []SvcAction, err } -func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, action string, begin time.Time, status_log string, sid string, cron bool) (int64, error) { - query := `INSERT INTO svcactions (svc_id, node_id, action, begin, status_log, sid, cron) - VALUES (?, ?, ?, ?, ?, ?, ?)` +func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, action string, begin time.Time, status_log string, sid string, cron bool, end time.Time, status string) (int64, error) { + query := "INSERT INTO svcactions (svc_id, node_id, action, begin, status_log, sid, cron" + placeholders := "?, ?, ?, ?, ?, ?, ?" + args := []any{svcID, nodeID, action, begin, status_log, sid, cron} + + if !end.IsZero() { + query += ", end" + placeholders += ", ?" + args = append(args, end) + } + if status != "" { + query += ", status" + placeholders += ", ?" + args = append(args, status) + } + query += fmt.Sprintf(") VALUES (%s)", placeholders) - result, err := oDb.DB.ExecContext(ctx, query, svcID, nodeID, action, begin, status_log, sid, cron) + result, err := oDb.DB.ExecContext(ctx, query, args...) if err != nil { return 0, err } @@ -234,7 +247,7 @@ func (oDb *DB) InsertSvcAction(ctx context.Context, svcID, nodeID uuid.UUID, act return id, nil } -func (oDb *DB) EndSvcAction(ctx context.Context, svcActionID int64, end time.Time, status string) error { +func (oDb *DB) UpdateSvcAction(ctx context.Context, svcActionID int64, end time.Time, status string) error { const query = `UPDATE svcactions SET end = ?, status = ?, time = TIMESTAMPDIFF(SECOND, begin, ?) WHERE id = ?` result, err := oDb.DB.ExecContext(ctx, query, end, status, end, svcActionID) if err != nil { diff --git a/worker/job_feed_action_begin.go b/worker/job_feed_action.go similarity index 54% rename from worker/job_feed_action_begin.go rename to worker/job_feed_action.go index 207d2a0..a8ea3e8 100644 --- a/worker/job_feed_action_begin.go +++ b/worker/job_feed_action.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log/slog" + "time" "github.com/google/uuid" "github.com/opensvc/oc3/api" @@ -11,32 +12,33 @@ import ( "github.com/opensvc/oc3/cdb" ) -type jobFeedActionBegin struct { +type jobFeedAction struct { *BaseJob nodeID string clusterID string node *cdb.DBNode - // idX is the id of the posted action begin with the pattern: @@: + // idX is the id of the posted action begin with the pattern: @@: idX string objectName string objectID string // data is the posted action begin payload - data *api.PostFeedActionBeginJSONRequestBody + data *api.PostFeedActionJSONRequestBody - rawData []byte // necessaire ? + rawData []byte } -func newActionBegin(objectName, nodeID, clusterID, action string) *jobFeedActionBegin { - idX := fmt.Sprintf("%s@%s@%s:%s", objectName, nodeID, clusterID, action) - return &jobFeedActionBegin{ +func newAction(objectName, nodeID, clusterID, uuid string) *jobFeedAction { + idX := fmt.Sprintf("%s@%s@%s:%s", objectName, nodeID, clusterID, uuid) + + return &jobFeedAction{ BaseJob: &BaseJob{ - name: "actionBegin", + name: "action", detail: "ID: " + idX, - cachePendingH: cachekeys.FeedActionBeginPendingH, + cachePendingH: cachekeys.FeedActionPendingH, cachePendingIDX: idX, }, idX: idX, @@ -46,25 +48,26 @@ func newActionBegin(objectName, nodeID, clusterID, action string) *jobFeedAction } } -func (d *jobFeedActionBegin) Operations() []operation { +func (d *jobFeedAction) Operations() []operation { return []operation{ {desc: "actionBegin/dropPending", do: d.dropPending}, - {desc: "actionBegin/findNodeFromDb", do: d.findNodeFromDb}, {desc: "actionBegin/getData", do: d.getData}, + {desc: "actionBegin/findNodeFromDb", do: d.findNodeFromDb}, {desc: "actionBegin/findObjectFromDb", do: d.findObjectFromDb}, {desc: "actionBegin/processAction", do: d.updateDB}, {desc: "actionBegin/pushFromTableChanges", do: d.pushFromTableChanges}, } } -func (d *jobFeedActionBegin) getData() error { +func (d *jobFeedAction) getData() error { var ( - data api.PostFeedActionBeginJSONRequestBody + data api.PostFeedActionJSONRequestBody ) - if b, err := d.redis.HGet(d.ctx, cachekeys.FeedActionBeginH, d.idX).Bytes(); err != nil { - return fmt.Errorf("getData: HGET %s %s: %w", cachekeys.FeedActionBeginH, d.idX, err) + slog.Info(fmt.Sprintf("xxx - get - %S", d.idX)) + if b, err := d.redis.HGet(d.ctx, cachekeys.FeedActionH, d.idX).Bytes(); err != nil { + return fmt.Errorf("getData: HGET %s %s: %w", cachekeys.FeedActionH, d.idX, err) } else if err = json.Unmarshal(b, &data); err != nil { - return fmt.Errorf("getData: unexpected data from %s %s: %w", cachekeys.FeedActionBeginH, d.idX, err) + return fmt.Errorf("getData: unexpected data from %s %s: %w", cachekeys.FeedActionH, d.idX, err) } else { d.rawData = b d.data = &data @@ -74,7 +77,8 @@ func (d *jobFeedActionBegin) getData() error { return nil } -func (d *jobFeedActionBegin) findNodeFromDb() error { +func (d *jobFeedAction) findNodeFromDb() error { + slog.Info(fmt.Sprintf("xxx - NOdename - %s", d.nodeID)) if n, err := d.oDb.NodeByNodeID(d.ctx, d.nodeID); err != nil { return fmt.Errorf("findNodeFromDb: node %s: %w", d.nodeID, err) } else { @@ -84,7 +88,7 @@ func (d *jobFeedActionBegin) findNodeFromDb() error { return nil } -func (d *jobFeedActionBegin) findObjectFromDb() error { +func (d *jobFeedAction) findObjectFromDb() error { if isNew, objId, err := d.oDb.ObjectIDFindOrCreate(d.ctx, d.objectName, d.clusterID); err != nil { return fmt.Errorf("find or create object ID failed for %s: %w", d.objectName, err) } else if isNew { @@ -97,22 +101,11 @@ func (d *jobFeedActionBegin) findObjectFromDb() error { return nil } -func (d *jobFeedActionBegin) updateDB() error { - // Log the action begin for audit/tracking purposes +func (d *jobFeedAction) updateDB() error { if d.data == nil || d.data.Path == "" { return fmt.Errorf("invalid action data: missing path") } - // slog.Info(fmt.Sprintf("====> action begin on node %s: path=%s action=%s begin=%s node_id=%s object_id=%s", - // d.node.Nodename, - // d.data.Path, - // d.data.Action, - // d.data.Begin, - // d.nodeID, - // d.objectID, - // )) - - // slog.Info(fmt.Sprintf("Object ID : %s", d.objectID)) objectUUID, err := uuid.Parse(d.objectID) if err != nil { return fmt.Errorf("invalid object ID UUID: %w", err) @@ -134,7 +127,43 @@ func (d *jobFeedActionBegin) updateDB() error { } } - d.oDb.InsertSvcAction(d.ctx, objectUUID, nodeUUID, d.data.Action, beginTime, status_log, d.data.SessionUuid, d.data.Cron) + if d.data.End != "" { + // field End is present, process as action end + endTime, err := cdb.ParseTimeWithTimezone(d.data.End, d.node.Tz) + if err != nil { + return fmt.Errorf("invalid end time format: %w", err) + } + + actionId, err := d.oDb.FindActionID(d.ctx, d.nodeID, d.objectID, beginTime, d.data.Action) + if err != nil { + return fmt.Errorf("find action ID failed: %w", err) + } + + if actionId == 0 { + // begin not processed yet, insert full record + if _, err := d.oDb.InsertSvcAction(d.ctx, objectUUID, nodeUUID, d.data.Action, beginTime, status_log, d.data.SessionUuid, d.data.Cron, endTime, d.data.Status); err != nil { + return fmt.Errorf("insert svc action failed: %w", err) + } + } else { + // begin already processed, update record with end info + if err := d.oDb.UpdateSvcAction(d.ctx, actionId, endTime, d.data.Status); err != nil { + return fmt.Errorf("end svc action failed: %w", err) + } + } + + if d.data.Status == "err" { + if err := d.oDb.UpdateActionErrors(d.ctx, d.objectID, d.nodeID); err != nil { + return fmt.Errorf("update action errors failed: %w", err) + } + if err := d.oDb.UpdateDashActionErrors(d.ctx, d.objectID, d.nodeID); err != nil { + return fmt.Errorf("update dash action errors failed: %w", err) + } + } + + } else { + // field End is not present, process as action begin + d.oDb.InsertSvcAction(d.ctx, objectUUID, nodeUUID, d.data.Action, beginTime, status_log, d.data.SessionUuid, d.data.Cron, time.Time{}, "") + } return nil } diff --git a/worker/job_feed_action_end.go b/worker/job_feed_action_end.go deleted file mode 100644 index 6faef90..0000000 --- a/worker/job_feed_action_end.go +++ /dev/null @@ -1,133 +0,0 @@ -package worker - -import ( - "encoding/json" - "fmt" - "log/slog" - - "github.com/opensvc/oc3/api" - "github.com/opensvc/oc3/cachekeys" - "github.com/opensvc/oc3/cdb" -) - -type jobFeedActionEnd struct { - *BaseJob - - nodeID string - clusterID string - node *cdb.DBNode - - // idX is the id of the posted action end with the pattern: @@: - idX string - - objectName string - objectID string - - // data is the posted action end payload - data *api.PutFeedActionEndJSONRequestBody - - rawData []byte -} - -func newActionEnd(objectName, nodeID, clusterID, action string) *jobFeedActionEnd { - idX := fmt.Sprintf("%s@%s@%s:%s", objectName, nodeID, clusterID, action) - return &jobFeedActionEnd{ - BaseJob: &BaseJob{ - name: "actionEnd", - detail: "ID: " + idX, - cachePendingH: cachekeys.FeedActionEndPendingH, - cachePendingIDX: idX, - }, - idX: idX, - nodeID: nodeID, - clusterID: clusterID, - objectName: objectName, - } -} - -func (d *jobFeedActionEnd) Operations() []operation { - return []operation{ - {desc: "actionEnd/dropPending", do: d.dropPending}, - {desc: "actionEnd/findNodeFromDb", do: d.findNodeFromDb}, - {desc: "actionEnd/getData", do: d.getData}, - {desc: "actionEnd/findObjectFromDb", do: d.findObjectFromDb}, - {desc: "actionEnd/processAction", do: d.updateDb}, - {desc: "actionEnd/pushFromTableChanges", do: d.pushFromTableChanges}, - } -} - -func (d *jobFeedActionEnd) getData() error { - var ( - data api.PutFeedActionEndJSONRequestBody - ) - if b, err := d.redis.HGet(d.ctx, cachekeys.FeedActionEndH, d.idX).Bytes(); err != nil { - return fmt.Errorf("getData: HGET %s %s: %w", cachekeys.FeedActionEndH, d.idX, err) - } else if err = json.Unmarshal(b, &data); err != nil { - return fmt.Errorf("getData: unexpected data from %s %s: %w", cachekeys.FeedActionEndH, d.idX, err) - } else { - d.rawData = b - d.data = &data - } - - slog.Info(fmt.Sprintf("got action end data for node %s:%#v", d.nodeID, d.data)) - return nil -} - -func (d *jobFeedActionEnd) findNodeFromDb() error { - if n, err := d.oDb.NodeByNodeID(d.ctx, d.nodeID); err != nil { - return fmt.Errorf("findNodeFromDb: node %s: %w", d.nodeID, err) - } else { - d.node = n - } - slog.Info(fmt.Sprintf("jobFeedActionEnd found node %s for id %s", d.node.Nodename, d.nodeID)) - return nil -} - -func (d *jobFeedActionEnd) findObjectFromDb() error { - if isNew, objId, err := d.oDb.ObjectIDFindOrCreate(d.ctx, d.objectName, d.clusterID); err != nil { - return fmt.Errorf("find or create object ID failed for %s: %w", d.objectName, err) - } else if isNew { - slog.Info(fmt.Sprintf("jobFeedActionEnd has created new object id %s@%s %s", d.objectName, d.clusterID, objId)) - } else { - d.objectID = objId - slog.Info(fmt.Sprintf("jobFeedActionEnd found object id %s@%s %s", d.objectName, d.clusterID, objId)) - } - - return nil -} - -func (d *jobFeedActionEnd) updateDb() error { - if d.data == nil || d.data.Path == "" { - return fmt.Errorf("invalid action data: missing path") - } - - beginTime, err := cdb.ParseTimeWithTimezone(d.data.Begin, d.node.Tz) - if err != nil { - return fmt.Errorf("invalid begin time format: %w", err) - } - - endTime, err := cdb.ParseTimeWithTimezone(d.data.End, d.node.Tz) - if err != nil { - return fmt.Errorf("invalid end time format: %w", err) - } - - actionId, err := d.oDb.FindActionID(d.ctx, d.nodeID, d.objectID, beginTime, d.data.Action) - if err != nil { - return fmt.Errorf("find action ID failed: %w", err) - } - - if err := d.oDb.EndSvcAction(d.ctx, actionId, endTime, d.data.Status); err != nil { - return fmt.Errorf("end svc action failed: %w", err) - } - - if d.data.Status == "err" { - if err := d.oDb.UpdateActionErrors(d.ctx, d.objectID, d.nodeID); err != nil { - return fmt.Errorf("update action errors failed: %w", err) - } - if err := d.oDb.UpdateDashActionErrors(d.ctx, d.objectID, d.nodeID); err != nil { - return fmt.Errorf("update dash action errors failed: %w", err) - } - } - - return nil -} diff --git a/worker/worker.go b/worker/worker.go index 46637f8..aaa8a9a 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -165,22 +165,14 @@ func (w *Worker) runJob(unqueuedJob []string) error { } j = newInstanceStatus(objectName, nodeID, clusterID) - case cachekeys.FeedActionBeginQ: - objectName, nodeID, ClusterID, action, err := w.jobToInstanceClusterIdAndAction(unqueuedJob[1]) + case cachekeys.FeedActionQ: + objectName, nodeID, ClusterID, uuid, err := w.jobToInstanceClusterIdAndUuid(unqueuedJob[1]) if err != nil { err := fmt.Errorf("invalid feed begin action index: %s", unqueuedJob[1]) slog.Warn(err.Error()) return err } - j = newActionBegin(objectName, nodeID, ClusterID, action) - case cachekeys.FeedActionEndQ: - objectName, nodeID, ClusterID, action, err := w.jobToInstanceClusterIdAndAction(unqueuedJob[1]) - if err != nil { - err := fmt.Errorf("invalid feed end action index: %s", unqueuedJob[1]) - slog.Warn(err.Error()) - return err - } - j = newActionEnd(objectName, nodeID, ClusterID, action) + j = newAction(objectName, nodeID, ClusterID, uuid) default: slog.Debug(fmt.Sprintf("ignore queue '%s'", unqueuedJob[0])) @@ -229,7 +221,7 @@ func (w *Worker) jobToInstanceAndClusterID(jobName string) (path, nodeID, cluste return } -func (w *Worker) jobToInstanceClusterIdAndAction(jobName string) (path, nodeID, clusterID, action string, err error) { +func (w *Worker) jobToInstanceClusterIdAndUuid(jobName string) (path, nodeID, clusterID, uuid string, err error) { l := strings.Split(jobName, ":") if len(l) != 2 { err = fmt.Errorf("unexpected job name: %s", jobName) @@ -239,6 +231,6 @@ func (w *Worker) jobToInstanceClusterIdAndAction(jobName string) (path, nodeID, err = fmt.Errorf("unexpected job name: %s", jobName) return } - action = l[1] + uuid = l[1] return } From 3a469c2ba5e61ebe5d4090f134c672387763da36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guih=C3=A9neuf?= Date: Fri, 16 Jan 2026 15:33:54 +0100 Subject: [PATCH 13/22] Remove debug logs --- worker/job_feed_action.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/worker/job_feed_action.go b/worker/job_feed_action.go index a8ea3e8..083c3f4 100644 --- a/worker/job_feed_action.go +++ b/worker/job_feed_action.go @@ -63,7 +63,6 @@ func (d *jobFeedAction) getData() error { var ( data api.PostFeedActionJSONRequestBody ) - slog.Info(fmt.Sprintf("xxx - get - %S", d.idX)) if b, err := d.redis.HGet(d.ctx, cachekeys.FeedActionH, d.idX).Bytes(); err != nil { return fmt.Errorf("getData: HGET %s %s: %w", cachekeys.FeedActionH, d.idX, err) } else if err = json.Unmarshal(b, &data); err != nil { @@ -78,7 +77,6 @@ func (d *jobFeedAction) getData() error { } func (d *jobFeedAction) findNodeFromDb() error { - slog.Info(fmt.Sprintf("xxx - NOdename - %s", d.nodeID)) if n, err := d.oDb.NodeByNodeID(d.ctx, d.nodeID); err != nil { return fmt.Errorf("findNodeFromDb: node %s: %w", d.nodeID, err) } else { From 6e2449c4c83bc8cfcc70bcca09f58d7182d5781c Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 14 Jan 2026 13:22:07 +0100 Subject: [PATCH 14/22] Add the "alert_package_differences_in_cluster" scheduler task --- cdb/db_dashboard.go | 148 +++++++++++++++++++++++++++++++++++++++ scheduler/task_alerts.go | 22 ++++++ 2 files changed, 170 insertions(+) diff --git a/cdb/db_dashboard.go b/cdb/db_dashboard.go index 748399d..d8d8f20 100644 --- a/cdb/db_dashboard.go +++ b/cdb/db_dashboard.go @@ -782,3 +782,151 @@ func (oDb *DB) DashboardUpdateAppWithoutResponsible(ctx context.Context) error { } return nil } + +func (oDb *DB) DashboardUpdatePkgDiff(ctx context.Context) error { + request := `SET @now = NOW()` + result, err := oDb.DB.ExecContext(ctx, request) + if err != nil { + return err + } + + request = ` + INSERT INTO dashboard ( + dash_type, + svc_id, + node_id, + dash_severity, + dash_fmt, + dash_dict, + dash_dict_md5, + dash_created, + dash_updated, + dash_env + ) + WITH + pkg_cluster_counts AS ( + SELECT + nodes.cluster_id, + nodes.node_id, + packages.pkg_name, + packages.pkg_version, + packages.pkg_arch, + packages.pkg_type, + COUNT(DISTINCT nodes.node_id) AS node_count + FROM packages + JOIN nodes USING (node_id) + WHERE packages.pkg_name NOT LIKE 'gpg-pubkey%' + GROUP BY + nodes.cluster_id, + packages.pkg_name, + packages.pkg_version, + packages.pkg_arch, + packages.pkg_type + ), + + cluster_nodes_counts AS ( + SELECT + cluster_id, + COUNT(node_id) AS node_count, + GROUP_CONCAT(nodename SEPARATOR ", ") AS nodenames + FROM nodes + GROUP BY cluster_id + ), + + pkg_cluster_diffs AS ( + SELECT + pkg_cluster_counts.*, + cluster_nodes_counts.nodenames, + cluster_nodes_counts.node_count AS cluster_node_count + FROM pkg_cluster_counts + JOIN cluster_nodes_counts + USING (cluster_id) + WHERE + pkg_cluster_counts.node_count < cluster_nodes_counts.node_count + ), + + pkg_cluster_diffcount AS ( + SELECT + nodes.cluster_id, + nodes.nodename, + nodes.node_id, + pkg_cluster_diffs.nodenames, + COUNT(*) AS pkg_diffs, + GROUP_CONCAT(DISTINCT pkg_cluster_diffs.pkg_name SEPARATOR ", ") AS pkg_names + FROM pkg_cluster_diffs + JOIN nodes USING (node_id) + GROUP BY + nodes.cluster_id + ), + + alerts AS ( + SELECT + services.svcname, + svcmon.svc_id, + svcmon.mon_svctype, + pkg_cluster_diffcount.* + FROM svcmon + JOIN pkg_cluster_diffcount USING (node_id) + JOIN services USING (svc_id) + ) + + SELECT + 'package differences in cluster' AS dash_type, + a.svc_id, + NULL AS node_id, + IF(a.mon_svctype = "PRD", 1, 0) AS dash_severity, + "%(n)d package differences in cluster %(nodes)s" AS dash_fmt, + JSON_OBJECT( + 'n', a.pkg_diffs, + 'nodes', a.nodenames, + 'cluster_id', a.cluster_id + ) AS dash_dict, + MD5(CONCAT( + a.pkg_diffs, + a.nodenames, + a.cluster_id + )) AS dash_dict_md5, + @now AS dash_created, + @now AS dash_updated, + a.mon_svctype AS dash_env + FROM alerts a + + ON DUPLICATE KEY UPDATE + dash_severity = VALUES(dash_severity), + dash_fmt = VALUES(dash_fmt), + dash_dict = VALUES(dash_dict), + dash_dict_md5 = VALUES(dash_dict_md5), + dash_updated = VALUES(dash_updated), + dash_env = VALUES(dash_env) + ` + result, err = oDb.DB.ExecContext(ctx, request) + if err != nil { + return err + } + if rowAffected, err := result.RowsAffected(); err != nil { + return err + } else if rowAffected > 0 { + oDb.SetChange("dashboard") + } + + request = ` + DELETE FROM dashboard + WHERE + dash_type="package differences in cluster" AND + ( + dash_updated < @now or + dash_updated IS NULL + ) + ` + result, err = oDb.DB.ExecContext(ctx, request) + if err != nil { + return err + } + if rowAffected, err := result.RowsAffected(); err != nil { + return err + } else if rowAffected > 0 { + oDb.SetChange("dashboard") + } + + return nil +} diff --git a/scheduler/task_alerts.go b/scheduler/task_alerts.go index 56daf20..b0ab056 100644 --- a/scheduler/task_alerts.go +++ b/scheduler/task_alerts.go @@ -45,6 +45,7 @@ var TaskAlert1D = Task{ TaskAlertNodeCloseToMaintenanceEnd, TaskAlertNodeMaintenanceExpired, TaskAlertNodeWithoutMaintenanceEnd, + TaskAlertPackageDifferencesInCluster, TaskAlertPurgeActionErrors, TaskPurgeAlertsOnDeletedInstances, }, @@ -130,6 +131,12 @@ var TaskLogInstancesNotUpdated = Task{ timeout: 5 * time.Minute, } +var TaskAlertPackageDifferencesInCluster = Task{ + name: "alert_package_differences_in_cluster", + fn: taskAlertPackageDifferencesInCluster, + timeout: 5 * time.Minute, +} + var TaskAlertPurgeActionErrors = Task{ name: "alert_purge_action_errors", fn: taskAlertPurgeActionErrors, @@ -265,6 +272,21 @@ func taskLogInstancesNotUpdated(ctx context.Context, task *Task) error { return odb.Commit() } +func taskAlertPackageDifferencesInCluster(ctx context.Context, task *Task) error { + odb, err := task.DBX(ctx) + if err != nil { + return err + } + defer odb.Rollback() + if err := odb.DashboardUpdatePkgDiff(ctx); err != nil { + return err + } + if err := odb.Session.NotifyChanges(ctx); err != nil { + return err + } + return odb.Commit() +} + func taskAlertPurgeActionErrors(ctx context.Context, task *Task) error { odb, err := task.DBX(ctx) if err != nil { From f586edacedfb79c69642555aa7b3075b6cbe6876 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 15 Jan 2026 16:33:01 +0100 Subject: [PATCH 15/22] Fix the logging of errors emitted by the worker tasks --- worker/job.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worker/job.go b/worker/job.go index 39c9054..8b1ff74 100644 --- a/worker/job.go +++ b/worker/job.go @@ -161,9 +161,10 @@ func runOps(ops ...operation) error { With(prometheus.Labels{"desc": op.desc, "status": operationStatusFailed}). Observe(duration.Seconds()) if op.blocking { - continue + return err } - return err + slog.Warn("%s: non blocking error: %s", op.desc, err) + continue } operationDuration. With(prometheus.Labels{"desc": op.desc, "status": operationStatusOk}). From a46aaec3715b4d4f7c9c5a832b0fb1291b5f5568 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 15 Jan 2026 16:33:51 +0100 Subject: [PATCH 16/22] Fix the timeout of some scheduler tasks --- scheduler/task_alerts.go | 2 +- scheduler/task_nodes.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scheduler/task_alerts.go b/scheduler/task_alerts.go index b0ab056..b162525 100644 --- a/scheduler/task_alerts.go +++ b/scheduler/task_alerts.go @@ -16,7 +16,7 @@ var TaskAlert1M = Task{ TaskAlertInstancesNotUpdated, }, period: time.Minute, - timeout: 15 * time.Minute, + timeout: 15 * time.Second, } var TaskAlert1H = Task{ diff --git a/scheduler/task_nodes.go b/scheduler/task_nodes.go index 28caa2c..c2c88f4 100644 --- a/scheduler/task_nodes.go +++ b/scheduler/task_nodes.go @@ -8,7 +8,7 @@ import ( var TaskUpdateVirtualAssets = Task{ name: "update_virtual_assets", fn: taskUpdateVirtualAssets, - timeout: time.Minute, + timeout: 10 * time.Second, } func taskUpdateVirtualAssets(ctx context.Context, task *Task) error { From 2f11c38c33a75a9727ebb8b44d3dfc26073bebbd Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 15 Jan 2026 16:34:33 +0100 Subject: [PATCH 17/22] Don't schedule tasks with no period --- scheduler/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scheduler/main.go b/scheduler/main.go index 7081f81..ed88667 100644 --- a/scheduler/main.go +++ b/scheduler/main.go @@ -44,6 +44,10 @@ func (t *Scheduler) Debugf(format string, args ...any) { func (t *Scheduler) toggleTasks(ctx context.Context, states map[string]State) { for _, task := range Tasks { + if task.period == 0 { + //task.Debugf("skip: no period") + continue + } name := task.Name() storedState, _ := states[name] cachedState, hasCachedState := t.states[name] From 205c32615e18371d5cbffd57057ab8fb846089ac Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 15 Jan 2026 16:35:14 +0100 Subject: [PATCH 18/22] Add the UpdateVirtualAsset(ctx, svcID, nodeID) db helper --- cdb/db_nodes.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/cdb/db_nodes.go b/cdb/db_nodes.go index 3d53260..26d216f 100644 --- a/cdb/db_nodes.go +++ b/cdb/db_nodes.go @@ -439,3 +439,64 @@ func (oDb *DB) UpdateVirtualAssets(ctx context.Context) error { } return nil } + +func (oDb *DB) UpdateVirtualAsset(ctx context.Context, svcID, nodeID string) error { + request := ` + UPDATE nodes n + JOIN ( + SELECT + svcmon.mon_vmname AS vmname, + services.svc_app AS svc_app, + nodes.app AS node_app, + nodes.loc_addr, + nodes.loc_city, + nodes.loc_zip, + nodes.loc_room, + nodes.loc_building, + nodes.loc_floor, + nodes.loc_rack, + nodes.power_cabinet1, + nodes.power_cabinet2, + nodes.power_supply_nb, + nodes.power_protect, + nodes.power_protect_breaker, + nodes.power_breaker1, + nodes.power_breaker2, + nodes.loc_country, + nodes.enclosure + FROM svcmon + JOIN services ON svcmon.svc_id = services.svc_id + JOIN nodes ON svcmon.node_id = nodes.node_id + WHERE svcmon.svc_id = ? AND svcmon.node_id = ? + ) AS source + SET + n.loc_addr = COALESCE(NULLIF(source.loc_addr, ''), n.loc_addr), + n.loc_city = COALESCE(NULLIF(source.loc_city, ''), n.loc_city), + n.loc_zip = COALESCE(NULLIF(source.loc_zip, ''), n.loc_zip), + n.loc_room = COALESCE(NULLIF(source.loc_room, ''), n.loc_room), + n.loc_building = COALESCE(NULLIF(source.loc_building, ''), n.loc_building), + n.loc_floor = COALESCE(NULLIF(source.loc_floor, ''), n.loc_floor), + n.loc_rack = COALESCE(NULLIF(source.loc_rack, ''), n.loc_rack), + n.power_cabinet1 = COALESCE(NULLIF(source.power_cabinet1, ''), n.power_cabinet1), + n.power_cabinet2 = COALESCE(NULLIF(source.power_cabinet2, ''), n.power_cabinet2), + n.power_supply_nb = COALESCE(NULLIF(source.power_supply_nb, ''), n.power_supply_nb), + n.power_protect = COALESCE(NULLIF(source.power_protect, ''), n.power_protect), + n.power_protect_breaker = COALESCE(NULLIF(source.power_protect_breaker, ''), n.power_protect_breaker), + n.power_breaker1 = COALESCE(NULLIF(source.power_breaker1, ''), n.power_breaker1), + n.power_breaker2 = COALESCE(NULLIF(source.power_breaker2, ''), n.power_breaker2), + n.loc_country = COALESCE(NULLIF(source.loc_country, ''), n.loc_country), + n.enclosure = COALESCE(NULLIF(source.enclosure, ''), n.enclosure) + WHERE + n.nodename = source.vmname AND + n.app IN (source.svc_app, source.node_app)` + result, err := oDb.DB.ExecContext(ctx, request, svcID, nodeID) + if err != nil { + return err + } + if rowAffected, err := result.RowsAffected(); err != nil { + return err + } else if rowAffected > 0 { + oDb.SetChange("nodes") + } + return nil +} From f45ec0dcf5643d7e482c79d3200e79975548360c Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 15 Jan 2026 16:35:59 +0100 Subject: [PATCH 19/22] Add the DashboardUpdatePkgDiffForNode(ctx, nodeID) db helper Call it at the end of the pushpkg feed job. --- cdb/db_dashboard.go | 172 ++++++++++++++++++++++++++++++++++++++ worker/job_feed_system.go | 4 +- 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/cdb/db_dashboard.go b/cdb/db_dashboard.go index d8d8f20..e416524 100644 --- a/cdb/db_dashboard.go +++ b/cdb/db_dashboard.go @@ -930,3 +930,175 @@ func (oDb *DB) DashboardUpdatePkgDiff(ctx context.Context) error { return nil } + +// DashboardUpdatePkgDiffForNode refreshes only "package differences in cluster" alerts +// for the node's cluster. +func (oDb *DB) DashboardUpdatePkgDiffForNode(ctx context.Context, nodeID string) error { + request := `SET @now = NOW()` + _, err := oDb.DB.ExecContext(ctx, request) + if err != nil { + return fmt.Errorf("failed to set @now: %v", err) + } + request = ` + INSERT INTO dashboard ( + dash_type, + svc_id, + node_id, + dash_severity, + dash_fmt, + dash_dict, + dash_dict_md5, + dash_created, + dash_updated, + dash_env + ) + + WITH + svc_nodes AS ( + SELECT + svcmon.svc_id, + nodes.node_id, + nodes.nodename, + nodes.cluster_id + FROM svcmon + JOIN nodes ON svcmon.node_id = nodes.node_id + WHERE + svcmon.mon_updated > DATE_SUB(@now, INTERVAL 20 MINUTE) AND + svcmon.node_id = ? + ), + + pkg_svc_counts AS ( + SELECT + svc_nodes.svc_id, + svc_nodes.node_id, + packages.pkg_name, + packages.pkg_version, + packages.pkg_arch, + packages.pkg_type, + COUNT(DISTINCT svc_nodes.node_id) AS node_count + FROM packages + JOIN svc_nodes ON packages.node_id = svc_nodes.node_id + WHERE packages.pkg_name NOT LIKE 'gpg-pubkey%' + GROUP BY + svc_nodes.svc_id, + packages.pkg_name, + packages.pkg_version, + packages.pkg_arch, + packages.pkg_type + ), + + svc_node_counts AS ( + SELECT + svc_id, + COUNT(node_id) AS node_count, + GROUP_CONCAT(nodename SEPARATOR ", ") AS nodenames + FROM svc_nodes + GROUP BY svc_id + ), + + pkg_svc_diffs AS ( + SELECT + pkg_svc_counts.*, + svc_node_counts.nodenames, + svc_node_counts.node_count AS svc_node_count + FROM pkg_svc_counts + JOIN svc_node_counts ON pkg_svc_counts.svc_id = svc_node_counts.svc_id + WHERE + pkg_svc_counts.node_count < svc_node_counts.node_count + ), + + pkg_svc_diffcount AS ( + SELECT + svc_nodes.svc_id, + svc_nodes.nodename, + svc_nodes.node_id, + svc_nodes.cluster_id, + pkg_svc_diffs.nodenames, + COUNT(*) AS pkg_diffs, + GROUP_CONCAT(DISTINCT pkg_svc_diffs.pkg_name SEPARATOR ", ") AS pkg_names + FROM pkg_svc_diffs + JOIN svc_nodes ON pkg_svc_diffs.node_id = svc_nodes.node_id + GROUP BY + svc_nodes.svc_id, + svc_nodes.nodename, + svc_nodes.node_id, + svc_nodes.cluster_id, + pkg_svc_diffs.nodenames + ), + + alerts AS ( + SELECT + services.svcname, + svcmon.mon_svctype, + pkg_svc_diffcount.* + FROM pkg_svc_diffcount + JOIN svcmon ON pkg_svc_diffcount.node_id = svcmon.node_id AND pkg_svc_diffcount.svc_id = svcmon.svc_id + JOIN services ON pkg_svc_diffcount.svc_id = services.svc_id + ) + + SELECT + 'package differences in service' AS dash_type, + a.svc_id, + NULL AS node_id, + IF(a.mon_svctype = 'PRD', 1, 0) AS dash_severity, + CONCAT(a.pkg_diffs, ' package differences in service ', a.nodenames) AS dash_fmt, + JSON_OBJECT( + 'n', a.pkg_diffs, + 'nodes', a.nodenames, + 'svc_id', a.svc_id, + 'cluster_id', a.cluster_id + ) AS dash_dict, + MD5(CONCAT( + a.pkg_diffs, + a.nodenames, + a.svc_id, + a.cluster_id + )) AS dash_dict_md5, + @now AS dash_created, + @now AS dash_updated, + a.mon_svctype AS dash_env + FROM alerts a + ON DUPLICATE KEY UPDATE + dash_severity = VALUES(dash_severity), + dash_fmt = VALUES(dash_fmt), + dash_dict = VALUES(dash_dict), + dash_dict_md5 = VALUES(dash_dict_md5), + dash_updated = VALUES(dash_updated), + dash_env = VALUES(dash_env); + ` + + result, err := oDb.DB.ExecContext(ctx, request, nodeID) + if err != nil { + return fmt.Errorf("failed to update dashboard: %v", err) + } + + rowAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %v", err) + } else if rowAffected > 0 { + oDb.SetChange("dashboard") + } + + request = ` + DELETE d FROM dashboard d + LEFT JOIN svcmon m ON d.svc_id=m.svc_id + WHERE + m.node_id = ? AND + dash_type = "package differences in cluster" AND + dash_updated < @now + ` + + result, err = oDb.DB.ExecContext(ctx, request, nodeID) + if err != nil { + return fmt.Errorf("failed to delete old dashboard entries: %v", err) + } + + rowAffected, err = result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %v", err) + } else if rowAffected > 0 { + oDb.SetChange("dashboard") + } + + return nil +} diff --git a/worker/job_feed_system.go b/worker/job_feed_system.go index 94f95fb..0c06413 100644 --- a/worker/job_feed_system.go +++ b/worker/job_feed_system.go @@ -100,7 +100,9 @@ func (d *jobFeedSystem) pkg() error { } else { defer rows.Close() } - + if err := d.oDb.DashboardUpdatePkgDiffForNode(d.ctx, nodeID); err != nil { + return err + } return nil } From f98187fc763ca33b66f25d62579d4092e85d0007 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 16 Jan 2026 10:03:51 +0100 Subject: [PATCH 20/22] Reimplement the DashboardUpdatePkgDiffForNode() db helper For speed by using sql simpler exec plans, at the cost of more requests. --- cdb/db_dashboard.go | 334 +++++++++++++++++++++++++------------------- 1 file changed, 188 insertions(+), 146 deletions(-) diff --git a/cdb/db_dashboard.go b/cdb/db_dashboard.go index e416524..c114845 100644 --- a/cdb/db_dashboard.go +++ b/cdb/db_dashboard.go @@ -2,8 +2,11 @@ package cdb import ( "context" + "crypto/md5" "database/sql" + "encoding/json" "fmt" + "strings" "time" ) @@ -931,173 +934,212 @@ func (oDb *DB) DashboardUpdatePkgDiff(ctx context.Context) error { return nil } -// DashboardUpdatePkgDiffForNode refreshes only "package differences in cluster" alerts -// for the node's cluster. func (oDb *DB) DashboardUpdatePkgDiffForNode(ctx context.Context, nodeID string) error { request := `SET @now = NOW()` _, err := oDb.DB.ExecContext(ctx, request) if err != nil { - return fmt.Errorf("failed to set @now: %v", err) + return err } - request = ` - INSERT INTO dashboard ( - dash_type, - svc_id, - node_id, - dash_severity, - dash_fmt, - dash_dict, - dash_dict_md5, - dash_created, - dash_updated, - dash_env - ) - WITH - svc_nodes AS ( - SELECT - svcmon.svc_id, - nodes.node_id, - nodes.nodename, - nodes.cluster_id - FROM svcmon - JOIN nodes ON svcmon.node_id = nodes.node_id - WHERE - svcmon.mon_updated > DATE_SUB(@now, INTERVAL 20 MINUTE) AND - svcmon.node_id = ? - ), + processSvcID := func(svcID, monSvctype, monVmtype string) error { + var query string + if monVmtype != "" { + // encap peers + query = ` + SELECT DISTINCT nodes.node_id, nodes.nodename + FROM svcmon + JOIN nodes ON svcmon.node_id = nodes.node_id + WHERE svcmon.svc_id = ? + AND svcmon.mon_updated > DATE_SUB(NOW(), INTERVAL 20 MINUTE) + AND svcmon.mon_vmtype != "" + ORDER BY nodes.nodename + ` + } else { + // non-encap peers + query = ` + SELECT DISTINCT nodes.node_id, nodes.nodename + FROM svcmon + JOIN nodes ON svcmon.node_id = nodes.node_id + WHERE svcmon.svc_id = ? + AND svcmon.mon_updated > DATE_SUB(NOW(), INTERVAL 20 MINUTE) + AND svcmon.mon_vmtype = "" + ORDER BY nodes.nodename + ` + } - pkg_svc_counts AS ( - SELECT - svc_nodes.svc_id, - svc_nodes.node_id, - packages.pkg_name, - packages.pkg_version, - packages.pkg_arch, - packages.pkg_type, - COUNT(DISTINCT svc_nodes.node_id) AS node_count - FROM packages - JOIN svc_nodes ON packages.node_id = svc_nodes.node_id - WHERE packages.pkg_name NOT LIKE 'gpg-pubkey%' - GROUP BY - svc_nodes.svc_id, - packages.pkg_name, - packages.pkg_version, - packages.pkg_arch, - packages.pkg_type - ), + rows, err := oDb.DB.QueryContext(ctx, query, svcID) + if err != nil { + return fmt.Errorf("failed to query nodes: %v", err) + } + defer rows.Close() - svc_node_counts AS ( - SELECT - svc_id, - COUNT(node_id) AS node_count, - GROUP_CONCAT(nodename SEPARATOR ", ") AS nodenames - FROM svc_nodes - GROUP BY svc_id - ), + var nodeIDs []string + var nodenames []string + for rows.Next() { + var nodeID string + var nodename string + if err := rows.Scan(&nodeID, &nodename); err != nil { + return fmt.Errorf("failed to scan node row: %v", err) + } + nodeIDs = append(nodeIDs, nodeID) + nodenames = append(nodenames, nodename) + } - pkg_svc_diffs AS ( - SELECT - pkg_svc_counts.*, - svc_node_counts.nodenames, - svc_node_counts.node_count AS svc_node_count - FROM pkg_svc_counts - JOIN svc_node_counts ON pkg_svc_counts.svc_id = svc_node_counts.svc_id - WHERE - pkg_svc_counts.node_count < svc_node_counts.node_count - ), + if len(nodeIDs) < 2 { + return nil + } - pkg_svc_diffcount AS ( - SELECT - svc_nodes.svc_id, - svc_nodes.nodename, - svc_nodes.node_id, - svc_nodes.cluster_id, - pkg_svc_diffs.nodenames, - COUNT(*) AS pkg_diffs, - GROUP_CONCAT(DISTINCT pkg_svc_diffs.pkg_name SEPARATOR ", ") AS pkg_names - FROM pkg_svc_diffs - JOIN svc_nodes ON pkg_svc_diffs.node_id = svc_nodes.node_id - GROUP BY - svc_nodes.svc_id, - svc_nodes.nodename, - svc_nodes.node_id, - svc_nodes.cluster_id, - pkg_svc_diffs.nodenames - ), + // Count pkg diffs + var pkgDiffCount int + placeholders := make([]string, len(nodeIDs)) + args := make([]any, len(nodeIDs)) + for i, id := range nodeIDs { + placeholders[i] = "?" + args[i] = id + } + query = fmt.Sprintf(` + SELECT COUNT(pkg_name) + FROM ( + SELECT + pkg_name, + pkg_version, + pkg_arch, + pkg_type, + COUNT(DISTINCT node_id) AS c + FROM packages + WHERE + node_id IN (%s) + AND pkg_name NOT LIKE "gpg-pubkey%%" + GROUP BY + pkg_name, + pkg_version, + pkg_arch, + pkg_type + ) AS t + WHERE t.c != ? + `, strings.Join(placeholders, ",")) + args = append(args, len(nodeIDs)) - alerts AS ( - SELECT - services.svcname, - svcmon.mon_svctype, - pkg_svc_diffcount.* - FROM pkg_svc_diffcount - JOIN svcmon ON pkg_svc_diffcount.node_id = svcmon.node_id AND pkg_svc_diffcount.svc_id = svcmon.svc_id - JOIN services ON pkg_svc_diffcount.svc_id = services.svc_id - ) + err = oDb.DB.QueryRowContext(ctx, query, args...).Scan(&pkgDiffCount) + if err != nil { + return fmt.Errorf("failed to count package differences: %v", err) + } - SELECT - 'package differences in service' AS dash_type, - a.svc_id, - NULL AS node_id, - IF(a.mon_svctype = 'PRD', 1, 0) AS dash_severity, - CONCAT(a.pkg_diffs, ' package differences in service ', a.nodenames) AS dash_fmt, - JSON_OBJECT( - 'n', a.pkg_diffs, - 'nodes', a.nodenames, - 'svc_id', a.svc_id, - 'cluster_id', a.cluster_id - ) AS dash_dict, - MD5(CONCAT( - a.pkg_diffs, - a.nodenames, - a.svc_id, - a.cluster_id - )) AS dash_dict_md5, - @now AS dash_created, - @now AS dash_updated, - a.mon_svctype AS dash_env - FROM alerts a - ON DUPLICATE KEY UPDATE - dash_severity = VALUES(dash_severity), - dash_fmt = VALUES(dash_fmt), - dash_dict = VALUES(dash_dict), - dash_dict_md5 = VALUES(dash_dict_md5), - dash_updated = VALUES(dash_updated), - dash_env = VALUES(dash_env); - ` + if pkgDiffCount == 0 { + return nil + } - result, err := oDb.DB.ExecContext(ctx, request, nodeID) - if err != nil { - return fmt.Errorf("failed to update dashboard: %v", err) + sev := 0 + if monSvctype == "PRD" { + sev = 1 + } + + // truncate too long node names list + skip := 0 + trail := "" + nodesStr := strings.Join(nodenames, ",") + for len(nodesStr)+len(trail) > 50 { + skip++ + nodenames = nodenames[:len(nodenames)-1] + nodesStr = strings.Join(nodenames, ",") + trail = fmt.Sprintf(", ... (+%d)", skip) + } + nodesStr += trail + + // Format dash_dict JSON content + dashDict := map[string]any{ + "n": pkgDiffCount, + "nodes": nodesStr, + } + dashDictJSON, err := json.Marshal(dashDict) + if err != nil { + return fmt.Errorf("failed to marshal dash_dict: %v", err) + } + + dashDictMD5 := fmt.Sprintf("%x", md5.Sum(dashDictJSON)) + + query = ` + INSERT INTO dashboard + SET + dash_type = "package differences in cluster", + svc_id = ?, + node_id = "", + dash_severity = ?, + dash_fmt = "%(n)s package differences in cluster %(nodes)s", + dash_dict = ?, + dash_dict_md5 = ?, + dash_created = @now, + dash_updated = @now, + dash_env = ? + ON DUPLICATE KEY UPDATE + dash_severity = ?, + dash_fmt = "%(n)s package differences in cluster %(nodes)s", + dash_dict = ?, + dash_dict_md5 = ?, + dash_updated = @now, + dash_env = ? + ` + + _, err = oDb.DB.ExecContext(ctx, query, + svcID, sev, dashDictJSON, dashDictMD5, monSvctype, + sev, dashDictJSON, dashDictMD5, monSvctype, + ) + if err != nil { + return fmt.Errorf("failed to insert/update dashboard: %v", err) + } + + return nil } - rowAffected, err := result.RowsAffected() + // Get the list of svc_id for instances having recently updated status + rows, err := oDb.DB.QueryContext(ctx, ` + SELECT svcmon.svc_id, svcmon.mon_svctype, svcmon.mon_vmtype + FROM svcmon + JOIN nodes ON svcmon.node_id = nodes.node_id + WHERE svcmon.node_id = ? AND svcmon.mon_updated > DATE_SUB(NOW(), INTERVAL 19 MINUTE) + `, nodeID) if err != nil { - return fmt.Errorf("failed to get affected rows: %v", err) - } else if rowAffected > 0 { - oDb.SetChange("dashboard") + return fmt.Errorf("failed to query svcmon: %v", err) } + defer rows.Close() - request = ` - DELETE d FROM dashboard d - LEFT JOIN svcmon m ON d.svc_id=m.svc_id - WHERE - m.node_id = ? AND - dash_type = "package differences in cluster" AND - dash_updated < @now - ` + var svcIDs []any + todo := make(map[string]func() error) + for rows.Next() { + var svcID string + var monSvctype, monVmtype sql.NullString + if err := rows.Scan(&svcID, &monSvctype, &monVmtype); err != nil { + return fmt.Errorf("failed to scan svcmon row: %v", err) + } - result, err = oDb.DB.ExecContext(ctx, request, nodeID) - if err != nil { - return fmt.Errorf("failed to delete old dashboard entries: %v", err) + // Remember which svc_id needs non-updated alert clean up + svcIDs = append(svcIDs, svcID) + + // Defer after rows.Close() to avoid busy db conn errors + todo[svcID] = func() error { + return processSvcID(svcID, monSvctype.String, monVmtype.String) + } + } + rows.Close() + for svcID, fn := range todo { + if err := fn(); err != nil { + return fmt.Errorf("failed to process svc_id %s: %v", svcID, err) + } } - rowAffected, err = result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get affected rows: %v", err) - } else if rowAffected > 0 { - oDb.SetChange("dashboard") + // Clean up non updated alerts + if len(svcIDs) > 0 { + query := fmt.Sprintf(` + DELETE FROM dashboard + WHERE svc_id IN (%s) + AND dash_type = "package differences in cluster" + AND dash_updated < @now + `, Placeholders(len(svcIDs))) + + _, err := oDb.DB.ExecContext(ctx, query, svcIDs...) + if err != nil { + return fmt.Errorf("failed to delete old dashboard entries: %v", err) + } } return nil From 462afdecd4af673af87e2c37ddd74d6a99b0bb4e Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 16 Jan 2026 10:18:03 +0100 Subject: [PATCH 21/22] Remove the "alert_package_differences_in_cluster" scheduler task The sql was too complex and the task did not exist in oc2 --- cdb/db_dashboard.go | 148 --------------------------------------- scheduler/task_alerts.go | 22 ------ 2 files changed, 170 deletions(-) diff --git a/cdb/db_dashboard.go b/cdb/db_dashboard.go index c114845..980723a 100644 --- a/cdb/db_dashboard.go +++ b/cdb/db_dashboard.go @@ -786,154 +786,6 @@ func (oDb *DB) DashboardUpdateAppWithoutResponsible(ctx context.Context) error { return nil } -func (oDb *DB) DashboardUpdatePkgDiff(ctx context.Context) error { - request := `SET @now = NOW()` - result, err := oDb.DB.ExecContext(ctx, request) - if err != nil { - return err - } - - request = ` - INSERT INTO dashboard ( - dash_type, - svc_id, - node_id, - dash_severity, - dash_fmt, - dash_dict, - dash_dict_md5, - dash_created, - dash_updated, - dash_env - ) - WITH - pkg_cluster_counts AS ( - SELECT - nodes.cluster_id, - nodes.node_id, - packages.pkg_name, - packages.pkg_version, - packages.pkg_arch, - packages.pkg_type, - COUNT(DISTINCT nodes.node_id) AS node_count - FROM packages - JOIN nodes USING (node_id) - WHERE packages.pkg_name NOT LIKE 'gpg-pubkey%' - GROUP BY - nodes.cluster_id, - packages.pkg_name, - packages.pkg_version, - packages.pkg_arch, - packages.pkg_type - ), - - cluster_nodes_counts AS ( - SELECT - cluster_id, - COUNT(node_id) AS node_count, - GROUP_CONCAT(nodename SEPARATOR ", ") AS nodenames - FROM nodes - GROUP BY cluster_id - ), - - pkg_cluster_diffs AS ( - SELECT - pkg_cluster_counts.*, - cluster_nodes_counts.nodenames, - cluster_nodes_counts.node_count AS cluster_node_count - FROM pkg_cluster_counts - JOIN cluster_nodes_counts - USING (cluster_id) - WHERE - pkg_cluster_counts.node_count < cluster_nodes_counts.node_count - ), - - pkg_cluster_diffcount AS ( - SELECT - nodes.cluster_id, - nodes.nodename, - nodes.node_id, - pkg_cluster_diffs.nodenames, - COUNT(*) AS pkg_diffs, - GROUP_CONCAT(DISTINCT pkg_cluster_diffs.pkg_name SEPARATOR ", ") AS pkg_names - FROM pkg_cluster_diffs - JOIN nodes USING (node_id) - GROUP BY - nodes.cluster_id - ), - - alerts AS ( - SELECT - services.svcname, - svcmon.svc_id, - svcmon.mon_svctype, - pkg_cluster_diffcount.* - FROM svcmon - JOIN pkg_cluster_diffcount USING (node_id) - JOIN services USING (svc_id) - ) - - SELECT - 'package differences in cluster' AS dash_type, - a.svc_id, - NULL AS node_id, - IF(a.mon_svctype = "PRD", 1, 0) AS dash_severity, - "%(n)d package differences in cluster %(nodes)s" AS dash_fmt, - JSON_OBJECT( - 'n', a.pkg_diffs, - 'nodes', a.nodenames, - 'cluster_id', a.cluster_id - ) AS dash_dict, - MD5(CONCAT( - a.pkg_diffs, - a.nodenames, - a.cluster_id - )) AS dash_dict_md5, - @now AS dash_created, - @now AS dash_updated, - a.mon_svctype AS dash_env - FROM alerts a - - ON DUPLICATE KEY UPDATE - dash_severity = VALUES(dash_severity), - dash_fmt = VALUES(dash_fmt), - dash_dict = VALUES(dash_dict), - dash_dict_md5 = VALUES(dash_dict_md5), - dash_updated = VALUES(dash_updated), - dash_env = VALUES(dash_env) - ` - result, err = oDb.DB.ExecContext(ctx, request) - if err != nil { - return err - } - if rowAffected, err := result.RowsAffected(); err != nil { - return err - } else if rowAffected > 0 { - oDb.SetChange("dashboard") - } - - request = ` - DELETE FROM dashboard - WHERE - dash_type="package differences in cluster" AND - ( - dash_updated < @now or - dash_updated IS NULL - ) - ` - result, err = oDb.DB.ExecContext(ctx, request) - if err != nil { - return err - } - if rowAffected, err := result.RowsAffected(); err != nil { - return err - } else if rowAffected > 0 { - oDb.SetChange("dashboard") - } - - return nil -} - func (oDb *DB) DashboardUpdatePkgDiffForNode(ctx context.Context, nodeID string) error { request := `SET @now = NOW()` _, err := oDb.DB.ExecContext(ctx, request) diff --git a/scheduler/task_alerts.go b/scheduler/task_alerts.go index b162525..b6ddf06 100644 --- a/scheduler/task_alerts.go +++ b/scheduler/task_alerts.go @@ -45,7 +45,6 @@ var TaskAlert1D = Task{ TaskAlertNodeCloseToMaintenanceEnd, TaskAlertNodeMaintenanceExpired, TaskAlertNodeWithoutMaintenanceEnd, - TaskAlertPackageDifferencesInCluster, TaskAlertPurgeActionErrors, TaskPurgeAlertsOnDeletedInstances, }, @@ -131,12 +130,6 @@ var TaskLogInstancesNotUpdated = Task{ timeout: 5 * time.Minute, } -var TaskAlertPackageDifferencesInCluster = Task{ - name: "alert_package_differences_in_cluster", - fn: taskAlertPackageDifferencesInCluster, - timeout: 5 * time.Minute, -} - var TaskAlertPurgeActionErrors = Task{ name: "alert_purge_action_errors", fn: taskAlertPurgeActionErrors, @@ -272,21 +265,6 @@ func taskLogInstancesNotUpdated(ctx context.Context, task *Task) error { return odb.Commit() } -func taskAlertPackageDifferencesInCluster(ctx context.Context, task *Task) error { - odb, err := task.DBX(ctx) - if err != nil { - return err - } - defer odb.Rollback() - if err := odb.DashboardUpdatePkgDiff(ctx); err != nil { - return err - } - if err := odb.Session.NotifyChanges(ctx); err != nil { - return err - } - return odb.Commit() -} - func taskAlertPurgeActionErrors(ctx context.Context, task *Task) error { odb, err := task.DBX(ctx) if err != nil { From 6a2d0fb991213bd4f65d1015d0294696ee464c34 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 16 Jan 2026 11:15:43 +0100 Subject: [PATCH 22/22] Add the "scrub_static" scheduler task Also simplify the scheduler config, moving away from per-task directories setup, to a scheduler-level directories map: scheduler: directories: uploads: /srv/oc3/uploads static: /srv/oc3/static ... --- scheduler/task_metrics.go | 19 ++------ scheduler/task_scrub.go | 93 +++++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/scheduler/task_metrics.go b/scheduler/task_metrics.go index 9c2f09f..d608fca 100644 --- a/scheduler/task_metrics.go +++ b/scheduler/task_metrics.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "fmt" - "os" "path/filepath" "strings" "time" @@ -25,21 +24,11 @@ var TaskMetrics = Task{ } func MakeWSPFilename(format string, args ...any) (string, error) { - var dir string - candidates := viper.GetStringSlice("scheduler.task.metrics.directories") - if len(candidates) == 0 { - return "", fmt.Errorf("scheduler.task.metrics.directories is not set") + directory := viper.GetString("scheduler.directories.uploads") + if directory == "" { + return "", fmt.Errorf("define scheduler.directories.uploads") } - for _, d := range candidates { - if _, err := os.Stat(d); err == nil { - dir = d - break - } - } - if dir == "" { - return "", fmt.Errorf("scheduler.task.metrics.directories has no existing entry") - } - return filepath.Join(dir, fmt.Sprintf(format+".wsp", args...)), nil + return filepath.Join(directory, "stats", fmt.Sprintf(format+".wsp", args...)), nil } func taskMetrics(ctx context.Context, task *Task) error { diff --git a/scheduler/task_scrub.go b/scheduler/task_scrub.go index da039d7..ad5a707 100644 --- a/scheduler/task_scrub.go +++ b/scheduler/task_scrub.go @@ -93,6 +93,12 @@ var TaskScrubSvcdisks = Task{ timeout: time.Minute, } +var TaskScrubStatic = Task{ + name: "scrub_static", + fn: taskScrubStatic, + timeout: time.Minute, +} + var TaskScrubTempviz = Task{ name: "scrub_tempviz", fn: taskScrubTempviz, @@ -157,6 +163,7 @@ var TaskScrub1D = Task{ TaskScrubPatches, TaskScrubPdf, TaskScrubResmon, + TaskScrubStatic, TaskScrubStorArray, TaskScrubSvcdisks, TaskUpdateStorArrayDGQuota, @@ -578,21 +585,12 @@ func taskUpdateStorArrayDGQuota(ctx context.Context, task *Task) error { return odb.Commit() } -func taskScrubTempviz(ctx context.Context, task *Task) error { - threshold := time.Now().Add(-1 * time.Hour) - directories := viper.GetStringSlice("scheduler.task.scrub_tempviz.directories") - if len(directories) == 0 { - slog.Warn("skip: define scheduler.task.scrub_tempviz.directories") - return nil - } +func scrubFiles(pattern string, threshold time.Time) error { var matches []string - for _, directory := range directories { - pattern := filepath.Join(directory, "tempviz*") - if m, err := filepath.Glob(pattern); err != nil { - return fmt.Errorf("failed to glob files: %w", err) - } else { - matches = append(matches, m...) - } + if m, err := filepath.Glob(pattern); err != nil { + return fmt.Errorf("failed to glob files: %w", err) + } else { + matches = append(matches, m...) } for _, fpath := range matches { fileInfo, err := os.Stat(fpath) @@ -610,36 +608,53 @@ func taskScrubTempviz(ctx context.Context, task *Task) error { return nil } -func taskScrubPdf(ctx context.Context, task *Task) error { - threshold := time.Now().Add(-24 * time.Hour) - directories := viper.GetStringSlice("scheduler.task.scrub_pdf.directories") - if len(directories) == 0 { - slog.Warn("skip: define scheduler.task.scrub_pdf.directories") +func taskScrubStatic(ctx context.Context, task *Task) error { + threshold := time.Now().Add(-1 * time.Hour) + directory := viper.GetString("scheduler.directories.static") + if directory == "" { + slog.Warn("skip: define scheduler.directories.static") return nil } - var matches []string - for _, directory := range directories { - pattern := filepath.Join(directory, "*-*-*-*-*.pdf") - if m, err := filepath.Glob(pattern); err != nil { - return fmt.Errorf("failed to glob files: %w", err) - } else { - matches = append(matches, m...) - } + if err := scrubFiles(filepath.Join(directory, "tempviz*.png"), threshold); err != nil { + return err } - for _, fpath := range matches { - fileInfo, err := os.Stat(fpath) - if err != nil { - return err - } - mtime := fileInfo.ModTime() - if mtime.Before(threshold) { - slog.Info(fmt.Sprintf("rm %s mtime %s", fpath, mtime)) - if err := os.Remove(fpath); err != nil { - return fmt.Errorf("failed to rm %s: %w", fpath, err) - } - } + if err := scrubFiles(filepath.Join(directory, "tempviz*.dot"), threshold); err != nil { + return err + } + if err := scrubFiles(filepath.Join(directory, "stats_*_[0-9]*.png"), threshold); err != nil { + return err + } + if err := scrubFiles(filepath.Join(directory, "stat_*_[0-9]*.png"), threshold); err != nil { + return err + } + if err := scrubFiles(filepath.Join(directory, "stats_*_[0-9]*.svg"), threshold); err != nil { + return err + } + if err := scrubFiles(filepath.Join(directory, "*-*-*-*.pdf"), threshold); err != nil { + return err } return nil + +} + +func taskScrubTempviz(ctx context.Context, task *Task) error { + threshold := time.Now().Add(-1 * time.Hour) + directory := viper.GetString("scheduler.directories.static") + if directory == "" { + slog.Warn("skip: define scheduler.directories.static") + return nil + } + return scrubFiles(filepath.Join(directory, "tempviz*"), threshold) +} + +func taskScrubPdf(ctx context.Context, task *Task) error { + threshold := time.Now().Add(-24 * time.Hour) + directory := viper.GetString("scheduler.directories.static") + if directory == "" { + slog.Warn("skip: define scheduler.directories.static") + return nil + } + return scrubFiles(filepath.Join(directory, "*-*-*-*-*.pdf"), threshold) } func taskScrubUnfinishedActions(ctx context.Context, task *Task) error {