mirror of
https://github.com/anand34577/ferrum.git
synced 2026-09-12 05:48:58 +00:00
22c1dd382c
- User-scoped API keys (Profile > API Keys) for 3rd-party REST API access
and MCP clients, each locked to one scope at creation, with expiry,
revocation, and last-used tracking.
- A hand-rolled MCP (Model Context Protocol) server exposing the fleet
(connections, nodes, guests, storage, pools, alerts, cluster status) as
read tools plus one admin-gated power-action tool, so Claude Code/Desktop
or any other MCP client can query and operate the fleet directly.
- Both the REST API and MCP are off by default and toggleable instance-wide
from Settings > API & MCP, enforced live on every request.
- Admin-managed AI providers (any OpenAI-chat-completions-compatible
endpoint) backing the AI Assistant's tool-calling loop, replacing the
single hardcoded provider.
- A built-in, zero-config, no-API-key local provider backed by Needle 2
(internal/needle) for fully offline tool-calling, wired in as a one-click
preset. Requires the operator to separately download the Needle 2 binary
and point FERRUM_NEEDLE_BIN at it -- Ferrum never fetches executable
content from the network itself; see README "Built-in LLM (Needle 2)".
- System settings (CORS allow-list, instance-wide toggles) moved to the
admin Settings UI; environment variables are now scoped to true
bootstrap-level config only (listen address, TLS, DB connection, secret,
optional Needle binary path).
- Fixed: node Journal tab 502'ing with "unexpected end of JSON input" on an
empty response, and separately with a decode error on PVE versions that
return a bare-string journal line instead of the documented {n,t} object.
- Fixed: bottom content padding disappearing on every page except the AI
Assistant (an unconditional h-full on the content wrapper let overflowing
content bleed through where the padding should render).
- Fixed: Profile page felt cramped despite a wide viewport (stray max-w-2xl
cap not present on the equivalent Settings page).
- Test coverage added for the previously-untested MCP package and the new
Needle adapter (20 new Go tests), plus a regression test for the journal
decode fix.
114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
package pve
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
)
|
|
|
|
// ReplicationJob is one PVE storage replication (pvesr) job.
|
|
type ReplicationJob struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Source string `json:"source,omitempty"`
|
|
Target string `json:"target"`
|
|
Schedule string `json:"schedule,omitempty"`
|
|
Guest int `json:"guest,omitempty"`
|
|
Disable int `json:"disable,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
}
|
|
|
|
func (c *Client) ReplicationJobs(ctx context.Context) ([]ReplicationJob, error) {
|
|
var out struct {
|
|
Data []ReplicationJob `json:"data"`
|
|
}
|
|
if err := c.get(ctx, "/cluster/replication", &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out.Data, nil
|
|
}
|
|
|
|
// ReplicationStatus is one node's view of a replication job's last run.
|
|
type ReplicationStatus struct {
|
|
ID string `json:"id"`
|
|
LastSync int64 `json:"last_sync,omitempty"`
|
|
NextSync int64 `json:"next_sync,omitempty"`
|
|
Duration float64 `json:"duration,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
FailCount int `json:"fail_count,omitempty"`
|
|
}
|
|
|
|
func (c *Client) NodeReplicationStatus(ctx context.Context, node string) ([]ReplicationStatus, error) {
|
|
var out struct {
|
|
Data []ReplicationStatus `json:"data"`
|
|
}
|
|
if err := c.get(ctx, fmt.Sprintf("/nodes/%s/replication", PathEscape(node)), &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out.Data, nil
|
|
}
|
|
|
|
func (c *Client) ScheduleReplicationNow(ctx context.Context, node, id string) (string, error) {
|
|
var out struct {
|
|
Data string `json:"data"`
|
|
}
|
|
if err := c.post(ctx, fmt.Sprintf("/nodes/%s/replication/%s/schedule_now", PathEscape(node), url.PathEscape(id)), url.Values{}, &out); err != nil {
|
|
return "", err
|
|
}
|
|
return out.Data, nil
|
|
}
|
|
|
|
// CreateReplicationJobOptions models the writable fields of a replication
|
|
// job (/cluster/replication). ID is "<vmid>-<jobnum>" (e.g. "100-0") —
|
|
// PVE's own naming scheme, chosen by the caller since there's no
|
|
// auto-numbering endpoint.
|
|
type CreateReplicationJobOptions struct {
|
|
ID string
|
|
Guest int
|
|
Target string // target node name
|
|
Schedule string // e.g. "*/15" (every 15 min); PVE default is every 15 minutes if omitted
|
|
Comment string
|
|
Disable bool
|
|
}
|
|
|
|
func replicationJobForm(opts CreateReplicationJobOptions) url.Values {
|
|
form := url.Values{"type": {"local"}}
|
|
if opts.Target != "" {
|
|
form.Set("target", opts.Target)
|
|
}
|
|
if opts.Schedule != "" {
|
|
form.Set("schedule", opts.Schedule)
|
|
}
|
|
if opts.Comment != "" {
|
|
form.Set("comment", opts.Comment)
|
|
}
|
|
form.Set("disable", boolFlag(opts.Disable))
|
|
return form
|
|
}
|
|
|
|
// CreateReplicationJob schedules a new storage replication job for a guest.
|
|
func (c *Client) CreateReplicationJob(ctx context.Context, opts CreateReplicationJobOptions) error {
|
|
form := replicationJobForm(opts)
|
|
form.Set("id", opts.ID)
|
|
if opts.Guest > 0 {
|
|
form.Set("guest", fmt.Sprintf("%d", opts.Guest))
|
|
}
|
|
return c.post(ctx, "/cluster/replication", form, nil)
|
|
}
|
|
|
|
// UpdateReplicationJob edits an existing replication job's schedule/target/comment/enabled state.
|
|
func (c *Client) UpdateReplicationJob(ctx context.Context, id string, opts CreateReplicationJobOptions) error {
|
|
return c.put(ctx, "/cluster/replication/"+url.PathEscape(id), replicationJobForm(opts), nil)
|
|
}
|
|
|
|
// DeleteReplicationJob removes a replication job. When force is true, PVE
|
|
// also removes the already-replicated disk images from the target node
|
|
// instead of leaving them behind.
|
|
func (c *Client) DeleteReplicationJob(ctx context.Context, id string, force bool) error {
|
|
form := url.Values{}
|
|
if force {
|
|
form.Set("force", "1")
|
|
}
|
|
return c.delete(ctx, "/cluster/replication/"+url.PathEscape(id), form, nil)
|
|
}
|