Build and publish / Test and build (darwin) (pull_request) Successful in 1m52s
Build and publish / Test and build (linux) (pull_request) Successful in 2m30s
Build and publish / Test and build (windows) (pull_request) Successful in 3m7s
Build and publish / Test and build (windows) (push) Successful in 3m7s
Build and publish / Test and build (darwin) (push) Successful in 1m45s
Build and publish / Test and build (linux) (push) Successful in 2m12s
Build and publish / Build and publish Docker image (pull_request) Successful in 1m49s
Build and publish / Build and publish Docker image (push) Successful in 2m4s
247 lines
6.4 KiB
Go
247 lines
6.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"ombi-mcp/internal/ombi"
|
|
)
|
|
|
|
// read_server — status, version, features, stats and the
|
|
// administrator-gated cron validation. Families: metrics,
|
|
// reference_page.
|
|
func handleServer(ctx context.Context, env *Env, raw json.RawMessage) *ToolResult {
|
|
o := newOp(ctx, env, "read_server", false)
|
|
var a ServerArgs
|
|
if fail := o.args(raw, &a); fail != nil {
|
|
return fail
|
|
}
|
|
switch a.Action {
|
|
case "status":
|
|
return o.serverStatus()
|
|
case "status_info":
|
|
return o.serverStatusInfo()
|
|
case "about":
|
|
return o.serverAbout()
|
|
case "update_info":
|
|
return o.serverUpdateInfo()
|
|
case "update_check":
|
|
return o.serverUpdateCheck()
|
|
case "landing":
|
|
return o.serverLanding()
|
|
case "features":
|
|
return o.serverFeatures()
|
|
case "stats":
|
|
return o.serverStats(&a)
|
|
case "cron_validate":
|
|
return o.serverCronValidate(&a)
|
|
default:
|
|
return o.invalid("action", "unsupported action %q", a.Action)
|
|
}
|
|
}
|
|
|
|
func (o *op) serverStatus() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/Status", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
v, fail := o.decodeScalar(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
val, ok := toInt(v)
|
|
if !ok {
|
|
if s, ok2 := v.(string); ok2 {
|
|
// Status may serialize as a named status string.
|
|
return o.ok(&Metrics{Kind: "metrics",
|
|
Values: []Metric{metric("status", s, "instance", "")}})
|
|
}
|
|
return o.fail("UPSTREAM_SCHEMA_MISMATCH", "upstream status was not a scalar", false)
|
|
}
|
|
return o.ok(&Metrics{Kind: "metrics",
|
|
Values: []Metric{metric("status", val, "instance", "")}})
|
|
}
|
|
|
|
func (o *op) serverStatusInfo() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/Status/info", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
v, fail := o.decodeScalar(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
return o.ok(&Metrics{Kind: "metrics",
|
|
Values: []Metric{metric("status_info", v, "instance", "")}})
|
|
}
|
|
|
|
// serverAbout projects AboutViewModel excluding connection strings.
|
|
func (o *op) serverAbout() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/Settings/about", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
m, fail := o.decodeObject(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
vals := []Metric{}
|
|
// Connection-string fields are never projected.
|
|
for _, k := range []string{"version", "branch", "frameworkDescription",
|
|
"osArchitecture", "osDescription", "processArchitecture",
|
|
"ombiDatabaseType", "externalDatabaseType", "settingsDatabaseType",
|
|
"notSupported"} {
|
|
if v, ok := m[k]; ok {
|
|
switch t := v.(type) {
|
|
case string, bool, float64:
|
|
vals = append(vals, metric(k, t, "instance", ""))
|
|
}
|
|
}
|
|
}
|
|
return o.ok(&Metrics{Kind: "metrics", Values: vals})
|
|
}
|
|
|
|
func (o *op) serverUpdateInfo() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/Update", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
m, fail := o.decodeObject(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
vals := []Metric{}
|
|
for k, v := range m {
|
|
switch t := v.(type) {
|
|
case string, bool, float64:
|
|
vals = append(vals, metric(k, t, "instance", ""))
|
|
}
|
|
}
|
|
vals = capItems(o, vals)
|
|
return o.ok(&Metrics{Kind: "metrics", Values: vals})
|
|
}
|
|
|
|
func (o *op) serverUpdateCheck() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/Job/update", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
v, fail := o.decodeScalar(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
return o.ok(&Metrics{Kind: "metrics",
|
|
Values: []Metric{metric("update_available", v, "instance", "")}})
|
|
}
|
|
|
|
func (o *op) serverLanding() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/LandingPage", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
m, fail := o.decodeObject(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
vals := []Metric{}
|
|
for k, v := range m {
|
|
switch t := v.(type) {
|
|
case string, bool, float64:
|
|
vals = append(vals, metric(k, t, "instance", ""))
|
|
}
|
|
}
|
|
vals = capItems(o, vals)
|
|
return o.ok(&Metrics{Kind: "metrics", Values: vals})
|
|
}
|
|
|
|
func (o *op) serverFeatures() *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v2/Features", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
items, fail := o.refArray(raw,
|
|
[]string{"name"}, []string{"name"}, "feature")
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
// Feature records carry enabled as their value.
|
|
var arr []map[string]any
|
|
_ = json.Unmarshal(raw, &arr)
|
|
for i, m := range arr {
|
|
if i < len(items) {
|
|
if v, ok := m["enabled"]; ok {
|
|
items[i].Value = v
|
|
}
|
|
}
|
|
}
|
|
return o.ok(&ReferencePage{Kind: "reference_page",
|
|
Items: items, Page: singlePage(len(items), "references")})
|
|
}
|
|
|
|
func (o *op) serverStats(a *ServerArgs) *ToolResult {
|
|
if !nonempty(a.From) || !nonempty(a.To) {
|
|
field := "from"
|
|
if nonempty(a.From) {
|
|
field = "to"
|
|
}
|
|
return o.invalid(field, "stats requires both from and to as RFC3339 date-time values")
|
|
}
|
|
// from <= to is enforced before the upstream call.
|
|
f, ferr := time.Parse(time.RFC3339, a.From)
|
|
t, terr := time.Parse(time.RFC3339, a.To)
|
|
if ferr != nil || terr != nil {
|
|
return o.invalid("from", "from/to must be RFC3339 date-time values")
|
|
}
|
|
if f.After(t) {
|
|
return o.invalid("from", "from must be on or before to")
|
|
}
|
|
q := map[string]string{"from": a.From, "to": a.To}
|
|
raw, fail := o.call("GET", "/api/v1/Stats", q, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
m, fail := o.decodeObject(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
// Only the seven numeric totals; nested user objects are dropped.
|
|
vals := []Metric{}
|
|
for _, k := range []string{"totalRequests", "totalMovieRequests",
|
|
"totalTvRequests", "totalIssues", "completedRequestsMovies",
|
|
"completedRequestsTv", "completedRequests"} {
|
|
if i, ok := toInt(m[k]); ok {
|
|
vals = append(vals, metric(k, i, "instance", "requests"))
|
|
}
|
|
}
|
|
return o.ok(&Metrics{Kind: "metrics", Values: vals})
|
|
}
|
|
|
|
func (o *op) serverCronValidate(a *ServerArgs) *ToolResult {
|
|
if !nonempty(a.Expression) {
|
|
return o.invalid("expression", "expression must contain non-whitespace text")
|
|
}
|
|
raw, fail := o.call("POST", "/api/v1/Settings/testcron", nil,
|
|
ombi.CronViewModelBody{Expression: a.Expression})
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
m, fail := o.decodeObject(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
vals := []Metric{}
|
|
if v, ok := m["success"]; ok {
|
|
if b, ok2 := v.(bool); ok2 {
|
|
vals = append(vals, metric("valid", b, "instance", ""))
|
|
}
|
|
}
|
|
if s := jstr(m, "message"); s != "" {
|
|
vals = append(vals, metric("message", s, "instance", ""))
|
|
}
|
|
if valid, ok := m["success"].(bool); ok && !valid {
|
|
o.warnf("Ombi validates Quartz cron syntax (6-7 fields: seconds minutes hours day-of-month month day-of-week [year]); use ? for the unused day-of-month/day-of-week field")
|
|
}
|
|
return o.ok(&Metrics{Kind: "metrics", Values: vals})
|
|
}
|