Add per-device routing long-tail: hosts, providers, routes, routing_rules
Storage + CRUD (migration 0006, model, id-keyed store, REST handlers) plus compiler rendering: each section is owned by a device and projected into that device's rendered config (hosts/providers/routes/routing_rules).
This commit is contained in:
@@ -22,36 +22,44 @@ import (
|
||||
// Input is the fully-resolved model needed to render one device. Keeping Render
|
||||
// pure (no store access) makes it unit-testable without a database.
|
||||
type Input struct {
|
||||
Generation int64
|
||||
Settings model.Settings
|
||||
Device model.Device
|
||||
Fabric *model.Fabric
|
||||
Zones map[string]model.Zone
|
||||
Groups map[string]model.AddressGroup
|
||||
PortGroups map[string]model.PortGroup
|
||||
Rules []model.Rule
|
||||
Policies []model.Policy
|
||||
Bindings []model.Binding
|
||||
SNAT []model.SNATRule
|
||||
Netmap []model.NetmapRule
|
||||
NAT []model.NATRule
|
||||
Generation int64
|
||||
Settings model.Settings
|
||||
Device model.Device
|
||||
Fabric *model.Fabric
|
||||
Zones map[string]model.Zone
|
||||
Groups map[string]model.AddressGroup
|
||||
PortGroups map[string]model.PortGroup
|
||||
Rules []model.Rule
|
||||
Policies []model.Policy
|
||||
Bindings []model.Binding
|
||||
SNAT []model.SNATRule
|
||||
Netmap []model.NetmapRule
|
||||
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.
|
||||
type RenderedConfig struct {
|
||||
Generation int64 `yaml:"generation" json:"generation"`
|
||||
Device string `yaml:"device" json:"device"`
|
||||
Class model.DeviceClass `yaml:"class" json:"class"`
|
||||
Enforcing bool `yaml:"enforcing" json:"enforcing"`
|
||||
Settings RenderedSettings `yaml:"settings" json:"settings"`
|
||||
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
|
||||
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
|
||||
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
|
||||
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
|
||||
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
|
||||
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
|
||||
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,omitempty"`
|
||||
NAT []RenderedNAT `yaml:"nat,omitempty" json:"nat,omitempty"`
|
||||
Generation int64 `yaml:"generation" json:"generation"`
|
||||
Device string `yaml:"device" json:"device"`
|
||||
Class model.DeviceClass `yaml:"class" json:"class"`
|
||||
Enforcing bool `yaml:"enforcing" json:"enforcing"`
|
||||
Settings RenderedSettings `yaml:"settings" json:"settings"`
|
||||
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
|
||||
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
|
||||
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
|
||||
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
|
||||
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
|
||||
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
|
||||
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,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
|
||||
@@ -197,6 +205,9 @@ func Render(in Input) (*RenderedConfig, error) {
|
||||
out.Netmap = renderNetmapRules(in, out.Bindings)
|
||||
out.NAT = renderNATRules(in)
|
||||
|
||||
// Per-device long-tail sections owned by this device.
|
||||
renderPerDevice(in, out)
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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.mountLongtail(r)
|
||||
s.mountPerDevice(r)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func respondDelete(w http.ResponseWriter, err error) {
|
||||
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