You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
5.2 KiB
145 lines
5.2 KiB
|
2 months ago
|
// tera-serial-check performs a read-only end-to-end check of tera-seriald.
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"tera-clone/services/tera-api/internal/serial"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
defaultHardwareProfile = "fet3568j-c"
|
||
|
|
defaultMappingVersion = "fet3568j-c-20260721-v1"
|
||
|
|
)
|
||
|
|
|
||
|
|
type finding struct {
|
||
|
|
Channel string `json:"channel,omitempty"`
|
||
|
|
Check string `json:"check"`
|
||
|
|
Expected string `json:"expected,omitempty"`
|
||
|
|
Actual string `json:"actual,omitempty"`
|
||
|
|
Message string `json:"message,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type checkReport struct {
|
||
|
|
Kind string `json:"kind"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
CheckedAt string `json:"checked_at"`
|
||
|
|
HardwareProfile string `json:"hardware_profile"`
|
||
|
|
MappingVersion string `json:"mapping_version"`
|
||
|
|
ExpectedChannels int `json:"expected_channels"`
|
||
|
|
ObservedChannels int `json:"observed_channels"`
|
||
|
|
AvailableChannels int `json:"available_channels"`
|
||
|
|
Findings []finding `json:"findings,omitempty"`
|
||
|
|
States []serial.State `json:"states,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, time.Now))
|
||
|
|
}
|
||
|
|
|
||
|
|
func run(args []string, stdout, stderr io.Writer, now func() time.Time) int {
|
||
|
|
flags := flag.NewFlagSet("tera-serial-check", flag.ContinueOnError)
|
||
|
|
flags.SetOutput(stderr)
|
||
|
|
socketPath := flags.String("socket", serial.DefaultSocketPath, "tera-seriald Unix socket")
|
||
|
|
hardwareProfile := flags.String("hardware-profile", defaultHardwareProfile, "expected hardware profile")
|
||
|
|
mappingVersion := flags.String("mapping-version", defaultMappingVersion, "expected mapping version")
|
||
|
|
timeout := flags.Duration("timeout", 10*time.Second, "overall read-only check timeout")
|
||
|
|
if err := flags.Parse(args); err != nil {
|
||
|
|
return 2
|
||
|
|
}
|
||
|
|
if flags.NArg() != 0 || !filepath.IsAbs(*socketPath) || strings.TrimSpace(*hardwareProfile) == "" || strings.TrimSpace(*mappingVersion) == "" || *timeout <= 0 {
|
||
|
|
fmt.Fprintln(stderr, "invalid arguments: require an absolute socket path, profile, mapping version, and positive timeout")
|
||
|
|
return 2
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||
|
|
defer cancel()
|
||
|
|
report := inspect(ctx, serial.NewSocketClient(*socketPath), *hardwareProfile, *mappingVersion, now().UTC())
|
||
|
|
encoder := json.NewEncoder(stdout)
|
||
|
|
encoder.SetIndent("", " ")
|
||
|
|
if err := encoder.Encode(report); err != nil {
|
||
|
|
fmt.Fprintln(stderr, "write serial check report:", err)
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
if report.Status != "pass" {
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func inspect(ctx context.Context, controller serial.Controller, expectedProfile, expectedVersion string, checkedAt time.Time) checkReport {
|
||
|
|
report := checkReport{
|
||
|
|
Kind: "tera-serial-readonly-check",
|
||
|
|
Status: "fail",
|
||
|
|
CheckedAt: checkedAt.UTC().Format(time.RFC3339Nano),
|
||
|
|
HardwareProfile: expectedProfile,
|
||
|
|
MappingVersion: expectedVersion,
|
||
|
|
ExpectedChannels: serial.ChannelCount,
|
||
|
|
}
|
||
|
|
|
||
|
|
states, err := controller.States(ctx)
|
||
|
|
if err != nil {
|
||
|
|
report.Findings = append(report.Findings, finding{Check: "seriald_states", Message: err.Error()})
|
||
|
|
return report
|
||
|
|
}
|
||
|
|
report.States = states
|
||
|
|
report.ObservedChannels = len(states)
|
||
|
|
if len(states) != serial.ChannelCount {
|
||
|
|
report.Findings = append(report.Findings, finding{
|
||
|
|
Check: "channel_count", Expected: fmt.Sprint(serial.ChannelCount), Actual: fmt.Sprint(len(states)),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
seenChannels := make(map[string]struct{}, len(states))
|
||
|
|
seenDevices := make(map[string]struct{}, len(states))
|
||
|
|
for index, state := range states {
|
||
|
|
channelNumber := index + 1
|
||
|
|
expectedChannel := fmt.Sprintf("CH%02d", channelNumber)
|
||
|
|
ttyNumber := channelNumber + 3
|
||
|
|
expectedBDF := "0000:01:00.0"
|
||
|
|
if channelNumber > 24 {
|
||
|
|
ttyNumber = channelNumber + 7
|
||
|
|
expectedBDF = "0002:21:00.0"
|
||
|
|
}
|
||
|
|
expectedDevice := fmt.Sprintf("/dev/ttyWCH%d", ttyNumber)
|
||
|
|
|
||
|
|
if _, duplicate := seenChannels[state.Channel]; duplicate {
|
||
|
|
report.Findings = append(report.Findings, finding{Channel: state.Channel, Check: "unique_channel", Message: "duplicate channel"})
|
||
|
|
}
|
||
|
|
seenChannels[state.Channel] = struct{}{}
|
||
|
|
if _, duplicate := seenDevices[state.Device]; duplicate {
|
||
|
|
report.Findings = append(report.Findings, finding{Channel: state.Channel, Check: "unique_device", Actual: state.Device, Message: "duplicate device"})
|
||
|
|
}
|
||
|
|
seenDevices[state.Device] = struct{}{}
|
||
|
|
|
||
|
|
checkEqual := func(check, expected, actual string) {
|
||
|
|
if actual != expected {
|
||
|
|
report.Findings = append(report.Findings, finding{Channel: expectedChannel, Check: check, Expected: expected, Actual: actual})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
checkEqual("channel_order", expectedChannel, state.Channel)
|
||
|
|
checkEqual("device_mapping", expectedDevice, state.Device)
|
||
|
|
checkEqual("pci_ancestry", expectedBDF, state.PCIBDF)
|
||
|
|
checkEqual("hardware_profile", expectedProfile, state.HardwareProfile)
|
||
|
|
checkEqual("mapping_version", expectedVersion, state.MappingVersion)
|
||
|
|
if state.Available {
|
||
|
|
report.AvailableChannels++
|
||
|
|
} else {
|
||
|
|
report.Findings = append(report.Findings, finding{Channel: expectedChannel, Check: "availability", Expected: "available", Actual: "unavailable", Message: state.Message})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(report.Findings) == 0 && report.AvailableChannels == serial.ChannelCount {
|
||
|
|
report.Status = "pass"
|
||
|
|
}
|
||
|
|
return report
|
||
|
|
}
|