Skip to content

prepare api for oc3-frontend - #157

Merged
cgalibern merged 61 commits into
opensvc:mainfrom
cvaroqui:dev
Sep 30, 2026
Merged

cgalibern merged 61 commits into
opensvc:mainfrom
cvaroqui:dev

Conversation

@cvaroqui

Copy link
Copy Markdown
Member

No description provided.

sghf added 30 commits September 30, 2026 08:52
the node and service names on the alert endpoints
The directory holds per-machine tool settings, which do not belong in the
repository. The rule comes last, after the whitelist entries: the JSON
exception would otherwise bring its settings file back.
JSONProblemf returns nil once the problem response is written. The
require* guards and the service resolvers returned its result, so their
callers went on with the action after answering 401, 403 or 404. They now
go through denyRequest, which returns errRequestDenied; echo leaves the
already committed response untouched.

resolveService also selects svc_id: the query builder refuses a query
without selected columns, and the existence check failed with a 500.
A log event names its node and service by id only. GET /logs and
GET /logs/{log_id} now LEFT JOIN nodes and services, so that the
nodes.nodename and services.svcname props can be selected; most events
name neither, hence the outer joins. The responses are typed as
LogListResponse instead of the generic ListResponse.
An instance row carries only the service and node ids. The svcmon
mapping joins nodes and services, nodes through a LEFT JOIN so that an
instance whose node row is missing is not dropped.

orderby resolves a "table.column" prop through the mapping joins, as
props selection does, so that instances can be sorted by service or node
name. The instance responses are typed as InstanceListResponse.
A node names its cluster by id only. The node mapping joins the clusters
table, through a LEFT JOIN since a node may name no cluster or one the
collector does not know, and NodeRow gains clusters.cluster_name.
Dates are validated as YYYY-MM-DD or YYYY-MM-DD hh:mm:ss, and an empty
date clears the setting instead of being rejected by the datetime column.
The nodes take the setting's dates by a copy inside the database: a date
read back through the driver came in RFC 3339 form, which a datetime
column refuses in strict mode. The responses are typed as
ObsolescenceSettingListResponse.
POST /filters/{filter_id} validates the resulting definition with the
rules of POST /filters: known table and operator, existing column, no
other filter with the same definition (409). f_label, a generated column,
is refused. The transaction is committed before the filter is read back,
which returned the previous values otherwise. The filter responses are
typed as FilterListResponse.
fset_stats and f_log_op are declared as enums and checked on every
endpoint that sets them; a filterset name cannot be emptied nor take the
name of another one (409). The transaction is committed before the
filterset is read back. The filterset, composition, usage and export
responses get their own schemas.
GET /nodes/{node_id}/hardware, hbas, disks, ips and alerts, and
GET /services/{svc_id}/disks, now declare their row schemas instead of
the generic ListResponse.
Declare a network, with the NetworkManager privilege. The network address
must match the netmask; begin, end and broadcast are computed by the
database, and team_responsible defaults to the caller's primary group. A
name or an address already declared is refused with a 409.
Create a user with its private group, with the UserManager privilege. An
email or username already in use is refused with a 409. The password is
hashed in the web2py format the sign-in already verifies; without one,
the account exists but cannot sign in.
A queued action names its node, and its service when it has one, by id
only. GET /actions LEFT JOINs nodes and services so that their names can
be selected, and the access check qualifies node_id, now ambiguous.
Queue an agent action on a node, as the historical collector action menu
did: inventory pushes, checks, sysreport, scanscsi, freeze and thaw. The
command is queued bare for a pull or feed node and wrapped in an ssh call
for a push node, reaching the node by its connect_to or best routable
address. Requires the NodeExec privilege and responsibility for the node.
POST /services/{svc_id}/actions queues an action for a node of the
service seen alive in the last 15 minutes, acting on the whole service.
POST /services/{svc_id}/instances/{node_id}/actions acts on that instance
only, with --local for agents that understand it. Accepted actions are
push resinfo, push config, freeze and thaw. Requires the NodeExec
privilege and responsibility for the service.
GET /nodes, /services, /services_instances and /actions, and the filterset
node and service lists, accept a repeated filter=prop:expr parameter,
combined with AND: a substring by default, ~regex, in:a,b, eq/ne/gt/gte/
lt/lte comparisons, empty and !empty. Props resolve like orderby, joined
names included, and the query builder adds the join of a filtered column
even when it is not selected. An unknown prop or an invalid regular
expression is refused with a 400.
Replace the dedicated POST /nodes/{node_id}/actions,
POST /services/{svc_id}/actions and
POST /services/{svc_id}/instances/{node_id}/actions endpoints with
PUT /actions, as the historical collector's REST API and action menu do
(rest_put_action_queue, json_action_one). The body is one entry or a
list; node_id and svc_id queue an instance action, svc_id alone a service
action from a live node, node_id alone a node action, and vmname names
the node. rid limits an instance action to resources, entries differing
only by rid being merged into one action. One entry answers with the
queued action, a list with the queued actions and the refusals.

Instance actions follow do_instance_action: NodeExec privilege and
responsibility for the service, the node only having to exist. The agent
version is read past a leading "v", so that om3 agents get --local and an
instance freeze no longer freezes the whole object.
List endpoints fill meta.total whenever meta is requested, the default,
as the historical collector did. The total comes from the page when it
tells it (no limit, partial page, empty first page); otherwise the
fetcher runs again with the same access control, filters and grouping,
selecting COUNT(*) OVER () without sort for one row, so that a grouped
list counts its groups. GET /alerts and GET /tags return it too. A failed
count is logged and the total left out.
The sysreports history is committed with git, and so is the history of
the form definitions: the image only had bash, so neither could work.
Port the forms API of the historical collector (api_forms.py, the forms
part of api_workflows.py and lib_forms.py), keeping its behavior:

- /forms and /forms/{form_id}: list, show, create, modify and delete,
  with the FormsManager privilege, the publication and responsibility
  checks and the collector's messages. The body may be one entry or a
  list, JSON or form-encoded. form_definition is form_yaml parsed.
- /forms/{form_id}/publications and /responsibles, and the bulk
  /forms_publications and /forms_responsibles.
- /forms/{form_id}/am_i_responsible, answering {"data": bool}.
- /forms/{form_id}/revisions, /revisions/{cid}, /diff/{cid} and
  /rollback/{cid}, over one git repository per form under
  server.directories.forms.
- PUT /forms/{form_id}: submission. The data is validated against the
  form inputs (conditions, forced keys, strict static and dynamic
  candidates), a results record is stored, and the outputs run: db,
  rest (calling this API as the submitter, or an external url, with the
  vm2 mangler), script, mail (server.mail.*) and workflow. Async forms
  answer at once and run in the background. Internal forms, the
  negative ids, are embedded.
- /form_output_results/{results_id} (GET, PUT), /forms_revisions and
  /forms_store, with /forms_store/{store_id}/dump.

List endpoints may declare virtual props, computed after the fetch
from the props they require, as the collector's vprops.
List the packages installed on the nodes the user can see, as the
historical packages table: joined with nodes, whose app scopes the
access, and left-joined with pkg_sig_provider for the provider of the
signing key (sig_provider). Ordered by node name, package name and
architecture by default; the nodes. props come through the join, and
the list takes the filter parameter and returns its total.

The response is typed in the spec (PackageRow, PackageListResponse).
sghf and others added 29 commits September 30, 2026 08:57
List the requests: the workflows started by the submission of a form
with a workflow output, as the historical rest_get_workflows, to any
authenticated user. The workflows query left-joins forms_revisions by
form_md5, so that the workflow mapping also offers form_name,
form_folder and form_yaml; they stay out of the default props, which
keeps the /forms_store dump output unchanged. The list takes the filter
parameter, and its response is typed (WorkflowRow, WorkflowListResponse).
The assigned parameter keeps the pending workflows related to the
caller's team, their full name and the roles of their non-privilege
groups, as the forced filters of the historical tables: "team" those
whose last assignee is in the team (Assigned to my team), "tiers" those
created by the team and assigned to someone else (Pending tiers
action). An unknown value is a 400.
formError, formErrorf, formProblem and formInternal carry an HTTP status
through the handlers and write it once, as the historical collector
raised HTTP(status, message). They are not specific to forms: renamed
httpError, httpErrorf, httpProblem and httpInternal, they move to
problems.go for the compliance handlers to use as well.
Port the /compliance/logs and /compliance/status endpoints of the
historical api_compliance: GET /compliance/logs and /logs/{log_id}, GET
/compliance/status and /status/{status_id}, scoped as q_filter(node_field)
to the nodes whose app is published to one of the caller's groups; DELETE
/compliance/status/{status_id} and its bulk form DELETE /compliance/status
({"id": ...}), which require the CompExec privilege and the node's
responsibility, and log the deletion.

The run and log rows are typed (ComplianceLogRow and its list response;
ComplianceStatusListResponse). The fset-id filterset of the historical
lists is not supported, as nowhere else in oc3; nor is the refresh of the
"compliance differences in cluster" dashboard alert, which oc3 does not
compute.
Port the ruleset endpoints of the historical api_compliance:
- GET /compliance/rulesets and /rulesets/{rset_id} (id or name), the
  rulesets published to one of the caller's groups;
- POST /compliance/rulesets, creating a ruleset published to and under
  the responsibility of the caller's default group (the Manager group
  without one), or updating it when the name exists;
- POST /compliance/rulesets/{rset_id}, updating its name, type or
  publicity, checked;
- PUT /compliance/rulesets/{rset_id} action=clone, copying its variables,
  children and, for a contextual ruleset, its filterset as <name>_clone;
- DELETE /compliance/rulesets/{rset_id} and the bulk DELETE
  /compliance/rulesets ({"id": ...}), removing its relations;
- GET /compliance/rulesets/{rset_id}/usage and /am_i_responsible.

Changes require the CompManager privilege and, but for creation, the
responsibility of the ruleset; refusals are 403, unknown rulesets 404,
taken names 409. Every change is logged, and creation, renaming, cloning
and deletion rebuild comp_rulesets_chains, as comp_rulesets_chains().
Bodies may be JSON or form-encoded, as the historical examples post.
Port the ruleset variable endpoints of the historical api_compliance:
- GET /compliance/rulesets/{rset_id}/variables and /variables/{var_id}
  (id or name), for a ruleset published to the caller, to one of their
  groups or to Everybody;
- POST /compliance/rulesets/{rset_id}/variables and its form naming the
  ruleset in the body, POST /compliance/rulesets_variables, creating a
  variable or updating the one of that name;
- POST /compliance/rulesets/{rset_id}/variables/{var_id}, updating its
  name, class or value; a name taken in the ruleset is a 409;
- PUT /compliance/rulesets/{rset_id}/variables/{var_id} action=copy or
  move to dst_ruleset;
- DELETE /compliance/rulesets/{rset_id}/variables/{var_id}.

Changes require the CompManager privilege and the responsibility of the
ruleset (for a copy, the publication of the source), record the author
and the date, and are logged. var_value may be any JSON value, stored
serialized. Two departures from the historical handlers: the deletion
checks the privilege and the responsibility, as its description says,
where the code only checked the publication; and a copy or a move to a
ruleset already holding a variable of that name is refused.

The ruleset_variable props mapping and the RulesetVariableRow schema
describe the rows.
Port the ruleset hierarchy and filterset endpoints of the historical
api_compliance:
- POST and DELETE /compliance/rulesets/{rset_id}/rulesets/{child_rset_id},
  as attach_ruleset_to_ruleset() and detach_ruleset_from_ruleset(): by a
  CompManager responsible for the parent, the child being published to
  them for a detach. Attaching a ruleset to itself (400), twice or under
  one of its descendants (409) is refused; the ruleset chains are rebuilt.
- POST and DELETE /compliance/rulesets/{rset_id}/filtersets/{fset_id}, the
  filterset of a contextual ruleset, replaced on attach and removed
  whichever it is on detach, as the historical handlers do.

Rulesets and filtersets are given by id or name; changes are logged.
Port the ruleset group endpoints of the historical api_compliance:
- GET /compliance/rulesets/{rset_id}/publications and /responsibles, the
  groups of a ruleset, a non-manager seeing only their own among them;
- POST and DELETE /compliance/rulesets/{rset_id}/publications/{group_id}
  and /responsibles/{group_id}, and their bulk forms naming both in the
  body, /compliance/rulesets_publications and /rulesets_responsibles.

As attach_group_to_ruleset() and detach_group_from_ruleset(), changes
require the CompManager privilege and the responsibility of the ruleset;
a non-manager may attach only one of their groups; Everybody may not be
made responsible, nor be given the publication of a contextual ruleset;
an already attached group is answered with an information. Groups are
given by id or role; changes are logged.

The group logic is written once over a CompKind naming the tables of a
kind, for the modulesets to use as well.
comp_rulesets_services.slave and comp_modulesets_services.slave are
varchar(1) columns the historical collector fills with "T" or "F", as
its web2py boolean fields do. oc3 passed a Go boolean, stored as "1" or
"0", which the historical collector reads as false; and compared the
column with a boolean, which MySQL casts "T" and "F" to 0 alike, so that
the non-encapsulated queries also matched encapsulated attachments.

Attachments are now stored "T" or "F"; queries read "T" and "1" as true,
anything else as false, so that rows stored earlier keep their meaning.
Port the ruleset attachment endpoints of the historical api_compliance:
- GET /compliance/rulesets/{rset_id}/nodes and /services (slave for the
  encapsulated services), what a ruleset published to the caller is
  attached to, among the nodes and services the caller may see;
- GET /compliance/rulesets/{rset_id}/candidate_nodes and
  /candidate_services, what it may be attached to: the nodes whose
  responsible team, the services whose app has a responsible group, among
  the ruleset's publication groups, not attached yet;
- POST and DELETE /compliance/rulesets_nodes and /rulesets_services, the
  bulk forms naming the ruleset (ruleset_id or ruleset_name), the node or
  the service and the slave flag (encap accepted) in the body, handled as
  the existing /nodes/{node_id}/compliance/rulesets/{rset_id} and
  /services/{svc_id}/compliance/rulesets/{rset_id}.

The list queries are written over the CompKind, which names the node and
service attachment tables too, for the modulesets to use as well.
Port the moduleset endpoints of the historical api_compliance:
- GET /compliance/modulesets and /modulesets/{modset_id} (id or name),
  the modulesets published to one of the caller's groups;
- POST /compliance/modulesets, creating a moduleset authored by the
  caller, published to and under the responsibility of their default
  group; a taken name is a 409;
- POST /compliance/modulesets/{modset_id}, renaming it and recording the
  update date;
- PUT /compliance/modulesets/{modset_id} action=clone, copying its
  modules, rulesets and children as <name>_clone;
- DELETE /compliance/modulesets/{modset_id} and the bulk DELETE
  /compliance/modulesets ({"id": ...}), removing its relations;
- GET /compliance/modulesets/{modset_id}/usage (the parent modulesets)
  and /am_i_responsible.

Changes require the CompManager privilege and, but for creation, the
responsibility of the moduleset; they are logged. The visibility and the
publication checks, Everybody included, are written over the CompKind.
List, show, create, update and delete the modules of a moduleset, as
the historical collector's /compliance/modulesets/<id>/modules
handlers. A module name is unique in its moduleset, the autofix flag is
stored as T or F, and changes require the CompManager privilege and
the responsibility of the moduleset.
Attach and detach child modulesets and rulesets of a moduleset, as the
historical /compliance/modulesets/<id>/modulesets/<id> and
/compliance/modulesets/<id>/rulesets/<id> handlers. Unlike the
historical collector, an attachment closing a moduleset loop is refused,
and the attached moduleset or ruleset must be published to the caller.
The loop walk is shared with the ruleset composition.
The standard library parses the form body of POST, PUT and PATCH
requests only, so a DELETE sent with curl -d, as the historical
examples of the bulk detach endpoints do, reached the handlers without
its keys.
List, attach and detach the publication and responsible groups of a
moduleset, by path or with the bulk /compliance/modulesets_publications
and /compliance/modulesets_responsibles forms, sharing the ruleset
rules: a CompManager responsible for the moduleset may attach only
their own groups, and Everybody may not be made responsible.
List the nodes and services a moduleset is attached to and its
candidates, and attach or detach a moduleset named in the body with the
bulk /compliance/modulesets_nodes and /compliance/modulesets_services
forms, which delegate to the node and service compliance handlers.
Export rulesets and modulesets, one or all those published to the
caller, with their descendants and the filtersets and rulesets they
use, and import such an export, as the historical /compliance/import
and /compliance/{rulesets,modulesets}/export handlers.

Objects existing by name are reused and the missing relations added.
Unlike the historical import, it requires the CompManager privilege,
adds to an existing ruleset or moduleset only when the caller is
responsible for it, refuses relations closing a loop, and runs in a
transaction so that a refused import leaves nothing behind.
GET /compliance/modulesets_modules lists, as the historical v_comp_modulesets
view, one row per module with its moduleset and the moduleset's responsible
and publication teams; a moduleset without module is listed on a row of its
own, with a zero module id. The rows are restricted to the modulesets
published to the caller, all of them for a manager.

The teams come from derived tables whose columns are declared in cdb, so
they can be filtered and sorted like the other props.
GET /compliance/rulesets_variables lists, as the historical v_comp_rulesets
view, the variables of each ruleset visible to the caller: its own, then
those of each ruleset it encapsulates, found through comp_rulesets_chains
and named by encap_rset and chain, with the ruleset's filterset and its
responsible and publication teams. A ruleset of the chain without variable
is listed on a row of its own, with a zero variable id.

The team join of the modulesets list becomes compTeamsJoin, shared by both
lists through their CompKind.
…rset

GET /compliance/logs joins the nodes and services tables, so the
nodes.* and services.* props give the names of a run's node and service,
and accepts fset_id, a filterset id or name restricting the runs to the
nodes it selects, as apply_filters_id() does on the node of a run. An
unknown filterset is a 404.
POST /users/self/password takes the current password, checked as at sign
in, and a new one of at least 8 characters, different from the current
one. The new password is stored as a web2py hash, accepted by both
collectors, and the change is logged without it. No privilege is required:
knowing the current password is the proof.
GET /networks lists the declared networks to any authenticated user, as
the historical collector did.

POST /networks/{net_id}/segments ports rest_post_network_segments: it
requires the NetworkManager privilege and, unless the caller is a
manager, membership of the network's responsible team. The range must be
IPv4, lie inside the network and not overlap another segment of it
(409). The caller's primary group becomes responsible for the segment,
and the creation is logged as networks.segment.create.
Each prop of the node_ip mapping keeps its expression and now names the
v_nodenetworks column it reads, so orderby, groupby and filter accept
them. GET /ips declares the filter parameter and applies its conditions.
POST /users/self/password refuses a new password shorter than 12
characters, counted in characters rather than bytes. User creation keeps
its own minimum.
GET /search looks for the text in 14 kinds of objects, as the historical
collector's search did: nodes, services, instances, applications, node
addresses, disks, tags, users, teams, requests, modulesets, rulesets,
filtersets and forms. Each kind is read through its own list and access
control, with a case-insensitive "contains" match on its identifying
props, all kinds at once. A request also matches by its number, private
user groups are left out of the teams. Groups come with more set when
other objects match beyond the limit, and a kind that fails reports it
without failing the others.

The Apps, Disks, Users, Groups and Filtersets lists now apply the column
filters they were given and declare the filter parameter.
The om3 agents inventory the SAN switches their configuration declares, and
fed them to the old collector only, through its update_brocade rpc, which
stored the command outputs and parsed them into the switches, san_zone and
san_zone_alias tables. oc3 had no feed for it, and will drop the rpc api.

The feeder serves POST /sanswitch, taking the switch name, its type and the
raw output of each command, by name: switchshow, nsshow and zoneshow for a
brocade. The report is keyed by switch, as <type>@<name>, and not by node:
a couple of nodes of the whole infrastructure inventory the switches, and
the latest report of a switch is the one to parse.

The sanSwitch job, on the san_switch queue, parses the outputs with a port
of the old collector parser, quirks included, and replaces the rows of the
switch, of the aliases of each of its configurations, and of the zones of
its effective one: the rows are upserted on the unique keys of the tables,
and the older rows deleted. A switchshow without its port table is refused
rather than read as a switch with no port, which would delete them all.

A deployment serves the feed once a worker lists san_switch in its queues.
…ions-only reports

The members of a sysreport archive and the deleted files were joined to the
tree of the node as given: a name climbing with ".." wrote or removed files
out of it. Every path is now rooted at the tree of the node, where a ".."
stops, and only the regular files of the archive are written, each closed
once written rather than at the end of the request.

The archive is optional: a report of the deletions of a node has none, and
was refused.

A full report, the full field set, holds every file and command output of
the node: the files the collector holds for the node and the archive does
not are removed, the git history of the tree left alone. It replaces the
listing of what the collector holds, which the agent asked the old
collector for to compute the deletions of a forced report.
POST /users/{user_id} changes the first name, last name or email of the
signed-in user, as the profile form of the historical collector allows:
only the caller's own account, without privilege. Names are trimmed and
at most 128 characters; the email must be a valid address, at most 512
characters and not used by another user. The change is logged with the
fields changed, and the old and new email.
POST /users/{user_id} now changes another user's first name, last name
or email when the caller has the UserManager privilege, or is a Manager,
as rest_post_user did. Other callers are refused with a 403, an unknown
user with a 404.
@cgalibern
cgalibern merged commit ca138b1 into opensvc:main Sep 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants