Skip to content

Commit 70212ee

Browse files
authored
Machine allocation with dns and ntp (#255)
1 parent 4109de9 commit 70212ee

File tree

12 files changed

+516
-13
lines changed

12 files changed

+516
-13
lines changed

cmd/install.go

+78-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212
"time"
1313

1414
config "github.com/flatcar/ignition/config/v2_4"
15+
"github.com/metal-stack/metal-go/api/models"
1516
"github.com/metal-stack/metal-hammer/pkg/api"
17+
"github.com/metal-stack/metal-images/cmd/templates"
1618
v1 "github.com/metal-stack/metal-images/cmd/v1"
1719
"github.com/metal-stack/metal-networker/pkg/netconf"
1820
"github.com/metal-stack/v"
@@ -69,6 +71,12 @@ func (i *installer) do() error {
6971
return err
7072
}
7173

74+
err = i.writeNTPConf()
75+
if err != nil {
76+
i.log.Warn("writing ntp configuration failed", "err", err)
77+
return err
78+
}
79+
7280
err = i.createMetalUser()
7381
if err != nil {
7482
return err
@@ -155,23 +163,86 @@ func (i *installer) fileExists(filename string) bool {
155163
}
156164

157165
func (i *installer) writeResolvConf() error {
158-
i.log.Info("write /etc/resolv.conf")
166+
const f = "/etc/resolv.conf"
167+
i.log.Info("write configuration", "file", f)
159168
// Must be written here because during docker build this file is synthetic
160169
// FIXME enable systemd-resolved based approach again once we figured out why it does not work on the firewall
161170
// most probably because the resolved must be running in the internet facing vrf.
162171
// ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
163172
// in ignite this file is a symlink to /proc/net/pnp, to pass integration test, remove this first
164-
err := i.fs.Remove("/etc/resolv.conf")
173+
err := i.fs.Remove(f)
165174
if err != nil {
166-
i.log.Info("no /etc/resolv.conf present")
175+
i.log.Info("config file not present", "file", f)
167176
}
168177

169-
// FIXME migrate to dns0.eu resolvers
170178
content := []byte(
171179
`nameserver 8.8.8.8
172180
nameserver 8.8.4.4
173181
`)
174-
return afero.WriteFile(i.fs, "/etc/resolv.conf", content, 0644)
182+
183+
if len(i.config.DNSServers) > 0 {
184+
var s string
185+
for _, dnsServer := range i.config.DNSServers {
186+
s += "nameserver " + *dnsServer.IP + "\n"
187+
}
188+
content = []byte(s)
189+
190+
}
191+
192+
return afero.WriteFile(i.fs, f, content, 0644)
193+
}
194+
195+
func (i *installer) writeNTPConf() error {
196+
if len(i.config.NTPServers) == 0 {
197+
return nil
198+
}
199+
200+
var (
201+
ntpConfigPath string
202+
s string
203+
err error
204+
)
205+
206+
switch i.config.Role {
207+
case models.V1MachineAllocationRoleFirewall:
208+
ntpConfigPath = "/etc/chrony/chrony.conf"
209+
s, err = templates.RenderChronyTemplate(templates.Chrony{NTPServers: i.config.NTPServers})
210+
if err != nil {
211+
return fmt.Errorf("error rendering chrony template %w", err)
212+
}
213+
214+
case models.V1MachineAllocationRoleMachine:
215+
if i.oss == osDebian || i.oss == osUbuntu {
216+
ntpConfigPath = "/etc/systemd/timesyncd.conf"
217+
var addresses []string
218+
for _, ntp := range i.config.NTPServers {
219+
if ntp.Address == nil {
220+
continue
221+
}
222+
addresses = append(addresses, *ntp.Address)
223+
}
224+
s = fmt.Sprintf("[Time]\nNTP=%s\n", strings.Join(addresses, " "))
225+
}
226+
227+
if i.oss == osAlmalinux {
228+
ntpConfigPath = "/etc/chrony.conf"
229+
s, err = templates.RenderChronyTemplate(templates.Chrony{NTPServers: i.config.NTPServers})
230+
if err != nil {
231+
return fmt.Errorf("error rendering chrony template %w", err)
232+
}
233+
}
234+
default:
235+
return fmt.Errorf("unknown role:%s", i.config.Role)
236+
}
237+
238+
content := []byte(s)
239+
i.log.Info("write configuration", "file", ntpConfigPath)
240+
err = i.fs.Remove(ntpConfigPath)
241+
if err != nil {
242+
i.log.Info("config file not present", "file", ntpConfigPath)
243+
}
244+
245+
return afero.WriteFile(i.fs, ntpConfigPath, content, 0644)
175246
}
176247

177248
func (i *installer) buildCMDLine() string {
@@ -324,9 +395,9 @@ func (i *installer) configureNetwork() error {
324395

325396
var kind netconf.BareMetalType
326397
switch i.config.Role {
327-
case "firewall":
398+
case models.V1MachineAllocationRoleFirewall:
328399
kind = netconf.Firewall
329-
case "machine":
400+
case models.V1MachineAllocationRoleMachine:
330401
kind = netconf.Machine
331402
default:
332403
return fmt.Errorf("unknown role:%s", i.config.Role)

cmd/install_test.go

+219-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"testing"
88

99
"github.com/google/go-cmp/cmp"
10+
"github.com/metal-stack/metal-go/api/models"
1011
"github.com/metal-stack/metal-hammer/pkg/api"
12+
"github.com/metal-stack/metal-lib/pkg/pointer"
1113
"github.com/metal-stack/metal-lib/pkg/testcommon"
1214
"github.com/metal-stack/v"
1315
"github.com/spf13/afero"
@@ -210,6 +212,7 @@ func Test_installer_writeResolvConf(t *testing.T) {
210212
tests := []struct {
211213
name string
212214
fsMocks func(fs afero.Fs)
215+
config *api.InstallerConfig
213216
want string
214217
wantErr error
215218
}{
@@ -227,6 +230,14 @@ nameserver 8.8.4.4
227230
name: "resolv.conf gets written, file is not present",
228231
want: `nameserver 8.8.8.8
229232
nameserver 8.8.4.4
233+
`,
234+
wantErr: nil,
235+
},
236+
{
237+
name: "overwrite resolv.conf with custom DNS",
238+
config: &api.InstallerConfig{DNSServers: []*models.V1DNSServer{{IP: pointer.Pointer("1.2.3.4")}, {IP: pointer.Pointer("5.6.7.8")}}},
239+
want: `nameserver 1.2.3.4
240+
nameserver 5.6.7.8
230241
`,
231242
wantErr: nil,
232243
},
@@ -235,14 +246,19 @@ nameserver 8.8.4.4
235246
tt := tt
236247
t.Run(tt.name, func(t *testing.T) {
237248
i := &installer{
238-
log: slog.Default(),
239-
fs: afero.NewMemMapFs(),
249+
log: slog.Default(),
250+
fs: afero.NewMemMapFs(),
251+
config: &api.InstallerConfig{},
240252
}
241253

242254
if tt.fsMocks != nil {
243255
tt.fsMocks(i.fs)
244256
}
245257

258+
if tt.config != nil {
259+
i.config = tt.config
260+
}
261+
246262
err := i.writeResolvConf()
247263
if diff := cmp.Diff(tt.wantErr, err, testcommon.ErrorStringComparer()); diff != "" {
248264
t.Errorf("error diff (+got -want):\n %s", diff)
@@ -258,6 +274,207 @@ nameserver 8.8.4.4
258274
}
259275
}
260276

277+
func Test_installer_writeNTPConf(t *testing.T) {
278+
tests := []struct {
279+
name string
280+
fsMocks func(fs afero.Fs)
281+
oss operatingsystem
282+
role string
283+
ntpServers []*models.V1NTPServer
284+
ntpPath string
285+
want string
286+
wantErr error
287+
}{
288+
{
289+
name: "configure custom ntp for ubuntu machine",
290+
fsMocks: func(fs afero.Fs) {
291+
require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644))
292+
},
293+
ntpPath: "/etc/systemd/timesyncd.conf",
294+
oss: osUbuntu,
295+
role: "machine",
296+
ntpServers: []*models.V1NTPServer{{Address: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("custom.2.ntp.org")}},
297+
want: `[Time]
298+
NTP=custom.1.ntp.org custom.2.ntp.org
299+
`,
300+
wantErr: nil,
301+
},
302+
{
303+
name: "use default ntp for ubuntu machine",
304+
fsMocks: func(fs afero.Fs) {
305+
require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644))
306+
},
307+
ntpPath: "/etc/systemd/timesyncd.conf",
308+
oss: osUbuntu,
309+
role: "machine",
310+
want: "",
311+
wantErr: nil,
312+
},
313+
{
314+
name: "configure custom ntp for debian machine",
315+
fsMocks: func(fs afero.Fs) {
316+
require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644))
317+
},
318+
ntpPath: "/etc/systemd/timesyncd.conf",
319+
oss: osDebian,
320+
role: "machine",
321+
ntpServers: []*models.V1NTPServer{{Address: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("custom.2.ntp.org")}},
322+
want: `[Time]
323+
NTP=custom.1.ntp.org custom.2.ntp.org
324+
`,
325+
wantErr: nil,
326+
},
327+
{
328+
name: "use default ntp for debian machine",
329+
fsMocks: func(fs afero.Fs) {
330+
require.NoError(t, afero.WriteFile(fs, "/etc/systemd/timesyncd.conf", []byte(""), 0644))
331+
},
332+
ntpPath: "/etc/systemd/timesyncd.conf",
333+
oss: osDebian,
334+
role: "machine",
335+
want: "",
336+
wantErr: nil,
337+
},
338+
{
339+
name: "configure ntp for almalinux machine",
340+
fsMocks: func(fs afero.Fs) {
341+
require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644))
342+
},
343+
oss: osAlmalinux,
344+
ntpPath: "/etc/chrony.conf",
345+
role: "machine",
346+
ntpServers: []*models.V1NTPServer{{Address: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("custom.2.ntp.org")}},
347+
want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more
348+
# information about usable directives.
349+
350+
# In case no custom NTP server is provided
351+
# Cloudflare offers a free public time service that allows us to use their
352+
# anycast network of 180+ locations to synchronize time from their closest server.
353+
# See https://blog.cloudflare.com/secure-time/
354+
pool custom.1.ntp.org iburst
355+
pool custom.2.ntp.org iburst
356+
357+
# This directive specify the location of the file containing ID/key pairs for
358+
# NTP authentication.
359+
keyfile /etc/chrony/chrony.keys
360+
361+
# This directive specify the file into which chronyd will store the rate
362+
# information.
363+
driftfile /var/lib/chrony/chrony.drift
364+
365+
# Uncomment the following line to turn logging on.
366+
#log tracking measurements statistics
367+
368+
# Log files location.
369+
logdir /var/log/chrony
370+
371+
# Stop bad estimates upsetting machine clock.
372+
maxupdateskew 100.0
373+
374+
# This directive enables kernel synchronisation (every 11 minutes) of the
375+
# real-time clock. Note that it can’t be used along with the 'rtcfile' directive.
376+
rtcsync
377+
378+
# Step the system clock instead of slewing it if the adjustment is larger than
379+
# one second, but only in the first three clock updates.
380+
makestep 1 3`,
381+
wantErr: nil,
382+
},
383+
{
384+
name: "use default ntp for almalinux machine",
385+
fsMocks: func(fs afero.Fs) {
386+
require.NoError(t, afero.WriteFile(fs, "/etc/chrony.conf", []byte(""), 0644))
387+
},
388+
oss: osAlmalinux,
389+
ntpPath: "/etc/chrony.conf",
390+
role: "machine",
391+
want: "",
392+
wantErr: nil,
393+
},
394+
{
395+
name: "configure custom ntp for firewall",
396+
fsMocks: func(fs afero.Fs) {
397+
require.NoError(t, afero.WriteFile(fs, "/etc/chrony/chrony.conf", []byte(""), 0644))
398+
},
399+
ntpPath: "/etc/chrony/chrony.conf",
400+
role: "firewall",
401+
ntpServers: []*models.V1NTPServer{{Address: pointer.Pointer("custom.1.ntp.org")}, {Address: pointer.Pointer("custom.2.ntp.org")}},
402+
want: `# Welcome to the chrony configuration file. See chrony.conf(5) for more
403+
# information about usable directives.
404+
405+
# In case no custom NTP server is provided
406+
# Cloudflare offers a free public time service that allows us to use their
407+
# anycast network of 180+ locations to synchronize time from their closest server.
408+
# See https://blog.cloudflare.com/secure-time/
409+
pool custom.1.ntp.org iburst
410+
pool custom.2.ntp.org iburst
411+
412+
# This directive specify the location of the file containing ID/key pairs for
413+
# NTP authentication.
414+
keyfile /etc/chrony/chrony.keys
415+
416+
# This directive specify the file into which chronyd will store the rate
417+
# information.
418+
driftfile /var/lib/chrony/chrony.drift
419+
420+
# Uncomment the following line to turn logging on.
421+
#log tracking measurements statistics
422+
423+
# Log files location.
424+
logdir /var/log/chrony
425+
426+
# Stop bad estimates upsetting machine clock.
427+
maxupdateskew 100.0
428+
429+
# This directive enables kernel synchronisation (every 11 minutes) of the
430+
# real-time clock. Note that it can’t be used along with the 'rtcfile' directive.
431+
rtcsync
432+
433+
# Step the system clock instead of slewing it if the adjustment is larger than
434+
# one second, but only in the first three clock updates.
435+
makestep 1 3`,
436+
wantErr: nil,
437+
},
438+
{
439+
name: "use default ntp for firewall",
440+
fsMocks: func(fs afero.Fs) {
441+
require.NoError(t, afero.WriteFile(fs, "/etc/chrony/chrony.conf", []byte(""), 0644))
442+
},
443+
ntpPath: "/etc/chrony/chrony.conf",
444+
role: "firewall",
445+
want: "",
446+
wantErr: nil,
447+
},
448+
}
449+
for _, tt := range tests {
450+
tt := tt
451+
t.Run(tt.name, func(t *testing.T) {
452+
i := &installer{
453+
log: slog.Default(),
454+
fs: afero.NewMemMapFs(),
455+
config: &api.InstallerConfig{Role: tt.role, NTPServers: tt.ntpServers},
456+
oss: tt.oss,
457+
}
458+
459+
if tt.fsMocks != nil {
460+
tt.fsMocks(i.fs)
461+
}
462+
463+
err := i.writeNTPConf()
464+
if diff := cmp.Diff(tt.wantErr, err, testcommon.ErrorStringComparer()); diff != "" {
465+
t.Errorf("error diff (+got -want):\n %s", diff)
466+
}
467+
468+
content, err := afero.ReadFile(i.fs, tt.ntpPath)
469+
require.NoError(t, err)
470+
471+
if diff := cmp.Diff(tt.want, string(content)); diff != "" {
472+
t.Errorf("error diff (+got -want):\n %s", diff)
473+
}
474+
})
475+
}
476+
}
477+
261478
func Test_installer_fixPermissions(t *testing.T) {
262479
tests := []struct {
263480
name string

0 commit comments

Comments
 (0)