diff --git a/cmd/install.go b/cmd/install.go index ff5179eb..6552abf7 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -12,7 +12,9 @@ import ( "time" config "github.com/flatcar/ignition/config/v2_4" + "github.com/metal-stack/metal-go/api/models" "github.com/metal-stack/metal-hammer/pkg/api" + "github.com/metal-stack/metal-images/cmd/templates" v1 "github.com/metal-stack/metal-images/cmd/v1" "github.com/metal-stack/metal-networker/pkg/netconf" "github.com/metal-stack/v" @@ -69,6 +71,12 @@ func (i *installer) do() error { 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 @@ -155,23 +163,86 @@ func (i *installer) fileExists(filename string) bool { } func (i *installer) writeResolvConf() error { - i.log.Info("write /etc/resolv.conf") + const f = "/etc/resolv.conf" + i.log.Info("write configuration", "file", f) // Must be written here because during docker build this file is synthetic // FIXME enable systemd-resolved based approach again once we figured out why it does not work on the firewall // most probably because the resolved must be running in the internet facing vrf. // ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf // in ignite this file is a symlink to /proc/net/pnp, to pass integration test, remove this first - err := i.fs.Remove("/etc/resolv.conf") + err := i.fs.Remove(f) if err != nil { - i.log.Info("no /etc/resolv.conf present") + i.log.Info("config file not present", "file", f) } - // FIXME migrate to dns0.eu resolvers content := []byte( `nameserver 8.8.8.8 nameserver 8.8.4.4 `) - return afero.WriteFile(i.fs, "/etc/resolv.conf", content, 0644) + + if len(i.config.DNSServers) > 0 { + var s string + for _, dnsServer := range i.config.DNSServers { + s += "nameserver " + *dnsServer.IP + "\n" + } + content = []byte(s) + + } + + 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 + ) + + switch i.config.Role { + case models.V1MachineAllocationRoleFirewall: + ntpConfigPath = "/etc/chrony/chrony.conf" + s, err = templates.RenderChronyTemplate(templates.Chrony{NTPServers: i.config.NTPServers}) + if err != nil { + return fmt.Errorf("error rendering chrony template %w", 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 { + ntpConfigPath = "/etc/chrony.conf" + s, err = templates.RenderChronyTemplate(templates.Chrony{NTPServers: i.config.NTPServers}) + if err != nil { + return fmt.Errorf("error rendering chrony template %w", 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 { @@ -324,9 +395,9 @@ func (i *installer) configureNetwork() error { var kind netconf.BareMetalType switch i.config.Role { - case "firewall": + case models.V1MachineAllocationRoleFirewall: kind = netconf.Firewall - case "machine": + case models.V1MachineAllocationRoleMachine: kind = netconf.Machine default: return fmt.Errorf("unknown role:%s", i.config.Role) diff --git a/cmd/install_test.go b/cmd/install_test.go index 3c066d0f..c84755a8 100644 --- a/cmd/install_test.go +++ b/cmd/install_test.go @@ -7,7 +7,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/metal-stack/metal-go/api/models" "github.com/metal-stack/metal-hammer/pkg/api" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/metal-stack/v" "github.com/spf13/afero" @@ -210,6 +212,7 @@ func Test_installer_writeResolvConf(t *testing.T) { tests := []struct { name string fsMocks func(fs afero.Fs) + config *api.InstallerConfig want string wantErr error }{ @@ -227,6 +230,14 @@ nameserver 8.8.4.4 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: &api.InstallerConfig{DNSServers: []*models.V1DNSServer{{IP: pointer.Pointer("1.2.3.4")}, {IP: pointer.Pointer("5.6.7.8")}}}, + want: `nameserver 1.2.3.4 +nameserver 5.6.7.8 `, wantErr: nil, }, @@ -235,14 +246,19 @@ nameserver 8.8.4.4 tt := tt t.Run(tt.name, func(t *testing.T) { i := &installer{ - log: slog.Default(), - fs: afero.NewMemMapFs(), + log: slog.Default(), + fs: afero.NewMemMapFs(), + config: &api.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, testcommon.ErrorStringComparer()); diff != "" { t.Errorf("error diff (+got -want):\n %s", diff) @@ -258,6 +274,207 @@ nameserver 8.8.4.4 } } +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: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("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: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("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: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("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: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("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 { + tt := tt + t.Run(tt.name, func(t *testing.T) { + i := &installer{ + log: slog.Default(), + fs: afero.NewMemMapFs(), + config: &api.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, testcommon.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 diff --git a/cmd/templates/chrony.conf.tpl b/cmd/templates/chrony.conf.tpl new file mode 100644 index 00000000..fb4523a6 --- /dev/null +++ b/cmd/templates/chrony.conf.tpl @@ -0,0 +1,36 @@ +# 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/ + +{{- range .NTPServers}} +pool {{ .Address }} iburst +{{- end }} + +# 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 \ No newline at end of file diff --git a/cmd/templates/template.go b/cmd/templates/template.go new file mode 100644 index 00000000..f25c9ab2 --- /dev/null +++ b/cmd/templates/template.go @@ -0,0 +1,30 @@ +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/cmd/templates/template_test.go b/cmd/templates/template_test.go new file mode 100644 index 00000000..d84cab3f --- /dev/null +++ b/cmd/templates/template_test.go @@ -0,0 +1,50 @@ +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 +} diff --git a/cmd/templates/test_data/customntp/chrony.conf b/cmd/templates/test_data/customntp/chrony.conf new file mode 100644 index 00000000..f8ad3a44 --- /dev/null +++ b/cmd/templates/test_data/customntp/chrony.conf @@ -0,0 +1,33 @@ +# 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 + +# 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 \ No newline at end of file diff --git a/cmd/templates/test_data/defaultntp/chrony.conf b/cmd/templates/test_data/defaultntp/chrony.conf new file mode 100644 index 00000000..30ce9da1 --- /dev/null +++ b/cmd/templates/test_data/defaultntp/chrony.conf @@ -0,0 +1,33 @@ +# 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 time.cloudflare.com 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 \ No newline at end of file diff --git a/go.mod b/go.mod index b794cd89..06d18fed 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,13 @@ module github.com/metal-stack/metal-images go 1.23.0 +toolchain go1.23.1 + require ( github.com/aws/aws-sdk-go v1.55.5 github.com/flatcar/ignition v0.36.2 github.com/google/go-cmp v0.6.0 + github.com/metal-stack/metal-go v0.39.1 github.com/metal-stack/metal-hammer v0.13.8 github.com/metal-stack/metal-lib v0.19.0 github.com/metal-stack/metal-networker v0.45.2 @@ -39,7 +42,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/metal-stack/metal-go v0.39.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -49,7 +51,7 @@ require ( go.mongodb.org/mongo-driver v1.17.1 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 10ad08dc..bb6f540c 100644 --- a/go.sum +++ b/go.sum @@ -274,8 +274,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/test/inputs/firewall.yaml b/test/inputs/firewall.yaml index 76e3105a..98fcfbab 100644 --- a/test/inputs/firewall.yaml +++ b/test/inputs/firewall.yaml @@ -60,3 +60,5 @@ nics: - mac: "44:38:39:00:00:04" name: null neighbors: [] +ntp_servers: + - address: 1.pool.ntp.org \ No newline at end of file diff --git a/test/inputs/goss.yaml b/test/inputs/goss.yaml index 55e7e75d..726c003c 100644 --- a/test/inputs/goss.yaml +++ b/test/inputs/goss.yaml @@ -54,6 +54,27 @@ file: contents: - "#!/bin/bash" {{ end }} +{{ if eq .Env.MACHINE_TYPE "machine" }} +{{ if eq .Env.OS "almalinux" }} + "/etc/chrony.conf": + exists: true + contents: + - pool 1.pool.ntp.org iburst +{{ else }} + "/etc/systemd/timesyncd.conf": + exists: true + contents: + - "NTP=1.pool.ntp.org" + "/etc/systemd/resolved.conf.d/dns.conf": + exists: true + contents: + - DNS=8.8.8.8 +{{ end }} + "/etc/resolv.conf": + exists: true + contents: + - nameserver 8.8.8.8 +{{ end }} {{ if eq .Env.MACHINE_TYPE "firewall" }} "/etc/hostname": exists: true @@ -68,6 +89,10 @@ file: contents: - "/var/log/suricata/*.log" - "/var/log/suricata/*.json" + "/etc/chrony/chrony.conf": + exists: true + contents: + - pool 1.pool.ntp.org iburst {{ end }} service: {{ if eq .Env.OS "almalinux" }} diff --git a/test/inputs/machine.yaml b/test/inputs/machine.yaml index 4f3a95a7..7ec896ea 100644 --- a/test/inputs/machine.yaml +++ b/test/inputs/machine.yaml @@ -33,3 +33,7 @@ nics: - mac: "44:38:39:00:00:04" name: null neighbors: [] +ntp_servers: + - address: 1.pool.ntp.org +dns_servers: + - ip: 8.8.8.8