// 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", "group3", "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\n\nall\n\n\nhost1.example.org\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: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo command one", }, { OrigGroup: "group", Cmd: "echo -n command two", }, }, }, []string{ "host1.example.org: 3 host groups: all group group3 host1.example.org remove", "host1.example.org: 3 host group priorities (descending): host1.example.org", }, nil, }, { "project: host1 (log level info)", "project", "host1.example.org", nil, safcm.LogInfo, safcm.MsgSyncReq{ Groups: []string{ "all", "group", "group3", "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\n\nall\n\n\nhost1.example.org\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: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo command one", }, { OrigGroup: "group", Cmd: "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 (descending): host1.example.org", }, fmt.Errorf("groups dns and all both provide file \"/etc/resolv.conf\"\nUse 'group_priority' 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 (descending): host2.example.org", }, fmt.Errorf("groups other and all both provide file \"/etc/resolv.conf\"\nUse 'group_priority' 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 (descending): host1.example.org", }, fmt.Errorf("groups dns and all both provide file \"/etc\"\nUse 'group_priority' 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 (descending): host2.example.org", }, fmt.Errorf("groups other and all both provide file \"/etc\"\nUse 'group_priority' 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_priority", "project-group_priority", "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 (descending): 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, }, { "group_priority (single group)", "project-group_priority-single", "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: "group-a", }, "/file.txt": { Path: "/file.txt", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("file.txt: from group-a\n"), OrigGroup: "group-a", }, }, }, []string{ "host1.example.org: 3 host groups: all group-a group-b host1.example.org", "host1.example.org: 3 host group priorities (descending): host1.example.org 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) }) } }