// Copyright (C) 2021 Simon Ruderich
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
package main
import (
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"testing"
"ruderich.org/simon/safcm"
"ruderich.org/simon/safcm/testutil"
)
func TestHostSyncReq(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(cwd)
tests := []struct {
name string
project string
host string
detected []string
level safcm.LogLevel
exp safcm.MsgSyncReq
expEvents []string
expErr error
}{
// NOTE: Also update MsgSyncReq in safcm-remote test cases
// when changing anything here!
{
"project: host1",
"project",
"host1.example.org",
nil,
safcm.LogDebug3,
safcm.MsgSyncReq{
Groups: []string{
"all",
"group",
"remove",
"host1.example.org",
},
Files: map[string]*safcm.File{
"/": &safcm.File{Path: "/",
OrigGroup: "group",
Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"touch /.update",
},
},
"/etc": &safcm.File{
OrigGroup: "group",
Path: "/etc",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
},
"/etc/.hidden": &safcm.File{
OrigGroup: "group",
Path: "/etc/.hidden",
Mode: 0100 | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky,
Uid: -1,
Gid: -1,
Data: []byte("..."),
},
"/etc/motd": &safcm.File{
OrigGroup: "group",
Path: "/etc/motd",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("Welcome to Host ONE\n\n\n\n"),
},
"/etc/rc.local": &safcm.File{
OrigGroup: "group",
Path: "/etc/rc.local",
Mode: 0700,
Uid: -1,
Gid: -1,
Data: []byte("#!/bin/sh\n"),
TriggerCommands: []string{
"/etc/rc.local",
},
},
"/etc/resolv.conf": &safcm.File{
OrigGroup: "group",
Path: "/etc/resolv.conf",
Mode: 0641,
User: "user",
Uid: -1,
Group: "group",
Gid: -1,
Data: []byte("nameserver ::1\n"),
TriggerCommands: []string{
"echo resolv.conf updated",
},
},
"/etc/test": &safcm.File{
OrigGroup: "group",
Path: "/etc/test",
Mode: os.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("doesnt-exist"),
},
},
Packages: []string{
"unbound",
"unbound-anchor",
},
Services: []string{
"unbound",
},
Commands: []string{
"echo command one",
"echo -n command two",
},
},
[]string{
"host1.example.org: 3 host groups: all group host1.example.org remove",
"host1.example.org: 3 host group priorities (desc. order): host1.example.org",
},
nil,
},
{
"project: host1 (log level info)",
"project",
"host1.example.org",
nil,
safcm.LogInfo,
safcm.MsgSyncReq{
Groups: []string{
"all",
"group",
"remove",
"host1.example.org",
},
Files: map[string]*safcm.File{
"/": &safcm.File{Path: "/",
OrigGroup: "group",
Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"touch /.update",
},
},
"/etc": &safcm.File{
OrigGroup: "group",
Path: "/etc",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
},
"/etc/.hidden": &safcm.File{
OrigGroup: "group",
Path: "/etc/.hidden",
Mode: 0100 | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky,
Uid: -1,
Gid: -1,
Data: []byte("..."),
},
"/etc/motd": &safcm.File{
OrigGroup: "group",
Path: "/etc/motd",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("Welcome to Host ONE\n\n\n\n"),
},
"/etc/rc.local": &safcm.File{
OrigGroup: "group",
Path: "/etc/rc.local",
Mode: 0700,
Uid: -1,
Gid: -1,
Data: []byte("#!/bin/sh\n"),
TriggerCommands: []string{
"/etc/rc.local",
},
},
"/etc/resolv.conf": &safcm.File{
OrigGroup: "group",
Path: "/etc/resolv.conf",
Mode: 0641,
User: "user",
Uid: -1,
Group: "group",
Gid: -1,
Data: []byte("nameserver ::1\n"),
TriggerCommands: []string{
"echo resolv.conf updated",
},
},
"/etc/test": &safcm.File{
OrigGroup: "group",
Path: "/etc/test",
Mode: os.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("doesnt-exist"),
},
},
Packages: []string{
"unbound",
"unbound-anchor",
},
Services: []string{
"unbound",
},
Commands: []string{
"echo command one",
"echo -n command two",
},
},
nil,
nil,
},
{
"conflict: file",
"project-conflict-file",
"host1.example.org",
nil,
safcm.LogDebug3,
safcm.MsgSyncReq{},
[]string{
"host1.example.org: 3 host groups: all dns host1.example.org",
"host1.example.org: 3 host group priorities (desc. order): host1.example.org",
},
fmt.Errorf("groups dns and all both provide file \"/etc/resolv.conf\"\nUse 'group_order' in config.yaml to declare preference"),
},
{
"conflict: file from detected group",
"project-conflict-file",
"host2.example.org",
[]string{
"detected_other",
},
safcm.LogDebug3,
safcm.MsgSyncReq{},
[]string{
"host2.example.org: 3 host groups: all detected_other host2.example.org other",
"host2.example.org: 3 host group priorities (desc. order): host2.example.org",
},
fmt.Errorf("groups other and all both provide file \"/etc/resolv.conf\"\nUse 'group_order' in config.yaml to declare preference"),
},
{
"conflict: dir",
"project-conflict-dir",
"host1.example.org",
nil,
safcm.LogDebug3,
safcm.MsgSyncReq{},
[]string{
"host1.example.org: 3 host groups: all dns host1.example.org",
"host1.example.org: 3 host group priorities (desc. order): host1.example.org",
},
fmt.Errorf("groups dns and all both provide file \"/etc\"\nUse 'group_order' in config.yaml to declare preference"),
},
{
"conflict: dir from detected group",
"project-conflict-dir",
"host2.example.org",
[]string{
"detected_other",
},
safcm.LogDebug3,
safcm.MsgSyncReq{},
[]string{
"host2.example.org: 3 host groups: all detected_other host2.example.org other",
"host2.example.org: 3 host group priorities (desc. order): host2.example.org",
},
fmt.Errorf("groups other and all both provide file \"/etc\"\nUse 'group_order' in config.yaml to declare preference"),
},
{
"group: cycle",
"project-group-cycle",
"host1.example.org",
nil,
safcm.LogDebug3,
safcm.MsgSyncReq{},
nil,
fmt.Errorf("groups.yaml: cycle while expanding group \"group-b\""),
},
{
"group_order",
"project-group_order",
"host1.example.org",
nil,
safcm.LogDebug3,
safcm.MsgSyncReq{
Groups: []string{"all", "group-b", "group-a", "host1.example.org"},
Files: map[string]*safcm.File{
"/": {
Path: "/",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
OrigGroup: "host1.example.org",
},
"/etc": {
Path: "/etc",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
OrigGroup: "host1.example.org",
},
"/etc/dir-to-file": {
Path: "/etc/dir-to-file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("dir-to-file: from group-a\n"),
OrigGroup: "group-a",
},
"/etc/dir-to-filex": {
OrigGroup: "group-b",
Path: "/etc/dir-to-filex",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("dir-to-filex\n"),
},
"/etc/dir-to-link": {
Path: "/etc/dir-to-link",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("target"),
OrigGroup: "group-a",
},
"/etc/dir-to-linkx": {
OrigGroup: "group-b",
Path: "/etc/dir-to-linkx",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("dir-to-linkx\n"),
},
"/etc/file-to-dir": {
Path: "/etc/file-to-dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
OrigGroup: "group-a",
},
"/etc/file-to-dir/file": {
Path: "/etc/file-to-dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("file: from group-a\n"),
OrigGroup: "group-a",
},
"/etc/file-to-dir/dir": {
Path: "/etc/file-to-dir/dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
OrigGroup: "group-a",
},
"/etc/file-to-dir/dir/file2": {
Path: "/etc/file-to-dir/dir/file2",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("file2: from group-a\n"),
OrigGroup: "group-a",
},
"/etc/motd": {
Path: "/etc/motd",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("motd: from host1\n"),
OrigGroup: "host1.example.org",
},
},
},
[]string{
"host1.example.org: 3 host groups: all group-a group-b host1.example.org",
"host1.example.org: 3 host group priorities (desc. order): host1.example.org group-a group-b all",
`host1.example.org: 4 files: "/etc": group group-a overwrites triggers from group group-b`,
`host1.example.org: 4 files: "/etc": group host1.example.org overwrites triggers from group group-a`,
},
nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err = os.Chdir(filepath.Join(cwd,
"testdata", tc.project))
if err != nil {
t.Fatal(err)
}
// `safcm fixperms` in case user has strict umask
log.SetOutput(io.Discard)
err := MainFixperms()
if err != nil {
t.Fatal(err)
}
log.SetOutput(os.Stderr)
cfg, allHosts, allGroups, err := LoadBaseFiles()
if err != nil {
t.Fatal(err)
}
cfg.LogLevel = tc.level
var events []string
ch := make(chan Event)
done := make(chan struct{})
go func() {
for {
x, ok := <-ch
if !ok {
break
}
if x.ConnEvent.Type != 0 {
panic("unexpected ConnEvent")
}
events = append(events,
fmt.Sprintf("%s: %v %d %s",
x.Host.Name,
x.Error, x.Log.Level,
x.Log.Text))
}
done <- struct{}{}
}()
s := &Sync{
host: allHosts.Map[tc.host],
config: cfg,
allHosts: allHosts,
allGroups: allGroups,
events: ch,
}
res, err := s.hostSyncReq(tc.detected)
testutil.AssertEqual(t, "res", res, tc.exp)
testutil.AssertErrorEqual(t, "err", err, tc.expErr)
close(ch)
<-done
testutil.AssertEqual(t, "events",
events, tc.expEvents)
})
}
}