Long-tail batch 2 (API): per-device routing (hosts/providers/routes/routing_rules) #10
@@ -22,36 +22,44 @@ import (
|
|||||||
// Input is the fully-resolved model needed to render one device. Keeping Render
|
// Input is the fully-resolved model needed to render one device. Keeping Render
|
||||||
// pure (no store access) makes it unit-testable without a database.
|
// pure (no store access) makes it unit-testable without a database.
|
||||||
type Input struct {
|
type Input struct {
|
||||||
Generation int64
|
Generation int64
|
||||||
Settings model.Settings
|
Settings model.Settings
|
||||||
Device model.Device
|
Device model.Device
|
||||||
Fabric *model.Fabric
|
Fabric *model.Fabric
|
||||||
Zones map[string]model.Zone
|
Zones map[string]model.Zone
|
||||||
Groups map[string]model.AddressGroup
|
Groups map[string]model.AddressGroup
|
||||||
PortGroups map[string]model.PortGroup
|
PortGroups map[string]model.PortGroup
|
||||||
Rules []model.Rule
|
Rules []model.Rule
|
||||||
Policies []model.Policy
|
Policies []model.Policy
|
||||||
Bindings []model.Binding
|
Bindings []model.Binding
|
||||||
SNAT []model.SNATRule
|
SNAT []model.SNATRule
|
||||||
Netmap []model.NetmapRule
|
Netmap []model.NetmapRule
|
||||||
NAT []model.NATRule
|
NAT []model.NATRule
|
||||||
|
Hosts []model.Host
|
||||||
|
Providers []model.Provider
|
||||||
|
Routes []model.Route
|
||||||
|
RoutingRules []model.RoutingRule
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderedConfig is the per-device output served to the agent.
|
// RenderedConfig is the per-device output served to the agent.
|
||||||
type RenderedConfig struct {
|
type RenderedConfig struct {
|
||||||
Generation int64 `yaml:"generation" json:"generation"`
|
Generation int64 `yaml:"generation" json:"generation"`
|
||||||
Device string `yaml:"device" json:"device"`
|
Device string `yaml:"device" json:"device"`
|
||||||
Class model.DeviceClass `yaml:"class" json:"class"`
|
Class model.DeviceClass `yaml:"class" json:"class"`
|
||||||
Enforcing bool `yaml:"enforcing" json:"enforcing"`
|
Enforcing bool `yaml:"enforcing" json:"enforcing"`
|
||||||
Settings RenderedSettings `yaml:"settings" json:"settings"`
|
Settings RenderedSettings `yaml:"settings" json:"settings"`
|
||||||
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
|
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
|
||||||
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
|
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
|
||||||
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
|
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
|
||||||
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
|
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
|
||||||
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
|
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
|
||||||
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
|
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
|
||||||
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,omitempty"`
|
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,omitempty"`
|
||||||
NAT []RenderedNAT `yaml:"nat,omitempty" json:"nat,omitempty"`
|
NAT []RenderedNAT `yaml:"nat,omitempty" json:"nat,omitempty"`
|
||||||
|
Hosts []RenderedHost `yaml:"hosts,omitempty" json:"hosts,omitempty"`
|
||||||
|
Providers []RenderedProvider `yaml:"providers,omitempty" json:"providers,omitempty"`
|
||||||
|
Routes []RenderedRoute `yaml:"routes,omitempty" json:"routes,omitempty"`
|
||||||
|
RoutingRules []RenderedRoutingRule `yaml:"routing_rules,omitempty" json:"routing_rules,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded
|
// RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded
|
||||||
@@ -197,6 +205,9 @@ func Render(in Input) (*RenderedConfig, error) {
|
|||||||
out.Netmap = renderNetmapRules(in, out.Bindings)
|
out.Netmap = renderNetmapRules(in, out.Bindings)
|
||||||
out.NAT = renderNATRules(in)
|
out.NAT = renderNATRules(in)
|
||||||
|
|
||||||
|
// Per-device long-tail sections owned by this device.
|
||||||
|
renderPerDevice(in, out)
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,5 +437,17 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi
|
|||||||
if in.NAT, err = s.ListNAT(ctx); err != nil {
|
if in.NAT, err = s.ListNAT(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if in.Hosts, err = s.ListHosts(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if in.Providers, err = s.ListProviders(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if in.Routes, err = s.ListRoutes(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if in.RoutingRules, err = s.ListRoutingRules(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return Render(in)
|
return Render(in)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package compiler
|
||||||
|
|
||||||
|
// Per-device long-tail sections rendered into a device's config. The compiler
|
||||||
|
// filters each global list to the entries owned by the device.
|
||||||
|
|
||||||
|
type RenderedHost struct {
|
||||||
|
Zone string `yaml:"zone" json:"zone"`
|
||||||
|
Interface string `yaml:"interface" json:"interface"`
|
||||||
|
Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"`
|
||||||
|
Exclusions []string `yaml:"exclusions,omitempty" json:"exclusions,omitempty"`
|
||||||
|
Dynamic bool `yaml:"dynamic,omitempty" json:"dynamic,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderedProvider struct {
|
||||||
|
Name string `yaml:"name" json:"name"`
|
||||||
|
Number int `yaml:"number" json:"number"`
|
||||||
|
Mark int `yaml:"mark,omitempty" json:"mark,omitempty"`
|
||||||
|
Duplicate string `yaml:"duplicate,omitempty" json:"duplicate,omitempty"`
|
||||||
|
Interface string `yaml:"interface" json:"interface"`
|
||||||
|
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
|
||||||
|
Copy []string `yaml:"copy,omitempty" json:"copy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderedRoute struct {
|
||||||
|
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
|
||||||
|
Dest string `yaml:"dest" json:"dest"`
|
||||||
|
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
|
||||||
|
Oif string `yaml:"oif,omitempty" json:"oif,omitempty"`
|
||||||
|
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
|
||||||
|
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderedRoutingRule struct {
|
||||||
|
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||||
|
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||||
|
Provider string `yaml:"provider" json:"provider"`
|
||||||
|
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
|
||||||
|
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
|
||||||
|
Mark string `yaml:"mark,omitempty" json:"mark,omitempty"`
|
||||||
|
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderPerDevice projects the per-device long-tail sections owned by this device.
|
||||||
|
func renderPerDevice(in Input, out *RenderedConfig) {
|
||||||
|
dev := in.Device.Name
|
||||||
|
for _, h := range in.Hosts {
|
||||||
|
if h.Device != dev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.Hosts = append(out.Hosts, RenderedHost{
|
||||||
|
Zone: h.Zone, Interface: h.Interface, Addresses: h.Addresses,
|
||||||
|
Exclusions: h.Exclusions, Dynamic: h.Dynamic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, p := range in.Providers {
|
||||||
|
if p.Device != dev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.Providers = append(out.Providers, RenderedProvider{
|
||||||
|
Name: p.Name, Number: p.Number, Mark: p.Mark, Duplicate: p.Duplicate,
|
||||||
|
Interface: p.Interface, Gateway: p.Gateway, Copy: p.Copy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, r := range in.Routes {
|
||||||
|
if r.Device != dev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.Routes = append(out.Routes, RenderedRoute{
|
||||||
|
Provider: r.Provider, Dest: r.Dest, Gateway: r.Gateway,
|
||||||
|
Oif: r.Oif, Persistent: r.Persistent, Comment: r.Comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, r := range in.RoutingRules {
|
||||||
|
if r.Device != dev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.RoutingRules = append(out.RoutingRules, RenderedRoutingRule{
|
||||||
|
Source: r.Source, Dest: r.Dest, Provider: r.Provider, Priority: r.Priority,
|
||||||
|
Persistent: r.Persistent, Mark: r.Mark, Comment: r.Comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Per-device long-tail sections: hosts, providers (multi-ISP), static routes,
|
||||||
|
-- and routing rules (rtrules). Each is owned by a device and renders into that
|
||||||
|
-- device's config. (Nested option structs on host/provider are deferred.)
|
||||||
|
|
||||||
|
CREATE TABLE hosts (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||||
|
zone TEXT NOT NULL,
|
||||||
|
interface TEXT NOT NULL,
|
||||||
|
addresses JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
exclusions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
dynamic BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE providers (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
number INT NOT NULL,
|
||||||
|
mark INT NOT NULL DEFAULT 0,
|
||||||
|
duplicate TEXT NOT NULL DEFAULT '',
|
||||||
|
interface TEXT NOT NULL,
|
||||||
|
gateway TEXT NOT NULL DEFAULT '',
|
||||||
|
copy JSONB NOT NULL DEFAULT '[]'::jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE routes (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||||
|
provider TEXT NOT NULL DEFAULT '',
|
||||||
|
dest TEXT NOT NULL,
|
||||||
|
gateway TEXT NOT NULL DEFAULT '',
|
||||||
|
oif TEXT NOT NULL DEFAULT '', -- egress interface (tomswall route "device")
|
||||||
|
persistent BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
comment TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE routing_rules (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||||
|
source TEXT NOT NULL DEFAULT '',
|
||||||
|
dest TEXT NOT NULL DEFAULT '',
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
priority INT NOT NULL DEFAULT 0,
|
||||||
|
persistent BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
mark TEXT NOT NULL DEFAULT '',
|
||||||
|
comment TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Per-device long-tail sections. Each is owned by a device (the fleet member it
|
||||||
|
// renders on) and maps to the corresponding tomswall config section.
|
||||||
|
|
||||||
|
// Host constrains a zone to specific addresses on a device's interface.
|
||||||
|
type Host struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Device string `json:"device"`
|
||||||
|
Zone string `json:"zone"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
Addresses []string `json:"addresses,omitempty"`
|
||||||
|
Exclusions []string `json:"exclusions,omitempty"`
|
||||||
|
Dynamic bool `json:"dynamic,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is a multi-ISP routing provider on a device.
|
||||||
|
type Provider struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Device string `json:"device"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Number int `json:"number"`
|
||||||
|
Mark int `json:"mark,omitempty"`
|
||||||
|
Duplicate string `json:"duplicate,omitempty"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
Gateway string `json:"gateway,omitempty"`
|
||||||
|
Copy []string `json:"copy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route is a static route on a device. Oif is the egress interface (tomswall's
|
||||||
|
// route "device" field, renamed to avoid colliding with the fleet device).
|
||||||
|
type Route struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Device string `json:"device"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Dest string `json:"dest"`
|
||||||
|
Gateway string `json:"gateway,omitempty"`
|
||||||
|
Oif string `json:"oif,omitempty"`
|
||||||
|
Persistent bool `json:"persistent,omitempty"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutingRule directs traffic to a provider's routing table on a device.
|
||||||
|
type RoutingRule struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Device string `json:"device"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Dest string `json:"dest,omitempty"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Persistent bool `json:"persistent,omitempty"`
|
||||||
|
Mark string `json:"mark,omitempty"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mountPerDevice wires the per-device long-tail sections: hosts, providers,
|
||||||
|
// routes, and routing-rules.
|
||||||
|
func (s *Server) mountPerDevice(r chi.Router) {
|
||||||
|
r.Route("/hosts", func(r chi.Router) {
|
||||||
|
r.Get("/", s.listHosts)
|
||||||
|
r.Post("/", s.createHost)
|
||||||
|
r.Get("/{id}", s.getHost)
|
||||||
|
r.Delete("/{id}", s.deleteHost)
|
||||||
|
})
|
||||||
|
r.Route("/providers", func(r chi.Router) {
|
||||||
|
r.Get("/", s.listProviders)
|
||||||
|
r.Post("/", s.createProvider)
|
||||||
|
r.Get("/{id}", s.getProvider)
|
||||||
|
r.Delete("/{id}", s.deleteProvider)
|
||||||
|
})
|
||||||
|
r.Route("/routes", func(r chi.Router) {
|
||||||
|
r.Get("/", s.listRoutes)
|
||||||
|
r.Post("/", s.createRoute)
|
||||||
|
r.Get("/{id}", s.getRoute)
|
||||||
|
r.Delete("/{id}", s.deleteRoute)
|
||||||
|
})
|
||||||
|
r.Route("/routing-rules", func(r chi.Router) {
|
||||||
|
r.Get("/", s.listRoutingRules)
|
||||||
|
r.Post("/", s.createRoutingRule)
|
||||||
|
r.Get("/{id}", s.getRoutingRule)
|
||||||
|
r.Delete("/{id}", s.deleteRoutingRule)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listHosts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := s.store.ListHosts(r.Context())
|
||||||
|
respondList(w, list, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createHost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v model.Host
|
||||||
|
if !decode(w, r, &v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.Device == "" || v.Zone == "" || v.Interface == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "device, zone, and interface are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := s.store.CreateHost(r.Context(), v)
|
||||||
|
if err == nil {
|
||||||
|
v.ID = id
|
||||||
|
}
|
||||||
|
respondCreated(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getHost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v, err := s.store.GetHost(r.Context(), id)
|
||||||
|
respondOne(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) deleteHost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondDelete(w, s.store.DeleteHost(r.Context(), id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listProviders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := s.store.ListProviders(r.Context())
|
||||||
|
respondList(w, list, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createProvider(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v model.Provider
|
||||||
|
if !decode(w, r, &v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.Device == "" || v.Name == "" || v.Interface == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "device, name, and interface are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := s.store.CreateProvider(r.Context(), v)
|
||||||
|
if err == nil {
|
||||||
|
v.ID = id
|
||||||
|
}
|
||||||
|
respondCreated(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getProvider(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v, err := s.store.GetProvider(r.Context(), id)
|
||||||
|
respondOne(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) deleteProvider(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondDelete(w, s.store.DeleteProvider(r.Context(), id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listRoutes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := s.store.ListRoutes(r.Context())
|
||||||
|
respondList(w, list, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v model.Route
|
||||||
|
if !decode(w, r, &v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.Device == "" || v.Dest == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "device and dest are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := s.store.CreateRoute(r.Context(), v)
|
||||||
|
if err == nil {
|
||||||
|
v.ID = id
|
||||||
|
}
|
||||||
|
respondCreated(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v, err := s.store.GetRoute(r.Context(), id)
|
||||||
|
respondOne(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) deleteRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondDelete(w, s.store.DeleteRoute(r.Context(), id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listRoutingRules(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := s.store.ListRoutingRules(r.Context())
|
||||||
|
respondList(w, list, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createRoutingRule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v model.RoutingRule
|
||||||
|
if !decode(w, r, &v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.Device == "" || v.Provider == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "device and provider are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := s.store.CreateRoutingRule(r.Context(), v)
|
||||||
|
if err == nil {
|
||||||
|
v.ID = id
|
||||||
|
}
|
||||||
|
respondCreated(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRoutingRule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v, err := s.store.GetRoutingRule(r.Context(), id)
|
||||||
|
respondOne(w, v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) deleteRoutingRule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondDelete(w, s.store.DeleteRoutingRule(r.Context(), id))
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ func (s *Server) mountResources(r chi.Router) {
|
|||||||
})
|
})
|
||||||
s.mountNAT(r)
|
s.mountNAT(r)
|
||||||
s.mountLongtail(r)
|
s.mountLongtail(r)
|
||||||
|
s.mountPerDevice(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// respondOne writes a single resource, mapping ErrNotFound to 404.
|
// respondOne writes a single resource, mapping ErrNotFound to 404.
|
||||||
@@ -76,6 +77,15 @@ func respondOne(w http.ResponseWriter, v any, err error) {
|
|||||||
writeJSON(w, http.StatusOK, v)
|
writeJSON(w, http.StatusOK, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// respondCreated writes a 201 with the created resource, or 500 on error.
|
||||||
|
func respondCreated(w http.ResponseWriter, v any, err error) {
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, v)
|
||||||
|
}
|
||||||
|
|
||||||
// respondDelete maps a delete result to 204/404/500.
|
// respondDelete maps a delete result to 204/404/500.
|
||||||
func respondDelete(w http.ResponseWriter, err error) {
|
func respondDelete(w http.ResponseWriter, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Hosts -----------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *Store) ListHosts(ctx context.Context) ([]model.Host, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, device, zone, interface, addresses, exclusions, dynamic FROM hosts ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []model.Host
|
||||||
|
for rows.Next() {
|
||||||
|
h, err := scanHost(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, h)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetHost(ctx context.Context, id int64) (model.Host, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, device, zone, interface, addresses, exclusions, dynamic FROM hosts WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return model.Host{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if !rows.Next() {
|
||||||
|
return model.Host{}, ErrNotFound
|
||||||
|
}
|
||||||
|
return scanHost(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanHost(rows pgx.Rows) (model.Host, error) {
|
||||||
|
var h model.Host
|
||||||
|
var addrs, excl []byte
|
||||||
|
if err := rows.Scan(&h.ID, &h.Device, &h.Zone, &h.Interface, &addrs, &excl, &h.Dynamic); err != nil {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
if err := unmarshalStrings(addrs, &h.Addresses); err != nil {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
return h, unmarshalStrings(excl, &h.Exclusions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateHost(ctx context.Context, h model.Host) (int64, error) {
|
||||||
|
addrs, _ := jsonb(h.Addresses)
|
||||||
|
excl, _ := jsonb(h.Exclusions)
|
||||||
|
var id int64
|
||||||
|
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO hosts (device, zone, interface, addresses, exclusions, dynamic)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||||
|
h.Device, h.Zone, h.Interface, addrs, excl, h.Dynamic).Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bump(ctx, tx)
|
||||||
|
})
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteHost(ctx context.Context, id int64) error {
|
||||||
|
return s.deleteOne(ctx, `DELETE FROM hosts WHERE id = $1`, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Providers -------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *Store) ListProviders(ctx context.Context) ([]model.Provider, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, device, name, number, mark, duplicate, interface, gateway, copy FROM providers ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []model.Provider
|
||||||
|
for rows.Next() {
|
||||||
|
p, err := scanProvider(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetProvider(ctx context.Context, id int64) (model.Provider, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, device, name, number, mark, duplicate, interface, gateway, copy FROM providers WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return model.Provider{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if !rows.Next() {
|
||||||
|
return model.Provider{}, ErrNotFound
|
||||||
|
}
|
||||||
|
return scanProvider(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanProvider(rows pgx.Rows) (model.Provider, error) {
|
||||||
|
var p model.Provider
|
||||||
|
var cp []byte
|
||||||
|
if err := rows.Scan(&p.ID, &p.Device, &p.Name, &p.Number, &p.Mark, &p.Duplicate, &p.Interface, &p.Gateway, &cp); err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
return p, unmarshalStrings(cp, &p.Copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateProvider(ctx context.Context, p model.Provider) (int64, error) {
|
||||||
|
cp, _ := jsonb(p.Copy)
|
||||||
|
var id int64
|
||||||
|
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO providers (device, name, number, mark, duplicate, interface, gateway, copy)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
||||||
|
p.Device, p.Name, p.Number, p.Mark, p.Duplicate, p.Interface, p.Gateway, cp).Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bump(ctx, tx)
|
||||||
|
})
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteProvider(ctx context.Context, id int64) error {
|
||||||
|
return s.deleteOne(ctx, `DELETE FROM providers WHERE id = $1`, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Routes ----------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *Store) ListRoutes(ctx context.Context) ([]model.Route, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, device, provider, dest, gateway, oif, persistent, comment FROM routes ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []model.Route
|
||||||
|
for rows.Next() {
|
||||||
|
var r model.Route
|
||||||
|
if err := rows.Scan(&r.ID, &r.Device, &r.Provider, &r.Dest, &r.Gateway, &r.Oif, &r.Persistent, &r.Comment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetRoute(ctx context.Context, id int64) (model.Route, error) {
|
||||||
|
var r model.Route
|
||||||
|
err := s.pool.QueryRow(ctx,
|
||||||
|
`SELECT id, device, provider, dest, gateway, oif, persistent, comment FROM routes WHERE id = $1`, id,
|
||||||
|
).Scan(&r.ID, &r.Device, &r.Provider, &r.Dest, &r.Gateway, &r.Oif, &r.Persistent, &r.Comment)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return r, ErrNotFound
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateRoute(ctx context.Context, r model.Route) (int64, error) {
|
||||||
|
var id int64
|
||||||
|
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO routes (device, provider, dest, gateway, oif, persistent, comment)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||||
|
r.Device, r.Provider, r.Dest, r.Gateway, r.Oif, r.Persistent, r.Comment).Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bump(ctx, tx)
|
||||||
|
})
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteRoute(ctx context.Context, id int64) error {
|
||||||
|
return s.deleteOne(ctx, `DELETE FROM routes WHERE id = $1`, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Routing rules ---------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *Store) ListRoutingRules(ctx context.Context) ([]model.RoutingRule, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, device, source, dest, provider, priority, persistent, mark, comment FROM routing_rules ORDER BY priority, id`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []model.RoutingRule
|
||||||
|
for rows.Next() {
|
||||||
|
var r model.RoutingRule
|
||||||
|
if err := rows.Scan(&r.ID, &r.Device, &r.Source, &r.Dest, &r.Provider, &r.Priority, &r.Persistent, &r.Mark, &r.Comment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetRoutingRule(ctx context.Context, id int64) (model.RoutingRule, error) {
|
||||||
|
var r model.RoutingRule
|
||||||
|
err := s.pool.QueryRow(ctx,
|
||||||
|
`SELECT id, device, source, dest, provider, priority, persistent, mark, comment FROM routing_rules WHERE id = $1`, id,
|
||||||
|
).Scan(&r.ID, &r.Device, &r.Source, &r.Dest, &r.Provider, &r.Priority, &r.Persistent, &r.Mark, &r.Comment)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return r, ErrNotFound
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateRoutingRule(ctx context.Context, r model.RoutingRule) (int64, error) {
|
||||||
|
var id int64
|
||||||
|
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO routing_rules (device, source, dest, provider, priority, persistent, mark, comment)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
||||||
|
r.Device, r.Source, r.Dest, r.Provider, r.Priority, r.Persistent, r.Mark, r.Comment).Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bump(ctx, tx)
|
||||||
|
})
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteRoutingRule(ctx context.Context, id int64) error {
|
||||||
|
return s.deleteOne(ctx, `DELETE FROM routing_rules WHERE id = $1`, id)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user