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 10 VMs: chaos-process=1 + chaos-network=1 + cpu=1 + memory=1 + disk=1 + database=1 + network=2 + tps=2
// Default run creates 11 VMs: chaos-disk=1 + chaos-network=1 + chaos-process=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()
}
// chaos-process=1 + chaos-network=1 + cpu=1 + database=1 + disk=1 + memory=1 + network=2 + tps=2 = 10
Expect(totalVMs).To(Equal(10))
// chaos-disk=1 + chaos-network=1 + chaos-process=1 + cpu=1 + database=1 + disk=1 + memory=1 + network=2 + tps=2 = 11
Expect(totalVMs).To(Equal(11))
})
})

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

package workloads

import (
kubevirtv1 "kubevirt.io/api/core/v1"

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

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

MOUNT_POINT="${CHAOS_DISK_MOUNT:-/mnt/data}"
FILL_PERCENT="${CHAOS_DISK_FILL_PERCENT:-90}"
FILL_FILE="${MOUNT_POINT}/chaos-disk-fill"
RELEASE_SLEEP="${CHAOS_DISK_RELEASE_SLEEP:-30}"
FILL_SLEEP="${CHAOS_DISK_FILL_SLEEP:-60}"

while true; do
TOTAL_KB=$(df -k "${MOUNT_POINT}" | awk 'NR==2 {print $2}')
USED_KB=$(df -k "${MOUNT_POINT}" | awk 'NR==2 {print $3}')
TARGET_KB=$(( TOTAL_KB * FILL_PERCENT / 100 ))
FILL_KB=$(( TARGET_KB - USED_KB ))

if [ "${FILL_KB}" -gt 0 ]; then
fallocate -l "${FILL_KB}K" "${FILL_FILE}" 2>/dev/null || \
dd if=/dev/zero of="${FILL_FILE}" bs=1K count="${FILL_KB}" status=none
fi

sleep "${FILL_SLEEP}"

rm -f "${FILL_FILE}"
sleep "${RELEASE_SLEEP}"
done
`

const chaosDiskSystemdUnit = `[Unit]
Description=Virtwork chaos-disk fill/release workload
After=network.target local-fs.target

[Service]
Type=simple
ExecStart=/usr/local/bin/chaos-disk.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
`

// ChaosDiskWorkload generates cloud-init userdata for a disk fill/release chaos
// workload using fallocate and dd.
type ChaosDiskWorkload struct {
BaseWorkload
DataDiskSize string
}

// NewChaosDiskWorkload creates a ChaosDiskWorkload with the given configuration,
// disk size, and SSH credentials.
func NewChaosDiskWorkload(
cfg config.WorkloadConfig,
dataDiskSize, sshUser, sshPassword string,
sshKeys []string,
) *ChaosDiskWorkload {
return &ChaosDiskWorkload{
BaseWorkload: BaseWorkload{
Config: cfg,
SSHUser: sshUser,
SSHPassword: sshPassword,
SSHAuthorizedKeys: sshKeys,
},
DataDiskSize: dataDiskSize,
}
}

// Name returns "chaos-disk".
func (w *ChaosDiskWorkload) Name() string {
return "chaos-disk"
}

// CloudInitUserdata returns cloud-init YAML that writes a fill/release script
// and a systemd service that runs it in a loop.
func (w *ChaosDiskWorkload) CloudInitUserdata() (string, error) {
return w.BuildCloudConfig(CloudConfigOpts{
WriteFiles: []WriteFile{
{
Path: "/usr/local/bin/chaos-disk.sh",
Content: chaosDiskScript,
Permissions: "0755",
},
{
Path: "/etc/systemd/system/virtwork-chaos-disk.service",
Content: chaosDiskSystemdUnit,
Permissions: "0644",
},
},
RunCmd: [][]string{
{"mkdir", "-p", "/mnt/data"},
{"systemctl", "daemon-reload"},
{"systemctl", "enable", "--now", "virtwork-chaos-disk.service"},
},
})
}

// DataVolumeTemplates returns a DataVolumeTemplateSpec for the data disk.
func (w *ChaosDiskWorkload) DataVolumeTemplates() []kubevirtv1.DataVolumeTemplateSpec {
return []kubevirtv1.DataVolumeTemplateSpec{
vm.BuildDataVolumeTemplate("virtwork-chaos-disk-data", w.DataDiskSize),
}
}

// ExtraDisks returns the data disk definition.
func (w *ChaosDiskWorkload) ExtraDisks() []kubevirtv1.Disk {
return []kubevirtv1.Disk{
{
Name: "datadisk",
DiskDevice: kubevirtv1.DiskDevice{
Disk: &kubevirtv1.DiskTarget{
Bus: "virtio",
},
},
},
}
}

// ExtraVolumes returns the data volume sourced from the DataVolume.
func (w *ChaosDiskWorkload) ExtraVolumes() []kubevirtv1.Volume {
return []kubevirtv1.Volume{
{
Name: "datadisk",
VolumeSource: kubevirtv1.VolumeSource{
DataVolume: &kubevirtv1.DataVolumeSource{
Name: "virtwork-chaos-disk-data",
},
},
},
}
}
172 changes: 172 additions & 0 deletions internal/workloads/chaos_disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// 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("ChaosDiskWorkload", func() {
var w *workloads.ChaosDiskWorkload

BeforeEach(func() {
w = workloads.NewChaosDiskWorkload(config.WorkloadConfig{
Enabled: true,
VMCount: 1,
CPUCores: 2,
Memory: "2Gi",
}, "10Gi", "virtwork", "", nil)
})

It("should return 'chaos-disk' for Name", func() {
Expect(w.Name()).To(Equal("chaos-disk"))
})

It("should include chaos-disk script in write_files", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

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

paths := make([]string, len(files))
for i, f := range files {
paths[i] = f.(map[string]interface{})["path"].(string)
}
Expect(paths).To(ContainElement("/usr/local/bin/chaos-disk.sh"))
Expect(paths).To(ContainElement("/etc/systemd/system/virtwork-chaos-disk.service"))
})

It("should include fallocate in the chaos-disk script", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
files := parsed["write_files"].([]interface{})

var scriptContent string
for _, f := range files {
fm := f.(map[string]interface{})
if fm["path"] == "/usr/local/bin/chaos-disk.sh" {
scriptContent = fm["content"].(string)
break
}
}
Expect(scriptContent).NotTo(BeEmpty())
Expect(scriptContent).To(ContainSubstring("fallocate"))
Expect(scriptContent).To(ContainSubstring("FILL_PERCENT"))
Expect(scriptContent).To(ContainSubstring("MOUNT_POINT"))
})

It("should make the chaos-disk script executable", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
files := parsed["write_files"].([]interface{})

for _, f := range files {
fm := f.(map[string]interface{})
if fm["path"] == "/usr/local/bin/chaos-disk.sh" {
Expect(fm["permissions"]).To(Equal("0755"))
return
}
}
Fail("chaos-disk.sh not found in write_files")
})

It("should include systemd unit referencing the script", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
files := parsed["write_files"].([]interface{})

for _, f := range files {
fm := f.(map[string]interface{})
if fm["path"] == "/etc/systemd/system/virtwork-chaos-disk.service" {
content := fm["content"].(string)
Expect(content).To(ContainSubstring("/usr/local/bin/chaos-disk.sh"))
Expect(content).To(ContainSubstring("Restart=always"))
return
}
}
Fail("systemd unit not found in write_files")
})

It("should produce valid YAML with cloud-config header", 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 data volume template", func() {
dvts := w.DataVolumeTemplates()
Expect(dvts).To(HaveLen(1))
Expect(dvts[0].Name).To(Equal("virtwork-chaos-disk-data"))
})

It("should have extra disk for data volume", func() {
disks := w.ExtraDisks()
Expect(disks).To(HaveLen(1))
Expect(disks[0].Name).To(Equal("datadisk"))

volumes := w.ExtraVolumes()
Expect(volumes).To(HaveLen(1))
Expect(volumes[0].Name).To(Equal("datadisk"))
})

It("should not require a 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(2))
Expect(res.Memory).To(Equal("2Gi"))
})

It("should not require packages (tools pre-installed via golden image)", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
_, hasPackages := parsed["packages"]
Expect(hasPackages).To(BeFalse())
})

It("should mount data disk and enable service via runcmd", func() {
result, err := w.CloudInitUserdata()
Expect(err).NotTo(HaveOccurred())

parsed := parseYAML(result)
Expect(parsed).To(HaveKey("runcmd"))

cmds := parsed["runcmd"].([]interface{})
cmdStrings := make([]string, 0)
for _, c := range cmds {
parts := c.([]interface{})
s := ""
for _, p := range parts {
if s != "" {
s += " "
}
s += p.(string)
}
cmdStrings = append(cmdStrings, s)
}

Expect(cmdStrings).To(ContainElement(ContainSubstring("daemon-reload")))
Expect(cmdStrings).To(ContainElement(ContainSubstring("virtwork-chaos-disk.service")))
})
})
3 changes: 3 additions & 0 deletions internal/workloads/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ func DefaultRegistry() Registry {
"chaos-network": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload {
return NewChaosNetworkWorkload(cfg, opts.SSHUser, opts.SSHPassword, opts.SSHAuthorizedKeys)
},
"chaos-disk": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload {
return NewChaosDiskWorkload(cfg, opts.DataDiskSize, 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
19 changes: 16 additions & 3 deletions internal/workloads/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@ var _ = Describe("Registry", func() {
reg = workloads.DefaultRegistry()
})

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

It("should return chaos-disk workload by name", func() {
w, err := reg.Get("chaos-disk", config.WorkloadConfig{
Enabled: true,
VMCount: 1,
CPUCores: 2,
Memory: "2Gi",
})
Expect(err).NotTo(HaveOccurred())
Expect(w.Name()).To(Equal("chaos-disk"))
})

It("should return CPU workload by name", func() {
Expand Down Expand Up @@ -103,8 +114,9 @@ var _ = Describe("Registry", 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-disk"))
Expect(err.Error()).To(ContainSubstring("chaos-network"))
Expect(err.Error()).To(ContainSubstring("chaos-process"))
Expect(err.Error()).To(ContainSubstring("cpu"))
Expect(err.Error()).To(ContainSubstring("database"))
Expect(err.Error()).To(ContainSubstring("disk"))
Expand All @@ -116,6 +128,7 @@ var _ = Describe("Registry", func() {
It("should list all names sorted alphabetically", func() {
names := reg.List()
Expect(names).To(Equal([]string{
"chaos-disk",
"chaos-network",
"chaos-process",
"cpu",
Expand Down
Loading