Skip to content

Commit

Permalink
Machine allocation with dns and ntp (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
simcod authored Nov 11, 2024
1 parent 4109de9 commit 70212ee
Show file tree
Hide file tree
Showing 12 changed files with 516 additions and 13 deletions.
85 changes: 78 additions & 7 deletions cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
221 changes: 219 additions & 2 deletions cmd/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}{
Expand All @@ -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,
},
Expand All @@ -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)
Expand All @@ -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
Expand Down
Loading

0 comments on commit 70212ee

Please sign in to comment.