Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/virtwork/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ var _ = Describe("CLI end-to-end scenarios", func() {

Context("when running with default arguments", func() {
It("should create VMs for all workloads", func() {
// Default run creates 9 VMs: chaos-process=1 + cpu=1 + memory=1 + disk=1 + database=1 + network=2 + tps=2
// Default run creates 10 VMs: chaos-process=1 + chaos-network=1 + cpu=1 + memory=1 + disk=1 + database=1 + network=2 + tps=2
registry := workloads.DefaultRegistry()
totalVMs := 0
for _, name := range workloads.AllWorkloadNames() {
Expand All @@ -646,8 +646,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 + tps=2 = 8
Expect(totalVMs).To(Equal(9))
// chaos-process=1 + chaos-network=1 + cpu=1 + database=1 + disk=1 + memory=1 + network=2 + tps=2 = 10
Expect(totalVMs).To(Equal(10))
})
})

Expand Down
118 changes: 118 additions & 0 deletions internal/workloads/chaos-network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2026 Red Hat
// SPDX-License-Identifier: Apache-2.0

package workloads

import (
"fmt"

"github.com/opdev/virtwork/internal/config"
)

const chaosNetworkStartScript = `#!/bin/bash
set -euo pipefail

DEV=$(ip route show default | awk '{print $5; exit}')
if [ -z "$DEV" ]; then
echo "no default route interface found" >&2
exit 1
fi

modprobe sch_netem 2>/dev/null || true

exec /usr/sbin/tc qdisc add dev "$DEV" root netem delay %sms loss %s%% limit 1000
`

const chaosNetworkStopScript = `#!/bin/bash
DEV=$(ip route show default | awk '{print $5; exit}')
[ -n "$DEV" ] && /usr/sbin/tc qdisc del dev "$DEV" root 2>/dev/null
exit 0
`

const chaosNetworkSystemdUnit = `[Unit]
Description=Virtwork network chaos workload (tc/netem)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/virtwork-chaos-network-start.sh
ExecStop=/usr/local/bin/virtwork-chaos-network-stop.sh
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) {
startScript := fmt.Sprintf(chaosNetworkStartScript,
fmt.Sprintf("%d", w.Latency),
fmt.Sprintf("%.1f", w.PacketLoss))

return w.BuildCloudConfig(CloudConfigOpts{
Packages: []string{"iproute-tc"},
WriteFiles: []WriteFile{
{
Path: "/usr/local/bin/virtwork-chaos-network-start.sh",
Content: startScript,
Permissions: "0755",
},
{
Path: "/usr/local/bin/virtwork-chaos-network-stop.sh",
Content: chaosNetworkStopScript,
Permissions: "0755",
},
{
Path: "/etc/systemd/system/virtwork-chaos-network.service",
Content: chaosNetworkSystemdUnit,
Permissions: "0644",
},
},
RunCmd: [][]string{
{"bash", "-c", "dnf install -y kernel-modules-extra-$(uname -r)"},
{"systemctl", "daemon-reload"},
{"systemctl", "enable", "--now", "virtwork-chaos-network.service"},
},
})
}
134 changes: 134 additions & 0 deletions internal/workloads/chaos-network_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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 include iproute-tc package", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
Expect(parsed).To(HaveKey("packages"))
packages := parsed["packages"].([]interface{})
Expect(packages).To(ContainElement("iproute-tc"))
})

It("should include start script, stop script, and 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(3))

filesByPath := map[string]string{}
for _, f := range files {
fm := f.(map[string]interface{})
filesByPath[fm["path"].(string)] = fm["content"].(string)
}

startScript := filesByPath["/usr/local/bin/virtwork-chaos-network-start.sh"]
Expect(startScript).To(ContainSubstring("tc qdisc"))
Expect(startScript).To(ContainSubstring("netem"))
Expect(startScript).To(ContainSubstring("delay 100ms"), "should include default 100ms latency")
Expect(startScript).To(ContainSubstring("loss 5.0%"), "should include default 5% packet loss")
Expect(startScript).To(ContainSubstring("ip route show default"), "should auto-detect interface")
Expect(startScript).To(ContainSubstring("modprobe sch_netem"), "should load netem kernel module")

Expect(filesByPath).To(HaveKey("/usr/local/bin/virtwork-chaos-network-stop.sh"))
Expect(filesByPath).To(HaveKey("/etc/systemd/system/virtwork-chaos-network.service"))
})

It("should install kernel-modules-extra for running kernel before enabling service", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
Expect(parsed).To(HaveKey("runcmd"))
runcmds := parsed["runcmd"].([]interface{})

foundKernelModules := false
foundEnable := false
kernelModulesIdx := -1
enableIdx := -1
for i, cmd := range runcmds {
cmdSlice := cmd.([]interface{})
if len(cmdSlice) >= 3 {
if cmdSlice[0] == "bash" && cmdSlice[1] == "-c" {
cmdStr := cmdSlice[2].(string)
if cmdStr == "dnf install -y kernel-modules-extra-$(uname -r)" {
foundKernelModules = true
kernelModulesIdx = i
}
}
if cmdSlice[0] == "systemctl" && cmdSlice[1] == "enable" {
foundEnable = true
enableIdx = i
Expect(cmdSlice[2]).To(Equal("--now"))
}
}
}
Expect(foundKernelModules).To(BeTrue(), "should install kernel-modules-extra for running kernel")
Expect(foundEnable).To(BeTrue(), "should have systemctl enable command")
Expect(kernelModulesIdx).To(
BeNumerically("<", enableIdx),
"kernel modules install should come before service enable")
})

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))
})
})
3 changes: 3 additions & 0 deletions internal/workloads/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func DefaultRegistry() Registry {
"chaos-process": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload {
return NewChaosProcessWorkload(cfg, opts.SSHUser, opts.SSHPassword, opts.SSHAuthorizedKeys)
},
"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)
},
Expand Down
27 changes: 24 additions & 3 deletions internal/workloads/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ var _ = Describe("Registry", func() {
reg = workloads.DefaultRegistry()
})

It("should have 7 entries registered", func() {
Expect(reg.List()).To(HaveLen(7))
It("should have 8 entries registered", func() {
Expect(reg.List()).To(HaveLen(8))
})

It("should return CPU workload by name", func() {
Expand Down Expand Up @@ -88,11 +88,23 @@ var _ = Describe("Registry", func() {
Expect(w.Name()).To(Equal("chaos-process"))
})

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-process"))
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"))
Expand All @@ -103,7 +115,16 @@ var _ = Describe("Registry", func() {

It("should list all names sorted alphabetically", func() {
names := reg.List()
Expect(names).To(Equal([]string{"chaos-process", "cpu", "database", "disk", "memory", "network", "tps"}))
Expect(names).To(Equal([]string{
"chaos-network",
"chaos-process",
"cpu",
"database",
"disk",
"memory",
"network",
"tps",
}))
})

It("should return tps workload by name", func() {
Expand Down
Loading