From de9d6335aa7dbe7b3b8a50f9153437499cc78a9d Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 6 Mar 2026 16:24:05 +0100 Subject: [PATCH 001/102] Add systemd template renderer. --- .gitignore | 3 +- go.mod | 15 +- go.sum | 36 +- install.go | 5 + pkg/services/droptailer/droptailer.go | 46 + .../droptailer/droptailer.service.tpl | 20 + pkg/services/droptailer/droptailer_test.go | 64 + .../droptailer/test/droptailer.service | 18 + .../firewall_controller.service.tpl | 15 + pkg/services/install.go | 12 + .../nftables_exporter.service.tpl | 13 + .../node-exporter/node_exporter.service.tpl | 13 + .../suricata/suricata_config.yaml.tpl | 1836 +++++++++++++++++ pkg/services/suricata/suricata_defaults.tpl | 29 + .../suricata/suricata_update.service.tpl | 12 + pkg/services/tailscale/tailscale.service.tpl | 13 + pkg/services/tailscale/tailscaled.service.tpl | 25 + pkg/template-renderer/renderer.go | 181 ++ pkg/template-renderer/renderer_test.go | 128 ++ pkg/test/common.go | 18 + 20 files changed, 2496 insertions(+), 6 deletions(-) create mode 100644 pkg/services/droptailer/droptailer.go create mode 100644 pkg/services/droptailer/droptailer.service.tpl create mode 100644 pkg/services/droptailer/droptailer_test.go create mode 100644 pkg/services/droptailer/test/droptailer.service create mode 100644 pkg/services/firewall-controller/firewall_controller.service.tpl create mode 100644 pkg/services/install.go create mode 100644 pkg/services/nftables-exporter/nftables_exporter.service.tpl create mode 100644 pkg/services/node-exporter/node_exporter.service.tpl create mode 100644 pkg/services/suricata/suricata_config.yaml.tpl create mode 100644 pkg/services/suricata/suricata_defaults.tpl create mode 100644 pkg/services/suricata/suricata_update.service.tpl create mode 100644 pkg/services/tailscale/tailscale.service.tpl create mode 100644 pkg/services/tailscale/tailscaled.service.tpl create mode 100644 pkg/template-renderer/renderer.go create mode 100644 pkg/template-renderer/renderer_test.go create mode 100644 pkg/test/common.go diff --git a/.gitignore b/.gitignore index efa6632..f476789 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -bin/* \ No newline at end of file +bin/* +.vscode diff --git a/go.mod b/go.mod index 545529b..f153977 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.26 require ( github.com/Masterminds/semver/v3 v3.4.0 + github.com/Masterminds/sprig/v3 v3.3.0 github.com/coreos/go-systemd/v22 v22.7.0 github.com/flatcar/ignition v0.36.2 github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 github.com/metal-stack/metal-go v0.43.0 github.com/metal-stack/metal-lib v0.24.0 github.com/metal-stack/v v1.0.3 @@ -16,6 +18,8 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect @@ -42,14 +46,21 @@ require ( github.com/go-openapi/validate v0.25.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/vincent-petithory/dataurl v1.0.0 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/net v0.51.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 58bedb8..3b3ea56 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437 h1:gZCtZ+Hh/e3CGEX8q/yAcp8wWu5ZS6NMk6VGzpQhI3s= github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437/go.mod h1:otnto4/Icqn88WCcM4bhIJNSgsh9VLBuspyyCfvof9c= github.com/aws/aws-sdk-go v1.8.39/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= @@ -16,6 +22,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/flatcar/ignition v0.36.2 h1:xGHgScUe0P4Fkprjqv7L2CE58emiQgP833OCCn9z2v4= github.com/flatcar/ignition v0.36.2/go.mod h1:uk1tpzLFRXus4RrvzgMI+IqmmB8a/RGFSBlI+tMTbbA= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50= github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE= @@ -73,14 +81,27 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/metal-stack/metal-go v0.43.0 h1:uODD0YCwnAYzyvFxWNakZrymBoMz1FAvP5hkhsR83VQ= github.com/metal-stack/metal-go v0.43.0/go.mod h1:GSfXrAj55LGsUSMHWGDsmq5n056NG0yb1JM8bgfvKOw= github.com/metal-stack/metal-lib v0.24.0 h1:wvQQPWIXcA2tP+I6zAHUNdtVLLJfQnnV9yG2SoqUkz4= github.com/metal-stack/metal-lib v0.24.0/go.mod h1:oITaqj/BtB9vDKM66jCXkeA+4D0eTZElgIKal5vtiNY= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= @@ -88,12 +109,18 @@ github.com/pin/tftp v2.1.0+incompatible/go.mod h1:xVpZOMCXTy+A5QMjEVN0Glwa1sUvaJ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigma/bdoor v0.0.0-20160202064022-babf2a4017b0/go.mod h1:WBu7REWbxC/s/J06jsk//d+9DOz9BbsmcIrimuGRFbs= github.com/sigma/vmw-guestinfo v0.0.0-20160204083807-95dd4126d6e8/go.mod h1:JrRFFC0veyh0cibh0DAhriSY7/gV3kDdNaVUOmfx01U= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -110,10 +137,12 @@ go4.org v0.0.0-20160314031811-03efcb870d84/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1 go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -123,8 +152,9 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/install.go b/install.go index 5362773..a1e3ce8 100644 --- a/install.go +++ b/install.go @@ -146,6 +146,11 @@ func (i *installer) do() error { return err } + err = i.systemdServices() + if err != nil { + return err + } + err = i.writeBuildMeta() if err != nil { return err diff --git a/pkg/services/droptailer/droptailer.go b/pkg/services/droptailer/droptailer.go new file mode 100644 index 0000000..45db898 --- /dev/null +++ b/pkg/services/droptailer/droptailer.go @@ -0,0 +1,46 @@ +package droptailer + +import ( + "context" + _ "embed" + "log/slog" + + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" +) + +const ( + droptailerServiceName = "droptailer.service" + droptailerServiceUnitPath = "/etc/systemd/system/" + droptailerServiceName +) + +var ( + //go:embed droptailer.service.tpl + droptailerTemplateString string +) + +type DroptailerConfig struct { + Log *slog.Logger + Reload bool + fs afero.Fs +} + +type DroptailerTemplateData struct { + Comment string + TenantVrf string +} + +func WriteSystemdUnit(ctx context.Context, cfg *DroptailerConfig, c *DroptailerTemplateData) (changed bool, err error) { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + ServiceName: droptailerServiceName, + TemplateString: droptailerTemplateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + return r.Render(ctx, droptailerServiceUnitPath, cfg.Reload) +} diff --git a/pkg/services/droptailer/droptailer.service.tpl b/pkg/services/droptailer/droptailer.service.tpl new file mode 100644 index 0000000..3e7fe04 --- /dev/null +++ b/pkg/services/droptailer/droptailer.service.tpl @@ -0,0 +1,20 @@ +{{ range $line := split "\n" .Comment }} +# {{ $line }} +{{ end }} + +[Unit] +Description=Droptailer +After=network.target + +[Service] +LimitMEMLOCK=infinity +Environment=DROPTAILER_SERVER_ADDRESS=droptailer:50051 +Environment=DROPTAILER_PREFIXES_OF_DROPS="nftables-metal-dropped: ,nftables-firewall-dropped: " +Environment=DROPTAILER_CLIENT_CERTIFICATE=/etc/droptailer-client/droptailer-client.crt +Environment=DROPTAILER_CLIENT_KEY=/etc/droptailer-client/droptailer-client.key +ExecStart=/bin/ip vrf exec {{ .TenantVrf }} /usr/local/bin/droptailer-client +Restart=always +RestartSec=10 + +[Install] +WantedBy=firewall-controller.service diff --git a/pkg/services/droptailer/droptailer_test.go b/pkg/services/droptailer/droptailer_test.go new file mode 100644 index 0000000..db31d54 --- /dev/null +++ b/pkg/services/droptailer/droptailer_test.go @@ -0,0 +1,64 @@ +package droptailer + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *DroptailerTemplateData + wantService string + wantChanged bool + wantErr error + }{ + { + name: "render", + c: &DroptailerTemplateData{ + Comment: `This is a test. +Do not edit.`, + TenantVrf: "vrf42", + }, + wantService: ` + bla + `, + wantChanged: true, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &DroptailerConfig{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c) + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(droptailerServiceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/services/droptailer/test/droptailer.service b/pkg/services/droptailer/test/droptailer.service new file mode 100644 index 0000000..78e513a --- /dev/null +++ b/pkg/services/droptailer/test/droptailer.service @@ -0,0 +1,18 @@ +# This is a test. +# Do not edit. +[Unit] +Description=Droptailer +After=network.target + +[Service] +LimitMEMLOCK=infinity +Environment=DROPTAILER_SERVER_ADDRESS=droptailer:50051 +Environment=DROPTAILER_PREFIXES_OF_DROPS="nftables-metal-dropped: ,nftables-firewall-dropped: " +Environment=DROPTAILER_CLIENT_CERTIFICATE=/etc/droptailer-client/droptailer-client.crt +Environment=DROPTAILER_CLIENT_KEY=/etc/droptailer-client/droptailer-client.key +ExecStart=/bin/ip vrf exec vrf3981 /usr/local/bin/droptailer-client +Restart=always +RestartSec=10 + +[Install] +WantedBy=firewall-controller.service diff --git a/pkg/services/firewall-controller/firewall_controller.service.tpl b/pkg/services/firewall-controller/firewall_controller.service.tpl new file mode 100644 index 0000000..8ec206c --- /dev/null +++ b/pkg/services/firewall-controller/firewall_controller.service.tpl @@ -0,0 +1,15 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallControllerData*/ -}} +{{ .Comment }} +[Unit] +Description=Firewall controller - configures the firewall based on k8s resources +After=network.target + +[Service] +LimitMEMLOCK=infinity +Environment=KUBECONFIG=/etc/firewall-controller/.kubeconfig +ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/firewall-controller +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/pkg/services/install.go b/pkg/services/install.go new file mode 100644 index 0000000..d38bd12 --- /dev/null +++ b/pkg/services/install.go @@ -0,0 +1,12 @@ +package services + +func WriteSystemdServices() error { + return nil +} + +// suricata +// tailscale(d) +// node-exporter +// chrony +// droptailer +// nftables-exporter diff --git a/pkg/services/nftables-exporter/nftables_exporter.service.tpl b/pkg/services/nftables-exporter/nftables_exporter.service.tpl new file mode 100644 index 0000000..2381523 --- /dev/null +++ b/pkg/services/nftables-exporter/nftables_exporter.service.tpl @@ -0,0 +1,13 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NftablesExporterData*/ -}} +{{ .Comment }} +[Unit] +Description=Nftables exporter - provides prometheus metrics for nftables +After=network.target + +[Service] +ExecStart=/usr/bin/nftables-exporter --config=/etc/nftables_exporter.yaml +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/node-exporter/node_exporter.service.tpl b/pkg/services/node-exporter/node_exporter.service.tpl new file mode 100644 index 0000000..3c5550e --- /dev/null +++ b/pkg/services/node-exporter/node_exporter.service.tpl @@ -0,0 +1,13 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NodeExporterData*/ -}} +{{ .Comment }} +[Unit] +Description=Node exporter - provides prometheus metrics about the node +After=network.target + +[Service] +ExecStart=/usr/local/bin/node_exporter --collector.tcpstat +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/suricata/suricata_config.yaml.tpl b/pkg/services/suricata/suricata_config.yaml.tpl new file mode 100644 index 0000000..378b618 --- /dev/null +++ b/pkg/services/suricata/suricata_config.yaml.tpl @@ -0,0 +1,1836 @@ +%YAML 1.1 +--- + +# Suricata configuration file located in /etc/suricata +# In addition to the comments describing all +# options in this file, full documentation can be found at: +# https://suricata.readthedocs.io/en/latest/configuration/suricata-yaml.html + +## +## Step 1: inform Suricata about your network +## + +vars: + # more specific is better for alert accuracy and performance + address-groups: + HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]" + #HOME_NET: "[192.168.0.0/16]" + #HOME_NET: "[10.0.0.0/8]" + #HOME_NET: "[172.16.0.0/12]" + #HOME_NET: "any" + + EXTERNAL_NET: "!$HOME_NET" + #EXTERNAL_NET: "any" + + HTTP_SERVERS: "$HOME_NET" + SMTP_SERVERS: "$HOME_NET" + SQL_SERVERS: "$HOME_NET" + DNS_SERVERS: "$HOME_NET" + TELNET_SERVERS: "$HOME_NET" + AIM_SERVERS: "$EXTERNAL_NET" + DC_SERVERS: "$HOME_NET" + DNP3_SERVER: "$HOME_NET" + DNP3_CLIENT: "$HOME_NET" + MODBUS_CLIENT: "$HOME_NET" + MODBUS_SERVER: "$HOME_NET" + ENIP_CLIENT: "$HOME_NET" + ENIP_SERVER: "$HOME_NET" + + port-groups: + HTTP_PORTS: "80" + SHELLCODE_PORTS: "!80" + ORACLE_PORTS: 1521 + SSH_PORTS: 22 + DNP3_PORTS: 20000 + MODBUS_PORTS: 502 + FILE_DATA_PORTS: "[$HTTP_PORTS,110,143]" + FTP_PORTS: 21 + VXLAN_PORTS: 4789 + +## +## Step 2: select outputs to enable +## + +# The default logging directory. Any log or output file will be +# placed here if its not specified with a full path name. This can be +# overridden with the -l command line parameter. +default-log-dir: /var/log/suricata/ + +# global stats configuration +stats: + enabled: yes + # The interval field (in seconds) controls at what interval + # the loggers are invoked. + interval: 8 + # Add decode events as stats. + #decoder-events: true + # Decoder event prefix in stats. Has been 'decoder' before, but that leads + # to missing events in the eve.stats records. See issue #2225. + #decoder-events-prefix: "decoder.event" + # Add stream events as stats. + #stream-events: false + +# Configure the type of alert (and other) logging you would like. +outputs: + # a line based alerts log similar to Snort's fast.log + - fast: + enabled: yes + filename: fast.log + append: yes + #filetype: regular # 'regular', 'unix_stream' or 'unix_dgram' + + # Extensible Event Format (nicknamed EVE) event log in JSON format + - eve-log: + enabled: yes + filetype: regular + filename: eve.json + #prefix: "@cee: " # prefix to prepend to each log entry + # the following are valid when type: syslog above + #identity: "suricata" + #facility: local5 + #level: Info ## possible levels: Emergency, Alert, Critical, + ## Error, Warning, Notice, Info, Debug + #redis: + # server: 127.0.0.1 + # port: 6379 + # async: true ## if redis replies are read asynchronously + # mode: list ## possible values: list|lpush (default), rpush, channel|publish + # ## lpush and rpush are using a Redis list. "list" is an alias for lpush + # ## publish is using a Redis channel. "channel" is an alias for publish + # key: suricata ## key or channel to use (default to suricata) + # Redis pipelining set up. This will enable to only do a query every + # 'batch-size' events. This should lower the latency induced by network + # connection at the cost of some memory. There is no flushing implemented + # so this setting as to be reserved to high traffic suricata. + # pipelining: + # enabled: yes ## set enable to yes to enable query pipelining + # batch-size: 10 ## number of entry to keep in buffer + + # Include top level metadata. Default yes. + #metadata: no + + # include the name of the input pcap file in pcap file processing mode + pcap-file: false + + # Community Flow ID + # Adds a 'community_id' field to EVE records. These are meant to give + # a records a predictable flow id that can be used to match records to + # output of other tools such as Bro. + # + # Takes a 'seed' that needs to be same across sensors and tools + # to make the id less predictable. + + # enable/disable the community id feature. + community-id: false + # Seed value for the ID output. Valid values are 0-65535. + community-id-seed: 0 + + # HTTP X-Forwarded-For support by adding an extra field or overwriting + # the source or destination IP address (depending on flow direction) + # with the one reported in the X-Forwarded-For HTTP header. This is + # helpful when reviewing alerts for traffic that is being reverse + # or forward proxied. + xff: + enabled: no + # Two operation modes are available, "extra-data" and "overwrite". + mode: extra-data + # Two proxy deployments are supported, "reverse" and "forward". In + # a "reverse" deployment the IP address used is the last one, in a + # "forward" deployment the first IP address is used. + deployment: reverse + # Header name where the actual IP address will be reported, if more + # than one IP address is present, the last IP address will be the + # one taken into consideration. + header: X-Forwarded-For + + types: + - alert: + # payload: yes # enable dumping payload in Base64 + # payload-buffer-size: 4kb # max size of payload buffer to output in eve-log + # payload-printable: yes # enable dumping payload in printable (lossy) format + # packet: yes # enable dumping of packet (without stream segments) + # metadata: no # enable inclusion of app layer metadata with alert. Default yes + # http-body: yes # Requires metadata; enable dumping of http body in Base64 + # http-body-printable: yes # Requires metadata; enable dumping of http body in printable format + + # Enable the logging of tagged packets for rules using the + # "tag" keyword. + tagged-packets: yes + - anomaly: + # Anomaly log records describe unexpected conditions such + # as truncated packets, packets with invalid IP/UDP/TCP + # length values, and other events that render the packet + # invalid for further processing or describe unexpected + # behavior on an established stream. Networks which + # experience high occurrences of anomalies may experience + # packet processing degradation. + # + # Anomalies are reported for the following: + # 1. Decode: Values and conditions that are detected while + # decoding individual packets. This includes invalid or + # unexpected values for low-level protocol lengths as well + # as stream related events (TCP 3-way handshake issues, + # unexpected sequence number, etc). + # 2. Stream: This includes stream related events (TCP + # 3-way handshake issues, unexpected sequence number, + # etc). + # 3. Application layer: These denote application layer + # specific conditions that are unexpected, invalid or are + # unexpected given the application monitoring state. + # + # By default, anomaly logging is disabled. When anomaly + # logging is enabled, applayer anomaly reporting is + # enabled. + enabled: yes + # + # Choose one or more types of anomaly logging and whether to enable + # logging of the packet header for packet anomalies. + types: + # decode: no + # stream: no + # applayer: yes + #packethdr: no + - http: + extended: yes # enable this for extended logging information + # custom allows additional http fields to be included in eve-log + # the example below adds three additional fields when uncommented + #custom: [Accept-Encoding, Accept-Language, Authorization] + # set this value to one and only one among {both, request, response} + # to dump all http headers for every http request and/or response + # dump-all-headers: none + - dns: + # This configuration uses the new DNS logging format, + # the old configuration is still available: + # https://suricata.readthedocs.io/en/latest/output/eve/eve-json-output.html#dns-v1-format + + # As of Suricata 5.0, version 2 of the eve dns output + # format is the default. + #version: 2 + + # Enable/disable this logger. Default: enabled. + #enabled: yes + + # Control logging of requests and responses: + # - requests: enable logging of DNS queries + # - responses: enable logging of DNS answers + # By default both requests and responses are logged. + #requests: no + #responses: no + + # Format of answer logging: + # - detailed: array item per answer + # - grouped: answers aggregated by type + # Default: all + #formats: [detailed, grouped] + + # Types to log, based on the query type. + # Default: all. + #types: [a, aaaa, cname, mx, ns, ptr, txt] + - tls: + extended: yes # enable this for extended logging information + # output TLS transaction where the session is resumed using a + # session id + #session-resumption: no + # custom allows to control which tls fields that are included + # in eve-log + #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s] + - files: + force-magic: no # force logging magic on all logged files + # force logging of checksums, available hash functions are md5, + # sha1 and sha256 + #force-hash: [md5] + #- drop: + # alerts: yes # log alerts that caused drops + # flows: all # start or all: 'start' logs only a single drop + # # per flow direction. All logs each dropped pkt. + - smtp: + #extended: yes # enable this for extended logging information + # this includes: bcc, message-id, subject, x_mailer, user-agent + # custom fields logging from the list: + # reply-to, bcc, message-id, subject, x-mailer, user-agent, received, + # x-originating-ip, in-reply-to, references, importance, priority, + # sensitivity, organization, content-md5, date + #custom: [received, x-mailer, x-originating-ip, relays, reply-to, bcc] + # output md5 of fields: body, subject + # for the body you need to set app-layer.protocols.smtp.mime.body-md5 + # to yes + #md5: [body, subject] + + #- dnp3 + - ftp + #- rdp + - nfs + - smb + - tftp + - ikev2 + - krb5 + - snmp + #- sip + - dhcp: + enabled: yes + # When extended mode is on, all DHCP messages are logged + # with full detail. When extended mode is off (the + # default), just enough information to map a MAC address + # to an IP address is logged. + extended: no + - ssh + - stats: + totals: yes # stats for all threads merged together + threads: no # per thread stats + deltas: no # include delta values + # bi-directional flows + - flow + # uni-directional flows + #- netflow + + # Metadata event type. Triggered whenever a pktvar is saved + # and will include the pktvars, flowvars, flowbits and + # flowints. + #- metadata + + # deprecated - unified2 alert format for use with Barnyard2 + - unified2-alert: + enabled: no + # for further options see: + # https://suricata.readthedocs.io/en/suricata-5.0.0/configuration/suricata-yaml.html#alert-output-for-use-with-barnyard2-unified2-alert + + # a line based log of HTTP requests (no alerts) + - http-log: + enabled: no + filename: http.log + append: yes + #extended: yes # enable this for extended logging information + #custom: yes # enabled the custom logging format (defined by customformat) + #customformat: "%{%D-%H:%M:%S}t.%z %{X-Forwarded-For}i %H %m %h %u %s %B %a:%p -> %A:%P" + #filetype: regular # 'regular', 'unix_stream' or 'unix_dgram' + + # a line based log of TLS handshake parameters (no alerts) + - tls-log: + enabled: no # Log TLS connections. + filename: tls.log # File to store TLS logs. + append: yes + #extended: yes # Log extended information like fingerprint + #custom: yes # enabled the custom logging format (defined by customformat) + #customformat: "%{%D-%H:%M:%S}t.%z %a:%p -> %A:%P %v %n %d %D" + #filetype: regular # 'regular', 'unix_stream' or 'unix_dgram' + # output TLS transaction where the session is resumed using a + # session id + #session-resumption: no + + # output module to store certificates chain to disk + - tls-store: + enabled: no + #certs-log-dir: certs # directory to store the certificates files + + # Packet log... log packets in pcap format. 3 modes of operation: "normal" + # "multi" and "sguil". + # + # In normal mode a pcap file "filename" is created in the default-log-dir, + # or are as specified by "dir". + # In multi mode, a file is created per thread. This will perform much + # better, but will create multiple files where 'normal' would create one. + # In multi mode the filename takes a few special variables: + # - %n -- thread number + # - %i -- thread id + # - %t -- timestamp (secs or secs.usecs based on 'ts-format' + # E.g. filename: pcap.%n.%t + # + # Note that it's possible to use directories, but the directories are not + # created by Suricata. E.g. filename: pcaps/%n/log.%s will log into the + # per thread directory. + # + # Also note that the limit and max-files settings are enforced per thread. + # So the size limit when using 8 threads with 1000mb files and 2000 files + # is: 8*1000*2000 ~ 16TiB. + # + # In Sguil mode "dir" indicates the base directory. In this base dir the + # pcaps are created in th directory structure Sguil expects: + # + # $sguil-base-dir/YYYY-MM-DD/$filename. + # + # By default all packets are logged except: + # - TCP streams beyond stream.reassembly.depth + # - encrypted streams after the key exchange + # + - pcap-log: + enabled: no + filename: log.pcap + + # File size limit. Can be specified in kb, mb, gb. Just a number + # is parsed as bytes. + limit: 1000mb + + # If set to a value will enable ring buffer mode. Will keep Maximum of "max-files" of size "limit" + max-files: 2000 + + # Compression algorithm for pcap files. Possible values: none, lz4. + # Enabling compression is incompatible with the sguil mode. Note also + # that on Windows, enabling compression will *increase* disk I/O. + compression: none + + # Further options for lz4 compression. The compression level can be set + # to a value between 0 and 16, where higher values result in higher + # compression. + #lz4-checksum: no + #lz4-level: 0 + + mode: normal # normal, multi or sguil. + + # Directory to place pcap files. If not provided the default log + # directory will be used. Required for "sguil" mode. + #dir: /nsm_data/ + + #ts-format: usec # sec or usec second format (default) is filename.sec usec is filename.sec.usec + use-stream-depth: no #If set to "yes" packets seen after reaching stream inspection depth are ignored. "no" logs all packets + honor-pass-rules: no # If set to "yes", flows in which a pass rule matched will stopped being logged. + + # a full alerts log containing much information for signature writers + # or for investigating suspected false positives. + - alert-debug: + enabled: no + filename: alert-debug.log + append: yes + #filetype: regular # 'regular', 'unix_stream' or 'unix_dgram' + + # alert output to prelude (https://www.prelude-siem.org/) only + # available if Suricata has been compiled with --enable-prelude + - alert-prelude: + enabled: no + profile: suricata + log-packet-content: no + log-packet-header: yes + + # Stats.log contains data from various counters of the Suricata engine. + - stats: + enabled: yes + filename: stats.log + append: no # append to file (yes) or overwrite it (no) + totals: yes # stats for all threads merged together + threads: no # per thread stats + null-values: yes # print counters that have value 0 + + # a line based alerts log similar to fast.log into syslog + - syslog: + enabled: no + # reported identity to syslog. If ommited the program name (usually + # suricata) will be used. + #identity: "suricata" + facility: local5 + #level: Info ## possible levels: Emergency, Alert, Critical, + ## Error, Warning, Notice, Info, Debug + + # deprecated a line based information for dropped packets in IPS mode + - drop: + enabled: no + # further options documented at: + # https://suricata.readthedocs.io/en/suricata-5.0.0/configuration/suricata-yaml.html#drop-log-a-line-based-information-for-dropped-packets + + # Output module for storing files on disk. Files are stored in a + # directory names consisting of the first 2 characters of the + # SHA256 of the file. Each file is given its SHA256 as a filename. + # + # When a duplicate file is found, the existing file is touched to + # have its timestamps updated. + # + # Unlike the older filestore, metadata is not written out by default + # as each file should already have a "fileinfo" record in the + # eve.log. If write-fileinfo is set to yes, the each file will have + # one more associated .json files that consists of the fileinfo + # record. A fileinfo file will be written for each occurrence of the + # file seen using a filename suffix to ensure uniqueness. + # + # To prune the filestore directory see the "suricatactl filestore + # prune" command which can delete files over a certain age. + - file-store: + version: 2 + enabled: no + + # Set the directory for the filestore. If the path is not + # absolute will be be relative to the default-log-dir. + #dir: filestore + + # Write out a fileinfo record for each occurrence of a + # file. Disabled by default as each occurrence is already logged + # as a fileinfo record to the main eve-log. + #write-fileinfo: yes + + # Force storing of all files. Default: no. + #force-filestore: yes + + # Override the global stream-depth for sessions in which we want + # to perform file extraction. Set to 0 for unlimited. + #stream-depth: 0 + + # Uncomment the following variable to define how many files can + # remain open for filestore by Suricata. Default value is 0 which + # means files get closed after each write + #max-open-files: 1000 + + # Force logging of checksums, available hash functions are md5, + # sha1 and sha256. Note that SHA256 is automatically forced by + # the use of this output module as it uses the SHA256 as the + # file naming scheme. + #force-hash: [sha1, md5] + # NOTE: X-Forwarded configuration is ignored if write-fileinfo is disabled + # HTTP X-Forwarded-For support by adding an extra field or overwriting + # the source or destination IP address (depending on flow direction) + # with the one reported in the X-Forwarded-For HTTP header. This is + # helpful when reviewing alerts for traffic that is being reverse + # or forward proxied. + xff: + enabled: no + # Two operation modes are available, "extra-data" and "overwrite". + mode: extra-data + # Two proxy deployments are supported, "reverse" and "forward". In + # a "reverse" deployment the IP address used is the last one, in a + # "forward" deployment the first IP address is used. + deployment: reverse + # Header name where the actual IP address will be reported, if more + # than one IP address is present, the last IP address will be the + # one taken into consideration. + header: X-Forwarded-For + + # deprecated - file-store v1 + - file-store: + enabled: no + # further options documented at: + # https://suricata.readthedocs.io/en/suricata-5.0.0/file-extraction/file-extraction.html#file-store-version-1 + + # Log TCP data after stream normalization + # 2 types: file or dir. File logs into a single logfile. Dir creates + # 2 files per TCP session and stores the raw TCP data into them. + # Using 'both' will enable both file and dir modes. + # + # Note: limited by stream.reassembly.depth + - tcp-data: + enabled: no + type: file + filename: tcp-data.log + + # Log HTTP body data after normalization, dechunking and unzipping. + # 2 types: file or dir. File logs into a single logfile. Dir creates + # 2 files per HTTP session and stores the normalized data into them. + # Using 'both' will enable both file and dir modes. + # + # Note: limited by the body limit settings + - http-body-data: + enabled: no + type: file + filename: http-data.log + + # Lua Output Support - execute lua script to generate alert and event + # output. + # Documented at: + # https://suricata.readthedocs.io/en/latest/output/lua-output.html + - lua: + enabled: no + #scripts-dir: /etc/suricata/lua-output/ + scripts: + # - script1.lua + +# Logging configuration. This is not about logging IDS alerts/events, but +# output about what Suricata is doing, like startup messages, errors, etc. +logging: + # The default log level, can be overridden in an output section. + # Note that debug level logging will only be emitted if Suricata was + # compiled with the --enable-debug configure option. + # + # This value is overridden by the SC_LOG_LEVEL env var. + default-log-level: notice + + # The default output format. Optional parameter, should default to + # something reasonable if not provided. Can be overridden in an + # output section. You can leave this out to get the default. + # + # This value is overridden by the SC_LOG_FORMAT env var. + #default-log-format: "[%i] %t - (%f:%l) <%d> (%n) -- " + + # A regex to filter output. Can be overridden in an output section. + # Defaults to empty (no filter). + # + # This value is overridden by the SC_LOG_OP_FILTER env var. + default-output-filter: + + # Define your logging outputs. If none are defined, or they are all + # disabled you will get the default - console output. + outputs: + - console: + enabled: yes + # type: json + - file: + enabled: yes + level: info + filename: suricata.log + # type: json + - syslog: + enabled: no + facility: local5 + format: "[%i] <%d> -- " + # type: json + + +## +## Step 4: configure common capture settings +## +## See "Advanced Capture Options" below for more options, including NETMAP +## and PF_RING. +## + +# Linux high speed capture support +af-packet: + - interface: {{ .Interface }} + # Number of receive threads. "auto" uses the number of cores + #threads: auto + # Default clusterid. AF_PACKET will load balance packets based on flow. + cluster-id: 99 + # Default AF_PACKET cluster type. AF_PACKET can load balance per flow or per hash. + # This is only supported for Linux kernel > 3.1 + # possible value are: + # * cluster_flow: all packets of a given flow are send to the same socket + # * cluster_cpu: all packets treated in kernel by a CPU are send to the same socket + # * cluster_qm: all packets linked by network card to a RSS queue are sent to the same + # socket. Requires at least Linux 3.14. + # * cluster_ebpf: eBPF file load balancing. See doc/userguide/capture-hardware/ebpf-xdp.rst for + # more info. + # Recommended modes are cluster_flow on most boxes and cluster_cpu or cluster_qm on system + # with capture card using RSS (require cpu affinity tuning and system irq tuning) + cluster-type: cluster_flow + # In some fragmentation case, the hash can not be computed. If "defrag" is set + # to yes, the kernel will do the needed defragmentation before sending the packets. + defrag: yes + # To use the ring feature of AF_PACKET, set 'use-mmap' to yes + #use-mmap: yes + # Lock memory map to avoid it goes to swap. Be careful that over subscribing could lock + # your system + #mmap-locked: yes + # Use tpacket_v3 capture mode, only active if use-mmap is true + # Don't use it in IPS or TAP mode as it causes severe latency + #tpacket-v3: yes + # Ring size will be computed with respect to max_pending_packets and number + # of threads. You can set manually the ring size in number of packets by setting + # the following value. If you are using flow cluster-type and have really network + # intensive single-flow you could want to set the ring-size independently of the number + # of threads: + #ring-size: 2048 + # Block size is used by tpacket_v3 only. It should set to a value high enough to contain + # a decent number of packets. Size is in bytes so please consider your MTU. It should be + # a power of 2 and it must be multiple of page size (usually 4096). + #block-size: 32768 + # tpacket_v3 block timeout: an open block is passed to userspace if it is not + # filled after block-timeout milliseconds. + #block-timeout: 10 + # On busy system, this could help to set it to yes to recover from a packet drop + # phase. This will result in some packets (at max a ring flush) being non treated. + #use-emergency-flush: yes + # recv buffer size, increase value could improve performance + # buffer-size: 32768 + # Set to yes to disable promiscuous mode + # disable-promisc: no + # Choose checksum verification mode for the interface. At the moment + # of the capture, some packets may be with an invalid checksum due to + # offloading to the network card of the checksum computation. + # Possible values are: + # - kernel: use indication sent by kernel for each packet (default) + # - yes: checksum validation is forced + # - no: checksum validation is disabled + # - auto: suricata uses a statistical approach to detect when + # checksum off-loading is used. + # Warning: 'checksum-validation' must be set to yes to have any validation + #checksum-checks: kernel + # BPF filter to apply to this interface. The pcap filter syntax apply here. + #bpf-filter: port 80 or udp + # You can use the following variables to activate AF_PACKET tap or IPS mode. + # If copy-mode is set to ips or tap, the traffic coming to the current + # interface will be copied to the copy-iface interface. If 'tap' is set, the + # copy is complete. If 'ips' is set, the packet matching a 'drop' action + # will not be copied. + #copy-mode: ips + #copy-iface: eth1 + # For eBPF and XDP setup including bypass, filter and load balancing, please + # see doc/userguide/capture-hardware/ebpf-xdp.rst for more info. + + # Put default values here. These will be used for an interface that is not + # in the list above. + - interface: default + #threads: auto + #use-mmap: no + #tpacket-v3: yes + +# Cross platform libpcap capture support +pcap: + - interface: eth0 + # On Linux, pcap will try to use mmaped capture and will use buffer-size + # as total of memory used by the ring. So set this to something bigger + # than 1% of your bandwidth. + #buffer-size: 16777216 + #bpf-filter: "tcp and port 25" + # Choose checksum verification mode for the interface. At the moment + # of the capture, some packets may be with an invalid checksum due to + # offloading to the network card of the checksum computation. + # Possible values are: + # - yes: checksum validation is forced + # - no: checksum validation is disabled + # - auto: Suricata uses a statistical approach to detect when + # checksum off-loading is used. (default) + # Warning: 'checksum-validation' must be set to yes to have any validation + #checksum-checks: auto + # With some accelerator cards using a modified libpcap (like myricom), you + # may want to have the same number of capture threads as the number of capture + # rings. In this case, set up the threads variable to N to start N threads + # listening on the same interface. + #threads: 16 + # set to no to disable promiscuous mode: + #promisc: no + # set snaplen, if not set it defaults to MTU if MTU can be known + # via ioctl call and to full capture if not. + #snaplen: 1518 + # Put default values here + - interface: default + #checksum-checks: auto + +# Settings for reading pcap files +pcap-file: + # Possible values are: + # - yes: checksum validation is forced + # - no: checksum validation is disabled + # - auto: Suricata uses a statistical approach to detect when + # checksum off-loading is used. (default) + # Warning: 'checksum-validation' must be set to yes to have checksum tested + checksum-checks: auto + +# See "Advanced Capture Options" below for more options, including NETMAP +# and PF_RING. + + +## +## Step 5: App Layer Protocol Configuration +## + +# Configure the app-layer parsers. The protocols section details each +# protocol. +# +# The option "enabled" takes 3 values - "yes", "no", "detection-only". +# "yes" enables both detection and the parser, "no" disables both, and +# "detection-only" enables protocol detection only (parser disabled). +app-layer: + protocols: + krb5: + enabled: yes + snmp: + enabled: yes + ikev2: + enabled: yes + tls: + enabled: yes + detection-ports: + dp: 443 + + # Generate JA3 fingerprint from client hello. If not specified it + # will be disabled by default, but enabled if rules require it. + #ja3-fingerprints: auto + + # What to do when the encrypted communications start: + # - default: keep tracking TLS session, check for protocol anomalies, + # inspect tls_* keywords. Disables inspection of unmodified + # 'content' signatures. + # - bypass: stop processing this flow as much as possible. No further + # TLS parsing and inspection. Offload flow bypass to kernel + # or hardware if possible. + # - full: keep tracking and inspection as normal. Unmodified content + # keyword signatures are inspected as well. + # + # For best performance, select 'bypass'. + # + #encryption-handling: default + + dcerpc: + enabled: yes + ftp: + enabled: yes + # memcap: 64mb + # RDP, disabled by default. + rdp: + #enabled: no + ssh: + enabled: yes + smtp: + enabled: yes + raw-extraction: no + # Configure SMTP-MIME Decoder + mime: + # Decode MIME messages from SMTP transactions + # (may be resource intensive) + # This field supercedes all others because it turns the entire + # process on or off + decode-mime: yes + + # Decode MIME entity bodies (ie. base64, quoted-printable, etc.) + decode-base64: yes + decode-quoted-printable: yes + + # Maximum bytes per header data value stored in the data structure + # (default is 2000) + header-value-depth: 2000 + + # Extract URLs and save in state data structure + extract-urls: yes + # Set to yes to compute the md5 of the mail body. You will then + # be able to journalize it. + body-md5: no + # Configure inspected-tracker for file_data keyword + inspected-tracker: + content-limit: 100000 + content-inspect-min-size: 32768 + content-inspect-window: 4096 + imap: + enabled: detection-only + smb: + enabled: yes + detection-ports: + dp: 139, 445 + + # Stream reassembly size for SMB streams. By default track it completely. + #stream-depth: 0 + + nfs: + enabled: yes + tftp: + enabled: yes + dns: + # memcaps. Globally and per flow/state. + #global-memcap: 16mb + #state-memcap: 512kb + + # How many unreplied DNS requests are considered a flood. + # If the limit is reached, app-layer-event:dns.flooded; will match. + #request-flood: 500 + + tcp: + enabled: yes + detection-ports: + dp: 53 + udp: + enabled: yes + detection-ports: + dp: 53 + http: + enabled: yes + # memcap: Maximum memory capacity for http + # Default is unlimited, value can be such as 64mb + + # default-config: Used when no server-config matches + # personality: List of personalities used by default + # request-body-limit: Limit reassembly of request body for inspection + # by http_client_body & pcre /P option. + # response-body-limit: Limit reassembly of response body for inspection + # by file_data, http_server_body & pcre /Q option. + # + # For advanced options, see the user guide + + + # server-config: List of server configurations to use if address matches + # address: List of IP addresses or networks for this block + # personalitiy: List of personalities used by this block + # + # Then, all the fields from default-config can be overloaded + # + # Currently Available Personalities: + # Minimal, Generic, IDS (default), IIS_4_0, IIS_5_0, IIS_5_1, IIS_6_0, + # IIS_7_0, IIS_7_5, Apache_2 + libhtp: + default-config: + personality: IDS + + # Can be specified in kb, mb, gb. Just a number indicates + # it's in bytes. + request-body-limit: 100kb + response-body-limit: 100kb + + # inspection limits + request-body-minimal-inspect-size: 32kb + request-body-inspect-window: 4kb + response-body-minimal-inspect-size: 40kb + response-body-inspect-window: 16kb + + # response body decompression (0 disables) + response-body-decompress-layer-limit: 2 + + # auto will use http-body-inline mode in IPS mode, yes or no set it statically + http-body-inline: auto + + # Decompress SWF files. + # 2 types: 'deflate', 'lzma', 'both' will decompress deflate and lzma + # compress-depth: + # Specifies the maximum amount of data to decompress, + # set 0 for unlimited. + # decompress-depth: + # Specifies the maximum amount of decompressed data to obtain, + # set 0 for unlimited. + swf-decompression: + enabled: yes + type: both + compress-depth: 0 + decompress-depth: 0 + + # Take a random value for inspection sizes around the specified value. + # This lower the risk of some evasion technics but could lead + # detection change between runs. It is set to 'yes' by default. + #randomize-inspection-sizes: yes + # If randomize-inspection-sizes is active, the value of various + # inspection size will be choosen in the [1 - range%, 1 + range%] + # range + # Default value of randomize-inspection-range is 10. + #randomize-inspection-range: 10 + + # decoding + double-decode-path: no + double-decode-query: no + + # Can disable LZMA decompression + #lzma-enabled: yes + # Memory limit usage for LZMA decompression dictionary + # Data is decompressed until dictionary reaches this size + #lzma-memlimit: 1mb + # Maximum decompressed size with a compression ratio + # above 2048 (only LZMA can reach this ratio, deflate cannot) + #compression-bomb-limit: 1mb + + server-config: + + #- apache: + # address: [192.168.1.0/24, 127.0.0.0/8, "::1"] + # personality: Apache_2 + # # Can be specified in kb, mb, gb. Just a number indicates + # # it's in bytes. + # request-body-limit: 4096 + # response-body-limit: 4096 + # double-decode-path: no + # double-decode-query: no + + #- iis7: + # address: + # - 192.168.0.0/24 + # - 192.168.10.0/24 + # personality: IIS_7_0 + # # Can be specified in kb, mb, gb. Just a number indicates + # # it's in bytes. + # request-body-limit: 4096 + # response-body-limit: 4096 + # double-decode-path: no + # double-decode-query: no + + # Note: Modbus probe parser is minimalist due to the poor significant field + # Only Modbus message length (greater than Modbus header length) + # And Protocol ID (equal to 0) are checked in probing parser + # It is important to enable detection port and define Modbus port + # to avoid false positive + modbus: + # How many unreplied Modbus requests are considered a flood. + # If the limit is reached, app-layer-event:modbus.flooded; will match. + #request-flood: 500 + + enabled: no + detection-ports: + dp: 502 + # According to MODBUS Messaging on TCP/IP Implementation Guide V1.0b, it + # is recommended to keep the TCP connection opened with a remote device + # and not to open and close it for each MODBUS/TCP transaction. In that + # case, it is important to set the depth of the stream reassembling as + # unlimited (stream.reassembly.depth: 0) + + # Stream reassembly size for modbus. By default track it completely. + stream-depth: 0 + + # DNP3 + dnp3: + enabled: no + detection-ports: + dp: 20000 + + # SCADA EtherNet/IP and CIP protocol support + enip: + enabled: no + detection-ports: + dp: 44818 + sp: 44818 + + ntp: + enabled: yes + + dhcp: + enabled: yes + + # SIP, disabled by default. + sip: + #enabled: no + +# Limit for the maximum number of asn1 frames to decode (default 256) +asn1-max-frames: 256 + + +############################################################################## +## +## Advanced settings below +## +############################################################################## + +## +## Run Options +## + +# Run suricata as user and group. +#run-as: +# user: suri +# group: suri + +# Some logging module will use that name in event as identifier. The default +# value is the hostname +#sensor-name: suricata + +# Default location of the pid file. The pid file is only used in +# daemon mode (start Suricata with -D). If not running in daemon mode +# the --pidfile command line option must be used to create a pid file. +#pid-file: /var/run/suricata.pid + +# Daemon working directory +# Suricata will change directory to this one if provided +# Default: "/" +#daemon-directory: "/" + +# Umask. +# Suricata will use this umask if it is provided. By default it will use the +# umask passed on by the shell. +#umask: 022 + +# Suricata core dump configuration. Limits the size of the core dump file to +# approximately max-dump. The actual core dump size will be a multiple of the +# page size. Core dumps that would be larger than max-dump are truncated. On +# Linux, the actual core dump size may be a few pages larger than max-dump. +# Setting max-dump to 0 disables core dumping. +# Setting max-dump to 'unlimited' will give the full core dump file. +# On 32-bit Linux, a max-dump value >= ULONG_MAX may cause the core dump size +# to be 'unlimited'. + +coredump: + max-dump: unlimited + +# If Suricata box is a router for the sniffed networks, set it to 'router'. If +# it is a pure sniffing setup, set it to 'sniffer-only'. +# If set to auto, the variable is internally switch to 'router' in IPS mode +# and 'sniffer-only' in IDS mode. +# This feature is currently only used by the reject* keywords. +host-mode: auto + +# Number of packets preallocated per thread. The default is 1024. A higher number +# will make sure each CPU will be more easily kept busy, but may negatively +# impact caching. +#max-pending-packets: 1024 + +# Runmode the engine should use. Please check --list-runmodes to get the available +# runmodes for each packet acquisition method. Default depends on selected capture +# method. 'workers' generally gives best performance. +#runmode: autofp + +# Specifies the kind of flow load balancer used by the flow pinned autofp mode. +# +# Supported schedulers are: +# +# hash - Flow assigned to threads using the 5-7 tuple hash. +# ippair - Flow assigned to threads using addresses only. +# +#autofp-scheduler: hash + +# Preallocated size for packet. Default is 1514 which is the classical +# size for pcap on ethernet. You should adjust this value to the highest +# packet size (MTU + hardware header) on your system. +#default-packet-size: 1514 + +# Unix command socket can be used to pass commands to Suricata. +# An external tool can then connect to get information from Suricata +# or trigger some modifications of the engine. Set enabled to yes +# to activate the feature. In auto mode, the feature will only be +# activated in live capture mode. You can use the filename variable to set +# the file name of the socket. +unix-command: + enabled: true + filename: /run/suricata-command.socket + +# Magic file. The extension .mgc is added to the value here. +#magic-file: /usr/share/file/magic +#magic-file: + +# GeoIP2 database file. Specify path and filename of GeoIP2 database +# if using rules with "geoip" rule option. +#geoip-database: /usr/local/share/GeoLite2/GeoLite2-Country.mmdb + +legacy: + uricontent: enabled + +## +## Detection settings +## + +# Set the order of alerts based on actions +# The default order is pass, drop, reject, alert +# action-order: +# - pass +# - drop +# - reject +# - alert + +# IP Reputation +#reputation-categories-file: /etc/suricata/iprep/categories.txt +#default-reputation-path: /etc/suricata/iprep +#reputation-files: +# - reputation.list + +# When run with the option --engine-analysis, the engine will read each of +# the parameters below, and print reports for each of the enabled sections +# and exit. The reports are printed to a file in the default log dir +# given by the parameter "default-log-dir", with engine reporting +# subsection below printing reports in its own report file. +engine-analysis: + # enables printing reports for fast-pattern for every rule. + rules-fast-pattern: yes + # enables printing reports for each rule + rules: yes + +#recursion and match limits for PCRE where supported +pcre: + match-limit: 3500 + match-limit-recursion: 1500 + +## +## Advanced Traffic Tracking and Reconstruction Settings +## + +# Host specific policies for defragmentation and TCP stream +# reassembly. The host OS lookup is done using a radix tree, just +# like a routing table so the most specific entry matches. +host-os-policy: + # Make the default policy windows. + windows: [0.0.0.0/0] + bsd: [] + bsd-right: [] + old-linux: [] + linux: [] + old-solaris: [] + solaris: [] + hpux10: [] + hpux11: [] + irix: [] + macos: [] + vista: [] + windows2k3: [] + +# Defrag settings: + +defrag: + memcap: 32mb + hash-size: 65536 + trackers: 65535 # number of defragmented flows to follow + max-frags: 65535 # number of fragments to keep (higher than trackers) + prealloc: yes + timeout: 60 + +# Enable defrag per host settings +# host-config: +# +# - dmz: +# timeout: 30 +# address: [192.168.1.0/24, 127.0.0.0/8, 1.1.1.0/24, 2.2.2.0/24, "1.1.1.1", "2.2.2.2", "::1"] +# +# - lan: +# timeout: 45 +# address: +# - 192.168.0.0/24 +# - 192.168.10.0/24 +# - 172.16.14.0/24 + +# Flow settings: +# By default, the reserved memory (memcap) for flows is 32MB. This is the limit +# for flow allocation inside the engine. You can change this value to allow +# more memory usage for flows. +# The hash-size determine the size of the hash used to identify flows inside +# the engine, and by default the value is 65536. +# At the startup, the engine can preallocate a number of flows, to get a better +# performance. The number of flows preallocated is 10000 by default. +# emergency-recovery is the percentage of flows that the engine need to +# prune before unsetting the emergency state. The emergency state is activated +# when the memcap limit is reached, allowing to create new flows, but +# pruning them with the emergency timeouts (they are defined below). +# If the memcap is reached, the engine will try to prune flows +# with the default timeouts. If it doesn't find a flow to prune, it will set +# the emergency bit and it will try again with more aggressive timeouts. +# If that doesn't work, then it will try to kill the last time seen flows +# not in use. +# The memcap can be specified in kb, mb, gb. Just a number indicates it's +# in bytes. + +flow: + memcap: 128mb + hash-size: 65536 + prealloc: 10000 + emergency-recovery: 30 + #managers: 1 # default to one flow manager + #recyclers: 1 # default to one flow recycler thread + +# This option controls the use of vlan ids in the flow (and defrag) +# hashing. Normally this should be enabled, but in some (broken) +# setups where both sides of a flow are not tagged with the same vlan +# tag, we can ignore the vlan id's in the flow hashing. +vlan: + use-for-tracking: true + +# Specific timeouts for flows. Here you can specify the timeouts that the +# active flows will wait to transit from the current state to another, on each +# protocol. The value of "new" determine the seconds to wait after a handshake or +# stream startup before the engine free the data of that flow it doesn't +# change the state to established (usually if we don't receive more packets +# of that flow). The value of "established" is the amount of +# seconds that the engine will wait to free the flow if it spend that amount +# without receiving new packets or closing the connection. "closed" is the +# amount of time to wait after a flow is closed (usually zero). "bypassed" +# timeout controls locally bypassed flows. For these flows we don't do any other +# tracking. If no packets have been seen after this timeout, the flow is discarded. +# +# There's an emergency mode that will become active under attack circumstances, +# making the engine to check flow status faster. This configuration variables +# use the prefix "emergency-" and work similar as the normal ones. +# Some timeouts doesn't apply to all the protocols, like "closed", for udp and +# icmp. + +flow-timeouts: + + default: + new: 30 + established: 300 + closed: 0 + bypassed: 100 + emergency-new: 10 + emergency-established: 100 + emergency-closed: 0 + emergency-bypassed: 50 + tcp: + new: 60 + established: 600 + closed: 60 + bypassed: 100 + emergency-new: 5 + emergency-established: 100 + emergency-closed: 10 + emergency-bypassed: 50 + udp: + new: 30 + established: 300 + bypassed: 100 + emergency-new: 10 + emergency-established: 100 + emergency-bypassed: 50 + icmp: + new: 30 + established: 300 + bypassed: 100 + emergency-new: 10 + emergency-established: 100 + emergency-bypassed: 50 + +# Stream engine settings. Here the TCP stream tracking and reassembly +# engine is configured. +# +# stream: +# memcap: 32mb # Can be specified in kb, mb, gb. Just a +# # number indicates it's in bytes. +# checksum-validation: yes # To validate the checksum of received +# # packet. If csum validation is specified as +# # "yes", then packet with invalid csum will not +# # be processed by the engine stream/app layer. +# # Warning: locally generated traffic can be +# # generated without checksum due to hardware offload +# # of checksum. You can control the handling of checksum +# # on a per-interface basis via the 'checksum-checks' +# # option +# prealloc-sessions: 2k # 2k sessions prealloc'd per stream thread +# midstream: false # don't allow midstream session pickups +# async-oneside: false # don't enable async stream handling +# inline: no # stream inline mode +# drop-invalid: yes # in inline mode, drop packets that are invalid with regards to streaming engine +# max-synack-queued: 5 # Max different SYN/ACKs to queue +# bypass: no # Bypass packets when stream.reassembly.depth is reached. +# # Warning: first side to reach this triggers +# # the bypass. +# +# reassembly: +# memcap: 64mb # Can be specified in kb, mb, gb. Just a number +# # indicates it's in bytes. +# depth: 1mb # Can be specified in kb, mb, gb. Just a number +# # indicates it's in bytes. +# toserver-chunk-size: 2560 # inspect raw stream in chunks of at least +# # this size. Can be specified in kb, mb, +# # gb. Just a number indicates it's in bytes. +# toclient-chunk-size: 2560 # inspect raw stream in chunks of at least +# # this size. Can be specified in kb, mb, +# # gb. Just a number indicates it's in bytes. +# randomize-chunk-size: yes # Take a random value for chunk size around the specified value. +# # This lower the risk of some evasion technics but could lead +# # detection change between runs. It is set to 'yes' by default. +# randomize-chunk-range: 10 # If randomize-chunk-size is active, the value of chunk-size is +# # a random value between (1 - randomize-chunk-range/100)*toserver-chunk-size +# # and (1 + randomize-chunk-range/100)*toserver-chunk-size and the same +# # calculation for toclient-chunk-size. +# # Default value of randomize-chunk-range is 10. +# +# raw: yes # 'Raw' reassembly enabled or disabled. +# # raw is for content inspection by detection +# # engine. +# +# segment-prealloc: 2048 # number of segments preallocated per thread +# +# check-overlap-different-data: true|false +# # check if a segment contains different data +# # than what we've already seen for that +# # position in the stream. +# # This is enabled automatically if inline mode +# # is used or when stream-event:reassembly_overlap_different_data; +# # is used in a rule. +# +stream: + memcap: 64mb + checksum-validation: yes # reject wrong csums + inline: auto # auto will use inline mode in IPS mode, yes or no set it statically + reassembly: + memcap: 256mb + depth: 1mb # reassemble 1mb into a stream + toserver-chunk-size: 2560 + toclient-chunk-size: 2560 + randomize-chunk-size: yes + #randomize-chunk-range: 10 + #raw: yes + #segment-prealloc: 2048 + #check-overlap-different-data: true + +# Host table: +# +# Host table is used by tagging and per host thresholding subsystems. +# +host: + hash-size: 4096 + prealloc: 1000 + memcap: 32mb + +# IP Pair table: +# +# Used by xbits 'ippair' tracking. +# +#ippair: +# hash-size: 4096 +# prealloc: 1000 +# memcap: 32mb + +# Decoder settings + +decoder: + # Teredo decoder is known to not be completely accurate + # as it will sometimes detect non-teredo as teredo. + teredo: + enabled: true + # VXLAN decoder is assigned to up to 4 UDP ports. By default only the + # IANA assigned port 4789 is enabled. + vxlan: + enabled: true + ports: $VXLAN_PORTS # syntax: '8472, 4789' + + +## +## Performance tuning and profiling +## + +# The detection engine builds internal groups of signatures. The engine +# allow us to specify the profile to use for them, to manage memory on an +# efficient way keeping a good performance. For the profile keyword you +# can use the words "low", "medium", "high" or "custom". If you use custom +# make sure to define the values at "- custom-values" as your convenience. +# Usually you would prefer medium/high/low. +# +# "sgh mpm-context", indicates how the staging should allot mpm contexts for +# the signature groups. "single" indicates the use of a single context for +# all the signature group heads. "full" indicates a mpm-context for each +# group head. "auto" lets the engine decide the distribution of contexts +# based on the information the engine gathers on the patterns from each +# group head. +# +# The option inspection-recursion-limit is used to limit the recursive calls +# in the content inspection code. For certain payload-sig combinations, we +# might end up taking too much time in the content inspection code. +# If the argument specified is 0, the engine uses an internally defined +# default limit. On not specifying a value, we use no limits on the recursion. +detect: + profile: medium + custom-values: + toclient-groups: 3 + toserver-groups: 25 + sgh-mpm-context: auto + inspection-recursion-limit: 3000 + # If set to yes, the loading of signatures will be made after the capture + # is started. This will limit the downtime in IPS mode. + #delayed-detect: yes + + prefilter: + # default prefiltering setting. "mpm" only creates MPM/fast_pattern + # engines. "auto" also sets up prefilter engines for other keywords. + # Use --list-keywords=all to see which keywords support prefiltering. + default: mpm + + # the grouping values above control how many groups are created per + # direction. Port whitelisting forces that port to get it's own group. + # Very common ports will benefit, as well as ports with many expensive + # rules. + grouping: + #tcp-whitelist: 53, 80, 139, 443, 445, 1433, 3306, 3389, 6666, 6667, 8080 + #udp-whitelist: 53, 135, 5060 + + profiling: + # Log the rules that made it past the prefilter stage, per packet + # default is off. The threshold setting determines how many rules + # must have made it past pre-filter for that rule to trigger the + # logging. + #inspect-logging-threshold: 200 + grouping: + dump-to-disk: false + include-rules: false # very verbose + include-mpm-stats: false + +# Select the multi pattern algorithm you want to run for scan/search the +# in the engine. +# +# The supported algorithms are: +# "ac" - Aho-Corasick, default implementation +# "ac-bs" - Aho-Corasick, reduced memory implementation +# "ac-ks" - Aho-Corasick, "Ken Steele" variant +# "hs" - Hyperscan, available when built with Hyperscan support +# +# The default mpm-algo value of "auto" will use "hs" if Hyperscan is +# available, "ac" otherwise. +# +# The mpm you choose also decides the distribution of mpm contexts for +# signature groups, specified by the conf - "detect.sgh-mpm-context". +# Selecting "ac" as the mpm would require "detect.sgh-mpm-context" +# to be set to "single", because of ac's memory requirements, unless the +# ruleset is small enough to fit in one's memory, in which case one can +# use "full" with "ac". Rest of the mpms can be run in "full" mode. + +mpm-algo: auto + +# Select the matching algorithm you want to use for single-pattern searches. +# +# Supported algorithms are "bm" (Boyer-Moore) and "hs" (Hyperscan, only +# available if Suricata has been built with Hyperscan support). +# +# The default of "auto" will use "hs" if available, otherwise "bm". + +spm-algo: auto + +# Suricata is multi-threaded. Here the threading can be influenced. +threading: + set-cpu-affinity: no + # Tune cpu affinity of threads. Each family of threads can be bound + # on specific CPUs. + # + # These 2 apply to the all runmodes: + # management-cpu-set is used for flow timeout handling, counters + # worker-cpu-set is used for 'worker' threads + # + # Additionally, for autofp these apply: + # receive-cpu-set is used for capture threads + # verdict-cpu-set is used for IPS verdict threads + # + cpu-affinity: + - management-cpu-set: + cpu: [ 0 ] # include only these CPUs in affinity settings + - receive-cpu-set: + cpu: [ 0 ] # include only these CPUs in affinity settings + - worker-cpu-set: + cpu: [ "all" ] + mode: "exclusive" + # Use explicitely 3 threads and don't compute number by using + # detect-thread-ratio variable: + # threads: 3 + prio: + low: [ 0 ] + medium: [ "1-2" ] + high: [ 3 ] + default: "medium" + #- verdict-cpu-set: + # cpu: [ 0 ] + # prio: + # default: "high" + # + # By default Suricata creates one "detect" thread per available CPU/CPU core. + # This setting allows controlling this behaviour. A ratio setting of 2 will + # create 2 detect threads for each CPU/CPU core. So for a dual core CPU this + # will result in 4 detect threads. If values below 1 are used, less threads + # are created. So on a dual core CPU a setting of 0.5 results in 1 detect + # thread being created. Regardless of the setting at a minimum 1 detect + # thread will always be created. + # + detect-thread-ratio: 0.2 + +# Luajit has a strange memory requirement, it's 'states' need to be in the +# first 2G of the process' memory. +# +# 'luajit.states' is used to control how many states are preallocated. +# State use: per detect script: 1 per detect thread. Per output script: 1 per +# script. +luajit: + states: 128 + +# Profiling settings. Only effective if Suricata has been built with the +# the --enable-profiling configure flag. +# +profiling: + # Run profiling for every xth packet. The default is 1, which means we + # profile every packet. If set to 1000, one packet is profiled for every + # 1000 received. + #sample-rate: 1000 + + # rule profiling + rules: + + # Profiling can be disabled here, but it will still have a + # performance impact if compiled in. + enabled: yes + filename: rule_perf.log + append: yes + + # Sort options: ticks, avgticks, checks, matches, maxticks + # If commented out all the sort options will be used. + #sort: avgticks + + # Limit the number of sids for which stats are shown at exit (per sort). + limit: 10 + + # output to json + json: yes + + # per keyword profiling + keywords: + enabled: yes + filename: keyword_perf.log + append: yes + + prefilter: + enabled: yes + filename: prefilter_perf.log + append: yes + + # per rulegroup profiling + rulegroups: + enabled: yes + filename: rule_group_perf.log + append: yes + + # packet profiling + packets: + + # Profiling can be disabled here, but it will still have a + # performance impact if compiled in. + enabled: yes + filename: packet_stats.log + append: yes + + # per packet csv output + csv: + + # Output can be disabled here, but it will still have a + # performance impact if compiled in. + enabled: no + filename: packet_stats.csv + + # profiling of locking. Only available when Suricata was built with + # --enable-profiling-locks. + locks: + enabled: no + filename: lock_stats.log + append: yes + + pcap-log: + enabled: no + filename: pcaplog_stats.log + append: yes + +## +## Netfilter integration +## + +# When running in NFQ inline mode, it is possible to use a simulated +# non-terminal NFQUEUE verdict. +# This permit to do send all needed packet to Suricata via this a rule: +# iptables -I FORWARD -m mark ! --mark $MARK/$MASK -j NFQUEUE +# And below, you can have your standard filtering ruleset. To activate +# this mode, you need to set mode to 'repeat' +# If you want packet to be sent to another queue after an ACCEPT decision +# set mode to 'route' and set next-queue value. +# On linux >= 3.1, you can set batchcount to a value > 1 to improve performance +# by processing several packets before sending a verdict (worker runmode only). +# On linux >= 3.6, you can set the fail-open option to yes to have the kernel +# accept the packet if Suricata is not able to keep pace. +# bypass mark and mask can be used to implement NFQ bypass. If bypass mark is +# set then the NFQ bypass is activated. Suricata will set the bypass mark/mask +# on packet of a flow that need to be bypassed. The Nefilter ruleset has to +# directly accept all packets of a flow once a packet has been marked. +nfq: +# mode: accept +# repeat-mark: 1 +# repeat-mask: 1 +# bypass-mark: 1 +# bypass-mask: 1 +# route-queue: 2 +# batchcount: 20 +# fail-open: yes + +#nflog support +nflog: + # netlink multicast group + # (the same as the iptables --nflog-group param) + # Group 0 is used by the kernel, so you can't use it + - group: 2 + # netlink buffer size + buffer-size: 18432 + # put default value here + - group: default + # set number of packet to queue inside kernel + qthreshold: 1 + # set the delay before flushing packet in the queue inside kernel + qtimeout: 100 + # netlink max buffer size + max-size: 20000 + +## +## Advanced Capture Options +## + +# general settings affecting packet capture +capture: + # disable NIC offloading. It's restored when Suricata exits. + # Enabled by default. + #disable-offloading: false + # + # disable checksum validation. Same as setting '-k none' on the + # commandline. + #checksum-validation: none + +# Netmap support +# +# Netmap operates with NIC directly in driver, so you need FreeBSD 11+ which have +# built-in netmap support or compile and install netmap module and appropriate +# NIC driver on your Linux system. +# To reach maximum throughput disable all receive-, segmentation-, +# checksum- offloadings on NIC. +# Disabling Tx checksum offloading is *required* for connecting OS endpoint +# with NIC endpoint. +# You can find more information at https://github.com/luigirizzo/netmap +# +netmap: + # To specify OS endpoint add plus sign at the end (e.g. "eth0+") + - interface: eth2 + # Number of capture threads. "auto" uses number of RSS queues on interface. + # Warning: unless the RSS hashing is symmetrical, this will lead to + # accuracy issues. + #threads: auto + # You can use the following variables to activate netmap tap or IPS mode. + # If copy-mode is set to ips or tap, the traffic coming to the current + # interface will be copied to the copy-iface interface. If 'tap' is set, the + # copy is complete. If 'ips' is set, the packet matching a 'drop' action + # will not be copied. + # To specify the OS as the copy-iface (so the OS can route packets, or forward + # to a service running on the same machine) add a plus sign at the end + # (e.g. "copy-iface: eth0+"). Don't forget to set up a symmetrical eth0+ -> eth0 + # for return packets. Hardware checksumming must be *off* on the interface if + # using an OS endpoint (e.g. 'ifconfig eth0 -rxcsum -txcsum -rxcsum6 -txcsum6' for FreeBSD + # or 'ethtool -K eth0 tx off rx off' for Linux). + #copy-mode: tap + #copy-iface: eth3 + # Set to yes to disable promiscuous mode + # disable-promisc: no + # Choose checksum verification mode for the interface. At the moment + # of the capture, some packets may be with an invalid checksum due to + # offloading to the network card of the checksum computation. + # Possible values are: + # - yes: checksum validation is forced + # - no: checksum validation is disabled + # - auto: Suricata uses a statistical approach to detect when + # checksum off-loading is used. + # Warning: 'checksum-validation' must be set to yes to have any validation + #checksum-checks: auto + # BPF filter to apply to this interface. The pcap filter syntax apply here. + #bpf-filter: port 80 or udp + #- interface: eth3 + #threads: auto + #copy-mode: tap + #copy-iface: eth2 + # Put default values here + - interface: default + +# PF_RING configuration. for use with native PF_RING support +# for more info see http://www.ntop.org/products/pf_ring/ +pfring: + - interface: eth0 + # Number of receive threads. If set to 'auto' Suricata will first try + # to use CPU (core) count and otherwise RSS queue count. + threads: auto + + # Default clusterid. PF_RING will load balance packets based on flow. + # All threads/processes that will participate need to have the same + # clusterid. + cluster-id: 99 + + # Default PF_RING cluster type. PF_RING can load balance per flow. + # Possible values are cluster_flow or cluster_round_robin. + cluster-type: cluster_flow + + # bpf filter for this interface + #bpf-filter: tcp + + # If bypass is set then the PF_RING hw bypass is activated, when supported + # by the interface in use. Suricata will instruct the interface to bypass + # all future packets for a flow that need to be bypassed. + #bypass: yes + + # Choose checksum verification mode for the interface. At the moment + # of the capture, some packets may be with an invalid checksum due to + # offloading to the network card of the checksum computation. + # Possible values are: + # - rxonly: only compute checksum for packets received by network card. + # - yes: checksum validation is forced + # - no: checksum validation is disabled + # - auto: Suricata uses a statistical approach to detect when + # checksum off-loading is used. (default) + # Warning: 'checksum-validation' must be set to yes to have any validation + #checksum-checks: auto + # Second interface + #- interface: eth1 + # threads: 3 + # cluster-id: 93 + # cluster-type: cluster_flow + # Put default values here + - interface: default + #threads: 2 + +# For FreeBSD ipfw(8) divert(4) support. +# Please make sure you have ipfw_load="YES" and ipdivert_load="YES" +# in /etc/loader.conf or kldload'ing the appropriate kernel modules. +# Additionally, you need to have an ipfw rule for the engine to see +# the packets from ipfw. For Example: +# +# ipfw add 100 divert 8000 ip from any to any +# +# The 8000 above should be the same number you passed on the command +# line, i.e. -d 8000 +# +ipfw: + + # Reinject packets at the specified ipfw rule number. This config + # option is the ipfw rule number AT WHICH rule processing continues + # in the ipfw processing system after the engine has finished + # inspecting the packet for acceptance. If no rule number is specified, + # accepted packets are reinjected at the divert rule which they entered + # and IPFW rule processing continues. No check is done to verify + # this will rule makes sense so care must be taken to avoid loops in ipfw. + # + ## The following example tells the engine to reinject packets + # back into the ipfw firewall AT rule number 5500: + # + # ipfw-reinjection-rule-number: 5500 + + +napatech: + # The Host Buffer Allowance for all streams + # (-1 = OFF, 1 - 100 = percentage of the host buffer that can be held back) + # This may be enabled when sharing streams with another application. + # Otherwise, it should be turned off. + #hba: -1 + + # When use_all_streams is set to "yes" the initialization code will query + # the Napatech service for all configured streams and listen on all of them. + # When set to "no" the streams config array will be used. + # + # This option necessitates running the appropriate NTPL commands to create + # the desired streams prior to running suricata. + #use-all-streams: no + + # The streams to listen on when auto-config is disabled or when and threading + # cpu-affinity is disabled. This can be either: + # an individual stream (e.g. streams: [0]) + # or + # a range of streams (e.g. streams: ["0-3"]) + # + streams: ["0-3"] + + # When auto-config is enabled the streams will be created and assigned + # automatically to the NUMA node where the thread resides. If cpu-affinity + # is enabled in the threading section. Then the streams will be created + # according to the number of worker threads specified in the worker cpu set. + # Otherwise, the streams array is used to define the streams. + # + # This option cannot be used simultaneous with "use-all-streams". + # + auto-config: yes + + # Ports indicates which napatech ports are to be used in auto-config mode. + # these are the port ID's of the ports that will be merged prior to the + # traffic being distributed to the streams. + # + # This can be specified in any of the following ways: + # + # a list of individual ports (e.g. ports: [0,1,2,3]) + # + # a range of ports (e.g. ports: [0-3]) + # + # "all" to indicate that all ports are to be merged together + # (e.g. ports: [all]) + # + # This has no effect if auto-config is disabled. + # + ports: [all] + + # When auto-config is enabled the hashmode specifies the algorithm for + # determining to which stream a given packet is to be delivered. + # This can be any valid Napatech NTPL hashmode command. + # + # The most common hashmode commands are: hash2tuple, hash2tuplesorted, + # hash5tuple, hash5tuplesorted and roundrobin. + # + # See Napatech NTPL documentation other hashmodes and details on their use. + # + # This has no effect if auto-config is disabled. + # + hashmode: hash5tuplesorted + +## +## Configure Suricata to load Suricata-Update managed rules. +## +## If this section is completely commented out move down to the "Advanced rule +## file configuration". +## + +default-rule-path: /var/lib/suricata/rules + +rule-files: + - suricata.rules + +## +## Auxiliary configuration files. +## + +classification-file: /etc/suricata/classification.config +reference-config-file: /etc/suricata/reference.config +# threshold-file: /etc/suricata/threshold.config + +## +## Include other configs +## + +# Includes. Files included here will be handled as if they were +# inlined in this configuration file. +#include: include1.yaml +#include: include2.yaml diff --git a/pkg/services/suricata/suricata_defaults.tpl b/pkg/services/suricata/suricata_defaults.tpl new file mode 100644 index 0000000..44ff7b8 --- /dev/null +++ b/pkg/services/suricata/suricata_defaults.tpl @@ -0,0 +1,29 @@ +# Default config for Suricata in /etc/default + +# set to yes to start the server in the init.d script +RUN=yes + +# set to user that will run suricata in the init.d script (used for dropping privileges only) +RUN_AS_USER= + +# Configuration file to load +SURCONF=/etc/suricata/suricata.yaml + +# Listen mode: pcap, nfqueue, custom_nfqueue or af-packet +# depending on this value, only one of the two following options +# will be used (af-packet uses neither). +# Please note that IPS mode is only available when using nfqueue +LISTENMODE=af-packet + +# Interface to listen on (for pcap mode) +IFACE={{ .Interface }} + +# Queue number to listen on (for nfqueue mode) +NFQUEUE="-q 0" + +# Queue numbers to listen on (for custom_nfqueue mode) +# Multiple queues can be specified +CUSTOM_NFQUEUE="-q 0 -q 1 -q 2 -q 3" + +# Pid file +PIDFILE=/var/run/suricata.pid diff --git a/pkg/services/suricata/suricata_update.service.tpl b/pkg/services/suricata/suricata_update.service.tpl new file mode 100644 index 0000000..0f4ef7b --- /dev/null +++ b/pkg/services/suricata/suricata_update.service.tpl @@ -0,0 +1,12 @@ +[Unit] +Description=Suricata Intrusion Detection Service Rules Update + +[Service] +LimitMEMLOCK=infinity +User=root +Group=root +Type=oneshot +ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/bin/suricata-update + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/tailscale/tailscale.service.tpl b/pkg/services/tailscale/tailscale.service.tpl new file mode 100644 index 0000000..2364d65 --- /dev/null +++ b/pkg/services/tailscale/tailscale.service.tpl @@ -0,0 +1,13 @@ +[Unit] +Description=Tailscale client +After=tailscaled.service + +[Service] +LimitMEMLOCK=infinity +User=root +Group=root +ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscale up --hostname {{ .MachineID }} --auth-key {{ .AuthKey }} --login-server {{ .Address }} +Restart=on-failure + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/tailscale/tailscaled.service.tpl b/pkg/services/tailscale/tailscaled.service.tpl new file mode 100644 index 0000000..3db63ad --- /dev/null +++ b/pkg/services/tailscale/tailscaled.service.tpl @@ -0,0 +1,25 @@ +[Unit] +Description=Tailscale node agent +Documentation=https://tailscale.com/kb/ +After=network.target + +[Service] +LimitMEMLOCK=infinity +User=root +Group=root +Type=notify +Environment="TS_NO_LOGS_NO_SUPPORT=true" +ExecStartPre=ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscaled --cleanup +ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscaled --port {{ .TailscaledPort }} +ExecStopPost=ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscaled --cleanup +Restart=on-failure + +RuntimeDirectory=tailscale +RuntimeDirectoryMode=0755 +StateDirectory=tailscale +StateDirectoryMode=0700 +CacheDirectory=tailscale +CacheDirectoryMode=0750 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/template-renderer/renderer.go b/pkg/template-renderer/renderer.go new file mode 100644 index 0000000..c551827 --- /dev/null +++ b/pkg/template-renderer/renderer.go @@ -0,0 +1,181 @@ +package renderer + +import ( + "bufio" + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "log/slog" + "os" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/coreos/go-systemd/v22/dbus" + "github.com/google/uuid" + "github.com/spf13/afero" +) + +// Config provides a config for the template renderer +type ( + Config struct { + Log *slog.Logger + ServiceName string + TemplateString string + Data any + // Validate allows the validation of the rendered template on a given temp file path, optional + Validate func(path string) error + Fs afero.Fs + } + + renderer struct { + fs afero.Afero + log *slog.Logger + serviceName string + tpl *template.Template + data any + validateFn func(path string) error + } +) + +// New returns a new template renderer +func New(c *Config) (*renderer, error) { + tpl, err := template.New("tpl").Funcs(sprig.FuncMap()).Parse(c.TemplateString) + if err != nil { + return nil, err + } + + fs := afero.NewOsFs() + if c.Fs != nil { + fs = c.Fs + } + + return &renderer{ + log: c.Log.WithGroup("template-renderer"), + serviceName: c.ServiceName, + tpl: tpl, + data: c.Data, + validateFn: c.Validate, + fs: afero.Afero{ + Fs: fs, + }, + }, nil +} + +// Render renders the given template to the given destination and reloads the unit if requested. +// Returns true when the template has changed. +func (r *renderer) Render(ctx context.Context, destFile string, reload bool) (changed bool, err error) { + r.log.Info("rendering template file", "service-name", r.serviceName, "destination", destFile) + + stagingFile := fmt.Sprintf("%s-%s", destFile, uuid.New().String()) + + f, err := r.fs.Create(stagingFile) + if err != nil { + return false, err + } + + defer func() { + if err := f.Close(); err != nil { + r.log.Error("unable to close file", "error", err) + } + + if removeErr := r.fs.Remove(stagingFile); removeErr != nil && !os.IsNotExist(removeErr) { + r.log.Error("unable to remove staging file", "error", removeErr) + err = removeErr + } + }() + + w := bufio.NewWriter(f) + + if err = r.tpl.Execute(w, r.data); err != nil { + return false, err + } + + if err = w.Flush(); err != nil { + return false, err + } + + if r.validateFn != nil { + if err := r.validateFn(f.Name()); err != nil { + return false, err + } + + r.log.Debug("validated template successfully") + } + + if equal := r.compare(f.Name(), destFile); equal { + return false, nil + } + + if err = r.fs.Rename(f.Name(), destFile); err != nil { + return false, err + } + + if !reload { + return true, nil + } + + if err := r.reload(ctx); err != nil { + return true, err + } + + return true, err +} + +func (r *renderer) compare(source, target string) bool { + sourceChecksum, err := r.checksum(source) + if err != nil { + return false + } + + targetChecksum, err := r.checksum(target) + if err != nil { + return false + } + + return bytes.Equal(sourceChecksum, targetChecksum) +} + +func (r *renderer) reload(ctx context.Context) error { + const done = "done" + + dbc, err := dbus.NewWithContext(ctx) + if err != nil { + return fmt.Errorf("unable to connect to dbus: %w", err) + } + defer dbc.Close() + + c := make(chan string) + + if _, err = dbc.ReloadUnitContext(ctx, r.serviceName, "replace", c); err != nil { + return err + } + + job := <-c + + if job != done { + return fmt.Errorf("reloading failed: %s", job) + } + + return nil +} + +func (r *renderer) checksum(file string) ([]byte, error) { + f, err := r.fs.Open(file) + if err != nil { + return nil, err + } + + defer func() { + _ = f.Close() + }() + + h := sha256.New() + + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + + return h.Sum(nil), nil +} diff --git a/pkg/template-renderer/renderer_test.go b/pkg/template-renderer/renderer_test.go new file mode 100644 index 0000000..9ef8bbc --- /dev/null +++ b/pkg/template-renderer/renderer_test.go @@ -0,0 +1,128 @@ +package renderer_test + +import ( + "fmt" + "log/slog" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_renderer_Render(t *testing.T) { + tests := []struct { + name string + c *renderer.Config + destFile string + fsMock func(fs afero.Afero) + wantRendered string + wantChanged bool + wantErr error + }{ + { + name: "render an initial unit", + c: &renderer.Config{ + ServiceName: "test.service", + TemplateString: "{{ .Hostname }}", + Data: map[string]string{ + "Hostname": "foo", + }, + }, + destFile: "/hostname", + wantRendered: "foo", + wantChanged: true, + wantErr: nil, + }, + { + name: "render an initial unit and call validation func", + c: &renderer.Config{ + ServiceName: "test.service", + TemplateString: "{{ .Hostname }}", + Data: map[string]string{ + "Hostname": "foo", + }, + Validate: func(path string) error { + assert.True(t, strings.HasPrefix(path, "/hostname")) + return fmt.Errorf("a validation error") + }, + }, + destFile: "/hostname", + wantRendered: "", + wantChanged: false, + wantErr: fmt.Errorf("a validation error"), + }, + { + name: "update existing file", + c: &renderer.Config{ + ServiceName: "test.service", + TemplateString: "{{ .Hostname }}", + Data: map[string]string{ + "Hostname": "foo", + }, + }, + destFile: "/hostname", + fsMock: func(fs afero.Afero) { + require.NoError(t, fs.WriteFile("/hostname", []byte("bar"), os.ModePerm)) + }, + wantRendered: "foo", + wantChanged: true, + wantErr: nil, + }, + { + name: "update existing file that did not change", + c: &renderer.Config{ + ServiceName: "test.service", + TemplateString: "{{ .Hostname }}", + Data: map[string]string{ + "Hostname": "foo", + }, + }, + destFile: "/hostname", + fsMock: func(fs afero.Afero) { + require.NoError(t, fs.WriteFile("/hostname", []byte("foo"), os.ModePerm)) + }, + wantRendered: "foo", + wantChanged: false, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.c.Log = slog.Default() + fs := afero.Afero{Fs: afero.NewMemMapFs()} + tt.c.Fs = fs + + r, err := renderer.New(tt.c) + require.NoError(t, err) + + if tt.fsMock != nil { + tt.fsMock(fs) + } + + gotChanged, gotErr := r.Render(t.Context(), tt.destFile, false) // reload cannot be easily tested here because it interacts with dbus + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(tt.destFile) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantRendered, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/test/common.go b/pkg/test/common.go new file mode 100644 index 0000000..3b36baa --- /dev/null +++ b/pkg/test/common.go @@ -0,0 +1,18 @@ +package test + +import "github.com/google/go-cmp/cmp" + +func ErrorStringComparer() cmp.Option { + return cmp.Comparer(func(x, y error) bool { + if x == nil && y == nil { + return true + } + if x == nil && y != nil { + return false + } + if x != nil && y == nil { + return false + } + return x.Error() == y.Error() + }) +} From f0834024bedeb310312a411987c29fa3911325ee Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Fri, 6 Mar 2026 17:50:54 +0100 Subject: [PATCH 002/102] Firewall Controller --- pkg/services/droptailer/droptailer_test.go | 13 ++-- .../firewall-controller.go | 47 +++++++++++++ .../firewall-controller_test.go | 67 +++++++++++++++++++ .../firewall_controller.service.tpl | 3 +- .../test/firewall-controller.service | 14 ++++ 5 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 pkg/services/firewall-controller/firewall-controller.go create mode 100644 pkg/services/firewall-controller/firewall-controller_test.go create mode 100644 pkg/services/firewall-controller/test/firewall-controller.service diff --git a/pkg/services/droptailer/droptailer_test.go b/pkg/services/droptailer/droptailer_test.go index db31d54..398f6fc 100644 --- a/pkg/services/droptailer/droptailer_test.go +++ b/pkg/services/droptailer/droptailer_test.go @@ -9,6 +9,13 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/droptailer.service + expectedDroptailerSystemdUnit string ) func TestWriteSystemdUnit(t *testing.T) { @@ -24,11 +31,9 @@ func TestWriteSystemdUnit(t *testing.T) { c: &DroptailerTemplateData{ Comment: `This is a test. Do not edit.`, - TenantVrf: "vrf42", + TenantVrf: "vrf3981", }, - wantService: ` - bla - `, + wantService: expectedDroptailerSystemdUnit, wantChanged: true, wantErr: nil, }, diff --git a/pkg/services/firewall-controller/firewall-controller.go b/pkg/services/firewall-controller/firewall-controller.go new file mode 100644 index 0000000..c108bf8 --- /dev/null +++ b/pkg/services/firewall-controller/firewall-controller.go @@ -0,0 +1,47 @@ +package firewallcontroller + +import ( + "context" + "log/slog" + + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" + + _ "embed" +) + +const ( + firewallControllerServiceName = "firewall-controller.service" + firewallControllerServiceUnitPath = "/etc/systemd/system/" + firewallControllerServiceName +) + +var ( + //go:embed firewall_controller.service.tpl + firewallControllerTemplateString string +) + +type FirewallControllerConfig struct { + Log *slog.Logger + Reload bool + fs afero.Fs +} + +type FirewallControllerTemplateData struct { + Comment string + DefaultRouteVrf string +} + +func WriteSystemdUnit(ctx context.Context, cfg *FirewallControllerConfig, c *FirewallControllerTemplateData) (changed bool, err error) { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + ServiceName: firewallControllerServiceName, + TemplateString: firewallControllerTemplateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + return r.Render(ctx, firewallControllerServiceUnitPath, cfg.Reload) +} diff --git a/pkg/services/firewall-controller/firewall-controller_test.go b/pkg/services/firewall-controller/firewall-controller_test.go new file mode 100644 index 0000000..7662029 --- /dev/null +++ b/pkg/services/firewall-controller/firewall-controller_test.go @@ -0,0 +1,67 @@ +package firewallcontroller + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/firewall-controller.service + expectedFirewallControllerSystemdUnit string +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *FirewallControllerTemplateData + wantService string + wantChanged bool + wantErr error + }{ + { + name: "render", + c: &FirewallControllerTemplateData{ + Comment: `Do not edit.`, + DefaultRouteVrf: "vrf104009", + }, + wantService: expectedFirewallControllerSystemdUnit, + wantChanged: true, + wantErr: nil, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &FirewallControllerConfig{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c) + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(firewallControllerServiceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/services/firewall-controller/firewall_controller.service.tpl b/pkg/services/firewall-controller/firewall_controller.service.tpl index 8ec206c..c356779 100644 --- a/pkg/services/firewall-controller/firewall_controller.service.tpl +++ b/pkg/services/firewall-controller/firewall_controller.service.tpl @@ -1,5 +1,4 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallControllerData*/ -}} -{{ .Comment }} +# {{ .Comment }} [Unit] Description=Firewall controller - configures the firewall based on k8s resources After=network.target diff --git a/pkg/services/firewall-controller/test/firewall-controller.service b/pkg/services/firewall-controller/test/firewall-controller.service new file mode 100644 index 0000000..e9491f6 --- /dev/null +++ b/pkg/services/firewall-controller/test/firewall-controller.service @@ -0,0 +1,14 @@ +# Do not edit. +[Unit] +Description=Firewall controller - configures the firewall based on k8s resources +After=network.target + +[Service] +LimitMEMLOCK=infinity +Environment=KUBECONFIG=/etc/firewall-controller/.kubeconfig +ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/firewall-controller +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target From 0bf87eb55c6d0d696206bbf4b4bff8dd4c997cbc Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 7 Mar 2026 13:28:11 +0100 Subject: [PATCH 003/102] nftables-exporter --- .../nftables-exporter/nftables-exporter.go | 45 +++++++++++++ .../nftables-exporter_test.go | 67 +++++++++++++++++++ .../nftables_exporter.service.tpl | 3 +- .../test/nftables-exporter.service | 12 ++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 pkg/services/nftables-exporter/nftables-exporter.go create mode 100644 pkg/services/nftables-exporter/nftables-exporter_test.go create mode 100644 pkg/services/nftables-exporter/test/nftables-exporter.service diff --git a/pkg/services/nftables-exporter/nftables-exporter.go b/pkg/services/nftables-exporter/nftables-exporter.go new file mode 100644 index 0000000..4e22c88 --- /dev/null +++ b/pkg/services/nftables-exporter/nftables-exporter.go @@ -0,0 +1,45 @@ +package nftablesexporter + +import ( + "context" + _ "embed" + "log/slog" + + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" +) + +const ( + nftablesExporterServiceName = "nftables-exporter.service" + nftablesExporterServiceUnitPath = "/etc/systemd/system/" + nftablesExporterServiceName +) + +var ( + //go:embed nftables_exporter.service.tpl + nftablesExporterTemplateString string +) + +type DroptailerConfig struct { + Log *slog.Logger + Reload bool + fs afero.Fs +} + +type DroptailerTemplateData struct { + Comment string +} + +func WriteSystemdUnit(ctx context.Context, cfg *DroptailerConfig, c *DroptailerTemplateData) (changed bool, err error) { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + ServiceName: nftablesExporterServiceName, + TemplateString: nftablesExporterTemplateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + return r.Render(ctx, nftablesExporterServiceUnitPath, cfg.Reload) +} diff --git a/pkg/services/nftables-exporter/nftables-exporter_test.go b/pkg/services/nftables-exporter/nftables-exporter_test.go new file mode 100644 index 0000000..3f7a608 --- /dev/null +++ b/pkg/services/nftables-exporter/nftables-exporter_test.go @@ -0,0 +1,67 @@ +package nftablesexporter + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/nftables-exporter.service + expectedSystemdUnit string +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *DroptailerTemplateData + wantService string + wantChanged bool + wantErr error + }{ + { + name: "render", + c: &DroptailerTemplateData{ + Comment: `Do not edit.`, + }, + wantService: expectedSystemdUnit, + wantChanged: true, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &DroptailerConfig{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c) + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(nftablesExporterServiceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/services/nftables-exporter/nftables_exporter.service.tpl b/pkg/services/nftables-exporter/nftables_exporter.service.tpl index 2381523..d11bbbb 100644 --- a/pkg/services/nftables-exporter/nftables_exporter.service.tpl +++ b/pkg/services/nftables-exporter/nftables_exporter.service.tpl @@ -1,5 +1,4 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NftablesExporterData*/ -}} -{{ .Comment }} +# {{ .Comment }} [Unit] Description=Nftables exporter - provides prometheus metrics for nftables After=network.target diff --git a/pkg/services/nftables-exporter/test/nftables-exporter.service b/pkg/services/nftables-exporter/test/nftables-exporter.service new file mode 100644 index 0000000..e855f07 --- /dev/null +++ b/pkg/services/nftables-exporter/test/nftables-exporter.service @@ -0,0 +1,12 @@ +# Do not edit. +[Unit] +Description=Nftables exporter - provides prometheus metrics for nftables +After=network.target + +[Service] +ExecStart=/usr/bin/nftables-exporter --config=/etc/nftables_exporter.yaml +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target \ No newline at end of file From b4fea5f415fa5a3f76ffe6b310dc042a2685c56c Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 7 Mar 2026 15:49:36 +0100 Subject: [PATCH 004/102] next service --- pkg/services/droptailer/droptailer.go | 18 ++--- pkg/services/droptailer/droptailer_test.go | 12 ++-- .../firewall-controller.go | 18 ++--- .../firewall-controller_test.go | 12 ++-- .../nftables-exporter/nftables-exporter.go | 20 +++--- .../nftables-exporter_test.go | 8 +-- pkg/services/node-exporter/node-exporter.go | 45 +++++++++++++ .../node-exporter/node-exporter_test.go | 67 +++++++++++++++++++ .../node-exporter/node_exporter.service.tpl | 3 +- .../node-exporter/test/node-exporter.service | 12 ++++ 10 files changed, 169 insertions(+), 46 deletions(-) create mode 100644 pkg/services/node-exporter/node-exporter.go create mode 100644 pkg/services/node-exporter/node-exporter_test.go create mode 100644 pkg/services/node-exporter/test/node-exporter.service diff --git a/pkg/services/droptailer/droptailer.go b/pkg/services/droptailer/droptailer.go index 45db898..2bee961 100644 --- a/pkg/services/droptailer/droptailer.go +++ b/pkg/services/droptailer/droptailer.go @@ -10,31 +10,31 @@ import ( ) const ( - droptailerServiceName = "droptailer.service" - droptailerServiceUnitPath = "/etc/systemd/system/" + droptailerServiceName + serviceName = "droptailer.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName ) var ( //go:embed droptailer.service.tpl - droptailerTemplateString string + templateString string ) -type DroptailerConfig struct { +type Config struct { Log *slog.Logger Reload bool fs afero.Fs } -type DroptailerTemplateData struct { +type TemplateData struct { Comment string TenantVrf string } -func WriteSystemdUnit(ctx context.Context, cfg *DroptailerConfig, c *DroptailerTemplateData) (changed bool, err error) { +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := renderer.New(&renderer.Config{ Log: cfg.Log, - ServiceName: droptailerServiceName, - TemplateString: droptailerTemplateString, + ServiceName: serviceName, + TemplateString: templateString, Data: c, Fs: cfg.fs, }) @@ -42,5 +42,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *DroptailerConfig, c *DroptailerT return false, err } - return r.Render(ctx, droptailerServiceUnitPath, cfg.Reload) + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/droptailer/droptailer_test.go b/pkg/services/droptailer/droptailer_test.go index 398f6fc..6561a20 100644 --- a/pkg/services/droptailer/droptailer_test.go +++ b/pkg/services/droptailer/droptailer_test.go @@ -15,25 +15,25 @@ import ( var ( //go:embed test/droptailer.service - expectedDroptailerSystemdUnit string + expectedSystemdUnit string ) func TestWriteSystemdUnit(t *testing.T) { tests := []struct { name string - c *DroptailerTemplateData + c *TemplateData wantService string wantChanged bool wantErr error }{ { name: "render", - c: &DroptailerTemplateData{ + c: &TemplateData{ Comment: `This is a test. Do not edit.`, TenantVrf: "vrf3981", }, - wantService: expectedDroptailerSystemdUnit, + wantService: expectedSystemdUnit, wantChanged: true, wantErr: nil, }, @@ -42,7 +42,7 @@ Do not edit.`, t.Run(tt.name, func(t *testing.T) { fs := afero.Afero{Fs: afero.NewMemMapFs()} - gotChanged, gotErr := WriteSystemdUnit(t.Context(), &DroptailerConfig{ + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ Log: slog.Default(), Reload: false, fs: fs, @@ -58,7 +58,7 @@ Do not edit.`, return } - content, err := fs.ReadFile(droptailerServiceUnitPath) + content, err := fs.ReadFile(serviceUnitPath) require.NoError(t, err) if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { diff --git a/pkg/services/firewall-controller/firewall-controller.go b/pkg/services/firewall-controller/firewall-controller.go index c108bf8..d838b92 100644 --- a/pkg/services/firewall-controller/firewall-controller.go +++ b/pkg/services/firewall-controller/firewall-controller.go @@ -11,31 +11,31 @@ import ( ) const ( - firewallControllerServiceName = "firewall-controller.service" - firewallControllerServiceUnitPath = "/etc/systemd/system/" + firewallControllerServiceName + serviceName = "firewall-controller.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName ) var ( //go:embed firewall_controller.service.tpl - firewallControllerTemplateString string + templateString string ) -type FirewallControllerConfig struct { +type Config struct { Log *slog.Logger Reload bool fs afero.Fs } -type FirewallControllerTemplateData struct { +type TemplateData struct { Comment string DefaultRouteVrf string } -func WriteSystemdUnit(ctx context.Context, cfg *FirewallControllerConfig, c *FirewallControllerTemplateData) (changed bool, err error) { +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := renderer.New(&renderer.Config{ Log: cfg.Log, - ServiceName: firewallControllerServiceName, - TemplateString: firewallControllerTemplateString, + ServiceName: serviceName, + TemplateString: templateString, Data: c, Fs: cfg.fs, }) @@ -43,5 +43,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *FirewallControllerConfig, c *Fir return false, err } - return r.Render(ctx, firewallControllerServiceUnitPath, cfg.Reload) + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/firewall-controller/firewall-controller_test.go b/pkg/services/firewall-controller/firewall-controller_test.go index 7662029..2e6f380 100644 --- a/pkg/services/firewall-controller/firewall-controller_test.go +++ b/pkg/services/firewall-controller/firewall-controller_test.go @@ -15,24 +15,24 @@ import ( var ( //go:embed test/firewall-controller.service - expectedFirewallControllerSystemdUnit string + expectedSystemdUnit string ) func TestWriteSystemdUnit(t *testing.T) { tests := []struct { name string - c *FirewallControllerTemplateData + c *TemplateData wantService string wantChanged bool wantErr error }{ { name: "render", - c: &FirewallControllerTemplateData{ + c: &TemplateData{ Comment: `Do not edit.`, DefaultRouteVrf: "vrf104009", }, - wantService: expectedFirewallControllerSystemdUnit, + wantService: expectedSystemdUnit, wantChanged: true, wantErr: nil, }} @@ -40,7 +40,7 @@ func TestWriteSystemdUnit(t *testing.T) { t.Run(tt.name, func(t *testing.T) { fs := afero.Afero{Fs: afero.NewMemMapFs()} - gotChanged, gotErr := WriteSystemdUnit(t.Context(), &FirewallControllerConfig{ + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ Log: slog.Default(), Reload: false, fs: fs, @@ -56,7 +56,7 @@ func TestWriteSystemdUnit(t *testing.T) { return } - content, err := fs.ReadFile(firewallControllerServiceUnitPath) + content, err := fs.ReadFile(serviceUnitPath) require.NoError(t, err) if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { diff --git a/pkg/services/nftables-exporter/nftables-exporter.go b/pkg/services/nftables-exporter/nftables-exporter.go index 4e22c88..d47b1d0 100644 --- a/pkg/services/nftables-exporter/nftables-exporter.go +++ b/pkg/services/nftables-exporter/nftables-exporter.go @@ -10,30 +10,30 @@ import ( ) const ( - nftablesExporterServiceName = "nftables-exporter.service" - nftablesExporterServiceUnitPath = "/etc/systemd/system/" + nftablesExporterServiceName + serviceName = "nftables-exporter.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName ) var ( //go:embed nftables_exporter.service.tpl - nftablesExporterTemplateString string + templateString string ) -type DroptailerConfig struct { +type Config struct { Log *slog.Logger Reload bool fs afero.Fs } -type DroptailerTemplateData struct { - Comment string +type TemplateData struct { + Comment string } -func WriteSystemdUnit(ctx context.Context, cfg *DroptailerConfig, c *DroptailerTemplateData) (changed bool, err error) { +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := renderer.New(&renderer.Config{ Log: cfg.Log, - ServiceName: nftablesExporterServiceName, - TemplateString: nftablesExporterTemplateString, + ServiceName: serviceName, + TemplateString: templateString, Data: c, Fs: cfg.fs, }) @@ -41,5 +41,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *DroptailerConfig, c *DroptailerT return false, err } - return r.Render(ctx, nftablesExporterServiceUnitPath, cfg.Reload) + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/nftables-exporter/nftables-exporter_test.go b/pkg/services/nftables-exporter/nftables-exporter_test.go index 3f7a608..e26f809 100644 --- a/pkg/services/nftables-exporter/nftables-exporter_test.go +++ b/pkg/services/nftables-exporter/nftables-exporter_test.go @@ -21,14 +21,14 @@ var ( func TestWriteSystemdUnit(t *testing.T) { tests := []struct { name string - c *DroptailerTemplateData + c *TemplateData wantService string wantChanged bool wantErr error }{ { name: "render", - c: &DroptailerTemplateData{ + c: &TemplateData{ Comment: `Do not edit.`, }, wantService: expectedSystemdUnit, @@ -40,7 +40,7 @@ func TestWriteSystemdUnit(t *testing.T) { t.Run(tt.name, func(t *testing.T) { fs := afero.Afero{Fs: afero.NewMemMapFs()} - gotChanged, gotErr := WriteSystemdUnit(t.Context(), &DroptailerConfig{ + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ Log: slog.Default(), Reload: false, fs: fs, @@ -56,7 +56,7 @@ func TestWriteSystemdUnit(t *testing.T) { return } - content, err := fs.ReadFile(nftablesExporterServiceUnitPath) + content, err := fs.ReadFile(serviceUnitPath) require.NoError(t, err) if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { diff --git a/pkg/services/node-exporter/node-exporter.go b/pkg/services/node-exporter/node-exporter.go new file mode 100644 index 0000000..cf129b6 --- /dev/null +++ b/pkg/services/node-exporter/node-exporter.go @@ -0,0 +1,45 @@ +package nodeexporter + +import ( + "context" + _ "embed" + "log/slog" + + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" +) + +const ( + serviceName = "node-exporter.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName +) + +var ( + //go:embed node_exporter.service.tpl + templateString string +) + +type Config struct { + Log *slog.Logger + Reload bool + fs afero.Fs +} + +type TemplateData struct { + Comment string +} + +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + ServiceName: serviceName, + TemplateString: templateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + return r.Render(ctx, serviceUnitPath, cfg.Reload) +} diff --git a/pkg/services/node-exporter/node-exporter_test.go b/pkg/services/node-exporter/node-exporter_test.go new file mode 100644 index 0000000..c3e652c --- /dev/null +++ b/pkg/services/node-exporter/node-exporter_test.go @@ -0,0 +1,67 @@ +package nodeexporter + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/node-exporter.service + expectedSystemdUnit string +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *TemplateData + wantService string + wantChanged bool + wantErr error + }{ + { + name: "render", + c: &TemplateData{ + Comment: `Do not edit.`, + }, + wantService: expectedSystemdUnit, + wantChanged: true, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c) + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(serviceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/services/node-exporter/node_exporter.service.tpl b/pkg/services/node-exporter/node_exporter.service.tpl index 3c5550e..48d9ad2 100644 --- a/pkg/services/node-exporter/node_exporter.service.tpl +++ b/pkg/services/node-exporter/node_exporter.service.tpl @@ -1,5 +1,4 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NodeExporterData*/ -}} -{{ .Comment }} +# {{ .Comment }} [Unit] Description=Node exporter - provides prometheus metrics about the node After=network.target diff --git a/pkg/services/node-exporter/test/node-exporter.service b/pkg/services/node-exporter/test/node-exporter.service new file mode 100644 index 0000000..a9cb459 --- /dev/null +++ b/pkg/services/node-exporter/test/node-exporter.service @@ -0,0 +1,12 @@ +# Do not edit. +[Unit] +Description=Node exporter - provides prometheus metrics about the node +After=network.target + +[Service] +ExecStart=/usr/local/bin/node_exporter --collector.tcpstat +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target \ No newline at end of file From b1c22f4506b5e790c43bc037c1e19b77957d45ef Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 7 Mar 2026 16:23:33 +0100 Subject: [PATCH 005/102] Remove unused --- pkg/network/droptailer.go | 41 ------------ pkg/network/firewall_controller.go | 51 -------------- pkg/network/nftables_exporter.go | 29 -------- pkg/network/node_exporter.go | 29 -------- pkg/network/testdata/droptailer.service | 18 ----- .../testdata/firewall-controller.service | 15 ----- .../testdata/nftables-exporter.service | 13 ---- pkg/network/testdata/node-exporter.service | 13 ---- pkg/services/tailscale/tailscale.go | 47 +++++++++++++ pkg/services/tailscale/tailscale_test.go | 67 +++++++++++++++++++ pkg/services/tailscale/test/tailscale.service | 12 ++++ .../tailscale/test/tailscaled.service | 25 +++++++ 12 files changed, 151 insertions(+), 209 deletions(-) delete mode 100644 pkg/network/droptailer.go delete mode 100644 pkg/network/firewall_controller.go delete mode 100644 pkg/network/nftables_exporter.go delete mode 100644 pkg/network/node_exporter.go delete mode 100644 pkg/network/testdata/droptailer.service delete mode 100644 pkg/network/testdata/firewall-controller.service delete mode 100644 pkg/network/testdata/nftables-exporter.service delete mode 100644 pkg/network/testdata/node-exporter.service create mode 100644 pkg/services/tailscale/tailscale.go create mode 100644 pkg/services/tailscale/tailscale_test.go create mode 100644 pkg/services/tailscale/test/tailscale.service create mode 100644 pkg/services/tailscale/test/tailscaled.service diff --git a/pkg/network/droptailer.go b/pkg/network/droptailer.go deleted file mode 100644 index 7786d0f..0000000 --- a/pkg/network/droptailer.go +++ /dev/null @@ -1,41 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/metal-stack/os-installer/pkg/net" -) - -// TplDroptailer is the name of the template for the droptailer service. -const tplDroptailer = "droptailer.service.tpl" - -// SystemdUnitDroptailer is the name of the systemd unit for the droptailer. -const systemdUnitDroptailer = "droptailer.service" - -// droptailerData contains the data to render the droptailer service template. -type droptailerData struct { - Comment string - TenantVrf string -} - -// newDroptailerServiceApplier constructs a new instance of this type. -func newDroptailerServiceApplier(kb config, v net.Validator) (net.Applier, error) { - tenantVrf, err := getTenantVRFName(kb) - if err != nil { - return nil, err - } - - data := droptailerData{Comment: versionHeader(kb.MachineUUID), TenantVrf: tenantVrf} - - return net.NewNetworkApplier(data, v, nil), nil -} - -func getTenantVRFName(kb config) (string, error) { - primary := kb.getPrivatePrimaryNetwork() - if primary.Vrf != nil && *primary.Vrf != 0 { - vrf := fmt.Sprintf("vrf%d", *primary.Vrf) - return vrf, nil - } - - return "", fmt.Errorf("there is no private tenant network") -} diff --git a/pkg/network/firewall_controller.go b/pkg/network/firewall_controller.go deleted file mode 100644 index 0f34da3..0000000 --- a/pkg/network/firewall_controller.go +++ /dev/null @@ -1,51 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/metal-stack/os-installer/pkg/net" -) - -// TplFirewallController is the name of the template for the firewall-policy-controller service. -const tplFirewallController = "firewall_controller.service.tpl" - -// SystemdUnitFirewallController is the name of the systemd unit for the firewall policy controller, -const systemdUnitFirewallController = "firewall-controller.service" - -// firewallControllerData contains the data to render the firewall-controller service template. -type firewallControllerData struct { - Comment string - DefaultRouteVrf string - ServiceIP string - PrivateVrfID int64 -} - -// newFirewallControllerServiceApplier constructs a new instance of this type. -func newFirewallControllerServiceApplier(kb config, v net.Validator) (net.Applier, error) { - defaultRouteVrf, err := kb.getDefaultRouteVRFName() - if err != nil { - return nil, err - } - - if len(kb.getPrivatePrimaryNetwork().Ips) == 0 { - return nil, fmt.Errorf("no private IP found useable for the firewall controller") - } - data := firewallControllerData{ - Comment: versionHeader(kb.MachineUUID), - DefaultRouteVrf: defaultRouteVrf, - } - - return net.NewNetworkApplier(data, v, nil), nil -} - -// serviceValidator holds information for systemd service validation. -type serviceValidator struct { - path string -} - -// Validate validates the service file. -func (v serviceValidator) Validate() error { - // Currently not implemented as systemd-analyze fails in the metal-hammer. - // Error: Cannot determine cgroup we are running in: No medium found - return nil -} diff --git a/pkg/network/nftables_exporter.go b/pkg/network/nftables_exporter.go deleted file mode 100644 index e178066..0000000 --- a/pkg/network/nftables_exporter.go +++ /dev/null @@ -1,29 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -// TplNftablesExporter is the name of the template for the nftables_exporter service. -const tplNftablesExporter = "nftables_exporter.service.tpl" - -// SystemdUnitNftablesExporter is the name of the systemd unit for the nftables_exporter. -const systemdUnitNftablesExporter = "nftables-exporter.service" - -// NftablesExporterData contains the data to render the nftables_exporter service template. -type NftablesExporterData struct { - Comment string - TenantVrf string -} - -// NewNftablesExporterServiceApplier constructs a new instance of this type. -func NewNftablesExporterServiceApplier(kb config, v net.Validator) (net.Applier, error) { - tenantVrf, err := getTenantVRFName(kb) - if err != nil { - return nil, err - } - - data := NftablesExporterData{Comment: versionHeader(kb.MachineUUID), TenantVrf: tenantVrf} - - return net.NewNetworkApplier(data, v, nil), nil -} diff --git a/pkg/network/node_exporter.go b/pkg/network/node_exporter.go deleted file mode 100644 index 3fd22b3..0000000 --- a/pkg/network/node_exporter.go +++ /dev/null @@ -1,29 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -// tplNodeExporter is the name of the template for the node_exporter service. -const tplNodeExporter = "node_exporter.service.tpl" - -// systemdUnitNodeExporter is the name of the systemd unit for the node_exporter. -const systemdUnitNodeExporter = "node-exporter.service" - -// NodeExporterData contains the data to render the node_exporter service template. -type NodeExporterData struct { - Comment string - TenantVrf string -} - -// newNodeExporterServiceApplier constructs a new instance of this type. -func newNodeExporterServiceApplier(kb config, v net.Validator) (net.Applier, error) { - tenantVrf, err := getTenantVRFName(kb) - if err != nil { - return nil, err - } - - data := NodeExporterData{Comment: versionHeader(kb.MachineUUID), TenantVrf: tenantVrf} - - return net.NewNetworkApplier(data, v, nil), nil -} diff --git a/pkg/network/testdata/droptailer.service b/pkg/network/testdata/droptailer.service deleted file mode 100644 index 1c8b53a..0000000 --- a/pkg/network/testdata/droptailer.service +++ /dev/null @@ -1,18 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Unit] -Description=Droptailer -After=network.target - -[Service] -LimitMEMLOCK=infinity -Environment=DROPTAILER_SERVER_ADDRESS=droptailer:50051 -Environment=DROPTAILER_PREFIXES_OF_DROPS="nftables-metal-dropped: ,nftables-firewall-dropped: " -Environment=DROPTAILER_CLIENT_CERTIFICATE=/etc/droptailer-client/droptailer-client.crt -Environment=DROPTAILER_CLIENT_KEY=/etc/droptailer-client/droptailer-client.key -ExecStart=/bin/ip vrf exec vrf3981 /usr/local/bin/droptailer-client -Restart=always -RestartSec=10 - -[Install] -WantedBy=firewall-controller.service diff --git a/pkg/network/testdata/firewall-controller.service b/pkg/network/testdata/firewall-controller.service deleted file mode 100644 index 8eb2430..0000000 --- a/pkg/network/testdata/firewall-controller.service +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Unit] -Description=Firewall controller - configures the firewall based on k8s resources -After=network.target - -[Service] -LimitMEMLOCK=infinity -Environment=KUBECONFIG=/etc/firewall-controller/.kubeconfig -ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/firewall-controller -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target diff --git a/pkg/network/testdata/nftables-exporter.service b/pkg/network/testdata/nftables-exporter.service deleted file mode 100644 index 4a2d3b9..0000000 --- a/pkg/network/testdata/nftables-exporter.service +++ /dev/null @@ -1,13 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Unit] -Description=Nftables exporter - provides prometheus metrics for nftables -After=network.target - -[Service] -ExecStart=/usr/bin/nftables-exporter --config=/etc/nftables_exporter.yaml -Restart=always -RestartSec=30 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/network/testdata/node-exporter.service b/pkg/network/testdata/node-exporter.service deleted file mode 100644 index cd38f40..0000000 --- a/pkg/network/testdata/node-exporter.service +++ /dev/null @@ -1,13 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Unit] -Description=Node exporter - provides prometheus metrics about the node -After=network.target - -[Service] -ExecStart=/usr/local/bin/node_exporter --collector.tcpstat -Restart=always -RestartSec=30 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/tailscale/tailscale.go b/pkg/services/tailscale/tailscale.go new file mode 100644 index 0000000..6aa481a --- /dev/null +++ b/pkg/services/tailscale/tailscale.go @@ -0,0 +1,47 @@ +package tailscale + +import ( + "context" + "log/slog" + + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" + + _ "embed" +) + +const ( + serviceName = "firewall-controller.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName +) + +var ( + //go:embed firewall_controller.service.tpl + templateString string +) + +type Config struct { + Log *slog.Logger + Reload bool + fs afero.Fs +} + +type TemplateData struct { + Comment string + DefaultRouteVrf string +} + +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + ServiceName: serviceName, + TemplateString: templateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + return r.Render(ctx, serviceUnitPath, cfg.Reload) +} diff --git a/pkg/services/tailscale/tailscale_test.go b/pkg/services/tailscale/tailscale_test.go new file mode 100644 index 0000000..0e153be --- /dev/null +++ b/pkg/services/tailscale/tailscale_test.go @@ -0,0 +1,67 @@ +package tailscale + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/firewall-controller.service + expectedSystemdUnit string +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *TemplateData + wantService string + wantChanged bool + wantErr error + }{ + { + name: "render", + c: &TemplateData{ + Comment: `Do not edit.`, + DefaultRouteVrf: "vrf104009", + }, + wantService: expectedSystemdUnit, + wantChanged: true, + wantErr: nil, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c) + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(serviceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/services/tailscale/test/tailscale.service b/pkg/services/tailscale/test/tailscale.service new file mode 100644 index 0000000..f47cef0 --- /dev/null +++ b/pkg/services/tailscale/test/tailscale.service @@ -0,0 +1,12 @@ +[Unit] +Description=Tailscale client +After=tailscaled.service + +[Service] +LimitMEMLOCK=infinity +User=root +Group=root +ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscale up --hostname c0115b51-5e4d-4f92-85c8-1cc504eafdd2 --auth-key a-authkey --login-server headscale.metal-stack.ioRestart=on-failure + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/tailscale/test/tailscaled.service b/pkg/services/tailscale/test/tailscaled.service new file mode 100644 index 0000000..8c94222 --- /dev/null +++ b/pkg/services/tailscale/test/tailscaled.service @@ -0,0 +1,25 @@ +[Unit] +Description=Tailscale node agent +Documentation=https://tailscale.com/kb/ +After=network.target + +[Service] +LimitMEMLOCK=infinity +User=root +Group=root +Type=notify +Environment="TS_NO_LOGS_NO_SUPPORT=true" +ExecStartPre=ip vrf exec vrf104009 /usr/local/bin/tailscaled --cleanup +ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscaled --port {{ .TailscaledPort }} +ExecStopPost=ip vrf exec vrf104009 /usr/local/bin/tailscaled --cleanup +Restart=on-failure + +RuntimeDirectory=tailscale +RuntimeDirectoryMode=0755 +StateDirectory=tailscale +StateDirectoryMode=0700 +CacheDirectory=tailscale +CacheDirectoryMode=0750 + +[Install] +WantedBy=multi-user.target \ No newline at end of file From 9d1d001117cad88f79b1d2a7fd2c4da03b92f651 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 09:15:04 +0100 Subject: [PATCH 006/102] Progress. --- pkg/services/droptailer/droptailer.go | 4 +- .../firewall-controller.go | 4 +- pkg/services/install.go | 2 + .../nftables-exporter/nftables-exporter.go | 4 +- pkg/services/node-exporter/node-exporter.go | 4 +- pkg/services/tailscale/tailscale.go | 64 ++++++++--- pkg/services/tailscale/tailscale.service.tpl | 2 +- pkg/services/tailscale/tailscale_test.go | 42 +++++--- pkg/services/tailscale/tailscaled.service.tpl | 2 +- pkg/services/tailscale/test/tailscale.service | 5 +- .../tailscale/test/tailscaled.service | 4 +- .../systemd_renderer.go | 100 ++++++++++++++++++ pkg/template-renderer/renderer.go | 76 ++++--------- pkg/template-renderer/renderer_test.go | 6 +- 14 files changed, 218 insertions(+), 101 deletions(-) create mode 100644 pkg/systemd-service-renderer/systemd_renderer.go diff --git a/pkg/services/droptailer/droptailer.go b/pkg/services/droptailer/droptailer.go index 2bee961..7b441b7 100644 --- a/pkg/services/droptailer/droptailer.go +++ b/pkg/services/droptailer/droptailer.go @@ -5,7 +5,7 @@ import ( _ "embed" "log/slog" - renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" "github.com/spf13/afero" ) @@ -31,7 +31,7 @@ type TemplateData struct { } func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { - r, err := renderer.New(&renderer.Config{ + r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, ServiceName: serviceName, TemplateString: templateString, diff --git a/pkg/services/firewall-controller/firewall-controller.go b/pkg/services/firewall-controller/firewall-controller.go index d838b92..f1430d8 100644 --- a/pkg/services/firewall-controller/firewall-controller.go +++ b/pkg/services/firewall-controller/firewall-controller.go @@ -4,7 +4,7 @@ import ( "context" "log/slog" - renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" "github.com/spf13/afero" _ "embed" @@ -32,7 +32,7 @@ type TemplateData struct { } func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { - r, err := renderer.New(&renderer.Config{ + r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, ServiceName: serviceName, TemplateString: templateString, diff --git a/pkg/services/install.go b/pkg/services/install.go index d38bd12..a55a575 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -4,6 +4,8 @@ func WriteSystemdServices() error { return nil } +func firewallServices() {} + // suricata // tailscale(d) // node-exporter diff --git a/pkg/services/nftables-exporter/nftables-exporter.go b/pkg/services/nftables-exporter/nftables-exporter.go index d47b1d0..ede6e8d 100644 --- a/pkg/services/nftables-exporter/nftables-exporter.go +++ b/pkg/services/nftables-exporter/nftables-exporter.go @@ -5,7 +5,7 @@ import ( _ "embed" "log/slog" - renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" "github.com/spf13/afero" ) @@ -30,7 +30,7 @@ type TemplateData struct { } func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { - r, err := renderer.New(&renderer.Config{ + r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, ServiceName: serviceName, TemplateString: templateString, diff --git a/pkg/services/node-exporter/node-exporter.go b/pkg/services/node-exporter/node-exporter.go index cf129b6..98afd70 100644 --- a/pkg/services/node-exporter/node-exporter.go +++ b/pkg/services/node-exporter/node-exporter.go @@ -5,7 +5,7 @@ import ( _ "embed" "log/slog" - renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" "github.com/spf13/afero" ) @@ -30,7 +30,7 @@ type TemplateData struct { } func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { - r, err := renderer.New(&renderer.Config{ + r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, ServiceName: serviceName, TemplateString: templateString, diff --git a/pkg/services/tailscale/tailscale.go b/pkg/services/tailscale/tailscale.go index 6aa481a..977fecd 100644 --- a/pkg/services/tailscale/tailscale.go +++ b/pkg/services/tailscale/tailscale.go @@ -4,20 +4,25 @@ import ( "context" "log/slog" - renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" "github.com/spf13/afero" _ "embed" ) const ( - serviceName = "firewall-controller.service" - serviceUnitPath = "/etc/systemd/system/" + serviceName + tailscaleServiceName = "tailscale.service" + tailscaleServiceUnitPath = "/etc/systemd/system/" + tailscaleServiceName + + tailscaledServiceName = "tailscaled.service" + tailscaledServiceUnitPath = "/etc/systemd/system/" + tailscaledServiceName ) var ( - //go:embed firewall_controller.service.tpl - templateString string + //go:embed tailscale.service.tpl + tailscaleTemplateString string + //go:embed tailscaled.service.tpl + tailscaledTemplateString string ) type Config struct { @@ -29,19 +34,48 @@ type Config struct { type TemplateData struct { Comment string DefaultRouteVrf string + TailscaledPort string + MachineID string + AuthKey string + Address string } func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { - r, err := renderer.New(&renderer.Config{ - Log: cfg.Log, - ServiceName: serviceName, - TemplateString: templateString, - Data: c, - Fs: cfg.fs, - }) - if err != nil { - return false, err + for _, spec := range []struct { + servicePath string + serviceName string + templateString string + }{ + { + servicePath: tailscaleServiceUnitPath, + serviceName: tailscaleServiceName, + templateString: tailscaleTemplateString, + }, + { + servicePath: tailscaledServiceUnitPath, + serviceName: tailscaledServiceName, + templateString: tailscaledTemplateString, + }, + } { + r, err := systemd_renderer.New(&systemd_renderer.Config{ + ServiceName: spec.serviceName, + Log: cfg.Log, + TemplateString: spec.templateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + chg, err := r.Render(ctx, spec.servicePath, cfg.Reload) + if err != nil { + return chg, err + } + + // return changed if one has changed + changed = changed || chg } - return r.Render(ctx, serviceUnitPath, cfg.Reload) + return true, nil } diff --git a/pkg/services/tailscale/tailscale.service.tpl b/pkg/services/tailscale/tailscale.service.tpl index 2364d65..a026fe5 100644 --- a/pkg/services/tailscale/tailscale.service.tpl +++ b/pkg/services/tailscale/tailscale.service.tpl @@ -10,4 +10,4 @@ ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscale up -- Restart=on-failure [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/pkg/services/tailscale/tailscale_test.go b/pkg/services/tailscale/tailscale_test.go index 0e153be..a7c691e 100644 --- a/pkg/services/tailscale/tailscale_test.go +++ b/pkg/services/tailscale/tailscale_test.go @@ -14,28 +14,37 @@ import ( ) var ( - //go:embed test/firewall-controller.service - expectedSystemdUnit string + //go:embed test/tailscale.service + expectedTailscaleSystemdUnit string + //go:embed test/tailscaled.service + expectedTailscaledSystemdUnit string ) func TestWriteSystemdUnit(t *testing.T) { tests := []struct { - name string - c *TemplateData - wantService string - wantChanged bool - wantErr error + name string + c *TemplateData + wantTailscaleService string + wantTailscaledService string + wantChanged bool + wantErr error }{ { name: "render", c: &TemplateData{ Comment: `Do not edit.`, DefaultRouteVrf: "vrf104009", + TailscaledPort: "41161", + MachineID: "c0115b51-5e4d-4f92-85c8-1cc504eafdd2", + AuthKey: "a-authkey", + Address: "headscale.metal-stack.io", }, - wantService: expectedSystemdUnit, - wantChanged: true, - wantErr: nil, - }} + wantTailscaleService: expectedTailscaleSystemdUnit, + wantTailscaledService: expectedTailscaledSystemdUnit, + wantChanged: true, + wantErr: nil, + }, + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fs := afero.Afero{Fs: afero.NewMemMapFs()} @@ -56,10 +65,17 @@ func TestWriteSystemdUnit(t *testing.T) { return } - content, err := fs.ReadFile(serviceUnitPath) + content, err := fs.ReadFile(tailscaleServiceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantTailscaleService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + + content, err = fs.ReadFile(tailscaledServiceUnitPath) require.NoError(t, err) - if diff := cmp.Diff(tt.wantService, string(content)); diff != "" { + if diff := cmp.Diff(tt.wantTailscaledService, string(content)); diff != "" { t.Errorf("diff (+got -want):\n%s", diff) } }) diff --git a/pkg/services/tailscale/tailscaled.service.tpl b/pkg/services/tailscale/tailscaled.service.tpl index 3db63ad..66a4983 100644 --- a/pkg/services/tailscale/tailscaled.service.tpl +++ b/pkg/services/tailscale/tailscaled.service.tpl @@ -22,4 +22,4 @@ CacheDirectory=tailscale CacheDirectoryMode=0750 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/pkg/services/tailscale/test/tailscale.service b/pkg/services/tailscale/test/tailscale.service index f47cef0..10a9d7e 100644 --- a/pkg/services/tailscale/test/tailscale.service +++ b/pkg/services/tailscale/test/tailscale.service @@ -6,7 +6,8 @@ After=tailscaled.service LimitMEMLOCK=infinity User=root Group=root -ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscale up --hostname c0115b51-5e4d-4f92-85c8-1cc504eafdd2 --auth-key a-authkey --login-server headscale.metal-stack.ioRestart=on-failure +ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscale up --hostname c0115b51-5e4d-4f92-85c8-1cc504eafdd2 --auth-key a-authkey --login-server headscale.metal-stack.io +Restart=on-failure [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/pkg/services/tailscale/test/tailscaled.service b/pkg/services/tailscale/test/tailscaled.service index 8c94222..0cc12e0 100644 --- a/pkg/services/tailscale/test/tailscaled.service +++ b/pkg/services/tailscale/test/tailscaled.service @@ -10,7 +10,7 @@ Group=root Type=notify Environment="TS_NO_LOGS_NO_SUPPORT=true" ExecStartPre=ip vrf exec vrf104009 /usr/local/bin/tailscaled --cleanup -ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscaled --port {{ .TailscaledPort }} +ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscaled --port 41161 ExecStopPost=ip vrf exec vrf104009 /usr/local/bin/tailscaled --cleanup Restart=on-failure @@ -22,4 +22,4 @@ CacheDirectory=tailscale CacheDirectoryMode=0750 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go new file mode 100644 index 0000000..d037286 --- /dev/null +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -0,0 +1,100 @@ +package systemd_renderer + +import ( + "context" + "fmt" + "log/slog" + + "github.com/coreos/go-systemd/v22/dbus" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" +) + +type ( + Config struct { + ServiceName string + Log *slog.Logger + TemplateString string + Data any + // Validate allows the validation of the rendered template on a given temp file path, optional + Validate func(path string) error + Fs afero.Fs + } + + systemdRenderer struct { + log *slog.Logger + r *renderer.Renderer + serviceName string + } +) + +// New returns a new system service renderer +func New(c *Config) (*systemdRenderer, error) { + if c == nil { + return nil, fmt.Errorf("systemd service renderer config is nil") + } + + r, err := renderer.New(&renderer.Config{ + Log: c.Log.With("service-name", c.ServiceName), + TemplateString: c.TemplateString, + Data: c.Data, + Validate: c.Validate, + Fs: c.Fs, + }) + if err != nil { + return nil, err + } + + return &systemdRenderer{ + log: c.Log.WithGroup("systemd-service-renderer").With("service-name", c.ServiceName), + serviceName: c.ServiceName, + r: r, + }, nil +} + +// Render renders the given template to the given destination and reloads the unit if requested. +// Returns true when the template has changed. +func (r *systemdRenderer) Render(ctx context.Context, destFile string, reload bool) (changed bool, err error) { + r.log.Info("rendering systemd service template file") + + changed, err = r.r.Render(ctx, destFile) + if err != nil { + return changed, err + } + + if !reload { + return changed, nil + } + + if err := r.reload(ctx); err != nil { + return true, err + } + + return true, err +} + +func (r *systemdRenderer) reload(ctx context.Context) error { + const done = "done" + + r.log.Info("reloading systemd service unit") + + dbc, err := dbus.NewWithContext(ctx) + if err != nil { + return fmt.Errorf("unable to connect to dbus: %w", err) + } + defer dbc.Close() + + c := make(chan string) + + if _, err = dbc.ReloadUnitContext(ctx, r.serviceName, "replace", c); err != nil { + return err + } + + job := <-c + + if job != done { + return fmt.Errorf("reloading failed: %s", job) + } + + return nil +} diff --git a/pkg/template-renderer/renderer.go b/pkg/template-renderer/renderer.go index c551827..9def240 100644 --- a/pkg/template-renderer/renderer.go +++ b/pkg/template-renderer/renderer.go @@ -12,16 +12,14 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" - "github.com/coreos/go-systemd/v22/dbus" "github.com/google/uuid" "github.com/spf13/afero" ) -// Config provides a config for the template renderer +// Config provides a config for the template Renderer type ( Config struct { Log *slog.Logger - ServiceName string TemplateString string Data any // Validate allows the validation of the rendered template on a given temp file path, optional @@ -29,18 +27,21 @@ type ( Fs afero.Fs } - renderer struct { - fs afero.Afero - log *slog.Logger - serviceName string - tpl *template.Template - data any - validateFn func(path string) error + Renderer struct { + fs afero.Afero + log *slog.Logger + tpl *template.Template + data any + validateFn func(path string) error } ) // New returns a new template renderer -func New(c *Config) (*renderer, error) { +func New(c *Config) (*Renderer, error) { + if c == nil { + return nil, fmt.Errorf("renderer config is nil") + } + tpl, err := template.New("tpl").Funcs(sprig.FuncMap()).Parse(c.TemplateString) if err != nil { return nil, err @@ -51,22 +52,21 @@ func New(c *Config) (*renderer, error) { fs = c.Fs } - return &renderer{ - log: c.Log.WithGroup("template-renderer"), - serviceName: c.ServiceName, - tpl: tpl, - data: c.Data, - validateFn: c.Validate, + return &Renderer{ + log: c.Log.WithGroup("template-renderer"), + tpl: tpl, + data: c.Data, + validateFn: c.Validate, fs: afero.Afero{ Fs: fs, }, }, nil } -// Render renders the given template to the given destination and reloads the unit if requested. +// Render renders the given template to the given destination. // Returns true when the template has changed. -func (r *renderer) Render(ctx context.Context, destFile string, reload bool) (changed bool, err error) { - r.log.Info("rendering template file", "service-name", r.serviceName, "destination", destFile) +func (r *Renderer) Render(ctx context.Context, destFile string) (changed bool, err error) { + r.log.Info("rendering template file", "destination", destFile) stagingFile := fmt.Sprintf("%s-%s", destFile, uuid.New().String()) @@ -112,18 +112,10 @@ func (r *renderer) Render(ctx context.Context, destFile string, reload bool) (ch return false, err } - if !reload { - return true, nil - } - - if err := r.reload(ctx); err != nil { - return true, err - } - return true, err } -func (r *renderer) compare(source, target string) bool { +func (r *Renderer) compare(source, target string) bool { sourceChecksum, err := r.checksum(source) if err != nil { return false @@ -137,31 +129,7 @@ func (r *renderer) compare(source, target string) bool { return bytes.Equal(sourceChecksum, targetChecksum) } -func (r *renderer) reload(ctx context.Context) error { - const done = "done" - - dbc, err := dbus.NewWithContext(ctx) - if err != nil { - return fmt.Errorf("unable to connect to dbus: %w", err) - } - defer dbc.Close() - - c := make(chan string) - - if _, err = dbc.ReloadUnitContext(ctx, r.serviceName, "replace", c); err != nil { - return err - } - - job := <-c - - if job != done { - return fmt.Errorf("reloading failed: %s", job) - } - - return nil -} - -func (r *renderer) checksum(file string) ([]byte, error) { +func (r *Renderer) checksum(file string) ([]byte, error) { f, err := r.fs.Open(file) if err != nil { return nil, err diff --git a/pkg/template-renderer/renderer_test.go b/pkg/template-renderer/renderer_test.go index 9ef8bbc..eab04f3 100644 --- a/pkg/template-renderer/renderer_test.go +++ b/pkg/template-renderer/renderer_test.go @@ -28,7 +28,6 @@ func Test_renderer_Render(t *testing.T) { { name: "render an initial unit", c: &renderer.Config{ - ServiceName: "test.service", TemplateString: "{{ .Hostname }}", Data: map[string]string{ "Hostname": "foo", @@ -42,7 +41,6 @@ func Test_renderer_Render(t *testing.T) { { name: "render an initial unit and call validation func", c: &renderer.Config{ - ServiceName: "test.service", TemplateString: "{{ .Hostname }}", Data: map[string]string{ "Hostname": "foo", @@ -60,7 +58,6 @@ func Test_renderer_Render(t *testing.T) { { name: "update existing file", c: &renderer.Config{ - ServiceName: "test.service", TemplateString: "{{ .Hostname }}", Data: map[string]string{ "Hostname": "foo", @@ -77,7 +74,6 @@ func Test_renderer_Render(t *testing.T) { { name: "update existing file that did not change", c: &renderer.Config{ - ServiceName: "test.service", TemplateString: "{{ .Hostname }}", Data: map[string]string{ "Hostname": "foo", @@ -105,7 +101,7 @@ func Test_renderer_Render(t *testing.T) { tt.fsMock(fs) } - gotChanged, gotErr := r.Render(t.Context(), tt.destFile, false) // reload cannot be easily tested here because it interacts with dbus + gotChanged, gotErr := r.Render(t.Context(), tt.destFile) assert.Equal(t, tt.wantChanged, gotChanged) From 3cb2e1c3fd81a4041209afb86268ae8d61ad6688 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 09:50:40 +0100 Subject: [PATCH 007/102] Suricata. --- install.go | 10 ++ pkg/network/hostname.go | 33 ------ pkg/network/hostname_test.go | 28 ----- pkg/network/suricata_config.go | 41 ------- pkg/network/suricata_defaults.go | 40 ------- pkg/network/suricata_update.go | 29 ----- pkg/network/tailscale.go | 37 ------- pkg/network/tailscaled.go | 31 ------ pkg/network/tpl/droptailer.service.tpl | 18 ---- .../tpl/firewall_controller.service.tpl | 15 --- pkg/network/tpl/hostname.tpl | 2 - pkg/network/tpl/nftables_exporter.service.tpl | 13 --- pkg/network/tpl/suricata_update.service.tpl | 12 --- pkg/network/tpl/tailscale.service.tpl | 13 --- pkg/network/tpl/tailscaled.service.tpl | 25 ----- pkg/services/suricata/suricata.go | 102 ++++++++++++++++++ .../suricata/suricata.yaml.tpl} | 6 +- pkg/services/suricata/suricata_test.go | 90 ++++++++++++++++ .../suricata/test}/suricata-update.service | 0 .../suricata.yaml} | 8 +- .../suricata/test/suricata_defaults} | 2 +- pkg/services/tailscale/tailscale.go | 8 +- pkg/services/tailscale/tailscale_test.go | 2 +- .../tailscale/test/tailscaled.service | 2 +- .../systemd_renderer.go | 8 +- 25 files changed, 223 insertions(+), 352 deletions(-) delete mode 100644 pkg/network/hostname.go delete mode 100644 pkg/network/hostname_test.go delete mode 100644 pkg/network/suricata_config.go delete mode 100644 pkg/network/suricata_defaults.go delete mode 100644 pkg/network/suricata_update.go delete mode 100644 pkg/network/tailscale.go delete mode 100644 pkg/network/tailscaled.go delete mode 100644 pkg/network/tpl/droptailer.service.tpl delete mode 100644 pkg/network/tpl/firewall_controller.service.tpl delete mode 100644 pkg/network/tpl/hostname.tpl delete mode 100644 pkg/network/tpl/nftables_exporter.service.tpl delete mode 100644 pkg/network/tpl/suricata_update.service.tpl delete mode 100644 pkg/network/tpl/tailscale.service.tpl delete mode 100644 pkg/network/tpl/tailscaled.service.tpl create mode 100644 pkg/services/suricata/suricata.go rename pkg/{network/tpl/suricata_config.yaml.tpl => services/suricata/suricata.yaml.tpl} (99%) create mode 100644 pkg/services/suricata/suricata_test.go rename pkg/{network/testdata => services/suricata/test}/suricata-update.service (100%) rename pkg/services/suricata/{suricata_config.yaml.tpl => test/suricata.yaml} (99%) rename pkg/{network/tpl/suricata_defaults.tpl => services/suricata/test/suricata_defaults} (97%) diff --git a/install.go b/install.go index a1e3ce8..928054f 100644 --- a/install.go +++ b/install.go @@ -93,6 +93,12 @@ func (i *installer) do() error { } } + err = i.writeHostname() + if err != nil { + i.log.Warn("writing hostname failed", "error", err) + return err + } + err = i.writeResolvConf() if err != nil { i.log.Warn("writing resolv.conf failed", "error", err) @@ -195,6 +201,10 @@ func (i *installer) fileExists(filename string) bool { return !info.IsDir() } +func (i *installer) writeHostname() error { + return afero.WriteFile(i.fs, "/etc/hostname", []byte(i.config.Hostname), 0644) +} + func (i *installer) writeResolvConf() error { const f = "/etc/resolv.conf" i.log.Info("write configuration", "file", f) diff --git a/pkg/network/hostname.go b/pkg/network/hostname.go deleted file mode 100644 index 7273f2c..0000000 --- a/pkg/network/hostname.go +++ /dev/null @@ -1,33 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -// tplHostname defines the name of the template to render /etc/hostname. -const tplHostname = "hostname.tpl" - -type ( - // HostnameData contains attributes to render hostname file. - HostnameData struct { - Comment, Hostname string - } - - // HostnameValidator validates hostname changes. - HostnameValidator struct { - path string - } -) - -// newHostnameApplier creates a new Applier to render hostname. -func newHostnameApplier(kb config, tmpFile string) net.Applier { - data := HostnameData{Comment: versionHeader(kb.MachineUUID), Hostname: kb.Hostname} - validator := HostnameValidator{tmpFile} - - return net.NewNetworkApplier(data, validator, nil) -} - -// Validate validates hostname rendering. -func (v HostnameValidator) Validate() error { - return nil -} diff --git a/pkg/network/hostname_test.go b/pkg/network/hostname_test.go deleted file mode 100644 index 78f1f9e..0000000 --- a/pkg/network/hostname_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package network - -import ( - "bytes" - "log/slog" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNameHostname(t *testing.T) { - expected, err := os.ReadFile("testdata/hostname") - require.NoError(t, err) - - log := slog.Default() - kb, err := New(log, "testdata/firewall.yaml") - require.NoError(t, err) - - a := newHostnameApplier(*kb, "") - b := bytes.Buffer{} - - tpl := MustParseTpl(tplHostname) - err = a.Render(&b, *tpl) - require.NoError(t, err) - assert.Equal(t, string(expected), b.String()) -} diff --git a/pkg/network/suricata_config.go b/pkg/network/suricata_config.go deleted file mode 100644 index 55d8c5e..0000000 --- a/pkg/network/suricata_config.go +++ /dev/null @@ -1,41 +0,0 @@ -package network - -import ( - "strings" - - "github.com/metal-stack/os-installer/pkg/net" -) - -// tplSuricataConfig is the name of the template for the suricata configuration. -const tplSuricataConfig = "suricata_config.yaml.tpl" - -// SuricataConfigData represents the information required to render suricata configuration. -type SuricataConfigData struct { - Comment string - DefaultRouteVrf string - Interface string -} - -// suricataConfigValidator can validate configuration for suricata. -type suricataConfigValidator struct { - path string -} - -// newSuricataConfigApplier constructs a new instance of this type. -func newSuricataConfigApplier(kb config, tmpFile string) (net.Applier, error) { - defaultRouteVrf, err := kb.getDefaultRouteVRFName() - if err != nil { - return nil, err - } - - i := strings.Replace(defaultRouteVrf, "vrf", "vlan", 1) - data := SuricataConfigData{Comment: versionHeader(kb.MachineUUID), DefaultRouteVrf: defaultRouteVrf, Interface: i} - validator := suricataConfigValidator{tmpFile} - - return net.NewNetworkApplier(data, validator, nil), nil -} - -// Validate validates suricata configuration. -func (v suricataConfigValidator) Validate() error { - return nil -} diff --git a/pkg/network/suricata_defaults.go b/pkg/network/suricata_defaults.go deleted file mode 100644 index bf1df2d..0000000 --- a/pkg/network/suricata_defaults.go +++ /dev/null @@ -1,40 +0,0 @@ -package network - -import ( - "strings" - - "github.com/metal-stack/os-installer/pkg/net" -) - -// tplSuricataDefaults is the name of the template for the suricata defaults. -const tplSuricataDefaults = "suricata_defaults.tpl" - -// SuricataDefaultsData represents the information required to render suricata defaults. -type SuricataDefaultsData struct { - Comment string - Interface string -} - -// suricataDefaultsValidator can validate defaults for suricata. -type suricataDefaultsValidator struct { - path string -} - -// newSuricataDefaultsApplier constructs a new instance of this type. -func newSuricataDefaultsApplier(kb config, tmpFile string) (net.Applier, error) { - defaultRouteVrf, err := kb.getDefaultRouteVRFName() - if err != nil { - return nil, err - } - - i := strings.Replace(defaultRouteVrf, "vrf", "vlan", 1) - data := SuricataDefaultsData{Comment: versionHeader(kb.MachineUUID), Interface: i} - validator := suricataDefaultsValidator{path: tmpFile} - - return net.NewNetworkApplier(data, validator, nil), nil -} - -// Validate validates suricata defaults. -func (v suricataDefaultsValidator) Validate() error { - return nil -} diff --git a/pkg/network/suricata_update.go b/pkg/network/suricata_update.go deleted file mode 100644 index 616817a..0000000 --- a/pkg/network/suricata_update.go +++ /dev/null @@ -1,29 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -// tplSuricataUpdate is the name of the template for the suricata-update service. -const tplSuricataUpdate = "suricata_update.service.tpl" - -// systemdUnitSuricataUpdate is the name of the systemd unit for the suricata-update. -const systemdUnitSuricataUpdate = "suricata-update.service" - -// SuricataUpdateData contains the data to render the suricata-update service template. -type SuricataUpdateData struct { - Comment string - DefaultRouteVrf string -} - -// newSuricataUpdateServiceApplier constructs a new instance of this type. -func newSuricataUpdateServiceApplier(kb config, v net.Validator) (net.Applier, error) { - defaultRouteVrf, err := kb.getDefaultRouteVRFName() - if err != nil { - return nil, err - } - - data := SuricataUpdateData{Comment: versionHeader(kb.MachineUUID), DefaultRouteVrf: defaultRouteVrf} - - return net.NewNetworkApplier(data, v, nil), nil -} diff --git a/pkg/network/tailscale.go b/pkg/network/tailscale.go deleted file mode 100644 index c3e5eee..0000000 --- a/pkg/network/tailscale.go +++ /dev/null @@ -1,37 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -const ( - // tplTailscale is the name of the template for the Tailscale client. - tplTailscale = "tailscale.service.tpl" - // systemdUnitTailscale is the name of the systemd unit for the Tailscale client. - systemdUnitTailscale = "tailscale.service" -) - -// TailscaleData contains the data to render the Tailscale service template. -type TailscaleData struct { - MachineID string - AuthKey string - Address string - DefaultRouteVrf string -} - -// newTailscaleServiceApplier constructs a new instance of this type. -func newTailscaleServiceApplier(kb config, v net.Validator) (net.Applier, error) { - defaultRouteVrf, err := kb.getDefaultRouteVRFName() - if err != nil { - return nil, err - } - - data := TailscaleData{ - MachineID: kb.MachineUUID, - AuthKey: *kb.VPN.AuthKey, - Address: *kb.VPN.Address, - DefaultRouteVrf: defaultRouteVrf, - } - - return net.NewNetworkApplier(data, v, nil), nil -} diff --git a/pkg/network/tailscaled.go b/pkg/network/tailscaled.go deleted file mode 100644 index f5e67ae..0000000 --- a/pkg/network/tailscaled.go +++ /dev/null @@ -1,31 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -const ( - // tplTailscaled is the name of the template for the tailscaled service. - tplTailscaled = "tailscaled.service.tpl" - // systemdUnitTailscaled is the name of the systemd unit for the tailscaled. - systemdUnitTailscaled = "tailscaled.service" - defaultTailscaledPort = "41641" -) - -// TailscaledData contains the data to render the tailscaled service template. -type TailscaledData struct { - TailscaledPort string - DefaultRouteVrf string -} - -// newTailscaledServiceApplier constructs a new instance of this type. -func newTailscaledServiceApplier(kb config, v net.Validator) (net.Applier, error) { - defaultRouteVrf, err := kb.getDefaultRouteVRFName() - if err != nil { - return nil, err - } - - data := TailscaledData{TailscaledPort: defaultTailscaledPort, DefaultRouteVrf: defaultRouteVrf} - - return net.NewNetworkApplier(data, v, nil), nil -} diff --git a/pkg/network/tpl/droptailer.service.tpl b/pkg/network/tpl/droptailer.service.tpl deleted file mode 100644 index 48e2c8c..0000000 --- a/pkg/network/tpl/droptailer.service.tpl +++ /dev/null @@ -1,18 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.DroptailerData*/ -}} -{{ .Comment }} -[Unit] -Description=Droptailer -After=network.target - -[Service] -LimitMEMLOCK=infinity -Environment=DROPTAILER_SERVER_ADDRESS=droptailer:50051 -Environment=DROPTAILER_PREFIXES_OF_DROPS="nftables-metal-dropped: ,nftables-firewall-dropped: " -Environment=DROPTAILER_CLIENT_CERTIFICATE=/etc/droptailer-client/droptailer-client.crt -Environment=DROPTAILER_CLIENT_KEY=/etc/droptailer-client/droptailer-client.key -ExecStart=/bin/ip vrf exec {{ .TenantVrf }} /usr/local/bin/droptailer-client -Restart=always -RestartSec=10 - -[Install] -WantedBy=firewall-controller.service diff --git a/pkg/network/tpl/firewall_controller.service.tpl b/pkg/network/tpl/firewall_controller.service.tpl deleted file mode 100644 index 8ec206c..0000000 --- a/pkg/network/tpl/firewall_controller.service.tpl +++ /dev/null @@ -1,15 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallControllerData*/ -}} -{{ .Comment }} -[Unit] -Description=Firewall controller - configures the firewall based on k8s resources -After=network.target - -[Service] -LimitMEMLOCK=infinity -Environment=KUBECONFIG=/etc/firewall-controller/.kubeconfig -ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/firewall-controller -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target diff --git a/pkg/network/tpl/hostname.tpl b/pkg/network/tpl/hostname.tpl deleted file mode 100644 index ffce2f3..0000000 --- a/pkg/network/tpl/hostname.tpl +++ /dev/null @@ -1,2 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.HostnameData*/ -}} -{{ .Hostname }} \ No newline at end of file diff --git a/pkg/network/tpl/nftables_exporter.service.tpl b/pkg/network/tpl/nftables_exporter.service.tpl deleted file mode 100644 index 2381523..0000000 --- a/pkg/network/tpl/nftables_exporter.service.tpl +++ /dev/null @@ -1,13 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NftablesExporterData*/ -}} -{{ .Comment }} -[Unit] -Description=Nftables exporter - provides prometheus metrics for nftables -After=network.target - -[Service] -ExecStart=/usr/bin/nftables-exporter --config=/etc/nftables_exporter.yaml -Restart=always -RestartSec=30 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/network/tpl/suricata_update.service.tpl b/pkg/network/tpl/suricata_update.service.tpl deleted file mode 100644 index 0f4ef7b..0000000 --- a/pkg/network/tpl/suricata_update.service.tpl +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Suricata Intrusion Detection Service Rules Update - -[Service] -LimitMEMLOCK=infinity -User=root -Group=root -Type=oneshot -ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/bin/suricata-update - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/network/tpl/tailscale.service.tpl b/pkg/network/tpl/tailscale.service.tpl deleted file mode 100644 index 2364d65..0000000 --- a/pkg/network/tpl/tailscale.service.tpl +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Tailscale client -After=tailscaled.service - -[Service] -LimitMEMLOCK=infinity -User=root -Group=root -ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscale up --hostname {{ .MachineID }} --auth-key {{ .AuthKey }} --login-server {{ .Address }} -Restart=on-failure - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/network/tpl/tailscaled.service.tpl b/pkg/network/tpl/tailscaled.service.tpl deleted file mode 100644 index 3db63ad..0000000 --- a/pkg/network/tpl/tailscaled.service.tpl +++ /dev/null @@ -1,25 +0,0 @@ -[Unit] -Description=Tailscale node agent -Documentation=https://tailscale.com/kb/ -After=network.target - -[Service] -LimitMEMLOCK=infinity -User=root -Group=root -Type=notify -Environment="TS_NO_LOGS_NO_SUPPORT=true" -ExecStartPre=ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscaled --cleanup -ExecStart=/bin/ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscaled --port {{ .TailscaledPort }} -ExecStopPost=ip vrf exec {{ .DefaultRouteVrf }} /usr/local/bin/tailscaled --cleanup -Restart=on-failure - -RuntimeDirectory=tailscale -RuntimeDirectoryMode=0755 -StateDirectory=tailscale -StateDirectoryMode=0700 -CacheDirectory=tailscale -CacheDirectoryMode=0750 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/services/suricata/suricata.go b/pkg/services/suricata/suricata.go new file mode 100644 index 0000000..7bfb2d9 --- /dev/null +++ b/pkg/services/suricata/suricata.go @@ -0,0 +1,102 @@ +package suricata + +import ( + "context" + "log/slog" + + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" + + _ "embed" +) + +const ( + suricataServiceName = "suricata.service" + + suricataUpdateServiceName = "suricata_update.service" + suricataUpdateServiceUnitPath = "/etc/systemd/system/" + suricataUpdateServiceName + + suricataDefaultsPath = "/etc/default/suricata" + suricataConfigPath = "/etc/suricata/suricata.yaml" +) + +var ( + //go:embed suricata.yaml.tpl + suricataConfigTemplateString string + //go:embed suricata_defaults.tpl + suricataDefaultsTemplateString string + //go:embed suricata_update.service.tpl + suricataUpdateServiceTemplateString string +) + +type Config struct { + Log *slog.Logger + Reload bool + fs afero.Fs +} + +type TemplateData struct { + Interface string + DefaultRouteVrf string +} + +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { + r, err := systemd_renderer.New(&systemd_renderer.Config{ + ServiceName: suricataUpdateServiceName, + Log: cfg.Log, + TemplateString: suricataUpdateServiceTemplateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + chg, err := r.Render(ctx, suricataUpdateServiceUnitPath, cfg.Reload) + if err != nil { + return chg, err + } + + // return changed if one has changed + changed = changed || chg + + for _, spec := range []struct { + path string + templateString string + }{ + { + path: suricataDefaultsPath, + templateString: suricataDefaultsTemplateString, + }, + { + path: suricataConfigPath, + templateString: suricataConfigTemplateString, + }, + } { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: spec.templateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + chg, err := r.Render(ctx, spec.path) + if err != nil { + return chg, err + } + + changed = changed || chg + } + + if cfg.Reload && changed { + if err := systemd_renderer.Reload(ctx, cfg.Log.With("service-name", "suricata"), suricataServiceName); err != nil { + return changed, err + } + } + + return changed, nil +} diff --git a/pkg/network/tpl/suricata_config.yaml.tpl b/pkg/services/suricata/suricata.yaml.tpl similarity index 99% rename from pkg/network/tpl/suricata_config.yaml.tpl rename to pkg/services/suricata/suricata.yaml.tpl index 378b618..0d3ab7f 100644 --- a/pkg/network/tpl/suricata_config.yaml.tpl +++ b/pkg/services/suricata/suricata.yaml.tpl @@ -1021,8 +1021,8 @@ coredump: # This feature is currently only used by the reject* keywords. host-mode: auto -# Number of packets preallocated per thread. The default is 1024. A higher number -# will make sure each CPU will be more easily kept busy, but may negatively +# Number of packets preallocated per thread. The default is 1024. A higher number +# will make sure each CPU will be more easily kept busy, but may negatively # impact caching. #max-pending-packets: 1024 @@ -1057,7 +1057,7 @@ unix-command: # Magic file. The extension .mgc is added to the value here. #magic-file: /usr/share/file/magic -#magic-file: +#magic-file: # GeoIP2 database file. Specify path and filename of GeoIP2 database # if using rules with "geoip" rule option. diff --git a/pkg/services/suricata/suricata_test.go b/pkg/services/suricata/suricata_test.go new file mode 100644 index 0000000..ca476ba --- /dev/null +++ b/pkg/services/suricata/suricata_test.go @@ -0,0 +1,90 @@ +package suricata + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/suricata-update.service + expectedSuricataSystemdUnit string + //go:embed test/suricata.yaml + expectedSuricataConfig string + //go:embed test/suricata_defaults + expectedSuricataDefaults string +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *TemplateData + wantSuricataService string + wantSuricataConfig string + wantSuricataDefaults string + wantChanged bool + wantErr error + }{ + { + name: "render", + c: &TemplateData{ + DefaultRouteVrf: "vrf104009", + Interface: "vlan104009", + }, + wantSuricataService: expectedSuricataSystemdUnit, + wantSuricataConfig: expectedSuricataConfig, + wantSuricataDefaults: expectedSuricataDefaults, + wantChanged: true, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c) + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(suricataUpdateServiceUnitPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantSuricataService, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + + content, err = fs.ReadFile(suricataConfigPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantSuricataConfig, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + + content, err = fs.ReadFile(suricataDefaultsPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantSuricataDefaults, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/pkg/network/testdata/suricata-update.service b/pkg/services/suricata/test/suricata-update.service similarity index 100% rename from pkg/network/testdata/suricata-update.service rename to pkg/services/suricata/test/suricata-update.service diff --git a/pkg/services/suricata/suricata_config.yaml.tpl b/pkg/services/suricata/test/suricata.yaml similarity index 99% rename from pkg/services/suricata/suricata_config.yaml.tpl rename to pkg/services/suricata/test/suricata.yaml index 378b618..b500946 100644 --- a/pkg/services/suricata/suricata_config.yaml.tpl +++ b/pkg/services/suricata/test/suricata.yaml @@ -578,7 +578,7 @@ logging: # Linux high speed capture support af-packet: - - interface: {{ .Interface }} + - interface: vlan104009 # Number of receive threads. "auto" uses the number of cores #threads: auto # Default clusterid. AF_PACKET will load balance packets based on flow. @@ -1021,8 +1021,8 @@ coredump: # This feature is currently only used by the reject* keywords. host-mode: auto -# Number of packets preallocated per thread. The default is 1024. A higher number -# will make sure each CPU will be more easily kept busy, but may negatively +# Number of packets preallocated per thread. The default is 1024. A higher number +# will make sure each CPU will be more easily kept busy, but may negatively # impact caching. #max-pending-packets: 1024 @@ -1057,7 +1057,7 @@ unix-command: # Magic file. The extension .mgc is added to the value here. #magic-file: /usr/share/file/magic -#magic-file: +#magic-file: # GeoIP2 database file. Specify path and filename of GeoIP2 database # if using rules with "geoip" rule option. diff --git a/pkg/network/tpl/suricata_defaults.tpl b/pkg/services/suricata/test/suricata_defaults similarity index 97% rename from pkg/network/tpl/suricata_defaults.tpl rename to pkg/services/suricata/test/suricata_defaults index 44ff7b8..d35a3b0 100644 --- a/pkg/network/tpl/suricata_defaults.tpl +++ b/pkg/services/suricata/test/suricata_defaults @@ -16,7 +16,7 @@ SURCONF=/etc/suricata/suricata.yaml LISTENMODE=af-packet # Interface to listen on (for pcap mode) -IFACE={{ .Interface }} +IFACE=vlan104009 # Queue number to listen on (for nfqueue mode) NFQUEUE="-q 0" diff --git a/pkg/services/tailscale/tailscale.go b/pkg/services/tailscale/tailscale.go index 977fecd..275b34c 100644 --- a/pkg/services/tailscale/tailscale.go +++ b/pkg/services/tailscale/tailscale.go @@ -16,6 +16,8 @@ const ( tailscaledServiceName = "tailscaled.service" tailscaledServiceUnitPath = "/etc/systemd/system/" + tailscaledServiceName + + tailscaledDefaultPort = "41641" ) var ( @@ -41,6 +43,10 @@ type TemplateData struct { } func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { + if c.TailscaledPort == "" { + c.TailscaledPort = tailscaledDefaultPort + } + for _, spec := range []struct { servicePath string serviceName string @@ -77,5 +83,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change changed = changed || chg } - return true, nil + return changed, nil } diff --git a/pkg/services/tailscale/tailscale_test.go b/pkg/services/tailscale/tailscale_test.go index a7c691e..c5dab42 100644 --- a/pkg/services/tailscale/tailscale_test.go +++ b/pkg/services/tailscale/tailscale_test.go @@ -34,7 +34,7 @@ func TestWriteSystemdUnit(t *testing.T) { c: &TemplateData{ Comment: `Do not edit.`, DefaultRouteVrf: "vrf104009", - TailscaledPort: "41161", + TailscaledPort: "41641", MachineID: "c0115b51-5e4d-4f92-85c8-1cc504eafdd2", AuthKey: "a-authkey", Address: "headscale.metal-stack.io", diff --git a/pkg/services/tailscale/test/tailscaled.service b/pkg/services/tailscale/test/tailscaled.service index 0cc12e0..7d70c44 100644 --- a/pkg/services/tailscale/test/tailscaled.service +++ b/pkg/services/tailscale/test/tailscaled.service @@ -10,7 +10,7 @@ Group=root Type=notify Environment="TS_NO_LOGS_NO_SUPPORT=true" ExecStartPre=ip vrf exec vrf104009 /usr/local/bin/tailscaled --cleanup -ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscaled --port 41161 +ExecStart=/bin/ip vrf exec vrf104009 /usr/local/bin/tailscaled --port 41641 ExecStopPost=ip vrf exec vrf104009 /usr/local/bin/tailscaled --cleanup Restart=on-failure diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index d037286..c8eea44 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -66,17 +66,17 @@ func (r *systemdRenderer) Render(ctx context.Context, destFile string, reload bo return changed, nil } - if err := r.reload(ctx); err != nil { + if err := Reload(ctx, r.log, r.serviceName); err != nil { return true, err } return true, err } -func (r *systemdRenderer) reload(ctx context.Context) error { +func Reload(ctx context.Context, log *slog.Logger, unitName string) error { const done = "done" - r.log.Info("reloading systemd service unit") + log.Info("reloading systemd service unit") dbc, err := dbus.NewWithContext(ctx) if err != nil { @@ -86,7 +86,7 @@ func (r *systemdRenderer) reload(ctx context.Context) error { c := make(chan string) - if _, err = dbc.ReloadUnitContext(ctx, r.serviceName, "replace", c); err != nil { + if _, err = dbc.ReloadUnitContext(ctx, unitName, "replace", c); err != nil { return err } From 55176a63580ba2d9a5f31768ecbd0ad0c874adc6 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 10:59:56 +0100 Subject: [PATCH 008/102] Chrony. --- install.go | 57 +++++++++++--- {pkg => old}/exec/doc.go | 0 {pkg => old}/exec/verbosecmd.go | 0 {pkg => old}/net/README.md | 0 {pkg => old}/net/applier.go | 0 {pkg => old}/net/applier_test.go | 0 {pkg => old}/net/doc.go | 0 {pkg => old}/net/reloader.go | 0 {pkg => old}/net/validator.go | 0 {pkg => old}/network/Dockerfile.validate | 0 {pkg => old}/network/configurator.go | 0 {pkg => old}/network/configurator_test.go | 0 {pkg => old}/network/doc.go | 0 {pkg => old}/network/frr.go | 0 {pkg => old}/network/frr_test.go | 0 {pkg => old}/network/interfaces.go | 0 {pkg => old}/network/interfaces_test.go | 0 {pkg => old}/network/knowledgebase.go | 9 +++ {pkg => old}/network/knowledgebase_test.go | 0 {pkg => old}/network/netobjects.go | 0 {pkg => old}/network/nftables.go | 0 {pkg => old}/network/nftables_test.go | 0 {pkg => old}/network/routemap.go | 0 {pkg => old}/network/routemap_test.go | 0 {pkg => old}/network/service_test.go | 0 {pkg => old}/network/systemd.go | 0 {pkg => old}/network/template.go | 0 {pkg => old}/network/testdata/firewall.yaml | 0 .../network/testdata/firewall_dmz.yaml | 0 .../network/testdata/firewall_dmz_app.yaml | 0 .../testdata/firewall_dmz_app_storage.yaml | 0 .../network/testdata/firewall_dualstack.yaml | 0 .../network/testdata/firewall_ipv6.yaml | 0 .../network/testdata/firewall_shared.yaml | 0 .../network/testdata/firewall_vpn.yaml | 0 .../network/testdata/firewall_with_rules.yaml | 0 .../network/testdata/frr.conf.firewall | 0 .../network/testdata/frr.conf.firewall_dmz | 0 .../testdata/frr.conf.firewall_dmz_app | 0 .../frr.conf.firewall_dmz_app_storage | 0 .../testdata/frr.conf.firewall_dualstack | 0 .../network/testdata/frr.conf.firewall_frr-10 | 0 .../network/testdata/frr.conf.firewall_frr-9 | 0 .../network/testdata/frr.conf.firewall_ipv6 | 0 .../network/testdata/frr.conf.firewall_shared | 0 .../network/testdata/frr.conf.machine | 0 {pkg => old}/network/testdata/machine.yaml | 0 .../testdata/networkd/firewall/00-lo.network | 0 .../testdata/networkd/firewall/10-lan0.link | 0 .../networkd/firewall/10-lan0.network | 0 .../testdata/networkd/firewall/11-lan1.link | 0 .../networkd/firewall/11-lan1.network | 0 .../networkd/firewall/20-bridge.netdev | 0 .../networkd/firewall/20-bridge.network | 0 .../networkd/firewall/30-svi-3981.netdev | 0 .../networkd/firewall/30-svi-3981.network | 0 .../networkd/firewall/30-vrf-3981.netdev | 0 .../networkd/firewall/30-vrf-3981.network | 0 .../networkd/firewall/30-vxlan-3981.netdev | 0 .../networkd/firewall/30-vxlan-3981.network | 0 .../networkd/firewall/31-svi-3982.netdev | 0 .../networkd/firewall/31-svi-3982.network | 0 .../networkd/firewall/31-vrf-3982.netdev | 0 .../networkd/firewall/31-vrf-3982.network | 0 .../networkd/firewall/31-vxlan-3982.netdev | 0 .../networkd/firewall/31-vxlan-3982.network | 0 .../networkd/firewall/32-svi-104009.netdev | 0 .../networkd/firewall/32-svi-104009.network | 0 .../networkd/firewall/32-vrf-104009.netdev | 0 .../networkd/firewall/32-vrf-104009.network | 0 .../networkd/firewall/32-vxlan-104009.netdev | 0 .../networkd/firewall/32-vxlan-104009.network | 0 .../networkd/firewall/33-svi-104010.netdev | 0 .../networkd/firewall/33-svi-104010.network | 0 .../networkd/firewall/33-vrf-104010.netdev | 0 .../networkd/firewall/33-vrf-104010.network | 0 .../networkd/firewall/33-vxlan-104010.netdev | 0 .../networkd/firewall/33-vxlan-104010.network | 0 .../testdata/networkd/machine/00-lo.network | 0 .../testdata/networkd/machine/10-lan0.link | 0 .../testdata/networkd/machine/10-lan0.network | 0 .../testdata/networkd/machine/11-lan1.link | 0 .../testdata/networkd/machine/11-lan1.network | 0 {pkg => old}/network/testdata/nftrules | 0 .../testdata/nftrules_accept_forwarding | 0 {pkg => old}/network/testdata/nftrules_dmz | 0 .../network/testdata/nftrules_dmz_app | 0 {pkg => old}/network/testdata/nftrules_ipv6 | 0 {pkg => old}/network/testdata/nftrules_shared | 0 {pkg => old}/network/testdata/nftrules_vpn | 0 .../network/testdata/nftrules_with_rules | 0 {pkg => old}/network/tpl/frr.firewall.tpl | 0 {pkg => old}/network/tpl/frr.machine.tpl | 0 .../network/tpl/networkd/00-lo.network.tpl | 0 .../network/tpl/networkd/10-lan.link.tpl | 0 .../network/tpl/networkd/10-lan.network.tpl | 0 .../network/tpl/networkd/20-bridge.netdev.tpl | 0 .../tpl/networkd/20-bridge.network.tpl | 0 .../network/tpl/networkd/30-svi.netdev.tpl | 0 .../network/tpl/networkd/30-svi.network.tpl | 0 .../network/tpl/networkd/30-vrf.netdev.tpl | 0 .../network/tpl/networkd/30-vrf.network.tpl | 0 .../network/tpl/networkd/30-vxlan.netdev.tpl | 0 .../network/tpl/networkd/30-vxlan.network.tpl | 0 {pkg => old}/network/tpl/nftrules.tpl | 0 {pkg => old}/network/validate.sh | 0 {pkg => old}/network/validate_os.sh | 0 pkg/frr/frr.go | 1 + pkg/interfaces/interfaces.go | 0 pkg/network/chrony.go | 40 ---------- pkg/network/chrony_test.go | 43 ---------- pkg/network/hosts.go | 37 --------- pkg/network/hosts_test.go | 27 ------- pkg/network/testdata/hostname | 1 - pkg/network/testdata/hosts | 4 - pkg/network/tpl/hosts.tpl | 4 - pkg/network/tpl/node_exporter.service.tpl | 13 ---- pkg/nftables/nftables.go | 0 .../services/chrony}/chrony.conf.tpl | 6 +- pkg/services/chrony/chrony.go | 73 +++++++++++++++++ pkg/services/chrony/chrony_test.go | 78 +++++++++++++++++++ .../services/chrony/test/custom}/chrony.conf | 5 +- .../services/chrony/test/default}/chrony.conf | 2 +- .../systemd_renderer.go | 25 ++++++ templates/template.go | 30 ------- templates/template_test.go | 50 ------------ 126 files changed, 239 insertions(+), 266 deletions(-) rename {pkg => old}/exec/doc.go (100%) rename {pkg => old}/exec/verbosecmd.go (100%) rename {pkg => old}/net/README.md (100%) rename {pkg => old}/net/applier.go (100%) rename {pkg => old}/net/applier_test.go (100%) rename {pkg => old}/net/doc.go (100%) rename {pkg => old}/net/reloader.go (100%) rename {pkg => old}/net/validator.go (100%) rename {pkg => old}/network/Dockerfile.validate (100%) rename {pkg => old}/network/configurator.go (100%) rename {pkg => old}/network/configurator_test.go (100%) rename {pkg => old}/network/doc.go (100%) rename {pkg => old}/network/frr.go (100%) rename {pkg => old}/network/frr_test.go (100%) rename {pkg => old}/network/interfaces.go (100%) rename {pkg => old}/network/interfaces_test.go (100%) rename {pkg => old}/network/knowledgebase.go (97%) rename {pkg => old}/network/knowledgebase_test.go (100%) rename {pkg => old}/network/netobjects.go (100%) rename {pkg => old}/network/nftables.go (100%) rename {pkg => old}/network/nftables_test.go (100%) rename {pkg => old}/network/routemap.go (100%) rename {pkg => old}/network/routemap_test.go (100%) rename {pkg => old}/network/service_test.go (100%) rename {pkg => old}/network/systemd.go (100%) rename {pkg => old}/network/template.go (100%) rename {pkg => old}/network/testdata/firewall.yaml (100%) rename {pkg => old}/network/testdata/firewall_dmz.yaml (100%) rename {pkg => old}/network/testdata/firewall_dmz_app.yaml (100%) rename {pkg => old}/network/testdata/firewall_dmz_app_storage.yaml (100%) rename {pkg => old}/network/testdata/firewall_dualstack.yaml (100%) rename {pkg => old}/network/testdata/firewall_ipv6.yaml (100%) rename {pkg => old}/network/testdata/firewall_shared.yaml (100%) rename {pkg => old}/network/testdata/firewall_vpn.yaml (100%) rename {pkg => old}/network/testdata/firewall_with_rules.yaml (100%) rename {pkg => old}/network/testdata/frr.conf.firewall (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_dmz (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_dmz_app (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_dmz_app_storage (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_dualstack (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_frr-10 (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_frr-9 (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_ipv6 (100%) rename {pkg => old}/network/testdata/frr.conf.firewall_shared (100%) rename {pkg => old}/network/testdata/frr.conf.machine (100%) rename {pkg => old}/network/testdata/machine.yaml (100%) rename {pkg => old}/network/testdata/networkd/firewall/00-lo.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/10-lan0.link (100%) rename {pkg => old}/network/testdata/networkd/firewall/10-lan0.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/11-lan1.link (100%) rename {pkg => old}/network/testdata/networkd/firewall/11-lan1.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/20-bridge.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/20-bridge.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/30-svi-3981.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/30-svi-3981.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/30-vrf-3981.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/30-vrf-3981.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/30-vxlan-3981.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/30-vxlan-3981.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/31-svi-3982.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/31-svi-3982.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/31-vrf-3982.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/31-vrf-3982.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/31-vxlan-3982.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/31-vxlan-3982.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/32-svi-104009.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/32-svi-104009.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/32-vrf-104009.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/32-vrf-104009.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/32-vxlan-104009.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/32-vxlan-104009.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/33-svi-104010.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/33-svi-104010.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/33-vrf-104010.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/33-vrf-104010.network (100%) rename {pkg => old}/network/testdata/networkd/firewall/33-vxlan-104010.netdev (100%) rename {pkg => old}/network/testdata/networkd/firewall/33-vxlan-104010.network (100%) rename {pkg => old}/network/testdata/networkd/machine/00-lo.network (100%) rename {pkg => old}/network/testdata/networkd/machine/10-lan0.link (100%) rename {pkg => old}/network/testdata/networkd/machine/10-lan0.network (100%) rename {pkg => old}/network/testdata/networkd/machine/11-lan1.link (100%) rename {pkg => old}/network/testdata/networkd/machine/11-lan1.network (100%) rename {pkg => old}/network/testdata/nftrules (100%) rename {pkg => old}/network/testdata/nftrules_accept_forwarding (100%) rename {pkg => old}/network/testdata/nftrules_dmz (100%) rename {pkg => old}/network/testdata/nftrules_dmz_app (100%) rename {pkg => old}/network/testdata/nftrules_ipv6 (100%) rename {pkg => old}/network/testdata/nftrules_shared (100%) rename {pkg => old}/network/testdata/nftrules_vpn (100%) rename {pkg => old}/network/testdata/nftrules_with_rules (100%) rename {pkg => old}/network/tpl/frr.firewall.tpl (100%) rename {pkg => old}/network/tpl/frr.machine.tpl (100%) rename {pkg => old}/network/tpl/networkd/00-lo.network.tpl (100%) rename {pkg => old}/network/tpl/networkd/10-lan.link.tpl (100%) rename {pkg => old}/network/tpl/networkd/10-lan.network.tpl (100%) rename {pkg => old}/network/tpl/networkd/20-bridge.netdev.tpl (100%) rename {pkg => old}/network/tpl/networkd/20-bridge.network.tpl (100%) rename {pkg => old}/network/tpl/networkd/30-svi.netdev.tpl (100%) rename {pkg => old}/network/tpl/networkd/30-svi.network.tpl (100%) rename {pkg => old}/network/tpl/networkd/30-vrf.netdev.tpl (100%) rename {pkg => old}/network/tpl/networkd/30-vrf.network.tpl (100%) rename {pkg => old}/network/tpl/networkd/30-vxlan.netdev.tpl (100%) rename {pkg => old}/network/tpl/networkd/30-vxlan.network.tpl (100%) rename {pkg => old}/network/tpl/nftrules.tpl (100%) rename {pkg => old}/network/validate.sh (100%) rename {pkg => old}/network/validate_os.sh (100%) create mode 100644 pkg/frr/frr.go create mode 100644 pkg/interfaces/interfaces.go delete mode 100644 pkg/network/chrony.go delete mode 100644 pkg/network/chrony_test.go delete mode 100644 pkg/network/hosts.go delete mode 100644 pkg/network/hosts_test.go delete mode 100644 pkg/network/testdata/hostname delete mode 100644 pkg/network/testdata/hosts delete mode 100644 pkg/network/tpl/hosts.tpl delete mode 100644 pkg/network/tpl/node_exporter.service.tpl create mode 100644 pkg/nftables/nftables.go rename {templates => pkg/services/chrony}/chrony.conf.tpl (94%) create mode 100644 pkg/services/chrony/chrony.go create mode 100644 pkg/services/chrony/chrony_test.go rename {templates/test_data/customntp => pkg/services/chrony/test/custom}/chrony.conf (95%) rename {templates/test_data/defaultntp => pkg/services/chrony/test/default}/chrony.conf (98%) delete mode 100644 templates/template.go delete mode 100644 templates/template_test.go diff --git a/install.go b/install.go index 928054f..7b80b91 100644 --- a/install.go +++ b/install.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io/fs" "log/slog" @@ -15,8 +16,8 @@ import ( ignitionConfig "github.com/flatcar/ignition/config/v2_4" "github.com/metal-stack/metal-go/api/models" v1 "github.com/metal-stack/os-installer/api/v1" - "github.com/metal-stack/os-installer/pkg/network" - "github.com/metal-stack/os-installer/templates" + "github.com/metal-stack/os-installer/old/network" + "github.com/metal-stack/os-installer/pkg/services/chrony" "github.com/metal-stack/v" "github.com/spf13/afero" "gopkg.in/yaml.v3" @@ -44,9 +45,10 @@ type installer struct { oss operatingsystem config *v1.InstallerConfig exec *cmdexec + ctx context.Context } -func Install(log *slog.Logger, config *v1.InstallerConfig) error { +func Install(ctx context.Context, log *slog.Logger, config *v1.InstallerConfig) error { start := time.Now() fs := afero.OsFs{} @@ -64,6 +66,7 @@ func Install(log *slog.Logger, config *v1.InstallerConfig) error { log: log.WithGroup("cmdexec"), c: exec.CommandContext, }, + ctx: ctx, } err = i.do() @@ -99,6 +102,12 @@ func (i *installer) do() error { return err } + err = i.writeHosts() + if err != nil { + i.log.Warn("writing hosts file failed", "error", err) + return err + } + err = i.writeResolvConf() if err != nil { i.log.Warn("writing resolv.conf failed", "error", err) @@ -205,6 +214,14 @@ func (i *installer) writeHostname() error { return afero.WriteFile(i.fs, "/etc/hostname", []byte(i.config.Hostname), 0644) } +func (i *installer) writeHosts() error { + // FIXME: figure out how to get the private primary ip + return afero.WriteFile(i.fs, "/etc/hosts", []byte(fmt.Sprintf(`# this file was auto generated by the os-installer +127.0.0.1 localhost +%s %s +`, i.config.PrivateIP, i.config.Hostname)), 0644) +} + func (i *installer) writeResolvConf() error { const f = "/etc/resolv.conf" i.log.Info("write configuration", "file", f) @@ -240,14 +257,25 @@ func (i *installer) writeNTPConf() error { ntpConfigPath string s string err error + servers []string ) + for _, srv := range i.config.NTPServers { + servers = append(servers, *srv.Address) + } + switch i.config.Role { case models.V1MachineAllocationRoleFirewall: - ntpConfigPath = "/etc/chrony/chrony.conf" - s, err = templates.RenderChronyTemplate(templates.Chrony{NTPServers: i.config.NTPServers}) + _, err := chrony.WriteSystemdUnit(i.ctx, &chrony.Config{ + Log: i.log, + Reload: false, + Enable: true, + }, &chrony.TemplateData{ + NTPServers: servers, + }, "TODO") // FIXME: default vrf + if err != nil { - return fmt.Errorf("error rendering chrony template %w", err) + return err } case models.V1MachineAllocationRoleMachine: @@ -264,12 +292,20 @@ func (i *installer) writeNTPConf() error { } if i.oss == osAlmalinux { - ntpConfigPath = "/etc/chrony.conf" - s, err = templates.RenderChronyTemplate(templates.Chrony{NTPServers: i.config.NTPServers}) + _, err := chrony.WriteSystemdUnit(i.ctx, &chrony.Config{ + Log: i.log, + Reload: false, + Enable: true, + ChronyConfigPath: "/etc/chrony.conf", + }, &chrony.TemplateData{ + NTPServers: servers, + }, "TODO") // FIXME: default vrf + if err != nil { - return fmt.Errorf("error rendering chrony template %w", err) + return err } } + default: return fmt.Errorf("unknown role:%s", i.config.Role) } @@ -490,8 +526,7 @@ func (i *installer) copySSHKeys() error { func (i *installer) fixPermissions() error { i.log.Info("fix permissions") for p, perm := range map[string]fs.FileMode{ - "/var/tmp": 01777, - "/etc/hosts": 0644, + "/var/tmp": 01777, } { err := i.fs.Chmod(p, perm) if err != nil { diff --git a/pkg/exec/doc.go b/old/exec/doc.go similarity index 100% rename from pkg/exec/doc.go rename to old/exec/doc.go diff --git a/pkg/exec/verbosecmd.go b/old/exec/verbosecmd.go similarity index 100% rename from pkg/exec/verbosecmd.go rename to old/exec/verbosecmd.go diff --git a/pkg/net/README.md b/old/net/README.md similarity index 100% rename from pkg/net/README.md rename to old/net/README.md diff --git a/pkg/net/applier.go b/old/net/applier.go similarity index 100% rename from pkg/net/applier.go rename to old/net/applier.go diff --git a/pkg/net/applier_test.go b/old/net/applier_test.go similarity index 100% rename from pkg/net/applier_test.go rename to old/net/applier_test.go diff --git a/pkg/net/doc.go b/old/net/doc.go similarity index 100% rename from pkg/net/doc.go rename to old/net/doc.go diff --git a/pkg/net/reloader.go b/old/net/reloader.go similarity index 100% rename from pkg/net/reloader.go rename to old/net/reloader.go diff --git a/pkg/net/validator.go b/old/net/validator.go similarity index 100% rename from pkg/net/validator.go rename to old/net/validator.go diff --git a/pkg/network/Dockerfile.validate b/old/network/Dockerfile.validate similarity index 100% rename from pkg/network/Dockerfile.validate rename to old/network/Dockerfile.validate diff --git a/pkg/network/configurator.go b/old/network/configurator.go similarity index 100% rename from pkg/network/configurator.go rename to old/network/configurator.go diff --git a/pkg/network/configurator_test.go b/old/network/configurator_test.go similarity index 100% rename from pkg/network/configurator_test.go rename to old/network/configurator_test.go diff --git a/pkg/network/doc.go b/old/network/doc.go similarity index 100% rename from pkg/network/doc.go rename to old/network/doc.go diff --git a/pkg/network/frr.go b/old/network/frr.go similarity index 100% rename from pkg/network/frr.go rename to old/network/frr.go diff --git a/pkg/network/frr_test.go b/old/network/frr_test.go similarity index 100% rename from pkg/network/frr_test.go rename to old/network/frr_test.go diff --git a/pkg/network/interfaces.go b/old/network/interfaces.go similarity index 100% rename from pkg/network/interfaces.go rename to old/network/interfaces.go diff --git a/pkg/network/interfaces_test.go b/old/network/interfaces_test.go similarity index 100% rename from pkg/network/interfaces_test.go rename to old/network/interfaces_test.go diff --git a/pkg/network/knowledgebase.go b/old/network/knowledgebase.go similarity index 97% rename from pkg/network/knowledgebase.go rename to old/network/knowledgebase.go index 2f2ff57..3ee54f4 100644 --- a/pkg/network/knowledgebase.go +++ b/old/network/knowledgebase.go @@ -247,3 +247,12 @@ func versionHeader(uuid string) string { return fmt.Sprintf("# This file was auto generated for machine: '%s' by app version %s.\n# Do not edit.", uuid, version) } + +func containsDefaultRoute(prefixes []string) bool { + for _, prefix := range prefixes { + if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { + return true + } + } + return false +} diff --git a/pkg/network/knowledgebase_test.go b/old/network/knowledgebase_test.go similarity index 100% rename from pkg/network/knowledgebase_test.go rename to old/network/knowledgebase_test.go diff --git a/pkg/network/netobjects.go b/old/network/netobjects.go similarity index 100% rename from pkg/network/netobjects.go rename to old/network/netobjects.go diff --git a/pkg/network/nftables.go b/old/network/nftables.go similarity index 100% rename from pkg/network/nftables.go rename to old/network/nftables.go diff --git a/pkg/network/nftables_test.go b/old/network/nftables_test.go similarity index 100% rename from pkg/network/nftables_test.go rename to old/network/nftables_test.go diff --git a/pkg/network/routemap.go b/old/network/routemap.go similarity index 100% rename from pkg/network/routemap.go rename to old/network/routemap.go diff --git a/pkg/network/routemap_test.go b/old/network/routemap_test.go similarity index 100% rename from pkg/network/routemap_test.go rename to old/network/routemap_test.go diff --git a/pkg/network/service_test.go b/old/network/service_test.go similarity index 100% rename from pkg/network/service_test.go rename to old/network/service_test.go diff --git a/pkg/network/systemd.go b/old/network/systemd.go similarity index 100% rename from pkg/network/systemd.go rename to old/network/systemd.go diff --git a/pkg/network/template.go b/old/network/template.go similarity index 100% rename from pkg/network/template.go rename to old/network/template.go diff --git a/pkg/network/testdata/firewall.yaml b/old/network/testdata/firewall.yaml similarity index 100% rename from pkg/network/testdata/firewall.yaml rename to old/network/testdata/firewall.yaml diff --git a/pkg/network/testdata/firewall_dmz.yaml b/old/network/testdata/firewall_dmz.yaml similarity index 100% rename from pkg/network/testdata/firewall_dmz.yaml rename to old/network/testdata/firewall_dmz.yaml diff --git a/pkg/network/testdata/firewall_dmz_app.yaml b/old/network/testdata/firewall_dmz_app.yaml similarity index 100% rename from pkg/network/testdata/firewall_dmz_app.yaml rename to old/network/testdata/firewall_dmz_app.yaml diff --git a/pkg/network/testdata/firewall_dmz_app_storage.yaml b/old/network/testdata/firewall_dmz_app_storage.yaml similarity index 100% rename from pkg/network/testdata/firewall_dmz_app_storage.yaml rename to old/network/testdata/firewall_dmz_app_storage.yaml diff --git a/pkg/network/testdata/firewall_dualstack.yaml b/old/network/testdata/firewall_dualstack.yaml similarity index 100% rename from pkg/network/testdata/firewall_dualstack.yaml rename to old/network/testdata/firewall_dualstack.yaml diff --git a/pkg/network/testdata/firewall_ipv6.yaml b/old/network/testdata/firewall_ipv6.yaml similarity index 100% rename from pkg/network/testdata/firewall_ipv6.yaml rename to old/network/testdata/firewall_ipv6.yaml diff --git a/pkg/network/testdata/firewall_shared.yaml b/old/network/testdata/firewall_shared.yaml similarity index 100% rename from pkg/network/testdata/firewall_shared.yaml rename to old/network/testdata/firewall_shared.yaml diff --git a/pkg/network/testdata/firewall_vpn.yaml b/old/network/testdata/firewall_vpn.yaml similarity index 100% rename from pkg/network/testdata/firewall_vpn.yaml rename to old/network/testdata/firewall_vpn.yaml diff --git a/pkg/network/testdata/firewall_with_rules.yaml b/old/network/testdata/firewall_with_rules.yaml similarity index 100% rename from pkg/network/testdata/firewall_with_rules.yaml rename to old/network/testdata/firewall_with_rules.yaml diff --git a/pkg/network/testdata/frr.conf.firewall b/old/network/testdata/frr.conf.firewall similarity index 100% rename from pkg/network/testdata/frr.conf.firewall rename to old/network/testdata/frr.conf.firewall diff --git a/pkg/network/testdata/frr.conf.firewall_dmz b/old/network/testdata/frr.conf.firewall_dmz similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_dmz rename to old/network/testdata/frr.conf.firewall_dmz diff --git a/pkg/network/testdata/frr.conf.firewall_dmz_app b/old/network/testdata/frr.conf.firewall_dmz_app similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_dmz_app rename to old/network/testdata/frr.conf.firewall_dmz_app diff --git a/pkg/network/testdata/frr.conf.firewall_dmz_app_storage b/old/network/testdata/frr.conf.firewall_dmz_app_storage similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_dmz_app_storage rename to old/network/testdata/frr.conf.firewall_dmz_app_storage diff --git a/pkg/network/testdata/frr.conf.firewall_dualstack b/old/network/testdata/frr.conf.firewall_dualstack similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_dualstack rename to old/network/testdata/frr.conf.firewall_dualstack diff --git a/pkg/network/testdata/frr.conf.firewall_frr-10 b/old/network/testdata/frr.conf.firewall_frr-10 similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_frr-10 rename to old/network/testdata/frr.conf.firewall_frr-10 diff --git a/pkg/network/testdata/frr.conf.firewall_frr-9 b/old/network/testdata/frr.conf.firewall_frr-9 similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_frr-9 rename to old/network/testdata/frr.conf.firewall_frr-9 diff --git a/pkg/network/testdata/frr.conf.firewall_ipv6 b/old/network/testdata/frr.conf.firewall_ipv6 similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_ipv6 rename to old/network/testdata/frr.conf.firewall_ipv6 diff --git a/pkg/network/testdata/frr.conf.firewall_shared b/old/network/testdata/frr.conf.firewall_shared similarity index 100% rename from pkg/network/testdata/frr.conf.firewall_shared rename to old/network/testdata/frr.conf.firewall_shared diff --git a/pkg/network/testdata/frr.conf.machine b/old/network/testdata/frr.conf.machine similarity index 100% rename from pkg/network/testdata/frr.conf.machine rename to old/network/testdata/frr.conf.machine diff --git a/pkg/network/testdata/machine.yaml b/old/network/testdata/machine.yaml similarity index 100% rename from pkg/network/testdata/machine.yaml rename to old/network/testdata/machine.yaml diff --git a/pkg/network/testdata/networkd/firewall/00-lo.network b/old/network/testdata/networkd/firewall/00-lo.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/00-lo.network rename to old/network/testdata/networkd/firewall/00-lo.network diff --git a/pkg/network/testdata/networkd/firewall/10-lan0.link b/old/network/testdata/networkd/firewall/10-lan0.link similarity index 100% rename from pkg/network/testdata/networkd/firewall/10-lan0.link rename to old/network/testdata/networkd/firewall/10-lan0.link diff --git a/pkg/network/testdata/networkd/firewall/10-lan0.network b/old/network/testdata/networkd/firewall/10-lan0.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/10-lan0.network rename to old/network/testdata/networkd/firewall/10-lan0.network diff --git a/pkg/network/testdata/networkd/firewall/11-lan1.link b/old/network/testdata/networkd/firewall/11-lan1.link similarity index 100% rename from pkg/network/testdata/networkd/firewall/11-lan1.link rename to old/network/testdata/networkd/firewall/11-lan1.link diff --git a/pkg/network/testdata/networkd/firewall/11-lan1.network b/old/network/testdata/networkd/firewall/11-lan1.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/11-lan1.network rename to old/network/testdata/networkd/firewall/11-lan1.network diff --git a/pkg/network/testdata/networkd/firewall/20-bridge.netdev b/old/network/testdata/networkd/firewall/20-bridge.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/20-bridge.netdev rename to old/network/testdata/networkd/firewall/20-bridge.netdev diff --git a/pkg/network/testdata/networkd/firewall/20-bridge.network b/old/network/testdata/networkd/firewall/20-bridge.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/20-bridge.network rename to old/network/testdata/networkd/firewall/20-bridge.network diff --git a/pkg/network/testdata/networkd/firewall/30-svi-3981.netdev b/old/network/testdata/networkd/firewall/30-svi-3981.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/30-svi-3981.netdev rename to old/network/testdata/networkd/firewall/30-svi-3981.netdev diff --git a/pkg/network/testdata/networkd/firewall/30-svi-3981.network b/old/network/testdata/networkd/firewall/30-svi-3981.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/30-svi-3981.network rename to old/network/testdata/networkd/firewall/30-svi-3981.network diff --git a/pkg/network/testdata/networkd/firewall/30-vrf-3981.netdev b/old/network/testdata/networkd/firewall/30-vrf-3981.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/30-vrf-3981.netdev rename to old/network/testdata/networkd/firewall/30-vrf-3981.netdev diff --git a/pkg/network/testdata/networkd/firewall/30-vrf-3981.network b/old/network/testdata/networkd/firewall/30-vrf-3981.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/30-vrf-3981.network rename to old/network/testdata/networkd/firewall/30-vrf-3981.network diff --git a/pkg/network/testdata/networkd/firewall/30-vxlan-3981.netdev b/old/network/testdata/networkd/firewall/30-vxlan-3981.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/30-vxlan-3981.netdev rename to old/network/testdata/networkd/firewall/30-vxlan-3981.netdev diff --git a/pkg/network/testdata/networkd/firewall/30-vxlan-3981.network b/old/network/testdata/networkd/firewall/30-vxlan-3981.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/30-vxlan-3981.network rename to old/network/testdata/networkd/firewall/30-vxlan-3981.network diff --git a/pkg/network/testdata/networkd/firewall/31-svi-3982.netdev b/old/network/testdata/networkd/firewall/31-svi-3982.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/31-svi-3982.netdev rename to old/network/testdata/networkd/firewall/31-svi-3982.netdev diff --git a/pkg/network/testdata/networkd/firewall/31-svi-3982.network b/old/network/testdata/networkd/firewall/31-svi-3982.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/31-svi-3982.network rename to old/network/testdata/networkd/firewall/31-svi-3982.network diff --git a/pkg/network/testdata/networkd/firewall/31-vrf-3982.netdev b/old/network/testdata/networkd/firewall/31-vrf-3982.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/31-vrf-3982.netdev rename to old/network/testdata/networkd/firewall/31-vrf-3982.netdev diff --git a/pkg/network/testdata/networkd/firewall/31-vrf-3982.network b/old/network/testdata/networkd/firewall/31-vrf-3982.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/31-vrf-3982.network rename to old/network/testdata/networkd/firewall/31-vrf-3982.network diff --git a/pkg/network/testdata/networkd/firewall/31-vxlan-3982.netdev b/old/network/testdata/networkd/firewall/31-vxlan-3982.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/31-vxlan-3982.netdev rename to old/network/testdata/networkd/firewall/31-vxlan-3982.netdev diff --git a/pkg/network/testdata/networkd/firewall/31-vxlan-3982.network b/old/network/testdata/networkd/firewall/31-vxlan-3982.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/31-vxlan-3982.network rename to old/network/testdata/networkd/firewall/31-vxlan-3982.network diff --git a/pkg/network/testdata/networkd/firewall/32-svi-104009.netdev b/old/network/testdata/networkd/firewall/32-svi-104009.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/32-svi-104009.netdev rename to old/network/testdata/networkd/firewall/32-svi-104009.netdev diff --git a/pkg/network/testdata/networkd/firewall/32-svi-104009.network b/old/network/testdata/networkd/firewall/32-svi-104009.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/32-svi-104009.network rename to old/network/testdata/networkd/firewall/32-svi-104009.network diff --git a/pkg/network/testdata/networkd/firewall/32-vrf-104009.netdev b/old/network/testdata/networkd/firewall/32-vrf-104009.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/32-vrf-104009.netdev rename to old/network/testdata/networkd/firewall/32-vrf-104009.netdev diff --git a/pkg/network/testdata/networkd/firewall/32-vrf-104009.network b/old/network/testdata/networkd/firewall/32-vrf-104009.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/32-vrf-104009.network rename to old/network/testdata/networkd/firewall/32-vrf-104009.network diff --git a/pkg/network/testdata/networkd/firewall/32-vxlan-104009.netdev b/old/network/testdata/networkd/firewall/32-vxlan-104009.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/32-vxlan-104009.netdev rename to old/network/testdata/networkd/firewall/32-vxlan-104009.netdev diff --git a/pkg/network/testdata/networkd/firewall/32-vxlan-104009.network b/old/network/testdata/networkd/firewall/32-vxlan-104009.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/32-vxlan-104009.network rename to old/network/testdata/networkd/firewall/32-vxlan-104009.network diff --git a/pkg/network/testdata/networkd/firewall/33-svi-104010.netdev b/old/network/testdata/networkd/firewall/33-svi-104010.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/33-svi-104010.netdev rename to old/network/testdata/networkd/firewall/33-svi-104010.netdev diff --git a/pkg/network/testdata/networkd/firewall/33-svi-104010.network b/old/network/testdata/networkd/firewall/33-svi-104010.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/33-svi-104010.network rename to old/network/testdata/networkd/firewall/33-svi-104010.network diff --git a/pkg/network/testdata/networkd/firewall/33-vrf-104010.netdev b/old/network/testdata/networkd/firewall/33-vrf-104010.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/33-vrf-104010.netdev rename to old/network/testdata/networkd/firewall/33-vrf-104010.netdev diff --git a/pkg/network/testdata/networkd/firewall/33-vrf-104010.network b/old/network/testdata/networkd/firewall/33-vrf-104010.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/33-vrf-104010.network rename to old/network/testdata/networkd/firewall/33-vrf-104010.network diff --git a/pkg/network/testdata/networkd/firewall/33-vxlan-104010.netdev b/old/network/testdata/networkd/firewall/33-vxlan-104010.netdev similarity index 100% rename from pkg/network/testdata/networkd/firewall/33-vxlan-104010.netdev rename to old/network/testdata/networkd/firewall/33-vxlan-104010.netdev diff --git a/pkg/network/testdata/networkd/firewall/33-vxlan-104010.network b/old/network/testdata/networkd/firewall/33-vxlan-104010.network similarity index 100% rename from pkg/network/testdata/networkd/firewall/33-vxlan-104010.network rename to old/network/testdata/networkd/firewall/33-vxlan-104010.network diff --git a/pkg/network/testdata/networkd/machine/00-lo.network b/old/network/testdata/networkd/machine/00-lo.network similarity index 100% rename from pkg/network/testdata/networkd/machine/00-lo.network rename to old/network/testdata/networkd/machine/00-lo.network diff --git a/pkg/network/testdata/networkd/machine/10-lan0.link b/old/network/testdata/networkd/machine/10-lan0.link similarity index 100% rename from pkg/network/testdata/networkd/machine/10-lan0.link rename to old/network/testdata/networkd/machine/10-lan0.link diff --git a/pkg/network/testdata/networkd/machine/10-lan0.network b/old/network/testdata/networkd/machine/10-lan0.network similarity index 100% rename from pkg/network/testdata/networkd/machine/10-lan0.network rename to old/network/testdata/networkd/machine/10-lan0.network diff --git a/pkg/network/testdata/networkd/machine/11-lan1.link b/old/network/testdata/networkd/machine/11-lan1.link similarity index 100% rename from pkg/network/testdata/networkd/machine/11-lan1.link rename to old/network/testdata/networkd/machine/11-lan1.link diff --git a/pkg/network/testdata/networkd/machine/11-lan1.network b/old/network/testdata/networkd/machine/11-lan1.network similarity index 100% rename from pkg/network/testdata/networkd/machine/11-lan1.network rename to old/network/testdata/networkd/machine/11-lan1.network diff --git a/pkg/network/testdata/nftrules b/old/network/testdata/nftrules similarity index 100% rename from pkg/network/testdata/nftrules rename to old/network/testdata/nftrules diff --git a/pkg/network/testdata/nftrules_accept_forwarding b/old/network/testdata/nftrules_accept_forwarding similarity index 100% rename from pkg/network/testdata/nftrules_accept_forwarding rename to old/network/testdata/nftrules_accept_forwarding diff --git a/pkg/network/testdata/nftrules_dmz b/old/network/testdata/nftrules_dmz similarity index 100% rename from pkg/network/testdata/nftrules_dmz rename to old/network/testdata/nftrules_dmz diff --git a/pkg/network/testdata/nftrules_dmz_app b/old/network/testdata/nftrules_dmz_app similarity index 100% rename from pkg/network/testdata/nftrules_dmz_app rename to old/network/testdata/nftrules_dmz_app diff --git a/pkg/network/testdata/nftrules_ipv6 b/old/network/testdata/nftrules_ipv6 similarity index 100% rename from pkg/network/testdata/nftrules_ipv6 rename to old/network/testdata/nftrules_ipv6 diff --git a/pkg/network/testdata/nftrules_shared b/old/network/testdata/nftrules_shared similarity index 100% rename from pkg/network/testdata/nftrules_shared rename to old/network/testdata/nftrules_shared diff --git a/pkg/network/testdata/nftrules_vpn b/old/network/testdata/nftrules_vpn similarity index 100% rename from pkg/network/testdata/nftrules_vpn rename to old/network/testdata/nftrules_vpn diff --git a/pkg/network/testdata/nftrules_with_rules b/old/network/testdata/nftrules_with_rules similarity index 100% rename from pkg/network/testdata/nftrules_with_rules rename to old/network/testdata/nftrules_with_rules diff --git a/pkg/network/tpl/frr.firewall.tpl b/old/network/tpl/frr.firewall.tpl similarity index 100% rename from pkg/network/tpl/frr.firewall.tpl rename to old/network/tpl/frr.firewall.tpl diff --git a/pkg/network/tpl/frr.machine.tpl b/old/network/tpl/frr.machine.tpl similarity index 100% rename from pkg/network/tpl/frr.machine.tpl rename to old/network/tpl/frr.machine.tpl diff --git a/pkg/network/tpl/networkd/00-lo.network.tpl b/old/network/tpl/networkd/00-lo.network.tpl similarity index 100% rename from pkg/network/tpl/networkd/00-lo.network.tpl rename to old/network/tpl/networkd/00-lo.network.tpl diff --git a/pkg/network/tpl/networkd/10-lan.link.tpl b/old/network/tpl/networkd/10-lan.link.tpl similarity index 100% rename from pkg/network/tpl/networkd/10-lan.link.tpl rename to old/network/tpl/networkd/10-lan.link.tpl diff --git a/pkg/network/tpl/networkd/10-lan.network.tpl b/old/network/tpl/networkd/10-lan.network.tpl similarity index 100% rename from pkg/network/tpl/networkd/10-lan.network.tpl rename to old/network/tpl/networkd/10-lan.network.tpl diff --git a/pkg/network/tpl/networkd/20-bridge.netdev.tpl b/old/network/tpl/networkd/20-bridge.netdev.tpl similarity index 100% rename from pkg/network/tpl/networkd/20-bridge.netdev.tpl rename to old/network/tpl/networkd/20-bridge.netdev.tpl diff --git a/pkg/network/tpl/networkd/20-bridge.network.tpl b/old/network/tpl/networkd/20-bridge.network.tpl similarity index 100% rename from pkg/network/tpl/networkd/20-bridge.network.tpl rename to old/network/tpl/networkd/20-bridge.network.tpl diff --git a/pkg/network/tpl/networkd/30-svi.netdev.tpl b/old/network/tpl/networkd/30-svi.netdev.tpl similarity index 100% rename from pkg/network/tpl/networkd/30-svi.netdev.tpl rename to old/network/tpl/networkd/30-svi.netdev.tpl diff --git a/pkg/network/tpl/networkd/30-svi.network.tpl b/old/network/tpl/networkd/30-svi.network.tpl similarity index 100% rename from pkg/network/tpl/networkd/30-svi.network.tpl rename to old/network/tpl/networkd/30-svi.network.tpl diff --git a/pkg/network/tpl/networkd/30-vrf.netdev.tpl b/old/network/tpl/networkd/30-vrf.netdev.tpl similarity index 100% rename from pkg/network/tpl/networkd/30-vrf.netdev.tpl rename to old/network/tpl/networkd/30-vrf.netdev.tpl diff --git a/pkg/network/tpl/networkd/30-vrf.network.tpl b/old/network/tpl/networkd/30-vrf.network.tpl similarity index 100% rename from pkg/network/tpl/networkd/30-vrf.network.tpl rename to old/network/tpl/networkd/30-vrf.network.tpl diff --git a/pkg/network/tpl/networkd/30-vxlan.netdev.tpl b/old/network/tpl/networkd/30-vxlan.netdev.tpl similarity index 100% rename from pkg/network/tpl/networkd/30-vxlan.netdev.tpl rename to old/network/tpl/networkd/30-vxlan.netdev.tpl diff --git a/pkg/network/tpl/networkd/30-vxlan.network.tpl b/old/network/tpl/networkd/30-vxlan.network.tpl similarity index 100% rename from pkg/network/tpl/networkd/30-vxlan.network.tpl rename to old/network/tpl/networkd/30-vxlan.network.tpl diff --git a/pkg/network/tpl/nftrules.tpl b/old/network/tpl/nftrules.tpl similarity index 100% rename from pkg/network/tpl/nftrules.tpl rename to old/network/tpl/nftrules.tpl diff --git a/pkg/network/validate.sh b/old/network/validate.sh similarity index 100% rename from pkg/network/validate.sh rename to old/network/validate.sh diff --git a/pkg/network/validate_os.sh b/old/network/validate_os.sh similarity index 100% rename from pkg/network/validate_os.sh rename to old/network/validate_os.sh diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go new file mode 100644 index 0000000..81d471d --- /dev/null +++ b/pkg/frr/frr.go @@ -0,0 +1 @@ +package frr diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go new file mode 100644 index 0000000..e69de29 diff --git a/pkg/network/chrony.go b/pkg/network/chrony.go deleted file mode 100644 index 882fdc6..0000000 --- a/pkg/network/chrony.go +++ /dev/null @@ -1,40 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - - "github.com/metal-stack/os-installer/pkg/exec" -) - -// chronyServiceEnabler can enable chrony systemd service for the given VRF. -type chronyServiceEnabler struct { - vrf string - log *slog.Logger -} - -// newChronyServiceEnabler constructs a new instance of this type. -func newChronyServiceEnabler(kb config) (chronyServiceEnabler, error) { - vrf, err := kb.getDefaultRouteVRFName() - return chronyServiceEnabler{ - vrf: vrf, - log: kb.log, - }, err -} - -// Enable enables chrony systemd service for the given VRF to be started after boot. -func (c chronyServiceEnabler) Enable() error { - cmd := fmt.Sprintf("systemctl enable chrony@%s", c.vrf) - c.log.Info("enable chrony", "command", cmd) - - return exec.NewVerboseCmd("bash", "-c", cmd).Run() -} - -func containsDefaultRoute(prefixes []string) bool { - for _, prefix := range prefixes { - if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { - return true - } - } - return false -} diff --git a/pkg/network/chrony_test.go b/pkg/network/chrony_test.go deleted file mode 100644 index 10bb313..0000000 --- a/pkg/network/chrony_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package network - -import ( - "testing" - - "github.com/metal-stack/metal-go/api/models" - mn "github.com/metal-stack/metal-lib/pkg/net" - apiv1 "github.com/metal-stack/os-installer/api/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestChronyServiceEnabler_Enable(t *testing.T) { - vrf := int64(104009) - external := mn.External - network := &models.V1MachineNetwork{Networktype: &external, Destinationprefixes: []string{IPv4ZeroCIDR}, Vrf: &vrf} - tests := []struct { - kb config - vrf string - isErrorExpected bool - }{ - { - kb: config{InstallerConfig: apiv1.InstallerConfig{Networks: []*models.V1MachineNetwork{network}}}, - vrf: "vrf104009", - isErrorExpected: false, - }, - { - kb: config{InstallerConfig: apiv1.InstallerConfig{Networks: []*models.V1MachineNetwork{}}}, - vrf: "", - isErrorExpected: true, - }, - } - - for _, tt := range tests { - e, err := newChronyServiceEnabler(tt.kb) - if tt.isErrorExpected { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, tt.vrf, e.vrf) - } -} diff --git a/pkg/network/hosts.go b/pkg/network/hosts.go deleted file mode 100644 index 0ae1d42..0000000 --- a/pkg/network/hosts.go +++ /dev/null @@ -1,37 +0,0 @@ -package network - -import ( - "github.com/metal-stack/os-installer/pkg/net" -) - -// tplHosts defines the name of the template to render hosts file. -const tplHosts = "hosts.tpl" - -type ( - // HostsData contains data to render hosts file. - HostsData struct { - Comment string - Hostname string - IP string - } - - // HostsValidator validates hosts file. - HostsValidator struct { - path string - } -) - -// newHostsApplier creates a new hosts applier. -func newHostsApplier(kb config, tmpFile string) net.Applier { - data := HostsData{Hostname: kb.Hostname, Comment: versionHeader(kb.MachineUUID), IP: kb.getPrivatePrimaryNetwork().Ips[0]} - validator := HostsValidator{tmpFile} - - return net.NewNetworkApplier(data, validator, nil) -} - -// Validate validates hosts file. -func (v HostsValidator) Validate() error { - //nolint:godox - // FIXME: How do we validate a hosts file? - return nil -} diff --git a/pkg/network/hosts_test.go b/pkg/network/hosts_test.go deleted file mode 100644 index 233fc2d..0000000 --- a/pkg/network/hosts_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package network - -import ( - "bytes" - "log/slog" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewHostsApplier(t *testing.T) { - expected, err := os.ReadFile("testdata/hosts") - require.NoError(t, err) - - log := slog.Default() - kb, err := New(log, "testdata/firewall.yaml") - require.NoError(t, err) - a := newHostsApplier(*kb, "") - b := bytes.Buffer{} - - tpl := MustParseTpl(tplHosts) - err = a.Render(&b, *tpl) - require.NoError(t, err) - assert.Equal(t, string(expected), b.String()) -} diff --git a/pkg/network/testdata/hostname b/pkg/network/testdata/hostname deleted file mode 100644 index d565e06..0000000 --- a/pkg/network/testdata/hostname +++ /dev/null @@ -1 +0,0 @@ -firewall \ No newline at end of file diff --git a/pkg/network/testdata/hosts b/pkg/network/testdata/hosts deleted file mode 100644 index aed7680..0000000 --- a/pkg/network/testdata/hosts +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -127.0.0.1 localhost -10.0.16.2 firewall diff --git a/pkg/network/tpl/hosts.tpl b/pkg/network/tpl/hosts.tpl deleted file mode 100644 index 820aad7..0000000 --- a/pkg/network/tpl/hosts.tpl +++ /dev/null @@ -1,4 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.HostsData*/ -}} -{{ .Comment }} -127.0.0.1 localhost -{{ .IP }} {{ .Hostname }} diff --git a/pkg/network/tpl/node_exporter.service.tpl b/pkg/network/tpl/node_exporter.service.tpl deleted file mode 100644 index 3c5550e..0000000 --- a/pkg/network/tpl/node_exporter.service.tpl +++ /dev/null @@ -1,13 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NodeExporterData*/ -}} -{{ .Comment }} -[Unit] -Description=Node exporter - provides prometheus metrics about the node -After=network.target - -[Service] -ExecStart=/usr/local/bin/node_exporter --collector.tcpstat -Restart=always -RestartSec=30 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go new file mode 100644 index 0000000..e69de29 diff --git a/templates/chrony.conf.tpl b/pkg/services/chrony/chrony.conf.tpl similarity index 94% rename from templates/chrony.conf.tpl rename to pkg/services/chrony/chrony.conf.tpl index fb4523a..fc70fe7 100644 --- a/templates/chrony.conf.tpl +++ b/pkg/services/chrony/chrony.conf.tpl @@ -6,8 +6,8 @@ # anycast network of 180+ locations to synchronize time from their closest server. # See https://blog.cloudflare.com/secure-time/ -{{- range .NTPServers}} -pool {{ .Address }} iburst +{{- range .NTPServers }} +pool {{ . }} iburst {{- end }} # This directive specify the location of the file containing ID/key pairs for @@ -33,4 +33,4 @@ rtcsync # Step the system clock instead of slewing it if the adjustment is larger than # one second, but only in the first three clock updates. -makestep 1 3 \ No newline at end of file +makestep 1 3 diff --git a/pkg/services/chrony/chrony.go b/pkg/services/chrony/chrony.go new file mode 100644 index 0000000..704fedd --- /dev/null +++ b/pkg/services/chrony/chrony.go @@ -0,0 +1,73 @@ +package chrony + +import ( + "context" + "fmt" + "log/slog" + + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" + + _ "embed" +) + +const ( + chronyConfigPath = "/etc/chrony/chrony.conf" +) + +var ( + //go:embed chrony.conf.tpl + chronyConfigTemplateString string +) + +type Config struct { + Log *slog.Logger + Reload bool + Enable bool + // ChronyConfigPath allows overwriting the default chrony config path + ChronyConfigPath string + fs afero.Fs +} + +type TemplateData struct { + NTPServers []string +} + +func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData, vrfName string) (changed bool, err error) { + serviceName := fmt.Sprintf("chrony@%s.service", vrfName) + + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: chronyConfigTemplateString, + Data: c, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + path := chronyConfigPath + if cfg.ChronyConfigPath != "" { + path = cfg.ChronyConfigPath + } + + changed, err = r.Render(ctx, path) + if err != nil { + return changed, err + } + + if cfg.Enable { + if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + + if cfg.Reload && changed { + if err := systemd_renderer.Reload(ctx, cfg.Log.With("service-name", "chrony"), serviceName); err != nil { + return changed, err + } + } + + return changed, nil +} diff --git a/pkg/services/chrony/chrony_test.go b/pkg/services/chrony/chrony_test.go new file mode 100644 index 0000000..eb92c4c --- /dev/null +++ b/pkg/services/chrony/chrony_test.go @@ -0,0 +1,78 @@ +package chrony + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test/default/chrony.conf + expectedDefaultConfig string + //go:embed test/custom/chrony.conf + expectedCustomConfig string +) + +func TestWriteSystemdUnit(t *testing.T) { + tests := []struct { + name string + c *TemplateData + wantConfig string + wantChanged bool + wantErr error + }{ + { + name: "render default", + c: &TemplateData{ + NTPServers: []string{"time.cloudflare.com"}, + }, + wantConfig: expectedDefaultConfig, + wantChanged: true, + wantErr: nil, + }, + { + name: "render custom", + c: &TemplateData{ + NTPServers: []string{"1.2.3.4", "1.2.3.5"}, + }, + wantConfig: expectedCustomConfig, + wantChanged: true, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotChanged, gotErr := WriteSystemdUnit(t.Context(), &Config{ + Log: slog.Default(), + Reload: false, + fs: fs, + }, tt.c, "vrf104009") + + assert.Equal(t, tt.wantChanged, gotChanged) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(chronyConfigPath) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantConfig, string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} diff --git a/templates/test_data/customntp/chrony.conf b/pkg/services/chrony/test/custom/chrony.conf similarity index 95% rename from templates/test_data/customntp/chrony.conf rename to pkg/services/chrony/test/custom/chrony.conf index f8ad3a4..8684b4f 100644 --- a/templates/test_data/customntp/chrony.conf +++ b/pkg/services/chrony/test/custom/chrony.conf @@ -5,7 +5,8 @@ # Cloudflare offers a free public time service that allows us to use their # anycast network of 180+ locations to synchronize time from their closest server. # See https://blog.cloudflare.com/secure-time/ -pool custom.1.ntp.org iburst +pool 1.2.3.4 iburst +pool 1.2.3.5 iburst # This directive specify the location of the file containing ID/key pairs for # NTP authentication. @@ -30,4 +31,4 @@ rtcsync # Step the system clock instead of slewing it if the adjustment is larger than # one second, but only in the first three clock updates. -makestep 1 3 \ No newline at end of file +makestep 1 3 diff --git a/templates/test_data/defaultntp/chrony.conf b/pkg/services/chrony/test/default/chrony.conf similarity index 98% rename from templates/test_data/defaultntp/chrony.conf rename to pkg/services/chrony/test/default/chrony.conf index 30ce9da..a747bea 100644 --- a/templates/test_data/defaultntp/chrony.conf +++ b/pkg/services/chrony/test/default/chrony.conf @@ -30,4 +30,4 @@ rtcsync # Step the system clock instead of slewing it if the adjustment is larger than # one second, but only in the first three clock updates. -makestep 1 3 \ No newline at end of file +makestep 1 3 diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index c8eea44..b9b1c64 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -13,6 +13,7 @@ import ( type ( Config struct { ServiceName string + Enable bool Log *slog.Logger TemplateString string Data any @@ -25,6 +26,7 @@ type ( log *slog.Logger r *renderer.Renderer serviceName string + enable bool } ) @@ -49,6 +51,7 @@ func New(c *Config) (*systemdRenderer, error) { log: c.Log.WithGroup("systemd-service-renderer").With("service-name", c.ServiceName), serviceName: c.ServiceName, r: r, + enable: c.Enable, }, nil } @@ -62,6 +65,12 @@ func (r *systemdRenderer) Render(ctx context.Context, destFile string, reload bo return changed, err } + if r.enable { + if err := Enable(ctx, r.log, r.serviceName); err != nil { + return changed, err + } + } + if !reload { return changed, nil } @@ -98,3 +107,19 @@ func Reload(ctx context.Context, log *slog.Logger, unitName string) error { return nil } + +func Enable(ctx context.Context, log *slog.Logger, unitName string) error { + log.Info("enable systemd service unit", "unit-name", unitName) + + dbc, err := dbus.NewWithContext(ctx) + if err != nil { + return fmt.Errorf("unable to connect to dbus: %w", err) + } + defer dbc.Close() + + if _, _, err = dbc.EnableUnitFilesContext(ctx, []string{unitName}, false, false); err != nil { + return fmt.Errorf("unable to enable systemd unit: %w", err) + } + + return nil +} diff --git a/templates/template.go b/templates/template.go deleted file mode 100644 index f25c9ab..0000000 --- a/templates/template.go +++ /dev/null @@ -1,30 +0,0 @@ -package templates - -import ( - "bytes" - _ "embed" - "text/template" - - "github.com/metal-stack/metal-go/api/models" -) - -type Chrony struct { - NTPServers []*models.V1NTPServer -} - -//go:embed chrony.conf.tpl -var chronyTemplate string - -func RenderChronyTemplate(chronyConfig Chrony) (string, error) { - templ, err := template.New("chrony").Parse(chronyTemplate) - if err != nil { - return "error parsing template", err - } - - rendered := new(bytes.Buffer) - err = templ.Execute(rendered, chronyConfig) - if err != nil { - return "error writing to template file", err - } - return rendered.String(), nil -} diff --git a/templates/template_test.go b/templates/template_test.go deleted file mode 100644 index d84cab3..0000000 --- a/templates/template_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package templates - -import ( - _ "embed" - "os" - "testing" - - "github.com/metal-stack/metal-go/api/models" - "github.com/stretchr/testify/require" -) - -func TestDefaultChronyTemplate(t *testing.T) { - defaultNTPServer := "time.cloudflare.com" - ntpServers := []*models.V1NTPServer{ - { - Address: &defaultNTPServer, - }, - } - - rendered := renderToString(t, Chrony{NTPServers: ntpServers}) - expected := readExpected(t, "test_data/defaultntp/chrony.conf") - - require.Equal(t, expected, rendered, "Wanted: %s\nGot: %s", expected, rendered) -} - -func TestCustomChronyTemplate(t *testing.T) { - customNTPServer := "custom.1.ntp.org" - ntpServers := []*models.V1NTPServer{ - { - Address: &customNTPServer, - }, - } - - rendered := renderToString(t, Chrony{NTPServers: ntpServers}) - expected := readExpected(t, "test_data/customntp/chrony.conf") - - require.Equal(t, expected, rendered, "Wanted: %s\nGot: %s", expected, rendered) -} - -func readExpected(t *testing.T, e string) string { - ex, err := os.ReadFile(e) - require.NoError(t, err, "Couldn't read %s", e) - return string(ex) -} - -func renderToString(t *testing.T, c Chrony) string { - r, err := RenderChronyTemplate(c) - require.NoError(t, err, "Could not render chrony configuration") - return r -} From 035b24646e0a1084f1685d22d059b1afb4c356ca Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 13:49:43 +0100 Subject: [PATCH 009/102] Progress. --- go.mod | 5 +- go.sum | 11 +- old/network/configurator.go | 4 +- old/network/frr.go | 4 +- old/network/nftables.go | 4 +- old/network/service_test.go | 2 +- old/network/systemd.go | 2 +- pkg/interfaces/interfaces.go | 154 +++++++++++ pkg/interfaces/interfaces_test.go | 261 ++++++++++++++++++ pkg/interfaces/templates/20-bridge.netdev.tpl | 10 + .../templates/20-bridge.network.tpl | 14 + pkg/interfaces/templates/30-svi.netdev.tpl | 8 + pkg/interfaces/templates/30-svi.network.tpl | 13 + pkg/interfaces/templates/30-vrf.netdev.tpl | 8 + pkg/interfaces/templates/30-vrf.network.tpl | 4 + pkg/interfaces/templates/30-vxlan.netdev.tpl | 12 + pkg/interfaces/templates/30-vxlan.network.tpl | 14 + pkg/interfaces/templates/lan.link.tpl | 8 + pkg/interfaces/templates/lan.network.tpl | 9 + pkg/interfaces/templates/lo.network.tpl | 11 + pkg/interfaces/test/firewall/00-lo.network | 9 + pkg/interfaces/test/firewall/10-lan0.link | 8 + pkg/interfaces/test/firewall/10-lan0.network | 10 + pkg/interfaces/test/firewall/11-lan1.link | 8 + pkg/interfaces/test/firewall/11-lan1.network | 10 + pkg/interfaces/test/firewall/20-bridge.netdev | 10 + .../test/firewall/20-bridge.network | 22 ++ .../test/firewall/30-svi-3981.netdev | 7 + .../test/firewall/30-svi-3981.network | 10 + .../test/firewall/30-vrf-3981.netdev | 7 + .../test/firewall/30-vrf-3981.network | 3 + .../test/firewall/30-vxlan-3981.netdev | 11 + .../test/firewall/30-vxlan-3981.network | 13 + .../test/firewall/31-svi-3982.netdev | 7 + .../test/firewall/31-svi-3982.network | 10 + .../test/firewall/31-vrf-3982.netdev | 7 + .../test/firewall/31-vrf-3982.network | 3 + .../test/firewall/31-vxlan-3982.netdev | 11 + .../test/firewall/31-vxlan-3982.network | 13 + .../test/firewall/32-svi-104009.netdev | 7 + .../test/firewall/32-svi-104009.network | 10 + .../test/firewall/32-vrf-104009.netdev | 7 + .../test/firewall/32-vrf-104009.network | 3 + .../test/firewall/32-vxlan-104009.netdev | 11 + .../test/firewall/32-vxlan-104009.network | 13 + .../test/firewall/33-svi-104010.netdev | 7 + .../test/firewall/33-svi-104010.network | 10 + .../test/firewall/33-vrf-104010.netdev | 7 + .../test/firewall/33-vrf-104010.network | 3 + .../test/firewall/33-vxlan-104010.netdev | 11 + .../test/firewall/33-vxlan-104010.network | 13 + pkg/interfaces/test/machine/00-lo.network | 18 ++ pkg/interfaces/test/machine/10-lan0.link | 8 + pkg/interfaces/test/machine/10-lan0.network | 6 + pkg/interfaces/test/machine/11-lan1.link | 8 + pkg/interfaces/test/machine/11-lan1.network | 6 + pkg/network/network.go | 129 +++++++++ 57 files changed, 1002 insertions(+), 12 deletions(-) create mode 100644 pkg/interfaces/interfaces_test.go create mode 100644 pkg/interfaces/templates/20-bridge.netdev.tpl create mode 100644 pkg/interfaces/templates/20-bridge.network.tpl create mode 100644 pkg/interfaces/templates/30-svi.netdev.tpl create mode 100644 pkg/interfaces/templates/30-svi.network.tpl create mode 100644 pkg/interfaces/templates/30-vrf.netdev.tpl create mode 100644 pkg/interfaces/templates/30-vrf.network.tpl create mode 100644 pkg/interfaces/templates/30-vxlan.netdev.tpl create mode 100644 pkg/interfaces/templates/30-vxlan.network.tpl create mode 100644 pkg/interfaces/templates/lan.link.tpl create mode 100644 pkg/interfaces/templates/lan.network.tpl create mode 100644 pkg/interfaces/templates/lo.network.tpl create mode 100644 pkg/interfaces/test/firewall/00-lo.network create mode 100644 pkg/interfaces/test/firewall/10-lan0.link create mode 100644 pkg/interfaces/test/firewall/10-lan0.network create mode 100644 pkg/interfaces/test/firewall/11-lan1.link create mode 100644 pkg/interfaces/test/firewall/11-lan1.network create mode 100644 pkg/interfaces/test/firewall/20-bridge.netdev create mode 100644 pkg/interfaces/test/firewall/20-bridge.network create mode 100644 pkg/interfaces/test/firewall/30-svi-3981.netdev create mode 100644 pkg/interfaces/test/firewall/30-svi-3981.network create mode 100644 pkg/interfaces/test/firewall/30-vrf-3981.netdev create mode 100644 pkg/interfaces/test/firewall/30-vrf-3981.network create mode 100644 pkg/interfaces/test/firewall/30-vxlan-3981.netdev create mode 100644 pkg/interfaces/test/firewall/30-vxlan-3981.network create mode 100644 pkg/interfaces/test/firewall/31-svi-3982.netdev create mode 100644 pkg/interfaces/test/firewall/31-svi-3982.network create mode 100644 pkg/interfaces/test/firewall/31-vrf-3982.netdev create mode 100644 pkg/interfaces/test/firewall/31-vrf-3982.network create mode 100644 pkg/interfaces/test/firewall/31-vxlan-3982.netdev create mode 100644 pkg/interfaces/test/firewall/31-vxlan-3982.network create mode 100644 pkg/interfaces/test/firewall/32-svi-104009.netdev create mode 100644 pkg/interfaces/test/firewall/32-svi-104009.network create mode 100644 pkg/interfaces/test/firewall/32-vrf-104009.netdev create mode 100644 pkg/interfaces/test/firewall/32-vrf-104009.network create mode 100644 pkg/interfaces/test/firewall/32-vxlan-104009.netdev create mode 100644 pkg/interfaces/test/firewall/32-vxlan-104009.network create mode 100644 pkg/interfaces/test/firewall/33-svi-104010.netdev create mode 100644 pkg/interfaces/test/firewall/33-svi-104010.network create mode 100644 pkg/interfaces/test/firewall/33-vrf-104010.netdev create mode 100644 pkg/interfaces/test/firewall/33-vrf-104010.network create mode 100644 pkg/interfaces/test/firewall/33-vxlan-104010.netdev create mode 100644 pkg/interfaces/test/firewall/33-vxlan-104010.network create mode 100644 pkg/interfaces/test/machine/00-lo.network create mode 100644 pkg/interfaces/test/machine/10-lan0.link create mode 100644 pkg/interfaces/test/machine/10-lan0.network create mode 100644 pkg/interfaces/test/machine/11-lan1.link create mode 100644 pkg/interfaces/test/machine/11-lan1.network create mode 100644 pkg/network/network.go diff --git a/go.mod b/go.mod index f153977..056c821 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,18 @@ require ( github.com/flatcar/ignition v0.36.2 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 + github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff github.com/metal-stack/metal-go v0.43.0 github.com/metal-stack/metal-lib v0.24.0 github.com/metal-stack/v v1.0.3 + github.com/samber/lo v1.53.0 github.com/spf13/afero v1.15.0 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437 // indirect @@ -62,5 +65,5 @@ require ( golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 3b3ea56..6053853 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -85,13 +87,12 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff h1:668iZE3tvpbhoARzpW8zdFnGTVqa7Ks5xJKeY4N0WtA= +github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff/go.mod h1:SAtqZaD4JvOn+NVc6bTlKzL2EDoj/QrlHF72ZMw+Btk= github.com/metal-stack/metal-go v0.43.0 h1:uODD0YCwnAYzyvFxWNakZrymBoMz1FAvP5hkhsR83VQ= github.com/metal-stack/metal-go v0.43.0/go.mod h1:GSfXrAj55LGsUSMHWGDsmq5n056NG0yb1JM8bgfvKOw= github.com/metal-stack/metal-lib v0.24.0 h1:wvQQPWIXcA2tP+I6zAHUNdtVLLJfQnnV9yG2SoqUkz4= @@ -111,6 +112,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigma/bdoor v0.0.0-20160202064022-babf2a4017b0/go.mod h1:WBu7REWbxC/s/J06jsk//d+9DOz9BbsmcIrimuGRFbs= @@ -152,6 +155,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/old/network/configurator.go b/old/network/configurator.go index 6fea055..f7fc46b 100644 --- a/old/network/configurator.go +++ b/old/network/configurator.go @@ -7,8 +7,8 @@ import ( "path" "text/template" - "github.com/metal-stack/os-installer/pkg/exec" - "github.com/metal-stack/os-installer/pkg/net" + "github.com/metal-stack/os-installer/old/exec" + "github.com/metal-stack/os-installer/old/net" ) // BareMetalType defines the type of configuration to apply. diff --git a/old/network/frr.go b/old/network/frr.go index ac6073e..43481e0 100644 --- a/old/network/frr.go +++ b/old/network/frr.go @@ -8,8 +8,8 @@ import ( "github.com/Masterminds/semver/v3" "github.com/metal-stack/metal-go/api/models" mn "github.com/metal-stack/metal-lib/pkg/net" - "github.com/metal-stack/os-installer/pkg/exec" - "github.com/metal-stack/os-installer/pkg/net" + "github.com/metal-stack/os-installer/old/exec" + "github.com/metal-stack/os-installer/old/net" ) const ( diff --git a/old/network/nftables.go b/old/network/nftables.go index d364d1f..46d4def 100644 --- a/old/network/nftables.go +++ b/old/network/nftables.go @@ -10,8 +10,8 @@ import ( "github.com/metal-stack/metal-go/api/models" mn "github.com/metal-stack/metal-lib/pkg/net" - "github.com/metal-stack/os-installer/pkg/exec" - "github.com/metal-stack/os-installer/pkg/net" + "github.com/metal-stack/os-installer/old/exec" + "github.com/metal-stack/os-installer/old/net" ) const ( diff --git a/old/network/service_test.go b/old/network/service_test.go index 301a519..3b0e46d 100644 --- a/old/network/service_test.go +++ b/old/network/service_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/metal-stack/os-installer/pkg/net" + "github.com/metal-stack/os-installer/old/net" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/old/network/systemd.go b/old/network/systemd.go index 9f57fef..78e6e5e 100644 --- a/old/network/systemd.go +++ b/old/network/systemd.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/metal-stack/metal-go/api/models" - "github.com/metal-stack/os-installer/pkg/net" + "github.com/metal-stack/os-installer/old/net" ) const ( diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index e69de29..a415937 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -0,0 +1,154 @@ +package interfaces + +import ( + "context" + "embed" + _ "embed" + "fmt" + "log/slog" + "path" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/network" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" +) + +const ( + systemdNetworkPath = "/etc/systemd/network/" + + loNetwork interfaceKind = "lo.network.tpl" + lanLink interfaceKind = "lan.link.tpl" + lanNetwork interfaceKind = "lan.network.tpl" + + comment = "generated by os-installer" +) + +var ( + //go:embed templates + interfaceTemplates embed.FS +) + +type ( + interfaceKind string + + Config struct { + Log *slog.Logger + Network *network.Network + Nics []*apiv2.MachineNic + fs afero.Fs + } + + loData struct { + Comment string + CIDRs []string + } + + lanLinkData struct { + Comment string + Mac string + Index int + MTU int + } + + lanNetworkData struct { + Comment string + VxlanIDs []uint64 + Index int + } +) + +func ConfigureInterfaces(ctx context.Context, cfg *Config) error { + if err := configureLoopbackInterface(ctx, cfg); err != nil { + return fmt.Errorf("error configuring loopback interface: %w", err) + } + + if err := configureLanInterfaces(ctx, cfg); err != nil { + return fmt.Errorf("error configuring lan interfaces: %w", err) + } + + return nil +} + +func configureLoopbackInterface(ctx context.Context, cfg *Config) error { + loopbackCIDRs, err := cfg.Network.LoopbackCIDRs() + if err != nil { + return err + } + + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: loNetwork.mustReadTemplate(), + Data: loData{ + Comment: comment, + CIDRs: loopbackCIDRs, + }, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, "00-lo.network")) + if err != nil { + return err + } + + return nil +} + +func configureLanInterfaces(ctx context.Context, cfg *Config) error { + const offset = 10 + + for idx, nic := range cfg.Nics { + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: lanLink.mustReadTemplate(), + Data: lanLinkData{ + Comment: comment, + Mac: nic.Mac, + Index: idx, + MTU: cfg.Network.MTU(), + }, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, fmt.Sprintf("%d-lan%d.link", offset+idx, idx))) + if err != nil { + return fmt.Errorf("unable to render lan link config: %w", err) + } + + r, err = renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: lanNetwork.mustReadTemplate(), + Data: lanNetworkData{ + Comment: comment, + VxlanIDs: cfg.Network.VxlanIDs(), + Index: idx, + }, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, fmt.Sprintf("%d-lan%d.network", offset+idx, idx))) + if err != nil { + return fmt.Errorf("unable to render lan network config: %w", err) + } + } + + return nil +} + +func (i interfaceKind) mustReadTemplate() string { + tpl, err := interfaceTemplates.ReadFile(path.Join("templates", string(i))) + if err != nil { + panic(err) + } + + return string(tpl) +} diff --git a/pkg/interfaces/interfaces_test.go b/pkg/interfaces/interfaces_test.go new file mode 100644 index 0000000..a5aa35d --- /dev/null +++ b/pkg/interfaces/interfaces_test.go @@ -0,0 +1,261 @@ +package interfaces + +import ( + "embed" + "log/slog" + "path" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed test + expectedInterfaceFiles embed.FS +) + +func Test_configureLoopbackInterface(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + wantFilePath string + wantErr error + }{ + { + name: "render machine", + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.17.2"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + }, + }, + wantFilePath: "machine/00-lo.network", + wantErr: nil, + }, + { + name: "render firewall", + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.17.2"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + }, + }, + wantFilePath: "firewall/00-lo.network", + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotErr := configureLoopbackInterface(t.Context(), &Config{ + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + }) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(path.Join(systemdNetworkPath, "00-lo.network")) + require.NoError(t, err) + + if diff := cmp.Diff(mustReadExpected(tt.wantFilePath), string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} + +func Test_configureLanInterface(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + nics []*apiv2.MachineNic + wantFilePaths []string + wantErr error + }{ + { + name: "render machine", + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.17.2"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + }, + }, + nics: []*apiv2.MachineNic{ + { + Mac: "00:03:00:11:11:01", + }, + { + Mac: "00:03:00:11:12:01", + }, + }, + wantFilePaths: []string{ + "machine/10-lan0.link", + "machine/10-lan0.network", + "machine/11-lan1.link", + "machine/11-lan1.network", + }, + wantErr: nil, + }, + { + name: "render firewall", + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.17.2"}, + Vrf: 3981, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Ips: []string{"10.0.18.1"}, + Vrf: 3982, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Vrf: 104009, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + }, + }, + nics: []*apiv2.MachineNic{ + { + Mac: "00:03:00:11:11:01", + }, + { + Mac: "00:03:00:11:12:01", + }, + }, + wantFilePaths: []string{ + "firewall/10-lan0.link", + "firewall/10-lan0.network", + "firewall/11-lan1.link", + "firewall/11-lan1.network", + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotErr := configureLanInterfaces(t.Context(), &Config{ + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + Nics: tt.nics, + }) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + for _, name := range tt.wantFilePaths { + content, err := fs.ReadFile(path.Join(systemdNetworkPath, path.Base(name))) + require.NoError(t, err) + + if diff := cmp.Diff(mustReadExpected(name), string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + } + }) + } +} + +func mustReadExpected(name string) string { + tpl, err := expectedInterfaceFiles.ReadFile(path.Join("test", name)) + if err != nil { + panic(err) + } + + return string(tpl) +} diff --git a/pkg/interfaces/templates/20-bridge.netdev.tpl b/pkg/interfaces/templates/20-bridge.netdev.tpl new file mode 100644 index 0000000..2fef44c --- /dev/null +++ b/pkg/interfaces/templates/20-bridge.netdev.tpl @@ -0,0 +1,10 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} +{{ .Comment }} +[NetDev] +Name=bridge +Kind=bridge +MTUBytes=9000 + +[Bridge] +DefaultPVID=none +VLANFiltering=yes diff --git a/pkg/interfaces/templates/20-bridge.network.tpl b/pkg/interfaces/templates/20-bridge.network.tpl new file mode 100644 index 0000000..360b48c --- /dev/null +++ b/pkg/interfaces/templates/20-bridge.network.tpl @@ -0,0 +1,14 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} +{{ .Comment }} +[Match] +Name=bridge + +[Network] +{{- range .EVPNIfaces }} +VLAN=vlan{{ .VRF.ID }} +{{- end }} +{{- range .EVPNIfaces }} + +[BridgeVLAN] +VLAN={{ .SVI.VLANID }} +{{- end }} \ No newline at end of file diff --git a/pkg/interfaces/templates/30-svi.netdev.tpl b/pkg/interfaces/templates/30-svi.netdev.tpl new file mode 100644 index 0000000..6aa6826 --- /dev/null +++ b/pkg/interfaces/templates/30-svi.netdev.tpl @@ -0,0 +1,8 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} +{{ .SVI.Comment }} +[NetDev] +Name=vlan{{ .VRF.ID }} +Kind=vlan + +[VLAN] +Id={{ .SVI.VLANID }} diff --git a/pkg/interfaces/templates/30-svi.network.tpl b/pkg/interfaces/templates/30-svi.network.tpl new file mode 100644 index 0000000..0ef4c10 --- /dev/null +++ b/pkg/interfaces/templates/30-svi.network.tpl @@ -0,0 +1,13 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} +{{ .SVI.Comment }} +[Match] +Name=vlan{{ .VRF.ID }} + +[Link] +MTUBytes=9000 + +[Network] +VRF=vrf{{ .VRF.ID }} +{{- range .SVI.Addresses }} +Address={{ . }} +{{- end }} diff --git a/pkg/interfaces/templates/30-vrf.netdev.tpl b/pkg/interfaces/templates/30-vrf.netdev.tpl new file mode 100644 index 0000000..282a910 --- /dev/null +++ b/pkg/interfaces/templates/30-vrf.netdev.tpl @@ -0,0 +1,8 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} +{{ .VRF.Comment }} +[NetDev] +Name=vrf{{ .VRF.ID }} +Kind=vrf + +[VRF] +Table={{ .VRF.Table }} \ No newline at end of file diff --git a/pkg/interfaces/templates/30-vrf.network.tpl b/pkg/interfaces/templates/30-vrf.network.tpl new file mode 100644 index 0000000..a7628dc --- /dev/null +++ b/pkg/interfaces/templates/30-vrf.network.tpl @@ -0,0 +1,4 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} +{{ .VRF.Comment }} +[Match] +Name=vrf{{ .VRF.ID }} \ No newline at end of file diff --git a/pkg/interfaces/templates/30-vxlan.netdev.tpl b/pkg/interfaces/templates/30-vxlan.netdev.tpl new file mode 100644 index 0000000..68ebf9b --- /dev/null +++ b/pkg/interfaces/templates/30-vxlan.netdev.tpl @@ -0,0 +1,12 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} +{{ .VXLAN.Comment }} +[NetDev] +Name=vni{{ .VXLAN.ID }} +Kind=vxlan + +[VXLAN] +VNI={{ .VXLAN.ID }} +Local={{ .VXLAN.TunnelIP }} +UDPChecksum=true +MacLearning=false +DestinationPort=4789 diff --git a/pkg/interfaces/templates/30-vxlan.network.tpl b/pkg/interfaces/templates/30-vxlan.network.tpl new file mode 100644 index 0000000..a49f111 --- /dev/null +++ b/pkg/interfaces/templates/30-vxlan.network.tpl @@ -0,0 +1,14 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} +{{ .VXLAN.Comment }} +[Match] +Name=vni{{ .VXLAN.ID }} + +[Link] +MTUBytes=9000 + +[Network] +Bridge=bridge + +[BridgeVLAN] +PVID={{ .SVI.VLANID }} +EgressUntagged={{ .SVI.VLANID }} diff --git a/pkg/interfaces/templates/lan.link.tpl b/pkg/interfaces/templates/lan.link.tpl new file mode 100644 index 0000000..d41161e --- /dev/null +++ b/pkg/interfaces/templates/lan.link.tpl @@ -0,0 +1,8 @@ +# {{ .Comment }} +[Match] +PermanentMACAddress={{ .Mac }} + +[Link] +Name=lan{{ .Index }} +NamePolicy= +MTUBytes={{ .MTU }} diff --git a/pkg/interfaces/templates/lan.network.tpl b/pkg/interfaces/templates/lan.network.tpl new file mode 100644 index 0000000..7430196 --- /dev/null +++ b/pkg/interfaces/templates/lan.network.tpl @@ -0,0 +1,9 @@ +# {{ .Comment }} +[Match] +Name=lan{{ .Index }} + +[Network] +IPv6AcceptRA=no +{{- range .VxlanIDs }} +VXLAN=vni{{ . }} +{{- end }} diff --git a/pkg/interfaces/templates/lo.network.tpl b/pkg/interfaces/templates/lo.network.tpl new file mode 100644 index 0000000..563cb75 --- /dev/null +++ b/pkg/interfaces/templates/lo.network.tpl @@ -0,0 +1,11 @@ +# {{ .Comment }} +[Match] +Name=lo + +[Address] +Address=127.0.0.1/8 +{{- range .CIDRs }} + +[Address] +Address={{ . }} +{{- end }} diff --git a/pkg/interfaces/test/firewall/00-lo.network b/pkg/interfaces/test/firewall/00-lo.network new file mode 100644 index 0000000..280382a --- /dev/null +++ b/pkg/interfaces/test/firewall/00-lo.network @@ -0,0 +1,9 @@ +# generated by os-installer +[Match] +Name=lo + +[Address] +Address=127.0.0.1/8 + +[Address] +Address=10.1.0.1/32 diff --git a/pkg/interfaces/test/firewall/10-lan0.link b/pkg/interfaces/test/firewall/10-lan0.link new file mode 100644 index 0000000..186fcfa --- /dev/null +++ b/pkg/interfaces/test/firewall/10-lan0.link @@ -0,0 +1,8 @@ +# generated by os-installer +[Match] +PermanentMACAddress=00:03:00:11:11:01 + +[Link] +Name=lan0 +NamePolicy= +MTUBytes=9216 diff --git a/pkg/interfaces/test/firewall/10-lan0.network b/pkg/interfaces/test/firewall/10-lan0.network new file mode 100644 index 0000000..c7f1767 --- /dev/null +++ b/pkg/interfaces/test/firewall/10-lan0.network @@ -0,0 +1,10 @@ +# generated by os-installer +[Match] +Name=lan0 + +[Network] +IPv6AcceptRA=no +VXLAN=vni3981 +VXLAN=vni3982 +VXLAN=vni104009 +VXLAN=vni104010 diff --git a/pkg/interfaces/test/firewall/11-lan1.link b/pkg/interfaces/test/firewall/11-lan1.link new file mode 100644 index 0000000..9179910 --- /dev/null +++ b/pkg/interfaces/test/firewall/11-lan1.link @@ -0,0 +1,8 @@ +# generated by os-installer +[Match] +PermanentMACAddress=00:03:00:11:12:01 + +[Link] +Name=lan1 +NamePolicy= +MTUBytes=9216 diff --git a/pkg/interfaces/test/firewall/11-lan1.network b/pkg/interfaces/test/firewall/11-lan1.network new file mode 100644 index 0000000..a6df3e6 --- /dev/null +++ b/pkg/interfaces/test/firewall/11-lan1.network @@ -0,0 +1,10 @@ +# generated by os-installer +[Match] +Name=lan1 + +[Network] +IPv6AcceptRA=no +VXLAN=vni3981 +VXLAN=vni3982 +VXLAN=vni104009 +VXLAN=vni104010 diff --git a/pkg/interfaces/test/firewall/20-bridge.netdev b/pkg/interfaces/test/firewall/20-bridge.netdev new file mode 100644 index 0000000..186b556 --- /dev/null +++ b/pkg/interfaces/test/firewall/20-bridge.netdev @@ -0,0 +1,10 @@ +# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . +# Do not edit. +[NetDev] +Name=bridge +Kind=bridge +MTUBytes=9000 + +[Bridge] +DefaultPVID=none +VLANFiltering=yes diff --git a/pkg/interfaces/test/firewall/20-bridge.network b/pkg/interfaces/test/firewall/20-bridge.network new file mode 100644 index 0000000..cfac429 --- /dev/null +++ b/pkg/interfaces/test/firewall/20-bridge.network @@ -0,0 +1,22 @@ +# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . +# Do not edit. +[Match] +Name=bridge + +[Network] +VLAN=vlan3981 +VLAN=vlan3982 +VLAN=vlan104009 +VLAN=vlan104010 + +[BridgeVLAN] +VLAN=1000 + +[BridgeVLAN] +VLAN=1001 + +[BridgeVLAN] +VLAN=1002 + +[BridgeVLAN] +VLAN=1004 diff --git a/pkg/interfaces/test/firewall/30-svi-3981.netdev b/pkg/interfaces/test/firewall/30-svi-3981.netdev new file mode 100644 index 0000000..1c4cb0b --- /dev/null +++ b/pkg/interfaces/test/firewall/30-svi-3981.netdev @@ -0,0 +1,7 @@ +# svi (networkid: bc830818-2df1-4904-8c40-4322296d393d) +[NetDev] +Name=vlan3981 +Kind=vlan + +[VLAN] +Id=1000 diff --git a/pkg/interfaces/test/firewall/30-svi-3981.network b/pkg/interfaces/test/firewall/30-svi-3981.network new file mode 100644 index 0000000..ff73189 --- /dev/null +++ b/pkg/interfaces/test/firewall/30-svi-3981.network @@ -0,0 +1,10 @@ +# svi (networkid: bc830818-2df1-4904-8c40-4322296d393d) +[Match] +Name=vlan3981 + +[Link] +MTUBytes=9000 + +[Network] +VRF=vrf3981 +Address=10.0.16.2/32 diff --git a/pkg/interfaces/test/firewall/30-vrf-3981.netdev b/pkg/interfaces/test/firewall/30-vrf-3981.netdev new file mode 100644 index 0000000..08779cf --- /dev/null +++ b/pkg/interfaces/test/firewall/30-vrf-3981.netdev @@ -0,0 +1,7 @@ +# vrf (networkid: bc830818-2df1-4904-8c40-4322296d393d) +[NetDev] +Name=vrf3981 +Kind=vrf + +[VRF] +Table=1000 diff --git a/pkg/interfaces/test/firewall/30-vrf-3981.network b/pkg/interfaces/test/firewall/30-vrf-3981.network new file mode 100644 index 0000000..ec4a8cc --- /dev/null +++ b/pkg/interfaces/test/firewall/30-vrf-3981.network @@ -0,0 +1,3 @@ +# vrf (networkid: bc830818-2df1-4904-8c40-4322296d393d) +[Match] +Name=vrf3981 diff --git a/pkg/interfaces/test/firewall/30-vxlan-3981.netdev b/pkg/interfaces/test/firewall/30-vxlan-3981.netdev new file mode 100644 index 0000000..4faac86 --- /dev/null +++ b/pkg/interfaces/test/firewall/30-vxlan-3981.netdev @@ -0,0 +1,11 @@ +# vxlan (networkid: bc830818-2df1-4904-8c40-4322296d393d) +[NetDev] +Name=vni3981 +Kind=vxlan + +[VXLAN] +VNI=3981 +Local=10.1.0.1 +UDPChecksum=true +MacLearning=false +DestinationPort=4789 diff --git a/pkg/interfaces/test/firewall/30-vxlan-3981.network b/pkg/interfaces/test/firewall/30-vxlan-3981.network new file mode 100644 index 0000000..0a51049 --- /dev/null +++ b/pkg/interfaces/test/firewall/30-vxlan-3981.network @@ -0,0 +1,13 @@ +# vxlan (networkid: bc830818-2df1-4904-8c40-4322296d393d) +[Match] +Name=vni3981 + +[Link] +MTUBytes=9000 + +[Network] +Bridge=bridge + +[BridgeVLAN] +PVID=1000 +EgressUntagged=1000 diff --git a/pkg/interfaces/test/firewall/31-svi-3982.netdev b/pkg/interfaces/test/firewall/31-svi-3982.netdev new file mode 100644 index 0000000..c82b275 --- /dev/null +++ b/pkg/interfaces/test/firewall/31-svi-3982.netdev @@ -0,0 +1,7 @@ +# svi (networkid: storage-net) +[NetDev] +Name=vlan3982 +Kind=vlan + +[VLAN] +Id=1001 diff --git a/pkg/interfaces/test/firewall/31-svi-3982.network b/pkg/interfaces/test/firewall/31-svi-3982.network new file mode 100644 index 0000000..855cf4d --- /dev/null +++ b/pkg/interfaces/test/firewall/31-svi-3982.network @@ -0,0 +1,10 @@ +# svi (networkid: storage-net) +[Match] +Name=vlan3982 + +[Link] +MTUBytes=9000 + +[Network] +VRF=vrf3982 +Address=10.0.18.2/32 diff --git a/pkg/interfaces/test/firewall/31-vrf-3982.netdev b/pkg/interfaces/test/firewall/31-vrf-3982.netdev new file mode 100644 index 0000000..0700dca --- /dev/null +++ b/pkg/interfaces/test/firewall/31-vrf-3982.netdev @@ -0,0 +1,7 @@ +# vrf (networkid: storage-net) +[NetDev] +Name=vrf3982 +Kind=vrf + +[VRF] +Table=1001 diff --git a/pkg/interfaces/test/firewall/31-vrf-3982.network b/pkg/interfaces/test/firewall/31-vrf-3982.network new file mode 100644 index 0000000..32646c7 --- /dev/null +++ b/pkg/interfaces/test/firewall/31-vrf-3982.network @@ -0,0 +1,3 @@ +# vrf (networkid: storage-net) +[Match] +Name=vrf3982 diff --git a/pkg/interfaces/test/firewall/31-vxlan-3982.netdev b/pkg/interfaces/test/firewall/31-vxlan-3982.netdev new file mode 100644 index 0000000..e909e12 --- /dev/null +++ b/pkg/interfaces/test/firewall/31-vxlan-3982.netdev @@ -0,0 +1,11 @@ +# vxlan (networkid: storage-net) +[NetDev] +Name=vni3982 +Kind=vxlan + +[VXLAN] +VNI=3982 +Local=10.1.0.1 +UDPChecksum=true +MacLearning=false +DestinationPort=4789 diff --git a/pkg/interfaces/test/firewall/31-vxlan-3982.network b/pkg/interfaces/test/firewall/31-vxlan-3982.network new file mode 100644 index 0000000..204c6b3 --- /dev/null +++ b/pkg/interfaces/test/firewall/31-vxlan-3982.network @@ -0,0 +1,13 @@ +# vxlan (networkid: storage-net) +[Match] +Name=vni3982 + +[Link] +MTUBytes=9000 + +[Network] +Bridge=bridge + +[BridgeVLAN] +PVID=1001 +EgressUntagged=1001 diff --git a/pkg/interfaces/test/firewall/32-svi-104009.netdev b/pkg/interfaces/test/firewall/32-svi-104009.netdev new file mode 100644 index 0000000..f941ea3 --- /dev/null +++ b/pkg/interfaces/test/firewall/32-svi-104009.netdev @@ -0,0 +1,7 @@ +# svi (networkid: internet-vagrant-lab) +[NetDev] +Name=vlan104009 +Kind=vlan + +[VLAN] +Id=1002 diff --git a/pkg/interfaces/test/firewall/32-svi-104009.network b/pkg/interfaces/test/firewall/32-svi-104009.network new file mode 100644 index 0000000..e8e16d8 --- /dev/null +++ b/pkg/interfaces/test/firewall/32-svi-104009.network @@ -0,0 +1,10 @@ +# svi (networkid: internet-vagrant-lab) +[Match] +Name=vlan104009 + +[Link] +MTUBytes=9000 + +[Network] +VRF=vrf104009 +Address=185.1.2.3/32 diff --git a/pkg/interfaces/test/firewall/32-vrf-104009.netdev b/pkg/interfaces/test/firewall/32-vrf-104009.netdev new file mode 100644 index 0000000..4a3716e --- /dev/null +++ b/pkg/interfaces/test/firewall/32-vrf-104009.netdev @@ -0,0 +1,7 @@ +# vrf (networkid: internet-vagrant-lab) +[NetDev] +Name=vrf104009 +Kind=vrf + +[VRF] +Table=1002 diff --git a/pkg/interfaces/test/firewall/32-vrf-104009.network b/pkg/interfaces/test/firewall/32-vrf-104009.network new file mode 100644 index 0000000..66fc9c8 --- /dev/null +++ b/pkg/interfaces/test/firewall/32-vrf-104009.network @@ -0,0 +1,3 @@ +# vrf (networkid: internet-vagrant-lab) +[Match] +Name=vrf104009 diff --git a/pkg/interfaces/test/firewall/32-vxlan-104009.netdev b/pkg/interfaces/test/firewall/32-vxlan-104009.netdev new file mode 100644 index 0000000..43ed598 --- /dev/null +++ b/pkg/interfaces/test/firewall/32-vxlan-104009.netdev @@ -0,0 +1,11 @@ +# vxlan (networkid: internet-vagrant-lab) +[NetDev] +Name=vni104009 +Kind=vxlan + +[VXLAN] +VNI=104009 +Local=10.1.0.1 +UDPChecksum=true +MacLearning=false +DestinationPort=4789 diff --git a/pkg/interfaces/test/firewall/32-vxlan-104009.network b/pkg/interfaces/test/firewall/32-vxlan-104009.network new file mode 100644 index 0000000..ea24f09 --- /dev/null +++ b/pkg/interfaces/test/firewall/32-vxlan-104009.network @@ -0,0 +1,13 @@ +# vxlan (networkid: internet-vagrant-lab) +[Match] +Name=vni104009 + +[Link] +MTUBytes=9000 + +[Network] +Bridge=bridge + +[BridgeVLAN] +PVID=1002 +EgressUntagged=1002 diff --git a/pkg/interfaces/test/firewall/33-svi-104010.netdev b/pkg/interfaces/test/firewall/33-svi-104010.netdev new file mode 100644 index 0000000..d1e68a3 --- /dev/null +++ b/pkg/interfaces/test/firewall/33-svi-104010.netdev @@ -0,0 +1,7 @@ +# svi (networkid: mpls-nbg-w8101-test) +[NetDev] +Name=vlan104010 +Kind=vlan + +[VLAN] +Id=1004 diff --git a/pkg/interfaces/test/firewall/33-svi-104010.network b/pkg/interfaces/test/firewall/33-svi-104010.network new file mode 100644 index 0000000..11165a4 --- /dev/null +++ b/pkg/interfaces/test/firewall/33-svi-104010.network @@ -0,0 +1,10 @@ +# svi (networkid: mpls-nbg-w8101-test) +[Match] +Name=vlan104010 + +[Link] +MTUBytes=9000 + +[Network] +VRF=vrf104010 +Address=100.127.129.1/32 diff --git a/pkg/interfaces/test/firewall/33-vrf-104010.netdev b/pkg/interfaces/test/firewall/33-vrf-104010.netdev new file mode 100644 index 0000000..795c7c4 --- /dev/null +++ b/pkg/interfaces/test/firewall/33-vrf-104010.netdev @@ -0,0 +1,7 @@ +# vrf (networkid: mpls-nbg-w8101-test) +[NetDev] +Name=vrf104010 +Kind=vrf + +[VRF] +Table=1004 diff --git a/pkg/interfaces/test/firewall/33-vrf-104010.network b/pkg/interfaces/test/firewall/33-vrf-104010.network new file mode 100644 index 0000000..2765bba --- /dev/null +++ b/pkg/interfaces/test/firewall/33-vrf-104010.network @@ -0,0 +1,3 @@ +# vrf (networkid: mpls-nbg-w8101-test) +[Match] +Name=vrf104010 diff --git a/pkg/interfaces/test/firewall/33-vxlan-104010.netdev b/pkg/interfaces/test/firewall/33-vxlan-104010.netdev new file mode 100644 index 0000000..55ac87b --- /dev/null +++ b/pkg/interfaces/test/firewall/33-vxlan-104010.netdev @@ -0,0 +1,11 @@ +# vxlan (networkid: mpls-nbg-w8101-test) +[NetDev] +Name=vni104010 +Kind=vxlan + +[VXLAN] +VNI=104010 +Local=10.1.0.1 +UDPChecksum=true +MacLearning=false +DestinationPort=4789 diff --git a/pkg/interfaces/test/firewall/33-vxlan-104010.network b/pkg/interfaces/test/firewall/33-vxlan-104010.network new file mode 100644 index 0000000..fff9745 --- /dev/null +++ b/pkg/interfaces/test/firewall/33-vxlan-104010.network @@ -0,0 +1,13 @@ +# vxlan (networkid: mpls-nbg-w8101-test) +[Match] +Name=vni104010 + +[Link] +MTUBytes=9000 + +[Network] +Bridge=bridge + +[BridgeVLAN] +PVID=1004 +EgressUntagged=1004 diff --git a/pkg/interfaces/test/machine/00-lo.network b/pkg/interfaces/test/machine/00-lo.network new file mode 100644 index 0000000..08d73ee --- /dev/null +++ b/pkg/interfaces/test/machine/00-lo.network @@ -0,0 +1,18 @@ +# generated by os-installer +[Match] +Name=lo + +[Address] +Address=127.0.0.1/8 + +[Address] +Address=10.0.17.2/32 + +[Address] +Address=185.1.2.3/32 + +[Address] +Address=100.127.129.1/32 + +[Address] +Address=2001::4/128 diff --git a/pkg/interfaces/test/machine/10-lan0.link b/pkg/interfaces/test/machine/10-lan0.link new file mode 100644 index 0000000..c98eb3d --- /dev/null +++ b/pkg/interfaces/test/machine/10-lan0.link @@ -0,0 +1,8 @@ +# generated by os-installer +[Match] +PermanentMACAddress=00:03:00:11:11:01 + +[Link] +Name=lan0 +NamePolicy= +MTUBytes=9000 diff --git a/pkg/interfaces/test/machine/10-lan0.network b/pkg/interfaces/test/machine/10-lan0.network new file mode 100644 index 0000000..aba2532 --- /dev/null +++ b/pkg/interfaces/test/machine/10-lan0.network @@ -0,0 +1,6 @@ +# generated by os-installer +[Match] +Name=lan0 + +[Network] +IPv6AcceptRA=no diff --git a/pkg/interfaces/test/machine/11-lan1.link b/pkg/interfaces/test/machine/11-lan1.link new file mode 100644 index 0000000..2be7376 --- /dev/null +++ b/pkg/interfaces/test/machine/11-lan1.link @@ -0,0 +1,8 @@ +# generated by os-installer +[Match] +PermanentMACAddress=00:03:00:11:12:01 + +[Link] +Name=lan1 +NamePolicy= +MTUBytes=9000 diff --git a/pkg/interfaces/test/machine/11-lan1.network b/pkg/interfaces/test/machine/11-lan1.network new file mode 100644 index 0000000..284c8a3 --- /dev/null +++ b/pkg/interfaces/test/machine/11-lan1.network @@ -0,0 +1,6 @@ +# generated by os-installer +[Match] +Name=lan1 + +[Network] +IPv6AcceptRA=no diff --git a/pkg/network/network.go b/pkg/network/network.go new file mode 100644 index 0000000..8522f4f --- /dev/null +++ b/pkg/network/network.go @@ -0,0 +1,129 @@ +package network + +import ( + "fmt" + "net/netip" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/samber/lo" +) + +const ( + // mtuFirewall defines the value for MTU specific to the needs of a firewall. VXLAN requires higher MTU. + mtuFirewall = 9216 + // mtuMachine defines the value for MTU specific to the needs of a machine. + mtuMachine = 9000 +) + +type Network struct { + allocation *apiv2.MachineAllocation +} + +func New(allocation *apiv2.MachineAllocation) *Network { + return &Network{ + allocation: allocation, + } +} + +func (n *Network) MTU() int { + if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + return mtuFirewall + } + + return mtuMachine +} + +func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { + var ips []string + + if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + ips, err = loFirewallIps(n.allocation.Networks) + if err != nil { + return nil, err + } + } else { + ips, err = loMachineIps(n.allocation.Networks) + if err != nil { + return nil, err + } + } + + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + return nil, err + } + + cidrs = append(cidrs, fmt.Sprintf("%s/%d", addr.String(), addr.BitLen())) + } + + return +} + +func (n *Network) PrivatePrimaryIPs() ([]string, error) { + if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + for _, nw := range n.allocation.Networks { + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_UNDERLAY { + return nw.Ips, nil + } + } + + return nil, fmt.Errorf("no private primary ip present in network allocation") + } + + for _, nw := range n.allocation.Networks { + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD { + return nw.Ips, nil + } + } + + for _, nw := range n.allocation.Networks { + if nw.Project == nil { + continue + } + + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED && *nw.Project == n.allocation.Project { + return nw.Ips, nil + } + } + + return nil, fmt.Errorf("no private primary ip present in network allocation") +} + +func (n *Network) VxlanIDs() (ids []uint64) { + if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + for _, nw := range n.allocation.Networks { + if nw.Vrf > 0 { + ids = append(ids, nw.Vrf) + } + } + } + + ids = lo.Uniq(ids) + + return +} + +func loFirewallIps(networks []*apiv2.MachineNetwork) (ips []string, err error) { + for _, nw := range networks { + switch nw.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_UNDERLAY: + ips = append(ips, nw.Ips...) + } + } + + return +} + +func loMachineIps(networks []*apiv2.MachineNetwork) (ips []string, err error) { + for _, nw := range networks { + switch nw.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_CHILD, apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: + ips = append(ips, nw.Ips...) + case apiv2.NetworkType_NETWORK_TYPE_EXTERNAL: + ips = append(ips, nw.Ips...) + } + } + + return +} From deb53cd44cdba2cfbfffd5628a104a40b833359e Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 15:32:46 +0100 Subject: [PATCH 010/102] Interfaces. --- pkg/interfaces/interfaces.go | 159 ++++++++++++++ pkg/interfaces/interfaces_test.go | 194 +++++++++++++++++- pkg/interfaces/templates/20-bridge.netdev.tpl | 10 - .../templates/20-bridge.network.tpl | 14 -- pkg/interfaces/templates/30-svi.netdev.tpl | 8 - pkg/interfaces/templates/30-svi.network.tpl | 13 -- pkg/interfaces/templates/30-vrf.netdev.tpl | 8 - pkg/interfaces/templates/30-vrf.network.tpl | 4 - pkg/interfaces/templates/30-vxlan.netdev.tpl | 12 -- pkg/interfaces/templates/30-vxlan.network.tpl | 14 -- pkg/interfaces/templates/bridge.netdev.tpl | 9 + pkg/interfaces/templates/bridge.network.tpl | 13 ++ pkg/interfaces/templates/svi.netdev.tpl | 7 + pkg/interfaces/templates/svi.network.tpl | 12 ++ pkg/interfaces/templates/vrf.netdev.tpl | 7 + pkg/interfaces/templates/vrf.network.tpl | 3 + pkg/interfaces/templates/vxlan.netdev.tpl | 11 + pkg/interfaces/templates/vxlan.network.tpl | 13 ++ pkg/interfaces/test/firewall/20-bridge.netdev | 3 +- .../test/firewall/20-bridge.network | 3 +- .../test/firewall/30-svi-3981.netdev | 2 +- .../test/firewall/30-svi-3981.network | 2 +- .../test/firewall/30-vrf-3981.netdev | 2 +- .../test/firewall/30-vrf-3981.network | 2 +- .../test/firewall/30-vxlan-3981.netdev | 2 +- .../test/firewall/30-vxlan-3981.network | 2 +- .../test/firewall/31-svi-3982.netdev | 2 +- .../test/firewall/31-svi-3982.network | 2 +- .../test/firewall/31-vrf-3982.netdev | 2 +- .../test/firewall/31-vrf-3982.network | 2 +- .../test/firewall/31-vxlan-3982.netdev | 2 +- .../test/firewall/31-vxlan-3982.network | 2 +- .../test/firewall/32-svi-104009.netdev | 2 +- .../test/firewall/32-svi-104009.network | 2 +- .../test/firewall/32-vrf-104009.netdev | 2 +- .../test/firewall/32-vrf-104009.network | 2 +- .../test/firewall/32-vxlan-104009.netdev | 2 +- .../test/firewall/32-vxlan-104009.network | 2 +- .../test/firewall/33-svi-104010.netdev | 2 +- .../test/firewall/33-svi-104010.network | 2 +- .../test/firewall/33-vrf-104010.netdev | 2 +- .../test/firewall/33-vrf-104010.network | 2 +- .../test/firewall/33-vxlan-104010.netdev | 2 +- .../test/firewall/33-vxlan-104010.network | 2 +- pkg/network/network.go | 53 ++++- pkg/nftables/nftables.go | 1 + 46 files changed, 500 insertions(+), 119 deletions(-) delete mode 100644 pkg/interfaces/templates/20-bridge.netdev.tpl delete mode 100644 pkg/interfaces/templates/20-bridge.network.tpl delete mode 100644 pkg/interfaces/templates/30-svi.netdev.tpl delete mode 100644 pkg/interfaces/templates/30-svi.network.tpl delete mode 100644 pkg/interfaces/templates/30-vrf.netdev.tpl delete mode 100644 pkg/interfaces/templates/30-vrf.network.tpl delete mode 100644 pkg/interfaces/templates/30-vxlan.netdev.tpl delete mode 100644 pkg/interfaces/templates/30-vxlan.network.tpl create mode 100644 pkg/interfaces/templates/bridge.netdev.tpl create mode 100644 pkg/interfaces/templates/bridge.network.tpl create mode 100644 pkg/interfaces/templates/svi.netdev.tpl create mode 100644 pkg/interfaces/templates/svi.network.tpl create mode 100644 pkg/interfaces/templates/vrf.netdev.tpl create mode 100644 pkg/interfaces/templates/vrf.network.tpl create mode 100644 pkg/interfaces/templates/vxlan.netdev.tpl create mode 100644 pkg/interfaces/templates/vxlan.network.tpl diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index a415937..cd9abbf 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -21,6 +21,16 @@ const ( lanLink interfaceKind = "lan.link.tpl" lanNetwork interfaceKind = "lan.network.tpl" + bridgeNetwork interfaceKind = "bridge.network.tpl" + bridgeNetdev interfaceKind = "bridge.netdev.tpl" + + sviNetwork interfaceKind = "svi.network.tpl" + sviNetdev interfaceKind = "svi.netdev.tpl" + vrfNetwork interfaceKind = "vrf.network.tpl" + vrfNetdev interfaceKind = "vrf.netdev.tpl" + vxlanNetwork interfaceKind = "vxlan.network.tpl" + vxlanNetdev interfaceKind = "vxlan.netdev.tpl" + comment = "generated by os-installer" ) @@ -56,6 +66,21 @@ type ( VxlanIDs []uint64 Index int } + + bridgeNetworkData struct { + Comment string + EVPNIfaces []network.EvpnIface + } + + bridgeNetdevData struct { + Comment string + } + + evpnData struct { + Comment string + EVPNIface network.EvpnIface + UnderlayIP string + } ) func ConfigureInterfaces(ctx context.Context, cfg *Config) error { @@ -67,6 +92,18 @@ func ConfigureInterfaces(ctx context.Context, cfg *Config) error { return fmt.Errorf("error configuring lan interfaces: %w", err) } + if cfg.Network.IsMachine() { + return nil + } + + if err := configureBridges(ctx, cfg); err != nil { + return fmt.Errorf("error configuring network bridges: %w", err) + } + + if err := configureEVPN(ctx, cfg); err != nil { + return fmt.Errorf("error configuring evnps: %w", err) + } + return nil } @@ -144,6 +181,128 @@ func configureLanInterfaces(ctx context.Context, cfg *Config) error { return nil } +func configureBridges(ctx context.Context, cfg *Config) error { + const offset = 20 + + ifaces, err := cfg.Network.EVPNIfaces() + if err != nil { + return fmt.Errorf("unable to get evpn interfaces: %w", err) + } + + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: bridgeNetwork.mustReadTemplate(), + Data: bridgeNetworkData{ + Comment: comment, + EVPNIfaces: ifaces, + }, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, "20-bridge.network")) + if err != nil { + return fmt.Errorf("unable to render bridge network config: %w", err) + } + + r, err = renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: bridgeNetdev.mustReadTemplate(), + Data: bridgeNetdevData{ + Comment: comment, + }, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, "20-bridge.netdev")) + if err != nil { + return fmt.Errorf("unable to render bridge netdev config: %w", err) + } + + return nil +} + +func configureEVPN(ctx context.Context, cfg *Config) error { + const offset = 30 + + ifaces, err := cfg.Network.EVPNIfaces() + if err != nil { + return fmt.Errorf("unable to get evpn interfaces: %w", err) + } + + underlayIPs, err := cfg.Network.PrivatePrimaryIPs() + if err != nil { + return err + } + + for idx, iface := range ifaces { + for _, component := range []struct { + ikindnetwork interfaceKind + ikindnetdev interfaceKind + name string + }{ + { + ikindnetwork: sviNetwork, + ikindnetdev: sviNetdev, + name: "svi", + }, + { + ikindnetwork: vrfNetwork, + ikindnetdev: vrfNetdev, + name: "vrf", + }, + { + ikindnetwork: vxlanNetwork, + ikindnetdev: vxlanNetdev, + name: "vxlan", + }, + } { + data := evpnData{ + Comment: comment, + EVPNIface: iface, + UnderlayIP: underlayIPs[0], + } + + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: component.ikindnetwork.mustReadTemplate(), + Data: data, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, fmt.Sprintf("%d-%s-%d.network", offset+idx, component.name, iface.VrfID))) + if err != nil { + return fmt.Errorf("unable to render %s network config: %w", component.name, err) + } + + r, err = renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: component.ikindnetdev.mustReadTemplate(), + Data: data, + Fs: cfg.fs, + }) + if err != nil { + return err + } + + _, err = r.Render(ctx, path.Join(systemdNetworkPath, fmt.Sprintf("%d-%s-%d.netdev", offset+idx, component.name, iface.VrfID))) + if err != nil { + return fmt.Errorf("unable to render %s netdev config: %w", component.name, err) + } + } + } + + return nil +} + func (i interfaceKind) mustReadTemplate() string { tpl, err := interfaceTemplates.ReadFile(path.Join("templates", string(i))) if err != nil { diff --git a/pkg/interfaces/interfaces_test.go b/pkg/interfaces/interfaces_test.go index a5aa35d..b4ea279 100644 --- a/pkg/interfaces/interfaces_test.go +++ b/pkg/interfaces/interfaces_test.go @@ -65,11 +65,13 @@ func Test_configureLoopbackInterface(t *testing.T) { Networks: []*apiv2.MachineNetwork{ { NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.17.2"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, }, { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"185.1.2.3"}, + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, }, { NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, @@ -175,12 +177,12 @@ func Test_configureLanInterface(t *testing.T) { Networks: []*apiv2.MachineNetwork{ { NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.17.2"}, + Ips: []string{"10.0.16.2"}, Vrf: 3981, }, { NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, - Ips: []string{"10.0.18.1"}, + Ips: []string{"10.0.18.2"}, Vrf: 3982, }, { @@ -251,6 +253,188 @@ func Test_configureLanInterface(t *testing.T) { } } +func Test_configureBridges(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + wantFilePaths []string + wantErr error + }{ + { + name: "render firewall", + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Vrf: 104009, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + }, + wantFilePaths: []string{ + "firewall/20-bridge.network", + "firewall/20-bridge.netdev", + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotErr := configureBridges(t.Context(), &Config{ + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + }) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + for _, name := range tt.wantFilePaths { + content, err := fs.ReadFile(path.Join(systemdNetworkPath, path.Base(name))) + require.NoError(t, err) + + if diff := cmp.Diff(mustReadExpected(name), string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + } + }) + } +} + +func Test_configureEVPN(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + wantFilePaths []string + wantErr error + }{ + { + name: "render firewall", + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Vrf: 104009, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + }, + wantFilePaths: []string{ + "firewall/30-svi-3981.network", + "firewall/30-svi-3981.netdev", + "firewall/31-svi-3982.network", + "firewall/31-svi-3982.netdev", + "firewall/32-svi-104009.network", + "firewall/32-svi-104009.netdev", + "firewall/33-svi-104010.network", + "firewall/33-svi-104010.netdev", + + "firewall/30-vrf-3981.network", + "firewall/30-vrf-3981.netdev", + "firewall/31-vrf-3982.network", + "firewall/31-vrf-3982.netdev", + "firewall/32-vrf-104009.network", + "firewall/32-vrf-104009.netdev", + "firewall/33-vrf-104010.network", + "firewall/33-vrf-104010.netdev", + + "firewall/30-vxlan-3981.network", + "firewall/30-vxlan-3981.netdev", + "firewall/31-vxlan-3982.network", + "firewall/31-vxlan-3982.netdev", + "firewall/32-vxlan-104009.network", + "firewall/32-vxlan-104009.netdev", + "firewall/33-vxlan-104010.network", + "firewall/33-vxlan-104010.netdev", + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + gotErr := configureEVPN(t.Context(), &Config{ + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + }) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + for _, name := range tt.wantFilePaths { + content, err := fs.ReadFile(path.Join(systemdNetworkPath, path.Base(name))) + require.NoError(t, err) + + if diff := cmp.Diff(mustReadExpected(name), string(content)); diff != "" { + t.Errorf("diff = %s", diff) + } + } + }) + } +} + func mustReadExpected(name string) string { tpl, err := expectedInterfaceFiles.ReadFile(path.Join("test", name)) if err != nil { diff --git a/pkg/interfaces/templates/20-bridge.netdev.tpl b/pkg/interfaces/templates/20-bridge.netdev.tpl deleted file mode 100644 index 2fef44c..0000000 --- a/pkg/interfaces/templates/20-bridge.netdev.tpl +++ /dev/null @@ -1,10 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} -{{ .Comment }} -[NetDev] -Name=bridge -Kind=bridge -MTUBytes=9000 - -[Bridge] -DefaultPVID=none -VLANFiltering=yes diff --git a/pkg/interfaces/templates/20-bridge.network.tpl b/pkg/interfaces/templates/20-bridge.network.tpl deleted file mode 100644 index 360b48c..0000000 --- a/pkg/interfaces/templates/20-bridge.network.tpl +++ /dev/null @@ -1,14 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} -{{ .Comment }} -[Match] -Name=bridge - -[Network] -{{- range .EVPNIfaces }} -VLAN=vlan{{ .VRF.ID }} -{{- end }} -{{- range .EVPNIfaces }} - -[BridgeVLAN] -VLAN={{ .SVI.VLANID }} -{{- end }} \ No newline at end of file diff --git a/pkg/interfaces/templates/30-svi.netdev.tpl b/pkg/interfaces/templates/30-svi.netdev.tpl deleted file mode 100644 index 6aa6826..0000000 --- a/pkg/interfaces/templates/30-svi.netdev.tpl +++ /dev/null @@ -1,8 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .SVI.Comment }} -[NetDev] -Name=vlan{{ .VRF.ID }} -Kind=vlan - -[VLAN] -Id={{ .SVI.VLANID }} diff --git a/pkg/interfaces/templates/30-svi.network.tpl b/pkg/interfaces/templates/30-svi.network.tpl deleted file mode 100644 index 0ef4c10..0000000 --- a/pkg/interfaces/templates/30-svi.network.tpl +++ /dev/null @@ -1,13 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .SVI.Comment }} -[Match] -Name=vlan{{ .VRF.ID }} - -[Link] -MTUBytes=9000 - -[Network] -VRF=vrf{{ .VRF.ID }} -{{- range .SVI.Addresses }} -Address={{ . }} -{{- end }} diff --git a/pkg/interfaces/templates/30-vrf.netdev.tpl b/pkg/interfaces/templates/30-vrf.netdev.tpl deleted file mode 100644 index 282a910..0000000 --- a/pkg/interfaces/templates/30-vrf.netdev.tpl +++ /dev/null @@ -1,8 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VRF.Comment }} -[NetDev] -Name=vrf{{ .VRF.ID }} -Kind=vrf - -[VRF] -Table={{ .VRF.Table }} \ No newline at end of file diff --git a/pkg/interfaces/templates/30-vrf.network.tpl b/pkg/interfaces/templates/30-vrf.network.tpl deleted file mode 100644 index a7628dc..0000000 --- a/pkg/interfaces/templates/30-vrf.network.tpl +++ /dev/null @@ -1,4 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VRF.Comment }} -[Match] -Name=vrf{{ .VRF.ID }} \ No newline at end of file diff --git a/pkg/interfaces/templates/30-vxlan.netdev.tpl b/pkg/interfaces/templates/30-vxlan.netdev.tpl deleted file mode 100644 index 68ebf9b..0000000 --- a/pkg/interfaces/templates/30-vxlan.netdev.tpl +++ /dev/null @@ -1,12 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VXLAN.Comment }} -[NetDev] -Name=vni{{ .VXLAN.ID }} -Kind=vxlan - -[VXLAN] -VNI={{ .VXLAN.ID }} -Local={{ .VXLAN.TunnelIP }} -UDPChecksum=true -MacLearning=false -DestinationPort=4789 diff --git a/pkg/interfaces/templates/30-vxlan.network.tpl b/pkg/interfaces/templates/30-vxlan.network.tpl deleted file mode 100644 index a49f111..0000000 --- a/pkg/interfaces/templates/30-vxlan.network.tpl +++ /dev/null @@ -1,14 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VXLAN.Comment }} -[Match] -Name=vni{{ .VXLAN.ID }} - -[Link] -MTUBytes=9000 - -[Network] -Bridge=bridge - -[BridgeVLAN] -PVID={{ .SVI.VLANID }} -EgressUntagged={{ .SVI.VLANID }} diff --git a/pkg/interfaces/templates/bridge.netdev.tpl b/pkg/interfaces/templates/bridge.netdev.tpl new file mode 100644 index 0000000..bb7bf8a --- /dev/null +++ b/pkg/interfaces/templates/bridge.netdev.tpl @@ -0,0 +1,9 @@ +# {{ .Comment }} +[NetDev] +Name=bridge +Kind=bridge +MTUBytes=9000 + +[Bridge] +DefaultPVID=none +VLANFiltering=yes diff --git a/pkg/interfaces/templates/bridge.network.tpl b/pkg/interfaces/templates/bridge.network.tpl new file mode 100644 index 0000000..66f2eca --- /dev/null +++ b/pkg/interfaces/templates/bridge.network.tpl @@ -0,0 +1,13 @@ +# {{ .Comment }} +[Match] +Name=bridge + +[Network] +{{- range .EVPNIfaces }} +VLAN=vlan{{ .VrfID }} +{{- end }} +{{- range .EVPNIfaces }} + +[BridgeVLAN] +VLAN={{ .VlanID }} +{{- end }} diff --git a/pkg/interfaces/templates/svi.netdev.tpl b/pkg/interfaces/templates/svi.netdev.tpl new file mode 100644 index 0000000..235494e --- /dev/null +++ b/pkg/interfaces/templates/svi.netdev.tpl @@ -0,0 +1,7 @@ +# {{ .Comment }} +[NetDev] +Name=vlan{{ .EVPNIface.VrfID }} +Kind=vlan + +[VLAN] +Id={{ .EVPNIface.VlanID }} diff --git a/pkg/interfaces/templates/svi.network.tpl b/pkg/interfaces/templates/svi.network.tpl new file mode 100644 index 0000000..3ad20ca --- /dev/null +++ b/pkg/interfaces/templates/svi.network.tpl @@ -0,0 +1,12 @@ +# {{ .Comment }} +[Match] +Name=vlan{{ .EVPNIface.VrfID }} + +[Link] +MTUBytes=9000 + +[Network] +VRF=vrf{{ .EVPNIface.VrfID }} +{{- range .EVPNIface.CIDRs }} +Address={{ . }} +{{- end }} diff --git a/pkg/interfaces/templates/vrf.netdev.tpl b/pkg/interfaces/templates/vrf.netdev.tpl new file mode 100644 index 0000000..12d5007 --- /dev/null +++ b/pkg/interfaces/templates/vrf.netdev.tpl @@ -0,0 +1,7 @@ +# {{ .Comment }} +[NetDev] +Name=vrf{{ .EVPNIface.VrfID }} +Kind=vrf + +[VRF] +Table={{ .EVPNIface.VlanID }} diff --git a/pkg/interfaces/templates/vrf.network.tpl b/pkg/interfaces/templates/vrf.network.tpl new file mode 100644 index 0000000..3c08199 --- /dev/null +++ b/pkg/interfaces/templates/vrf.network.tpl @@ -0,0 +1,3 @@ +# {{ .Comment }} +[Match] +Name=vrf{{ .EVPNIface.VrfID }} diff --git a/pkg/interfaces/templates/vxlan.netdev.tpl b/pkg/interfaces/templates/vxlan.netdev.tpl new file mode 100644 index 0000000..d5670d1 --- /dev/null +++ b/pkg/interfaces/templates/vxlan.netdev.tpl @@ -0,0 +1,11 @@ +# {{ .Comment }} +[NetDev] +Name=vni{{ .EVPNIface.VrfID }} +Kind=vxlan + +[VXLAN] +VNI={{ .EVPNIface.VrfID }} +Local={{ .UnderlayIP }} +UDPChecksum=true +MacLearning=false +DestinationPort=4789 diff --git a/pkg/interfaces/templates/vxlan.network.tpl b/pkg/interfaces/templates/vxlan.network.tpl new file mode 100644 index 0000000..d6135e6 --- /dev/null +++ b/pkg/interfaces/templates/vxlan.network.tpl @@ -0,0 +1,13 @@ +# {{ .Comment }} +[Match] +Name=vni{{ .EVPNIface.VrfID }} + +[Link] +MTUBytes=9000 + +[Network] +Bridge=bridge + +[BridgeVLAN] +PVID={{ .EVPNIface.VlanID }} +EgressUntagged={{ .EVPNIface.VlanID }} diff --git a/pkg/interfaces/test/firewall/20-bridge.netdev b/pkg/interfaces/test/firewall/20-bridge.netdev index 186b556..2723c13 100644 --- a/pkg/interfaces/test/firewall/20-bridge.netdev +++ b/pkg/interfaces/test/firewall/20-bridge.netdev @@ -1,5 +1,4 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. +# generated by os-installer [NetDev] Name=bridge Kind=bridge diff --git a/pkg/interfaces/test/firewall/20-bridge.network b/pkg/interfaces/test/firewall/20-bridge.network index cfac429..5a6e1e9 100644 --- a/pkg/interfaces/test/firewall/20-bridge.network +++ b/pkg/interfaces/test/firewall/20-bridge.network @@ -1,5 +1,4 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. +# generated by os-installer [Match] Name=bridge diff --git a/pkg/interfaces/test/firewall/30-svi-3981.netdev b/pkg/interfaces/test/firewall/30-svi-3981.netdev index 1c4cb0b..97ccc24 100644 --- a/pkg/interfaces/test/firewall/30-svi-3981.netdev +++ b/pkg/interfaces/test/firewall/30-svi-3981.netdev @@ -1,4 +1,4 @@ -# svi (networkid: bc830818-2df1-4904-8c40-4322296d393d) +# generated by os-installer [NetDev] Name=vlan3981 Kind=vlan diff --git a/pkg/interfaces/test/firewall/30-svi-3981.network b/pkg/interfaces/test/firewall/30-svi-3981.network index ff73189..e41cbfe 100644 --- a/pkg/interfaces/test/firewall/30-svi-3981.network +++ b/pkg/interfaces/test/firewall/30-svi-3981.network @@ -1,4 +1,4 @@ -# svi (networkid: bc830818-2df1-4904-8c40-4322296d393d) +# generated by os-installer [Match] Name=vlan3981 diff --git a/pkg/interfaces/test/firewall/30-vrf-3981.netdev b/pkg/interfaces/test/firewall/30-vrf-3981.netdev index 08779cf..4e0579f 100644 --- a/pkg/interfaces/test/firewall/30-vrf-3981.netdev +++ b/pkg/interfaces/test/firewall/30-vrf-3981.netdev @@ -1,4 +1,4 @@ -# vrf (networkid: bc830818-2df1-4904-8c40-4322296d393d) +# generated by os-installer [NetDev] Name=vrf3981 Kind=vrf diff --git a/pkg/interfaces/test/firewall/30-vrf-3981.network b/pkg/interfaces/test/firewall/30-vrf-3981.network index ec4a8cc..24d9edb 100644 --- a/pkg/interfaces/test/firewall/30-vrf-3981.network +++ b/pkg/interfaces/test/firewall/30-vrf-3981.network @@ -1,3 +1,3 @@ -# vrf (networkid: bc830818-2df1-4904-8c40-4322296d393d) +# generated by os-installer [Match] Name=vrf3981 diff --git a/pkg/interfaces/test/firewall/30-vxlan-3981.netdev b/pkg/interfaces/test/firewall/30-vxlan-3981.netdev index 4faac86..1f92080 100644 --- a/pkg/interfaces/test/firewall/30-vxlan-3981.netdev +++ b/pkg/interfaces/test/firewall/30-vxlan-3981.netdev @@ -1,4 +1,4 @@ -# vxlan (networkid: bc830818-2df1-4904-8c40-4322296d393d) +# generated by os-installer [NetDev] Name=vni3981 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/30-vxlan-3981.network b/pkg/interfaces/test/firewall/30-vxlan-3981.network index 0a51049..be6ec17 100644 --- a/pkg/interfaces/test/firewall/30-vxlan-3981.network +++ b/pkg/interfaces/test/firewall/30-vxlan-3981.network @@ -1,4 +1,4 @@ -# vxlan (networkid: bc830818-2df1-4904-8c40-4322296d393d) +# generated by os-installer [Match] Name=vni3981 diff --git a/pkg/interfaces/test/firewall/31-svi-3982.netdev b/pkg/interfaces/test/firewall/31-svi-3982.netdev index c82b275..3d44d50 100644 --- a/pkg/interfaces/test/firewall/31-svi-3982.netdev +++ b/pkg/interfaces/test/firewall/31-svi-3982.netdev @@ -1,4 +1,4 @@ -# svi (networkid: storage-net) +# generated by os-installer [NetDev] Name=vlan3982 Kind=vlan diff --git a/pkg/interfaces/test/firewall/31-svi-3982.network b/pkg/interfaces/test/firewall/31-svi-3982.network index 855cf4d..7777f59 100644 --- a/pkg/interfaces/test/firewall/31-svi-3982.network +++ b/pkg/interfaces/test/firewall/31-svi-3982.network @@ -1,4 +1,4 @@ -# svi (networkid: storage-net) +# generated by os-installer [Match] Name=vlan3982 diff --git a/pkg/interfaces/test/firewall/31-vrf-3982.netdev b/pkg/interfaces/test/firewall/31-vrf-3982.netdev index 0700dca..76451be 100644 --- a/pkg/interfaces/test/firewall/31-vrf-3982.netdev +++ b/pkg/interfaces/test/firewall/31-vrf-3982.netdev @@ -1,4 +1,4 @@ -# vrf (networkid: storage-net) +# generated by os-installer [NetDev] Name=vrf3982 Kind=vrf diff --git a/pkg/interfaces/test/firewall/31-vrf-3982.network b/pkg/interfaces/test/firewall/31-vrf-3982.network index 32646c7..1e08850 100644 --- a/pkg/interfaces/test/firewall/31-vrf-3982.network +++ b/pkg/interfaces/test/firewall/31-vrf-3982.network @@ -1,3 +1,3 @@ -# vrf (networkid: storage-net) +# generated by os-installer [Match] Name=vrf3982 diff --git a/pkg/interfaces/test/firewall/31-vxlan-3982.netdev b/pkg/interfaces/test/firewall/31-vxlan-3982.netdev index e909e12..2952e24 100644 --- a/pkg/interfaces/test/firewall/31-vxlan-3982.netdev +++ b/pkg/interfaces/test/firewall/31-vxlan-3982.netdev @@ -1,4 +1,4 @@ -# vxlan (networkid: storage-net) +# generated by os-installer [NetDev] Name=vni3982 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/31-vxlan-3982.network b/pkg/interfaces/test/firewall/31-vxlan-3982.network index 204c6b3..6b07018 100644 --- a/pkg/interfaces/test/firewall/31-vxlan-3982.network +++ b/pkg/interfaces/test/firewall/31-vxlan-3982.network @@ -1,4 +1,4 @@ -# vxlan (networkid: storage-net) +# generated by os-installer [Match] Name=vni3982 diff --git a/pkg/interfaces/test/firewall/32-svi-104009.netdev b/pkg/interfaces/test/firewall/32-svi-104009.netdev index f941ea3..636a93e 100644 --- a/pkg/interfaces/test/firewall/32-svi-104009.netdev +++ b/pkg/interfaces/test/firewall/32-svi-104009.netdev @@ -1,4 +1,4 @@ -# svi (networkid: internet-vagrant-lab) +# generated by os-installer [NetDev] Name=vlan104009 Kind=vlan diff --git a/pkg/interfaces/test/firewall/32-svi-104009.network b/pkg/interfaces/test/firewall/32-svi-104009.network index e8e16d8..9c7a6ec 100644 --- a/pkg/interfaces/test/firewall/32-svi-104009.network +++ b/pkg/interfaces/test/firewall/32-svi-104009.network @@ -1,4 +1,4 @@ -# svi (networkid: internet-vagrant-lab) +# generated by os-installer [Match] Name=vlan104009 diff --git a/pkg/interfaces/test/firewall/32-vrf-104009.netdev b/pkg/interfaces/test/firewall/32-vrf-104009.netdev index 4a3716e..3dc718c 100644 --- a/pkg/interfaces/test/firewall/32-vrf-104009.netdev +++ b/pkg/interfaces/test/firewall/32-vrf-104009.netdev @@ -1,4 +1,4 @@ -# vrf (networkid: internet-vagrant-lab) +# generated by os-installer [NetDev] Name=vrf104009 Kind=vrf diff --git a/pkg/interfaces/test/firewall/32-vrf-104009.network b/pkg/interfaces/test/firewall/32-vrf-104009.network index 66fc9c8..886c7e3 100644 --- a/pkg/interfaces/test/firewall/32-vrf-104009.network +++ b/pkg/interfaces/test/firewall/32-vrf-104009.network @@ -1,3 +1,3 @@ -# vrf (networkid: internet-vagrant-lab) +# generated by os-installer [Match] Name=vrf104009 diff --git a/pkg/interfaces/test/firewall/32-vxlan-104009.netdev b/pkg/interfaces/test/firewall/32-vxlan-104009.netdev index 43ed598..b55af06 100644 --- a/pkg/interfaces/test/firewall/32-vxlan-104009.netdev +++ b/pkg/interfaces/test/firewall/32-vxlan-104009.netdev @@ -1,4 +1,4 @@ -# vxlan (networkid: internet-vagrant-lab) +# generated by os-installer [NetDev] Name=vni104009 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/32-vxlan-104009.network b/pkg/interfaces/test/firewall/32-vxlan-104009.network index ea24f09..25c939c 100644 --- a/pkg/interfaces/test/firewall/32-vxlan-104009.network +++ b/pkg/interfaces/test/firewall/32-vxlan-104009.network @@ -1,4 +1,4 @@ -# vxlan (networkid: internet-vagrant-lab) +# generated by os-installer [Match] Name=vni104009 diff --git a/pkg/interfaces/test/firewall/33-svi-104010.netdev b/pkg/interfaces/test/firewall/33-svi-104010.netdev index d1e68a3..f825084 100644 --- a/pkg/interfaces/test/firewall/33-svi-104010.netdev +++ b/pkg/interfaces/test/firewall/33-svi-104010.netdev @@ -1,4 +1,4 @@ -# svi (networkid: mpls-nbg-w8101-test) +# generated by os-installer [NetDev] Name=vlan104010 Kind=vlan diff --git a/pkg/interfaces/test/firewall/33-svi-104010.network b/pkg/interfaces/test/firewall/33-svi-104010.network index 11165a4..f5a199c 100644 --- a/pkg/interfaces/test/firewall/33-svi-104010.network +++ b/pkg/interfaces/test/firewall/33-svi-104010.network @@ -1,4 +1,4 @@ -# svi (networkid: mpls-nbg-w8101-test) +# generated by os-installer [Match] Name=vlan104010 diff --git a/pkg/interfaces/test/firewall/33-vrf-104010.netdev b/pkg/interfaces/test/firewall/33-vrf-104010.netdev index 795c7c4..1ababb5 100644 --- a/pkg/interfaces/test/firewall/33-vrf-104010.netdev +++ b/pkg/interfaces/test/firewall/33-vrf-104010.netdev @@ -1,4 +1,4 @@ -# vrf (networkid: mpls-nbg-w8101-test) +# generated by os-installer [NetDev] Name=vrf104010 Kind=vrf diff --git a/pkg/interfaces/test/firewall/33-vrf-104010.network b/pkg/interfaces/test/firewall/33-vrf-104010.network index 2765bba..46b0ee0 100644 --- a/pkg/interfaces/test/firewall/33-vrf-104010.network +++ b/pkg/interfaces/test/firewall/33-vrf-104010.network @@ -1,3 +1,3 @@ -# vrf (networkid: mpls-nbg-w8101-test) +# generated by os-installer [Match] Name=vrf104010 diff --git a/pkg/interfaces/test/firewall/33-vxlan-104010.netdev b/pkg/interfaces/test/firewall/33-vxlan-104010.netdev index 55ac87b..c5ea341 100644 --- a/pkg/interfaces/test/firewall/33-vxlan-104010.netdev +++ b/pkg/interfaces/test/firewall/33-vxlan-104010.netdev @@ -1,4 +1,4 @@ -# vxlan (networkid: mpls-nbg-w8101-test) +# generated by os-installer [NetDev] Name=vni104010 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/33-vxlan-104010.network b/pkg/interfaces/test/firewall/33-vxlan-104010.network index fff9745..3429518 100644 --- a/pkg/interfaces/test/firewall/33-vxlan-104010.network +++ b/pkg/interfaces/test/firewall/33-vxlan-104010.network @@ -1,4 +1,4 @@ -# vxlan (networkid: mpls-nbg-w8101-test) +# generated by os-installer [Match] Name=vni104010 diff --git a/pkg/network/network.go b/pkg/network/network.go index 8522f4f..a292ae7 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -15,9 +15,17 @@ const ( mtuMachine = 9000 ) -type Network struct { - allocation *apiv2.MachineAllocation -} +type ( + Network struct { + allocation *apiv2.MachineAllocation + } + + EvpnIface struct { + CIDRs []string + VlanID int + VrfID uint64 + } +) func New(allocation *apiv2.MachineAllocation) *Network { return &Network{ @@ -33,6 +41,10 @@ func (n *Network) MTU() int { return mtuMachine } +func (n *Network) IsMachine() bool { + return n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE +} + func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { var ips []string @@ -104,6 +116,41 @@ func (n *Network) VxlanIDs() (ids []uint64) { return } +func (n *Network) EVPNIfaces() (ifaces []EvpnIface, err error) { + if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE { + return nil, fmt.Errorf("no evpn interfaces supported on machines") + } + + const vlanOffset = 1000 + + for i, nw := range n.allocation.Networks { + if nw.Vrf > 0 { + var cidrs []string + + for _, ip := range nw.Ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + return nil, err + } + + cidrs = append(cidrs, fmt.Sprintf("%s/%d", addr.String(), addr.BitLen())) + } + + ifaces = append(ifaces, EvpnIface{ + CIDRs: cidrs, + VlanID: vlanOffset + i, + VrfID: nw.Vrf, + }) + } + } + + ifaces = lo.UniqBy(ifaces, func(iface EvpnIface) uint64 { + return iface.VrfID + }) + + return +} + func loFirewallIps(networks []*apiv2.MachineNetwork) (ips []string, err error) { for _, nw := range networks { switch nw.NetworkType { diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index e69de29..7027fe9 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -0,0 +1 @@ +package nftables From 9cfeaf4c8536012d300f3d7b4efe7999c56cf66e Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 15:35:29 +0100 Subject: [PATCH 011/102] Less. --- pkg/interfaces/interfaces_test.go | 254 +++++++++--------------------- 1 file changed, 72 insertions(+), 182 deletions(-) diff --git a/pkg/interfaces/interfaces_test.go b/pkg/interfaces/interfaces_test.go index b4ea279..421488f 100644 --- a/pkg/interfaces/interfaces_test.go +++ b/pkg/interfaces/interfaces_test.go @@ -19,6 +19,66 @@ import ( var ( //go:embed test expectedInterfaceFiles embed.FS + + machineAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.17.2"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + }, + } + + firewallAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Vrf: 104009, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + }, + { + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } ) func Test_configureLoopbackInterface(t *testing.T) { @@ -29,64 +89,14 @@ func Test_configureLoopbackInterface(t *testing.T) { wantErr error }{ { - name: "render machine", - allocation: &apiv2.MachineAllocation{ - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, - Networks: []*apiv2.MachineNetwork{ - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.17.2"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"185.1.2.3"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"100.127.129.1"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, - Ips: []string{"10.1.0.1"}, - }, - }, - }, + name: "render machine", + allocation: machineAllocation, wantFilePath: "machine/00-lo.network", wantErr: nil, }, { - name: "render firewall", - allocation: &apiv2.MachineAllocation{ - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, - Networks: []*apiv2.MachineNetwork{ - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.16.2"}, - Vrf: 3981, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, - Ips: []string{"10.0.18.2"}, - Vrf: 3982, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"100.127.129.1"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, - Ips: []string{"10.1.0.1"}, - }, - }, - }, + name: "render firewall", + allocation: firewallAllocation, wantFilePath: "firewall/00-lo.network", wantErr: nil, }, @@ -128,32 +138,8 @@ func Test_configureLanInterface(t *testing.T) { wantErr error }{ { - name: "render machine", - allocation: &apiv2.MachineAllocation{ - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, - Networks: []*apiv2.MachineNetwork{ - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.17.2"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"185.1.2.3"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"100.127.129.1"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, - Ips: []string{"10.1.0.1"}, - }, - }, - }, + name: "render machine", + allocation: machineAllocation, nics: []*apiv2.MachineNic{ { Mac: "00:03:00:11:11:01", @@ -171,40 +157,8 @@ func Test_configureLanInterface(t *testing.T) { wantErr: nil, }, { - name: "render firewall", - allocation: &apiv2.MachineAllocation{ - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, - Networks: []*apiv2.MachineNetwork{ - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.16.2"}, - Vrf: 3981, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, - Ips: []string{"10.0.18.2"}, - Vrf: 3982, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"185.1.2.3"}, - Vrf: 104009, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, - Ips: []string{"10.1.0.1"}, - }, - }, - }, + name: "render firewall", + allocation: firewallAllocation, nics: []*apiv2.MachineNic{ { Mac: "00:03:00:11:11:01", @@ -261,40 +215,8 @@ func Test_configureBridges(t *testing.T) { wantErr error }{ { - name: "render firewall", - allocation: &apiv2.MachineAllocation{ - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, - Networks: []*apiv2.MachineNetwork{ - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.16.2"}, - Vrf: 3981, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, - Ips: []string{"10.0.18.2"}, - Vrf: 3982, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"185.1.2.3"}, - Vrf: 104009, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, - Ips: []string{"10.1.0.1"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, - }, - }, - }, + name: "render firewall", + allocation: firewallAllocation, wantFilePaths: []string{ "firewall/20-bridge.network", "firewall/20-bridge.netdev", @@ -340,40 +262,8 @@ func Test_configureEVPN(t *testing.T) { wantErr error }{ { - name: "render firewall", - allocation: &apiv2.MachineAllocation{ - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, - Networks: []*apiv2.MachineNetwork{ - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - Ips: []string{"10.0.16.2"}, - Vrf: 3981, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, - Ips: []string{"10.0.18.2"}, - Vrf: 3982, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"185.1.2.3"}, - Vrf: 104009, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, - Ips: []string{"10.1.0.1"}, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - }, - { - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, - }, - }, - }, + name: "render firewall", + allocation: firewallAllocation, wantFilePaths: []string{ "firewall/30-svi-3981.network", "firewall/30-svi-3981.netdev", From 614fb5e90e3544aa6205e382e2852c971264da9e Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 9 Mar 2026 16:05:02 +0100 Subject: [PATCH 012/102] =?UTF-8?q?Uffr=C3=A4ume=20und=20tsch=C3=BCss.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../testdata/networkd/firewall/00-lo.network | 9 - .../testdata/networkd/firewall/10-lan0.link | 9 - .../networkd/firewall/10-lan0.network | 11 - .../testdata/networkd/firewall/11-lan1.link | 9 - .../networkd/firewall/11-lan1.network | 11 - .../networkd/firewall/20-bridge.netdev | 10 - .../networkd/firewall/20-bridge.network | 22 -- .../networkd/firewall/30-svi-3981.netdev | 7 - .../networkd/firewall/30-svi-3981.network | 10 - .../networkd/firewall/30-vrf-3981.netdev | 7 - .../networkd/firewall/30-vrf-3981.network | 3 - .../networkd/firewall/30-vxlan-3981.netdev | 11 - .../networkd/firewall/30-vxlan-3981.network | 13 - .../networkd/firewall/31-svi-3982.netdev | 7 - .../networkd/firewall/31-svi-3982.network | 10 - .../networkd/firewall/31-vrf-3982.netdev | 7 - .../networkd/firewall/31-vrf-3982.network | 3 - .../networkd/firewall/31-vxlan-3982.netdev | 11 - .../networkd/firewall/31-vxlan-3982.network | 13 - .../networkd/firewall/32-svi-104009.netdev | 7 - .../networkd/firewall/32-svi-104009.network | 10 - .../networkd/firewall/32-vrf-104009.netdev | 7 - .../networkd/firewall/32-vrf-104009.network | 3 - .../networkd/firewall/32-vxlan-104009.netdev | 11 - .../networkd/firewall/32-vxlan-104009.network | 13 - .../networkd/firewall/33-svi-104010.netdev | 7 - .../networkd/firewall/33-svi-104010.network | 10 - .../networkd/firewall/33-vrf-104010.netdev | 7 - .../networkd/firewall/33-vrf-104010.network | 3 - .../networkd/firewall/33-vxlan-104010.netdev | 11 - .../networkd/firewall/33-vxlan-104010.network | 13 - .../testdata/networkd/machine/00-lo.network | 15 - .../testdata/networkd/machine/10-lan0.link | 9 - .../testdata/networkd/machine/10-lan0.network | 7 - .../testdata/networkd/machine/11-lan1.link | 9 - .../testdata/networkd/machine/11-lan1.network | 7 - old/network/tpl/networkd/00-lo.network.tpl | 12 - old/network/tpl/networkd/10-lan.link.tpl | 9 - old/network/tpl/networkd/10-lan.network.tpl | 10 - old/network/tpl/networkd/20-bridge.netdev.tpl | 10 - .../tpl/networkd/20-bridge.network.tpl | 14 - old/network/tpl/networkd/30-svi.netdev.tpl | 8 - old/network/tpl/networkd/30-svi.network.tpl | 13 - old/network/tpl/networkd/30-vrf.netdev.tpl | 8 - old/network/tpl/networkd/30-vrf.network.tpl | 4 - old/network/tpl/networkd/30-vxlan.netdev.tpl | 12 - old/network/tpl/networkd/30-vxlan.network.tpl | 14 - pkg/network/network.go | 24 ++ pkg/nftables/nftables.go | 362 ++++++++++++++++++ pkg/nftables/nftrules.tpl | 130 +++++++ 50 files changed, 516 insertions(+), 446 deletions(-) delete mode 100644 old/network/testdata/networkd/firewall/00-lo.network delete mode 100644 old/network/testdata/networkd/firewall/10-lan0.link delete mode 100644 old/network/testdata/networkd/firewall/10-lan0.network delete mode 100644 old/network/testdata/networkd/firewall/11-lan1.link delete mode 100644 old/network/testdata/networkd/firewall/11-lan1.network delete mode 100644 old/network/testdata/networkd/firewall/20-bridge.netdev delete mode 100644 old/network/testdata/networkd/firewall/20-bridge.network delete mode 100644 old/network/testdata/networkd/firewall/30-svi-3981.netdev delete mode 100644 old/network/testdata/networkd/firewall/30-svi-3981.network delete mode 100644 old/network/testdata/networkd/firewall/30-vrf-3981.netdev delete mode 100644 old/network/testdata/networkd/firewall/30-vrf-3981.network delete mode 100644 old/network/testdata/networkd/firewall/30-vxlan-3981.netdev delete mode 100644 old/network/testdata/networkd/firewall/30-vxlan-3981.network delete mode 100644 old/network/testdata/networkd/firewall/31-svi-3982.netdev delete mode 100644 old/network/testdata/networkd/firewall/31-svi-3982.network delete mode 100644 old/network/testdata/networkd/firewall/31-vrf-3982.netdev delete mode 100644 old/network/testdata/networkd/firewall/31-vrf-3982.network delete mode 100644 old/network/testdata/networkd/firewall/31-vxlan-3982.netdev delete mode 100644 old/network/testdata/networkd/firewall/31-vxlan-3982.network delete mode 100644 old/network/testdata/networkd/firewall/32-svi-104009.netdev delete mode 100644 old/network/testdata/networkd/firewall/32-svi-104009.network delete mode 100644 old/network/testdata/networkd/firewall/32-vrf-104009.netdev delete mode 100644 old/network/testdata/networkd/firewall/32-vrf-104009.network delete mode 100644 old/network/testdata/networkd/firewall/32-vxlan-104009.netdev delete mode 100644 old/network/testdata/networkd/firewall/32-vxlan-104009.network delete mode 100644 old/network/testdata/networkd/firewall/33-svi-104010.netdev delete mode 100644 old/network/testdata/networkd/firewall/33-svi-104010.network delete mode 100644 old/network/testdata/networkd/firewall/33-vrf-104010.netdev delete mode 100644 old/network/testdata/networkd/firewall/33-vrf-104010.network delete mode 100644 old/network/testdata/networkd/firewall/33-vxlan-104010.netdev delete mode 100644 old/network/testdata/networkd/firewall/33-vxlan-104010.network delete mode 100644 old/network/testdata/networkd/machine/00-lo.network delete mode 100644 old/network/testdata/networkd/machine/10-lan0.link delete mode 100644 old/network/testdata/networkd/machine/10-lan0.network delete mode 100644 old/network/testdata/networkd/machine/11-lan1.link delete mode 100644 old/network/testdata/networkd/machine/11-lan1.network delete mode 100644 old/network/tpl/networkd/00-lo.network.tpl delete mode 100644 old/network/tpl/networkd/10-lan.link.tpl delete mode 100644 old/network/tpl/networkd/10-lan.network.tpl delete mode 100644 old/network/tpl/networkd/20-bridge.netdev.tpl delete mode 100644 old/network/tpl/networkd/20-bridge.network.tpl delete mode 100644 old/network/tpl/networkd/30-svi.netdev.tpl delete mode 100644 old/network/tpl/networkd/30-svi.network.tpl delete mode 100644 old/network/tpl/networkd/30-vrf.netdev.tpl delete mode 100644 old/network/tpl/networkd/30-vrf.network.tpl delete mode 100644 old/network/tpl/networkd/30-vxlan.netdev.tpl delete mode 100644 old/network/tpl/networkd/30-vxlan.network.tpl create mode 100644 pkg/nftables/nftrules.tpl diff --git a/old/network/testdata/networkd/firewall/00-lo.network b/old/network/testdata/networkd/firewall/00-lo.network deleted file mode 100644 index 4cb3725..0000000 --- a/old/network/testdata/networkd/firewall/00-lo.network +++ /dev/null @@ -1,9 +0,0 @@ -# networkid: underlay-vagrant-lab -[Match] -Name=lo - -[Address] -Address=127.0.0.1/8 - -[Address] -Address=10.1.0.1/32 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/10-lan0.link b/old/network/testdata/networkd/firewall/10-lan0.link deleted file mode 100644 index 6b00713..0000000 --- a/old/network/testdata/networkd/firewall/10-lan0.link +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -PermanentMACAddress=00:03:00:11:11:01 - -[Link] -Name=lan0 -NamePolicy= -MTUBytes=9216 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/10-lan0.network b/old/network/testdata/networkd/firewall/10-lan0.network deleted file mode 100644 index 1232fed..0000000 --- a/old/network/testdata/networkd/firewall/10-lan0.network +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -Name=lan0 - -[Network] -IPv6AcceptRA=no -VXLAN=vni3981 -VXLAN=vni3982 -VXLAN=vni104009 -VXLAN=vni104010 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/11-lan1.link b/old/network/testdata/networkd/firewall/11-lan1.link deleted file mode 100644 index 348f26f..0000000 --- a/old/network/testdata/networkd/firewall/11-lan1.link +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -PermanentMACAddress=00:03:00:11:12:01 - -[Link] -Name=lan1 -NamePolicy= -MTUBytes=9216 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/11-lan1.network b/old/network/testdata/networkd/firewall/11-lan1.network deleted file mode 100644 index a17badb..0000000 --- a/old/network/testdata/networkd/firewall/11-lan1.network +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -Name=lan1 - -[Network] -IPv6AcceptRA=no -VXLAN=vni3981 -VXLAN=vni3982 -VXLAN=vni104009 -VXLAN=vni104010 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/20-bridge.netdev b/old/network/testdata/networkd/firewall/20-bridge.netdev deleted file mode 100644 index 186b556..0000000 --- a/old/network/testdata/networkd/firewall/20-bridge.netdev +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[NetDev] -Name=bridge -Kind=bridge -MTUBytes=9000 - -[Bridge] -DefaultPVID=none -VLANFiltering=yes diff --git a/old/network/testdata/networkd/firewall/20-bridge.network b/old/network/testdata/networkd/firewall/20-bridge.network deleted file mode 100644 index 9ed8e6a..0000000 --- a/old/network/testdata/networkd/firewall/20-bridge.network +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -Name=bridge - -[Network] -VLAN=vlan3981 -VLAN=vlan3982 -VLAN=vlan104009 -VLAN=vlan104010 - -[BridgeVLAN] -VLAN=1000 - -[BridgeVLAN] -VLAN=1001 - -[BridgeVLAN] -VLAN=1002 - -[BridgeVLAN] -VLAN=1004 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/30-svi-3981.netdev b/old/network/testdata/networkd/firewall/30-svi-3981.netdev deleted file mode 100644 index 1c4cb0b..0000000 --- a/old/network/testdata/networkd/firewall/30-svi-3981.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# svi (networkid: bc830818-2df1-4904-8c40-4322296d393d) -[NetDev] -Name=vlan3981 -Kind=vlan - -[VLAN] -Id=1000 diff --git a/old/network/testdata/networkd/firewall/30-svi-3981.network b/old/network/testdata/networkd/firewall/30-svi-3981.network deleted file mode 100644 index ff73189..0000000 --- a/old/network/testdata/networkd/firewall/30-svi-3981.network +++ /dev/null @@ -1,10 +0,0 @@ -# svi (networkid: bc830818-2df1-4904-8c40-4322296d393d) -[Match] -Name=vlan3981 - -[Link] -MTUBytes=9000 - -[Network] -VRF=vrf3981 -Address=10.0.16.2/32 diff --git a/old/network/testdata/networkd/firewall/30-vrf-3981.netdev b/old/network/testdata/networkd/firewall/30-vrf-3981.netdev deleted file mode 100644 index f0ef0cd..0000000 --- a/old/network/testdata/networkd/firewall/30-vrf-3981.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# vrf (networkid: bc830818-2df1-4904-8c40-4322296d393d) -[NetDev] -Name=vrf3981 -Kind=vrf - -[VRF] -Table=1000 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/30-vrf-3981.network b/old/network/testdata/networkd/firewall/30-vrf-3981.network deleted file mode 100644 index 05a2c16..0000000 --- a/old/network/testdata/networkd/firewall/30-vrf-3981.network +++ /dev/null @@ -1,3 +0,0 @@ -# vrf (networkid: bc830818-2df1-4904-8c40-4322296d393d) -[Match] -Name=vrf3981 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/30-vxlan-3981.netdev b/old/network/testdata/networkd/firewall/30-vxlan-3981.netdev deleted file mode 100644 index 4faac86..0000000 --- a/old/network/testdata/networkd/firewall/30-vxlan-3981.netdev +++ /dev/null @@ -1,11 +0,0 @@ -# vxlan (networkid: bc830818-2df1-4904-8c40-4322296d393d) -[NetDev] -Name=vni3981 -Kind=vxlan - -[VXLAN] -VNI=3981 -Local=10.1.0.1 -UDPChecksum=true -MacLearning=false -DestinationPort=4789 diff --git a/old/network/testdata/networkd/firewall/30-vxlan-3981.network b/old/network/testdata/networkd/firewall/30-vxlan-3981.network deleted file mode 100644 index 0a51049..0000000 --- a/old/network/testdata/networkd/firewall/30-vxlan-3981.network +++ /dev/null @@ -1,13 +0,0 @@ -# vxlan (networkid: bc830818-2df1-4904-8c40-4322296d393d) -[Match] -Name=vni3981 - -[Link] -MTUBytes=9000 - -[Network] -Bridge=bridge - -[BridgeVLAN] -PVID=1000 -EgressUntagged=1000 diff --git a/old/network/testdata/networkd/firewall/31-svi-3982.netdev b/old/network/testdata/networkd/firewall/31-svi-3982.netdev deleted file mode 100644 index c82b275..0000000 --- a/old/network/testdata/networkd/firewall/31-svi-3982.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# svi (networkid: storage-net) -[NetDev] -Name=vlan3982 -Kind=vlan - -[VLAN] -Id=1001 diff --git a/old/network/testdata/networkd/firewall/31-svi-3982.network b/old/network/testdata/networkd/firewall/31-svi-3982.network deleted file mode 100644 index 855cf4d..0000000 --- a/old/network/testdata/networkd/firewall/31-svi-3982.network +++ /dev/null @@ -1,10 +0,0 @@ -# svi (networkid: storage-net) -[Match] -Name=vlan3982 - -[Link] -MTUBytes=9000 - -[Network] -VRF=vrf3982 -Address=10.0.18.2/32 diff --git a/old/network/testdata/networkd/firewall/31-vrf-3982.netdev b/old/network/testdata/networkd/firewall/31-vrf-3982.netdev deleted file mode 100644 index 0005eb6..0000000 --- a/old/network/testdata/networkd/firewall/31-vrf-3982.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# vrf (networkid: storage-net) -[NetDev] -Name=vrf3982 -Kind=vrf - -[VRF] -Table=1001 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/31-vrf-3982.network b/old/network/testdata/networkd/firewall/31-vrf-3982.network deleted file mode 100644 index f328ca1..0000000 --- a/old/network/testdata/networkd/firewall/31-vrf-3982.network +++ /dev/null @@ -1,3 +0,0 @@ -# vrf (networkid: storage-net) -[Match] -Name=vrf3982 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/31-vxlan-3982.netdev b/old/network/testdata/networkd/firewall/31-vxlan-3982.netdev deleted file mode 100644 index e909e12..0000000 --- a/old/network/testdata/networkd/firewall/31-vxlan-3982.netdev +++ /dev/null @@ -1,11 +0,0 @@ -# vxlan (networkid: storage-net) -[NetDev] -Name=vni3982 -Kind=vxlan - -[VXLAN] -VNI=3982 -Local=10.1.0.1 -UDPChecksum=true -MacLearning=false -DestinationPort=4789 diff --git a/old/network/testdata/networkd/firewall/31-vxlan-3982.network b/old/network/testdata/networkd/firewall/31-vxlan-3982.network deleted file mode 100644 index 204c6b3..0000000 --- a/old/network/testdata/networkd/firewall/31-vxlan-3982.network +++ /dev/null @@ -1,13 +0,0 @@ -# vxlan (networkid: storage-net) -[Match] -Name=vni3982 - -[Link] -MTUBytes=9000 - -[Network] -Bridge=bridge - -[BridgeVLAN] -PVID=1001 -EgressUntagged=1001 diff --git a/old/network/testdata/networkd/firewall/32-svi-104009.netdev b/old/network/testdata/networkd/firewall/32-svi-104009.netdev deleted file mode 100644 index f941ea3..0000000 --- a/old/network/testdata/networkd/firewall/32-svi-104009.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# svi (networkid: internet-vagrant-lab) -[NetDev] -Name=vlan104009 -Kind=vlan - -[VLAN] -Id=1002 diff --git a/old/network/testdata/networkd/firewall/32-svi-104009.network b/old/network/testdata/networkd/firewall/32-svi-104009.network deleted file mode 100644 index e8e16d8..0000000 --- a/old/network/testdata/networkd/firewall/32-svi-104009.network +++ /dev/null @@ -1,10 +0,0 @@ -# svi (networkid: internet-vagrant-lab) -[Match] -Name=vlan104009 - -[Link] -MTUBytes=9000 - -[Network] -VRF=vrf104009 -Address=185.1.2.3/32 diff --git a/old/network/testdata/networkd/firewall/32-vrf-104009.netdev b/old/network/testdata/networkd/firewall/32-vrf-104009.netdev deleted file mode 100644 index f81e30f..0000000 --- a/old/network/testdata/networkd/firewall/32-vrf-104009.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# vrf (networkid: internet-vagrant-lab) -[NetDev] -Name=vrf104009 -Kind=vrf - -[VRF] -Table=1002 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/32-vrf-104009.network b/old/network/testdata/networkd/firewall/32-vrf-104009.network deleted file mode 100644 index 760c0a2..0000000 --- a/old/network/testdata/networkd/firewall/32-vrf-104009.network +++ /dev/null @@ -1,3 +0,0 @@ -# vrf (networkid: internet-vagrant-lab) -[Match] -Name=vrf104009 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/32-vxlan-104009.netdev b/old/network/testdata/networkd/firewall/32-vxlan-104009.netdev deleted file mode 100644 index 43ed598..0000000 --- a/old/network/testdata/networkd/firewall/32-vxlan-104009.netdev +++ /dev/null @@ -1,11 +0,0 @@ -# vxlan (networkid: internet-vagrant-lab) -[NetDev] -Name=vni104009 -Kind=vxlan - -[VXLAN] -VNI=104009 -Local=10.1.0.1 -UDPChecksum=true -MacLearning=false -DestinationPort=4789 diff --git a/old/network/testdata/networkd/firewall/32-vxlan-104009.network b/old/network/testdata/networkd/firewall/32-vxlan-104009.network deleted file mode 100644 index ea24f09..0000000 --- a/old/network/testdata/networkd/firewall/32-vxlan-104009.network +++ /dev/null @@ -1,13 +0,0 @@ -# vxlan (networkid: internet-vagrant-lab) -[Match] -Name=vni104009 - -[Link] -MTUBytes=9000 - -[Network] -Bridge=bridge - -[BridgeVLAN] -PVID=1002 -EgressUntagged=1002 diff --git a/old/network/testdata/networkd/firewall/33-svi-104010.netdev b/old/network/testdata/networkd/firewall/33-svi-104010.netdev deleted file mode 100644 index d1e68a3..0000000 --- a/old/network/testdata/networkd/firewall/33-svi-104010.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# svi (networkid: mpls-nbg-w8101-test) -[NetDev] -Name=vlan104010 -Kind=vlan - -[VLAN] -Id=1004 diff --git a/old/network/testdata/networkd/firewall/33-svi-104010.network b/old/network/testdata/networkd/firewall/33-svi-104010.network deleted file mode 100644 index 11165a4..0000000 --- a/old/network/testdata/networkd/firewall/33-svi-104010.network +++ /dev/null @@ -1,10 +0,0 @@ -# svi (networkid: mpls-nbg-w8101-test) -[Match] -Name=vlan104010 - -[Link] -MTUBytes=9000 - -[Network] -VRF=vrf104010 -Address=100.127.129.1/32 diff --git a/old/network/testdata/networkd/firewall/33-vrf-104010.netdev b/old/network/testdata/networkd/firewall/33-vrf-104010.netdev deleted file mode 100644 index 0d851b6..0000000 --- a/old/network/testdata/networkd/firewall/33-vrf-104010.netdev +++ /dev/null @@ -1,7 +0,0 @@ -# vrf (networkid: mpls-nbg-w8101-test) -[NetDev] -Name=vrf104010 -Kind=vrf - -[VRF] -Table=1004 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/33-vrf-104010.network b/old/network/testdata/networkd/firewall/33-vrf-104010.network deleted file mode 100644 index ffe489c..0000000 --- a/old/network/testdata/networkd/firewall/33-vrf-104010.network +++ /dev/null @@ -1,3 +0,0 @@ -# vrf (networkid: mpls-nbg-w8101-test) -[Match] -Name=vrf104010 \ No newline at end of file diff --git a/old/network/testdata/networkd/firewall/33-vxlan-104010.netdev b/old/network/testdata/networkd/firewall/33-vxlan-104010.netdev deleted file mode 100644 index 55ac87b..0000000 --- a/old/network/testdata/networkd/firewall/33-vxlan-104010.netdev +++ /dev/null @@ -1,11 +0,0 @@ -# vxlan (networkid: mpls-nbg-w8101-test) -[NetDev] -Name=vni104010 -Kind=vxlan - -[VXLAN] -VNI=104010 -Local=10.1.0.1 -UDPChecksum=true -MacLearning=false -DestinationPort=4789 diff --git a/old/network/testdata/networkd/firewall/33-vxlan-104010.network b/old/network/testdata/networkd/firewall/33-vxlan-104010.network deleted file mode 100644 index fff9745..0000000 --- a/old/network/testdata/networkd/firewall/33-vxlan-104010.network +++ /dev/null @@ -1,13 +0,0 @@ -# vxlan (networkid: mpls-nbg-w8101-test) -[Match] -Name=vni104010 - -[Link] -MTUBytes=9000 - -[Network] -Bridge=bridge - -[BridgeVLAN] -PVID=1004 -EgressUntagged=1004 diff --git a/old/network/testdata/networkd/machine/00-lo.network b/old/network/testdata/networkd/machine/00-lo.network deleted file mode 100644 index ec7ec41..0000000 --- a/old/network/testdata/networkd/machine/00-lo.network +++ /dev/null @@ -1,15 +0,0 @@ -# networkid: bc830818-2df1-4904-8c40-4322296d393d -[Match] -Name=lo - -[Address] -Address=127.0.0.1/8 - -[Address] -Address=10.0.17.2/32 - -[Address] -Address=185.1.2.3/32 - -[Address] -Address=100.127.129.1/32 \ No newline at end of file diff --git a/old/network/testdata/networkd/machine/10-lan0.link b/old/network/testdata/networkd/machine/10-lan0.link deleted file mode 100644 index 498c09d..0000000 --- a/old/network/testdata/networkd/machine/10-lan0.link +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -PermanentMACAddress=00:03:00:11:11:01 - -[Link] -Name=lan0 -NamePolicy= -MTUBytes=9000 \ No newline at end of file diff --git a/old/network/testdata/networkd/machine/10-lan0.network b/old/network/testdata/networkd/machine/10-lan0.network deleted file mode 100644 index 74c29ad..0000000 --- a/old/network/testdata/networkd/machine/10-lan0.network +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -Name=lan0 - -[Network] -IPv6AcceptRA=no \ No newline at end of file diff --git a/old/network/testdata/networkd/machine/11-lan1.link b/old/network/testdata/networkd/machine/11-lan1.link deleted file mode 100644 index 5d15b91..0000000 --- a/old/network/testdata/networkd/machine/11-lan1.link +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -PermanentMACAddress=00:03:00:11:12:01 - -[Link] -Name=lan1 -NamePolicy= -MTUBytes=9000 \ No newline at end of file diff --git a/old/network/testdata/networkd/machine/11-lan1.network b/old/network/testdata/networkd/machine/11-lan1.network deleted file mode 100644 index 79a6cab..0000000 --- a/old/network/testdata/networkd/machine/11-lan1.network +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -[Match] -Name=lan1 - -[Network] -IPv6AcceptRA=no \ No newline at end of file diff --git a/old/network/tpl/networkd/00-lo.network.tpl b/old/network/tpl/networkd/00-lo.network.tpl deleted file mode 100644 index 5e4d39a..0000000 --- a/old/network/tpl/networkd/00-lo.network.tpl +++ /dev/null @@ -1,12 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} -{{ .Loopback.Comment }} -[Match] -Name=lo - -[Address] -Address=127.0.0.1/8 -{{- range .Loopback.IPs }} - -[Address] -Address={{ . }} -{{- end }} \ No newline at end of file diff --git a/old/network/tpl/networkd/10-lan.link.tpl b/old/network/tpl/networkd/10-lan.link.tpl deleted file mode 100644 index 476786b..0000000 --- a/old/network/tpl/networkd/10-lan.link.tpl +++ /dev/null @@ -1,9 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.SystemdLinkData*/ -}} -{{ .Comment }} -[Match] -PermanentMACAddress={{ .MAC }} - -[Link] -Name=lan{{ .Index }} -NamePolicy= -MTUBytes={{ .MTU }} \ No newline at end of file diff --git a/old/network/tpl/networkd/10-lan.network.tpl b/old/network/tpl/networkd/10-lan.network.tpl deleted file mode 100644 index 73fb471..0000000 --- a/old/network/tpl/networkd/10-lan.network.tpl +++ /dev/null @@ -1,10 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} -{{ .Comment }} -[Match] -Name=lan{{ .Index }} - -[Network] -IPv6AcceptRA=no -{{- range .EVPNIfaces }} -VXLAN=vni{{ .VXLAN.ID }} -{{- end }} \ No newline at end of file diff --git a/old/network/tpl/networkd/20-bridge.netdev.tpl b/old/network/tpl/networkd/20-bridge.netdev.tpl deleted file mode 100644 index 2fef44c..0000000 --- a/old/network/tpl/networkd/20-bridge.netdev.tpl +++ /dev/null @@ -1,10 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} -{{ .Comment }} -[NetDev] -Name=bridge -Kind=bridge -MTUBytes=9000 - -[Bridge] -DefaultPVID=none -VLANFiltering=yes diff --git a/old/network/tpl/networkd/20-bridge.network.tpl b/old/network/tpl/networkd/20-bridge.network.tpl deleted file mode 100644 index 360b48c..0000000 --- a/old/network/tpl/networkd/20-bridge.network.tpl +++ /dev/null @@ -1,14 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.IfacesData*/ -}} -{{ .Comment }} -[Match] -Name=bridge - -[Network] -{{- range .EVPNIfaces }} -VLAN=vlan{{ .VRF.ID }} -{{- end }} -{{- range .EVPNIfaces }} - -[BridgeVLAN] -VLAN={{ .SVI.VLANID }} -{{- end }} \ No newline at end of file diff --git a/old/network/tpl/networkd/30-svi.netdev.tpl b/old/network/tpl/networkd/30-svi.netdev.tpl deleted file mode 100644 index 6aa6826..0000000 --- a/old/network/tpl/networkd/30-svi.netdev.tpl +++ /dev/null @@ -1,8 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .SVI.Comment }} -[NetDev] -Name=vlan{{ .VRF.ID }} -Kind=vlan - -[VLAN] -Id={{ .SVI.VLANID }} diff --git a/old/network/tpl/networkd/30-svi.network.tpl b/old/network/tpl/networkd/30-svi.network.tpl deleted file mode 100644 index 0ef4c10..0000000 --- a/old/network/tpl/networkd/30-svi.network.tpl +++ /dev/null @@ -1,13 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .SVI.Comment }} -[Match] -Name=vlan{{ .VRF.ID }} - -[Link] -MTUBytes=9000 - -[Network] -VRF=vrf{{ .VRF.ID }} -{{- range .SVI.Addresses }} -Address={{ . }} -{{- end }} diff --git a/old/network/tpl/networkd/30-vrf.netdev.tpl b/old/network/tpl/networkd/30-vrf.netdev.tpl deleted file mode 100644 index 282a910..0000000 --- a/old/network/tpl/networkd/30-vrf.netdev.tpl +++ /dev/null @@ -1,8 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VRF.Comment }} -[NetDev] -Name=vrf{{ .VRF.ID }} -Kind=vrf - -[VRF] -Table={{ .VRF.Table }} \ No newline at end of file diff --git a/old/network/tpl/networkd/30-vrf.network.tpl b/old/network/tpl/networkd/30-vrf.network.tpl deleted file mode 100644 index a7628dc..0000000 --- a/old/network/tpl/networkd/30-vrf.network.tpl +++ /dev/null @@ -1,4 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VRF.Comment }} -[Match] -Name=vrf{{ .VRF.ID }} \ No newline at end of file diff --git a/old/network/tpl/networkd/30-vxlan.netdev.tpl b/old/network/tpl/networkd/30-vxlan.netdev.tpl deleted file mode 100644 index 68ebf9b..0000000 --- a/old/network/tpl/networkd/30-vxlan.netdev.tpl +++ /dev/null @@ -1,12 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VXLAN.Comment }} -[NetDev] -Name=vni{{ .VXLAN.ID }} -Kind=vxlan - -[VXLAN] -VNI={{ .VXLAN.ID }} -Local={{ .VXLAN.TunnelIP }} -UDPChecksum=true -MacLearning=false -DestinationPort=4789 diff --git a/old/network/tpl/networkd/30-vxlan.network.tpl b/old/network/tpl/networkd/30-vxlan.network.tpl deleted file mode 100644 index a49f111..0000000 --- a/old/network/tpl/networkd/30-vxlan.network.tpl +++ /dev/null @@ -1,14 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.EVPNIface*/ -}} -{{ .VXLAN.Comment }} -[Match] -Name=vni{{ .VXLAN.ID }} - -[Link] -MTUBytes=9000 - -[Network] -Bridge=bridge - -[BridgeVLAN] -PVID={{ .SVI.VLANID }} -EgressUntagged={{ .SVI.VLANID }} diff --git a/pkg/network/network.go b/pkg/network/network.go index a292ae7..678f5b1 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -45,6 +45,10 @@ func (n *Network) IsMachine() bool { return n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE } +func (n *Network) AllocationNetworks() []*apiv2.MachineNetwork { + return n.allocation.Networks +} + func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { var ips []string @@ -102,6 +106,26 @@ func (n *Network) PrivatePrimaryIPs() ([]string, error) { return nil, fmt.Errorf("no private primary ip present in network allocation") } +func (n *Network) PrivatePrimaryNetworks() ([]string, error) { + for _, nw := range n.allocation.Networks { + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD { + return nw.Prefixes, nil + } + } + + for _, nw := range n.allocation.Networks { + if nw.Project == nil { + continue + } + + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED && *nw.Project == n.allocation.Project { + return nw.Prefixes, nil + } + } + + return nil, fmt.Errorf("no private primary networks present in network allocation") +} + func (n *Network) VxlanIDs() (ids []uint64) { if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { for _, nw := range n.allocation.Networks { diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 7027fe9..3770135 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -1 +1,363 @@ package nftables + +import ( + "context" + "fmt" + "log/slog" + "net/netip" + "strconv" + "strings" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/os-installer/old/exec" + "github.com/metal-stack/os-installer/pkg/network" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" + + _ "embed" +) + +const ( + serviceName = "nftables.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName + + // Set up additional conntrack zone for DNS traffic. + // There was a problem that duplicate packets were registered by conntrack + // when packet was leaking from private VRF to the internet VRF. + // Isolating traffic to special zone solves the problem. + // Zone number(3) was obtained by experiments. + dnsProxyZone = "3" + + dnsPort = "domain" + systemctlBin = "/bin/systemctl" + + // ForwardPolicyDrop drops packets which try to go through the forwarding chain + ForwardPolicyDrop = ForwardPolicy("drop") + // ForwardPolicyAccept accepts packets which try to go through the forwarding chain + ForwardPolicyAccept = ForwardPolicy("accept") +) + +var ( + //go:embed nftrules.tpl + templateString string +) + +type ( + Config struct { + Log *slog.Logger + Reload bool + + Network *network.Network + + // FIXME validator net.Validator, + EnableDNSProxy bool + ForwardPolicy ForwardPolicy + + fs afero.Fs + } + + // ForwardPolicy defines how packets in the forwarding chain are handled, can be either drop or accept. + // drop will be the standard for firewalls which are not managed by kubernetes resources (CWNPs) + ForwardPolicy string + + // NftablesData represents the information required to render nftables configuration. + NftablesData struct { + Comment string + SNAT []SNAT + DNSProxyDNAT DNAT + VPN bool + ForwardPolicy string + FirewallRules FirewallRules + Input Input + } + + Input struct { + InInterfaces []string + } + + FirewallRules struct { + Egress []string + Ingress []string + } + + // SNAT holds the information required to configure Source NAT. + SNAT struct { + Comment string + OutInterface string + OutIntSpec AddrSpec + SourceSpecs []AddrSpec + } + + // DNAT holds the information required to configure DNAT. + DNAT struct { + Comment string + InInterfaces []string + SAddr string + DAddr string + Port string + Zone string + DestSpec AddrSpec + } + + AddrSpec struct { + AddressFamily string + Address string + } + + // NftablesValidator can validate configuration for nftables rules. + NftablesValidator struct { + path string + log *slog.Logger + } + + NftablesReloader struct{} +) + +// Renders renders nftables rules according to the given input data and reloads the service if necessary +func Render(ctx context.Context, cfg *Config) (changed bool, err error) { + const comment = "generated by os-installer" + + data := NftablesData{ + Comment: comment, + SNAT: getSNAT(ctx, cfg), + ForwardPolicy: string(forwardPolicy), + FirewallRules: getFirewallRules(c), + Input: getInput(c), + } + + if enableDNSProxy { + data.DNSProxyDNAT = getDNSProxyDNAT(c, dnsPort, dnsProxyZone) + } + + if c.VPN != nil { + data.VPN = true + } + + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: templateString, + Data: data, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + changed, err = r.Render(ctx, serviceUnitPath) + if err != nil { + return changed, err + } + + if cfg.Reload && changed { + if err := systemd_renderer.Reload(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + + return +} + +func getInput(ctx context.Context, cfg *Config) Input { + input := Input{} + networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared) + for _, n := range networks { + input.InInterfaces = append(input.InInterfaces, fmt.Sprintf("vrf%d", *n.Vrf)) + } + return input +} + +func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { + var ( + result []SNAT + defaultNetwork models.V1MachineNetwork + defaultAF string + ) + + defaultNetworkName, err := c.getDefaultRouteVRFName() + if err == nil { + defaultNetwork = *c.GetDefaultRouteNetwork() + ip, _ := netip.ParseAddr(defaultNetwork.Ips[0]) + defaultAF = "ip" + if ip.Is6() { + defaultAF = "ip6" + } + } + + primaryNetworks, err := cfg.Network.PrivatePrimaryNetworks() + if err != nil { + return nil, err + } + + for _, n := range cfg.Network.AllocationNetworks() { + switch n.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, apiv2.NetworkType_NETWORK_TYPE_SUPER, apiv2.NetworkType_NETWORK_TYPE_SUPER_NAMESPACED: + continue + } + + if n.NatType != apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE { + continue + } + + var ( + sources []AddrSpec + cmt = fmt.Sprintf("snat (networkid: %s)", n.Network) + svi = fmt.Sprintf("vlan%d", n.Vrf) + vrf = fmt.Sprintf("vrf%d", n.Vrf) + ) + + for _, nw := range primaryNetworks { + af, err := getAddressFamily(nw) + if err != nil { + return nil, fmt.Errorf("unable to determine address family: %w", err) + } + + sources = append(sources, AddrSpec{ + Address: nw, + AddressFamily: af, + }) + } + + s := SNAT{ + Comment: cmt, + OutInterface: svi, + SourceSpecs: sources, + } + + if cfg.EnableDNSProxy && (vrf == defaultNetworkName) { + s.OutIntSpec = AddrSpec{ + AddressFamily: defaultAF, + Address: defaultNetwork.Ips[0], + } + } + + result = append(result, s) + } + + return result, nil +} + +func getDNSProxyDNAT(c config, port, zone string) DNAT { + networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared) + svis := []string{} + for _, n := range networks { + svi := fmt.Sprintf("vlan%d", *n.Vrf) + svis = append(svis, svi) + } + + n := c.GetDefaultRouteNetwork() + if n == nil { + return DNAT{} + } + + ip, _ := netip.ParseAddr(n.Ips[0]) + af := "ip" + saddr := "10.0.0.0/8" + daddr := "@proxy_dns_servers" + if ip.Is6() { + af = "ip6" + saddr = "fd00::/8" + daddr = "@proxy_dns_servers_v6" + } + return DNAT{ + Comment: "dnat to dns proxy", + InInterfaces: svis, + SAddr: saddr, + DAddr: daddr, + Port: port, + Zone: zone, + DestSpec: AddrSpec{ + AddressFamily: af, + Address: n.Ips[0], + }, + } +} + +func getFirewallRules(c config) FirewallRules { + if c.FirewallRules == nil { + return FirewallRules{} + } + var ( + egressRules = []string{"# egress rules specified during firewall creation"} + ingressRules = []string{"# ingress rules specified during firewall creation"} + inputInterfaces = getInput(c) + quotedInputInterfaces []string + ) + for _, i := range inputInterfaces.InInterfaces { + quotedInputInterfaces = append(quotedInputInterfaces, "\""+i+"\"") + } + + for _, r := range c.FirewallRules.Egress { + ports := make([]string, len(r.Ports)) + for i, v := range r.Ports { + ports[i] = strconv.Itoa(int(v)) + } + for _, daddr := range r.To { + af, err := getAddressFamily(daddr) + if err != nil { + continue + } + egressRules = append(egressRules, + fmt.Sprintf("iifname { %s } %s daddr %s %s dport { %s } counter accept comment %q", strings.Join(quotedInputInterfaces, ","), af, daddr, strings.ToLower(r.Protocol), strings.Join(ports, ","), r.Comment)) + } + } + + privatePrimaryNetwork := c.getPrivatePrimaryNetwork() + outputInterfacenames := "" + if privatePrimaryNetwork != nil && privatePrimaryNetwork.Vrf != nil { + outputInterfacenames = fmt.Sprintf("oifname { \"vrf%d\", \"vni%d\", \"vlan%d\" }", *privatePrimaryNetwork.Vrf, *privatePrimaryNetwork.Vrf, *privatePrimaryNetwork.Vrf) + } + + for _, r := range c.FirewallRules.Ingress { + ports := make([]string, len(r.Ports)) + for i, v := range r.Ports { + ports[i] = strconv.Itoa(int(v)) + } + destinationSpec := "" + if len(r.To) > 0 { + af, err := getAddressFamily(r.To[0]) // To is validated to contain no mixed addressfamilies in metal-api + if err != nil { + continue + } + destinationSpec = fmt.Sprintf("%s daddr { %s }", af, strings.Join(r.To, ", ")) + } else if outputInterfacenames != "" { + destinationSpec = outputInterfacenames + } else { + c.log.Warn("no to address specified but not private primary network present, skipping this rule", "rule", r) + continue + } + + for _, saddr := range r.From { + af, err := getAddressFamily(saddr) + if err != nil { + continue + } + ingressRules = append(ingressRules, fmt.Sprintf("%s %s saddr %s %s dport { %s } counter accept comment %q", destinationSpec, af, saddr, strings.ToLower(r.Protocol), strings.Join(ports, ","), r.Comment)) + } + } + return FirewallRules{ + Egress: egressRules, + Ingress: ingressRules, + } +} + +func getAddressFamily(p string) (string, error) { + prefix, err := netip.ParsePrefix(p) + if err != nil { + return "", err + } + + family := "ip" + if prefix.Addr().Is6() { + family = "ip6" + } + + return family, nil +} + +// Validate validates network interfaces configuration. +func (v NftablesValidator) Validate() error { + v.log.Info("running 'nft --check --file' to validate changes.", "file", v.path) + return exec.NewVerboseCmd("nft", "--check", "--file", v.path).Run() +} diff --git a/pkg/nftables/nftrules.tpl b/pkg/nftables/nftrules.tpl new file mode 100644 index 0000000..5105326 --- /dev/null +++ b/pkg/nftables/nftrules.tpl @@ -0,0 +1,130 @@ +# {{ .Comment }} +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + {{- if .DNSProxyDNAT.DestSpec.Address }} + + {{ .DNSProxyDNAT.DestSpec.AddressFamily }} saddr {{ .DNSProxyDNAT.SAddr }} tcp dport {{ .DNSProxyDNAT.Port }} {{ .DNSProxyDNAT.DestSpec.AddressFamily }} daddr {{ .DNSProxyDNAT.DestSpec.Address }} accept comment "{{ .DNSProxyDNAT.Comment }}" + {{ .DNSProxyDNAT.DestSpec.AddressFamily }} saddr {{ .DNSProxyDNAT.SAddr }} udp dport {{ .DNSProxyDNAT.Port }} {{ .DNSProxyDNAT.DestSpec.AddressFamily }} daddr {{ .DNSProxyDNAT.DestSpec.Address }} accept comment "{{ .DNSProxyDNAT.Comment }}" + {{- end }} + + {{ if .VPN -}} + iifname "tailscale*" accept comment "Accept tailscale traffic" + {{- else -}} + tcp dport ssh ct state new counter accept comment "SSH incoming connections" + {{- end }} + {{- range .Input.InInterfaces }} + iifname "{{ . }}" tcp dport 9100 counter accept comment "node metrics" + iifname "{{ . }}" tcp dport 9630 counter accept comment "nftables metrics" + {{- end }} + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy {{ .ForwardPolicy }}; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + {{- range .FirewallRules.Egress }} + {{ . }} + {{- end }} + {{- range .FirewallRules.Ingress }} + {{ . }} + {{- end }} + {{ if eq .ForwardPolicy "drop" -}} + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + {{- end }} + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + {{- $port:=.DNSProxyDNAT.Port }} + {{- $zone:=.DNSProxyDNAT.Zone }} + {{- range .DNSProxyDNAT.InInterfaces }} + oifname "{{ . }}" tcp sport {{ $port }} ct zone set {{ $zone }} + oifname "{{ . }}" udp sport {{ $port }} ct zone set {{ $zone }} + {{- end }} + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + {{- if eq .DNSProxyDNAT.DestSpec.AddressFamily "ip6" }} + + set proxy_dns_servers_v6 { + type ipv6_addr + flags interval + auto-merge + elements = { 2001:4860:4860::8888, 2001:4860:4860::8844, 2606:4700:4700::1111, 2606:4700:4700::1001 } + } + {{- end }} + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + {{- $port:=.DNSProxyDNAT.Port }} + {{- $dst:=.DNSProxyDNAT.DestSpec }} + {{- $daddr:=.DNSProxyDNAT.DAddr }} + {{- $cmt:=.DNSProxyDNAT.Comment }} + {{- range .DNSProxyDNAT.InInterfaces }} + {{ if $daddr -}} {{ $dst.AddressFamily }} daddr {{ $daddr }} {{ end -}} iifname "{{ . }}" tcp dport {{ $port }} dnat {{ $dst.AddressFamily }} to {{ $dst.Address }} comment "{{ $cmt }}" + {{ if $daddr -}} {{ $dst.AddressFamily }} daddr {{ $daddr }} {{ end -}} iifname "{{ . }}" udp dport {{ $port }} dnat {{ $dst.AddressFamily }} to {{ $dst.Address }} comment "{{ $cmt }}" + {{- end }} + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + {{- $port:=.DNSProxyDNAT.Port }} + {{- $zone:=.DNSProxyDNAT.Zone }} + {{- range .DNSProxyDNAT.InInterfaces }} + iifname "{{ . }}" tcp dport {{ $port }} ct zone set {{ $zone }} + iifname "{{ . }}" udp dport {{ $port }} ct zone set {{ $zone }} + {{- end }} + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + {{- range .SNAT }} + {{- $cmt:=.Comment }} + {{- $out:=.OutInterface }} + {{- $outspec:=.OutIntSpec }} + {{- range .SourceSpecs }} + {{- if and $outspec.Address (eq $outspec.AddressFamily .AddressFamily) }} + oifname "{{ $out }}" {{ .AddressFamily }} saddr {{ .Address }} {{ .AddressFamily }} daddr != {{ $outspec.Address }} counter masquerade random comment "{{ $cmt }}"{{ else }} + oifname "{{ $out }}" {{ .AddressFamily }} saddr {{ .Address }} counter masquerade random comment "{{ $cmt }}" + {{- end }} + {{- end }} + {{- end }} + } +} From 835c1db4366a9c7b1ff352e8604ea5029b828a89 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 9 Mar 2026 16:17:45 +0100 Subject: [PATCH 013/102] Smallish --- pkg/nftables/nftables.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 3770135..47f7ad1 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -119,15 +119,20 @@ type ( func Render(ctx context.Context, cfg *Config) (changed bool, err error) { const comment = "generated by os-installer" + snat, err := getSNAT(ctx, cfg) + if err != nil { + return false, err + } + data := NftablesData{ Comment: comment, - SNAT: getSNAT(ctx, cfg), - ForwardPolicy: string(forwardPolicy), - FirewallRules: getFirewallRules(c), - Input: getInput(c), + SNAT: snat, + ForwardPolicy: string(cfg.ForwardPolicy), + FirewallRules: getFirewallRules(ctx, cfg), + Input: getInput(ctx, cfg), } - if enableDNSProxy { + if cfg.EnableDNSProxy { data.DNSProxyDNAT = getDNSProxyDNAT(c, dnsPort, dnsProxyZone) } @@ -274,7 +279,7 @@ func getDNSProxyDNAT(c config, port, zone string) DNAT { } } -func getFirewallRules(c config) FirewallRules { +func getFirewallRules(ctx context.Context, cfg *Config) FirewallRules { if c.FirewallRules == nil { return FirewallRules{} } From d40150a540d38d10507c20b8039d516604238790 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 9 Mar 2026 17:42:29 +0100 Subject: [PATCH 014/102] Smallish --- pkg/network/network.go | 9 ++++++++- pkg/nftables/nftables.go | 15 +++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/pkg/network/network.go b/pkg/network/network.go index 678f5b1..ff13b78 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -45,6 +45,13 @@ func (n *Network) IsMachine() bool { return n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE } +func (n *Network) HasVpn() bool { + if n.allocation.Vpn != nil && n.allocation.Vpn.AuthKey != "" { + return true + } + return false +} + func (n *Network) AllocationNetworks() []*apiv2.MachineNetwork { return n.allocation.Networks } @@ -106,7 +113,7 @@ func (n *Network) PrivatePrimaryIPs() ([]string, error) { return nil, fmt.Errorf("no private primary ip present in network allocation") } -func (n *Network) PrivatePrimaryNetworks() ([]string, error) { +func (n *Network) PrivatePrimaryNetworksPrefixes() ([]string, error) { for _, nw := range n.allocation.Networks { if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD { return nw.Prefixes, nil diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 47f7ad1..a530940 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -130,16 +130,13 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { ForwardPolicy: string(cfg.ForwardPolicy), FirewallRules: getFirewallRules(ctx, cfg), Input: getInput(ctx, cfg), + VPN: cfg.Network.HasVpn(), } if cfg.EnableDNSProxy { data.DNSProxyDNAT = getDNSProxyDNAT(c, dnsPort, dnsProxyZone) } - if c.VPN != nil { - data.VPN = true - } - r, err := renderer.New(&renderer.Config{ Log: cfg.Log, TemplateString: templateString, @@ -166,9 +163,11 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { func getInput(ctx context.Context, cfg *Config) Input { input := Input{} - networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared) - for _, n := range networks { - input.InInterfaces = append(input.InInterfaces, fmt.Sprintf("vrf%d", *n.Vrf)) + for _, n := range cfg.Network.AllocationNetworks() { + switch n.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_CHILD, apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: + input.InInterfaces = append(input.InInterfaces, fmt.Sprintf("vrf%d", n.Vrf)) + } } return input } @@ -190,7 +189,7 @@ func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { } } - primaryNetworks, err := cfg.Network.PrivatePrimaryNetworks() + primaryNetworks, err := cfg.Network.PrivatePrimaryNetworksPrefixes() if err != nil { return nil, err } From 97eaf7bcc9c78f4043dc73119499f09b74ef2862 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 9 Mar 2026 18:18:29 +0100 Subject: [PATCH 015/102] nftables --- go.mod | 17 ++++--- go.sum | 43 +++++++++--------- pkg/network/network.go | 49 ++++++++++++++++++++ pkg/nftables/nftables.go | 96 ++++++++++++++++++++++++---------------- 4 files changed, 135 insertions(+), 70 deletions(-) diff --git a/go.mod b/go.mod index 056c821..ff586d2 100644 --- a/go.mod +++ b/go.mod @@ -27,13 +27,13 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-openapi/analysis v0.24.2 // indirect - github.com/go-openapi/errors v0.22.6 // indirect + github.com/go-openapi/analysis v0.24.3 // indirect + github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/loads v0.23.2 // indirect + github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/strfmt v0.26.0 // indirect github.com/go-openapi/swag v0.25.5 // indirect github.com/go-openapi/swag/cmdutils v0.25.5 // indirect github.com/go-openapi/swag/conv v0.25.5 // indirect @@ -46,24 +46,23 @@ require ( github.com/go-openapi/swag/stringutils v0.25.5 // indirect github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect - github.com/go-openapi/validate v0.25.1 // indirect + github.com/go-openapi/validate v0.25.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/vincent-petithory/dataurl v1.0.0 // indirect - go.mongodb.org/mongo-driver v1.17.9 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 6053853..78e129c 100644 --- a/go.sum +++ b/go.sum @@ -27,20 +27,20 @@ github.com/flatcar/ignition v0.36.2/go.mod h1:uk1tpzLFRXus4RrvzgMI+IqmmB8a/RGFSB github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50= -github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE= -github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo= -github.com/go-openapi/errors v0.22.6/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= +github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= @@ -67,12 +67,12 @@ github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzz github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= -github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= -github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= -github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus v0.0.0-20181025153459-66d97aec3384/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -103,8 +103,9 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pin/tftp v2.1.0+incompatible/go.mod h1:xVpZOMCXTy+A5QMjEVN0Glwa1sUvaJhFXbr/aAxuxGY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -132,8 +133,6 @@ github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8A github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= github.com/vmware/vmw-ovflib v0.0.0-20170608004843-1f217b9dc714/go.mod h1:jiPk45kn7klhByRvUq5i2vo1RtHKBHj+iWGFpxbXuuI= -go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= -go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org v0.0.0-20160314031811-03efcb870d84/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= @@ -144,11 +143,11 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= diff --git a/pkg/network/network.go b/pkg/network/network.go index ff13b78..e1f420e 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -13,6 +13,11 @@ const ( mtuFirewall = 9216 // mtuMachine defines the value for MTU specific to the needs of a machine. mtuMachine = 9000 + + // IPv4ZeroCIDR is the CIDR block for the whole IPv4 address space + IPv4ZeroCIDR = "0.0.0.0/0" + // IPv6ZeroCIDR is the CIDR block for the whole IPv6 address space + IPv6ZeroCIDR = "::/0" ) type ( @@ -56,6 +61,10 @@ func (n *Network) AllocationNetworks() []*apiv2.MachineNetwork { return n.allocation.Networks } +func (n *Network) FirewallRules() *apiv2.FirewallRules { + return n.allocation.FirewallRules +} + func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { var ips []string @@ -83,6 +92,26 @@ func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { return } +func (n *Network) PrivatePrimaryNetwork() (*apiv2.MachineNetwork, error) { + for _, nw := range n.allocation.Networks { + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD { + return nw, nil + } + } + + for _, nw := range n.allocation.Networks { + if nw.Project == nil { + continue + } + + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED && *nw.Project == n.allocation.Project { + return nw, nil + } + } + + return nil, fmt.Errorf("no private primary network present in network allocation") +} + func (n *Network) PrivatePrimaryIPs() ([]string, error) { if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { for _, nw := range n.allocation.Networks { @@ -182,6 +211,26 @@ func (n *Network) EVPNIfaces() (ifaces []EvpnIface, err error) { return } +func (n *Network) GetDefaultRouteNetwork() (*apiv2.MachineNetwork, error) { + for _, nw := range n.allocation.Networks { + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_EXTERNAL { + if containsDefaultRoute(nw.DestinationPrefixes) { + return nw, nil + } + } + } + return nil, fmt.Errorf("no network which provides a default route found") +} + +func containsDefaultRoute(prefixes []string) bool { + for _, prefix := range prefixes { + if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { + return true + } + } + return false +} + func loFirewallIps(networks []*apiv2.MachineNetwork) (ips []string, err error) { for _, nw := range networks { switch nw.NetworkType { diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index a530940..1737af8 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -8,8 +8,8 @@ import ( "strconv" "strings" + "github.com/metal-stack/api/go/enum" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/metal-go/api/models" "github.com/metal-stack/os-installer/old/exec" "github.com/metal-stack/os-installer/pkg/network" systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" @@ -69,7 +69,7 @@ type ( DNSProxyDNAT DNAT VPN bool ForwardPolicy string - FirewallRules FirewallRules + FirewallRules *FirewallRules Input Input } @@ -124,17 +124,22 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return false, err } + firewallRules, err := getFirewallRules(ctx, cfg) + if err != nil { + return false, err + } + data := NftablesData{ Comment: comment, SNAT: snat, ForwardPolicy: string(cfg.ForwardPolicy), - FirewallRules: getFirewallRules(ctx, cfg), + FirewallRules: firewallRules, Input: getInput(ctx, cfg), VPN: cfg.Network.HasVpn(), } if cfg.EnableDNSProxy { - data.DNSProxyDNAT = getDNSProxyDNAT(c, dnsPort, dnsProxyZone) + data.DNSProxyDNAT = getDNSProxyDNAT(ctx, cfg) } r, err := renderer.New(&renderer.Config{ @@ -175,18 +180,20 @@ func getInput(ctx context.Context, cfg *Config) Input { func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { var ( result []SNAT - defaultNetwork models.V1MachineNetwork + defaultNetwork *apiv2.MachineNetwork defaultAF string ) - defaultNetworkName, err := c.getDefaultRouteVRFName() - if err == nil { - defaultNetwork = *c.GetDefaultRouteNetwork() - ip, _ := netip.ParseAddr(defaultNetwork.Ips[0]) - defaultAF = "ip" - if ip.Is6() { - defaultAF = "ip6" - } + defaultNetwork, err := cfg.Network.GetDefaultRouteNetwork() + if err != nil { + return nil, err + } + defaultNetworkName := fmt.Sprintf("vrf%d", defaultNetwork.Vrf) + + ip, _ := netip.ParseAddr(defaultNetwork.Ips[0]) + defaultAF = "ip" + if ip.Is6() { + defaultAF = "ip6" } primaryNetworks, err := cfg.Network.PrivatePrimaryNetworksPrefixes() @@ -242,16 +249,19 @@ func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { return result, nil } -func getDNSProxyDNAT(c config, port, zone string) DNAT { - networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared) +func getDNSProxyDNAT(ctx context.Context, cfg *Config) DNAT { svis := []string{} - for _, n := range networks { - svi := fmt.Sprintf("vlan%d", *n.Vrf) - svis = append(svis, svi) + for _, n := range cfg.Network.AllocationNetworks() { + switch n.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_CHILD, apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: + svi := fmt.Sprintf("vlan%d", n.Vrf) + svis = append(svis, svi) + } + } - n := c.GetDefaultRouteNetwork() - if n == nil { + n, err := cfg.Network.GetDefaultRouteNetwork() + if err != nil { return DNAT{} } @@ -269,8 +279,8 @@ func getDNSProxyDNAT(c config, port, zone string) DNAT { InInterfaces: svis, SAddr: saddr, DAddr: daddr, - Port: port, - Zone: zone, + Port: dnsPort, + Zone: dnsProxyZone, DestSpec: AddrSpec{ AddressFamily: af, Address: n.Ips[0], @@ -278,21 +288,21 @@ func getDNSProxyDNAT(c config, port, zone string) DNAT { } } -func getFirewallRules(ctx context.Context, cfg *Config) FirewallRules { - if c.FirewallRules == nil { - return FirewallRules{} +func getFirewallRules(ctx context.Context, cfg *Config) (*FirewallRules, error) { + if cfg.Network.FirewallRules() == nil { + return &FirewallRules{}, nil } var ( egressRules = []string{"# egress rules specified during firewall creation"} ingressRules = []string{"# ingress rules specified during firewall creation"} - inputInterfaces = getInput(c) + inputInterfaces = getInput(ctx, cfg) quotedInputInterfaces []string ) for _, i := range inputInterfaces.InInterfaces { quotedInputInterfaces = append(quotedInputInterfaces, "\""+i+"\"") } - for _, r := range c.FirewallRules.Egress { + for _, r := range cfg.Network.FirewallRules().Egress { ports := make([]string, len(r.Ports)) for i, v := range r.Ports { ports[i] = strconv.Itoa(int(v)) @@ -300,20 +310,24 @@ func getFirewallRules(ctx context.Context, cfg *Config) FirewallRules { for _, daddr := range r.To { af, err := getAddressFamily(daddr) if err != nil { - continue + return nil, err + } + protocolString, err := enum.GetStringValue(r.Protocol) + if err != nil { + return nil, err } egressRules = append(egressRules, - fmt.Sprintf("iifname { %s } %s daddr %s %s dport { %s } counter accept comment %q", strings.Join(quotedInputInterfaces, ","), af, daddr, strings.ToLower(r.Protocol), strings.Join(ports, ","), r.Comment)) + fmt.Sprintf("iifname { %s } %s daddr %s %s dport { %s } counter accept comment %q", strings.Join(quotedInputInterfaces, ","), af, daddr, strings.ToLower(*protocolString), strings.Join(ports, ","), r.Comment)) } } - privatePrimaryNetwork := c.getPrivatePrimaryNetwork() - outputInterfacenames := "" - if privatePrimaryNetwork != nil && privatePrimaryNetwork.Vrf != nil { - outputInterfacenames = fmt.Sprintf("oifname { \"vrf%d\", \"vni%d\", \"vlan%d\" }", *privatePrimaryNetwork.Vrf, *privatePrimaryNetwork.Vrf, *privatePrimaryNetwork.Vrf) + privatePrimaryNetwork, err := cfg.Network.PrivatePrimaryNetwork() + if err != nil { + return nil, err } + outputInterfacenames := fmt.Sprintf("oifname { \"vrf%d\", \"vni%d\", \"vlan%d\" }", privatePrimaryNetwork.Vrf, privatePrimaryNetwork.Vrf, privatePrimaryNetwork.Vrf) - for _, r := range c.FirewallRules.Ingress { + for _, r := range cfg.Network.FirewallRules().Ingress { ports := make([]string, len(r.Ports)) for i, v := range r.Ports { ports[i] = strconv.Itoa(int(v)) @@ -328,22 +342,26 @@ func getFirewallRules(ctx context.Context, cfg *Config) FirewallRules { } else if outputInterfacenames != "" { destinationSpec = outputInterfacenames } else { - c.log.Warn("no to address specified but not private primary network present, skipping this rule", "rule", r) + cfg.Log.Warn("no to address specified but not private primary network present, skipping this rule", "rule", r) continue } for _, saddr := range r.From { af, err := getAddressFamily(saddr) if err != nil { - continue + return nil, err } - ingressRules = append(ingressRules, fmt.Sprintf("%s %s saddr %s %s dport { %s } counter accept comment %q", destinationSpec, af, saddr, strings.ToLower(r.Protocol), strings.Join(ports, ","), r.Comment)) + protocolString, err := enum.GetStringValue(r.Protocol) + if err != nil { + return nil, err + } + ingressRules = append(ingressRules, fmt.Sprintf("%s %s saddr %s %s dport { %s } counter accept comment %q", destinationSpec, af, saddr, strings.ToLower(*protocolString), strings.Join(ports, ","), r.Comment)) } } - return FirewallRules{ + return &FirewallRules{ Egress: egressRules, Ingress: ingressRules, - } + }, nil } func getAddressFamily(p string) (string, error) { From eecabd33556a5d7af59e71da7ea98aa5a69b4c1a Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 08:51:13 +0100 Subject: [PATCH 016/102] first nftables test --- pkg/nftables/nftables.go | 32 ++--- pkg/nftables/nftables_test.go | 123 +++++++++++++++++++ pkg/nftables/test/nftrules | 75 +++++++++++ pkg/nftables/test/nftrules_accept_forwarding | 75 +++++++++++ pkg/nftables/test/nftrules_ipv6 | 97 +++++++++++++++ pkg/nftables/test/nftrules_shared | 82 +++++++++++++ pkg/nftables/test/nftrules_vpn | 75 +++++++++++ pkg/nftables/test/nftrules_with_rules | 85 +++++++++++++ pkg/template-renderer/renderer.go | 2 +- 9 files changed, 631 insertions(+), 15 deletions(-) create mode 100644 pkg/nftables/nftables_test.go create mode 100644 pkg/nftables/test/nftrules create mode 100644 pkg/nftables/test/nftrules_accept_forwarding create mode 100644 pkg/nftables/test/nftrules_ipv6 create mode 100644 pkg/nftables/test/nftrules_shared create mode 100644 pkg/nftables/test/nftrules_vpn create mode 100644 pkg/nftables/test/nftrules_with_rules diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 1737af8..c00cda6 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -23,6 +23,8 @@ const ( serviceName = "nftables.service" serviceUnitPath = "/etc/systemd/system/" + serviceName + nftrulesPath = "/etc/nftables/rules" + // Set up additional conntrack zone for DNS traffic. // There was a problem that duplicate packets were registered by conntrack // when packet was leaking from private VRF to the internet VRF. @@ -119,12 +121,12 @@ type ( func Render(ctx context.Context, cfg *Config) (changed bool, err error) { const comment = "generated by os-installer" - snat, err := getSNAT(ctx, cfg) + snat, err := getSNAT(cfg) if err != nil { return false, err } - firewallRules, err := getFirewallRules(ctx, cfg) + firewallRules, err := getFirewallRules(cfg) if err != nil { return false, err } @@ -134,12 +136,12 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { SNAT: snat, ForwardPolicy: string(cfg.ForwardPolicy), FirewallRules: firewallRules, - Input: getInput(ctx, cfg), + Input: getInput(cfg), VPN: cfg.Network.HasVpn(), } if cfg.EnableDNSProxy { - data.DNSProxyDNAT = getDNSProxyDNAT(ctx, cfg) + data.DNSProxyDNAT = getDNSProxyDNAT(cfg) } r, err := renderer.New(&renderer.Config{ @@ -152,7 +154,7 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return false, err } - changed, err = r.Render(ctx, serviceUnitPath) + changed, err = r.Render(ctx, nftrulesPath) if err != nil { return changed, err } @@ -166,7 +168,7 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return } -func getInput(ctx context.Context, cfg *Config) Input { +func getInput(cfg *Config) Input { input := Input{} for _, n := range cfg.Network.AllocationNetworks() { switch n.NetworkType { @@ -177,7 +179,7 @@ func getInput(ctx context.Context, cfg *Config) Input { return input } -func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { +func getSNAT(cfg *Config) ([]SNAT, error) { var ( result []SNAT defaultNetwork *apiv2.MachineNetwork @@ -196,7 +198,7 @@ func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { defaultAF = "ip6" } - primaryNetworks, err := cfg.Network.PrivatePrimaryNetworksPrefixes() + privatePrimaryPrefixes, err := cfg.Network.PrivatePrimaryNetworksPrefixes() if err != nil { return nil, err } @@ -211,6 +213,7 @@ func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { continue } + cfg.Log.Info("getSNAT", "network", n.Network) var ( sources []AddrSpec cmt = fmt.Sprintf("snat (networkid: %s)", n.Network) @@ -218,16 +221,17 @@ func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { vrf = fmt.Sprintf("vrf%d", n.Vrf) ) - for _, nw := range primaryNetworks { - af, err := getAddressFamily(nw) + for _, pfx := range privatePrimaryPrefixes { + af, err := getAddressFamily(pfx) if err != nil { return nil, fmt.Errorf("unable to determine address family: %w", err) } sources = append(sources, AddrSpec{ - Address: nw, + Address: pfx, AddressFamily: af, }) + cfg.Log.Info("getSNAT", "network", n.Network, "prefixes", pfx, "af", af) } s := SNAT{ @@ -249,7 +253,7 @@ func getSNAT(ctx context.Context, cfg *Config) ([]SNAT, error) { return result, nil } -func getDNSProxyDNAT(ctx context.Context, cfg *Config) DNAT { +func getDNSProxyDNAT(cfg *Config) DNAT { svis := []string{} for _, n := range cfg.Network.AllocationNetworks() { switch n.NetworkType { @@ -288,14 +292,14 @@ func getDNSProxyDNAT(ctx context.Context, cfg *Config) DNAT { } } -func getFirewallRules(ctx context.Context, cfg *Config) (*FirewallRules, error) { +func getFirewallRules(cfg *Config) (*FirewallRules, error) { if cfg.Network.FirewallRules() == nil { return &FirewallRules{}, nil } var ( egressRules = []string{"# egress rules specified during firewall creation"} ingressRules = []string{"# ingress rules specified during firewall creation"} - inputInterfaces = getInput(ctx, cfg) + inputInterfaces = getInput(cfg) quotedInputInterfaces []string ) for _, i := range inputInterfaces.InInterfaces { diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go new file mode 100644 index 0000000..faf13f8 --- /dev/null +++ b/pkg/nftables/nftables_test.go @@ -0,0 +1,123 @@ +package nftables + +import ( + "embed" + "log/slog" + "path" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +var ( + //go:embed test + expectedNftableFiles embed.FS + + firewallAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } +) + +func TestRender(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + enableDNSProxy bool + forwardPolicy ForwardPolicy + wantFilePath string + wantErr error + }{ + { + name: "render firewall", + allocation: firewallAllocation, + wantFilePath: "nftrules", + enableDNSProxy: false, + forwardPolicy: ForwardPolicyDrop, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + _, gotErr := Render(t.Context(), &Config{ + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + EnableDNSProxy: tt.enableDNSProxy, + ForwardPolicy: tt.forwardPolicy, + }) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(nftrulesPath) + require.NoError(t, err) + + if diff := cmp.Diff(mustReadExpected(tt.wantFilePath), string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} + +func mustReadExpected(name string) string { + tpl, err := expectedNftableFiles.ReadFile(path.Join("test", name)) + if err != nil { + panic(err) + } + + return string(tpl) +} diff --git a/pkg/nftables/test/nftrules b/pkg/nftables/test/nftrules new file mode 100644 index 0000000..99033c8 --- /dev/null +++ b/pkg/nftables/test/nftrules @@ -0,0 +1,75 @@ +# generated by os-installer +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + + tcp dport ssh ct state new counter accept comment "SSH incoming connections" + iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" + iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy drop; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls)" + } +} diff --git a/pkg/nftables/test/nftrules_accept_forwarding b/pkg/nftables/test/nftrules_accept_forwarding new file mode 100644 index 0000000..7dad2cb --- /dev/null +++ b/pkg/nftables/test/nftrules_accept_forwarding @@ -0,0 +1,75 @@ +# generated by os-installer +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + + tcp dport ssh ct state new counter accept comment "SSH incoming connections" + iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" + iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy accept; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + } +} \ No newline at end of file diff --git a/pkg/nftables/test/nftrules_ipv6 b/pkg/nftables/test/nftrules_ipv6 new file mode 100644 index 0000000..d47b042 --- /dev/null +++ b/pkg/nftables/test/nftrules_ipv6 @@ -0,0 +1,97 @@ +# generated by os-installer +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + + ip6 saddr fd00::/8 tcp dport domain ip6 daddr 2a02:c00:20::1 accept comment "dnat to dns proxy" + ip6 saddr fd00::/8 udp dport domain ip6 daddr 2a02:c00:20::1 accept comment "dnat to dns proxy" + + tcp dport ssh ct state new counter accept comment "SSH incoming connections" + iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" + iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy drop; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + oifname "vlan3981" tcp sport domain ct zone set 3 + oifname "vlan3981" udp sport domain ct zone set 3 + oifname "vlan3982" tcp sport domain ct zone set 3 + oifname "vlan3982" udp sport domain ct zone set 3 + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + + set proxy_dns_servers_v6 { + type ipv6_addr + flags interval + auto-merge + elements = { 2001:4860:4860::8888, 2001:4860:4860::8844, 2606:4700:4700::1111, 2606:4700:4700::1001 } + } + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + ip6 daddr @proxy_dns_servers_v6 iifname "vlan3981" tcp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" + ip6 daddr @proxy_dns_servers_v6 iifname "vlan3981" udp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" + ip6 daddr @proxy_dns_servers_v6 iifname "vlan3982" tcp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" + ip6 daddr @proxy_dns_servers_v6 iifname "vlan3982" udp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + iifname "vlan3981" tcp dport domain ct zone set 3 + iifname "vlan3981" udp dport domain ct zone set 3 + iifname "vlan3982" tcp dport domain ct zone set 3 + iifname "vlan3982" udp dport domain ct zone set 3 + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + oifname "vlan104009" ip6 saddr 2002::/64 ip6 daddr != 2a02:c00:20::1 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" + oifname "vlan104010" ip6 saddr 2002::/64 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + } +} \ No newline at end of file diff --git a/pkg/nftables/test/nftrules_shared b/pkg/nftables/test/nftrules_shared new file mode 100644 index 0000000..f0d0f55 --- /dev/null +++ b/pkg/nftables/test/nftrules_shared @@ -0,0 +1,82 @@ +# generated by os-installer +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + + ip saddr 10.0.0.0/8 tcp dport domain ip daddr 185.1.2.3 accept comment "dnat to dns proxy" + ip saddr 10.0.0.0/8 udp dport domain ip daddr 185.1.2.3 accept comment "dnat to dns proxy" + + tcp dport ssh ct state new counter accept comment "SSH incoming connections" + iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy drop; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + oifname "vlan3982" tcp sport domain ct zone set 3 + oifname "vlan3982" udp sport domain ct zone set 3 + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + ip daddr @proxy_dns_servers iifname "vlan3982" tcp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" + ip daddr @proxy_dns_servers iifname "vlan3982" udp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + iifname "vlan3982" tcp dport domain ct zone set 3 + iifname "vlan3982" udp dport domain ct zone set 3 + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + oifname "vlan3982" ip saddr 10.0.18.0/22 counter masquerade random comment "snat (networkid: storage-net)" + oifname "vlan104009" ip saddr 10.0.18.0/22 ip daddr != 185.1.2.3 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" + } +} \ No newline at end of file diff --git a/pkg/nftables/test/nftrules_vpn b/pkg/nftables/test/nftrules_vpn new file mode 100644 index 0000000..dbefb1b --- /dev/null +++ b/pkg/nftables/test/nftrules_vpn @@ -0,0 +1,75 @@ +# generated by os-installer +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + + iifname "tailscale*" accept comment "Accept tailscale traffic" + iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" + iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy drop; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + } +} \ No newline at end of file diff --git a/pkg/nftables/test/nftrules_with_rules b/pkg/nftables/test/nftrules_with_rules new file mode 100644 index 0000000..97486e7 --- /dev/null +++ b/pkg/nftables/test/nftrules_with_rules @@ -0,0 +1,85 @@ +# generated by os-installer +table inet metal { + chain input { + type filter hook input priority 0; policy drop; + meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" + iifname "lo" counter accept comment "BGP unnumbered" + iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" + iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" + iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" + iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" + + ct state established,related counter accept comment "stateful input" + + tcp dport ssh ct state new counter accept comment "SSH incoming connections" + iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" + iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" + iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" + + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" + counter jump refuse + } + chain forward { + type filter hook forward priority 0; policy drop; + ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" + ct state established,related counter accept comment "stateful forward" + tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" + # egress rules specified during firewall creation + iifname { "vrf3981","vrf3982" } ip daddr 0.0.0.0/0 tcp dport { 443 } counter accept comment "allow apt update" + iifname { "vrf3981","vrf3982" } ip daddr 1.2.3.4/32 tcp dport { 443 } counter accept comment "allow apt update" + iifname { "vrf3981","vrf3982" } ip6 daddr ::/0 tcp dport { 443 } counter accept comment "allow apt update v6" + # ingress rules specified during firewall creation + ip daddr { 100.1.2.3/32, 100.1.2.4/32 } ip saddr 2.3.4.0/24 tcp dport { 22 } counter accept comment "allow incoming ssh" + ip daddr { 100.1.2.3/32, 100.1.2.4/32 } ip saddr 192.168.1.0/16 tcp dport { 22 } counter accept comment "allow incoming ssh" + ip6 daddr { 2001:db8:0:113::/64 } ip6 saddr 2001:db8::1/128 tcp dport { 22 } counter accept comment "allow incoming ssh ipv6" + oifname { "vrf3981", "vni3981", "vlan3981" } ip saddr 1.2.3.0/24 tcp dport { 80,443,8080 } counter accept comment "" + oifname { "vrf3981", "vni3981", "vlan3981" } ip saddr 192.168.0.0/16 tcp dport { 80,443,8080 } counter accept comment "" + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + } + chain output { + type filter hook output priority 0; policy accept; + meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" + oifname "lo" counter accept comment "lo output required e.g. for chrony" + oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" + oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" + + ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" + + ct state established,related counter accept comment "stateful output" + ct state invalid counter drop comment "drop invalid packets" + } + chain output_ct { + type filter hook output priority raw; policy accept; + } + chain refuse { + limit rate 2/minute counter log prefix "nftables-metal-dropped: " + counter drop + } +} +table inet nat { + set proxy_dns_servers { + type ipv4_addr + flags interval + auto-merge + elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } + } + + chain prerouting { + type nat hook prerouting priority 0; policy accept; + } + chain prerouting_ct { + type filter hook prerouting priority raw; policy accept; + } + chain input { + type nat hook input priority 0; policy accept; + } + chain output { + type nat hook output priority 0; policy accept; + } + chain postrouting { + type nat hook postrouting priority 0; policy accept; + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + } +} \ No newline at end of file diff --git a/pkg/template-renderer/renderer.go b/pkg/template-renderer/renderer.go index 9def240..f945e66 100644 --- a/pkg/template-renderer/renderer.go +++ b/pkg/template-renderer/renderer.go @@ -66,7 +66,7 @@ func New(c *Config) (*Renderer, error) { // Render renders the given template to the given destination. // Returns true when the template has changed. func (r *Renderer) Render(ctx context.Context, destFile string) (changed bool, err error) { - r.log.Info("rendering template file", "destination", destFile) + r.log.Info("rendering template file", "destination", destFile, "data", r.data) stagingFile := fmt.Sprintf("%s-%s", destFile, uuid.New().String()) From 6f22f308348c52863cdcaca9bea96b8d1580d8f9 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 09:06:29 +0100 Subject: [PATCH 017/102] next nftables test --- pkg/nftables/nftables_test.go | 10 +++++++++- pkg/nftables/test/nftrules_accept_forwarding | 11 +++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index faf13f8..43f8e2b 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -75,13 +75,21 @@ func TestRender(t *testing.T) { wantErr error }{ { - name: "render firewall", + name: "render firewall, forward drop", allocation: firewallAllocation, wantFilePath: "nftrules", enableDNSProxy: false, forwardPolicy: ForwardPolicyDrop, wantErr: nil, }, + { + name: "render firewall, forward accept", + allocation: firewallAllocation, + wantFilePath: "nftrules_accept_forwarding", + enableDNSProxy: false, + forwardPolicy: ForwardPolicyAccept, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/nftables/test/nftrules_accept_forwarding b/pkg/nftables/test/nftrules_accept_forwarding index 7dad2cb..9ea7b03 100644 --- a/pkg/nftables/test/nftrules_accept_forwarding +++ b/pkg/nftables/test/nftrules_accept_forwarding @@ -16,7 +16,7 @@ table inet metal { iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" counter jump refuse } @@ -25,7 +25,6 @@ table inet metal { ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" ct state established,related counter accept comment "stateful forward" tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - } chain output { type filter hook output priority 0; policy accept; @@ -35,7 +34,7 @@ table inet metal { oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - + ct state established,related counter accept comment "stateful output" ct state invalid counter drop comment "drop invalid packets" } @@ -69,7 +68,7 @@ table inet nat { } chain postrouting { type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls)" } -} \ No newline at end of file +} From de380fd6260586930029a3590c8c487e131fecfc Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 09:14:50 +0100 Subject: [PATCH 018/102] vpn nftables test --- pkg/nftables/nftables_test.go | 79 +++++++++++++++++++++++---- pkg/nftables/test/nftrules_vpn | 10 ++-- pkg/nftables/test/nftrules_with_rules | 10 ++-- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index 43f8e2b..41dbd23 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -63,6 +63,55 @@ var ( }, }, } + firewallWithVPNAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + Vpn: &apiv2.MachineVPN{ + ControlPlaneAddress: "https://test.test.dev", + AuthKey: "abracadabra", + }, + } ) func TestRender(t *testing.T) { @@ -74,22 +123,30 @@ func TestRender(t *testing.T) { wantFilePath string wantErr error }{ + // { + // name: "render firewall, forward drop", + // allocation: firewallAllocation, + // wantFilePath: "nftrules", + // enableDNSProxy: false, + // forwardPolicy: ForwardPolicyDrop, + // wantErr: nil, + // }, + // { + // name: "render firewall, forward accept", + // allocation: firewallAllocation, + // wantFilePath: "nftrules_accept_forwarding", + // enableDNSProxy: false, + // forwardPolicy: ForwardPolicyAccept, + // wantErr: nil, + // }, { - name: "render firewall, forward drop", - allocation: firewallAllocation, - wantFilePath: "nftrules", + name: "render firewall with vpn", + allocation: firewallWithVPNAllocation, + wantFilePath: "nftrules_vpn", enableDNSProxy: false, forwardPolicy: ForwardPolicyDrop, wantErr: nil, }, - { - name: "render firewall, forward accept", - allocation: firewallAllocation, - wantFilePath: "nftrules_accept_forwarding", - enableDNSProxy: false, - forwardPolicy: ForwardPolicyAccept, - wantErr: nil, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/nftables/test/nftrules_vpn b/pkg/nftables/test/nftrules_vpn index dbefb1b..988e3c3 100644 --- a/pkg/nftables/test/nftrules_vpn +++ b/pkg/nftables/test/nftrules_vpn @@ -16,7 +16,7 @@ table inet metal { iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" counter jump refuse } @@ -35,7 +35,7 @@ table inet metal { oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - + ct state established,related counter accept comment "stateful output" ct state invalid counter drop comment "drop invalid packets" } @@ -69,7 +69,7 @@ table inet nat { } chain postrouting { type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls)" } -} \ No newline at end of file +} diff --git a/pkg/nftables/test/nftrules_with_rules b/pkg/nftables/test/nftrules_with_rules index 97486e7..5941306 100644 --- a/pkg/nftables/test/nftrules_with_rules +++ b/pkg/nftables/test/nftrules_with_rules @@ -16,7 +16,7 @@ table inet metal { iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" counter jump refuse } @@ -45,7 +45,7 @@ table inet metal { oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - + ct state established,related counter accept comment "stateful output" ct state invalid counter drop comment "drop invalid packets" } @@ -79,7 +79,7 @@ table inet nat { } chain postrouting { type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet)" + oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls)" } -} \ No newline at end of file +} From 67c6dc42a52433297862f5c0291c3e502af21979 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 09:34:12 +0100 Subject: [PATCH 019/102] shared nftables test, not working yet --- pkg/nftables/nftables_test.go | 167 +++++++++++++++++++++++++++--- pkg/nftables/test/nftrules_shared | 10 +- 2 files changed, 156 insertions(+), 21 deletions(-) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index 41dbd23..6ff425f 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -34,6 +34,8 @@ var ( Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, + // FIXME clarify if this is required + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -63,6 +65,7 @@ var ( }, }, } + firewallWithVPNAllocation = &apiv2.MachineAllocation{ AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Networks: []*apiv2.MachineNetwork{ @@ -79,6 +82,7 @@ var ( Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -112,6 +116,121 @@ var ( AuthKey: "abracadabra", }, } + firewallWithRulesAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + FirewallRules: &apiv2.FirewallRules{ + Egress: []*apiv2.FirewallEgressRule{ + { + Comment: "allow apt update", + Protocol: apiv2.IPProtocol_IP_PROTOCOL_TCP, + Ports: []uint32{443}, + To: []string{"0.0.0.0/0", "1.2.3.4/32"}, + }, + { + Comment: "allow apt update v6", + Protocol: apiv2.IPProtocol_IP_PROTOCOL_TCP, + Ports: []uint32{443}, + To: []string{"::/0"}, + }, + }, + Ingress: []*apiv2.FirewallIngressRule{ + { + Comment: "allow incoming ssh", + Protocol: apiv2.IPProtocol_IP_PROTOCOL_TCP, + Ports: []uint32{22}, + From: []string{"2.3.4.0/24", "192.168.1.0/16"}, + To: []string{"100.1.2.3/32", "100.1.2.4/32"}, + }, + { + Comment: "allow incoming ssh ipv6", + Protocol: apiv2.IPProtocol_IP_PROTOCOL_TCP, + Ports: []uint32{22}, + From: []string{"2001:db8::1/128"}, + To: []string{"2001:db8:0:113::/64"}, + }, + { + Protocol: apiv2.IPProtocol_IP_PROTOCOL_TCP, + Ports: []uint32{80, 443, 8080}, + From: []string{"1.2.3.0/24", "192.168.0.0/16"}, + }, + }, + }, + } + + firewallSharedAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } ) func TestRender(t *testing.T) { @@ -123,22 +242,22 @@ func TestRender(t *testing.T) { wantFilePath string wantErr error }{ - // { - // name: "render firewall, forward drop", - // allocation: firewallAllocation, - // wantFilePath: "nftrules", - // enableDNSProxy: false, - // forwardPolicy: ForwardPolicyDrop, - // wantErr: nil, - // }, - // { - // name: "render firewall, forward accept", - // allocation: firewallAllocation, - // wantFilePath: "nftrules_accept_forwarding", - // enableDNSProxy: false, - // forwardPolicy: ForwardPolicyAccept, - // wantErr: nil, - // }, + { + name: "render firewall, forward drop", + allocation: firewallAllocation, + wantFilePath: "nftrules", + enableDNSProxy: false, + forwardPolicy: ForwardPolicyDrop, + wantErr: nil, + }, + { + name: "render firewall, forward accept", + allocation: firewallAllocation, + wantFilePath: "nftrules_accept_forwarding", + enableDNSProxy: false, + forwardPolicy: ForwardPolicyAccept, + wantErr: nil, + }, { name: "render firewall with vpn", allocation: firewallWithVPNAllocation, @@ -147,6 +266,22 @@ func TestRender(t *testing.T) { forwardPolicy: ForwardPolicyDrop, wantErr: nil, }, + { + name: "render firewall with rules", + allocation: firewallWithRulesAllocation, + wantFilePath: "nftrules_with_rules", + enableDNSProxy: false, + forwardPolicy: ForwardPolicyDrop, + wantErr: nil, + }, + { + name: "render firewall shared", + allocation: firewallSharedAllocation, + wantFilePath: "nftrules_shared", + enableDNSProxy: false, + forwardPolicy: ForwardPolicyDrop, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/nftables/test/nftrules_shared b/pkg/nftables/test/nftrules_shared index f0d0f55..382c127 100644 --- a/pkg/nftables/test/nftrules_shared +++ b/pkg/nftables/test/nftrules_shared @@ -17,7 +17,7 @@ table inet metal { tcp dport ssh ct state new counter accept comment "SSH incoming connections" iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" counter jump refuse } @@ -36,7 +36,7 @@ table inet metal { oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - + ct state established,related counter accept comment "stateful output" ct state invalid counter drop comment "drop invalid packets" } @@ -76,7 +76,7 @@ table inet nat { } chain postrouting { type nat hook postrouting priority 0; policy accept; - oifname "vlan3982" ip saddr 10.0.18.0/22 counter masquerade random comment "snat (networkid: storage-net)" - oifname "vlan104009" ip saddr 10.0.18.0/22 ip daddr != 185.1.2.3 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" + oifname "vlan3982" ip saddr 10.0.18.0/22 counter masquerade random comment "snat (networkid: partition-storage)" + oifname "vlan104009" ip saddr 10.0.18.0/22 ip daddr != 185.1.2.3 counter masquerade random comment "snat (networkid: internet)" } -} \ No newline at end of file +} From b8abd29a6e7f61be8aa9638cc3d7580cb20f826f Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 10:16:24 +0100 Subject: [PATCH 020/102] Shared nftables test --- pkg/nftables/nftables_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index 6ff425f..f177056 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -202,6 +202,7 @@ var ( firewallSharedAllocation = &apiv2.MachineAllocation{ AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "dd429d45-db03-4627-887f-bf7761d376a5", Networks: []*apiv2.MachineNetwork{ { Network: "partition-storage", @@ -209,7 +210,8 @@ var ( Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + Project: new("dd429d45-db03-4627-887f-bf7761d376a5"), + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -278,7 +280,7 @@ func TestRender(t *testing.T) { name: "render firewall shared", allocation: firewallSharedAllocation, wantFilePath: "nftrules_shared", - enableDNSProxy: false, + enableDNSProxy: true, forwardPolicy: ForwardPolicyDrop, wantErr: nil, }, From 0435dc5ab531986a17346aae1a90791e91a46465 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 10:22:30 +0100 Subject: [PATCH 021/102] IPv6 nftables test --- pkg/nftables/nftables_test.go | 57 +++++++++++++++++++++++++++++++++ pkg/nftables/test/nftrules_ipv6 | 10 +++--- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index f177056..052db4a 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -233,6 +233,55 @@ var ( }, }, } + + firewallIPv6Allocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"2002::/64"}, + Ips: []string{"2002::1"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // FIXME clarify if this is required + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"2a02:c00:20::/45"}, + Ips: []string{"2a02:c00:20::1"}, + DestinationPrefixes: []string{"::/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } ) func TestRender(t *testing.T) { @@ -284,6 +333,14 @@ func TestRender(t *testing.T) { forwardPolicy: ForwardPolicyDrop, wantErr: nil, }, + { + name: "render firewall ipv6", + allocation: firewallIPv6Allocation, + wantFilePath: "nftrules_ipv6", + enableDNSProxy: true, + forwardPolicy: ForwardPolicyDrop, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/nftables/test/nftrules_ipv6 b/pkg/nftables/test/nftrules_ipv6 index d47b042..3d7cb48 100644 --- a/pkg/nftables/test/nftrules_ipv6 +++ b/pkg/nftables/test/nftrules_ipv6 @@ -19,7 +19,7 @@ table inet metal { iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - + ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" counter jump refuse } @@ -38,7 +38,7 @@ table inet metal { oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - + ct state established,related counter accept comment "stateful output" ct state invalid counter drop comment "drop invalid packets" } @@ -91,7 +91,7 @@ table inet nat { } chain postrouting { type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip6 saddr 2002::/64 ip6 daddr != 2a02:c00:20::1 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip6 saddr 2002::/64 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" + oifname "vlan104009" ip6 saddr 2002::/64 ip6 daddr != 2a02:c00:20::1 counter masquerade random comment "snat (networkid: internet)" + oifname "vlan104010" ip6 saddr 2002::/64 counter masquerade random comment "snat (networkid: mpls)" } -} \ No newline at end of file +} From 79bbe9ca281587da898e75b4353ae859497efc5e Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 13:17:58 +0100 Subject: [PATCH 022/102] Conversion part 1 of frr --- pkg/frr/frr.firewall.tpl | 106 +++++++ pkg/frr/frr.go | 251 +++++++++++++++ pkg/frr/frr.machine.tpl | 62 ++++ pkg/frr/routemap.go | 380 +++++++++++++++++++++++ pkg/frr/routemap_test.go | 321 +++++++++++++++++++ pkg/frr/test/frr.conf.firewall | 207 ++++++++++++ pkg/frr/test/frr.conf.firewall_dualstack | 217 +++++++++++++ pkg/frr/test/frr.conf.firewall_frr-10 | 211 +++++++++++++ pkg/frr/test/frr.conf.firewall_frr-9 | 207 ++++++++++++ pkg/frr/test/frr.conf.firewall_ipv6 | 208 +++++++++++++ pkg/frr/test/frr.conf.firewall_shared | 126 ++++++++ pkg/frr/test/frr.conf.machine | 59 ++++ pkg/network/network.go | 27 +- pkg/nftables/nftables.go | 2 + 14 files changed, 2382 insertions(+), 2 deletions(-) create mode 100644 pkg/frr/frr.firewall.tpl create mode 100644 pkg/frr/frr.machine.tpl create mode 100644 pkg/frr/routemap.go create mode 100644 pkg/frr/routemap_test.go create mode 100644 pkg/frr/test/frr.conf.firewall create mode 100644 pkg/frr/test/frr.conf.firewall_dualstack create mode 100644 pkg/frr/test/frr.conf.firewall_frr-10 create mode 100644 pkg/frr/test/frr.conf.firewall_frr-9 create mode 100644 pkg/frr/test/frr.conf.firewall_ipv6 create mode 100644 pkg/frr/test/frr.conf.firewall_shared create mode 100644 pkg/frr/test/frr.conf.machine diff --git a/pkg/frr/frr.firewall.tpl b/pkg/frr/frr.firewall.tpl new file mode 100644 index 0000000..97fa8ee --- /dev/null +++ b/pkg/frr/frr.firewall.tpl @@ -0,0 +1,106 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallFRRData*/ -}} +{{- $ASN := .ASN -}} +{{- $RouterId := .RouterID -}} +{{ .Comment }} +frr version {{ .FRRVersion }} +frr defaults datacenter +hostname {{ .Hostname }} +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +{{ range .VRFs -}} +! +vrf vrf{{ .ID}} + vni {{ .VNI }} + exit-vrf +{{ end -}} +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp {{ .ASN }} + bgp router-id {{ .RouterID }} + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +{{- range .VRFs }} +router bgp {{ $ASN }} vrf vrf{{ .ID }} + bgp router-id {{ $RouterId }} +{{- if and (.FRRVersion) (gt .FRRVersion.Major 9) }} + no bgp enforce-first-as +{{- end }} + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + {{- range .ImportVRFNames }} + import vrf {{ . }} + {{- end }} + import vrf route-map vrf{{ .ID }}-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + {{- range .ImportVRFNames }} + import vrf {{ . }} + {{- end }} + import vrf route-map vrf{{ .ID }}-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +{{- end }} +{{- range .VRFs }} + {{- range .IPPrefixLists }} +{{ .AddressFamily }} prefix-list {{ .Name }} {{ .Spec }} + {{- end}} + {{- range .RouteMaps }} +route-map {{ .Name }} {{ .Policy }} {{ .Order }} + {{- range .Entries }} + {{ . }} + {{- end }} + {{- end }} +! +{{- end }} +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 81d471d..e58657e 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -1 +1,252 @@ package frr + +import ( + "context" + "fmt" + "log/slog" + "net/netip" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/old/exec" + "github.com/metal-stack/os-installer/pkg/network" + systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" + "github.com/spf13/afero" + + _ "embed" +) + +const ( + comment = "generated by os-installer" + + serviceName = "frr.service" + serviceUnitPath = "/etc/systemd/system/" + serviceName + + frrConfigPath = "/etc/frr/frr.conf" + + // FRRVersion holds a string that is used in the frr.conf to define the FRR version. + FRRVersion = "8.5" + // TplFirewallFRR defines the name of the template to render FRR configuration to a 'firewall'. + TplFirewallFRR = "frr.firewall.tpl" + // TplMachineFRR defines the name of the template to render FRR configuration to a 'machine'. + TplMachineFRR = "frr.machine.tpl" + // IPPrefixListSeqSeed specifies the initial value for prefix lists sequence number. + IPPrefixListSeqSeed = 100 + // IPPrefixListNoExportSuffix defines the suffix to use for private IP ranges that must not be exported. + IPPrefixListNoExportSuffix = "-no-export" + // RouteMapOrderSeed defines the initial value for route-map order. + RouteMapOrderSeed = 10 + // AddressFamilyIPv4 is the name for this address family for the routing daemon. + AddressFamilyIPv4 = "ip" + // AddressFamilyIPv6 is the name for this address family for the routing daemon. + AddressFamilyIPv6 = "ipv6" +) + +var ( + //go:embed frr.firewall.tpl + firewallTemplateString string + //go:embed frr.machine.tpl + machineTemplateString string +) + +type ( + Config struct { + Log *slog.Logger + Reload bool + Validate bool + + Network *network.Network + + fs afero.Fs + } + + // CommonFRRData contains attributes that are common to FRR configuration of all kind of bare metal servers. + CommonFRRData struct { + ASN int64 + Comment string + FRRVersion string + Hostname string + RouterID string + } + + // MachineFRRData contains attributes required to render frr.conf of bare metal servers that function as 'machine'. + MachineFRRData struct { + CommonFRRData + } + + // FirewallFRRData contains attributes required to render frr.conf of bare metal servers that function as 'firewall'. + FirewallFRRData struct { + CommonFRRData + VRFs []VRF + } + + // VRF represents data required to render VRF information into frr.conf. + VRF struct { + Comment string + ID uint64 + Table uint64 + VNI uint64 + ImportVRFNames []string + IPPrefixLists []IPPrefixList + RouteMaps []RouteMap + FRRVersion *FRR + } + + // IPPrefixList represents 'ip prefix-list' filtering mechanism to be used in combination with route-maps. + IPPrefixList struct { + Name string + Spec string + AddressFamily *apiv2.NetworkAddressFamily + // SourceVRF specifies from which VRF the given prefix list should be imported + SourceVRF string + } + // RouteMap represents a route-map to permit or deny routes. + RouteMap struct { + Name string + Entries []string + Policy string + Order int + } + + FRR struct { + Major uint64 + Minor uint64 + } +) + +// Renders renders frr configuration according to the given input data and reloads the service if necessary +func Render(ctx context.Context, cfg *Config) (changed bool, err error) { + var ( + data any + template string + ) + + if cfg.Network.IsMachine() { + net, err := cfg.Network.PrivatePrimaryNetwork() + if err != nil { + return false, err + } + data = MachineFRRData{ + CommonFRRData: CommonFRRData{ + FRRVersion: FRRVersion, + Hostname: cfg.Network.Hostname(), + Comment: comment, + ASN: int64(net.Asn), + RouterID: routerID(net), + }, + } + template = machineTemplateString + } else { + net, err := cfg.Network.UnderlayNetwork() + if err != nil { + return false, err + } + vrfs, err := assembleVRFs(cfg) + if err != nil { + return false, err + } + + data = FirewallFRRData{ + CommonFRRData: CommonFRRData{ + FRRVersion: FRRVersion, + Hostname: cfg.Network.Hostname(), + Comment: comment, + ASN: int64(net.Asn), + RouterID: routerID(net), + }, + VRFs: vrfs, + } + template = firewallTemplateString + } + + r, err := renderer.New(&renderer.Config{ + Log: cfg.Log, + TemplateString: template, + Data: data, + Fs: cfg.fs, + }) + if err != nil { + return false, err + } + + changed, err = r.Render(ctx, frrConfigPath) + if err != nil { + return changed, err + } + + if cfg.Validate { + if err := validate(frrConfigPath); err != nil { + return changed, err + } + } + + if cfg.Reload && changed { + if err := systemd_renderer.Reload(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + + return +} + +// routerID will calculate the bgp router-id which must only be specified in the ipv6 range. +// returns 0.0.0.0 for erroneous ip addresses and 169.254.255.255 for ipv6 +// TODO prepare machine allocations with ipv6 primary address and tests +func routerID(net *apiv2.MachineNetwork) string { + if len(net.Ips) < 1 { + return "0.0.0.0" + } + ip, err := netip.ParseAddr(net.Ips[0]) + if err != nil { + return "0.0.0.0" + } + if ip.Is4() { + return ip.String() + } + return "169.254.255.255" +} + +// Validate can be used to run validation on FRR configuration using vtysh. +func validate(frrConfigPath string) error { + vtysh := fmt.Sprintf("vtysh --dryrun --inputfile %s", frrConfigPath) + + return exec.NewVerboseCmd("bash", "-c", vtysh, frrConfigPath).Run() +} + +func assembleVRFs(cfg *Config) ([]VRF, error) { + var ( + result []VRF + frr *FRR + ) + + // FIXME do we need to support older frr versions <9 + // if frrVersion != nil { + // frr = &FRR{ + // Major: frrVersion.Major(), + // Minor: frrVersion.Minor(), + // } + // } + + for _, n := range cfg.Network.AllocationNetworks() { + switch n.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, apiv2.NetworkType_NETWORK_TYPE_SUPER, apiv2.NetworkType_NETWORK_TYPE_SUPER_NAMESPACED: + continue + } + + i, err := importRulesForNetwork(cfg, n) + if err != nil { + return nil, err + } + vrf := VRF{ + ID: n.Vrf, + VNI: n.Vrf, + ImportVRFNames: i.ImportVRFs, + IPPrefixLists: i.prefixLists(), + RouteMaps: i.routeMaps(), + FRRVersion: frr, + } + result = append(result, vrf) + } + + return result, nil +} diff --git a/pkg/frr/frr.machine.tpl b/pkg/frr/frr.machine.tpl new file mode 100644 index 0000000..f8f1d61 --- /dev/null +++ b/pkg/frr/frr.machine.tpl @@ -0,0 +1,62 @@ +{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallFRRData*/ -}} +{{- $ASN := .ASN -}} +{{- $RouterId := .RouterID -}} +{{ .Comment }} +frr version {{ .FRRVersion }} +frr defaults datacenter +hostname {{ .Hostname }} +allow-reserved-ranges +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +no zebra nexthop kernel enable +! +router bgp {{ .ASN }} + bgp router-id {{ .RouterID }} + bgp bestpath as-path multipath-relax + neighbor TOR peer-group + neighbor TOR remote-as external + neighbor TOR timers 2 8 + neighbor lan0 interface peer-group TOR + neighbor lan1 interface peer-group TOR + neighbor LOCAL peer-group + neighbor LOCAL remote-as internal + neighbor LOCAL timers 2 8 + neighbor LOCAL route-map local-in in + bgp listen range 0.0.0.0/0 peer-group LOCAL + ! + address-family ipv4 unicast + redistribute connected + redistribute kernel + neighbor TOR route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + redistribute kernel + neighbor TOR route-map only-self-out out + neighbor TOR activate + exit-address-family +! +bgp as-path access-list SELF permit ^$ +! +route-map local-in permit 10 + set weight 32768 +! +route-map only-self-out permit 10 + match as-path SELF +! +route-map only-self-out deny 99 +! diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go new file mode 100644 index 0000000..4c63919 --- /dev/null +++ b/pkg/frr/routemap.go @@ -0,0 +1,380 @@ +package frr + +import ( + "fmt" + "net/netip" + "sort" + "strings" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + mn "github.com/metal-stack/metal-lib/pkg/net" + nwutil "github.com/metal-stack/os-installer/pkg/network" +) + +const ( + // Permit defines an access policy that allows access. + Permit AccessPolicy = "permit" + // Deny defines an access policy that forbids access. + Deny AccessPolicy = "deny" +) + +// AccessPolicy is a type that represents a policy to manage access roles. +type ( + AccessPolicy string + + importPrefix struct { + Prefix netip.Prefix + Policy AccessPolicy + SourceVRF string + } + + importRule struct { + TargetVRF string + ImportVRFs []string + ImportPrefixes []importPrefix + ImportPrefixesNoExport []importPrefix + } + + ImportSettings struct { + ImportPrefixes []importPrefix + ImportPrefixesNoExport []importPrefix + } +) + +func (i *importRule) bySourceVrf() map[string]ImportSettings { + r := map[string]ImportSettings{} + for _, vrf := range i.ImportVRFs { + r[vrf] = ImportSettings{} + } + + for _, pfx := range i.ImportPrefixes { + e := r[pfx.SourceVRF] + e.ImportPrefixes = append(e.ImportPrefixes, pfx) + r[pfx.SourceVRF] = e + } + + for _, pfx := range i.ImportPrefixesNoExport { + e := r[pfx.SourceVRF] + e.ImportPrefixesNoExport = append(e.ImportPrefixesNoExport, pfx) + r[pfx.SourceVRF] = e + } + + return r +} + +func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importRule, error) { + if network.NetworkType == apiv2.NetworkType_NETWORK_TYPE_UNDERLAY { + return nil, nil + } + + vrfName := vrfNameOf(network) + i := importRule{ + TargetVRF: vrfName, + } + privatePrimaryNet, err := cfg.Network.PrivatePrimaryNetwork() + if err != nil { + return nil, err + } + + externalNets := cfg.Network.GetNetworks(apiv2.NetworkType_NETWORK_TYPE_EXTERNAL) + privateSecondarySharedNets := cfg.Network.GetNetworks(mn.PrivateSecondaryShared) + + switch network.NetworkType { + // case mn.PrivatePrimaryUnshared: + // fallthrough + case apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: + // reach out from private network into public networks + i.ImportVRFs = vrfNamesOf(externalNets) + i.ImportPrefixes = getDestinationPrefixes(externalNets) + + // deny public address of default network + defaultNet, err := cfg.Network.GetDefaultRouteNetwork() + if err != nil { + return nil, err + } + for _, ip := range defaultNet.Ips { + if parsed, err := netip.ParseAddr(ip); err == nil { + var bl = 32 + if parsed.Is6() { + bl = 128 + } + i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ + Prefix: netip.PrefixFrom(parsed, bl), + Policy: Deny, + SourceVRF: vrfNameOf(defaultNet), + }) + } + } + + // permit external routes + i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetworks(externalNets)...) + + // reach out from private network into shared private networks + i.ImportVRFs = append(i.ImportVRFs, vrfNamesOf(privateSecondarySharedNets)...) + i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetworks(privateSecondarySharedNets)...) + + // reach out from private network to destination prefixes of private secondays shared networks + for _, n := range privateSecondarySharedNets { + for _, pfx := range n.DestinationPrefixes { + ppfx := netip.MustParsePrefix(pfx) + isThere := false + for _, i := range i.ImportPrefixes { + if i.Prefix == ppfx { + isThere = true + } + } + if !isThere { + i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ + Prefix: ppfx, + Policy: Permit, + SourceVRF: vrfNameOf(n), + }) + } + } + } + case mn.PrivateSecondaryShared: + // reach out from private shared networks into private primary network + i.ImportVRFs = []string{vrfNameOf(privatePrimaryNet)} + i.ImportPrefixes = concatPfxSlices(prefixesOfNetwork(privatePrimaryNet, vrfNameOf(privatePrimaryNet)), prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet))) + + // import destination prefixes of dmz networks from external networks + if len(network.DestinationPrefixes) > 0 { + for _, pfx := range network.DestinationPrefixes { + for _, e := range externalNets { + importExternalNet := false + for _, epfx := range e.DestinationPrefixes { + if pfx == epfx { + importExternalNet = true + i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ + Prefix: netip.MustParsePrefix(pfx), + Policy: Permit, + SourceVRF: vrfNameOf(e), + }) + } + } + if importExternalNet { + i.ImportVRFs = append(i.ImportVRFs, vrfNameOf(e)) + i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetwork(e, vrfNameOf(e))...) + } + } + } + } + case apiv2.NetworkType_NETWORK_TYPE_EXTERNAL: + // reach out from public into private and other public networks + i.ImportVRFs = []string{vrfNameOf(privatePrimaryNet)} + i.ImportPrefixes = prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet)) + + nets := []*apiv2.MachineNetwork{privatePrimaryNet} + + if nwutil.ContainsDefaultRoute(network.DestinationPrefixes) { + for _, r := range privateSecondarySharedNets { + if nwutil.ContainsDefaultRoute(r.DestinationPrefixes) { + nets = append(nets, r) + i.ImportVRFs = append(i.ImportVRFs, vrfNameOf(r)) + } + } + } + i.ImportPrefixesNoExport = prefixesOfNetworks(nets) + } + + return &i, nil +} + +func (i *importRule) prefixLists() []IPPrefixList { + var result []IPPrefixList + seed := IPPrefixListSeqSeed + afs := []apiv2.NetworkAddressFamily{apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4, apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6} + for _, af := range afs { + pfxList := prefixLists(i.ImportPrefixesNoExport, &af, false, seed, i.TargetVRF) + result = append(result, pfxList...) + + seed = IPPrefixListSeqSeed + len(result) + result = append(result, prefixLists(i.ImportPrefixes, &af, true, seed, i.TargetVRF)...) + } + + return result +} + +func prefixLists( + prefixes []importPrefix, + af *apiv2.NetworkAddressFamily, + isExported bool, + seed int, + vrf string, +) []IPPrefixList { + var result []IPPrefixList + for _, p := range prefixes { + if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4 && !p.Prefix.Addr().Is4() { + continue + } + + if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 && !p.Prefix.Addr().Is6() { + continue + } + + specs := p.buildSpecs(seed) + for _, spec := range specs { + // self-importing prefixes is nonsense + if vrf == p.SourceVRF { + continue + } + name := p.name(vrf, isExported) + prefixList := IPPrefixList{ + Name: name, + Spec: spec, + AddressFamily: af, + SourceVRF: p.SourceVRF, + } + result = append(result, prefixList) + } + seed++ + } + return result +} + +func concatPfxSlices(pfxSlices ...[]importPrefix) []importPrefix { + res := []importPrefix{} + for _, pfxSlice := range pfxSlices { + res = append(res, pfxSlice...) + } + return res +} + +func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { + var result []importPrefix + for _, e := range s { + ipp, err := netip.ParsePrefix(e) + if err != nil { + continue + } + result = append(result, importPrefix{ + Prefix: ipp, + Policy: Permit, + SourceVRF: sourceVrf, + }) + } + return result +} + +func getDestinationPrefixes(networks []*apiv2.MachineNetwork) []importPrefix { + var result []importPrefix + for _, network := range networks { + result = append(result, stringSliceToIPPrefix(network.DestinationPrefixes, vrfNameOf(network))...) + } + return result +} + +func prefixesOfNetworks(networks []*apiv2.MachineNetwork) []importPrefix { + var result []importPrefix + for _, network := range networks { + result = append(result, prefixesOfNetwork(network, vrfNameOf(network))...) + } + return result +} + +func prefixesOfNetwork(network *apiv2.MachineNetwork, sourceVrf string) []importPrefix { + return stringSliceToIPPrefix(network.Prefixes, sourceVrf) +} + +func vrfNameOf(n *apiv2.MachineNetwork) string { + return fmt.Sprintf("vrf%d", n.Vrf) +} + +func vrfNamesOf(networks []*apiv2.MachineNetwork) []string { + var result []string + for _, n := range networks { + result = append(result, vrfNameOf(n)) + } + + return result +} + +func byName(prefixLists []IPPrefixList) map[string]IPPrefixList { + byName := map[string]IPPrefixList{} + for _, prefixList := range prefixLists { + if _, isPresent := byName[prefixList.Name]; isPresent { + continue + } + + byName[prefixList.Name] = prefixList + } + + return byName +} + +func (i *importRule) routeMaps() []RouteMap { + var result []RouteMap + + order := RouteMapOrderSeed + byName := byName(i.prefixLists()) + + names := []string{} + for n := range byName { + names = append(names, n) + } + sort.Sort(sort.Reverse(sort.StringSlice(names))) + + for _, n := range names { + prefixList := byName[n] + + matchVrf := fmt.Sprintf("match source-vrf %s", prefixList.SourceVRF) + matchPfxList := fmt.Sprintf("match %s address prefix-list %s", prefixList.AddressFamily, n) + entries := []string{matchVrf, matchPfxList} + if strings.HasSuffix(n, IPPrefixListNoExportSuffix) { + entries = append(entries, "set community additive no-export") + } + + routeMap := RouteMap{ + Name: routeMapName(i.TargetVRF), + Policy: string(Permit), + Order: order, + Entries: entries, + } + order += RouteMapOrderSeed + + result = append(result, routeMap) + } + + routeMap := RouteMap{ + Name: routeMapName(i.TargetVRF), + Policy: string(Deny), + Order: order, + } + + result = append(result, routeMap) + + return result +} + +func routeMapName(vrfName string) string { + return vrfName + "-import-map" +} + +func (i *importPrefix) buildSpecs(seq int) []string { + var result []string + var spec string + + if i.Prefix.Bits() == 0 { + spec = fmt.Sprintf("%s %s", i.Policy, i.Prefix) + + } else { + spec = fmt.Sprintf("seq %d %s %s le %d", seq, i.Policy, i.Prefix, i.Prefix.Addr().BitLen()) + } + + result = append(result, spec) + + return result +} + +func (i *importPrefix) name(targetVrf string, isExported bool) string { + suffix := "" + + if i.Prefix.Addr().Is6() { + suffix = "-ipv6" + } + if !isExported { + suffix += IPPrefixListNoExportSuffix + } + + return fmt.Sprintf("%s-import-from-%s%s", targetVrf, i.SourceVRF, suffix) +} diff --git a/pkg/frr/routemap_test.go b/pkg/frr/routemap_test.go new file mode 100644 index 0000000..4e8e565 --- /dev/null +++ b/pkg/frr/routemap_test.go @@ -0,0 +1,321 @@ +package frr + +import ( + "fmt" + "log/slog" + "net/netip" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +type testnetwork struct { + vrf string + prefixes []importPrefix + destinations []importPrefix +} + +var ( + defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: Permit, SourceVRF: inetVrf} + defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: Permit, SourceVRF: inetVrf} + defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: Permit, SourceVRF: dmzVrf} + externalVrf = "vrf104010" + externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: Permit, SourceVRF: externalVrf} + externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: Permit, SourceVRF: externalVrf} + privateVrf = "vrf3981" + privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: Permit, SourceVRF: privateVrf} + privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: Permit, SourceVRF: privateVrf} + sharedVrf = "vrf3982" + sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: Permit, SourceVRF: sharedVrf} + dmzVrf = "vrf3983" + dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: Permit, SourceVRF: dmzVrf} + inetVrf = "vrf104009" + inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: Permit, SourceVRF: inetVrf} + inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: Permit, SourceVRF: inetVrf} + inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: Permit, SourceVRF: inetVrf} + publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: Deny, SourceVRF: inetVrf} + publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: Deny, SourceVRF: dmzVrf} + publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: Deny, SourceVRF: inetVrf} + + private = testnetwork{ + vrf: privateVrf, + prefixes: []importPrefix{privateNet}, + } + + private6 = testnetwork{ + vrf: privateVrf, + prefixes: []importPrefix{privateNet6}, + } + + inet = testnetwork{ + vrf: inetVrf, + prefixes: []importPrefix{inetNet1, inetNet2}, + destinations: []importPrefix{defaultRoute}, + } + + inet6 = testnetwork{ + vrf: inetVrf, + prefixes: []importPrefix{inetNet6}, + destinations: []importPrefix{defaultRoute6}, + } + dualstack = testnetwork{ + vrf: inetVrf, + prefixes: []importPrefix{inetNet1, inetNet6}, + destinations: []importPrefix{defaultRoute6}, + } + external = testnetwork{ + vrf: externalVrf, + destinations: []importPrefix{externalDestinationNet}, + prefixes: []importPrefix{externalNet}, + } + + shared = testnetwork{ + vrf: sharedVrf, + prefixes: []importPrefix{sharedNet}, + } + + dmz = testnetwork{ + vrf: dmzVrf, + prefixes: []importPrefix{dmzNet}, + destinations: []importPrefix{defaultRouteFromDMZ}, + } +) + +func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { + r := []importPrefix{} + for _, e := range pfxs { + i := e + i.SourceVRF = sourceVrf + r = append(r, i) + } + return r +} + +func Test_importRulesForNetwork(t *testing.T) { + tests := []struct { + name string + input string + want map[string]map[string]ImportSettings + }{ + { + name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", + input: "testdata/firewall.yaml", + want: map[string]map[string]ImportSettings{ + // The target VRF + private.vrf: { + // Imported VRFs with their restrictions + inet.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + }, + external.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + }, + shared.vrf: ImportSettings{ + ImportPrefixes: shared.prefixes, + }, + }, + shared.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), + }, + }, + inet.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: leakFrom(inet.prefixes, private.vrf), + ImportPrefixesNoExport: private.prefixes, + }, + }, + external.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: leakFrom(external.prefixes, private.vrf), + ImportPrefixesNoExport: private.prefixes, + }, + }, + }, + }, + { + name: "firewall of a shared private network (shared/storage firewall)", + input: "testdata/firewall_shared.yaml", + want: map[string]map[string]ImportSettings{ + shared.vrf: { + inet.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + }, + }, + inet.vrf: { + shared.vrf: ImportSettings{ + ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), + ImportPrefixesNoExport: shared.prefixes, + }, + }, + }, + }, + { + name: "firewall of a private network with dmz network and internet (dmz firewall)", + input: "testdata/firewall_dmz.yaml", + want: map[string]map[string]ImportSettings{ + private.vrf: { + inet.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + }, + dmz.vrf: ImportSettings{ + ImportPrefixes: dmz.prefixes, + }, + }, + dmz.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), + }, + inet.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(inet.destinations, inet.prefixes), + }, + }, + inet.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: leakFrom(inet.prefixes, private.vrf), + ImportPrefixesNoExport: private.prefixes, + }, + dmz.vrf: ImportSettings{ + ImportPrefixesNoExport: dmz.prefixes, + }, + }, + }, + }, + { + name: "firewall of a private network with dmz network (dmz app firewall)", + input: "testdata/firewall_dmz_app.yaml", + want: map[string]map[string]ImportSettings{ + private.vrf: { + dmz.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), + }, + }, + dmz.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), + }, + }, + }, + }, + { + name: "firewall of a private network with dmz network and storage (dmz app firewall)", + input: "testdata/firewall_dmz_app_storage.yaml", + want: map[string]map[string]ImportSettings{ + private.vrf: { + shared.vrf: ImportSettings{ + ImportPrefixes: shared.prefixes, + }, + dmz.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), + }, + }, + dmz.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), + }, + }, + shared.vrf: { + private.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), + }, + }, + }, + }, + { + name: "firewall with ipv6 private network and ipv6 internet network", + input: "testdata/firewall_ipv6.yaml", + want: map[string]map[string]ImportSettings{ + private6.vrf: { + inet6.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), + }, + external.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + }, + shared.vrf: ImportSettings{ + ImportPrefixes: shared.prefixes, + }, + }, + shared.vrf: { + private6.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), + }, + }, + inet6.vrf: { + private6.vrf: ImportSettings{ + ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + external.vrf: { + private6.vrf: ImportSettings{ + ImportPrefixes: leakFrom(external.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + }, + }, + { + name: "firewall with ipv6 private network and dualstack internet network", + input: "testdata/firewall_dualstack.yaml", + want: map[string]map[string]ImportSettings{ + private6.vrf: { + inet6.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), + }, + external.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + }, + shared.vrf: ImportSettings{ + ImportPrefixes: shared.prefixes, + }, + }, + shared.vrf: { + private6.vrf: ImportSettings{ + ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), + }, + }, + inet6.vrf: { + private6.vrf: ImportSettings{ + ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + external.vrf: { + private6.vrf: ImportSettings{ + ImportPrefixes: leakFrom(external.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + }, + }, + } + log := slog.Default() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // kb, err := New(log, tt.input) + // require.NoError(t, err) + // err = validate(Firewall) + // if err != nil { + // t.Errorf("%s is not valid: %v", tt.input, err) + // return + // } + for _, network := range kb.Networks { + got, err := importRulesForNetwork(*kb, network) + require.NoError(t, err) + if got == nil { + continue + } + gotBySourceVrf := got.bySourceVrf() + targetVrf := fmt.Sprintf("vrf%d", *network.Vrf) + want := tt.want[targetVrf] + + if !reflect.DeepEqual(gotBySourceVrf, want) { + t.Errorf("importRulesForNetwork() \ntargetVrf: %s \ng: %v, \nw: %v", targetVrf, gotBySourceVrf, want) + } + } + }) + } +} diff --git a/pkg/frr/test/frr.conf.firewall b/pkg/frr/test/frr.conf.firewall new file mode 100644 index 0000000..eba5aae --- /dev/null +++ b/pkg/frr/test/frr.conf.firewall @@ -0,0 +1,207 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname firewall +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +vrf vrf3981 + vni 3981 + exit-vrf +! +vrf vrf3982 + vni 3982 + exit-vrf +! +vrf vrf104009 + vni 104009 + exit-vrf +! +vrf vrf104010 + vni 104010 + exit-vrf +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp 4200003073 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +router bgp 4200003073 vrf vrf3981 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf3982 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104009 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104010 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 +ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.1.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 102 deny 185.1.2.3/32 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 +ip prefix-list vrf3981-import-from-vrf104010 seq 105 permit 100.127.129.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf3982 seq 106 permit 10.0.18.0/22 le 32 +route-map vrf3981-import-map permit 10 + match source-vrf vrf3982 + match ip address prefix-list vrf3981-import-from-vrf3982 +route-map vrf3981-import-map permit 20 + match source-vrf vrf104010 + match ip address prefix-list vrf3981-import-from-vrf104010 +route-map vrf3981-import-map permit 30 + match source-vrf vrf104009 + match ip address prefix-list vrf3981-import-from-vrf104009 +route-map vrf3981-import-map deny 40 +! +ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 +route-map vrf3982-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf3982-import-from-vrf3981 +route-map vrf3982-import-map deny 20 +! +ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf104009-import-from-vrf3981 seq 101 permit 185.1.2.0/24 le 32 +ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.27.0.0/22 le 32 +route-map vrf104009-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981-no-export + set community additive no-export +route-map vrf104009-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981 +route-map vrf104009-import-map deny 30 +! +ip prefix-list vrf104010-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf104010-import-from-vrf3981 seq 101 permit 100.127.129.0/24 le 32 +route-map vrf104010-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981-no-export + set community additive no-export +route-map vrf104010-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981 +route-map vrf104010-import-map deny 30 +! +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/test/frr.conf.firewall_dualstack b/pkg/frr/test/frr.conf.firewall_dualstack new file mode 100644 index 0000000..25e29f5 --- /dev/null +++ b/pkg/frr/test/frr.conf.firewall_dualstack @@ -0,0 +1,217 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname firewall +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +vrf vrf3981 + vni 3981 + exit-vrf +! +vrf vrf3982 + vni 3982 + exit-vrf +! +vrf vrf104009 + vni 104009 + exit-vrf +! +vrf vrf104010 + vni 104010 + exit-vrf +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp 4200003073 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +router bgp 4200003073 vrf vrf3981 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf3982 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104009 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104010 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +ip prefix-list vrf3981-import-from-vrf104010 seq 100 permit 100.127.1.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 101 deny 185.1.2.3/32 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 102 permit 185.1.2.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104010 seq 103 permit 100.127.129.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf3982 seq 104 permit 10.0.18.0/22 le 32 +ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 permit ::/0 +ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 106 deny 2a02:c00:20::1/128 le 128 +ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 107 permit 2a02:c00:20::/45 le 128 +route-map vrf3981-import-map permit 10 + match source-vrf vrf3982 + match ip address prefix-list vrf3981-import-from-vrf3982 +route-map vrf3981-import-map permit 20 + match source-vrf vrf104010 + match ip address prefix-list vrf3981-import-from-vrf104010 +route-map vrf3981-import-map permit 30 + match source-vrf vrf104009 + match ipv6 address prefix-list vrf3981-import-from-vrf104009-ipv6 +route-map vrf3981-import-map permit 40 + match source-vrf vrf104009 + match ip address prefix-list vrf3981-import-from-vrf104009 +route-map vrf3981-import-map deny 50 +! +ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.18.0/22 le 32 +ipv6 prefix-list vrf3982-import-from-vrf3981-ipv6 seq 101 permit 2002::/64 le 128 +route-map vrf3982-import-map permit 10 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf3982-import-from-vrf3981-ipv6 +route-map vrf3982-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf3982-import-from-vrf3981 +route-map vrf3982-import-map deny 30 +! +ip prefix-list vrf104009-import-from-vrf3981 seq 100 permit 185.1.2.0/24 le 32 +ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 +ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6 seq 102 permit 2a02:c00:20::/45 le 128 +route-map vrf104009-import-map permit 10 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6-no-export + set community additive no-export +route-map vrf104009-import-map permit 20 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6 +route-map vrf104009-import-map permit 30 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981 +route-map vrf104009-import-map deny 40 +! +ip prefix-list vrf104010-import-from-vrf3981 seq 100 permit 100.127.129.0/24 le 32 +ipv6 prefix-list vrf104010-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 +route-map vrf104010-import-map permit 10 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf104010-import-from-vrf3981-ipv6-no-export + set community additive no-export +route-map vrf104010-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981 +route-map vrf104010-import-map deny 30 +! +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/test/frr.conf.firewall_frr-10 b/pkg/frr/test/frr.conf.firewall_frr-10 new file mode 100644 index 0000000..6400deb --- /dev/null +++ b/pkg/frr/test/frr.conf.firewall_frr-10 @@ -0,0 +1,211 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname firewall +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +vrf vrf3981 + vni 3981 + exit-vrf +! +vrf vrf3982 + vni 3982 + exit-vrf +! +vrf vrf104009 + vni 104009 + exit-vrf +! +vrf vrf104010 + vni 104010 + exit-vrf +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp 4200003073 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +router bgp 4200003073 vrf vrf3981 + bgp router-id 10.1.0.1 + no bgp enforce-first-as + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf3982 + bgp router-id 10.1.0.1 + no bgp enforce-first-as + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104009 + bgp router-id 10.1.0.1 + no bgp enforce-first-as + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104010 + bgp router-id 10.1.0.1 + no bgp enforce-first-as + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 +ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.1.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 102 deny 185.1.2.3/32 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 +ip prefix-list vrf3981-import-from-vrf104010 seq 105 permit 100.127.129.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf3982 seq 106 permit 10.0.18.0/22 le 32 +route-map vrf3981-import-map permit 10 + match source-vrf vrf3982 + match ip address prefix-list vrf3981-import-from-vrf3982 +route-map vrf3981-import-map permit 20 + match source-vrf vrf104010 + match ip address prefix-list vrf3981-import-from-vrf104010 +route-map vrf3981-import-map permit 30 + match source-vrf vrf104009 + match ip address prefix-list vrf3981-import-from-vrf104009 +route-map vrf3981-import-map deny 40 +! +ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 +route-map vrf3982-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf3982-import-from-vrf3981 +route-map vrf3982-import-map deny 20 +! +ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf104009-import-from-vrf3981 seq 101 permit 185.1.2.0/24 le 32 +ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.27.0.0/22 le 32 +route-map vrf104009-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981-no-export + set community additive no-export +route-map vrf104009-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981 +route-map vrf104009-import-map deny 30 +! +ip prefix-list vrf104010-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf104010-import-from-vrf3981 seq 101 permit 100.127.129.0/24 le 32 +route-map vrf104010-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981-no-export + set community additive no-export +route-map vrf104010-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981 +route-map vrf104010-import-map deny 30 +! +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/test/frr.conf.firewall_frr-9 b/pkg/frr/test/frr.conf.firewall_frr-9 new file mode 100644 index 0000000..eba5aae --- /dev/null +++ b/pkg/frr/test/frr.conf.firewall_frr-9 @@ -0,0 +1,207 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname firewall +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +vrf vrf3981 + vni 3981 + exit-vrf +! +vrf vrf3982 + vni 3982 + exit-vrf +! +vrf vrf104009 + vni 104009 + exit-vrf +! +vrf vrf104010 + vni 104010 + exit-vrf +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp 4200003073 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +router bgp 4200003073 vrf vrf3981 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf3982 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104009 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104010 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 +ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.1.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 102 deny 185.1.2.3/32 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 +ip prefix-list vrf3981-import-from-vrf104010 seq 105 permit 100.127.129.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf3982 seq 106 permit 10.0.18.0/22 le 32 +route-map vrf3981-import-map permit 10 + match source-vrf vrf3982 + match ip address prefix-list vrf3981-import-from-vrf3982 +route-map vrf3981-import-map permit 20 + match source-vrf vrf104010 + match ip address prefix-list vrf3981-import-from-vrf104010 +route-map vrf3981-import-map permit 30 + match source-vrf vrf104009 + match ip address prefix-list vrf3981-import-from-vrf104009 +route-map vrf3981-import-map deny 40 +! +ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 +route-map vrf3982-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf3982-import-from-vrf3981 +route-map vrf3982-import-map deny 20 +! +ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf104009-import-from-vrf3981 seq 101 permit 185.1.2.0/24 le 32 +ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.27.0.0/22 le 32 +route-map vrf104009-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981-no-export + set community additive no-export +route-map vrf104009-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104009-import-from-vrf3981 +route-map vrf104009-import-map deny 30 +! +ip prefix-list vrf104010-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 +ip prefix-list vrf104010-import-from-vrf3981 seq 101 permit 100.127.129.0/24 le 32 +route-map vrf104010-import-map permit 10 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981-no-export + set community additive no-export +route-map vrf104010-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981 +route-map vrf104010-import-map deny 30 +! +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/test/frr.conf.firewall_ipv6 b/pkg/frr/test/frr.conf.firewall_ipv6 new file mode 100644 index 0000000..fb259f9 --- /dev/null +++ b/pkg/frr/test/frr.conf.firewall_ipv6 @@ -0,0 +1,208 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname firewall +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +vrf vrf3981 + vni 3981 + exit-vrf +! +vrf vrf3982 + vni 3982 + exit-vrf +! +vrf vrf104009 + vni 104009 + exit-vrf +! +vrf vrf104010 + vni 104010 + exit-vrf +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp 4200003073 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +router bgp 4200003073 vrf vrf3981 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf104009 + import vrf vrf104010 + import vrf vrf3982 + import vrf route-map vrf3981-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf3982 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104009 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104010 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3981 + import vrf route-map vrf104010-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +ip prefix-list vrf3981-import-from-vrf104010 seq 100 permit 100.127.1.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.129.0/24 le 32 +ip prefix-list vrf3981-import-from-vrf3982 seq 102 permit 10.0.18.0/22 le 32 +ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 permit ::/0 +ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 104 deny 2a02:c00:20::1/128 le 128 +ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 105 permit 2a02:c00:20::/45 le 128 +route-map vrf3981-import-map permit 10 + match source-vrf vrf3982 + match ip address prefix-list vrf3981-import-from-vrf3982 +route-map vrf3981-import-map permit 20 + match source-vrf vrf104010 + match ip address prefix-list vrf3981-import-from-vrf104010 +route-map vrf3981-import-map permit 30 + match source-vrf vrf104009 + match ipv6 address prefix-list vrf3981-import-from-vrf104009-ipv6 +route-map vrf3981-import-map deny 40 +! +ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.18.0/22 le 32 +ipv6 prefix-list vrf3982-import-from-vrf3981-ipv6 seq 101 permit 2002::/64 le 128 +route-map vrf3982-import-map permit 10 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf3982-import-from-vrf3981-ipv6 +route-map vrf3982-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf3982-import-from-vrf3981 +route-map vrf3982-import-map deny 30 +! +ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 +ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6 seq 101 permit 2a02:c00:20::/45 le 128 +route-map vrf104009-import-map permit 10 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6-no-export + set community additive no-export +route-map vrf104009-import-map permit 20 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6 +route-map vrf104009-import-map deny 30 +! +ip prefix-list vrf104010-import-from-vrf3981 seq 100 permit 100.127.129.0/24 le 32 +ipv6 prefix-list vrf104010-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 +route-map vrf104010-import-map permit 10 + match source-vrf vrf3981 + match ipv6 address prefix-list vrf104010-import-from-vrf3981-ipv6-no-export + set community additive no-export +route-map vrf104010-import-map permit 20 + match source-vrf vrf3981 + match ip address prefix-list vrf104010-import-from-vrf3981 +route-map vrf104010-import-map deny 30 +! +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/test/frr.conf.firewall_shared b/pkg/frr/test/frr.conf.firewall_shared new file mode 100644 index 0000000..92e27cb --- /dev/null +++ b/pkg/frr/test/frr.conf.firewall_shared @@ -0,0 +1,126 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname firewall +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +vrf vrf3982 + vni 3982 + exit-vrf +! +vrf vrf104009 + vni 104009 + exit-vrf +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +router bgp 4200003073 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + neighbor FABRIC peer-group + neighbor FABRIC remote-as external + neighbor FABRIC timers 2 8 + neighbor lan0 interface peer-group FABRIC + neighbor lan1 interface peer-group FABRIC + ! + address-family ipv4 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected route-map LOOPBACKS + neighbor FABRIC route-map only-self-out out + neighbor FABRIC activate + exit-address-family + ! + address-family l2vpn evpn + neighbor FABRIC activate + advertise-all-vni + exit-address-family +! +router bgp 4200003073 vrf vrf3982 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf104009 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf104009 + import vrf route-map vrf3982-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +router bgp 4200003073 vrf vrf104009 + bgp router-id 10.1.0.1 + bgp bestpath as-path multipath-relax + ! + address-family ipv4 unicast + redistribute connected + import vrf vrf3982 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + import vrf vrf3982 + import vrf route-map vrf104009-import-map + exit-address-family + ! + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family +! +ip prefix-list vrf3982-import-from-vrf104009 permit 0.0.0.0/0 +ip prefix-list vrf3982-import-from-vrf104009 seq 101 deny 185.1.2.3/32 le 32 +ip prefix-list vrf3982-import-from-vrf104009 seq 102 permit 185.1.2.0/24 le 32 +ip prefix-list vrf3982-import-from-vrf104009 seq 103 permit 185.27.0.0/22 le 32 +route-map vrf3982-import-map permit 10 + match source-vrf vrf104009 + match ip address prefix-list vrf3982-import-from-vrf104009 +route-map vrf3982-import-map deny 20 +! +ip prefix-list vrf104009-import-from-vrf3982-no-export seq 100 permit 10.0.18.0/22 le 32 +ip prefix-list vrf104009-import-from-vrf3982 seq 101 permit 185.1.2.0/24 le 32 +ip prefix-list vrf104009-import-from-vrf3982 seq 102 permit 185.27.0.0/22 le 32 +route-map vrf104009-import-map permit 10 + match source-vrf vrf3982 + match ip address prefix-list vrf104009-import-from-vrf3982-no-export + set community additive no-export +route-map vrf104009-import-map permit 20 + match source-vrf vrf3982 + match ip address prefix-list vrf104009-import-from-vrf3982 +route-map vrf104009-import-map deny 30 +! +route-map only-self-out permit 10 + match as-path SELF +route-map only-self-out deny 20 +! +route-map LOOPBACKS permit 10 + match interface lo +! +bgp as-path access-list SELF permit ^$ +! +line vty +! diff --git a/pkg/frr/test/frr.conf.machine b/pkg/frr/test/frr.conf.machine new file mode 100644 index 0000000..686462e --- /dev/null +++ b/pkg/frr/test/frr.conf.machine @@ -0,0 +1,59 @@ +# generated by os-installer +frr version 8.5 +frr defaults datacenter +hostname machine +allow-reserved-ranges +! +log syslog debugging +debug bgp updates +debug bgp nht +debug bgp update-groups +debug bgp zebra +! +interface lan0 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +interface lan1 + ipv6 nd ra-interval 6 + no ipv6 nd suppress-ra +! +no zebra nexthop kernel enable +! +router bgp 4200003073 + bgp router-id 10.0.17.2 + bgp bestpath as-path multipath-relax + neighbor TOR peer-group + neighbor TOR remote-as external + neighbor TOR timers 2 8 + neighbor lan0 interface peer-group TOR + neighbor lan1 interface peer-group TOR + neighbor LOCAL peer-group + neighbor LOCAL remote-as internal + neighbor LOCAL timers 2 8 + neighbor LOCAL route-map local-in in + bgp listen range 0.0.0.0/0 peer-group LOCAL + ! + address-family ipv4 unicast + redistribute connected + redistribute kernel + neighbor TOR route-map only-self-out out + exit-address-family + ! + address-family ipv6 unicast + redistribute connected + redistribute kernel + neighbor TOR route-map only-self-out out + neighbor TOR activate + exit-address-family +! +bgp as-path access-list SELF permit ^$ +! +route-map local-in permit 10 + set weight 32768 +! +route-map only-self-out permit 10 + match as-path SELF +! +route-map only-self-out deny 99 +! diff --git a/pkg/network/network.go b/pkg/network/network.go index e1f420e..e50d2c8 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -46,6 +46,10 @@ func (n *Network) MTU() int { return mtuMachine } +func (n *Network) Hostname() string { + return n.allocation.Hostname +} + func (n *Network) IsMachine() bool { return n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE } @@ -92,6 +96,15 @@ func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { return } +func (n *Network) UnderlayNetwork() (*apiv2.MachineNetwork, error) { + for _, nw := range n.allocation.Networks { + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_UNDERLAY { + return nw, nil + } + } + return nil, fmt.Errorf("no underlay network present in network allocation") +} + func (n *Network) PrivatePrimaryNetwork() (*apiv2.MachineNetwork, error) { for _, nw := range n.allocation.Networks { if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD { @@ -211,10 +224,20 @@ func (n *Network) EVPNIfaces() (ifaces []EvpnIface, err error) { return } +func (n *Network) GetNetworks(networkType apiv2.NetworkType) []*apiv2.MachineNetwork { + var networks []*apiv2.MachineNetwork + for _, nw := range n.allocation.Networks { + if nw.NetworkType == networkType { + networks = append(networks, nw) + } + } + return networks +} + func (n *Network) GetDefaultRouteNetwork() (*apiv2.MachineNetwork, error) { for _, nw := range n.allocation.Networks { if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_EXTERNAL { - if containsDefaultRoute(nw.DestinationPrefixes) { + if ContainsDefaultRoute(nw.DestinationPrefixes) { return nw, nil } } @@ -222,7 +245,7 @@ func (n *Network) GetDefaultRouteNetwork() (*apiv2.MachineNetwork, error) { return nil, fmt.Errorf("no network which provides a default route found") } -func containsDefaultRoute(prefixes []string) bool { +func ContainsDefaultRoute(prefixes []string) bool { for _, prefix := range prefixes { if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { return true diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index c00cda6..50fdc97 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -159,6 +159,8 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return changed, err } + // FIXME validate generated rule file before reloading + if cfg.Reload && changed { if err := systemd_renderer.Reload(ctx, cfg.Log, serviceName); err != nil { return changed, err From 2c13f412f8098f4a9baeec928576d69790916c8b Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 10 Mar 2026 14:07:28 +0100 Subject: [PATCH 023/102] Add frr test, does not compile yet --- pkg/frr/frr_test.go | 362 ++++++++++++++++++++++++++++++++++++++++++++ pkg/frr/routemap.go | 5 +- 2 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 pkg/frr/frr_test.go diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go new file mode 100644 index 0000000..f35021f --- /dev/null +++ b/pkg/frr/frr_test.go @@ -0,0 +1,362 @@ +package frr + +import ( + "embed" + "log/slog" + "path" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +var ( + //go:embed test + expectedFrrFiles embed.FS + + firewallAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // FIXME clarify if this is required + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } + + firewallFrr9Allocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } + + firewallFrr10Allocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } + + firewallSharedAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "dd429d45-db03-4627-887f-bf7761d376a5", + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Project: new("dd429d45-db03-4627-887f-bf7761d376a5"), + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } + + firewallIPv6Allocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"2002::/64"}, + Ips: []string{"2002::1"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // FIXME clarify if this is required + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"2a02:c00:20::/45"}, + Ips: []string{"2a02:c00:20::1"}, + DestinationPrefixes: []string{"::/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Ips: []string{"10.1.0.1"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/22"}, + Ips: []string{"100.127.129.1"}, + Vrf: 104010, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet-v6", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2001::4"}, + }, + }, + } + + machineAllocation = &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + // FIXME clarify if this is required + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + } +) + +func TestRender(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + wantFilePath string + wantErr error + }{ + { + name: "render firewall", + allocation: firewallAllocation, + wantFilePath: "frr.conf.firewall", + wantErr: nil, + }, + { + name: "render firewall, dualstack", + allocation: firewallAllocation, + wantFilePath: "frr.conf.firewall_dualstack", + wantErr: nil, + }, + { + name: "render firewall frr-9", + allocation: firewallFrr9Allocation, + wantFilePath: "frr.conf.firewall_frr-9", + wantErr: nil, + }, + { + name: "render firewall frr-10", + allocation: firewallFrr10Allocation, + wantFilePath: "frr.conf.firewall_frr-10", + wantErr: nil, + }, + { + name: "render firewall shared", + allocation: firewallSharedAllocation, + wantFilePath: "frr.conf.firewall_shared", + wantErr: nil, + }, + { + name: "render firewall ipv6", + allocation: firewallIPv6Allocation, + wantFilePath: "frr.conf.firewall_ipv6", + wantErr: nil, + }, + { + name: "render machine", + allocation: machineAllocation, + wantFilePath: "frr.conf.machine", + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + _, gotErr := Render(t.Context(), &Config{ + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + }) + + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(frrConfigPath) + require.NoError(t, err) + + if diff := cmp.Diff(mustReadExpected(tt.wantFilePath), string(content)); diff != "" { + t.Errorf("diff (+got -want):\n%s", diff) + } + }) + } +} + +func mustReadExpected(name string) string { + tpl, err := expectedFrrFiles.ReadFile(path.Join("test", name)) + if err != nil { + panic(err) + } + + return string(tpl) +} diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 4c63919..0ecc45f 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -80,8 +80,9 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR privateSecondarySharedNets := cfg.Network.GetNetworks(mn.PrivateSecondaryShared) switch network.NetworkType { - // case mn.PrivatePrimaryUnshared: - // fallthrough + case mn.PrivatePrimaryUnshared: + fallthrough + // case mn.PrivatePrimaryShared: case apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: // reach out from private network into public networks i.ImportVRFs = vrfNamesOf(externalNets) From d3e4159ed46004e817eb6662bac645f968e36668 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 08:04:29 +0100 Subject: [PATCH 024/102] Network names --- pkg/interfaces/interfaces_test.go | 6 ++++++ pkg/interfaces/templates/svi.netdev.tpl | 1 + pkg/interfaces/templates/svi.network.tpl | 1 + pkg/interfaces/templates/vrf.netdev.tpl | 1 + pkg/interfaces/templates/vrf.network.tpl | 1 + pkg/interfaces/templates/vxlan.netdev.tpl | 1 + pkg/interfaces/templates/vxlan.network.tpl | 1 + pkg/interfaces/test/firewall/30-svi-3981.netdev | 1 + pkg/interfaces/test/firewall/30-svi-3981.network | 1 + pkg/interfaces/test/firewall/30-vrf-3981.netdev | 1 + pkg/interfaces/test/firewall/30-vrf-3981.network | 1 + pkg/interfaces/test/firewall/30-vxlan-3981.netdev | 1 + pkg/interfaces/test/firewall/30-vxlan-3981.network | 1 + pkg/interfaces/test/firewall/31-svi-3982.netdev | 1 + pkg/interfaces/test/firewall/31-svi-3982.network | 1 + pkg/interfaces/test/firewall/31-vrf-3982.netdev | 1 + pkg/interfaces/test/firewall/31-vrf-3982.network | 1 + pkg/interfaces/test/firewall/31-vxlan-3982.netdev | 1 + pkg/interfaces/test/firewall/31-vxlan-3982.network | 1 + pkg/interfaces/test/firewall/32-svi-104009.netdev | 1 + pkg/interfaces/test/firewall/32-svi-104009.network | 1 + pkg/interfaces/test/firewall/32-vrf-104009.netdev | 1 + pkg/interfaces/test/firewall/32-vrf-104009.network | 1 + .../test/firewall/32-vxlan-104009.netdev | 1 + .../test/firewall/32-vxlan-104009.network | 1 + pkg/interfaces/test/firewall/33-svi-104010.netdev | 1 + pkg/interfaces/test/firewall/33-svi-104010.network | 1 + pkg/interfaces/test/firewall/33-vrf-104010.netdev | 1 + pkg/interfaces/test/firewall/33-vrf-104010.network | 1 + .../test/firewall/33-vxlan-104010.netdev | 1 + .../test/firewall/33-vxlan-104010.network | 1 + pkg/network/network.go | 14 ++++++++------ 32 files changed, 44 insertions(+), 6 deletions(-) diff --git a/pkg/interfaces/interfaces_test.go b/pkg/interfaces/interfaces_test.go index 421488f..49bbfd0 100644 --- a/pkg/interfaces/interfaces_test.go +++ b/pkg/interfaces/interfaces_test.go @@ -50,30 +50,36 @@ var ( AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Networks: []*apiv2.MachineNetwork{ { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, Ips: []string{"10.0.16.2"}, Vrf: 3981, }, { + Network: "partition-storage", NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, Ips: []string{"10.0.18.2"}, Vrf: 3982, }, { + Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"185.1.2.3"}, Vrf: 104009, }, { + Network: "underlay", NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, Ips: []string{"10.1.0.1"}, }, { + Network: "mpls", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"100.127.129.1"}, Vrf: 104010, }, { + Network: "internet-v6", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"2001::4"}, }, diff --git a/pkg/interfaces/templates/svi.netdev.tpl b/pkg/interfaces/templates/svi.netdev.tpl index 235494e..123c410 100644 --- a/pkg/interfaces/templates/svi.netdev.tpl +++ b/pkg/interfaces/templates/svi.netdev.tpl @@ -1,4 +1,5 @@ # {{ .Comment }} +# network: {{ .EVPNIface.Network }} [NetDev] Name=vlan{{ .EVPNIface.VrfID }} Kind=vlan diff --git a/pkg/interfaces/templates/svi.network.tpl b/pkg/interfaces/templates/svi.network.tpl index 3ad20ca..b059829 100644 --- a/pkg/interfaces/templates/svi.network.tpl +++ b/pkg/interfaces/templates/svi.network.tpl @@ -1,4 +1,5 @@ # {{ .Comment }} +# network: {{ .EVPNIface.Network }} [Match] Name=vlan{{ .EVPNIface.VrfID }} diff --git a/pkg/interfaces/templates/vrf.netdev.tpl b/pkg/interfaces/templates/vrf.netdev.tpl index 12d5007..9e5e840 100644 --- a/pkg/interfaces/templates/vrf.netdev.tpl +++ b/pkg/interfaces/templates/vrf.netdev.tpl @@ -1,4 +1,5 @@ # {{ .Comment }} +# network: {{ .EVPNIface.Network }} [NetDev] Name=vrf{{ .EVPNIface.VrfID }} Kind=vrf diff --git a/pkg/interfaces/templates/vrf.network.tpl b/pkg/interfaces/templates/vrf.network.tpl index 3c08199..a24e0bc 100644 --- a/pkg/interfaces/templates/vrf.network.tpl +++ b/pkg/interfaces/templates/vrf.network.tpl @@ -1,3 +1,4 @@ # {{ .Comment }} +# network: {{ .EVPNIface.Network }} [Match] Name=vrf{{ .EVPNIface.VrfID }} diff --git a/pkg/interfaces/templates/vxlan.netdev.tpl b/pkg/interfaces/templates/vxlan.netdev.tpl index d5670d1..23e7d38 100644 --- a/pkg/interfaces/templates/vxlan.netdev.tpl +++ b/pkg/interfaces/templates/vxlan.netdev.tpl @@ -1,4 +1,5 @@ # {{ .Comment }} +# network: {{ .EVPNIface.Network }} [NetDev] Name=vni{{ .EVPNIface.VrfID }} Kind=vxlan diff --git a/pkg/interfaces/templates/vxlan.network.tpl b/pkg/interfaces/templates/vxlan.network.tpl index d6135e6..b18f4fc 100644 --- a/pkg/interfaces/templates/vxlan.network.tpl +++ b/pkg/interfaces/templates/vxlan.network.tpl @@ -1,4 +1,5 @@ # {{ .Comment }} +# network: {{ .EVPNIface.Network }} [Match] Name=vni{{ .EVPNIface.VrfID }} diff --git a/pkg/interfaces/test/firewall/30-svi-3981.netdev b/pkg/interfaces/test/firewall/30-svi-3981.netdev index 97ccc24..e281323 100644 --- a/pkg/interfaces/test/firewall/30-svi-3981.netdev +++ b/pkg/interfaces/test/firewall/30-svi-3981.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: 379d294d-22e8-4aed-82e1-62c6c2f08d6a [NetDev] Name=vlan3981 Kind=vlan diff --git a/pkg/interfaces/test/firewall/30-svi-3981.network b/pkg/interfaces/test/firewall/30-svi-3981.network index e41cbfe..e57cd63 100644 --- a/pkg/interfaces/test/firewall/30-svi-3981.network +++ b/pkg/interfaces/test/firewall/30-svi-3981.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: 379d294d-22e8-4aed-82e1-62c6c2f08d6a [Match] Name=vlan3981 diff --git a/pkg/interfaces/test/firewall/30-vrf-3981.netdev b/pkg/interfaces/test/firewall/30-vrf-3981.netdev index 4e0579f..e58d053 100644 --- a/pkg/interfaces/test/firewall/30-vrf-3981.netdev +++ b/pkg/interfaces/test/firewall/30-vrf-3981.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: 379d294d-22e8-4aed-82e1-62c6c2f08d6a [NetDev] Name=vrf3981 Kind=vrf diff --git a/pkg/interfaces/test/firewall/30-vrf-3981.network b/pkg/interfaces/test/firewall/30-vrf-3981.network index 24d9edb..46bf5b3 100644 --- a/pkg/interfaces/test/firewall/30-vrf-3981.network +++ b/pkg/interfaces/test/firewall/30-vrf-3981.network @@ -1,3 +1,4 @@ # generated by os-installer +# network: 379d294d-22e8-4aed-82e1-62c6c2f08d6a [Match] Name=vrf3981 diff --git a/pkg/interfaces/test/firewall/30-vxlan-3981.netdev b/pkg/interfaces/test/firewall/30-vxlan-3981.netdev index 1f92080..a413ac8 100644 --- a/pkg/interfaces/test/firewall/30-vxlan-3981.netdev +++ b/pkg/interfaces/test/firewall/30-vxlan-3981.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: 379d294d-22e8-4aed-82e1-62c6c2f08d6a [NetDev] Name=vni3981 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/30-vxlan-3981.network b/pkg/interfaces/test/firewall/30-vxlan-3981.network index be6ec17..2645337 100644 --- a/pkg/interfaces/test/firewall/30-vxlan-3981.network +++ b/pkg/interfaces/test/firewall/30-vxlan-3981.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: 379d294d-22e8-4aed-82e1-62c6c2f08d6a [Match] Name=vni3981 diff --git a/pkg/interfaces/test/firewall/31-svi-3982.netdev b/pkg/interfaces/test/firewall/31-svi-3982.netdev index 3d44d50..17d489f 100644 --- a/pkg/interfaces/test/firewall/31-svi-3982.netdev +++ b/pkg/interfaces/test/firewall/31-svi-3982.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: partition-storage [NetDev] Name=vlan3982 Kind=vlan diff --git a/pkg/interfaces/test/firewall/31-svi-3982.network b/pkg/interfaces/test/firewall/31-svi-3982.network index 7777f59..2608d7e 100644 --- a/pkg/interfaces/test/firewall/31-svi-3982.network +++ b/pkg/interfaces/test/firewall/31-svi-3982.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: partition-storage [Match] Name=vlan3982 diff --git a/pkg/interfaces/test/firewall/31-vrf-3982.netdev b/pkg/interfaces/test/firewall/31-vrf-3982.netdev index 76451be..cc70188 100644 --- a/pkg/interfaces/test/firewall/31-vrf-3982.netdev +++ b/pkg/interfaces/test/firewall/31-vrf-3982.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: partition-storage [NetDev] Name=vrf3982 Kind=vrf diff --git a/pkg/interfaces/test/firewall/31-vrf-3982.network b/pkg/interfaces/test/firewall/31-vrf-3982.network index 1e08850..9293265 100644 --- a/pkg/interfaces/test/firewall/31-vrf-3982.network +++ b/pkg/interfaces/test/firewall/31-vrf-3982.network @@ -1,3 +1,4 @@ # generated by os-installer +# network: partition-storage [Match] Name=vrf3982 diff --git a/pkg/interfaces/test/firewall/31-vxlan-3982.netdev b/pkg/interfaces/test/firewall/31-vxlan-3982.netdev index 2952e24..9559950 100644 --- a/pkg/interfaces/test/firewall/31-vxlan-3982.netdev +++ b/pkg/interfaces/test/firewall/31-vxlan-3982.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: partition-storage [NetDev] Name=vni3982 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/31-vxlan-3982.network b/pkg/interfaces/test/firewall/31-vxlan-3982.network index 6b07018..70b855c 100644 --- a/pkg/interfaces/test/firewall/31-vxlan-3982.network +++ b/pkg/interfaces/test/firewall/31-vxlan-3982.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: partition-storage [Match] Name=vni3982 diff --git a/pkg/interfaces/test/firewall/32-svi-104009.netdev b/pkg/interfaces/test/firewall/32-svi-104009.netdev index 636a93e..a4b9d7f 100644 --- a/pkg/interfaces/test/firewall/32-svi-104009.netdev +++ b/pkg/interfaces/test/firewall/32-svi-104009.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: internet [NetDev] Name=vlan104009 Kind=vlan diff --git a/pkg/interfaces/test/firewall/32-svi-104009.network b/pkg/interfaces/test/firewall/32-svi-104009.network index 9c7a6ec..9daaae6 100644 --- a/pkg/interfaces/test/firewall/32-svi-104009.network +++ b/pkg/interfaces/test/firewall/32-svi-104009.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: internet [Match] Name=vlan104009 diff --git a/pkg/interfaces/test/firewall/32-vrf-104009.netdev b/pkg/interfaces/test/firewall/32-vrf-104009.netdev index 3dc718c..1f9316b 100644 --- a/pkg/interfaces/test/firewall/32-vrf-104009.netdev +++ b/pkg/interfaces/test/firewall/32-vrf-104009.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: internet [NetDev] Name=vrf104009 Kind=vrf diff --git a/pkg/interfaces/test/firewall/32-vrf-104009.network b/pkg/interfaces/test/firewall/32-vrf-104009.network index 886c7e3..da70561 100644 --- a/pkg/interfaces/test/firewall/32-vrf-104009.network +++ b/pkg/interfaces/test/firewall/32-vrf-104009.network @@ -1,3 +1,4 @@ # generated by os-installer +# network: internet [Match] Name=vrf104009 diff --git a/pkg/interfaces/test/firewall/32-vxlan-104009.netdev b/pkg/interfaces/test/firewall/32-vxlan-104009.netdev index b55af06..1f0be16 100644 --- a/pkg/interfaces/test/firewall/32-vxlan-104009.netdev +++ b/pkg/interfaces/test/firewall/32-vxlan-104009.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: internet [NetDev] Name=vni104009 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/32-vxlan-104009.network b/pkg/interfaces/test/firewall/32-vxlan-104009.network index 25c939c..1db18e0 100644 --- a/pkg/interfaces/test/firewall/32-vxlan-104009.network +++ b/pkg/interfaces/test/firewall/32-vxlan-104009.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: internet [Match] Name=vni104009 diff --git a/pkg/interfaces/test/firewall/33-svi-104010.netdev b/pkg/interfaces/test/firewall/33-svi-104010.netdev index f825084..7e80ab2 100644 --- a/pkg/interfaces/test/firewall/33-svi-104010.netdev +++ b/pkg/interfaces/test/firewall/33-svi-104010.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: mpls [NetDev] Name=vlan104010 Kind=vlan diff --git a/pkg/interfaces/test/firewall/33-svi-104010.network b/pkg/interfaces/test/firewall/33-svi-104010.network index f5a199c..4665e50 100644 --- a/pkg/interfaces/test/firewall/33-svi-104010.network +++ b/pkg/interfaces/test/firewall/33-svi-104010.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: mpls [Match] Name=vlan104010 diff --git a/pkg/interfaces/test/firewall/33-vrf-104010.netdev b/pkg/interfaces/test/firewall/33-vrf-104010.netdev index 1ababb5..d3f564d 100644 --- a/pkg/interfaces/test/firewall/33-vrf-104010.netdev +++ b/pkg/interfaces/test/firewall/33-vrf-104010.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: mpls [NetDev] Name=vrf104010 Kind=vrf diff --git a/pkg/interfaces/test/firewall/33-vrf-104010.network b/pkg/interfaces/test/firewall/33-vrf-104010.network index 46b0ee0..51e2fc5 100644 --- a/pkg/interfaces/test/firewall/33-vrf-104010.network +++ b/pkg/interfaces/test/firewall/33-vrf-104010.network @@ -1,3 +1,4 @@ # generated by os-installer +# network: mpls [Match] Name=vrf104010 diff --git a/pkg/interfaces/test/firewall/33-vxlan-104010.netdev b/pkg/interfaces/test/firewall/33-vxlan-104010.netdev index c5ea341..6e3336f 100644 --- a/pkg/interfaces/test/firewall/33-vxlan-104010.netdev +++ b/pkg/interfaces/test/firewall/33-vxlan-104010.netdev @@ -1,4 +1,5 @@ # generated by os-installer +# network: mpls [NetDev] Name=vni104010 Kind=vxlan diff --git a/pkg/interfaces/test/firewall/33-vxlan-104010.network b/pkg/interfaces/test/firewall/33-vxlan-104010.network index 3429518..bdb5b09 100644 --- a/pkg/interfaces/test/firewall/33-vxlan-104010.network +++ b/pkg/interfaces/test/firewall/33-vxlan-104010.network @@ -1,4 +1,5 @@ # generated by os-installer +# network: mpls [Match] Name=vni104010 diff --git a/pkg/network/network.go b/pkg/network/network.go index e50d2c8..77529fc 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -26,9 +26,10 @@ type ( } EvpnIface struct { - CIDRs []string - VlanID int - VrfID uint64 + Network string + CIDRs []string + VlanID int + VrfID uint64 } ) @@ -210,9 +211,10 @@ func (n *Network) EVPNIfaces() (ifaces []EvpnIface, err error) { } ifaces = append(ifaces, EvpnIface{ - CIDRs: cidrs, - VlanID: vlanOffset + i, - VrfID: nw.Vrf, + Network: nw.Network, + CIDRs: cidrs, + VlanID: vlanOffset + i, + VrfID: nw.Vrf, }) } } From 66fd5b50d43e5d56308353c9627d489a0bb416f5 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 08:13:33 +0100 Subject: [PATCH 025/102] Remove unused --- old/exec/doc.go | 4 - old/net/applier.go | 128 ---------- old/net/applier_test.go | 33 --- old/net/doc.go | 2 - old/net/reloader.go | 51 ---- old/net/validator.go | 6 - old/network/doc.go | 4 - old/network/interfaces.go | 165 ------------- old/network/interfaces_test.go | 96 -------- old/network/nftables_test.go | 89 ------- old/network/systemd.go | 82 ------- old/network/template.go | 23 -- old/network/testdata/firewall.yaml | 182 --------------- old/network/testdata/firewall_dmz.yaml | 164 ------------- old/network/testdata/firewall_dmz_app.yaml | 141 ----------- .../testdata/firewall_dmz_app_storage.yaml | 160 ------------- old/network/testdata/firewall_dualstack.yaml | 183 --------------- old/network/testdata/firewall_ipv6.yaml | 181 --------------- old/network/testdata/firewall_shared.yaml | 141 ----------- old/network/testdata/firewall_vpn.yaml | 184 --------------- old/network/testdata/firewall_with_rules.yaml | 213 ----------------- old/network/testdata/frr.conf.firewall | 208 ----------------- old/network/testdata/frr.conf.firewall_dmz | 180 --------------- .../testdata/frr.conf.firewall_dmz_app | 121 ---------- .../frr.conf.firewall_dmz_app_storage | 159 ------------- .../testdata/frr.conf.firewall_dualstack | 218 ------------------ old/network/testdata/frr.conf.firewall_frr-10 | 212 ----------------- old/network/testdata/frr.conf.firewall_frr-9 | 208 ----------------- old/network/testdata/frr.conf.firewall_ipv6 | 209 ----------------- old/network/testdata/frr.conf.firewall_shared | 127 ---------- old/network/testdata/frr.conf.machine | 60 ----- old/network/testdata/machine.yaml | 84 ------- old/network/testdata/nftrules | 76 ------ .../testdata/nftrules_accept_forwarding | 76 ------ old/network/testdata/nftrules_dmz | 91 -------- old/network/testdata/nftrules_dmz_app | 89 ------- old/network/testdata/nftrules_ipv6 | 98 -------- old/network/testdata/nftrules_shared | 83 ------- old/network/testdata/nftrules_vpn | 76 ------ old/network/testdata/nftrules_with_rules | 86 ------- old/network/tpl/frr.firewall.tpl | 106 --------- old/network/tpl/frr.machine.tpl | 62 ----- old/network/tpl/nftrules.tpl | 131 ----------- 43 files changed, 4992 deletions(-) delete mode 100644 old/exec/doc.go delete mode 100644 old/net/applier.go delete mode 100644 old/net/applier_test.go delete mode 100644 old/net/doc.go delete mode 100644 old/net/reloader.go delete mode 100644 old/net/validator.go delete mode 100644 old/network/doc.go delete mode 100644 old/network/interfaces.go delete mode 100644 old/network/interfaces_test.go delete mode 100644 old/network/nftables_test.go delete mode 100644 old/network/systemd.go delete mode 100644 old/network/template.go delete mode 100644 old/network/testdata/firewall.yaml delete mode 100644 old/network/testdata/firewall_dmz.yaml delete mode 100644 old/network/testdata/firewall_dmz_app.yaml delete mode 100644 old/network/testdata/firewall_dmz_app_storage.yaml delete mode 100644 old/network/testdata/firewall_dualstack.yaml delete mode 100644 old/network/testdata/firewall_ipv6.yaml delete mode 100644 old/network/testdata/firewall_shared.yaml delete mode 100644 old/network/testdata/firewall_vpn.yaml delete mode 100644 old/network/testdata/firewall_with_rules.yaml delete mode 100644 old/network/testdata/frr.conf.firewall delete mode 100644 old/network/testdata/frr.conf.firewall_dmz delete mode 100644 old/network/testdata/frr.conf.firewall_dmz_app delete mode 100644 old/network/testdata/frr.conf.firewall_dmz_app_storage delete mode 100644 old/network/testdata/frr.conf.firewall_dualstack delete mode 100644 old/network/testdata/frr.conf.firewall_frr-10 delete mode 100644 old/network/testdata/frr.conf.firewall_frr-9 delete mode 100644 old/network/testdata/frr.conf.firewall_ipv6 delete mode 100644 old/network/testdata/frr.conf.firewall_shared delete mode 100644 old/network/testdata/frr.conf.machine delete mode 100644 old/network/testdata/machine.yaml delete mode 100644 old/network/testdata/nftrules delete mode 100644 old/network/testdata/nftrules_accept_forwarding delete mode 100644 old/network/testdata/nftrules_dmz delete mode 100644 old/network/testdata/nftrules_dmz_app delete mode 100644 old/network/testdata/nftrules_ipv6 delete mode 100644 old/network/testdata/nftrules_shared delete mode 100644 old/network/testdata/nftrules_vpn delete mode 100644 old/network/testdata/nftrules_with_rules delete mode 100644 old/network/tpl/frr.firewall.tpl delete mode 100644 old/network/tpl/frr.machine.tpl delete mode 100644 old/network/tpl/nftrules.tpl diff --git a/old/exec/doc.go b/old/exec/doc.go deleted file mode 100644 index 67e089d..0000000 --- a/old/exec/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package exec groups functionality related for command execution. -*/ -package exec diff --git a/old/net/applier.go b/old/net/applier.go deleted file mode 100644 index 89ecf38..0000000 --- a/old/net/applier.go +++ /dev/null @@ -1,128 +0,0 @@ -package net - -import ( - "bufio" - "bytes" - "crypto/sha256" - "io" - "os" - "text/template" -) - -// Applier is an interface to render changes and reload services to apply them. -type Applier interface { - Apply(tpl template.Template, tmpFile, destFile string, reload bool) (bool, error) - Render(writer io.Writer, tpl template.Template) error - Reload() error - Validate() error - Compare(tmpFile, destFile string) bool -} - -// networkApplier holds the toolset for applying network configuration changes. -type networkApplier struct { - data any - validator Validator - reloader Reloader -} - -// NewNetworkApplier creates a new NewNetworkApplier. -func NewNetworkApplier(data any, validator Validator, reloader Reloader) Applier { - return &networkApplier{data: data, validator: validator, reloader: reloader} -} - -// Apply applies the current configuration with the given template. -func (n *networkApplier) Apply(tpl template.Template, tmpFile, destFile string, reload bool) (bool, error) { - f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return false, err - } - - defer func() { - _ = f.Close() - }() - - w := bufio.NewWriter(f) - err = n.Render(w, tpl) - if err != nil { - return false, err - } - - err = w.Flush() - if err != nil { - return false, err - } - - err = n.Validate() - if err != nil { - return false, err - } - - equal := n.Compare(tmpFile, destFile) - if equal { - return false, nil - } - - err = os.Rename(tmpFile, destFile) - if err != nil { - return false, err - } - - if !reload { - return true, nil - } - - err = n.Reload() - if err != nil { - return true, err - } - - return true, nil -} - -// Render renders the network interfaces to the given writer using the given template. -func (n *networkApplier) Render(w io.Writer, tpl template.Template) error { - return tpl.Execute(w, n.data) -} - -// Validate applies the given validator to validate current changes. -func (n *networkApplier) Validate() error { - return n.validator.Validate() -} - -// Reload reloads the necessary services when the network interfaces configuration was changed. -func (n *networkApplier) Reload() error { - return n.reloader.Reload() -} - -// Compare compare source and target for hash equality. -func (n *networkApplier) Compare(source, target string) bool { - sourceChecksum, err := checksum(source) - if err != nil { - return false - } - - targetChecksum, err := checksum(target) - if err != nil { - return false - } - - return bytes.Equal(sourceChecksum, targetChecksum) -} - -func checksum(file string) ([]byte, error) { - f, err := os.Open(file) - if err != nil { - return nil, err - } - - defer func() { - _ = f.Close() - }() - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return nil, err - } - - return h.Sum(nil), nil -} diff --git a/old/net/applier_test.go b/old/net/applier_test.go deleted file mode 100644 index 2164419..0000000 --- a/old/net/applier_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package net - -import "testing" - -func TestNetworkApplier_Compare(t *testing.T) { - tests := []struct { - name string - source string - target string - want bool - }{ - { - name: "simple test", - source: "/etc/hostname", - target: "/etc/passwd", - want: false, - }, - { - name: "simple test", - source: "/etc/hostname", - target: "/etc/hostname", - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - n := &networkApplier{} - if got := n.Compare(tt.source, tt.target); got != tt.want { - t.Errorf("NetworkApplier.Compare() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/old/net/doc.go b/old/net/doc.go deleted file mode 100644 index 94b9ea7..0000000 --- a/old/net/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package net contains code to apply changes to network interfaces and FRR (Free Range Routing). -package net diff --git a/old/net/reloader.go b/old/net/reloader.go deleted file mode 100644 index e876cd6..0000000 --- a/old/net/reloader.go +++ /dev/null @@ -1,51 +0,0 @@ -package net - -import ( - "context" - "fmt" - - "github.com/coreos/go-systemd/v22/dbus" -) - -const done = "done" - -// Reloader triggers the reload to carry out the changes of an applier. -type Reloader interface { - Reload() error -} - -// NewDBusReloader is a reloader for systemd units with dbus. -func NewDBusReloader(service string) dbusReloader { - return dbusReloader{ - serviceFilename: service, - } -} - -// dbusReloader applies a systemd unit reload to apply reloading. -type dbusReloader struct { - serviceFilename string -} - -// Reload reloads a systemd unit. -func (r dbusReloader) Reload() error { - ctx := context.Background() - dbc, err := dbus.NewWithContext(ctx) - if err != nil { - return fmt.Errorf("unable to connect to dbus: %w", err) - } - defer dbc.Close() - - c := make(chan string) - _, err = dbc.ReloadUnitContext(ctx, r.serviceFilename, "replace", c) - - if err != nil { - return err - } - - job := <-c - if job != done { - return fmt.Errorf("reloading failed %s", job) - } - - return nil -} diff --git a/old/net/validator.go b/old/net/validator.go deleted file mode 100644 index bf2f67f..0000000 --- a/old/net/validator.go +++ /dev/null @@ -1,6 +0,0 @@ -package net - -// Validator is an interface to apply common validation. -type Validator interface { - Validate() error -} diff --git a/old/network/doc.go b/old/network/doc.go deleted file mode 100644 index 79fbeff..0000000 --- a/old/network/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -package network groups functionality to configure networking related resources. -*/ -package network diff --git a/old/network/interfaces.go b/old/network/interfaces.go deleted file mode 100644 index 81b8d6e..0000000 --- a/old/network/interfaces.go +++ /dev/null @@ -1,165 +0,0 @@ -package network - -import ( - "fmt" - "io" - "log/slog" - "net/netip" - "text/template" - - mn "github.com/metal-stack/metal-lib/pkg/net" -) - -type ( - // IfacesData contains attributes required to render network interfaces configuration of a bare metal - // server. - IfacesData struct { - Comment string - Loopback Loopback - EVPNIfaces []EVPNIface - } -) - -// ifacesApplier applies interfaces configuration. -type ifacesApplier struct { - kind BareMetalType - kb config - data IfacesData -} - -// newIfacesApplier constructs a new instance of this type. -func newIfacesApplier(kind BareMetalType, c config) ifacesApplier { - d := IfacesData{ - Comment: versionHeader(c.MachineUUID), - } - - switch kind { - case Firewall: - underlay := c.getUnderlayNetwork() - d.Loopback.Comment = fmt.Sprintf("# networkid: %s", *underlay.Networkid) - d.Loopback.IPs = addBitlen(underlay.Ips) - d.EVPNIfaces = getEVPNIfaces(c) - case Machine: - private := c.getPrivatePrimaryNetwork() - d.Loopback.Comment = fmt.Sprintf("# networkid: %s", *private.Networkid) - // Ensure that the ips of the private network are the first ips at the loopback interface. - // The first lo IP is used within network communication and other systems depend on seeing the first private ip. - d.Loopback.IPs = addBitlen(append(private.Ips, c.CollectIPs(mn.External)...)) - default: - c.log.Error("unknown configuratorType", "kind", kind) - panic(fmt.Errorf("unknown configurator type:%v", kind)) - } - - return ifacesApplier{kind: kind, kb: c, data: d} -} - -func addBitlen(ips []string) []string { - ipsWithMask := []string{} - for _, ip := range ips { - parsedIP, err := netip.ParseAddr(ip) - if err != nil { - continue - } - ipWithMask := fmt.Sprintf("%s/%d", ip, parsedIP.BitLen()) - ipsWithMask = append(ipsWithMask, ipWithMask) - } - return ipsWithMask -} - -// Render renders the network interfaces to the given writer using the given template. -func (a *ifacesApplier) Render(w io.Writer, tpl template.Template) error { - return tpl.Execute(w, a.data) -} - -// Apply applies the interface configuration with systemd-networkd. -func (a *ifacesApplier) Apply() { - uuid := a.kb.MachineUUID - evpnIfaces := a.data.EVPNIfaces - - // /etc/systemd/network/00 loopback - src := mustTmpFile("lo_network_") - applier := newSystemdNetworkdApplier(src, a.data) - dest := fmt.Sprintf("%s/00-lo.network", systemdNetworkPath) - applyAndCleanUp(a.kb.log, applier, tplSystemdNetworkLo, src, dest, fileModeSystemd, false) - - // /etc/systemd/network/1x* lan interfaces - offset := 10 - for i, nic := range a.kb.Nics { - prefix := fmt.Sprintf("lan%d_link_", i) - src := mustTmpFile(prefix) - applier, err := newSystemdLinkApplier(a.kind, uuid, i, nic, src, evpnIfaces) - if err != nil { - a.kb.log.Error("unable to create systemdlinkapplier", "error", err) - panic(err) - } - dest := fmt.Sprintf("%s/%d-lan%d.link", systemdNetworkPath, offset+i, i) - applyAndCleanUp(a.kb.log, applier, tplSystemdLinkLan, src, dest, fileModeSystemd, false) - - prefix = fmt.Sprintf("lan%d_network_", i) - src = mustTmpFile(prefix) - applier, err = newSystemdLinkApplier(a.kind, uuid, i, nic, src, evpnIfaces) - if err != nil { - a.kb.log.Error("unable to create systemdlinkapplier", "error", err) - panic(err) - } - dest = fmt.Sprintf("%s/%d-lan%d.network", systemdNetworkPath, offset+i, i) - applyAndCleanUp(a.kb.log, applier, tplSystemdNetworkLan, src, dest, fileModeSystemd, false) - } - - if a.kind == Machine { - return - } - - // /etc/systemd/network/20 bridge interface - applyNetdevAndNetwork(a.kb.log, 20, 20, "bridge", "", a.data) - - // /etc/systemd/network/3x* triplet of interfaces for a tenant: vrf, svi, vxlan - offset = 30 - for i, tenant := range a.data.EVPNIfaces { - suffix := fmt.Sprintf("-%d", tenant.VRF.ID) - applyNetdevAndNetwork(a.kb.log, offset, offset+i, "vrf", suffix, tenant) - applyNetdevAndNetwork(a.kb.log, offset, offset+i, "svi", suffix, tenant) - applyNetdevAndNetwork(a.kb.log, offset, offset+i, "vxlan", suffix, tenant) - } -} - -func applyNetdevAndNetwork(log *slog.Logger, si, di int, prefix, suffix string, data any) { - src := mustTmpFile(prefix + "_netdev_") - applier := newSystemdNetworkdApplier(src, data) - dest := fmt.Sprintf("%s/%d-%s%s.netdev", systemdNetworkPath, di, prefix, suffix) - tpl := fmt.Sprintf("networkd/%d-%s.netdev.tpl", si, prefix) - applyAndCleanUp(log, applier, tpl, src, dest, fileModeSystemd, false) - - src = mustTmpFile(prefix + "_network_") - applier = newSystemdNetworkdApplier(src, data) - dest = fmt.Sprintf("%s/%d-%s%s.network", systemdNetworkPath, di, prefix, suffix) - tpl = fmt.Sprintf("networkd/%d-%s.network.tpl", si, prefix) - applyAndCleanUp(log, applier, tpl, src, dest, fileModeSystemd, false) -} - -func getEVPNIfaces(kb config) []EVPNIface { - var result []EVPNIface - - vrfTableOffset := 1000 - for i, n := range kb.Networks { - if n.Underlay != nil && *n.Underlay { - continue - } - - vrf := int(*n.Vrf) - e := EVPNIface{} - e.Comment = versionHeader(kb.MachineUUID) - e.SVI.Comment = fmt.Sprintf("# svi (networkid: %s)", *n.Networkid) - e.SVI.VLANID = VLANOffset + i - e.SVI.Addresses = addBitlen(n.Ips) - e.VXLAN.Comment = fmt.Sprintf("# vxlan (networkid: %s)", *n.Networkid) - e.VXLAN.ID = vrf - e.VXLAN.TunnelIP = kb.getUnderlayNetwork().Ips[0] - e.VRF.Comment = fmt.Sprintf("# vrf (networkid: %s)", *n.Networkid) - e.VRF.ID = vrf - e.VRF.Table = vrfTableOffset + i - result = append(result, e) - } - - return result -} diff --git a/old/network/interfaces_test.go b/old/network/interfaces_test.go deleted file mode 100644 index 594d83c..0000000 --- a/old/network/interfaces_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - "os" - "sort" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" -) - -func TestIfacesApplier(t *testing.T) { - tests := []struct { - input string - expectedOutput string - configuratorType BareMetalType - }{ - { - input: "testdata/firewall.yaml", - expectedOutput: "testdata/networkd/firewall", - configuratorType: Firewall, - }, - { - input: "testdata/machine.yaml", - expectedOutput: "testdata/networkd/machine", - configuratorType: Machine, - }, - } - log := slog.Default() - - tmpPath = os.TempDir() - for _, tc := range tests { - func() { - old := systemdNetworkPath - tempdir, err := os.MkdirTemp(os.TempDir(), "networkd*") - require.NoError(t, err) - systemdNetworkPath = tempdir - defer func() { - _ = os.RemoveAll(systemdNetworkPath) - systemdNetworkPath = old - }() - kb, err := New(log, tc.input) - require.NoError(t, err) - a := newIfacesApplier(tc.configuratorType, *kb) - a.Apply() - if equal, s := equalDirs(systemdNetworkPath, tc.expectedOutput); !equal { - t.Error(s) - } - }() - } -} - -func equalDirs(dir1, dir2 string) (bool, string) { - files1 := list(dir1) - files2 := list(dir2) - if !cmp.Equal(files1, files2) { - return false, fmt.Sprintf("list of files is different: %v", cmp.Diff(files1, files2)) - } - - for _, f := range files1 { - f1, err := os.ReadFile(fmt.Sprintf("%s/%s", dir1, f)) - if err != nil { - panic(err) - } - f2, err := os.ReadFile(fmt.Sprintf("%s/%s", dir2, f)) - if err != nil { - panic(err) - } - s1 := string(f1) - s2 := string(f2) - if !cmp.Equal(s1, s2) { - return false, fmt.Sprintf("file %s differs: %v", f, cmp.Diff(s1, s2)) - } - } - return true, "" -} - -func list(dir string) []string { - f, err := os.Open(dir) - if err != nil { - panic(err) - } - finfos, err := f.Readdir(-1) - _ = f.Close() - if err != nil { - panic(err) - } - files := []string{} - for _, file := range finfos { - files = append(files, file.Name()) - } - sort.Strings(files) - return files -} diff --git a/old/network/nftables_test.go b/old/network/nftables_test.go deleted file mode 100644 index c51c270..0000000 --- a/old/network/nftables_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package network - -import ( - "bytes" - "log/slog" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCompileNftRules(t *testing.T) { - - tests := []struct { - input string - expected string - enableDNSProxy bool - forwardPolicy ForwardPolicy - }{ - { - input: "testdata/firewall.yaml", - expected: "testdata/nftrules", - enableDNSProxy: false, - forwardPolicy: ForwardPolicyDrop, - }, - { - input: "testdata/firewall.yaml", - expected: "testdata/nftrules_accept_forwarding", - enableDNSProxy: false, - forwardPolicy: ForwardPolicyAccept, - }, - { - input: "testdata/firewall_dmz.yaml", - expected: "testdata/nftrules_dmz", - enableDNSProxy: true, - forwardPolicy: ForwardPolicyDrop, - }, - { - input: "testdata/firewall_dmz_app.yaml", - expected: "testdata/nftrules_dmz_app", - enableDNSProxy: true, - forwardPolicy: ForwardPolicyDrop, - }, - { - input: "testdata/firewall_ipv6.yaml", - expected: "testdata/nftrules_ipv6", - enableDNSProxy: true, - forwardPolicy: ForwardPolicyDrop, - }, - { - input: "testdata/firewall_shared.yaml", - expected: "testdata/nftrules_shared", - enableDNSProxy: true, - forwardPolicy: ForwardPolicyDrop, - }, - { - input: "testdata/firewall_vpn.yaml", - expected: "testdata/nftrules_vpn", - enableDNSProxy: false, - forwardPolicy: ForwardPolicyDrop, - }, - { - input: "testdata/firewall_with_rules.yaml", - expected: "testdata/nftrules_with_rules", - enableDNSProxy: false, - forwardPolicy: ForwardPolicyDrop, - }, - } - log := slog.Default() - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - expected, err := os.ReadFile(tt.expected) - require.NoError(t, err) - - kb, err := New(log, tt.input) - require.NoError(t, err) - - a := newNftablesConfigApplier(*kb, nil, tt.enableDNSProxy, tt.forwardPolicy) - b := bytes.Buffer{} - - tpl := MustParseTpl(TplNftables) - err = a.Render(&b, *tpl) - require.NoError(t, err) - assert.Equal(t, string(expected), b.String()) - }) - } -} diff --git a/old/network/systemd.go b/old/network/systemd.go deleted file mode 100644 index 78e6e5e..0000000 --- a/old/network/systemd.go +++ /dev/null @@ -1,82 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/metal-stack/metal-go/api/models" - "github.com/metal-stack/os-installer/old/net" -) - -const ( - // tplSystemdLinkLan defines the name of the template to render system.link file. - tplSystemdLinkLan = "networkd/10-lan.link.tpl" - - tplSystemdNetworkLo = "networkd/00-lo.network.tpl" - // tplSystemdNetworkLan defines the name of the template to render system.network file. - tplSystemdNetworkLan = "networkd/10-lan.network.tpl" - // mtuFirewall defines the value for MTU specific to the needs of a firewall. VXLAN requires higher MTU. - mtuFirewall = 9216 - // mtuMachine defines the value for MTU specific to the needs of a machine. - mtuMachine = 9000 -) - -type ( - // SystemdCommonData contains attributes common to systemd.network and systemd.link files. - SystemdCommonData struct { - Comment string - Index int - } - - // SystemdLinkData contains attributes required to render systemd.link files. - SystemdLinkData struct { - SystemdCommonData - MAC string - MTU int - EVPNIfaces []EVPNIface - } - - // systemdValidator validates systemd.network and system.link files. - systemdValidator struct { - path string - } -) - -// newSystemdNetworkdApplier creates a new Applier to configure systemd.network. -func newSystemdNetworkdApplier(tmpFile string, data any) net.Applier { - validator := systemdValidator{tmpFile} - - return net.NewNetworkApplier(data, validator, nil) -} - -// newSystemdLinkApplier creates a new Applier to configure systemd.link. -func newSystemdLinkApplier(kind BareMetalType, machineUUID string, nicIndex int, nic *models.V1MachineNic, - tmpFile string, evpnIfaces []EVPNIface) (net.Applier, error) { - var mtu int - - switch kind { - case Firewall: - mtu = mtuFirewall - case Machine: - mtu = mtuMachine - default: - return nil, fmt.Errorf("unknown configuratorType of configurator: %d", kind) - } - - data := SystemdLinkData{ - SystemdCommonData: SystemdCommonData{ - Comment: versionHeader(machineUUID), - Index: nicIndex, - }, - MTU: mtu, - MAC: *nic.Mac, - EVPNIfaces: evpnIfaces, - } - validator := systemdValidator{tmpFile} - - return net.NewNetworkApplier(data, validator, nil), nil -} - -// Validate validates systemd.network and systemd.link files. -func (v systemdValidator) Validate() error { - return nil -} diff --git a/old/network/template.go b/old/network/template.go deleted file mode 100644 index 6545a7e..0000000 --- a/old/network/template.go +++ /dev/null @@ -1,23 +0,0 @@ -package network - -import ( - "embed" - "path" - "text/template" -) - -//go:embed tpl -var templates embed.FS - -func mustReadTpl(tplName string) string { - contents, err := templates.ReadFile(path.Join("tpl", tplName)) - if err != nil { - panic(err) - } - return string(contents) -} - -func MustParseTpl(tplName string) *template.Template { - s := mustReadTpl(tplName) - return template.Must(template.New(tplName).Parse(string(s))) -} diff --git a/old/network/testdata/firewall.yaml b/old/network/testdata/firewall.yaml deleted file mode 100644 index d8f19c3..0000000 --- a/old/network/testdata/firewall.yaml +++ /dev/null @@ -1,182 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 10.0.16.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - # === Private shared networks to route to - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 185.27.0.0/22 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - privateprimary: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 - - asn: 4200003073 - # considered to figure out allowed prefixes for route imports from public network into tenant network - destinationprefixes: - - 100.127.1.0/24 - # Applied to local loopback as /32. - ips: - - 100.127.129.1 - nat: true - networkid: mpls-nbg-w8101-test - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 100.127.129.0/24 - private: false - underlay: false - networktype: external - vrf: 104010 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_dmz.yaml b/old/network/testdata/firewall_dmz.yaml deleted file mode 100644 index cb7e76c..0000000 --- a/old/network/testdata/firewall_dmz.yaml +++ /dev/null @@ -1,164 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 10.0.16.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - - asn: 4200003073 - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 10.0.20.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: dmz-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.20.0/22 - private: true - underlay: false - privateprimary: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3983 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 185.27.0.0/22 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_dmz_app.yaml b/old/network/testdata/firewall_dmz_app.yaml deleted file mode 100644 index 414ece6..0000000 --- a/old/network/testdata/firewall_dmz_app.yaml +++ /dev/null @@ -1,141 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 10.0.16.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - - asn: 4200003073 - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 10.0.20.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: dmz-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.20.0/22 - private: true - underlay: false - privateprimary: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3983 - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_dmz_app_storage.yaml b/old/network/testdata/firewall_dmz_app_storage.yaml deleted file mode 100644 index 71af69b..0000000 --- a/old/network/testdata/firewall_dmz_app_storage.yaml +++ /dev/null @@ -1,160 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 10.0.16.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - - asn: 4200003073 - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 10.0.20.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: dmz-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.20.0/22 - private: true - underlay: false - privateprimary: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3983 - # === Private shared networks to route to - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_dualstack.yaml b/old/network/testdata/firewall_dualstack.yaml deleted file mode 100644 index 32c48bb..0000000 --- a/old/network/testdata/firewall_dualstack.yaml +++ /dev/null @@ -1,183 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 2002::1 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 2002::/64 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - # === Private shared networks to route to - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - ::/0 - # Applied to the SVI (as /32) - ips: - - 2a02:c00:20::1 - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 2a02:c00:20::/45 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - privateprimary: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 - - asn: 4200003073 - # considered to figure out allowed prefixes for route imports from public network into tenant network - destinationprefixes: - - 100.127.1.0/24 - # Applied to local loopback as /32. - ips: - - 100.127.129.1 - nat: true - networkid: mpls-nbg-w8101-test - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 100.127.129.0/24 - private: false - underlay: false - networktype: external - vrf: 104010 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_ipv6.yaml b/old/network/testdata/firewall_ipv6.yaml deleted file mode 100644 index 6f9aec1..0000000 --- a/old/network/testdata/firewall_ipv6.yaml +++ /dev/null @@ -1,181 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 2002::1 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 2002::/64 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - # === Private shared networks to route to - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - ::/0 - # Applied to the SVI (as /32) - ips: - - 2a02:c00:20::1 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 2a02:c00:20::/45 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - privateprimary: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 - - asn: 4200003073 - # considered to figure out allowed prefixes for route imports from public network into tenant network - destinationprefixes: - - 100.127.1.0/24 - # Applied to local loopback as /32. - ips: - - 100.127.129.1 - nat: true - networkid: mpls-nbg-w8101-test - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 100.127.129.0/24 - private: false - underlay: false - networktype: external - vrf: 104010 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_shared.yaml b/old/network/testdata/firewall_shared.yaml deleted file mode 100644 index fec137f..0000000 --- a/old/network/testdata/firewall_shared.yaml +++ /dev/null @@ -1,141 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - privateprimary: true - networktype: privateprimaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 185.27.0.0/22 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] - - - - diff --git a/old/network/testdata/firewall_vpn.yaml b/old/network/testdata/firewall_vpn.yaml deleted file mode 100644 index f2aed3d..0000000 --- a/old/network/testdata/firewall_vpn.yaml +++ /dev/null @@ -1,184 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 10.0.16.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - # === Private shared networks to route to - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 185.27.0.0/22 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - privateprimary: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 - - asn: 4200003073 - # considered to figure out allowed prefixes for route imports from public network into tenant network - destinationprefixes: - - 100.127.1.0/24 - # Applied to local loopback as /32. - ips: - - 100.127.129.1 - nat: true - networkid: mpls-nbg-w8101-test - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 100.127.129.0/24 - private: false - underlay: false - networktype: external - vrf: 104010 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] -vpn: - address: https://test.test.dev - auth_key: abracadabra - - - diff --git a/old/network/testdata/firewall_with_rules.yaml b/old/network/testdata/firewall_with_rules.yaml deleted file mode 100644 index 954b125..0000000 --- a/old/network/testdata/firewall_with_rules.yaml +++ /dev/null @@ -1,213 +0,0 @@ -# Note: This is a general-purpose configuration file that contains information not only for this app. -# -# This file is considered to be used to configure the tenant firewall! -# -########################################### -# root@firewall:/etc/metal# date -# Thu May 16 13:48:11 CEST 2019 -# root@firewall:/etc/metal# cat install.yaml -# hostname: firewall -# ipaddress: 10.0.12.1 -# asn: "4200003073" -# networks: -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.0.12.1 -# nat: false -# networkid: bc830818-2df1-4904-8c40-4322296d393d -# prefixes: -# - 10.0.12.0/22 -# private: true -# underlay: false -# vrf: 3981 -# - asn: 4200003073 -# destinationprefixes: -# - 0.0.0.0/0 -# ips: -# - 185.24.0.1 -# nat: false -# networkid: internet-vagrant-lab -# prefixes: -# - 185.24.0.0/22 -# - 185.27.0.0/22 -# private: false -# underlay: false -# vrf: 104009 -# - asn: 4200003073 -# destinationprefixes: [] -# ips: -# - 10.1.0.1 -# nat: false -# networkid: underlay-vagrant-lab -# prefixes: -# - 10.0.12.0/22 -# private: false -# underlay: true -# vrf: 0 -# machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# sshpublickey: "" -# password: KAWT5DugqSPAezMl -# devmode: false -# console: ttyS0,115200n8 -########################################### ---- -# Applies to hostname of the firewall. -hostname: firewall -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Firewall: Used to consider the set of prefixes that originate the given IP's to establish route leak in public - # network VRF's for return traffic. Applied to the SVI (as /32) - # For Machine: Used to set the loopback ips. - ips: - - 10.0.16.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - underlay: false - networktype: privateprimaryunshared - # [IGNORED in case of private network] - # Defines the tenant VRF id. - vrf: 3981 - # === Private shared networks to route to - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # Applied to the SVI (as /32) - ips: - - 10.0.18.2 - # In case nat equals true, Source NAT via SVI is added. - nat: false - networkid: storage-net - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.18.0/22 - private: true - underlay: false - networktype: privatesecondaryshared - # VRF id considered to define EVPN interfaces. - vrf: 3982 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - 0.0.0.0/0 - # Applied to the SVI (as /32) - ips: - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 185.27.0.0/22 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - # === Underlay Network (underlay=true) - # Considered to define the BGP ASN. - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: [] - # Applied to local loopback as /32. - ips: - - 10.1.0.1 - nat: false - networkid: underlay-vagrant-lab - # [IGNORED in case of UNDERLAY] - prefixes: - - 10.0.12.0/22 - private: false - privateprimary: false - underlay: true - networktype: underlay - # [IGNORED] Underlay runs in default VRF. - vrf: 0 - - asn: 4200003073 - # considered to figure out allowed prefixes for route imports from public network into tenant network - destinationprefixes: - - 100.127.1.0/24 - # Applied to local loopback as /32. - ips: - - 100.127.129.1 - nat: true - networkid: mpls-nbg-w8101-test - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 100.127.129.0/24 - private: false - underlay: false - networktype: external - vrf: 104010 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] -firewall_rules: - egress: - - comment: "allow apt update" - protocol: tcp - ports: [443] - to: - - "0.0.0.0/0" - - "1.2.3.4/32" - - comment: "allow apt update v6" - protocol: tcp - ports: [443] - to: - - "::/0" - ingress: - - protocol: TCP - ports: [22] - from: - - "2.3.4.0/24" - - "192.168.1.0/16" - to: - - "100.1.2.3/32" - - "100.1.2.4/32" - comment: "allow incoming ssh" - - protocol: TCP - ports: [22] - from: - - 2001:db8::1/128 - to: - - 2001:db8:0:113::/64 - comment: "allow incoming ssh ipv6" - - protocol: TCP - ports: [80,443,8080] - from: - - "1.2.3.0/24" - - "192.168.0.0/16" diff --git a/old/network/testdata/frr.conf.firewall b/old/network/testdata/frr.conf.firewall deleted file mode 100644 index e684dba..0000000 --- a/old/network/testdata/frr.conf.firewall +++ /dev/null @@ -1,208 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3982 - vni 3982 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -vrf vrf104010 - vni 104010 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104010 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 -ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.1.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 102 deny 185.1.2.3/32 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf104010 seq 105 permit 100.127.129.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf3982 seq 106 permit 10.0.18.0/22 le 32 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3982 - match ip address prefix-list vrf3981-import-from-vrf3982 -route-map vrf3981-import-map permit 20 - match source-vrf vrf104010 - match ip address prefix-list vrf3981-import-from-vrf104010 -route-map vrf3981-import-map permit 30 - match source-vrf vrf104009 - match ip address prefix-list vrf3981-import-from-vrf104009 -route-map vrf3981-import-map deny 40 -! -ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 -route-map vrf3982-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3982-import-from-vrf3981 -route-map vrf3982-import-map deny 20 -! -ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 101 permit 185.1.2.0/24 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.27.0.0/22 le 32 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981 -route-map vrf104009-import-map deny 30 -! -ip prefix-list vrf104010-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104010-import-from-vrf3981 seq 101 permit 100.127.129.0/24 le 32 -route-map vrf104010-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104010-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981 -route-map vrf104010-import-map deny 30 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_dmz b/old/network/testdata/frr.conf.firewall_dmz deleted file mode 100644 index 35fdfc8..0000000 --- a/old/network/testdata/frr.conf.firewall_dmz +++ /dev/null @@ -1,180 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3983 - vni 3983 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf3983 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf3983 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3983 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf vrf104009 - import vrf route-map vrf3983-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf vrf104009 - import vrf route-map vrf3983-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf vrf3983 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf vrf3983 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 -ip prefix-list vrf3981-import-from-vrf104009 seq 101 deny 185.1.2.3/32 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 102 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.27.0.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf3983 seq 104 permit 10.0.20.0/22 le 32 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3983 - match ip address prefix-list vrf3981-import-from-vrf3983 -route-map vrf3981-import-map permit 20 - match source-vrf vrf104009 - match ip address prefix-list vrf3981-import-from-vrf104009 -route-map vrf3981-import-map deny 30 -! -ip prefix-list vrf3983-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3983-import-from-vrf3981 seq 101 permit 10.0.20.0/22 le 32 -ip prefix-list vrf3983-import-from-vrf104009 permit 0.0.0.0/0 -ip prefix-list vrf3983-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3983-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 -route-map vrf3983-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3983-import-from-vrf3981 -route-map vrf3983-import-map permit 20 - match source-vrf vrf104009 - match ip address prefix-list vrf3983-import-from-vrf104009 -route-map vrf3983-import-map deny 30 -! -ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104009-import-from-vrf3983-no-export seq 101 permit 10.0.20.0/22 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.1.2.0/24 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 103 permit 185.27.0.0/22 le 32 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3983 - match ip address prefix-list vrf104009-import-from-vrf3983-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104009-import-map permit 30 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981 -route-map vrf104009-import-map deny 40 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_dmz_app b/old/network/testdata/frr.conf.firewall_dmz_app deleted file mode 100644 index 0c6c82c..0000000 --- a/old/network/testdata/frr.conf.firewall_dmz_app +++ /dev/null @@ -1,121 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3983 - vni 3983 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3983 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3983 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3983 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3983-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3983-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf3983 seq 100 deny 10.0.20.2/32 le 32 -ip prefix-list vrf3981-import-from-vrf3983 seq 101 permit 10.0.20.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf3983 permit 0.0.0.0/0 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3983 - match ip address prefix-list vrf3981-import-from-vrf3983 -route-map vrf3981-import-map deny 20 -! -ip prefix-list vrf3983-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3983-import-from-vrf3981 seq 101 permit 10.0.20.0/22 le 32 -route-map vrf3983-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3983-import-from-vrf3981 -route-map vrf3983-import-map deny 20 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_dmz_app_storage b/old/network/testdata/frr.conf.firewall_dmz_app_storage deleted file mode 100644 index a9c951d..0000000 --- a/old/network/testdata/frr.conf.firewall_dmz_app_storage +++ /dev/null @@ -1,159 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3983 - vni 3983 - exit-vrf -! -vrf vrf3982 - vni 3982 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3983 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3983 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3983 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3983-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3983-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf3983 seq 100 deny 10.0.20.2/32 le 32 -ip prefix-list vrf3981-import-from-vrf3983 seq 101 permit 10.0.20.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf3982 seq 102 permit 10.0.18.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf3983 permit 0.0.0.0/0 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3983 - match ip address prefix-list vrf3981-import-from-vrf3983 -route-map vrf3981-import-map permit 20 - match source-vrf vrf3982 - match ip address prefix-list vrf3981-import-from-vrf3982 -route-map vrf3981-import-map deny 30 -! -ip prefix-list vrf3983-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3983-import-from-vrf3981 seq 101 permit 10.0.20.0/22 le 32 -route-map vrf3983-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3983-import-from-vrf3981 -route-map vrf3983-import-map deny 20 -! -ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 -route-map vrf3982-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3982-import-from-vrf3981 -route-map vrf3982-import-map deny 20 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_dualstack b/old/network/testdata/frr.conf.firewall_dualstack deleted file mode 100644 index 3a2c140..0000000 --- a/old/network/testdata/frr.conf.firewall_dualstack +++ /dev/null @@ -1,218 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3982 - vni 3982 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -vrf vrf104010 - vni 104010 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104010 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf104010 seq 100 permit 100.127.1.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 101 deny 185.1.2.3/32 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 102 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104010 seq 103 permit 100.127.129.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf3982 seq 104 permit 10.0.18.0/22 le 32 -ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 permit ::/0 -ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 106 deny 2a02:c00:20::1/128 le 128 -ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 107 permit 2a02:c00:20::/45 le 128 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3982 - match ip address prefix-list vrf3981-import-from-vrf3982 -route-map vrf3981-import-map permit 20 - match source-vrf vrf104010 - match ip address prefix-list vrf3981-import-from-vrf104010 -route-map vrf3981-import-map permit 30 - match source-vrf vrf104009 - match ipv6 address prefix-list vrf3981-import-from-vrf104009-ipv6 -route-map vrf3981-import-map permit 40 - match source-vrf vrf104009 - match ip address prefix-list vrf3981-import-from-vrf104009 -route-map vrf3981-import-map deny 50 -! -ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.18.0/22 le 32 -ipv6 prefix-list vrf3982-import-from-vrf3981-ipv6 seq 101 permit 2002::/64 le 128 -route-map vrf3982-import-map permit 10 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf3982-import-from-vrf3981-ipv6 -route-map vrf3982-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf3982-import-from-vrf3981 -route-map vrf3982-import-map deny 30 -! -ip prefix-list vrf104009-import-from-vrf3981 seq 100 permit 185.1.2.0/24 le 32 -ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 -ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6 seq 102 permit 2a02:c00:20::/45 le 128 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6 -route-map vrf104009-import-map permit 30 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981 -route-map vrf104009-import-map deny 40 -! -ip prefix-list vrf104010-import-from-vrf3981 seq 100 permit 100.127.129.0/24 le 32 -ipv6 prefix-list vrf104010-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 -route-map vrf104010-import-map permit 10 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf104010-import-from-vrf3981-ipv6-no-export - set community additive no-export -route-map vrf104010-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981 -route-map vrf104010-import-map deny 30 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_frr-10 b/old/network/testdata/frr.conf.firewall_frr-10 deleted file mode 100644 index 45a2e01..0000000 --- a/old/network/testdata/frr.conf.firewall_frr-10 +++ /dev/null @@ -1,212 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3982 - vni 3982 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -vrf vrf104010 - vni 104010 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - no bgp enforce-first-as - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - no bgp enforce-first-as - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - no bgp enforce-first-as - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104010 - bgp router-id 10.1.0.1 - no bgp enforce-first-as - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 -ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.1.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 102 deny 185.1.2.3/32 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf104010 seq 105 permit 100.127.129.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf3982 seq 106 permit 10.0.18.0/22 le 32 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3982 - match ip address prefix-list vrf3981-import-from-vrf3982 -route-map vrf3981-import-map permit 20 - match source-vrf vrf104010 - match ip address prefix-list vrf3981-import-from-vrf104010 -route-map vrf3981-import-map permit 30 - match source-vrf vrf104009 - match ip address prefix-list vrf3981-import-from-vrf104009 -route-map vrf3981-import-map deny 40 -! -ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 -route-map vrf3982-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3982-import-from-vrf3981 -route-map vrf3982-import-map deny 20 -! -ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 101 permit 185.1.2.0/24 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.27.0.0/22 le 32 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981 -route-map vrf104009-import-map deny 30 -! -ip prefix-list vrf104010-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104010-import-from-vrf3981 seq 101 permit 100.127.129.0/24 le 32 -route-map vrf104010-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104010-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981 -route-map vrf104010-import-map deny 30 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_frr-9 b/old/network/testdata/frr.conf.firewall_frr-9 deleted file mode 100644 index e684dba..0000000 --- a/old/network/testdata/frr.conf.firewall_frr-9 +++ /dev/null @@ -1,208 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3982 - vni 3982 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -vrf vrf104010 - vni 104010 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104010 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf104009 permit 0.0.0.0/0 -ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.1.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 102 deny 185.1.2.3/32 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 103 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104009 seq 104 permit 185.27.0.0/22 le 32 -ip prefix-list vrf3981-import-from-vrf104010 seq 105 permit 100.127.129.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf3982 seq 106 permit 10.0.18.0/22 le 32 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3982 - match ip address prefix-list vrf3981-import-from-vrf3982 -route-map vrf3981-import-map permit 20 - match source-vrf vrf104010 - match ip address prefix-list vrf3981-import-from-vrf104010 -route-map vrf3981-import-map permit 30 - match source-vrf vrf104009 - match ip address prefix-list vrf3981-import-from-vrf104009 -route-map vrf3981-import-map deny 40 -! -ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf3982-import-from-vrf3981 seq 101 permit 10.0.18.0/22 le 32 -route-map vrf3982-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf3982-import-from-vrf3981 -route-map vrf3982-import-map deny 20 -! -ip prefix-list vrf104009-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 101 permit 185.1.2.0/24 le 32 -ip prefix-list vrf104009-import-from-vrf3981 seq 102 permit 185.27.0.0/22 le 32 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104009-import-from-vrf3981 -route-map vrf104009-import-map deny 30 -! -ip prefix-list vrf104010-import-from-vrf3981-no-export seq 100 permit 10.0.16.0/22 le 32 -ip prefix-list vrf104010-import-from-vrf3981 seq 101 permit 100.127.129.0/24 le 32 -route-map vrf104010-import-map permit 10 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981-no-export - set community additive no-export -route-map vrf104010-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981 -route-map vrf104010-import-map deny 30 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_ipv6 b/old/network/testdata/frr.conf.firewall_ipv6 deleted file mode 100644 index 984ffed..0000000 --- a/old/network/testdata/frr.conf.firewall_ipv6 +++ /dev/null @@ -1,209 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3981 - vni 3981 - exit-vrf -! -vrf vrf3982 - vni 3982 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -vrf vrf104010 - vni 104010 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3981 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf vrf104010 - import vrf vrf3982 - import vrf route-map vrf3981-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104010 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3981 - import vrf route-map vrf104010-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3981-import-from-vrf104010 seq 100 permit 100.127.1.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf104010 seq 101 permit 100.127.129.0/24 le 32 -ip prefix-list vrf3981-import-from-vrf3982 seq 102 permit 10.0.18.0/22 le 32 -ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 permit ::/0 -ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 104 deny 2a02:c00:20::1/128 le 128 -ipv6 prefix-list vrf3981-import-from-vrf104009-ipv6 seq 105 permit 2a02:c00:20::/45 le 128 -route-map vrf3981-import-map permit 10 - match source-vrf vrf3982 - match ip address prefix-list vrf3981-import-from-vrf3982 -route-map vrf3981-import-map permit 20 - match source-vrf vrf104010 - match ip address prefix-list vrf3981-import-from-vrf104010 -route-map vrf3981-import-map permit 30 - match source-vrf vrf104009 - match ipv6 address prefix-list vrf3981-import-from-vrf104009-ipv6 -route-map vrf3981-import-map deny 40 -! -ip prefix-list vrf3982-import-from-vrf3981 seq 100 permit 10.0.18.0/22 le 32 -ipv6 prefix-list vrf3982-import-from-vrf3981-ipv6 seq 101 permit 2002::/64 le 128 -route-map vrf3982-import-map permit 10 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf3982-import-from-vrf3981-ipv6 -route-map vrf3982-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf3982-import-from-vrf3981 -route-map vrf3982-import-map deny 30 -! -ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 -ipv6 prefix-list vrf104009-import-from-vrf3981-ipv6 seq 101 permit 2a02:c00:20::/45 le 128 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf104009-import-from-vrf3981-ipv6 -route-map vrf104009-import-map deny 30 -! -ip prefix-list vrf104010-import-from-vrf3981 seq 100 permit 100.127.129.0/24 le 32 -ipv6 prefix-list vrf104010-import-from-vrf3981-ipv6-no-export seq 100 permit 2002::/64 le 128 -route-map vrf104010-import-map permit 10 - match source-vrf vrf3981 - match ipv6 address prefix-list vrf104010-import-from-vrf3981-ipv6-no-export - set community additive no-export -route-map vrf104010-import-map permit 20 - match source-vrf vrf3981 - match ip address prefix-list vrf104010-import-from-vrf3981 -route-map vrf104010-import-map deny 30 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.firewall_shared b/old/network/testdata/frr.conf.firewall_shared deleted file mode 100644 index 67cead6..0000000 --- a/old/network/testdata/frr.conf.firewall_shared +++ /dev/null @@ -1,127 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname firewall -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -vrf vrf3982 - vni 3982 - exit-vrf -! -vrf vrf104009 - vni 104009 - exit-vrf -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp 4200003073 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -router bgp 4200003073 vrf vrf3982 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf104009 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf104009 - import vrf route-map vrf3982-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -router bgp 4200003073 vrf vrf104009 - bgp router-id 10.1.0.1 - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - import vrf vrf3982 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - import vrf vrf3982 - import vrf route-map vrf104009-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -ip prefix-list vrf3982-import-from-vrf104009 permit 0.0.0.0/0 -ip prefix-list vrf3982-import-from-vrf104009 seq 101 deny 185.1.2.3/32 le 32 -ip prefix-list vrf3982-import-from-vrf104009 seq 102 permit 185.1.2.0/24 le 32 -ip prefix-list vrf3982-import-from-vrf104009 seq 103 permit 185.27.0.0/22 le 32 -route-map vrf3982-import-map permit 10 - match source-vrf vrf104009 - match ip address prefix-list vrf3982-import-from-vrf104009 -route-map vrf3982-import-map deny 20 -! -ip prefix-list vrf104009-import-from-vrf3982-no-export seq 100 permit 10.0.18.0/22 le 32 -ip prefix-list vrf104009-import-from-vrf3982 seq 101 permit 185.1.2.0/24 le 32 -ip prefix-list vrf104009-import-from-vrf3982 seq 102 permit 185.27.0.0/22 le 32 -route-map vrf104009-import-map permit 10 - match source-vrf vrf3982 - match ip address prefix-list vrf104009-import-from-vrf3982-no-export - set community additive no-export -route-map vrf104009-import-map permit 20 - match source-vrf vrf3982 - match ip address prefix-list vrf104009-import-from-vrf3982 -route-map vrf104009-import-map deny 30 -! -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/testdata/frr.conf.machine b/old/network/testdata/frr.conf.machine deleted file mode 100644 index d1daa28..0000000 --- a/old/network/testdata/frr.conf.machine +++ /dev/null @@ -1,60 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -frr version 8.5 -frr defaults datacenter -hostname machine -allow-reserved-ranges -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -no zebra nexthop kernel enable -! -router bgp 4200003073 - bgp router-id 10.0.17.2 - bgp bestpath as-path multipath-relax - neighbor TOR peer-group - neighbor TOR remote-as external - neighbor TOR timers 2 8 - neighbor lan0 interface peer-group TOR - neighbor lan1 interface peer-group TOR - neighbor LOCAL peer-group - neighbor LOCAL remote-as internal - neighbor LOCAL timers 2 8 - neighbor LOCAL route-map local-in in - bgp listen range 0.0.0.0/0 peer-group LOCAL - ! - address-family ipv4 unicast - redistribute connected - redistribute kernel - neighbor TOR route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - redistribute kernel - neighbor TOR route-map only-self-out out - neighbor TOR activate - exit-address-family -! -bgp as-path access-list SELF permit ^$ -! -route-map local-in permit 10 - set weight 32768 -! -route-map only-self-out permit 10 - match as-path SELF -! -route-map only-self-out deny 99 -! \ No newline at end of file diff --git a/old/network/testdata/machine.yaml b/old/network/testdata/machine.yaml deleted file mode 100644 index dd4fddb..0000000 --- a/old/network/testdata/machine.yaml +++ /dev/null @@ -1,84 +0,0 @@ ---- -hostname: machine -networks: - # === Tenant Network (private=true) - # [IGNORED] - - asn: 4200003073 - # [IGNORED in case of private network] - destinationprefixes: [] - # For Machine: Used to set the loopback ips. - ips: - - 10.0.17.2 - # [IGNORED in case of private network] - nat: false - # [IGNORED in case of private network] - networkid: bc830818-2df1-4904-8c40-4322296d393d - # considered as source range for nat and to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 10.0.16.0/22 - private: true - # [IGNORED in case of private network] - underlay: false - networktype: privateprimaryunshared - # Defines the tenant VRF id. - vrf: 3981 - # === Public networks to route to - # [IGNORED] - - asn: 4200003073 - # Considered to establish static route leak to reach out from tenant VRF into the public networks. - destinationprefixes: - - 0.0.0.0/0 - # For Machine: Used to set the loopback ips. - ips: - - 185.1.2.3 - # In case nat equals true, Source NAT via SVI is added. - nat: true - networkid: internet-vagrant-lab - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 185.1.2.0/24 - - 185.27.0.0/22 - private: false - underlay: false - networktype: external - # VRF id considered to define EVPN interfaces. - vrf: 104009 - - asn: 4200003073 - # considered to figure out allowed prefixes for route imports from public network into tenant network - destinationprefixes: - - 100.127.1.0/24 - # For Machine: Used to set the loopback ips. - ips: - - 100.127.129.1 - nat: true - networkid: mpls-nbg-w8101-test - # considered to figure out allowed prefixes for route imports from private network into non-private, non-underlay network - prefixes: - - 100.127.129.0/24 - private: false - underlay: false - networktype: external - vrf: 104010 -machineuuid: e0ab02d2-27cd-5a5e-8efc-080ba80cf258 -# [IGNORED] -sshpublickey: "" -# [IGNORED] -password: KAWT5DugqSPAezMl -# [IGNORED] -devmode: false -# [IGNORED] -console: ttyS1,115200n8 -timestamp: "2019-07-01T09:41:43Z" -nics: - - mac: "00:03:00:11:11:01" - name: lan0 - neighbors: - - mac: 44:38:39:00:00:1a - name: null - neighbors: [] - - mac: "00:03:00:11:12:01" - name: lan1 - neighbors: - - mac: "44:38:39:00:00:04" - name: null - neighbors: [] diff --git a/old/network/testdata/nftrules b/old/network/testdata/nftrules deleted file mode 100644 index 9c9fc40..0000000 --- a/old/network/testdata/nftrules +++ /dev/null @@ -1,76 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_accept_forwarding b/old/network/testdata/nftrules_accept_forwarding deleted file mode 100644 index bdbd8da..0000000 --- a/old/network/testdata/nftrules_accept_forwarding +++ /dev/null @@ -1,76 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy accept; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_dmz b/old/network/testdata/nftrules_dmz deleted file mode 100644 index a609824..0000000 --- a/old/network/testdata/nftrules_dmz +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - ip saddr 10.0.0.0/8 tcp dport domain ip daddr 185.1.2.3 accept comment "dnat to dns proxy" - ip saddr 10.0.0.0/8 udp dport domain ip daddr 185.1.2.3 accept comment "dnat to dns proxy" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3983" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3983" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - oifname "vlan3981" tcp sport domain ct zone set 3 - oifname "vlan3981" udp sport domain ct zone set 3 - oifname "vlan3983" tcp sport domain ct zone set 3 - oifname "vlan3983" udp sport domain ct zone set 3 - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - ip daddr @proxy_dns_servers iifname "vlan3981" tcp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3981" udp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3983" tcp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3983" udp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - iifname "vlan3981" tcp dport domain ct zone set 3 - iifname "vlan3981" udp dport domain ct zone set 3 - iifname "vlan3983" tcp dport domain ct zone set 3 - iifname "vlan3983" udp dport domain ct zone set 3 - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 ip daddr != 185.1.2.3 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104009" ip saddr 10.0.20.0/22 ip daddr != 185.1.2.3 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_dmz_app b/old/network/testdata/nftrules_dmz_app deleted file mode 100644 index 83bee38..0000000 --- a/old/network/testdata/nftrules_dmz_app +++ /dev/null @@ -1,89 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - ip saddr 10.0.0.0/8 tcp dport domain ip daddr 10.0.20.2 accept comment "dnat to dns proxy" - ip saddr 10.0.0.0/8 udp dport domain ip daddr 10.0.20.2 accept comment "dnat to dns proxy" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3983" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3983" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - oifname "vlan3981" tcp sport domain ct zone set 3 - oifname "vlan3981" udp sport domain ct zone set 3 - oifname "vlan3983" tcp sport domain ct zone set 3 - oifname "vlan3983" udp sport domain ct zone set 3 - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - ip daddr @proxy_dns_servers iifname "vlan3981" tcp dport domain dnat ip to 10.0.20.2 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3981" udp dport domain dnat ip to 10.0.20.2 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3983" tcp dport domain dnat ip to 10.0.20.2 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3983" udp dport domain dnat ip to 10.0.20.2 comment "dnat to dns proxy" - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - iifname "vlan3981" tcp dport domain ct zone set 3 - iifname "vlan3981" udp dport domain ct zone set 3 - iifname "vlan3983" tcp dport domain ct zone set 3 - iifname "vlan3983" udp dport domain ct zone set 3 - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_ipv6 b/old/network/testdata/nftrules_ipv6 deleted file mode 100644 index 4dc27b7..0000000 --- a/old/network/testdata/nftrules_ipv6 +++ /dev/null @@ -1,98 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - ip6 saddr fd00::/8 tcp dport domain ip6 daddr 2a02:c00:20::1 accept comment "dnat to dns proxy" - ip6 saddr fd00::/8 udp dport domain ip6 daddr 2a02:c00:20::1 accept comment "dnat to dns proxy" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - oifname "vlan3981" tcp sport domain ct zone set 3 - oifname "vlan3981" udp sport domain ct zone set 3 - oifname "vlan3982" tcp sport domain ct zone set 3 - oifname "vlan3982" udp sport domain ct zone set 3 - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - set proxy_dns_servers_v6 { - type ipv6_addr - flags interval - auto-merge - elements = { 2001:4860:4860::8888, 2001:4860:4860::8844, 2606:4700:4700::1111, 2606:4700:4700::1001 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - ip6 daddr @proxy_dns_servers_v6 iifname "vlan3981" tcp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" - ip6 daddr @proxy_dns_servers_v6 iifname "vlan3981" udp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" - ip6 daddr @proxy_dns_servers_v6 iifname "vlan3982" tcp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" - ip6 daddr @proxy_dns_servers_v6 iifname "vlan3982" udp dport domain dnat ip6 to 2a02:c00:20::1 comment "dnat to dns proxy" - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - iifname "vlan3981" tcp dport domain ct zone set 3 - iifname "vlan3981" udp dport domain ct zone set 3 - iifname "vlan3982" tcp dport domain ct zone set 3 - iifname "vlan3982" udp dport domain ct zone set 3 - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip6 saddr 2002::/64 ip6 daddr != 2a02:c00:20::1 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip6 saddr 2002::/64 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_shared b/old/network/testdata/nftrules_shared deleted file mode 100644 index ff571e6..0000000 --- a/old/network/testdata/nftrules_shared +++ /dev/null @@ -1,83 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - ip saddr 10.0.0.0/8 tcp dport domain ip daddr 185.1.2.3 accept comment "dnat to dns proxy" - ip saddr 10.0.0.0/8 udp dport domain ip daddr 185.1.2.3 accept comment "dnat to dns proxy" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - oifname "vlan3982" tcp sport domain ct zone set 3 - oifname "vlan3982" udp sport domain ct zone set 3 - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - ip daddr @proxy_dns_servers iifname "vlan3982" tcp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" - ip daddr @proxy_dns_servers iifname "vlan3982" udp dport domain dnat ip to 185.1.2.3 comment "dnat to dns proxy" - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - iifname "vlan3982" tcp dport domain ct zone set 3 - iifname "vlan3982" udp dport domain ct zone set 3 - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan3982" ip saddr 10.0.18.0/22 counter masquerade random comment "snat (networkid: storage-net)" - oifname "vlan104009" ip saddr 10.0.18.0/22 ip daddr != 185.1.2.3 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_vpn b/old/network/testdata/nftrules_vpn deleted file mode 100644 index 55c5d06..0000000 --- a/old/network/testdata/nftrules_vpn +++ /dev/null @@ -1,76 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - iifname "tailscale*" accept comment "Accept tailscale traffic" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" - } -} \ No newline at end of file diff --git a/old/network/testdata/nftrules_with_rules b/old/network/testdata/nftrules_with_rules deleted file mode 100644 index 0ec7073..0000000 --- a/old/network/testdata/nftrules_with_rules +++ /dev/null @@ -1,86 +0,0 @@ -# This file was auto generated for machine: 'e0ab02d2-27cd-5a5e-8efc-080ba80cf258' by app version . -# Do not edit. -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - iifname "vrf3981" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3981" tcp dport 9630 counter accept comment "nftables metrics" - iifname "vrf3982" tcp dport 9100 counter accept comment "node metrics" - iifname "vrf3982" tcp dport 9630 counter accept comment "nftables metrics" - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy drop; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - # egress rules specified during firewall creation - iifname { "vrf3981","vrf3982" } ip daddr 0.0.0.0/0 tcp dport { 443 } counter accept comment "allow apt update" - iifname { "vrf3981","vrf3982" } ip daddr 1.2.3.4/32 tcp dport { 443 } counter accept comment "allow apt update" - iifname { "vrf3981","vrf3982" } ip6 daddr ::/0 tcp dport { 443 } counter accept comment "allow apt update v6" - # ingress rules specified during firewall creation - ip daddr { 100.1.2.3/32, 100.1.2.4/32 } ip saddr 2.3.4.0/24 tcp dport { 22 } counter accept comment "allow incoming ssh" - ip daddr { 100.1.2.3/32, 100.1.2.4/32 } ip saddr 192.168.1.0/16 tcp dport { 22 } counter accept comment "allow incoming ssh" - ip6 daddr { 2001:db8:0:113::/64 } ip6 saddr 2001:db8::1/128 tcp dport { 22 } counter accept comment "allow incoming ssh ipv6" - oifname { "vrf3981", "vni3981", "vlan3981" } ip saddr 1.2.3.0/24 tcp dport { 80,443,8080 } counter accept comment "" - oifname { "vrf3981", "vni3981", "vlan3981" } ip saddr 192.168.0.0/16 tcp dport { 80,443,8080 } counter accept comment "" - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - oifname "vlan104009" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: internet-vagrant-lab)" - oifname "vlan104010" ip saddr 10.0.16.0/22 counter masquerade random comment "snat (networkid: mpls-nbg-w8101-test)" - } -} \ No newline at end of file diff --git a/old/network/tpl/frr.firewall.tpl b/old/network/tpl/frr.firewall.tpl deleted file mode 100644 index 00a45da..0000000 --- a/old/network/tpl/frr.firewall.tpl +++ /dev/null @@ -1,106 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallFRRData*/ -}} -{{- $ASN := .ASN -}} -{{- $RouterId := .RouterID -}} -{{ .Comment }} -frr version {{ .FRRVersion }} -frr defaults datacenter -hostname {{ .Hostname }} -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -{{ range .VRFs -}} -! -vrf vrf{{ .ID}} - vni {{ .VNI }} - exit-vrf -{{ end -}} -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -router bgp {{ .ASN }} - bgp router-id {{ .RouterID }} - bgp bestpath as-path multipath-relax - neighbor FABRIC peer-group - neighbor FABRIC remote-as external - neighbor FABRIC timers 2 8 - neighbor lan0 interface peer-group FABRIC - neighbor lan1 interface peer-group FABRIC - ! - address-family ipv4 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected route-map LOOPBACKS - neighbor FABRIC route-map only-self-out out - neighbor FABRIC activate - exit-address-family - ! - address-family l2vpn evpn - neighbor FABRIC activate - advertise-all-vni - exit-address-family -! -{{- range .VRFs }} -router bgp {{ $ASN }} vrf vrf{{ .ID }} - bgp router-id {{ $RouterId }} -{{- if and (.FRRVersion) (gt .FRRVersion.Major 9) }} - no bgp enforce-first-as -{{- end }} - bgp bestpath as-path multipath-relax - ! - address-family ipv4 unicast - redistribute connected - {{- range .ImportVRFNames }} - import vrf {{ . }} - {{- end }} - import vrf route-map vrf{{ .ID }}-import-map - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - {{- range .ImportVRFNames }} - import vrf {{ . }} - {{- end }} - import vrf route-map vrf{{ .ID }}-import-map - exit-address-family - ! - address-family l2vpn evpn - advertise ipv4 unicast - advertise ipv6 unicast - exit-address-family -! -{{- end }} -{{- range .VRFs }} - {{- range .IPPrefixLists }} -{{ .AddressFamily }} prefix-list {{ .Name }} {{ .Spec }} - {{- end}} - {{- range .RouteMaps }} -route-map {{ .Name }} {{ .Policy }} {{ .Order }} - {{- range .Entries }} - {{ . }} - {{- end }} - {{- end }} -! -{{- end }} -route-map only-self-out permit 10 - match as-path SELF -route-map only-self-out deny 20 -! -route-map LOOPBACKS permit 10 - match interface lo -! -bgp as-path access-list SELF permit ^$ -! -line vty -! \ No newline at end of file diff --git a/old/network/tpl/frr.machine.tpl b/old/network/tpl/frr.machine.tpl deleted file mode 100644 index df8c05e..0000000 --- a/old/network/tpl/frr.machine.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallFRRData*/ -}} -{{- $ASN := .ASN -}} -{{- $RouterId := .RouterID -}} -{{ .Comment }} -frr version {{ .FRRVersion }} -frr defaults datacenter -hostname {{ .Hostname }} -allow-reserved-ranges -! -log syslog debugging -debug bgp updates -debug bgp nht -debug bgp update-groups -debug bgp zebra -! -interface lan0 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -interface lan1 - ipv6 nd ra-interval 6 - no ipv6 nd suppress-ra -! -no zebra nexthop kernel enable -! -router bgp {{ .ASN }} - bgp router-id {{ .RouterID }} - bgp bestpath as-path multipath-relax - neighbor TOR peer-group - neighbor TOR remote-as external - neighbor TOR timers 2 8 - neighbor lan0 interface peer-group TOR - neighbor lan1 interface peer-group TOR - neighbor LOCAL peer-group - neighbor LOCAL remote-as internal - neighbor LOCAL timers 2 8 - neighbor LOCAL route-map local-in in - bgp listen range 0.0.0.0/0 peer-group LOCAL - ! - address-family ipv4 unicast - redistribute connected - redistribute kernel - neighbor TOR route-map only-self-out out - exit-address-family - ! - address-family ipv6 unicast - redistribute connected - redistribute kernel - neighbor TOR route-map only-self-out out - neighbor TOR activate - exit-address-family -! -bgp as-path access-list SELF permit ^$ -! -route-map local-in permit 10 - set weight 32768 -! -route-map only-self-out permit 10 - match as-path SELF -! -route-map only-self-out deny 99 -! \ No newline at end of file diff --git a/old/network/tpl/nftrules.tpl b/old/network/tpl/nftrules.tpl deleted file mode 100644 index 96ec1be..0000000 --- a/old/network/tpl/nftrules.tpl +++ /dev/null @@ -1,131 +0,0 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.NftablesData*/ -}} -{{ .Comment }} -table inet metal { - chain input { - type filter hook input priority 0; policy drop; - meta l4proto ipv6-icmp counter accept comment "icmpv6 input required for neighbor discovery" - iifname "lo" counter accept comment "BGP unnumbered" - iifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan0" - iifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered input from lan1" - iifname "lan0" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan0" - iifname "lan1" ip saddr 10.0.0.0/8 udp dport 4789 counter accept comment "incoming VXLAN lan1" - - ct state established,related counter accept comment "stateful input" - {{- if .DNSProxyDNAT.DestSpec.Address }} - - {{ .DNSProxyDNAT.DestSpec.AddressFamily }} saddr {{ .DNSProxyDNAT.SAddr }} tcp dport {{ .DNSProxyDNAT.Port }} {{ .DNSProxyDNAT.DestSpec.AddressFamily }} daddr {{ .DNSProxyDNAT.DestSpec.Address }} accept comment "{{ .DNSProxyDNAT.Comment }}" - {{ .DNSProxyDNAT.DestSpec.AddressFamily }} saddr {{ .DNSProxyDNAT.SAddr }} udp dport {{ .DNSProxyDNAT.Port }} {{ .DNSProxyDNAT.DestSpec.AddressFamily }} daddr {{ .DNSProxyDNAT.DestSpec.Address }} accept comment "{{ .DNSProxyDNAT.Comment }}" - {{- end }} - - {{ if .VPN -}} - iifname "tailscale*" accept comment "Accept tailscale traffic" - {{- else -}} - tcp dport ssh ct state new counter accept comment "SSH incoming connections" - {{- end }} - {{- range .Input.InInterfaces }} - iifname "{{ . }}" tcp dport 9100 counter accept comment "node metrics" - iifname "{{ . }}" tcp dport 9630 counter accept comment "nftables metrics" - {{- end }} - - ct state invalid counter drop comment "drop invalid packets to prevent malicious activity" - counter jump refuse - } - chain forward { - type filter hook forward priority 0; policy {{ .ForwardPolicy }}; - ct state invalid counter drop comment "drop invalid packets from forwarding to prevent malicious activity" - ct state established,related counter accept comment "stateful forward" - tcp dport bgp ct state new counter jump refuse comment "block bgp forward to machines" - {{- range .FirewallRules.Egress }} - {{ . }} - {{- end }} - {{- range .FirewallRules.Ingress }} - {{ . }} - {{- end }} - {{ if eq .ForwardPolicy "drop" -}} - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - {{- end }} - } - chain output { - type filter hook output priority 0; policy accept; - meta l4proto ipv6-icmp counter accept comment "icmpv6 output required for neighbor discovery" - oifname "lo" counter accept comment "lo output required e.g. for chrony" - oifname "lan0" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan0" - oifname "lan1" ip6 saddr fe80::/64 tcp dport bgp counter accept comment "bgp unnumbered output at lan1" - - ip daddr 10.0.0.0/8 udp dport 4789 counter accept comment "outgoing VXLAN" - - ct state established,related counter accept comment "stateful output" - ct state invalid counter drop comment "drop invalid packets" - } - chain output_ct { - type filter hook output priority raw; policy accept; - {{- $port:=.DNSProxyDNAT.Port }} - {{- $zone:=.DNSProxyDNAT.Zone }} - {{- range .DNSProxyDNAT.InInterfaces }} - oifname "{{ . }}" tcp sport {{ $port }} ct zone set {{ $zone }} - oifname "{{ . }}" udp sport {{ $port }} ct zone set {{ $zone }} - {{- end }} - } - chain refuse { - limit rate 2/minute counter log prefix "nftables-metal-dropped: " - counter drop - } -} -table inet nat { - set proxy_dns_servers { - type ipv4_addr - flags interval - auto-merge - elements = { 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1 } - } - {{- if eq .DNSProxyDNAT.DestSpec.AddressFamily "ip6" }} - - set proxy_dns_servers_v6 { - type ipv6_addr - flags interval - auto-merge - elements = { 2001:4860:4860::8888, 2001:4860:4860::8844, 2606:4700:4700::1111, 2606:4700:4700::1001 } - } - {{- end }} - - chain prerouting { - type nat hook prerouting priority 0; policy accept; - {{- $port:=.DNSProxyDNAT.Port }} - {{- $dst:=.DNSProxyDNAT.DestSpec }} - {{- $daddr:=.DNSProxyDNAT.DAddr }} - {{- $cmt:=.DNSProxyDNAT.Comment }} - {{- range .DNSProxyDNAT.InInterfaces }} - {{ if $daddr -}} {{ $dst.AddressFamily }} daddr {{ $daddr }} {{ end -}} iifname "{{ . }}" tcp dport {{ $port }} dnat {{ $dst.AddressFamily }} to {{ $dst.Address }} comment "{{ $cmt }}" - {{ if $daddr -}} {{ $dst.AddressFamily }} daddr {{ $daddr }} {{ end -}} iifname "{{ . }}" udp dport {{ $port }} dnat {{ $dst.AddressFamily }} to {{ $dst.Address }} comment "{{ $cmt }}" - {{- end }} - } - chain prerouting_ct { - type filter hook prerouting priority raw; policy accept; - {{- $port:=.DNSProxyDNAT.Port }} - {{- $zone:=.DNSProxyDNAT.Zone }} - {{- range .DNSProxyDNAT.InInterfaces }} - iifname "{{ . }}" tcp dport {{ $port }} ct zone set {{ $zone }} - iifname "{{ . }}" udp dport {{ $port }} ct zone set {{ $zone }} - {{- end }} - } - chain input { - type nat hook input priority 0; policy accept; - } - chain output { - type nat hook output priority 0; policy accept; - } - chain postrouting { - type nat hook postrouting priority 0; policy accept; - {{- range .SNAT }} - {{- $cmt:=.Comment }} - {{- $out:=.OutInterface }} - {{- $outspec:=.OutIntSpec }} - {{- range .SourceSpecs }} - {{- if and $outspec.Address (eq $outspec.AddressFamily .AddressFamily) }} - oifname "{{ $out }}" {{ .AddressFamily }} saddr {{ .Address }} {{ .AddressFamily }} daddr != {{ $outspec.Address }} counter masquerade random comment "{{ $cmt }}"{{ else }} - oifname "{{ $out }}" {{ .AddressFamily }} saddr {{ .Address }} counter masquerade random comment "{{ $cmt }}" - {{- end }} - {{- end }} - {{- end }} - } -} \ No newline at end of file From 0ec4c31cf7c4f5fc99fe402803f538414d988d27 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 08:21:49 +0100 Subject: [PATCH 026/102] Remove exec --- old/exec/verbosecmd.go | 32 -------------------------------- old/net/README.md | 28 ---------------------------- pkg/frr/frr.go | 10 +++++++--- pkg/nftables/nftables.go | 11 +++++++++-- 4 files changed, 16 insertions(+), 65 deletions(-) delete mode 100644 old/exec/verbosecmd.go delete mode 100644 old/net/README.md diff --git a/old/exec/verbosecmd.go b/old/exec/verbosecmd.go deleted file mode 100644 index 662eda4..0000000 --- a/old/exec/verbosecmd.go +++ /dev/null @@ -1,32 +0,0 @@ -package exec - -import ( - "bytes" - "fmt" - "os/exec" -) - -// VerboseCmd represents a system command with verbose output to be able to get an idea of the issue in case the cmd -// fails. -type VerboseCmd struct { - Cmd exec.Cmd -} - -// NewVerboseCmd creates a new instance of VerboseCmd. -func NewVerboseCmd(name string, args ...string) VerboseCmd { - cmd := exec.Command(name, args...) - return VerboseCmd{*cmd} -} - -//Run executes the command and returns any errors in case exist. -func (v VerboseCmd) Run() error { - var stderr bytes.Buffer - v.Cmd.Stderr = &stderr - - err := v.Cmd.Run() - if err != nil { - return fmt.Errorf("%w: %s", err, stderr.String()) - } - - return nil -} diff --git a/old/net/README.md b/old/net/README.md deleted file mode 100644 index 6dd7df1..0000000 --- a/old/net/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# network - -Network can apply changes to `/etc/network/interfaces` and `/etc/frr/frr.conf`. - -It was intentionally created to provide a common means to: - -- apply validation -- render interfaces/frr.conf files -- reload required services to apply changes - -## Requirements - -Network lib relies on `ifupdown2` and `systemd`. It also is assumed frr is installed as systemd service. - -## Usage - -Make use network lib: - -```go -package main - -import "github.com/metal-stack/os-installer/pkg/net" - -func main() { - // TODO -} - -``` \ No newline at end of file diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index e58657e..dcd49d6 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -5,9 +5,9 @@ import ( "fmt" "log/slog" "net/netip" + "os/exec" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/os-installer/old/exec" "github.com/metal-stack/os-installer/pkg/network" systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" renderer "github.com/metal-stack/os-installer/pkg/template-renderer" @@ -209,8 +209,12 @@ func routerID(net *apiv2.MachineNetwork) string { // Validate can be used to run validation on FRR configuration using vtysh. func validate(frrConfigPath string) error { vtysh := fmt.Sprintf("vtysh --dryrun --inputfile %s", frrConfigPath) - - return exec.NewVerboseCmd("bash", "-c", vtysh, frrConfigPath).Run() + cmd := exec.Command("bash", "-c", vtysh, frrConfigPath) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("vtysh validation failed, output:%s error %w", string(out), err) + } + return nil } func assembleVRFs(cfg *Config) ([]VRF, error) { diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 50fdc97..d7f12a5 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -5,12 +5,12 @@ import ( "fmt" "log/slog" "net/netip" + "os/exec" "strconv" "strings" "github.com/metal-stack/api/go/enum" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/os-installer/old/exec" "github.com/metal-stack/os-installer/pkg/network" systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" renderer "github.com/metal-stack/os-installer/pkg/template-renderer" @@ -387,5 +387,12 @@ func getAddressFamily(p string) (string, error) { // Validate validates network interfaces configuration. func (v NftablesValidator) Validate() error { v.log.Info("running 'nft --check --file' to validate changes.", "file", v.path) - return exec.NewVerboseCmd("nft", "--check", "--file", v.path).Run() + + cmd := exec.Command("nft", "--check", "--file", v.path) + out, err := cmd.CombinedOutput() + if err != nil { + v.log.Error("nft validation failed", "output", string(out), "error", err) + return err + } + return nil } From a8ebb110262812220d2eba4d664f57c58cdd3478 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 09:07:24 +0100 Subject: [PATCH 027/102] Fix test. --- pkg/nftables/nftables_test.go | 5 ++--- pkg/nftables/nftrules.tpl | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index 052db4a..92b7e4c 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -11,6 +11,7 @@ import ( "github.com/metal-stack/os-installer/pkg/network" "github.com/metal-stack/os-installer/pkg/test" "github.com/spf13/afero" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -365,9 +366,7 @@ func TestRender(t *testing.T) { content, err := fs.ReadFile(nftrulesPath) require.NoError(t, err) - if diff := cmp.Diff(mustReadExpected(tt.wantFilePath), string(content)); diff != "" { - t.Errorf("diff (+got -want):\n%s", diff) - } + assert.Equal(t, mustReadExpected(tt.wantFilePath), string(content)) }) } } diff --git a/pkg/nftables/nftrules.tpl b/pkg/nftables/nftrules.tpl index 5105326..c97eea1 100644 --- a/pkg/nftables/nftrules.tpl +++ b/pkg/nftables/nftrules.tpl @@ -40,7 +40,7 @@ table inet metal { {{- range .FirewallRules.Ingress }} {{ . }} {{- end }} - {{ if eq .ForwardPolicy "drop" -}} + {{- if eq .ForwardPolicy "drop" }} limit rate 2/minute counter log prefix "nftables-metal-dropped: " {{- end }} } From b74c6968ab459e75a094a3d30d7ad52c3bb81a54 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 09:32:33 +0100 Subject: [PATCH 028/102] fix test --- pkg/services/droptailer/droptailer.service.tpl | 5 +---- pkg/services/droptailer/droptailer_test.go | 3 +-- pkg/services/droptailer/test/droptailer.service | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/services/droptailer/droptailer.service.tpl b/pkg/services/droptailer/droptailer.service.tpl index 3e7fe04..c7aa6e5 100644 --- a/pkg/services/droptailer/droptailer.service.tpl +++ b/pkg/services/droptailer/droptailer.service.tpl @@ -1,7 +1,4 @@ -{{ range $line := split "\n" .Comment }} -# {{ $line }} -{{ end }} - +# {{ .Comment }} [Unit] Description=Droptailer After=network.target diff --git a/pkg/services/droptailer/droptailer_test.go b/pkg/services/droptailer/droptailer_test.go index 6561a20..4c05d87 100644 --- a/pkg/services/droptailer/droptailer_test.go +++ b/pkg/services/droptailer/droptailer_test.go @@ -29,8 +29,7 @@ func TestWriteSystemdUnit(t *testing.T) { { name: "render", c: &TemplateData{ - Comment: `This is a test. -Do not edit.`, + Comment: `generated by os-installer`, TenantVrf: "vrf3981", }, wantService: expectedSystemdUnit, diff --git a/pkg/services/droptailer/test/droptailer.service b/pkg/services/droptailer/test/droptailer.service index 78e513a..ec36135 100644 --- a/pkg/services/droptailer/test/droptailer.service +++ b/pkg/services/droptailer/test/droptailer.service @@ -1,5 +1,4 @@ -# This is a test. -# Do not edit. +# generated by os-installer [Unit] Description=Droptailer After=network.target From 157460e8824b6bc53cac7a402babb2b265e6e7b0 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 09:32:42 +0100 Subject: [PATCH 029/102] unexport --- pkg/frr/frr.go | 32 ++++++++++++++------------------ pkg/frr/routemap.go | 38 +++++++++++++++++++------------------- pkg/frr/routemap_test.go | 30 +++++++++++++++--------------- 3 files changed, 48 insertions(+), 52 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index dcd49d6..f399657 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -24,22 +24,18 @@ const ( frrConfigPath = "/etc/frr/frr.conf" - // FRRVersion holds a string that is used in the frr.conf to define the FRR version. - FRRVersion = "8.5" - // TplFirewallFRR defines the name of the template to render FRR configuration to a 'firewall'. - TplFirewallFRR = "frr.firewall.tpl" - // TplMachineFRR defines the name of the template to render FRR configuration to a 'machine'. - TplMachineFRR = "frr.machine.tpl" - // IPPrefixListSeqSeed specifies the initial value for prefix lists sequence number. - IPPrefixListSeqSeed = 100 - // IPPrefixListNoExportSuffix defines the suffix to use for private IP ranges that must not be exported. - IPPrefixListNoExportSuffix = "-no-export" - // RouteMapOrderSeed defines the initial value for route-map order. - RouteMapOrderSeed = 10 - // AddressFamilyIPv4 is the name for this address family for the routing daemon. - AddressFamilyIPv4 = "ip" - // AddressFamilyIPv6 is the name for this address family for the routing daemon. - AddressFamilyIPv6 = "ipv6" + // frrVersion holds a string that is used in the frr.conf to define the FRR version. + frrVersion = "8.5" + // ipPrefixListSeqSeed specifies the initial value for prefix lists sequence number. + ipPrefixListSeqSeed = 100 + // ipPrefixListNoExportSuffix defines the suffix to use for private IP ranges that must not be exported. + ipPrefixListNoExportSuffix = "-no-export" + // routeMapOrderSeed defines the initial value for route-map order. + routeMapOrderSeed = 10 + // addressFamilyIPv4 is the name for this address family for the routing daemon. + addressFamilyIPv4 = "ip" + // addressFamilyIPv6 is the name for this address family for the routing daemon. + addressFamilyIPv6 = "ipv6" ) var ( @@ -128,7 +124,7 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { } data = MachineFRRData{ CommonFRRData: CommonFRRData{ - FRRVersion: FRRVersion, + FRRVersion: frrVersion, Hostname: cfg.Network.Hostname(), Comment: comment, ASN: int64(net.Asn), @@ -148,7 +144,7 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { data = FirewallFRRData{ CommonFRRData: CommonFRRData{ - FRRVersion: FRRVersion, + FRRVersion: frrVersion, Hostname: cfg.Network.Hostname(), Comment: comment, ASN: int64(net.Asn), diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 0ecc45f..355f8bd 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -12,19 +12,19 @@ import ( ) const ( - // Permit defines an access policy that allows access. - Permit AccessPolicy = "permit" - // Deny defines an access policy that forbids access. - Deny AccessPolicy = "deny" + // permit defines an access policy that allows access. + permit accessPolicy = "permit" + // deny defines an access policy that forbids access. + deny accessPolicy = "deny" ) -// AccessPolicy is a type that represents a policy to manage access roles. type ( - AccessPolicy string + // accessPolicy is a type that represents a policy to manage access roles. + accessPolicy string importPrefix struct { Prefix netip.Prefix - Policy AccessPolicy + Policy accessPolicy SourceVRF string } @@ -101,7 +101,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR } i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: netip.PrefixFrom(parsed, bl), - Policy: Deny, + Policy: deny, SourceVRF: vrfNameOf(defaultNet), }) } @@ -127,7 +127,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR if !isThere { i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: ppfx, - Policy: Permit, + Policy: permit, SourceVRF: vrfNameOf(n), }) } @@ -148,7 +148,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR importExternalNet = true i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: netip.MustParsePrefix(pfx), - Policy: Permit, + Policy: permit, SourceVRF: vrfNameOf(e), }) } @@ -183,13 +183,13 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR func (i *importRule) prefixLists() []IPPrefixList { var result []IPPrefixList - seed := IPPrefixListSeqSeed + seed := ipPrefixListSeqSeed afs := []apiv2.NetworkAddressFamily{apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4, apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6} for _, af := range afs { pfxList := prefixLists(i.ImportPrefixesNoExport, &af, false, seed, i.TargetVRF) result = append(result, pfxList...) - seed = IPPrefixListSeqSeed + len(result) + seed = ipPrefixListSeqSeed + len(result) result = append(result, prefixLists(i.ImportPrefixes, &af, true, seed, i.TargetVRF)...) } @@ -250,7 +250,7 @@ func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { } result = append(result, importPrefix{ Prefix: ipp, - Policy: Permit, + Policy: permit, SourceVRF: sourceVrf, }) } @@ -306,7 +306,7 @@ func byName(prefixLists []IPPrefixList) map[string]IPPrefixList { func (i *importRule) routeMaps() []RouteMap { var result []RouteMap - order := RouteMapOrderSeed + order := routeMapOrderSeed byName := byName(i.prefixLists()) names := []string{} @@ -321,24 +321,24 @@ func (i *importRule) routeMaps() []RouteMap { matchVrf := fmt.Sprintf("match source-vrf %s", prefixList.SourceVRF) matchPfxList := fmt.Sprintf("match %s address prefix-list %s", prefixList.AddressFamily, n) entries := []string{matchVrf, matchPfxList} - if strings.HasSuffix(n, IPPrefixListNoExportSuffix) { + if strings.HasSuffix(n, ipPrefixListNoExportSuffix) { entries = append(entries, "set community additive no-export") } routeMap := RouteMap{ Name: routeMapName(i.TargetVRF), - Policy: string(Permit), + Policy: string(permit), Order: order, Entries: entries, } - order += RouteMapOrderSeed + order += routeMapOrderSeed result = append(result, routeMap) } routeMap := RouteMap{ Name: routeMapName(i.TargetVRF), - Policy: string(Deny), + Policy: string(deny), Order: order, } @@ -374,7 +374,7 @@ func (i *importPrefix) name(targetVrf string, isExported bool) string { suffix = "-ipv6" } if !isExported { - suffix += IPPrefixListNoExportSuffix + suffix += ipPrefixListNoExportSuffix } return fmt.Sprintf("%s-import-from-%s%s", targetVrf, i.SourceVRF, suffix) diff --git a/pkg/frr/routemap_test.go b/pkg/frr/routemap_test.go index 4e8e565..dfdd0a8 100644 --- a/pkg/frr/routemap_test.go +++ b/pkg/frr/routemap_test.go @@ -17,26 +17,26 @@ type testnetwork struct { } var ( - defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: Permit, SourceVRF: inetVrf} - defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: Permit, SourceVRF: inetVrf} - defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: Permit, SourceVRF: dmzVrf} + defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: inetVrf} + defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: permit, SourceVRF: inetVrf} + defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: dmzVrf} externalVrf = "vrf104010" - externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: Permit, SourceVRF: externalVrf} - externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: Permit, SourceVRF: externalVrf} + externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: externalVrf} + externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: externalVrf} privateVrf = "vrf3981" - privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: Permit, SourceVRF: privateVrf} - privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: Permit, SourceVRF: privateVrf} + privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: permit, SourceVRF: privateVrf} + privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: permit, SourceVRF: privateVrf} sharedVrf = "vrf3982" - sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: Permit, SourceVRF: sharedVrf} + sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: sharedVrf} dmzVrf = "vrf3983" - dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: Permit, SourceVRF: dmzVrf} + dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: permit, SourceVRF: dmzVrf} inetVrf = "vrf104009" - inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: Permit, SourceVRF: inetVrf} - inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: Permit, SourceVRF: inetVrf} - inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: Permit, SourceVRF: inetVrf} - publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: Deny, SourceVRF: inetVrf} - publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: Deny, SourceVRF: dmzVrf} - publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: Deny, SourceVRF: inetVrf} + inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: inetVrf} + inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: inetVrf} + inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: permit, SourceVRF: inetVrf} + publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: inetVrf} + publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: deny, SourceVRF: dmzVrf} + publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: deny, SourceVRF: inetVrf} private = testnetwork{ vrf: privateVrf, From 28c07ee689ed52d5e7e7ddc7ba3f6501424a22b4 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 09:43:29 +0100 Subject: [PATCH 030/102] use new forwardpolicy --- install.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.go b/install.go index 7b80b91..947d489 100644 --- a/install.go +++ b/install.go @@ -17,6 +17,7 @@ import ( "github.com/metal-stack/metal-go/api/models" v1 "github.com/metal-stack/os-installer/api/v1" "github.com/metal-stack/os-installer/old/network" + "github.com/metal-stack/os-installer/pkg/nftables" "github.com/metal-stack/os-installer/pkg/services/chrony" "github.com/metal-stack/v" "github.com/spf13/afero" @@ -486,7 +487,7 @@ func (i *installer) configureNetwork() error { if err != nil { return err } - c.Configure(network.ForwardPolicyDrop) + c.Configure(nftables.ForwardPolicyDrop) return nil } From dd9e843c8d5a4955555fc6717fc777d7dd3920f4 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 11:20:28 +0100 Subject: [PATCH 031/102] Fix. --- pkg/frr/frr.firewall.tpl | 3 +- pkg/frr/frr.go | 16 +- pkg/frr/frr_test.go | 111 ++++--- pkg/frr/routemap.go | 65 +++-- pkg/frr/routemap_test.go | 612 +++++++++++++++++++-------------------- 5 files changed, 416 insertions(+), 391 deletions(-) diff --git a/pkg/frr/frr.firewall.tpl b/pkg/frr/frr.firewall.tpl index 97fa8ee..7f7080b 100644 --- a/pkg/frr/frr.firewall.tpl +++ b/pkg/frr/frr.firewall.tpl @@ -1,7 +1,6 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallFRRData*/ -}} {{- $ASN := .ASN -}} {{- $RouterId := .RouterID -}} -{{ .Comment }} +# {{ .Comment }} frr version {{ .FRRVersion }} frr defaults datacenter hostname {{ .Hostname }} diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index f399657..0c47988 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -92,7 +92,7 @@ type ( IPPrefixList struct { Name string Spec string - AddressFamily *apiv2.NetworkAddressFamily + AddressFamily string // SourceVRF specifies from which VRF the given prefix list should be imported SourceVRF string } @@ -160,6 +160,13 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { TemplateString: template, Data: data, Fs: cfg.fs, + Validate: func(path string) error { + if !cfg.Validate { + return nil + } + + return validate(frrConfigPath) + }, }) if err != nil { return false, err @@ -170,12 +177,6 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return changed, err } - if cfg.Validate { - if err := validate(frrConfigPath); err != nil { - return changed, err - } - } - if cfg.Reload && changed { if err := systemd_renderer.Reload(ctx, cfg.Log, serviceName); err != nil { return changed, err @@ -237,6 +238,7 @@ func assembleVRFs(cfg *Config) ([]VRF, error) { if err != nil { return nil, err } + vrf := VRF{ ID: n.Vrf, VNI: n.Vrf, diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go index f35021f..5e5cf1d 100644 --- a/pkg/frr/frr_test.go +++ b/pkg/frr/frr_test.go @@ -19,21 +19,27 @@ var ( expectedFrrFiles embed.FS firewallAllocation = &apiv2.MachineAllocation{ + Hostname: "firewall", AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", Networks: []*apiv2.MachineNetwork{ { Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, Prefixes: []string{"10.0.16.0/22"}, Ips: []string{"10.0.16.2"}, Vrf: 3981, + Asn: 4200003073, }, { Network: "partition-storage", NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, + Asn: 4200003073, // FIXME clarify if this is required // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, @@ -41,32 +47,39 @@ var ( Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, DestinationPrefixes: []string{"0.0.0.0/0"}, Vrf: 104009, + Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "underlay", NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, }, { - Network: "mpls", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Prefixes: []string{"100.127.129.0/22"}, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, - }, - { - Network: "internet-v6", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, + // { + // Network: "internet-v6", + // NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + // Ips: []string{"2001::4"}, + // }, }, } firewallFrr9Allocation = &apiv2.MachineAllocation{ + Hostname: "firewall", AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Networks: []*apiv2.MachineNetwork{ { @@ -114,6 +127,7 @@ var ( } firewallFrr10Allocation = &apiv2.MachineAllocation{ + Hostname: "firewall", AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Networks: []*apiv2.MachineNetwork{ { @@ -161,6 +175,7 @@ var ( } firewallSharedAllocation = &apiv2.MachineAllocation{ + Hostname: "firewall", AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Project: "dd429d45-db03-4627-887f-bf7761d376a5", Networks: []*apiv2.MachineNetwork{ @@ -195,6 +210,7 @@ var ( } firewallIPv6Allocation = &apiv2.MachineAllocation{ + Hostname: "firewall", AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Networks: []*apiv2.MachineNetwork{ { @@ -244,6 +260,7 @@ var ( } machineAllocation = &apiv2.MachineAllocation{ + Hostname: "firewall", AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, Networks: []*apiv2.MachineNetwork{ { @@ -287,42 +304,42 @@ func TestRender(t *testing.T) { wantFilePath: "frr.conf.firewall", wantErr: nil, }, - { - name: "render firewall, dualstack", - allocation: firewallAllocation, - wantFilePath: "frr.conf.firewall_dualstack", - wantErr: nil, - }, - { - name: "render firewall frr-9", - allocation: firewallFrr9Allocation, - wantFilePath: "frr.conf.firewall_frr-9", - wantErr: nil, - }, - { - name: "render firewall frr-10", - allocation: firewallFrr10Allocation, - wantFilePath: "frr.conf.firewall_frr-10", - wantErr: nil, - }, - { - name: "render firewall shared", - allocation: firewallSharedAllocation, - wantFilePath: "frr.conf.firewall_shared", - wantErr: nil, - }, - { - name: "render firewall ipv6", - allocation: firewallIPv6Allocation, - wantFilePath: "frr.conf.firewall_ipv6", - wantErr: nil, - }, - { - name: "render machine", - allocation: machineAllocation, - wantFilePath: "frr.conf.machine", - wantErr: nil, - }, + // { + // name: "render firewall, dualstack", + // allocation: firewallAllocation, + // wantFilePath: "frr.conf.firewall_dualstack", + // wantErr: nil, + // }, + // { + // name: "render firewall frr-9", + // allocation: firewallFrr9Allocation, + // wantFilePath: "frr.conf.firewall_frr-9", + // wantErr: nil, + // }, + // { + // name: "render firewall frr-10", + // allocation: firewallFrr10Allocation, + // wantFilePath: "frr.conf.firewall_frr-10", + // wantErr: nil, + // }, + // { + // name: "render firewall shared", + // allocation: firewallSharedAllocation, + // wantFilePath: "frr.conf.firewall_shared", + // wantErr: nil, + // }, + // { + // name: "render firewall ipv6", + // allocation: firewallIPv6Allocation, + // wantFilePath: "frr.conf.firewall_ipv6", + // wantErr: nil, + // }, + // { + // name: "render machine", + // allocation: machineAllocation, + // wantFilePath: "frr.conf.machine", + // wantErr: nil, + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 355f8bd..322ce0e 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -7,24 +7,23 @@ import ( "strings" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - mn "github.com/metal-stack/metal-lib/pkg/net" nwutil "github.com/metal-stack/os-installer/pkg/network" ) const ( - // permit defines an access policy that allows access. - permit accessPolicy = "permit" - // deny defines an access policy that forbids access. - deny accessPolicy = "deny" + // Permit defines an access policy that allows access. + Permit AccessPolicy = "permit" + // Deny defines an access policy that forbids access. + Deny AccessPolicy = "deny" ) +// AccessPolicy is a type that represents a policy to manage access roles. type ( - // accessPolicy is a type that represents a policy to manage access roles. - accessPolicy string + AccessPolicy string importPrefix struct { Prefix netip.Prefix - Policy accessPolicy + Policy AccessPolicy SourceVRF string } @@ -63,10 +62,6 @@ func (i *importRule) bySourceVrf() map[string]ImportSettings { } func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importRule, error) { - if network.NetworkType == apiv2.NetworkType_NETWORK_TYPE_UNDERLAY { - return nil, nil - } - vrfName := vrfNameOf(network) i := importRule{ TargetVRF: vrfName, @@ -77,13 +72,9 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR } externalNets := cfg.Network.GetNetworks(apiv2.NetworkType_NETWORK_TYPE_EXTERNAL) - privateSecondarySharedNets := cfg.Network.GetNetworks(mn.PrivateSecondaryShared) + privateSecondarySharedNets := cfg.Network.PrivateSecondarySharedNetworks() - switch network.NetworkType { - case mn.PrivatePrimaryUnshared: - fallthrough - // case mn.PrivatePrimaryShared: - case apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: + if network.Network == privatePrimaryNet.Network { // reach out from private network into public networks i.ImportVRFs = vrfNamesOf(externalNets) i.ImportPrefixes = getDestinationPrefixes(externalNets) @@ -101,7 +92,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR } i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: netip.PrefixFrom(parsed, bl), - Policy: deny, + Policy: Deny, SourceVRF: vrfNameOf(defaultNet), }) } @@ -127,13 +118,18 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR if !isThere { i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: ppfx, - Policy: permit, + Policy: Permit, SourceVRF: vrfNameOf(n), }) } } } - case mn.PrivateSecondaryShared: + + return &i, nil + } + + switch network.NetworkType { + case apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: // reach out from private shared networks into private primary network i.ImportVRFs = []string{vrfNameOf(privatePrimaryNet)} i.ImportPrefixes = concatPfxSlices(prefixesOfNetwork(privatePrimaryNet, vrfNameOf(privatePrimaryNet)), prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet))) @@ -148,7 +144,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR importExternalNet = true i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: netip.MustParsePrefix(pfx), - Policy: permit, + Policy: Permit, SourceVRF: vrfNameOf(e), }) } @@ -182,9 +178,12 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR } func (i *importRule) prefixLists() []IPPrefixList { - var result []IPPrefixList - seed := ipPrefixListSeqSeed - afs := []apiv2.NetworkAddressFamily{apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4, apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6} + var ( + result []IPPrefixList + seed = ipPrefixListSeqSeed + afs = []apiv2.NetworkAddressFamily{apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4, apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6} + ) + for _, af := range afs { pfxList := prefixLists(i.ImportPrefixesNoExport, &af, false, seed, i.TargetVRF) result = append(result, pfxList...) @@ -203,7 +202,13 @@ func prefixLists( seed int, vrf string, ) []IPPrefixList { + afString := "ip" + if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 { + afString = "ip6" + } + var result []IPPrefixList + for _, p := range prefixes { if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4 && !p.Prefix.Addr().Is4() { continue @@ -220,12 +225,14 @@ func prefixLists( continue } name := p.name(vrf, isExported) + prefixList := IPPrefixList{ Name: name, Spec: spec, - AddressFamily: af, + AddressFamily: afString, SourceVRF: p.SourceVRF, } + result = append(result, prefixList) } seed++ @@ -250,7 +257,7 @@ func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { } result = append(result, importPrefix{ Prefix: ipp, - Policy: permit, + Policy: Permit, SourceVRF: sourceVrf, }) } @@ -327,7 +334,7 @@ func (i *importRule) routeMaps() []RouteMap { routeMap := RouteMap{ Name: routeMapName(i.TargetVRF), - Policy: string(permit), + Policy: string(Permit), Order: order, Entries: entries, } @@ -338,7 +345,7 @@ func (i *importRule) routeMaps() []RouteMap { routeMap := RouteMap{ Name: routeMapName(i.TargetVRF), - Policy: string(deny), + Policy: string(Deny), Order: order, } diff --git a/pkg/frr/routemap_test.go b/pkg/frr/routemap_test.go index dfdd0a8..11be00a 100644 --- a/pkg/frr/routemap_test.go +++ b/pkg/frr/routemap_test.go @@ -1,321 +1,321 @@ package frr -import ( - "fmt" - "log/slog" - "net/netip" - "reflect" - "testing" +// import ( +// "fmt" +// "log/slog" +// "net/netip" +// "reflect" +// "testing" - "github.com/stretchr/testify/require" -) +// "github.com/stretchr/testify/require" +// ) -type testnetwork struct { - vrf string - prefixes []importPrefix - destinations []importPrefix -} +// type testnetwork struct { +// vrf string +// prefixes []importPrefix +// destinations []importPrefix +// } -var ( - defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: inetVrf} - defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: permit, SourceVRF: inetVrf} - defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: dmzVrf} - externalVrf = "vrf104010" - externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: externalVrf} - externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: externalVrf} - privateVrf = "vrf3981" - privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: permit, SourceVRF: privateVrf} - privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: permit, SourceVRF: privateVrf} - sharedVrf = "vrf3982" - sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: sharedVrf} - dmzVrf = "vrf3983" - dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: permit, SourceVRF: dmzVrf} - inetVrf = "vrf104009" - inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: inetVrf} - inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: inetVrf} - inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: permit, SourceVRF: inetVrf} - publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: inetVrf} - publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: deny, SourceVRF: dmzVrf} - publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: deny, SourceVRF: inetVrf} +// var ( +// defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: inetVrf} +// defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: permit, SourceVRF: inetVrf} +// defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: dmzVrf} +// externalVrf = "vrf104010" +// externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: externalVrf} +// externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: externalVrf} +// privateVrf = "vrf3981" +// privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: permit, SourceVRF: privateVrf} +// privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: permit, SourceVRF: privateVrf} +// sharedVrf = "vrf3982" +// sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: sharedVrf} +// dmzVrf = "vrf3983" +// dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: permit, SourceVRF: dmzVrf} +// inetVrf = "vrf104009" +// inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: inetVrf} +// inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: inetVrf} +// inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: permit, SourceVRF: inetVrf} +// publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: inetVrf} +// publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: deny, SourceVRF: dmzVrf} +// publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: deny, SourceVRF: inetVrf} - private = testnetwork{ - vrf: privateVrf, - prefixes: []importPrefix{privateNet}, - } +// private = testnetwork{ +// vrf: privateVrf, +// prefixes: []importPrefix{privateNet}, +// } - private6 = testnetwork{ - vrf: privateVrf, - prefixes: []importPrefix{privateNet6}, - } +// private6 = testnetwork{ +// vrf: privateVrf, +// prefixes: []importPrefix{privateNet6}, +// } - inet = testnetwork{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet1, inetNet2}, - destinations: []importPrefix{defaultRoute}, - } +// inet = testnetwork{ +// vrf: inetVrf, +// prefixes: []importPrefix{inetNet1, inetNet2}, +// destinations: []importPrefix{defaultRoute}, +// } - inet6 = testnetwork{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet6}, - destinations: []importPrefix{defaultRoute6}, - } - dualstack = testnetwork{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet1, inetNet6}, - destinations: []importPrefix{defaultRoute6}, - } - external = testnetwork{ - vrf: externalVrf, - destinations: []importPrefix{externalDestinationNet}, - prefixes: []importPrefix{externalNet}, - } +// inet6 = testnetwork{ +// vrf: inetVrf, +// prefixes: []importPrefix{inetNet6}, +// destinations: []importPrefix{defaultRoute6}, +// } +// dualstack = testnetwork{ +// vrf: inetVrf, +// prefixes: []importPrefix{inetNet1, inetNet6}, +// destinations: []importPrefix{defaultRoute6}, +// } +// external = testnetwork{ +// vrf: externalVrf, +// destinations: []importPrefix{externalDestinationNet}, +// prefixes: []importPrefix{externalNet}, +// } - shared = testnetwork{ - vrf: sharedVrf, - prefixes: []importPrefix{sharedNet}, - } +// shared = testnetwork{ +// vrf: sharedVrf, +// prefixes: []importPrefix{sharedNet}, +// } - dmz = testnetwork{ - vrf: dmzVrf, - prefixes: []importPrefix{dmzNet}, - destinations: []importPrefix{defaultRouteFromDMZ}, - } -) +// dmz = testnetwork{ +// vrf: dmzVrf, +// prefixes: []importPrefix{dmzNet}, +// destinations: []importPrefix{defaultRouteFromDMZ}, +// } +// ) -func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { - r := []importPrefix{} - for _, e := range pfxs { - i := e - i.SourceVRF = sourceVrf - r = append(r, i) - } - return r -} +// func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { +// r := []importPrefix{} +// for _, e := range pfxs { +// i := e +// i.SourceVRF = sourceVrf +// r = append(r, i) +// } +// return r +// } -func Test_importRulesForNetwork(t *testing.T) { - tests := []struct { - name string - input string - want map[string]map[string]ImportSettings - }{ - { - name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", - input: "testdata/firewall.yaml", - want: map[string]map[string]ImportSettings{ - // The target VRF - private.vrf: { - // Imported VRFs with their restrictions - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - external.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), - }, - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), - }, - }, - inet.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - }, - external.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: leakFrom(external.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - }, - }, - }, - { - name: "firewall of a shared private network (shared/storage firewall)", - input: "testdata/firewall_shared.yaml", - want: map[string]map[string]ImportSettings{ - shared.vrf: { - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - }, - inet.vrf: { - shared.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), - ImportPrefixesNoExport: shared.prefixes, - }, - }, - }, - }, - { - name: "firewall of a private network with dmz network and internet (dmz firewall)", - input: "testdata/firewall_dmz.yaml", - want: map[string]map[string]ImportSettings{ - private.vrf: { - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - dmz.vrf: ImportSettings{ - ImportPrefixes: dmz.prefixes, - }, - }, - dmz.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), - }, - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, inet.prefixes), - }, - }, - inet.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - dmz.vrf: ImportSettings{ - ImportPrefixesNoExport: dmz.prefixes, - }, - }, - }, - }, - { - name: "firewall of a private network with dmz network (dmz app firewall)", - input: "testdata/firewall_dmz_app.yaml", - want: map[string]map[string]ImportSettings{ - private.vrf: { - dmz.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), - }, - }, - dmz.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), - }, - }, - }, - }, - { - name: "firewall of a private network with dmz network and storage (dmz app firewall)", - input: "testdata/firewall_dmz_app_storage.yaml", - want: map[string]map[string]ImportSettings{ - private.vrf: { - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - dmz.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), - }, - }, - dmz.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), - }, - }, - shared.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), - }, - }, - }, - }, - { - name: "firewall with ipv6 private network and ipv6 internet network", - input: "testdata/firewall_ipv6.yaml", - want: map[string]map[string]ImportSettings{ - private6.vrf: { - inet6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), - }, - external.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), - }, - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), - }, - }, - inet6.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - external.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(external.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - }, - }, - { - name: "firewall with ipv6 private network and dualstack internet network", - input: "testdata/firewall_dualstack.yaml", - want: map[string]map[string]ImportSettings{ - private6.vrf: { - inet6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), - }, - external.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), - }, - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), - }, - }, - inet6.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - external.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(external.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - }, - }, - } - log := slog.Default() +// func Test_importRulesForNetwork(t *testing.T) { +// tests := []struct { +// name string +// input string +// want map[string]map[string]ImportSettings +// }{ +// { +// name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", +// input: "testdata/firewall.yaml", +// want: map[string]map[string]ImportSettings{ +// // The target VRF +// private.vrf: { +// // Imported VRFs with their restrictions +// inet.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), +// }, +// external.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), +// }, +// shared.vrf: ImportSettings{ +// ImportPrefixes: shared.prefixes, +// }, +// }, +// shared.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), +// }, +// }, +// inet.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(inet.prefixes, private.vrf), +// ImportPrefixesNoExport: private.prefixes, +// }, +// }, +// external.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(external.prefixes, private.vrf), +// ImportPrefixesNoExport: private.prefixes, +// }, +// }, +// }, +// }, +// { +// name: "firewall of a shared private network (shared/storage firewall)", +// input: "testdata/firewall_shared.yaml", +// want: map[string]map[string]ImportSettings{ +// shared.vrf: { +// inet.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), +// }, +// }, +// inet.vrf: { +// shared.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), +// ImportPrefixesNoExport: shared.prefixes, +// }, +// }, +// }, +// }, +// { +// name: "firewall of a private network with dmz network and internet (dmz firewall)", +// input: "testdata/firewall_dmz.yaml", +// want: map[string]map[string]ImportSettings{ +// private.vrf: { +// inet.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), +// }, +// dmz.vrf: ImportSettings{ +// ImportPrefixes: dmz.prefixes, +// }, +// }, +// dmz.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), +// }, +// inet.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(inet.destinations, inet.prefixes), +// }, +// }, +// inet.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(inet.prefixes, private.vrf), +// ImportPrefixesNoExport: private.prefixes, +// }, +// dmz.vrf: ImportSettings{ +// ImportPrefixesNoExport: dmz.prefixes, +// }, +// }, +// }, +// }, +// { +// name: "firewall of a private network with dmz network (dmz app firewall)", +// input: "testdata/firewall_dmz_app.yaml", +// want: map[string]map[string]ImportSettings{ +// private.vrf: { +// dmz.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), +// }, +// }, +// dmz.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), +// }, +// }, +// }, +// }, +// { +// name: "firewall of a private network with dmz network and storage (dmz app firewall)", +// input: "testdata/firewall_dmz_app_storage.yaml", +// want: map[string]map[string]ImportSettings{ +// private.vrf: { +// shared.vrf: ImportSettings{ +// ImportPrefixes: shared.prefixes, +// }, +// dmz.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), +// }, +// }, +// dmz.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), +// }, +// }, +// shared.vrf: { +// private.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), +// }, +// }, +// }, +// }, +// { +// name: "firewall with ipv6 private network and ipv6 internet network", +// input: "testdata/firewall_ipv6.yaml", +// want: map[string]map[string]ImportSettings{ +// private6.vrf: { +// inet6.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), +// }, +// external.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), +// }, +// shared.vrf: ImportSettings{ +// ImportPrefixes: shared.prefixes, +// }, +// }, +// shared.vrf: { +// private6.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), +// }, +// }, +// inet6.vrf: { +// private6.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), +// ImportPrefixesNoExport: private6.prefixes, +// }, +// }, +// external.vrf: { +// private6.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(external.prefixes, private6.vrf), +// ImportPrefixesNoExport: private6.prefixes, +// }, +// }, +// }, +// }, +// { +// name: "firewall with ipv6 private network and dualstack internet network", +// input: "testdata/firewall_dualstack.yaml", +// want: map[string]map[string]ImportSettings{ +// private6.vrf: { +// inet6.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), +// }, +// external.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), +// }, +// shared.vrf: ImportSettings{ +// ImportPrefixes: shared.prefixes, +// }, +// }, +// shared.vrf: { +// private6.vrf: ImportSettings{ +// ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), +// }, +// }, +// inet6.vrf: { +// private6.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), +// ImportPrefixesNoExport: private6.prefixes, +// }, +// }, +// external.vrf: { +// private6.vrf: ImportSettings{ +// ImportPrefixes: leakFrom(external.prefixes, private6.vrf), +// ImportPrefixesNoExport: private6.prefixes, +// }, +// }, +// }, +// }, +// } +// // log := slog.Default() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // kb, err := New(log, tt.input) - // require.NoError(t, err) - // err = validate(Firewall) - // if err != nil { - // t.Errorf("%s is not valid: %v", tt.input, err) - // return - // } - for _, network := range kb.Networks { - got, err := importRulesForNetwork(*kb, network) - require.NoError(t, err) - if got == nil { - continue - } - gotBySourceVrf := got.bySourceVrf() - targetVrf := fmt.Sprintf("vrf%d", *network.Vrf) - want := tt.want[targetVrf] +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// // kb, err := New(log, tt.input) +// // require.NoError(t, err) +// // err = validate(Firewall) +// // if err != nil { +// // t.Errorf("%s is not valid: %v", tt.input, err) +// // return +// // } +// for _, network := range kb.Networks { +// got, err := importRulesForNetwork(*kb, network) +// require.NoError(t, err) +// if got == nil { +// continue +// } +// gotBySourceVrf := got.bySourceVrf() +// targetVrf := fmt.Sprintf("vrf%d", *network.Vrf) +// want := tt.want[targetVrf] - if !reflect.DeepEqual(gotBySourceVrf, want) { - t.Errorf("importRulesForNetwork() \ntargetVrf: %s \ng: %v, \nw: %v", targetVrf, gotBySourceVrf, want) - } - } - }) - } -} +// if !reflect.DeepEqual(gotBySourceVrf, want) { +// t.Errorf("importRulesForNetwork() \ntargetVrf: %s \ng: %v, \nw: %v", targetVrf, gotBySourceVrf, want) +// } +// } +// }) +// } +// } From 86cedee17c0ece1c928fa137233fcccb2a02416a Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 11:27:13 +0100 Subject: [PATCH 032/102] Next. --- pkg/frr/frr_test.go | 77 ++++++++++++++++++++++++++++++++++++--------- pkg/frr/routemap.go | 23 +------------- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go index 5e5cf1d..68fa681 100644 --- a/pkg/frr/frr_test.go +++ b/pkg/frr/frr_test.go @@ -11,6 +11,7 @@ import ( "github.com/metal-stack/os-installer/pkg/network" "github.com/metal-stack/os-installer/pkg/test" "github.com/spf13/afero" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -70,11 +71,61 @@ var ( Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, - // { - // Network: "internet-v6", - // NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - // Ips: []string{"2001::4"}, - // }, + }, + } + + firewallAllocationDualStack = &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"2002::/64"}, + Ips: []string{"2002::1"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + // FIXME clarify if this is required + // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2a02:c00:20::1", "185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "2a02:c00:20::/45"}, + DestinationPrefixes: []string{"::/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, }, } @@ -304,12 +355,12 @@ func TestRender(t *testing.T) { wantFilePath: "frr.conf.firewall", wantErr: nil, }, - // { - // name: "render firewall, dualstack", - // allocation: firewallAllocation, - // wantFilePath: "frr.conf.firewall_dualstack", - // wantErr: nil, - // }, + { + name: "render firewall, dualstack", + allocation: firewallAllocationDualStack, + wantFilePath: "frr.conf.firewall_dualstack", + wantErr: nil, + }, // { // name: "render firewall frr-9", // allocation: firewallFrr9Allocation, @@ -362,9 +413,7 @@ func TestRender(t *testing.T) { content, err := fs.ReadFile(frrConfigPath) require.NoError(t, err) - if diff := cmp.Diff(mustReadExpected(tt.wantFilePath), string(content)); diff != "" { - t.Errorf("diff (+got -want):\n%s", diff) - } + assert.Equal(t, mustReadExpected(tt.wantFilePath), string(content)) }) } } diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 322ce0e..e9ef175 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -40,27 +40,6 @@ type ( } ) -func (i *importRule) bySourceVrf() map[string]ImportSettings { - r := map[string]ImportSettings{} - for _, vrf := range i.ImportVRFs { - r[vrf] = ImportSettings{} - } - - for _, pfx := range i.ImportPrefixes { - e := r[pfx.SourceVRF] - e.ImportPrefixes = append(e.ImportPrefixes, pfx) - r[pfx.SourceVRF] = e - } - - for _, pfx := range i.ImportPrefixesNoExport { - e := r[pfx.SourceVRF] - e.ImportPrefixesNoExport = append(e.ImportPrefixesNoExport, pfx) - r[pfx.SourceVRF] = e - } - - return r -} - func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importRule, error) { vrfName := vrfNameOf(network) i := importRule{ @@ -204,7 +183,7 @@ func prefixLists( ) []IPPrefixList { afString := "ip" if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 { - afString = "ip6" + afString = "ipv6" } var result []IPPrefixList From def5364d24582d0c86ecfd9b149c2dc4fabc2b21 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 11:35:15 +0100 Subject: [PATCH 033/102] fixes --- pkg/nftables/nftables.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index d7f12a5..9d12e5d 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -20,9 +20,9 @@ import ( ) const ( - serviceName = "nftables.service" - serviceUnitPath = "/etc/systemd/system/" + serviceName - + // nftables system service name + serviceName = "nftables.service" + // nftables rules file nftrulesPath = "/etc/nftables/rules" // Set up additional conntrack zone for DNS traffic. @@ -31,9 +31,7 @@ const ( // Isolating traffic to special zone solves the problem. // Zone number(3) was obtained by experiments. dnsProxyZone = "3" - dnsPort = "domain" - systemctlBin = "/bin/systemctl" // ForwardPolicyDrop drops packets which try to go through the forwarding chain ForwardPolicyDrop = ForwardPolicy("drop") @@ -385,13 +383,13 @@ func getAddressFamily(p string) (string, error) { } // Validate validates network interfaces configuration. -func (v NftablesValidator) Validate() error { - v.log.Info("running 'nft --check --file' to validate changes.", "file", v.path) +func Validate(cfg *Config) error { + cfg.Log.Info("running 'nft --check --file' to validate changes.", "file", nftrulesPath) - cmd := exec.Command("nft", "--check", "--file", v.path) + cmd := exec.Command("nft", "--check", "--file", nftrulesPath) out, err := cmd.CombinedOutput() if err != nil { - v.log.Error("nft validation failed", "output", string(out), "error", err) + cfg.Log.Error("nft validation failed", "output", string(out), "error", err) return err } return nil From 9e4608cd9c0383023e1943626016705fc36d08f4 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 11:36:00 +0100 Subject: [PATCH 034/102] Pusn network. --- pkg/network/network.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/network/network.go b/pkg/network/network.go index 77529fc..708a59a 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -126,6 +126,20 @@ func (n *Network) PrivatePrimaryNetwork() (*apiv2.MachineNetwork, error) { return nil, fmt.Errorf("no private primary network present in network allocation") } +func (n *Network) PrivateSecondarySharedNetworks() (nws []*apiv2.MachineNetwork) { + for _, nw := range n.allocation.Networks { + if nw.Project == nil { + continue + } + + if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED && *nw.Project != n.allocation.Project { + nws = append(nws, nw) + } + } + + return +} + func (n *Network) PrivatePrimaryIPs() ([]string, error) { if n.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { for _, nw := range n.allocation.Networks { @@ -236,6 +250,18 @@ func (n *Network) GetNetworks(networkType apiv2.NetworkType) []*apiv2.MachineNet return networks } +func (n *Network) GetExternalNetworkVrfNames() (vrfNames []string) { + for _, nw := range n.allocation.Networks { + if nw.NetworkType != apiv2.NetworkType_NETWORK_TYPE_EXTERNAL { + continue + } + + vrfNames = append(vrfNames, fmt.Sprintf("vrf%d", nw.Vrf)) + } + + return +} + func (n *Network) GetDefaultRouteNetwork() (*apiv2.MachineNetwork, error) { for _, nw := range n.allocation.Networks { if nw.NetworkType == apiv2.NetworkType_NETWORK_TYPE_EXTERNAL { From 7f3dfe7cfc3aee1f8d4810caa31aa7e364819e53 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 12:37:37 +0100 Subject: [PATCH 035/102] Install systemd services --- pkg/network/network.go | 30 ++++++++++ pkg/services/install.go | 130 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 150 insertions(+), 10 deletions(-) diff --git a/pkg/network/network.go b/pkg/network/network.go index 708a59a..6642a0d 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -62,6 +62,13 @@ func (n *Network) HasVpn() bool { return false } +func (n *Network) Vpn() *apiv2.MachineVPN { + if n.allocation.Vpn != nil && n.allocation.Vpn.AuthKey != "" { + return n.allocation.Vpn + } + return nil +} + func (n *Network) AllocationNetworks() []*apiv2.MachineNetwork { return n.allocation.Networks } @@ -70,6 +77,13 @@ func (n *Network) FirewallRules() *apiv2.FirewallRules { return n.allocation.FirewallRules } +func (n *Network) NTPServers() (ntpServers []string) { + for _, ntpserver := range n.allocation.NtpServer { + ntpServers = append(ntpServers, ntpserver.Address) + } + return +} + func (n *Network) LoopbackCIDRs() (cidrs []string, err error) { var ips []string @@ -273,6 +287,22 @@ func (n *Network) GetDefaultRouteNetwork() (*apiv2.MachineNetwork, error) { return nil, fmt.Errorf("no network which provides a default route found") } +func (n *Network) GetDefaultRouteNetworkVrfName() (string, error) { + nw, err := n.GetDefaultRouteNetwork() + if err != nil { + return "", err + } + return fmt.Sprintf("vrf%d", nw.Vrf), nil +} + +func (n *Network) GetTenantNetworkVrfName() (string, error) { + nw, err := n.PrivatePrimaryNetwork() + if err != nil { + return "", err + } + return fmt.Sprintf("vrf%d", nw.Vrf), nil +} + func ContainsDefaultRoute(prefixes []string) bool { for _, prefix := range prefixes { if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { diff --git a/pkg/services/install.go b/pkg/services/install.go index a55a575..986c62c 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -1,14 +1,124 @@ package services -func WriteSystemdServices() error { - return nil -} +import ( + "context" + "errors" + "log/slog" + + "github.com/metal-stack/os-installer/pkg/network" + "github.com/metal-stack/os-installer/pkg/services/chrony" + "github.com/metal-stack/os-installer/pkg/services/droptailer" + firewallcontroller "github.com/metal-stack/os-installer/pkg/services/firewall-controller" + nftablesexporter "github.com/metal-stack/os-installer/pkg/services/nftables-exporter" + nodeexporter "github.com/metal-stack/os-installer/pkg/services/node-exporter" + "github.com/metal-stack/os-installer/pkg/services/suricata" + "github.com/metal-stack/os-installer/pkg/services/tailscale" +) + +func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *network.Network, machineUUID string) error { + if network.IsMachine() { + return nil + } + + var ( + errs []error + defaultRouteVRF string + tenantVRF string + ) + + defaultRouteVRF, err := network.GetDefaultRouteNetworkVrfName() + if err != nil { + errs = append(errs, err) + } + tenantVRF, err = network.GetTenantNetworkVrfName() + if err != nil { + errs = append(errs, err) + } + + // Droptailer + if _, err = droptailer.WriteSystemdUnit(ctx, &droptailer.Config{ + Log: log, + Reload: false, + }, &droptailer.TemplateData{ + Comment: "created from os-installer", + TenantVrf: tenantVRF, + }); err != nil { + errs = append(errs, err) + } -func firewallServices() {} + // Chrony + if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ + Log: log, + Reload: false, + Enable: true, + ChronyConfigPath: "", + }, &chrony.TemplateData{ + NTPServers: network.NTPServers(), + }, defaultRouteVRF); err != nil { + errs = append(errs, err) + } -// suricata -// tailscale(d) -// node-exporter -// chrony -// droptailer -// nftables-exporter + // firewall-controller + if _, err = firewallcontroller.WriteSystemdUnit(ctx, &firewallcontroller.Config{ + Log: log, + Reload: false, + }, &firewallcontroller.TemplateData{ + Comment: "created from os-installer", + DefaultRouteVrf: defaultRouteVRF, + }); err != nil { + errs = append(errs, err) + } + + // nftables-exporter + if _, err := nftablesexporter.WriteSystemdUnit(ctx, &nftablesexporter.Config{ + Log: log, + Reload: false, + }, &nftablesexporter.TemplateData{ + Comment: "created from os-installer", + }); err != nil { + errs = append(errs, err) + } + + // node-exporter + if _, err := nodeexporter.WriteSystemdUnit(ctx, &nodeexporter.Config{ + Log: log, + Reload: false, + }, &nodeexporter.TemplateData{ + Comment: "created from os-installer", + }); err != nil { + errs = append(errs, err) + } + + // suricata + if _, err := suricata.WriteSystemdUnit(ctx, &suricata.Config{ + Log: log, + Reload: false, + }, &suricata.TemplateData{ + Interface: "TODO", + DefaultRouteVrf: defaultRouteVRF, + }); err != nil { + errs = append(errs, err) + } + + // tailscale + if network.HasVpn() { + vpn := network.Vpn() + if _, err := tailscale.WriteSystemdUnit(ctx, &tailscale.Config{ + Log: log, + Reload: false, + }, &tailscale.TemplateData{ + Comment: "created from os-installer", + DefaultRouteVrf: defaultRouteVRF, + MachineID: machineUUID, + AuthKey: vpn.AuthKey, + Address: vpn.ControlPlaneAddress, + }); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} From 264694f17e44d955e3f9f0cd7b31cac8e516f2c5 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 13:27:25 +0100 Subject: [PATCH 036/102] all frr tests pass --- pkg/frr/frr.go | 16 ++-- pkg/frr/frr.machine.tpl | 3 +- pkg/frr/frr_test.go | 194 +++++++++++++++++++++++----------------- 3 files changed, 120 insertions(+), 93 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 0c47988..ec33add 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -7,6 +7,7 @@ import ( "net/netip" "os/exec" + "github.com/Masterminds/semver/v3" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/os-installer/pkg/network" systemd_renderer "github.com/metal-stack/os-installer/pkg/systemd-service-renderer" @@ -53,6 +54,8 @@ type ( Network *network.Network + FRRVersion *semver.Version + fs afero.Fs } @@ -220,13 +223,12 @@ func assembleVRFs(cfg *Config) ([]VRF, error) { frr *FRR ) - // FIXME do we need to support older frr versions <9 - // if frrVersion != nil { - // frr = &FRR{ - // Major: frrVersion.Major(), - // Minor: frrVersion.Minor(), - // } - // } + if cfg.FRRVersion != nil { + frr = &FRR{ + Major: cfg.FRRVersion.Major(), + Minor: cfg.FRRVersion.Minor(), + } + } for _, n := range cfg.Network.AllocationNetworks() { switch n.NetworkType { diff --git a/pkg/frr/frr.machine.tpl b/pkg/frr/frr.machine.tpl index f8f1d61..263dc38 100644 --- a/pkg/frr/frr.machine.tpl +++ b/pkg/frr/frr.machine.tpl @@ -1,7 +1,6 @@ -{{- /*gotype: github.com/metal-stack/os-installer/pkg/network.FirewallFRRData*/ -}} {{- $ASN := .ASN -}} {{- $RouterId := .RouterID -}} -{{ .Comment }} +# {{ .Comment }} frr version {{ .FRRVersion }} frr defaults datacenter hostname {{ .Hostname }} diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go index 68fa681..26d48f1 100644 --- a/pkg/frr/frr_test.go +++ b/pkg/frr/frr_test.go @@ -6,6 +6,7 @@ import ( "path" "testing" + "github.com/Masterminds/semver/v3" "github.com/google/go-cmp/cmp" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/os-installer/pkg/network" @@ -135,44 +136,50 @@ var ( Networks: []*apiv2.MachineNetwork{ { Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, Prefixes: []string{"10.0.16.0/22"}, Ips: []string{"10.0.16.2"}, Vrf: 3981, + Asn: 4200003073, }, { Network: "partition-storage", NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, + Asn: 4200003073, + // FIXME clarify if this is required // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, DestinationPrefixes: []string{"0.0.0.0/0"}, Vrf: 104009, + Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "underlay", NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, }, { - Network: "mpls", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Prefixes: []string{"100.127.129.0/22"}, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, - }, - { - Network: "internet-v6", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, }, } @@ -183,44 +190,50 @@ var ( Networks: []*apiv2.MachineNetwork{ { Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, Prefixes: []string{"10.0.16.0/22"}, Ips: []string{"10.0.16.2"}, Vrf: 3981, + Asn: 4200003073, }, { Network: "partition-storage", NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, + Asn: 4200003073, + // FIXME clarify if this is required // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, DestinationPrefixes: []string{"0.0.0.0/0"}, Vrf: 104009, + Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "underlay", NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, }, { - Network: "mpls", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Prefixes: []string{"100.127.129.0/22"}, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, - }, - { - Network: "internet-v6", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, }, } @@ -243,19 +256,18 @@ var ( Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, DestinationPrefixes: []string{"0.0.0.0/0"}, Vrf: 104009, + Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "underlay", NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, Ips: []string{"10.1.0.1"}, - }, - { - Network: "internet-v6", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, + Prefixes: []string{"10.0.12.0/22"}, }, }, } @@ -266,76 +278,86 @@ var ( Networks: []*apiv2.MachineNetwork{ { Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, Prefixes: []string{"2002::/64"}, Ips: []string{"2002::1"}, Vrf: 3981, + Asn: 4200003073, }, { Network: "partition-storage", NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, + Asn: 4200003073, // FIXME clarify if this is required // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Prefixes: []string{"2a02:c00:20::/45"}, Ips: []string{"2a02:c00:20::1"}, + Prefixes: []string{"2a02:c00:20::/45"}, DestinationPrefixes: []string{"::/0"}, Vrf: 104009, + Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "underlay", NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, }, { - Network: "mpls", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Prefixes: []string{"100.127.129.0/22"}, - Ips: []string{"100.127.129.1"}, - Vrf: 104010, - NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, - }, - { - Network: "internet-v6", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, - Ips: []string{"2001::4"}, + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, }, } machineAllocation = &apiv2.MachineAllocation{ - Hostname: "firewall", - AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Hostname: "machine", + Project: "project-a", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, Networks: []*apiv2.MachineNetwork{ { Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, Prefixes: []string{"10.0.16.0/22"}, - Ips: []string{"10.0.16.2"}, + Ips: []string{"10.0.17.2"}, Vrf: 3981, - }, - { - Network: "partition-storage", - NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, - Prefixes: []string{"10.0.18.0/22"}, - Ips: []string{"10.0.18.2"}, - Vrf: 3982, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + Asn: 4200003073, }, { Network: "internet", NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, DestinationPrefixes: []string{"0.0.0.0/0"}, Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, }, @@ -346,6 +368,7 @@ func TestRender(t *testing.T) { tests := []struct { name string allocation *apiv2.MachineAllocation + frrVersion *semver.Version wantFilePath string wantErr error }{ @@ -361,45 +384,48 @@ func TestRender(t *testing.T) { wantFilePath: "frr.conf.firewall_dualstack", wantErr: nil, }, - // { - // name: "render firewall frr-9", - // allocation: firewallFrr9Allocation, - // wantFilePath: "frr.conf.firewall_frr-9", - // wantErr: nil, - // }, - // { - // name: "render firewall frr-10", - // allocation: firewallFrr10Allocation, - // wantFilePath: "frr.conf.firewall_frr-10", - // wantErr: nil, - // }, - // { - // name: "render firewall shared", - // allocation: firewallSharedAllocation, - // wantFilePath: "frr.conf.firewall_shared", - // wantErr: nil, - // }, - // { - // name: "render firewall ipv6", - // allocation: firewallIPv6Allocation, - // wantFilePath: "frr.conf.firewall_ipv6", - // wantErr: nil, - // }, - // { - // name: "render machine", - // allocation: machineAllocation, - // wantFilePath: "frr.conf.machine", - // wantErr: nil, - // }, + { + name: "render firewall frr-9", + allocation: firewallFrr9Allocation, + frrVersion: semver.MustParse("9.0.1"), + wantFilePath: "frr.conf.firewall_frr-9", + wantErr: nil, + }, + { + name: "render firewall frr-10", + allocation: firewallFrr10Allocation, + frrVersion: semver.MustParse("10.4.1"), + wantFilePath: "frr.conf.firewall_frr-10", + wantErr: nil, + }, + { + name: "render firewall shared", + allocation: firewallSharedAllocation, + wantFilePath: "frr.conf.firewall_shared", + wantErr: nil, + }, + { + name: "render firewall ipv6", + allocation: firewallIPv6Allocation, + wantFilePath: "frr.conf.firewall_ipv6", + wantErr: nil, + }, + { + name: "render machine", + allocation: machineAllocation, + wantFilePath: "frr.conf.machine", + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fs := afero.Afero{Fs: afero.NewMemMapFs()} _, gotErr := Render(t.Context(), &Config{ - Log: slog.Default(), - fs: fs, - Network: network.New(tt.allocation), + Log: slog.Default(), + fs: fs, + Network: network.New(tt.allocation), + FRRVersion: tt.frrVersion, }) if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { From e9a1fdb3e6c372bdaabb409d85da3683756b0e07 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 13:43:21 +0100 Subject: [PATCH 037/102] Remove old, reactivate validation --- ...Dockerfile.validate => Dockerfile.validate | 0 Makefile | 2 - old/network/configurator.go | 298 -------------- old/network/configurator_test.go | 30 -- old/network/frr.go | 164 -------- old/network/frr_test.go | 131 ------- old/network/knowledgebase.go | 258 ------------- old/network/knowledgebase_test.go | 299 --------------- old/network/netobjects.go | 100 ----- old/network/nftables.go | 309 --------------- old/network/routemap.go | 362 ------------------ old/network/routemap_test.go | 320 ---------------- old/network/service_test.go | 73 ---- old/network/validate.sh => validate.sh | 2 +- old/network/validate_os.sh => validate_os.sh | 4 +- 15 files changed, 3 insertions(+), 2349 deletions(-) rename old/network/Dockerfile.validate => Dockerfile.validate (100%) delete mode 100644 old/network/configurator.go delete mode 100644 old/network/configurator_test.go delete mode 100644 old/network/frr.go delete mode 100644 old/network/frr_test.go delete mode 100644 old/network/knowledgebase.go delete mode 100644 old/network/knowledgebase_test.go delete mode 100644 old/network/netobjects.go delete mode 100644 old/network/nftables.go delete mode 100644 old/network/routemap.go delete mode 100644 old/network/routemap_test.go delete mode 100644 old/network/service_test.go rename old/network/validate.sh => validate.sh (95%) rename old/network/validate_os.sh => validate_os.sh (89%) diff --git a/old/network/Dockerfile.validate b/Dockerfile.validate similarity index 100% rename from old/network/Dockerfile.validate rename to Dockerfile.validate diff --git a/Makefile b/Makefile index c678aa4..36541b9 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,4 @@ test: .PHONY: validate validate: - cd pkg/network ./validate.sh - cd - diff --git a/old/network/configurator.go b/old/network/configurator.go deleted file mode 100644 index f7fc46b..0000000 --- a/old/network/configurator.go +++ /dev/null @@ -1,298 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - "os" - "path" - "text/template" - - "github.com/metal-stack/os-installer/old/exec" - "github.com/metal-stack/os-installer/old/net" -) - -// BareMetalType defines the type of configuration to apply. -type BareMetalType int - -const ( - // Firewall defines the bare metal server to function as firewall. - Firewall BareMetalType = iota - // Machine defines the bare metal server to function as machine. - Machine -) -const ( - // fileModeSystemd represents a file mode that allows systemd to read e.g. /etc/systemd/network files. - fileModeSystemd = 0644 - // fileModeSixFourFour represents file mode 0644 - fileModeSixFourFour = 0644 - // fileModeDefault represents the default file mode sufficient e.g. to /etc/network/interfaces or /etc/frr.conf. - fileModeDefault = 0600 - // systemdUnitPath is the path where systemd units will be generated. - systemdUnitPath = "/etc/systemd/system/" -) - -var ( - // systemdNetworkPath is the path where systemd-networkd expects its configuration files. - systemdNetworkPath = "/etc/systemd/network" - // tmpPath is the path where temporary files are stored for validation before they are moved to their intended place. - tmpPath = "/etc/metal/networker/" -) - -// ForwardPolicy defines how packets in the forwarding chain are handled, can be either drop or accept. -// drop will be the standard for firewalls which are not managed by kubernetes resources (CWNPs) -type ForwardPolicy string - -const ( - // ForwardPolicyDrop drops packets which try to go through the forwarding chain - ForwardPolicyDrop = ForwardPolicy("drop") - // ForwardPolicyAccept accepts packets which try to go through the forwarding chain - ForwardPolicyAccept = ForwardPolicy("accept") -) - -type ( - // Configurator is an interface to configure bare metal servers. - Configurator interface { - Configure(forwardPolicy ForwardPolicy) - ConfigureNftables(forwardPolicy ForwardPolicy) - } - - // machineConfigurator is a configurator that configures a bare metal server as 'machine'. - machineConfigurator struct { - c config - } - - // firewallConfigurator is a configurator that configures a bare metal server as 'firewall'. - firewallConfigurator struct { - c config - enableDNSProxy bool - } -) - -type unitConfiguration struct { - unit string - templateFile string - constructApplier func(kb config, v serviceValidator) (net.Applier, error) - enabled bool -} - -// NewConfigurator creates a new configurator. -func NewConfigurator(kind BareMetalType, c config, enableDNS bool) (Configurator, error) { - switch kind { - case Firewall: - return firewallConfigurator{ - c: c, - enableDNSProxy: enableDNS, - }, nil - case Machine: - return machineConfigurator{ - c: c, - }, nil - default: - return nil, fmt.Errorf("unknown type:%d", kind) - } -} - -// Configure applies configuration to a bare metal server to function as 'machine'. -func (mc machineConfigurator) Configure(forwardPolicy ForwardPolicy) { - applyCommonConfiguration(mc.c.log, Machine, mc.c) -} - -// ConfigureNftables is empty function that exists just to satisfy the Configurator interface -func (mc machineConfigurator) ConfigureNftables(forwardPolicy ForwardPolicy) {} - -// Configure applies configuration to a bare metal server to function as 'firewall'. -func (fc firewallConfigurator) Configure(forwardPolicy ForwardPolicy) { - kb := fc.c - applyCommonConfiguration(fc.c.log, Firewall, kb) - - fc.ConfigureNftables(forwardPolicy) - - chrony, err := newChronyServiceEnabler(fc.c) - if err != nil { - fc.c.log.Warn("failed to configure chrony", "error", err) - } else { - err := chrony.Enable() - if err != nil { - fc.c.log.Error("enabling chrony failed", "error", err) - } - } - - for _, u := range fc.getUnits() { - src := mustTmpFile(u.unit) - validatorService := serviceValidator{src} - nfe, err := u.constructApplier(fc.c, validatorService) - - if err != nil { - fc.c.log.Warn("failed to deploy", "unit", u.unit, "error", err) - } - - applyAndCleanUp(fc.c.log, nfe, u.templateFile, src, path.Join(systemdUnitPath, u.unit), fileModeSystemd, false) - - if u.enabled { - mustEnableUnit(fc.c.log, u.unit) - } - } - - src := mustTmpFile("suricata_") - applier, err := newSuricataDefaultsApplier(kb, src) - - if err != nil { - fc.c.log.Warn("failed to configure suricata defaults", "error", err) - } - - applyAndCleanUp(fc.c.log, applier, tplSuricataDefaults, src, "/etc/default/suricata", fileModeSixFourFour, false) - - src = mustTmpFile("suricata.yaml_") - applier, err = newSuricataConfigApplier(kb, src) - - if err != nil { - fc.c.log.Warn("failed to configure suricata", "error", err) - } - - applyAndCleanUp(fc.c.log, applier, tplSuricataConfig, src, "/etc/suricata/suricata.yaml", fileModeSixFourFour, false) -} - -func (fc firewallConfigurator) ConfigureNftables(forwardPolicy ForwardPolicy) { - src := mustTmpFile("nftrules_") - validator := NftablesValidator{ - path: src, - log: fc.c.log, - } - applier := newNftablesConfigApplier(fc.c, validator, fc.enableDNSProxy, forwardPolicy) - applyAndCleanUp(fc.c.log, applier, TplNftables, src, "/etc/nftables/rules", fileModeDefault, true) -} - -func (fc firewallConfigurator) getUnits() (units []unitConfiguration) { - units = []unitConfiguration{ - { - unit: systemdUnitDroptailer, - templateFile: tplDroptailer, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return newDroptailerServiceApplier(kb, v) - }, - enabled: false, // will be enabled in the case of k8s deployments with ignition on first boot - }, - { - unit: systemdUnitFirewallController, - templateFile: tplFirewallController, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return newFirewallControllerServiceApplier(kb, v) - }, - enabled: false, // will be enabled in the case of k8s deployments with ignition on first boot - }, - { - unit: systemdUnitNftablesExporter, - templateFile: tplNftablesExporter, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return NewNftablesExporterServiceApplier(kb, v) - }, - enabled: true, - }, - { - unit: systemdUnitNodeExporter, - templateFile: tplNodeExporter, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return newNodeExporterServiceApplier(kb, v) - }, - enabled: true, - }, - { - unit: systemdUnitSuricataUpdate, - templateFile: tplSuricataUpdate, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return newSuricataUpdateServiceApplier(kb, v) - }, - enabled: true, - }, - } - - if fc.c.VPN != nil { - units = append(units, unitConfiguration{ - unit: systemdUnitTailscaled, - templateFile: tplTailscaled, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return newTailscaledServiceApplier(kb, v) - }, - enabled: true, - }, unitConfiguration{ - unit: systemdUnitTailscale, - templateFile: tplTailscale, - constructApplier: func(kb config, v serviceValidator) (net.Applier, error) { - return newTailscaleServiceApplier(kb, v) - }, - enabled: true, - }) - } - - return units -} - -func applyCommonConfiguration(log *slog.Logger, kind BareMetalType, kb config) { - a := newIfacesApplier(kind, kb) - a.Apply() - - src := mustTmpFile("hosts_") - applier := newHostsApplier(kb, src) - applyAndCleanUp(log, applier, tplHosts, src, "/etc/hosts", fileModeDefault, false) - - src = mustTmpFile("hostname_") - applier = newHostnameApplier(kb, src) - applyAndCleanUp(log, applier, tplHostname, src, "/etc/hostname", fileModeSixFourFour, false) - - src = mustTmpFile("frr_") - applier = NewFrrConfigApplier(kind, kb, src, nil) - tpl := TplFirewallFRR - - if kind == Machine { - tpl = TplMachineFRR - } - - applyAndCleanUp(log, applier, tpl, src, "/etc/frr/frr.conf", fileModeDefault, false) -} - -func applyAndCleanUp(log *slog.Logger, applier net.Applier, tpl, src, dest string, mode os.FileMode, reload bool) { - log.Info("rendering", "template", tpl, "destination", dest, "mode", mode) - file := mustReadTpl(tpl) - mustApply(applier, file, src, dest, reload) - - err := os.Chmod(dest, mode) - if err != nil { - log.Error("unable change mode", "file", dest, "mode", mode, "error", err) - } - - _ = os.Remove(src) -} - -func mustEnableUnit(log *slog.Logger, unit string) { - cmd := fmt.Sprintf("systemctl enable %s", unit) - log.Info("enable unit", "command", cmd) - - err := exec.NewVerboseCmd("bash", "-c", cmd).Run() - - if err != nil { - panic(err) - } -} - -func mustApply(applier net.Applier, tpl, src, dest string, reload bool) { - t := template.Must(template.New(src).Parse(tpl)) - _, err := applier.Apply(*t, src, dest, reload) - - if err != nil { - panic(err) - } -} - -func mustTmpFile(prefix string) string { - f, err := os.CreateTemp(tmpPath, prefix) - if err != nil { - panic(err) - } - - err = f.Close() - if err != nil { - panic(err) - } - - return f.Name() -} diff --git a/old/network/configurator_test.go b/old/network/configurator_test.go deleted file mode 100644 index 764afb2..0000000 --- a/old/network/configurator_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewConfigurator(t *testing.T) { - tests := []struct { - kind BareMetalType - expected any - }{ - { - kind: Firewall, - expected: firewallConfigurator{}, - }, - { - kind: Machine, - expected: machineConfigurator{}, - }, - } - - for _, tt := range tests { - actual, err := NewConfigurator(tt.kind, config{}, false) - require.NoError(t, err) - assert.IsType(t, tt.expected, actual) - } -} diff --git a/old/network/frr.go b/old/network/frr.go deleted file mode 100644 index 43481e0..0000000 --- a/old/network/frr.go +++ /dev/null @@ -1,164 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - "net/netip" - - "github.com/Masterminds/semver/v3" - "github.com/metal-stack/metal-go/api/models" - mn "github.com/metal-stack/metal-lib/pkg/net" - "github.com/metal-stack/os-installer/old/exec" - "github.com/metal-stack/os-installer/old/net" -) - -const ( - // FRRVersion holds a string that is used in the frr.conf to define the FRR version. - FRRVersion = "8.5" - // TplFirewallFRR defines the name of the template to render FRR configuration to a 'firewall'. - TplFirewallFRR = "frr.firewall.tpl" - // TplMachineFRR defines the name of the template to render FRR configuration to a 'machine'. - TplMachineFRR = "frr.machine.tpl" - // IPPrefixListSeqSeed specifies the initial value for prefix lists sequence number. - IPPrefixListSeqSeed = 100 - // IPPrefixListNoExportSuffix defines the suffix to use for private IP ranges that must not be exported. - IPPrefixListNoExportSuffix = "-no-export" - // RouteMapOrderSeed defines the initial value for route-map order. - RouteMapOrderSeed = 10 - // AddressFamilyIPv4 is the name for this address family for the routing daemon. - AddressFamilyIPv4 = "ip" - // AddressFamilyIPv6 is the name for this address family for the routing daemon. - AddressFamilyIPv6 = "ipv6" -) - -type ( - // CommonFRRData contains attributes that are common to FRR configuration of all kind of bare metal servers. - CommonFRRData struct { - ASN int64 - Comment string - FRRVersion string - Hostname string - RouterID string - } - - // MachineFRRData contains attributes required to render frr.conf of bare metal servers that function as 'machine'. - MachineFRRData struct { - CommonFRRData - } - - // FirewallFRRData contains attributes required to render frr.conf of bare metal servers that function as 'firewall'. - FirewallFRRData struct { - CommonFRRData - VRFs []VRF - } - - // frrValidator validates the frr.conf to apply. - frrValidator struct { - path string - log *slog.Logger - } - - // AddressFamily is the address family for the routing daemon. - AddressFamily string -) - -// NewFrrConfigApplier constructs a new Applier of the given type of Bare Metal. -func NewFrrConfigApplier(kind BareMetalType, c config, tmpFile string, frrVersion *semver.Version) net.Applier { - var data any - - switch kind { - case Firewall: - net := c.getUnderlayNetwork() - data = FirewallFRRData{ - CommonFRRData: CommonFRRData{ - FRRVersion: FRRVersion, - Hostname: c.Hostname, - Comment: versionHeader(c.MachineUUID), - ASN: *net.Asn, - RouterID: routerID(net), - }, - VRFs: assembleVRFs(c, frrVersion), - } - case Machine: - net := c.getPrivatePrimaryNetwork() - data = MachineFRRData{ - CommonFRRData: CommonFRRData{ - FRRVersion: FRRVersion, - Hostname: c.Hostname, - Comment: versionHeader(c.MachineUUID), - ASN: *net.Asn, - RouterID: routerID(net), - }, - } - default: - c.log.Error("unknown kind of bare metal", "kind", kind) - panic(fmt.Errorf("unknown kind %v", kind)) - } - - validator := frrValidator{ - path: tmpFile, - log: c.log, - } - - return net.NewNetworkApplier(data, validator, net.NewDBusReloader("frr.service")) -} - -// routerID will calculate the bgp router-id which must only be specified in the ipv6 range. -// returns 0.0.0.0 for erroneous ip addresses and 169.254.255.255 for ipv6 -// TODO prepare machine allocations with ipv6 primary address and tests -func routerID(net *models.V1MachineNetwork) string { - if len(net.Ips) < 1 { - return "0.0.0.0" - } - ip, err := netip.ParseAddr(net.Ips[0]) - if err != nil { - return "0.0.0.0" - } - if ip.Is4() { - return ip.String() - } - return "169.254.255.255" -} - -// Validate can be used to run validation on FRR configuration using vtysh. -func (v frrValidator) Validate() error { - vtysh := fmt.Sprintf("vtysh --dryrun --inputfile %s", v.path) - v.log.Info("validate changes", "command", vtysh) - - return exec.NewVerboseCmd("bash", "-c", vtysh, v.path).Run() -} - -func assembleVRFs(kb config, frrVersion *semver.Version) []VRF { - var ( - result []VRF - frr *FRR - ) - if frrVersion != nil { - frr = &FRR{ - Major: frrVersion.Major(), - Minor: frrVersion.Minor(), - } - } - - networks := kb.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared, mn.External) - for _, network := range networks { - if network.Networktype == nil { - continue - } - - i := importRulesForNetwork(kb, network) - vrf := VRF{ - Identity: Identity{ - ID: int(*network.Vrf), - }, - VNI: int(*network.Vrf), - ImportVRFNames: i.ImportVRFs, - IPPrefixLists: i.prefixLists(), - RouteMaps: i.routeMaps(), - FRRVersion: frr, - } - result = append(result, vrf) - } - - return result -} diff --git a/old/network/frr_test.go b/old/network/frr_test.go deleted file mode 100644 index 999e128..0000000 --- a/old/network/frr_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package network - -import ( - "bytes" - "log/slog" - "os" - "testing" - - "github.com/Masterminds/semver/v3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestFrrConfigApplier(t *testing.T) { - tests := []struct { - name string - input string - frrVersion *semver.Version - expectedOutput string - configuratorType BareMetalType - tpl string - }{ - { - name: "firewall of a shared private network", - input: "testdata/firewall_shared.yaml", - expectedOutput: "testdata/frr.conf.firewall_shared", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", - input: "testdata/firewall.yaml", - expectedOutput: "testdata/frr.conf.firewall", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "dmz firewall with private primary unshared network, private secondary shared dmz network, internet and mpls", - input: "testdata/firewall_dmz.yaml", - expectedOutput: "testdata/frr.conf.firewall_dmz", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "dmz firewall with private primary unshared network, private secondary shared dmz network", - input: "testdata/firewall_dmz_app.yaml", - expectedOutput: "testdata/frr.conf.firewall_dmz_app", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "firewall with private primary unshared network, private secondary shared dmz network and private secondary shared storage network", - input: "testdata/firewall_dmz_app_storage.yaml", - expectedOutput: "testdata/frr.conf.firewall_dmz_app_storage", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "firewall with private primary unshared ipv6 network, private secondary shared ipv4 network, ipv6 internet and ipv4 mpls", - input: "testdata/firewall_ipv6.yaml", - expectedOutput: "testdata/frr.conf.firewall_ipv6", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "firewall with private primary unshared ipv6 network, private secondary shared ipv4 network, dualstack internet and ipv4 mpls", - input: "testdata/firewall_dualstack.yaml", - expectedOutput: "testdata/frr.conf.firewall_dualstack", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "standard machine", - input: "testdata/machine.yaml", - expectedOutput: "testdata/frr.conf.machine", - configuratorType: Machine, - tpl: TplMachineFRR, - }, - { - name: "standard firewall with lower frr version", - input: "testdata/firewall.yaml", - frrVersion: semver.MustParse("9.0.5-0"), - expectedOutput: "testdata/frr.conf.firewall_frr-9", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - { - name: "standard firewall with higher frr version", - input: "testdata/firewall.yaml", - frrVersion: semver.MustParse("10.1.5"), - expectedOutput: "testdata/frr.conf.firewall_frr-10", - configuratorType: Firewall, - tpl: TplFirewallFRR, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - log := slog.Default() - kb, err := New(log, test.input) - require.NoError(t, err) - a := NewFrrConfigApplier(test.configuratorType, *kb, "", test.frrVersion) - b := bytes.Buffer{} - - tpl := MustParseTpl(test.tpl) - err = a.Render(&b, *tpl) - require.NoError(t, err) - - // eases adjustment of test fixtures - // just remove old test fixture after a code change - // let the new fixtures get generated - // check them manually before commit - if _, err := os.Stat(test.expectedOutput); os.IsNotExist(err) { - err = os.WriteFile(test.expectedOutput, b.Bytes(), fileModeDefault) - require.NoError(t, err) - return - } - - expected, err := os.ReadFile(test.expectedOutput) - require.NoError(t, err) - assert.Equal(t, string(expected), b.String()) - }) - } -} - -func TestFRRValidator_Validate(t *testing.T) { - validator := frrValidator{ - log: slog.Default(), - } - actual := validator.Validate() - require.Error(t, actual) -} diff --git a/old/network/knowledgebase.go b/old/network/knowledgebase.go deleted file mode 100644 index 3ee54f4..0000000 --- a/old/network/knowledgebase.go +++ /dev/null @@ -1,258 +0,0 @@ -package network - -import ( - "errors" - "fmt" - "log/slog" - "net" - "os" - "slices" - - apiv1 "github.com/metal-stack/os-installer/api/v1" - - "github.com/metal-stack/metal-go/api/models" - mn "github.com/metal-stack/metal-lib/pkg/net" - "github.com/metal-stack/v" - - "gopkg.in/yaml.v3" -) - -const ( - // VLANOffset defines a number to start with when creating new VLAN IDs. - VLANOffset = 1000 -) - -type ( - // config was generated with: https://mengzhuo.github.io/yaml-to-go/. - // It represents the input yaml that is needed to render network configuration files. - config struct { - apiv1.InstallerConfig - log *slog.Logger - } -) - -// New creates a new instance of this type. -func New(log *slog.Logger, path string) (*config, error) { - log.Info("loading", "path", path) - - f, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - installer := &apiv1.InstallerConfig{} - err = yaml.Unmarshal(f, &installer) - - if err != nil { - return nil, err - } - - return &config{ - InstallerConfig: *installer, - log: log, - }, nil -} - -// Validate validates the containing information depending on the demands of the bare metal type. -func (c config) Validate(kind BareMetalType) error { - if len(c.Networks) == 0 { - return errors.New("expectation at least one network is present failed") - } - - if !c.containsSinglePrivatePrimary() { - return errors.New("expectation exactly one 'private: true' network is present failed") - } - - if kind == Firewall { - if !c.allNonUnderlayNetworksHaveNonZeroVRF() { - return errors.New("networks with 'underlay: false' must contain a value of 'vrf' as it is used for BGP") - } - - if !c.containsSingleUnderlay() { - return errors.New("expectation exactly one underlay network is present failed") - } - - if !c.containsAnyPublicNetwork() { - return errors.New("expectation at least one public network (private: false, " + - "underlay: false) is present failed") - } - - for _, net := range c.GetNetworks(mn.External) { - if len(net.Destinationprefixes) == 0 { - return errors.New("non-private, non-underlay networks must contain destination prefix(es) to make " + - "any sense of it") - } - } - - if c.isAnyNAT() && len(c.getPrivatePrimaryNetwork().Prefixes) == 0 { - return errors.New("private network must not lack prefixes since nat is required") - } - } - - net := c.getPrivatePrimaryNetwork() - - if kind == Firewall { - net = c.getUnderlayNetwork() - } - - if len(net.Ips) == 0 { - return errors.New("at least one IP must be present to be considered as LOOPBACK IP (" + - "'private: true' network IP for machine, 'underlay: true' network IP for firewall") - } - - if net.Asn != nil && *net.Asn <= 0 { - return errors.New("'asn' of private (machine) resp. underlay (firewall) network must not be missing") - } - - if len(c.Nics) == 0 { - return errors.New("at least one 'nics/nic' definition must be present") - } - - if !c.nicsContainValidMACs() { - return errors.New("each 'nic' definition must contain a valid 'mac'") - } - - return nil -} - -func (c config) containsAnyPublicNetwork() bool { - if len(c.GetNetworks(mn.External)) > 0 { - return true - } - return slices.ContainsFunc(c.Networks, isDMZNetwork) -} - -func (c config) containsSinglePrivatePrimary() bool { - return c.containsSingleNetworkOf(mn.PrivatePrimaryUnshared) != c.containsSingleNetworkOf(mn.PrivatePrimaryShared) -} - -func (c config) containsSingleUnderlay() bool { - return c.containsSingleNetworkOf(mn.Underlay) -} - -func (c config) containsSingleNetworkOf(t string) bool { - possibleNetworks := c.GetNetworks(t) - return len(possibleNetworks) == 1 -} - -// CollectIPs collects IPs of the given networks. -func (c config) CollectIPs(types ...string) []string { - var result []string - - networks := c.GetNetworks(types...) - for _, network := range networks { - result = append(result, network.Ips...) - } - - return result -} - -// GetNetworks returns all networks present. -func (c config) GetNetworks(types ...string) []*models.V1MachineNetwork { - var result []*models.V1MachineNetwork - - for _, t := range types { - for _, n := range c.Networks { - if n.Networktype == nil { - continue - } - if *n.Networktype == t { - result = append(result, n) - } - } - } - - return result -} - -func (c config) isAnyNAT() bool { - for _, net := range c.Networks { - if net.Nat != nil && *net.Nat { - return true - } - } - - return false -} - -func (c config) getPrivatePrimaryNetwork() *models.V1MachineNetwork { - return c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared)[0] -} - -func (c config) getUnderlayNetwork() *models.V1MachineNetwork { - // Safe access since validation ensures there is exactly one. - return c.GetNetworks(mn.Underlay)[0] -} - -func (c config) GetDefaultRouteNetwork() *models.V1MachineNetwork { - externalNets := c.GetNetworks(mn.External) - for _, network := range externalNets { - if containsDefaultRoute(network.Destinationprefixes) { - return network - } - } - - privateSecondarySharedNets := c.GetNetworks(mn.PrivateSecondaryShared) - for _, network := range privateSecondarySharedNets { - if containsDefaultRoute(network.Destinationprefixes) { - return network - } - } - - return nil -} - -func (c config) getDefaultRouteVRFName() (string, error) { - if network := c.GetDefaultRouteNetwork(); network != nil { - return vrfNameOf(network), nil - } - - return "", fmt.Errorf("there is no network providing a default (0.0.0.0/0) route") -} - -func (c config) nicsContainValidMACs() bool { - for _, nic := range c.Nics { - if nic.Mac == nil || *nic.Mac == "" { - return false - } - - if _, err := net.ParseMAC(*nic.Mac); err != nil { - c.log.Error("invalid mac", "mac", *nic.Mac) - return false - } - } - - return true -} - -func (c config) allNonUnderlayNetworksHaveNonZeroVRF() bool { - for _, net := range c.Networks { - if net.Underlay != nil && *net.Underlay { - continue - } - - if net.Vrf != nil && *net.Vrf <= 0 { - return false - } - } - - return true -} - -func versionHeader(uuid string) string { - version := v.V.String() - if os.Getenv("GO_ENV") == "testing" { - version = "" - } - return fmt.Sprintf("# This file was auto generated for machine: '%s' by app version %s.\n# Do not edit.", - uuid, version) -} - -func containsDefaultRoute(prefixes []string) bool { - for _, prefix := range prefixes { - if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { - return true - } - } - return false -} diff --git a/old/network/knowledgebase_test.go b/old/network/knowledgebase_test.go deleted file mode 100644 index 90e707f..0000000 --- a/old/network/knowledgebase_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - "testing" - - "github.com/metal-stack/metal-go/api/models" - mn "github.com/metal-stack/metal-lib/pkg/net" - apiv1 "github.com/metal-stack/os-installer/api/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mustNewKnowledgeBase(t *testing.T) config { - log := slog.Default() - - d, err := New(log, "testdata/firewall.yaml") - require.NoError(t, err) - assert.NotNil(t, d) - - return *d -} - -func TestNewKnowledgeBase(t *testing.T) { - - d := mustNewKnowledgeBase(t) - - assert.Equal(t, "firewall", d.Hostname) - assert.NotEmpty(t, d.Networks) - assert.Len(t, d.Networks, 5) - - // private network - n := d.Networks[0] - assert.Len(t, n.Ips, 1) - assert.Equal(t, "10.0.16.2", n.Ips[0]) - assert.Len(t, n.Prefixes, 1) - assert.Equal(t, "10.0.16.0/22", n.Prefixes[0]) - assert.True(t, *n.Private) - assert.Equal(t, mn.PrivatePrimaryUnshared, *n.Networktype) - assert.Equal(t, int64(3981), *n.Vrf) - - // private shared network - n = d.Networks[1] - assert.Len(t, n.Ips, 1) - assert.Equal(t, "10.0.18.2", n.Ips[0]) - assert.Len(t, n.Prefixes, 1) - assert.Equal(t, "10.0.18.0/22", n.Prefixes[0]) - assert.True(t, *n.Private) - assert.Equal(t, mn.PrivateSecondaryShared, *n.Networktype) - assert.Equal(t, int64(3982), *n.Vrf) - - // public networks - n = d.Networks[2] - assert.Len(t, n.Destinationprefixes, 1) - assert.Equal(t, IPv4ZeroCIDR, n.Destinationprefixes[0]) - assert.Len(t, n.Ips, 1) - assert.Equal(t, "185.1.2.3", n.Ips[0]) - assert.Len(t, n.Prefixes, 2) - assert.Equal(t, "185.1.2.0/24", n.Prefixes[0]) - assert.Equal(t, "185.27.0.0/22", n.Prefixes[1]) - assert.False(t, *n.Underlay) - assert.False(t, *n.Private) - assert.True(t, *n.Nat) - assert.Equal(t, mn.External, *n.Networktype) - assert.Equal(t, int64(104009), *n.Vrf) - - // underlay network - n = d.Networks[3] - assert.Equal(t, int64(4200003073), *n.Asn) - assert.Len(t, n.Ips, 1) - assert.Equal(t, "10.1.0.1", n.Ips[0]) - assert.Len(t, n.Prefixes, 1) - assert.Equal(t, "10.0.12.0/22", n.Prefixes[0]) - assert.True(t, *n.Underlay) - assert.Equal(t, mn.Underlay, *n.Networktype) - - // public network mpls - n = d.Networks[4] - assert.Len(t, n.Destinationprefixes, 1) - assert.Equal(t, "100.127.1.0/24", n.Destinationprefixes[0]) - assert.Len(t, n.Ips, 1) - assert.Equal(t, "100.127.129.1", n.Ips[0]) - assert.Len(t, n.Prefixes, 1) - assert.Equal(t, "100.127.129.0/24", n.Prefixes[0]) - assert.False(t, *n.Underlay) - assert.False(t, *n.Private) - assert.True(t, *n.Nat) - assert.Equal(t, mn.External, *n.Networktype) - assert.Equal(t, int64(104010), *n.Vrf) -} - -var ( - boolTrue = true - boolFalse = false - asn0 = int64(0) - asn1 = int64(1011209) - vrf0 = int64(0) - vrf1 = int64(1011209) -) - -func stubKnowledgeBase() config { - privateNetID := "private" - underlayNetID := "underlay" - mac := "00:00:00:00:00:00" - privatePrimaryUnshared := mn.PrivatePrimaryUnshared - underlay := mn.Underlay - external := mn.External - log := slog.Default() - - return config{ - InstallerConfig: apiv1.InstallerConfig{ - Networks: []*models.V1MachineNetwork{ - {Private: &boolTrue, Networktype: &privatePrimaryUnshared, Ips: []string{"10.0.0.1"}, Asn: &asn1, Vrf: &vrf1, Networkid: &privateNetID}, - {Underlay: &boolTrue, Networktype: &underlay, Ips: []string{"10.0.0.1"}, Asn: &asn1, Vrf: &vrf0, Networkid: &underlayNetID}, - {Private: &boolFalse, Networktype: &external, Underlay: &boolFalse, Destinationprefixes: []string{"10.0.0.1/24"}, Asn: &asn1, Vrf: &vrf1, Networkid: &underlayNetID}, - }, - Nics: []*models.V1MachineNic{ - { - Mac: &mac}, - }, - }, - log: log, - } -} - -func TestKnowledgeBase_Validate(t *testing.T) { - tests := []struct { - expectedErrMsg string - kb config - kinds []BareMetalType - }{{ - expectedErrMsg: "", - kb: stubKnowledgeBase(), - kinds: []BareMetalType{Firewall, Machine}, - }, - { - expectedErrMsg: "expectation at least one network is present failed", - kb: stripNetworks(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall, Machine}, - }, - { - expectedErrMsg: "at least one IP must be present to be considered as LOOPBACK IP (" + - "'private: true' network IP for machine, 'underlay: true' network IP for firewall", - kb: stripIPs(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall, Machine}, - }, - {expectedErrMsg: "expectation exactly one underlay network is present failed", - kb: maskUnderlayNetworks(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall}}, - {expectedErrMsg: "expectation exactly one 'private: true' network is present failed", - kb: maskPrivatePrimaryNetworks(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall, Machine}}, - {expectedErrMsg: "'asn' of private (machine) resp. underlay (firewall) network must not be missing", - kb: stripPrivateNetworkASN(stubKnowledgeBase()), - kinds: []BareMetalType{Machine}}, - {expectedErrMsg: "'asn' of private (machine) resp. underlay (firewall) network must not be missing", - kb: stripUnderlayNetworkASN(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall}}, - {expectedErrMsg: "at least one 'nics/nic' definition must be present", - kb: stripNICs(stubKnowledgeBase()), - kinds: []BareMetalType{Machine}}, - {expectedErrMsg: "each 'nic' definition must contain a valid 'mac'", - kb: stripMACs(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall, Machine}}, - {expectedErrMsg: "private network must not lack prefixes since nat is required", - kb: setupIllegalNat(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall}}, - {expectedErrMsg: "non-private, non-underlay networks must contain destination prefix(es) to make any sense of it", - kb: stripDestinationPrefixesFromPublicNetworks(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall}}, - {expectedErrMsg: "networks with 'underlay: false' must contain a value of 'vrf' as it is used for BGP", - kb: stripVRFValueOfNonUnderlayNetworks(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall}}, - {expectedErrMsg: "each 'nic' definition must contain a valid 'mac'", - kb: unlegalizeMACs(stubKnowledgeBase()), - kinds: []BareMetalType{Firewall, Machine}}, - } - - for i, test := range tests { - for _, kind := range test.kinds { - t.Run(fmt.Sprintf("testcase %d - kind %v", i, kind), func(t *testing.T) { - actualErr := test.kb.Validate(kind) - if test.expectedErrMsg == "" { - require.NoError(t, actualErr) - return - } - require.EqualError(t, actualErr, test.expectedErrMsg, "expected error: %s", test.expectedErrMsg) - }) - } - } -} - -func stripVRFValueOfNonUnderlayNetworks(kb config) config { - for i := 0; i < len(kb.Networks); i++ { - // underlay runs in default vrf and no name is required - if kb.Networks[i].Underlay != nil && *kb.Networks[i].Underlay { - continue - } - vrf := int64(0) - kb.Networks[i].Vrf = &vrf - } - return kb -} - -// It makes no sense to have an public network without destination prefixes. -// Destination prefixes are used to import routes from the public network. -// Without route import there is no communication into that public network. -func stripDestinationPrefixesFromPublicNetworks(kb config) config { - kb.Networks[0].Nat = &boolTrue - for i := 0; i < len(kb.Networks); i++ { - if kb.Networks[i].Underlay != nil && !*kb.Networks[i].Underlay && kb.Networks[i].Private != nil && !*kb.Networks[i].Private { - kb.Networks[i].Destinationprefixes = []string{} - } - } - return kb -} - -func setupIllegalNat(kb config) config { - kb.Networks[0].Nat = &boolTrue - for i := 0; i < len(kb.Networks); i++ { - if kb.Networks[i].Private != nil && *kb.Networks[i].Private { - kb.Networks[i].Prefixes = []string{} - } - } - return kb -} - -func unlegalizeMACs(kb config) config { - mac := "1:2.3" - for i := 0; i < len(kb.Nics); i++ { - kb.Nics[i].Mac = &mac - } - return kb -} - -func stripMACs(kb config) config { - mac := "" - for i := 0; i < len(kb.Nics); i++ { - kb.Nics[i].Mac = &mac - } - return kb -} - -func stripNICs(kb config) config { - kb.Nics = []*models.V1MachineNic{} - return kb -} - -func stripUnderlayNetworkASN(kb config) config { - for i := 0; i < len(kb.Networks); i++ { - if kb.Networks[i].Underlay != nil && *kb.Networks[i].Underlay { - kb.Networks[i].Asn = &asn0 - } - } - return kb -} - -func stripPrivateNetworkASN(kb config) config { - for i := 0; i < len(kb.Networks); i++ { - if kb.Networks[i].Private != nil && *kb.Networks[i].Private { - kb.Networks[i].Asn = &asn0 - } - } - return kb -} - -func stripIPs(kb config) config { - for i := 0; i < len(kb.Networks); i++ { - kb.Networks[i].Ips = []string{} - } - return kb -} - -func stripNetworks(kb config) config { - kb.Networks = []*models.V1MachineNetwork{} - return kb -} - -func maskUnderlayNetworks(kb config) config { - privateSecondary := mn.PrivateSecondaryShared - for i, n := range kb.Networks { - if n.Networktype != nil && *n.Networktype == mn.Underlay { - kb.Networks[i].Underlay = &boolFalse - kb.Networks[i].Networktype = &privateSecondary - // avoid to run into validation error for absent vrf - kb.Networks[i].Vrf = &vrf1 - } - } - return kb -} - -func maskPrivatePrimaryNetworks(kb config) config { - privateUnshared := mn.PrivatePrimaryUnshared - for i := range kb.Networks { - kb.Networks[i].Networktype = &privateUnshared - } - return kb -} diff --git a/old/network/netobjects.go b/old/network/netobjects.go deleted file mode 100644 index 3393d26..0000000 --- a/old/network/netobjects.go +++ /dev/null @@ -1,100 +0,0 @@ -package network - -const ( - // IPv4ZeroCIDR is the CIDR block for the whole IPv4 address space - IPv4ZeroCIDR = "0.0.0.0/0" - - // IPv6ZeroCIDR is the CIDR block for the whole IPv6 address space - IPv6ZeroCIDR = "::/0" - // Permit defines an access policy that allows access. - Permit AccessPolicy = iota - // Deny defines an access policy that forbids access. - Deny -) - -type ( - // AccessPolicy is a type that represents a policy to manage access roles. - AccessPolicy int - - // Identity represents an object's identity. - Identity struct { - Comment string - ID int - } - - // Loopback represents a loopback interface (lo). - Loopback struct { - Comment string - IPs []string - } - - // VRF represents data required to render VRF information into frr.conf. - VRF struct { - Identity - Table int - VNI int - ImportVRFNames []string - IPPrefixLists []IPPrefixList - RouteMaps []RouteMap - FRRVersion *FRR - } - - FRR struct { - Major uint64 - Minor uint64 - } - // RouteMap represents a route-map to permit or deny routes. - RouteMap struct { - Name string - Entries []string - Policy string - Order int - } - - // IPPrefixList represents 'ip prefix-list' filtering mechanism to be used in combination with route-maps. - IPPrefixList struct { - Name string - Spec string - AddressFamily AddressFamily - // SourceVRF specifies from which VRF the given prefix list should be imported - SourceVRF string - } - - // SVI represents a switched virtual interface. - SVI struct { - VLANID int - Comment string - Addresses []string - } - - // VXLAN represents a VXLAN interface. - VXLAN struct { - Identity - TunnelIP string - } - - // EVPNIface represents the information required to render EVPN interfaces configuration. - EVPNIface struct { - Comment string - VRF VRF - SVI SVI - VXLAN VXLAN - } - - // Bridge represents a network bridge. - Bridge struct { - Ports string - Vids string - } -) - -func (p AccessPolicy) String() string { - switch p { - case Permit: - return "permit" - case Deny: - return "deny" - } - - return "undefined" -} diff --git a/old/network/nftables.go b/old/network/nftables.go deleted file mode 100644 index 46d4def..0000000 --- a/old/network/nftables.go +++ /dev/null @@ -1,309 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - "net/netip" - "strconv" - "strings" - - "github.com/metal-stack/metal-go/api/models" - mn "github.com/metal-stack/metal-lib/pkg/net" - - "github.com/metal-stack/os-installer/old/exec" - "github.com/metal-stack/os-installer/old/net" -) - -const ( - // TplNftables defines the name of the template to render nftables configuration. - TplNftables = "nftrules.tpl" - dnsPort = "domain" - nftablesService = "nftables.service" - systemctlBin = "/bin/systemctl" - - // Set up additional conntrack zone for DNS traffic. - // There was a problem that duplicate packets were registered by conntrack - // when packet was leaking from private VRF to the internet VRF. - // Isolating traffic to special zone solves the problem. - // Zone number(3) was obtained by experiments. - dnsProxyZone = "3" -) - -type ( - // NftablesData represents the information required to render nftables configuration. - NftablesData struct { - Comment string - SNAT []SNAT - DNSProxyDNAT DNAT - VPN bool - ForwardPolicy string - FirewallRules FirewallRules - Input Input - } - - Input struct { - InInterfaces []string - } - - FirewallRules struct { - Egress []string - Ingress []string - } - - // SNAT holds the information required to configure Source NAT. - SNAT struct { - Comment string - OutInterface string - OutIntSpec AddrSpec - SourceSpecs []AddrSpec - } - - // DNAT holds the information required to configure DNAT. - DNAT struct { - Comment string - InInterfaces []string - SAddr string - DAddr string - Port string - Zone string - DestSpec AddrSpec - } - - AddrSpec struct { - AddressFamily string - Address string - } - - // NftablesValidator can validate configuration for nftables rules. - NftablesValidator struct { - path string - log *slog.Logger - } - - NftablesReloader struct{} -) - -// newNftablesConfigApplier constructs a new instance of this type. -func newNftablesConfigApplier(c config, validator net.Validator, enableDNSProxy bool, forwardPolicy ForwardPolicy) net.Applier { - data := NftablesData{ - Comment: versionHeader(c.MachineUUID), - SNAT: getSNAT(c, enableDNSProxy), - ForwardPolicy: string(forwardPolicy), - FirewallRules: getFirewallRules(c), - Input: getInput(c), - } - - if enableDNSProxy { - data.DNSProxyDNAT = getDNSProxyDNAT(c, dnsPort, dnsProxyZone) - } - - if c.VPN != nil { - data.VPN = true - } - - return net.NewNetworkApplier(data, validator, &NftablesReloader{}) -} - -func (*NftablesReloader) Reload() error { - return exec.NewVerboseCmd(systemctlBin, "reload", nftablesService).Run() -} - -func isDMZNetwork(n *models.V1MachineNetwork) bool { - return *n.Networktype == mn.PrivateSecondaryShared && containsDefaultRoute(n.Destinationprefixes) -} - -func getInput(c config) Input { - input := Input{} - networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared) - for _, n := range networks { - input.InInterfaces = append(input.InInterfaces, fmt.Sprintf("vrf%d", *n.Vrf)) - } - return input -} - -func getSNAT(c config, enableDNSProxy bool) []SNAT { - var result []SNAT - - private := c.getPrivatePrimaryNetwork() - networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared, mn.External) - - privatePfx := private.Prefixes - for _, n := range c.Networks { - if isDMZNetwork(n) { - privatePfx = append(privatePfx, n.Prefixes...) - } - - } - - var ( - defaultNetwork models.V1MachineNetwork - defaultAF string - ) - defaultNetworkName, err := c.getDefaultRouteVRFName() - if err == nil { - defaultNetwork = *c.GetDefaultRouteNetwork() - ip, _ := netip.ParseAddr(defaultNetwork.Ips[0]) - defaultAF = "ip" - if ip.Is6() { - defaultAF = "ip6" - } - } - for _, n := range networks { - if n.Nat != nil && !*n.Nat { - continue - } - - var sources []AddrSpec - cmt := fmt.Sprintf("snat (networkid: %s)", *n.Networkid) - svi := fmt.Sprintf("vlan%d", *n.Vrf) - - for _, p := range privatePfx { - af, err := getAddressFamily(p) - if err != nil { - continue - } - sspec := AddrSpec{ - Address: p, - AddressFamily: af, - } - sources = append(sources, sspec) - } - s := SNAT{ - Comment: cmt, - OutInterface: svi, - SourceSpecs: sources, - } - - if enableDNSProxy && (vrfNameOf(n) == defaultNetworkName) { - s.OutIntSpec = AddrSpec{ - AddressFamily: defaultAF, - Address: defaultNetwork.Ips[0], - } - } - result = append(result, s) - } - - return result -} - -func getDNSProxyDNAT(c config, port, zone string) DNAT { - networks := c.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared) - svis := []string{} - for _, n := range networks { - svi := fmt.Sprintf("vlan%d", *n.Vrf) - svis = append(svis, svi) - } - - n := c.GetDefaultRouteNetwork() - if n == nil { - return DNAT{} - } - - ip, _ := netip.ParseAddr(n.Ips[0]) - af := "ip" - saddr := "10.0.0.0/8" - daddr := "@proxy_dns_servers" - if ip.Is6() { - af = "ip6" - saddr = "fd00::/8" - daddr = "@proxy_dns_servers_v6" - } - return DNAT{ - Comment: "dnat to dns proxy", - InInterfaces: svis, - SAddr: saddr, - DAddr: daddr, - Port: port, - Zone: zone, - DestSpec: AddrSpec{ - AddressFamily: af, - Address: n.Ips[0], - }, - } -} - -func getFirewallRules(c config) FirewallRules { - if c.FirewallRules == nil { - return FirewallRules{} - } - var ( - egressRules = []string{"# egress rules specified during firewall creation"} - ingressRules = []string{"# ingress rules specified during firewall creation"} - inputInterfaces = getInput(c) - quotedInputInterfaces []string - ) - for _, i := range inputInterfaces.InInterfaces { - quotedInputInterfaces = append(quotedInputInterfaces, "\""+i+"\"") - } - - for _, r := range c.FirewallRules.Egress { - ports := make([]string, len(r.Ports)) - for i, v := range r.Ports { - ports[i] = strconv.Itoa(int(v)) - } - for _, daddr := range r.To { - af, err := getAddressFamily(daddr) - if err != nil { - continue - } - egressRules = append(egressRules, - fmt.Sprintf("iifname { %s } %s daddr %s %s dport { %s } counter accept comment %q", strings.Join(quotedInputInterfaces, ","), af, daddr, strings.ToLower(r.Protocol), strings.Join(ports, ","), r.Comment)) - } - } - - privatePrimaryNetwork := c.getPrivatePrimaryNetwork() - outputInterfacenames := "" - if privatePrimaryNetwork != nil && privatePrimaryNetwork.Vrf != nil { - outputInterfacenames = fmt.Sprintf("oifname { \"vrf%d\", \"vni%d\", \"vlan%d\" }", *privatePrimaryNetwork.Vrf, *privatePrimaryNetwork.Vrf, *privatePrimaryNetwork.Vrf) - } - - for _, r := range c.FirewallRules.Ingress { - ports := make([]string, len(r.Ports)) - for i, v := range r.Ports { - ports[i] = strconv.Itoa(int(v)) - } - destinationSpec := "" - if len(r.To) > 0 { - af, err := getAddressFamily(r.To[0]) // To is validated to contain no mixed addressfamilies in metal-api - if err != nil { - continue - } - destinationSpec = fmt.Sprintf("%s daddr { %s }", af, strings.Join(r.To, ", ")) - } else if outputInterfacenames != "" { - destinationSpec = outputInterfacenames - } else { - c.log.Warn("no to address specified but not private primary network present, skipping this rule", "rule", r) - continue - } - - for _, saddr := range r.From { - af, err := getAddressFamily(saddr) - if err != nil { - continue - } - ingressRules = append(ingressRules, fmt.Sprintf("%s %s saddr %s %s dport { %s } counter accept comment %q", destinationSpec, af, saddr, strings.ToLower(r.Protocol), strings.Join(ports, ","), r.Comment)) - } - } - return FirewallRules{ - Egress: egressRules, - Ingress: ingressRules, - } -} - -func getAddressFamily(p string) (string, error) { - prefix, err := netip.ParsePrefix(p) - if err != nil { - return "", err - } - family := "ip" - if prefix.Addr().Is6() { - family = "ip6" - } - return family, nil -} - -// Validate validates network interfaces configuration. -func (v NftablesValidator) Validate() error { - v.log.Info("running 'nft --check --file' to validate changes.", "file", v.path) - return exec.NewVerboseCmd("nft", "--check", "--file", v.path).Run() -} diff --git a/old/network/routemap.go b/old/network/routemap.go deleted file mode 100644 index 157e6ba..0000000 --- a/old/network/routemap.go +++ /dev/null @@ -1,362 +0,0 @@ -package network - -import ( - "fmt" - "net/netip" - "sort" - "strings" - - "github.com/metal-stack/metal-go/api/models" - mn "github.com/metal-stack/metal-lib/pkg/net" -) - -type importPrefix struct { - Prefix netip.Prefix - Policy AccessPolicy - SourceVRF string -} - -type importRule struct { - TargetVRF string - ImportVRFs []string - ImportPrefixes []importPrefix - ImportPrefixesNoExport []importPrefix -} - -type ImportSettings struct { - ImportPrefixes []importPrefix - ImportPrefixesNoExport []importPrefix -} - -func (i *importRule) bySourceVrf() map[string]ImportSettings { - r := map[string]ImportSettings{} - for _, vrf := range i.ImportVRFs { - r[vrf] = ImportSettings{} - } - - for _, pfx := range i.ImportPrefixes { - e := r[pfx.SourceVRF] - e.ImportPrefixes = append(e.ImportPrefixes, pfx) - r[pfx.SourceVRF] = e - } - - for _, pfx := range i.ImportPrefixesNoExport { - e := r[pfx.SourceVRF] - e.ImportPrefixesNoExport = append(e.ImportPrefixesNoExport, pfx) - r[pfx.SourceVRF] = e - } - - return r -} - -func importRulesForNetwork(kb config, network *models.V1MachineNetwork) *importRule { - vrfName := vrfNameOf(network) - - if network.Networktype == nil || *network.Networktype == mn.Underlay { - return nil - } - i := importRule{ - TargetVRF: vrfName, - } - privatePrimaryNet := kb.getPrivatePrimaryNetwork() - - externalNets := kb.GetNetworks(mn.External) - privateSecondarySharedNets := kb.GetNetworks(mn.PrivateSecondaryShared) - - nt := *network.Networktype - switch nt { - case mn.PrivatePrimaryUnshared: - fallthrough - case mn.PrivatePrimaryShared: - // reach out from private network into public networks - i.ImportVRFs = vrfNamesOf(externalNets) - i.ImportPrefixes = getDestinationPrefixes(externalNets) - - // deny public address of default network - defaultNet := kb.GetDefaultRouteNetwork() - for _, ip := range defaultNet.Ips { - if parsed, err := netip.ParseAddr(ip); err == nil { - var bl = 32 - if parsed.Is6() { - bl = 128 - } - i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ - Prefix: netip.PrefixFrom(parsed, bl), - Policy: Deny, - SourceVRF: vrfNameOf(defaultNet), - }) - } - } - - // permit external routes - i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetworks(externalNets)...) - - // reach out from private network into shared private networks - i.ImportVRFs = append(i.ImportVRFs, vrfNamesOf(privateSecondarySharedNets)...) - i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetworks(privateSecondarySharedNets)...) - - // reach out from private network to destination prefixes of private secondays shared networks - for _, n := range privateSecondarySharedNets { - for _, pfx := range n.Destinationprefixes { - ppfx := netip.MustParsePrefix(pfx) - isThere := false - for _, i := range i.ImportPrefixes { - if i.Prefix == ppfx { - isThere = true - } - } - if !isThere { - i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ - Prefix: ppfx, - Policy: Permit, - SourceVRF: vrfNameOf(n), - }) - } - } - } - case mn.PrivateSecondaryShared: - // reach out from private shared networks into private primary network - i.ImportVRFs = []string{vrfNameOf(privatePrimaryNet)} - i.ImportPrefixes = concatPfxSlices(prefixesOfNetwork(privatePrimaryNet, vrfNameOf(privatePrimaryNet)), prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet))) - - // import destination prefixes of dmz networks from external networks - if len(network.Destinationprefixes) > 0 { - for _, pfx := range network.Destinationprefixes { - for _, e := range externalNets { - importExternalNet := false - for _, epfx := range e.Destinationprefixes { - if pfx == epfx { - importExternalNet = true - i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ - Prefix: netip.MustParsePrefix(pfx), - Policy: Permit, - SourceVRF: vrfNameOf(e), - }) - } - } - if importExternalNet { - i.ImportVRFs = append(i.ImportVRFs, vrfNameOf(e)) - i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetwork(e, vrfNameOf(e))...) - } - } - } - } - case mn.External: - // reach out from public into private and other public networks - i.ImportVRFs = []string{vrfNameOf(privatePrimaryNet)} - i.ImportPrefixes = prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet)) - - nets := []*models.V1MachineNetwork{privatePrimaryNet} - - if containsDefaultRoute(network.Destinationprefixes) { - for _, r := range privateSecondarySharedNets { - if containsDefaultRoute(r.Destinationprefixes) { - nets = append(nets, r) - i.ImportVRFs = append(i.ImportVRFs, vrfNameOf(r)) - } - } - } - i.ImportPrefixesNoExport = prefixesOfNetworks(nets) - } - - return &i -} - -func (i *importRule) prefixLists() []IPPrefixList { - var result []IPPrefixList - seed := IPPrefixListSeqSeed - afs := []AddressFamily{AddressFamilyIPv4, AddressFamilyIPv6} - for _, af := range afs { - pfxList := prefixLists(i.ImportPrefixesNoExport, af, false, seed, i.TargetVRF) - result = append(result, pfxList...) - - seed = IPPrefixListSeqSeed + len(result) - result = append(result, prefixLists(i.ImportPrefixes, af, true, seed, i.TargetVRF)...) - } - - return result -} - -func prefixLists( - prefixes []importPrefix, - af AddressFamily, - isExported bool, - seed int, - vrf string, -) []IPPrefixList { - var result []IPPrefixList - for _, p := range prefixes { - if af == AddressFamilyIPv4 && !p.Prefix.Addr().Is4() { - continue - } - - if af == AddressFamilyIPv6 && !p.Prefix.Addr().Is6() { - continue - } - - specs := p.buildSpecs(seed) - for _, spec := range specs { - // self-importing prefixes is nonsense - if vrf == p.SourceVRF { - continue - } - name := p.name(vrf, isExported) - prefixList := IPPrefixList{ - Name: name, - Spec: spec, - AddressFamily: af, - SourceVRF: p.SourceVRF, - } - result = append(result, prefixList) - } - seed++ - } - return result -} - -func concatPfxSlices(pfxSlices ...[]importPrefix) []importPrefix { - res := []importPrefix{} - for _, pfxSlice := range pfxSlices { - res = append(res, pfxSlice...) - } - return res -} - -func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { - var result []importPrefix - for _, e := range s { - ipp, err := netip.ParsePrefix(e) - if err != nil { - continue - } - result = append(result, importPrefix{ - Prefix: ipp, - Policy: Permit, - SourceVRF: sourceVrf, - }) - } - return result -} - -func getDestinationPrefixes(networks []*models.V1MachineNetwork) []importPrefix { - var result []importPrefix - for _, network := range networks { - result = append(result, stringSliceToIPPrefix(network.Destinationprefixes, vrfNameOf(network))...) - } - return result -} - -func prefixesOfNetworks(networks []*models.V1MachineNetwork) []importPrefix { - var result []importPrefix - for _, network := range networks { - result = append(result, prefixesOfNetwork(network, vrfNameOf(network))...) - } - return result -} - -func prefixesOfNetwork(network *models.V1MachineNetwork, sourceVrf string) []importPrefix { - return stringSliceToIPPrefix(network.Prefixes, sourceVrf) -} - -func vrfNameOf(n *models.V1MachineNetwork) string { - return fmt.Sprintf("vrf%d", *n.Vrf) -} - -func vrfNamesOf(networks []*models.V1MachineNetwork) []string { - var result []string - for _, n := range networks { - result = append(result, vrfNameOf(n)) - } - - return result -} - -func byName(prefixLists []IPPrefixList) map[string]IPPrefixList { - byName := map[string]IPPrefixList{} - for _, prefixList := range prefixLists { - if _, isPresent := byName[prefixList.Name]; isPresent { - continue - } - - byName[prefixList.Name] = prefixList - } - - return byName -} - -func (i *importRule) routeMaps() []RouteMap { - var result []RouteMap - - order := RouteMapOrderSeed - byName := byName(i.prefixLists()) - - names := []string{} - for n := range byName { - names = append(names, n) - } - sort.Sort(sort.Reverse(sort.StringSlice(names))) - - for _, n := range names { - prefixList := byName[n] - - matchVrf := fmt.Sprintf("match source-vrf %s", prefixList.SourceVRF) - matchPfxList := fmt.Sprintf("match %s address prefix-list %s", prefixList.AddressFamily, n) - entries := []string{matchVrf, matchPfxList} - if strings.HasSuffix(n, IPPrefixListNoExportSuffix) { - entries = append(entries, "set community additive no-export") - } - - routeMap := RouteMap{ - Name: routeMapName(i.TargetVRF), - Policy: Permit.String(), - Order: order, - Entries: entries, - } - order += RouteMapOrderSeed - - result = append(result, routeMap) - } - - routeMap := RouteMap{ - Name: routeMapName(i.TargetVRF), - Policy: Deny.String(), - Order: order, - } - - result = append(result, routeMap) - - return result -} - -func routeMapName(vrfName string) string { - return vrfName + "-import-map" -} - -func (i *importPrefix) buildSpecs(seq int) []string { - var result []string - var spec string - - if i.Prefix.Bits() == 0 { - spec = fmt.Sprintf("%s %s", i.Policy, i.Prefix) - - } else { - spec = fmt.Sprintf("seq %d %s %s le %d", seq, i.Policy, i.Prefix, i.Prefix.Addr().BitLen()) - } - - result = append(result, spec) - - return result -} - -func (i *importPrefix) name(targetVrf string, isExported bool) string { - suffix := "" - - if i.Prefix.Addr().Is6() { - suffix = "-ipv6" - } - if !isExported { - suffix += IPPrefixListNoExportSuffix - } - - return fmt.Sprintf("%s-import-from-%s%s", targetVrf, i.SourceVRF, suffix) -} diff --git a/old/network/routemap_test.go b/old/network/routemap_test.go deleted file mode 100644 index 6f51a31..0000000 --- a/old/network/routemap_test.go +++ /dev/null @@ -1,320 +0,0 @@ -package network - -import ( - "fmt" - "log/slog" - "net/netip" - "reflect" - "testing" - - "github.com/stretchr/testify/require" -) - -type network struct { - vrf string - prefixes []importPrefix - destinations []importPrefix -} - -var ( - defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: Permit, SourceVRF: inetVrf} - defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: Permit, SourceVRF: inetVrf} - defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: Permit, SourceVRF: dmzVrf} - externalVrf = "vrf104010" - externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: Permit, SourceVRF: externalVrf} - externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: Permit, SourceVRF: externalVrf} - privateVrf = "vrf3981" - privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: Permit, SourceVRF: privateVrf} - privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: Permit, SourceVRF: privateVrf} - sharedVrf = "vrf3982" - sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: Permit, SourceVRF: sharedVrf} - dmzVrf = "vrf3983" - dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: Permit, SourceVRF: dmzVrf} - inetVrf = "vrf104009" - inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: Permit, SourceVRF: inetVrf} - inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: Permit, SourceVRF: inetVrf} - inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: Permit, SourceVRF: inetVrf} - publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: Deny, SourceVRF: inetVrf} - publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: Deny, SourceVRF: dmzVrf} - publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: Deny, SourceVRF: inetVrf} - - private = network{ - vrf: privateVrf, - prefixes: []importPrefix{privateNet}, - } - - private6 = network{ - vrf: privateVrf, - prefixes: []importPrefix{privateNet6}, - } - - inet = network{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet1, inetNet2}, - destinations: []importPrefix{defaultRoute}, - } - - inet6 = network{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet6}, - destinations: []importPrefix{defaultRoute6}, - } - dualstack = network{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet1, inetNet6}, - destinations: []importPrefix{defaultRoute6}, - } - external = network{ - vrf: externalVrf, - destinations: []importPrefix{externalDestinationNet}, - prefixes: []importPrefix{externalNet}, - } - - shared = network{ - vrf: sharedVrf, - prefixes: []importPrefix{sharedNet}, - } - - dmz = network{ - vrf: dmzVrf, - prefixes: []importPrefix{dmzNet}, - destinations: []importPrefix{defaultRouteFromDMZ}, - } -) - -func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { - r := []importPrefix{} - for _, e := range pfxs { - i := e - i.SourceVRF = sourceVrf - r = append(r, i) - } - return r -} - -func Test_importRulesForNetwork(t *testing.T) { - tests := []struct { - name string - input string - want map[string]map[string]ImportSettings - }{ - { - name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", - input: "testdata/firewall.yaml", - want: map[string]map[string]ImportSettings{ - // The target VRF - private.vrf: { - // Imported VRFs with their restrictions - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - external.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), - }, - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), - }, - }, - inet.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - }, - external.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: leakFrom(external.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - }, - }, - }, - { - name: "firewall of a shared private network (shared/storage firewall)", - input: "testdata/firewall_shared.yaml", - want: map[string]map[string]ImportSettings{ - shared.vrf: { - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - }, - inet.vrf: { - shared.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), - ImportPrefixesNoExport: shared.prefixes, - }, - }, - }, - }, - { - name: "firewall of a private network with dmz network and internet (dmz firewall)", - input: "testdata/firewall_dmz.yaml", - want: map[string]map[string]ImportSettings{ - private.vrf: { - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - dmz.vrf: ImportSettings{ - ImportPrefixes: dmz.prefixes, - }, - }, - dmz.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), - }, - inet.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, inet.prefixes), - }, - }, - inet.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - dmz.vrf: ImportSettings{ - ImportPrefixesNoExport: dmz.prefixes, - }, - }, - }, - }, - { - name: "firewall of a private network with dmz network (dmz app firewall)", - input: "testdata/firewall_dmz_app.yaml", - want: map[string]map[string]ImportSettings{ - private.vrf: { - dmz.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), - }, - }, - dmz.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), - }, - }, - }, - }, - { - name: "firewall of a private network with dmz network and storage (dmz app firewall)", - input: "testdata/firewall_dmz_app_storage.yaml", - want: map[string]map[string]ImportSettings{ - private.vrf: { - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - dmz.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), - }, - }, - dmz.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), - }, - }, - shared.vrf: { - private.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), - }, - }, - }, - }, - { - name: "firewall with ipv6 private network and ipv6 internet network", - input: "testdata/firewall_ipv6.yaml", - want: map[string]map[string]ImportSettings{ - private6.vrf: { - inet6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), - }, - external.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), - }, - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), - }, - }, - inet6.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - external.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(external.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - }, - }, - { - name: "firewall with ipv6 private network and dualstack internet network", - input: "testdata/firewall_dualstack.yaml", - want: map[string]map[string]ImportSettings{ - private6.vrf: { - inet6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), - }, - external.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), - }, - shared.vrf: ImportSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), - }, - }, - inet6.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - external.vrf: { - private6.vrf: ImportSettings{ - ImportPrefixes: leakFrom(external.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - }, - }, - } - log := slog.Default() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - kb, err := New(log, tt.input) - require.NoError(t, err) - err = kb.Validate(Firewall) - if err != nil { - t.Errorf("%s is not valid: %v", tt.input, err) - return - } - for _, network := range kb.Networks { - got := importRulesForNetwork(*kb, network) - if got == nil { - continue - } - gotBySourceVrf := got.bySourceVrf() - targetVrf := fmt.Sprintf("vrf%d", *network.Vrf) - want := tt.want[targetVrf] - - if !reflect.DeepEqual(gotBySourceVrf, want) { - t.Errorf("importRulesForNetwork() \ntargetVrf: %s \ng: %v, \nw: %v", targetVrf, gotBySourceVrf, want) - } - } - }) - } -} diff --git a/old/network/service_test.go b/old/network/service_test.go deleted file mode 100644 index 3b0e46d..0000000 --- a/old/network/service_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package network - -import ( - "bytes" - "log/slog" - "os" - "testing" - - "github.com/metal-stack/os-installer/old/net" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestServices(t *testing.T) { - log := slog.Default() - - kb, err := New(log, "testdata/firewall.yaml") - require.NoError(t, err) - v := serviceValidator{} - dsApplier, err := newDroptailerServiceApplier(*kb, v) - require.NoError(t, err) - fcApplier, err := newFirewallControllerServiceApplier(*kb, v) - require.NoError(t, err) - nodeExporterApplier, err := newNodeExporterServiceApplier(*kb, v) - require.NoError(t, err) - suApplier, err := newSuricataUpdateServiceApplier(*kb, v) - require.NoError(t, err) - nftablesExporterApplier, err := NewNftablesExporterServiceApplier(*kb, v) - require.NoError(t, err) - - tests := []struct { - applier net.Applier - expected string - template string - }{ - { - applier: dsApplier, - expected: "testdata/droptailer.service", - template: tplDroptailer, - }, - { - applier: fcApplier, - expected: "testdata/firewall-controller.service", - template: tplFirewallController, - }, - { - applier: nodeExporterApplier, - expected: "testdata/node-exporter.service", - template: tplNodeExporter, - }, - { - applier: nftablesExporterApplier, - expected: "testdata/nftables-exporter.service", - template: tplNftablesExporter, - }, - { - applier: suApplier, - expected: "testdata/suricata-update.service", - template: tplSuricataUpdate, - }, - } - - for _, test := range tests { - expected, err := os.ReadFile(test.expected) - require.NoError(t, err) - - b := bytes.Buffer{} - tpl := MustParseTpl(test.template) - err = test.applier.Render(&b, *tpl) - require.NoError(t, err) - assert.Equal(t, string(expected), b.String()) - } -} diff --git a/old/network/validate.sh b/validate.sh similarity index 95% rename from old/network/validate.sh rename to validate.sh index f2d62a2..ff8d46d 100755 --- a/old/network/validate.sh +++ b/validate.sh @@ -21,7 +21,7 @@ validate () { --cap-add=NET_ADMIN \ --cap-add=NET_RAW \ --name vali \ - --volume ./testdata:/testdata \ + --volume ./pkg:/testdata:ro \ metal-networker-validate:${tag} /validate_os.sh } diff --git a/old/network/validate_os.sh b/validate_os.sh similarity index 89% rename from old/network/validate_os.sh rename to validate_os.sh index 67c1d19..3c9cddb 100755 --- a/old/network/validate_os.sh +++ b/validate_os.sh @@ -1,6 +1,6 @@ #!/bin/bash -testcases="/testdata/frr.conf.*" +testcases="/testdata/frr/test/frr.conf.*" for tc in $testcases; do echo -n "Testing ${FRR_VERSION} on ${OS_NAME}:${OS_VERSION} with input ${tc}: " if vtysh --dryrun --inputfile "${tc}"; @@ -13,7 +13,7 @@ for tc in $testcases; do fi done -testcases="/testdata/nftrules*" +testcases="/testdata/nftables/test/nftrules*" for tc in $testcases; do echo -n "Testing nft rules on ${OS_NAME}:${OS_VERSION} with input ${tc}: " if nft -c -f "${tc}"; From 492bfd674ab6d962a2f3832bf7ecdfc1287089b6 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 13:50:03 +0100 Subject: [PATCH 038/102] package removed --- install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.go b/install.go index 947d489..5e855ae 100644 --- a/install.go +++ b/install.go @@ -16,7 +16,7 @@ import ( ignitionConfig "github.com/flatcar/ignition/config/v2_4" "github.com/metal-stack/metal-go/api/models" v1 "github.com/metal-stack/os-installer/api/v1" - "github.com/metal-stack/os-installer/old/network" + "github.com/metal-stack/os-installer/pkg/network" "github.com/metal-stack/os-installer/pkg/nftables" "github.com/metal-stack/os-installer/pkg/services/chrony" "github.com/metal-stack/v" From 99ffb45ce66fec11794f5cf8623cd63f6681c908 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 13:53:55 +0100 Subject: [PATCH 039/102] go mod tidy --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index ff586d2..445d26c 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/google/uuid v1.6.0 github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff github.com/metal-stack/metal-go v0.43.0 - github.com/metal-stack/metal-lib v0.24.0 github.com/metal-stack/v v1.0.3 github.com/samber/lo v1.53.0 github.com/spf13/afero v1.15.0 diff --git a/go.sum b/go.sum index 78e129c..94d99b8 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,6 @@ github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff h1:668iZE3tvpbh github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff/go.mod h1:SAtqZaD4JvOn+NVc6bTlKzL2EDoj/QrlHF72ZMw+Btk= github.com/metal-stack/metal-go v0.43.0 h1:uODD0YCwnAYzyvFxWNakZrymBoMz1FAvP5hkhsR83VQ= github.com/metal-stack/metal-go v0.43.0/go.mod h1:GSfXrAj55LGsUSMHWGDsmq5n056NG0yb1JM8bgfvKOw= -github.com/metal-stack/metal-lib v0.24.0 h1:wvQQPWIXcA2tP+I6zAHUNdtVLLJfQnnV9yG2SoqUkz4= -github.com/metal-stack/metal-lib v0.24.0/go.mod h1:oITaqj/BtB9vDKM66jCXkeA+4D0eTZElgIKal5vtiNY= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= From d34df4ebd741361304e1f3f0cbff0941d93797eb Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 14:17:44 +0100 Subject: [PATCH 040/102] Add frr version detection. --- pkg/frr/frr.go | 15 +++++++++---- pkg/frr/frr_version.go | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 pkg/frr/frr_version.go diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index ec33add..1d47116 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -223,11 +223,18 @@ func assembleVRFs(cfg *Config) ([]VRF, error) { frr *FRR ) - if cfg.FRRVersion != nil { - frr = &FRR{ - Major: cfg.FRRVersion.Major(), - Minor: cfg.FRRVersion.Minor(), + if cfg.FRRVersion == nil { + frrVersion, err := DetectVersion() + if err != nil { + return nil, fmt.Errorf("unable to detect frr version: %w", err) } + + cfg.FRRVersion = frrVersion + } + + frr = &FRR{ + Major: cfg.FRRVersion.Major(), + Minor: cfg.FRRVersion.Minor(), } for _, n := range cfg.Network.AllocationNetworks() { diff --git a/pkg/frr/frr_version.go b/pkg/frr/frr_version.go new file mode 100644 index 0000000..7857ef3 --- /dev/null +++ b/pkg/frr/frr_version.go @@ -0,0 +1,51 @@ +package frr + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/Masterminds/semver/v3" +) + +func DetectVersion() (*semver.Version, error) { + vtysh, err := exec.LookPath("vtysh") + if err != nil { + return nil, fmt.Errorf("unable to detect path to vtysh: %w", err) + } + + // $ vtysh -c "show version"|grep FRRouting + // FRRouting 10.2.1 (shoot--pz9cjf--mwen-fel-firewall-dcedd) on Linux(6.6.60-060660-generic). + c := exec.Command(vtysh, "-c", "show version") + out, err := c.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("unable to detect frr version with dpkg: %w", err) + } + + var frrVersion string + + for line := range strings.SplitSeq(string(out), "\n") { + if !strings.Contains(line, "FRRouting") { + continue + } + + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + frrVersion = fields[1] + break + } + + if frrVersion == "" { + return nil, fmt.Errorf("unable to detect frr version") + } + + ver, err := semver.NewVersion(frrVersion) + if err != nil { + return nil, fmt.Errorf("unable to parse frr version to semver: %w", err) + } + + return ver, nil +} From 7ecc53f6f61abe464c7701df65b48cfeb2dce773 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 14:29:06 +0100 Subject: [PATCH 041/102] Change to API v2. --- api/v1/api.go | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/api/v1/api.go b/api/v1/api.go index fb737a3..08cd9ba 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -1,6 +1,8 @@ package v1 -import "github.com/metal-stack/metal-go/api/models" +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" +) // Bootinfo is written by the installer in the target os to tell us // which kernel, initrd and cmdline must be used for kexec @@ -11,39 +13,19 @@ type Bootinfo struct { BootloaderID string `yaml:"bootloader_id"` } -// InstallerConfig contains configuration items which are -// used to install the os. -type InstallerConfig struct { - // Hostname of the machine - Hostname string `yaml:"hostname"` - // Networks all networks connected to this machine - Networks []*models.V1MachineNetwork `yaml:"networks"` - // MachineUUID is the unique UUID for this machine, usually the board serial. - MachineUUID string `yaml:"machineuuid"` - // SSHPublicKey of the user - SSHPublicKey string `yaml:"sshpublickey"` +type MachineDetails struct { + // Id is the machine UUID + ID string `yaml:"id"` + // Nics are the nics of the machine + Nics []*apiv2.MachineNic `yaml:"nics"` // Password is the password for the metal user. Password string `yaml:"password"` // Console specifies where the kernel should connect its console to. Console string `yaml:"console"` - // Timestamp is the the timestamp of installer config creation. - Timestamp string `yaml:"timestamp"` - // Nics are the network interfaces of this machine including their neighbors. - Nics []*models.V1MachineNic `yaml:"nics"` - // VPN is the config for connecting machine to VPN - VPN *models.V1MachineVPN `yaml:"vpn"` - // Role is either firewall or machine - Role string `yaml:"role"` // RaidEnabled is set to true if any raid devices are specified RaidEnabled bool `yaml:"raidenabled"` // RootUUID is the fs uuid if the root fs RootUUID string `yaml:"root_uuid"` - // FirewallRules if not empty firewall rules to enforce - FirewallRules *models.V1FirewallRules `yaml:"firewall_rules"` - // DNSServers for the machine - DNSServers []*models.V1DNSServer `yaml:"dns_servers"` - // NTPServers for the machine - NTPServers []*models.V1NTPServer `yaml:"ntp_servers"` } // FIXME legacy structs remove once old images are gone From 1422fd8935b31520fefdf224b6a33d317603a6bf Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 14:50:34 +0100 Subject: [PATCH 042/102] Emojis. --- validate_os.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/validate_os.sh b/validate_os.sh index 3c9cddb..a6029f9 100755 --- a/validate_os.sh +++ b/validate_os.sh @@ -5,9 +5,9 @@ for tc in $testcases; do echo -n "Testing ${FRR_VERSION} on ${OS_NAME}:${OS_VERSION} with input ${tc}: " if vtysh --dryrun --inputfile "${tc}"; then - printf "\e[32m\xE2\x9C\x94\e[0m\n" + echo "✅" else - printf "\e[31m\xE2\x9D\x8C\e[0m\n" + echo "❌" echo "FRR ${FRR_VERSION} on ${OS_NAME}:${OS_VERSION} produces an invalid configuration" exit 1 fi @@ -18,10 +18,10 @@ for tc in $testcases; do echo -n "Testing nft rules on ${OS_NAME}:${OS_VERSION} with input ${tc}: " if nft -c -f "${tc}"; then - printf "\e[32m\xE2\x9C\x94\e[0m\n" + echo "✅" else - printf "\e[31m\xE2\x9D\x8C\e[0m\n" + echo "❌" echo "nft input ${tc} on ${OS_NAME}:${OS_VERSION} produces an invalid configuration" exit 1 fi -done \ No newline at end of file +done From 51ae978c4cad3fd29bc1850b747b678d8f911985 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 11 Mar 2026 15:07:36 +0100 Subject: [PATCH 043/102] Remove unused --- pkg/frr/frr.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 1d47116..985a035 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -20,8 +20,7 @@ import ( const ( comment = "generated by os-installer" - serviceName = "frr.service" - serviceUnitPath = "/etc/systemd/system/" + serviceName + serviceName = "frr.service" frrConfigPath = "/etc/frr/frr.conf" @@ -33,10 +32,6 @@ const ( ipPrefixListNoExportSuffix = "-no-export" // routeMapOrderSeed defines the initial value for route-map order. routeMapOrderSeed = 10 - // addressFamilyIPv4 is the name for this address family for the routing daemon. - addressFamilyIPv4 = "ip" - // addressFamilyIPv6 is the name for this address family for the routing daemon. - addressFamilyIPv6 = "ipv6" ) var ( From f36ed89828dd8539b1b920f947a244b486586225 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 11 Mar 2026 18:25:35 +0100 Subject: [PATCH 044/102] Refactor installer. --- install.go | 939 -------------- install_test.go | 1078 ----------------- main.go | 67 - os.go | 114 -- os_test.go | 98 -- pkg/exec/cmdexec.go | 101 ++ pkg/installer/installer.go | 190 +++ pkg/installer/installer_test.go | 59 + pkg/installer/os/almalinux/almalinux.go | 53 + .../os/almalinux/create_metal_user.go | 28 + .../os/almalinux/install_bootloader.go | 132 ++ pkg/installer/os/almalinux/write_ntp_conf.go | 30 + pkg/installer/os/common/cmd_line.go | 99 ++ pkg/installer/os/common/cmd_line_test.go | 108 ++ pkg/installer/os/common/configure_network.go | 45 + pkg/installer/os/common/copy_ssh_keys.go | 52 + pkg/installer/os/common/create_metal_user.go | 57 + pkg/installer/os/common/fix_permissions.go | 19 + pkg/installer/os/common/install_bootloader.go | 155 +++ pkg/installer/os/common/oscommon.go | 188 +++ pkg/installer/os/common/oscommon_test.go | 26 + pkg/installer/os/common/process_userdata.go | 87 ++ pkg/installer/os/common/systemd_services.go | 11 + pkg/installer/os/common/unset_machine_id.go | 25 + pkg/installer/os/common/write_boot_info.go | 32 + pkg/installer/os/common/write_build_meta.go | 45 + pkg/installer/os/common/write_hostname.go | 13 + pkg/installer/os/common/write_hosts.go | 23 + pkg/installer/os/common/write_ntp_conf.go | 65 + pkg/installer/os/common/write_resolv_conf.go | 37 + pkg/installer/os/debian/debian.go | 35 + pkg/installer/os/debian/tests/debian_test.go | 26 + .../debian/tests/install_bootloader_test.go | 166 +++ .../os/debian/tests/write_boot_info_test.go | 124 ++ pkg/installer/os/os.go | 64 + .../os/ubuntu/tests/cmd_line_test.go | 97 ++ .../os/ubuntu/tests/fix_permissions_test.go | 65 + .../ubuntu/tests/install_bootloader_test.go | 166 +++ .../os/ubuntu/tests/process_userdata_test.go | 107 ++ pkg/installer/os/ubuntu/tests/ubuntu_test.go | 26 + .../os/ubuntu/tests/unset_machine_id_test.go | 76 ++ .../os/ubuntu/tests/write_boot_info_test.go | 124 ++ .../os/ubuntu/tests/write_build_meta_test.go | 80 ++ .../os/ubuntu/tests/write_hostname_test.go | 82 ++ .../os/ubuntu/tests/write_hosts_test.go | 85 ++ .../os/ubuntu/tests/write_ntp_conf_test.go | 228 ++++ .../os/ubuntu/tests/write_resolv_conf_test.go | 94 ++ pkg/installer/os/ubuntu/ubuntu.go | 35 + pkg/test/fakeexec.go | 56 + pkg/test/fakeexec_test.go | 25 + 50 files changed, 3441 insertions(+), 2296 deletions(-) delete mode 100644 install.go delete mode 100644 install_test.go delete mode 100644 main.go delete mode 100644 os.go delete mode 100644 os_test.go create mode 100644 pkg/exec/cmdexec.go create mode 100644 pkg/installer/installer.go create mode 100644 pkg/installer/installer_test.go create mode 100644 pkg/installer/os/almalinux/almalinux.go create mode 100644 pkg/installer/os/almalinux/create_metal_user.go create mode 100644 pkg/installer/os/almalinux/install_bootloader.go create mode 100644 pkg/installer/os/almalinux/write_ntp_conf.go create mode 100644 pkg/installer/os/common/cmd_line.go create mode 100644 pkg/installer/os/common/cmd_line_test.go create mode 100644 pkg/installer/os/common/configure_network.go create mode 100644 pkg/installer/os/common/copy_ssh_keys.go create mode 100644 pkg/installer/os/common/create_metal_user.go create mode 100644 pkg/installer/os/common/fix_permissions.go create mode 100644 pkg/installer/os/common/install_bootloader.go create mode 100644 pkg/installer/os/common/oscommon.go create mode 100644 pkg/installer/os/common/oscommon_test.go create mode 100644 pkg/installer/os/common/process_userdata.go create mode 100644 pkg/installer/os/common/systemd_services.go create mode 100644 pkg/installer/os/common/unset_machine_id.go create mode 100644 pkg/installer/os/common/write_boot_info.go create mode 100644 pkg/installer/os/common/write_build_meta.go create mode 100644 pkg/installer/os/common/write_hostname.go create mode 100644 pkg/installer/os/common/write_hosts.go create mode 100644 pkg/installer/os/common/write_ntp_conf.go create mode 100644 pkg/installer/os/common/write_resolv_conf.go create mode 100644 pkg/installer/os/debian/debian.go create mode 100644 pkg/installer/os/debian/tests/debian_test.go create mode 100644 pkg/installer/os/debian/tests/install_bootloader_test.go create mode 100644 pkg/installer/os/debian/tests/write_boot_info_test.go create mode 100644 pkg/installer/os/os.go create mode 100644 pkg/installer/os/ubuntu/tests/cmd_line_test.go create mode 100644 pkg/installer/os/ubuntu/tests/fix_permissions_test.go create mode 100644 pkg/installer/os/ubuntu/tests/install_bootloader_test.go create mode 100644 pkg/installer/os/ubuntu/tests/process_userdata_test.go create mode 100644 pkg/installer/os/ubuntu/tests/ubuntu_test.go create mode 100644 pkg/installer/os/ubuntu/tests/unset_machine_id_test.go create mode 100644 pkg/installer/os/ubuntu/tests/write_boot_info_test.go create mode 100644 pkg/installer/os/ubuntu/tests/write_build_meta_test.go create mode 100644 pkg/installer/os/ubuntu/tests/write_hostname_test.go create mode 100644 pkg/installer/os/ubuntu/tests/write_hosts_test.go create mode 100644 pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go create mode 100644 pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go create mode 100644 pkg/installer/os/ubuntu/ubuntu.go create mode 100644 pkg/test/fakeexec.go create mode 100644 pkg/test/fakeexec_test.go diff --git a/install.go b/install.go deleted file mode 100644 index 5e855ae..0000000 --- a/install.go +++ /dev/null @@ -1,939 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io/fs" - "log/slog" - "os" - "os/exec" - "os/user" - "path" - "strconv" - "strings" - "time" - - ignitionConfig "github.com/flatcar/ignition/config/v2_4" - "github.com/metal-stack/metal-go/api/models" - v1 "github.com/metal-stack/os-installer/api/v1" - "github.com/metal-stack/os-installer/pkg/network" - "github.com/metal-stack/os-installer/pkg/nftables" - "github.com/metal-stack/os-installer/pkg/services/chrony" - "github.com/metal-stack/v" - "github.com/spf13/afero" - "gopkg.in/yaml.v3" -) - -const ( - installYAML = "/etc/metal/install.yaml" - userdata = "/etc/metal/userdata" -) - -func runFromCI() bool { - ciEnv := os.Getenv("INSTALL_FROM_CI") - - ci, err := strconv.ParseBool(ciEnv) - if err != nil { - return false - } - - return ci -} - -type installer struct { - log *slog.Logger - fs afero.Fs - oss operatingsystem - config *v1.InstallerConfig - exec *cmdexec - ctx context.Context -} - -func Install(ctx context.Context, log *slog.Logger, config *v1.InstallerConfig) error { - start := time.Now() - fs := afero.OsFs{} - - oss, err := detectOS(fs) - if err != nil { - return fmt.Errorf("os detection failed %w", err) - } - - i := installer{ - log: log.WithGroup("os-installer"), - fs: fs, - oss: oss, - config: config, - exec: &cmdexec{ - log: log.WithGroup("cmdexec"), - c: exec.CommandContext, - }, - ctx: ctx, - } - - err = i.do() - if err != nil { - return fmt.Errorf("installation failed duration %s %w", time.Since(start).String(), err) - } - i.log.Info("installation succeeded", "duration", time.Since(start).String()) - return nil -} - -func (i *installer) do() error { - err := i.detectFirmware() - if err != nil { - i.log.Warn("no efi detected", "error", err) - return err - } - - if !i.fileExists(installYAML) { - return fmt.Errorf("no install.yaml found") - } - - // remove .dockerenv, otherwise systemd-detect-virt guesses docker which modifies the behavior of many services. - if i.fileExists("/.dockerenv") { - err := os.Remove("/.dockerenv") - if err != nil { - return fmt.Errorf("unable to delete .dockerenv") - } - } - - err = i.writeHostname() - if err != nil { - i.log.Warn("writing hostname failed", "error", err) - return err - } - - err = i.writeHosts() - if err != nil { - i.log.Warn("writing hosts file failed", "error", err) - return err - } - - err = i.writeResolvConf() - if err != nil { - i.log.Warn("writing resolv.conf failed", "error", err) - return err - } - - err = i.writeNTPConf() - if err != nil { - i.log.Warn("writing ntp configuration failed", "err", err) - return err - } - - err = i.createMetalUser() - if err != nil { - return err - } - err = i.configureNetwork() - if err != nil { - return err - } - - err = i.copySSHKeys() - if err != nil { - return err - } - - err = i.fixPermissions() - if err != nil { - return err - } - - err = i.processUserdata() - if err != nil { - return err - } - - cmdLine := i.buildCMDLine() - - err = i.writeBootInfo(cmdLine) - if err != nil { - return err - } - - err = i.grubInstall(cmdLine) - if err != nil { - return err - } - - err = i.unsetMachineID() - if err != nil { - return err - } - - err = i.systemdServices() - if err != nil { - return err - } - - err = i.writeBuildMeta() - if err != nil { - return err - } - - return nil -} - -func (i *installer) detectFirmware() error { - i.log.Info("detect firmware") - - if !i.isVirtual() && !i.fileExists("/sys/firmware/efi") { - return fmt.Errorf("not running efi mode") - } - return nil -} - -func (i *installer) isVirtual() bool { - return !i.fileExists("/sys/class/dmi") -} - -func (i *installer) unsetMachineID() error { - i.log.Info("unset machine-id") - for _, p := range []string{"/etc/machine-id", "/var/lib/dbus/machine-id"} { - if !i.fileExists(p) { - continue - } - f, err := i.fs.Create(p) - if err != nil { - return err - } - _ = f.Close() - } - return nil -} - -func (i *installer) fileExists(filename string) bool { - info, err := i.fs.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -func (i *installer) writeHostname() error { - return afero.WriteFile(i.fs, "/etc/hostname", []byte(i.config.Hostname), 0644) -} - -func (i *installer) writeHosts() error { - // FIXME: figure out how to get the private primary ip - return afero.WriteFile(i.fs, "/etc/hosts", []byte(fmt.Sprintf(`# this file was auto generated by the os-installer -127.0.0.1 localhost -%s %s -`, i.config.PrivateIP, i.config.Hostname)), 0644) -} - -func (i *installer) writeResolvConf() error { - const f = "/etc/resolv.conf" - i.log.Info("write configuration", "file", f) - // Must be written here because during docker build this file is synthetic - err := i.fs.Remove(f) - if err != nil { - i.log.Info("config file not present", "file", f) - } - - content := []byte( - `nameserver 8.8.8.8 -nameserver 8.8.4.4 -`) - - if len(i.config.DNSServers) > 0 { - var s strings.Builder - for _, dnsServer := range i.config.DNSServers { - s.WriteString("nameserver " + *dnsServer.IP + "\n") - } - content = []byte(s.String()) - - } - - return afero.WriteFile(i.fs, f, content, 0644) -} - -func (i *installer) writeNTPConf() error { - if len(i.config.NTPServers) == 0 { - return nil - } - - var ( - ntpConfigPath string - s string - err error - servers []string - ) - - for _, srv := range i.config.NTPServers { - servers = append(servers, *srv.Address) - } - - switch i.config.Role { - case models.V1MachineAllocationRoleFirewall: - _, err := chrony.WriteSystemdUnit(i.ctx, &chrony.Config{ - Log: i.log, - Reload: false, - Enable: true, - }, &chrony.TemplateData{ - NTPServers: servers, - }, "TODO") // FIXME: default vrf - - if err != nil { - return err - } - - case models.V1MachineAllocationRoleMachine: - if i.oss == osDebian || i.oss == osUbuntu { - ntpConfigPath = "/etc/systemd/timesyncd.conf" - var addresses []string - for _, ntp := range i.config.NTPServers { - if ntp.Address == nil { - continue - } - addresses = append(addresses, *ntp.Address) - } - s = fmt.Sprintf("[Time]\nNTP=%s\n", strings.Join(addresses, " ")) - } - - if i.oss == osAlmalinux { - _, err := chrony.WriteSystemdUnit(i.ctx, &chrony.Config{ - Log: i.log, - Reload: false, - Enable: true, - ChronyConfigPath: "/etc/chrony.conf", - }, &chrony.TemplateData{ - NTPServers: servers, - }, "TODO") // FIXME: default vrf - - if err != nil { - return err - } - } - - default: - return fmt.Errorf("unknown role:%s", i.config.Role) - } - - content := []byte(s) - i.log.Info("write configuration", "file", ntpConfigPath) - err = i.fs.Remove(ntpConfigPath) - if err != nil { - i.log.Info("config file not present", "file", ntpConfigPath) - } - - return afero.WriteFile(i.fs, ntpConfigPath, content, 0644) -} - -func (i *installer) buildCMDLine() string { - i.log.Info("build kernel cmdline") - - rootUUID := i.config.RootUUID - - parts := []string{ - fmt.Sprintf("console=%s", i.config.Console), - fmt.Sprintf("root=UUID=%s", rootUUID), - "init=/sbin/init", - "net.ifnames=0", - "biosdevname=0", - "nvme_core.io_timeout=300", // 300 sec should be enough for firewalls to be replaced - } - - mdUUID, found := i.findMDUUID() - if found { - mdParts := []string{ - "rdloaddriver=raid0", - "rdloaddriver=raid1", - fmt.Sprintf("rd.md.uuid=%s", mdUUID), - } - parts = append(parts, mdParts...) - } - - return strings.Join(parts, " ") -} - -func (i *installer) findMDUUID() (mdUUID string, found bool) { - i.log.Info("detect software raid uuid") - if !i.config.RaidEnabled { - return "", false - } - - blkidOut, err := i.exec.command(&cmdParams{ - name: "blkid", - timeout: 10 * time.Second, - }) - if err != nil { - i.log.Error("unable to run blkid", "error", err) - return "", false - } - rootUUID := i.config.RootUUID - - var rootDisk string - for line := range strings.SplitSeq(string(blkidOut), "\n") { - if strings.Contains(line, rootUUID) { - rd, _, found := strings.Cut(line, ":") - if found { - rootDisk = strings.TrimSpace(rd) - break - } - } - } - if rootDisk == "" { - i.log.Error("unable to detect rootdisk") - return "", false - } - - mdadmOut, err := i.exec.command(&cmdParams{ - name: "mdadm", - args: []string{"--detail", "--export", rootDisk}, - timeout: 10 * time.Second, - }) - if err != nil { - i.log.Error("unable to run mdadm", "error", err) - return "", false - } - - for line := range strings.SplitSeq(string(mdadmOut), "\n") { - _, md, found := strings.Cut(line, "MD_UUID=") - if found { - mdUUID = md - break - } - } - - if mdUUID == "" { - i.log.Error("unable to detect md root disk") - return "", false - } - - return mdUUID, true -} - -func (i *installer) createMetalUser() error { - i.log.Info("create user", "user", "metal") - - u, err := user.Lookup("metal") - if err != nil { - if err.Error() != user.UnknownUserError("metal").Error() { - return err - } - } - if u != nil { - i.log.Info("user already exists, recreating") - _, err = i.exec.command(&cmdParams{ - name: "userdel", - args: []string{"metal"}, - timeout: 10 * time.Second, - }) - if err != nil { - return err - } - } - - _, err = i.exec.command(&cmdParams{ - name: "useradd", - args: []string{"--create-home", "--uid", "1000", "--gid", i.oss.SudoGroup(), "--shell", "/bin/bash", "metal"}, - timeout: 10 * time.Second, - }) - if err != nil { - return err - } - - _, err = i.exec.command(&cmdParams{ - name: "passwd", - args: []string{"metal"}, - timeout: 10 * time.Second, - stdin: i.config.Password + "\n" + i.config.Password + "\n", - }) - if err != nil { - return err - } - - if i.oss == osAlmalinux { - // otherwise in rescue mode the root account is locked - _, err = i.exec.command(&cmdParams{ - name: "passwd", - args: []string{"root"}, - timeout: 10 * time.Second, - stdin: i.config.Password + "\n" + i.config.Password + "\n", - }) - if err != nil { - return err - } - } - - return nil -} - -func (i *installer) configureNetwork() error { - i.log.Info("configure network") - kb, err := network.New(i.log.WithGroup("networker"), installYAML) - if err != nil { - return err - } - - var kind network.BareMetalType - switch i.config.Role { - case models.V1MachineAllocationRoleFirewall: - kind = network.Firewall - case models.V1MachineAllocationRoleMachine: - kind = network.Machine - default: - return fmt.Errorf("unknown role:%s", i.config.Role) - } - - err = kb.Validate(kind) - if err != nil { - return err - } - - c, err := network.NewConfigurator(kind, *kb, false) - if err != nil { - return err - } - c.Configure(nftables.ForwardPolicyDrop) - return nil -} - -func (i *installer) copySSHKeys() error { - i.log.Info("copy ssh keys") - err := i.fs.MkdirAll("/home/metal/.ssh", 0700) - if err != nil { - return err - } - - u, err := user.Lookup("metal") - if err != nil { - return err - } - - uid, err := strconv.Atoi(u.Uid) - if err != nil { - return err - } - gid, err := strconv.Atoi(u.Gid) - if err != nil { - return err - } - - err = i.fs.Chown("/home/metal/.ssh", uid, gid) - if err != nil { - return err - } - - err = afero.WriteFile(i.fs, "/home/metal/.ssh/authorized_keys", []byte(i.config.SSHPublicKey), 0600) - if err != nil { - return err - } - return i.fs.Chown("/home/metal/.ssh/authorized_keys", uid, gid) -} - -func (i *installer) fixPermissions() error { - i.log.Info("fix permissions") - for p, perm := range map[string]fs.FileMode{ - "/var/tmp": 01777, - } { - err := i.fs.Chmod(p, perm) - if err != nil { - return err - } - } - - return nil -} - -func (i *installer) processUserdata() error { - i.log.Info("process userdata") - if ok := i.fileExists(userdata); !ok { - i.log.Info("no userdata present, not processing userdata", "path", userdata) - return nil - } - - content, err := afero.ReadFile(i.fs, userdata) - if err != nil { - return err - } - - defer func() { - out, err := i.exec.command(&cmdParams{ - name: "systemctl", - args: []string{"preset-all"}, - }) - if err != nil { - i.log.Error("error when running systemctl preset-all, continuing anyway", "error", err, "output", string(out)) - } - }() - - if isCloudInitFile(content) { - _, err := i.exec.command(&cmdParams{ - name: "cloud-init", - args: []string{"devel", "schema", "--config-file", userdata}, - }) - if err != nil { - i.log.Error("error when running cloud-init userdata, continuing anyway", "error", err) - } - - return nil - } - - err = i.fs.Rename(userdata, "/etc/metal/config.ign") - if err != nil { - return err - } - - rawConfig, err := afero.ReadFile(i.fs, "/etc/metal/config.ign") - if err != nil { - return err - } - _, report, err := ignitionConfig.Parse(rawConfig) - if err != nil { - i.log.Error("error when validating ignition userdata, continuing anyway", "error", err) - } - - i.log.Info("executing ignition") - _, err = i.exec.command(&cmdParams{ - name: "ignition", - args: []string{"-oem", "file", "-stage", "files", "-log-to-stdout"}, - dir: "/etc/metal", - }) - if err != nil { - i.log.Error("error when running ignition, continuing anyway", "report", report.Entries, "error", err) - } - - return nil -} - -func isCloudInitFile(content []byte) bool { - for i, line := range strings.Split(string(content), "\n") { - if strings.Contains(line, "#cloud-config") { - return true - } - if i > 1 { - return false - } - } - return false -} - -func (i *installer) writeBootInfo(cmdLine string) error { - i.log.Info("write boot-info") - - kern, initrd, err := i.kernelAndInitrdPath() - if err != nil { - return err - } - - content, err := yaml.Marshal(v1.Bootinfo{ - Initrd: initrd, - Cmdline: cmdLine, - Kernel: kern, - BootloaderID: i.oss.BootloaderID(), - }) - if err != nil { - return fmt.Errorf("unable to write boot-info.yaml %w", err) - } - - return afero.WriteFile(i.fs, "/etc/metal/boot-info.yaml", content, 0700) -} - -func (i *installer) kernelAndInitrdPath() (kern string, initrd string, err error) { - // Debian 10 - // root@1f223b59051bcb12:/boot# ls -l - // total 83500 - // -rw-r--r-- 1 root root 83 Aug 13 15:25 System.map-5.10.0-17-amd64 - // -rw-r--r-- 1 root root 236286 Aug 13 15:25 config-5.10.0-17-amd64 - // -rw-r--r-- 1 root root 93842 Jul 19 2021 config-5.10.51 - // drwxr-xr-x 2 root root 4096 Oct 3 11:21 grub - // -rw-r--r-- 1 root root 34665690 Oct 3 11:22 initrd.img-5.10.0-17-amd64 - // lrwxrwxrwx 1 root root 21 Jul 19 2021 vmlinux -> /boot/vmlinux-5.10.51 - // -rwxr-xr-x 1 root root 43526368 Jul 19 2021 vmlinux-5.10.51 - // -rw-r--r-- 1 root root 6962816 Aug 13 15:25 vmlinuz-5.10.0-17-amd64 - - // Ubuntu 20.04 - // root@568551f94559b121:~# ls -l /boot/ - // total 83500 - // -rw-r--r-- 1 root root 83 Aug 13 15:25 System.map-5.10.0-17-amd64 - // -rw-r--r-- 1 root root 236286 Aug 13 15:25 config-5.10.0-17-amd64 - // -rw-r--r-- 1 root root 93842 Jul 19 2021 config-5.10.51 - // drwxr-xr-x 2 root root 4096 Oct 3 11:21 grub - // -rw-r--r-- 1 root root 34665690 Oct 3 11:22 initrd.img-5.10.0-17-amd64 - // lrwxrwxrwx 1 root root 21 Jul 19 2021 vmlinux -> /boot/vmlinux-5.10.51 - // -rwxr-xr-x 1 root root 43526368 Jul 19 2021 vmlinux-5.10.51 - // -rw-r--r-- 1 root root 6962816 Aug 13 15:25 vmlinuz-5.10.0-17-amd64 - - // Almalinux 9 - // [root@14231d4e67d28390 ~]# ls -l /boot/ - // total 160420 - // -rw------- 1 root root 8876661 Jan 7 23:19 System.map-5.14.0-503.19.1.el9_5.x86_64 - // -rw-r--r-- 1 root root 93842 Jul 19 2021 config-5.10.51 - // -rw-r--r-- 1 root root 226249 Jan 7 23:19 config-5.14.0-503.19.1.el9_5.x86_64 - // drwx------ 3 root root 4096 Jun 8 2022 efi - // drwx------ 3 root root 4096 Jan 9 08:02 grub2 - // -rw------- 1 root root 97054329 Jan 9 08:04 initramfs-5.14.0-503.19.1.el9_5.x86_64.img - // drwxr-xr-x 3 root root 4096 Jan 9 08:02 loader - // lrwxrwxrwx 1 root root 52 Jan 9 08:03 symvers-5.14.0-503.19.1.el9_5.x86_64.gz -> /lib/modules/5.14.0-503.19.1.el9_5.x86_64/symvers.gz - // lrwxrwxrwx 1 root root 21 Jul 19 2021 vmlinux -> /boot/vmlinux-5.10.51 - // -rwxr-xr-x 1 root root 43526368 Jul 19 2021 vmlinux-5.10.51 - // -rwxr-xr-x 1 root root 14467384 Jan 7 23:19 vmlinuz-5.14.0-503.19.1.el9_5.x86_64 - - var ( - bootPartition = "/boot" - systemMapPrefix = "/boot/System.map-" - ) - - systemMaps, err := afero.Glob(i.fs, systemMapPrefix+"*") - if err != nil { - return "", "", fmt.Errorf("unable to find a System.map, probably no kernel installed %w", err) - } - if len(systemMaps) != 1 { - return "", "", fmt.Errorf("more or less than a single System.map found(%v), probably no kernel or more than one kernel installed", systemMaps) - } - - systemMap := systemMaps[0] - _, kernelVersion, found := strings.Cut(systemMap, systemMapPrefix) - if !found { - return "", "", fmt.Errorf("unable to detect kernel version in System.map :%q", systemMap) - } - - kern = path.Join(bootPartition, "vmlinuz"+"-"+kernelVersion) - if !i.fileExists(kern) { - return "", "", fmt.Errorf("kernel image %q not found", kern) - } - initrd = path.Join(bootPartition, i.oss.Initramdisk(kernelVersion)) - if !i.fileExists(initrd) { - return "", "", fmt.Errorf("ramdisk %q not found", initrd) - } - - i.log.Info("detect kernel and initrd", "kernel", kern, "initrd", initrd) - - return -} - -func (i *installer) grubInstall(cmdLine string) error { - i.log.Info("install grub") - // ttyS1,115200n8 - serialPort, serialSpeed, found := strings.Cut(i.config.Console, ",") - if !found { - return fmt.Errorf("serial console could not be split into port and speed") - } - - _, serialPort, found = strings.Cut(serialPort, "ttyS") - if !found { - return fmt.Errorf("serial port could not be split") - } - - serialSpeed, _, found = strings.Cut(serialSpeed, "n8") - if !found { - return fmt.Errorf("serial speed could not be split") - } - - defaultGrub := fmt.Sprintf(`GRUB_DEFAULT=0 -GRUB_TIMEOUT=5 -GRUB_DISTRIBUTOR=%s -GRUB_CMDLINE_LINUX_DEFAULT="" -GRUB_CMDLINE_LINUX="%s" -GRUB_TERMINAL=serial -GRUB_SERIAL_COMMAND="serial --speed=%s --unit=%s --word=8" -`, i.oss.BootloaderID(), cmdLine, serialSpeed, serialPort) - - if i.oss == osAlmalinux { - defaultGrub += fmt.Sprintf("GRUB_DEVICE=UUID=%s\n", i.config.RootUUID) - defaultGrub += "GRUB_ENABLE_BLSCFG=false\n" - } - - err := afero.WriteFile(i.fs, "/etc/default/grub", []byte(defaultGrub), 0755) - if err != nil { - return err - } - - grubInstallArgs := []string{ - "--target=x86_64-efi", - "--efi-directory=/boot/efi", - "--boot-directory=/boot", - "--bootloader-id=" + i.oss.BootloaderID(), - } - if i.config.RaidEnabled { - grubInstallArgs = append(grubInstallArgs, "--no-nvram") - } - - if i.oss == osAlmalinux { - path := "/boot/grub2/grub.cfg" - if i.oss == osAlmalinux { - path = "/boot/efi/EFI/almalinux/grub.cfg" - } - _, err := i.exec.command(&cmdParams{ - name: "grub2-mkconfig", - args: []string{"-o", path}, - }) - if err != nil { - return err - } - - grubInstallArgs = append(grubInstallArgs, fmt.Sprintf("UUID=%s", i.config.RootUUID)) - } else { - grubInstallArgs = append(grubInstallArgs, "--removable") - } - - if i.config.RaidEnabled { - out, err := i.exec.command(&cmdParams{ - name: "mdadm", - args: []string{"--examine", "--scan"}, - timeout: 10 * time.Second, - }) - if err != nil { - return err - } - - out += "\nMAILADDR root\n" - - err = afero.WriteFile(i.fs, "/etc/mdadm.conf", []byte(out), 0700) - if err != nil { - return err - } - - if i.oss.NeedUpdateInitRamfs() { - err = i.fs.MkdirAll("/var/lib/initramfs-tools", 0755) - if err != nil { - return err - } - - _, err = i.exec.command(&cmdParams{ - name: "update-initramfs", - args: []string{"-u"}, - }) - if err != nil { - return err - } - } - - out, err = i.exec.command(&cmdParams{ - name: "blkid", - }) - if err != nil { - return err - } - - for line := range strings.SplitSeq(string(out), "\n") { - if strings.Contains(line, `PARTLABEL="efi"`) { - disk, _, found := strings.Cut(line, ":") - if !found { - return fmt.Errorf("unable to process blkid output lines") - } - shim := fmt.Sprintf(`\\EFI\\%s\\grubx64.efi`, i.oss.BootloaderID()) - if i.oss == osAlmalinux { - shim = fmt.Sprintf(`\\EFI\\%s\\shimx64.efi`, i.oss.BootloaderID()) - } - - _, err = i.exec.command(&cmdParams{ - name: "efibootmgr", - args: []string{"-c", "-d", disk, "-p1", "-l", shim, "-L", i.oss.BootloaderID()}, - }) - if err != nil { - return err - } - } - } - } - - if i.oss.GrubInstallCmd() != "" && !runFromCI() { - _, err = i.exec.command(&cmdParams{ - name: i.oss.GrubInstallCmd(), - args: grubInstallArgs, - }) - if err != nil { - return err - } - } - - if i.oss == osAlmalinux { - if !i.config.RaidEnabled { - return nil - } - - v, err := i.getKernelVersion() - if err != nil { - return err - } - - _, err = i.exec.command(&cmdParams{ - name: "dracut", - args: []string{ - "--mdadmconf", - "--kver", v, - "--kmoddir", "/lib/modules/" + v, - "--include", "/lib/modules/" + v, "/lib/modules/" + v, - "--fstab", - "--add=dm mdraid", - "--add-drivers=raid0 raid1", - "--hostonly", - "--force", - }, - }) - if err != nil { - return err - } - - return nil - } - - _, err = i.exec.command(&cmdParams{ - name: "update-grub2", - }) - if err != nil { - return err - } - - _, err = i.exec.command(&cmdParams{ - name: "dpkg-reconfigure", - args: []string{"grub-efi-amd64-bin"}, - env: []string{ - "DEBCONF_NONINTERACTIVE_SEEN=true", - "DEBIAN_FRONTEND=noninteractive", - }, - }) - if err != nil { - return err - } - - return nil -} - -func (i *installer) writeBuildMeta() error { - i.log.Info("writing build meta file", "path", "/etc/metal/build-meta.yaml") - - meta := &v1.BuildMeta{ - Version: v.Version, - Date: v.BuildDate, - SHA: v.GitSHA1, - Revision: v.Revision, - } - - out, err := i.exec.command(&cmdParams{ - name: "ignition", - args: []string{"-version"}, - }) - if err != nil { - i.log.Error("error detecting ignition version for build meta, continuing anyway", "error", err) - } else { - meta.IgnitionVersion = strings.TrimSpace(out) - } - - content, err := yaml.Marshal(meta) - if err != nil { - return err - } - - content = append([]byte("---\n"), content...) - - return afero.WriteFile(i.fs, "/etc/metal/build-meta.yaml", content, 0644) -} - -func (i *installer) getKernelVersion() (string, error) { - kern, _, err := i.kernelAndInitrdPath() - if err != nil { - return "", err - } - - _, version, found := strings.Cut(kern, "vmlinuz-") - if !found { - return "", fmt.Errorf("unable to determine kernel version from: %s", kern) - } - - return version, nil -} diff --git a/install_test.go b/install_test.go deleted file mode 100644 index 4a49e67..0000000 --- a/install_test.go +++ /dev/null @@ -1,1078 +0,0 @@ -package main - -import ( - "fmt" - "io/fs" - "log/slog" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/metal-stack/metal-go/api/models" - v1 "github.com/metal-stack/os-installer/api/v1" - "github.com/metal-stack/v" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" -) - -const ( - sampleInstallYAML = `--- -hostname: test-machine -networks: -- asn: 4210000000 - destinationprefixes: [] - ips: - - 192.168.0.1 - nat: false - networkid: 931b1568-9f2b-4b83-8bcb-cfc8f2a99e85 - networktype: privateprimaryshared - prefixes: - - 192.168.0.0/24 - private: true - underlay: false - vrf: 1 -- asn: 4210000000 - destinationprefixes: - - 0.0.0.0/0 - ips: - - 192.168.1.1 - nat: true - networkid: internet - networktype: external - prefixes: - - 192.168.1.0/24 - private: false - underlay: false - vrf: 104009 -machineuuid: c647818b-0573-45a1-bac4-e311db1df753 -sshpublickey: ssh-ed25519 key -password: a-password -devmode: false -console: ttyS1,115200n8 -raidenabled: false -root_uuid: "543eb7f8-98d4-d986-e669-824dbebe69e5" -timestamp: "2022-02-24T14:54:58Z" -nics: -- mac: b4:96:91:cb:64:e0 - name: eth4 - neighbors: - - mac: b8:6a:97:73:f8:5f - name: null - neighbors: [] -- mac: b4:96:91:cb:64:e1 - name: eth5 - neighbors: - - mac: b8:6a:97:74:00:5f - name: null - neighbors: []` - sampleInstallWithRaidYAML = `--- -hostname: test-machine -networks: -- asn: 4210000000 - destinationprefixes: [] - ips: - - 192.168.0.1 - nat: false - networkid: 931b1568-9f2b-4b83-8bcb-cfc8f2a99e85 - networktype: privateprimaryshared - prefixes: - - 192.168.0.0/24 - private: true - underlay: false - vrf: 1 -- asn: 4210000000 - destinationprefixes: - - 0.0.0.0/0 - ips: - - 192.168.1.1 - nat: true - networkid: internet - networktype: external - prefixes: - - 192.168.1.0/24 - private: false - underlay: false - vrf: 104009 -machineuuid: c647818b-0573-45a1-bac4-e311db1df753 -sshpublickey: ssh-ed25519 key -password: a-password -devmode: false -console: ttyS1,115200n8 -raidenabled: true -root_uuid: "ace079b5-06be-4429-bbf0-081ea4d7d0d9" -timestamp: "2022-02-24T14:54:58Z" -nics: -- mac: b4:96:91:cb:64:e0 - name: eth4 - neighbors: - - mac: b8:6a:97:73:f8:5f - name: null - neighbors: [] -- mac: b4:96:91:cb:64:e1 - name: eth5 - neighbors: - - mac: b8:6a:97:74:00:5f - name: null - neighbors: []` - sampleBlkidOutput = `/dev/sda1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="cc57c456-0b2f-6345-c597-d861cc6dd8ac" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="273985c8-d097-4123-bcd0-80b4e4e14728" -/dev/sda2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="54748c60-b566-f391-142c-fb78bb1fc6a9" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="d7863f4e-af7c-47fc-8c03-6ecdc69bc72d" -/dev/sda3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="582e9b4f-f191-e01e-85fd-2f7d969fbef6" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="e8b44f09-b7f7-4e0d-a7c3-d909617d1f05" -/dev/sdb1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="61bd5d8b-1bb8-673b-9e61-8c28dccc3812" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="13a4c568-57b0-4259-9927-9ac023aaa5f0" -/dev/sdb2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="e7d01e93-9340-5b90-68f8-d8f815595132" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="ab11cd86-37b8-4bae-81e5-21fe0a9c9ae0" -/dev/sdb3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="764217ad-1591-a83a-c799-23397f968729" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="9afbf9c1-b2ba-4b46-8db1-e802d26c93b6" -/dev/md1: LABEL="root" UUID="ace079b5-06be-4429-bbf0-081ea4d7d0d9" TYPE="ext4" -/dev/md0: LABEL="efi" UUID="C236-297F" TYPE="vfat" -/dev/md2: LABEL="varlib" UUID="385e8e8e-dbfd-481e-93a4-cba7f4d5fa02" TYPE="ext4"` - sampleMdadmDetailOutput = `MD_LEVEL=raid1 -MD_DEVICES=2 -MD_METADATA=1.0 -MD_UUID=543eb7f8:98d4d986:e669824d:bebe69e5 -MD_DEVNAME=1 -MD_NAME=any:1 -MD_DEVICE_dev_sdb2_ROLE=1 -MD_DEVICE_dev_sdb2_DEV=/dev/sdb2 -MD_DEVICE_dev_sda2_ROLE=0 -MD_DEVICE_dev_sda2_DEV=/dev/sda2` - sampleMdadmScanOutput = `ARRAY /dev/md/0 metadata=1.0 UUID=42d10089:ee1e0399:445e7550:62b63ec8 name=any:0 -ARRAY /dev/md/1 metadata=1.0 UUID=543eb7f8:98d4d986:e669824d:bebe69e5 name=any:1 -ARRAY /dev/md/2 metadata=1.0 UUID=fc32a6f0:ee40d9db:87c8c9f3:a8400c8b name=any:2` - sampleCloudInit = `#cloud-config -# Add groups to the system -# The following example adds the ubuntu group with members 'root' and 'sys' -# and the empty group cloud-users. -groups: - - admingroup: [root,sys] - - cloud-users` - sampleIgnition = `{"ignition":{"config":{},"security":{"tls":{}},"timeouts":{},"version":"2.2.0"}}` -) - -func mustParseInstallYAML(t *testing.T, fs afero.Fs) *v1.InstallerConfig { - config, err := parseInstallYAML(fs) - require.NoError(t, err) - return config -} - -func Test_installer_detectFirmware(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - execMocks []fakeexecparams - wantErr error - }{ - { - name: "is efi", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/sys/firmware/efi", []byte(""), 0755)) - require.NoError(t, afero.WriteFile(fs, "/sys/class/dmi", []byte(""), 0755)) - }, - wantErr: nil, - }, - { - name: "is not efi", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/sys/class/dmi", []byte(""), 0755)) - }, - wantErr: fmt.Errorf("not running efi mode"), - }, - { - name: "is not efi but virtual", - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - log := slog.Default() - - i := &installer{ - log: log, - fs: afero.NewMemMapFs(), - exec: &cmdexec{ - log: log, - c: fakeCmd(t, tt.execMocks...), - }, - } - - if tt.fsMocks != nil { - tt.fsMocks(i.fs) - } - - err := i.detectFirmware() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_writeResolvConf(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - config *v1.InstallerConfig - want string - wantErr error - }{ - { - name: "resolv.conf gets written", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/resolv.conf", []byte(""), 0755)) - }, - want: `nameserver 8.8.8.8 -nameserver 8.8.4.4 -`, - wantErr: nil, - }, - { - name: "resolv.conf gets written, file is not present", - want: `nameserver 8.8.8.8 -nameserver 8.8.4.4 -`, - wantErr: nil, - }, - { - name: "overwrite resolv.conf with custom DNS", - config: &v1.InstallerConfig{DNSServers: []*models.V1DNSServer{{IP: new("1.2.3.4")}, {IP: new("5.6.7.8")}}}, - want: `nameserver 1.2.3.4 -nameserver 5.6.7.8 -`, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - i := &installer{ - log: slog.Default(), - fs: afero.NewMemMapFs(), - config: &v1.InstallerConfig{}, - } - - if tt.fsMocks != nil { - tt.fsMocks(i.fs) - } - - if tt.config != nil { - i.config = tt.config - } - - err := i.writeResolvConf() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - content, err := afero.ReadFile(i.fs, "/etc/resolv.conf") - require.NoError(t, err) - - if diff := cmp.Diff(tt.want, string(content)); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_writeNTPConf(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - oss operatingsystem - role string - ntpServers []*models.V1NTPServer - ntpPath string - want string - wantErr error - }{ - { - name: "configure custom ntp for ubuntu machine", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644)) - }, - ntpPath: "/etc/systemd/timesyncd.conf", - oss: osUbuntu, - role: "machine", - ntpServers: []*models.V1NTPServer{{Address: new("custom.1.ntp.org")}, {Address: new("custom.2.ntp.org")}}, - want: `[Time] -NTP=custom.1.ntp.org custom.2.ntp.org -`, - wantErr: nil, - }, - { - name: "use default ntp for ubuntu machine", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644)) - }, - ntpPath: "/etc/systemd/timesyncd.conf", - oss: osUbuntu, - role: "machine", - want: "", - wantErr: nil, - }, - { - name: "configure custom ntp for debian machine", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644)) - }, - ntpPath: "/etc/systemd/timesyncd.conf", - oss: osDebian, - role: "machine", - ntpServers: []*models.V1NTPServer{{Address: new("custom.1.ntp.org")}, {Address: new("custom.2.ntp.org")}}, - want: `[Time] -NTP=custom.1.ntp.org custom.2.ntp.org -`, - wantErr: nil, - }, - { - name: "use default ntp for debian machine", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644)) - }, - ntpPath: "/etc/systemd/timesyncd.conf", - oss: osDebian, - role: "machine", - want: "", - wantErr: nil, - }, - { - name: "configure ntp for almalinux machine", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644)) - }, - oss: osAlmalinux, - ntpPath: "/etc/chrony.conf", - role: "machine", - ntpServers: []*models.V1NTPServer{{Address: new("custom.1.ntp.org")}, {Address: new("custom.2.ntp.org")}}, - want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more -# information about usable directives. - -# In case no custom NTP server is provided -# Cloudflare offers a free public time service that allows us to use their -# anycast network of 180+ locations to synchronize time from their closest server. -# See https://blog.cloudflare.com/secure-time/ -pool custom.1.ntp.org iburst -pool custom.2.ntp.org iburst - -# This directive specify the location of the file containing ID/key pairs for -# NTP authentication. -keyfile /etc/chrony/chrony.keys - -# This directive specify the file into which chronyd will store the rate -# information. -driftfile /var/lib/chrony/chrony.drift - -# Uncomment the following line to turn logging on. -#log tracking measurements statistics - -# Log files location. -logdir /var/log/chrony - -# Stop bad estimates upsetting machine clock. -maxupdateskew 100.0 - -# This directive enables kernel synchronisation (every 11 minutes) of the -# real-time clock. Note that it can’t be used along with the 'rtcfile' directive. -rtcsync - -# Step the system clock instead of slewing it if the adjustment is larger than -# one second, but only in the first three clock updates. -makestep 1 3`, - wantErr: nil, - }, - { - name: "use default ntp for almalinux machine", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644)) - }, - oss: osAlmalinux, - ntpPath: "/etc/chrony.conf", - role: "machine", - want: "", - wantErr: nil, - }, - { - name: "configure custom ntp for firewall", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/chrony/chrony.conf", []byte(""), 0644)) - }, - ntpPath: "/etc/chrony/chrony.conf", - role: "firewall", - ntpServers: []*models.V1NTPServer{{Address: new("custom.1.ntp.org")}, {Address: new("custom.2.ntp.org")}}, - want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more -# information about usable directives. - -# In case no custom NTP server is provided -# Cloudflare offers a free public time service that allows us to use their -# anycast network of 180+ locations to synchronize time from their closest server. -# See https://blog.cloudflare.com/secure-time/ -pool custom.1.ntp.org iburst -pool custom.2.ntp.org iburst - -# This directive specify the location of the file containing ID/key pairs for -# NTP authentication. -keyfile /etc/chrony/chrony.keys - -# This directive specify the file into which chronyd will store the rate -# information. -driftfile /var/lib/chrony/chrony.drift - -# Uncomment the following line to turn logging on. -#log tracking measurements statistics - -# Log files location. -logdir /var/log/chrony - -# Stop bad estimates upsetting machine clock. -maxupdateskew 100.0 - -# This directive enables kernel synchronisation (every 11 minutes) of the -# real-time clock. Note that it can’t be used along with the 'rtcfile' directive. -rtcsync - -# Step the system clock instead of slewing it if the adjustment is larger than -# one second, but only in the first three clock updates. -makestep 1 3`, - wantErr: nil, - }, - { - name: "use default ntp for firewall", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/chrony/chrony.conf", []byte(""), 0644)) - }, - ntpPath: "/etc/chrony/chrony.conf", - role: "firewall", - want: "", - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - i := &installer{ - log: slog.Default(), - fs: afero.NewMemMapFs(), - config: &v1.InstallerConfig{Role: tt.role, NTPServers: tt.ntpServers}, - oss: tt.oss, - } - - if tt.fsMocks != nil { - tt.fsMocks(i.fs) - } - - err := i.writeNTPConf() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - content, err := afero.ReadFile(i.fs, tt.ntpPath) - require.NoError(t, err) - - if diff := cmp.Diff(tt.want, string(content)); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_fixPermissions(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - wantErr error - }{ - { - name: "fix permissions", - fsMocks: func(fs afero.Fs) { - require.NoError(t, fs.MkdirAll("/var/tmp", 0000)) - require.NoError(t, afero.WriteFile(fs, "/etc/hosts", []byte("127.0.0.1"), 0000)) - }, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - i := &installer{ - log: slog.Default(), - fs: afero.NewMemMapFs(), - } - - if tt.fsMocks != nil { - tt.fsMocks(i.fs) - } - - err := i.fixPermissions() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - info, err := i.fs.Stat("/var/tmp") - require.NoError(t, err) - assert.Equal(t, fs.FileMode(01777).Perm(), info.Mode().Perm()) - - info, err = i.fs.Stat("/etc/hosts") - require.NoError(t, err) - assert.Equal(t, fs.FileMode(0644).Perm(), info.Mode().Perm()) - }) - } -} - -func Test_installer_findMDUUID(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - execMocks []fakeexecparams - want string - wantFound bool - }{ - { - name: "has mdadm", - execMocks: []fakeexecparams{ - { - WantCmd: []string{"blkid"}, - Output: sampleBlkidOutput, - ExitCode: 0, - }, - { - WantCmd: []string{"mdadm", "--detail", "--export", "/dev/md1"}, - Output: sampleMdadmDetailOutput, - ExitCode: 0, - }, - }, - want: "543eb7f8:98d4d986:e669824d:bebe69e5", - wantFound: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - log := slog.Default() - - i := &installer{ - log: log, - exec: &cmdexec{ - log: log, - c: fakeCmd(t, tt.execMocks...), - }, - fs: fs, - config: &v1.InstallerConfig{RaidEnabled: true, RootUUID: "ace079b5-06be-4429-bbf0-081ea4d7d0d9"}, - } - - uuid, found := i.findMDUUID() - assert.Equal(t, tt.wantFound, found) - if diff := cmp.Diff(tt.want, uuid); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_buildCMDLine(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - execMocks []fakeexecparams - want string - }{ - { - name: "without raid", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/metal/install.yaml", []byte(sampleInstallYAML), 0700)) - }, - execMocks: []fakeexecparams{ - { - WantCmd: []string{"blkid"}, - Output: sampleBlkidOutput, - ExitCode: 0, - }, - { - WantCmd: []string{"mdadm", "--detail", "--export", "/dev/md1"}, - Output: sampleMdadmDetailOutput, - ExitCode: 0, - }, - }, - // CMDLINE="console=${CONSOLE} root=UUID=${ROOT_UUID} init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" - want: "console=ttyS1,115200n8 root=UUID=543eb7f8-98d4-d986-e669-824dbebe69e5 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", - }, - { - name: "with raid", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/metal/install.yaml", []byte(sampleInstallWithRaidYAML), 0700)) - }, - execMocks: []fakeexecparams{ - { - WantCmd: []string{"blkid"}, - Output: sampleBlkidOutput, - ExitCode: 0, - }, - { - WantCmd: []string{"mdadm", "--detail", "--export", "/dev/md1"}, - Output: sampleMdadmDetailOutput, - ExitCode: 0, - }, - }, - // CMDLINE="console=${CONSOLE} root=UUID=${ROOT_UUID} init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" - want: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300 rdloaddriver=raid0 rdloaddriver=raid1 rd.md.uuid=543eb7f8:98d4d986:e669824d:bebe69e5", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - log := slog.Default() - - i := &installer{ - log: log, - exec: &cmdexec{ - log: log, - c: fakeCmd(t, tt.execMocks...), - }, - fs: fs, - config: mustParseInstallYAML(t, fs), - } - - got := i.buildCMDLine() - if diff := cmp.Diff(tt.want, got); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_unsetMachineID(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - wantErr error - }{ - { - name: "unset", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/machine-id", []byte("uuid"), 0700)) - require.NoError(t, afero.WriteFile(fs, "/var/lib/dbus/machine-id", []byte("uuid"), 0700)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - i := &installer{ - log: slog.Default(), - fs: fs, - } - - err := i.unsetMachineID() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - content, err := afero.ReadFile(i.fs, "/etc/machine-id") - require.NoError(t, err) - assert.Empty(t, content) - - content, err = afero.ReadFile(i.fs, "/var/lib/dbus/machine-id") - require.NoError(t, err) - assert.Empty(t, content) - }) - } -} - -func Test_installer_writeBootInfo(t *testing.T) { - tests := []struct { - name string - cmdline string - fsMocks func(fs afero.Fs) - oss operatingsystem - want *v1.Bootinfo - wantErr error - }{ - { - name: "boot-info ubuntu", - cmdline: "a-cmd-line", - oss: osUbuntu, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/boot/System.map-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/vmlinuz-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/initrd.img-1.2.3", nil, 0700)) - }, - want: &v1.Bootinfo{ - Initrd: "/boot/initrd.img-1.2.3", - Cmdline: "a-cmd-line", - Kernel: "/boot/vmlinuz-1.2.3", - BootloaderID: "metal-ubuntu", - }, - }, - { - name: "more than one system.map present", - cmdline: "a-cmd-line", - oss: osUbuntu, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/boot/System.map-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/System.map-1.2.4", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/vmlinuz-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/initrd.img-1.2.3", nil, 0700)) - }, - want: nil, - wantErr: fmt.Errorf("more or less than a single System.map found([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), - }, - { - name: "no system.map present", - cmdline: "a-cmd-line", - oss: osUbuntu, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/boot/vmlinuz-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/initrd.img-1.2.3", nil, 0700)) - }, - want: nil, - wantErr: fmt.Errorf("more or less than a single System.map found([]), probably no kernel or more than one kernel installed"), - }, - { - name: "no vmlinuz present", - cmdline: "a-cmd-line", - oss: osUbuntu, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/boot/System.map-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/initrd.img-1.2.3", nil, 0700)) - }, - want: nil, - wantErr: fmt.Errorf("kernel image \"/boot/vmlinuz-1.2.3\" not found"), - }, - { - name: "no ramdisk present", - cmdline: "a-cmd-line", - oss: osUbuntu, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/boot/System.map-1.2.3", nil, 0700)) - require.NoError(t, afero.WriteFile(fs, "/boot/vmlinuz-1.2.3", nil, 0700)) - }, - want: nil, - wantErr: fmt.Errorf("ramdisk \"/boot/initrd.img-1.2.3\" not found"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - i := &installer{ - log: slog.Default(), - fs: fs, - oss: tt.oss, - } - - err := i.writeBootInfo(tt.cmdline) - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - if tt.want != nil { - content, err := afero.ReadFile(i.fs, "/etc/metal/boot-info.yaml") - require.NoError(t, err) - - var bootInfo v1.Bootinfo - err = yaml.Unmarshal(content, &bootInfo) - require.NoError(t, err) - - if diff := cmp.Diff(tt.want, &bootInfo); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - } - }) - } -} - -func Test_installer_processUserdata(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - execMocks []fakeexecparams - oss operatingsystem - wantErr error - }{ - { - name: "no userdata given", - }, - { - name: "cloud-init", - oss: osDebian, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/metal/userdata", []byte(sampleCloudInit), 0700)) - }, - execMocks: []fakeexecparams{ - { - WantCmd: []string{"cloud-init", "devel", "schema", "--config-file", "/etc/metal/userdata"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"systemctl", "preset-all"}, - Output: "", - ExitCode: 0, - }, - }, - }, - { - name: "ignition", - oss: osDebian, - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/metal/userdata", []byte(sampleIgnition), 0700)) - }, - execMocks: []fakeexecparams{ - { - WantCmd: []string{"ignition", "-oem", "file", "-stage", "files", "-log-to-stdout"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"systemctl", "preset-all"}, - Output: "", - ExitCode: 0, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - log := slog.Default() - - i := &installer{ - log: log, - exec: &cmdexec{ - log: log, - c: fakeCmd(t, tt.execMocks...), - }, - fs: fs, - oss: tt.oss, - } - - err := i.processUserdata() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_grubInstall(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - cmdline string - execMocks []fakeexecparams - oss operatingsystem - wantGrubCfg string - wantErr error - }{ - { - name: "without raid debian/ubuntu", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/metal/install.yaml", []byte(sampleInstallYAML), 0700)) - }, - cmdline: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", - oss: osUbuntu, - execMocks: []fakeexecparams{ - { - WantCmd: []string{"grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--boot-directory=/boot", "--bootloader-id=metal-ubuntu", "--removable"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"update-grub2"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"dpkg-reconfigure", "grub-efi-amd64-bin"}, - Output: "", - ExitCode: 0, - }, - }, - wantGrubCfg: `GRUB_DEFAULT=0 -GRUB_TIMEOUT=5 -GRUB_DISTRIBUTOR=metal-ubuntu -GRUB_CMDLINE_LINUX_DEFAULT="" -GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" -GRUB_TERMINAL=serial -GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" -`, - }, - { - name: "with raid debian/ubuntu", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/metal/install.yaml", []byte(sampleInstallWithRaidYAML), 0700)) - }, - cmdline: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", - oss: osUbuntu, - execMocks: []fakeexecparams{ - { - WantCmd: []string{"mdadm", "--examine", "--scan"}, - Output: sampleMdadmScanOutput, - ExitCode: 0, - }, - { - WantCmd: []string{"update-initramfs", "-u"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"blkid"}, - Output: sampleBlkidOutput, - ExitCode: 0, - }, - { - WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sda1", "-p1", "-l", "\\\\EFI\\\\metal-ubuntu\\\\grubx64.efi", "-L", "metal-ubuntu"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sdb1", "-p1", "-l", "\\\\EFI\\\\metal-ubuntu\\\\grubx64.efi", "-L", "metal-ubuntu"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--boot-directory=/boot", "--bootloader-id=metal-ubuntu", "--no-nvram", "--removable"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"update-grub2"}, - Output: "", - ExitCode: 0, - }, - { - WantCmd: []string{"dpkg-reconfigure", "grub-efi-amd64-bin"}, - Output: "", - ExitCode: 0, - }, - }, - wantGrubCfg: `GRUB_DEFAULT=0 -GRUB_TIMEOUT=5 -GRUB_DISTRIBUTOR=metal-ubuntu -GRUB_CMDLINE_LINUX_DEFAULT="" -GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" -GRUB_TERMINAL=serial -GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - log := slog.Default() - - i := &installer{ - log: log, - exec: &cmdexec{ - log: log, - c: fakeCmd(t, tt.execMocks...), - }, - fs: fs, - oss: tt.oss, - config: mustParseInstallYAML(t, fs), - } - - err := i.grubInstall(tt.cmdline) - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - content, err := afero.ReadFile(i.fs, "/etc/default/grub") - require.NoError(t, err) - - if diff := cmp.Diff(tt.wantGrubCfg, string(content)); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} - -func Test_installer_writeBuildMeta(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - execMocks []fakeexecparams - want string - wantErr error - }{ - { - name: "build meta gets written", - execMocks: []fakeexecparams{ - { - WantCmd: []string{"ignition", "-version"}, - Output: "Ignition v0.36.2", - ExitCode: 0, - }, - }, - want: `--- -buildVersion: "456" -buildDate: "" -buildSHA: abc -buildRevision: revision -ignitionVersion: Ignition v0.36.2 -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - log := slog.Default() - - i := &installer{ - log: slog.Default(), - fs: fs, - exec: &cmdexec{ - log: log, - c: fakeCmd(t, tt.execMocks...), - }, - } - - v.Version = "456" - v.GitSHA1 = "abc" - v.Revision = "revision" - - err := i.writeBuildMeta() - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - - content, err := afero.ReadFile(i.fs, "/etc/metal/build-meta.yaml") - require.NoError(t, err) - assert.Equal(t, tt.want, string(content)) - }) - } -} - -func errorStringComparer() cmp.Option { - return cmp.Comparer(func(x, y error) bool { - if x == nil && y == nil { - return true - } - if x == nil && y != nil { - return false - } - if x != nil && y == nil { - return false - } - return x.Error() == y.Error() - }) -} diff --git a/main.go b/main.go deleted file mode 100644 index 7fad8a8..0000000 --- a/main.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "log/slog" - "os" - "os/exec" - "time" - - v1 "github.com/metal-stack/os-installer/api/v1" - "github.com/metal-stack/v" - "github.com/spf13/afero" - "gopkg.in/yaml.v3" -) - -func main() { - start := time.Now() - jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}) - log := slog.New(jsonHandler) - - log.Info("running install", "version", v.V.String()) - - fs := afero.OsFs{} - - oss, err := detectOS(fs) - if err != nil { - log.Error("installation failed", "error", err) - os.Exit(1) - } - - config, err := parseInstallYAML(fs) - if err != nil { - log.Error("installation failed", "error", err) - os.Exit(1) - } - - i := installer{ - log: log.WithGroup("os-installer"), - fs: fs, - oss: oss, - config: config, - exec: &cmdexec{ - log: log.WithGroup("cmdexec"), - c: exec.CommandContext, - }, - } - - err = i.do() - if err != nil { - i.log.Error("installation failed", "error", err, "duration", time.Since(start).String()) - os.Exit(1) - } - - i.log.Info("installation succeeded", "duration", time.Since(start).String()) -} - -func parseInstallYAML(fs afero.Fs) (*v1.InstallerConfig, error) { - var config v1.InstallerConfig - content, err := afero.ReadFile(fs, installYAML) - if err != nil { - return nil, err - } - err = yaml.Unmarshal(content, &config) - if err != nil { - return nil, err - } - return &config, nil -} diff --git a/os.go b/os.go deleted file mode 100644 index 45d35bf..0000000 --- a/os.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "strings" - - "github.com/spf13/afero" -) - -type operatingsystem string - -const ( - osUbuntu = operatingsystem("ubuntu") - osDebian = operatingsystem("debian") - osAlmalinux = operatingsystem("almalinux") -) - -func (o operatingsystem) BootloaderID() string { - switch o { - case osAlmalinux: - return string(o) - case osDebian, osUbuntu: - return fmt.Sprintf("metal-%s", o) - default: - return fmt.Sprintf("metal-%s", o) - } -} - -func (o operatingsystem) SudoGroup() string { - switch o { - case osAlmalinux: - return "wheel" - case osDebian, osUbuntu: - return "sudo" - default: - return "sudo" - } -} - -func (o operatingsystem) Initramdisk(kernversion string) string { - switch o { - case osAlmalinux: - return fmt.Sprintf("initramfs-%s.img", kernversion) - case osDebian, osUbuntu: - return fmt.Sprintf("initrd.img-%s", kernversion) - default: - return fmt.Sprintf("initrd.img-%s", kernversion) - } -} -func (o operatingsystem) NeedUpdateInitRamfs() bool { - switch o { - case osAlmalinux: - return false - case osDebian, osUbuntu: - return true - default: - return true - } -} - -func (o operatingsystem) GrubInstallCmd() string { - switch o { - case osAlmalinux: - return "" // no execution required - case osDebian, osUbuntu: - return "grub-install" - default: - return "grub-install" - } -} - -func operatingSystemFromString(s string) (operatingsystem, error) { - unquoted, err := strconv.Unquote(s) - if err == nil { - s = unquoted - } - - switch operatingsystem(strings.ToLower(s)) { - case osUbuntu: - return osUbuntu, nil - case osDebian: - return osDebian, nil - case osAlmalinux: - return osAlmalinux, nil - default: - return operatingsystem(""), fmt.Errorf("unsupported operating system: %s", s) - } -} - -func detectOS(fs afero.Fs) (operatingsystem, error) { - content, err := afero.ReadFile(fs, "/etc/os-release") - if err != nil { - return operatingsystem(""), err - } - - env := map[string]string{} - for line := range strings.SplitSeq(string(content), "\n") { - k, v, found := strings.Cut(line, "=") - if found { - env[k] = v - } - } - - if os, ok := env["ID"]; ok { - oss, err := operatingSystemFromString(os) - if err != nil { - return operatingsystem(""), err - } - return oss, nil - } - - return operatingsystem(""), fmt.Errorf("unable to detect OS") -} diff --git a/os_test.go b/os_test.go deleted file mode 100644 index feb9f11..0000000 --- a/os_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/spf13/afero" - "github.com/stretchr/testify/require" -) - -func Test_detectOS(t *testing.T) { - tests := []struct { - name string - fsMocks func(fs afero.Fs) - want operatingsystem - wantErr error - }{ - { - name: "ubuntu 22.04 os", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/os-release", []byte(`PRETTY_NAME="Ubuntu 22.04.1 LTS" -NAME="Ubuntu" -VERSION_ID="22.04" -VERSION="22.04.1 LTS (Jammy Jellyfish)" -VERSION_CODENAME=jammy -ID=ubuntu -ID_LIKE=debian -HOME_URL="https://www.ubuntu.com/" -SUPPORT_URL="https://help.ubuntu.com/" -BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" -PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" -UBUNTU_CODENAME=jammy`), 0755)) - }, - want: osUbuntu, - wantErr: nil, - }, - { - name: "almalinux 9", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/os-release", []byte(`NAME="AlmaLinux" -VERSION="9.4 (Seafoam Ocelot)" -ID="almalinux" -ID_LIKE="rhel centos fedora" -VERSION_ID="9.4" -PLATFORM_ID="platform:el9" -PRETTY_NAME="AlmaLinux 9.4 (Seafoam Ocelot)" -ANSI_COLOR="0;34" -LOGO="fedora-logo-icon" -CPE_NAME="cpe:/o:almalinux:almalinux:9::baseos" -HOME_URL="https://almalinux.org/" -DOCUMENTATION_URL="https://wiki.almalinux.org/" -BUG_REPORT_URL="https://bugs.almalinux.org/" - -ALMALINUX_MANTISBT_PROJECT="AlmaLinux-9" -ALMALINUX_MANTISBT_PROJECT_VERSION="9.4" -REDHAT_SUPPORT_PRODUCT="AlmaLinux" -REDHAT_SUPPORT_PRODUCT_VERSION="9.4" -SUPPORT_END=2032-06-01 -`), 0755)) - }, - want: osAlmalinux, - wantErr: nil, - }, - { - name: "debian 10", - fsMocks: func(fs afero.Fs) { - require.NoError(t, afero.WriteFile(fs, "/etc/os-release", []byte(`PRETTY_NAME="Debian GNU/Linux 10 (buster)" -NAME="Debian GNU/Linux" -VERSION_ID="10" -VERSION="10 (buster)" -VERSION_CODENAME=buster -ID=debian -HOME_URL="https://www.debian.org/" -SUPPORT_URL="https://www.debian.org/support" -BUG_REPORT_URL="https://bugs.debian.org/"`), 0755)) - }, - want: osDebian, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - - if tt.fsMocks != nil { - tt.fsMocks(fs) - } - - oss, err := detectOS(fs) - if diff := cmp.Diff(tt.wantErr, err, errorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - if diff := cmp.Diff(tt.want, oss); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) - } - }) - } -} diff --git a/pkg/exec/cmdexec.go b/pkg/exec/cmdexec.go new file mode 100644 index 0000000..2b8516f --- /dev/null +++ b/pkg/exec/cmdexec.go @@ -0,0 +1,101 @@ +package exec + +import ( + "context" + "io" + "log/slog" + "os" + "os/exec" + "strings" + "time" +) + +const ( + defaultExecDir = "/etc/metal" +) + +type CmdExecutor struct { + log *slog.Logger + c func(ctx context.Context, name string, arg ...string) *exec.Cmd +} + +type Params struct { + Name string + Args []string + Dir string + Timeout time.Duration + Combined bool + Stdin string + Env []string +} + +func New(log *slog.Logger) *CmdExecutor { + return &CmdExecutor{ + log: log, + c: exec.CommandContext, + } +} + +func (i *CmdExecutor) WithCommandFn(c func(ctx context.Context, name string, arg ...string) *exec.Cmd) *CmdExecutor { + i.c = c + return i +} + +func (i *CmdExecutor) Execute(ctx context.Context, p *Params) (out string, err error) { + var ( + start = time.Now() + output []byte + ) + i.log.Info("running command", "command", strings.Join(append([]string{p.Name}, p.Args...), " "), "start", start.String()) + + if p.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, p.Timeout) + defer cancel() + } + + cmd := i.c(ctx, p.Name, p.Args...) + if p.Dir != "" { + cmd.Dir = defaultExecDir + } + + cmd.Env = append(cmd.Env, p.Env...) + + // show stderr + cmd.Stderr = os.Stderr + + if p.Stdin != "" { + stdin, err := cmd.StdinPipe() + if err != nil { + return "", err + } + + go func() { + defer func() { + _ = stdin.Close() + }() + _, err = io.WriteString(stdin, p.Stdin) + if err != nil { + i.log.Error("error when writing to command's stdin", "error", err) + } + }() + } + + if p.Combined { + output, err = cmd.CombinedOutput() + } else { + output, err = cmd.Output() + } + + out = string(output) + took := time.Since(start) + + if err != nil { + i.log.Error("executed command with error", "output", out, "duration", took.String(), "error", err) + return "", err + } + + i.log.Info("executed command", "output", out, "duration", took.String()) + + return +} diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go new file mode 100644 index 0000000..7f3cc90 --- /dev/null +++ b/pkg/installer/installer.go @@ -0,0 +1,190 @@ +package os + +import ( + "context" + "fmt" + "log/slog" + "os" + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" + operatingsystem "github.com/metal-stack/os-installer/pkg/installer/os" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/spf13/afero" +) + +type installer struct { + log *slog.Logger + oss oscommon.OperatingSystem + fs *afero.Afero +} + +func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, allocation *apiv2.MachineAllocation) error { + log = log.WithGroup("os-installer") + + var ( + start = time.Now() + fs = &afero.Afero{ + Fs: afero.OsFs{}, + } + ) + + oss, err := operatingsystem.New(&oscommon.Config{ + Log: log, + Fs: fs, + MachineDetails: details, + Allocation: allocation, + }) + if err != nil { + return fmt.Errorf("os detection failed: %w", err) + } + + i := installer{ + log: log, + oss: oss, + fs: fs, + } + + if err = i.run(ctx); err != nil { + i.log.Info("running os installer failed", "took", time.Since(start).String()) + return fmt.Errorf("os installer failed: %w", err) + } + + i.log.Info("os installer succeeded", "took", time.Since(start).String()) + + return nil +} + +func (i *installer) run(ctx context.Context) error { + var ( + cmdLine string + ) + + for _, task := range []struct { + name string + fn func(ctx context.Context) error + }{ + { + name: "check if running in efi mode", + fn: i.validateRunningInEfiMode, + }, + { + name: "remove .dockerenv if running in virtual environment", + fn: i.removeDockerEnv, + }, + { + name: "write hostname", + fn: i.oss.WriteHostname, + }, + { + name: "write /etc/hosts", + fn: i.oss.WriteHosts, + }, + { + name: "write /etc/resolv.conf", + fn: i.oss.WriteResolvConf, + }, + { + name: "write ntp configuration", + fn: i.oss.WriteNTPConf, + }, + { + name: "create metal user", + fn: i.oss.CreateMetalUser, + }, + { + name: "configure network", + fn: i.oss.ConfigureNetwork, + }, + { + name: "authorized ssh keys", + fn: i.oss.CopySSHKeys, + }, + { + name: "fix wrong filesystem permissions", + fn: i.oss.FixPermissions, + }, + { + name: "build kernel cmdline", + fn: func(ctx context.Context) error { + l, err := i.oss.BuildCMDLine(ctx) + if err != nil { + return err + } + + cmdLine = l + + return nil + }, + }, + { + name: "write /etc/metal/boot-info.yaml", + fn: func(ctx context.Context) error { + return i.oss.WriteBootInfo(ctx, cmdLine) + }, + }, + { + name: "write booatloader config", + fn: func(ctx context.Context) error { + return i.oss.GrubInstall(ctx, cmdLine) + }, + }, + { + name: "unset machine id", + fn: i.oss.UnsetMachineID, + }, + { + name: "deploy systemd services", + fn: i.oss.SystemdServices, + }, + { + name: "write /etc/metal/build-meta.yaml", + fn: i.oss.WriteBuildMeta, + }, + } { + var ( + log = i.log.With("task-name", task.name) + start = time.Now() + ) + + log.Info("running install task", "start-at", start.String()) + + if err := task.fn(ctx); err != nil { + i.log.Info("running install task failed", "took", time.Since(start).String()) + return fmt.Errorf("installation task failed, aborting install: %w", err) + } + } + + return nil +} + +func (i *installer) validateRunningInEfiMode(ctx context.Context) error { + if !i.isVirtual() && !i.fileExists("/sys/firmware/efi") { + return fmt.Errorf("not running efi mode") + } + + return nil +} + +func (i *installer) removeDockerEnv(_ context.Context) error { + // systemd-detect-virt guesses docker which modifies the behavior of many services. + if !i.fileExists("/.dockerenv") { + return nil + } + + return i.fs.Remove("/.dockerenv") +} + +func (i *installer) isVirtual() bool { + return !i.fileExists("/sys/class/dmi") +} + +func (i *installer) fileExists(filename string) bool { + info, err := i.fs.Stat(filename) + if os.IsNotExist(err) { + return false + } + + return !info.IsDir() +} diff --git a/pkg/installer/installer_test.go b/pkg/installer/installer_test.go new file mode 100644 index 0000000..af013e7 --- /dev/null +++ b/pkg/installer/installer_test.go @@ -0,0 +1,59 @@ +package os + +import ( + "fmt" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func Test_installer_validateRunningInEfiMode(t *testing.T) { + tests := []struct { + name string + fsMocks func(fs *afero.Afero) + wantErr error + }{ + { + name: "is efi", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/sys/firmware/efi", []byte(""), 0755)) + require.NoError(t, fs.WriteFile("/sys/class/dmi", []byte(""), 0755)) + }, + wantErr: nil, + }, + { + name: "is not efi", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/sys/class/dmi", []byte(""), 0755)) + }, + wantErr: fmt.Errorf("not running efi mode"), + }, + { + name: "is not efi but virtual", + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &installer{ + log: slog.Default(), + fs: &afero.Afero{ + Fs: afero.NewMemMapFs(), + }, + } + + if tt.fsMocks != nil { + tt.fsMocks(i.fs) + } + + err := i.validateRunningInEfiMode(t.Context()) + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n %s", diff) + } + }) + } +} diff --git a/pkg/installer/os/almalinux/almalinux.go b/pkg/installer/os/almalinux/almalinux.go new file mode 100644 index 0000000..8f178b7 --- /dev/null +++ b/pkg/installer/os/almalinux/almalinux.go @@ -0,0 +1,53 @@ +package almalinux + +import ( + "context" + "log/slog" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/spf13/afero" +) + +type ( + os struct { + *oscommon.DefaultOS + log *slog.Logger + details *v1.MachineDetails + allocation *apiv2.MachineAllocation + exec *exec.CmdExecutor + network *network.Network + fs *afero.Afero + } +) + +func New(cfg *oscommon.Config) *os { + return &os{ + DefaultOS: oscommon.New(cfg), + log: cfg.Log, + details: cfg.MachineDetails, + allocation: cfg.Allocation, + exec: cfg.Exec, + network: network.New(cfg.Allocation), + fs: cfg.Fs, + } +} + +func (o *os) SudoGroup() string { + return "wheel" +} + +func (o *os) BootloaderID() string { + return "almalinux" +} + +func (o *os) InitramdiskFormatString() string { + return "initramfs-%s.img" +} + +func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { + return o.DefaultOS.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) +} diff --git a/pkg/installer/os/almalinux/create_metal_user.go b/pkg/installer/os/almalinux/create_metal_user.go new file mode 100644 index 0000000..d13b53d --- /dev/null +++ b/pkg/installer/os/almalinux/create_metal_user.go @@ -0,0 +1,28 @@ +package almalinux + +import ( + "context" + "time" + + "github.com/metal-stack/os-installer/pkg/exec" +) + +func (o *os) CreateMetalUser(ctx context.Context) error { + err := o.DefaultOS.CreateMetalUser(ctx, o.SudoGroup()) + if err != nil { + return err + } + + // otherwise in rescue mode the root account is locked + _, err = o.exec.Execute(ctx, &exec.Params{ + Name: "passwd", + Args: []string{"root"}, + Timeout: 10 * time.Second, + Stdin: o.details.Password + "\n" + o.details.Password + "\n", + }) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/installer/os/almalinux/install_bootloader.go b/pkg/installer/os/almalinux/install_bootloader.go new file mode 100644 index 0000000..11724ba --- /dev/null +++ b/pkg/installer/os/almalinux/install_bootloader.go @@ -0,0 +1,132 @@ +package almalinux + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/metal-stack/os-installer/pkg/exec" +) + +const ( + defaultGrubPath = "/etc/default/grub" + defaultGrubFileContent = `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=%s +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="%s" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=%s --unit=%s --word=8" +GRUB_DEVICE=UUID=%s +GRUB_ENABLE_BLSCFG=false +` + grubConfigPath = "/boot/efi/EFI/almalinux/grub.cfg" +) + +func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { + serialSpeed, serialPort, err := o.FigureOutSerialSpeed() + if err != nil { + return err + } + + defaultGrub := fmt.Sprintf(defaultGrubFileContent, o.BootloaderID(), cmdLine, serialSpeed, serialPort, o.details.RootUUID) + + err = o.fs.WriteFile(defaultGrubPath, []byte(defaultGrub), 0755) + if err != nil { + return err + } + + grubInstallArgs := []string{ + "--target=x86_64-efi", + "--efi-directory=/boot/efi", + "--boot-directory=/boot", + "--bootloader-id=" + o.BootloaderID(), + } + if o.details.RaidEnabled { + grubInstallArgs = append(grubInstallArgs, "--no-nvram") + } + + _, err = o.exec.Execute(ctx, &exec.Params{ + Name: "grub2-mkconfig", + Args: []string{"-o", grubConfigPath}, + }) + if err != nil { + return err + } + + grubInstallArgs = append(grubInstallArgs, fmt.Sprintf("UUID=%s", o.details.RootUUID)) + + if o.details.RaidEnabled { + out, err := o.exec.Execute(ctx, &exec.Params{ + Name: "mdadm", + Args: []string{"--examine", "--scan"}, + Timeout: 10 * time.Second, + }) + if err != nil { + return err + } + + out += "\nMAILADDR root\n" + + err = o.fs.WriteFile("/etc/mdadm.conf", []byte(out), 0700) + if err != nil { + return err + } + + out, err = o.exec.Execute(ctx, &exec.Params{ + Name: "blkid", + }) + if err != nil { + return err + } + + for line := range strings.SplitSeq(string(out), "\n") { + if strings.Contains(line, `PARTLABEL="efi"`) { + disk, _, found := strings.Cut(line, ":") + if !found { + return fmt.Errorf("unable to process blkid output lines") + } + + shim := fmt.Sprintf(`\\EFI\\%s\\shimx64.efi`, o.BootloaderID()) + + _, err = o.exec.Execute(ctx, &exec.Params{ + Name: "efibootmgr", + Args: []string{"-c", "-d", disk, "-p1", "-l", shim, "-L", o.BootloaderID()}, + }) + if err != nil { + return err + } + } + } + } + + if !o.details.RaidEnabled { + return nil + } + + v, err := o.DefaultOS.GetKernelVersion(o.InitramdiskFormatString()) + if err != nil { + return err + } + + _, err = o.exec.Execute(ctx, &exec.Params{ + Name: "dracut", + Args: []string{ + "--mdadmconf", + "--kver", v, + "--kmoddir", "/lib/modules/" + v, + "--include", "/lib/modules/" + v, "/lib/modules/" + v, + "--fstab", + "--add=dm mdraid", + "--add-drivers=raid0 raid1", + "--hostonly", + "--force", + }, + }) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/installer/os/almalinux/write_ntp_conf.go b/pkg/installer/os/almalinux/write_ntp_conf.go new file mode 100644 index 0000000..95332f4 --- /dev/null +++ b/pkg/installer/os/almalinux/write_ntp_conf.go @@ -0,0 +1,30 @@ +package almalinux_test + +import ( + "context" + "fmt" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" +) + +const ( + chronyConfigPath = "/etc/chrony.conf" +) + +func (o *os) WriteNTPConf(ctx context.Context) error { + if len(o.allocation.NtpServer) == 0 { + return nil + } + + var ntpServers []string + + for _, ntp := range o.allocation.NtpServer { + ntpServers = append(ntpServers, ntp.Address) + } + + if o.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + return fmt.Errorf("almalinux as firewall is currently not supported") + } + + return o.DefaultOS.WriteNtpConfToPath(chronyConfigPath, ntpServers) +} diff --git a/pkg/installer/os/common/cmd_line.go b/pkg/installer/os/common/cmd_line.go new file mode 100644 index 0000000..159ed4d --- /dev/null +++ b/pkg/installer/os/common/cmd_line.go @@ -0,0 +1,99 @@ +package oscommon + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/metal-stack/os-installer/pkg/exec" +) + +func (d *DefaultOS) BuildCMDLine(ctx context.Context) (string, error) { + parts := []string{ + fmt.Sprintf("console=%s", d.details.Console), + fmt.Sprintf("root=UUID=%s", d.details.RootUUID), + "init=/sbin/init", + "net.ifnames=0", + "biosdevname=0", + "nvme_core.io_timeout=300", // 300 sec should be enough for firewalls to be replaced + } + + mdUUID, found, err := d.findMDUUID(ctx) + if err != nil { + return "", err + } + + if found { + mdParts := []string{ + "rdloaddriver=raid0", + "rdloaddriver=raid1", + fmt.Sprintf("rd.md.uuid=%s", mdUUID), + } + + parts = append(parts, mdParts...) + } + + return strings.Join(parts, " "), nil +} + +func (d *DefaultOS) findMDUUID(ctx context.Context) (mdUUID string, found bool, err error) { + d.log.Info("detect software raid uuid") + + if !d.details.RaidEnabled { + return "", false, nil + } + + blkidOut, err := d.exec.Execute(ctx, &exec.Params{ + Name: "blkid", + Timeout: 10 * time.Second, + }) + if err != nil { + return "", false, fmt.Errorf("unable to run blkid: %w", err) + } + + if d.details.RootUUID == "" { + return "", false, fmt.Errorf("no root uuid set in machine details") + } + + var ( + rootUUID = d.details.RootUUID + rootDisk string + ) + + for line := range strings.SplitSeq(string(blkidOut), "\n") { + if strings.Contains(line, rootUUID) { + rd, _, found := strings.Cut(line, ":") + if found { + rootDisk = strings.TrimSpace(rd) + break + } + } + } + if rootDisk == "" { + return "", false, fmt.Errorf("unable to detect rootdisk") + } + + mdadmOut, err := d.exec.Execute(ctx, &exec.Params{ + Name: "mdadm", + Args: []string{"--detail", "--export", rootDisk}, + Timeout: 10 * time.Second, + }) + if err != nil { + return "", false, fmt.Errorf("unable to run mdadm: %w", err) + } + + for line := range strings.SplitSeq(string(mdadmOut), "\n") { + _, md, found := strings.Cut(line, "MD_UUID=") + if found { + mdUUID = md + break + } + } + + if mdUUID == "" { + return "", false, fmt.Errorf("unable to detect md root disk") + } + + return mdUUID, true, nil +} diff --git a/pkg/installer/os/common/cmd_line_test.go b/pkg/installer/os/common/cmd_line_test.go new file mode 100644 index 0000000..5d3d30b --- /dev/null +++ b/pkg/installer/os/common/cmd_line_test.go @@ -0,0 +1,108 @@ +package oscommon + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" +) + +const ( + sampleBlkidOutput = `/dev/sda1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="cc57c456-0b2f-6345-c597-d861cc6dd8ac" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="273985c8-d097-4123-bcd0-80b4e4e14728" +/dev/sda2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="54748c60-b566-f391-142c-fb78bb1fc6a9" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="d7863f4e-af7c-47fc-8c03-6ecdc69bc72d" +/dev/sda3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="582e9b4f-f191-e01e-85fd-2f7d969fbef6" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="e8b44f09-b7f7-4e0d-a7c3-d909617d1f05" +/dev/sdb1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="61bd5d8b-1bb8-673b-9e61-8c28dccc3812" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="13a4c568-57b0-4259-9927-9ac023aaa5f0" +/dev/sdb2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="e7d01e93-9340-5b90-68f8-d8f815595132" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="ab11cd86-37b8-4bae-81e5-21fe0a9c9ae0" +/dev/sdb3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="764217ad-1591-a83a-c799-23397f968729" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="9afbf9c1-b2ba-4b46-8db1-e802d26c93b6" +/dev/md1: LABEL="root" UUID="ace079b5-06be-4429-bbf0-081ea4d7d0d9" TYPE="ext4" +/dev/md0: LABEL="efi" UUID="C236-297F" TYPE="vfat" +/dev/md2: LABEL="varlib" UUID="385e8e8e-dbfd-481e-93a4-cba7f4d5fa02" TYPE="ext4"` + sampleMdadmDetailOutput = `MD_LEVEL=raid1 +MD_DEVICES=2 +MD_METADATA=1.0 +MD_UUID=543eb7f8:98d4d986:e669824d:bebe69e5 +MD_DEVNAME=1 +MD_NAME=any:1 +MD_DEVICE_dev_sdb2_ROLE=1 +MD_DEVICE_dev_sdb2_DEV=/dev/sdb2 +MD_DEVICE_dev_sda2_ROLE=0 +MD_DEVICE_dev_sda2_DEV=/dev/sda2` +) + +func TestDefaultOS_findMDUUID(t *testing.T) { + tests := []struct { + name string + details *v1.MachineDetails + execMocks []test.FakeExecParams + want string + wantFound bool + wantErr error + }{ + { + name: "no raid", + details: &v1.MachineDetails{ + RaidEnabled: false, + }, + want: "", + wantFound: false, + wantErr: nil, + }, + { + name: "with raid", + details: &v1.MachineDetails{ + RootUUID: "ace079b5-06be-4429-bbf0-081ea4d7d0d9", + RaidEnabled: true, + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"blkid"}, + Output: sampleBlkidOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"mdadm", "--detail", "--export", "/dev/md1"}, + Output: sampleMdadmDetailOutput, + ExitCode: 0, + }, + }, + want: "543eb7f8:98d4d986:e669824d:bebe69e5", + wantFound: true, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := slog.Default() + + d := New(&Config{ + Log: log, + Fs: &afero.Afero{ + Fs: afero.NewMemMapFs(), + }, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + }) + + got, gotFound, gotErr := d.findMDUUID(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } + + if diff := cmp.Diff(tt.wantFound, gotFound); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } + }) + } +} diff --git a/pkg/installer/os/common/configure_network.go b/pkg/installer/os/common/configure_network.go new file mode 100644 index 0000000..05654b8 --- /dev/null +++ b/pkg/installer/os/common/configure_network.go @@ -0,0 +1,45 @@ +package oscommon + +import ( + "context" + "fmt" + + "github.com/metal-stack/os-installer/pkg/frr" + "github.com/metal-stack/os-installer/pkg/interfaces" + "github.com/metal-stack/os-installer/pkg/nftables" +) + +func (d *DefaultOS) ConfigureNetwork(ctx context.Context) error { + if err := interfaces.ConfigureInterfaces(ctx, &interfaces.Config{ + Log: d.log, + Network: d.network, + Nics: d.details.Nics, + }); err != nil { + return fmt.Errorf("error configuring interfaces: %w", err) + } + + if _, err := frr.Render(ctx, &frr.Config{ + Log: d.log, + Reload: false, + Validate: true, + Network: d.network, + }); err != nil { + return fmt.Errorf("unable to render frr config: %w", err) + } + + if d.network.IsMachine() { + return nil + } + + if _, err := nftables.Render(ctx, &nftables.Config{ + Log: d.log, + Reload: false, + Network: d.network, + EnableDNSProxy: false, + ForwardPolicy: nftables.ForwardPolicyDrop, + }); err != nil { + return fmt.Errorf("unable to render nftables config: %w", err) + } + + return nil +} diff --git a/pkg/installer/os/common/copy_ssh_keys.go b/pkg/installer/os/common/copy_ssh_keys.go new file mode 100644 index 0000000..0c8644f --- /dev/null +++ b/pkg/installer/os/common/copy_ssh_keys.go @@ -0,0 +1,52 @@ +package oscommon + +import ( + "context" + "os/user" + "path" + "strconv" + "strings" +) + +func (d *DefaultOS) CopySSHKeys(ctx context.Context) error { + var ( + sshPath = path.Join("/home", metalUser, ".ssh") + sshAuthorizedKeysPath = path.Join(sshPath, "authorized_keys") + ) + + err := d.fs.MkdirAll(sshPath, 0700) + if err != nil { + return err + } + + u, err := user.Lookup(metalUser) + if err != nil { + return err + } + + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return err + } + gid, err := strconv.Atoi(u.Gid) + if err != nil { + return err + } + + err = d.fs.Chown(sshPath, uid, gid) + if err != nil { + return err + } + + var lines []string + for _, key := range d.allocation.SshPublicKeys { + lines = append(lines, key) + } + + err = d.fs.WriteFile(sshAuthorizedKeysPath, []byte(strings.Join(lines, "\n")), 0600) + if err != nil { + return err + } + + return d.fs.Chown(sshAuthorizedKeysPath, uid, gid) +} diff --git a/pkg/installer/os/common/create_metal_user.go b/pkg/installer/os/common/create_metal_user.go new file mode 100644 index 0000000..5f94217 --- /dev/null +++ b/pkg/installer/os/common/create_metal_user.go @@ -0,0 +1,57 @@ +package oscommon + +import ( + "context" + "fmt" + "os/user" + "time" + + "github.com/metal-stack/os-installer/pkg/exec" +) + +const ( + metalUser = "metal" +) + +func (d *DefaultOS) CreateMetalUser(ctx context.Context, sudoGroup string) error { + u, err := user.Lookup(metalUser) + if err != nil { + if err.Error() != user.UnknownUserError(metalUser).Error() { + return err + } + } + + if u != nil { + d.log.Info("user already exists, recreating") + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "userdel", + Args: []string{metalUser}, + Timeout: 10 * time.Second, + }) + if err != nil { + return err + } + } + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "useradd", + Args: []string{"--create-home", "--uid", "1000", "--gid", sudoGroup, "--shell", "/bin/bash", metalUser}, + Timeout: 10 * time.Second, + }) + if err != nil { + return err + } + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "passwd", + Args: []string{metalUser}, + Timeout: 10 * time.Second, + Stdin: d.details.Password + "\n" + d.details.Password + "\n", + }) + if err != nil { + return fmt.Errorf("unable to set password for metal user: %w", err) + } + + return nil +} diff --git a/pkg/installer/os/common/fix_permissions.go b/pkg/installer/os/common/fix_permissions.go new file mode 100644 index 0000000..f6f738b --- /dev/null +++ b/pkg/installer/os/common/fix_permissions.go @@ -0,0 +1,19 @@ +package oscommon + +import ( + "context" + "io/fs" +) + +func (d *DefaultOS) FixPermissions(ctx context.Context) error { + for p, perm := range map[string]fs.FileMode{ + "/var/tmp": 01777, + } { + err := d.fs.Chmod(p, perm) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/installer/os/common/install_bootloader.go b/pkg/installer/os/common/install_bootloader.go new file mode 100644 index 0000000..7d24b71 --- /dev/null +++ b/pkg/installer/os/common/install_bootloader.go @@ -0,0 +1,155 @@ +package oscommon + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/spf13/afero" +) + +const ( + DefaultGrubPath = "/etc/default/grub" + defaultGrubFileContent = `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=%s +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="%s" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=%s --unit=%s --word=8" +` +) + +func (d *DefaultOS) GrubInstall(ctx context.Context, bootloaderID, cmdLine string) error { + serialPort, serialSpeed, err := d.FigureOutSerialSpeed() + if err != nil { + return err + } + + defaultGrub := fmt.Sprintf(defaultGrubFileContent, bootloaderID, cmdLine, serialSpeed, serialPort) + + err = d.fs.WriteFile(DefaultGrubPath, []byte(defaultGrub), 0755) + if err != nil { + return err + } + + grubInstallArgs := []string{ + "--target=x86_64-efi", + "--efi-directory=/boot/efi", + "--boot-directory=/boot", + "--bootloader-id=" + bootloaderID, + "--removable", + } + + if d.details.RaidEnabled { + grubInstallArgs = append(grubInstallArgs, "--no-nvram") + + out, err := d.exec.Execute(ctx, &exec.Params{ + Name: "mdadm", + Args: []string{"--examine", "--scan"}, + Timeout: 10 * time.Second, + }) + if err != nil { + return err + } + + out += "\nMAILADDR root\n" + + err = afero.WriteFile(d.fs, "/etc/mdadm.conf", []byte(out), 0700) + if err != nil { + return err + } + + err = d.fs.MkdirAll("/var/lib/initramfs-tools", 0755) + if err != nil { + return err + } + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "update-initramfs", + Args: []string{"-u"}, + }) + if err != nil { + return err + } + + out, err = d.exec.Execute(ctx, &exec.Params{ + Name: "blkid", + }) + if err != nil { + return err + } + + for line := range strings.SplitSeq(string(out), "\n") { + if strings.Contains(line, `PARTLABEL="efi"`) { + disk, _, found := strings.Cut(line, ":") + if !found { + return fmt.Errorf("unable to process blkid output lines") + } + + shim := fmt.Sprintf(`\\EFI\\%s\\grubx64.efi`, bootloaderID) + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "efibootmgr", + Args: []string{"-c", "-d", disk, "-p1", "-l", shim, "-L", bootloaderID}, + }) + if err != nil { + return err + } + } + } + } + + if !runFromCI() { + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "grub-install", + Args: grubInstallArgs, + }) + if err != nil { + return err + } + } + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "update-grub2", + }) + if err != nil { + return err + } + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "dpkg-reconfigure", + Args: []string{"grub-efi-amd64-bin"}, + Env: []string{ + "DEBCONF_NONINTERACTIVE_SEEN=true", + "DEBIAN_FRONTEND=noninteractive", + }, + }) + if err != nil { + return err + } + + return nil +} + +func (d *DefaultOS) FigureOutSerialSpeed() (serialPort, serialSpeed string, err error) { + // ttyS1,115200n8 + serialPort, serialSpeed, found := strings.Cut(d.details.Console, ",") + if !found { + return "", "", fmt.Errorf("serial console could not be split into port and speed") + } + + _, serialPort, found = strings.Cut(serialPort, "ttyS") + if !found { + return "", "", fmt.Errorf("serial port could not be split") + } + + serialSpeed, _, found = strings.Cut(serialSpeed, "n8") + if !found { + return "", "", fmt.Errorf("serial speed could not be split") + } + + return +} diff --git a/pkg/installer/os/common/oscommon.go b/pkg/installer/os/common/oscommon.go new file mode 100644 index 0000000..8833f3b --- /dev/null +++ b/pkg/installer/os/common/oscommon.go @@ -0,0 +1,188 @@ +package oscommon + +import ( + "context" + "fmt" + "log/slog" + "os" + "path" + "strconv" + "strings" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/spf13/afero" +) + +type ( + OperatingSystem interface { + WriteHostname(ctx context.Context) error + WriteHosts(ctx context.Context) error + WriteResolvConf(ctx context.Context) error + WriteNTPConf(ctx context.Context) error + CreateMetalUser(ctx context.Context) error + ConfigureNetwork(ctx context.Context) error + CopySSHKeys(ctx context.Context) error + FixPermissions(ctx context.Context) error + ProcessUserdata(ctx context.Context) error + BuildCMDLine(ctx context.Context) (string, error) + WriteBootInfo(ctx context.Context, cmdLine string) error + GrubInstall(ctx context.Context, cmdLine string) error + UnsetMachineID(ctx context.Context) error + SystemdServices(ctx context.Context) error + WriteBuildMeta(ctx context.Context) error + + SudoGroup() string + InitramdiskFormatString() string + BootloaderID() string + } + + Config struct { + Log *slog.Logger + Fs *afero.Afero + Exec *exec.CmdExecutor + MachineDetails *v1.MachineDetails + Allocation *apiv2.MachineAllocation + } + + DefaultOS struct { + log *slog.Logger + fs *afero.Afero + details *v1.MachineDetails + allocation *apiv2.MachineAllocation + exec *exec.CmdExecutor + network *network.Network + } +) + +func New(cfg *Config) *DefaultOS { + return &DefaultOS{ + log: cfg.Log, + fs: cfg.Fs, + details: cfg.MachineDetails, + allocation: cfg.Allocation, + exec: cfg.Exec, + network: network.New(cfg.Allocation), + } +} + +func (d *DefaultOS) SudoGroup() string { + return "sudo" +} + +func (d *DefaultOS) BootloaderID() string { + panic("default os does not provide a bootloader id") +} + +func (d *DefaultOS) InitramdiskFormatString() string { + return "initrd.img-%s" +} + +func (d *DefaultOS) GetKernelVersion(initramdiskFormatString string) (string, error) { + kern, _, err := d.KernelAndInitrdPath(initramdiskFormatString) + if err != nil { + return "", err + } + + _, version, found := strings.Cut(kern, "vmlinuz-") + if !found { + return "", fmt.Errorf("unable to determine kernel version from: %s", kern) + } + + return version, nil +} + +func (d *DefaultOS) KernelAndInitrdPath(initramdiskFormatString string) (kern string, initrd string, err error) { + // Debian 10 + // root@1f223b59051bcb12:/boot# ls -l + // total 83500 + // -rw-r--r-- 1 root root 83 Aug 13 15:25 System.map-5.10.0-17-amd64 + // -rw-r--r-- 1 root root 236286 Aug 13 15:25 config-5.10.0-17-amd64 + // -rw-r--r-- 1 root root 93842 Jul 19 2021 config-5.10.51 + // drwxr-xr-x 2 root root 4096 Oct 3 11:21 grub + // -rw-r--r-- 1 root root 34665690 Oct 3 11:22 initrd.img-5.10.0-17-amd64 + // lrwxrwxrwx 1 root root 21 Jul 19 2021 vmlinux -> /boot/vmlinux-5.10.51 + // -rwxr-xr-x 1 root root 43526368 Jul 19 2021 vmlinux-5.10.51 + // -rw-r--r-- 1 root root 6962816 Aug 13 15:25 vmlinuz-5.10.0-17-amd64 + + // Ubuntu 20.04 + // root@568551f94559b121:~# ls -l /boot/ + // total 83500 + // -rw-r--r-- 1 root root 83 Aug 13 15:25 System.map-5.10.0-17-amd64 + // -rw-r--r-- 1 root root 236286 Aug 13 15:25 config-5.10.0-17-amd64 + // -rw-r--r-- 1 root root 93842 Jul 19 2021 config-5.10.51 + // drwxr-xr-x 2 root root 4096 Oct 3 11:21 grub + // -rw-r--r-- 1 root root 34665690 Oct 3 11:22 initrd.img-5.10.0-17-amd64 + // lrwxrwxrwx 1 root root 21 Jul 19 2021 vmlinux -> /boot/vmlinux-5.10.51 + // -rwxr-xr-x 1 root root 43526368 Jul 19 2021 vmlinux-5.10.51 + // -rw-r--r-- 1 root root 6962816 Aug 13 15:25 vmlinuz-5.10.0-17-amd64 + + // Almalinux 9 + // [root@14231d4e67d28390 ~]# ls -l /boot/ + // total 160420 + // -rw------- 1 root root 8876661 Jan 7 23:19 System.map-5.14.0-503.19.1.el9_5.x86_64 + // -rw-r--r-- 1 root root 93842 Jul 19 2021 config-5.10.51 + // -rw-r--r-- 1 root root 226249 Jan 7 23:19 config-5.14.0-503.19.1.el9_5.x86_64 + // drwx------ 3 root root 4096 Jun 8 2022 efi + // drwx------ 3 root root 4096 Jan 9 08:02 grub2 + // -rw------- 1 root root 97054329 Jan 9 08:04 initramfs-5.14.0-503.19.1.el9_5.x86_64.img + // drwxr-xr-x 3 root root 4096 Jan 9 08:02 loader + // lrwxrwxrwx 1 root root 52 Jan 9 08:03 symvers-5.14.0-503.19.1.el9_5.x86_64.gz -> /lib/modules/5.14.0-503.19.1.el9_5.x86_64/symvers.gz + // lrwxrwxrwx 1 root root 21 Jul 19 2021 vmlinux -> /boot/vmlinux-5.10.51 + // -rwxr-xr-x 1 root root 43526368 Jul 19 2021 vmlinux-5.10.51 + // -rwxr-xr-x 1 root root 14467384 Jan 7 23:19 vmlinuz-5.14.0-503.19.1.el9_5.x86_64 + + var ( + bootPartition = "/boot" + systemMapPrefix = "/boot/System.map-" + ) + + systemMaps, err := afero.Glob(d.fs, systemMapPrefix+"*") + if err != nil { + return "", "", fmt.Errorf("unable to find a System.map, probably no kernel installed: %w", err) + } + if len(systemMaps) != 1 { + return "", "", fmt.Errorf("more or less than a single System.map found (%v), probably no kernel or more than one kernel installed", systemMaps) + } + + systemMap := systemMaps[0] + _, kernelVersion, found := strings.Cut(systemMap, systemMapPrefix) + if !found { + return "", "", fmt.Errorf("unable to detect kernel version in System.map: %q", systemMap) + } + + kern = path.Join(bootPartition, "vmlinuz"+"-"+kernelVersion) + if !d.fileExists(kern) { + return "", "", fmt.Errorf("kernel image %q not found", kern) + } + + initrd = path.Join(bootPartition, fmt.Sprintf(initramdiskFormatString, kernelVersion)) + if !d.fileExists(initrd) { + return "", "", fmt.Errorf("ramdisk %q not found", initrd) + } + + d.log.Info("detect kernel and initrd", "kernel", kern, "initrd", initrd) + + return +} + +func (d *DefaultOS) fileExists(filename string) bool { + info, err := d.fs.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func runFromCI() bool { + ciEnv := os.Getenv("INSTALL_FROM_CI") + + ci, err := strconv.ParseBool(ciEnv) + if err != nil { + return false + } + + return ci +} diff --git a/pkg/installer/os/common/oscommon_test.go b/pkg/installer/os/common/oscommon_test.go new file mode 100644 index 0000000..b44e21f --- /dev/null +++ b/pkg/installer/os/common/oscommon_test.go @@ -0,0 +1,26 @@ +package oscommon + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/metal-stack/os-installer/pkg/test" + "github.com/stretchr/testify/require" +) + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + var f test.FakeExecParams + err := json.Unmarshal([]byte(os.Args[3]), &f) + require.NoError(t, err) + + _, err = fmt.Fprint(os.Stdout, f.Output) + require.NoError(t, err) + + os.Exit(f.ExitCode) +} diff --git a/pkg/installer/os/common/process_userdata.go b/pkg/installer/os/common/process_userdata.go new file mode 100644 index 0000000..53a0f26 --- /dev/null +++ b/pkg/installer/os/common/process_userdata.go @@ -0,0 +1,87 @@ +package oscommon + +import ( + "context" + "strings" + + "github.com/metal-stack/os-installer/pkg/exec" + + ignitionConfig "github.com/flatcar/ignition/config/v2_4" +) + +const ( + UserdataPath = "/etc/metal/userdata" + ignitionUserdataPath = "/etc/metal/config.ign" +) + +func (d *DefaultOS) ProcessUserdata(ctx context.Context) error { + if ok := d.fileExists(UserdataPath); !ok { + d.log.Info("no userdata present, not processing userdata", "path", UserdataPath) + return nil + } + + content, err := d.fs.ReadFile(UserdataPath) + if err != nil { + return err + } + + defer func() { + out, err := d.exec.Execute(ctx, &exec.Params{ + Name: "systemctl", + Args: []string{"preset-all"}, + }) + if err != nil { + d.log.Error("error when running systemctl preset-all, continuing anyway", "error", err, "output", string(out)) + } + }() + + if isCloudInitFile(content) { + _, err := d.exec.Execute(ctx, &exec.Params{ + Name: "cloud-init", + Args: []string{"devel", "schema", "--config-file", UserdataPath}, + }) + if err != nil { + d.log.Error("error when running cloud-init userdata, continuing anyway", "error", err) + } + + return nil + } + + err = d.fs.Rename(UserdataPath, ignitionUserdataPath) + if err != nil { + return err + } + + rawConfig, err := d.fs.ReadFile(ignitionUserdataPath) + if err != nil { + return err + } + _, report, err := ignitionConfig.Parse(rawConfig) + if err != nil { + d.log.Error("error when validating ignition userdata, continuing anyway", "error", err) + } + + d.log.Info("executing ignition") + + _, err = d.exec.Execute(ctx, &exec.Params{ + Name: "ignition", + Args: []string{"-oem", "file", "-stage", "files", "-log-to-stdout"}, + }) + if err != nil { + d.log.Error("error when running ignition, continuing anyway", "report", report.Entries, "error", err) + } + + return nil +} + +func isCloudInitFile(content []byte) bool { + for i, line := range strings.Split(string(content), "\n") { + if strings.Contains(line, "#cloud-config") { + return true + } + if i > 1 { + return false + } + } + return false +} diff --git a/pkg/installer/os/common/systemd_services.go b/pkg/installer/os/common/systemd_services.go new file mode 100644 index 0000000..a72189f --- /dev/null +++ b/pkg/installer/os/common/systemd_services.go @@ -0,0 +1,11 @@ +package oscommon + +import ( + "context" + + "github.com/metal-stack/os-installer/pkg/services" +) + +func (d *DefaultOS) SystemdServices(ctx context.Context) error { + return services.WriteSystemdServices(ctx, d.log, d.network, d.details.ID) +} diff --git a/pkg/installer/os/common/unset_machine_id.go b/pkg/installer/os/common/unset_machine_id.go new file mode 100644 index 0000000..006335f --- /dev/null +++ b/pkg/installer/os/common/unset_machine_id.go @@ -0,0 +1,25 @@ +package oscommon + +import "context" + +const ( + EtcMachineID = "/etc/machine-id" + DbusMachineID = "/var/lib/dbus/machine-id" +) + +func (d *DefaultOS) UnsetMachineID(ctx context.Context) error { + for _, filePath := range []string{EtcMachineID, DbusMachineID} { + if !d.fileExists(filePath) { + continue + } + + f, err := d.fs.Create(filePath) + if err != nil { + return err + } + + _ = f.Close() + } + + return nil +} diff --git a/pkg/installer/os/common/write_boot_info.go b/pkg/installer/os/common/write_boot_info.go new file mode 100644 index 0000000..01b5818 --- /dev/null +++ b/pkg/installer/os/common/write_boot_info.go @@ -0,0 +1,32 @@ +package oscommon + +import ( + "context" + "fmt" + + v1 "github.com/metal-stack/os-installer/api/v1" + "go.yaml.in/yaml/v3" +) + +const ( + BootInfoPath = "/etc/metal/boot-info.yaml" +) + +func (d *DefaultOS) WriteBootInfo(ctx context.Context, initramdiskFormatString, bootloaderID, cmdLine string) error { + kern, initrd, err := d.KernelAndInitrdPath(initramdiskFormatString) + if err != nil { + return err + } + + content, err := yaml.Marshal(v1.Bootinfo{ + Initrd: initrd, + Cmdline: cmdLine, + Kernel: kern, + BootloaderID: bootloaderID, + }) + if err != nil { + return fmt.Errorf("unable to write boot-info.yaml: %w", err) + } + + return d.fs.WriteFile(BootInfoPath, content, 0700) +} diff --git a/pkg/installer/os/common/write_build_meta.go b/pkg/installer/os/common/write_build_meta.go new file mode 100644 index 0000000..d53562d --- /dev/null +++ b/pkg/installer/os/common/write_build_meta.go @@ -0,0 +1,45 @@ +package oscommon + +import ( + "context" + "strings" + + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/metal-stack/v" + "go.yaml.in/yaml/v3" +) + +const ( + BuildMetaPath = "/etc/metal/build-meta.yaml" +) + +func (d *DefaultOS) WriteBuildMeta(ctx context.Context) error { + d.log.Info("writing build meta file", "path", BuildMetaPath) + + meta := &v1.BuildMeta{ + Version: v.Version, + Date: v.BuildDate, + SHA: v.GitSHA1, + Revision: v.Revision, + } + + out, err := d.exec.Execute(ctx, &exec.Params{ + Name: "ignition", + Args: []string{"-version"}, + }) + if err != nil { + d.log.Error("error detecting ignition version for build meta, continuing anyway", "error", err) + } else { + meta.IgnitionVersion = strings.TrimSpace(out) + } + + content, err := yaml.Marshal(meta) + if err != nil { + return err + } + + content = append([]byte("---\n"), content...) + + return d.fs.WriteFile(BuildMetaPath, content, 0644) +} diff --git a/pkg/installer/os/common/write_hostname.go b/pkg/installer/os/common/write_hostname.go new file mode 100644 index 0000000..3a42ea0 --- /dev/null +++ b/pkg/installer/os/common/write_hostname.go @@ -0,0 +1,13 @@ +package oscommon + +import ( + "context" +) + +const ( + HostnameFilePath = "/etc/hostname" +) + +func (d *DefaultOS) WriteHostname(ctx context.Context) error { + return d.fs.WriteFile(HostnameFilePath, []byte(d.allocation.Hostname), 0644) +} diff --git a/pkg/installer/os/common/write_hosts.go b/pkg/installer/os/common/write_hosts.go new file mode 100644 index 0000000..387e4ee --- /dev/null +++ b/pkg/installer/os/common/write_hosts.go @@ -0,0 +1,23 @@ +package oscommon + +import ( + "context" + "fmt" +) + +const ( + EtcHostsPath = "/etc/hosts" + etcHostsFileContent = `# this file was auto generated by the os-installer +127.0.0.1 localhost +%s %s +` +) + +func (d *DefaultOS) WriteHosts(ctx context.Context) error { + ips, err := d.network.PrivatePrimaryIPs() + if err != nil { + return err + } + + return d.fs.WriteFile(EtcHostsPath, fmt.Appendf(nil, etcHostsFileContent, ips[0], d.allocation.Hostname), 0644) +} diff --git a/pkg/installer/os/common/write_ntp_conf.go b/pkg/installer/os/common/write_ntp_conf.go new file mode 100644 index 0000000..2ac95ac --- /dev/null +++ b/pkg/installer/os/common/write_ntp_conf.go @@ -0,0 +1,65 @@ +package oscommon + +import ( + "context" + "fmt" + "strings" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/services/chrony" +) + +const ( + TimesyncdConfigPath = "/etc/systemd/timesyncd.conf" + ChronyConfigPath = "/etc/chrony/chrony.conf" +) + +func (d *DefaultOS) WriteNTPConf(ctx context.Context) error { + if len(d.allocation.NtpServer) == 0 { + return nil + } + + var ntpServers []string + + for _, ntp := range d.allocation.NtpServer { + ntpServers = append(ntpServers, ntp.Address) + } + + if d.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + defaultVRF, err := d.network.GetTenantNetworkVrfName() + if err != nil { + return err + } + + // TODO: check if this is really required as chrony also gets set up in systemd service task? + + _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ + Log: d.log, + Reload: false, + Enable: true, + }, &chrony.TemplateData{ + NTPServers: ntpServers, + }, defaultVRF) + + if err != nil { + return err + } + + d.WriteNtpConfToPath(ChronyConfigPath, ntpServers) + + return nil + } + + return d.WriteNtpConfToPath(TimesyncdConfigPath, ntpServers) +} + +func (d *DefaultOS) WriteNtpConfToPath(configPath string, ntpServers []string) error { + content := fmt.Sprintf("[Time]\nNTP=%s\n", strings.Join(ntpServers, " ")) + + err := d.fs.Remove(configPath) + if err != nil { + d.log.Info("ntp config file not present", "file", configPath) + } + + return d.fs.WriteFile(configPath, []byte(content), 0644) +} diff --git a/pkg/installer/os/common/write_resolv_conf.go b/pkg/installer/os/common/write_resolv_conf.go new file mode 100644 index 0000000..e0cd310 --- /dev/null +++ b/pkg/installer/os/common/write_resolv_conf.go @@ -0,0 +1,37 @@ +package oscommon + +import ( + "context" + "strings" + + "github.com/spf13/afero" +) + +const ( + ResolvConfPath = "/etc/resolv.conf" +) + +func (d *DefaultOS) WriteResolvConf(ctx context.Context) error { + d.log.Info("write configuration", "file", ResolvConfPath) + // Must be written here because during docker build this file is synthetic + err := d.fs.Remove(ResolvConfPath) + if err != nil { + d.log.Info("config file not present", "file", ResolvConfPath) + } + + content := []byte( + `nameserver 8.8.8.8 +nameserver 8.8.4.4 +`) + + if len(d.allocation.DnsServer) > 0 { + var s strings.Builder + for _, dnsServer := range d.allocation.DnsServer { + s.WriteString("nameserver " + dnsServer.Ip + "\n") + } + + content = []byte(s.String()) + } + + return afero.WriteFile(d.fs, ResolvConfPath, content, 0644) +} diff --git a/pkg/installer/os/debian/debian.go b/pkg/installer/os/debian/debian.go new file mode 100644 index 0000000..29cac34 --- /dev/null +++ b/pkg/installer/os/debian/debian.go @@ -0,0 +1,35 @@ +package debian + +import ( + "context" + + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" +) + +type ( + os struct { + *oscommon.DefaultOS + } +) + +func New(cfg *oscommon.Config) *os { + return &os{ + DefaultOS: oscommon.New(cfg), + } +} + +func (o *os) BootloaderID() string { + return "metal-debian" +} + +func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { + return o.DefaultOS.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) +} + +func (o *os) CreateMetalUser(ctx context.Context) error { + return o.DefaultOS.CreateMetalUser(ctx, o.SudoGroup()) +} + +func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { + return o.DefaultOS.GrubInstall(ctx, o.BootloaderID(), cmdLine) +} diff --git a/pkg/installer/os/debian/tests/debian_test.go b/pkg/installer/os/debian/tests/debian_test.go new file mode 100644 index 0000000..6cbdf33 --- /dev/null +++ b/pkg/installer/os/debian/tests/debian_test.go @@ -0,0 +1,26 @@ +package debian_test + +import ( + "encoding/json" + "fmt" + goos "os" + "testing" + + "github.com/metal-stack/os-installer/pkg/test" + "github.com/stretchr/testify/require" +) + +func TestHelperProcess(t *testing.T) { + if goos.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + var f test.FakeExecParams + err := json.Unmarshal([]byte(goos.Args[3]), &f) + require.NoError(t, err) + + _, err = fmt.Fprint(goos.Stdout, f.Output) + require.NoError(t, err) + + goos.Exit(f.ExitCode) +} diff --git a/pkg/installer/os/debian/tests/install_bootloader_test.go b/pkg/installer/os/debian/tests/install_bootloader_test.go new file mode 100644 index 0000000..6d5d4a5 --- /dev/null +++ b/pkg/installer/os/debian/tests/install_bootloader_test.go @@ -0,0 +1,166 @@ +package debian_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/debian" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sampleMdadmScanOutput = `ARRAY /dev/md/0 metadata=1.0 UUID=42d10089:ee1e0399:445e7550:62b63ec8 name=any:0 +ARRAY /dev/md/1 metadata=1.0 UUID=543eb7f8:98d4d986:e669824d:bebe69e5 name=any:1 +ARRAY /dev/md/2 metadata=1.0 UUID=fc32a6f0:ee40d9db:87c8c9f3:a8400c8b name=any:2` + + sampleBlkidOutput = `/dev/sda1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="cc57c456-0b2f-6345-c597-d861cc6dd8ac" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="273985c8-d097-4123-bcd0-80b4e4e14728" +/dev/sda2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="54748c60-b566-f391-142c-fb78bb1fc6a9" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="d7863f4e-af7c-47fc-8c03-6ecdc69bc72d" +/dev/sda3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="582e9b4f-f191-e01e-85fd-2f7d969fbef6" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="e8b44f09-b7f7-4e0d-a7c3-d909617d1f05" +/dev/sdb1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="61bd5d8b-1bb8-673b-9e61-8c28dccc3812" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="13a4c568-57b0-4259-9927-9ac023aaa5f0" +/dev/sdb2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="e7d01e93-9340-5b90-68f8-d8f815595132" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="ab11cd86-37b8-4bae-81e5-21fe0a9c9ae0" +/dev/sdb3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="764217ad-1591-a83a-c799-23397f968729" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="9afbf9c1-b2ba-4b46-8db1-e802d26c93b6" +/dev/md1: LABEL="root" UUID="ace079b5-06be-4429-bbf0-081ea4d7d0d9" TYPE="ext4" +/dev/md0: LABEL="efi" UUID="C236-297F" TYPE="vfat" +/dev/md2: LABEL="varlib" UUID="385e8e8e-dbfd-481e-93a4-cba7f4d5fa02" TYPE="ext4"` +) + +func Test_os_GrubInstall(t *testing.T) { + tests := []struct { + name string + cmdLine string + details *v1.MachineDetails + execMocks []test.FakeExecParams + want string + wantErr error + }{ + { + name: "without raid", + cmdLine: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + details: &v1.MachineDetails{ + Console: "ttyS1,115200n8", + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--boot-directory=/boot", "--bootloader-id=metal-debian", "--removable"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"update-grub2"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"dpkg-reconfigure", "grub-efi-amd64-bin"}, + Output: "", + ExitCode: 0, + }, + }, + want: `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=metal-debian +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" +`, + }, + { + name: "with raid", + cmdLine: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + details: &v1.MachineDetails{ + RaidEnabled: true, + RootUUID: "ace079b5-06be-4429-bbf0-081ea4d7d0d9", + Console: "ttyS1,115200n8", + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"mdadm", "--examine", "--scan"}, + Output: sampleMdadmScanOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"update-initramfs", "-u"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"blkid"}, + Output: sampleBlkidOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sda1", "-p1", "-l", "\\\\EFI\\\\metal-debian\\\\grubx64.efi", "-L", "metal-debian"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sdb1", "-p1", "-l", "\\\\EFI\\\\metal-debian\\\\grubx64.efi", "-L", "metal-debian"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--boot-directory=/boot", "--bootloader-id=metal-debian", "--removable", "--no-nvram"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"update-grub2"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"dpkg-reconfigure", "grub-efi-amd64-bin"}, + Output: "", + ExitCode: 0, + }, + }, + want: `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=metal-debian +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + d := debian.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + }) + + gotErr := d.GrubInstall(t.Context(), tt.cmdLine) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.DefaultGrubPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/debian/tests/write_boot_info_test.go b/pkg/installer/os/debian/tests/write_boot_info_test.go new file mode 100644 index 0000000..1f863d3 --- /dev/null +++ b/pkg/installer/os/debian/tests/write_boot_info_test.go @@ -0,0 +1,124 @@ +package debian_test + +import ( + "fmt" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/debian" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert/yaml" + "github.com/stretchr/testify/require" +) + +func Test_os_WriteBootInfo(t *testing.T) { + tests := []struct { + name string + cmdLine string + fsMocks func(fs *afero.Afero) + want *v1.Bootinfo + wantErr error + }{ + { + name: "boot-info debian", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: &v1.Bootinfo{ + Initrd: "/boot/initrd.img-1.2.3", + Cmdline: "a-cmd-line", + Kernel: "/boot/vmlinuz-1.2.3", + BootloaderID: "metal-debian", + }, + }, + { + name: "more than one system.map present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.4", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("more or less than a single System.map found ([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), + }, + { + name: "no system.map present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("more or less than a single System.map found ([]), probably no kernel or more than one kernel installed"), + }, + { + name: "no vmlinuz present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("kernel image \"/boot/vmlinuz-1.2.3\" not found"), + }, + { + name: "no ramdisk present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("ramdisk \"/boot/initrd.img-1.2.3\" not found"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := debian.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteBootInfo(t.Context(), tt.cmdLine) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.BootInfoPath) + require.NoError(t, err) + + var bootInfo v1.Bootinfo + err = yaml.Unmarshal(content, &bootInfo) + require.NoError(t, err) + + assert.Equal(t, tt.want, &bootInfo) + }) + } +} diff --git a/pkg/installer/os/os.go b/pkg/installer/os/os.go new file mode 100644 index 0000000..8be1946 --- /dev/null +++ b/pkg/installer/os/os.go @@ -0,0 +1,64 @@ +package operatingsystem + +import ( + "fmt" + "strconv" + "strings" + + "github.com/metal-stack/os-installer/pkg/installer/os/almalinux" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/debian" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/spf13/afero" +) + +const ( + ubuntuOS = osName("ubuntu") + debianOS = osName("debian") + almalinuxOS = osName("almalinux") +) + +type ( + osName string +) + +func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { + if cfg.Fs == nil { + cfg.Fs = &afero.Afero{ + Fs: afero.OsFs{}, + } + } + + content, err := cfg.Fs.ReadFile("/etc/os-release") + if err != nil { + return nil, err + } + + env := map[string]string{} + for line := range strings.SplitSeq(string(content), "\n") { + k, v, found := strings.Cut(line, "=") + if found { + env[k] = v + } + } + + if os, ok := env["ID"]; ok { + unquoted, err := strconv.Unquote(os) + if err == nil { + os = unquoted + } + + switch os := osName(strings.ToLower(os)); os { + case ubuntuOS: + return ubuntu.New(cfg), nil + case debianOS: + return debian.New(cfg), nil + case almalinuxOS: + return almalinux.New(cfg), nil + default: + return nil, fmt.Errorf("unsupported operating system: %s", os) + } + } + + return nil, fmt.Errorf("unable to detect OS") +} diff --git a/pkg/installer/os/ubuntu/tests/cmd_line_test.go b/pkg/installer/os/ubuntu/tests/cmd_line_test.go new file mode 100644 index 0000000..6bb3608 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/cmd_line_test.go @@ -0,0 +1,97 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" +) + +const ( + sampleMdadmDetailOutput = `MD_LEVEL=raid1 +MD_DEVICES=2 +MD_METADATA=1.0 +MD_UUID=543eb7f8:98d4d986:e669824d:bebe69e5 +MD_DEVNAME=1 +MD_NAME=any:1 +MD_DEVICE_dev_sdb2_ROLE=1 +MD_DEVICE_dev_sdb2_DEV=/dev/sdb2 +MD_DEVICE_dev_sda2_ROLE=0 +MD_DEVICE_dev_sda2_DEV=/dev/sda2` +) + +func TestDefaultOS_BuildCMDLine(t *testing.T) { + tests := []struct { + name string + details *v1.MachineDetails + execMocks []test.FakeExecParams + want string + wantErr error + }{ + { + name: "no raid", + details: &v1.MachineDetails{ + RootUUID: "543eb7f8-98d4-d986-e669-824dbebe69e5", + RaidEnabled: false, + Console: "ttyS1,115200n8", + }, + want: "console=ttyS1,115200n8 root=UUID=543eb7f8-98d4-d986-e669-824dbebe69e5 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + wantErr: nil, + }, + { + name: "with raid", + details: &v1.MachineDetails{ + RootUUID: "ace079b5-06be-4429-bbf0-081ea4d7d0d9", + RaidEnabled: true, + Console: "ttyS1,115200n8", + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"blkid"}, + Output: sampleBlkidOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"mdadm", "--detail", "--export", "/dev/md1"}, + Output: sampleMdadmDetailOutput, + ExitCode: 0, + }, + }, + want: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300 rdloaddriver=raid0 rdloaddriver=raid1 rd.md.uuid=543eb7f8:98d4d986:e669824d:bebe69e5", + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := slog.Default() + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: &afero.Afero{ + Fs: afero.NewMemMapFs(), + }, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + }) + + got, gotErr := d.BuildCMDLine(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/fix_permissions_test.go b/pkg/installer/os/ubuntu/tests/fix_permissions_test.go new file mode 100644 index 0000000..07b77d6 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/fix_permissions_test.go @@ -0,0 +1,65 @@ +package ubuntu_test + +import ( + iofs "io/fs" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultOS_FixPermissions(t *testing.T) { + tests := []struct { + name string + fsMocks func(fs afero.Fs) + wantErr error + }{ + { + name: "fix permissions", + fsMocks: func(fs afero.Fs) { + require.NoError(t, fs.MkdirAll("/var/tmp", 0000)) + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + err := d.FixPermissions(t.Context()) + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n %s", diff) + } + + info, err := fs.Stat("/var/tmp") + require.NoError(t, err) + assert.Equal(t, iofs.FileMode(01777).Perm(), info.Mode().Perm()) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/install_bootloader_test.go b/pkg/installer/os/ubuntu/tests/install_bootloader_test.go new file mode 100644 index 0000000..b0a72ee --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/install_bootloader_test.go @@ -0,0 +1,166 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sampleMdadmScanOutput = `ARRAY /dev/md/0 metadata=1.0 UUID=42d10089:ee1e0399:445e7550:62b63ec8 name=any:0 +ARRAY /dev/md/1 metadata=1.0 UUID=543eb7f8:98d4d986:e669824d:bebe69e5 name=any:1 +ARRAY /dev/md/2 metadata=1.0 UUID=fc32a6f0:ee40d9db:87c8c9f3:a8400c8b name=any:2` + + sampleBlkidOutput = `/dev/sda1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="cc57c456-0b2f-6345-c597-d861cc6dd8ac" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="273985c8-d097-4123-bcd0-80b4e4e14728" +/dev/sda2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="54748c60-b566-f391-142c-fb78bb1fc6a9" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="d7863f4e-af7c-47fc-8c03-6ecdc69bc72d" +/dev/sda3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="582e9b4f-f191-e01e-85fd-2f7d969fbef6" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="e8b44f09-b7f7-4e0d-a7c3-d909617d1f05" +/dev/sdb1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="61bd5d8b-1bb8-673b-9e61-8c28dccc3812" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="13a4c568-57b0-4259-9927-9ac023aaa5f0" +/dev/sdb2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="e7d01e93-9340-5b90-68f8-d8f815595132" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="ab11cd86-37b8-4bae-81e5-21fe0a9c9ae0" +/dev/sdb3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="764217ad-1591-a83a-c799-23397f968729" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="9afbf9c1-b2ba-4b46-8db1-e802d26c93b6" +/dev/md1: LABEL="root" UUID="ace079b5-06be-4429-bbf0-081ea4d7d0d9" TYPE="ext4" +/dev/md0: LABEL="efi" UUID="C236-297F" TYPE="vfat" +/dev/md2: LABEL="varlib" UUID="385e8e8e-dbfd-481e-93a4-cba7f4d5fa02" TYPE="ext4"` +) + +func Test_os_GrubInstall(t *testing.T) { + tests := []struct { + name string + cmdLine string + details *v1.MachineDetails + execMocks []test.FakeExecParams + want string + wantErr error + }{ + { + name: "without raid", + cmdLine: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + details: &v1.MachineDetails{ + Console: "ttyS1,115200n8", + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--boot-directory=/boot", "--bootloader-id=metal-ubuntu", "--removable"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"update-grub2"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"dpkg-reconfigure", "grub-efi-amd64-bin"}, + Output: "", + ExitCode: 0, + }, + }, + want: `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=metal-ubuntu +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" +`, + }, + { + name: "with raid", + cmdLine: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + details: &v1.MachineDetails{ + RaidEnabled: true, + RootUUID: "ace079b5-06be-4429-bbf0-081ea4d7d0d9", + Console: "ttyS1,115200n8", + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"mdadm", "--examine", "--scan"}, + Output: sampleMdadmScanOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"update-initramfs", "-u"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"blkid"}, + Output: sampleBlkidOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sda1", "-p1", "-l", "\\\\EFI\\\\metal-ubuntu\\\\grubx64.efi", "-L", "metal-ubuntu"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sdb1", "-p1", "-l", "\\\\EFI\\\\metal-ubuntu\\\\grubx64.efi", "-L", "metal-ubuntu"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--boot-directory=/boot", "--bootloader-id=metal-ubuntu", "--removable", "--no-nvram"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"update-grub2"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"dpkg-reconfigure", "grub-efi-amd64-bin"}, + Output: "", + ExitCode: 0, + }, + }, + want: `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=metal-ubuntu +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + }) + + gotErr := d.GrubInstall(t.Context(), tt.cmdLine) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.DefaultGrubPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/process_userdata_test.go b/pkg/installer/os/ubuntu/tests/process_userdata_test.go new file mode 100644 index 0000000..7e788c5 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/process_userdata_test.go @@ -0,0 +1,107 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +const ( + sampleCloudInit = `#cloud-config +# Add groups to the system +# The following example adds the ubuntu group with members 'root' and 'sys' +# and the empty group cloud-users. +groups: + - admingroup: [root,sys] + - cloud-users` + sampleIgnition = `{"ignition":{"config":{},"security":{"tls":{}},"timeouts":{},"version":"2.2.0"}}` +) + +func TestDefaultOS_ProcessUserdata(t *testing.T) { + tests := []struct { + name string + details *v1.MachineDetails + fsMocks func(fs *afero.Afero) + execMocks []test.FakeExecParams + want string + wantErr error + }{ + { + name: "no userdata given", + }, + { + name: "cloud-init", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, oscommon.UserdataPath, []byte(sampleCloudInit), 0700)) + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"cloud-init", "devel", "schema", "--config-file", oscommon.UserdataPath}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"systemctl", "preset-all"}, + Output: "", + ExitCode: 0, + }, + }, + }, + { + name: "ignition", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, oscommon.UserdataPath, []byte(sampleIgnition), 0700)) + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"ignition", "-oem", "file", "-stage", "files", "-log-to-stdout"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"systemctl", "preset-all"}, + Output: "", + ExitCode: 0, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + }) + + gotErr := d.ProcessUserdata(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/ubuntu_test.go b/pkg/installer/os/ubuntu/tests/ubuntu_test.go new file mode 100644 index 0000000..7b56718 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/ubuntu_test.go @@ -0,0 +1,26 @@ +package ubuntu_test + +import ( + "encoding/json" + "fmt" + goos "os" + "testing" + + "github.com/metal-stack/os-installer/pkg/test" + "github.com/stretchr/testify/require" +) + +func TestHelperProcess(t *testing.T) { + if goos.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + var f test.FakeExecParams + err := json.Unmarshal([]byte(goos.Args[3]), &f) + require.NoError(t, err) + + _, err = fmt.Fprint(goos.Stdout, f.Output) + require.NoError(t, err) + + goos.Exit(f.ExitCode) +} diff --git a/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go b/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go new file mode 100644 index 0000000..8ceb6c8 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go @@ -0,0 +1,76 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultOS_UnsetMachineID(t *testing.T) { + tests := []struct { + name string + fsMocks func(fs *afero.Afero) + wantErr error + }{ + { + name: "unset", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/etc/machine-id", []byte("uuid"), 0700)) + require.NoError(t, fs.WriteFile("/var/lib/dbus/machine-id", []byte("uuid"), 0700)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + content, err := fs.ReadFile(oscommon.EtcMachineID) + require.NoError(t, err) + require.Equal(t, "uuid", string(content)) + + content, err = fs.ReadFile(oscommon.DbusMachineID) + require.NoError(t, err) + require.Equal(t, "uuid", string(content)) + + gotErr := d.UnsetMachineID(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err = fs.ReadFile(oscommon.EtcMachineID) + require.NoError(t, err) + assert.Empty(t, content) + + content, err = fs.ReadFile(oscommon.DbusMachineID) + require.NoError(t, err) + assert.Empty(t, content) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go new file mode 100644 index 0000000..c1cb9ce --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go @@ -0,0 +1,124 @@ +package ubuntu_test + +import ( + "fmt" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert/yaml" + "github.com/stretchr/testify/require" +) + +func Test_os_WriteBootInfo(t *testing.T) { + tests := []struct { + name string + cmdLine string + fsMocks func(fs *afero.Afero) + want *v1.Bootinfo + wantErr error + }{ + { + name: "boot-info ubuntu", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: &v1.Bootinfo{ + Initrd: "/boot/initrd.img-1.2.3", + Cmdline: "a-cmd-line", + Kernel: "/boot/vmlinuz-1.2.3", + BootloaderID: "metal-ubuntu", + }, + }, + { + name: "more than one system.map present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.4", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("more or less than a single System.map found ([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), + }, + { + name: "no system.map present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("more or less than a single System.map found ([]), probably no kernel or more than one kernel installed"), + }, + { + name: "no vmlinuz present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("kernel image \"/boot/vmlinuz-1.2.3\" not found"), + }, + { + name: "no ramdisk present", + cmdLine: "a-cmd-line", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-1.2.3", nil, 0700)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-1.2.3", nil, 0700)) + }, + want: nil, + wantErr: fmt.Errorf("ramdisk \"/boot/initrd.img-1.2.3\" not found"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteBootInfo(t.Context(), tt.cmdLine) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.BootInfoPath) + require.NoError(t, err) + + var bootInfo v1.Bootinfo + err = yaml.Unmarshal(content, &bootInfo) + require.NoError(t, err) + + assert.Equal(t, tt.want, &bootInfo) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/write_build_meta_test.go b/pkg/installer/os/ubuntu/tests/write_build_meta_test.go new file mode 100644 index 0000000..bc16c47 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/write_build_meta_test.go @@ -0,0 +1,80 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/metal-stack/v" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultOS_WriteBuildMeta(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + execMocks []test.FakeExecParams + want string + wantErr error + }{ + { + name: "build meta gets written", + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"ignition", "-version"}, + Output: "Ignition v0.36.2", + ExitCode: 0, + }, + }, + want: `--- +buildVersion: "456" +buildDate: "" +buildSHA: abc +buildRevision: revision +ignitionVersion: Ignition v0.36.2 +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Allocation: tt.allocation, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + }) + + v.Version = "456" + v.GitSHA1 = "abc" + v.Revision = "revision" + + gotErr := d.WriteBuildMeta(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.BuildMetaPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/write_hostname_test.go b/pkg/installer/os/ubuntu/tests/write_hostname_test.go new file mode 100644 index 0000000..1a12c68 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/write_hostname_test.go @@ -0,0 +1,82 @@ +package ubuntu_test + +import ( + "log/slog" + goos "os" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultOS_WriteHostname(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + fsMocks func(fs *afero.Afero) + want string + wantErr error + }{ + { + name: "write hostname", + allocation: &apiv2.MachineAllocation{ + Hostname: "test-hostname", + }, + want: "test-hostname", + wantErr: nil, + }, + { + name: "overwrite when already exists", + allocation: &apiv2.MachineAllocation{ + Hostname: "test-hostname", + }, + want: "test-hostname", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(oscommon.HostnameFilePath, []byte("bar"), goos.ModePerm)) + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Allocation: tt.allocation, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteHostname(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.HostnameFilePath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/write_hosts_test.go b/pkg/installer/os/ubuntu/tests/write_hosts_test.go new file mode 100644 index 0000000..8a2a9f0 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/write_hosts_test.go @@ -0,0 +1,85 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultOS_WriteHosts(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + fsMocks func(fs *afero.Afero) + want string + wantErr error + }{ + { + name: "write hosts", + allocation: &apiv2.MachineAllocation{ + Hostname: "my-host", + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + }, + }, + want: `# this file was auto generated by the os-installer +127.0.0.1 localhost +10.0.16.2 my-host +`, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Allocation: tt.allocation, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteHosts(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.EtcHostsPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go b/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go new file mode 100644 index 0000000..21c3391 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go @@ -0,0 +1,228 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_os_WriteNTPConf(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + fsMocks func(fs *afero.Afero) + want string + wantErr error + }{ + { + name: "configure custom ntp", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(oscommon.TimesyncdConfigPath, []byte(""), 0644)) + }, + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + NtpServer: []*apiv2.NTPServer{ + {Address: "custom.1.ntp.org"}, + {Address: "custom.2.ntp.org"}, + }, + }, + want: `[Time] +NTP=custom.1.ntp.org custom.2.ntp.org +`, + wantErr: nil, + }, + { + name: "use default ntp", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(oscommon.TimesyncdConfigPath, []byte(""), 0644)) + }, + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + }, + want: "", + wantErr: nil, + }, + // FIXME! + // { + // name: "configure custom ntp for firewall", + // fsMocks: func(fs *afero.Afero) { + // require.NoError(t, fs.WriteFile(oscommon.ChronyConfigPath, []byte(""), 0644)) + // }, + // allocation: &apiv2.MachineAllocation{ + // AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + // NtpServer: []*apiv2.NTPServer{ + // {Address: "custom.1.ntp.org"}, + // {Address: "custom.2.ntp.org"}, + // }, + // Project: "project-a", + // Networks: []*apiv2.MachineNetwork{ + // { + // Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + // Project: new("project-a"), + // NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + // Prefixes: []string{"10.0.16.0/22"}, + // Ips: []string{"10.0.16.2"}, + // Vrf: 3981, + // Asn: 4200003073, + // }, + // }, + // }, + // want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more + // # information about usable directives. + + // # In case no custom NTP server is provided + // # Cloudflare offers a free public time service that allows us to use their + // # anycast network of 180+ locations to synchronize time from their closest server. + // # See https://blog.cloudflare.com/secure-time/ + // pool custom.1.ntp.org iburst + // pool custom.2.ntp.org iburst + + // # This directive specify the location of the file containing ID/key pairs for + // # NTP authentication. + // keyfile /etc/chrony/chrony.keys + + // # This directive specify the file into which chronyd will store the rate + // # information. + // driftfile /var/lib/chrony/chrony.drift + + // # Uncomment the following line to turn logging on. + // #log tracking measurements statistics + + // # Log files location. + // logdir /var/log/chrony + + // # Stop bad estimates upsetting machine clock. + // maxupdateskew 100.0 + + // # This directive enables kernel synchronisation (every 11 minutes) of the + // # real-time clock. Note that it can’t be used along with the 'rtcfile' directive. + // rtcsync + + // # Step the system clock instead of slewing it if the adjustment is larger than + // # one second, but only in the first three clock updates. + // makestep 1 3`, + // wantErr: nil, + // }, + // { + // name: "use default ntp for firewall", + // fsMocks: func(fs *afero.Afero) { + // require.NoError(t, fs.WriteFile(oscommon.ChronyConfigPath, []byte(""), 0644)) + // }, + // allocation: &apiv2.MachineAllocation{ + // AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + // }, + // want: "", + // wantErr: nil, + // }, + + // { + // name: "configure ntp for almalinux machine", + // fsMocks: func(fs afero.Fs) { + // require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644)) + // }, + // oss: osAlmalinux, + // ntpPath: "/etc/chrony.conf", + // role: "machine", + // ntpServers: []*models.V1NTPServer{{Address: new("custom.1.ntp.org")}, {Address: new("custom.2.ntp.org")}}, + // want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more + // # information about usable directives. + + // # In case no custom NTP server is provided + // # Cloudflare offers a free public time service that allows us to use their + // # anycast network of 180+ locations to synchronize time from their closest server. + // # See https://blog.cloudflare.com/secure-time/ + // pool custom.1.ntp.org iburst + // pool custom.2.ntp.org iburst + + // # This directive specify the location of the file containing ID/key pairs for + // # NTP authentication. + // keyfile /etc/chrony/chrony.keys + + // # This directive specify the file into which chronyd will store the rate + // # information. + // driftfile /var/lib/chrony/chrony.drift + + // # Uncomment the following line to turn logging on. + // #log tracking measurements statistics + + // # Log files location. + // logdir /var/log/chrony + + // # Stop bad estimates upsetting machine clock. + // maxupdateskew 100.0 + + // # This directive enables kernel synchronisation (every 11 minutes) of the + // # real-time clock. Note that it can’t be used along with the 'rtcfile' directive. + // rtcsync + + // # Step the system clock instead of slewing it if the adjustment is larger than + // # one second, but only in the first three clock updates. + // makestep 1 3`, + // wantErr: nil, + // }, + // { + // name: "use default ntp for almalinux machine", + // fsMocks: func(fs afero.Fs) { + // require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644)) + // }, + // oss: osAlmalinux, + // ntpPath: "/etc/chrony.conf", + // role: "machine", + // want: "", + // wantErr: nil, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Allocation: tt.allocation, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteNTPConf(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + if tt.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + content, err := fs.ReadFile(oscommon.ChronyConfigPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + + return + } + + content, err := fs.ReadFile(oscommon.TimesyncdConfigPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go b/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go new file mode 100644 index 0000000..d4b6a31 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go @@ -0,0 +1,94 @@ +package ubuntu_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Tes_os_WriteResolvConf(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + fsMocks func(fs *afero.Afero) + want string + wantErr error + }{ + { + name: "resolv.conf gets written", + allocation: &apiv2.MachineAllocation{}, + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(oscommon.ResolvConfPath, []byte(""), 0755)) + }, + want: `nameserver 8.8.8.8 +nameserver 8.8.4.4 +`, + wantErr: nil, + }, + { + name: "resolv.conf gets written, file is not present", + allocation: &apiv2.MachineAllocation{}, + want: `nameserver 8.8.8.8 +nameserver 8.8.4.4 +`, + wantErr: nil, + }, + { + name: "overwrite resolv.conf with custom DNS", + allocation: &apiv2.MachineAllocation{ + DnsServer: []*apiv2.DNSServer{ + {Ip: "1.2.3.4"}, + {Ip: "5.6.7.8"}, + }, + }, + want: `nameserver 1.2.3.4 +nameserver 5.6.7.8 +`, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Allocation: tt.allocation, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteResolvConf(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(oscommon.ResolvConfPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/ubuntu.go b/pkg/installer/os/ubuntu/ubuntu.go new file mode 100644 index 0000000..ed12844 --- /dev/null +++ b/pkg/installer/os/ubuntu/ubuntu.go @@ -0,0 +1,35 @@ +package ubuntu + +import ( + "context" + + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" +) + +type ( + os struct { + *oscommon.DefaultOS + } +) + +func New(cfg *oscommon.Config) *os { + return &os{ + DefaultOS: oscommon.New(cfg), + } +} + +func (o *os) BootloaderID() string { + return "metal-ubuntu" +} + +func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { + return o.DefaultOS.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) +} + +func (o *os) CreateMetalUser(ctx context.Context) error { + return o.DefaultOS.CreateMetalUser(ctx, o.SudoGroup()) +} + +func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { + return o.DefaultOS.GrubInstall(ctx, o.BootloaderID(), cmdLine) +} diff --git a/pkg/test/fakeexec.go b/pkg/test/fakeexec.go new file mode 100644 index 0000000..64f5def --- /dev/null +++ b/pkg/test/fakeexec.go @@ -0,0 +1,56 @@ +package test + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// inspired by this blog article: https://npf.io/2015/06/testing-exec-command/ + +type fakeexec struct { + t *testing.T + mockCount int + mocks []FakeExecParams +} + +// nolint:musttag +type FakeExecParams struct { + WantCmd []string `json:"want_cmd"` + Output string `json:"output"` + ExitCode int `json:"exit_code"` +} + +func FakeCmd(t *testing.T, params ...FakeExecParams) func(ctx context.Context, command string, args ...string) *exec.Cmd { + f := fakeexec{ + t: t, + mocks: params, + } + + return f.command +} + +func (f *fakeexec) command(ctx context.Context, command string, args ...string) *exec.Cmd { + if f.mockCount >= len(f.mocks) { + require.Fail(f.t, "more commands called than mocks are available") + } + + params := f.mocks[f.mockCount] + f.mockCount++ + + assert.Equal(f.t, params.WantCmd, append([]string{command}, args...)) + + j, err := json.Marshal(params) + require.NoError(f.t, err) + + cs := []string{"-test.run=TestHelperProcess", "--", string(j)} + cmd := exec.CommandContext(ctx, os.Args[0], cs...) //nolint + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + + return cmd +} diff --git a/pkg/test/fakeexec_test.go b/pkg/test/fakeexec_test.go new file mode 100644 index 0000000..d2c94a2 --- /dev/null +++ b/pkg/test/fakeexec_test.go @@ -0,0 +1,25 @@ +package test + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + var f FakeExecParams + err := json.Unmarshal([]byte(os.Args[3]), &f) + require.NoError(t, err) + + _, err = fmt.Fprint(os.Stdout, f.Output) + require.NoError(t, err) + + os.Exit(f.ExitCode) +} From b9595b8aed827ad522ca8387665de3c4fe13436a Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 07:50:00 +0100 Subject: [PATCH 045/102] Validate nftables --- pkg/frr/frr_test.go | 1 + pkg/installer/os/almalinux/write_ntp_conf.go | 2 +- pkg/installer/os/common/configure_network.go | 1 + pkg/nftables/nftables.go | 15 +++++++++++---- pkg/nftables/nftables_test.go | 1 + 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go index 26d48f1..ef145fc 100644 --- a/pkg/frr/frr_test.go +++ b/pkg/frr/frr_test.go @@ -426,6 +426,7 @@ func TestRender(t *testing.T) { fs: fs, Network: network.New(tt.allocation), FRRVersion: tt.frrVersion, + Validate: false, }) if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { diff --git a/pkg/installer/os/almalinux/write_ntp_conf.go b/pkg/installer/os/almalinux/write_ntp_conf.go index 95332f4..bc04ff0 100644 --- a/pkg/installer/os/almalinux/write_ntp_conf.go +++ b/pkg/installer/os/almalinux/write_ntp_conf.go @@ -1,4 +1,4 @@ -package almalinux_test +package almalinux import ( "context" diff --git a/pkg/installer/os/common/configure_network.go b/pkg/installer/os/common/configure_network.go index 05654b8..6d9d3cd 100644 --- a/pkg/installer/os/common/configure_network.go +++ b/pkg/installer/os/common/configure_network.go @@ -34,6 +34,7 @@ func (d *DefaultOS) ConfigureNetwork(ctx context.Context) error { if _, err := nftables.Render(ctx, &nftables.Config{ Log: d.log, Reload: false, + Validate: true, Network: d.network, EnableDNSProxy: false, ForwardPolicy: nftables.ForwardPolicyDrop, diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 9d12e5d..5a0b0db 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -46,8 +46,9 @@ var ( type ( Config struct { - Log *slog.Logger - Reload bool + Log *slog.Logger + Reload bool + Validate bool Network *network.Network @@ -147,6 +148,12 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { TemplateString: templateString, Data: data, Fs: cfg.fs, + Validate: func(path string) error { + if !cfg.Validate { + return nil + } + return validate(cfg) + }, }) if err != nil { return false, err @@ -382,8 +389,8 @@ func getAddressFamily(p string) (string, error) { return family, nil } -// Validate validates network interfaces configuration. -func Validate(cfg *Config) error { +// validate validates network interfaces configuration. +func validate(cfg *Config) error { cfg.Log.Info("running 'nft --check --file' to validate changes.", "file", nftrulesPath) cmd := exec.Command("nft", "--check", "--file", nftrulesPath) diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index 92b7e4c..ab6014c 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -353,6 +353,7 @@ func TestRender(t *testing.T) { Network: network.New(tt.allocation), EnableDNSProxy: tt.enableDNSProxy, ForwardPolicy: tt.forwardPolicy, + Validate: false, }) if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { From 33a7416839a269ea0a1cef1aefaa2d8b3ab1f058 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 07:57:16 +0100 Subject: [PATCH 046/102] Fix tests, go mod tidy --- go.mod | 28 ++--------------------- go.sum | 55 --------------------------------------------- pkg/frr/frr_test.go | 5 +++++ 3 files changed, 7 insertions(+), 81 deletions(-) diff --git a/go.mod b/go.mod index 445d26c..927b63a 100644 --- a/go.mod +++ b/go.mod @@ -10,12 +10,11 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff - github.com/metal-stack/metal-go v0.43.0 github.com/metal-stack/v v1.0.3 github.com/samber/lo v1.53.0 github.com/spf13/afero v1.15.0 github.com/stretchr/testify v1.11.1 - gopkg.in/yaml.v3 v3.0.1 + go.yaml.in/yaml/v3 v3.0.4 ) require ( @@ -26,42 +25,19 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-openapi/analysis v0.24.3 // indirect - github.com/go-openapi/errors v0.22.7 // indirect - github.com/go-openapi/jsonpointer v0.22.5 // indirect - github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/loads v0.23.3 // indirect - github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/strfmt v0.26.0 // indirect - github.com/go-openapi/swag v0.25.5 // indirect - github.com/go-openapi/swag/cmdutils v0.25.5 // indirect - github.com/go-openapi/swag/conv v0.25.5 // indirect - github.com/go-openapi/swag/fileutils v0.25.5 // indirect - github.com/go-openapi/swag/jsonname v0.25.5 // indirect - github.com/go-openapi/swag/jsonutils v0.25.5 // indirect - github.com/go-openapi/swag/loading v0.25.5 // indirect - github.com/go-openapi/swag/mangling v0.25.5 // indirect - github.com/go-openapi/swag/netutils v0.25.5 // indirect - github.com/go-openapi/swag/stringutils v0.25.5 // indirect - github.com/go-openapi/swag/typeutils v0.25.5 // indirect - github.com/go-openapi/swag/yamlutils v0.25.5 // indirect - github.com/go-openapi/validate v0.25.2 // indirect - github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/vincent-petithory/dataurl v1.0.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 94d99b8..8a5c3e4 100644 --- a/go.sum +++ b/go.sum @@ -27,54 +27,6 @@ github.com/flatcar/ignition v0.36.2/go.mod h1:uk1tpzLFRXus4RrvzgMI+IqmmB8a/RGFSB github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= -github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= -github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= -github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= -github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= -github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= -github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= -github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= -github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= -github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= -github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= -github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= -github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= -github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= -github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= -github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= -github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= -github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= -github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= -github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= -github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= -github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= -github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= -github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= -github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= -github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= -github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= -github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= -github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= -github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= -github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= -github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= -github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= -github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= -github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus v0.0.0-20181025153459-66d97aec3384/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= @@ -93,17 +45,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff h1:668iZE3tvpbhoARzpW8zdFnGTVqa7Ks5xJKeY4N0WtA= github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff/go.mod h1:SAtqZaD4JvOn+NVc6bTlKzL2EDoj/QrlHF72ZMw+Btk= -github.com/metal-stack/metal-go v0.43.0 h1:uODD0YCwnAYzyvFxWNakZrymBoMz1FAvP5hkhsR83VQ= -github.com/metal-stack/metal-go v0.43.0/go.mod h1:GSfXrAj55LGsUSMHWGDsmq5n056NG0yb1JM8bgfvKOw= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pin/tftp v2.1.0+incompatible/go.mod h1:xVpZOMCXTy+A5QMjEVN0Glwa1sUvaJhFXbr/aAxuxGY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -141,8 +88,6 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go index ef145fc..86e8d94 100644 --- a/pkg/frr/frr_test.go +++ b/pkg/frr/frr_test.go @@ -375,12 +375,14 @@ func TestRender(t *testing.T) { { name: "render firewall", allocation: firewallAllocation, + frrVersion: semver.MustParse("9.0.1"), wantFilePath: "frr.conf.firewall", wantErr: nil, }, { name: "render firewall, dualstack", allocation: firewallAllocationDualStack, + frrVersion: semver.MustParse("9.0.1"), wantFilePath: "frr.conf.firewall_dualstack", wantErr: nil, }, @@ -401,18 +403,21 @@ func TestRender(t *testing.T) { { name: "render firewall shared", allocation: firewallSharedAllocation, + frrVersion: semver.MustParse("9.0.1"), wantFilePath: "frr.conf.firewall_shared", wantErr: nil, }, { name: "render firewall ipv6", allocation: firewallIPv6Allocation, + frrVersion: semver.MustParse("9.0.1"), wantFilePath: "frr.conf.firewall_ipv6", wantErr: nil, }, { name: "render machine", allocation: machineAllocation, + frrVersion: semver.MustParse("9.0.1"), wantFilePath: "frr.conf.machine", wantErr: nil, }, From af82b36e77a14747933cd3196ce944efc1dd54d8 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 08:00:18 +0100 Subject: [PATCH 047/102] Satisfy linter --- cmdexec.go | 86 ------------------------ cmdexec_test.go | 70 ------------------- pkg/installer/os/common/copy_ssh_keys.go | 7 +- pkg/interfaces/interfaces.go | 2 - pkg/nftables/nftables.go | 6 -- 5 files changed, 1 insertion(+), 170 deletions(-) delete mode 100644 cmdexec.go delete mode 100644 cmdexec_test.go diff --git a/cmdexec.go b/cmdexec.go deleted file mode 100644 index 5eba4cf..0000000 --- a/cmdexec.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "context" - "io" - "log/slog" - "os" - "os/exec" - "strings" - "time" -) - -type cmdexec struct { - log *slog.Logger - c func(ctx context.Context, name string, arg ...string) *exec.Cmd -} - -type cmdParams struct { - name string - args []string - dir string - timeout time.Duration - combined bool - stdin string - env []string -} - -func (i *cmdexec) command(p *cmdParams) (out string, err error) { - var ( - start = time.Now() - output []byte - ) - i.log.Info("running command", "command", strings.Join(append([]string{p.name}, p.args...), " "), "start", start.String()) - - ctx := context.Background() - if p.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, p.timeout) - defer cancel() - } - - cmd := i.c(ctx, p.name, p.args...) - if p.dir != "" { - cmd.Dir = "/etc/metal" - } - - cmd.Env = append(cmd.Env, p.env...) - - // show stderr - cmd.Stderr = os.Stderr - - if p.stdin != "" { - stdin, err := cmd.StdinPipe() - if err != nil { - return "", err - } - - go func() { - defer func() { - _ = stdin.Close() - }() - _, err = io.WriteString(stdin, p.stdin) - if err != nil { - i.log.Error("error when writing to command's stdin", "error", err) - } - }() - } - - if p.combined { - output, err = cmd.CombinedOutput() - } else { - output, err = cmd.Output() - } - - out = string(output) - took := time.Since(start) - - if err != nil { - i.log.Error("executed command with error", "output", out, "duration", took.String(), "error", err) - return "", err - } - - i.log.Info("executed command", "output", out, "duration", took.String()) - - return -} diff --git a/cmdexec_test.go b/cmdexec_test.go deleted file mode 100644 index eba25a6..0000000 --- a/cmdexec_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// tests were inspired by this blog article: https://npf.io/2015/06/testing-exec-command/ - -type fakeexec struct { - t *testing.T - mockCount int - mocks []fakeexecparams -} - -// nolint:musttag -type fakeexecparams struct { - WantCmd []string `json:"want_cmd"` - Output string `json:"output"` - ExitCode int `json:"exit_code"` -} - -func fakeCmd(t *testing.T, params ...fakeexecparams) func(ctx context.Context, command string, args ...string) *exec.Cmd { - f := fakeexec{ - t: t, - mocks: params, - } - return f.command -} - -func (f *fakeexec) command(ctx context.Context, command string, args ...string) *exec.Cmd { - if f.mockCount >= len(f.mocks) { - require.Fail(f.t, "more commands called than mocks are available") - } - - params := f.mocks[f.mockCount] - f.mockCount++ - - assert.Equal(f.t, params.WantCmd, append([]string{command}, args...)) - - j, err := json.Marshal(params) - require.NoError(f.t, err) - - cs := []string{"-test.run=TestHelperProcess", "--", string(j)} - cmd := exec.CommandContext(ctx, os.Args[0], cs...) //nolint - cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} - return cmd -} - -func TestHelperProcess(t *testing.T) { - if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { - return - } - - var f fakeexecparams - err := json.Unmarshal([]byte(os.Args[3]), &f) - require.NoError(t, err) - - _, err = fmt.Fprint(os.Stdout, f.Output) - require.NoError(t, err) - - os.Exit(f.ExitCode) -} diff --git a/pkg/installer/os/common/copy_ssh_keys.go b/pkg/installer/os/common/copy_ssh_keys.go index 0c8644f..5777f38 100644 --- a/pkg/installer/os/common/copy_ssh_keys.go +++ b/pkg/installer/os/common/copy_ssh_keys.go @@ -38,12 +38,7 @@ func (d *DefaultOS) CopySSHKeys(ctx context.Context) error { return err } - var lines []string - for _, key := range d.allocation.SshPublicKeys { - lines = append(lines, key) - } - - err = d.fs.WriteFile(sshAuthorizedKeysPath, []byte(strings.Join(lines, "\n")), 0600) + err = d.fs.WriteFile(sshAuthorizedKeysPath, []byte(strings.Join(d.allocation.SshPublicKeys, "\n")), 0600) if err != nil { return err } diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index cd9abbf..7f652a2 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -182,8 +182,6 @@ func configureLanInterfaces(ctx context.Context, cfg *Config) error { } func configureBridges(ctx context.Context, cfg *Config) error { - const offset = 20 - ifaces, err := cfg.Network.EVPNIfaces() if err != nil { return fmt.Errorf("unable to get evpn interfaces: %w", err) diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 5a0b0db..34d91c0 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -107,12 +107,6 @@ type ( Address string } - // NftablesValidator can validate configuration for nftables rules. - NftablesValidator struct { - path string - log *slog.Logger - } - NftablesReloader struct{} ) From cfd84e810f115e621e632fc8497f33657d9a75b6 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 08:03:51 +0100 Subject: [PATCH 048/102] More linter fixes --- pkg/installer/os/almalinux/install_bootloader.go | 2 +- pkg/installer/os/almalinux/write_ntp_conf.go | 2 +- pkg/installer/os/common/write_ntp_conf.go | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/installer/os/almalinux/install_bootloader.go b/pkg/installer/os/almalinux/install_bootloader.go index 11724ba..33b9ff6 100644 --- a/pkg/installer/os/almalinux/install_bootloader.go +++ b/pkg/installer/os/almalinux/install_bootloader.go @@ -105,7 +105,7 @@ func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { return nil } - v, err := o.DefaultOS.GetKernelVersion(o.InitramdiskFormatString()) + v, err := o.GetKernelVersion(o.InitramdiskFormatString()) if err != nil { return err } diff --git a/pkg/installer/os/almalinux/write_ntp_conf.go b/pkg/installer/os/almalinux/write_ntp_conf.go index bc04ff0..82bc5ff 100644 --- a/pkg/installer/os/almalinux/write_ntp_conf.go +++ b/pkg/installer/os/almalinux/write_ntp_conf.go @@ -26,5 +26,5 @@ func (o *os) WriteNTPConf(ctx context.Context) error { return fmt.Errorf("almalinux as firewall is currently not supported") } - return o.DefaultOS.WriteNtpConfToPath(chronyConfigPath, ntpServers) + return o.WriteNtpConfToPath(chronyConfigPath, ntpServers) } diff --git a/pkg/installer/os/common/write_ntp_conf.go b/pkg/installer/os/common/write_ntp_conf.go index 2ac95ac..e8f2822 100644 --- a/pkg/installer/os/common/write_ntp_conf.go +++ b/pkg/installer/os/common/write_ntp_conf.go @@ -45,9 +45,7 @@ func (d *DefaultOS) WriteNTPConf(ctx context.Context) error { return err } - d.WriteNtpConfToPath(ChronyConfigPath, ntpServers) - - return nil + return d.WriteNtpConfToPath(ChronyConfigPath, ntpServers) } return d.WriteNtpConfToPath(TimesyncdConfigPath, ntpServers) From 2596486e553f58ff5250fae98b5aa888f47856cc Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 08:18:05 +0100 Subject: [PATCH 049/102] Unexport --- go.mod | 4 +-- go.sum | 8 +++--- pkg/frr/frr.go | 64 ++++++++++++++++++++++----------------------- pkg/frr/routemap.go | 53 +++++++++++++++++-------------------- 4 files changed, 62 insertions(+), 67 deletions(-) diff --git a/go.mod b/go.mod index 927b63a..0c49f76 100644 --- a/go.mod +++ b/go.mod @@ -35,9 +35,9 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/vincent-petithory/dataurl v1.0.0 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/crypto v0.48.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8a5c3e4..3912772 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ go4.org v0.0.0-20160314031811-03efcb870d84/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1 go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -93,8 +93,8 @@ golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 985a035..3fd4c10 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -24,8 +24,8 @@ const ( frrConfigPath = "/etc/frr/frr.conf" - // frrVersion holds a string that is used in the frr.conf to define the FRR version. - frrVersion = "8.5" + // defaultFrrVersion holds a string that is used in the frr.conf to define the FRR version. + defaultFrrVersion = "8.5" // ipPrefixListSeqSeed specifies the initial value for prefix lists sequence number. ipPrefixListSeqSeed = 100 // ipPrefixListNoExportSuffix defines the suffix to use for private IP ranges that must not be exported. @@ -54,8 +54,8 @@ type ( fs afero.Fs } - // CommonFRRData contains attributes that are common to FRR configuration of all kind of bare metal servers. - CommonFRRData struct { + // commonFRRData contains attributes that are common to FRR configuration of all kind of bare metal servers. + commonFRRData struct { ASN int64 Comment string FRRVersion string @@ -63,46 +63,46 @@ type ( RouterID string } - // MachineFRRData contains attributes required to render frr.conf of bare metal servers that function as 'machine'. - MachineFRRData struct { - CommonFRRData + // machineFRRData contains attributes required to render frr.conf of bare metal servers that function as 'machine'. + machineFRRData struct { + commonFRRData } - // FirewallFRRData contains attributes required to render frr.conf of bare metal servers that function as 'firewall'. - FirewallFRRData struct { - CommonFRRData - VRFs []VRF + // firewallFRRData contains attributes required to render frr.conf of bare metal servers that function as 'firewall'. + firewallFRRData struct { + commonFRRData + VRFs []vrf } - // VRF represents data required to render VRF information into frr.conf. - VRF struct { + // vrf represents data required to render vrf information into frr.conf. + vrf struct { Comment string ID uint64 Table uint64 VNI uint64 ImportVRFNames []string - IPPrefixLists []IPPrefixList - RouteMaps []RouteMap - FRRVersion *FRR + IPPrefixLists []ipPrefixList + RouteMaps []routeMap + FRRVersion *frrVersion } - // IPPrefixList represents 'ip prefix-list' filtering mechanism to be used in combination with route-maps. - IPPrefixList struct { + // ipPrefixList represents 'ip prefix-list' filtering mechanism to be used in combination with route-maps. + ipPrefixList struct { Name string Spec string AddressFamily string // SourceVRF specifies from which VRF the given prefix list should be imported SourceVRF string } - // RouteMap represents a route-map to permit or deny routes. - RouteMap struct { + // routeMap represents a route-map to permit or deny routes. + routeMap struct { Name string Entries []string Policy string Order int } - FRR struct { + frrVersion struct { Major uint64 Minor uint64 } @@ -120,9 +120,9 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { if err != nil { return false, err } - data = MachineFRRData{ - CommonFRRData: CommonFRRData{ - FRRVersion: frrVersion, + data = machineFRRData{ + commonFRRData: commonFRRData{ + FRRVersion: defaultFrrVersion, Hostname: cfg.Network.Hostname(), Comment: comment, ASN: int64(net.Asn), @@ -140,9 +140,9 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return false, err } - data = FirewallFRRData{ - CommonFRRData: CommonFRRData{ - FRRVersion: frrVersion, + data = firewallFRRData{ + commonFRRData: commonFRRData{ + FRRVersion: defaultFrrVersion, Hostname: cfg.Network.Hostname(), Comment: comment, ASN: int64(net.Asn), @@ -212,10 +212,10 @@ func validate(frrConfigPath string) error { return nil } -func assembleVRFs(cfg *Config) ([]VRF, error) { +func assembleVRFs(cfg *Config) ([]vrf, error) { var ( - result []VRF - frr *FRR + result []vrf + frr *frrVersion ) if cfg.FRRVersion == nil { @@ -227,7 +227,7 @@ func assembleVRFs(cfg *Config) ([]VRF, error) { cfg.FRRVersion = frrVersion } - frr = &FRR{ + frr = &frrVersion{ Major: cfg.FRRVersion.Major(), Minor: cfg.FRRVersion.Minor(), } @@ -243,7 +243,7 @@ func assembleVRFs(cfg *Config) ([]VRF, error) { return nil, err } - vrf := VRF{ + vrf := vrf{ ID: n.Vrf, VNI: n.Vrf, ImportVRFNames: i.ImportVRFs, diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index e9ef175..15d2908 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -11,19 +11,19 @@ import ( ) const ( - // Permit defines an access policy that allows access. - Permit AccessPolicy = "permit" - // Deny defines an access policy that forbids access. - Deny AccessPolicy = "deny" + // permit defines an access policy that allows access. + permit accessPolicy = "permit" + // deny defines an access policy that forbids access. + deny accessPolicy = "deny" ) -// AccessPolicy is a type that represents a policy to manage access roles. type ( - AccessPolicy string + // accessPolicy is a type that represents a policy to manage access roles. + accessPolicy string importPrefix struct { Prefix netip.Prefix - Policy AccessPolicy + Policy accessPolicy SourceVRF string } @@ -33,11 +33,6 @@ type ( ImportPrefixes []importPrefix ImportPrefixesNoExport []importPrefix } - - ImportSettings struct { - ImportPrefixes []importPrefix - ImportPrefixesNoExport []importPrefix - } ) func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importRule, error) { @@ -71,7 +66,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR } i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: netip.PrefixFrom(parsed, bl), - Policy: Deny, + Policy: deny, SourceVRF: vrfNameOf(defaultNet), }) } @@ -97,7 +92,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR if !isThere { i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: ppfx, - Policy: Permit, + Policy: permit, SourceVRF: vrfNameOf(n), }) } @@ -123,7 +118,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR importExternalNet = true i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: netip.MustParsePrefix(pfx), - Policy: Permit, + Policy: permit, SourceVRF: vrfNameOf(e), }) } @@ -156,9 +151,9 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR return &i, nil } -func (i *importRule) prefixLists() []IPPrefixList { +func (i *importRule) prefixLists() []ipPrefixList { var ( - result []IPPrefixList + result []ipPrefixList seed = ipPrefixListSeqSeed afs = []apiv2.NetworkAddressFamily{apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4, apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6} ) @@ -180,13 +175,13 @@ func prefixLists( isExported bool, seed int, vrf string, -) []IPPrefixList { +) []ipPrefixList { afString := "ip" if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 { afString = "ipv6" } - var result []IPPrefixList + var result []ipPrefixList for _, p := range prefixes { if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4 && !p.Prefix.Addr().Is4() { @@ -205,7 +200,7 @@ func prefixLists( } name := p.name(vrf, isExported) - prefixList := IPPrefixList{ + prefixList := ipPrefixList{ Name: name, Spec: spec, AddressFamily: afString, @@ -236,7 +231,7 @@ func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { } result = append(result, importPrefix{ Prefix: ipp, - Policy: Permit, + Policy: permit, SourceVRF: sourceVrf, }) } @@ -276,8 +271,8 @@ func vrfNamesOf(networks []*apiv2.MachineNetwork) []string { return result } -func byName(prefixLists []IPPrefixList) map[string]IPPrefixList { - byName := map[string]IPPrefixList{} +func byName(prefixLists []ipPrefixList) map[string]ipPrefixList { + byName := map[string]ipPrefixList{} for _, prefixList := range prefixLists { if _, isPresent := byName[prefixList.Name]; isPresent { continue @@ -289,8 +284,8 @@ func byName(prefixLists []IPPrefixList) map[string]IPPrefixList { return byName } -func (i *importRule) routeMaps() []RouteMap { - var result []RouteMap +func (i *importRule) routeMaps() []routeMap { + var result []routeMap order := routeMapOrderSeed byName := byName(i.prefixLists()) @@ -311,9 +306,9 @@ func (i *importRule) routeMaps() []RouteMap { entries = append(entries, "set community additive no-export") } - routeMap := RouteMap{ + routeMap := routeMap{ Name: routeMapName(i.TargetVRF), - Policy: string(Permit), + Policy: string(permit), Order: order, Entries: entries, } @@ -322,9 +317,9 @@ func (i *importRule) routeMaps() []RouteMap { result = append(result, routeMap) } - routeMap := RouteMap{ + routeMap := routeMap{ Name: routeMapName(i.TargetVRF), - Policy: string(Deny), + Policy: string(deny), Order: order, } From 5e8ebf2db7b609b98ae87656fc0eace3798c5290 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 12 Mar 2026 08:18:23 +0100 Subject: [PATCH 050/102] Allow hooking into installer through config. --- api/v1/api.go | 21 ++++++ pkg/installer/installer.go | 72 +++++++++++++++---- pkg/installer/os/almalinux/almalinux.go | 18 ++--- .../os/almalinux/create_metal_user.go | 2 +- pkg/installer/os/common/cmd_line.go | 4 +- pkg/installer/os/common/configure_network.go | 2 +- pkg/installer/os/common/copy_ssh_keys.go | 2 +- pkg/installer/os/common/create_metal_user.go | 2 +- pkg/installer/os/common/defaultos.go | 27 +++++++ pkg/installer/os/common/fix_permissions.go | 2 +- pkg/installer/os/common/install_bootloader.go | 4 +- pkg/installer/os/common/oscommon.go | 52 +++++++++----- pkg/installer/os/common/process_userdata.go | 2 +- pkg/installer/os/common/systemd_services.go | 2 +- pkg/installer/os/common/unset_machine_id.go | 2 +- pkg/installer/os/common/write_boot_info.go | 2 +- pkg/installer/os/common/write_build_meta.go | 2 +- pkg/installer/os/common/write_hostname.go | 2 +- pkg/installer/os/common/write_hosts.go | 2 +- pkg/installer/os/common/write_ntp_conf.go | 4 +- pkg/installer/os/common/write_resolv_conf.go | 2 +- pkg/installer/os/debian/debian.go | 10 +-- pkg/installer/os/os.go | 38 +++++++--- pkg/installer/os/ubuntu/ubuntu.go | 10 +-- 24 files changed, 206 insertions(+), 80 deletions(-) create mode 100644 pkg/installer/os/common/defaultos.go diff --git a/api/v1/api.go b/api/v1/api.go index 08cd9ba..8e754cc 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -13,6 +13,27 @@ type Bootinfo struct { BootloaderID string `yaml:"bootloader_id"` } +const InstallerConfigPath = "/etc/metal/os-installer.yaml" + +// InstallerConfig can be placed inside the target OS to customize the os-installer. +type InstallerConfig struct { + // OsName enforces a specific os-installer implementation, defaults to auto-detection + OsName *string `yaml:"os_name"` + // Only allows to run installer tasks only with the given names + Only []string `yaml:"only"` + // Except allows to run installer tasks except for the given names + Except []string `yaml:"except"` + // CustomScript allows executing a custom script that's placed inside the OS at the end of the installer execution + CustomScript *struct { + ExecutablePath string `yaml:"executable_path"` + WorkDir string `yaml:"workdir"` + } `yaml:"custom_script"` + // Overwrites allows specifying os-installer overwrites for the default implementation + Overwrites struct { + BootloaderID *string `yaml:"bootloader_id"` + } +} + type MachineDetails struct { // Id is the machine UUID ID string `yaml:"id"` diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 7f3cc90..7d16a8b 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -4,20 +4,24 @@ import ( "context" "fmt" "log/slog" - "os" + "slices" "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" operatingsystem "github.com/metal-stack/os-installer/pkg/installer/os" oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" "github.com/spf13/afero" + "github.com/stretchr/testify/assert/yaml" ) type installer struct { - log *slog.Logger - oss oscommon.OperatingSystem - fs *afero.Afero + log *slog.Logger + cfg *v1.InstallerConfig + oss oscommon.OperatingSystem + fs *afero.Afero + exec *exec.CmdExecutor } func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, allocation *apiv2.MachineAllocation) error { @@ -28,22 +32,38 @@ func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, fs = &afero.Afero{ Fs: afero.OsFs{}, } + installerConfig = &v1.InstallerConfig{} ) + if oscommon.FileExists(fs, v1.InstallerConfigPath) { + data, err := fs.ReadFile(v1.InstallerConfigPath) + if err != nil { + return fmt.Errorf("unable to read installer config: %w", err) + } + + if err = yaml.Unmarshal(data, &installerConfig); err != nil { + return fmt.Errorf("unable to parse installer config: %w", err) + } + } + oss, err := operatingsystem.New(&oscommon.Config{ Log: log, Fs: fs, MachineDetails: details, Allocation: allocation, + Name: installerConfig.OsName, + BootloaderID: installerConfig.Overwrites.BootloaderID, }) if err != nil { return fmt.Errorf("os detection failed: %w", err) } i := installer{ - log: log, - oss: oss, - fs: fs, + log: log, + cfg: installerConfig, + oss: oss, + exec: exec.New(log), + fs: fs, } if err = i.run(ctx); err != nil { @@ -142,12 +162,26 @@ func (i *installer) run(ctx context.Context) error { name: "write /etc/metal/build-meta.yaml", fn: i.oss.WriteBuildMeta, }, + { + name: "execute custom executable", + fn: i.customExecutable, + }, } { var ( log = i.log.With("task-name", task.name) start = time.Now() ) + if len(i.cfg.Only) > 0 && !slices.Contains(i.cfg.Only, task.name) { + log.Info("skipping task as defined by installer configuration") + continue + } + + if slices.Contains(i.cfg.Except, task.name) { + log.Info("skipping task as defined by installer configuration") + continue + } + log.Info("running install task", "start-at", start.String()) if err := task.fn(ctx); err != nil { @@ -160,7 +194,7 @@ func (i *installer) run(ctx context.Context) error { } func (i *installer) validateRunningInEfiMode(ctx context.Context) error { - if !i.isVirtual() && !i.fileExists("/sys/firmware/efi") { + if !i.isVirtual() && !oscommon.FileExists(i.fs, "/sys/firmware/efi") { return fmt.Errorf("not running efi mode") } @@ -169,7 +203,7 @@ func (i *installer) validateRunningInEfiMode(ctx context.Context) error { func (i *installer) removeDockerEnv(_ context.Context) error { // systemd-detect-virt guesses docker which modifies the behavior of many services. - if !i.fileExists("/.dockerenv") { + if !oscommon.FileExists(i.fs, "/.dockerenv") { return nil } @@ -177,14 +211,22 @@ func (i *installer) removeDockerEnv(_ context.Context) error { } func (i *installer) isVirtual() bool { - return !i.fileExists("/sys/class/dmi") + return !oscommon.FileExists(i.fs, "/sys/class/dmi") } -func (i *installer) fileExists(filename string) bool { - info, err := i.fs.Stat(filename) - if os.IsNotExist(err) { - return false +func (i *installer) customExecutable(ctx context.Context) error { + if i.cfg.CustomScript == nil { + i.log.Info("no custom executable to execute, skipping") + return nil + } + + _, err := i.exec.Execute(ctx, &exec.Params{ + Name: i.cfg.CustomScript.ExecutablePath, + Dir: i.cfg.CustomScript.WorkDir, + }) + if err != nil { + return fmt.Errorf("custom executable returned an error code: %w", err) } - return !info.IsDir() + return nil } diff --git a/pkg/installer/os/almalinux/almalinux.go b/pkg/installer/os/almalinux/almalinux.go index 8f178b7..c11d1b1 100644 --- a/pkg/installer/os/almalinux/almalinux.go +++ b/pkg/installer/os/almalinux/almalinux.go @@ -14,7 +14,7 @@ import ( type ( os struct { - *oscommon.DefaultOS + *oscommon.CommonTasks log *slog.Logger details *v1.MachineDetails allocation *apiv2.MachineAllocation @@ -26,13 +26,13 @@ type ( func New(cfg *oscommon.Config) *os { return &os{ - DefaultOS: oscommon.New(cfg), - log: cfg.Log, - details: cfg.MachineDetails, - allocation: cfg.Allocation, - exec: cfg.Exec, - network: network.New(cfg.Allocation), - fs: cfg.Fs, + CommonTasks: oscommon.New(cfg), + log: cfg.Log, + details: cfg.MachineDetails, + allocation: cfg.Allocation, + exec: cfg.Exec, + network: network.New(cfg.Allocation), + fs: cfg.Fs, } } @@ -49,5 +49,5 @@ func (o *os) InitramdiskFormatString() string { } func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { - return o.DefaultOS.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) + return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } diff --git a/pkg/installer/os/almalinux/create_metal_user.go b/pkg/installer/os/almalinux/create_metal_user.go index d13b53d..f99cb85 100644 --- a/pkg/installer/os/almalinux/create_metal_user.go +++ b/pkg/installer/os/almalinux/create_metal_user.go @@ -8,7 +8,7 @@ import ( ) func (o *os) CreateMetalUser(ctx context.Context) error { - err := o.DefaultOS.CreateMetalUser(ctx, o.SudoGroup()) + err := o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) if err != nil { return err } diff --git a/pkg/installer/os/common/cmd_line.go b/pkg/installer/os/common/cmd_line.go index 159ed4d..f613714 100644 --- a/pkg/installer/os/common/cmd_line.go +++ b/pkg/installer/os/common/cmd_line.go @@ -9,7 +9,7 @@ import ( "github.com/metal-stack/os-installer/pkg/exec" ) -func (d *DefaultOS) BuildCMDLine(ctx context.Context) (string, error) { +func (d *CommonTasks) BuildCMDLine(ctx context.Context) (string, error) { parts := []string{ fmt.Sprintf("console=%s", d.details.Console), fmt.Sprintf("root=UUID=%s", d.details.RootUUID), @@ -37,7 +37,7 @@ func (d *DefaultOS) BuildCMDLine(ctx context.Context) (string, error) { return strings.Join(parts, " "), nil } -func (d *DefaultOS) findMDUUID(ctx context.Context) (mdUUID string, found bool, err error) { +func (d *CommonTasks) findMDUUID(ctx context.Context) (mdUUID string, found bool, err error) { d.log.Info("detect software raid uuid") if !d.details.RaidEnabled { diff --git a/pkg/installer/os/common/configure_network.go b/pkg/installer/os/common/configure_network.go index 6d9d3cd..9db5dcc 100644 --- a/pkg/installer/os/common/configure_network.go +++ b/pkg/installer/os/common/configure_network.go @@ -9,7 +9,7 @@ import ( "github.com/metal-stack/os-installer/pkg/nftables" ) -func (d *DefaultOS) ConfigureNetwork(ctx context.Context) error { +func (d *CommonTasks) ConfigureNetwork(ctx context.Context) error { if err := interfaces.ConfigureInterfaces(ctx, &interfaces.Config{ Log: d.log, Network: d.network, diff --git a/pkg/installer/os/common/copy_ssh_keys.go b/pkg/installer/os/common/copy_ssh_keys.go index 5777f38..dd3d5f4 100644 --- a/pkg/installer/os/common/copy_ssh_keys.go +++ b/pkg/installer/os/common/copy_ssh_keys.go @@ -8,7 +8,7 @@ import ( "strings" ) -func (d *DefaultOS) CopySSHKeys(ctx context.Context) error { +func (d *CommonTasks) CopySSHKeys(ctx context.Context) error { var ( sshPath = path.Join("/home", metalUser, ".ssh") sshAuthorizedKeysPath = path.Join(sshPath, "authorized_keys") diff --git a/pkg/installer/os/common/create_metal_user.go b/pkg/installer/os/common/create_metal_user.go index 5f94217..b6339ee 100644 --- a/pkg/installer/os/common/create_metal_user.go +++ b/pkg/installer/os/common/create_metal_user.go @@ -13,7 +13,7 @@ const ( metalUser = "metal" ) -func (d *DefaultOS) CreateMetalUser(ctx context.Context, sudoGroup string) error { +func (d *CommonTasks) CreateMetalUser(ctx context.Context, sudoGroup string) error { u, err := user.Lookup(metalUser) if err != nil { if err.Error() != user.UnknownUserError(metalUser).Error() { diff --git a/pkg/installer/os/common/defaultos.go b/pkg/installer/os/common/defaultos.go new file mode 100644 index 0000000..53220d2 --- /dev/null +++ b/pkg/installer/os/common/defaultos.go @@ -0,0 +1,27 @@ +package oscommon + +import "context" + +type ( + defaultOS struct { + *CommonTasks + } +) + +func NewDefaultOS(cfg *Config) *defaultOS { + return &defaultOS{ + CommonTasks: New(cfg), + } +} + +func (o *defaultOS) WriteBootInfo(ctx context.Context, cmdLine string) error { + return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) +} + +func (o *defaultOS) CreateMetalUser(ctx context.Context) error { + return o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) +} + +func (o *defaultOS) GrubInstall(ctx context.Context, cmdLine string) error { + return o.CommonTasks.GrubInstall(ctx, o.BootloaderID(), cmdLine) +} diff --git a/pkg/installer/os/common/fix_permissions.go b/pkg/installer/os/common/fix_permissions.go index f6f738b..8409fef 100644 --- a/pkg/installer/os/common/fix_permissions.go +++ b/pkg/installer/os/common/fix_permissions.go @@ -5,7 +5,7 @@ import ( "io/fs" ) -func (d *DefaultOS) FixPermissions(ctx context.Context) error { +func (d *CommonTasks) FixPermissions(ctx context.Context) error { for p, perm := range map[string]fs.FileMode{ "/var/tmp": 01777, } { diff --git a/pkg/installer/os/common/install_bootloader.go b/pkg/installer/os/common/install_bootloader.go index 7d24b71..6a62db4 100644 --- a/pkg/installer/os/common/install_bootloader.go +++ b/pkg/installer/os/common/install_bootloader.go @@ -22,7 +22,7 @@ GRUB_SERIAL_COMMAND="serial --speed=%s --unit=%s --word=8" ` ) -func (d *DefaultOS) GrubInstall(ctx context.Context, bootloaderID, cmdLine string) error { +func (d *CommonTasks) GrubInstall(ctx context.Context, bootloaderID, cmdLine string) error { serialPort, serialSpeed, err := d.FigureOutSerialSpeed() if err != nil { return err @@ -134,7 +134,7 @@ func (d *DefaultOS) GrubInstall(ctx context.Context, bootloaderID, cmdLine strin return nil } -func (d *DefaultOS) FigureOutSerialSpeed() (serialPort, serialSpeed string, err error) { +func (d *CommonTasks) FigureOutSerialSpeed() (serialPort, serialSpeed string, err error) { // ttyS1,115200n8 serialPort, serialSpeed, found := strings.Cut(d.details.Console, ",") if !found { diff --git a/pkg/installer/os/common/oscommon.go b/pkg/installer/os/common/oscommon.go index 8833f3b..b95846b 100644 --- a/pkg/installer/os/common/oscommon.go +++ b/pkg/installer/os/common/oscommon.go @@ -45,42 +45,53 @@ type ( Exec *exec.CmdExecutor MachineDetails *v1.MachineDetails Allocation *apiv2.MachineAllocation + + // customization options from installer config + Name *string + BootloaderID *string } - DefaultOS struct { + CommonTasks struct { log *slog.Logger fs *afero.Afero details *v1.MachineDetails allocation *apiv2.MachineAllocation exec *exec.CmdExecutor network *network.Network + + bootloaderID *string } ) -func New(cfg *Config) *DefaultOS { - return &DefaultOS{ - log: cfg.Log, - fs: cfg.Fs, - details: cfg.MachineDetails, - allocation: cfg.Allocation, - exec: cfg.Exec, - network: network.New(cfg.Allocation), +func New(cfg *Config) *CommonTasks { + return &CommonTasks{ + log: cfg.Log, + fs: cfg.Fs, + details: cfg.MachineDetails, + allocation: cfg.Allocation, + exec: cfg.Exec, + network: network.New(cfg.Allocation), + bootloaderID: cfg.BootloaderID, } } -func (d *DefaultOS) SudoGroup() string { +func (d *CommonTasks) SudoGroup() string { return "sudo" } -func (d *DefaultOS) BootloaderID() string { - panic("default os does not provide a bootloader id") +func (d *CommonTasks) BootloaderID() string { + if d.bootloaderID == nil { + panic("no bootloader id provided for default os") + } + + return *d.bootloaderID } -func (d *DefaultOS) InitramdiskFormatString() string { +func (d *CommonTasks) InitramdiskFormatString() string { return "initrd.img-%s" } -func (d *DefaultOS) GetKernelVersion(initramdiskFormatString string) (string, error) { +func (d *CommonTasks) GetKernelVersion(initramdiskFormatString string) (string, error) { kern, _, err := d.KernelAndInitrdPath(initramdiskFormatString) if err != nil { return "", err @@ -94,7 +105,7 @@ func (d *DefaultOS) GetKernelVersion(initramdiskFormatString string) (string, er return version, nil } -func (d *DefaultOS) KernelAndInitrdPath(initramdiskFormatString string) (kern string, initrd string, err error) { +func (d *CommonTasks) KernelAndInitrdPath(initramdiskFormatString string) (kern string, initrd string, err error) { // Debian 10 // root@1f223b59051bcb12:/boot# ls -l // total 83500 @@ -168,7 +179,7 @@ func (d *DefaultOS) KernelAndInitrdPath(initramdiskFormatString string) (kern st return } -func (d *DefaultOS) fileExists(filename string) bool { +func (d *CommonTasks) fileExists(filename string) bool { info, err := d.fs.Stat(filename) if os.IsNotExist(err) { return false @@ -186,3 +197,12 @@ func runFromCI() bool { return ci } + +func FileExists(fs *afero.Afero, filename string) bool { + info, err := fs.Stat(filename) + if os.IsNotExist(err) { + return false + } + + return !info.IsDir() +} diff --git a/pkg/installer/os/common/process_userdata.go b/pkg/installer/os/common/process_userdata.go index 53a0f26..93784d3 100644 --- a/pkg/installer/os/common/process_userdata.go +++ b/pkg/installer/os/common/process_userdata.go @@ -14,7 +14,7 @@ const ( ignitionUserdataPath = "/etc/metal/config.ign" ) -func (d *DefaultOS) ProcessUserdata(ctx context.Context) error { +func (d *CommonTasks) ProcessUserdata(ctx context.Context) error { if ok := d.fileExists(UserdataPath); !ok { d.log.Info("no userdata present, not processing userdata", "path", UserdataPath) return nil diff --git a/pkg/installer/os/common/systemd_services.go b/pkg/installer/os/common/systemd_services.go index a72189f..2169c54 100644 --- a/pkg/installer/os/common/systemd_services.go +++ b/pkg/installer/os/common/systemd_services.go @@ -6,6 +6,6 @@ import ( "github.com/metal-stack/os-installer/pkg/services" ) -func (d *DefaultOS) SystemdServices(ctx context.Context) error { +func (d *CommonTasks) SystemdServices(ctx context.Context) error { return services.WriteSystemdServices(ctx, d.log, d.network, d.details.ID) } diff --git a/pkg/installer/os/common/unset_machine_id.go b/pkg/installer/os/common/unset_machine_id.go index 006335f..d3361d5 100644 --- a/pkg/installer/os/common/unset_machine_id.go +++ b/pkg/installer/os/common/unset_machine_id.go @@ -7,7 +7,7 @@ const ( DbusMachineID = "/var/lib/dbus/machine-id" ) -func (d *DefaultOS) UnsetMachineID(ctx context.Context) error { +func (d *CommonTasks) UnsetMachineID(ctx context.Context) error { for _, filePath := range []string{EtcMachineID, DbusMachineID} { if !d.fileExists(filePath) { continue diff --git a/pkg/installer/os/common/write_boot_info.go b/pkg/installer/os/common/write_boot_info.go index 01b5818..70e937b 100644 --- a/pkg/installer/os/common/write_boot_info.go +++ b/pkg/installer/os/common/write_boot_info.go @@ -12,7 +12,7 @@ const ( BootInfoPath = "/etc/metal/boot-info.yaml" ) -func (d *DefaultOS) WriteBootInfo(ctx context.Context, initramdiskFormatString, bootloaderID, cmdLine string) error { +func (d *CommonTasks) WriteBootInfo(ctx context.Context, initramdiskFormatString, bootloaderID, cmdLine string) error { kern, initrd, err := d.KernelAndInitrdPath(initramdiskFormatString) if err != nil { return err diff --git a/pkg/installer/os/common/write_build_meta.go b/pkg/installer/os/common/write_build_meta.go index d53562d..78907f4 100644 --- a/pkg/installer/os/common/write_build_meta.go +++ b/pkg/installer/os/common/write_build_meta.go @@ -14,7 +14,7 @@ const ( BuildMetaPath = "/etc/metal/build-meta.yaml" ) -func (d *DefaultOS) WriteBuildMeta(ctx context.Context) error { +func (d *CommonTasks) WriteBuildMeta(ctx context.Context) error { d.log.Info("writing build meta file", "path", BuildMetaPath) meta := &v1.BuildMeta{ diff --git a/pkg/installer/os/common/write_hostname.go b/pkg/installer/os/common/write_hostname.go index 3a42ea0..1bcfc70 100644 --- a/pkg/installer/os/common/write_hostname.go +++ b/pkg/installer/os/common/write_hostname.go @@ -8,6 +8,6 @@ const ( HostnameFilePath = "/etc/hostname" ) -func (d *DefaultOS) WriteHostname(ctx context.Context) error { +func (d *CommonTasks) WriteHostname(ctx context.Context) error { return d.fs.WriteFile(HostnameFilePath, []byte(d.allocation.Hostname), 0644) } diff --git a/pkg/installer/os/common/write_hosts.go b/pkg/installer/os/common/write_hosts.go index 387e4ee..fd3ad89 100644 --- a/pkg/installer/os/common/write_hosts.go +++ b/pkg/installer/os/common/write_hosts.go @@ -13,7 +13,7 @@ const ( ` ) -func (d *DefaultOS) WriteHosts(ctx context.Context) error { +func (d *CommonTasks) WriteHosts(ctx context.Context) error { ips, err := d.network.PrivatePrimaryIPs() if err != nil { return err diff --git a/pkg/installer/os/common/write_ntp_conf.go b/pkg/installer/os/common/write_ntp_conf.go index e8f2822..a3bb449 100644 --- a/pkg/installer/os/common/write_ntp_conf.go +++ b/pkg/installer/os/common/write_ntp_conf.go @@ -14,7 +14,7 @@ const ( ChronyConfigPath = "/etc/chrony/chrony.conf" ) -func (d *DefaultOS) WriteNTPConf(ctx context.Context) error { +func (d *CommonTasks) WriteNTPConf(ctx context.Context) error { if len(d.allocation.NtpServer) == 0 { return nil } @@ -51,7 +51,7 @@ func (d *DefaultOS) WriteNTPConf(ctx context.Context) error { return d.WriteNtpConfToPath(TimesyncdConfigPath, ntpServers) } -func (d *DefaultOS) WriteNtpConfToPath(configPath string, ntpServers []string) error { +func (d *CommonTasks) WriteNtpConfToPath(configPath string, ntpServers []string) error { content := fmt.Sprintf("[Time]\nNTP=%s\n", strings.Join(ntpServers, " ")) err := d.fs.Remove(configPath) diff --git a/pkg/installer/os/common/write_resolv_conf.go b/pkg/installer/os/common/write_resolv_conf.go index e0cd310..02ca88c 100644 --- a/pkg/installer/os/common/write_resolv_conf.go +++ b/pkg/installer/os/common/write_resolv_conf.go @@ -11,7 +11,7 @@ const ( ResolvConfPath = "/etc/resolv.conf" ) -func (d *DefaultOS) WriteResolvConf(ctx context.Context) error { +func (d *CommonTasks) WriteResolvConf(ctx context.Context) error { d.log.Info("write configuration", "file", ResolvConfPath) // Must be written here because during docker build this file is synthetic err := d.fs.Remove(ResolvConfPath) diff --git a/pkg/installer/os/debian/debian.go b/pkg/installer/os/debian/debian.go index 29cac34..5ae11d8 100644 --- a/pkg/installer/os/debian/debian.go +++ b/pkg/installer/os/debian/debian.go @@ -8,13 +8,13 @@ import ( type ( os struct { - *oscommon.DefaultOS + *oscommon.CommonTasks } ) func New(cfg *oscommon.Config) *os { return &os{ - DefaultOS: oscommon.New(cfg), + CommonTasks: oscommon.New(cfg), } } @@ -23,13 +23,13 @@ func (o *os) BootloaderID() string { } func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { - return o.DefaultOS.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) + return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } func (o *os) CreateMetalUser(ctx context.Context) error { - return o.DefaultOS.CreateMetalUser(ctx, o.SudoGroup()) + return o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) } func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { - return o.DefaultOS.GrubInstall(ctx, o.BootloaderID(), cmdLine) + return o.CommonTasks.GrubInstall(ctx, o.BootloaderID(), cmdLine) } diff --git a/pkg/installer/os/os.go b/pkg/installer/os/os.go index 8be1946..f9306aa 100644 --- a/pkg/installer/os/os.go +++ b/pkg/installer/os/os.go @@ -29,6 +29,14 @@ func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { } } + if cfg.Name != nil { + return fromOsName(*cfg.Name, cfg) + } + + return detectOS(cfg) +} + +func detectOS(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { content, err := cfg.Fs.ReadFile("/etc/os-release") if err != nil { return nil, err @@ -48,17 +56,25 @@ func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { os = unquoted } - switch os := osName(strings.ToLower(os)); os { - case ubuntuOS: - return ubuntu.New(cfg), nil - case debianOS: - return debian.New(cfg), nil - case almalinuxOS: - return almalinux.New(cfg), nil - default: - return nil, fmt.Errorf("unsupported operating system: %s", os) - } + return fromOsName(os, cfg) } - return nil, fmt.Errorf("unable to detect OS") + return nil, fmt.Errorf("unable to detect os, no ID field found /etc/os-release") +} + +func fromOsName(name string, cfg *oscommon.Config) (oscommon.OperatingSystem, error) { + switch os := osName(strings.ToLower(name)); os { + case ubuntuOS: + cfg.Log.Info("using ubuntu os-installer") + return ubuntu.New(cfg), nil + case debianOS: + cfg.Log.Info("using debian os-installer") + return debian.New(cfg), nil + case almalinuxOS: + cfg.Log.Info("using almalinux os-installer") + return almalinux.New(cfg), nil + default: + cfg.Log.Info("using default os-installer implementation") + return oscommon.NewDefaultOS(cfg), nil + } } diff --git a/pkg/installer/os/ubuntu/ubuntu.go b/pkg/installer/os/ubuntu/ubuntu.go index ed12844..c61768a 100644 --- a/pkg/installer/os/ubuntu/ubuntu.go +++ b/pkg/installer/os/ubuntu/ubuntu.go @@ -8,13 +8,13 @@ import ( type ( os struct { - *oscommon.DefaultOS + *oscommon.CommonTasks } ) func New(cfg *oscommon.Config) *os { return &os{ - DefaultOS: oscommon.New(cfg), + CommonTasks: oscommon.New(cfg), } } @@ -23,13 +23,13 @@ func (o *os) BootloaderID() string { } func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { - return o.DefaultOS.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) + return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } func (o *os) CreateMetalUser(ctx context.Context) error { - return o.DefaultOS.CreateMetalUser(ctx, o.SudoGroup()) + return o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) } func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { - return o.DefaultOS.GrubInstall(ctx, o.BootloaderID(), cmdLine) + return o.CommonTasks.GrubInstall(ctx, o.BootloaderID(), cmdLine) } From a55ed1a89f31f142ca1a01e42221aa1502c5e808 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 08:23:47 +0100 Subject: [PATCH 051/102] simpler --- pkg/frr/frr.go | 46 ++++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 3fd4c10..6c776a5 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -54,24 +54,14 @@ type ( fs afero.Fs } - // commonFRRData contains attributes that are common to FRR configuration of all kind of bare metal servers. - commonFRRData struct { + // frrData contains attributes to hold FRR configuration of all kind of bare metal servers. + frrData struct { ASN int64 Comment string FRRVersion string Hostname string RouterID string - } - - // machineFRRData contains attributes required to render frr.conf of bare metal servers that function as 'machine'. - machineFRRData struct { - commonFRRData - } - - // firewallFRRData contains attributes required to render frr.conf of bare metal servers that function as 'firewall'. - firewallFRRData struct { - commonFRRData - VRFs []vrf + VRFs []vrf } // vrf represents data required to render vrf information into frr.conf. @@ -120,14 +110,12 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { if err != nil { return false, err } - data = machineFRRData{ - commonFRRData: commonFRRData{ - FRRVersion: defaultFrrVersion, - Hostname: cfg.Network.Hostname(), - Comment: comment, - ASN: int64(net.Asn), - RouterID: routerID(net), - }, + data = frrData{ + FRRVersion: defaultFrrVersion, + Hostname: cfg.Network.Hostname(), + Comment: comment, + ASN: int64(net.Asn), + RouterID: routerID(net), } template = machineTemplateString } else { @@ -140,15 +128,13 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return false, err } - data = firewallFRRData{ - commonFRRData: commonFRRData{ - FRRVersion: defaultFrrVersion, - Hostname: cfg.Network.Hostname(), - Comment: comment, - ASN: int64(net.Asn), - RouterID: routerID(net), - }, - VRFs: vrfs, + data = frrData{ + FRRVersion: defaultFrrVersion, + Hostname: cfg.Network.Hostname(), + Comment: comment, + ASN: int64(net.Asn), + RouterID: routerID(net), + VRFs: vrfs, } template = firewallTemplateString } From 8f4114ac33ade31eeb060459d0859ddbdf840df4 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 08:28:30 +0100 Subject: [PATCH 052/102] Unexport --- pkg/nftables/nftables.go | 60 +++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 34d91c0..ff26a50 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -66,48 +66,46 @@ type ( // NftablesData represents the information required to render nftables configuration. NftablesData struct { Comment string - SNAT []SNAT - DNSProxyDNAT DNAT + SNAT []snat + DNSProxyDNAT dnat VPN bool ForwardPolicy string - FirewallRules *FirewallRules - Input Input + FirewallRules *firewallRules + Input input } - Input struct { + input struct { InInterfaces []string } - FirewallRules struct { + firewallRules struct { Egress []string Ingress []string } - // SNAT holds the information required to configure Source NAT. - SNAT struct { + // snat holds the information required to configure Source NAT. + snat struct { Comment string OutInterface string - OutIntSpec AddrSpec - SourceSpecs []AddrSpec + OutIntSpec addrSpec + SourceSpecs []addrSpec } - // DNAT holds the information required to configure DNAT. - DNAT struct { + // dnat holds the information required to configure dnat. + dnat struct { Comment string InInterfaces []string SAddr string DAddr string Port string Zone string - DestSpec AddrSpec + DestSpec addrSpec } - AddrSpec struct { + addrSpec struct { AddressFamily string Address string } - - NftablesReloader struct{} ) // Renders renders nftables rules according to the given input data and reloads the service if necessary @@ -169,8 +167,8 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return } -func getInput(cfg *Config) Input { - input := Input{} +func getInput(cfg *Config) input { + input := input{} for _, n := range cfg.Network.AllocationNetworks() { switch n.NetworkType { case apiv2.NetworkType_NETWORK_TYPE_CHILD, apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: @@ -180,9 +178,9 @@ func getInput(cfg *Config) Input { return input } -func getSNAT(cfg *Config) ([]SNAT, error) { +func getSNAT(cfg *Config) ([]snat, error) { var ( - result []SNAT + result []snat defaultNetwork *apiv2.MachineNetwork defaultAF string ) @@ -216,7 +214,7 @@ func getSNAT(cfg *Config) ([]SNAT, error) { cfg.Log.Info("getSNAT", "network", n.Network) var ( - sources []AddrSpec + sources []addrSpec cmt = fmt.Sprintf("snat (networkid: %s)", n.Network) svi = fmt.Sprintf("vlan%d", n.Vrf) vrf = fmt.Sprintf("vrf%d", n.Vrf) @@ -228,21 +226,21 @@ func getSNAT(cfg *Config) ([]SNAT, error) { return nil, fmt.Errorf("unable to determine address family: %w", err) } - sources = append(sources, AddrSpec{ + sources = append(sources, addrSpec{ Address: pfx, AddressFamily: af, }) cfg.Log.Info("getSNAT", "network", n.Network, "prefixes", pfx, "af", af) } - s := SNAT{ + s := snat{ Comment: cmt, OutInterface: svi, SourceSpecs: sources, } if cfg.EnableDNSProxy && (vrf == defaultNetworkName) { - s.OutIntSpec = AddrSpec{ + s.OutIntSpec = addrSpec{ AddressFamily: defaultAF, Address: defaultNetwork.Ips[0], } @@ -254,7 +252,7 @@ func getSNAT(cfg *Config) ([]SNAT, error) { return result, nil } -func getDNSProxyDNAT(cfg *Config) DNAT { +func getDNSProxyDNAT(cfg *Config) dnat { svis := []string{} for _, n := range cfg.Network.AllocationNetworks() { switch n.NetworkType { @@ -267,7 +265,7 @@ func getDNSProxyDNAT(cfg *Config) DNAT { n, err := cfg.Network.GetDefaultRouteNetwork() if err != nil { - return DNAT{} + return dnat{} } ip, _ := netip.ParseAddr(n.Ips[0]) @@ -279,23 +277,23 @@ func getDNSProxyDNAT(cfg *Config) DNAT { saddr = "fd00::/8" daddr = "@proxy_dns_servers_v6" } - return DNAT{ + return dnat{ Comment: "dnat to dns proxy", InInterfaces: svis, SAddr: saddr, DAddr: daddr, Port: dnsPort, Zone: dnsProxyZone, - DestSpec: AddrSpec{ + DestSpec: addrSpec{ AddressFamily: af, Address: n.Ips[0], }, } } -func getFirewallRules(cfg *Config) (*FirewallRules, error) { +func getFirewallRules(cfg *Config) (*firewallRules, error) { if cfg.Network.FirewallRules() == nil { - return &FirewallRules{}, nil + return &firewallRules{}, nil } var ( egressRules = []string{"# egress rules specified during firewall creation"} @@ -363,7 +361,7 @@ func getFirewallRules(cfg *Config) (*FirewallRules, error) { ingressRules = append(ingressRules, fmt.Sprintf("%s %s saddr %s %s dport { %s } counter accept comment %q", destinationSpec, af, saddr, strings.ToLower(*protocolString), strings.Join(ports, ","), r.Comment)) } } - return &FirewallRules{ + return &firewallRules{ Egress: egressRules, Ingress: ingressRules, }, nil From 9c584cc1d1d4be104cc9080a017b359ee8d44bb8 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 08:30:41 +0100 Subject: [PATCH 053/102] Unexport --- pkg/network/network.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/network/network.go b/pkg/network/network.go index 6642a0d..537b579 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -14,10 +14,10 @@ const ( // mtuMachine defines the value for MTU specific to the needs of a machine. mtuMachine = 9000 - // IPv4ZeroCIDR is the CIDR block for the whole IPv4 address space - IPv4ZeroCIDR = "0.0.0.0/0" - // IPv6ZeroCIDR is the CIDR block for the whole IPv6 address space - IPv6ZeroCIDR = "::/0" + // ipv4ZeroCIDR is the CIDR block for the whole IPv4 address space + ipv4ZeroCIDR = "0.0.0.0/0" + // ipv6ZeroCIDR is the CIDR block for the whole IPv6 address space + ipv6ZeroCIDR = "::/0" ) type ( @@ -305,7 +305,7 @@ func (n *Network) GetTenantNetworkVrfName() (string, error) { func ContainsDefaultRoute(prefixes []string) bool { for _, prefix := range prefixes { - if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR { + if prefix == ipv4ZeroCIDR || prefix == ipv6ZeroCIDR { return true } } From 52b1bf7ff402be7eb487b71ef370477455a04aa6 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 12 Mar 2026 08:53:45 +0100 Subject: [PATCH 054/102] Fixes. --- pkg/installer/installer.go | 2 +- .../os/almalinux/install_bootloader.go | 12 ----------- pkg/installer/os/common/defaultos.go | 12 ++++++++++- pkg/installer/os/common/oscommon.go | 21 +++++++------------ .../os/debian/tests/write_boot_info_test.go | 2 +- pkg/installer/os/os.go | 12 ++++++++++- .../os/ubuntu/tests/write_boot_info_test.go | 2 +- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 7d16a8b..6354b67 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -13,7 +13,7 @@ import ( operatingsystem "github.com/metal-stack/os-installer/pkg/installer/os" oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" "github.com/spf13/afero" - "github.com/stretchr/testify/assert/yaml" + "go.yaml.in/yaml/v3" ) type installer struct { diff --git a/pkg/installer/os/almalinux/install_bootloader.go b/pkg/installer/os/almalinux/install_bootloader.go index 33b9ff6..89bab78 100644 --- a/pkg/installer/os/almalinux/install_bootloader.go +++ b/pkg/installer/os/almalinux/install_bootloader.go @@ -37,16 +37,6 @@ func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { return err } - grubInstallArgs := []string{ - "--target=x86_64-efi", - "--efi-directory=/boot/efi", - "--boot-directory=/boot", - "--bootloader-id=" + o.BootloaderID(), - } - if o.details.RaidEnabled { - grubInstallArgs = append(grubInstallArgs, "--no-nvram") - } - _, err = o.exec.Execute(ctx, &exec.Params{ Name: "grub2-mkconfig", Args: []string{"-o", grubConfigPath}, @@ -55,8 +45,6 @@ func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { return err } - grubInstallArgs = append(grubInstallArgs, fmt.Sprintf("UUID=%s", o.details.RootUUID)) - if o.details.RaidEnabled { out, err := o.exec.Execute(ctx, &exec.Params{ Name: "mdadm", diff --git a/pkg/installer/os/common/defaultos.go b/pkg/installer/os/common/defaultos.go index 53220d2..dbdd8d1 100644 --- a/pkg/installer/os/common/defaultos.go +++ b/pkg/installer/os/common/defaultos.go @@ -5,15 +5,25 @@ import "context" type ( defaultOS struct { *CommonTasks + bootloaderID *string } ) func NewDefaultOS(cfg *Config) *defaultOS { return &defaultOS{ - CommonTasks: New(cfg), + CommonTasks: New(cfg), + bootloaderID: cfg.BootloaderID, } } +func (o *defaultOS) BootloaderID() string { + if o.bootloaderID == nil { + panic("no bootloader id provided for default os") + } + + return *o.bootloaderID +} + func (o *defaultOS) WriteBootInfo(ctx context.Context, cmdLine string) error { return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } diff --git a/pkg/installer/os/common/oscommon.go b/pkg/installer/os/common/oscommon.go index b95846b..1187d25 100644 --- a/pkg/installer/os/common/oscommon.go +++ b/pkg/installer/os/common/oscommon.go @@ -58,20 +58,17 @@ type ( allocation *apiv2.MachineAllocation exec *exec.CmdExecutor network *network.Network - - bootloaderID *string } ) func New(cfg *Config) *CommonTasks { return &CommonTasks{ - log: cfg.Log, - fs: cfg.Fs, - details: cfg.MachineDetails, - allocation: cfg.Allocation, - exec: cfg.Exec, - network: network.New(cfg.Allocation), - bootloaderID: cfg.BootloaderID, + log: cfg.Log, + fs: cfg.Fs, + details: cfg.MachineDetails, + allocation: cfg.Allocation, + exec: cfg.Exec, + network: network.New(cfg.Allocation), } } @@ -80,11 +77,7 @@ func (d *CommonTasks) SudoGroup() string { } func (d *CommonTasks) BootloaderID() string { - if d.bootloaderID == nil { - panic("no bootloader id provided for default os") - } - - return *d.bootloaderID + panic("common tasks do not provide a bootloader id") } func (d *CommonTasks) InitramdiskFormatString() string { diff --git a/pkg/installer/os/debian/tests/write_boot_info_test.go b/pkg/installer/os/debian/tests/write_boot_info_test.go index 1f863d3..731039f 100644 --- a/pkg/installer/os/debian/tests/write_boot_info_test.go +++ b/pkg/installer/os/debian/tests/write_boot_info_test.go @@ -13,8 +13,8 @@ import ( "github.com/metal-stack/os-installer/pkg/test" "github.com/spf13/afero" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/assert/yaml" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" ) func Test_os_WriteBootInfo(t *testing.T) { diff --git a/pkg/installer/os/os.go b/pkg/installer/os/os.go index f9306aa..61ae9b8 100644 --- a/pkg/installer/os/os.go +++ b/pkg/installer/os/os.go @@ -16,6 +16,8 @@ const ( ubuntuOS = osName("ubuntu") debianOS = osName("debian") almalinuxOS = osName("almalinux") + // defaultOS contains no specific overwrites and can be used by out-of-tree images + defaultOS = osName("default") ) type ( @@ -33,10 +35,18 @@ func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { return fromOsName(*cfg.Name, cfg) } - return detectOS(cfg) + os, err := detectOS(cfg) + if err != nil { + cfg.Log.Error("unable to detect operating system, falling back to default implementation", "error", err) + return fromOsName(string(defaultOS), cfg) + } + + return os, nil } func detectOS(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { + cfg.Log.Info("automatically detecting operating system for installation") + content, err := cfg.Fs.ReadFile("/etc/os-release") if err != nil { return nil, err diff --git a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go index c1cb9ce..325d5c9 100644 --- a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go +++ b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go @@ -13,8 +13,8 @@ import ( "github.com/metal-stack/os-installer/pkg/test" "github.com/spf13/afero" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/assert/yaml" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" ) func Test_os_WriteBootInfo(t *testing.T) { From 72cb44da4616a057c4b68f8accd5b811f81ce034 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 12 Mar 2026 09:00:51 +0100 Subject: [PATCH 055/102] Add main. --- api/v1/api.go | 12 ++++++---- main.go | 45 ++++++++++++++++++++++++++++++++++++++ pkg/installer/installer.go | 4 ++-- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 main.go diff --git a/api/v1/api.go b/api/v1/api.go index 8e754cc..08645bb 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -4,6 +4,12 @@ import ( apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" ) +const ( + MachineDetailsPath = "/etc/metal/machine-details.yaml" + MachineAllocationPath = "/etc/metal/machine-allocation.yaml" + InstallerConfigPath = "/etc/metal/os-installer.yaml" +) + // Bootinfo is written by the installer in the target os to tell us // which kernel, initrd and cmdline must be used for kexec type Bootinfo struct { @@ -13,10 +19,8 @@ type Bootinfo struct { BootloaderID string `yaml:"bootloader_id"` } -const InstallerConfigPath = "/etc/metal/os-installer.yaml" - -// InstallerConfig can be placed inside the target OS to customize the os-installer. -type InstallerConfig struct { +// Config can be placed inside the target OS to customize the os-installer. +type Config struct { // OsName enforces a specific os-installer implementation, defaults to auto-detection OsName *string `yaml:"os_name"` // Only allows to run installer tasks only with the given names diff --git a/main.go b/main.go new file mode 100644 index 0000000..4fb43c3 --- /dev/null +++ b/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "log/slog" + goos "os" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" + os "github.com/metal-stack/os-installer/pkg/installer" + "github.com/stretchr/testify/assert/yaml" +) + +func main() { + log := slog.New(slog.NewJSONHandler(goos.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + + data, err := goos.ReadFile(v1.MachineDetailsPath) + if err != nil { + log.Error("unable to read machine details", "error", err) + goos.Exit(1) + } + + var details v1.MachineDetails + if err = yaml.Unmarshal(data, &details); err != nil { + log.Error("unable to parse machine details", "error", err) + goos.Exit(1) + } + + data, err = goos.ReadFile(v1.MachineAllocationPath) + if err != nil { + log.Error("unable to read machine allocation", "error", err) + goos.Exit(1) + } + + var allocation apiv2.MachineAllocation + if err = yaml.Unmarshal(data, &allocation); err != nil { + log.Error("unable to parse machine allocation", "error", err) + goos.Exit(1) + } + + if err := os.Install(context.Background(), log, &details, &allocation); err != nil { + log.Error("error while running machine installer", "error", err) + goos.Exit(1) + } +} diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 6354b67..b53a168 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -18,7 +18,7 @@ import ( type installer struct { log *slog.Logger - cfg *v1.InstallerConfig + cfg *v1.Config oss oscommon.OperatingSystem fs *afero.Afero exec *exec.CmdExecutor @@ -32,7 +32,7 @@ func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, fs = &afero.Afero{ Fs: afero.OsFs{}, } - installerConfig = &v1.InstallerConfig{} + installerConfig = &v1.Config{} ) if oscommon.FileExists(fs, v1.InstallerConfigPath) { From b750188e25e3651def1065a98bd18802d2b3709a Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 09:34:04 +0100 Subject: [PATCH 056/102] Protoyaml --- go.mod | 8 ++++++++ go.sum | 20 ++++++++++++++++++++ main.go | 25 +++++++++++++------------ pkg/installer/installer.go | 2 +- pkg/installer/installer_test.go | 2 +- 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 0c49f76..50c58ef 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/metal-stack/os-installer go 1.26 require ( + buf.build/go/protoyaml v0.6.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/coreos/go-systemd/v22 v22.7.0 @@ -19,13 +20,17 @@ require ( require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect + buf.build/go/protovalidate v1.1.3 // indirect + cel.dev/expr v0.25.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/google/cel-go v0.27.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -36,8 +41,11 @@ require ( github.com/vincent-petithory/dataurl v1.0.0 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3912772..3ff2587 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE= +buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= +buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= +buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -10,7 +16,11 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437 h1:gZCtZ+Hh/e3CGEX8q/yAcp8wWu5ZS6NMk6VGzpQhI3s= github.com/ajeddeloh/go-json v0.0.0-20160803184958-73d058cf8437/go.mod h1:otnto4/Icqn88WCcM4bhIJNSgsh9VLBuspyyCfvof9c= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-sdk-go v1.8.39/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/coreos/go-semver v0.1.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= @@ -30,6 +40,8 @@ github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I github.com/godbus/dbus v0.0.0-20181025153459-66d97aec3384/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -56,6 +68,8 @@ github.com/pin/tftp v2.1.0+incompatible/go.mod h1:xVpZOMCXTy+A5QMjEVN0Glwa1sUvaJ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= @@ -86,6 +100,8 @@ go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSy golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -97,6 +113,10 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 4fb43c3..ecbdef8 100644 --- a/main.go +++ b/main.go @@ -3,43 +3,44 @@ package main import ( "context" "log/slog" - goos "os" + "os" + "buf.build/go/protoyaml" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" v1 "github.com/metal-stack/os-installer/api/v1" - os "github.com/metal-stack/os-installer/pkg/installer" + "github.com/metal-stack/os-installer/pkg/installer" "github.com/stretchr/testify/assert/yaml" ) func main() { - log := slog.New(slog.NewJSONHandler(goos.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - data, err := goos.ReadFile(v1.MachineDetailsPath) + data, err := os.ReadFile(v1.MachineDetailsPath) if err != nil { log.Error("unable to read machine details", "error", err) - goos.Exit(1) + os.Exit(1) } var details v1.MachineDetails if err = yaml.Unmarshal(data, &details); err != nil { log.Error("unable to parse machine details", "error", err) - goos.Exit(1) + os.Exit(1) } - data, err = goos.ReadFile(v1.MachineAllocationPath) + data, err = os.ReadFile(v1.MachineAllocationPath) if err != nil { log.Error("unable to read machine allocation", "error", err) - goos.Exit(1) + os.Exit(1) } var allocation apiv2.MachineAllocation - if err = yaml.Unmarshal(data, &allocation); err != nil { + if err = protoyaml.Unmarshal(data, &allocation); err != nil { log.Error("unable to parse machine allocation", "error", err) - goos.Exit(1) + os.Exit(1) } - if err := os.Install(context.Background(), log, &details, &allocation); err != nil { + if err := installer.Install(context.Background(), log, &details, &allocation); err != nil { log.Error("error while running machine installer", "error", err) - goos.Exit(1) + os.Exit(1) } } diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index b53a168..842247c 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -1,4 +1,4 @@ -package os +package installer import ( "context" diff --git a/pkg/installer/installer_test.go b/pkg/installer/installer_test.go index af013e7..5aa0659 100644 --- a/pkg/installer/installer_test.go +++ b/pkg/installer/installer_test.go @@ -1,4 +1,4 @@ -package os +package installer import ( "fmt" From 39ef0dfa13fca4d39ad02795e400426d911fc240 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 12 Mar 2026 10:31:21 +0100 Subject: [PATCH 057/102] Update README. --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc5d253..7239b48 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ -# OS Installer +# os-installer -OS installer is used to configure the already untarred metal-image on the machine based on the configuration which must be located at `/etc/metal/install.yaml`. +The OS installer is used to configure a machine according to the given allocation specification, like configuring: + +- Network interfaces +- FRR configuration +- Metal user and authorized keys +- Bootloader configuration +- Ignition and cloud-init userdata +- ... + +It currently supports the officially published operating system images from the [metal-images repository](https://github.com/metal-stack/metal-images). + +The installer is executed by the [metal-hammer](https://github.com/metal-stack/metal-hammer) in a chroot, pointing to the root of the uncompressed operating system image. + +The input configuration for the installer are: + +- The `MachineDetails` as defined in [api/v1/api.go](./api/v1/api.go) +- The `MachineAllocation` as defined in the [API repository](https://github.com/metal-stack/api/) +- An optional installer `Config` as defined in [api/v1/api.go](./api/v1/api.go) (for building own images) From b6677ac24a10f5a4eccc4e5aa48fa808e295ea1b Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 12 Mar 2026 10:31:48 +0100 Subject: [PATCH 058/102] Adding CODEOWNERS. --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..6227ace --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @metal-stack/os-installer-maintainers From 4eb964b4c43f4ab4d415207ec60c39ded5e2faa2 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 12 Mar 2026 12:54:22 +0100 Subject: [PATCH 059/102] Remove legacy disk, move buildmeta --- api/v1/api.go | 22 +++++++--------------- api/v1/build-meta.go | 10 ---------- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 api/v1/build-meta.go diff --git a/api/v1/api.go b/api/v1/api.go index 08645bb..50783f8 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -53,19 +53,11 @@ type MachineDetails struct { RootUUID string `yaml:"root_uuid"` } -// FIXME legacy structs remove once old images are gone +type BuildMeta struct { + Version string `json:"buildVersion" yaml:"buildVersion"` + Date string `json:"buildDate" yaml:"buildDate"` + SHA string `json:"buildSHA" yaml:"buildSHA"` + Revision string `json:"buildRevision" yaml:"buildRevision"` -type ( - // Disk is a physical Disk - Disk struct { - // Device the name of the disk device visible from kernel side, e.g. sda - Device string - // Partitions to create on this disk, order is preserved - Partitions []Partition - } - Partition struct { - Label string - Filesystem string - Properties map[string]string - } -) + IgnitionVersion string `json:"ignitionVersion" yaml:"ignitionVersion"` +} diff --git a/api/v1/build-meta.go b/api/v1/build-meta.go deleted file mode 100644 index f0e1105..0000000 --- a/api/v1/build-meta.go +++ /dev/null @@ -1,10 +0,0 @@ -package v1 - -type BuildMeta struct { - Version string `json:"buildVersion" yaml:"buildVersion"` - Date string `json:"buildDate" yaml:"buildDate"` - SHA string `json:"buildSHA" yaml:"buildSHA"` - Revision string `json:"buildRevision" yaml:"buildRevision"` - - IgnitionVersion string `json:"ignitionVersion" yaml:"ignitionVersion"` -} From b4e91c290b04956e993a6ae2ca59b451f7107917 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Fri, 13 Mar 2026 14:48:06 +0100 Subject: [PATCH 060/102] Path of configs in one place --- api/v1/api.go | 118 ++++++++++-------- go.mod | 2 +- go.sum | 4 +- pkg/installer/os/common/write_boot_info.go | 6 +- pkg/installer/os/common/write_build_meta.go | 8 +- .../os/debian/tests/write_boot_info_test.go | 2 +- .../os/ubuntu/tests/write_boot_info_test.go | 2 +- .../os/ubuntu/tests/write_build_meta_test.go | 3 +- 8 files changed, 78 insertions(+), 67 deletions(-) diff --git a/api/v1/api.go b/api/v1/api.go index 50783f8..ed5d54e 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -8,56 +8,74 @@ const ( MachineDetailsPath = "/etc/metal/machine-details.yaml" MachineAllocationPath = "/etc/metal/machine-allocation.yaml" InstallerConfigPath = "/etc/metal/os-installer.yaml" + LLDPDConfigPath = "/etc/metal/install.yaml" + BuildMetaPath = "/etc/metal/build-meta.yaml" + BootInfoPath = "/etc/metal/boot-info.yaml" ) -// Bootinfo is written by the installer in the target os to tell us -// which kernel, initrd and cmdline must be used for kexec -type Bootinfo struct { - Initrd string `yaml:"initrd"` - Cmdline string `yaml:"cmdline"` - Kernel string `yaml:"kernel"` - BootloaderID string `yaml:"bootloader_id"` -} - -// Config can be placed inside the target OS to customize the os-installer. -type Config struct { - // OsName enforces a specific os-installer implementation, defaults to auto-detection - OsName *string `yaml:"os_name"` - // Only allows to run installer tasks only with the given names - Only []string `yaml:"only"` - // Except allows to run installer tasks except for the given names - Except []string `yaml:"except"` - // CustomScript allows executing a custom script that's placed inside the OS at the end of the installer execution - CustomScript *struct { - ExecutablePath string `yaml:"executable_path"` - WorkDir string `yaml:"workdir"` - } `yaml:"custom_script"` - // Overwrites allows specifying os-installer overwrites for the default implementation - Overwrites struct { - BootloaderID *string `yaml:"bootloader_id"` +type ( + // Bootinfo is written by the installer in the target os to tell us + // which kernel, initrd and cmdline must be used for kexec + Bootinfo struct { + Initrd string `yaml:"initrd"` + Cmdline string `yaml:"cmdline"` + Kernel string `yaml:"kernel"` + BootloaderID string `yaml:"bootloader_id"` } -} - -type MachineDetails struct { - // Id is the machine UUID - ID string `yaml:"id"` - // Nics are the nics of the machine - Nics []*apiv2.MachineNic `yaml:"nics"` - // Password is the password for the metal user. - Password string `yaml:"password"` - // Console specifies where the kernel should connect its console to. - Console string `yaml:"console"` - // RaidEnabled is set to true if any raid devices are specified - RaidEnabled bool `yaml:"raidenabled"` - // RootUUID is the fs uuid if the root fs - RootUUID string `yaml:"root_uuid"` -} - -type BuildMeta struct { - Version string `json:"buildVersion" yaml:"buildVersion"` - Date string `json:"buildDate" yaml:"buildDate"` - SHA string `json:"buildSHA" yaml:"buildSHA"` - Revision string `json:"buildRevision" yaml:"buildRevision"` - - IgnitionVersion string `json:"ignitionVersion" yaml:"ignitionVersion"` -} + + // Config can be placed inside the target OS to customize the os-installer. + Config struct { + // OsName enforces a specific os-installer implementation, defaults to auto-detection + OsName *string `yaml:"os_name"` + // Only allows to run installer tasks only with the given names + Only []string `yaml:"only"` + // Except allows to run installer tasks except for the given names + Except []string `yaml:"except"` + // CustomScript allows executing a custom script that's placed inside the OS at the end of the installer execution + CustomScript *struct { + ExecutablePath string `yaml:"executable_path"` + WorkDir string `yaml:"workdir"` + } `yaml:"custom_script"` + // Overwrites allows specifying os-installer overwrites for the default implementation + Overwrites struct { + BootloaderID *string `yaml:"bootloader_id"` + } + } + + // MachineDetails which are not part of the MachineAllocation but required to complete the installation. + // Is written by by the metal-hammer + MachineDetails struct { + // Id is the machine UUID + ID string `yaml:"id"` + // Nics are the nics of the machine + Nics []*apiv2.MachineNic `yaml:"nics"` + // Password is the password for the metal user. + Password string `yaml:"password"` + // Console specifies where the kernel should connect its console to. + Console string `yaml:"console"` + // RaidEnabled is set to true if any raid devices are specified + RaidEnabled bool `yaml:"raidenabled"` + // RootUUID is the fs uuid if the root fs + RootUUID string `yaml:"root_uuid"` + } + + // LLDPDConfig contains the configuration which is required for the lldpd to start. + // must be stored in yaml format at /etc/metal/install.yaml + // Is written by by the metal-hammer + LLDPDConfig struct { + // MachineUUID is the unique UUID for this machine, usually the board serial. + MachineUUID string `yaml:"machineuuid"` + // Timestamp is the the timestamp of installer config creation. + Timestamp string `yaml:"timestamp"` + } + + // BuildMeta is written after the installation finished to store details about the installation version. + BuildMeta struct { + Version string `json:"buildVersion" yaml:"buildVersion"` + Date string `json:"buildDate" yaml:"buildDate"` + SHA string `json:"buildSHA" yaml:"buildSHA"` + Revision string `json:"buildRevision" yaml:"buildRevision"` + + IgnitionVersion string `json:"ignitionVersion" yaml:"ignitionVersion"` + } +) diff --git a/go.mod b/go.mod index 50c58ef..13cbe4a 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/vincent-petithory/dataurl v1.0.0 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/crypto v0.49.0 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect diff --git a/go.sum b/go.sum index 3ff2587..5bcb6f7 100644 --- a/go.sum +++ b/go.sum @@ -100,8 +100,8 @@ go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSy golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/installer/os/common/write_boot_info.go b/pkg/installer/os/common/write_boot_info.go index 70e937b..dc57c23 100644 --- a/pkg/installer/os/common/write_boot_info.go +++ b/pkg/installer/os/common/write_boot_info.go @@ -8,10 +8,6 @@ import ( "go.yaml.in/yaml/v3" ) -const ( - BootInfoPath = "/etc/metal/boot-info.yaml" -) - func (d *CommonTasks) WriteBootInfo(ctx context.Context, initramdiskFormatString, bootloaderID, cmdLine string) error { kern, initrd, err := d.KernelAndInitrdPath(initramdiskFormatString) if err != nil { @@ -28,5 +24,5 @@ func (d *CommonTasks) WriteBootInfo(ctx context.Context, initramdiskFormatString return fmt.Errorf("unable to write boot-info.yaml: %w", err) } - return d.fs.WriteFile(BootInfoPath, content, 0700) + return d.fs.WriteFile(v1.BootInfoPath, content, 0700) } diff --git a/pkg/installer/os/common/write_build_meta.go b/pkg/installer/os/common/write_build_meta.go index 78907f4..6226003 100644 --- a/pkg/installer/os/common/write_build_meta.go +++ b/pkg/installer/os/common/write_build_meta.go @@ -10,12 +10,8 @@ import ( "go.yaml.in/yaml/v3" ) -const ( - BuildMetaPath = "/etc/metal/build-meta.yaml" -) - func (d *CommonTasks) WriteBuildMeta(ctx context.Context) error { - d.log.Info("writing build meta file", "path", BuildMetaPath) + d.log.Info("writing build meta file", "path", v1.BuildMetaPath) meta := &v1.BuildMeta{ Version: v.Version, @@ -41,5 +37,5 @@ func (d *CommonTasks) WriteBuildMeta(ctx context.Context) error { content = append([]byte("---\n"), content...) - return d.fs.WriteFile(BuildMetaPath, content, 0644) + return d.fs.WriteFile(v1.BuildMetaPath, content, 0644) } diff --git a/pkg/installer/os/debian/tests/write_boot_info_test.go b/pkg/installer/os/debian/tests/write_boot_info_test.go index 731039f..8ef6e0b 100644 --- a/pkg/installer/os/debian/tests/write_boot_info_test.go +++ b/pkg/installer/os/debian/tests/write_boot_info_test.go @@ -111,7 +111,7 @@ func Test_os_WriteBootInfo(t *testing.T) { return } - content, err := fs.ReadFile(oscommon.BootInfoPath) + content, err := fs.ReadFile(v1.BootInfoPath) require.NoError(t, err) var bootInfo v1.Bootinfo diff --git a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go index 325d5c9..ad3da3d 100644 --- a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go +++ b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go @@ -111,7 +111,7 @@ func Test_os_WriteBootInfo(t *testing.T) { return } - content, err := fs.ReadFile(oscommon.BootInfoPath) + content, err := fs.ReadFile(v1.BootInfoPath) require.NoError(t, err) var bootInfo v1.Bootinfo diff --git a/pkg/installer/os/ubuntu/tests/write_build_meta_test.go b/pkg/installer/os/ubuntu/tests/write_build_meta_test.go index bc16c47..d5e5ccc 100644 --- a/pkg/installer/os/ubuntu/tests/write_build_meta_test.go +++ b/pkg/installer/os/ubuntu/tests/write_build_meta_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" "github.com/metal-stack/os-installer/pkg/exec" oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" @@ -71,7 +72,7 @@ ignitionVersion: Ignition v0.36.2 return } - content, err := fs.ReadFile(oscommon.BuildMetaPath) + content, err := fs.ReadFile(v1.BuildMetaPath) require.NoError(t, err) assert.Equal(t, tt.want, string(content)) From f6d4c1c6c420511c5cb768a9e1215095a870a6b4 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 14 Mar 2026 09:57:01 +0100 Subject: [PATCH 061/102] Add Parse/Read Configuration --- main.go | 29 ++--------- pkg/installer/installer.go | 100 ++++++++++++++++++++++++++++--------- 2 files changed, 80 insertions(+), 49 deletions(-) diff --git a/main.go b/main.go index ecbdef8..240cc9c 100644 --- a/main.go +++ b/main.go @@ -5,41 +5,20 @@ import ( "log/slog" "os" - "buf.build/go/protoyaml" - apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - v1 "github.com/metal-stack/os-installer/api/v1" "github.com/metal-stack/os-installer/pkg/installer" - "github.com/stretchr/testify/assert/yaml" ) func main() { log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - data, err := os.ReadFile(v1.MachineDetailsPath) + details, allocation, err := installer.ReadConfigurations() if err != nil { - log.Error("unable to read machine details", "error", err) - os.Exit(1) - } - - var details v1.MachineDetails - if err = yaml.Unmarshal(data, &details); err != nil { - log.Error("unable to parse machine details", "error", err) - os.Exit(1) - } - - data, err = os.ReadFile(v1.MachineAllocationPath) - if err != nil { - log.Error("unable to read machine allocation", "error", err) - os.Exit(1) + log.Error("unable to read configuration", "error", err) } - var allocation apiv2.MachineAllocation - if err = protoyaml.Unmarshal(data, &allocation); err != nil { - log.Error("unable to parse machine allocation", "error", err) - os.Exit(1) - } + i := installer.New(log, details, allocation) - if err := installer.Install(context.Background(), log, &details, &allocation); err != nil { + if err := i.Install(context.Background()); err != nil { log.Error("error while running machine installer", "error", err) os.Exit(1) } diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 842247c..dd9cf45 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "log/slog" + "os" "slices" "time" + "buf.build/go/protoyaml" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" v1 "github.com/metal-stack/os-installer/api/v1" "github.com/metal-stack/os-installer/pkg/exec" @@ -17,26 +19,80 @@ import ( ) type installer struct { - log *slog.Logger - cfg *v1.Config - oss oscommon.OperatingSystem - fs *afero.Afero - exec *exec.CmdExecutor + log *slog.Logger + cfg *v1.Config + oss oscommon.OperatingSystem + fs *afero.Afero + exec *exec.CmdExecutor + details *v1.MachineDetails + allocation *apiv2.MachineAllocation } -func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, allocation *apiv2.MachineAllocation) error { - log = log.WithGroup("os-installer") +func New(log *slog.Logger, details *v1.MachineDetails, allocation *apiv2.MachineAllocation) *installer { + return &installer{ + log: log.WithGroup("os-installer"), + cfg: &v1.Config{}, + fs: &afero.Afero{ + Fs: afero.OsFs{}, + }, + details: details, + allocation: allocation, + } +} + +func (i *installer) PersistConfigurations() error { + detailsBytes, err := yaml.Marshal(i.details) + if err != nil { + return fmt.Errorf("unable to marshal machine details: %w", err) + } + err = os.WriteFile(v1.MachineDetailsPath, detailsBytes, os.ModePerm) + if err != nil { + return fmt.Errorf("unable to persist machine details: %w", err) + } + + allocationBytes, err := protoyaml.Marshal(i.allocation) + if err != nil { + return fmt.Errorf("unable to marshal machine allocation: %w", err) + } + err = os.WriteFile(v1.MachineAllocationPath, allocationBytes, os.ModePerm) + if err != nil { + return fmt.Errorf("unable to persist machine allocation: %w", err) + } + return nil +} +func ReadConfigurations() (*v1.MachineDetails, *apiv2.MachineAllocation, error) { + data, err := os.ReadFile(v1.MachineDetailsPath) + if err != nil { + return nil, nil, fmt.Errorf("unable to read machine details: %w", err) + } + + var details v1.MachineDetails + if err = yaml.Unmarshal(data, &details); err != nil { + return nil, nil, fmt.Errorf("unable to parse machine details: %w", err) + } + + data, err = os.ReadFile(v1.MachineAllocationPath) + if err != nil { + return nil, nil, fmt.Errorf("unable to read machine allocation: %w", err) + } + + var allocation apiv2.MachineAllocation + if err = protoyaml.Unmarshal(data, &allocation); err != nil { + return nil, nil, fmt.Errorf("unable to parse machine allocation: %w", err) + } + + return &details, &allocation, nil +} + +func (i *installer) Install(ctx context.Context) error { var ( - start = time.Now() - fs = &afero.Afero{ - Fs: afero.OsFs{}, - } + start = time.Now() installerConfig = &v1.Config{} ) - if oscommon.FileExists(fs, v1.InstallerConfigPath) { - data, err := fs.ReadFile(v1.InstallerConfigPath) + if oscommon.FileExists(i.fs, v1.InstallerConfigPath) { + data, err := i.fs.ReadFile(v1.InstallerConfigPath) if err != nil { return fmt.Errorf("unable to read installer config: %w", err) } @@ -47,10 +103,10 @@ func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, } oss, err := operatingsystem.New(&oscommon.Config{ - Log: log, - Fs: fs, - MachineDetails: details, - Allocation: allocation, + Log: i.log, + Fs: i.fs, + MachineDetails: i.details, + Allocation: i.allocation, Name: installerConfig.OsName, BootloaderID: installerConfig.Overwrites.BootloaderID, }) @@ -58,13 +114,9 @@ func Install(ctx context.Context, log *slog.Logger, details *v1.MachineDetails, return fmt.Errorf("os detection failed: %w", err) } - i := installer{ - log: log, - cfg: installerConfig, - oss: oss, - exec: exec.New(log), - fs: fs, - } + i.cfg = installerConfig + i.oss = oss + i.exec = exec.New(i.log) if err = i.run(ctx); err != nil { i.log.Info("running os installer failed", "took", time.Since(start).String()) From aa3e90a77b21963b6e4ec378dc647ed2cb1825b3 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 16 Mar 2026 11:22:49 +0100 Subject: [PATCH 062/102] More logs --- pkg/frr/frr.go | 3 +++ pkg/interfaces/interfaces.go | 4 ++++ pkg/nftables/nftables.go | 3 +++ 3 files changed, 10 insertions(+) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 6c776a5..96439ac 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -100,6 +100,7 @@ type ( // Renders renders frr configuration according to the given input data and reloads the service if necessary func Render(ctx context.Context, cfg *Config) (changed bool, err error) { + cfg.Log.Info("render frr configuration") var ( data any template string @@ -139,6 +140,8 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { template = firewallTemplateString } + cfg.Log.Info("render frr configuration", "templatedata", data) + r, err := renderer.New(&renderer.Config{ Log: cfg.Log, TemplateString: template, diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index 7f652a2..e1fe0e9 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -84,10 +84,12 @@ type ( ) func ConfigureInterfaces(ctx context.Context, cfg *Config) error { + cfg.Log.Info("create loopback interfaces") if err := configureLoopbackInterface(ctx, cfg); err != nil { return fmt.Errorf("error configuring loopback interface: %w", err) } + cfg.Log.Info("create lan interfaces") if err := configureLanInterfaces(ctx, cfg); err != nil { return fmt.Errorf("error configuring lan interfaces: %w", err) } @@ -96,10 +98,12 @@ func ConfigureInterfaces(ctx context.Context, cfg *Config) error { return nil } + cfg.Log.Info("create bridges") if err := configureBridges(ctx, cfg); err != nil { return fmt.Errorf("error configuring network bridges: %w", err) } + cfg.Log.Info("create evpn") if err := configureEVPN(ctx, cfg); err != nil { return fmt.Errorf("error configuring evnps: %w", err) } diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index ff26a50..db0ff7f 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -110,6 +110,7 @@ type ( // Renders renders nftables rules according to the given input data and reloads the service if necessary func Render(ctx context.Context, cfg *Config) (changed bool, err error) { + cfg.Log.Info("render nftables configuration") const comment = "generated by os-installer" snat, err := getSNAT(cfg) @@ -135,6 +136,8 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { data.DNSProxyDNAT = getDNSProxyDNAT(cfg) } + cfg.Log.Info("render nftables configuration", "templatedata", data) + r, err := renderer.New(&renderer.Config{ Log: cfg.Log, TemplateString: templateString, From edec9335d8a0fabfc86dcb2d521dfdf5ce296b69 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 16 Mar 2026 12:01:11 +0100 Subject: [PATCH 063/102] More logs --- pkg/installer/installer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index dd9cf45..8b4bea1 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -237,7 +237,7 @@ func (i *installer) run(ctx context.Context) error { log.Info("running install task", "start-at", start.String()) if err := task.fn(ctx); err != nil { - i.log.Info("running install task failed", "took", time.Since(start).String()) + i.log.Error("running install task failed", "error", err, "took", time.Since(start).String()) return fmt.Errorf("installation task failed, aborting install: %w", err) } } From 0d0d563b7d9c649c99a0ec5ddfc1b3fdceb4ee99 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 16 Mar 2026 12:44:51 +0100 Subject: [PATCH 064/102] Pass exec. --- pkg/installer/installer.go | 7 +++++-- pkg/installer/os/os.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 8b4bea1..78b7b4a 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -29,14 +29,17 @@ type installer struct { } func New(log *slog.Logger, details *v1.MachineDetails, allocation *apiv2.MachineAllocation) *installer { + log = log.WithGroup("os-installer") + return &installer{ - log: log.WithGroup("os-installer"), + log: log, cfg: &v1.Config{}, fs: &afero.Afero{ Fs: afero.OsFs{}, }, details: details, allocation: allocation, + exec: exec.New(log), } } @@ -104,6 +107,7 @@ func (i *installer) Install(ctx context.Context) error { oss, err := operatingsystem.New(&oscommon.Config{ Log: i.log, + Exec: i.exec, Fs: i.fs, MachineDetails: i.details, Allocation: i.allocation, @@ -116,7 +120,6 @@ func (i *installer) Install(ctx context.Context) error { i.cfg = installerConfig i.oss = oss - i.exec = exec.New(i.log) if err = i.run(ctx); err != nil { i.log.Info("running os installer failed", "took", time.Since(start).String()) diff --git a/pkg/installer/os/os.go b/pkg/installer/os/os.go index 61ae9b8..67729ed 100644 --- a/pkg/installer/os/os.go +++ b/pkg/installer/os/os.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + "github.com/metal-stack/os-installer/pkg/exec" "github.com/metal-stack/os-installer/pkg/installer/os/almalinux" oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" "github.com/metal-stack/os-installer/pkg/installer/os/debian" @@ -25,12 +26,26 @@ type ( ) func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { + if cfg.Log == nil { + return nil, fmt.Errorf("log must be passed to os-installer") + } + if cfg.Allocation == nil { + return nil, fmt.Errorf("allocation must be passed to os-installer") + } + if cfg.MachineDetails == nil { + return nil, fmt.Errorf("machine details must be passed to os-installer") + } + if cfg.Fs == nil { cfg.Fs = &afero.Afero{ Fs: afero.OsFs{}, } } + if cfg.Exec == nil { + cfg.Exec = exec.New(cfg.Log) + } + if cfg.Name != nil { return fromOsName(*cfg.Name, cfg) } From 23c4e387ef65cabe456d17bc32e7a454eba8077f Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 16 Mar 2026 13:41:46 +0100 Subject: [PATCH 065/102] Update api --- go.mod | 2 +- go.sum | 4 ++-- pkg/installer/os/almalinux/write_ntp_conf.go | 4 ++-- pkg/installer/os/common/write_ntp_conf.go | 4 ++-- pkg/installer/os/common/write_resolv_conf.go | 4 ++-- pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go | 2 +- pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go | 2 +- pkg/network/network.go | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 13cbe4a..64e8e95 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/flatcar/ignition v0.36.2 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff + github.com/metal-stack/api v0.0.55-0.20260316085710-1f98c8226b9e github.com/metal-stack/v v1.0.3 github.com/samber/lo v1.53.0 github.com/spf13/afero v1.15.0 diff --git a/go.sum b/go.sum index 5bcb6f7..33fa380 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff h1:668iZE3tvpbhoARzpW8zdFnGTVqa7Ks5xJKeY4N0WtA= -github.com/metal-stack/api v0.0.54-0.20260309104254-e1a94cd811ff/go.mod h1:SAtqZaD4JvOn+NVc6bTlKzL2EDoj/QrlHF72ZMw+Btk= +github.com/metal-stack/api v0.0.55-0.20260316085710-1f98c8226b9e h1:ixdWJR5ltPm4e4FMU+f1QgopdnN/GnFXvpDhNnXjsDg= +github.com/metal-stack/api v0.0.55-0.20260316085710-1f98c8226b9e/go.mod h1:OU8KDSOw5JEfeEs9q8FY5TcaklBAiGx+Q9Em0BMZrlY= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= diff --git a/pkg/installer/os/almalinux/write_ntp_conf.go b/pkg/installer/os/almalinux/write_ntp_conf.go index 82bc5ff..14c4a0c 100644 --- a/pkg/installer/os/almalinux/write_ntp_conf.go +++ b/pkg/installer/os/almalinux/write_ntp_conf.go @@ -12,13 +12,13 @@ const ( ) func (o *os) WriteNTPConf(ctx context.Context) error { - if len(o.allocation.NtpServer) == 0 { + if len(o.allocation.NtpServers) == 0 { return nil } var ntpServers []string - for _, ntp := range o.allocation.NtpServer { + for _, ntp := range o.allocation.NtpServers { ntpServers = append(ntpServers, ntp.Address) } diff --git a/pkg/installer/os/common/write_ntp_conf.go b/pkg/installer/os/common/write_ntp_conf.go index a3bb449..3d14355 100644 --- a/pkg/installer/os/common/write_ntp_conf.go +++ b/pkg/installer/os/common/write_ntp_conf.go @@ -15,13 +15,13 @@ const ( ) func (d *CommonTasks) WriteNTPConf(ctx context.Context) error { - if len(d.allocation.NtpServer) == 0 { + if len(d.allocation.NtpServers) == 0 { return nil } var ntpServers []string - for _, ntp := range d.allocation.NtpServer { + for _, ntp := range d.allocation.NtpServers { ntpServers = append(ntpServers, ntp.Address) } diff --git a/pkg/installer/os/common/write_resolv_conf.go b/pkg/installer/os/common/write_resolv_conf.go index 02ca88c..804fd5b 100644 --- a/pkg/installer/os/common/write_resolv_conf.go +++ b/pkg/installer/os/common/write_resolv_conf.go @@ -24,9 +24,9 @@ func (d *CommonTasks) WriteResolvConf(ctx context.Context) error { nameserver 8.8.4.4 `) - if len(d.allocation.DnsServer) > 0 { + if len(d.allocation.DnsServers) > 0 { var s strings.Builder - for _, dnsServer := range d.allocation.DnsServer { + for _, dnsServer := range d.allocation.DnsServers { s.WriteString("nameserver " + dnsServer.Ip + "\n") } diff --git a/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go b/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go index 21c3391..645bbd5 100644 --- a/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go +++ b/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go @@ -30,7 +30,7 @@ func Test_os_WriteNTPConf(t *testing.T) { }, allocation: &apiv2.MachineAllocation{ AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, - NtpServer: []*apiv2.NTPServer{ + NtpServers: []*apiv2.NTPServer{ {Address: "custom.1.ntp.org"}, {Address: "custom.2.ntp.org"}, }, diff --git a/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go b/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go index d4b6a31..d83ce4f 100644 --- a/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go +++ b/pkg/installer/os/ubuntu/tests/write_resolv_conf_test.go @@ -45,7 +45,7 @@ nameserver 8.8.4.4 { name: "overwrite resolv.conf with custom DNS", allocation: &apiv2.MachineAllocation{ - DnsServer: []*apiv2.DNSServer{ + DnsServers: []*apiv2.DNSServer{ {Ip: "1.2.3.4"}, {Ip: "5.6.7.8"}, }, diff --git a/pkg/network/network.go b/pkg/network/network.go index 537b579..d299e75 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -78,7 +78,7 @@ func (n *Network) FirewallRules() *apiv2.FirewallRules { } func (n *Network) NTPServers() (ntpServers []string) { - for _, ntpserver := range n.allocation.NtpServer { + for _, ntpserver := range n.allocation.NtpServers { ntpServers = append(ntpServers, ntpserver.Address) } return From 30a28bb5486ca11108798c9a03d708d1b123dc60 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 16 Mar 2026 15:55:39 +0100 Subject: [PATCH 066/102] Verbose cmd output --- pkg/frr/frr_version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/frr/frr_version.go b/pkg/frr/frr_version.go index 7857ef3..4b32099 100644 --- a/pkg/frr/frr_version.go +++ b/pkg/frr/frr_version.go @@ -19,7 +19,7 @@ func DetectVersion() (*semver.Version, error) { c := exec.Command(vtysh, "-c", "show version") out, err := c.CombinedOutput() if err != nil { - return nil, fmt.Errorf("unable to detect frr version with dpkg: %w", err) + return nil, fmt.Errorf("unable to detect frr version with vtysh output:%s error: %w", string(out), err) } var frrVersion string From d19d2fa048935b1a8d601b30fb46ce0543940b99 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 09:28:42 +0100 Subject: [PATCH 067/102] Detect frr client version instead of server version --- pkg/frr/frr_version.go | 29 +++++++++++++---- pkg/frr/frr_version_test.go | 62 +++++++++++++++++++++++++++++++++++++ validate.sh | 4 +-- 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 pkg/frr/frr_version_test.go diff --git a/pkg/frr/frr_version.go b/pkg/frr/frr_version.go index 4b32099..be95dbd 100644 --- a/pkg/frr/frr_version.go +++ b/pkg/frr/frr_version.go @@ -16,25 +16,42 @@ func DetectVersion() (*semver.Version, error) { // $ vtysh -c "show version"|grep FRRouting // FRRouting 10.2.1 (shoot--pz9cjf--mwen-fel-firewall-dcedd) on Linux(6.6.60-060660-generic). - c := exec.Command(vtysh, "-c", "show version") + + // $ vtysh -h + // Usage : vtysh [OPTION...] + // Integrated shell for FRR (version 10.4.3). + + // Usage : vtysh [OPTION...] + // Integrated shell for FRR (version 8.4.4). + + c := exec.Command(vtysh, "-h") out, err := c.CombinedOutput() if err != nil { return nil, fmt.Errorf("unable to detect frr version with vtysh output:%s error: %w", string(out), err) } + return parseVersion(string(out)) +} + +func parseVersion(vtyshOutput string) (*semver.Version, error) { var frrVersion string - for line := range strings.SplitSeq(string(out), "\n") { - if !strings.Contains(line, "FRRouting") { + for line := range strings.SplitSeq(vtyshOutput, "\n") { + if !strings.Contains(line, "Integrated shell for FRR") { + continue + } + + _, dirtyVersion, found := strings.Cut(line, "(version ") + if !found { continue } - fields := strings.Fields(line) - if len(fields) < 2 { + version, found := strings.CutSuffix(dirtyVersion, ").") + if !found { continue } - frrVersion = fields[1] + frrVersion = version break } diff --git a/pkg/frr/frr_version_test.go b/pkg/frr/frr_version_test.go new file mode 100644 index 0000000..271eda9 --- /dev/null +++ b/pkg/frr/frr_version_test.go @@ -0,0 +1,62 @@ +package frr + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/stretchr/testify/require" +) + +func TestDetectVersion(t *testing.T) { + tests := []struct { + name string + cmdoutput string + want *semver.Version + wantErr error + }{ + { + name: "frr 10.4", + cmdoutput: ` +vtysh -h +Usage : vtysh [OPTION...] +Integrated shell for FRR (version 10.4.3). +`, + want: semver.MustParse("10.4.3"), + wantErr: nil, + }, + { + name: "frr 8.4", + cmdoutput: ` +Integrated shell for FRR (version 8.4.4). +Configured with: + '--build=x86_64-linux-gnu' '--prefix=/usr' '--includedir=${prefix}/include' '--mandir=${prefix}/share/man' '--infodir=${prefix}/share/info' '--sysconfdir=/etc' '--localstatedir=/var' '--disable-option-checking' '--disable-silent-rules' '--libdir=${prefix}/lib/x86_64-linux-gnu' '--libexecdir=${prefix}/lib/x86_64-linux-gnu' '--disable-maintainer-mode' '--localstatedir=/var/run/frr' '--sbindir=/usr/lib/frr' '--sysconfdir=/etc/frr' '--with-vtysh-pager=/usr/bin/pager' '--libdir=/usr/lib/x86_64-linux-gnu/frr' '--with-moduledir=/usr/lib/x86_64-linux-gnu/frr/modules' '--disable-dependency-tracking' '--enable-rpki' '--disable-scripting' '--disable-pim6d' '--with-libpam' '--enable-doc' '--enable-doc-html' '--enable-snmp' '--enable-fpm' '--disable-protobuf' '--disable-zeromq' '--enable-ospfapi' '--enable-bgp-vnc' '--enable-multipath=256' '--enable-user=frr' '--enable-group=frr' '--enable-vty-group=frrvty' '--enable-configfile-mask=0640' '--enable-logfile-mask=0640' 'build_alias=x86_64-linux-gnu' 'PYTHON=python3' + +-b, --boot Execute boot startup configuration +-c, --command Execute argument as command +-d, --daemon Connect only to the specified daemon +-f, --inputfile Execute commands from specific file and exit +-E, --echo Echo prompt and command in -c mode +-C, --dryrun Check configuration for validity and exit +-m, --markfile Mark input file with context end + --vty_socket Override vty socket path + --config_dir Override config directory path +`, + want: semver.MustParse("8.4.4"), + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseVersion(tt.cmdoutput) + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + if tt.wantErr != nil { + return + } + require.Equal(t, tt.want, got) + }) + } +} diff --git a/validate.sh b/validate.sh index ff8d46d..7ce4061 100755 --- a/validate.sh +++ b/validate.sh @@ -13,7 +13,7 @@ validate () { --build-arg FRR_VERSION="${3}" \ --build-arg FRR_APT_CHANNEL="${4}" \ --file Dockerfile.validate \ - . -t metal-networker-validate:${tag} + . -t os-installer:${tag} docker run --interactive \ --rm \ @@ -22,7 +22,7 @@ validate () { --cap-add=NET_RAW \ --name vali \ --volume ./pkg:/testdata:ro \ - metal-networker-validate:${tag} /validate_os.sh + os-installer:${tag} /validate_os.sh } validate "ubuntu" "24.04" "frr-10.4" "noble" From ad59a08176e3b9ca9dc6b2a0f3aa31c50bc1fd14 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 17 Mar 2026 09:34:15 +0100 Subject: [PATCH 068/102] Provide more tests. --- pkg/installer/os/almalinux/almalinux.go | 14 +- .../os/almalinux/create_metal_user.go | 2 +- .../os/almalinux/install_bootloader.go | 12 +- .../os/almalinux/tests/almalinux_test.go | 26 +++ .../almalinux/tests/create_metal_user_test.go | 117 ++++++++++++ .../tests/install_bootloader_test.go | 172 ++++++++++++++++++ pkg/installer/os/almalinux/write_ntp_conf.go | 2 +- pkg/installer/os/common/copy_ssh_keys.go | 21 +-- pkg/installer/os/common/create_metal_user.go | 14 +- pkg/installer/os/common/defaultos.go | 14 +- pkg/installer/os/common/oscommon.go | 35 ++-- pkg/installer/os/debian/debian.go | 14 +- pkg/installer/os/debian/tests/doc.go | 3 + pkg/installer/os/os.go | 7 +- pkg/installer/os/os_test.go | 170 +++++++++++++++++ .../os/ubuntu/tests/cmd_line_test.go | 2 +- .../os/ubuntu/tests/copy_ssh_keys_test.go | 74 ++++++++ .../os/ubuntu/tests/create_metal_user_test.go | 107 +++++++++++ .../os/ubuntu/tests/fix_permissions_test.go | 2 +- .../os/ubuntu/tests/process_userdata_test.go | 2 +- .../os/ubuntu/tests/unset_machine_id_test.go | 2 +- .../os/ubuntu/tests/write_build_meta_test.go | 2 +- .../os/ubuntu/tests/write_hostname_test.go | 2 +- .../os/ubuntu/tests/write_hosts_test.go | 2 +- pkg/installer/os/ubuntu/ubuntu.go | 14 +- 25 files changed, 758 insertions(+), 74 deletions(-) create mode 100644 pkg/installer/os/almalinux/tests/almalinux_test.go create mode 100644 pkg/installer/os/almalinux/tests/create_metal_user_test.go create mode 100644 pkg/installer/os/almalinux/tests/install_bootloader_test.go create mode 100644 pkg/installer/os/debian/tests/doc.go create mode 100644 pkg/installer/os/os_test.go create mode 100644 pkg/installer/os/ubuntu/tests/copy_ssh_keys_test.go create mode 100644 pkg/installer/os/ubuntu/tests/create_metal_user_test.go diff --git a/pkg/installer/os/almalinux/almalinux.go b/pkg/installer/os/almalinux/almalinux.go index c11d1b1..85c22d9 100644 --- a/pkg/installer/os/almalinux/almalinux.go +++ b/pkg/installer/os/almalinux/almalinux.go @@ -13,7 +13,7 @@ import ( ) type ( - os struct { + Os struct { *oscommon.CommonTasks log *slog.Logger details *v1.MachineDetails @@ -24,8 +24,8 @@ type ( } ) -func New(cfg *oscommon.Config) *os { - return &os{ +func New(cfg *oscommon.Config) *Os { + return &Os{ CommonTasks: oscommon.New(cfg), log: cfg.Log, details: cfg.MachineDetails, @@ -36,18 +36,18 @@ func New(cfg *oscommon.Config) *os { } } -func (o *os) SudoGroup() string { +func (o *Os) SudoGroup() string { return "wheel" } -func (o *os) BootloaderID() string { +func (o *Os) BootloaderID() string { return "almalinux" } -func (o *os) InitramdiskFormatString() string { +func (o *Os) InitramdiskFormatString() string { return "initramfs-%s.img" } -func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { +func (o *Os) WriteBootInfo(ctx context.Context, cmdLine string) error { return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } diff --git a/pkg/installer/os/almalinux/create_metal_user.go b/pkg/installer/os/almalinux/create_metal_user.go index f99cb85..2286bd0 100644 --- a/pkg/installer/os/almalinux/create_metal_user.go +++ b/pkg/installer/os/almalinux/create_metal_user.go @@ -7,7 +7,7 @@ import ( "github.com/metal-stack/os-installer/pkg/exec" ) -func (o *os) CreateMetalUser(ctx context.Context) error { +func (o *Os) CreateMetalUser(ctx context.Context) error { err := o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) if err != nil { return err diff --git a/pkg/installer/os/almalinux/install_bootloader.go b/pkg/installer/os/almalinux/install_bootloader.go index 89bab78..793004f 100644 --- a/pkg/installer/os/almalinux/install_bootloader.go +++ b/pkg/installer/os/almalinux/install_bootloader.go @@ -10,7 +10,8 @@ import ( ) const ( - defaultGrubPath = "/etc/default/grub" + DefaultGrubPath = "/etc/default/grub" + GrubConfigPath = "/boot/efi/EFI/almalinux/grub.cfg" defaultGrubFileContent = `GRUB_DEFAULT=0 GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR=%s @@ -21,25 +22,24 @@ GRUB_SERIAL_COMMAND="serial --speed=%s --unit=%s --word=8" GRUB_DEVICE=UUID=%s GRUB_ENABLE_BLSCFG=false ` - grubConfigPath = "/boot/efi/EFI/almalinux/grub.cfg" ) -func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { - serialSpeed, serialPort, err := o.FigureOutSerialSpeed() +func (o *Os) GrubInstall(ctx context.Context, cmdLine string) error { + serialPort, serialSpeed, err := o.FigureOutSerialSpeed() if err != nil { return err } defaultGrub := fmt.Sprintf(defaultGrubFileContent, o.BootloaderID(), cmdLine, serialSpeed, serialPort, o.details.RootUUID) - err = o.fs.WriteFile(defaultGrubPath, []byte(defaultGrub), 0755) + err = o.fs.WriteFile(DefaultGrubPath, []byte(defaultGrub), 0755) if err != nil { return err } _, err = o.exec.Execute(ctx, &exec.Params{ Name: "grub2-mkconfig", - Args: []string{"-o", grubConfigPath}, + Args: []string{"-o", GrubConfigPath}, }) if err != nil { return err diff --git a/pkg/installer/os/almalinux/tests/almalinux_test.go b/pkg/installer/os/almalinux/tests/almalinux_test.go new file mode 100644 index 0000000..7821fa3 --- /dev/null +++ b/pkg/installer/os/almalinux/tests/almalinux_test.go @@ -0,0 +1,26 @@ +package almalinux_test + +import ( + "encoding/json" + "fmt" + goos "os" + "testing" + + "github.com/metal-stack/os-installer/pkg/test" + "github.com/stretchr/testify/require" +) + +func TestHelperProcess(t *testing.T) { + if goos.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + var f test.FakeExecParams + err := json.Unmarshal([]byte(goos.Args[3]), &f) + require.NoError(t, err) + + _, err = fmt.Fprint(goos.Stdout, f.Output) + require.NoError(t, err) + + goos.Exit(f.ExitCode) +} diff --git a/pkg/installer/os/almalinux/tests/create_metal_user_test.go b/pkg/installer/os/almalinux/tests/create_metal_user_test.go new file mode 100644 index 0000000..3aae01b --- /dev/null +++ b/pkg/installer/os/almalinux/tests/create_metal_user_test.go @@ -0,0 +1,117 @@ +package almalinux_test + +import ( + "log/slog" + "os/user" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/metal-stack/os-installer/pkg/installer/os/almalinux" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" +) + +func Test_os_CreateMetalUser(t *testing.T) { + tests := []struct { + name string + details *v1.MachineDetails + execMocks []test.FakeExecParams + lookupUserFn oscommon.LookupUserFn + want string + wantErr error + }{ + { + name: "create user already exists", + details: &v1.MachineDetails{ + Password: "abc", + }, + lookupUserFn: func(name string) (*user.User, error) { + return &user.User{ + Uid: "1000", + Gid: "1000", + Username: oscommon.MetalUser, + Name: oscommon.MetalUser, + HomeDir: "/home/metal", + }, nil + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"userdel", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"useradd", "--create-home", "--uid", "1000", "--gid", "wheel", "--shell", "/bin/bash", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"passwd", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"passwd", "root"}, + Output: "", + ExitCode: 0, + }, + }, + }, + { + name: "create user does not yet exist", + details: &v1.MachineDetails{ + Password: "abc", + }, + lookupUserFn: func(name string) (*user.User, error) { + return nil, user.UnknownUserError(oscommon.MetalUser) + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"useradd", "--create-home", "--uid", "1000", "--gid", "wheel", "--shell", "/bin/bash", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"passwd", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"passwd", "root"}, + Output: "", + ExitCode: 0, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + d := almalinux.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + LookupUserFn: tt.lookupUserFn, + }) + + gotErr := d.CreateMetalUser(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + }) + } +} diff --git a/pkg/installer/os/almalinux/tests/install_bootloader_test.go b/pkg/installer/os/almalinux/tests/install_bootloader_test.go new file mode 100644 index 0000000..8cc5072 --- /dev/null +++ b/pkg/installer/os/almalinux/tests/install_bootloader_test.go @@ -0,0 +1,172 @@ +package almalinux_test + +import ( + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/metal-stack/os-installer/pkg/installer/os/almalinux" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sampleMdadmScanOutput = `ARRAY /dev/md/0 metadata=1.0 UUID=42d10089:ee1e0399:445e7550:62b63ec8 name=any:0 +ARRAY /dev/md/1 metadata=1.0 UUID=543eb7f8:98d4d986:e669824d:bebe69e5 name=any:1 +ARRAY /dev/md/2 metadata=1.0 UUID=fc32a6f0:ee40d9db:87c8c9f3:a8400c8b name=any:2` + + sampleBlkidOutput = `/dev/sda1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="cc57c456-0b2f-6345-c597-d861cc6dd8ac" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="273985c8-d097-4123-bcd0-80b4e4e14728" +/dev/sda2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="54748c60-b566-f391-142c-fb78bb1fc6a9" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="d7863f4e-af7c-47fc-8c03-6ecdc69bc72d" +/dev/sda3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="582e9b4f-f191-e01e-85fd-2f7d969fbef6" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="e8b44f09-b7f7-4e0d-a7c3-d909617d1f05" +/dev/sdb1: UUID="42d10089-ee1e-0399-445e-755062b63ec8" UUID_SUB="61bd5d8b-1bb8-673b-9e61-8c28dccc3812" LABEL="any:0" TYPE="linux_raid_member" PARTLABEL="efi" PARTUUID="13a4c568-57b0-4259-9927-9ac023aaa5f0" +/dev/sdb2: UUID="543eb7f8-98d4-d986-e669-824dbebe69e5" UUID_SUB="e7d01e93-9340-5b90-68f8-d8f815595132" LABEL="any:1" TYPE="linux_raid_member" PARTLABEL="root" PARTUUID="ab11cd86-37b8-4bae-81e5-21fe0a9c9ae0" +/dev/sdb3: UUID="fc32a6f0-ee40-d9db-87c8-c9f3a8400c8b" UUID_SUB="764217ad-1591-a83a-c799-23397f968729" LABEL="any:2" TYPE="linux_raid_member" PARTLABEL="varlib" PARTUUID="9afbf9c1-b2ba-4b46-8db1-e802d26c93b6" +/dev/md1: LABEL="root" UUID="ace079b5-06be-4429-bbf0-081ea4d7d0d9" TYPE="ext4" +/dev/md0: LABEL="efi" UUID="C236-297F" TYPE="vfat" +/dev/md2: LABEL="varlib" UUID="385e8e8e-dbfd-481e-93a4-cba7f4d5fa02" TYPE="ext4"` +) + +func Test_os_GrubInstall(t *testing.T) { + tests := []struct { + name string + cmdLine string + details *v1.MachineDetails + fsMocks func(fs *afero.Afero) + execMocks []test.FakeExecParams + want string + wantErr error + }{ + { + name: "without raid", + cmdLine: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + details: &v1.MachineDetails{ + Console: "ttyS1,115200n8", + RootUUID: "78cd4dfe-8825-4f45-816e-d284adb0261e", + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"grub2-mkconfig", "-o", almalinux.GrubConfigPath}, + Output: "", + ExitCode: 0, + }, + }, + want: `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=almalinux +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" +GRUB_DEVICE=UUID=78cd4dfe-8825-4f45-816e-d284adb0261e +GRUB_ENABLE_BLSCFG=false +`, + }, + { + name: "with raid", + cmdLine: "console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300", + details: &v1.MachineDetails{ + RaidEnabled: true, + RootUUID: "ace079b5-06be-4429-bbf0-081ea4d7d0d9", + Console: "ttyS1,115200n8", + }, + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile("/boot/System.map-5.14.0-503.19.1.el9_5.x86_64", []byte{}, 0600)) + require.NoError(t, fs.WriteFile("/boot/vmlinuz-5.14.0-503.19.1.el9_5.x86_64", []byte{}, 0755)) + require.NoError(t, fs.WriteFile("/boot/initramfs-5.14.0-503.19.1.el9_5.x86_64.img", []byte{}, 0600)) + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"grub2-mkconfig", "-o", almalinux.GrubConfigPath}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"mdadm", "--examine", "--scan"}, + Output: sampleMdadmScanOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"blkid"}, + Output: sampleBlkidOutput, + ExitCode: 0, + }, + { + WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sda1", "-p1", "-l", "\\\\EFI\\\\almalinux\\\\shimx64.efi", "-L", "almalinux"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"efibootmgr", "-c", "-d", "/dev/sdb1", "-p1", "-l", "\\\\EFI\\\\almalinux\\\\shimx64.efi", "-L", "almalinux"}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{ + "dracut", + "--mdadmconf", + "--kver", "5.14.0-503.19.1.el9_5.x86_64", + "--kmoddir", "/lib/modules/5.14.0-503.19.1.el9_5.x86_64", + "--include", "/lib/modules/5.14.0-503.19.1.el9_5.x86_64", "/lib/modules/5.14.0-503.19.1.el9_5.x86_64", + "--fstab", + "--add=dm mdraid", + "--add-drivers=raid0 raid1", + "--hostonly", + "--force", + }, + Output: "", + ExitCode: 0, + }, + }, + want: `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=almalinux +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="console=ttyS1,115200n8 root=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 init=/sbin/init net.ifnames=0 biosdevname=0 nvme_core.io_timeout=300" +GRUB_TERMINAL=serial +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=1 --word=8" +GRUB_DEVICE=UUID=ace079b5-06be-4429-bbf0-081ea4d7d0d9 +GRUB_ENABLE_BLSCFG=false +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := almalinux.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + }) + + gotErr := d.GrubInstall(t.Context(), tt.cmdLine) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(almalinux.DefaultGrubPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/almalinux/write_ntp_conf.go b/pkg/installer/os/almalinux/write_ntp_conf.go index 14c4a0c..e0bf1e9 100644 --- a/pkg/installer/os/almalinux/write_ntp_conf.go +++ b/pkg/installer/os/almalinux/write_ntp_conf.go @@ -11,7 +11,7 @@ const ( chronyConfigPath = "/etc/chrony.conf" ) -func (o *os) WriteNTPConf(ctx context.Context) error { +func (o *Os) WriteNTPConf(ctx context.Context) error { if len(o.allocation.NtpServers) == 0 { return nil } diff --git a/pkg/installer/os/common/copy_ssh_keys.go b/pkg/installer/os/common/copy_ssh_keys.go index dd3d5f4..87ebbb3 100644 --- a/pkg/installer/os/common/copy_ssh_keys.go +++ b/pkg/installer/os/common/copy_ssh_keys.go @@ -2,33 +2,32 @@ package oscommon import ( "context" - "os/user" "path" "strconv" "strings" ) func (d *CommonTasks) CopySSHKeys(ctx context.Context) error { - var ( - sshPath = path.Join("/home", metalUser, ".ssh") - sshAuthorizedKeysPath = path.Join(sshPath, "authorized_keys") - ) - - err := d.fs.MkdirAll(sshPath, 0700) + u, err := d.lookupUserFn(MetalUser) if err != nil { return err } - u, err := user.Lookup(metalUser) + uid, err := strconv.Atoi(u.Uid) if err != nil { return err } - - uid, err := strconv.Atoi(u.Uid) + gid, err := strconv.Atoi(u.Gid) if err != nil { return err } - gid, err := strconv.Atoi(u.Gid) + + var ( + sshPath = path.Join(u.HomeDir, ".ssh") + sshAuthorizedKeysPath = path.Join(sshPath, "authorized_keys") + ) + + err = d.fs.MkdirAll(sshPath, 0700) if err != nil { return err } diff --git a/pkg/installer/os/common/create_metal_user.go b/pkg/installer/os/common/create_metal_user.go index b6339ee..ca5eb50 100644 --- a/pkg/installer/os/common/create_metal_user.go +++ b/pkg/installer/os/common/create_metal_user.go @@ -10,13 +10,15 @@ import ( ) const ( - metalUser = "metal" + MetalUser = "metal" ) +type LookupUserFn func(name string) (*user.User, error) + func (d *CommonTasks) CreateMetalUser(ctx context.Context, sudoGroup string) error { - u, err := user.Lookup(metalUser) + u, err := d.lookupUserFn(MetalUser) if err != nil { - if err.Error() != user.UnknownUserError(metalUser).Error() { + if err.Error() != user.UnknownUserError(MetalUser).Error() { return err } } @@ -26,7 +28,7 @@ func (d *CommonTasks) CreateMetalUser(ctx context.Context, sudoGroup string) err _, err = d.exec.Execute(ctx, &exec.Params{ Name: "userdel", - Args: []string{metalUser}, + Args: []string{MetalUser}, Timeout: 10 * time.Second, }) if err != nil { @@ -36,7 +38,7 @@ func (d *CommonTasks) CreateMetalUser(ctx context.Context, sudoGroup string) err _, err = d.exec.Execute(ctx, &exec.Params{ Name: "useradd", - Args: []string{"--create-home", "--uid", "1000", "--gid", sudoGroup, "--shell", "/bin/bash", metalUser}, + Args: []string{"--create-home", "--uid", "1000", "--gid", sudoGroup, "--shell", "/bin/bash", MetalUser}, Timeout: 10 * time.Second, }) if err != nil { @@ -45,7 +47,7 @@ func (d *CommonTasks) CreateMetalUser(ctx context.Context, sudoGroup string) err _, err = d.exec.Execute(ctx, &exec.Params{ Name: "passwd", - Args: []string{metalUser}, + Args: []string{MetalUser}, Timeout: 10 * time.Second, Stdin: d.details.Password + "\n" + d.details.Password + "\n", }) diff --git a/pkg/installer/os/common/defaultos.go b/pkg/installer/os/common/defaultos.go index dbdd8d1..384bf8b 100644 --- a/pkg/installer/os/common/defaultos.go +++ b/pkg/installer/os/common/defaultos.go @@ -3,20 +3,20 @@ package oscommon import "context" type ( - defaultOS struct { + DefaultOS struct { *CommonTasks bootloaderID *string } ) -func NewDefaultOS(cfg *Config) *defaultOS { - return &defaultOS{ +func NewDefaultOS(cfg *Config) *DefaultOS { + return &DefaultOS{ CommonTasks: New(cfg), bootloaderID: cfg.BootloaderID, } } -func (o *defaultOS) BootloaderID() string { +func (o *DefaultOS) BootloaderID() string { if o.bootloaderID == nil { panic("no bootloader id provided for default os") } @@ -24,14 +24,14 @@ func (o *defaultOS) BootloaderID() string { return *o.bootloaderID } -func (o *defaultOS) WriteBootInfo(ctx context.Context, cmdLine string) error { +func (o *DefaultOS) WriteBootInfo(ctx context.Context, cmdLine string) error { return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } -func (o *defaultOS) CreateMetalUser(ctx context.Context) error { +func (o *DefaultOS) CreateMetalUser(ctx context.Context) error { return o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) } -func (o *defaultOS) GrubInstall(ctx context.Context, cmdLine string) error { +func (o *DefaultOS) GrubInstall(ctx context.Context, cmdLine string) error { return o.CommonTasks.GrubInstall(ctx, o.BootloaderID(), cmdLine) } diff --git a/pkg/installer/os/common/oscommon.go b/pkg/installer/os/common/oscommon.go index 1187d25..a796ce3 100644 --- a/pkg/installer/os/common/oscommon.go +++ b/pkg/installer/os/common/oscommon.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "os" + "os/user" "path" "strconv" "strings" @@ -45,6 +46,7 @@ type ( Exec *exec.CmdExecutor MachineDetails *v1.MachineDetails Allocation *apiv2.MachineAllocation + LookupUserFn LookupUserFn // customization options from installer config Name *string @@ -52,23 +54,30 @@ type ( } CommonTasks struct { - log *slog.Logger - fs *afero.Afero - details *v1.MachineDetails - allocation *apiv2.MachineAllocation - exec *exec.CmdExecutor - network *network.Network + log *slog.Logger + fs *afero.Afero + details *v1.MachineDetails + allocation *apiv2.MachineAllocation + exec *exec.CmdExecutor + network *network.Network + lookupUserFn LookupUserFn } ) func New(cfg *Config) *CommonTasks { + lookupUserFn := user.Lookup + if cfg.LookupUserFn != nil { + lookupUserFn = cfg.LookupUserFn + } + return &CommonTasks{ - log: cfg.Log, - fs: cfg.Fs, - details: cfg.MachineDetails, - allocation: cfg.Allocation, - exec: cfg.Exec, - network: network.New(cfg.Allocation), + log: cfg.Log, + fs: cfg.Fs, + details: cfg.MachineDetails, + allocation: cfg.Allocation, + exec: cfg.Exec, + network: network.New(cfg.Allocation), + lookupUserFn: lookupUserFn, } } @@ -148,7 +157,7 @@ func (d *CommonTasks) KernelAndInitrdPath(initramdiskFormatString string) (kern return "", "", fmt.Errorf("unable to find a System.map, probably no kernel installed: %w", err) } if len(systemMaps) != 1 { - return "", "", fmt.Errorf("more or less than a single System.map found (%v), probably no kernel or more than one kernel installed", systemMaps) + return "", "", fmt.Errorf("no single System.map found (%v), probably no kernel or more than one kernel installed", systemMaps) } systemMap := systemMaps[0] diff --git a/pkg/installer/os/debian/debian.go b/pkg/installer/os/debian/debian.go index 5ae11d8..480f2b8 100644 --- a/pkg/installer/os/debian/debian.go +++ b/pkg/installer/os/debian/debian.go @@ -7,29 +7,29 @@ import ( ) type ( - os struct { + Os struct { *oscommon.CommonTasks } ) -func New(cfg *oscommon.Config) *os { - return &os{ +func New(cfg *oscommon.Config) *Os { + return &Os{ CommonTasks: oscommon.New(cfg), } } -func (o *os) BootloaderID() string { +func (o *Os) BootloaderID() string { return "metal-debian" } -func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { +func (o *Os) WriteBootInfo(ctx context.Context, cmdLine string) error { return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } -func (o *os) CreateMetalUser(ctx context.Context) error { +func (o *Os) CreateMetalUser(ctx context.Context) error { return o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) } -func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { +func (o *Os) GrubInstall(ctx context.Context, cmdLine string) error { return o.CommonTasks.GrubInstall(ctx, o.BootloaderID(), cmdLine) } diff --git a/pkg/installer/os/debian/tests/doc.go b/pkg/installer/os/debian/tests/doc.go new file mode 100644 index 0000000..b77ea8e --- /dev/null +++ b/pkg/installer/os/debian/tests/doc.go @@ -0,0 +1,3 @@ +package debian + +// as most of the implementation is shared, it's sufficient to put all tests in the ubuntu os implementation and just test the differences here diff --git a/pkg/installer/os/os.go b/pkg/installer/os/os.go index 67729ed..79624a2 100644 --- a/pkg/installer/os/os.go +++ b/pkg/installer/os/os.go @@ -14,6 +14,8 @@ import ( ) const ( + OsReleasePath = "/etc/os-release" + ubuntuOS = osName("ubuntu") debianOS = osName("debian") almalinuxOS = osName("almalinux") @@ -62,7 +64,7 @@ func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { func detectOS(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { cfg.Log.Info("automatically detecting operating system for installation") - content, err := cfg.Fs.ReadFile("/etc/os-release") + content, err := cfg.Fs.ReadFile(OsReleasePath) if err != nil { return nil, err } @@ -99,6 +101,9 @@ func fromOsName(name string, cfg *oscommon.Config) (oscommon.OperatingSystem, er cfg.Log.Info("using almalinux os-installer") return almalinux.New(cfg), nil default: + if cfg.Name != nil { + return nil, fmt.Errorf("os with name %q is not supported", os) + } cfg.Log.Info("using default os-installer implementation") return oscommon.NewDefaultOS(cfg), nil } diff --git a/pkg/installer/os/os_test.go b/pkg/installer/os/os_test.go new file mode 100644 index 0000000..3bc3357 --- /dev/null +++ b/pkg/installer/os/os_test.go @@ -0,0 +1,170 @@ +package operatingsystem_test + +import ( + "fmt" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + operatingsystem "github.com/metal-stack/os-installer/pkg/installer/os" + "github.com/metal-stack/os-installer/pkg/installer/os/almalinux" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/debian" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + ubuntuRelease = `PRETTY_NAME="Ubuntu 24.04.4 LTS" +NAME="Ubuntu" +VERSION_ID="24.04" +VERSION="24.04.4 LTS (Noble Numbat)" +VERSION_CODENAME=noble +ID=ubuntu +ID_LIKE=debian +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +UBUNTU_CODENAME=noble +LOGO=ubuntu-logo +` + debianRelease = `PRETTY_NAME="Debian GNU/Linux 13 (trixie)" +NAME="Debian GNU/Linux" +VERSION_ID="13" +VERSION="13 (trixie)" +VERSION_CODENAME=trixie +DEBIAN_VERSION_FULL=13.4 +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +` + almalinuxRelease = `NAME="AlmaLinux" +VERSION="10.1 (Heliotrope Lion)" +ID="almalinux" +ID_LIKE="rhel centos fedora" +VERSION_ID="10.1" +PLATFORM_ID="platform:el10" +PRETTY_NAME="AlmaLinux 10.1 (Heliotrope Lion)" +ANSI_COLOR="0;34" +LOGO="fedora-logo-icon" +CPE_NAME="cpe:/o:almalinux:almalinux:10.1" +HOME_URL="https://almalinux.org/" +DOCUMENTATION_URL="https://wiki.almalinux.org/" +VENDOR_NAME="AlmaLinux" +VENDOR_URL="https://almalinux.org/" +BUG_REPORT_URL="https://bugs.almalinux.org/" + +ALMALINUX_MANTISBT_PROJECT="AlmaLinux-10" +ALMALINUX_MANTISBT_PROJECT_VERSION="10.1" +REDHAT_SUPPORT_PRODUCT="AlmaLinux" +REDHAT_SUPPORT_PRODUCT_VERSION="10.1" +SUPPORT_END=2035-06-01 +` + unknownRelease = `NAME="EndeavourOS" +PRETTY_NAME="EndeavourOS" +ID="endeavouros" +ID_LIKE="arch" +BUILD_ID="2025.03.19" +ANSI_COLOR="38;2;23;147;209" +HOME_URL="https://endeavouros.com" +DOCUMENTATION_URL="https://discovery.endeavouros.com" +SUPPORT_URL="https://forum.endeavouros.com" +BUG_REPORT_URL="https://forum.endeavouros.com/c/general-system/endeavouros-installation" +PRIVACY_POLICY_URL="https://endeavouros.com/privacy-policy-2" +LOGO="endeavouros"` +) + +func Test_New(t *testing.T) { + tests := []struct { + name string + explicitOS *string + fsMocks func(fs *afero.Afero) + want any + wantErr error + }{ + { + name: "detect ubuntu", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, operatingsystem.OsReleasePath, []byte(ubuntuRelease), 0777)) + }, + want: &ubuntu.Os{}, + }, + { + name: "detect debian", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, operatingsystem.OsReleasePath, []byte(debianRelease), 0777)) + }, + want: &debian.Os{}, + }, + { + name: "detect almalinux", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, operatingsystem.OsReleasePath, []byte(almalinuxRelease), 0777)) + }, + want: &almalinux.Os{}, + }, + { + name: "detect default for unknown", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, operatingsystem.OsReleasePath, []byte(unknownRelease), 0777)) + }, + want: &oscommon.DefaultOS{}, + }, + { + name: "explicitly want almalinux impl on unknown os", + explicitOS: new("almalinux"), + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, operatingsystem.OsReleasePath, []byte(unknownRelease), 0777)) + }, + want: &almalinux.Os{}, + }, + { + name: "explicitly want unsupported", + explicitOS: new("foo"), + fsMocks: func(fs *afero.Afero) { + require.NoError(t, afero.WriteFile(fs, operatingsystem.OsReleasePath, []byte(unknownRelease), 0777)) + }, + wantErr: fmt.Errorf(`os with name "foo" is not supported`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + os, gotErr := operatingsystem.New(&oscommon.Config{ + Log: log, + Name: tt.explicitOS, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + MachineDetails: &v1.MachineDetails{}, + Allocation: &apiv2.MachineAllocation{}, + }) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + assert.IsType(t, tt.want, os) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/cmd_line_test.go b/pkg/installer/os/ubuntu/tests/cmd_line_test.go index 6bb3608..693a273 100644 --- a/pkg/installer/os/ubuntu/tests/cmd_line_test.go +++ b/pkg/installer/os/ubuntu/tests/cmd_line_test.go @@ -26,7 +26,7 @@ MD_DEVICE_dev_sda2_ROLE=0 MD_DEVICE_dev_sda2_DEV=/dev/sda2` ) -func TestDefaultOS_BuildCMDLine(t *testing.T) { +func Test_os_BuildCMDLine(t *testing.T) { tests := []struct { name string details *v1.MachineDetails diff --git a/pkg/installer/os/ubuntu/tests/copy_ssh_keys_test.go b/pkg/installer/os/ubuntu/tests/copy_ssh_keys_test.go new file mode 100644 index 0000000..e922924 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/copy_ssh_keys_test.go @@ -0,0 +1,74 @@ +package ubuntu_test + +import ( + "log/slog" + "os/user" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_os_CopySSHKeys(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + lookupUserFn oscommon.LookupUserFn + wantErr error + }{ + { + name: "copy ssh keys", + lookupUserFn: func(name string) (*user.User, error) { + return &user.User{ + Uid: "1000", + Gid: "1000", + Username: oscommon.MetalUser, + Name: oscommon.MetalUser, + HomeDir: "/home/metal", + }, nil + }, + allocation: &apiv2.MachineAllocation{ + SshPublicKeys: []string{"a", "b"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + LookupUserFn: tt.lookupUserFn, + Allocation: tt.allocation, + }) + + gotErr := d.CopySSHKeys(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile("/home/metal/.ssh/authorized_keys") + require.NoError(t, err) + + assert.Equal(t, "a\nb", string(content)) + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/create_metal_user_test.go b/pkg/installer/os/ubuntu/tests/create_metal_user_test.go new file mode 100644 index 0000000..45af7b1 --- /dev/null +++ b/pkg/installer/os/ubuntu/tests/create_metal_user_test.go @@ -0,0 +1,107 @@ +package ubuntu_test + +import ( + "log/slog" + "os/user" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/metal-stack/os-installer/api/v1" + "github.com/metal-stack/os-installer/pkg/exec" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/installer/os/ubuntu" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" +) + +func Test_os_CreateMetalUser(t *testing.T) { + tests := []struct { + name string + details *v1.MachineDetails + execMocks []test.FakeExecParams + lookupUserFn oscommon.LookupUserFn + want string + wantErr error + }{ + { + name: "create user already exists", + details: &v1.MachineDetails{ + Password: "abc", + }, + lookupUserFn: func(name string) (*user.User, error) { + return &user.User{ + Uid: "1000", + Gid: "1000", + Username: oscommon.MetalUser, + Name: oscommon.MetalUser, + HomeDir: "/home/metal", + }, nil + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"userdel", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"useradd", "--create-home", "--uid", "1000", "--gid", "sudo", "--shell", "/bin/bash", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"passwd", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + }, + }, + { + name: "create user does not yet exist", + details: &v1.MachineDetails{ + Password: "abc", + }, + lookupUserFn: func(name string) (*user.User, error) { + return nil, user.UnknownUserError(oscommon.MetalUser) + }, + execMocks: []test.FakeExecParams{ + { + WantCmd: []string{"useradd", "--create-home", "--uid", "1000", "--gid", "sudo", "--shell", "/bin/bash", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + { + WantCmd: []string{"passwd", oscommon.MetalUser}, + Output: "", + ExitCode: 0, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + d := ubuntu.New(&oscommon.Config{ + Log: log, + Fs: fs, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t, tt.execMocks...)), + MachineDetails: tt.details, + LookupUserFn: tt.lookupUserFn, + }) + + gotErr := d.CreateMetalUser(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + }) + } +} diff --git a/pkg/installer/os/ubuntu/tests/fix_permissions_test.go b/pkg/installer/os/ubuntu/tests/fix_permissions_test.go index 07b77d6..f2d3bee 100644 --- a/pkg/installer/os/ubuntu/tests/fix_permissions_test.go +++ b/pkg/installer/os/ubuntu/tests/fix_permissions_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDefaultOS_FixPermissions(t *testing.T) { +func Test_os_FixPermissions(t *testing.T) { tests := []struct { name string fsMocks func(fs afero.Fs) diff --git a/pkg/installer/os/ubuntu/tests/process_userdata_test.go b/pkg/installer/os/ubuntu/tests/process_userdata_test.go index 7e788c5..14c2795 100644 --- a/pkg/installer/os/ubuntu/tests/process_userdata_test.go +++ b/pkg/installer/os/ubuntu/tests/process_userdata_test.go @@ -25,7 +25,7 @@ groups: sampleIgnition = `{"ignition":{"config":{},"security":{"tls":{}},"timeouts":{},"version":"2.2.0"}}` ) -func TestDefaultOS_ProcessUserdata(t *testing.T) { +func Test_os_ProcessUserdata(t *testing.T) { tests := []struct { name string details *v1.MachineDetails diff --git a/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go b/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go index 8ceb6c8..dcf6078 100644 --- a/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go +++ b/pkg/installer/os/ubuntu/tests/unset_machine_id_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDefaultOS_UnsetMachineID(t *testing.T) { +func Test_os_UnsetMachineID(t *testing.T) { tests := []struct { name string fsMocks func(fs *afero.Afero) diff --git a/pkg/installer/os/ubuntu/tests/write_build_meta_test.go b/pkg/installer/os/ubuntu/tests/write_build_meta_test.go index d5e5ccc..2a1b308 100644 --- a/pkg/installer/os/ubuntu/tests/write_build_meta_test.go +++ b/pkg/installer/os/ubuntu/tests/write_build_meta_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDefaultOS_WriteBuildMeta(t *testing.T) { +func Test_os_WriteBuildMeta(t *testing.T) { tests := []struct { name string allocation *apiv2.MachineAllocation diff --git a/pkg/installer/os/ubuntu/tests/write_hostname_test.go b/pkg/installer/os/ubuntu/tests/write_hostname_test.go index 1a12c68..55b75b3 100644 --- a/pkg/installer/os/ubuntu/tests/write_hostname_test.go +++ b/pkg/installer/os/ubuntu/tests/write_hostname_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDefaultOS_WriteHostname(t *testing.T) { +func Test_os_WriteHostname(t *testing.T) { tests := []struct { name string allocation *apiv2.MachineAllocation diff --git a/pkg/installer/os/ubuntu/tests/write_hosts_test.go b/pkg/installer/os/ubuntu/tests/write_hosts_test.go index 8a2a9f0..3f68ed7 100644 --- a/pkg/installer/os/ubuntu/tests/write_hosts_test.go +++ b/pkg/installer/os/ubuntu/tests/write_hosts_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestDefaultOS_WriteHosts(t *testing.T) { +func Test_os_WriteHosts(t *testing.T) { tests := []struct { name string allocation *apiv2.MachineAllocation diff --git a/pkg/installer/os/ubuntu/ubuntu.go b/pkg/installer/os/ubuntu/ubuntu.go index c61768a..6466847 100644 --- a/pkg/installer/os/ubuntu/ubuntu.go +++ b/pkg/installer/os/ubuntu/ubuntu.go @@ -7,29 +7,29 @@ import ( ) type ( - os struct { + Os struct { *oscommon.CommonTasks } ) -func New(cfg *oscommon.Config) *os { - return &os{ +func New(cfg *oscommon.Config) *Os { + return &Os{ CommonTasks: oscommon.New(cfg), } } -func (o *os) BootloaderID() string { +func (o *Os) BootloaderID() string { return "metal-ubuntu" } -func (o *os) WriteBootInfo(ctx context.Context, cmdLine string) error { +func (o *Os) WriteBootInfo(ctx context.Context, cmdLine string) error { return o.CommonTasks.WriteBootInfo(ctx, o.InitramdiskFormatString(), o.BootloaderID(), cmdLine) } -func (o *os) CreateMetalUser(ctx context.Context) error { +func (o *Os) CreateMetalUser(ctx context.Context) error { return o.CommonTasks.CreateMetalUser(ctx, o.SudoGroup()) } -func (o *os) GrubInstall(ctx context.Context, cmdLine string) error { +func (o *Os) GrubInstall(ctx context.Context, cmdLine string) error { return o.CommonTasks.GrubInstall(ctx, o.BootloaderID(), cmdLine) } From 0434a697d38bc8a4aa1f5138972064e569fd5613 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 17 Mar 2026 09:38:52 +0100 Subject: [PATCH 069/102] Fix text. --- pkg/installer/os/debian/tests/write_boot_info_test.go | 4 ++-- pkg/installer/os/ubuntu/tests/write_boot_info_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/installer/os/debian/tests/write_boot_info_test.go b/pkg/installer/os/debian/tests/write_boot_info_test.go index 8ef6e0b..80b55ba 100644 --- a/pkg/installer/os/debian/tests/write_boot_info_test.go +++ b/pkg/installer/os/debian/tests/write_boot_info_test.go @@ -50,7 +50,7 @@ func Test_os_WriteBootInfo(t *testing.T) { require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) }, want: nil, - wantErr: fmt.Errorf("more or less than a single System.map found ([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), + wantErr: fmt.Errorf("no single System.map found ([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), }, { name: "no system.map present", @@ -60,7 +60,7 @@ func Test_os_WriteBootInfo(t *testing.T) { require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) }, want: nil, - wantErr: fmt.Errorf("more or less than a single System.map found ([]), probably no kernel or more than one kernel installed"), + wantErr: fmt.Errorf("no single System.map found ([]), probably no kernel or more than one kernel installed"), }, { name: "no vmlinuz present", diff --git a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go index ad3da3d..f3d313a 100644 --- a/pkg/installer/os/ubuntu/tests/write_boot_info_test.go +++ b/pkg/installer/os/ubuntu/tests/write_boot_info_test.go @@ -50,7 +50,7 @@ func Test_os_WriteBootInfo(t *testing.T) { require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) }, want: nil, - wantErr: fmt.Errorf("more or less than a single System.map found ([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), + wantErr: fmt.Errorf("no single System.map found ([/boot/System.map-1.2.3 /boot/System.map-1.2.4]), probably no kernel or more than one kernel installed"), }, { name: "no system.map present", @@ -60,7 +60,7 @@ func Test_os_WriteBootInfo(t *testing.T) { require.NoError(t, fs.WriteFile("/boot/initrd.img-1.2.3", nil, 0700)) }, want: nil, - wantErr: fmt.Errorf("more or less than a single System.map found ([]), probably no kernel or more than one kernel installed"), + wantErr: fmt.Errorf("no single System.map found ([]), probably no kernel or more than one kernel installed"), }, { name: "no vmlinuz present", From 8063af8f614a51d9ae062eee6837cee581137c95 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 09:47:58 +0100 Subject: [PATCH 070/102] More debug log --- pkg/frr/frr.go | 2 +- pkg/frr/frr_version.go | 8 +++++--- pkg/frr/frr_version_test.go | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 96439ac..5377b4c 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -208,7 +208,7 @@ func assembleVRFs(cfg *Config) ([]vrf, error) { ) if cfg.FRRVersion == nil { - frrVersion, err := DetectVersion() + frrVersion, err := DetectVersion(cfg.Log) if err != nil { return nil, fmt.Errorf("unable to detect frr version: %w", err) } diff --git a/pkg/frr/frr_version.go b/pkg/frr/frr_version.go index be95dbd..1570242 100644 --- a/pkg/frr/frr_version.go +++ b/pkg/frr/frr_version.go @@ -2,13 +2,14 @@ package frr import ( "fmt" + "log/slog" "os/exec" "strings" "github.com/Masterminds/semver/v3" ) -func DetectVersion() (*semver.Version, error) { +func DetectVersion(log *slog.Logger) (*semver.Version, error) { vtysh, err := exec.LookPath("vtysh") if err != nil { return nil, fmt.Errorf("unable to detect path to vtysh: %w", err) @@ -30,12 +31,13 @@ func DetectVersion() (*semver.Version, error) { return nil, fmt.Errorf("unable to detect frr version with vtysh output:%s error: %w", string(out), err) } - return parseVersion(string(out)) + return parseVersion(log, string(out)) } -func parseVersion(vtyshOutput string) (*semver.Version, error) { +func parseVersion(log *slog.Logger, vtyshOutput string) (*semver.Version, error) { var frrVersion string + log.Info("parseVersion", "vtysh output", vtyshOutput) for line := range strings.SplitSeq(vtyshOutput, "\n") { if !strings.Contains(line, "Integrated shell for FRR") { continue diff --git a/pkg/frr/frr_version_test.go b/pkg/frr/frr_version_test.go index 271eda9..5cdbf07 100644 --- a/pkg/frr/frr_version_test.go +++ b/pkg/frr/frr_version_test.go @@ -1,6 +1,7 @@ package frr import ( + "log/slog" "testing" "github.com/Masterminds/semver/v3" @@ -49,7 +50,7 @@ Configured with: } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parseVersion(tt.cmdoutput) + got, err := parseVersion(slog.Default(), tt.cmdoutput) if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { t.Errorf("error diff (+got -want):\n%s", diff) } From d34b9a3120b2a1a991dcb251bb511272cf074a12 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 10:02:28 +0100 Subject: [PATCH 071/102] More edgecase covered --- pkg/frr/frr_version.go | 2 +- pkg/frr/frr_version_test.go | 50 ++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/pkg/frr/frr_version.go b/pkg/frr/frr_version.go index 1570242..d52c0b7 100644 --- a/pkg/frr/frr_version.go +++ b/pkg/frr/frr_version.go @@ -48,7 +48,7 @@ func parseVersion(log *slog.Logger, vtyshOutput string) (*semver.Version, error) continue } - version, found := strings.CutSuffix(dirtyVersion, ").") + version, _, found := strings.Cut(dirtyVersion, ").") if !found { continue } diff --git a/pkg/frr/frr_version_test.go b/pkg/frr/frr_version_test.go index 5cdbf07..4c0e301 100644 --- a/pkg/frr/frr_version_test.go +++ b/pkg/frr/frr_version_test.go @@ -20,19 +20,41 @@ func TestDetectVersion(t *testing.T) { { name: "frr 10.4", cmdoutput: ` -vtysh -h -Usage : vtysh [OPTION...] -Integrated shell for FRR (version 10.4.3). -`, + vtysh -h + Usage : vtysh [OPTION...] + Integrated shell for FRR (version 10.4.3). + `, want: semver.MustParse("10.4.3"), wantErr: nil, }, { name: "frr 8.4", cmdoutput: ` -Integrated shell for FRR (version 8.4.4). + Integrated shell for FRR (version 8.4.4). + Configured with: + '--build=x86_64-linux-gnu' '--prefix=/usr' '--includedir=${prefix}/include' '--mandir=${prefix}/share/man' '--infodir=${prefix}/share/info' '--sysconfdir=/etc' '--localstatedir=/var' '--disable-option-checking' '--disable-silent-rules' '--libdir=${prefix}/lib/x86_64-linux-gnu' '--libexecdir=${prefix}/lib/x86_64-linux-gnu' '--disable-maintainer-mode' '--localstatedir=/var/run/frr' '--sbindir=/usr/lib/frr' '--sysconfdir=/etc/frr' '--with-vtysh-pager=/usr/bin/pager' '--libdir=/usr/lib/x86_64-linux-gnu/frr' '--with-moduledir=/usr/lib/x86_64-linux-gnu/frr/modules' '--disable-dependency-tracking' '--enable-rpki' '--disable-scripting' '--disable-pim6d' '--with-libpam' '--enable-doc' '--enable-doc-html' '--enable-snmp' '--enable-fpm' '--disable-protobuf' '--disable-zeromq' '--enable-ospfapi' '--enable-bgp-vnc' '--enable-multipath=256' '--enable-user=frr' '--enable-group=frr' '--enable-vty-group=frrvty' '--enable-configfile-mask=0640' '--enable-logfile-mask=0640' 'build_alias=x86_64-linux-gnu' 'PYTHON=python3' + + -b, --boot Execute boot startup configuration + -c, --command Execute argument as command + -d, --daemon Connect only to the specified daemon + -f, --inputfile Execute commands from specific file and exit + -E, --echo Echo prompt and command in -c mode + -C, --dryrun Check configuration for validity and exit + -m, --markfile Mark input file with context end + --vty_socket Override vty socket path + --config_dir Override config directory path + `, + want: semver.MustParse("8.4.4"), + wantErr: nil, + }, + + { + name: "10.4.1", + cmdoutput: `Usage : vtysh [OPTION...] + +Integrated shell for FRR (version 10.4.1). Configured with: - '--build=x86_64-linux-gnu' '--prefix=/usr' '--includedir=${prefix}/include' '--mandir=${prefix}/share/man' '--infodir=${prefix}/share/info' '--sysconfdir=/etc' '--localstatedir=/var' '--disable-option-checking' '--disable-silent-rules' '--libdir=${prefix}/lib/x86_64-linux-gnu' '--libexecdir=${prefix}/lib/x86_64-linux-gnu' '--disable-maintainer-mode' '--localstatedir=/var/run/frr' '--sbindir=/usr/lib/frr' '--sysconfdir=/etc/frr' '--with-vtysh-pager=/usr/bin/pager' '--libdir=/usr/lib/x86_64-linux-gnu/frr' '--with-moduledir=/usr/lib/x86_64-linux-gnu/frr/modules' '--disable-dependency-tracking' '--enable-rpki' '--disable-scripting' '--disable-pim6d' '--with-libpam' '--enable-doc' '--enable-doc-html' '--enable-snmp' '--enable-fpm' '--disable-protobuf' '--disable-zeromq' '--enable-ospfapi' '--enable-bgp-vnc' '--enable-multipath=256' '--enable-user=frr' '--enable-group=frr' '--enable-vty-group=frrvty' '--enable-configfile-mask=0640' '--enable-logfile-mask=0640' 'build_alias=x86_64-linux-gnu' 'PYTHON=python3' + '--build=x86_64-linux-gnu' '--prefix=/usr' '--includedir=${prefix}/include' '--mandir=${prefix}/share/man' '--infodir=${prefix}/share/info' '--sysconfdir=/etc' '--localstatedir=/var' '--disable-option-checking' '--disable-silent-rules' '--libdir=${prefix}/lib/x86_64-linux-gnu' '--libexecdir=${prefix}/lib/x86_64-linux-gnu' '--disable-maintainer-mode' '--sbindir=/usr/lib/frr' '--with-vtysh-pager=/usr/bin/pager' '--libdir=/usr/lib/x86_64-linux-gnu/frr' '--with-moduledir=/usr/lib/x86_64-linux-gnu/frr/modules' '--disable-dependency-tracking' '--enable-rpki' '--disable-scripting' '--enable-pim6d' '--disable-grpc' '--with-libpam' '--enable-doc' '--enable-doc-html' '--enable-snmp' '--enable-fpm' '--disable-protobuf' '--disable-zeromq' '--enable-ospfapi' '--enable-bgp-vnc' '--enable-cumulus=yes' '--enable-multipath=256' '--enable-pcre2posix' '--enable-user=frr' '--enable-group=frr' '--enable-vty-group=frrvty' '--enable-configfile-mask=0640' '--enable-logfile-mask=0640' 'build_alias=x86_64-linux-gnu' 'PYTHON=python3' -b, --boot Execute boot startup configuration -c, --command Execute argument as command @@ -43,9 +65,19 @@ Configured with: -m, --markfile Mark input file with context end --vty_socket Override vty socket path --config_dir Override config directory path -`, - want: semver.MustParse("8.4.4"), - wantErr: nil, +-N --pathspace Insert prefix into config & socket paths +-u --user Run as an unprivileged user +-w, --writeconfig Write integrated config (frr.conf) and exit +-H, --histfile Override history file +-t, --timestamp Print a timestamp before going to shell or reading the configuration + --no-fork Don't fork clients to handle daemons (slower for large configs) + --exec-timeout Set an idle timeout for this vtysh session +-h, --help Display this help and exit + +Note that multiple commands may be executed from the command +line by passing multiple -c args, or by embedding linefeed +characters in one or more of the commands.`, + want: semver.MustParse("10.4.1"), }, } for _, tt := range tests { From 31287d7c790ed753a49488be053ad92e44a132a1 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 10:18:07 +0100 Subject: [PATCH 072/102] Fix nft validation --- pkg/frr/frr.go | 2 +- pkg/nftables/nftables.go | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 5377b4c..8d8f46e 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -152,7 +152,7 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return nil } - return validate(frrConfigPath) + return validate(path) }, }) if err != nil { diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index db0ff7f..5253217 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -147,7 +147,7 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { if !cfg.Validate { return nil } - return validate(cfg) + return validate(cfg, path) }, }) if err != nil { @@ -159,8 +159,6 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { return changed, err } - // FIXME validate generated rule file before reloading - if cfg.Reload && changed { if err := systemd_renderer.Reload(ctx, cfg.Log, serviceName); err != nil { return changed, err @@ -385,10 +383,10 @@ func getAddressFamily(p string) (string, error) { } // validate validates network interfaces configuration. -func validate(cfg *Config) error { - cfg.Log.Info("running 'nft --check --file' to validate changes.", "file", nftrulesPath) +func validate(cfg *Config, path string) error { + cfg.Log.Info("running 'nft --check --file' to validate changes.", "file", path) - cmd := exec.Command("nft", "--check", "--file", nftrulesPath) + cmd := exec.Command("nft", "--check", "--file", path) out, err := cmd.CombinedOutput() if err != nil { cfg.Log.Error("nft validation failed", "output", string(out), "error", err) From 37d7cba35920303ab7c07d340e07d4b3708e4dec Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 10:39:19 +0100 Subject: [PATCH 073/102] Debug systemd reload --- pkg/systemd-service-renderer/systemd_renderer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index b9b1c64..46a0d41 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -85,11 +85,11 @@ func (r *systemdRenderer) Render(ctx context.Context, destFile string, reload bo func Reload(ctx context.Context, log *slog.Logger, unitName string) error { const done = "done" - log.Info("reloading systemd service unit") + log.Info("reloading systemd service unit", "unit", unitName) dbc, err := dbus.NewWithContext(ctx) if err != nil { - return fmt.Errorf("unable to connect to dbus: %w", err) + return fmt.Errorf("unable to connect to dbus to reload unit:%s %w", unitName, err) } defer dbc.Close() @@ -102,7 +102,7 @@ func Reload(ctx context.Context, log *slog.Logger, unitName string) error { job := <-c if job != done { - return fmt.Errorf("reloading failed: %s", job) + return fmt.Errorf("reloading of unit:%s failed: %s", unitName, job) } return nil From a663aefa918318bb51e9d42c1a6478284e17a992 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 10:56:06 +0100 Subject: [PATCH 074/102] Do not enable services on install --- pkg/services/droptailer/droptailer.go | 1 + pkg/services/firewall-controller/firewall-controller.go | 1 + pkg/services/install.go | 8 +++++++- pkg/services/nftables-exporter/nftables-exporter.go | 1 + pkg/services/node-exporter/node-exporter.go | 1 + pkg/services/suricata/suricata.go | 1 + pkg/services/tailscale/tailscale.go | 1 + pkg/systemd-service-renderer/systemd_renderer.go | 4 ++-- 8 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/services/droptailer/droptailer.go b/pkg/services/droptailer/droptailer.go index 7b441b7..e4fc53b 100644 --- a/pkg/services/droptailer/droptailer.go +++ b/pkg/services/droptailer/droptailer.go @@ -21,6 +21,7 @@ var ( type Config struct { Log *slog.Logger + Enable bool Reload bool fs afero.Fs } diff --git a/pkg/services/firewall-controller/firewall-controller.go b/pkg/services/firewall-controller/firewall-controller.go index f1430d8..c25ce85 100644 --- a/pkg/services/firewall-controller/firewall-controller.go +++ b/pkg/services/firewall-controller/firewall-controller.go @@ -22,6 +22,7 @@ var ( type Config struct { Log *slog.Logger + Enable bool Reload bool fs afero.Fs } diff --git a/pkg/services/install.go b/pkg/services/install.go index 986c62c..1d937ca 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -38,6 +38,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Droptailer if _, err = droptailer.WriteSystemdUnit(ctx, &droptailer.Config{ Log: log, + Enable: false, Reload: false, }, &droptailer.TemplateData{ Comment: "created from os-installer", @@ -49,8 +50,8 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Chrony if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ Log: log, + Enable: false, Reload: false, - Enable: true, ChronyConfigPath: "", }, &chrony.TemplateData{ NTPServers: network.NTPServers(), @@ -61,6 +62,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // firewall-controller if _, err = firewallcontroller.WriteSystemdUnit(ctx, &firewallcontroller.Config{ Log: log, + Enable: false, Reload: false, }, &firewallcontroller.TemplateData{ Comment: "created from os-installer", @@ -72,6 +74,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // nftables-exporter if _, err := nftablesexporter.WriteSystemdUnit(ctx, &nftablesexporter.Config{ Log: log, + Enable: false, Reload: false, }, &nftablesexporter.TemplateData{ Comment: "created from os-installer", @@ -82,6 +85,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // node-exporter if _, err := nodeexporter.WriteSystemdUnit(ctx, &nodeexporter.Config{ Log: log, + Enable: false, Reload: false, }, &nodeexporter.TemplateData{ Comment: "created from os-installer", @@ -92,6 +96,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // suricata if _, err := suricata.WriteSystemdUnit(ctx, &suricata.Config{ Log: log, + Enable: false, Reload: false, }, &suricata.TemplateData{ Interface: "TODO", @@ -105,6 +110,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ vpn := network.Vpn() if _, err := tailscale.WriteSystemdUnit(ctx, &tailscale.Config{ Log: log, + Enable: false, Reload: false, }, &tailscale.TemplateData{ Comment: "created from os-installer", diff --git a/pkg/services/nftables-exporter/nftables-exporter.go b/pkg/services/nftables-exporter/nftables-exporter.go index ede6e8d..cadb139 100644 --- a/pkg/services/nftables-exporter/nftables-exporter.go +++ b/pkg/services/nftables-exporter/nftables-exporter.go @@ -21,6 +21,7 @@ var ( type Config struct { Log *slog.Logger + Enable bool Reload bool fs afero.Fs } diff --git a/pkg/services/node-exporter/node-exporter.go b/pkg/services/node-exporter/node-exporter.go index 98afd70..dad6dec 100644 --- a/pkg/services/node-exporter/node-exporter.go +++ b/pkg/services/node-exporter/node-exporter.go @@ -21,6 +21,7 @@ var ( type Config struct { Log *slog.Logger + Enable bool Reload bool fs afero.Fs } diff --git a/pkg/services/suricata/suricata.go b/pkg/services/suricata/suricata.go index 7bfb2d9..6da07ab 100644 --- a/pkg/services/suricata/suricata.go +++ b/pkg/services/suricata/suricata.go @@ -32,6 +32,7 @@ var ( type Config struct { Log *slog.Logger + Enable bool Reload bool fs afero.Fs } diff --git a/pkg/services/tailscale/tailscale.go b/pkg/services/tailscale/tailscale.go index 275b34c..118d277 100644 --- a/pkg/services/tailscale/tailscale.go +++ b/pkg/services/tailscale/tailscale.go @@ -29,6 +29,7 @@ var ( type Config struct { Log *slog.Logger + Enable bool Reload bool fs afero.Fs } diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index 46a0d41..e63bee1 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -113,12 +113,12 @@ func Enable(ctx context.Context, log *slog.Logger, unitName string) error { dbc, err := dbus.NewWithContext(ctx) if err != nil { - return fmt.Errorf("unable to connect to dbus: %w", err) + return fmt.Errorf("unable to connect to dbus to enable unit:%s %w", unitName, err) } defer dbc.Close() if _, _, err = dbc.EnableUnitFilesContext(ctx, []string{unitName}, false, false); err != nil { - return fmt.Errorf("unable to enable systemd unit: %w", err) + return fmt.Errorf("unable to enable systemd unit: %s %w", unitName, err) } return nil From cf10a959934f5c3a007168877ce8d1914980461d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 11:31:49 +0100 Subject: [PATCH 075/102] Enable systemd services without dbus --- pkg/services/install.go | 14 +++++++------- .../systemd_renderer.go | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pkg/services/install.go b/pkg/services/install.go index 1d937ca..610b173 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -38,7 +38,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Droptailer if _, err = droptailer.WriteSystemdUnit(ctx, &droptailer.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, }, &droptailer.TemplateData{ Comment: "created from os-installer", @@ -50,7 +50,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Chrony if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, ChronyConfigPath: "", }, &chrony.TemplateData{ @@ -62,7 +62,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // firewall-controller if _, err = firewallcontroller.WriteSystemdUnit(ctx, &firewallcontroller.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, }, &firewallcontroller.TemplateData{ Comment: "created from os-installer", @@ -74,7 +74,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // nftables-exporter if _, err := nftablesexporter.WriteSystemdUnit(ctx, &nftablesexporter.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, }, &nftablesexporter.TemplateData{ Comment: "created from os-installer", @@ -85,7 +85,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // node-exporter if _, err := nodeexporter.WriteSystemdUnit(ctx, &nodeexporter.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, }, &nodeexporter.TemplateData{ Comment: "created from os-installer", @@ -96,7 +96,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // suricata if _, err := suricata.WriteSystemdUnit(ctx, &suricata.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, }, &suricata.TemplateData{ Interface: "TODO", @@ -110,7 +110,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ vpn := network.Vpn() if _, err := tailscale.WriteSystemdUnit(ctx, &tailscale.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, }, &tailscale.TemplateData{ Comment: "created from os-installer", diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index e63bee1..430a839 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "log/slog" + "time" "github.com/coreos/go-systemd/v22/dbus" + "github.com/metal-stack/os-installer/pkg/exec" renderer "github.com/metal-stack/os-installer/pkg/template-renderer" "github.com/spf13/afero" ) @@ -111,15 +113,15 @@ func Reload(ctx context.Context, log *slog.Logger, unitName string) error { func Enable(ctx context.Context, log *slog.Logger, unitName string) error { log.Info("enable systemd service unit", "unit-name", unitName) - dbc, err := dbus.NewWithContext(ctx) - if err != nil { - return fmt.Errorf("unable to connect to dbus to enable unit:%s %w", unitName, err) - } - defer dbc.Close() + ex := exec.New(log) - if _, _, err = dbc.EnableUnitFilesContext(ctx, []string{unitName}, false, false); err != nil { - return fmt.Errorf("unable to enable systemd unit: %s %w", unitName, err) + out, err := ex.Execute(ctx, &exec.Params{ + Name: "systemctl", + Args: []string{"enable", unitName}, + Timeout: 10 * time.Second, + }) + if err != nil { + return fmt.Errorf("unable to enable systemd unit:%s output:%s error:%w", unitName, out, err) } - return nil } From 346ba1c632588bb91d3dca5ea41ca546f2a5899c Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 12:09:36 +0100 Subject: [PATCH 076/102] Disable chrony for now --- pkg/services/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/install.go b/pkg/services/install.go index 610b173..91b95e2 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -50,7 +50,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Chrony if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ Log: log, - Enable: true, + Enable: false, Reload: false, ChronyConfigPath: "", }, &chrony.TemplateData{ From bf724badc84292ca5e799429951843eaeef46d31 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 12:27:47 +0100 Subject: [PATCH 077/102] Enable services if set --- pkg/services/droptailer/droptailer.go | 6 ++++++ pkg/services/firewall-controller/firewall-controller.go | 6 ++++++ pkg/services/nftables-exporter/nftables-exporter.go | 6 ++++++ pkg/services/node-exporter/node-exporter.go | 6 ++++++ pkg/services/tailscale/tailscale.go | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/pkg/services/droptailer/droptailer.go b/pkg/services/droptailer/droptailer.go index e4fc53b..37a5962 100644 --- a/pkg/services/droptailer/droptailer.go +++ b/pkg/services/droptailer/droptailer.go @@ -43,5 +43,11 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } + if cfg.Enable { + if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/firewall-controller/firewall-controller.go b/pkg/services/firewall-controller/firewall-controller.go index c25ce85..ebb9e6e 100644 --- a/pkg/services/firewall-controller/firewall-controller.go +++ b/pkg/services/firewall-controller/firewall-controller.go @@ -44,5 +44,11 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } + if cfg.Enable { + if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/nftables-exporter/nftables-exporter.go b/pkg/services/nftables-exporter/nftables-exporter.go index cadb139..fd9fe79 100644 --- a/pkg/services/nftables-exporter/nftables-exporter.go +++ b/pkg/services/nftables-exporter/nftables-exporter.go @@ -42,5 +42,11 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } + if cfg.Enable { + if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/node-exporter/node-exporter.go b/pkg/services/node-exporter/node-exporter.go index dad6dec..02d8c10 100644 --- a/pkg/services/node-exporter/node-exporter.go +++ b/pkg/services/node-exporter/node-exporter.go @@ -42,5 +42,11 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } + if cfg.Enable { + if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { + return changed, err + } + } + return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/tailscale/tailscale.go b/pkg/services/tailscale/tailscale.go index 118d277..702f35d 100644 --- a/pkg/services/tailscale/tailscale.go +++ b/pkg/services/tailscale/tailscale.go @@ -80,6 +80,12 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return chg, err } + if cfg.Enable { + if err := systemd_renderer.Enable(ctx, cfg.Log, spec.serviceName); err != nil { + return changed, err + } + } + // return changed if one has changed changed = changed || chg } From b6b1c589cdecc91dced9d5d2e014d1c0202c30d2 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 12:46:45 +0100 Subject: [PATCH 078/102] Use common enable --- pkg/services/droptailer/droptailer.go | 7 +------ pkg/services/firewall-controller/firewall-controller.go | 7 +------ pkg/services/nftables-exporter/nftables-exporter.go | 7 +------ pkg/services/node-exporter/node-exporter.go | 7 +------ pkg/services/tailscale/tailscale.go | 7 +------ 5 files changed, 5 insertions(+), 30 deletions(-) diff --git a/pkg/services/droptailer/droptailer.go b/pkg/services/droptailer/droptailer.go index 37a5962..2165fff 100644 --- a/pkg/services/droptailer/droptailer.go +++ b/pkg/services/droptailer/droptailer.go @@ -34,6 +34,7 @@ type TemplateData struct { func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, + Enable: cfg.Enable, ServiceName: serviceName, TemplateString: templateString, Data: c, @@ -43,11 +44,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } - if cfg.Enable { - if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { - return changed, err - } - } - return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/firewall-controller/firewall-controller.go b/pkg/services/firewall-controller/firewall-controller.go index ebb9e6e..361820b 100644 --- a/pkg/services/firewall-controller/firewall-controller.go +++ b/pkg/services/firewall-controller/firewall-controller.go @@ -35,6 +35,7 @@ type TemplateData struct { func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, + Enable: cfg.Enable, ServiceName: serviceName, TemplateString: templateString, Data: c, @@ -44,11 +45,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } - if cfg.Enable { - if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { - return changed, err - } - } - return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/nftables-exporter/nftables-exporter.go b/pkg/services/nftables-exporter/nftables-exporter.go index fd9fe79..c5dc984 100644 --- a/pkg/services/nftables-exporter/nftables-exporter.go +++ b/pkg/services/nftables-exporter/nftables-exporter.go @@ -33,6 +33,7 @@ type TemplateData struct { func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, + Enable: cfg.Enable, ServiceName: serviceName, TemplateString: templateString, Data: c, @@ -42,11 +43,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } - if cfg.Enable { - if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { - return changed, err - } - } - return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/node-exporter/node-exporter.go b/pkg/services/node-exporter/node-exporter.go index 02d8c10..04c9db2 100644 --- a/pkg/services/node-exporter/node-exporter.go +++ b/pkg/services/node-exporter/node-exporter.go @@ -33,6 +33,7 @@ type TemplateData struct { func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (changed bool, err error) { r, err := systemd_renderer.New(&systemd_renderer.Config{ Log: cfg.Log, + Enable: cfg.Enable, ServiceName: serviceName, TemplateString: templateString, Data: c, @@ -42,11 +43,5 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return false, err } - if cfg.Enable { - if err := systemd_renderer.Enable(ctx, cfg.Log, serviceName); err != nil { - return changed, err - } - } - return r.Render(ctx, serviceUnitPath, cfg.Reload) } diff --git a/pkg/services/tailscale/tailscale.go b/pkg/services/tailscale/tailscale.go index 702f35d..04747cc 100644 --- a/pkg/services/tailscale/tailscale.go +++ b/pkg/services/tailscale/tailscale.go @@ -66,6 +66,7 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change } { r, err := systemd_renderer.New(&systemd_renderer.Config{ ServiceName: spec.serviceName, + Enable: cfg.Enable, Log: cfg.Log, TemplateString: spec.templateString, Data: c, @@ -80,12 +81,6 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData) (change return chg, err } - if cfg.Enable { - if err := systemd_renderer.Enable(ctx, cfg.Log, spec.serviceName); err != nil { - return changed, err - } - } - // return changed if one has changed changed = changed || chg } From 24d0e07da563f61645b6472204699fff994c95e2 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 12:48:24 +0100 Subject: [PATCH 079/102] wrap in bash --- pkg/systemd-service-renderer/systemd_renderer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index 430a839..7dd4a44 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -116,8 +116,8 @@ func Enable(ctx context.Context, log *slog.Logger, unitName string) error { ex := exec.New(log) out, err := ex.Execute(ctx, &exec.Params{ - Name: "systemctl", - Args: []string{"enable", unitName}, + Name: "bash", + Args: []string{"-c", fmt.Sprintf("systemctl enable %s", unitName)}, Timeout: 10 * time.Second, }) if err != nil { From 0d12444f5641b9ae6dfa11217d963f8ecb6ada00 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 12:48:54 +0100 Subject: [PATCH 080/102] Enable chrony again --- pkg/services/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/install.go b/pkg/services/install.go index 91b95e2..610b173 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -50,7 +50,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Chrony if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, ChronyConfigPath: "", }, &chrony.TemplateData{ From 13045232c749d705712e09b82dc7a08d696dde31 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 13:04:45 +0100 Subject: [PATCH 081/102] Disable chrony again --- pkg/services/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/install.go b/pkg/services/install.go index 610b173..91b95e2 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -50,7 +50,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Chrony if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ Log: log, - Enable: true, + Enable: false, Reload: false, ChronyConfigPath: "", }, &chrony.TemplateData{ From b67d8f167dd7bd5b05916329117baecd90ff1641 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 15:30:11 +0100 Subject: [PATCH 082/102] Do not error out on systemctl enable --- pkg/services/install.go | 2 +- pkg/systemd-service-renderer/systemd_renderer.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/services/install.go b/pkg/services/install.go index 91b95e2..610b173 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -50,7 +50,7 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ // Chrony if _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ Log: log, - Enable: false, + Enable: true, Reload: false, ChronyConfigPath: "", }, &chrony.TemplateData{ diff --git a/pkg/systemd-service-renderer/systemd_renderer.go b/pkg/systemd-service-renderer/systemd_renderer.go index 7dd4a44..7354b71 100644 --- a/pkg/systemd-service-renderer/systemd_renderer.go +++ b/pkg/systemd-service-renderer/systemd_renderer.go @@ -121,7 +121,8 @@ func Enable(ctx context.Context, log *slog.Logger, unitName string) error { Timeout: 10 * time.Second, }) if err != nil { - return fmt.Errorf("unable to enable systemd unit:%s output:%s error:%w", unitName, out, err) + // Do not error out because some service can be enabled, but the enable command returns an error. + log.Error("unable to enable systemd unit", "unit", unitName, "output", out, "error", err) } return nil } From bd9e7f60bb72718f4907a56471d2f28aebe44709 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 17 Mar 2026 15:56:25 +0100 Subject: [PATCH 083/102] Remove last networker artifact --- go.mod | 4 ++-- go.sum | 8 ++++---- validate.sh | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 64e8e95..181acd0 100644 --- a/go.mod +++ b/go.mod @@ -44,8 +44,8 @@ require ( golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 33fa380..2b66d78 100644 --- a/go.sum +++ b/go.sum @@ -113,10 +113,10 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= -google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= +google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/validate.sh b/validate.sh index 7ce4061..4ac7850 100755 --- a/validate.sh +++ b/validate.sh @@ -4,7 +4,7 @@ set -e validate () { echo "----------------------------------------------------------------" - echo "Validating sample artifacts of metal-networker with ${1}:${2} frr:${3}" + echo "Validating sample artifacts of os-installer with ${1}:${2} frr:${3}" echo "----------------------------------------------------------------" tag="${1}_${2}_${3}" docker build \ From 6c055d576ced734c86a7321fbeaad511c6f21251 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 18 Mar 2026 09:28:53 +0100 Subject: [PATCH 084/102] Better Tests --- .gitignore | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f476789..bb0a578 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin/* .vscode +coverage.out \ No newline at end of file diff --git a/Makefile b/Makefile index 36541b9..b06cdde 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ binary: .PHONY: test test: - GO_ENV=testing go test -race -cover ./... + GO_ENV=testing go test ./... -race -coverpkg=./... -coverprofile=coverage.out -covermode=atomic && go tool cover -func=coverage.out .PHONY: validate From c16d51a4847b64edb1abde27feb8145996b972e3 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 18 Mar 2026 09:29:22 +0100 Subject: [PATCH 085/102] Legacy InstallConfig --- api/v1/api.go | 11 --- api/v1/legacy.go | 166 +++++++++++++++++++++++++++++++++++++ pkg/installer/installer.go | 4 +- pkg/installer/legacy.go | 35 ++++++++ 4 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 api/v1/legacy.go create mode 100644 pkg/installer/legacy.go diff --git a/api/v1/api.go b/api/v1/api.go index ed5d54e..dfdd701 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -8,7 +8,6 @@ const ( MachineDetailsPath = "/etc/metal/machine-details.yaml" MachineAllocationPath = "/etc/metal/machine-allocation.yaml" InstallerConfigPath = "/etc/metal/os-installer.yaml" - LLDPDConfigPath = "/etc/metal/install.yaml" BuildMetaPath = "/etc/metal/build-meta.yaml" BootInfoPath = "/etc/metal/boot-info.yaml" ) @@ -59,16 +58,6 @@ type ( RootUUID string `yaml:"root_uuid"` } - // LLDPDConfig contains the configuration which is required for the lldpd to start. - // must be stored in yaml format at /etc/metal/install.yaml - // Is written by by the metal-hammer - LLDPDConfig struct { - // MachineUUID is the unique UUID for this machine, usually the board serial. - MachineUUID string `yaml:"machineuuid"` - // Timestamp is the the timestamp of installer config creation. - Timestamp string `yaml:"timestamp"` - } - // BuildMeta is written after the installation finished to store details about the installation version. BuildMeta struct { Version string `json:"buildVersion" yaml:"buildVersion"` diff --git a/api/v1/legacy.go b/api/v1/legacy.go new file mode 100644 index 0000000..45d9c53 --- /dev/null +++ b/api/v1/legacy.go @@ -0,0 +1,166 @@ +package v1 + +const ( + LegacyInstallPath = "/etc/metal/install.yaml" +) + +type ( + + // InstallerConfig contains legacy configuration items which are + // used to install the os. + // It must be serialized to /etc/metal/install.yaml to guarantee compatibility for older + // firewall-controller and lldpd + InstallerConfig struct { + // Hostname of the machine + Hostname string `yaml:"hostname"` + // Networks all networks connected to this machine + Networks []*V1MachineNetwork `yaml:"networks"` + // MachineUUID is the unique UUID for this machine, usually the board serial. + MachineUUID string `yaml:"machineuuid"` + // SSHPublicKey of the user + SSHPublicKey string `yaml:"sshpublickey"` + // Password is the password for the metal user. + Password string `yaml:"password"` + // Console specifies where the kernel should connect its console to. + Console string `yaml:"console"` + // Timestamp is the the timestamp of installer config creation. + Timestamp string `yaml:"timestamp"` + // Nics are the network interfaces of this machine including their neighbors. + Nics []*V1MachineNic `yaml:"nics"` + // VPN is the config for connecting machine to VPN + VPN *V1MachineVPN `yaml:"vpn"` + // Role is either firewall or machine + Role string `yaml:"role"` + // RaidEnabled is set to true if any raid devices are specified + RaidEnabled bool `yaml:"raidenabled"` + // RootUUID is the fs uuid if the root fs + RootUUID string `yaml:"root_uuid"` + // FirewallRules if not empty firewall rules to enforce + FirewallRules *V1FirewallRules `yaml:"firewall_rules"` + // DNSServers for the machine + DNSServers []*V1DNSServer `yaml:"dns_servers"` + // NTPServers for the machine + NTPServers []*V1NTPServer `yaml:"ntp_servers"` + } + + // Copies of metal-go models.V1* structs in use in Installerconfig + // to prevent the import of metal-go. + + V1MachineNetwork struct { + // ASN number for this network in the bgp configuration + // Required: true + Asn *int64 `json:"asn" yaml:"asn"` + // the destination prefixes of this network + // Required: true + Destinationprefixes []string `json:"destinationprefixes" yaml:"destinationprefixes"` + // the ip addresses of the allocated machine in this vrf + // Required: true + Ips []string `json:"ips" yaml:"ips"` + // if set to true, packets leaving this network get masqueraded behind interface ip + // Required: true + Nat *bool `json:"nat" yaml:"nat"` + // nattypev2 + // Required: true + Nattypev2 *string `json:"nattypev2" yaml:"nattypev2"` + // the networkID of the allocated machine in this vrf + // Required: true + Networkid *string `json:"networkid" yaml:"networkid"` + // the network type, types can be looked up in the network package of metal-lib + // Required: true + Networktype *string `json:"networktype" yaml:"networktype"` + // networktypev2 + // Required: true + Networktypev2 *string `json:"networktypev2" yaml:"networktypev2"` + // the prefixes of this network + // Required: true + Prefixes []string `json:"prefixes" yaml:"prefixes"` + // indicates whether this network is the private network of this machine + // Required: true + Private *bool `json:"private" yaml:"private"` + // project of this network, empty string if not project scoped + // Required: true + Projectid *string `json:"projectid" yaml:"projectid"` + // if set to true, this network can be used for underlay communication + // Required: true + Underlay *bool `json:"underlay" yaml:"underlay"` + // the vrf of the allocated machine + // Required: true + Vrf *int64 `json:"vrf" yaml:"vrf"` + } + + V1MachineNic struct { + // the unique identifier of this network interface + // Required: true + Identifier *string `json:"identifier" yaml:"identifier"` + // the mac address of this network interface + // Required: true + Mac *string `json:"mac" yaml:"mac"` + // the name of this network interface + // Required: true + Name *string `json:"name" yaml:"name"` + // the neighbors visible to this network interface + // Required: true + Neighbors []*V1MachineNic `json:"neighbors" yaml:"neighbors"` + } + + V1MachineVPN struct { + // address of VPN control plane + // Required: true + Address *string `json:"address" yaml:"address"` + // auth key used to connect to VPN + // Required: true + AuthKey *string `json:"auth_key" yaml:"auth_key"` + // connected to the VPN + // Required: true + Connected *bool `json:"connected" yaml:"connected"` + } + + V1FirewallRules struct { + // list of egress rules to be deployed during firewall allocation + Egress []*V1FirewallEgressRule `json:"egress" yaml:"egress"` + // list of ingress rules to be deployed during firewall allocation + Ingress []*V1FirewallIngressRule `json:"ingress" yaml:"ingress"` + } + + V1FirewallEgressRule struct { + // an optional comment describing what this rule is used for + Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` + // the ports affected by this rule + // Required: true + Ports []int32 `json:"ports" yaml:"ports"` + // the protocol for the rule, defaults to tcp + // Enum: ["tcp","udp"] + Protocol string `json:"protocol,omitempty" yaml:"protocol,omitempty"` + // the cidrs affected by this rule + // Required: true + To []string `json:"to" yaml:"to"` + } + + V1FirewallIngressRule struct { + // an optional comment describing what this rule is used for + Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` + // the cidrs affected by this rule + // Required: true + From []string `json:"from" yaml:"from"` + // the ports affected by this rule + // Required: true + Ports []int32 `json:"ports" yaml:"ports"` + // the protocol for the rule, defaults to tcp + // Enum: ["tcp","udp"] + Protocol string `json:"protocol,omitempty" yaml:"protocol,omitempty"` + // the cidrs affected by this rule + To []string `json:"to" yaml:"to"` + } + + V1DNSServer struct { + // ip address of this dns server + // Required: true + IP *string `json:"ip" yaml:"ip"` + } + + V1NTPServer struct { + // ip address or dns hostname of this ntp server + // Required: true + Address *string `json:"address" yaml:"address"` + } +) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 78b7b4a..ef4f079 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -48,7 +48,7 @@ func (i *installer) PersistConfigurations() error { if err != nil { return fmt.Errorf("unable to marshal machine details: %w", err) } - err = os.WriteFile(v1.MachineDetailsPath, detailsBytes, os.ModePerm) + err = i.fs.WriteFile(v1.MachineDetailsPath, detailsBytes, os.ModePerm) if err != nil { return fmt.Errorf("unable to persist machine details: %w", err) } @@ -57,7 +57,7 @@ func (i *installer) PersistConfigurations() error { if err != nil { return fmt.Errorf("unable to marshal machine allocation: %w", err) } - err = os.WriteFile(v1.MachineAllocationPath, allocationBytes, os.ModePerm) + err = i.fs.WriteFile(v1.MachineAllocationPath, allocationBytes, os.ModePerm) if err != nil { return fmt.Errorf("unable to persist machine allocation: %w", err) } diff --git a/pkg/installer/legacy.go b/pkg/installer/legacy.go new file mode 100644 index 0000000..138f688 --- /dev/null +++ b/pkg/installer/legacy.go @@ -0,0 +1,35 @@ +package installer + +import ( + "fmt" + "os" + + v1 "github.com/metal-stack/os-installer/api/v1" + "go.yaml.in/yaml/v3" +) + +func (i *installer) PersistLegacyInstallYaml(installConfig *v1.InstallerConfig) error { + installBytes, err := yaml.Marshal(installConfig) + if err != nil { + return fmt.Errorf("unable to marshal legacy installer config: %w", err) + } + err = i.fs.WriteFile(v1.LegacyInstallPath, installBytes, os.ModePerm) + if err != nil { + return fmt.Errorf("unable to persist legacy installer config: %w", err) + } + return nil +} + +func ReadLegacyInstallYaml() (*v1.InstallerConfig, error) { + data, err := os.ReadFile(v1.LegacyInstallPath) + if err != nil { + return nil, fmt.Errorf("unable to read legacy installer config: %w", err) + } + + var installConfig v1.InstallerConfig + if err = yaml.Unmarshal(data, &installConfig); err != nil { + return nil, fmt.Errorf("unable to parse legacy installer config: %w", err) + } + + return &installConfig, nil +} From 525a6328b6959455e710f432befb6a64d754bb56 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 18 Mar 2026 10:54:38 +0100 Subject: [PATCH 086/102] Add missing process userdata task. --- pkg/installer/installer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index ef4f079..0d70d15 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -180,6 +180,10 @@ func (i *installer) run(ctx context.Context) error { name: "fix wrong filesystem permissions", fn: i.oss.FixPermissions, }, + { + name: "process userdata", + fn: i.oss.ProcessUserdata, + }, { name: "build kernel cmdline", fn: func(ctx context.Context) error { From 0f0e215cf6f0bd976ca2c9b7332bf2e741aed1fe Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 18 Mar 2026 12:20:22 +0100 Subject: [PATCH 087/102] Execute ignition with directory --- pkg/exec/cmdexec.go | 8 -------- pkg/installer/os/common/process_userdata.go | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/exec/cmdexec.go b/pkg/exec/cmdexec.go index 2b8516f..a5bf0e1 100644 --- a/pkg/exec/cmdexec.go +++ b/pkg/exec/cmdexec.go @@ -10,10 +10,6 @@ import ( "time" ) -const ( - defaultExecDir = "/etc/metal" -) - type CmdExecutor struct { log *slog.Logger c func(ctx context.Context, name string, arg ...string) *exec.Cmd @@ -55,10 +51,6 @@ func (i *CmdExecutor) Execute(ctx context.Context, p *Params) (out string, err e } cmd := i.c(ctx, p.Name, p.Args...) - if p.Dir != "" { - cmd.Dir = defaultExecDir - } - cmd.Env = append(cmd.Env, p.Env...) // show stderr diff --git a/pkg/installer/os/common/process_userdata.go b/pkg/installer/os/common/process_userdata.go index 93784d3..4683342 100644 --- a/pkg/installer/os/common/process_userdata.go +++ b/pkg/installer/os/common/process_userdata.go @@ -66,6 +66,7 @@ func (d *CommonTasks) ProcessUserdata(ctx context.Context) error { _, err = d.exec.Execute(ctx, &exec.Params{ Name: "ignition", Args: []string{"-oem", "file", "-stage", "files", "-log-to-stdout"}, + Dir: "/etc/metal", }) if err != nil { d.log.Error("error when running ignition, continuing anyway", "report", report.Entries, "error", err) From 386c29e831affadad06615299f771af1715a3d79 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 18 Mar 2026 15:24:47 +0100 Subject: [PATCH 088/102] Next try --- pkg/installer/os/common/process_userdata.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/installer/os/common/process_userdata.go b/pkg/installer/os/common/process_userdata.go index 4683342..22f5c68 100644 --- a/pkg/installer/os/common/process_userdata.go +++ b/pkg/installer/os/common/process_userdata.go @@ -11,7 +11,7 @@ import ( const ( UserdataPath = "/etc/metal/userdata" - ignitionUserdataPath = "/etc/metal/config.ign" + ignitionUserdataPath = "/config.ign" ) func (d *CommonTasks) ProcessUserdata(ctx context.Context) error { @@ -66,7 +66,7 @@ func (d *CommonTasks) ProcessUserdata(ctx context.Context) error { _, err = d.exec.Execute(ctx, &exec.Params{ Name: "ignition", Args: []string{"-oem", "file", "-stage", "files", "-log-to-stdout"}, - Dir: "/etc/metal", + Dir: "/", }) if err != nil { d.log.Error("error when running ignition, continuing anyway", "report", report.Entries, "error", err) From 1d52e128dfcbe357ee9c92ee598e0a6d16e0d335 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 18 Mar 2026 16:13:46 +0100 Subject: [PATCH 089/102] fix suricata external interface --- pkg/services/install.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/install.go b/pkg/services/install.go index 610b173..7e099e7 100644 --- a/pkg/services/install.go +++ b/pkg/services/install.go @@ -4,6 +4,7 @@ import ( "context" "errors" "log/slog" + "strings" "github.com/metal-stack/os-installer/pkg/network" "github.com/metal-stack/os-installer/pkg/services/chrony" @@ -94,12 +95,15 @@ func WriteSystemdServices(ctx context.Context, log *slog.Logger, network *networ } // suricata + // + // TODO: this listens only on one internet facing interface, but should listening on all external interfaces. + suricataInterface := strings.ReplaceAll(defaultRouteVRF, "vrf", "vlan") if _, err := suricata.WriteSystemdUnit(ctx, &suricata.Config{ Log: log, Enable: true, Reload: false, }, &suricata.TemplateData{ - Interface: "TODO", + Interface: suricataInterface, DefaultRouteVrf: defaultRouteVRF, }); err != nil { errs = append(errs, err) From 09ea6ce2cc43dbf19bc33440950fa2f1b5f4465d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 18 Mar 2026 16:15:23 +0100 Subject: [PATCH 090/102] remove solved fixmes --- pkg/nftables/nftables.go | 1 - pkg/nftables/nftables_test.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 5253217..012f1db 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -52,7 +52,6 @@ type ( Network *network.Network - // FIXME validator net.Validator, EnableDNSProxy bool ForwardPolicy ForwardPolicy diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index ab6014c..ad119a5 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -251,8 +251,6 @@ var ( Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", From 2f5a75d683f83131670d4086a85f9f29d38c5a9d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 19 Mar 2026 08:26:54 +0100 Subject: [PATCH 091/102] Debug instead of info --- pkg/exec/cmdexec.go | 2 +- pkg/frr/frr.go | 4 +--- pkg/frr/frr_version.go | 2 +- pkg/installer/installer.go | 6 +++--- pkg/installer/os/common/cmd_line.go | 2 +- pkg/installer/os/common/create_metal_user.go | 2 +- pkg/installer/os/common/write_build_meta.go | 2 +- pkg/installer/os/os.go | 2 +- pkg/interfaces/interfaces.go | 8 ++++---- pkg/nftables/nftables.go | 10 ++++------ 10 files changed, 18 insertions(+), 22 deletions(-) diff --git a/pkg/exec/cmdexec.go b/pkg/exec/cmdexec.go index a5bf0e1..50e4a50 100644 --- a/pkg/exec/cmdexec.go +++ b/pkg/exec/cmdexec.go @@ -42,7 +42,7 @@ func (i *CmdExecutor) Execute(ctx context.Context, p *Params) (out string, err e start = time.Now() output []byte ) - i.log.Info("running command", "command", strings.Join(append([]string{p.Name}, p.Args...), " "), "start", start.String()) + i.log.Debug("running command", "command", strings.Join(append([]string{p.Name}, p.Args...), " "), "start", start.String()) if p.Timeout != 0 { var cancel context.CancelFunc diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 8d8f46e..613ff32 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -100,7 +100,7 @@ type ( // Renders renders frr configuration according to the given input data and reloads the service if necessary func Render(ctx context.Context, cfg *Config) (changed bool, err error) { - cfg.Log.Info("render frr configuration") + cfg.Log.Debug("render frr configuration") var ( data any template string @@ -140,8 +140,6 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { template = firewallTemplateString } - cfg.Log.Info("render frr configuration", "templatedata", data) - r, err := renderer.New(&renderer.Config{ Log: cfg.Log, TemplateString: template, diff --git a/pkg/frr/frr_version.go b/pkg/frr/frr_version.go index d52c0b7..9a50f47 100644 --- a/pkg/frr/frr_version.go +++ b/pkg/frr/frr_version.go @@ -37,7 +37,7 @@ func DetectVersion(log *slog.Logger) (*semver.Version, error) { func parseVersion(log *slog.Logger, vtyshOutput string) (*semver.Version, error) { var frrVersion string - log.Info("parseVersion", "vtysh output", vtyshOutput) + log.Debug("parseVersion", "vtysh output", vtyshOutput) for line := range strings.SplitSeq(vtyshOutput, "\n") { if !strings.Contains(line, "Integrated shell for FRR") { continue diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 0d70d15..3719361 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -122,7 +122,7 @@ func (i *installer) Install(ctx context.Context) error { i.oss = oss if err = i.run(ctx); err != nil { - i.log.Info("running os installer failed", "took", time.Since(start).String()) + i.log.Error("running os installer failed", "took", time.Since(start).String()) return fmt.Errorf("os installer failed: %w", err) } @@ -232,12 +232,12 @@ func (i *installer) run(ctx context.Context) error { ) if len(i.cfg.Only) > 0 && !slices.Contains(i.cfg.Only, task.name) { - log.Info("skipping task as defined by installer configuration") + log.Warn("skipping task as defined by installer configuration") continue } if slices.Contains(i.cfg.Except, task.name) { - log.Info("skipping task as defined by installer configuration") + log.Warn("skipping task as defined by installer configuration") continue } diff --git a/pkg/installer/os/common/cmd_line.go b/pkg/installer/os/common/cmd_line.go index f613714..a2a25ac 100644 --- a/pkg/installer/os/common/cmd_line.go +++ b/pkg/installer/os/common/cmd_line.go @@ -38,7 +38,7 @@ func (d *CommonTasks) BuildCMDLine(ctx context.Context) (string, error) { } func (d *CommonTasks) findMDUUID(ctx context.Context) (mdUUID string, found bool, err error) { - d.log.Info("detect software raid uuid") + d.log.Debug("detect software raid uuid") if !d.details.RaidEnabled { return "", false, nil diff --git a/pkg/installer/os/common/create_metal_user.go b/pkg/installer/os/common/create_metal_user.go index ca5eb50..6501011 100644 --- a/pkg/installer/os/common/create_metal_user.go +++ b/pkg/installer/os/common/create_metal_user.go @@ -24,7 +24,7 @@ func (d *CommonTasks) CreateMetalUser(ctx context.Context, sudoGroup string) err } if u != nil { - d.log.Info("user already exists, recreating") + d.log.Debug("user already exists, recreating") _, err = d.exec.Execute(ctx, &exec.Params{ Name: "userdel", diff --git a/pkg/installer/os/common/write_build_meta.go b/pkg/installer/os/common/write_build_meta.go index 6226003..7a75a72 100644 --- a/pkg/installer/os/common/write_build_meta.go +++ b/pkg/installer/os/common/write_build_meta.go @@ -11,7 +11,7 @@ import ( ) func (d *CommonTasks) WriteBuildMeta(ctx context.Context) error { - d.log.Info("writing build meta file", "path", v1.BuildMetaPath) + d.log.Debug("writing build meta file", "path", v1.BuildMetaPath) meta := &v1.BuildMeta{ Version: v.Version, diff --git a/pkg/installer/os/os.go b/pkg/installer/os/os.go index 79624a2..86f05fb 100644 --- a/pkg/installer/os/os.go +++ b/pkg/installer/os/os.go @@ -62,7 +62,7 @@ func New(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { } func detectOS(cfg *oscommon.Config) (oscommon.OperatingSystem, error) { - cfg.Log.Info("automatically detecting operating system for installation") + cfg.Log.Debug("automatically detecting operating system for installation") content, err := cfg.Fs.ReadFile(OsReleasePath) if err != nil { diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index e1fe0e9..b5b1e2f 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -84,12 +84,12 @@ type ( ) func ConfigureInterfaces(ctx context.Context, cfg *Config) error { - cfg.Log.Info("create loopback interfaces") + cfg.Log.Debug("create loopback interfaces") if err := configureLoopbackInterface(ctx, cfg); err != nil { return fmt.Errorf("error configuring loopback interface: %w", err) } - cfg.Log.Info("create lan interfaces") + cfg.Log.Debug("create lan interfaces") if err := configureLanInterfaces(ctx, cfg); err != nil { return fmt.Errorf("error configuring lan interfaces: %w", err) } @@ -98,12 +98,12 @@ func ConfigureInterfaces(ctx context.Context, cfg *Config) error { return nil } - cfg.Log.Info("create bridges") + cfg.Log.Debug("create bridges") if err := configureBridges(ctx, cfg); err != nil { return fmt.Errorf("error configuring network bridges: %w", err) } - cfg.Log.Info("create evpn") + cfg.Log.Debug("create evpn") if err := configureEVPN(ctx, cfg); err != nil { return fmt.Errorf("error configuring evnps: %w", err) } diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go index 012f1db..d02cd66 100644 --- a/pkg/nftables/nftables.go +++ b/pkg/nftables/nftables.go @@ -109,7 +109,7 @@ type ( // Renders renders nftables rules according to the given input data and reloads the service if necessary func Render(ctx context.Context, cfg *Config) (changed bool, err error) { - cfg.Log.Info("render nftables configuration") + cfg.Log.Debug("render nftables configuration") const comment = "generated by os-installer" snat, err := getSNAT(cfg) @@ -135,8 +135,6 @@ func Render(ctx context.Context, cfg *Config) (changed bool, err error) { data.DNSProxyDNAT = getDNSProxyDNAT(cfg) } - cfg.Log.Info("render nftables configuration", "templatedata", data) - r, err := renderer.New(&renderer.Config{ Log: cfg.Log, TemplateString: templateString, @@ -212,7 +210,7 @@ func getSNAT(cfg *Config) ([]snat, error) { continue } - cfg.Log.Info("getSNAT", "network", n.Network) + cfg.Log.Debug("getSNAT", "network", n.Network) var ( sources []addrSpec cmt = fmt.Sprintf("snat (networkid: %s)", n.Network) @@ -230,7 +228,7 @@ func getSNAT(cfg *Config) ([]snat, error) { Address: pfx, AddressFamily: af, }) - cfg.Log.Info("getSNAT", "network", n.Network, "prefixes", pfx, "af", af) + cfg.Log.Debug("getSNAT", "network", n.Network, "prefixes", pfx, "af", af) } s := snat{ @@ -383,7 +381,7 @@ func getAddressFamily(p string) (string, error) { // validate validates network interfaces configuration. func validate(cfg *Config, path string) error { - cfg.Log.Info("running 'nft --check --file' to validate changes.", "file", path) + cfg.Log.Debug("running 'nft --check --file' to validate changes.", "file", path) cmd := exec.Command("nft", "--check", "--file", path) out, err := cmd.CombinedOutput() From 111abf488bf61218bfe1dee4218daade16660bd8 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Fri, 20 Mar 2026 14:51:46 +0100 Subject: [PATCH 092/102] Pin --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 181acd0..956ddcb 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/flatcar/ignition v0.36.2 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/metal-stack/api v0.0.55-0.20260316085710-1f98c8226b9e + github.com/metal-stack/api v0.0.56 github.com/metal-stack/v v1.0.3 github.com/samber/lo v1.53.0 github.com/spf13/afero v1.15.0 diff --git a/go.sum b/go.sum index 2b66d78..ad808dc 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/metal-stack/api v0.0.55-0.20260316085710-1f98c8226b9e h1:ixdWJR5ltPm4e4FMU+f1QgopdnN/GnFXvpDhNnXjsDg= -github.com/metal-stack/api v0.0.55-0.20260316085710-1f98c8226b9e/go.mod h1:OU8KDSOw5JEfeEs9q8FY5TcaklBAiGx+Q9Em0BMZrlY= +github.com/metal-stack/api v0.0.56 h1:wrW2zUKAOQd2qsRMyEg4Km7jkI688OZGzqas9agxMro= +github.com/metal-stack/api v0.0.56/go.mod h1:hEgtKVD7UnUwUExdA7pbFvVRxNRxSGUnU+bZce46//c= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= From 37abcaae173b6647b3c49a184ef46284898991f7 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 21 Mar 2026 08:08:57 +0100 Subject: [PATCH 093/102] prevent potential nilpointer --- pkg/frr/routemap.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 15d2908..7151f63 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -159,11 +159,11 @@ func (i *importRule) prefixLists() []ipPrefixList { ) for _, af := range afs { - pfxList := prefixLists(i.ImportPrefixesNoExport, &af, false, seed, i.TargetVRF) + pfxList := prefixLists(i.ImportPrefixesNoExport, af, false, seed, i.TargetVRF) result = append(result, pfxList...) seed = ipPrefixListSeqSeed + len(result) - result = append(result, prefixLists(i.ImportPrefixes, &af, true, seed, i.TargetVRF)...) + result = append(result, prefixLists(i.ImportPrefixes, af, true, seed, i.TargetVRF)...) } return result @@ -171,24 +171,24 @@ func (i *importRule) prefixLists() []ipPrefixList { func prefixLists( prefixes []importPrefix, - af *apiv2.NetworkAddressFamily, + af apiv2.NetworkAddressFamily, isExported bool, seed int, vrf string, ) []ipPrefixList { afString := "ip" - if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 { + if af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 { afString = "ipv6" } var result []ipPrefixList for _, p := range prefixes { - if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4 && !p.Prefix.Addr().Is4() { + if af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4 && !p.Prefix.Addr().Is4() { continue } - if *af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 && !p.Prefix.Addr().Is6() { + if af == apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6 && !p.Prefix.Addr().Is6() { continue } From b78afa079410c364a49374f9ef00bcc44539c76a Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 21 Mar 2026 08:42:39 +0100 Subject: [PATCH 094/102] bring back routemap_test, but skip for now --- pkg/frr/routemap_test.go | 556 ++++++++++++++++++--------------------- 1 file changed, 250 insertions(+), 306 deletions(-) diff --git a/pkg/frr/routemap_test.go b/pkg/frr/routemap_test.go index 11be00a..6210424 100644 --- a/pkg/frr/routemap_test.go +++ b/pkg/frr/routemap_test.go @@ -1,321 +1,265 @@ package frr -// import ( -// "fmt" -// "log/slog" -// "net/netip" -// "reflect" -// "testing" +import ( + "fmt" + "log/slog" + "net/netip" + "testing" -// "github.com/stretchr/testify/require" -// ) + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/stretchr/testify/require" +) -// type testnetwork struct { -// vrf string -// prefixes []importPrefix -// destinations []importPrefix -// } +type ( + testnetwork struct { + vrf string + prefixes []importPrefix + destinations []importPrefix + } + importSettings struct { + ImportPrefixes []importPrefix + ImportPrefixesNoExport []importPrefix + } +) -// var ( -// defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: inetVrf} -// defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: permit, SourceVRF: inetVrf} -// defaultRouteFromDMZ = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: dmzVrf} -// externalVrf = "vrf104010" -// externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: externalVrf} -// externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: externalVrf} -// privateVrf = "vrf3981" -// privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: permit, SourceVRF: privateVrf} -// privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: permit, SourceVRF: privateVrf} -// sharedVrf = "vrf3982" -// sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: sharedVrf} -// dmzVrf = "vrf3983" -// dmzNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.0/22"), Policy: permit, SourceVRF: dmzVrf} -// inetVrf = "vrf104009" -// inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: inetVrf} -// inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: inetVrf} -// inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: permit, SourceVRF: inetVrf} -// publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: inetVrf} -// publicDefaultNet2 = importPrefix{Prefix: netip.MustParsePrefix("10.0.20.2/32"), Policy: deny, SourceVRF: dmzVrf} -// publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: deny, SourceVRF: inetVrf} +var ( + defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: inetVrf} + defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: permit, SourceVRF: inetVrf} + externalVrf = "vrf104010" + externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: externalVrf} + externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: externalVrf} + privateVrf = "vrf3981" + privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: permit, SourceVRF: privateVrf} + privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: permit, SourceVRF: privateVrf} + sharedVrf = "vrf3982" + sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: sharedVrf} + inetVrf = "vrf104009" + inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: inetVrf} + inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: inetVrf} + inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: permit, SourceVRF: inetVrf} + publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: inetVrf} + publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: deny, SourceVRF: inetVrf} -// private = testnetwork{ -// vrf: privateVrf, -// prefixes: []importPrefix{privateNet}, -// } + private = testnetwork{ + vrf: privateVrf, + prefixes: []importPrefix{privateNet}, + } -// private6 = testnetwork{ -// vrf: privateVrf, -// prefixes: []importPrefix{privateNet6}, -// } + private6 = testnetwork{ + vrf: privateVrf, + prefixes: []importPrefix{privateNet6}, + } -// inet = testnetwork{ -// vrf: inetVrf, -// prefixes: []importPrefix{inetNet1, inetNet2}, -// destinations: []importPrefix{defaultRoute}, -// } + inet = testnetwork{ + vrf: inetVrf, + prefixes: []importPrefix{inetNet1, inetNet2}, + destinations: []importPrefix{defaultRoute}, + } -// inet6 = testnetwork{ -// vrf: inetVrf, -// prefixes: []importPrefix{inetNet6}, -// destinations: []importPrefix{defaultRoute6}, -// } -// dualstack = testnetwork{ -// vrf: inetVrf, -// prefixes: []importPrefix{inetNet1, inetNet6}, -// destinations: []importPrefix{defaultRoute6}, -// } -// external = testnetwork{ -// vrf: externalVrf, -// destinations: []importPrefix{externalDestinationNet}, -// prefixes: []importPrefix{externalNet}, -// } + inet6 = testnetwork{ + vrf: inetVrf, + prefixes: []importPrefix{inetNet6}, + destinations: []importPrefix{defaultRoute6}, + } + dualstack = testnetwork{ + vrf: inetVrf, + prefixes: []importPrefix{inetNet1, inetNet6}, + destinations: []importPrefix{defaultRoute6}, + } + external = testnetwork{ + vrf: externalVrf, + destinations: []importPrefix{externalDestinationNet}, + prefixes: []importPrefix{externalNet}, + } -// shared = testnetwork{ -// vrf: sharedVrf, -// prefixes: []importPrefix{sharedNet}, -// } + shared = testnetwork{ + vrf: sharedVrf, + prefixes: []importPrefix{sharedNet}, + } +) -// dmz = testnetwork{ -// vrf: dmzVrf, -// prefixes: []importPrefix{dmzNet}, -// destinations: []importPrefix{defaultRouteFromDMZ}, -// } -// ) +func Test_importRulesForNetwork(t *testing.T) { + // FIXME enable once we understand whats actually tested + t.Skip() + tests := []struct { + name string + input *apiv2.MachineAllocation + want map[string]map[string]importSettings + }{ + { + name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", + input: firewallAllocation, + want: map[string]map[string]importSettings{ + // The target VRF + private.vrf: { + // Imported VRFs with their restrictions + inet.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + }, + external.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + }, + shared.vrf: importSettings{ + ImportPrefixes: shared.prefixes, + }, + }, + shared.vrf: { + private.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), + }, + }, + inet.vrf: { + private.vrf: importSettings{ + ImportPrefixes: leakFrom(inet.prefixes, private.vrf), + ImportPrefixesNoExport: private.prefixes, + }, + }, + external.vrf: { + private.vrf: importSettings{ + ImportPrefixes: leakFrom(external.prefixes, private.vrf), + ImportPrefixesNoExport: private.prefixes, + }, + }, + }, + }, + { + name: "firewall of a shared private network (shared/storage firewall)", + input: firewallSharedAllocation, + want: map[string]map[string]importSettings{ + shared.vrf: { + inet.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + }, + }, + inet.vrf: { + shared.vrf: importSettings{ + ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), + ImportPrefixesNoExport: shared.prefixes, + }, + }, + }, + }, + { + name: "firewall with ipv6 private network and ipv6 internet network", + input: firewallIPv6Allocation, + want: map[string]map[string]importSettings{ + private6.vrf: { + inet6.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), + }, + external.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + }, + shared.vrf: importSettings{ + ImportPrefixes: shared.prefixes, + }, + }, + shared.vrf: { + private6.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), + }, + }, + inet6.vrf: { + private6.vrf: importSettings{ + ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + external.vrf: { + private6.vrf: importSettings{ + ImportPrefixes: leakFrom(external.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + }, + }, + { + name: "firewall with ipv6 private network and dualstack internet network", + input: firewallAllocationDualStack, + want: map[string]map[string]importSettings{ + private6.vrf: { + inet6.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), + }, + external.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + }, + shared.vrf: importSettings{ + ImportPrefixes: shared.prefixes, + }, + }, + shared.vrf: { + private6.vrf: importSettings{ + ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), + }, + }, + inet6.vrf: { + private6.vrf: importSettings{ + ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + external.vrf: { + private6.vrf: importSettings{ + ImportPrefixes: leakFrom(external.prefixes, private6.vrf), + ImportPrefixesNoExport: private6.prefixes, + }, + }, + }, + }, + } + log := slog.Default() -// func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { -// r := []importPrefix{} -// for _, e := range pfxs { -// i := e -// i.SourceVRF = sourceVrf -// r = append(r, i) -// } -// return r -// } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &Config{ + Log: log, + Network: network.New(tt.input), + } + for _, network := range tt.input.Networks { + got, err := importRulesForNetwork(cfg, network) + require.NoError(t, err) + if got == nil { + continue + } + gotBySourceVrf := got.bySourceVrf() + targetVrf := fmt.Sprintf("vrf%d", network.Vrf) + want := tt.want[targetVrf] -// func Test_importRulesForNetwork(t *testing.T) { -// tests := []struct { -// name string -// input string -// want map[string]map[string]ImportSettings -// }{ -// { -// name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", -// input: "testdata/firewall.yaml", -// want: map[string]map[string]ImportSettings{ -// // The target VRF -// private.vrf: { -// // Imported VRFs with their restrictions -// inet.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), -// }, -// external.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), -// }, -// shared.vrf: ImportSettings{ -// ImportPrefixes: shared.prefixes, -// }, -// }, -// shared.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), -// }, -// }, -// inet.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(inet.prefixes, private.vrf), -// ImportPrefixesNoExport: private.prefixes, -// }, -// }, -// external.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(external.prefixes, private.vrf), -// ImportPrefixesNoExport: private.prefixes, -// }, -// }, -// }, -// }, -// { -// name: "firewall of a shared private network (shared/storage firewall)", -// input: "testdata/firewall_shared.yaml", -// want: map[string]map[string]ImportSettings{ -// shared.vrf: { -// inet.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), -// }, -// }, -// inet.vrf: { -// shared.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), -// ImportPrefixesNoExport: shared.prefixes, -// }, -// }, -// }, -// }, -// { -// name: "firewall of a private network with dmz network and internet (dmz firewall)", -// input: "testdata/firewall_dmz.yaml", -// want: map[string]map[string]ImportSettings{ -// private.vrf: { -// inet.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), -// }, -// dmz.vrf: ImportSettings{ -// ImportPrefixes: dmz.prefixes, -// }, -// }, -// dmz.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), -// }, -// inet.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(inet.destinations, inet.prefixes), -// }, -// }, -// inet.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(inet.prefixes, private.vrf), -// ImportPrefixesNoExport: private.prefixes, -// }, -// dmz.vrf: ImportSettings{ -// ImportPrefixesNoExport: dmz.prefixes, -// }, -// }, -// }, -// }, -// { -// name: "firewall of a private network with dmz network (dmz app firewall)", -// input: "testdata/firewall_dmz_app.yaml", -// want: map[string]map[string]ImportSettings{ -// private.vrf: { -// dmz.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), -// }, -// }, -// dmz.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), -// }, -// }, -// }, -// }, -// { -// name: "firewall of a private network with dmz network and storage (dmz app firewall)", -// input: "testdata/firewall_dmz_app_storage.yaml", -// want: map[string]map[string]ImportSettings{ -// private.vrf: { -// shared.vrf: ImportSettings{ -// ImportPrefixes: shared.prefixes, -// }, -// dmz.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices([]importPrefix{publicDefaultNet2}, dmz.prefixes, dmz.destinations), -// }, -// }, -// dmz.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(dmz.prefixes, private.vrf)), -// }, -// }, -// shared.vrf: { -// private.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), -// }, -// }, -// }, -// }, -// { -// name: "firewall with ipv6 private network and ipv6 internet network", -// input: "testdata/firewall_ipv6.yaml", -// want: map[string]map[string]ImportSettings{ -// private6.vrf: { -// inet6.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), -// }, -// external.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), -// }, -// shared.vrf: ImportSettings{ -// ImportPrefixes: shared.prefixes, -// }, -// }, -// shared.vrf: { -// private6.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), -// }, -// }, -// inet6.vrf: { -// private6.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), -// ImportPrefixesNoExport: private6.prefixes, -// }, -// }, -// external.vrf: { -// private6.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(external.prefixes, private6.vrf), -// ImportPrefixesNoExport: private6.prefixes, -// }, -// }, -// }, -// }, -// { -// name: "firewall with ipv6 private network and dualstack internet network", -// input: "testdata/firewall_dualstack.yaml", -// want: map[string]map[string]ImportSettings{ -// private6.vrf: { -// inet6.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), -// }, -// external.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), -// }, -// shared.vrf: ImportSettings{ -// ImportPrefixes: shared.prefixes, -// }, -// }, -// shared.vrf: { -// private6.vrf: ImportSettings{ -// ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), -// }, -// }, -// inet6.vrf: { -// private6.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), -// ImportPrefixesNoExport: private6.prefixes, -// }, -// }, -// external.vrf: { -// private6.vrf: ImportSettings{ -// ImportPrefixes: leakFrom(external.prefixes, private6.vrf), -// ImportPrefixesNoExport: private6.prefixes, -// }, -// }, -// }, -// }, -// } -// // log := slog.Default() + require.Equal(t, want, gotBySourceVrf) + } + }) + } +} -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// // kb, err := New(log, tt.input) -// // require.NoError(t, err) -// // err = validate(Firewall) -// // if err != nil { -// // t.Errorf("%s is not valid: %v", tt.input, err) -// // return -// // } -// for _, network := range kb.Networks { -// got, err := importRulesForNetwork(*kb, network) -// require.NoError(t, err) -// if got == nil { -// continue -// } -// gotBySourceVrf := got.bySourceVrf() -// targetVrf := fmt.Sprintf("vrf%d", *network.Vrf) -// want := tt.want[targetVrf] +func (i *importRule) bySourceVrf() map[string]importSettings { + r := map[string]importSettings{} + for _, vrf := range i.ImportVRFs { + r[vrf] = importSettings{} + } -// if !reflect.DeepEqual(gotBySourceVrf, want) { -// t.Errorf("importRulesForNetwork() \ntargetVrf: %s \ng: %v, \nw: %v", targetVrf, gotBySourceVrf, want) -// } -// } -// }) -// } -// } + for _, pfx := range i.ImportPrefixes { + e := r[pfx.SourceVRF] + e.ImportPrefixes = append(e.ImportPrefixes, pfx) + r[pfx.SourceVRF] = e + } + + for _, pfx := range i.ImportPrefixesNoExport { + e := r[pfx.SourceVRF] + e.ImportPrefixesNoExport = append(e.ImportPrefixesNoExport, pfx) + r[pfx.SourceVRF] = e + } + + return r +} + +func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { + r := []importPrefix{} + for _, e := range pfxs { + i := e + i.SourceVRF = sourceVrf + r = append(r, i) + } + return r +} From 4418aef966f706a225072b91dfff411c8a625424 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 23 Mar 2026 07:55:51 +0100 Subject: [PATCH 095/102] Make persisting installer config a task. --- pkg/installer/config.go | 65 ++++++++++++++++++++++++++++++++++++++ pkg/installer/installer.go | 51 +++--------------------------- 2 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 pkg/installer/config.go diff --git a/pkg/installer/config.go b/pkg/installer/config.go new file mode 100644 index 0000000..f816c18 --- /dev/null +++ b/pkg/installer/config.go @@ -0,0 +1,65 @@ +package installer + +import ( + "context" + "fmt" + "os" + + "buf.build/go/protoyaml" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + v1 "github.com/metal-stack/os-installer/api/v1" + "go.yaml.in/yaml/v3" +) + +// ReadConfigurations returns the configuration that were provided from the metal-hammer, which +// were persisted as configuration files on the disk. +// The installer must have run before calling this function, otherwise the files are not there! +func ReadConfigurations() (*v1.MachineDetails, *apiv2.MachineAllocation, error) { + data, err := os.ReadFile(v1.MachineDetailsPath) + if err != nil { + return nil, nil, fmt.Errorf("unable to read machine details: %w", err) + } + + var details v1.MachineDetails + if err = yaml.Unmarshal(data, &details); err != nil { + return nil, nil, fmt.Errorf("unable to parse machine details: %w", err) + } + + data, err = os.ReadFile(v1.MachineAllocationPath) + if err != nil { + return nil, nil, fmt.Errorf("unable to read machine allocation: %w", err) + } + + var allocation apiv2.MachineAllocation + if err = protoyaml.Unmarshal(data, &allocation); err != nil { + return nil, nil, fmt.Errorf("unable to parse machine allocation: %w", err) + } + + return &details, &allocation, nil +} + +// persistConfigurations writes the configuration data provided from the metal-hammer to the os. +// these can be used again for other applications like the firewall-controller at a later point in time. +func (i *installer) persistConfigurations(context.Context) error { + detailsBytes, err := yaml.Marshal(i.details) + if err != nil { + return fmt.Errorf("unable to marshal machine details: %w", err) + } + + err = i.fs.WriteFile(v1.MachineDetailsPath, detailsBytes, os.ModePerm) + if err != nil { + return fmt.Errorf("unable to persist machine details: %w", err) + } + + allocationBytes, err := protoyaml.Marshal(i.allocation) + if err != nil { + return fmt.Errorf("unable to marshal machine allocation: %w", err) + } + + err = i.fs.WriteFile(v1.MachineAllocationPath, allocationBytes, os.ModePerm) + if err != nil { + return fmt.Errorf("unable to persist machine allocation: %w", err) + } + + return nil +} diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 3719361..193bb80 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -4,11 +4,9 @@ import ( "context" "fmt" "log/slog" - "os" "slices" "time" - "buf.build/go/protoyaml" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" v1 "github.com/metal-stack/os-installer/api/v1" "github.com/metal-stack/os-installer/pkg/exec" @@ -43,51 +41,6 @@ func New(log *slog.Logger, details *v1.MachineDetails, allocation *apiv2.Machine } } -func (i *installer) PersistConfigurations() error { - detailsBytes, err := yaml.Marshal(i.details) - if err != nil { - return fmt.Errorf("unable to marshal machine details: %w", err) - } - err = i.fs.WriteFile(v1.MachineDetailsPath, detailsBytes, os.ModePerm) - if err != nil { - return fmt.Errorf("unable to persist machine details: %w", err) - } - - allocationBytes, err := protoyaml.Marshal(i.allocation) - if err != nil { - return fmt.Errorf("unable to marshal machine allocation: %w", err) - } - err = i.fs.WriteFile(v1.MachineAllocationPath, allocationBytes, os.ModePerm) - if err != nil { - return fmt.Errorf("unable to persist machine allocation: %w", err) - } - return nil -} - -func ReadConfigurations() (*v1.MachineDetails, *apiv2.MachineAllocation, error) { - data, err := os.ReadFile(v1.MachineDetailsPath) - if err != nil { - return nil, nil, fmt.Errorf("unable to read machine details: %w", err) - } - - var details v1.MachineDetails - if err = yaml.Unmarshal(data, &details); err != nil { - return nil, nil, fmt.Errorf("unable to parse machine details: %w", err) - } - - data, err = os.ReadFile(v1.MachineAllocationPath) - if err != nil { - return nil, nil, fmt.Errorf("unable to read machine allocation: %w", err) - } - - var allocation apiv2.MachineAllocation - if err = protoyaml.Unmarshal(data, &allocation); err != nil { - return nil, nil, fmt.Errorf("unable to parse machine allocation: %w", err) - } - - return &details, &allocation, nil -} - func (i *installer) Install(ctx context.Context) error { var ( start = time.Now() @@ -140,6 +93,10 @@ func (i *installer) run(ctx context.Context) error { name string fn func(ctx context.Context) error }{ + { + name: "persist configuration data from metal-hammer", + fn: i.persistConfigurations, + }, { name: "check if running in efi mode", fn: i.validateRunningInEfiMode, From b3d309a24c4789d48a0e8a7c7c6956e49c2252f8 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 23 Mar 2026 09:51:55 +0100 Subject: [PATCH 096/102] Network tests --- pkg/network/network_test.go | 1903 +++++++++++++++++++++++++++++++++++ 1 file changed, 1903 insertions(+) create mode 100644 pkg/network/network_test.go diff --git a/pkg/network/network_test.go b/pkg/network/network_test.go new file mode 100644 index 0000000..2b66a7f --- /dev/null +++ b/pkg/network/network_test.go @@ -0,0 +1,1903 @@ +package network_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/network" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/stretchr/testify/require" +) + +func TestNetwork_MTU(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want int + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL}, + want: 9216, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE}, + want: 9000, + }, + { + name: "unknown", + allocation: &apiv2.MachineAllocation{AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_UNSPECIFIED}, + want: 9000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.MTU() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_Hostname(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want string + }{ + { + name: "with hostname", + allocation: &apiv2.MachineAllocation{Hostname: "metal"}, + want: "metal", + }, + { + name: "without hostname", + allocation: &apiv2.MachineAllocation{Hostname: ""}, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.Hostname() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_IsMachine(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want bool + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL}, + want: false, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE}, + want: true, + }, + { + name: "unknown", + allocation: &apiv2.MachineAllocation{AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_UNSPECIFIED}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.IsMachine() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_HasVpn(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want bool + }{ + { + name: "firewall with vpn", + allocation: &apiv2.MachineAllocation{Vpn: &apiv2.MachineVPN{AuthKey: "secret"}}, + want: true, + }, + { + name: "firewall with vpn but not authkey", + allocation: &apiv2.MachineAllocation{Vpn: &apiv2.MachineVPN{}}, + want: false, + }, + { + name: "firewall without vpn", + allocation: &apiv2.MachineAllocation{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.HasVpn() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_NTPServers(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []string + }{ + { + name: "with one ntp", + allocation: &apiv2.MachineAllocation{NtpServers: []*apiv2.NTPServer{{Address: "ntp.pool.org"}}}, + want: []string{"ntp.pool.org"}, + }, + { + name: "with two ntp", + allocation: &apiv2.MachineAllocation{NtpServers: []*apiv2.NTPServer{{Address: "ntp.pool.org"}, {Address: "ntp2.pool.org"}}}, + want: []string{"ntp.pool.org", "ntp2.pool.org"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.NTPServers() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_LoopbackCIDRs(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []string + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []string{"10.1.0.1/32"}, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []string{"10.0.16.2/32", "10.0.18.2/32"}, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.LoopbackCIDRs() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_UnderlayNetwork(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want *apiv2.MachineNetwork + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: &apiv2.MachineNetwork{ + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + wantErr: errors.New("no underlay network present in network allocation"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.UnderlayNetwork() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_PrivatePrimaryNetwork(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want *apiv2.MachineNetwork + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: &apiv2.MachineNetwork{ + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: &apiv2.MachineNetwork{ + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + wantErr: nil, + }, + { + name: "storage machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: &apiv2.MachineNetwork{ + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + wantErr: nil, + }, + { + name: "storage machine in wrong project", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-c"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + wantErr: errors.New("no private primary network present in network allocation"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.PrivatePrimaryNetwork() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_PrivateSecondarySharedNetworks(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []*apiv2.MachineNetwork + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + { + name: "storage machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.PrivateSecondarySharedNetworks() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_PrivatePrimaryIPs(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []string + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []string{"10.1.0.1"}, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []string{"10.0.16.2"}, + wantErr: nil, + }, + { + name: "storage machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []string{"10.0.18.2"}, + wantErr: nil, + }, + { + name: "storage machine in wrong project", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-c"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + wantErr: errors.New("no private primary ip present in network allocation"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.PrivatePrimaryIPs() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_PrivatePrimaryNetworksPrefixes(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []string + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []string{"10.0.16.0/22"}, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []string{"10.0.16.0/22"}, + wantErr: nil, + }, + { + name: "storage machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []string{"10.0.16.0/22"}, + wantErr: nil, + }, + { + name: "storage machine in wrong project", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + Project: "project-b", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-c"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + wantErr: errors.New("no private primary networks present in network allocation"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.PrivatePrimaryNetworksPrefixes() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_VxlanIDs(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []uint64 + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []uint64{3981, 3982, 104009, 104010}, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: []uint64{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.VxlanIDs() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_EVPNIfaces(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []network.EvpnIface + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []network.EvpnIface{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + CIDRs: []string{"10.0.16.2/32"}, + VlanID: 1000, + VrfID: 3981, + }, + { + Network: "partition-storage", + CIDRs: []string{"10.0.18.2/32"}, + VlanID: 1001, + VrfID: 3982, + }, + { + Network: "internet", + CIDRs: []string{"185.1.2.3/32", "185.1.2.4/32"}, + VlanID: 1002, + VrfID: 104009, + }, + { + Network: "mpls", + CIDRs: []string{"100.127.129.1/32"}, + VlanID: 1004, + VrfID: 104010, + }, + }, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + wantErr: errors.New("no evpn interfaces supported on machines"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.EVPNIfaces() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_GetNetworks(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + networkType apiv2.NetworkType + want []*apiv2.MachineNetwork + }{ + { + name: "firewall external networks", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + networkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + want: []*apiv2.MachineNetwork{ + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + + { + name: "firewall underlay networks", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + networkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + want: []*apiv2.MachineNetwork{ + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.GetNetworks(tt.networkType) + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_GetExternalNetworkVrfNames(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want []string + }{ + { + name: "firewall external networks", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: []string{"vrf104009", "vrf104010"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got := n.GetExternalNetworkVrfNames() + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_GetDefaultRouteNetwork(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want *apiv2.MachineNetwork + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: &apiv2.MachineNetwork{ + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: nil, + wantErr: errors.New("no network which provides a default route found"), + }, + + { + name: "firewall dualstack", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2a02:c00:20::1", "185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "2a02:c00:20::/45"}, + DestinationPrefixes: []string{"::/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: &apiv2.MachineNetwork{ + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2a02:c00:20::1", "185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "2a02:c00:20::/45"}, + DestinationPrefixes: []string{"::/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.GetDefaultRouteNetwork() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_GetDefaultRouteNetworkVrfName(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want string + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: "vrf104009", + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: "", + wantErr: errors.New("no network which provides a default route found"), + }, + + { + name: "firewall dualstack", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"2a02:c00:20::1", "185.1.2.3"}, + Prefixes: []string{"185.1.2.0/24", "2a02:c00:20::/45"}, + DestinationPrefixes: []string{"::/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: "vrf104009", + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.GetDefaultRouteNetworkVrfName() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + require.Equal(t, tt.want, got) + }) + } +} + +func TestNetwork_GetTenantNetworkVrfName(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + want string + wantErr error + }{ + { + name: "firewall", + allocation: &apiv2.MachineAllocation{ + Hostname: "firewall", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + Project: "project-a", + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + { + Network: "internet", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Ips: []string{"185.1.2.3", "185.1.2.4"}, + Prefixes: []string{"185.1.2.0/24", "185.27.0.0/22"}, + DestinationPrefixes: []string{"0.0.0.0/0"}, + Vrf: 104009, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + { + Network: "underlay", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Asn: 4200003073, + Ips: []string{"10.1.0.1"}, + Prefixes: []string{"10.0.12.0/22"}, + }, + { + Network: "mpls", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"100.127.129.0/24"}, + Ips: []string{"100.127.129.1"}, + DestinationPrefixes: []string{"100.127.1.0/24"}, + Vrf: 104010, + Asn: 4200003073, + NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, + }, + }, + }, + want: "vrf3981", + wantErr: nil, + }, + { + name: "machine", + allocation: &apiv2.MachineAllocation{ + Hostname: "machine", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + Networks: []*apiv2.MachineNetwork{ + { + Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", + Project: new("project-a"), + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, + Prefixes: []string{"10.0.16.0/22"}, + Ips: []string{"10.0.16.2"}, + Vrf: 3981, + Asn: 4200003073, + }, + { + Network: "partition-storage", + NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, + Project: new("project-b"), + Prefixes: []string{"10.0.18.0/22"}, + Ips: []string{"10.0.18.2"}, + Vrf: 3982, + Asn: 4200003073, + }, + }, + }, + want: "vrf3981", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := network.New(tt.allocation) + got, err := n.GetTenantNetworkVrfName() + if diff := cmp.Diff(tt.wantErr, err, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + require.Equal(t, tt.want, got) + }) + } +} From 0f62218ce3b9d9698da0a254ee4fdf0e3bdb1340 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 23 Mar 2026 11:24:44 +0100 Subject: [PATCH 097/102] Remove duplicate chrony template rendering --- pkg/installer/os/common/write_ntp_conf.go | 26 ----------------------- 1 file changed, 26 deletions(-) diff --git a/pkg/installer/os/common/write_ntp_conf.go b/pkg/installer/os/common/write_ntp_conf.go index 3d14355..4c41c86 100644 --- a/pkg/installer/os/common/write_ntp_conf.go +++ b/pkg/installer/os/common/write_ntp_conf.go @@ -4,9 +4,6 @@ import ( "context" "fmt" "strings" - - apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/os-installer/pkg/services/chrony" ) const ( @@ -25,29 +22,6 @@ func (d *CommonTasks) WriteNTPConf(ctx context.Context) error { ntpServers = append(ntpServers, ntp.Address) } - if d.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { - defaultVRF, err := d.network.GetTenantNetworkVrfName() - if err != nil { - return err - } - - // TODO: check if this is really required as chrony also gets set up in systemd service task? - - _, err = chrony.WriteSystemdUnit(ctx, &chrony.Config{ - Log: d.log, - Reload: false, - Enable: true, - }, &chrony.TemplateData{ - NTPServers: ntpServers, - }, defaultVRF) - - if err != nil { - return err - } - - return d.WriteNtpConfToPath(ChronyConfigPath, ntpServers) - } - return d.WriteNtpConfToPath(TimesyncdConfigPath, ntpServers) } From ca10d048aafe47e7034a549840efa8cc95e9d358 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Mon, 23 Mar 2026 14:59:55 +0100 Subject: [PATCH 098/102] Provide tests for ntp conf. --- .../os/almalinux/tests/write_ntp_conf_test.go | 134 +++++++++++++++ pkg/installer/os/almalinux/write_ntp_conf.go | 23 ++- pkg/installer/os/common/write_ntp_conf.go | 8 +- .../os/ubuntu/tests/write_ntp_conf_test.go | 153 ++---------------- pkg/services/chrony/chrony.go | 4 +- 5 files changed, 177 insertions(+), 145 deletions(-) create mode 100644 pkg/installer/os/almalinux/tests/write_ntp_conf_test.go diff --git a/pkg/installer/os/almalinux/tests/write_ntp_conf_test.go b/pkg/installer/os/almalinux/tests/write_ntp_conf_test.go new file mode 100644 index 0000000..96b58f6 --- /dev/null +++ b/pkg/installer/os/almalinux/tests/write_ntp_conf_test.go @@ -0,0 +1,134 @@ +package almalinux_test + +import ( + "fmt" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/exec" + "github.com/metal-stack/os-installer/pkg/installer/os/almalinux" + oscommon "github.com/metal-stack/os-installer/pkg/installer/os/common" + "github.com/metal-stack/os-installer/pkg/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_os_WriteNTPConf(t *testing.T) { + tests := []struct { + name string + allocation *apiv2.MachineAllocation + fsMocks func(fs *afero.Afero) + want string + wantErr error + }{ + { + name: "configure custom ntp", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(almalinux.ChronyConfigPath, []byte(""), 0644)) + }, + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + NtpServers: []*apiv2.NTPServer{ + {Address: "custom.1.ntp.org"}, + {Address: "custom.2.ntp.org"}, + }, + }, + want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more +# information about usable directives. + +# In case no custom NTP server is provided +# Cloudflare offers a free public time service that allows us to use their +# anycast network of 180+ locations to synchronize time from their closest server. +# See https://blog.cloudflare.com/secure-time/ +pool custom.1.ntp.org iburst +pool custom.2.ntp.org iburst + +# This directive specify the location of the file containing ID/key pairs for +# NTP authentication. +keyfile /etc/chrony/chrony.keys + +# This directive specify the file into which chronyd will store the rate +# information. +driftfile /var/lib/chrony/chrony.drift + +# Uncomment the following line to turn logging on. +#log tracking measurements statistics + +# Log files location. +logdir /var/log/chrony + +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 + +# This directive enables kernel synchronisation (every 11 minutes) of the +# real-time clock. Note that it can’t be used along with the 'rtcfile' directive. +rtcsync + +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`, + wantErr: nil, + }, + { + name: "use default ntp", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(almalinux.ChronyConfigPath, []byte(""), 0644)) + }, + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + }, + want: "", + wantErr: nil, + }, + { + name: "firewalls are not possible", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(almalinux.ChronyConfigPath, []byte(""), 0644)) + }, + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + }, + want: "", + wantErr: fmt.Errorf("almalinux as firewall is currently not supported"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + log = slog.Default() + fs = &afero.Afero{ + Fs: afero.NewMemMapFs(), + } + ) + + if tt.fsMocks != nil { + tt.fsMocks(fs) + } + + d := almalinux.New(&oscommon.Config{ + Log: log, + Fs: fs, + Allocation: tt.allocation, + Exec: exec.New(log).WithCommandFn(test.FakeCmd(t)), + }) + + gotErr := d.WriteNTPConf(t.Context()) + if diff := cmp.Diff(tt.wantErr, gotErr, test.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n%s", diff) + } + + if tt.wantErr != nil { + return + } + + content, err := fs.ReadFile(almalinux.ChronyConfigPath) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(content)) + }) + } +} diff --git a/pkg/installer/os/almalinux/write_ntp_conf.go b/pkg/installer/os/almalinux/write_ntp_conf.go index e0bf1e9..5168c76 100644 --- a/pkg/installer/os/almalinux/write_ntp_conf.go +++ b/pkg/installer/os/almalinux/write_ntp_conf.go @@ -5,13 +5,19 @@ import ( "fmt" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/os-installer/pkg/services/chrony" + renderer "github.com/metal-stack/os-installer/pkg/template-renderer" ) const ( - chronyConfigPath = "/etc/chrony.conf" + ChronyConfigPath = "/etc/chrony.conf" ) func (o *Os) WriteNTPConf(ctx context.Context) error { + if o.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + return fmt.Errorf("almalinux as firewall is currently not supported") + } + if len(o.allocation.NtpServers) == 0 { return nil } @@ -22,9 +28,18 @@ func (o *Os) WriteNTPConf(ctx context.Context) error { ntpServers = append(ntpServers, ntp.Address) } - if o.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { - return fmt.Errorf("almalinux as firewall is currently not supported") + r, err := renderer.New(&renderer.Config{ + Log: o.log, + TemplateString: chrony.ChronyConfigTemplateString, + Data: chrony.TemplateData{ + NTPServers: ntpServers, + }, + Fs: o.fs, + }) + if err != nil { + return err } - return o.WriteNtpConfToPath(chronyConfigPath, ntpServers) + _, err = r.Render(ctx, ChronyConfigPath) + return err } diff --git a/pkg/installer/os/common/write_ntp_conf.go b/pkg/installer/os/common/write_ntp_conf.go index 4c41c86..a08898a 100644 --- a/pkg/installer/os/common/write_ntp_conf.go +++ b/pkg/installer/os/common/write_ntp_conf.go @@ -4,14 +4,20 @@ import ( "context" "fmt" "strings" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" ) const ( TimesyncdConfigPath = "/etc/systemd/timesyncd.conf" - ChronyConfigPath = "/etc/chrony/chrony.conf" ) func (d *CommonTasks) WriteNTPConf(ctx context.Context) error { + if d.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + d.log.Info("skipping timesyncd config for firewalls as chrony will be configured later on through systemd service renderer") + return nil + } + if len(d.allocation.NtpServers) == 0 { return nil } diff --git a/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go b/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go index 645bbd5..36ccba5 100644 --- a/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go +++ b/pkg/installer/os/ubuntu/tests/write_ntp_conf_test.go @@ -51,135 +51,21 @@ NTP=custom.1.ntp.org custom.2.ntp.org want: "", wantErr: nil, }, - // FIXME! - // { - // name: "configure custom ntp for firewall", - // fsMocks: func(fs *afero.Afero) { - // require.NoError(t, fs.WriteFile(oscommon.ChronyConfigPath, []byte(""), 0644)) - // }, - // allocation: &apiv2.MachineAllocation{ - // AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, - // NtpServer: []*apiv2.NTPServer{ - // {Address: "custom.1.ntp.org"}, - // {Address: "custom.2.ntp.org"}, - // }, - // Project: "project-a", - // Networks: []*apiv2.MachineNetwork{ - // { - // Network: "379d294d-22e8-4aed-82e1-62c6c2f08d6a", - // Project: new("project-a"), - // NetworkType: apiv2.NetworkType_NETWORK_TYPE_CHILD, - // Prefixes: []string{"10.0.16.0/22"}, - // Ips: []string{"10.0.16.2"}, - // Vrf: 3981, - // Asn: 4200003073, - // }, - // }, - // }, - // want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more - // # information about usable directives. - - // # In case no custom NTP server is provided - // # Cloudflare offers a free public time service that allows us to use their - // # anycast network of 180+ locations to synchronize time from their closest server. - // # See https://blog.cloudflare.com/secure-time/ - // pool custom.1.ntp.org iburst - // pool custom.2.ntp.org iburst - - // # This directive specify the location of the file containing ID/key pairs for - // # NTP authentication. - // keyfile /etc/chrony/chrony.keys - - // # This directive specify the file into which chronyd will store the rate - // # information. - // driftfile /var/lib/chrony/chrony.drift - - // # Uncomment the following line to turn logging on. - // #log tracking measurements statistics - - // # Log files location. - // logdir /var/log/chrony - - // # Stop bad estimates upsetting machine clock. - // maxupdateskew 100.0 - - // # This directive enables kernel synchronisation (every 11 minutes) of the - // # real-time clock. Note that it can’t be used along with the 'rtcfile' directive. - // rtcsync - - // # Step the system clock instead of slewing it if the adjustment is larger than - // # one second, but only in the first three clock updates. - // makestep 1 3`, - // wantErr: nil, - // }, - // { - // name: "use default ntp for firewall", - // fsMocks: func(fs *afero.Afero) { - // require.NoError(t, fs.WriteFile(oscommon.ChronyConfigPath, []byte(""), 0644)) - // }, - // allocation: &apiv2.MachineAllocation{ - // AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, - // }, - // want: "", - // wantErr: nil, - // }, - - // { - // name: "configure ntp for almalinux machine", - // fsMocks: func(fs afero.Fs) { - // require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644)) - // }, - // oss: osAlmalinux, - // ntpPath: "/etc/chrony.conf", - // role: "machine", - // ntpServers: []*models.V1NTPServer{{Address: new("custom.1.ntp.org")}, {Address: new("custom.2.ntp.org")}}, - // want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more - // # information about usable directives. - - // # In case no custom NTP server is provided - // # Cloudflare offers a free public time service that allows us to use their - // # anycast network of 180+ locations to synchronize time from their closest server. - // # See https://blog.cloudflare.com/secure-time/ - // pool custom.1.ntp.org iburst - // pool custom.2.ntp.org iburst - - // # This directive specify the location of the file containing ID/key pairs for - // # NTP authentication. - // keyfile /etc/chrony/chrony.keys - - // # This directive specify the file into which chronyd will store the rate - // # information. - // driftfile /var/lib/chrony/chrony.drift - - // # Uncomment the following line to turn logging on. - // #log tracking measurements statistics - - // # Log files location. - // logdir /var/log/chrony - - // # Stop bad estimates upsetting machine clock. - // maxupdateskew 100.0 - - // # This directive enables kernel synchronisation (every 11 minutes) of the - // # real-time clock. Note that it can’t be used along with the 'rtcfile' directive. - // rtcsync - - // # Step the system clock instead of slewing it if the adjustment is larger than - // # one second, but only in the first three clock updates. - // makestep 1 3`, - // wantErr: nil, - // }, - // { - // name: "use default ntp for almalinux machine", - // fsMocks: func(fs afero.Fs) { - // require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644)) - // }, - // oss: osAlmalinux, - // ntpPath: "/etc/chrony.conf", - // role: "machine", - // want: "", - // wantErr: nil, - // }, + { + name: "skip firewalls", + fsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(oscommon.TimesyncdConfigPath, []byte(""), 0644)) + }, + allocation: &apiv2.MachineAllocation{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL, + NtpServers: []*apiv2.NTPServer{ + {Address: "custom.1.ntp.org"}, + {Address: "custom.2.ntp.org"}, + }, + }, + want: "", + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -210,15 +96,6 @@ NTP=custom.1.ntp.org custom.2.ntp.org return } - if tt.allocation.AllocationType == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { - content, err := fs.ReadFile(oscommon.ChronyConfigPath) - require.NoError(t, err) - - assert.Equal(t, tt.want, string(content)) - - return - } - content, err := fs.ReadFile(oscommon.TimesyncdConfigPath) require.NoError(t, err) diff --git a/pkg/services/chrony/chrony.go b/pkg/services/chrony/chrony.go index 704fedd..3cb3801 100644 --- a/pkg/services/chrony/chrony.go +++ b/pkg/services/chrony/chrony.go @@ -18,7 +18,7 @@ const ( var ( //go:embed chrony.conf.tpl - chronyConfigTemplateString string + ChronyConfigTemplateString string ) type Config struct { @@ -39,7 +39,7 @@ func WriteSystemdUnit(ctx context.Context, cfg *Config, c *TemplateData, vrfName r, err := renderer.New(&renderer.Config{ Log: cfg.Log, - TemplateString: chronyConfigTemplateString, + TemplateString: ChronyConfigTemplateString, Data: c, Fs: cfg.fs, }) From 59876ffa65ce3c1c374f7032d2bcbaf75024233d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 24 Mar 2026 11:29:01 +0100 Subject: [PATCH 099/102] Slightly better go --- pkg/frr/routemap.go | 29 +++++++++++------------------ pkg/frr/routemap_test.go | 21 +++++++++++---------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 7151f63..4bcc3d3 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -3,6 +3,7 @@ package frr import ( "fmt" "net/netip" + "slices" "sort" "strings" @@ -106,7 +107,7 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR case apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED: // reach out from private shared networks into private primary network i.ImportVRFs = []string{vrfNameOf(privatePrimaryNet)} - i.ImportPrefixes = concatPfxSlices(prefixesOfNetwork(privatePrimaryNet, vrfNameOf(privatePrimaryNet)), prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet))) + i.ImportPrefixes = slices.Concat(prefixesOfNetwork(privatePrimaryNet, vrfNameOf(privatePrimaryNet)), prefixesOfNetwork(network, vrfNameOf(privatePrimaryNet))) // import destination prefixes of dmz networks from external networks if len(network.DestinationPrefixes) > 0 { @@ -214,18 +215,10 @@ func prefixLists( return result } -func concatPfxSlices(pfxSlices ...[]importPrefix) []importPrefix { - res := []importPrefix{} - for _, pfxSlice := range pfxSlices { - res = append(res, pfxSlice...) - } - return res -} - -func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { +func convertToImportPrefixes(prefixes []string, sourceVrf string) []importPrefix { var result []importPrefix - for _, e := range s { - ipp, err := netip.ParsePrefix(e) + for _, prefix := range prefixes { + ipp, err := netip.ParsePrefix(prefix) if err != nil { continue } @@ -241,7 +234,7 @@ func stringSliceToIPPrefix(s []string, sourceVrf string) []importPrefix { func getDestinationPrefixes(networks []*apiv2.MachineNetwork) []importPrefix { var result []importPrefix for _, network := range networks { - result = append(result, stringSliceToIPPrefix(network.DestinationPrefixes, vrfNameOf(network))...) + result = append(result, convertToImportPrefixes(network.DestinationPrefixes, vrfNameOf(network))...) } return result } @@ -255,7 +248,7 @@ func prefixesOfNetworks(networks []*apiv2.MachineNetwork) []importPrefix { } func prefixesOfNetwork(network *apiv2.MachineNetwork, sourceVrf string) []importPrefix { - return stringSliceToIPPrefix(network.Prefixes, sourceVrf) + return convertToImportPrefixes(network.Prefixes, sourceVrf) } func vrfNameOf(n *apiv2.MachineNetwork) string { @@ -333,18 +326,18 @@ func routeMapName(vrfName string) string { } func (i *importPrefix) buildSpecs(seq int) []string { - var result []string - var spec string + var ( + result []string + spec string + ) if i.Prefix.Bits() == 0 { spec = fmt.Sprintf("%s %s", i.Policy, i.Prefix) - } else { spec = fmt.Sprintf("seq %d %s %s le %d", seq, i.Policy, i.Prefix, i.Prefix.Addr().BitLen()) } result = append(result, spec) - return result } diff --git a/pkg/frr/routemap_test.go b/pkg/frr/routemap_test.go index 6210424..9a2e5fa 100644 --- a/pkg/frr/routemap_test.go +++ b/pkg/frr/routemap_test.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "net/netip" + "slices" "testing" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" @@ -95,10 +96,10 @@ func Test_importRulesForNetwork(t *testing.T) { private.vrf: { // Imported VRFs with their restrictions inet.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + ImportPrefixes: slices.Concat(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), }, external.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + ImportPrefixes: slices.Concat(external.destinations, external.prefixes), }, shared.vrf: importSettings{ ImportPrefixes: shared.prefixes, @@ -106,7 +107,7 @@ func Test_importRulesForNetwork(t *testing.T) { }, shared.vrf: { private.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(private.prefixes, leakFrom(shared.prefixes, private.vrf)), + ImportPrefixes: slices.Concat(private.prefixes, leakFrom(shared.prefixes, private.vrf)), }, }, inet.vrf: { @@ -129,7 +130,7 @@ func Test_importRulesForNetwork(t *testing.T) { want: map[string]map[string]importSettings{ shared.vrf: { inet.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), + ImportPrefixes: slices.Concat(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), }, }, inet.vrf: { @@ -146,10 +147,10 @@ func Test_importRulesForNetwork(t *testing.T) { want: map[string]map[string]importSettings{ private6.vrf: { inet6.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), + ImportPrefixes: slices.Concat(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), }, external.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + ImportPrefixes: slices.Concat(external.destinations, external.prefixes), }, shared.vrf: importSettings{ ImportPrefixes: shared.prefixes, @@ -157,7 +158,7 @@ func Test_importRulesForNetwork(t *testing.T) { }, shared.vrf: { private6.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), + ImportPrefixes: slices.Concat(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), }, }, inet6.vrf: { @@ -180,10 +181,10 @@ func Test_importRulesForNetwork(t *testing.T) { want: map[string]map[string]importSettings{ private6.vrf: { inet6.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), + ImportPrefixes: slices.Concat(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), }, external.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(external.destinations, external.prefixes), + ImportPrefixes: slices.Concat(external.destinations, external.prefixes), }, shared.vrf: importSettings{ ImportPrefixes: shared.prefixes, @@ -191,7 +192,7 @@ func Test_importRulesForNetwork(t *testing.T) { }, shared.vrf: { private6.vrf: importSettings{ - ImportPrefixes: concatPfxSlices(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), + ImportPrefixes: slices.Concat(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), }, }, inet6.vrf: { From 9a05368481e86aaa54d01c61b7477e0c108fd9c3 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 25 Mar 2026 08:51:58 +0100 Subject: [PATCH 100/102] add first passing testcase for importRulesForNetwork --- pkg/frr/frr.go | 3 +- pkg/frr/routemap.go | 12 +- pkg/frr/routemap_test.go | 269 +++++---------------------------------- 3 files changed, 40 insertions(+), 244 deletions(-) diff --git a/pkg/frr/frr.go b/pkg/frr/frr.go index 613ff32..c013a09 100644 --- a/pkg/frr/frr.go +++ b/pkg/frr/frr.go @@ -84,11 +84,12 @@ type ( // SourceVRF specifies from which VRF the given prefix list should be imported SourceVRF string } + // routeMap represents a route-map to permit or deny routes. routeMap struct { Name string Entries []string - Policy string + Policy accessPolicy Order int } diff --git a/pkg/frr/routemap.go b/pkg/frr/routemap.go index 4bcc3d3..ef2c464 100644 --- a/pkg/frr/routemap.go +++ b/pkg/frr/routemap.go @@ -80,17 +80,17 @@ func importRulesForNetwork(cfg *Config, network *apiv2.MachineNetwork) (*importR i.ImportVRFs = append(i.ImportVRFs, vrfNamesOf(privateSecondarySharedNets)...) i.ImportPrefixes = append(i.ImportPrefixes, prefixesOfNetworks(privateSecondarySharedNets)...) - // reach out from private network to destination prefixes of private secondays shared networks + // reach out from private network to destination prefixes of private secondary shared networks for _, n := range privateSecondarySharedNets { for _, pfx := range n.DestinationPrefixes { ppfx := netip.MustParsePrefix(pfx) - isThere := false + var exists bool for _, i := range i.ImportPrefixes { if i.Prefix == ppfx { - isThere = true + exists = true } } - if !isThere { + if !exists { i.ImportPrefixes = append(i.ImportPrefixes, importPrefix{ Prefix: ppfx, Policy: permit, @@ -301,7 +301,7 @@ func (i *importRule) routeMaps() []routeMap { routeMap := routeMap{ Name: routeMapName(i.TargetVRF), - Policy: string(permit), + Policy: permit, Order: order, Entries: entries, } @@ -312,7 +312,7 @@ func (i *importRule) routeMaps() []routeMap { routeMap := routeMap{ Name: routeMapName(i.TargetVRF), - Policy: string(deny), + Policy: deny, Order: order, } diff --git a/pkg/frr/routemap_test.go b/pkg/frr/routemap_test.go index 9a2e5fa..9e03b5d 100644 --- a/pkg/frr/routemap_test.go +++ b/pkg/frr/routemap_test.go @@ -1,266 +1,61 @@ package frr import ( - "fmt" "log/slog" "net/netip" - "slices" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/os-installer/pkg/network" "github.com/stretchr/testify/require" ) -type ( - testnetwork struct { - vrf string - prefixes []importPrefix - destinations []importPrefix - } - importSettings struct { - ImportPrefixes []importPrefix - ImportPrefixesNoExport []importPrefix - } -) - -var ( - defaultRoute = importPrefix{Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: inetVrf} - defaultRoute6 = importPrefix{Prefix: netip.MustParsePrefix("::/0"), Policy: permit, SourceVRF: inetVrf} - externalVrf = "vrf104010" - externalNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: externalVrf} - externalDestinationNet = importPrefix{Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: externalVrf} - privateVrf = "vrf3981" - privateNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.16.0/22"), Policy: permit, SourceVRF: privateVrf} - privateNet6 = importPrefix{Prefix: netip.MustParsePrefix("2002::/64"), Policy: permit, SourceVRF: privateVrf} - sharedVrf = "vrf3982" - sharedNet = importPrefix{Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: sharedVrf} - inetVrf = "vrf104009" - inetNet1 = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: inetVrf} - inetNet2 = importPrefix{Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: inetVrf} - inetNet6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::/45"), Policy: permit, SourceVRF: inetVrf} - publicDefaultNet = importPrefix{Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: inetVrf} - publicDefaultNetIPv6 = importPrefix{Prefix: netip.MustParsePrefix("2a02:c00:20::1/128"), Policy: deny, SourceVRF: inetVrf} - - private = testnetwork{ - vrf: privateVrf, - prefixes: []importPrefix{privateNet}, - } - - private6 = testnetwork{ - vrf: privateVrf, - prefixes: []importPrefix{privateNet6}, - } - - inet = testnetwork{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet1, inetNet2}, - destinations: []importPrefix{defaultRoute}, - } - - inet6 = testnetwork{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet6}, - destinations: []importPrefix{defaultRoute6}, - } - dualstack = testnetwork{ - vrf: inetVrf, - prefixes: []importPrefix{inetNet1, inetNet6}, - destinations: []importPrefix{defaultRoute6}, - } - external = testnetwork{ - vrf: externalVrf, - destinations: []importPrefix{externalDestinationNet}, - prefixes: []importPrefix{externalNet}, - } - - shared = testnetwork{ - vrf: sharedVrf, - prefixes: []importPrefix{sharedNet}, - } -) - func Test_importRulesForNetwork(t *testing.T) { - // FIXME enable once we understand whats actually tested - t.Skip() + log := slog.Default() + tests := []struct { - name string - input *apiv2.MachineAllocation - want map[string]map[string]importSettings + name string + cfg *Config + network *apiv2.MachineNetwork + want *importRule }{ { - name: "standard firewall with private primary unshared network, private secondary shared network, internet and mpls", - input: firewallAllocation, - want: map[string]map[string]importSettings{ - // The target VRF - private.vrf: { - // Imported VRFs with their restrictions - inet.vrf: importSettings{ - ImportPrefixes: slices.Concat(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - external.vrf: importSettings{ - ImportPrefixes: slices.Concat(external.destinations, external.prefixes), - }, - shared.vrf: importSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private.vrf: importSettings{ - ImportPrefixes: slices.Concat(private.prefixes, leakFrom(shared.prefixes, private.vrf)), - }, - }, - inet.vrf: { - private.vrf: importSettings{ - ImportPrefixes: leakFrom(inet.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - }, - external.vrf: { - private.vrf: importSettings{ - ImportPrefixes: leakFrom(external.prefixes, private.vrf), - ImportPrefixesNoExport: private.prefixes, - }, - }, - }, - }, - { - name: "firewall of a shared private network (shared/storage firewall)", - input: firewallSharedAllocation, - want: map[string]map[string]importSettings{ - shared.vrf: { - inet.vrf: importSettings{ - ImportPrefixes: slices.Concat(inet.destinations, []importPrefix{publicDefaultNet}, inet.prefixes), - }, - }, - inet.vrf: { - shared.vrf: importSettings{ - ImportPrefixes: leakFrom(inet.prefixes, shared.vrf), - ImportPrefixesNoExport: shared.prefixes, - }, - }, - }, - }, - { - name: "firewall with ipv6 private network and ipv6 internet network", - input: firewallIPv6Allocation, - want: map[string]map[string]importSettings{ - private6.vrf: { - inet6.vrf: importSettings{ - ImportPrefixes: slices.Concat(inet6.destinations, []importPrefix{publicDefaultNetIPv6}, inet6.prefixes), - }, - external.vrf: importSettings{ - ImportPrefixes: slices.Concat(external.destinations, external.prefixes), - }, - shared.vrf: importSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private6.vrf: importSettings{ - ImportPrefixes: slices.Concat(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), - }, - }, - inet6.vrf: { - private6.vrf: importSettings{ - ImportPrefixes: leakFrom(inet6.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - external.vrf: { - private6.vrf: importSettings{ - ImportPrefixes: leakFrom(external.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, + name: "primary private network of a firewall", + cfg: &Config{ + Log: log, + Network: network.New(firewallAllocation), }, - }, - { - name: "firewall with ipv6 private network and dualstack internet network", - input: firewallAllocationDualStack, - want: map[string]map[string]importSettings{ - private6.vrf: { - inet6.vrf: importSettings{ - ImportPrefixes: slices.Concat(inet6.destinations, []importPrefix{publicDefaultNetIPv6, publicDefaultNet}, dualstack.prefixes), - }, - external.vrf: importSettings{ - ImportPrefixes: slices.Concat(external.destinations, external.prefixes), - }, - shared.vrf: importSettings{ - ImportPrefixes: shared.prefixes, - }, - }, - shared.vrf: { - private6.vrf: importSettings{ - ImportPrefixes: slices.Concat(private6.prefixes, leakFrom(shared.prefixes, private6.vrf)), - }, - }, - inet6.vrf: { - private6.vrf: importSettings{ - ImportPrefixes: leakFrom(dualstack.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, - }, - external.vrf: { - private6.vrf: importSettings{ - ImportPrefixes: leakFrom(external.prefixes, private6.vrf), - ImportPrefixesNoExport: private6.prefixes, - }, + network: firewallAllocation.Networks[0], + want: &importRule{ + TargetVRF: vrfNameOf(firewallAllocation.Networks[0]), + ImportVRFs: []string{ + vrfNameOf(firewallAllocation.Networks[2]), + vrfNameOf(firewallAllocation.Networks[4]), + vrfNameOf(firewallAllocation.Networks[1]), + }, + ImportPrefixes: []importPrefix{ + {Prefix: netip.MustParsePrefix("0.0.0.0/0"), Policy: permit, SourceVRF: vrfNameOf(firewallAllocation.Networks[2])}, + {Prefix: netip.MustParsePrefix("100.127.1.0/24"), Policy: permit, SourceVRF: vrfNameOf(firewallAllocation.Networks[4])}, + {Prefix: netip.MustParsePrefix("185.1.2.3/32"), Policy: deny, SourceVRF: vrfNameOf(firewallAllocation.Networks[2])}, + {Prefix: netip.MustParsePrefix("185.1.2.0/24"), Policy: permit, SourceVRF: vrfNameOf(firewallAllocation.Networks[2])}, + {Prefix: netip.MustParsePrefix("185.27.0.0/22"), Policy: permit, SourceVRF: vrfNameOf(firewallAllocation.Networks[2])}, + {Prefix: netip.MustParsePrefix("100.127.129.0/24"), Policy: permit, SourceVRF: vrfNameOf(firewallAllocation.Networks[4])}, + {Prefix: netip.MustParsePrefix("10.0.18.0/22"), Policy: permit, SourceVRF: vrfNameOf(firewallAllocation.Networks[1])}, }, }, }, } - log := slog.Default() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cfg := &Config{ - Log: log, - Network: network.New(tt.input), - } - for _, network := range tt.input.Networks { - got, err := importRulesForNetwork(cfg, network) - require.NoError(t, err) - if got == nil { - continue - } - gotBySourceVrf := got.bySourceVrf() - targetVrf := fmt.Sprintf("vrf%d", network.Vrf) - want := tt.want[targetVrf] + got, err := importRulesForNetwork(tt.cfg, tt.network) + require.NoError(t, err) - require.Equal(t, want, gotBySourceVrf) + if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(netip.Prefix{}), cmpopts.IgnoreUnexported(netip.Addr{})); diff != "" { + t.Errorf("importRulesForNetwork() diff = %s", diff) } }) } } - -func (i *importRule) bySourceVrf() map[string]importSettings { - r := map[string]importSettings{} - for _, vrf := range i.ImportVRFs { - r[vrf] = importSettings{} - } - - for _, pfx := range i.ImportPrefixes { - e := r[pfx.SourceVRF] - e.ImportPrefixes = append(e.ImportPrefixes, pfx) - r[pfx.SourceVRF] = e - } - - for _, pfx := range i.ImportPrefixesNoExport { - e := r[pfx.SourceVRF] - e.ImportPrefixesNoExport = append(e.ImportPrefixesNoExport, pfx) - r[pfx.SourceVRF] = e - } - - return r -} - -func leakFrom(pfxs []importPrefix, sourceVrf string) []importPrefix { - r := []importPrefix{} - for _, e := range pfxs { - i := e - i.SourceVRF = sourceVrf - r = append(r, i) - } - return r -} From e7b267c8ed4b89e59587c4264db01fe0731ffe90 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 1 Apr 2026 13:05:49 +0200 Subject: [PATCH 101/102] Add comment. --- pkg/installer/os/common/process_userdata.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/installer/os/common/process_userdata.go b/pkg/installer/os/common/process_userdata.go index 22f5c68..6521021 100644 --- a/pkg/installer/os/common/process_userdata.go +++ b/pkg/installer/os/common/process_userdata.go @@ -69,6 +69,7 @@ func (d *CommonTasks) ProcessUserdata(ctx context.Context) error { Dir: "/", }) if err != nil { + // if the user provides userdata that does not work out we still want the machine to start up d.log.Error("error when running ignition, continuing anyway", "report", report.Entries, "error", err) } From 7f8b4953d65ede52432f7c75da30f31967f7c3e8 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 1 Apr 2026 13:07:21 +0200 Subject: [PATCH 102/102] Remove fixme --- pkg/frr/frr_test.go | 10 ---------- pkg/nftables/nftables_test.go | 2 -- 2 files changed, 12 deletions(-) diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go index 86e8d94..2ebecdf 100644 --- a/pkg/frr/frr_test.go +++ b/pkg/frr/frr_test.go @@ -42,8 +42,6 @@ var ( Ips: []string{"10.0.18.2"}, Vrf: 3982, Asn: 4200003073, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -97,8 +95,6 @@ var ( Ips: []string{"10.0.18.2"}, Vrf: 3982, Asn: 4200003073, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -151,8 +147,6 @@ var ( Ips: []string{"10.0.18.2"}, Vrf: 3982, Asn: 4200003073, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -205,8 +199,6 @@ var ( Ips: []string{"10.0.18.2"}, Vrf: 3982, Asn: 4200003073, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", @@ -293,8 +285,6 @@ var ( Ips: []string{"10.0.18.2"}, Vrf: 3982, Asn: 4200003073, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet", diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go index ad119a5..ef5e7fa 100644 --- a/pkg/nftables/nftables_test.go +++ b/pkg/nftables/nftables_test.go @@ -35,8 +35,6 @@ var ( Prefixes: []string{"10.0.18.0/22"}, Ips: []string{"10.0.18.2"}, Vrf: 3982, - // FIXME clarify if this is required - // NatType: apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE, }, { Network: "internet",