diff --git a/.gitignore b/.gitignore index 96c3c6e..45e95ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ emulator/rf-emulator **.tar.gz **.tar.zst **.part +dist/* +**coverage.out** diff --git a/Makefile b/Makefile index 3285e00..bb8fa18 100644 --- a/Makefile +++ b/Makefile @@ -52,10 +52,10 @@ inst: ## go install tools $(call print-target) go install github.com/client9/misspell/cmd/misspell@v0.3.4 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 - go install github.com/goreleaser/goreleaser@v1.18.2 + go install github.com/goreleaser/goreleaser/v2@v2.3.2 go install github.com/cpuguy83/go-md2man/v2@latest -.PHONY: goreleaser +.PHONY: release release: ## goreleaser build $(call print-target) $(GOPATH)/bin/goreleaser build --clean --single-target --snapshot @@ -83,7 +83,9 @@ lint: ## golangci-lint .PHONY: test test: ## go test $(call print-target) - go test -race -covermode=atomic -coverprofile=coverage.out -coverpkg=./... ./... + ./emulator/setup.sh & + sleep 10 + go test -race -covermode=atomic -coverprofile=coverage.out -coverpkg=./... tests/api_test.go tests/compatibility_test.go go tool cover -html=coverage.out -o coverage.html .PHONY: diff diff --git a/go.mod b/go.mod index e00981d..e153625 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - ) require ( diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..3edeeff --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,27 @@ +package util + +import ( + "fmt" + "time" +) + +// CheckUntil regularly check a predicate until it's true or time out is reached. +func CheckUntil(interval time.Duration, timeout time.Duration, predicate func() (bool, error)) error { + timeoutCh := time.After(timeout) + + for { + select { + case <-time.After(interval): + predTrue, err := predicate() + if predTrue { + return nil + } + + if err != nil { + return err + } + case <-timeoutCh: + return fmt.Errorf("timeout of %ds reached", int64(timeout/time.Second)) + } + } +} diff --git a/tests/api_test.go b/tests/api_test.go index c213451..e823bfa 100644 --- a/tests/api_test.go +++ b/tests/api_test.go @@ -8,59 +8,189 @@ package tests import ( + "bytes" + "crypto/tls" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" "testing" + "time" + + "flag" magellan "github.com/OpenCHAMI/magellan/internal" + "github.com/OpenCHAMI/magellan/internal/util" + "github.com/OpenCHAMI/magellan/pkg/client" "github.com/rs/zerolog/log" ) var ( - scanParams = &magellan.ScanParams{ - TargetHosts: [][]string{ - []string{ - "http://127.0.0.1:443", - "http://127.0.0.1:5000", - }, - }, - Scheme: "https", - Protocol: "tcp", - Concurrency: 1, - Timeout: 30, - DisableProbing: false, - Verbose: false, - } + exePath = flag.String("exe", "../magellan", "path to 'magellan' binary executable") + emuPath = flag.String("emu", "./emulator/setup.sh", "path to emulator 'setup.sh' script") ) func TestScanAndCollect(t *testing.T) { - // do a scan on the emulator cluster with probing disabled and check results - results := magellan.ScanForAssets(scanParams) - if len(results) <= 0 { - t.Fatal("expected to find at least one BMC node, but found none") - } - // do a scan on the emulator cluster with probing enabled - results = magellan.ScanForAssets(scanParams) - if len(results) <= 0 { - t.Fatal("expected to find at least one BMC node, but found none") + var ( + err error + // tempDir = t.TempDir() + path string + command []string + cwd string + cmd *exec.Cmd + bufout bytes.Buffer + buferr bytes.Buffer + ) + + // set up the emulator to run before test + err = waitUntilEmulatorIsReady() + if err != nil { + t.Fatalf("failed while waiting for emulator: %v", err) } - // do a collect on the emulator cluster to collect Redfish info - err := magellan.CollectInventory(&results, &magellan.CollectParams{}) + // get the current working directory and print + cwd, err = os.Getwd() if err != nil { - log.Error().Err(err).Msg("failed to collect inventory") + t.Fatalf("failed to get working directory: %v", err) } + fmt.Printf("cwd: %s\n", cwd) + + // path, err := exec.LookPath("dexdump") + // if err != nil { + // log.Fatal(err) + // } + + // try and run a "scan" with the emulator + // set up the emulator to run before test + path, err = filepath.Abs(*exePath) + if err != nil { + t.Fatalf("failed to get absolute path: %v", err) + } + command = strings.Split("scan https://127.0.0.1 --port 5000 --verbose", " ") + cmd = exec.Command(path, command...) + cmd.Stdout = &bufout + cmd.Stderr = &buferr + err = cmd.Run() + fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String()) + if err != nil { + t.Fatalf("failed to run 'scan' command: %v", err) + } + + // make sure that the expected output is not empty + if len(buferr.Bytes()) <= 0 { + t.Fatalf("expected the 'scan' output to not be empty") + } + + // try and run a "collect" with the emulator + + command = strings.Split("collect --username root --password root_password --verbose", " ") + cmd = exec.Command(path, command...) + cmd.Stdout = &bufout + cmd.Stderr = &buferr + err = cmd.Run() + fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String()) + if err != nil { + t.Fatalf("failed to run 'collect' command: %v", err) + } + + // make sure that the output is not empty + if len(bufout.Bytes()) <= 0 { + t.Fatalf("expected the 'collect' output to not be empty") + } + + // TODO: check for at least one System/EthernetInterface that we know should exist } func TestCrawlCommand(t *testing.T) { - // TODO: add test to check the crawl command's behavior + var ( + err error + command []string + cmd *exec.Cmd + bufout bytes.Buffer + buferr bytes.Buffer + path string + ) + + // set up the emulator to run before test + path, err = filepath.Abs(*exePath) + if err != nil { + t.Fatalf("failed to get absolute path: %v", err) + } + fmt.Printf("path: %s\n", path) + err = waitUntilEmulatorIsReady() + if err != nil { + t.Fatalf("failed while waiting for emulator: %v", err) + } + + // try and run a "collect" with the emulator + command = strings.Split("crawl --username root --password root_password -i https://127.0.0.1:5000", " ") + cmd = exec.Command(path, command...) + cmd.Stdout = &bufout + cmd.Stderr = &buferr + err = cmd.Run() + fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String()) + if err != nil { + t.Fatalf("failed to run 'crawl' command: %v", err) + } + + // err = cmd.Wait() + // if err != nil { + // t.Fatalf("failed to call 'wait' for crawl: %v", err) + // } + + // make sure that the output is not empty + if len(bufout.Bytes()) <= 0 { + t.Fatalf("expected the 'crawl' output to not be empty") + } + } func TestListCommand(t *testing.T) { - // TODO: add test to check the list command's output + var ( + err error + cmd *exec.Cmd + ) + + // set up the emulator to run before test + err = waitUntilEmulatorIsReady() + if err != nil { + t.Fatalf("failed while waiting for emulator: %v", err) + } + + // set up temporary directory + cmd = exec.Command("bash", "-c", fmt.Sprintf("%s list", *exePath)) + err = cmd.Start() + if err != nil { + t.Fatalf("failed to run 'list' command: %v", err) + } + // NOTE: the output of `list` can be empty if no scan has been performed + } func TestUpdateCommand(t *testing.T) { // TODO: add test that does a Redfish simple update checking it success and // failure points + var ( + cmd *exec.Cmd + err error + ) + + // set up the emulator to run before test + err = waitUntilEmulatorIsReady() + if err != nil { + t.Fatalf("failed while waiting for emulator: %v", err) + } + + // set up temporary directory + cmd = exec.Command("bash", "-c", fmt.Sprintf("%s update", *exePath)) + err = cmd.Start() + if err != nil { + t.Fatalf("failed to run 'update' command: %v", err) + } + } func TestGofishFunctions(t *testing.T) { @@ -68,6 +198,115 @@ func TestGofishFunctions(t *testing.T) { // gofish's output isn't changing spontaneously and remains predictable } +// TestGenerateHosts() tests creating a collection of hosts by changing arguments +// and calling GenerateHostsWithSubnet(). func TestGenerateHosts(t *testing.T) { - // TODO: add test to generate hosts using a collection of subnets/masks + var ( + subnet = "127.0.0.1" + subnetMask = &net.IPMask{255, 255, 255, 0} + ports = []int{443} + scheme = "https" + hosts = [][]string{} + ) + t.Run("generate-hosts", func(t *testing.T) { + hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme) + + // check for at least one host to be generated + if len(hosts) <= 0 { + t.Fatalf("expected at least one host to be generated for subnet %s", subnet) + } + }) + + t.Run("generate-hosts-with-multiple-ports", func(t *testing.T) { + ports = []int{443, 5000} + hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme) + + // check for at least one host to be generated + if len(hosts) <= 0 { + t.Fatalf("expected at least one host to be generated for subnet %s", subnet) + } + }) + + t.Run("generate-hosts-with-subnet-mask", func(t *testing.T) { + subnetMask = &net.IPMask{255, 255, 0, 0} + hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme) + + // check for at least one host to be generated + if len(hosts) <= 0 { + t.Fatalf("expected at least one host to be generated for subnet %s", subnet) + } + }) + +} + +func startEmulatorInBackground(path string) (int, error) { + // try and start the emulator in the background if arg passed + var ( + cmd *exec.Cmd + err error + ) + if path != "" { + cmd = exec.Command("bash", "-c", path) + err = cmd.Start() + if err != nil { + return -1, fmt.Errorf("failed while executing emulator startup script: %v", err) + } + } else { + return -1, fmt.Errorf("path to emulator start up script is required") + } + return cmd.Process.Pid, nil +} + +// waitUntilEmulatorIsReady() polls with +func waitUntilEmulatorIsReady() error { + var ( + interval = time.Second * 2 + timeout = time.Second * 6 + testClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + body client.HTTPBody + header client.HTTPHeader + err error + ) + err = util.CheckUntil(interval, timeout, func() (bool, error) { + // send request to host until we get expected response + res, _, err := client.MakeRequest(testClient, "https://127.0.0.1:5000/redfish/v1/", http.MethodGet, body, header) + if err != nil { + return false, fmt.Errorf("failed to make request to emulator: %w", err) + } + if res == nil { + return false, fmt.Errorf("invalid response from emulator (response is nil)") + } + if res.StatusCode == http.StatusOK { + return true, nil + } else { + return false, fmt.Errorf("unexpected status code %d", res.StatusCode) + } + + }) + return err +} + +func init() { + var ( + cwd string + err error + ) + // get the current working directory + cwd, err = os.Getwd() + if err != nil { + log.Error().Err(err).Msg("failed to get working directory") + } + fmt.Printf("cwd: %s\n", cwd) + + // start emulator in the background before running tests + pid, err := startEmulatorInBackground(*emuPath) + if err != nil { + log.Error().Err(err).Msg("failed to start emulator in background") + os.Exit(1) + } + _ = pid } diff --git a/tests/compatibility_test.go b/tests/compatibility_test.go index 86e3a4f..03917ec 100644 --- a/tests/compatibility_test.go +++ b/tests/compatibility_test.go @@ -7,6 +7,7 @@ package tests import ( + "crypto/tls" "encoding/json" "flag" "fmt" @@ -15,56 +16,97 @@ import ( "github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/crawler" - "github.com/rs/zerolog/log" ) var ( - host = flag.String("host", "localhost", "set the BMC host") - username = flag.String("username", "", "set the BMC username used for the tests") - password = flag.String("password", "", "set the BMC password used for the tests") + host = flag.String("host", "https://127.0.0.1:5000", "set the BMC host") + username = flag.String("username", "root", "set the BMC username used for the tests") + password = flag.String("password", "root_password", "set the BMC password used for the tests") ) -// Simple test to fetch the base Redfish URL and assert a 200 OK response. -func TestRedfishV1Availability(t *testing.T) { - var ( - url = fmt.Sprintf("%s/redfish/v1", *host) - body = []byte{} - headers = map[string]string{} - ) - res, b, err := client.MakeRequest(nil, url, http.MethodGet, body, headers) - if err != nil { - t.Fatalf("failed to make request to BMC: %v", err) - } - +func checkResponse(res *http.Response, b []byte) error { // test for a 200 response code here if res.StatusCode != http.StatusOK { - t.Fatalf("expected response code to return status code 200") + return fmt.Errorf("expected response code to return status code 200") } // make sure the response body is not empty if len(b) <= 0 { - t.Fatalf("expected response body to not be empty") + return fmt.Errorf("expected response body to not be empty") } - // make sure the response body is in a JSON format - if json.Valid(b) { - t.Fatalf("expected response body to be valid JSON") + // make sure the response body is in a valid JSON format + if !json.Valid(b) { + return fmt.Errorf("expected response body to be valid JSON") + } + return nil +} + +// Simple test to fetch the base Redfish URL and assert a 200 OK response. +func TestRedfishV1ServiceRootAvailability(t *testing.T) { + var ( + url = fmt.Sprintf("%s/redfish/v1/", *host) + body = []byte{} + headers = map[string]string{} + testClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + err error + ) + + // set up the emulator to run before test + err = waitUntilEmulatorIsReady() + if err != nil { + t.Fatalf("failed while waiting for emulator: %v", err) + } + + res, b, err := client.MakeRequest(testClient, url, http.MethodGet, body, headers) + if err != nil { + t.Fatalf("failed to make request to BMC node: %v", err) + } + + err = checkResponse(res, b) + if err != nil { + t.Fatalf("failed to check response for redfish service root: %v", err) } } // Simple test to ensure an expected Redfish version minimum requirement. -func TestRedfishVersion(t *testing.T) { +func TestRedfishV1Version(t *testing.T) { var ( - url string = fmt.Sprintf("%s/redfish/v1", *host) - body client.HTTPBody = []byte{} - headers client.HTTPHeader = map[string]string{} - err error + url string = fmt.Sprintf("%s/redfish/v1/", *host) + body client.HTTPBody = []byte{} + headers client.HTTPHeader = map[string]string{} + testClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + root map[string]any + err error ) - _, _, err = client.MakeRequest(nil, url, http.MethodGet, body, headers) + res, b, err := client.MakeRequest(testClient, url, http.MethodGet, body, headers) if err != nil { - log.Error().Err(err).Msg("failed to make request") + t.Fatalf("failed to make request to BMC node: %v", err) + } + err = checkResponse(res, b) + if err != nil { + t.Fatalf("failed to check response for redfish version: %v", err) + } + + // check the "RedfishVersion" from service root + err = json.Unmarshal(b, &root) + if err != nil { + t.Fatalf("failed to unmarshal redfish response: %v", err) + } + + _, ok := root["RedfishVersion"] + if !ok { + t.Fatalf("failed to get 'RedfishVersion' from service root") } } @@ -72,12 +114,18 @@ func TestRedfishVersion(t *testing.T) { // that we need for Magellan to run correctly. This test differs from the // `TestCrawlCommand` testing function as it is not checking specifically // for functionality. -func TestExpectedProperties(t *testing.T) { +func TestExpectedOutput(t *testing.T) { // make sure what have a valid host if host == nil { t.Fatal("invalid host (host is nil)") } + // set up the emulator to run before test + err := waitUntilEmulatorIsReady() + if err != nil { + t.Fatalf("failed while waiting for emulator: %v", err) + } + systems, err := crawler.CrawlBMC( crawler.CrawlerConfig{ URI: *host, @@ -106,8 +154,5 @@ func TestExpectedProperties(t *testing.T) { if len(system.EthernetInterfaces) <= 0 { t.Errorf("no ethernet interfaces found for system '%s'", system.Name) } - if len(system.NetworkInterfaces) <= 0 { - t.Errorf("no network interfaces found for system '%s'", system.Name) - } } }