Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
c8b6273
Bump oapi-codegen runtime to v1.7.0
sghf Sep 16, 2026
3426bbc
Replace the generic ListResponse with per-resource response schemas
sghf Sep 16, 2026
36ac3fa
Update codegen_*
sghf Sep 16, 2026
872f57a
Clear node datetime columns instead of writing an empty string
sghf Sep 16, 2026
981c1d0
Fix : Guard NotifyTableChangeWithData against a nil event publisher
sghf Sep 16, 2026
c70eb6c
Store node boolean fields as the "T"/"F" strings
sghf Sep 16, 2026
9f98e98
Fix the node network prio column name in the net_prio prop and expose
sghf Sep 17, 2026
71bdd7a
Forward the authenticated user id to the list endpoint
sghf Sep 17, 2026
6b0a5ea
Port the /users and /users/{user_id}/prefs endpoints
sghf Sep 17, 2026
3cee1b3
Expose the node and service names on the alert endpoints
sghf Sep 17, 2026
63843bc
Ignore local tool settings
sghf Sep 24, 2026
03caa51
Stop the request after a denied permission check
sghf Sep 24, 2026
9c25da4
Expose the node and service names on the log endpoints
sghf Sep 24, 2026
2d319f5
Expose the node and service names on the instance endpoints
sghf Sep 24, 2026
b9ebe8d
Expose the cluster name on the node endpoints
sghf Sep 24, 2026
7028caf
Fix the obsolescence setting date updates
sghf Sep 24, 2026
f12ebd0
Validate filter modifications
sghf Sep 24, 2026
5a5a9db
Validate filterset modifications
sghf Sep 24, 2026
04b8905
Type the node and service sub-resource responses
sghf Sep 24, 2026
15e72b6
Add POST /networks
sghf Sep 24, 2026
0805fdb
Add POST /users
sghf Sep 24, 2026
d5a8a4c
Expose the node and service names on the action queue
sghf Sep 24, 2026
9b7e891
Add POST /nodes/{node_id}/actions
sghf Sep 24, 2026
ed3ff17
Add service and instance action endpoints
sghf Sep 24, 2026
4d5b3d0
Add column filters to the list endpoints
sghf Sep 24, 2026
8ac29e4
Queue agent actions through PUT /actions
sghf Sep 24, 2026
c27cbcf
Return the total number of rows with paginated lists
sghf Sep 24, 2026
48b0093
Install git in the image
sghf Sep 24, 2026
0832d25
Port the /form* endpoints
sghf Sep 24, 2026
eba9a89
Add the GET /packages endpoint
sghf Sep 28, 2026
25f4f6f
Type the node compliance list responses
sghf Sep 28, 2026
0536080
Filter and join nodes in GET /nodes/hardware
sghf Sep 28, 2026
0c45106
Add the GET /workflows endpoint
sghf Sep 28, 2026
477c7c5
Filter GET /workflows on the caller's team
sghf Sep 28, 2026
a9e733a
Share the HTTP error helpers of the forms handlers
sghf Sep 28, 2026
4b3d12b
Add the compliance status and logs endpoints
sghf Sep 28, 2026
2a59c8c
Add the compliance rulesets endpoints
sghf Sep 28, 2026
8853980
Add the compliance ruleset variables endpoints
sghf Sep 28, 2026
246ab6a
Add the compliance ruleset composition endpoints
sghf Sep 28, 2026
268715a
Add the compliance ruleset publication and responsible endpoints
sghf Sep 28, 2026
75a1bd2
Store the slave flag of compliance service attachments as T or F
sghf Sep 28, 2026
264e663
Add the compliance ruleset node and service attachment endpoints
sghf Sep 28, 2026
c3ab970
Add the compliance modulesets endpoints
sghf Sep 28, 2026
e35bdfb
Add the compliance moduleset modules endpoints
sghf Sep 28, 2026
1f18c25
Add the compliance moduleset composition endpoints
sghf Sep 28, 2026
a79aad8
Read the form-encoded body of DELETE requests
sghf Sep 28, 2026
6bcc4f9
Add the compliance moduleset publications and responsibles endpoints
sghf Sep 28, 2026
633a803
Add the compliance moduleset node and service attachment endpoints
sghf Sep 28, 2026
e20592f
Add the compliance import and export endpoints
sghf Sep 28, 2026
a67e61c
Add a list of the modules of every moduleset
sghf Sep 28, 2026
6f74ffd
Add a list of the variables of every ruleset
sghf Sep 28, 2026
4d97723
Name the node and service of compliance runs and filter them by filte…
sghf Sep 28, 2026
6d858de
Let users change their own password
sghf Sep 29, 2026
6615a77
Add a list of the networks and the creation of network segments
sghf Sep 29, 2026
28c8329
Let the list of node addresses be sorted and filtered
sghf Sep 29, 2026
4be4c58
Require 12 characters for a password change
sghf Sep 29, 2026
d6c587e
Add a global search and filters to five lists
sghf Sep 29, 2026
b0b3454
Receive the SAN switch configurations the agents inventory
cvaroqui Sep 29, 2026
93ffff4
Confine the sysreport files to the node tree, and take full and delet…
cvaroqui Sep 29, 2026
405ff54
Let users change their own name and email
sghf Sep 29, 2026
8a14f4c
Let UserManagers change other users' name and email
sghf Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
!Makefile
.idea
core/version/version.go
.trash
.trash
.claude
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ RUN echo "Cache busted at $(date): oc3 version: $(./dist/oc3 version)"
FROM alpine:3.22.4
ARG BUILDTIME

RUN apk add --no-cache bash
# git keeps the history of the form definitions (forms revisions) and of
# the sysreports.
RUN apk add --no-cache bash git

COPY --from=builder /opt/oc3/dist/oc3 /usr/bin/oc3

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ worker.fast:
- "instance_action"
- "node_disk"
- "object_config"
- "san_switch"
- "system"
```

Expand Down
4 changes: 4 additions & 0 deletions cachekeys/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const (
FeedNodeDiskQ = "oc3:q:feed_node_disk"
FeedNodeDiskPendingH = "oc3:h:feed_node_disk_pending"

FeedSANSwitchH = "oc3:h:feed_san_switch"
FeedSANSwitchQ = "oc3:q:feed_san_switch"
FeedSANSwitchPendingH = "oc3:h:feed_san_switch_pending"

FeedObjectConfigForClusterIDH = "oc3:h:feed_object_config_for_cluster_id"

FeedObjectConfigH = "oc3:h:feed_object_config"
Expand Down
145 changes: 143 additions & 2 deletions cdb/db_action_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,19 @@ func (oDb *DB) UpdateActionStatus(ctx context.Context, id int64, status string,
return nil
}

// actionQueueJoins makes the "nodes." and "services." props selectable: a queued
// action carries only the ids, and a node action names no service (empty svc_id),
// hence the LEFT JOINs. Same approach as logJoins.
const actionQueueJoins = " LEFT JOIN nodes ON nodes.node_id = action_queue.node_id" +
" LEFT JOIN services ON services.svc_id = action_queue.svc_id"

func buildActionsQuery(p ListParams, idCond string, idArgs []any) (string, []any, error) {
if len(p.SelectExprs) == 0 {
return "", nil, fmt.Errorf("buildActionsQuery: no columns selected")
}

sb := &strings.Builder{}
fmt.Fprintf(sb, "SELECT %s\nFROM action_queue", strings.Join(p.SelectExprs, ", "))
fmt.Fprintf(sb, "SELECT %s\nFROM action_queue%s", strings.Join(p.SelectExprs, ", "), actionQueueJoins)

var conds []string
var args []any
Expand All @@ -92,7 +98,7 @@ func buildActionsQuery(p ListParams, idCond string, idArgs []any) (string, []any
conds = append(conds, "1=0")
} else {
conds = append(conds,
"node_id IN ("+
"action_queue.node_id IN ("+
"SELECT n.node_id FROM nodes n"+
" JOIN apps a ON n.app = a.app"+
" JOIN apps_responsibles ar ON ar.app_id = a.id"+
Expand All @@ -103,6 +109,12 @@ func buildActionsQuery(p ListParams, idCond string, idArgs []any) (string, []any
}
}

// Column filters of the request: the nodes and services joins are always in
// place here, so a filter on their names needs nothing more.
filterConds, filterArgs := p.FilterConditions()
conds = append(conds, filterConds...)
args = append(args, filterArgs...)

if len(conds) > 0 {
sb.WriteString("\nWHERE " + strings.Join(conds, " AND "))
}
Expand Down Expand Up @@ -147,3 +159,132 @@ func (oDb *DB) GetActionOne(ctx context.Context, id string, p ListParams) ([]map

return scanRowsToMaps(rows, p.Props, p.TypeHints)
}

// NodeActionTarget is what building an agent action command needs from a node.
type NodeActionTarget struct {
NodeID string
Nodename string
OSName string
ActionType string
Collector string
ConnectTo string
}

// NodeActionTargetByID returns the node fields the action queue entry is built from.
func (oDb *DB) NodeActionTargetByID(ctx context.Context, nodeID string) (*NodeActionTarget, error) {
const query = "SELECT node_id, COALESCE(nodename, ''), COALESCE(os_name, ''), COALESCE(action_type, '')," +
" COALESCE(collector, ''), COALESCE(connect_to, '') FROM nodes WHERE node_id = ?"
var t NodeActionTarget
err := oDb.DB.QueryRowContext(ctx, query, nodeID).
Scan(&t.NodeID, &t.Nodename, &t.OSName, &t.ActionType, &t.Collector, &t.ConnectTo)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, nil
case err != nil:
return nil, fmt.Errorf("nodeActionTargetByID: %w", err)
}
return &t, nil
}

// NodeReachableAddress returns the address an action should connect to, mirroring
// get_reachable_name() of the python collector: the explicit connect_to, else the
// best routable address of the node, else its name.
func (oDb *DB) NodeReachableAddress(ctx context.Context, t *NodeActionTarget) (string, error) {
if t.ConnectTo != "" {
return t.ConnectTo, nil
}
const query = "SELECT addr FROM v_nodenetworks WHERE node_id = ?" +
" AND mask IS NOT NULL AND mask != '' AND flag_deprecated = 0" +
" AND net_gateway IS NOT NULL AND net_gateway != '' AND net_gateway != '0.0.0.0'" +
" ORDER BY prio DESC, type LIMIT 1"
var addr string
err := oDb.DB.QueryRowContext(ctx, query, t.NodeID).Scan(&addr)
switch {
case errors.Is(err, sql.ErrNoRows):
return t.Nodename, nil
case err != nil:
return "", fmt.Errorf("nodeReachableAddress: %w", err)
}
return addr, nil
}

// EnqueueNodeAction posts a node action to the action queue and returns its id.
func (oDb *DB) EnqueueNodeAction(ctx context.Context, nodeID, actionType, command, connectTo string, userID *int64) (int64, error) {
const query = "INSERT INTO action_queue (node_id, svc_id, action_type, command, user_id, connect_to)" +
" VALUES (?, '', ?, ?, ?, ?)"
res, err := oDb.ExecContext(ctx, query, nodeID, actionType, command, userID, connectTo)
if err != nil {
return 0, fmt.Errorf("enqueueNodeAction: %w", err)
}
id, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("enqueueNodeAction lastInsertId: %w", err)
}
oDb.SetChange("action_queue")
return id, nil
}

// ServiceLiveNode returns a node of the service seen alive in the last 15 minutes,
// as get_svc_live_nodes() does, plus the agent version an action command needs.
func (oDb *DB) ServiceLiveNode(ctx context.Context, svcID string) (*NodeActionTarget, string, error) {
const query = "SELECT nodes.node_id, COALESCE(nodes.nodename, ''), COALESCE(nodes.os_name, '')," +
" COALESCE(nodes.action_type, ''), COALESCE(nodes.collector, ''), COALESCE(nodes.connect_to, '')," +
" COALESCE(nodes.version, '')" +
" FROM svcmon JOIN nodes ON nodes.node_id = svcmon.node_id" +
" WHERE svcmon.svc_id = ? AND nodes.last_comm > NOW() - INTERVAL 15 MINUTE" +
" ORDER BY nodes.nodename LIMIT 1"
var t NodeActionTarget
var version string
err := oDb.DB.QueryRowContext(ctx, query, svcID).
Scan(&t.NodeID, &t.Nodename, &t.OSName, &t.ActionType, &t.Collector, &t.ConnectTo, &version)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, "", nil
case err != nil:
return nil, "", fmt.Errorf("serviceLiveNode: %w", err)
}
return &t, version, nil
}

// NodeAgentVersion returns the agent version reported by a node, empty when unknown.
func (oDb *DB) NodeAgentVersion(ctx context.Context, nodeID string) (string, error) {
var version string
err := oDb.DB.QueryRowContext(ctx, "SELECT COALESCE(version, '') FROM nodes WHERE node_id = ?", nodeID).Scan(&version)
switch {
case errors.Is(err, sql.ErrNoRows):
return "", nil
case err != nil:
return "", fmt.Errorf("nodeAgentVersion: %w", err)
}
return version, nil
}

// HasServiceInstance reports whether the service runs on that node.
func (oDb *DB) HasServiceInstance(ctx context.Context, svcID, nodeID string) (bool, error) {
var one int
err := oDb.DB.QueryRowContext(ctx,
"SELECT 1 FROM svcmon WHERE svc_id = ? AND node_id = ? LIMIT 1", svcID, nodeID).Scan(&one)
switch {
case errors.Is(err, sql.ErrNoRows):
return false, nil
case err != nil:
return false, fmt.Errorf("hasServiceInstance: %w", err)
}
return true, nil
}

// EnqueueServiceAction posts a service action to the action queue and returns its id.
func (oDb *DB) EnqueueServiceAction(ctx context.Context, nodeID, svcID, actionType, command, connectTo string, userID *int64) (int64, error) {
const query = "INSERT INTO action_queue (node_id, svc_id, action_type, command, user_id, connect_to)" +
" VALUES (?, ?, ?, ?, ?, ?)"
res, err := oDb.ExecContext(ctx, query, nodeID, svcID, actionType, command, userID, connectTo)
if err != nil {
return 0, fmt.Errorf("enqueueServiceAction: %w", err)
}
id, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("enqueueServiceAction lastInsertId: %w", err)
}
oDb.SetChange("action_queue")
return id, nil
}
13 changes: 8 additions & 5 deletions cdb/db_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func scanApps(rows *sql.Rows) ([]App, error) {
return apps, nil
}

func buildAppsQuery(groups []string, isManager bool, selectExprs []string) (string, []any, error) {
func buildAppsQuery(groups []string, isManager bool, selectExprs []string, filters []ColumnFilter) (string, []any, error) {
q := From(schema.TApps).
Distinct().
RawSelect(selectExprs...)
Expand All @@ -75,6 +75,9 @@ func buildAppsQuery(groups []string, isManager bool, selectExprs []string) (stri
q = q.Where(schema.AppsID, ">", 0)
}

// Column filters of the request, ANDed with the access control above.
q = q.WhereFilters(filters)

query, args, err := q.Build()
if err != nil {
return "", nil, fmt.Errorf("buildAppsQuery: %w", err)
Expand All @@ -87,11 +90,11 @@ func buildAppsQueryAll(groups []string, isManager bool) (string, []any, error) {
"apps.id", "apps.app",
"COALESCE(apps.updated, '')", "COALESCE(apps.app_domain, '')",
"COALESCE(apps.app_team_ops, '')", "COALESCE(apps.description, '')",
})
}, nil)
}

func (oDb *DB) GetApps(ctx context.Context, p ListParams) ([]map[string]any, error) {
query, args, err := buildAppsQuery(p.Groups, p.IsManager, p.SelectExprs)
query, args, err := buildAppsQuery(p.Groups, p.IsManager, p.SelectExprs, p.Filters)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -366,7 +369,7 @@ func (oDb *DB) GetAppNodes(ctx context.Context, appIDOrName string, p ListParams
}
}

query, args, err := buildNodesQuery(p.Groups, p.IsManager, p.SelectExprs)
query, args, err := buildNodesQuery(p.Groups, p.IsManager, p.SelectExprs, p.Filters)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -407,7 +410,7 @@ func (oDb *DB) GetAppServices(ctx context.Context, appIDOrName string, p ListPar
}
}

query, args, err := buildServicesQuery(p.Groups, p.IsManager, p.SelectExprs)
query, args, err := buildServicesQuery(p.Groups, p.IsManager, p.SelectExprs, p.Filters)
if err != nil {
return nil, err
}
Expand Down
5 changes: 5 additions & 0 deletions cdb/db_auth_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func (oDb *DB) GetGroups(ctx context.Context, p ListParams) ([]map[string]any, e
args = append(args, stringsToAny(cleanG)...)
}
}
filterConds, filterArgs := p.FilterConditions()
for _, cond := range filterConds {
query += " AND " + cond
}
args = append(args, filterArgs...)
if gb := p.GroupByClause(""); gb != "" {
query += " " + gb
}
Expand Down
Loading
Loading