scraper_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. // Copyright The OpenTelemetry Authors
  2. // SPDX-License-Identifier: Apache-2.0
  3. package sshcheckreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sshcheckreceiver"
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net"
  10. "os"
  11. "path/filepath"
  12. "testing"
  13. "time"
  14. "github.com/pkg/sftp"
  15. "github.com/stretchr/testify/require"
  16. "go.opentelemetry.io/collector/component/componenttest"
  17. "go.opentelemetry.io/collector/receiver/receivertest"
  18. "golang.org/x/crypto/ssh"
  19. "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden"
  20. "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest"
  21. )
  22. func setupSSHServer(t *testing.T) string {
  23. config := &ssh.ServerConfig{
  24. NoClientAuth: true,
  25. PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
  26. if c.User() == "otelu" && string(pass) == "otelp" {
  27. return nil, nil
  28. }
  29. return nil, fmt.Errorf("wrong username or password")
  30. },
  31. }
  32. privateBytes, err := os.ReadFile("testdata/keys/id_rsa")
  33. require.NoError(t, err)
  34. private, err := ssh.ParsePrivateKey(privateBytes)
  35. require.NoError(t, err)
  36. config.AddHostKey(private)
  37. listener, err := net.Listen("tcp", "127.0.0.1:0")
  38. require.NoError(t, err)
  39. go func() {
  40. for {
  41. conn, err := listener.Accept()
  42. if err != nil {
  43. break
  44. }
  45. _, chans, reqs, err := ssh.NewServerConn(conn, config)
  46. if err != nil {
  47. t.Logf("Failed to handshake: %v", err)
  48. continue
  49. }
  50. go ssh.DiscardRequests(reqs)
  51. go handleChannels(chans)
  52. }
  53. }()
  54. return listener.Addr().String()
  55. }
  56. func handleChannels(chans <-chan ssh.NewChannel) {
  57. for newChannel := range chans {
  58. if t := newChannel.ChannelType(); t != "session" {
  59. if err := newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)); err != nil {
  60. return
  61. }
  62. continue
  63. }
  64. channel, requests, err := newChannel.Accept()
  65. if err != nil {
  66. continue
  67. }
  68. go func(in <-chan *ssh.Request) {
  69. for req := range in {
  70. ok := false
  71. if req.Type == "subsystem" && string(req.Payload[4:]) == "sftp" {
  72. ok = true
  73. go func() {
  74. defer channel.Close()
  75. server := sftp.NewRequestServer(channel, sftp.Handlers{
  76. FileGet: sftp.InMemHandler().FileGet,
  77. FilePut: sftp.InMemHandler().FilePut,
  78. FileCmd: sftp.InMemHandler().FileCmd,
  79. FileList: sftp.InMemHandler().FileList,
  80. })
  81. if err != nil {
  82. return
  83. }
  84. if err := server.Serve(); errors.Is(err, io.EOF) {
  85. server.Close()
  86. } else if err != nil {
  87. return
  88. }
  89. }()
  90. }
  91. if err := req.Reply(ok, nil); err != nil {
  92. return
  93. }
  94. }
  95. }(requests)
  96. }
  97. }
  98. func TestScraper(t *testing.T) {
  99. if !supportedOS() {
  100. t.Skip("Skip tests if not running on one of: [linux, darwin, freebsd, openbsd]")
  101. }
  102. endpoint := setupSSHServer(t)
  103. require.NotEmpty(t, endpoint)
  104. testCases := []struct {
  105. name string
  106. filename string
  107. enableSFTP bool
  108. }{
  109. {
  110. name: "metrics_golden",
  111. filename: "metrics_golden.yaml",
  112. },
  113. {
  114. name: "metrics_golden_sftp",
  115. filename: "metrics_golden_sftp.yaml",
  116. enableSFTP: true,
  117. },
  118. {
  119. name: "cannot_authenticate",
  120. filename: "cannot_authenticate.yaml",
  121. },
  122. {
  123. name: "invalid_endpoint",
  124. filename: "invalid_endpoint.yaml",
  125. },
  126. }
  127. for _, tc := range testCases {
  128. t.Run(tc.name, func(t *testing.T) {
  129. expectedFile := filepath.Join("testdata", "expected_metrics", tc.filename)
  130. expectedMetrics, err := golden.ReadMetrics(expectedFile)
  131. require.NoError(t, err)
  132. f := NewFactory()
  133. cfg := f.CreateDefaultConfig().(*Config)
  134. cfg.ScraperControllerSettings.CollectionInterval = 100 * time.Millisecond
  135. cfg.Username = "otelu"
  136. cfg.Password = "otelp"
  137. cfg.Endpoint = endpoint
  138. cfg.IgnoreHostKey = true
  139. if tc.enableSFTP {
  140. cfg.MetricsBuilderConfig.Metrics.SshcheckSftpStatus.Enabled = true
  141. cfg.MetricsBuilderConfig.Metrics.SshcheckSftpDuration.Enabled = true
  142. }
  143. settings := receivertest.NewNopCreateSettings()
  144. scrpr := newScraper(cfg, settings)
  145. require.NoError(t, scrpr.start(context.Background(), componenttest.NewNopHost()), "failed starting scraper")
  146. actualMetrics, err := scrpr.scrape(context.Background())
  147. require.NoError(t, err, "failed scrape")
  148. require.NoError(
  149. t,
  150. pmetrictest.CompareMetrics(
  151. expectedMetrics,
  152. actualMetrics,
  153. pmetrictest.IgnoreMetricValues("sshcheck.duration", "sshcheck.sftp_duration"),
  154. pmetrictest.IgnoreTimestamp(),
  155. pmetrictest.IgnoreStartTimestamp(),
  156. pmetrictest.IgnoreMetricAttributeValue("sshcheck", "endpoint"),
  157. ),
  158. )
  159. })
  160. }
  161. }
  162. func TestScraperPropagatesResourceAttributes(t *testing.T) {
  163. if !supportedOS() {
  164. t.Skip("Skip tests if not running on one of: [linux, darwin, freebsd, openbsd]")
  165. }
  166. endpoint := setupSSHServer(t)
  167. require.NotEmpty(t, endpoint)
  168. f := NewFactory()
  169. cfg := f.CreateDefaultConfig().(*Config)
  170. cfg.MetricsBuilderConfig.ResourceAttributes.SSHEndpoint.Enabled = true
  171. cfg.ScraperControllerSettings.CollectionInterval = 100 * time.Millisecond
  172. cfg.Username = "otelu"
  173. cfg.Password = "otelp"
  174. cfg.Endpoint = endpoint
  175. cfg.IgnoreHostKey = true
  176. settings := receivertest.NewNopCreateSettings()
  177. scraper := newScraper(cfg, settings)
  178. require.NoError(t, scraper.start(context.Background(), componenttest.NewNopHost()), "failed starting scraper")
  179. actualMetrics, err := scraper.scrape(context.Background())
  180. require.NoError(t, err, "failed scrape")
  181. resourceMetrics := actualMetrics.ResourceMetrics()
  182. expectedResourceAttributes := map[string]any{"ssh.endpoint": endpoint}
  183. for i := 0; i < resourceMetrics.Len(); i++ {
  184. resourceAttributes := resourceMetrics.At(i).Resource().Attributes()
  185. for name, value := range expectedResourceAttributes {
  186. actualAttributeValue, ok := resourceAttributes.Get(name)
  187. require.True(t, ok)
  188. require.Equal(t, value, actualAttributeValue.Str())
  189. }
  190. }
  191. }
  192. func TestScraperDoesNotErrForSSHErr(t *testing.T) {
  193. if !supportedOS() {
  194. t.Skip("Skip tests if not running on one of: [linux, darwin, freebsd, openbsd]")
  195. }
  196. endpoint := setupSSHServer(t)
  197. require.NotEmpty(t, endpoint)
  198. f := NewFactory()
  199. cfg := f.CreateDefaultConfig().(*Config)
  200. cfg.ScraperControllerSettings.CollectionInterval = 100 * time.Millisecond
  201. cfg.Username = "not-the-user"
  202. cfg.Password = "not-the-password"
  203. cfg.Endpoint = endpoint
  204. cfg.IgnoreHostKey = true
  205. settings := receivertest.NewNopCreateSettings()
  206. scraper := newScraper(cfg, settings)
  207. require.NoError(t, scraper.start(context.Background(), componenttest.NewNopHost()), "should not err to start")
  208. _, err := scraper.scrape(context.Background())
  209. require.NoError(t, err, "should not err")
  210. }
  211. func TestTimeout(t *testing.T) {
  212. if !supportedOS() {
  213. t.Skip("Skip tests if not running on one of: [linux, darwin, freebsd, openbsd]")
  214. }
  215. testCases := []struct {
  216. name string
  217. deadline time.Time
  218. timeout time.Duration
  219. want time.Duration
  220. }{
  221. {
  222. name: "timeout is shorter",
  223. deadline: time.Now().Add(time.Second),
  224. timeout: time.Second * 2,
  225. want: time.Second,
  226. },
  227. {
  228. name: "deadline is shorter",
  229. deadline: time.Now().Add(time.Second * 2),
  230. timeout: time.Second,
  231. want: time.Second,
  232. },
  233. }
  234. for _, tc := range testCases {
  235. t.Run(tc.name, func(t *testing.T) {
  236. to := timeout(tc.deadline, tc.timeout)
  237. if to < (tc.want-10*time.Millisecond) || to > tc.want {
  238. t.Fatalf("wanted time within 10 milliseconds: %s, got: %s", time.Second, to)
  239. }
  240. })
  241. }
  242. }
  243. func TestCancellation(t *testing.T) {
  244. f := NewFactory()
  245. cfg := f.CreateDefaultConfig().(*Config)
  246. cfg.ScraperControllerSettings.CollectionInterval = 100 * time.Millisecond
  247. settings := receivertest.NewNopCreateSettings()
  248. scrpr := newScraper(cfg, settings)
  249. if !supportedOS() {
  250. require.Error(t, scrpr.start(context.Background(), componenttest.NewNopHost()), "should err starting scraper")
  251. return
  252. }
  253. require.NoError(t, scrpr.start(context.Background(), componenttest.NewNopHost()), "failed starting scraper")
  254. ctx, cancel := context.WithCancel(context.Background())
  255. cancel()
  256. _, err := scrpr.scrape(ctx)
  257. require.Error(t, err, "should have returned error on canceled context")
  258. require.EqualValues(t, err.Error(), ctx.Err().Error(), "scrape should return context's error")
  259. }
  260. // issue # 18193
  261. // init failures resulted in scrape panic for SFTP client
  262. func TestWithoutStartErrsNotPanics(t *testing.T) {
  263. f := NewFactory()
  264. cfg := f.CreateDefaultConfig().(*Config)
  265. cfg.ScraperControllerSettings.CollectionInterval = 100 * time.Millisecond
  266. cfg.Username = "otelu"
  267. cfg.Password = "otelp"
  268. cfg.Endpoint = "localhost:22"
  269. cfg.IgnoreHostKey = true
  270. cfg.MetricsBuilderConfig.Metrics.SshcheckSftpStatus.Enabled = true
  271. cfg.MetricsBuilderConfig.Metrics.SshcheckSftpDuration.Enabled = true
  272. // create the scraper without starting it, so Client is nil
  273. scrpr := newScraper(cfg, receivertest.NewNopCreateSettings())
  274. // scrape should error not panic
  275. var err error
  276. require.NotPanics(t, func() { _, err = scrpr.scrape(context.Background()) }, "scrape should not panic")
  277. require.Error(t, err, "expected scrape to err when without start")
  278. }