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

// 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
}