Documentation
¶
Index ¶
- Constants
- Variables
- func BaselineImagePath(baseImagePath string) string
- func BootedImagePath(baseImagePath string) string
- func CreateICMPv4EchoPacket(srcIP, dstIP net.IP, id, seq uint16, payload []byte) []byte
- func CreateTCPIPv4Packet(srcIP, dstIP net.IP, payload []byte, opts *TCPPacketOpts) []byte
- func CreateTCPIPv6Packet(srcIP, dstIP net.IP, payload []byte, opts *TCPPacketOpts) []byte
- func HasBaselineSnapshot() bool
- func HasBootedSnapshot(overlayPath string) bool
- func IsDebugEnabled() bool
- func MarkBaselineSaved()
- func MustParseMAC(mac string) net.HardwareAddr
- func OverlayHasSnapshot(imagePath, name string) bool
- func PoolSize() int
- func ShouldKeepVMAlive() bool
- func SnapshotImagePath(baseImagePath string, snapshotName string) string
- type CLIManager
- func (c *CLIManager) ExecuteCommand(command string) (string, error)
- func (c *CLIManager) ExecuteCommandWithTimeout(command string, timeout time.Duration) (string, error)
- func (c *CLIManager) ExecuteCommands(commands ...string) ([]string, error)
- func (c *CLIManager) WithLog(log *zap.SugaredLogger) *CLIManager
- type CLIOption
- type Config
- type Framework
- type FrameworkOption
- type GuestPaths
- type PacketInfo
- type PacketParser
- type QEMUManager
- func (q *QEMUManager) GetStdin() io.WriteCloser
- func (q *QEMUManager) IsVMReady() bool
- func (q *QEMUManager) ReconnectSerial() error
- func (q *QEMUManager) RestoreBooted() error
- func (q *QEMUManager) RestoreSnapshot(name string) error
- func (q *QEMUManager) SaveBootedOverlay() (string, error)
- func (q *QEMUManager) SaveSnapshot(name string) error
- func (q *QEMUManager) SaveSnapshotOverlay(name string) (string, error)
- func (q *QEMUManager) SendMonitorCommand(cmd string) (string, error)
- func (q *QEMUManager) Start() (bool, error)
- func (q *QEMUManager) Stop() error
- func (q *QEMUManager) WaitForReady(timeout time.Duration) error
- type SocketClient
- func (sc *SocketClient) Close() error
- func (sc *SocketClient) Connect() error
- func (sc *SocketClient) GetSocketPort() int
- func (sc *SocketClient) ReceiveAllPackets(timeout time.Duration, dumpPath string) ([][]byte, error)
- func (sc *SocketClient) ReceivePacket(timeout time.Duration, dumpPath string) ([]byte, error)
- func (sc *SocketClient) ResetConnection() error
- func (sc *SocketClient) SendPacket(packet []byte, dumpPath string) error
- func (sc *SocketClient) SendPackets(packets [][]byte, dumpPath string) error
- func (sc *SocketClient) WithLog(log *zap.SugaredLogger) *SocketClient
- type SocketClientOption
- type TCPPacketOpts
- type TestFramework
- func (f *TestFramework) CommonConfigCommands() []string
- func (f *TestFramework) CreateConfigFile(name string, config string) error
- func (f *TestFramework) CreateForwardConfig(config string) error
- func (f *TestFramework) ExecuteCommand(command string) (string, error)
- func (f *TestFramework) ExecuteCommandWithTimeout(command string, timeout time.Duration) (string, error)
- func (f *TestFramework) ExecuteCommands(commands ...string) ([]string, error)
- func (f *TestFramework) ExportCurrentOverlay(dst string) error
- func (f *TestFramework) ForTest(t *testing.T) *TestFramework
- func (f *TestFramework) GetSocketClient(ifaceIndex int) (*SocketClient, error)
- func (f *TestFramework) GetSocketPaths() []string
- func (f *TestFramework) Mount9P() error
- func (f *TestFramework) PrepareLocalStorage() error
- func (f *TestFramework) ResetConnections()
- func (f *TestFramework) RestartYANET() error
- func (f *TestFramework) RestoreAndReconnect(snapshot string) error
- func (f *TestFramework) RestoreBooted() error
- func (f *TestFramework) RestoreClean(snapshot string) error
- func (f *TestFramework) Run(name string, fn func(fw *TestFramework, t *testing.T)) bool
- func (f *TestFramework) RunWith(snapshot string, name string, fn func(fw *TestFramework, t *testing.T)) bool
- func (f *TestFramework) SaveSnapshot(name string) error
- func (f *TestFramework) SaveSnapshotKeepUnmounted(name string) error
- func (f *TestFramework) SendPacketAndCapture(inputIfaceIndex int, outputIfaceIndex int, packet []byte, ...) ([]byte, error)
- func (f *TestFramework) SendPacketAndCaptureAll(inputIfaceIndex int, outputIfaceIndex int, packet []byte, ...) ([][]byte, error)
- func (f *TestFramework) SendPacketAndParse(inputIfaceIndex int, outputIfaceIndex int, packet []byte, ...) (*PacketInfo, *PacketInfo, error)
- func (f *TestFramework) SendPacketAndParseAll(inputIfaceIndex int, outputIfaceIndex int, packet []byte, ...) ([]*PacketInfo, error)
- func (f *TestFramework) SendPacketsAndParseAll(inputIfaceIndex int, outputIfaceIndex int, packets [][]byte, ...) ([]*PacketInfo, error)
- func (f *TestFramework) Start() (bool, error)
- func (f *TestFramework) StartLogTailers() error
- func (f *TestFramework) StartYANET(dataplaneConfig string, controlplaneConfig string) error
- func (f *TestFramework) Stop() error
- func (f *TestFramework) Unmount9P() error
- func (f *TestFramework) ValidateCounter(name string, expect uint64) error
- func (f *TestFramework) WaitForDatapathReady(timeout time.Duration) error
- func (f *TestFramework) WaitForReady(timeout time.Duration) error
- func (f *TestFramework) WaitOutputPresent(cmd string, checker func(string) bool, timeout time.Duration) error
- type VMPool
- func (p *VMPool) Acquire() *TestFramework
- func (p *VMPool) ForEachParallel(fn func(idx int, fw *TestFramework) error) error
- func (p *VMPool) Release(fw *TestFramework)
- func (p *VMPool) Shutdown() error
- func (p *VMPool) Size() int
- func (p *VMPool) StartAll() error
- func (p *VMPool) StopAllCPU()
- func (p *VMPool) WaitAllReady(timeout time.Duration) error
Constants ¶
const ( // MAC addresses used in test framework SrcMAC = "52:54:00:6b:ff:a1" DstMAC = "52:54:00:6b:ff:a5" // IP addresses used in test framework VMIPv4Host = "203.0.113.14" VMIPv4Gateway = "203.0.113.1" VMIPv6Gateway = "fe80::1" VMIPv6Host = "fe80::5054:ff:fe6b:ffa5" // CLI tool paths (9P defaults, used in single-VM mode) CLIBasePath = "/mnt/target/release" CLIRoute = CLIBasePath + "/yanet-cli-route" CLIRouteMPLS = CLIBasePath + "/yanet-cli-route-mpls" CLINAT64 = CLIBasePath + "/yanet-cli-nat64" CLIACL = CLIBasePath + "/yanet-cli-acl" CLIFWState = CLIBasePath + "/yanet-cli-fwstate" CLIPipeline = CLIBasePath + "/yanet-cli-pipeline" CLIFunction = CLIBasePath + "/yanet-cli-function" CLIDevicePlain = CLIBasePath + "/yanet-cli-device-plain" CLIDecap = CLIBasePath + "/yanet-cli-decap" CLIForward = CLIBasePath + "/yanet-cli-forward" CLIGeneric = CLIBasePath + "/yanet-cli" )
const BootedSnapshotName = "booted"
BootedSnapshotName is the name of the QEMU snapshot saved after a clean boot. When a pool VM's overlay contains this snapshot, Start() restores it instantly via -loadvm instead of waiting ~44s for boot.
Variables ¶
var CLIBinaryNames = []string{
"yanet-cli",
"yanet-cli-route", "yanet-cli-route-mpls",
"yanet-cli-nat64", "yanet-cli-acl",
"yanet-cli-fwstate", "yanet-cli-pipeline", "yanet-cli-function",
"yanet-cli-device-plain", "yanet-cli-device-vlan",
"yanet-cli-decap", "yanet-cli-forward",
"yanet-cli-common", "yanet-cli-dscp", "yanet-cli-counters",
"yanet-cli-pdump", "yanet-cli-inspect",
}
CLIBinaryNames is the canonical list of all CLI binaries installed in the YANET target/release directory. Used by StartYANET verification, PrepareLocalStorage, and test assertions to keep binary lists in sync.
var CommonConfigCommands = (&TestFramework{Paths: DefaultGuestPaths()}).CommonConfigCommands()
CommonConfigCommands is the package-level variable for backward compatibility. Uses default 9P paths. In pool mode tests use fw.CommonConfigCommands() instead.
Functions ¶
func BaselineImagePath ¶
BaselineImagePath returns the path to the cached baseline snapshot image placed next to the base image.
func BootedImagePath ¶
BootedImagePath returns the path to the booted snapshot image that should be placed next to the base image. For example, if the base image is "/path/to/yanet-test.qcow2", the booted image will be "/path/to/yanet-test-booted.qcow2".
func CreateICMPv4EchoPacket ¶
CreateICMPv4EchoPacket builds a serialized Ethernet/IPv4/ICMP Echo Request.
func CreateTCPIPv4Packet ¶
func CreateTCPIPv4Packet(srcIP, dstIP net.IP, payload []byte, opts *TCPPacketOpts) []byte
CreateTCPIPv4Packet builds a serialized Ethernet/IPv4/TCP packet. Pass nil for opts to use defaults (SrcMAC/DstMAC from framework constants, SrcPort 12345, DstPort 80, PSH+ACK flags).
func CreateTCPIPv6Packet ¶
func CreateTCPIPv6Packet(srcIP, dstIP net.IP, payload []byte, opts *TCPPacketOpts) []byte
CreateTCPIPv6Packet builds a serialized Ethernet/IPv6/TCP packet. Pass nil for opts to use defaults (same as CreateTCPIPv4Packet).
func HasBaselineSnapshot ¶
func HasBaselineSnapshot() bool
HasBaselineSnapshot returns true if the "baseline" VM snapshot is available for per-test state isolation via RunWith("baseline", ...).
func HasBootedSnapshot ¶
HasBootedSnapshot checks if the given overlay file contains a snapshot named "booted". Returns true if the snapshot exists.
func IsDebugEnabled ¶
func IsDebugEnabled() bool
IsDebugEnabled checks if debug mode is enabled for tests. Returns true if the YANET_TEST_DEBUG environment variable is set.
Example:
if IsDebugEnabled() {
log.Info("Debug mode enabled")
// Enable verbose logging
}
func MarkBaselineSaved ¶
func MarkBaselineSaved()
MarkBaselineSaved records that the "baseline" VM snapshot was successfully created. Called once from TestMain after setup completes.
func MustParseMAC ¶
func MustParseMAC(mac string) net.HardwareAddr
MustParseMAC parses a MAC address string and panics if parsing fails. This utility function is designed for use with known-good MAC address constants where parsing failure indicates a programming error rather than runtime input error.
Parameters:
- mac: MAC address string in standard format (e.g., "52:54:00:6b:ff:a1")
Returns:
- net.HardwareAddr: Parsed hardware address
Panics:
- If the MAC address string is malformed or invalid
Example:
hwAddr := MustParseMAC("52:54:00:6b:ff:a1")
func OverlayHasSnapshot ¶
OverlayHasSnapshot returns true if the given qcow2 image contains a snapshot with the given name.
func ShouldKeepVMAlive ¶
func ShouldKeepVMAlive() bool
ShouldKeepVMAlive checks if the VM should be kept running after tests. Returns true if the YANET_KEEP_VM_ALIVE environment variable is set. When enabled, the QEMU process will not be killed, allowing manual connection to the serial console for debugging.
Example:
if ShouldKeepVMAlive() {
log.Info("VM will remain running for manual debugging")
// Skip QEMU process termination
}
func SnapshotImagePath ¶
SnapshotImagePath returns the path to the cached image containing the named snapshot next to the base image.
Types ¶
type CLIManager ¶
type CLIManager struct {
// contains filtered or unexported fields
}
CLIManager handles YANET CLI operations within a QEMU virtual machine environment. It provides thread-safe command execution capabilities through the VM's serial console, with support for output buffering, command synchronization, and proper cleanup of control characters from terminal output.
The manager uses markers to reliably detect command completion and extract return codes, ensuring accurate command execution status reporting even in concurrent scenarios.
func NewCLIManager ¶
func NewCLIManager(qemu *QEMUManager, opts ...CLIOption) (*CLIManager, error)
NewCLIManager creates and initializes a new CLI manager instance for executing commands within a QEMU virtual machine. The manager provides thread-safe command execution with proper output handling and error reporting.
Parameters:
- qemu: A QEMUManager instance representing the target virtual machine
- opts: Optional functional options for customizing the CLI manager behavior
Returns:
- *CLIManager: A configured CLI manager instance
- error: An error if initialization fails or options cannot be applied
func (*CLIManager) ExecuteCommand ¶
func (c *CLIManager) ExecuteCommand(command string) (string, error)
ExecuteCommand executes a single CLI command within the QEMU virtual machine via the serial console interface. The method ensures thread-safe execution, proper output collection, and reliable command completion detection using unique markers.
The execution process includes:
- VM readiness verification
- Command wrapping with start/end markers for reliable parsing
- Output buffering and cleanup of control characters
- Return code extraction and error handling
Parameters:
- command: The shell command to execute in the virtual machine
Returns:
- string: The cleaned command output (stdout/stderr combined)
- error: An error if the VM is not ready, command fails, or timeout occurs
Example:
output, err := cli.ExecuteCommand("ls -la /etc")
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println("Directory listing:", output)
func (*CLIManager) ExecuteCommandWithTimeout ¶
func (c *CLIManager) ExecuteCommandWithTimeout(command string, timeout time.Duration) (string, error)
ExecuteCommandWithTimeout is like ExecuteCommand but with a custom timeout.
func (*CLIManager) ExecuteCommands ¶
func (c *CLIManager) ExecuteCommands(commands ...string) ([]string, error)
ExecuteCommands executes multiple CLI commands sequentially within the QEMU virtual machine. Each command is executed in order, and execution stops at the first command that returns an error.
This method is useful for executing a series of related commands where each command may depend on the success of previous ones, such as configuration setup or multi-step operations.
Parameters:
- commands: Variable number of shell commands to execute sequentially
Returns:
- []string: Slice of command outputs in execution order (may be partial if error occurs)
- error: An error from the first failed command, or nil if all commands succeed
Example:
outputs, err := cli.ExecuteCommands(
"mkdir -p /tmp/test",
"echo 'hello' > /tmp/test/file.txt",
"cat /tmp/test/file.txt",
)
if err != nil {
log.Fatalf("Command sequence failed: %v", err)
}
func (*CLIManager) WithLog ¶
func (c *CLIManager) WithLog(log *zap.SugaredLogger) *CLIManager
WithLog creates a new CLIManager instance with a different logger while sharing the same underlying connection (inner state). This allows each test to have its own logging context while sharing the CLI manager connection.
Parameters:
- log: Logger to use for this CLI manager instance
Returns:
- *CLIManager: A new CLI manager instance with the specified logger
Example:
namedCLI := cli.WithLog(logger.Named("test1"))
type CLIOption ¶
type CLIOption func(*CLIManager) error
CLIOption defines functional options for configuring CLIManager instances. This pattern allows for flexible initialization with optional parameters while maintaining backward compatibility.
func CLIWithLog ¶
func CLIWithLog(log *zap.SugaredLogger) CLIOption
CLIWithLog configures the CLIManager to use the specified logger for debugging and monitoring command execution. If not provided, a no-op logger is used by default.
Parameters:
- log: A zap.SugaredLogger instance for logging CLI operations
Returns:
- CLIOption: A functional option that sets the logger
type Config ¶
Config contains essential configuration parameters for initializing the test framework. It specifies the QEMU virtual machine image and working directory for test execution.
type Framework ¶
type Framework struct {
// contains filtered or unexported fields
}
Framework is a safe wrapper that prevents direct access to framework methods. It provides two ways to access the framework:
- Global() - returns TestFramework with testName="global" for global operations in TestMain
- ForTest(t) - returns TestFramework bound to *testing.T for test-specific operations
This design ensures that framework methods cannot be called without proper context.
Example usage:
var globalFramework *Framework
func TestMain(m *testing.M) {
fw, _ := New(config)
globalFramework = fw
globalFramework.Global().Start()
defer globalFramework.Global().Stop()
m.Run()
}
func TestMyFeature(t *testing.T) {
fw := globalFramework.ForTest(t)
fw.Run("SubTest", func(fw *TestFramework, t *testing.T) {
// Use fw for framework operations, t for assertions
})
}
func New ¶
func New(config *Config, opts ...FrameworkOption) (*Framework, error)
New creates and initializes a new Framework instance with the specified configuration and optional functional parameters. The framework sets up all necessary components including QEMU VM management, CLI operations, and packet processing capabilities.
The initialization process includes:
- Working directory creation and validation
- QEMU manager setup with the specified VM image
- CLI manager initialization for VM command execution
- Packet parser setup for network analysis
- Socket client cache initialization
Parameters:
- config: Required configuration containing VM image path and working directory
- opts: Optional functional options for customizing framework behavior
Returns:
- *Framework: Fully initialized framework wrapper
- error: An error if initialization fails or configuration is invalid
Example:
config := &Config{
Name: "main",
QEMUImage: "/path/to/vm-image.qcow2",
}
fw, err := New(config, WithLog(logger))
if err != nil {
log.Fatalf("Failed to create framework: %v", err)
}
func (*Framework) ForTest ¶
func (f *Framework) ForTest(t *testing.T) *TestFramework
ForTest creates a TestFramework instance bound to the provided *testing.T. This method should be called at the beginning of each test function to create a test-specific framework instance with automatic test name tracking.
Parameters:
- t: The *testing.T instance for the current test
Returns:
- *TestFramework: A test-bound framework instance
Example:
func TestMyFeature(t *testing.T) {
fw := globalFramework.ForTest(t)
fw.Run("SubTest", func(fw *TestFramework, t *testing.T) {
// Test code here
})
}
func (*Framework) Global ¶
func (f *Framework) Global() *TestFramework
Global returns the underlying TestFramework with testName="global" for global operations. This method should only be used in TestMain for framework lifecycle management (Start, Stop, StartYANET, etc.).
Returns:
- *TestFramework: The internal framework instance with testName="global"
Example:
func TestMain(m *testing.M) {
fw, _ := New(config)
globalFramework = fw
gfw := fw.Global()
gfw.Start()
defer gfw.Stop()
m.Run()
}
type FrameworkOption ¶
type FrameworkOption func(*TestFramework) error
FrameworkOption defines functional options for configuring TestFramework instances. This pattern enables flexible initialization with optional parameters while maintaining backward compatibility and clean API design.
func WithLog ¶
func WithLog(log *zap.SugaredLogger) FrameworkOption
WithLog configures the TestFramework to use the specified logger for debugging and monitoring test execution. This functional option allows detailed logging of framework operations, packet flows, and VM interactions.
Parameters:
- log: A zap.SugaredLogger instance for structured logging
Returns:
- FrameworkOption: A functional option that sets the logger
type GuestPaths ¶
type GuestPaths struct {
CLIBase string // directory with yanet-cli-* binaries
BuildDir string // directory with yanet-dataplane, yanet-controlplane
ConfigDir string // directory for config files
LogDir string // directory for log files
DPDKDevbindDir string // directory containing dpdk-devbind.py
ForwardYAML string // path to forward.yaml
LocalMode bool // true when paths are on guest tmpfs (pool/snapshot mode)
}
GuestPaths holds all guest-side filesystem paths used by the framework. In single-VM mode these point to 9P mounts. In pool/snapshot mode they point to /tmp/yanet/ (local tmpfs) so that no YANET process holds open fids on 9P mounts, allowing savevm/loadvm to succeed.
func DefaultGuestPaths ¶
func DefaultGuestPaths() GuestPaths
DefaultGuestPaths returns the standard 9P-backed paths used in single-VM mode.
func LocalGuestPaths ¶
func LocalGuestPaths() GuestPaths
LocalGuestPaths returns paths on local tmpfs inside the guest. Used in pool/snapshot mode after PrepareLocalStorage() copies files from 9P mounts to /tmp/yanet/.
func (GuestPaths) CLI ¶
func (p GuestPaths) CLI(name string) string
CLI returns the full path to a CLI binary by name.
type PacketInfo ¶
type PacketInfo struct {
// Ethernet layer addressing information
SrcMAC net.HardwareAddr // Source MAC address from Ethernet header
DstMAC net.HardwareAddr // Destination MAC address from Ethernet header
// IP layer information (supports both IPv4 and IPv6)
IsIPv4 bool // True if packet contains IPv4 header
IsIPv6 bool // True if packet contains IPv6 header
SrcIP net.IP // Source IP address (IPv4 or IPv6)
DstIP net.IP // Destination IP address (IPv4 or IPv6)
Protocol layers.IPProtocol // IPv4 protocol field
NextHeader layers.IPProtocol // IPv6 next header field
// Transport layer information
SrcPort uint16 // Source port number (TCP/UDP)
DstPort uint16 // Destination port number (TCP/UDP)
// Tunnel detection and analysis
IsTunneled bool // True if packet is tunneled/encapsulated
TunnelType string // Tunnel type: "ip4in4", "ip6in4", "ip4in6", "ip6in6", "gre"
InnerPacket *PacketInfo // Parsed information about the inner/encapsulated packet
// Raw packet data
RawData []byte // Complete raw packet bytes including all headers
Payload []byte // Application layer payload data
}
PacketInfo contains comprehensive parsed information about a network packet, including all protocol layers from Ethernet to application data. It supports complex scenarios such as tunneled packets, fragmentation, and various encapsulation protocols commonly used in network testing.
The structure provides detailed information about:
- Ethernet layer addressing (MAC addresses)
- IP layer information (IPv4/IPv6 addressing and protocols)
- Transport layer details (TCP/UDP port information)
- Tunnel detection and inner packet parsing
- Raw packet data and payload extraction
func (*PacketInfo) GetTransportProtocol ¶
func (info *PacketInfo) GetTransportProtocol() (layers.IPProtocol, bool)
GetTransportProtocol returns the transport layer protocol (TCP/UDP) from the packet. For tunneled packets, it returns the protocol from the inner packet. Returns the IPProtocol value and a boolean indicating if a valid transport protocol was found.
func (*PacketInfo) String ¶
func (info *PacketInfo) String() string
String returns a human-readable string representation of the packet information, including all relevant protocol details, addressing information, and tunnel status. This method is useful for debugging, logging, and test result presentation.
The string representation includes:
- Source and destination IP addresses
- IP version information (IPv4/IPv6) with protocol details
- Transport layer port information (if available)
- Tunnel information and inner packet details (if tunneled)
Returns:
- string: Formatted packet information suitable for display and logging
Example output:
"Packet: 192.168.1.1 -> 10.0.0.1 (IPv4, proto=4), tunnel=ip4in4, inner=172.16.1.1->172.16.1.2"
type PacketParser ¶
type PacketParser struct{}
PacketParser provides comprehensive packet parsing and verification functionality for network testing scenarios. It supports complex packet analysis including tunnel detection, protocol verification, and NAT64 translation validation.
The parser handles various networking protocols and encapsulation methods commonly encountered in YANET testing environments.
func NewPacketParser ¶
func NewPacketParser() *PacketParser
NewPacketParser creates a new packet parser instance ready for packet analysis. The parser is stateless and can be safely used concurrently across multiple goroutines for parallel packet processing.
Returns:
- *PacketParser: A new packet parser instance
func (*PacketParser) ParsePacket ¶
func (p *PacketParser) ParsePacket(data []byte) (*PacketInfo, error)
ParsePacket parses raw packet data and extracts comprehensive protocol information into a structured PacketInfo object. The method handles various networking protocols and automatically detects tunneled/encapsulated packets.
The parsing process includes:
- Ethernet frame validation and padding to minimum size
- Multi-layer protocol parsing (Ethernet, IP, Transport)
- Tunnel detection and inner packet analysis
- Payload extraction and error handling
Parameters:
- data: Raw packet bytes to parse (minimum 60 bytes after padding)
Returns:
- *PacketInfo: Structured packet information with all parsed layers
- error: An error if packet parsing fails or data is malformed
Example:
parser := NewPacketParser()
info, err := parser.ParsePacket(rawPacketData)
if err != nil {
log.Fatalf("Packet parsing failed: %v", err)
}
fmt.Printf("Parsed packet: %s", info.String())
func (*PacketParser) VerifyDecapsulation ¶
func (p *PacketParser) VerifyDecapsulation(originalPacket, processedPacket *PacketInfo) error
VerifyDecapsulation validates that a tunneled packet has been properly decapsulated by comparing the processed packet against the original inner packet. This verification method is essential for testing tunnel processing functionality.
The verification process includes:
- Confirming the original packet was tunneled
- Validating inner packet information exists
- Comparing IP version consistency (IPv4/IPv6)
- Verifying source and destination IP address preservation
- Ensuring tunnel headers have been properly removed
Parameters:
- originalPacket: The original tunneled packet before processing
- processedPacket: The packet after decapsulation processing
Returns:
- error: An error if decapsulation verification fails or packets don't match
Example:
err := parser.VerifyDecapsulation(tunneled, decapsulated)
if err != nil {
log.Fatalf("Decapsulation verification failed: %v", err)
}
func (*PacketParser) VerifyNAT64Translation ¶
func (p *PacketParser) VerifyNAT64Translation(originalPacket, translatedPacket *PacketInfo, nat64Prefix string) error
VerifyNAT64Translation validates NAT64 protocol translation between IPv4 and IPv6 packets, ensuring proper address mapping and prefix usage. This method is crucial for testing NAT64 functionality in dual-stack network environments.
The verification process includes:
- IPv4-to-IPv6 translation validation with prefix checking
- IPv6-to-IPv4 translation validation with embedded address extraction
- NAT64 prefix compliance verification (typically 64:ff9b::/96)
- Embedded IPv4 address consistency checking
Supported translation directions:
- IPv4 → IPv6: Verifies IPv6 address is in NAT64 prefix with embedded IPv4
- IPv6 → IPv4: Verifies original IPv6 was in prefix and extracts embedded IPv4
Parameters:
- originalPacket: The packet before NAT64 translation
- translatedPacket: The packet after NAT64 translation
- nat64Prefix: NAT64 prefix in CIDR notation (e.g., "64:ff9b::/96")
Returns:
- error: An error if NAT64 translation verification fails or addresses don't match
Example:
err := parser.VerifyNAT64Translation(ipv4Pkt, ipv6Pkt, "64:ff9b::/96")
if err != nil {
log.Fatalf("NAT64 translation verification failed: %v", err)
}
type QEMUManager ¶
type QEMUManager struct {
Name string
ImagePath string
WorkDir string
Command *exec.Cmd
LogsDir string
ConfigDir string
BuildDir string
TargetDir string
SerialPath string
MonitorPath string
SocketPaths []string
Ninepmounted atomic.Bool
// TemplateOverlay is an optional path to a qcow2 overlay that already
// contains a reusable VM snapshot. When set, Start() copies it instead
// of creating a blank overlay, then boots with -loadvm TemplateSnapshotName.
TemplateOverlay string
// TemplateSnapshotName is the snapshot name loaded from TemplateOverlay.
// When empty, Start() falls back to BootedSnapshotName.
TemplateSnapshotName string
// contains filtered or unexported fields
}
QEMUManager handles the complete lifecycle and operations of QEMU virtual machines for YANET functional testing. It provides comprehensive VM management including startup, networking configuration, filesystem sharing, and graceful shutdown.
The manager supports:
- QEMU VM lifecycle management with proper resource cleanup
- Unix socket-based networking for packet injection and capture
- 9P filesystem sharing for host-VM file exchange
- Serial console and monitor interface access
- VM readiness detection and synchronization
- Parallel test execution with unique instance isolation
All operations are thread-safe and support concurrent access patterns required for comprehensive network testing scenarios.
func NewQEMUManager ¶
func NewQEMUManager(name string, imagePath string, logger *zap.SugaredLogger) (*QEMUManager, error)
NewQEMUManager creates and initializes a new QEMU manager instance for virtual machine testing. The manager sets up all necessary directories, generates unique instance identifiers for parallel execution, and configures filesystem sharing paths for host-VM communication.
The initialization process includes:
- Unique instance ID generation for parallel test isolation
- Working directory creation in system temporary space
- Project root detection for build and target directory sharing
- Socket path configuration for VM networking and console access
Parameters:
- name: Name of this manager
- imagePath: Path to the QEMU disk image file (must exist and be accessible)
- logger: Structured logger for debugging and monitoring VM operations
Returns:
- *QEMUManager: Configured QEMU manager ready for VM startup
- error: An error if project root detection fails or paths are invalid
Example:
manager, err := NewQEMUManager("main", "/path/to/vm-image.qcow2", logger)
if err != nil {
log.Fatalf("Failed to create QEMU manager: %v", err)
}
func (*QEMUManager) GetStdin ¶
func (q *QEMUManager) GetStdin() io.WriteCloser
GetStdin returns the stdin pipe for the QEMU process
func (*QEMUManager) IsVMReady ¶
func (q *QEMUManager) IsVMReady() bool
IsVMReady returns the current readiness state of the virtual machine in a thread-safe manner. This method can be called from multiple goroutines without synchronization concerns.
VM readiness indicates that the virtual machine has completed its boot process and is ready to accept and execute commands through the serial console.
Returns:
- bool: True if the VM is ready for command execution, false otherwise
Example:
if manager.IsVMReady() {
// Safe to execute commands
output, err := cli.ExecuteCommand("ls -la")
}
func (*QEMUManager) ReconnectSerial ¶
func (q *QEMUManager) ReconnectSerial() error
ReconnectSerial closes the current serial connection and opens a new one. The caller is responsible for resetting readySignal and launching a new readSerial goroutine after this returns.
func (*QEMUManager) RestoreBooted ¶
func (q *QEMUManager) RestoreBooted() error
RestoreBooted restores the VM to the "booted" snapshot without going through the full framework-level RestoreAndReconnect. It handles the monitor loadvm, serial reconnect, and ready wait at the QEMUManager level.
Callers are responsible for unmounting 9P before calling and remounting after.
func (*QEMUManager) RestoreSnapshot ¶
func (q *QEMUManager) RestoreSnapshot(name string) error
RestoreSnapshot restores the VM to a previously saved snapshot. After restore, the serial console connection is broken (the guest-side state reverts) and must be re-established via ReconnectSerial.
The monitor connection itself survives because it is external to the guest state.
func (*QEMUManager) SaveBootedOverlay ¶
func (q *QEMUManager) SaveBootedOverlay() (string, error)
SaveBootedOverlay saves the "booted" snapshot to the VM's current overlay and returns the overlay file path. The caller can copy this path to a cache location and use it as TemplateOverlay for other VMs.
func (*QEMUManager) SaveSnapshot ¶
func (q *QEMUManager) SaveSnapshot(name string) error
SaveSnapshot creates a named QEMU VM snapshot via the monitor. The snapshot captures the full VM state (CPU, RAM, devices) and can be restored later with RestoreSnapshot. This is the key primitive for per-test state isolation.
func (*QEMUManager) SaveSnapshotOverlay ¶
func (q *QEMUManager) SaveSnapshotOverlay(name string) (string, error)
SaveSnapshotOverlay saves the named snapshot to the VM's current overlay and returns the overlay file path. The caller can copy this path to a cache location and use it as TemplateOverlay for other VMs.
The 9P shares must be unmounted before calling (savevm blocks when VirtFS mounts are active).
func (*QEMUManager) SendMonitorCommand ¶
func (q *QEMUManager) SendMonitorCommand(cmd string) (string, error)
SendMonitorCommand sends a command to the QEMU monitor (HMP) and returns the response. The monitor socket must already be connected via Start(). Commands are terminated with a newline; the method waits for the next "(qemu) " prompt to determine when the response is complete.
func (*QEMUManager) Start ¶
func (q *QEMUManager) Start() (bool, error)
Start launches a QEMU virtual machine. When TemplateOverlay is set the overlay is copied from it and QEMU starts with -loadvm for the requested TemplateSnapshotName, skipping the slow cold boot path.
Returns (true, nil) when booted from snapshot, (false, nil) when doing a full cold boot.
func (*QEMUManager) Stop ¶
func (q *QEMUManager) Stop() error
Stop performs graceful termination of the QEMU virtual machine and comprehensive cleanup of all associated resources. This method ensures proper resource deallocation and prevents resource leaks in testing environments.
The cleanup process includes:
- Graceful closure of monitor and serial console connections
- QEMU process termination with proper signal handling
- Working directory and temporary file cleanup
- Unix socket file removal from filesystem
- Error collection and reporting for failed cleanup operations
Multiple cleanup errors are collected and returned as a combined error to provide comprehensive information about any cleanup failures.
Returns:
- error: A combined error if any cleanup operations fail, or nil if successful
Example:
if err := manager.Stop(); err != nil {
log.Errorf("VM cleanup encountered errors: %v", err)
}
func (*QEMUManager) WaitForReady ¶
func (q *QEMUManager) WaitForReady(timeout time.Duration) error
WaitForReady blocks until the virtual machine becomes ready for command execution or the specified timeout expires. This method provides synchronous waiting for VM readiness with proper timeout handling.
The method first checks if the VM is already ready to avoid unnecessary waiting. If not ready, it waits for the readiness signal from the background monitoring goroutine.
Parameters:
- timeout: Maximum time to wait for VM readiness
Returns:
- error: An error if the timeout expires before VM becomes ready, or nil if ready
Example:
if err := manager.WaitForReady(60 * time.Second); err != nil {
log.Fatalf("VM failed to become ready: %v", err)
}
type SocketClient ¶
type SocketClient struct {
// contains filtered or unexported fields
}
SocketClient handles bidirectional communication with QEMU virtual machine network interfaces through socket connections. It supports both TCP and Unix socket connections for packet injection and capture in testing scenarios.
The client provides:
- Reliable packet transmission with length prefixing
- Packet reception with filtering and timeout handling
- Connection management with automatic retry logic
- MAC address-based packet filtering for test isolation
- Configurable timeouts and logging for debugging
All network operations include proper error handling and timeout management to ensure robust testing in various network conditions.
func NewSocketClient ¶
func NewSocketClient(socketPath string, opts ...SocketClientOption) (*SocketClient, error)
NewSocketClient creates a new socket client configured for Unix socket connections to QEMU network interfaces. This method provides the primary interface for packet injection and capture through Unix domain sockets.
Unix socket connections offer better performance and lower latency compared to TCP sockets, making them ideal for high-throughput network testing scenarios.
Parameters:
- socketPath: Path to the Unix socket file created by QEMU
- opts: Optional functional options for client customization
Returns:
- *SocketClient: Configured socket client ready for Unix socket connections
- error: An error if the socket path is empty or options cannot be applied
Example:
client, err := NewSocketClient("/tmp/qemu-socket.sock", SocketClientWithLog(logger))
if err != nil {
log.Fatalf("Failed to create Unix socket client: %v", err)
}
func NewSocketClientTCP ¶
func NewSocketClientTCP(port int, opts ...SocketClientOption) (*SocketClient, error)
NewSocketClientTCP creates a new socket client configured for TCP connections to QEMU network interfaces. This method is used when QEMU is configured with TCP socket networking for packet injection and capture.
The client is initialized with default settings and can be customized using functional options for timeout configuration and logging setup.
Parameters:
- port: TCP port number for QEMU socket connection (1-65535)
- opts: Optional functional options for client customization
Returns:
- *SocketClient: Configured socket client ready for TCP connections
- error: An error if the port number is invalid or options cannot be applied
Example:
client, err := NewSocketClientTCP(8080, WithTimeout(10*time.Second))
if err != nil {
log.Fatalf("Failed to create TCP socket client: %v", err)
}
func (*SocketClient) Close ¶
func (sc *SocketClient) Close() error
Close gracefully terminates the socket connection and releases associated network resources. This method should be called when the socket client is no longer needed to prevent resource leaks.
The method safely handles cases where no connection is established and provides proper error reporting for connection closure failures.
Returns:
- error: An error if connection closure fails, or nil if successful or no connection exists
Example:
defer func() {
if err := client.Close(); err != nil {
log.Errorf("Failed to close socket connection: %v", err)
}
}()
func (*SocketClient) Connect ¶
func (sc *SocketClient) Connect() error
Connect establishes a network connection to the QEMU socket interface with automatic retry logic and proper error handling. The method supports both TCP and Unix socket connections based on the client configuration.
The connection process includes:
- Automatic detection of connection type (TCP vs Unix socket)
- Retry logic with exponential backoff for handling timing issues
- Proper error reporting and logging for debugging
- Connection state management to prevent duplicate connections
The method attempts connection up to 10 times with 2-second intervals to accommodate QEMU startup timing and network interface availability.
Returns:
- error: An error if connection cannot be established after all retry attempts
Example:
if err := client.Connect(); err != nil {
log.Fatalf("Failed to connect to QEMU socket: %v", err)
}
func (*SocketClient) GetSocketPort ¶
func (sc *SocketClient) GetSocketPort() int
GetSocketPort returns the TCP port number configured for this socket client. This method is useful for debugging and logging purposes, particularly when working with multiple socket clients on different ports.
Returns:
- int: TCP port number, or 0 if the client is configured for Unix sockets
Example:
port := client.GetSocketPort()
if port > 0 {
log.Infof("Client configured for TCP port %d", port)
} else {
log.Info("Client configured for Unix socket")
}
func (*SocketClient) ReceiveAllPackets ¶
ReceiveAllPackets receives all packets available on the socket within the timeout.
func (*SocketClient) ReceivePacket ¶
ReceivePacket captures a network packet from the QEMU socket connection with MAC address filtering and timeout handling. The method implements the QEMU socket protocol by reading length-prefixed packets and filtering based on destination MAC address for test isolation.
The reception process includes:
- Connection state validation and timeout configuration
- Length prefix parsing from network byte order
- Packet size validation to prevent buffer overflows
- Complete packet data reception
- MAC address-based filtering for test packet isolation
- Automatic packet parsing and validation
- Optional dumping to file if dumpFilePaths is provided
The method continuously reads packets until it finds one with the correct destination MAC address matching the test framework's source MAC, ensuring proper packet isolation in multi-test scenarios.
Parameters:
- timeout: Maximum time to wait for packet reception
- dumpPath: Path to dump file for recording socket data (empty string to skip dumping)
Returns:
- []byte: Raw packet data with correct destination MAC address
- error: An error if connection fails, timeout occurs, or no matching packet is received
Example:
packet, err := client.ReceivePacket(5 * time.Second, "/path/to/dump.file")
if err != nil {
log.Fatalf("Packet reception failed: %v", err)
}
fmt.Printf("Received packet: %x", packet)
func (*SocketClient) ResetConnection ¶
func (sc *SocketClient) ResetConnection() error
ResetConnection closes the existing connection and creates a new one. This ensures a clean stream with no buffered data from previous operations. This method is used for test isolation to prevent packet leakage between tests.
The method:
- Closes any existing connection (discarding buffered data)
- Creates a new connection to the same socket
- Returns an error if reconnection fails
Returns:
- error: An error if reconnection fails, or nil if successful
Example:
if err := client.ResetConnection(); err != nil {
log.Fatalf("Failed to reset connection: %v", err)
}
func (*SocketClient) SendPacket ¶
func (sc *SocketClient) SendPacket(packet []byte, dumpPath string) error
SendPacket transmits a raw network packet through the QEMU socket connection with proper length prefixing and timeout handling. The method implements the QEMU socket protocol by prefixing each packet with its length in network byte order.
The transmission process includes:
- Connection state validation
- Write timeout configuration for reliable transmission
- Length prefix encoding in big-endian format
- Complete packet data transmission
- Comprehensive error handling and logging
- Optional dumping to file if dumpFilePaths is provided
The packet format follows QEMU socket networking protocol:
[4-byte length][packet data]
Parameters:
- packet: Raw packet bytes to transmit through the socket
- dumpPath: Path to dump file for recording socket data (empty string to skip dumping)
Returns:
- error: An error if the connection is not established, timeout occurs, or transmission fails
Example:
packetData := []byte{0x52, 0x54, 0x00, 0x6b, 0xff, 0xa1, ...} // Ethernet frame
if err := client.SendPacket(packetData, "/path/to/dump.file"); err != nil {
log.Fatalf("Packet transmission failed: %v", err)
}
func (*SocketClient) SendPackets ¶
func (sc *SocketClient) SendPackets(packets [][]byte, dumpPath string) error
func (*SocketClient) WithLog ¶
func (sc *SocketClient) WithLog(log *zap.SugaredLogger) *SocketClient
WithLog creates a new SocketClient instance with a different logger while sharing the same underlying connection (inner state). This allows each test to have its own logging context while sharing the socket connection.
Parameters:
- log: Logger to use for this client instance
Returns:
- *SocketClient: A new socket client instance with the specified logger
Example:
namedClient := client.WithLog(logger.Named("test1"))
type SocketClientOption ¶
type SocketClientOption func(*SocketClient) error
SocketClientOption defines functional options for configuring SocketClient instances. This pattern allows flexible initialization with optional parameters while maintaining clean API design and backward compatibility.
func WithTimeout ¶
func WithTimeout(timeout time.Duration) SocketClientOption
WithTimeout configures the default timeout for socket read and write operations. This timeout applies to both packet transmission and reception operations, providing consistent behavior across all network interactions.
Parameters:
- timeout: Duration for socket operation timeouts (must be positive)
Returns:
- SocketClientOption: Functional option that sets the timeout
- error: An error if the timeout value is invalid (zero or negative)
Example:
client, err := NewSocketClient(path, WithTimeout(5*time.Second))
type TCPPacketOpts ¶
type TCPPacketOpts struct {
SrcMAC string // default: framework.SrcMAC
DstMAC string // default: framework.DstMAC
SrcPort uint16 // default: 12345
DstPort uint16 // default: 80
SYN bool // default: false
ACK bool // default: true
PSH bool // default: true
Seq uint32 // default: 1
Ack uint32 // default: 1
Window uint16 // default: 1024
}
TCPPacketOpts configures optional fields for CreateTCPIPv4Packet and CreateTCPIPv6Packet. Zero values use sensible defaults matching the most common test pattern.
type TestFramework ¶
type TestFramework struct {
PacketParser *PacketParser
Paths GuestPaths // Guest-side filesystem paths (9P or local tmpfs)
// contains filtered or unexported fields
}
F represents the main test framework structure for YANET functional testing. It orchestrates QEMU virtual machine management, CLI command execution, packet processing, and network socket communication to provide a comprehensive testing environment.
The framework manages:
- QEMU virtual machine lifecycle and networking
- CLI command execution within the VM
- Packet parsing and analysis capabilities
- Socket-based network communication with VM interfaces
- Test context via optional *testing.T field
All operations are thread-safe through internal synchronization mechanisms. This type has private fields and cannot be constructed directly - use Global() and ForTest(t).
func (*TestFramework) CommonConfigCommands ¶
func (f *TestFramework) CommonConfigCommands() []string
CommonConfigCommands returns the shell commands that configure the baseline YANET state (kni0, forwarding, route FIB, pipelines, devices). Paths are resolved from f.Paths so the commands work with both 9P and local tmpfs layouts.
func (*TestFramework) CreateConfigFile ¶
func (f *TestFramework) CreateConfigFile(name string, config string) error
func (*TestFramework) CreateForwardConfig ¶
func (f *TestFramework) CreateForwardConfig(config string) error
CreateForwardConfig writes forward.yaml to the path referenced by f.Paths.ForwardYAML. In 9P mode this writes to the host filesystem; in local mode it writes via serial console to the guest tmpfs.
func (*TestFramework) ExecuteCommand ¶
func (f *TestFramework) ExecuteCommand(command string) (string, error)
ExecuteCommand executes a single CLI command within the QEMU virtual machine via the serial console interface. This is a proxy method that delegates to the underlying CLI manager.
For test-specific frameworks (created via ForTest), commands are logged with the test's unique log ID for easy debugging.
Parameters:
- command: The shell command to execute in the virtual machine
Returns:
- string: The cleaned command output (stdout/stderr combined)
- error: An error if the VM is not ready, command fails, or timeout occurs
Example:
fw := globalFramework.ForTest(t)
fw.Run("ExecuteCommand", func(fw *TestFramework, t *testing.T) {
output, err := fw.ExecuteCommand("ls -la /etc")
require.NoError(t, err)
t.Logf("Output: %s", output)
})
func (*TestFramework) ExecuteCommandWithTimeout ¶
func (f *TestFramework) ExecuteCommandWithTimeout(command string, timeout time.Duration) (string, error)
ExecuteCommandWithTimeout executes a single CLI command with a custom timeout. Use this for operations that may take longer than the default 30s, such as copying large binaries on slow emulated VMs.
func (*TestFramework) ExecuteCommands ¶
func (f *TestFramework) ExecuteCommands(commands ...string) ([]string, error)
ExecuteCommands executes multiple CLI commands sequentially within the QEMU virtual machine. This is a proxy method that delegates to the underlying CLI manager.
Each command is executed in order, and execution stops at the first command that returns an error.
Parameters:
- commands: Variable number of shell commands to execute sequentially
Returns:
- []string: Slice of command outputs in execution order (may be partial if error occurs)
- error: An error from the first failed command, or nil if all commands succeed
Example:
fw := globalFramework.ForTest(t)
fw.Run("ExecuteCommands", func(fw *TestFramework, t *testing.T) {
outputs, err := fw.ExecuteCommands(
"mkdir -p /tmp/test",
"echo 'hello' > /tmp/test/file.txt",
"cat /tmp/test/file.txt",
)
require.NoError(t, err)
t.Logf("Outputs: %v", outputs)
})
func (*TestFramework) ExportCurrentOverlay ¶
func (f *TestFramework) ExportCurrentOverlay(dst string) error
ExportCurrentOverlay copies the VM's current qcow2 overlay to dst. This is used to cache prepared template overlays (for example a prebuilt baseline) and start future pool VMs from the same snapshot source.
func (*TestFramework) ForTest ¶
func (f *TestFramework) ForTest(t *testing.T) *TestFramework
ForTest binds an already constructed framework instance to a specific test. This is used by pooled VMs, where tests acquire a shared base framework from VMPool and then need a test-scoped copy with the proper logger and *testing.T.
func (*TestFramework) GetSocketClient ¶
func (f *TestFramework) GetSocketClient(ifaceIndex int) (*SocketClient, error)
GetSocketClient retrieves or creates a socket client for the specified network interface. The method implements caching to reuse existing connections and ensures thread-safe access to the socket client pool.
For test-specific frameworks (created via ForTest), the socket client is returned with a named logger using the test's log ID.
The method handles:
- Interface index validation against available QEMU socket paths
- Thread-safe access to the socket client cache
- Automatic socket client creation for new interfaces
- Client caching for performance optimization
- Logger customization for test-specific contexts
Parameters:
- ifaceIndex: Zero-based index of the network interface (must be < len(SocketPaths))
Returns:
- *SocketClient: Socket client for the specified interface
- error: An error if the interface index is invalid or client creation fails
Example:
client, err := fw.GetSocketClient(0)
if err != nil {
log.Fatalf("Failed to get socket client: %v", err)
}
defer client.Close()
func (*TestFramework) GetSocketPaths ¶
func (f *TestFramework) GetSocketPaths() []string
func (*TestFramework) Mount9P ¶
func (f *TestFramework) Mount9P() error
Mount9P remounts all 9P shares inside the guest VM. Uses mount -a for speed, then verifies each mount point is present.
func (*TestFramework) PrepareLocalStorage ¶
func (f *TestFramework) PrepareLocalStorage() error
PrepareLocalStorage copies all YANET binaries, CLI tools, configs and scripts from 9P mounts to /tmp/yanet/ inside the guest. After this call, YANET can be started from local tmpfs paths so that no process holds open fids on 9P mounts, making savevm/loadvm work cleanly.
This also switches f.Paths to LocalGuestPaths().
func (*TestFramework) ResetConnections ¶
func (f *TestFramework) ResetConnections()
ResetConnections closes and reconnects all socket clients. This ensures a clean state after a snapshot restore, preventing stale connections from causing false heartbeat failures. Unlike draining, this approach guarantees a completely clean stream by discarding any buffered data with the old connection.
Must be called after loadvm and before any packet operations (WaitForDatapathReady, SendPacketAndCapture, etc.) because loadvm restores QEMU's internal stream-netdev state but the host-side UNIX socket connections are stale — Connect() short-circuits on the dead net.Conn and never reconnects.
Errors during reset are logged but do not cause the operation to fail.
func (*TestFramework) RestartYANET ¶
func (f *TestFramework) RestartYANET() error
func (*TestFramework) RestoreAndReconnect ¶
func (f *TestFramework) RestoreAndReconnect(snapshot string) error
RestoreAndReconnect reverts the VM to a previously saved snapshot and re-establishes the serial console and socket connections that break when the guest state is rolled back.
Call sequence after restore:
- Unmount 9P shares
- loadvm via QEMU monitor (monitor socket survives)
- Reconnect serial console
- Wait for shell prompts
- Mount 9P shares
- Reset socket connections (close stale connections, reconnect)
- Wait for dataplane ready (ICMP heartbeat)
Socket connections MUST be reset before the heartbeat because loadvm restores QEMU's internal stream-netdev state but the host-side UNIX socket connections are stale. Without reset, Connect() short-circuits on the dead net.Conn and heartbeat fails silently.
func (*TestFramework) RestoreBooted ¶
func (f *TestFramework) RestoreBooted() error
RestoreBooted restores the VM to the "booted" snapshot and re-establishes all connections. This is the primary per-test isolation primitive.
Unlike RestoreAndReconnect("baseline"), RestoreBooted does not depend on a baseline snapshot being created at test startup. It only requires that the VM pool was started from a pre-prepared booted template overlay.
func (*TestFramework) RestoreClean ¶
func (f *TestFramework) RestoreClean(snapshot string) error
RestoreClean reverts the VM to a previously saved snapshot and re-establishes serial console and 9P mounts WITHOUT running the dataplane heartbeat check. Use this for snapshots where YANET is not yet running (e.g. "preyanet") -- StartYANET will be called separately after restore.
func (*TestFramework) Run ¶
func (f *TestFramework) Run(name string, fn func(fw *TestFramework, t *testing.T)) bool
Run executes a subtest with the given name and function. This method wraps t.Run() and automatically creates a new TestFramework instance with the correct test name. This ensures that all framework operations within the subtest are properly tracked and logged.
The callback function receives two parameters:
- fw: A new *TestFramework instance with the correct test name set
- t: The *testing.T instance for the subtest
This design separates framework operations (via fw) from test assertions (via t), making the code more explicit and preventing accidental use of the wrong test context.
Parameters:
- name: The name of the subtest
- fn: The test function that receives fw and t
Returns:
- bool: True if the test passed, false otherwise (same as t.Run)
Example:
func TestMyFeature(t *testing.T) {
fw := globalFramework.ForTest(t)
fw.Run("BasicTest", func(fw *TestFramework, t *testing.T) {
input, output, err := fw.SendPacketAndParse(0, 0, packet, timeout)
require.NoError(t, err)
})
fw.Run("AdvancedTest", func(fw *TestFramework, t *testing.T) {
_, err := fw.ExecuteCommand("some command")
require.NoError(t, err)
})
}
func (*TestFramework) RunWith ¶
func (f *TestFramework) RunWith(snapshot string, name string, fn func(fw *TestFramework, t *testing.T)) bool
RunWith is like Run but restores the named VM snapshot before executing the test function. This gives each subtest a guaranteed clean VM state matching the configuration at snapshot time.
Use Run() (without snapshot restore) for subtests that intentionally build on the state left by previous subtests. Use RunWith() when each subtest needs full isolation.
func (*TestFramework) SaveSnapshot ¶
func (f *TestFramework) SaveSnapshot(name string) error
SaveSnapshot saves a named VM snapshot that captures the full machine state (CPU, RAM, devices). Subsequent calls to RestoreAndReconnect can revert the VM to this point. This is typically called from TestMain after YANET is configured to create a "baseline" snapshot.
The method unmounts all 9P shares before savevm (to remove QEMU migration blockers) and remounts them afterward. This only works cleanly when PrepareLocalStorage() was called first so that no process holds open fids on 9P mounts.
func (*TestFramework) SaveSnapshotKeepUnmounted ¶
func (f *TestFramework) SaveSnapshotKeepUnmounted(name string) error
SaveSnapshotKeepUnmounted saves a snapshot like SaveSnapshot but does NOT remount 9P shares afterward. Use this for baseline snapshots in pool mode: since RestoreAndReconnect will loadvm immediately, the intermediate remount would just be unmounted again before loadvm, wasting ~4s per test.
func (*TestFramework) SendPacketAndCapture ¶
func (f *TestFramework) SendPacketAndCapture(inputIfaceIndex int, outputIfaceIndex int, packet []byte, timeout time.Duration) ([]byte, error)
SendPacketAndCapture sends a network packet through the specified input interface and captures any response from the output interface without performing packet verification or parsing. This is a low-level method for raw packet testing.
The method handles:
- Socket client retrieval and connection management
- Packet transmission through the input interface
- Response capture from the output interface with timeout
- Automatic socket connection establishment
- Optional socket data dumping when debug mode is enabled
Parameters:
- inputIfaceIndex: Index of the network interface to send the packet through
- outputIfaceIndex: Index of the network interface to capture response from
- packet: Raw packet data to transmit
- timeout: Maximum time to wait for response capture
Returns:
- []byte: Raw response packet data, or nil if no response received
- error: An error if packet transmission fails or interfaces are unavailable
Example:
fw := globalFramework.ForTest(t)
fw.Run("SendPacket", func(fw *TestFramework, t *testing.T) {
response, err := fw.SendPacketAndCapture(0, 1, packetData, 5*time.Second)
require.NoError(t, err)
})
func (*TestFramework) SendPacketAndCaptureAll ¶
func (f *TestFramework) SendPacketAndCaptureAll(inputIfaceIndex int, outputIfaceIndex int, packet []byte, timeout time.Duration) ([][]byte, error)
SendPacketAndCaptureAll sends a network packet and captures all response packets.
func (*TestFramework) SendPacketAndParse ¶
func (f *TestFramework) SendPacketAndParse(inputIfaceIndex int, outputIfaceIndex int, packet []byte, timeout time.Duration) (*PacketInfo, *PacketInfo, error)
SendPacketAndParse sends a network packet, captures the response, and parses both the input and output packets into structured PacketInfo objects. This high-level method provides comprehensive packet analysis for detailed testing scenarios.
The method performs:
- Input packet parsing and validation
- Packet transmission through the specified interfaces
- Response capture with timeout handling
- Output packet parsing and analysis
- Detailed logging of packet flow for debugging
- Optional socket data dumping when debug mode is enabled
Parameters:
- inputIfaceIndex: Index of the network interface to send the packet through
- outputIfaceIndex: Index of the network interface to capture response from
- packet: Raw packet data to transmit
- timeout: Maximum time to wait for response capture
Returns:
- *PacketInfo: Parsed information about the input packet
- *PacketInfo: Parsed information about the output packet (nil if no response)
- error: An error if packet processing, transmission, or parsing fails
Example:
fw := globalFramework.ForTest(t)
fw.Run("ParsePacket", func(fw *TestFramework, t *testing.T) {
input, output, err := fw.SendPacketAndParse(0, 1, packetData, 5*time.Second)
require.NoError(t, err)
t.Logf("Sent: %s, Received: %s", input.String(), output.String())
})
func (*TestFramework) SendPacketAndParseAll ¶
func (f *TestFramework) SendPacketAndParseAll(inputIfaceIndex int, outputIfaceIndex int, packet []byte, timeout time.Duration) ([]*PacketInfo, error)
SendPacketAndParseAll sends a network packet and captures ALL response packets.
func (*TestFramework) SendPacketsAndParseAll ¶
func (f *TestFramework) SendPacketsAndParseAll(inputIfaceIndex int, outputIfaceIndex int, packets [][]byte, timeout time.Duration) ([]*PacketInfo, error)
func (*TestFramework) Start ¶
func (f *TestFramework) Start() (bool, error)
Start initializes and launches the complete test environment, including the QEMU virtual machine with configured networking. This method must be called before executing any tests or VM operations.
The startup process includes:
- QEMU virtual machine launch with socket networking
- Network interface initialization
- VM readiness verification
Returns:
- error: An error if VM startup fails or networking cannot be established
Example:
if err := framework.Start(); err != nil {
log.Fatalf("Failed to start test environment: %v", err)
}
func (*TestFramework) StartLogTailers ¶
func (f *TestFramework) StartLogTailers() error
StartLogTailers starts background processes that tail YANET log files from local tmpfs to the 9P-mounted /mnt/logs/ directory, making logs visible on the host in real time. Must be called after YANET is running and 9P mounts are available.
func (*TestFramework) StartYANET ¶
func (f *TestFramework) StartYANET(dataplaneConfig string, controlplaneConfig string) error
StartYANET initializes and launches the complete YANET network processing stack within the virtual machine environment. This comprehensive method handles all aspects of YANET deployment including configuration, kernel module loading, network interface binding, and service startup.
The startup process includes:
- Configuration file creation and validation
- YANET binary availability verification
- Required kernel module loading (vfio-pci)
- Network interface binding to DPDK drivers
- YANET dataplane service startup with background execution
- YANET controlplane service startup and readiness verification
- Service health checks and log monitoring
Parameters:
- dataplaneConfig: YAML configuration content for the YANET dataplane service
- controlplaneConfig: YAML configuration content for the YANET controlplane service
Returns:
- error: An error if any step of the YANET startup process fails
Example:
dataplaneYAML := `
interfaces:
- name: "eth0"
pci: "01:00.0"
`
controlplaneYAML := `
modules:
- name: "forward"
`
if err := fw.StartYANET(dataplaneYAML, controlplaneYAML); err != nil {
log.Fatalf("YANET startup failed: %v", err)
}
func (*TestFramework) Stop ¶
func (f *TestFramework) Stop() error
Stop performs cleanup of the test environment, closing socket clients and stopping the QEMU virtual machine.
Multiple cleanup errors are collected and returned as a combined error.
func (*TestFramework) Unmount9P ¶
func (f *TestFramework) Unmount9P() error
Unmount9P unmounts all 9P shares inside the guest VM. This removes the QEMU migration blockers so that savevm/loadvm can proceed. When PrepareLocalStorage() has been called first, no process holds open fids on 9P mounts and plain umount works. Log tailer processes (tail -f >> /mnt/logs/...) are killed first because they hold open fids on /mnt/logs via write-append.
func (*TestFramework) ValidateCounter ¶
func (f *TestFramework) ValidateCounter(name string, expect uint64) error
ValidateCounter queries a named counter via yanet-cli-counters and compares its value against expect. Sums all instances for counters with multiple instances. Returns an error if the counter is not found or the value does not match.
func (*TestFramework) WaitForDatapathReady ¶
func (f *TestFramework) WaitForDatapathReady(timeout time.Duration) error
WaitForDatapathReady sends ICMP heartbeat packets until the dataplane responds, confirming DPDK virtio-user reconnect after a snapshot restore. No operstate check — on macOS TCG and Linux KVM, kni0 operstate stays DOWN even when DPDK is actively forwarding. Only an end-to-end packet test works.
func (*TestFramework) WaitForReady ¶
func (f *TestFramework) WaitForReady(timeout time.Duration) error
WaitForReady blocks until the QEMU virtual machine becomes ready for command execution or the specified timeout expires. This is a proxy method for QEMU.WaitForReady.
Parameters:
- timeout: Maximum time to wait for VM readiness
Returns:
- error: An error if the timeout expires before VM becomes ready, or nil if ready
Example:
if err := fw.WaitForReady(60 * time.Second); err != nil {
log.Fatalf("VM failed to become ready: %v", err)
}
func (*TestFramework) WaitOutputPresent ¶
func (f *TestFramework) WaitOutputPresent(cmd string, checker func(string) bool, timeout time.Duration) error
WaitOutputPresent repeatedly executes a command until the output satisfies the provided checker function or the timeout expires. This utility method is used for waiting on asynchronous operations and service readiness verification.
The method polls the command output at regular intervals (100ms) and applies the checker function to determine if the expected condition has been met. This is particularly useful for waiting on service startup, configuration application, or system state changes.
Parameters:
- cmd: Shell command to execute repeatedly
- checker: Function that returns true when the desired condition is met
- timeout: Maximum time to wait for the condition
Returns:
- error: An error if the timeout expires or command execution fails
Example:
err := fw.WaitOutputPresent("ps aux | grep yanet", func(output string) bool {
return strings.Contains(output, "yanet-dataplane")
}, 30*time.Second)
type VMPool ¶
type VMPool struct {
// contains filtered or unexported fields
}
VMPool manages a pool of QEMU virtual machines for parallel test execution. Each VM slot holds a long-lived QEMUManager + framework instance. Pool slots are started from a pre-prepared template overlay (typically baseline, falling back to booted) and remain running between tests. Per-test isolation is achieved via RestoreBooted(), which does a fast loadvm+reconnect without restarting the QEMU process.
Pool size is controlled by YANET_VM_POOL_SIZE (default 1).
func NewVMPool ¶
func NewVMPool(size int, baseName string, qemuImage string, bootedTemplate string, templateOverlay string, templateSnapshotName string, log *zap.SugaredLogger) (_ *VMPool, err error)
NewVMPool creates a pool of size VMs. The bootedTemplate overlay is the canonical fallback source for pool slots. If templateOverlay/templateSnapshotName are provided and valid, StartAll prefers them. Otherwise it falls back to the booted template and bootstraps it if needed.
VMs are not started yet - call StartAll after creating the pool.
func (*VMPool) Acquire ¶
func (p *VMPool) Acquire() *TestFramework
Acquire blocks until a VM slot is available and returns its framework instance. The caller MUST call Release when done.
func (*VMPool) ForEachParallel ¶
func (p *VMPool) ForEachParallel(fn func(idx int, fw *TestFramework) error) error
ForEachParallel calls fn for each VM's framework instance in parallel.
func (*VMPool) Release ¶
func (p *VMPool) Release(fw *TestFramework)
Release returns a VM slot back to the pool.
func (*VMPool) StartAll ¶
StartAll starts all VM slots. It prefers the configured template overlay when available and otherwise falls back to the cached booted template.
- Fast path: preferred template or booted template already exists -> copy it to each slot and boot via -loadvm <snapshot>.
- Slow path: booted template missing -> boot VM0 from scratch, save the booted snapshot, cache it as the template, then restart all slots from it.
After StartAll all slots are added to the available channel.
func (*VMPool) StopAllCPU ¶
func (p *VMPool) StopAllCPU()
StopAllCPU pauses every VM's CPU via the QEMU monitor. Useful after initial setup to avoid idle DPDK busy-polling consuming host CPU.