195 lines
6.3 KiB
Go
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
|
|
})
|
|
}
|