Skip to content
Closed
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
4 changes: 2 additions & 2 deletions cmd/virtwork/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
})

Expand Down
83 changes: 83 additions & 0 deletions internal/workloads/chaos-network.go
Original file line number Diff line number Diff line change
@@ -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 {

Check failure on line 37 in internal/workloads/chaos-network.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (golines)
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"},
},
})
}
111 changes: 111 additions & 0 deletions internal/workloads/chaos-network_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
})
5 changes: 4 additions & 1 deletion internal/workloads/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down
22 changes: 17 additions & 5 deletions internal/workloads/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
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() {
Expand Down Expand Up @@ -77,10 +77,22 @@
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"))
Expand All @@ -90,7 +102,7 @@

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() {
Expand Down Expand Up @@ -157,7 +169,7 @@
})

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"}))

Check failure on line 173 in internal/workloads/registry_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (golines)
})
})
Loading