diff --git a/Dockerfile b/Dockerfile index ada5149..100eb50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,6 +69,22 @@ ENV BSS_HSM_RETRIEVAL_DELAY=10 # # URL of SPIRE token service (not necessary to run BSS). # SPIRE_TOKEN_URL=https://spire-tokens.spire:54440 +# +# URL of JSON Web Key Set (JWKS) server to use for verifying JWTs. +# When this is set, JWT authentication is enabled. Otherwise, it +# is disabled. +# BSS_JWKS_URL="" +# +# Base URL of the Oauth2 server admin endpoints to use for client authorizations +# when JWT authentication is enabled. This is used to authorize BSS via a client +# credentials grant to be able to communicate with protected SMD endpoints when +# it is queried for a boot script. +# BSS_OAUTH2_ADMIN_BASE_URL=http://127.0.0.1:4445 +# +# Base URL of the OAuth2 server public endpoints to use for non-admin requests +# like a client (e.g. BSS) requesting an access token after it has been +# authorized. +# BSS_OAUTH2_USER_BASE_URL=http://127.0.0.1:4444 # Etcd variables with default values: # diff --git a/cmd/boot-script-service/main.go b/cmd/boot-script-service/main.go index 2878fa8..a183386 100644 --- a/cmd/boot-script-service/main.go +++ b/cmd/boot-script-service/main.go @@ -53,11 +53,14 @@ import ( "github.com/OpenCHAMI/bss/internal/postgres" ) -const kvDefaultRetryCount uint64 = 10 -const kvDefaultRetryWait uint64 = 5 -const sqlDefaultRetryCount uint64 = 10 -const sqlDefaultRetryWait uint64 = 5 -const authDefaultRetryCount uint64 = 10 +const ( + kvDefaultRetryCount uint64 = 10 + kvDefaultRetryWait uint64 = 5 + sqlDefaultRetryCount uint64 = 10 + sqlDefaultRetryWait uint64 = 5 + authDefaultRetryCount uint64 = 10 + authDefaultRetryWait uint64 = 5 +) var ( httpListen = ":27778" @@ -84,20 +87,23 @@ var ( // TODO: Set the default to a well known link local address when we have it. // This will also mean we change the virtual service into an Ingress with // this well known IP. - advertiseAddress = "" // i.e. http://{IP to reach this service} - insecure = false - debugFlag = false - kvstore hmetcd.Kvi - retryDelay = uint(30) - hsmRetrievalDelay = uint(10) - sqlRetryCount = sqlDefaultRetryCount - sqlRetryWait = sqlDefaultRetryWait - notifier *ScnNotifier - useSQL = false // Use ETCD by default - authRetryCount = authDefaultRetryCount - jwksURL = "" - sqlDbOpts = "" - spireServiceURL = "https://spire-tokens.spire:54440" + advertiseAddress = "" // i.e. http://{IP to reach this service} + insecure = false + debugFlag = false + kvstore hmetcd.Kvi + retryDelay = uint(30) + hsmRetrievalDelay = uint(10) + sqlRetryCount = sqlDefaultRetryCount + sqlRetryWait = sqlDefaultRetryWait + notifier *ScnNotifier + useSQL = false // Use ETCD by default + authRetryCount = authDefaultRetryCount + authRetryWait = authDefaultRetryWait + jwksURL = "" + sqlDbOpts = "" + spireServiceURL = "https://spire-tokens.spire:54440" + oauth2AdminBaseURL = "http://127.0.0.1:4445" + oauth2PublicBaseURL = "http://127.0.0.1:4444" ) func parseEnv(evar string, v interface{}) (ret error) { @@ -302,10 +308,22 @@ func parseEnvVars() error { if parseErr != nil { errList = append(errList, fmt.Errorf("BSS_AUTH_RETRY_COUNT: %q", parseErr)) } + parseErr = parseEnv("BSS_AUTH_RETRY_WAIT", &authRetryWait) + if parseErr != nil { + errList = append(errList, fmt.Errorf("BSS_AUTH_RETRY_WAIT: %q", parseErr)) + } parseErr = parseEnv("BSS_JWKS_URL", &jwksURL) if parseErr != nil { errList = append(errList, fmt.Errorf("BSS_JWKS_URL: %q", parseErr)) } + parseErr = parseEnv("BSS_OAUTH2_ADMIN_BASE_URL", &oauth2AdminBaseURL) + if parseErr != nil { + errList = append(errList, fmt.Errorf("BSS_OAUTH2_ADMIN_BASE_URL: %q", parseErr)) + } + parseErr = parseEnv("BSS_OAUTH2_PUBLIC_BASE_URL", &oauth2PublicBaseURL) + if parseErr != nil { + errList = append(errList, fmt.Errorf("BSS_OAUTH2_PUBLIC_BASE_URL: %q", parseErr)) + } // // Etcd environment variables @@ -401,6 +419,8 @@ func parseCmdLine() { flag.StringVar(&sqlUser, "postgres-username", sqlUser, "(BSS_DBUSER) Postgres username") flag.StringVar(&sqlPass, "postgres-password", sqlPass, "(BSS_DBPASS) Postgres password") flag.StringVar(&jwksURL, "jwks-url", jwksURL, "(BSS_JWKS_URL) Set the JWKS URL to fetch the public key for authorization (enables authentication)") + flag.StringVar(&oauth2AdminBaseURL, "oauth2-admin-base-url", oauth2AdminBaseURL, "(BSS_OAUTH2_ADMIN_BASE_URL) Base URL of the OAUTH2 server admin endpoints for client authorizations") + flag.StringVar(&oauth2PublicBaseURL, "oauth2-public-base-url", oauth2PublicBaseURL, "(BSS_OAUTH2_PUBLIC_BASE_URL) Base URL of the OAUTH2 server public endpoints (e.g. for token grants)") flag.BoolVar(&insecure, "insecure", insecure, "(BSS_INSECURE) Don't enforce https certificate security") flag.BoolVar(&debugFlag, "debug", debugFlag, "(BSS_DEBUG) Enable debug output") flag.BoolVar(&useSQL, "postgres", useSQL, "(BSS_USESQL) Use Postgres instead of ETCD") @@ -408,6 +428,7 @@ func parseCmdLine() { flag.UintVar(&hsmRetrievalDelay, "hsm-retrieval-delay", hsmRetrievalDelay, "(BSS_HSM_RETRIEVAL_DELAY) SM Retrieval delay in seconds") flag.UintVar(&sqlPort, "postgres-port", sqlPort, "(BSS_DBPORT) Postgres port") flag.Uint64Var(&authRetryCount, "auth-retry-count", authRetryCount, "(BSS_AUTH_RETRY_COUNT) Retry fetching JWKS public key set") + flag.Uint64Var(&authRetryWait, "auth-retry-wait", authRetryWait, "(BSS_AUTH_RETRY_WAIT) Interval in seconds between authentication request attempts") flag.Uint64Var(&sqlRetryCount, "postgres-retry-count", sqlRetryCount, "(BSS_SQL_RETRY_COUNT) Amount of times to retry connecting to Postgres") flag.Uint64Var(&sqlRetryWait, "postgres-retry-wait", sqlRetryCount, "(BSS_SQL_RETRY_WAIT) Interval in seconds between connection attempts to Postgres") flag.Parse() diff --git a/cmd/boot-script-service/oauth.go b/cmd/boot-script-service/oauth.go new file mode 100644 index 0000000..102fda5 --- /dev/null +++ b/cmd/boot-script-service/oauth.go @@ -0,0 +1,305 @@ +// Copyright © 2024 Triad National Security, LLC. All rights reserved. +// +// This program was produced under U.S. Government contract 89233218CNA000001 +// for Los Alamos National Laboratory (LANL), which is operated by Triad +// National Security, LLC for the U.S. Department of Energy/National Nuclear +// Security Administration. All rights in the program are reserved by Triad +// National Security, LLC, and the U.S. Department of Energy/National Nuclear +// Security Administration. The Government is granted for itself and others +// acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +// in this material to reproduce, prepare derivative works, distribute copies to +// the public, perform publicly and display publicly, and to permit others to do +// so. + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "time" + + "github.com/lestrrat-go/jwx/jwt" +) + +var accessToken = "" + +type OAuthClient struct { + http.Client + Id string + Secret string + RegistrationAccessToken string + RedirectUris []string +} + +// This is to implement jwt.Clock and provide the Now() function. An empty +// instance of this struct will be passed to the jwt.WithClock() function so it +// knows how to verify the timestamps. +type nowClock struct { + jwt.Clock +} + +// This function returns whatever "now" is for jwt.Clock. We simply return +// time.Now(). It would be nice if we could just pass time.Now() to the +// jwt.WithClock function, but it forces us to have something that implements +// the jwt.Clock interface to do it. +func (nc nowClock) Now() time.Time { + return time.Now() +} + +func (client *OAuthClient) CreateOAuthClient(registerUrl string) ([]byte, error) { + // hydra endpoint: POST /clients + data := []byte(`{ + "client_name": "bss", + "token_endpoint_auth_method": "client_secret_post", + "scope": "openid email profile read", + "grant_types": ["client_credentials"], + "response_types": ["token"], + "redirect_uris": ["http://hydra:5555/callback"], + "state": "12345678910" + }`) + + req, err := http.NewRequest(http.MethodPost, registerUrl, bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + req.Header.Add("Content-Type", "application/json") + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + // fmt.Printf("%v\n", string(b)) + var rjson map[string]any + err = json.Unmarshal(b, &rjson) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %v", err) + } + // set the client ID and secret of registered client + client.Id = rjson["client_id"].(string) + client.Secret = rjson["client_secret"].(string) + client.RegistrationAccessToken = rjson["registration_access_token"].(string) + return b, nil +} + +func (client *OAuthClient) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) { + // encode ID and secret for authorization header basic authentication + // basicAuth := base64.StdEncoding.EncodeToString( + // []byte(fmt.Sprintf("%s:%s", + // url.QueryEscape(client.Id), + // url.QueryEscape(client.Secret), + // )), + // ) + body := []byte("grant_type=client_credentials&scope=read&client_id=" + client.Id + + "&client_secret=" + client.Secret + + "&redirect_uri=" + url.QueryEscape("http://hydra:5555/callback") + + "&response_type=token" + + "&state=12345678910", + ) + headers := map[string][]string{ + "Authorization": {"Bearer " + client.RegistrationAccessToken}, + "Content-Type": {"application/x-www-form-urlencoded"}, + } + + req, err := http.NewRequest(http.MethodPost, authorizeUrl, bytes.NewBuffer(body)) + req.Header = headers + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + return io.ReadAll(res.Body) +} + +func (client *OAuthClient) PerformTokenGrant(remoteUrl string) (string, error) { + // hydra endpoint: /oauth/token + body := "grant_type=" + url.QueryEscape("client_credentials") + + "&client_id=" + client.Id + + "&client_secret=" + client.Secret + + "&scope=read" + headers := map[string][]string{ + "Content-Type": {"application/x-www-form-urlencoded"}, + "Authorization": {"Bearer " + client.RegistrationAccessToken}, + } + req, err := http.NewRequest(http.MethodPost, remoteUrl, bytes.NewBuffer([]byte(body))) + req.Header = headers + if err != nil { + return "", fmt.Errorf("failed to make request: %s", err) + } + res, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %v", err) + } + + var rjson map[string]any + err = json.Unmarshal(b, &rjson) + if err != nil { + return "", fmt.Errorf("failed to unmarshal response body: %v", err) + } + + return rjson["access_token"].(string), nil +} + +func QuoteArrayStrings(arr []string) []string { + for i, v := range arr { + arr[i] = "\"" + v + "\"" + } + return arr +} + +// RequestClientCreds performs the requests to the OAuth2 server to obtain an +// access token for this client (BSS). +// +// 1. Register as OAuth2 client. +// 2. Authorize OAuth2 client that was created. +// 3. Obtain access token if OAuth2 client is authorized. +// +// Returns the OAuthClient struct containing the client ID, secret, etc. as well +// as the access token and an error if one occurred. +func (client *OAuthClient) RequestClientCreds() (accessToken string, err error) { + var ( + url string + resp []byte + ) + + url = oauth2AdminBaseURL + "/admin/clients" + log.Printf("Attempting to register OAuth2 client") + debugf("Sending request to %s", url) + resp, err = client.CreateOAuthClient(url) + if err != nil { + err = fmt.Errorf("Failed to register OAuth2 client: %v", err) + debugf("Response: %v", string(resp)) + return + } + log.Printf("Successfully registered OAuth2 client") + debugf("Client ID: %s", client.Id) + + url = oauth2AdminBaseURL + "/oauth2/auth" + log.Printf("Attempting to authorize OAuth2 client") + debugf("Sending request to %s", url) + _, err = client.AuthorizeOAuthClient(url) + if err != nil { + err = fmt.Errorf("Failed to authorize OAuth2 client: %v", err) + debugf("Response: %v", string(resp)) + return + } + log.Printf("Successfully authorized OAuth2 client") + + url = oauth2PublicBaseURL + "/oauth2/token" + log.Printf("Attempting to fetch token from authorization server") + debugf("Sending request to %s", url) + accessToken, err = client.PerformTokenGrant(url) + if err != nil { + err = fmt.Errorf("Failed to fetch token from authorization server: %v", err) + return + } + log.Printf("Successfully fetched token") + + return +} + +// PollClientCreds tries retryCount times every retryInterval seconds to request +// client credentials and an access token (JWT) from the OAuth2 server. If +// attempts are exhausted or an invalid retryInterval is passed, an error is +// returned. If a JWT was successfully obtained, nil is returned. +func (client *OAuthClient) PollClientCreds(retryCount, retryInterval uint64) error { + retryDuration, err := time.ParseDuration(fmt.Sprintf("%ds", retryInterval)) + if err != nil { + return fmt.Errorf("Invalid retry interval: %v", err) + } + for i := uint64(0); i < retryCount; i++ { + log.Printf("Attempting to obtain access token (attempt %d/%d)", i+1, retryCount) + token, err := client.RequestClientCreds() + if err != nil { + log.Printf("Failed to obtain client credentials and token: %v", err) + time.Sleep(retryDuration) + continue + } + log.Printf("Successfully obtained client credentials and token with %d attempts", i+1) + accessToken = token + return nil + } + log.Printf("Exhausted attempts to obtain client credentials and token") + return fmt.Errorf("Exhausted %d attempts at obtaining client credentials and token", retryCount) +} + +// JWTTestAndRefresh tests the current JWT. If either a parsing error occurs +// with it or the JWT is invalid, it attempts to fetch a new one. If all of this +// succeeds, nil is returned. Otherwise, an error is returned. +func (client *OAuthClient) JWTTestAndRefresh() (err error) { + var ( + jwtIsValid bool + reason error + ) + + log.Printf("Validating JWT") + if accessToken != "" { + jwtIsValid, reason, err = JWTIsValid(accessToken) + if err != nil { + log.Printf("Unable to parse JWT, attempting to fetch a new one") + } else if !jwtIsValid { + log.Printf("JWT invalid, reason: %v", reason) + log.Printf("Attempting to fetch a new one") + } else { + log.Printf("JWT is valid") + return nil + } + } else { + log.Printf("No JWT detected, fetching a new one") + } + + err = client.PollClientCreds(authRetryCount, authRetryWait) + if err != nil { + log.Printf("Polling for OAuth2 client credentials failed") + return fmt.Errorf("Failed to get access token: %v", err) + } + log.Printf("Successfully fetched new JWT") + return nil +} + +// JWTIsValid takes a string representing a JWT and validates that it is not +// expired. If the JWT is invalid (timestamp(s) is/are out of range), jwtValid +// is set to false, reason is set to the reason why the JWT is not valid, and +// err is nil. If the JWT is valid (timestamps are all in range), jwtValid is +// set to true, reason is nil, and err is nil. +func JWTIsValid(jwtStr string) (jwtValid bool, reason, err error) { + var token jwt.Token + token, err = jwt.Parse([]byte(jwtStr)) + if err != nil { + err = fmt.Errorf("failed to parse JWT string: %v", err) + return + } + + // Right now, we only validate the issued at, expiry, and not before + // fields. + // TODO: Add full validation. + reason = jwt.Validate(token, jwt.WithClock(nowClock{})) + debugf("JWT valid between %v and %v", token.NotBefore(), token.Expiration()) + debugf("Current time: %v", time.Now()) + if reason == nil { + jwtValid = true + } else { + jwtValid = false + } + + return +} diff --git a/cmd/boot-script-service/sm.go b/cmd/boot-script-service/sm.go index 47de9c9..c2a67a3 100644 --- a/cmd/boot-script-service/sm.go +++ b/cmd/boot-script-service/sm.go @@ -49,8 +49,11 @@ import ( "github.com/OpenCHAMI/smd/v2/pkg/sm" ) -const badMAC = "not available" -const undefinedMAC = "ff:ff:ff:ff:ff:ff" +const ( + badMAC = "not available" + undefinedMAC = "ff:ff:ff:ff:ff:ff" + hsmTestEP = "/Inventory/RedfishEndpoints" +) type SMComponent struct { base.Component @@ -67,7 +70,7 @@ type SMData struct { var ( smMutex sync.Mutex smData *SMData - smClient *http.Client + smClient *OAuthClient smDataMap map[string]SMComponent smBaseURL string smJSONFile string @@ -82,6 +85,85 @@ func makeSmMap(state *SMData) map[string]SMComponent { return m } +func TestSMAuthEnabled(retryCount, retryInterval uint64) (authEnabled bool, err error) { + var ( + testURL string + resp *http.Response + ) + + if smClient == nil { + err = fmt.Errorf("smClient nil. Has a connection been opened yet?") + return + } + + // If this endpoint is protected (querying it returns a 401), + // auth is enabled. + testURL, err = url.JoinPath(smBaseURL, hsmTestEP) + if err != nil { + err = fmt.Errorf("Could not join URL paths %q and %q: %v", smBaseURL, hsmTestEP, err) + return + } + + retryDuration, err := time.ParseDuration(fmt.Sprintf("%ds", retryInterval)) + if err != nil { + err = fmt.Errorf("Invalid retry interval: %v", err) + return + } + for retry := uint64(0); retry < retryCount; retry++ { + log.Printf("Attempting connection to %s (attempt %d/%d)", testURL, retry+1, retryCount) + resp, err = smClient.Get(testURL) + if err != nil { + err = fmt.Errorf("Could not GET %q: %v", testURL, err) + time.Sleep(retryDuration) + continue + } + log.Printf("Connected to %s on attempt %d", testURL, retry+1) + if resp.StatusCode == 401 { + authEnabled = true + } else { + authEnabled = false + } + + return + } + + err = fmt.Errorf("Number of retries (%d) exhausted when testing if SMD auth is enabled", retryCount) + return +} + +func TestSMProtectedAccess() error { + var ( + req *http.Request + res *http.Response + ) + + if accessToken == "" { + return fmt.Errorf("Access token is empty") + } + if smClient == nil { + return fmt.Errorf("smClient nil. Has a connection been opened yet?") + } + + testURL, err := url.JoinPath(smBaseURL, hsmTestEP) + if err != nil { + err = fmt.Errorf("Could not join URL paths %q and %q: %v", smBaseURL, hsmTestEP, err) + return err + } + + req, err = http.NewRequest(http.MethodGet, testURL, nil) + headers := map[string][]string{ + "Authorization": {"Bearer " + accessToken}, + } + req.Header = headers + res, err = smClient.Do(req) + if err != nil { + return fmt.Errorf("Could not execute request: %v", err) + } + defer res.Body.Close() + + return nil +} + func SmOpen(base, options string) error { u, err := url.Parse(base) if err != nil { @@ -125,7 +207,7 @@ func SmOpen(base, options string) error { } } // Using the Datastore service - smClient = new(http.Client) + smClient = new(OAuthClient) if https && insecure { tcfg := new(tls.Config) tcfg.InsecureSkipVerify = true @@ -136,6 +218,19 @@ func SmOpen(base, options string) error { } smBaseURL = base + "/hsm/v2" log.Printf("Accessing state manager via %s\n", smBaseURL) + + var smAuthEnabled bool + smAuthEnabled, err = TestSMAuthEnabled(authRetryCount, authRetryWait) + if err != nil { + return fmt.Errorf("Failed testing if HSM auth is enabled: %v", err) + } + if smAuthEnabled { + log.Printf("HSM authenticated endpoints enabled, checking token") + err = smClient.JWTTestAndRefresh() + if err != nil { + return fmt.Errorf("Failed refreshing JWT: %v", err) + } + } return nil } @@ -183,6 +278,23 @@ func ensureLegalMAC(mac string) string { func getStateFromHSM() *SMData { if smClient != nil { + var headers map[string][]string + var body []byte + authEnabled, err := TestSMAuthEnabled(authRetryCount, authRetryWait) + if err != nil { + log.Printf("Failed to test if SM auth is enabled: %v", err) + return nil + } + if authEnabled { + err = smClient.JWTTestAndRefresh() + if err != nil { + log.Printf("Failed to refresh JWT: %v", err) + return nil + } + headers = map[string][]string{ + "Authorization": {"Bearer " + accessToken}, + } + } log.Printf("Retrieving state info from %s", smBaseURL) url := smBaseURL + "/State/Components?type=Node" debugf("url: %s, smClient: %v\n", url, smClient) @@ -191,16 +303,29 @@ func getStateFromHSM() *SMData { log.Printf("Failed to create HTTP request for '%s': %v", url, rerr) return nil } + if authEnabled { + req.Header = headers + } req.Close = true base.SetHTTPUserAgent(req, serviceName) r, err := smClient.Do(req) + debugf("getStateFromHSM(): GET %s -> r: %v, err: %v\n", url, r, err) if err != nil { log.Printf("Sm State request %s failed: %v", url, err) return nil } - debugf("getStateFromHSM(): GET %s -> r: %v, err: %v\n", url, r, err) var comps SMData - err = json.NewDecoder(r.Body).Decode(&comps) + body, err = ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("Failed to read response body: %v", err) + return nil + } + debugf("Response: %v", string(body)) + err = json.Unmarshal(body, &comps) + if err != nil { + log.Printf("Failed to unmarshal response body: %v", err) + return nil + } r.Body.Close() // Set up an indexing map to speed up lookup of components in the list compsIndex := make(map[string]int, len(comps.Components)) @@ -210,22 +335,35 @@ func getStateFromHSM() *SMData { url = smBaseURL + "/Inventory/ComponentEndpoints?type=Node" req, rerr = http.NewRequest(http.MethodGet, url, nil) - if err != nil { + if rerr != nil { log.Printf("Failed to create HTTP request for '%s': %v", url, rerr) return nil } + if authEnabled { + req.Header = headers + } req.Close = true base.SetHTTPUserAgent(req, serviceName) r, err = smClient.Do(req) + debugf("getStateFromHSM(): GET %s -> r: %v, err: %v\n", url, r, err) if err != nil { log.Printf("Sm Inventory request %s failed: %v", url, err) return nil } debugf("getStateFromHSM(): GET %s -> r: %v, err: %v\n", url, r, err) var ep sm.ComponentEndpointArray - ce, err := ioutil.ReadAll(r.Body) + var ce []byte + ce, err = ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("Failed to read response body: %v", err) + return nil + } + debugf("Response: %v", string(ce)) err = json.Unmarshal(ce, &ep) - debugf("getStateFromHSM(): GET %s -> r: %v, err: %v\n", url, r, err) + if err != nil { + log.Printf("Failed to unmarshal response body: %v", err) + return nil + } r.Body.Close() type myCompEndpt struct { @@ -277,10 +415,13 @@ func getStateFromHSM() *SMData { //ip address url = smBaseURL + "/Inventory/EthernetInterfaces?type=Node" req, rerr = http.NewRequest(http.MethodGet, url, nil) - if err != nil { + if rerr != nil { log.Printf("Failed to create HTTP request for '%s': %v", url, rerr) return nil } + if authEnabled { + req.Header = headers + } req.Close = true base.SetHTTPUserAgent(req, serviceName) r, err = smClient.Do(req) @@ -288,12 +429,19 @@ func getStateFromHSM() *SMData { log.Printf("Sm Inventory request %s failed: %v", url, err) return nil } + ce, err = ioutil.ReadAll(r.Body) debugf("getStateFromHSM(): GET %s -> r: %v, err: %v\n", url, r, err) - + if err != nil { + log.Printf("Failed to read response body: %v", err) + return nil + } + debugf("Response: %v", string(ce)) var ethIfaces []sm.CompEthInterfaceV2 - - ce, err = ioutil.ReadAll(r.Body) err = json.Unmarshal(ce, ðIfaces) + if err != nil { + log.Printf("Failed to unmarshal response body: %v", err) + return nil + } r.Body.Close() addresses := make(map[string]sm.CompEthInterfaceV2)