Files
2026-06-22 16:06:57 +02:00

195 lines
6.3 KiB
Go

package networking
import (
"context"
"errors"
"nadir/internal/oscmd"
"github.com/danielgtaylor/huma/v2"
)
// registerWrites adds all write endpoints for the networking module. Every
// handler checks m.be != nil first and returns 501 when no backend was detected.
type ApplyInput struct {
Name string `path:"name" example:"eth0" doc:"Interface name"`
Body IfaceConfig `doc:"Desired interface configuration"`
}
type ApplyOutput struct {
Body struct {
Status string `json:"status" example:"pending" doc:"Always \"pending\" — confirm to make permanent"`
Interface string `json:"interface" example:"eth0"`
Backend string `json:"backend" example:"nmcli" doc:"Network backend that applied the change"`
RollbackSeconds int `json:"rollback_seconds" example:"60" doc:"Seconds until auto-rollback unless confirmed"`
}
}
type IfacePathInput struct {
Name string `path:"name" example:"eth0" doc:"Interface name"`
}
type PendingOutput struct {
Body PendingInfo
}
func registerWrites(api huma.API, m *Module) {
huma.Register(api, huma.Operation{
OperationID: "networking-apply-config",
Method: "PUT",
Path: "/api/networking/interfaces/{name}",
Summary: "Apply interface configuration",
Description: "Replaces the interface's IPv4 configuration. The change is applied " +
"immediately but starts a rollback timer — if not confirmed within the timeout " +
"(default 60s), the prior configuration is automatically restored. This prevents " +
"lock-yourself-out mistakes on remote hosts.",
Tags: []string{tagNetworking},
Metadata: op("write"),
Errors: writeErrors,
}, func(ctx context.Context, in *ApplyInput) (*ApplyOutput, error) {
if m.be == nil {
return nil, huma.Error501NotImplemented("", errNoBackend)
}
if err := validateIface(in.Name); err != nil {
return nil, err
}
if err := in.Body.validate(); err != nil {
return nil, err
}
seconds, err := m.startRollback(ctx, in.Name, in.Body)
if err != nil {
if errors.Is(err, errAlreadyPending) {
return nil, huma.Error409Conflict(err.Error())
}
return nil, huma.Error500InternalServerError("apply failed", err)
}
out := &ApplyOutput{}
out.Body.Status = "pending"
out.Body.Interface = in.Name
out.Body.Backend = m.be.Name()
out.Body.RollbackSeconds = seconds
return out, nil
})
huma.Register(api, huma.Operation{
OperationID: "networking-confirm-change",
Method: "POST",
Path: "/api/networking/interfaces/{name}/confirm",
Summary: "Confirm a pending change",
Description: "Cancels the rollback timer, making the applied configuration permanent.",
Tags: []string{tagNetworking},
Metadata: op("write"),
Errors: writeErrors,
}, func(ctx context.Context, in *IfacePathInput) (*oscmd.StatusOutput, error) {
if m.be == nil {
return nil, huma.Error501NotImplemented("", errNoBackend)
}
if err := validateIface(in.Name); err != nil {
return nil, err
}
if err := m.confirm(in.Name); err != nil {
return nil, huma.Error409Conflict(err.Error())
}
return oscmd.OK(), nil
})
huma.Register(api, huma.Operation{
OperationID: "networking-rollback-change",
Method: "POST",
Path: "/api/networking/interfaces/{name}/rollback",
Summary: "Immediately revert a pending change",
Description: "Reverts the interface to its prior configuration and clears the pending change.",
Tags: []string{tagNetworking},
Metadata: op("write"),
Errors: writeErrors,
}, func(ctx context.Context, in *IfacePathInput) (*oscmd.StatusOutput, error) {
if m.be == nil {
return nil, huma.Error501NotImplemented("", errNoBackend)
}
if err := validateIface(in.Name); err != nil {
return nil, err
}
if err := m.rollbackNow(in.Name); err != nil {
return nil, huma.Error500InternalServerError("rollback failed", err)
}
return oscmd.OK(), nil
})
huma.Register(api, huma.Operation{
OperationID: "networking-link-up",
Method: "POST",
Path: "/api/networking/interfaces/{name}/up",
Summary: "Bring an interface up",
Tags: []string{tagNetworking},
Metadata: op("write"),
Errors: writeErrors,
}, func(ctx context.Context, in *IfacePathInput) (*oscmd.StatusOutput, error) {
if m.be == nil {
return nil, huma.Error501NotImplemented("", errNoBackend)
}
if err := validateIface(in.Name); err != nil {
return nil, err
}
if err := m.be.SetLinkUp(ctx, in.Name); err != nil {
return nil, huma.Error500InternalServerError("link up failed", err)
}
return oscmd.OK(), nil
})
huma.Register(api, huma.Operation{
OperationID: "networking-link-down",
Method: "POST",
Path: "/api/networking/interfaces/{name}/down",
Summary: "Take an interface down",
Description: "Brings the interface down behind the rollback safety net: it is brought " +
"back up automatically if not confirmed within the timeout (default 60s). This " +
"prevents taking down the link you're managing the host over and losing access.",
Tags: []string{tagNetworking},
Metadata: op("write"),
Errors: writeErrors,
}, func(ctx context.Context, in *IfacePathInput) (*ApplyOutput, error) {
if m.be == nil {
return nil, huma.Error501NotImplemented("", errNoBackend)
}
if err := validateIface(in.Name); err != nil {
return nil, err
}
seconds, err := m.startLinkDown(ctx, in.Name)
if err != nil {
if errors.Is(err, errAlreadyPending) {
return nil, huma.Error409Conflict(err.Error())
}
return nil, huma.Error500InternalServerError("link down failed", err)
}
out := &ApplyOutput{}
out.Body.Status = "pending"
out.Body.Interface = in.Name
out.Body.Backend = m.be.Name()
out.Body.RollbackSeconds = seconds
return out, nil
})
huma.Register(api, huma.Operation{
OperationID: "networking-get-pending",
Method: "GET",
Path: "/api/networking/pending",
Summary: "Get pending change status",
Description: "Returns the currently pending change (interface name and seconds until " +
"auto-rollback), or 404 if there is no pending change.",
Tags: []string{tagNetworking},
Metadata: op("read"),
Errors: []int{401, 403, 404, 500},
}, func(ctx context.Context, _ *struct{}) (*PendingOutput, error) {
info := m.pendingInfo()
if info == nil {
return nil, huma.Error404NotFound("no pending change")
}
out := &PendingOutput{}
out.Body = *info
return out, nil
})
}