]> ruderich.org/simon Gitweb - safcm/safcm.git/commitdiff
tests: add very basic end-to-end test with real ssh server
authorSimon Ruderich <simon@ruderich.org>
Wed, 21 Apr 2021 06:16:40 +0000 (08:16 +0200)
committerSimon Ruderich <simon@ruderich.org>
Thu, 22 Apr 2021 06:06:50 +0000 (08:06 +0200)
At the moment only the helper upload without any actual configuration is
tested.

17 files changed:
.builds/archlinux.yml
.builds/freebsd.yml
.builds/openbsd.yml
.gitignore
.gitlab-ci.yml
Makefile
cmd/safcm/config/config.go
cmd/safcm/main_sync_test.go [new file with mode: 0644]
cmd/safcm/sync.go
cmd/safcm/testdata/ssh/prepare.sh [new file with mode: 0755]
cmd/safcm/testdata/ssh/project/groups.yaml [new file with mode: 0644]
cmd/safcm/testdata/ssh/project/hosts.yaml [new file with mode: 0644]
cmd/safcm/testdata/ssh/ssh/ssh_config [new file with mode: 0644]
cmd/safcm/testdata/ssh/sshd/sshd_config [new file with mode: 0644]
cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd [new file with mode: 0644]
rpc/conn.go
rpc/dial.go

index d8ebea79af0a1702e0f0840a70749e9139934d37..4adc85f0307e230c5af696e6261acea51eb92664 100644 (file)
@@ -6,5 +6,5 @@ tasks:
       cd safcm
       ./ci/run
       # Also run all tests as root
-      sudo chown -Rh root:root .
+      sudo chown -Rh root:root ..
       sudo ./ci/run
index e5dba6db9c7d32288bef957579373f7dee46caa5..e3ea472768e717889699f35210d829932188a924 100644 (file)
@@ -8,5 +8,5 @@ tasks:
       cd safcm
       ./ci/run
       # Also run all tests as root
-      sudo chown -Rh root:wheel .
+      sudo chown -Rh root:wheel ..
       sudo ./ci/run
index 7f1d40c9208e6ea97a15b0135c261d1c09a65e64..025d49a1b5412d6ba94708664e288ef5b7da1f90 100644 (file)
@@ -16,6 +16,6 @@ tasks:
       # Go does not yet support -race on OpenBSD
       ./ci/run GOFLAGS=
       # Also run all tests as root (-R and -h conflict on OpenBSD)
-      doas chown -R root:wheel .
+      doas chown -R root:wheel ..
       # Remove $HOME hack once Go 1.16 is available
       doas /bin/sh -c "HOME=$HOME ./ci/run GOFLAGS="
index 0b4325c42a202bdd0f1a081fb75a514222f6f13d..37a596ec7e174cad786d40f33a8a6edfadd7c38f 100644 (file)
@@ -1,3 +1,9 @@
+/cmd/safcm/testdata/ssh/ssh/authorized_keys
+/cmd/safcm/testdata/ssh/ssh/id_ed25519
+/cmd/safcm/testdata/ssh/ssh/id_ed25519.pub
+/cmd/safcm/testdata/ssh/ssh/known_hosts
+/cmd/safcm/testdata/ssh/sshd/ssh_host_key
+/cmd/safcm/testdata/ssh/sshd/ssh_host_key.pub
 /remote/helpers/
 /safcm
 /tags
index 10f98119fbe3f117b91f93f7b888d91c2dc764fe..e2ec0a52a7582a9265fd431dbf90b2d41bebaabc 100644 (file)
@@ -1,13 +1,19 @@
 .template-docker: &template-docker
   before_script:
     - apt-get update
-    - apt-get install --no-install-recommends --yes build-essential ca-certificates git golang golang-1.16 golang-golang-x-tools make
+    - apt-get install --no-install-recommends --yes build-essential ca-certificates git golang golang-1.16 golang-golang-x-tools make openssh-server
   script:
+    # Gitlab-runner uses umask 0000 (wtf?!) and mixes nobody and root user
+    # when setting up the environment. This breaks ssh's permission check on
+    # authorized_keys.
+    - chown -R root:root /builds
+    - chmod -R go-w /builds
     # NOTE: golang is still using golang-1.15
     - mkdir -p $HOME/go/bin
     - ln -sf /usr/lib/go-1.16/bin/go $HOME/go/bin
     - ln -sf /usr/lib/go-1.16/bin/gofmt $HOME/go/bin
     #
+    - mkdir /run/sshd
     - ./ci/run
 
 debian-sid:
index f4cc011e6aa6abdab01f603e4d50d3193e54b8a0..5549539245c535c6d0e92d7022ea531622a83e3a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -16,11 +16,18 @@ safcm:
        cd cmd/safcm/testdata/project && ../../../../safcm fixperms 2> /dev/null
 
 test:
+       ./cmd/safcm/testdata/ssh/prepare.sh
        go vet ./...
        go test $(GOFLAGS) ./...
 
 clean:
        rm -rf remote/helpers/
        rm -f safcm
+       rm -f cmd/safcm/testdata/ssh/ssh/authorized_keys
+       rm -f cmd/safcm/testdata/ssh/ssh/id_ed25519
+       rm -f cmd/safcm/testdata/ssh/ssh/id_ed25519.pub
+       rm -f cmd/safcm/testdata/ssh/ssh/known_hosts
+       rm -f cmd/safcm/testdata/ssh/sshd/ssh_host_key
+       rm -f cmd/safcm/testdata/ssh/sshd/ssh_host_key.pub
 
 .PHONY: all test clean safcm
index bae137f3173ae402fc7da9260276fe7bca61d2ac..fc01c8da1a3a61b21200c738dc771c3d134f775f 100644 (file)
@@ -30,6 +30,7 @@ type Config struct {
        DryRun   bool           `yaml:"-"` // set via command line
        Quiet    bool           `yaml:"-"` // set via command line
        LogLevel safcm.LogLevel `yaml:"-"` // set via command line
+       SshConfig string         `yaml:"-"` // set via command line
 
        DetectGroups []string `yaml:"detect_groups"`
        GroupOrder   []string `yaml:"group_order"`
diff --git a/cmd/safcm/main_sync_test.go b/cmd/safcm/main_sync_test.go
new file mode 100644 (file)
index 0000000..8d702db
--- /dev/null
@@ -0,0 +1,153 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+package main_test
+
+import (
+       "fmt"
+       "net"
+       "os"
+       "os/exec"
+       "regexp"
+       "runtime"
+       "strings"
+       "testing"
+       "time"
+
+       "ruderich.org/simon/safcm/testutil"
+)
+
+func TestSyncSshEndToEnd(t *testing.T) {
+       cwd, err := os.Getwd()
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.Chdir(cwd)
+
+       var suffix string
+       // Needs different options in sshd_config
+       if runtime.GOOS == "openbsd" {
+               suffix = ".openbsd"
+       }
+
+       sshDir := cwd + "/testdata/ssh"
+       sshCmd := exec.Command("/usr/sbin/sshd",
+               "-D", // stay in foreground
+               "-e", // write messages to stderr instead of syslog
+               "-f", sshDir+"/sshd/sshd_config"+suffix,
+               "-h", sshDir+"/sshd/ssh_host_key",
+               "-o", "AuthorizedKeysFile="+sshDir+"/ssh/authorized_keys",
+       )
+       sshCmd.Stderr = os.Stderr
+       err = sshCmd.Start()
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer sshCmd.Process.Kill()
+
+       // Wait until SSH server is ready (up to 30 seconds)
+       for i := 0; i < 30; i++ {
+               conn, err := net.Dial("tcp", "127.0.0.1:29327")
+               if err == nil {
+                       conn.Close()
+                       break
+               }
+               time.Sleep(time.Second)
+       }
+
+       err = os.Chdir(sshDir + "/project")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tests := []struct {
+               name   string
+               remove bool
+               args   []string
+               exp    string
+               expErr error
+       }{
+
+               {
+                       "no settings",
+                       true,
+                       []string{"no-settings.example.org"},
+                       `<LOG>[info]    [no-settings.example.org] remote helper upload in progress
+`,
+                       nil,
+               },
+               {
+                       "no settings (no helper upload)",
+                       false,
+                       []string{"no-settings.example.org"},
+                       ``,
+                       nil,
+               },
+               {
+                       "no settings (verbose)",
+                       true,
+                       []string{"-log", "verbose", "no-settings.example.org"},
+                       `<LOG>[info]    [no-settings.example.org] remote helper upload in progress
+<LOG>[verbose] [no-settings.example.org] host groups: all <DET> <DET> no-settings.example.org
+<LOG>[verbose] [no-settings.example.org] host group priorities (desc. order): no-settings.example.org
+`,
+                       nil,
+               },
+               {
+                       "no settings (debug2)",
+                       true,
+                       []string{"-log", "debug2", "no-settings.example.org"},
+                       `<LOG>[info]    [no-settings.example.org] remote helper upload in progress
+<LOG>[verbose] [no-settings.example.org] host groups: all <DET> <DET> no-settings.example.org
+<LOG>[verbose] [no-settings.example.org] host group priorities (desc. order): no-settings.example.org
+`,
+                       nil,
+               },
+       }
+
+       remotePath := fmt.Sprintf("/tmp/safcm-remote-%d", os.Getuid())
+
+       logRegexp := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
+       detectedRegexp := regexp.MustCompile(`detected_\S+`)
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       if tc.remove {
+                               os.Remove(remotePath)
+                       }
+
+                       args := append([]string{"sync",
+                               "-sshconfig", sshDir + "/ssh/ssh_config",
+                       }, tc.args...)
+                       cmd := exec.Command("../../../../../safcm", args...)
+                       out, err := cmd.CombinedOutput()
+
+                       var tmp []string
+                       for _, x := range strings.Split(string(out), "\n") {
+                               // Strip parts which change on each run (LOG)
+                               // or depending on the system (DET)
+                               x = logRegexp.ReplaceAllString(x, "<LOG>")
+                               x = detectedRegexp.ReplaceAllString(x, "<DET>")
+                               tmp = append(tmp, x)
+                       }
+                       res := strings.Join(tmp, "\n")
+
+                       testutil.AssertEqual(t, "res", res, tc.exp)
+                       testutil.AssertErrorEqual(t, "err", err, tc.expErr)
+               })
+       }
+
+       os.Remove(remotePath)
+}
index 238ed62ae5e50f5f3516847817363a3bf894f8b7..98b118ed821d2224bf4f7e77e0ed002b368d9df3 100644 (file)
@@ -76,6 +76,8 @@ func MainSync(args []string) error {
                "hide successful, non-trigger commands with no output from host changes listing")
        optionLog := flag.String("log", "info", "set log `level`; "+
                "levels: error, info, verbose, debug, debug2, debug3")
+       optionSshConfig := flag.String("sshconfig", "",
+               "`path` to ssh configuration file; used for tests")
 
        flag.CommandLine.Parse(args[2:])
 
@@ -110,6 +112,7 @@ func MainSync(args []string) error {
        cfg.DryRun = *optionDryRun
        cfg.Quiet = *optionQuiet
        cfg.LogLevel = level
+       cfg.SshConfig = *optionSshConfig
 
        toSync, err := hostsToSync(names, allHosts, allGroups)
        if err != nil {
@@ -364,7 +367,7 @@ func (s *Sync) Host(wg *sync.WaitGroup) error {
        }()
 
        // Connect to remote host
-       err := conn.DialSSH(s.host.SshUser, s.host.Name)
+       err := conn.DialSSH(s.host.SshUser, s.host.Name, s.config.SshConfig)
        if err != nil {
                return err
        }
diff --git a/cmd/safcm/testdata/ssh/prepare.sh b/cmd/safcm/testdata/ssh/prepare.sh
new file mode 100755 (executable)
index 0000000..6608a3a
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+# Create files for SSH server
+
+set -eu
+
+
+cd "$(dirname "$0")"
+
+# Generate new keys so nobody else can login as this user. Host keys would be
+# safe to commit but lets not needlessly commit private keys anyway.
+if ! test -e sshd/ssh_host_key; then
+    ssh-keygen -q -t ed25519 -N '' -f sshd/ssh_host_key
+fi
+if ! test -e ssh/id_ed25519; then
+    ssh-keygen -q -t ed25519 -N '' -f ssh/id_ed25519
+fi
+if ! test -e ssh/authorized_keys; then
+    cat ssh/id_ed25519.pub > ssh/authorized_keys
+fi
+if ! test -e ssh/known_hosts; then
+    printf '[127.0.0.1]:29327 ' > ssh/known_hosts
+    cat sshd/ssh_host_key.pub >> ssh/known_hosts
+fi
diff --git a/cmd/safcm/testdata/ssh/project/groups.yaml b/cmd/safcm/testdata/ssh/project/groups.yaml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/cmd/safcm/testdata/ssh/project/hosts.yaml b/cmd/safcm/testdata/ssh/project/hosts.yaml
new file mode 100644 (file)
index 0000000..b6a0dce
--- /dev/null
@@ -0,0 +1 @@
+- name: no-settings.example.org
diff --git a/cmd/safcm/testdata/ssh/ssh/ssh_config b/cmd/safcm/testdata/ssh/ssh/ssh_config
new file mode 100644 (file)
index 0000000..77f6fd5
--- /dev/null
@@ -0,0 +1,7 @@
+IdentityFile ../ssh/id_ed25519
+IdentitiesOnly yes
+UserKnownHostsFile ../ssh/known_hosts
+
+Host no-settings.example.org
+    Hostname 127.0.0.1
+    Port 29327
diff --git a/cmd/safcm/testdata/ssh/sshd/sshd_config b/cmd/safcm/testdata/ssh/sshd/sshd_config
new file mode 100644 (file)
index 0000000..d43eb49
--- /dev/null
@@ -0,0 +1,16 @@
+Port 29327
+ListenAddress 127.0.0.1
+PidFile none
+LogLevel ERROR
+
+# Only permit pubkey authentication
+PubkeyAuthentication yes
+PermitRootLogin prohibit-password
+#
+ChallengeResponseAuthentication no
+GSSAPIAuthentication no
+HostbasedAuthentication no
+KbdInteractiveAuthentication no
+KerberosAuthentication no
+PasswordAuthentication no
+UsePAM no
diff --git a/cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd b/cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd
new file mode 100644 (file)
index 0000000..76efd8c
--- /dev/null
@@ -0,0 +1,15 @@
+Port 29327
+ListenAddress 127.0.0.1
+PidFile none
+LogLevel ERROR
+
+# Only permit pubkey authentication
+PubkeyAuthentication yes
+PermitRootLogin prohibit-password
+#
+ChallengeResponseAuthentication no
+HostbasedAuthentication no
+KbdInteractiveAuthentication no
+PasswordAuthentication no
+
+# vim: ft=sshdconfig
index b2bcf28a05025317c729136cc7680579dffcfe36..65226121b3e1006586335d37ed99b20ac0d6813a 100644 (file)
@@ -33,7 +33,8 @@ type Conn struct {
        eventsWg sync.WaitGroup
 
        debug  bool
-       remote string
+       sshRemote string
+       sshOpts   []string
 
        cmd  *exec.Cmd
        conn *safcm.GobConn
index d8a63386333d2c76c27affca33418d3378bcc4dc..945a75ff86c365fd1e251e205a0b68efbd2b3aef 100644 (file)
@@ -32,7 +32,7 @@ import (
        "ruderich.org/simon/safcm/remote"
 )
 
-func (c *Conn) DialSSH(user, host string) error {
+func (c *Conn) DialSSH(user, host, sshConfig string) error {
        if c.events == nil {
                return fmt.Errorf("cannot reuse Conn")
        }
@@ -48,8 +48,14 @@ func (c *Conn) DialSSH(user, host string) error {
                // Help debugging by showing executed shell commands
                opts += "x"
        }
-       c.cmd = exec.Command("ssh", remote, "/bin/sh", opts)
-       c.remote = remote
+
+       c.sshRemote = remote
+       if sshConfig != "" {
+               c.sshOpts = append(c.sshOpts, "-F", sshConfig)
+       }
+       c.cmd = exec.Command("ssh",
+               append(append([]string{}, c.sshOpts...),
+                       c.sshRemote, "/bin/sh", opts)...)
 
        stdin, err := c.cmd.StdinPipe()
        if err != nil {
@@ -251,10 +257,12 @@ f
                path = strings.TrimSuffix(path, "\n")
 
                c.debugf("DialSSH: uploading new remote to %q at %q",
-                       c.remote, path)
+                       c.sshRemote, path)
 
-               cmd := exec.Command("ssh", c.remote,
-                       fmt.Sprintf("cat > %q", path))
+               cmd := exec.Command("ssh",
+                       append(append([]string{}, c.sshOpts...),
+                               c.sshRemote,
+                               fmt.Sprintf("cat > %q", path))...)
                cmd.Stdin = bytes.NewReader(helper)
                err = c.handleStderrAsEvents(cmd)
                if err != nil {