From 4be38c473874d6ecc0c8e63f2a31b43d1d0db06c Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Wed, 20 May 2026 17:03:11 -0500 Subject: [PATCH] feat: add chaos-network workload for network chaos injection Implements chaos-network workload using tc/netem for network chaos injection. Supports configurable latency (default 100ms) and packet loss (default 5%). Features: - Single VM workload model - Uses tc qdisc and netem for network emulation - Systemd service for continuous operation - Assumes iproute-tc pre-installed via golden image - Comprehensive test coverage Updates: - Add ChaosNetworkWorkload struct and factory - Register in DefaultRegistry - Update AllWorkloadNames - Add test suite with 11 test cases - Update main_test.go for new workload count Implements #17 Signed-off-by: Melvin Hillsman Co-Authored-By: Claude Sonnet 4.5 --- cmd/virtwork/main_test.go | 4 +- internal/workloads/chaos-network.go | 83 +++++++++++++++++ internal/workloads/chaos-network_test.go | 111 +++++++++++++++++++++++ internal/workloads/registry.go | 5 +- internal/workloads/registry_test.go | 22 ++++- 5 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 internal/workloads/chaos-network.go create mode 100644 internal/workloads/chaos-network_test.go diff --git a/cmd/virtwork/main_test.go b/cmd/virtwork/main_test.go index bc83541..cf14df3 100644 --- a/cmd/virtwork/main_test.go +++ b/cmd/virtwork/main_test.go @@ -643,8 +643,8 @@ var _ = Describe("CLI end-to-end scenarios", func() { Expect(err).NotTo(HaveOccurred()) totalVMs += w.VMCount() } - // cpu=1 + database=1 + disk=1 + memory=1 + network=2 = 6 - Expect(totalVMs).To(Equal(6)) + // chaos-network=1 + cpu=1 + database=1 + disk=1 + memory=1 + network=2 = 7 + Expect(totalVMs).To(Equal(7)) }) }) diff --git a/internal/workloads/chaos-network.go b/internal/workloads/chaos-network.go new file mode 100644 index 0000000..af97c96 --- /dev/null +++ b/internal/workloads/chaos-network.go @@ -0,0 +1,83 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package workloads + +import ( + "fmt" + + "github.com/opdev/virtwork/internal/config" +) + +const chaosNetworkSystemdUnit = `[Unit] +Description=Virtwork network chaos workload (tc/netem) +After=network.target + +[Service] +Type=simple +ExecStart=/usr/sbin/tc qdisc add dev eth0 root netem delay %sms loss %s%% limit 1000 +ExecStop=/usr/sbin/tc qdisc del dev eth0 root +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +` + +// ChaosNetworkWorkload generates cloud-init userdata for network chaos injection +// using tc (traffic control) and netem (network emulation). +type ChaosNetworkWorkload struct { + BaseWorkload + Latency int // Latency in milliseconds (default: 100) + PacketLoss float64 // Packet loss percentage (default: 5.0) +} + +// NewChaosNetworkWorkload creates a ChaosNetworkWorkload with the given configuration +// and SSH credentials. +func NewChaosNetworkWorkload(cfg config.WorkloadConfig, sshUser, sshPassword string, sshKeys []string) *ChaosNetworkWorkload { + latency := 100 + packetLoss := 5.0 + + // Allow configuration via WorkloadConfig if needed in future + // For now, use sensible defaults as specified in the issue + + return &ChaosNetworkWorkload{ + BaseWorkload: BaseWorkload{ + Config: cfg, + SSHUser: sshUser, + SSHPassword: sshPassword, + SSHAuthorizedKeys: sshKeys, + }, + Latency: latency, + PacketLoss: packetLoss, + } +} + +// Name returns "chaos-network". +func (w *ChaosNetworkWorkload) Name() string { + return "chaos-network" +} + +// CloudInitUserdata returns cloud-init YAML that configures tc/netem for network chaos +// injection via systemd. Assumes iproute-tc is pre-installed (golden image dependency). +func (w *ChaosNetworkWorkload) CloudInitUserdata() (string, error) { + // Format the systemd unit with latency and packet loss parameters + systemdContent := fmt.Sprintf(chaosNetworkSystemdUnit, + fmt.Sprintf("%d", w.Latency), + fmt.Sprintf("%.1f", w.PacketLoss)) + + return w.BuildCloudConfig(CloudConfigOpts{ + Packages: nil, // iproute-tc assumed pre-installed via golden image + WriteFiles: []WriteFile{ + { + Path: "/etc/systemd/system/virtwork-chaos-network.service", + Content: systemdContent, + Permissions: "0644", + }, + }, + RunCmd: [][]string{ + {"systemctl", "daemon-reload"}, + {"systemctl", "enable", "--now", "virtwork-chaos-network.service"}, + }, + }) +} diff --git a/internal/workloads/chaos-network_test.go b/internal/workloads/chaos-network_test.go new file mode 100644 index 0000000..0de1a48 --- /dev/null +++ b/internal/workloads/chaos-network_test.go @@ -0,0 +1,111 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package workloads_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opdev/virtwork/internal/config" + "github.com/opdev/virtwork/internal/workloads" +) + +var _ = Describe("ChaosNetworkWorkload", func() { + var w *workloads.ChaosNetworkWorkload + + BeforeEach(func() { + w = workloads.NewChaosNetworkWorkload(config.WorkloadConfig{ + Enabled: true, + VMCount: 1, + CPUCores: 1, + Memory: "1Gi", + }, "virtwork", "", nil) + }) + + It("should return 'chaos-network' for Name", func() { + Expect(w.Name()).To(Equal("chaos-network")) + }) + + It("should not include packages (assumes golden image)", func() { + result, err := w.CloudInitUserdata() + Expect(err).NotTo(HaveOccurred()) + + parsed := parseYAML(result) + _, hasPackages := parsed["packages"] + Expect(hasPackages).To(BeFalse(), "chaos-network should not install packages (assumes golden image)") + }) + + It("should include systemd service in cloud-init", func() { + result, err := w.CloudInitUserdata() + Expect(err).NotTo(HaveOccurred()) + + parsed := parseYAML(result) + Expect(parsed).To(HaveKey("write_files")) + files := parsed["write_files"].([]interface{}) + Expect(files).To(HaveLen(1)) + + file := files[0].(map[string]interface{}) + Expect(file["path"]).To(Equal("/etc/systemd/system/virtwork-chaos-network.service")) + + content := file["content"].(string) + Expect(content).To(ContainSubstring("tc qdisc")) + Expect(content).To(ContainSubstring("netem")) + Expect(content).To(ContainSubstring("delay 100ms"), "should include default 100ms latency") + Expect(content).To(ContainSubstring("loss 5.0%"), "should include default 5% packet loss") + }) + + It("should include systemd enable commands", func() { + result, err := w.CloudInitUserdata() + Expect(err).NotTo(HaveOccurred()) + + parsed := parseYAML(result) + Expect(parsed).To(HaveKey("runcmd")) + runcmds := parsed["runcmd"].([]interface{}) + + // Should have daemon-reload and enable commands + found := false + for _, cmd := range runcmds { + cmdSlice := cmd.([]interface{}) + if len(cmdSlice) >= 3 { + if cmdSlice[0] == "systemctl" && cmdSlice[1] == "enable" { + found = true + Expect(cmdSlice[2]).To(Equal("--now")) + } + } + } + Expect(found).To(BeTrue(), "should have systemctl enable command") + }) + + It("should produce valid YAML", func() { + result, err := w.CloudInitUserdata() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HavePrefix("#cloud-config\n")) + + parsed := parseYAML(result) + Expect(parsed).NotTo(BeNil()) + }) + + It("should have no extra disks", func() { + Expect(w.ExtraDisks()).To(BeNil()) + }) + + It("should have no data volumes", func() { + Expect(w.DataVolumeTemplates()).To(BeNil()) + }) + + It("should have no service", func() { + Expect(w.RequiresService()).To(BeFalse()) + Expect(w.ServiceSpec()).To(BeNil()) + }) + + It("should reflect config in VMResources", func() { + res := w.VMResources() + Expect(res.CPUCores).To(Equal(1)) + Expect(res.Memory).To(Equal("1Gi")) + }) + + It("should have VMCount of 1", func() { + Expect(w.VMCount()).To(Equal(1)) + }) +}) diff --git a/internal/workloads/registry.go b/internal/workloads/registry.go index 950d55e..e6f2577 100644 --- a/internal/workloads/registry.go +++ b/internal/workloads/registry.go @@ -54,11 +54,14 @@ type WorkloadFactory func(config.WorkloadConfig, *RegistryOpts) Workload type Registry map[string]WorkloadFactory // AllWorkloadNames is a sorted list of all built-in workload names. -var AllWorkloadNames = []string{"cpu", "database", "disk", "memory", "network"} +var AllWorkloadNames = []string{"chaos-network", "cpu", "database", "disk", "memory", "network"} // DefaultRegistry returns a Registry pre-populated with all built-in workloads. func DefaultRegistry() Registry { return Registry{ + "chaos-network": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload { + return NewChaosNetworkWorkload(cfg, opts.SSHUser, opts.SSHPassword, opts.SSHAuthorizedKeys) + }, "cpu": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload { return NewCPUWorkload(cfg, opts.SSHUser, opts.SSHPassword, opts.SSHAuthorizedKeys) }, diff --git a/internal/workloads/registry_test.go b/internal/workloads/registry_test.go index 4041b1c..a88e6c2 100644 --- a/internal/workloads/registry_test.go +++ b/internal/workloads/registry_test.go @@ -18,8 +18,8 @@ var _ = Describe("Registry", func() { reg = workloads.DefaultRegistry() }) - It("should have 5 entries registered", func() { - Expect(reg.List()).To(HaveLen(5)) + It("should have 6 entries registered", func() { + Expect(reg.List()).To(HaveLen(6)) }) It("should return CPU workload by name", func() { @@ -77,10 +77,22 @@ var _ = Describe("Registry", func() { Expect(w.Name()).To(Equal("disk")) }) + It("should return chaos-network workload by name", func() { + w, err := reg.Get("chaos-network", config.WorkloadConfig{ + Enabled: true, + VMCount: 1, + CPUCores: 1, + Memory: "1Gi", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(w.Name()).To(Equal("chaos-network")) + }) + It("should return error for unknown name with available names", func() { _, err := reg.Get("unknown", config.WorkloadConfig{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("unknown")) + Expect(err.Error()).To(ContainSubstring("chaos-network")) Expect(err.Error()).To(ContainSubstring("cpu")) Expect(err.Error()).To(ContainSubstring("database")) Expect(err.Error()).To(ContainSubstring("disk")) @@ -90,7 +102,7 @@ var _ = Describe("Registry", func() { It("should list all names sorted alphabetically", func() { names := reg.List() - Expect(names).To(Equal([]string{"cpu", "database", "disk", "memory", "network"})) + Expect(names).To(Equal([]string{"chaos-network", "cpu", "database", "disk", "memory", "network"})) }) It("should create workloads with provided config", func() { @@ -157,7 +169,7 @@ var _ = Describe("Registry", func() { }) var _ = Describe("AllWorkloadNames", func() { - It("should contain all five workload names sorted", func() { - Expect(workloads.AllWorkloadNames).To(Equal([]string{"cpu", "database", "disk", "memory", "network"})) + It("should contain all six workload names sorted", func() { + Expect(workloads.AllWorkloadNames).To(Equal([]string{"chaos-network", "cpu", "database", "disk", "memory", "network"})) }) })