framework

package
v0.0.0-...-26e3ead Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Index

Constants

View Source
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"
)
View Source
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

View Source
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.

View Source
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

func BaselineImagePath(baseImagePath string) string

BaselineImagePath returns the path to the cached baseline snapshot image placed next to the base image.

func BootedImagePath

func BootedImagePath(baseImagePath string) string

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

func CreateICMPv4EchoPacket(srcIP, dstIP net.IP, id, seq uint16, payload []byte) []byte

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

func HasBootedSnapshot(overlayPath string) bool

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

func OverlayHasSnapshot(imagePath, name string) bool

OverlayHasSnapshot returns true if the given qcow2 image contains a snapshot with the given name.

func PoolSize

func PoolSize() int

PoolSize returns the desired VM pool size from the environment.

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

func SnapshotImagePath(baseImagePath string, snapshotName string) string

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

type Config struct {
	Name      string
	QEMUImage string // Path to the QEMU virtual machine image file
}

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

func (sc *SocketClient) ReceiveAllPackets(timeout time.Duration, dumpPath string) ([][]byte, error)

ReceiveAllPackets receives all packets available on the socket within the timeout.

func (*SocketClient) ReceivePacket

func (sc *SocketClient) ReceivePacket(timeout time.Duration, dumpPath string) ([]byte, error)

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:

  1. Unmount 9P shares
  2. loadvm via QEMU monitor (monitor socket survives)
  3. Reconnect serial console
  4. Wait for shell prompts
  5. Mount 9P shares
  6. Reset socket connections (close stale connections, reconnect)
  7. 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) Shutdown

func (p *VMPool) Shutdown() error

Shutdown stops all VM slots in the pool.

func (*VMPool) Size

func (p *VMPool) Size() int

Size returns the number of VM slots in the pool.

func (*VMPool) StartAll

func (p *VMPool) StartAll() error

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.

func (*VMPool) WaitAllReady

func (p *VMPool) WaitAllReady(timeout time.Duration) error

WaitAllReady waits for every VM in the pool to reach the shell prompt.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL