magellan/pkg/sessions/sessions.go

286 lines
8.4 KiB
Go

package sessions
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"slices"
"github.com/davidallendj/magellan/internal/util"
"github.com/davidallendj/magellan/pkg/bmc"
"github.com/davidallendj/magellan/pkg/client"
"github.com/davidallendj/magellan/pkg/secrets"
"github.com/stmcginnis/gofish"
"github.com/stmcginnis/gofish/redfish"
)
type Params struct {
Host string
Username string
Password string
SessionID string
SessionToken string
}
type ServiceStatus struct {
Enabled bool
Timeout int
}
// Login() makes a single POST to the /redfish/v1/SessionService/Session endpoint to obtain a session token that can used for subsequent calls to the BMC. Using the SessionService reduces the overhead associated with logging into the BMC.
//
// Returns session ID, session token (X-AUTH-TOKEN), and/or error.
func Login(host string, store secrets.SecretStore) (*redfish.Session, string, error) {
var (
c client.DefaultClient
body client.HTTPBody
res *http.Response
encoded string
err error
)
creds, err := loadBMCCreds(host, store)
if err != nil {
return nil, "", fmt.Errorf("failed to load BMC credentials: %v", err)
}
encoded = base64.StdEncoding.EncodeToString(fmt.Appendf([]byte{}, "%s:%s", creds.Username, creds.Password))
res, body, err = client.MakeRequest(c.Client,
host+"/redfish/v1/SessionService/Sessions",
http.MethodPost,
client.HTTPBody(fmt.Sprintf("{\"UserName\": \"%s\", \"Password\": \"%s\"}", creds.Username, creds.Password)),
client.HTTPHeader{
"Authorization": fmt.Sprintf("Basic %s", encoded),
"Content-Type": "application/json",
})
if err != nil {
return nil, "", fmt.Errorf("failed to make request to session service: %v", err)
}
if res.StatusCode != http.StatusCreated {
return nil, "", fmt.Errorf("response returned status code %d", res.StatusCode)
}
// extract the session endpoint from body if JSON
obj, err := util.FromJSON(body)
if err != nil {
return nil, "", fmt.Errorf("failed to unmarshal JSON from session login response: %v", err)
}
// make a request to endpoint to get session data
endpoint, ok := obj["message"]
session := &redfish.Session{}
if ok {
switch endpoint.(type) {
case string:
_, body, err := client.MakeRequest(c.Client, host+endpoint.(string), http.MethodGet, nil, nil)
if err != nil {
return nil, "", fmt.Errorf("failed to make request for session token for host '%s': %v", host, err)
}
err = json.Unmarshal(body, &session)
if err != nil {
return nil, "", fmt.Errorf("failed to unmarshal response from session request for host '%s': %v", host, err)
}
}
} else {
return nil, "", fmt.Errorf("failed to extract session token from response JSON")
}
// extract the X-AUTH-TOKEN from the response header
return session, res.Header.Get("X-Auth-Token"), nil
}
func Logout(host string, sessionID string, store secrets.SecretStore, insecure bool) error {
creds, err := loadBMCCreds(host, store)
if err != nil {
return fmt.Errorf("failed to load BMC credentials: %v", err)
}
// initialize gofish client
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: host,
Insecure: insecure,
Username: creds.Username,
Password: creds.Password,
BasicAuth: true,
})
if err != nil {
return fmt.Errorf("failed to connect to BMC: %v", err)
}
defer client.Logout()
res, err := client.Delete("/redfish/v1/SessionService/Sessions/" + sessionID)
if err != nil {
return fmt.Errorf("failed to make request to delete session: %v", err)
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("response to delete session returned code %d", res.StatusCode)
}
return nil
}
func LogoutWithToken(host string, sessionID string, sessionToken string, insecure bool) error {
// initialize gofish client
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: host,
Insecure: insecure,
BasicAuth: false,
Session: &gofish.Session{
ID: sessionID,
Token: sessionToken,
},
})
if err != nil {
return fmt.Errorf("failed to connect to BMC: %v", err)
}
defer client.Logout()
client.Delete(host + "/redfish/v1/SessionService/Sessions/" + sessionID)
return nil
}
func GetServiceStatus(host string, store secrets.SecretStore, insecure bool) (*ServiceStatus, error) {
creds, err := loadBMCCreds(host, store)
if err != nil {
return nil, fmt.Errorf("failed to load BMC credentials: %v", err)
}
// initialize gofish client
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: host,
Username: creds.Username,
Password: creds.Password,
Insecure: insecure,
BasicAuth: true,
})
if err != nil {
return nil, fmt.Errorf("failed to connect to BMC: %v", err)
}
defer client.Logout()
sessionService, err := client.GetService().SessionService()
if err != nil {
return nil, fmt.Errorf("failed to get session service: %v", err)
}
return &ServiceStatus{
Enabled: sessionService.ServiceEnabled,
Timeout: sessionService.SessionTimeout,
}, nil
}
func GetSessionIDs(host string, store secrets.SecretStore, insecure bool) ([]string, error) {
sessions, err := getSessions(host, store, insecure)
if err != nil {
return nil, fmt.Errorf("failed to get the active session IDs from the session service: %v", err)
}
sessionIDs := []string{}
for _, session := range sessions {
sessionIDs = append(sessionIDs, session.ID)
}
return sessionIDs, nil
}
func GetSession(id string, host string, store secrets.SecretStore, insecure bool) (*redfish.Session, error) {
// get all of the sessions from the BMC node
sessions, err := getSessions(host, store, insecure)
if err != nil {
return nil, fmt.Errorf("failed to get the active session IDs from the session service: %v", err)
}
// now, only return the one that we want using the session ID
index := slices.IndexFunc(sessions, func(item *redfish.Session) bool {
return id == item.ID
})
if index >= 0 {
return sessions[index], nil
}
// we didn't find anything so return error
return nil, fmt.Errorf("could not find session")
}
func getSessions(host string, store secrets.SecretStore, insecure bool) ([]*redfish.Session, error) {
creds, err := loadBMCCreds(host, store)
if err != nil {
return nil, fmt.Errorf("failed to load BMC credentials: %v", err)
}
// initialize gofish client
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: host,
Username: creds.Username,
Password: creds.Password,
Insecure: insecure,
BasicAuth: true,
})
if err != nil {
return nil, fmt.Errorf("failed to connect to BMC: %v", err)
}
defer client.Logout()
service, err := client.Service.SessionService()
if err != nil {
return nil, fmt.Errorf("failed to get the session service: %v", err)
}
return service.Sessions()
}
// SessionLogin() makes a single POST to the /redfish/v1/SessionService/Session using the `gofish` library which can be used for subsequent requests to the BMC.
//
// NOTE: This implementation makes multiple requests to the BMC to obtain the need session token whereas the other implementation makes a single request to the required endpoint.
// func SessionLogin(config crawler.CrawlerConfig) (string, error) {
// // get username and password from secret store
// bmc_creds, err := crawler.LoadBMCCreds(config)
// if err != nil {
// event := log.Error()
// event.Err(err)
// event.Msg("failed to load BMC credentials")
// return "", err
// }
// // initialize gofish client
// client, err := gofish.Connect(gofish.ClientConfig{
// Endpoint: config.URI,
// Username: bmc_creds.Username,
// Password: bmc_creds.Password,
// Insecure: config.Insecure,
// BasicAuth: true,
// })
// if err != nil {
// return "", fmt.Errorf("failed to connect to BMC: %v")
// }
// service, err := client.Service.SessionService()
// if err != nil {
// return "", fmt.Errorf("failed to get the session service: %v", err)
// }
// res, err := service.PostWithResponse(uri string, map[string]any{
// "UserName": bmc_creds.Username,
// "Password": bmc_creds.Password,
// })
// // extract the session token from headers
// }
func loadBMCCreds(host string, store secrets.SecretStore) (bmc.BMCCredentials, error) {
var (
creds bmc.BMCCredentials
secretValue string
err error
)
// get creds from secret store
secretValue, err = store.GetSecretByID(host)
if err != nil {
return creds, fmt.Errorf("failed to get secret by ID: %v", err)
}
err = json.Unmarshal([]byte(secretValue), &creds)
if err != nil {
return creds, fmt.Errorf("failed to unmarshal credentials: %v", err)
}
return creds, nil
}