blob: 813cccc20fa965d682d0012af555df0a313041d1 [file] [log] [blame]
// Package cswitch ...
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The Thunderbolt Quality Center (TQC) API is which exposes a REST API for automating Thunderbolt test cases.
// Package cswitch provides utilities to interact with cswitch to perform hotplug-unplug and device enumeration.
// The CSwitch contains 4 connectors. Only 1 connector at any given time is enabled. An additional
// virtual connector, referred to as '0', is used in order to simulate disconnection.
// CSwitch using FTDI controller.
package cswitch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
// Cswitch request URLs.
const (
// URL request for C-switch command execution.
commandsURL = "http://%s/api/v1/commands"
// URL request to create session.
sessionURL = "http://%s/api/v1/sessions"
// URL request to close session.
closeSessionURL = "http://%s/api/v1/sessions/%s"
// URL request to get command status.
commandStatusURL = "http://%s/api/v1/commands/%s"
)
// performHTTPRequest formats request with accept, content and application headers return http response.
func performHTTPRequest(req *http.Request) (*http.Response, error) {
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-type", "application/json")
var client http.Client
// Request for creating a session.
return client.Do(req)
}
// CreateSession Creates session id for controlling cswitch operations and on success it will return session id.
// If it fails it will return the respective error.
// domain holds the local host IP with port number ex: domain = "localhost:9000"
func CreateSession(ctx context.Context, domain string) (string, error) {
testing.ContextLog(ctx, "Creating new session")
sessionURL := fmt.Sprintf(sessionURL, domain)
var jsonStr = []byte(`{}`)
// Requesting to create new session id
req, err := http.NewRequest(http.MethodPost, sessionURL, bytes.NewBuffer(jsonStr))
if err != nil {
return "", errors.Wrapf(err, "failed to create new request for session url : %s", sessionURL)
}
resp, err := performHTTPRequest(req)
if err != nil {
return "", errors.Wrap(err, "failed to send post request for creating a session")
}
defer resp.Body.Close()
// Reading response data for session id.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.New("failed to read response data for session id")
}
if http.StatusOK != resp.StatusCode {
return "", errors.Errorf("invalid response code received: %v", resp.StatusCode)
}
// Mapping response body to map data type.
responseMap := map[string]string{}
if err := json.Unmarshal(body, &responseMap); err != nil {
return "", err
}
return responseMap["session-id"], nil
}
// CloseSession closes the created session and on success it returns nil.
// If it fails it will return the respective error.
// domain holds the local host ip with port number ex: domain = "localhost:9000"
func CloseSession(ctx context.Context, sessionID, domain string) error {
testing.ContextLogf(ctx, "Closing Session: %s", sessionID)
closesessionURL := fmt.Sprintf(closeSessionURL, domain, sessionID)
// Creating the request for closing session
req, err := http.NewRequest(http.MethodDelete, closesessionURL, nil)
if err != nil {
return errors.Wrap(err, "failed to create request for closing session")
}
resp, err := performHTTPRequest(req)
if err != nil {
return errors.Wrap(err, "failed to send post request for creating a session")
}
defer resp.Body.Close()
if http.StatusOK != resp.StatusCode {
return errors.Errorf("invalid response code received: %v", resp.StatusCode)
}
return nil
}
// execCommand Performs POST request on the given URL.
// If it fails, will return an error message .
// session_id holds session id created by CreateSession function.
// cmd_line holds command need to executed.
// domain holds the local host ip with port number ex: domain = "localhost:9000".
func execCommand(ctx context.Context, sessionID, cmdLine, domain string) error {
var cswitchResp map[string]interface{}
jsonCmd := map[string]string{}
jsonCmd["session-id"] = sessionID
jsonCmd["command-line"] = fmt.Sprintf("cswitch %s", cmdLine)
runCommandURL := fmt.Sprintf(commandsURL, domain)
postJSON, err := json.Marshal(jsonCmd)
if err != nil {
return errors.Wrap(err, "unable to process JSON data ")
}
// Create new http post request.
req, err := http.NewRequest(http.MethodPost, runCommandURL, bytes.NewBuffer(postJSON))
if err != nil {
return errors.Wrap(err, "failed to create request")
}
resp, err := performHTTPRequest(req)
if err != nil {
return errors.Wrap(err, "failed to send post request for creating a session")
}
defer resp.Body.Close()
// Read the response body.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "failed to read response data")
}
// Verify the response code to check request status.
if http.StatusOK != resp.StatusCode {
return errors.Errorf("invalid response code received: %v", resp.StatusCode)
}
if err := json.Unmarshal(body, &cswitchResp); err != nil {
return err
}
// Verifies results-status "Failures","Running" and returns error.
switch cswitchResp["result-status"].(string) {
case "Failure":
return errors.Errorf("%s", cswitchResp["messages"].([]interface{})[0].(map[string]interface{})["message"])
case "Running":
if err := waitForCommandComplete(ctx, cswitchResp["command-id"].(string), domain); err != nil {
return errors.Wrap(err, "failed to execute command with in time")
}
}
return nil
}
// commandStatus performs GET request on the given URL.
// commandID holds command need to executed.
// domain holds the local host ip with port number ex: domain = "localhost:9000".
func commandStatus(ctx context.Context, commandID, domain string) (string, error) {
testing.ContextLogf(ctx, "Checking command %s status", commandID)
responseMap := map[string]string{}
commandStateURL := fmt.Sprintf(commandStatusURL, domain, commandID)
// Creates new GET request for given URL.
req, err := http.NewRequest(http.MethodGet, commandStateURL, nil)
if err != nil {
return "", errors.Wrap(err, "failed to create request")
}
resp, err := performHTTPRequest(req)
if err != nil {
return "", errors.Wrap(err, "failed to send post request for creating a session")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "failed to read response data")
}
testing.ContextLog(ctx, "Response body : ", string(body))
// verifies return response code and returns err if failed.
if http.StatusOK != resp.StatusCode {
return "", errors.Errorf("invalid response code received: %v", resp.StatusCode)
}
if err := json.Unmarshal(body, &responseMap); err != nil {
return "", err
}
testing.ContextLog(ctx, "response body : ", string(body))
return responseMap["result-status"], nil
}
// waitForCommandComplete waits till command execution completion.
// commandID holds command need to executed.
// domain holds the local host ip with port number ex: domain = "localhost:9000".
func waitForCommandComplete(ctx context.Context, commandID, domain string) error {
resultStatus := "Running"
// Waits till timeout and returns err if failed to process request with in timeout.
if err := testing.Poll(ctx, func(ctx context.Context) error {
status, err := commandStatus(ctx, commandID, domain)
if err != nil {
return testing.PollBreak(err)
}
if status == "Success" {
return nil
}
return errors.New("still running")
}, &testing.PollOptions{Timeout: 100 * time.Second, Interval: 10 * time.Second}); err != nil {
return errors.Wrapf(err, "command status is %s", resultStatus)
}
return nil
}
// ToggleCSwitchPort enable/disable cswitch port with given parameters session id, toggle, domain ip.
// sessionID holds session id created by CreateSession function.
// domain holds the local host ip with port number ex: domain = "localhost:9000".
func ToggleCSwitchPort(ctx context.Context, sessionID, toggle, domain string) error {
// Executes enCSwitch command to enable/disable port on cswitch.
return execCommand(ctx, sessionID, toggle, domain)
}
// TxSpeed returns Tx speed of the connected cable.
// port holds the TBT port id in DUT.
func TxSpeed(ctx context.Context, port string) (string, error) {
txSpeedCmd := fmt.Sprintf("cat /sys/bus/thunderbolt/devices/%s/tx_speed", port)
// Executes txSpeedCmd and returns err on command execution failure.
out, err := testexec.CommandContext(ctx, "bash", "-c", txSpeedCmd).Output(testexec.DumpLogOnError)
if err != nil {
return "", errors.Wrap(err, "txSpeedCmd execution failed")
}
return string(out), nil
}
// RxSpeed returns RX speed of the connected cable.
// port holds the TBT port id in DUT.
func RxSpeed(ctx context.Context, port string) (string, error) {
rxSpeedCmd := fmt.Sprintf("cat /sys/bus/thunderbolt/devices/%s/rx_speed", port)
// Executes rxSpeedCmd and returns err on command execution failure.
out, err := testexec.CommandContext(ctx, "bash", "-c", rxSpeedCmd).Output(testexec.DumpLogOnError)
if err != nil {
return "", errors.Wrap(err, "rxSpeedCmd execution failed")
}
return string(out), nil
}
// NvmVersion returns the NVM version of the TBT device connected to the DUT.
// port holds the TBT port id in DUT.
func NvmVersion(ctx context.Context, port string) (string, error) {
nvmeCmd := fmt.Sprintf("cat /sys/bus/thunderbolt/devices/%s/nvm_version", port)
// Executes nvmeCmd and returns err on command execution failure.
out, err := testexec.CommandContext(ctx, "bash", "-c", nvmeCmd).Output(testexec.DumpLogOnError)
if err != nil {
return "", errors.Wrap(err, "nvmeCmd execution failed")
}
return string(out), nil
}
// IsDeviceEnumerated validates device enumeration in DUT.
// device holds the device name of connected TBT device.
// port holds the TBT port id in DUT.
func IsDeviceEnumerated(ctx context.Context, device, port string) (bool, error) {
deviceCmd := fmt.Sprintf("cat /sys/bus/thunderbolt/devices/%s/device_name", port)
if err := testing.Poll(ctx, func(ctx context.Context) error {
out, err := testexec.CommandContext(ctx, "bash", "-c", deviceCmd).Output()
if err != nil {
return errors.Wrapf(err, "device_name command %s failed", deviceCmd)
}
if strings.TrimSpace(string(out)) != device {
return errors.New("Device enumeration failed")
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second, Interval: 3 * time.Second}); err != nil {
return false, err
}
return true, nil
}
// Generation returns the generation of the TBT device connected to the DUT.
// port holds the TBT port id in DUT.
func Generation(ctx context.Context, port string) (string, error) {
generationCmd := fmt.Sprintf("cat /sys/bus/thunderbolt/devices/%s/generation", port)
out, err := testexec.CommandContext(ctx, "bash", "-c", generationCmd).Output(testexec.DumpLogOnError)
if err != nil {
return "", errors.Wrap(err, "generationCmd execution failed")
}
return string(out), nil
}