Documentation
¶
Overview ¶
Package capture is instigator's opt-in install recorder. Enabled with serve --capture-dir, it writes a per-run bundle - run.json (provenance and media manifest), events.jsonl (the source of truth: one JSON object per BOOTP/TFTP/rsh/inst lifecycle event), and a derived summary.json - that becomes debugging data, performance numbers, and the command corpus for later CI replay.
The package is a standard-library-only leaf: it never imports config, vfs, or any protocol package, so bootp/tftp/instcmd/serve can all import it without a cycle. Provenance and event fields arrive as plain primitives assembled by the caller.
A *Recorder is nil-safe: every method is a no-op on a nil receiver, exactly like logging.Logger. That is the whole "disabled" path - when --capture-dir is unset the recorder is nil, no files are opened, and callers install none of the counting wrappers, so capture costs nothing.
Index ¶
- type Binary
- type Client
- type Command
- type ConfigInfo
- type Media
- type Option
- type PathReuse
- type Provenance
- type Recorder
- func (r *Recorder) BeginSession(alias, remoteAddr, remoteUser, localUser string) *Session
- func (r *Recorder) BootpReply(alias, mac, file, offeredIP, result string)
- func (r *Recorder) Close() error
- func (r *Recorder) Finish(reason string) error
- func (r *Recorder) ListenerExit(name string)
- func (r *Recorder) RSHRejected(alias, remoteAddr, reason string)
- func (r *Recorder) ServerStart()
- func (r *Recorder) ServerStop(reason string)
- func (r *Recorder) TFTPTransferEnd(rec TransferRecord)
- func (r *Recorder) WriteRun(p Provenance) error
- type Services
- type Session
- type SessionSummary
- type SlowCommand
- type Summary
- type TransferRecord
- type VerbLatency
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Binary ¶
type Binary struct {
Version string `json:"version,omitempty"`
VCSRevision string `json:"vcs_revision,omitempty"`
VCSDirty bool `json:"vcs_dirty"`
GoVersion string `json:"go_version"`
SHA256 string `json:"sha256,omitempty"`
}
Binary identifies the running instigator build, so a capture can be tied back to exact source. Fields are best-effort: a build without VCS stamping or an unreadable executable leaves them empty rather than failing capture.
type Client ¶
Client is one configured install client, by alias. The bundle also carries its MAC and IP: LAN identifiers, not secrets, but the reason the capture directory is private (0700).
type Command ¶
type Command struct {
// contains filtered or unexported fields
}
Command is one wire line's recording context: its counters and the files it served, snapshotted into an inst_command_end event by End.
type ConfigInfo ¶
type ConfigInfo struct {
ServerIP string `json:"server_ip"`
Netmask string `json:"netmask"`
Ports map[string]int `json:"ports,omitempty"`
}
ConfigInfo is the redacted effective config: enough to reproduce the serving layout, none of it secret (the config carries no credentials).
type Media ¶
type Media struct {
Media string `json:"media"`
Disc string `json:"disc"`
Image string `json:"image"`
Size int64 `json:"size"`
Mtime string `json:"mtime"`
}
Media is one backing image in the served tree. Image is a basename, and there is deliberately no digest: hashing multi-GB ISOs at startup is out of scope (a separate manifest step can add hashes later). Size and Mtime are cheap identity that a later run can compare.
type Option ¶
type Option func(*Recorder)
Option configures a Recorder at construction.
func WithSummaryWriter ¶
WithSummaryWriter directs the human summary Finish renders at shutdown to w instead of os.Stdout, so a test can capture it.
type PathReuse ¶
type PathReuse struct {
Path string `json:"path"`
Opens int `json:"opens"`
Bytes int64 `json:"bytes"`
}
PathReuse is how often one backing image:path was served and how many backing bytes its commands read - whether a bounded cache might pay off.
type Provenance ¶
type Provenance struct {
Schema int `json:"schema"`
RunID string `json:"run_id"`
Start string `json:"start"`
End string `json:"end,omitempty"`
Binary Binary `json:"binary"`
Services Services `json:"services"`
Config ConfigInfo `json:"config"`
Clients []Client `json:"clients"`
Media []Media `json:"media"`
}
Provenance is the run.json payload. serve assembles it (it alone knows config and the media tree) and hands it here as plain primitives; the recorder never imports config or vfs. Schema and RunID are filled by WriteRun.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder writes a run's events to events.jsonl and owns the run identity and provenance. It is safe for concurrent use: TFTP serves each transfer in its own goroutine and multiple rsh sessions run at once, so every event write is serialized by mu. A nil *Recorder is a no-op.
func New ¶
New creates the capture directory (0700) and opens events.jsonl (0600) for appending. The run id is generated once here and is stable for the life of the Recorder.
func (*Recorder) BeginSession ¶
BeginSession emits rsh_session_start and returns the session's recording context. A nil Recorder yields a nil Session.
func (*Recorder) BootpReply ¶
BootpReply records the outcome of a BOOTP request: answered (a configured client was sent its reply), or ignored (a well-formed request from a MAC that is not configured). alias is the client's configured name, empty when unknown.
func (*Recorder) Close ¶
Close flushes and closes events.jsonl. It is safe on a nil Recorder and idempotent enough for a deferred call.
func (*Recorder) Finish ¶
Finish closes the run at a clean shutdown: it emits server_stop, closes events.jsonl, rewrites run.json with the end time, then reads the events back to write summary.json and render the human summary. It must run after every session and transfer has ended; a late event from an in-flight goroutine is safely dropped once events.jsonl is closed.
An event dropped mid-run means the summary was computed from an incomplete file, so that error outranks anything that goes wrong here.
func (*Recorder) ListenerExit ¶
ListenerExit records a protocol listener returning while the server was not shutting down, so a listener that died mid-run is visible in both the log and the trace.
func (*Recorder) RSHRejected ¶
RSHRejected records a connection rcmd refused before any session began - a non-reserved source port, or a client outside the allow list. It is a start/end pair with result "refused" so it shows in the session list as a connection that never became a session. reason is a short class.
func (*Recorder) ServerStart ¶
func (r *Recorder) ServerStart()
ServerStart records that the server bound its listeners and began serving.
func (*Recorder) ServerStop ¶
ServerStop records a clean shutdown. reason is the stop cause (today always "clean"); it rides in Result so the summary can tell a graceful stop from a future abnormal one.
func (*Recorder) TFTPTransferEnd ¶
func (r *Recorder) TFTPTransferEnd(rec TransferRecord)
TFTPTransferEnd records a completed (or abandoned) TFTP transfer. There is no start event: a boot transfer is short, and a missing end for an interrupted one is not worth a second line here.
func (*Recorder) WriteRun ¶
func (r *Recorder) WriteRun(p Provenance) error
WriteRun marshals the provenance to run.json (0600). It stamps the schema version and run id so the caller need not. The stored copy lets a clean shutdown rewrite run.json with the end time.
type Services ¶
type Services struct {
BOOTP bool `json:"bootp"`
TFTP bool `json:"tftp"`
RSH bool `json:"rsh"`
TFTPPortRange [2]int `json:"tftp_port_range"`
}
Services records which listeners were enabled for the run.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is one rsh connection's recording context. It is created by BeginSession, hands out per-command counting wrappers, and is closed by End. A nil *Session is a no-op, so instcmd can hold one unconditionally.
The rsh shell runs one command at a time, but a single command's pipeline stages (cat a | fgrep b) execute in concurrent goroutines that both write the current command's counters - so the counters are atomic. cur is an atomic pointer because the session-level stdout wrapper reads it on every write while BeginCommand replaces it between commands.
func (*Session) BeginCommand ¶
BeginCommand emits inst_command_start and makes this command current, so the session's counting wrappers attribute to it. Every non-empty wire line is a command, including the marker wrapper.
func (*Session) End ¶
End emits rsh_session_end, classifying the outcome: a nil error is a clean close, a deadline is the idle timeout firing, anything else is an interrupted session (a lost connection mid-stream).
func (*Session) RecordServed ¶
RecordServed notes a file a leaf command actually read, with the backing image and in-image path when the tree could resolve it. Safe to call from concurrent pipeline stages.
func (*Session) WrapReaderAt ¶
WrapReaderAt wraps a backing EFS file so its ReadAt calls and bytes are counted against the command current when the file was opened - which is the command that reads it, since these leaf commands open and consume a file within one line. Returns r unchanged on a nil session.
func (*Session) WrapWriter ¶
WrapWriter wraps a session output stream, both of which are bound to the rsh socket, so every write is counted against the session total and the current command. stderr selects which per-command counters it feeds, so dd's data (stdout) and its records summary (stderr) are told apart. Returns w unchanged on a nil session.
type SessionSummary ¶
type SessionSummary struct {
ID string `json:"id"`
Client string `json:"client,omitempty"`
Result string `json:"result"`
WallMS int64 `json:"wall_ms"`
ActiveMS int64 `json:"active_ms"`
IdleMS int64 `json:"idle_ms"`
Commands int `json:"commands"`
BytesOut int64 `json:"bytes_out"`
ActiveBps int64 `json:"active_bps"` // bytes_out over active command time: the server-path rate
WallBps int64 `json:"wall_bps"` // bytes_out over wall time: end-to-end, including client think time
}
SessionSummary is one rsh session's timing. Active is the sum of its command durations, idle is the rest of its wall time - the client thinking and the round trips, not server work.
type SlowCommand ¶
type SlowCommand struct {
Session string `json:"session"`
Seq int `json:"seq"`
Verb string `json:"verb"`
DurationMS int64 `json:"duration_ms"`
Path string `json:"path,omitempty"`
}
SlowCommand names one slow command so it can be found in the serial transcript by session and sequence.
type Summary ¶
type Summary struct {
Sessions []SessionSummary `json:"sessions"`
BytesServed int64 `json:"bytes_served"`
Commands int `json:"commands"`
RefusedCommands int `json:"refused_commands"`
NonzeroCommands int `json:"nonzero_commands"`
Transfers int `json:"transfers"`
TFTPRetransmits int `json:"tftp_retransmits"`
AbortedTransfers int `json:"aborted_transfers"`
GaveupTransfers int `json:"gaveup_transfers"`
ListenerExits int `json:"listener_exits"`
BootpAnswered int `json:"bootp_answered"`
EFSReadCalls int64 `json:"efs_read_calls"`
EFSReadBytes int64 `json:"efs_read_bytes"`
StdoutCalls int64 `json:"stdout_calls"`
StdoutBytes int64 `json:"stdout_bytes"`
StderrCalls int64 `json:"stderr_calls"`
StderrBytes int64 `json:"stderr_bytes"`
Verbs []VerbLatency `json:"verbs"`
Paths []PathReuse `json:"path_reuse"`
Slowest []SlowCommand `json:"slowest_commands"`
}
Summary is the derived view of one run's events.jsonl: per-session timing, run-wide totals, per-verb latency, and path reuse. It is what summary.json holds and what the shutdown/`trace summary` human report is rendered from. Idle is reported per session and never summed run-wide - two sessions idle in the same wall-second are not two seconds of idle.
type TransferRecord ¶
type TransferRecord struct {
Client string
Name string
TreePath string
Image string
ImagePath string
Size int64
BlockSize int
Blocks int
BytesSent int64
BytesAcked int64
Retransmits int
DurationMS int64
Result string
}
TransferRecord is one completed TFTP transfer, assembled by the tftp server and handed here for recording. Result is the outcome: ok, unacked_final (all blocks sent but the client never acked the last one, the normal SGI PROM case), aborted (the client sent ERROR or a send failed), gaveup (a mid-transfer block went unacked within the retry budget), notfound, or error. BytesSent is what went on the wire; BytesAcked is what the client confirmed - they differ exactly when the final block is unacked.