// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "encoding/hex" "encoding/json" "errors" "fmt" "gopkg.in/yaml.v3" "io" "net/url" "regexp" _ "strconv" "strings" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/data" "decl/internal/folio" ) const ( NetworkRouteTypeName TypeName = "route" ) func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"route"}, func(u *url.URL) data.Resource { n := NewNetworkRoute() return n }) } /* ROUTE := NODE_SPEC [ INFO_SPEC ] NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ] [ table TABLE_ID ] [ proto RTPROTO ] [ scope SCOPE ] [ metric METRIC ] [ ttl-propagate { enabled | disabled } ] INFO_SPEC := { NH | nhid ID } OPTIONS FLAGS [ nexthop NH ]... NH := [ encap ENCAPTYPE ENCAPHDR ] [ via [ FAMILY ] ADDRESS ] [ dev STRING ] [ weight NUMBER ] NHFLAGS FAMILY := [ inet | inet6 | mpls | bridge | link ] OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ] [ rtt TIME ] [ rttvar TIME ] [ reordering NUMBER ] [ window NUMBER ] [ cwnd NUMBER ] [ initcwnd NUMBER ] [ ssthresh NUMBER ] [ realms REALM ] [ src ADDRESS ] [ rto_min TIME ] [ hoplimit NUMBER ] [ initrwnd NUMBER ] [ features FEATURES ] [ quickack BOOL ] [ congctl NAME ] [ pref PREF ] [ expires TIME ] [ fastopen_no_cookie BOOL ] NHFLAGS := [ onlink | pervasive ] PREF := [ low | medium | high ] TIME := NUMBER[s|ms] BOOL := [1|0] FEATURES := ecn ENCAPTYPE := [ mpls | ip | ip6 | seg6 | seg6local | rpl | ioam6 ] ENCAPHDR := [ MPLSLABEL | SEG6HDR | SEG6LOCAL | IOAM6HDR ] SEG6HDR := [ mode SEGMODE ] segs ADDR1,ADDRi,ADDRn [hmac HMACKEYID] [cleanup] SEGMODE := [ encap | inline ] SEG6LOCAL := action ACTION [ OPTIONS ] [ count ] ACTION := { End | End.X | End.T | End.DX2 | End.DX6 | End.DX4 | End.DT6 | End.DT4 | End.DT46 | End.B6 | End.B6.Encaps | End.BM | End.S | End.AS | End.AM | End.BPF } OPTIONS := OPTION [ OPTIONS ] OPTION := { srh SEG6HDR | nh4 ADDR | nh6 ADDR | iif DEV | oif DEV | table TABLEID | vrftable TABLEID | endpoint PROGNAME } IOAM6HDR := trace prealloc type IOAM6_TRACE_TYPE ns IOAM6_NAMESPACE size IOAM6_TRACE_SIZE ROUTE_GET_FLAGS := [ fibmatch ] */ type NetworkRouteType string const ( NetworkRouteTypeUnicast = "unicast" NetworkRouteTypeLocal = "local" NetworkRouteTypeBroadcast = "broadcast" NetworkRouteTypeMulticast = "multicast" NetworkRouteTypeThrow = "throw" NetworkRouteTypeUnreachable = "unreachable" NetworkRouteTypeProhibit = "prohibit" NetworkRouteTypeBlackhole = "blackhole" NetworkRouteTypeNat = "nat" ) type NetworkRouteTableId string const ( NetworkRouteTableLocal = "local" NetworkRouteTableMain = "main" NetworkRouteTableDefault = "default" NetworkRouteTableAll = "all" ) var NetworkRouteNumber = regexp.MustCompile(`^[0-9]+$`) type NetworkRouteScope string const ( NetworkRouteScopeHost = "host" NetworkRouteScopeLink = "link" NetworkRouteScopeGlobal = "global" ) type NetworkRouteProto string const ( NetworkRouteProtoKernel = "kernel" NetworkRouteProtoBoot = "boot" NetworkRouteProtoStatic = "static" ) // Manage the state of network routes type NetworkRoute struct { *Common `json:",inline" yaml:",inline"` stater machine.Stater `json:"-" yaml:"-"` Id string To string `json:"to" yaml:"to"` Interface string `json:"interface" yaml:"interface"` Gateway string `json:"gateway" yaml:"gateway"` Metric uint `json:"metric" yaml:"metric"` Rtid NetworkRouteTableId `json:"rtid" yaml:"rtid"` RouteType NetworkRouteType `json:"routetype" yaml:"routetype"` Scope NetworkRouteScope `json:"scope" yaml:"scope"` Proto NetworkRouteProto `json:"proto" yaml:"proto"` CreateCommand *Command `yaml:"-" json:"-"` ReadCommand *Command `yaml:"-" json:"-"` UpdateCommand *Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"` config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` } func NewNetworkRoute() *NetworkRoute { n := &NetworkRoute{Rtid: NetworkRouteTableMain, Common: &Common{ resourceType: NetworkRouteTypeName } } n.CreateCommand, n.ReadCommand, n.UpdateCommand, n.DeleteCommand = n.NewCRUD() return n } func (n *NetworkRoute) SetResourceMapper(resources data.ResourceMapper) { n.Resources = resources } func (n *NetworkRoute) Clone() data.Resource { newn := &NetworkRoute { Common: n.Common, Id: n.Id, To: n.To, Interface: n.Interface, Gateway: n.Gateway, Metric: n.Metric, Rtid: n.Rtid, RouteType: n.RouteType, Scope: n.Scope, Proto: n.Proto, } newn.CreateCommand, newn.ReadCommand, newn.UpdateCommand, newn.DeleteCommand = n.NewCRUD() return newn } func (n *NetworkRoute) StateMachine() machine.Stater { if n.stater == nil { n.stater = StorageMachine(n) } return n.stater } func (n *NetworkRoute) Notify(m *machine.EventMessage) { ctx := context.Background() switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_create": if e := n.Create(ctx); e == nil { if triggerErr := n.stater.Trigger("created"); triggerErr == nil { return } } n.Common.State = "absent" case "present": n.Common.State = "present" } case machine.EXITSTATEEVENT: } } func (n *NetworkRoute) Create(ctx context.Context) error { _, err := n.CreateCommand.Execute(n) if n.CreateCommand.Extractor != nil { if err != nil { return n.CreateCommand.Extractor([]byte(err.Error()), n) } } return nil } func (n *NetworkRoute) Update(ctx context.Context) error { return n.Create(ctx) } func (n *NetworkRoute) Delete(ctx context.Context) error { return nil } func (n *NetworkRoute) URI() string { return fmt.Sprintf("route://%s", n.Id) } func (n *NetworkRoute) SetURI(uri string) error { return nil } func (n *NetworkRoute) UseConfig(config data.ConfigurationValueGetter) { n.config = config } func (n *NetworkRoute) Validate() error { return fmt.Errorf("failed") } func (n *NetworkRoute) Apply() error { switch n.Common.State { case "absent": case "present": } return nil } func (n *NetworkRoute) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(n) return } func (n *NetworkRoute) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(n) return } func (n *NetworkRoute) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(n) return } func (n *NetworkRoute) LoadDecl(yamlResourceDeclaration string) error { return n.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (n *NetworkRoute) ResolveId(ctx context.Context) string { uri := fmt.Sprintf("%s?gateway=%s&interface=%s&rtid=%s&metric=%d&type=%s&scope=%s", n.To, n.Gateway, n.Interface, n.Rtid, n.Metric, n.RouteType, n.Scope) fmt.Printf("%#v\n", uri) n.Id = hex.EncodeToString([]byte(uri)) return n.Id } func (n *NetworkRoute) Read(ctx context.Context) ([]byte, error) { out, err := n.ReadCommand.Execute(n) if err != nil { return nil, err } exErr := n.ReadCommand.Extractor(out, n) if exErr != nil { return nil, exErr } /* var cmdArgs []string = make([]string, 17) cmdArgs[0] = "route" cmdArgs[1] = "show" if len(n.To) > 0 { cmdArgs = append(cmdArgs, "to", n.To) } if len(n.Rtid) > 0 { cmdArgs = append(cmdArgs, "table", string(n.Rtid)) } if len(n.Gateway) > 0 { cmdArgs = append(cmdArgs, "via", n.Gateway) } if len(n.Proto) > 0 { cmdArgs = append(cmdArgs, "protocol", string(n.Proto)) } if len(n.Scope) > 0 { cmdArgs = append(cmdArgs, "scope", string(n.Scope)) } if len(n.RouteType) > 0 { cmdArgs = append(cmdArgs, "type", string(n.RouteType)) } if len(n.Interface) > 0 { cmdArgs = append(cmdArgs, "dev", n.Interface) } readRouteTable, errRouteList := exec.Command("ip", cmdArgs...).Output() if errRouteList != nil { return nil, errRouteList } routes := strings.Split(string(readRouteTable), "\n") if len(routes) == 1 { fields := strings.Split(routes[0], " ") numberOfFields := len(fields) if numberOfFields > 1 { n.To = fields[0] for i := 1; i < numberOfFields; i += 2 { n.SetField(fields[i], fields[i+1]) } } } */ n.ResolveId(ctx) return yaml.Marshal(n) } func (n *NetworkRoute) SetField(name string, value string) { switch name { case "to": n.To = value case "table": n.Rtid = NetworkRouteTableId(value) case "via": n.Gateway = value case "protocol": n.Proto = NetworkRouteProto(value) case "scope": n.Scope = NetworkRouteScope(value) case "type": n.RouteType = NetworkRouteType(value) case "dev": n.Interface = value } } func (n *NetworkRoute) GetField(name string) string { switch name { case "to": return n.To case "table": return string(n.Rtid) case "via": return n.Gateway case "protocol": return string(n.Proto) case "scope": return string(n.Scope) case "type": return string(n.RouteType) case "dev": return n.Interface } return "" } func (n *NetworkRoute) Type() string { return "route" } func (n *NetworkRouteType) UnmarshalValue(value string) error { switch value { case string(NetworkRouteTypeUnicast), string(NetworkRouteTypeLocal), string(NetworkRouteTypeBroadcast), string(NetworkRouteTypeMulticast), string(NetworkRouteTypeThrow), string(NetworkRouteTypeUnreachable), string(NetworkRouteTypeProhibit), string(NetworkRouteTypeBlackhole), string(NetworkRouteTypeNat): *n = NetworkRouteType(value) return nil default: return errors.New("invalid NetworkRouteType value") } } func (n *NetworkRouteType) UnmarshalJSON(data []byte) error { var s string if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { return unmarshalRouteTypeErr } return n.UnmarshalValue(s) } func (n *NetworkRouteType) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return n.UnmarshalValue(s) } func (n *NetworkRouteTableId) UnmarshalValue(value string) error { switch value { case NetworkRouteNumber.FindString(value): fallthrough case string(NetworkRouteTableLocal), string(NetworkRouteTableMain), string(NetworkRouteTableDefault), string(NetworkRouteTableAll): *n = NetworkRouteTableId(value) return nil default: return errors.New("invalid NetworkRouteTableId value") } } func (n *NetworkRouteTableId) UnmarshalJSON(data []byte) error { var s string if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { return unmarshalRouteTypeErr } return n.UnmarshalValue(s) } func (n *NetworkRouteTableId) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return n.UnmarshalValue(s) } func (n *NetworkRouteScope) UnmarshalValue(value string) error { switch value { case NetworkRouteNumber.FindString(value): fallthrough case string(NetworkRouteScopeHost), string(NetworkRouteScopeLink), string(NetworkRouteScopeGlobal): *n = NetworkRouteScope(value) return nil default: return errors.New("invalid NetworkRouteScope value") } } func (n *NetworkRouteScope) UnmarshalJSON(data []byte) error { var s string if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { return unmarshalRouteTypeErr } return n.UnmarshalValue(s) } func (n *NetworkRouteScope) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return n.UnmarshalValue(s) } func (n *NetworkRouteProto) UnmarshalValue(value string) error { switch value { case NetworkRouteNumber.FindString(value): fallthrough case string(NetworkRouteProtoKernel), string(NetworkRouteProtoBoot), string(NetworkRouteProtoStatic): *n = NetworkRouteProto(value) return nil default: return errors.New("invalid NetworkRouteProto value") } } func (n *NetworkRouteProto) UnmarshalJSON(data []byte) error { var s string if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { return unmarshalRouteTypeErr } return n.UnmarshalValue(s) } func (n *NetworkRouteProto) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return n.UnmarshalValue(s) } func (n *NetworkRoute) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, n); err != nil { return err } return nil } func (n *NetworkRoute) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { return NewNetworkRouteCreateCommand(), NewNetworkRouteReadCommand(), NewNetworkRouteUpdateCommand(), NewNetworkRouteDeleteCommand() } func NewNetworkRouteCreateCommand() *Command { c := NewCommand() c.Path = "ip" c.Args = []CommandArg{ CommandArg("route"), CommandArg("add"), CommandArg("{{ if .To }}to {{ .To }}{{ end }}"), CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"), CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"), CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"), CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"), CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"), CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"), CommandArg("{{ if .Metric }}metric {{ .Metric }}{{ end }}"), } return c } func NewNetworkRouteReadCommand() *Command { c := NewCommand() c.Path = "ip" c.Args = []CommandArg{ CommandArg("route"), CommandArg("show"), CommandArg("{{ if .To }}to {{ .To }}{{ end }}"), CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"), CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"), CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"), CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"), CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"), CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"), } c.Extractor = func(out []byte, target any) error { n := target.(*NetworkRoute) routes := strings.Split(string(out), "\n") if len(routes) == 1 { fields := strings.Split(routes[0], " ") numberOfFields := len(fields) if numberOfFields > 1 { n.To = fields[0] for i := 1; i < numberOfFields; i += 2 { n.SetField(fields[i], fields[i + 1]) } } n.Common.State = "present" } else { n.Common.State = "absent" } return nil } return c } func NewNetworkRouteUpdateCommand() *Command { c := NewCommand() c.Path = "ip" c.Args = []CommandArg{ CommandArg("del"), CommandArg("{{ .Name }}"), } return c } func NewNetworkRouteDeleteCommand() *Command { return nil }