diff --git a/README.md b/README.md index bf4b21b..b1d7485 100644 --- a/README.md +++ b/README.md @@ -106,16 +106,16 @@ virtwork ships nine built-in workloads, grouped by purpose. With `--vm-count 1` | Workload | VMs | Description | Tools | |----------|-----|-------------|-------| -| **cpu** | N | Continuous CPU stress | `stress-ng --cpu 0 --cpu-method all` | -| **memory** | N | Memory pressure at 80% | `stress-ng --vm 1 --vm-bytes 80%` | -| **disk** | N | Mixed random and sequential I/O on a data disk | `fio` with multiple profiles | -| **database** | N | PostgreSQL with pgbench loop on a data disk | `pgbench -c 10 -j 2 -T 300` | +| **cpu** | N | Continuous CPU stress | `stress-ng --cpu 0 --cpu-load 100 --cpu-method all` (configurable via `params`) | +| **memory** | N | Memory pressure | `stress-ng --vm 1 --vm-bytes 80% --vm-method all` (configurable via `params`) | +| **disk** | N | Mixed random and sequential I/O on a data disk | `fio` with multiple profiles (configurable via `params`) | +| **database** | N | PostgreSQL with pgbench loop on a data disk | `pgbench -c 10 -j 2 -T 300` (configurable via `params`) | **Multi-VM benchmarks** — server/client pairs coordinated via a ClusterIP Service: | Workload | VMs | Description | Tools | |----------|-----|-------------|-------| -| **network** | N × 2 | Bidirectional throughput | `iperf3 --bidir` on port 5201 | +| **network** | N × 2 | Bidirectional throughput | `iperf3 -P 4 -t 60 --bidir` on port 5201 (configurable via `params`) | | **tps** | N × 2 | TCP request/response + HTTP file transfer | `netperf` (12865/12866) + Python HTTP server (8080) | **Chaos engineering** — inject failures inside the VM to test resilience. ⚠️ See [docs/chaos-workloads.md](docs/chaos-workloads.md) before deploying: diff --git a/docs/architecture.md b/docs/architecture.md index 9be5141..7b0cdbd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -358,11 +358,11 @@ classDiagram | Workload | VM Count | Data Volume | K8s Service | Packages | Workload Tool | |----------|----------|-------------|-------------|----------|---------------| -| CPU | N (configurable) | No | No | stress-ng | `stress-ng --cpu 0 --cpu-method all` | -| Memory | N (configurable) | No | No | stress-ng | `stress-ng --vm 1 --vm-bytes 80% --vm-method all` | -| Disk | N (configurable) | Yes (`/mnt/data`) | No | fio | Mixed R/W + sequential write profiles | -| Database | N (configurable) | Yes (`/var/lib/pgsql/data`) | No | postgresql-server | `pgbench -c 10 -j 2 -T 300` loop | -| Network | N × 2 (server + client) | No | Yes — `virtwork-iperf3-server` :5201 | iperf3 | `iperf3 -s` / `iperf3 -c ... --bidir` | +| CPU | N (configurable) | No | No | stress-ng | `stress-ng --cpu 0 --cpu-load 100 --cpu-method all` (defaults; tunable via `params`) | +| Memory | N (configurable) | No | No | stress-ng | `stress-ng --vm 1 --vm-bytes 80% --vm-method all` (defaults; tunable via `params`) | +| Disk | N (configurable) | Yes (`/mnt/data`) | No | fio | Mixed R/W + sequential write profiles (defaults; tunable via `params`) | +| Database | N (configurable) | Yes (`/var/lib/pgsql/data`) | No | postgresql-server | `pgbench -c 10 -j 2 -T 300` loop (defaults; tunable via `params`) | +| Network | N × 2 (server + client) | No | Yes — `virtwork-iperf3-server` :5201 | iperf3 | `iperf3 -s` / `iperf3 -c ... -P 4 -t 60 --bidir` (defaults; tunable via `params`) | | TPS | N × 2 (server + client) | No | Yes — `virtwork-tps-server` :12865 / :12866 / :8080 | netperf, python3 | `netperf -t TCP_RR` + curl HTTP file fetch loop | | Chaos-disk | N (configurable) | Yes (`/mnt/data`) | No | (golden image: `fallocate`, `dd`) | Fill to target percent, sleep, release, repeat | | Chaos-network | N (configurable) | No | No | iproute-tc (+ `sch_netem` kernel module) | `tc qdisc add ... netem delay 100ms loss 5%` | diff --git a/docs/configuration.md b/docs/configuration.md index 4191c9e..395c0aa 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -140,15 +140,35 @@ workloads: vm_count: 2 cpu_cores: 4 memory: 4Gi + params: + cpu-load-percent: "50" # default 100 + cpu-method: "matrixprod" # default all memory: vm_count: 1 # enabled by default when not specified + params: + memory-percent: "60" # default 80 + vm-stressors: "2" # default 1 + vm-method: "flip" # default all disk: enabled: false # skip this workload entirely + params: + block-size-rw: "8k" # default 4k + block-size-seq: "256k" # default 128k + rwmixread: "50" # default 70 + numjobs: "8" # default 4 + runtime: "600" # default 300 database: cpu_cores: 2 memory: 4Gi + params: + scale-factor: "100" # default 50 + clients: "20" # default 10 + duration: "600" # default 300 network: vm_count: 1 # creates 1 server + 1 client = 2 VMs + params: + parallel-streams: "8" # default 4 + test-duration: "120" # default 60 tps: vm_count: 1 # creates 1 server + 1 client = 2 VMs params: @@ -215,8 +235,21 @@ Each workload's `params` block accepts string-valued keys. The current parameter | **chaos-process** | `signal` | `SIGTERM` | Signal sent to victims | | **chaos-process** | `interval` | `30` | Seconds between kills | | **chaos-process** | `min-pid` | `1000` | Minimum PID considered eligible | - -Workloads without entries above accept no per-workload `params` today (cpu, memory, disk, database, network). +| **cpu** | `cpu-load-percent` | `100` | Target CPU load percentage for stress-ng (`--cpu-load`) | +| **cpu** | `cpu-method` | `all` | CPU stressor method for stress-ng (`--cpu-method`) | +| **memory** | `memory-percent` | `80` | Target memory usage percentage (`--vm-bytes`) | +| **memory** | `vm-stressors` | `1` | Number of VM worker stressors (`--vm`) | +| **memory** | `vm-method` | `all` | Memory stressor method for stress-ng (`--vm-method`) | +| **disk** | `block-size-rw` | `4k` | Block size for the random read/write fio profile | +| **disk** | `block-size-seq` | `128k` | Block size for the sequential write fio profile | +| **disk** | `rwmixread` | `70` | Read percentage in the mixed read/write fio profile | +| **disk** | `numjobs` | `4` | Number of parallel fio jobs for the mixed read/write profile | +| **disk** | `runtime` | `300` | Runtime in seconds for each fio profile | +| **database** | `scale-factor` | `50` | pgbench initialization scale factor (`-s`) | +| **database** | `clients` | `10` | Number of concurrent pgbench clients (`-c`) | +| **database** | `duration` | `300` | Seconds per pgbench benchmark run (`-T`) | +| **network** | `parallel-streams` | `4` | Number of parallel iperf3 streams (`-P`) | +| **network** | `test-duration` | `60` | Seconds per iperf3 test run (`-t`) | --- @@ -293,7 +326,7 @@ The `--no-audit` flag is checked separately by `initAuditor` in `cmd/virtwork/ma - All config field definitions live in `internal/config/config.go` — `WorkloadConfig` struct, `Config` struct, `SetDefaults`, `BindFlags`, `LoadConfig`. - The mapstructure tag on each field is the YAML key name (e.g., `mapstructure:"data-disk-size"` → YAML key `data_disk_size` after the `_`/`-` replacement). Match this convention when adding new fields. -- Per-workload `params` are surfaced into the workload as `WorkloadConfig.Params map[string]string`. New chaos / multi-VM knobs go here so they're uniformly addressable from YAML. +- Per-workload `params` are surfaced into the workload as `WorkloadConfig.Params map[string]string`. All workloads use this mechanism for tunable knobs so they're uniformly addressable from YAML. - When adding a new env var, prefer letting Viper bind it automatically rather than hand-rolling `os.Getenv` (see `VIRTWORK_SSH_AUTHORIZED_KEYS` in `resolveSSHKeys` for the one current exception, driven by the comma-split list semantics). - Update this document, the ConfigMap default list, and the per-workload `params` table whenever you add a new knob. diff --git a/docs/development.md b/docs/development.md index c32fb56..b0470f8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -591,7 +591,7 @@ Pattern: 5. Set `RequiresService()` to `true` and provide a `ServiceSpec()` selecting server VMs by the `virtwork/role: server` label that the orchestrator applies automatically. 6. Clients reach servers via the in-cluster DNS name `..svc.cluster.local` — never poll for pod IPs. -The canonical references are `internal/workloads/network.go` (simplest — one port, iperf3) and `internal/workloads/tps.go` (multi-port Service with configurable `Params` for `file-size`, `iterations`, `duration`). +The canonical references are `internal/workloads/network.go` (simplest — one port, iperf3) and `internal/workloads/tps.go` (multi-port Service). All workloads support configurable `Params` via the getter-with-default pattern (see [Configurable Params](#going-further-configurable-params) below). ### Going Further: Storage-Backed Workloads @@ -604,6 +604,56 @@ If your workload needs persistent storage inside the VM: Reference workloads: `disk.go` (single fio mount), `database.go` (PostgreSQL data dir), `chaos_disk.go` (fill/release loop). All three follow the same pattern. +### Going Further: Configurable Params + +All workloads expose tunable knobs through `WorkloadConfig.Params map[string]string`. Users set these in YAML config under `workloads..params`. The standard pattern is a nil-safe getter method with a hardcoded default: + +```go +func (w *MyWorkload) concurrency() string { + if w.Config.Params != nil { + if val, ok := w.Config.Params["concurrency"]; ok && val != "" { + return val + } + } + return "10" +} +``` + +Then use the getter when building cloud-init content. Convert your systemd unit constant to a template with `%s` placeholders and interpolate with `fmt.Sprintf`: + +```go +const mySystemdUnitTemplate = `[Unit] +Description=My workload + +[Service] +Type=simple +ExecStart=/usr/bin/my-tool --concurrency %s --duration %s +Restart=always + +[Install] +WantedBy=multi-user.target +` + +func (w *MyWorkload) CloudInitUserdata() (string, error) { + unit := fmt.Sprintf(mySystemdUnitTemplate, w.concurrency(), w.duration()) + return w.BuildCloudConfig(CloudConfigOpts{ + // ... + WriteFiles: []WriteFile{{ + Path: "/etc/systemd/system/virtwork-my-workload.service", + Content: unit, + }}, + }) +} +``` + +Every workload should have a `Context("param wiring")` test block with three cases: + +1. **Nil params** — `Params` field omitted, output contains default values +2. **Full override** — all param keys set, output reflects custom values +3. **Partial override** — some keys set, unset keys fall back to defaults + +See `internal/workloads/cpu_test.go` for the simplest example, or `disk_test.go` for a workload with multiple output files to verify. Document new param keys in `docs/configuration.md` — both in the YAML example and the params table. + ### Going Further: Structured Logging The `internal/logging` package provides a shared `*slog.Logger` returned by `NewLogger(w io.Writer, verbose bool)`. Use it instead of `fmt.Fprintf` or `log.Printf` in any code path under `cmd/` or in packages that perform I/O (`internal/wait` is the current example): diff --git a/docs/guide/01-overview.md b/docs/guide/01-overview.md index 91404b5..aa6e2df 100644 --- a/docs/guide/01-overview.md +++ b/docs/guide/01-overview.md @@ -56,7 +56,7 @@ write_files: [Service] Type=simple - ExecStart=/usr/bin/stress-ng --cpu 0 --cpu-method all --timeout 0 + ExecStart=/usr/bin/stress-ng --cpu 0 --cpu-load 100 --cpu-method all --timeout 0 Restart=always RestartSec=10 @@ -72,7 +72,7 @@ runcmd: - virtwork-cpu.service ``` -This is a standard [cloud-init](https://cloudinit.readthedocs.io/) configuration. When the VM boots, cloud-init will install `stress-ng` from the package manager, write the systemd unit file, and enable the service. The workload starts automatically. +This is a standard [cloud-init](https://cloudinit.readthedocs.io/) configuration. When the VM boots, cloud-init will install `stress-ng` from the package manager, write the systemd unit file, and enable the service. The workload starts automatically. The values shown above are defaults — all core workloads accept per-workload `params` to override them (see [configuration.md](../configuration.md#per-workload-params-keys)). If SSH credentials were configured, a `users` section is appended with the SSH public keys and/or password. diff --git a/docs/guide/03-adding-a-workload.md b/docs/guide/03-adding-a-workload.md index 505aa6e..d79be33 100644 --- a/docs/guide/03-adding-a-workload.md +++ b/docs/guide/03-adding-a-workload.md @@ -518,7 +518,7 @@ return w.BuildCloudConfig(CloudConfigOpts{ ### Making It Multi-VM -If your workload needs more than one role of VM (a server and one or more clients, for example), implement the `MultiVMWorkload` interface. The two canonical references are `internal/workloads/network.go` (simplest — one Service port, iperf3) and `internal/workloads/tps.go` (multi-port Service with configurable `Params` for `file-size`, `iterations`, `duration`). +If your workload needs more than one role of VM (a server and one or more clients, for example), implement the `MultiVMWorkload` interface. The two canonical references are `internal/workloads/network.go` (simplest — one Service port, iperf3) and `internal/workloads/tps.go` (multi-port Service). All workloads support configurable `Params` via the getter-with-default pattern — see [development.md](../development.md#going-further-configurable-params). 1. Add a `Namespace` field to your struct — the client needs it to build the server's in-cluster DNS name. 2. Implement `RoleDistribution() []RoleSpec` — return a slice of `RoleSpec{Role: "server", VMCount: 1}` entries declaring how many VMs each role needs. @@ -552,6 +552,9 @@ Before submitting a new workload, verify: - [ ] Packages used are available in Fedora's default repos (or pre-installed in the golden image, if applicable) - [ ] Systemd unit has `Restart=always` and `WantedBy=multi-user.target` - [ ] Tests cover: Name, packages, systemd unit content, valid YAML, VMResources, defaults for optional methods +- [ ] Tunable values exposed as `params` with getter-with-default methods (see [development.md](../development.md#going-further-configurable-params)) +- [ ] Param wiring tests: nil params → defaults, full override, partial override +- [ ] New param keys documented in `docs/configuration.md` (YAML example + params table) - [ ] Registered in `DefaultRegistry()` (`internal/workloads/registry.go`) with a factory function - [ ] Existing registry/orchestration test counts updated - [ ] `go test ./...` passes diff --git a/docs/virtwork-vs-kube-burner.md b/docs/virtwork-vs-kube-burner.md index bd88f20..5bf4564 100644 --- a/docs/virtwork-vs-kube-burner.md +++ b/docs/virtwork-vs-kube-burner.md @@ -62,10 +62,10 @@ Workloads deployed inside VMs: | Workload | Tool | What It Generates | |---|---|---| -| `cpu` | `stress-ng --cpu 0 --cpu-method all` | Continuous CPU pressure across all cores | -| `memory` | `stress-ng --vm 1 --vm-bytes 80%` | Sustained memory pressure at 80% | -| `database` | PostgreSQL + `pgbench -c 10 -j 2 -T 300` | Realistic OLTP database transactions | -| `network` | `iperf3 --bidir` (server + client VM pairs) | Bidirectional throughput between VMs | +| `cpu` | `stress-ng --cpu 0 --cpu-load 100 --cpu-method all` | Continuous CPU pressure across all cores (configurable via `params`) | +| `memory` | `stress-ng --vm 1 --vm-bytes 80% --vm-method all` | Sustained memory pressure (configurable via `params`) | +| `database` | PostgreSQL + `pgbench -c 10 -j 2 -T 300` | Realistic OLTP database transactions (configurable via `params`) | +| `network` | `iperf3 -P 4 -t 60 --bidir` (server + client VM pairs) | Bidirectional throughput between VMs (configurable via `params`) | | `tps` | `netperf` + `curl` (server + client VM pairs) | Multi-port HTTP throughput with configurable file size, iterations, and duration | | `disk` | `fio` with mixed random + sequential profiles | Mixed I/O patterns on a dedicated data disk | | `chaos-disk` | `fallocate`/`dd` fill-release loop | Sustained disk-pressure events on a data disk |