Add more disk attack support (#60)

This commit is contained in:
Andrewmatilde 2021-05-10 14:29:52 +08:00 committed by GitHub
parent 5f8e051e28
commit e765b99d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 745 additions and 123 deletions

View File

@ -26,10 +26,10 @@ import (
)
func NewDiskAttackCommand() *cobra.Command {
options := core.NewDiskCommand()
options := core.NewDiskOption()
dep := fx.Options(
server.Module,
fx.Provide(func() *core.DiskCommand {
fx.Provide(func() *core.DiskOption {
return options
}),
)
@ -45,7 +45,7 @@ func NewDiskAttackCommand() *cobra.Command {
return cmd
}
func NewDiskPayloadCommand(dep fx.Option, options *core.DiskCommand) *cobra.Command {
func NewDiskPayloadCommand(dep fx.Option, options *core.DiskOption) *cobra.Command {
cmd := &cobra.Command{
Use: "add-payload <subcommand>",
Short: "add disk payload",
@ -59,7 +59,7 @@ func NewDiskPayloadCommand(dep fx.Option, options *core.DiskCommand) *cobra.Comm
return cmd
}
func NewDiskWritePayloadCommand(dep fx.Option, options *core.DiskCommand) *cobra.Command {
func NewDiskWritePayloadCommand(dep fx.Option, options *core.DiskOption) *cobra.Command {
cmd := &cobra.Command{
Use: "write",
Short: "write payload",
@ -70,14 +70,19 @@ func NewDiskWritePayloadCommand(dep fx.Option, options *core.DiskCommand) *cobra
}
cmd.Flags().StringVarP(&options.Size, "size", "s", "",
"'size' specifies how many data will fill in the file path with unit MB.")
cmd.Flags().StringVarP(&options.Path, "path", "p", "/dev/null",
"'path' specifies the location to fill data in.\n"+
"If path not provided, payload will write into /dev/null")
"'size' specifies how many units of data will write into the file path."+
"'unit' specifies the unit of data, support c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000,"+
"M=1024*1024, , GB=1000*1000*1000, G=1024*1024*1024 BYTES"+
"example : 1M | 512kB")
cmd.Flags().StringVarP(&options.Path, "path", "p", "",
"'path' specifies the location to fill data in."+
"If path not provided, payload will write into a temp file, temp file will be deleted after writing")
cmd.Flags().Uint8VarP(&options.PayloadProcessNum, "process-num", "n", 1,
"'process-num' specifies the number of process work on writing , default 1, only 1-255 is valid value")
return cmd
}
func NewDiskReadPayloadCommand(dep fx.Option, options *core.DiskCommand) *cobra.Command {
func NewDiskReadPayloadCommand(dep fx.Option, options *core.DiskOption) *cobra.Command {
cmd := &cobra.Command{
Use: "read",
Short: "read payload",
@ -88,14 +93,19 @@ func NewDiskReadPayloadCommand(dep fx.Option, options *core.DiskCommand) *cobra.
}
cmd.Flags().StringVarP(&options.Size, "size", "s", "",
"'size' specifies how many data will read from the file path with unit MB.")
"'size' specifies how many units of data will read from the file path."+
"'unit' specifies the unit of data, support c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000,"+
"M=1024*1024, , GB=1000*1000*1000, G=1024*1024*1024 BYTES"+
"example : 1M | 512kB")
cmd.Flags().StringVarP(&options.Path, "path", "p", "",
"'path' specifies the location to read data.\n"+
"If path not provided, payload will raise an error")
"'path' specifies the location to read data."+
"If path not provided, payload will read from disk mount on \"/\"")
cmd.Flags().Uint8VarP(&options.PayloadProcessNum, "process-num", "n", 1,
"'process-num' specifies the number of process work on reading , default 1, only 1-255 is valid value")
return cmd
}
func NewDiskFillCommand(dep fx.Option, options *core.DiskCommand) *cobra.Command {
func NewDiskFillCommand(dep fx.Option, options *core.DiskOption) *cobra.Command {
cmd := &cobra.Command{
Use: "fill",
Short: "fill disk",
@ -106,17 +116,21 @@ func NewDiskFillCommand(dep fx.Option, options *core.DiskCommand) *cobra.Command
}
cmd.Flags().StringVarP(&options.Size, "size", "s", "",
"'size' specifies how many data will fill in the file path with unit MB.")
"'size' specifies how many units of data will fill in the file path."+
"'unit' specifies the unit of data, support c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000,"+
"M=1024*1024, , GB=1000*1000*1000, G=1024*1024*1024 BYTES"+
"example : 1M | 512kB")
cmd.Flags().StringVarP(&options.Path, "path", "p", "",
"'path' specifies the location to fill data in.\n"+
"'path' specifies the location to fill data in."+
"If path not provided, a temp file will be generated and deleted immediately after data filled in or allocated")
cmd.Flags().StringVarP(&options.Percent, "percent", "c", "",
"'percent' how many percent data of disk will fill in the file path")
cmd.Flags().BoolVarP(&options.FillByFallocate, "fallocate", "f", true, "fill disk by fallocate instead of dd")
cmd.Flags().BoolVarP(&options.DestroyFile, "destroy", "d", false, "destroy file after filled in or allocated")
return cmd
}
func processDiskAttack(options *core.DiskCommand, chaos *chaosd.Server) {
func processDiskAttack(options *core.DiskOption, chaos *chaosd.Server) {
if err := options.Validate(); err != nil {
utils.ExitWithError(utils.ExitBadArgs, err)
}

View File

@ -15,20 +15,21 @@ package attack
import (
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"github.com/chaos-mesh/chaosd/cmd/server"
"github.com/chaos-mesh/chaosd/pkg/core"
"github.com/chaos-mesh/chaosd/pkg/server/chaosd"
"github.com/chaos-mesh/chaosd/pkg/utils"
)
type diskTest struct {
name string
command *core.DiskCommand
option *core.DiskOption
wantErr bool
}
@ -40,26 +41,28 @@ func TestServer_DiskFill(t *testing.T) {
return []diskTest{
{
name: "0",
command: &core.DiskCommand{
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: "1024",
Path: "temp",
FillByFallocate: true,
Size: "1024M",
Path: "temp",
FillByFallocate: true,
PayloadProcessNum: 1,
},
wantErr: false,
}, {
name: "1",
command: &core.DiskCommand{
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: "24",
Path: "temp",
FillByFallocate: false,
Size: "24MB",
Path: "temp",
FillByFallocate: false,
PayloadProcessNum: 1,
},
wantErr: false,
},
@ -68,7 +71,7 @@ func TestServer_DiskFill(t *testing.T) {
fx.Invoke(func(s *chaosd.Server, tests []diskTest) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Create(tt.command.Path)
f, err := os.Create(tt.option.Path)
if err != nil {
t.Errorf("unexpected err %v when creating temp file", err)
return
@ -76,24 +79,23 @@ func TestServer_DiskFill(t *testing.T) {
if f != nil {
_ = f.Close()
}
_, err = s.ExecuteAttack(chaosd.DiskAttack, tt.command)
_, err = s.ExecuteAttack(chaosd.DiskAttack, tt.option)
if (err != nil) != tt.wantErr {
t.Errorf("DiskFill() error = %v, wantErr %v", err, tt.wantErr)
return
}
stat, err := os.Stat(tt.command.Path)
stat, err := os.Stat(tt.option.Path)
if err != nil {
t.Errorf("unexpected err %v when stat temp file", err)
return
}
size, _ := strconv.ParseUint(tt.command.Size, 10, 0)
if uint64(stat.Size()) != size*1024*1024 {
t.Errorf("DiskFill() size %v, expect %d", stat.Size(), size*1024*1024)
size, _ := utils.ParseUnit(tt.option.Size)
if stat.Size() != int64(size) {
t.Errorf("DiskFill() size %v, expect %d", stat.Size(), size)
return
}
os.Remove(tt.command.Path)
os.Remove(tt.option.Path)
})
}
}),
@ -108,24 +110,26 @@ func TestServer_DiskPayload(t *testing.T) {
return []diskTest{
{
name: "0",
command: &core.DiskCommand{
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskWritePayloadAction,
Kind: core.DiskAttack,
},
Size: "24",
Path: "temp",
Size: "24M",
Path: "temp",
PayloadProcessNum: 1,
},
wantErr: false,
}, {
name: "1",
command: &core.DiskCommand{
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskReadPayloadAction,
Kind: core.DiskAttack,
},
Size: "24",
Path: "temp",
Size: "24M",
Path: "temp",
PayloadProcessNum: 1,
},
wantErr: false,
},
@ -134,7 +138,7 @@ func TestServer_DiskPayload(t *testing.T) {
fx.Invoke(func(s *chaosd.Server, tests []diskTest) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Create(tt.command.Path)
f, err := os.Create(tt.option.Path)
if err != nil {
t.Errorf("unexpected err %v when creating temp file", err)
return
@ -143,23 +147,191 @@ func TestServer_DiskPayload(t *testing.T) {
_ = f.Close()
}
_, err = s.ExecuteAttack(chaosd.DiskAttack, &core.DiskCommand{
_, err = s.ExecuteAttack(chaosd.DiskAttack, &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: tt.command.Size,
Size: tt.option.Size,
Path: "temp",
FillByFallocate: true,
})
_, err = s.ExecuteAttack(chaosd.DiskAttack, tt.command)
_, err = s.ExecuteAttack(chaosd.DiskAttack, tt.option)
if (err != nil) != tt.wantErr {
t.Errorf("DiskPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
os.Remove(tt.command.Path)
os.Remove(tt.option.Path)
})
}
}),
)
}
type writeArgs struct {
Size string
Path string
PayloadProcessNum uint8
}
func writeArgsToDiskOption(args writeArgs) core.DiskOption {
return core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
SchedulerConfig: core.SchedulerConfig{},
Action: core.DiskWritePayloadAction,
Kind: "",
},
Size: args.Size,
Path: args.Path,
Percent: "",
FillByFallocate: false,
DestroyFile: false,
PayloadProcessNum: args.PayloadProcessNum,
}
}
func writeArgsAttack(args writeArgs) error {
opt := writeArgsToDiskOption(args)
return chaosd.DiskAttack.Attack(&opt, chaosd.Environment{})
}
func TestNewDiskWritePayloadCommand(t *testing.T) {
var opt core.DiskOption
var err error
opt = writeArgsToDiskOption(writeArgs{
Size: "",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "one of percent and size must not be empty, DiskOption : write-payload")
opt = writeArgsToDiskOption(writeArgs{
Size: "1Ms",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unknown units of size : 1Ms, DiskOption : write-payload")
opt = writeArgsToDiskOption(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unsupport process num : 0, DiskOption : write-payload")
opt = writeArgsToDiskOption(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 1,
})
err = opt.Validate()
assert.NoError(t, err)
assert.NoError(t, writeArgsAttack(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 1,
}))
assert.NoError(t, writeArgsAttack(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 255,
}))
assert.NoError(t, writeArgsAttack(writeArgs{
Size: "1",
Path: "",
PayloadProcessNum: 2,
}))
assert.Error(t, writeArgsAttack(writeArgs{
Size: "1",
Path: "&^%$#@#$%^&*(",
PayloadProcessNum: 5,
}))
}
type readArgs struct {
Size string
Path string
PayloadProcessNum uint8
}
func readArgsToDiskOption(args readArgs) core.DiskOption {
return core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
SchedulerConfig: core.SchedulerConfig{},
Action: core.DiskReadPayloadAction,
Kind: "",
},
Size: args.Size,
Path: args.Path,
Percent: "",
FillByFallocate: false,
DestroyFile: false,
PayloadProcessNum: args.PayloadProcessNum,
}
}
func readArgsAttack(args readArgs) error {
opt := readArgsToDiskOption(args)
return chaosd.DiskAttack.Attack(&opt, chaosd.Environment{})
}
func TestNewDiskReadPayloadCommand(t *testing.T) {
var opt core.DiskOption
var err error
opt = readArgsToDiskOption(readArgs{
Size: "",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "one of percent and size must not be empty, DiskOption : read-payload")
opt = readArgsToDiskOption(readArgs{
Size: "1Ms",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unknown units of size : 1Ms, DiskOption : read-payload")
opt = readArgsToDiskOption(readArgs{
Size: "0",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unsupport process num : 0, DiskOption : read-payload")
opt = readArgsToDiskOption(readArgs{
Size: "0",
Path: "",
PayloadProcessNum: 1,
})
err = opt.Validate()
assert.NoError(t, err)
assert.NoError(t, readArgsAttack(readArgs{
Size: "0",
Path: "/dev/zero",
PayloadProcessNum: 1,
}))
assert.NoError(t, readArgsAttack(readArgs{
Size: "1",
Path: "/dev/zero",
PayloadProcessNum: 2,
}))
assert.NoError(t, readArgsAttack(readArgs{
Size: "100GB",
Path: "/dev/zero",
PayloadProcessNum: 1,
}))
}

4
go.mod
View File

@ -2,12 +2,14 @@ module github.com/chaos-mesh/chaosd
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf
github.com/chaos-mesh/chaos-mesh v0.9.1-0.20210329064057-23471399d8f4
github.com/chaos-mesh/chaos-mesh/api/v1alpha1 v0.0.0
github.com/containerd/containerd v1.2.3
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
github.com/gin-gonic/gin v1.6.3
github.com/google/uuid v1.1.1
github.com/hashicorp/go-multierror v1.1.0
github.com/joomcode/errorx v1.0.1
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
github.com/olekukonko/tablewriter v0.0.4
@ -20,7 +22,7 @@ require (
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7
github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 // indirect
github.com/stretchr/testify v1.7.0
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/gin-swagger v1.2.0
github.com/swaggo/swag v1.6.7

3
go.sum
View File

@ -56,6 +56,7 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
@ -415,11 +416,13 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway v1.14.1/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=

View File

@ -17,6 +17,8 @@ import (
"encoding/json"
"fmt"
"strconv"
"github.com/chaos-mesh/chaosd/pkg/utils"
)
const (
@ -25,40 +27,56 @@ const (
DiskReadPayloadAction = "read-payload"
)
type DiskCommand struct {
type DiskOption struct {
CommonAttackConfig
Size string `json:"size"`
Path string `json:"path"`
Percent string `json:"percent"`
FillByFallocate bool `json:"fill_by_fallocate"`
Size string `json:"size"`
Path string `json:"path"`
Percent string `json:"percent"`
FillByFallocate bool `json:"fill_by_fallocate"`
DestroyFile bool `json:"destroy_file"`
PayloadProcessNum uint8 `json:"payload_process_num"`
}
var _ AttackConfig = &DiskCommand{}
var _ AttackConfig = &DiskOption{}
func (d *DiskCommand) Validate() error {
if d.Percent == "" && d.Size == "" {
return fmt.Errorf("one of percent and size must not be empty, DiskCommand : %v", d)
func (d *DiskOption) Validate() error {
var byteSize uint64
var err error
if d.Size == "" {
if d.Percent == "" {
return fmt.Errorf("one of percent and size must not be empty, DiskOption : %v", d)
}
if byteSize, err = strconv.ParseUint(d.Percent, 10, 0); err != nil {
return fmt.Errorf("unsupport percent : %s, DiskOption : %v", d.Percent, d)
}
} else {
if byteSize, err = utils.ParseUnit(d.Size); err != nil {
return fmt.Errorf("unknown units of size : %s, DiskOption : %v", d.Size, d)
}
}
if d.FillByFallocate && (d.Size == "0" || (d.Size == "" && d.Percent == "0")) {
return fmt.Errorf("fallocate not suppurt 0 size or 0 percent data, "+
"if you want allocate a 0 size file please set fallocate=false, DiskCommand : %v", d)
if d.Action == DiskFillAction {
if d.FillByFallocate && byteSize == 0 {
return fmt.Errorf("fallocate not suppurt 0 size or 0 percent data, "+
"if you want allocate a 0 size file please set fallocate=false, DiskOption : %v", d)
}
}
_, err := strconv.ParseUint(d.Percent, 10, 0)
if d.Size == "" && err != nil {
return fmt.Errorf("unsupport percent : %s, DiskCommand : %v", d.Percent, d)
if d.PayloadProcessNum == 0 {
return fmt.Errorf("unsupport process num : %d, DiskOption : %v", d.PayloadProcessNum, d.Action)
}
return nil
}
func (d DiskCommand) RecoverData() string {
func (d DiskOption) RecoverData() string {
data, _ := json.Marshal(d)
return string(data)
}
func NewDiskCommand() *DiskCommand {
return &DiskCommand{
func NewDiskOption() *DiskOption {
return &DiskOption{
CommonAttackConfig: CommonAttackConfig{
Kind: DiskAttack,
},

View File

@ -15,13 +15,14 @@ package chaosd
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/pingcap/errors"
"github.com/pingcap/log"
"go.uber.org/zap"
@ -35,87 +36,151 @@ type diskAttack struct{}
var DiskAttack AttackType = diskAttack{}
const DDWritePayloadCommand = "dd if=/dev/zero of=%s bs=%s count=%s oflag=dsync"
const DDReadPayloadCommand = "dd if=%s of=/dev/null bs=%s count=%s iflag=dsync,direct,fullblock"
const DDReadPayloadCommand = "dd if=%s of=/dev/null bs=%s count=%s iflag=dsync,fullblock,nocache"
func (disk diskAttack) Attack(options core.AttackConfig, env Environment) (err error) {
attack := options.(*core.DiskCommand)
attack := options.(*core.DiskOption)
if options.String() == core.DiskFillAction {
return disk.attackDiskFill(attack)
return disk.diskFill(attack)
}
return disk.attackDiskPayload(attack)
return disk.diskPayload(attack)
}
func (diskAttack) attackDiskPayload(payload *core.DiskCommand) error {
func initWritePayloadPath(payload *core.DiskOption) error {
var err error
payload.Path, err = utils.CreateTempFile()
if err != nil {
log.Error(fmt.Sprintf("unexpected err when CreateTempFile in action: %s", payload.Action))
return err
}
return nil
}
func initReadPayloadPath(payload *core.DiskOption) error {
path, err := utils.GetRootDevice()
if err != nil {
log.Error("err when GetRootDevice in reading payload", zap.Error(err))
return err
}
if path == "" {
err = errors.Errorf("can not get root device path")
log.Error(fmt.Sprintf("payload action: %s", payload.Action), zap.Error(err))
return err
}
payload.Path = path
return nil
}
// diskPayload will execute a dd command (DDWritePayloadCommand or DDReadPayloadCommand)
// to add a write or read payload.
func (diskAttack) diskPayload(payload *core.DiskOption) error {
var cmdFormat string
switch payload.Action {
case core.DiskWritePayloadAction:
cmdFormat = DDWritePayloadCommand
if payload.Path == "" {
payload.Path = "/dev/null"
err := initWritePayloadPath(payload)
if err != nil {
return err
}
defer func() {
err := os.Remove(payload.Path)
if err != nil {
log.Error(fmt.Sprintf("unexpected err when removing temp file %s", payload.Path), zap.Error(err))
}
}()
}
cmd := exec.Command("bash", "-c", fmt.Sprintf(DDWritePayloadCommand, payload.Path, "1M", payload.Size))
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(string(output), zap.Error(err))
} else {
log.Info(string(output))
}
return err
case core.DiskReadPayloadAction:
cmdFormat = DDReadPayloadCommand
if payload.Path == "" {
err := errors.Errorf("empty read payload path")
log.Error(fmt.Sprintf("payload action: %s", payload.Action), zap.Error(err))
return err
err := initReadPayloadPath(payload)
if err != nil {
return err
}
}
cmd := exec.Command("bash", "-c", fmt.Sprintf(DDReadPayloadCommand, payload.Path, "1M", payload.Size))
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(string(output), zap.Error(err))
} else {
log.Info(string(output))
}
return err
default:
err := errors.Errorf("invalid payload action")
log.Error(fmt.Sprintf("payload action: %s", payload.Action), zap.Error(err))
return err
}
byteSize, err := utils.ParseUnit(payload.Size)
if err != nil {
log.Error(fmt.Sprintf("fail to get parse size per units , %s", payload.Size), zap.Error(err))
return err
}
ddBlocks, err := utils.SplitBytesByProcessNum(byteSize, payload.PayloadProcessNum)
if err != nil {
log.Error(fmt.Sprintf("split size ,process num %d", payload.PayloadProcessNum), zap.Error(err))
return err
}
if len(ddBlocks) == 0 {
return nil
}
rest := ddBlocks[len(ddBlocks)-1]
ddBlocks = ddBlocks[:len(ddBlocks)-1]
cmd := exec.Command("bash", "-c", fmt.Sprintf(cmdFormat, payload.Path, rest.BlockSize, rest.Count))
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(cmd.String()+string(output), zap.Error(err))
}
log.Info(string(output))
var wg sync.WaitGroup
var mu sync.Mutex
var errs error
wg.Add(len(ddBlocks))
for _, sizeBlock := range ddBlocks {
cmd := exec.Command("bash", "-c", fmt.Sprintf(cmdFormat, payload.Path, sizeBlock.BlockSize, sizeBlock.Count))
go func(cmd *exec.Cmd) {
defer wg.Done()
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(cmd.String()+string(output), zap.Error(err))
mu.Lock()
defer mu.Unlock()
errs = multierror.Append(errs, err)
return
}
log.Info(string(output))
}(cmd)
}
wg.Wait()
if errs != nil {
return errs
}
return nil
}
const DDFillCommand = "dd if=/dev/zero of=%s bs=%s count=%s iflag=fullblock"
const DDFallocateCommand = "fallocate -l %sM %s"
const FallocateCommand = "fallocate -l %s %s"
func (diskAttack) attackDiskFill(fill *core.DiskCommand) error {
// diskFill will execute a dd command (DDFillCommand or FallocateCommand)
// to fill the disk.
func (diskAttack) diskFill(fill *core.DiskOption) error {
if fill.Path == "" {
tempFile, err := ioutil.TempFile("", "example")
var err error
fill.Path, err = utils.CreateTempFile()
if err != nil {
log.Error("unexpected err when open temp file", zap.Error(err))
log.Error(fmt.Sprintf("unexpected err when CreateTempFile in action: %s", fill.Action))
return err
}
if tempFile != nil {
err = tempFile.Close()
if err != nil {
log.Error("unexpected err when close temp file", zap.Error(err))
return err
}
} else {
err := errors.Errorf("unexpected err : file get from ioutil.TempFile is nil")
log.Error(fmt.Sprintf("payload action: %s", fill.Action), zap.Error(err))
return err
}
fill.Path = tempFile.Name()
defer func() {
err := os.Remove(fill.Path)
if err != nil {
log.Error(fmt.Sprintf("unexpected err when removing temp file %s", fill.Path), zap.Error(err))
}
}()
}
var cmd *exec.Cmd
defer func() {
if fill.DestroyFile {
err := os.Remove(fill.Path)
if err != nil {
log.Error(fmt.Sprintf("unexpected err when removing file %s", fill.Path), zap.Error(err))
}
}
}()
if fill.Size != "" {
fill.Size = strings.Trim(fill.Size, " ")
} else if fill.Percent != "" {
@ -131,14 +196,15 @@ func (diskAttack) attackDiskFill(fill *core.DiskCommand) error {
log.Error("fail to get disk total size", zap.Error(err))
return err
}
fill.Size = strconv.FormatUint(totalSize/1024/1024*percent/100, 10)
fill.Size = strconv.FormatUint(totalSize*percent/100, 10)
}
var cmd *exec.Cmd
if fill.FillByFallocate {
cmd = exec.Command("bash", "-c", fmt.Sprintf(DDFallocateCommand, fill.Size, fill.Path))
cmd = exec.Command("bash", "-c", fmt.Sprintf(FallocateCommand, fill.Size, fill.Path))
} else {
//1M means the block size. The bytes size dd read | write is (block size) * (size).
cmd = exec.Command("bash", "-c", fmt.Sprintf(DDFillCommand, fill.Path, "1M", fill.Size))
//1Unit means the block size. The bytes size dd read | write is (block size) * (size).
cmd = exec.Command("bash", "-c", fmt.Sprintf(DDFillCommand, fill.Path, fill.Size, "1"))
}
output, err := cmd.CombinedOutput()

View File

@ -187,13 +187,13 @@ func (s *httpServer) createStressAttack(c *gin.Context) {
// @Description Create disk attack.
// @Tags attack
// @Produce json
// @Param request body core.DiskCommand true "Request body"
// @Param request body core.DiskOption true "Request body"
// @Success 200 {object} utils.Response
// @Failure 400 {object} utils.APIError
// @Failure 500 {object} utils.APIError
// @Router /api/attack/disk [post]
func (s *httpServer) createDiskAttack(c *gin.Context) {
attack := &core.DiskCommand{
attack := &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Kind: core.ProcessAttack,
},

View File

@ -28,3 +28,8 @@ func GetDiskTotalSize(path string) (total uint64, err error) {
total = uint64(s.Bsize) * (s.Blocks - reservedBlocks)
return total, err
}
func GetRootDevice() (string, error) {
// TODO: complete get device of root on darwin
return "", nil
}

View File

@ -15,6 +15,8 @@ package utils
import (
"syscall"
"github.com/shirou/gopsutil/disk"
)
// GetDiskTotalSize returns the total bytes in disk
@ -28,3 +30,17 @@ func GetDiskTotalSize(path string) (total uint64, err error) {
total = uint64(s.Frsize) * (s.Blocks - reservedBlocks)
return total, nil
}
// GetRootDevice returns the device which "/" mount on.
func GetRootDevice() (string, error) {
mapStat, err := disk.Partitions(false)
if err != nil {
return "", err
}
for _, stat := range mapStat {
if stat.Mountpoint == "/" {
return stat.Device, nil
}
}
return "", nil
}

49
pkg/utils/tempfile.go Normal file
View File

@ -0,0 +1,49 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"io/ioutil"
"os"
"github.com/pingcap/errors"
"github.com/pingcap/log"
"go.uber.org/zap"
)
// CreateTempFile will create a temp file in current directory.
func CreateTempFile() (string, error) {
path, err := os.Getwd()
if err != nil {
log.Error("unexpected err when execute os.Getwd()", zap.Error(err))
return "", err
}
tempFile, err := ioutil.TempFile(path, "example")
if err != nil {
log.Error("unexpected err when open temp file", zap.Error(err))
return "", err
}
if tempFile != nil {
err = tempFile.Close()
if err != nil {
log.Error("unexpected err when close temp file", zap.Error(err))
return "", err
}
} else {
err := errors.Errorf("unexpected err : file get from ioutil.TempFile is nil")
return "", err
}
return tempFile.Name(), nil
}

96
pkg/utils/units.go Normal file
View File

@ -0,0 +1,96 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"fmt"
"strconv"
"github.com/alecthomas/units"
)
var (
// See https://en.wikipedia.org/wiki/Binary_prefix
shortBinaryUnitMap = units.MakeUnitMap("", "c", 1024)
binaryUnitMap = units.MakeUnitMap("iB", "c", 1024)
decimalUnitMap = units.MakeUnitMap("B", "c", 1000)
)
// ParseUnit parse a digit with unit such as "K" , "KiB", "KB", "c", "MiB", "MB", "M".
// If input string is a digit without unit ,
// it will be regarded as a digit with unit M(1024*1024 bytes).
func ParseUnit(s string) (uint64, error) {
if _, err := strconv.Atoi(s); err == nil {
s += "M"
}
if n, err := units.ParseUnit(s, shortBinaryUnitMap); err == nil {
return uint64(n), nil
}
if n, err := units.ParseUnit(s, binaryUnitMap); err == nil {
return uint64(n), nil
}
if n, err := units.ParseUnit(s, decimalUnitMap); err == nil {
return uint64(n), nil
}
return 0, fmt.Errorf("units: unknown unit %s", s)
}
// DdArgBlock is command arg for dd. BlockSize is bs.Count is count.
type DdArgBlock struct {
BlockSize string
Count string
}
// This func split bytes in to processNum + 1 dd arg blocks.
// Every ddArgBlock can generate one dd command.
// If bytes is bigger than processNum M ,
// bytes will be split into processNum dd commands with bs = 1M ,count = bytes/ processNum M.
// If bytes is not bigger than processNum M ,
// bytes will be split into processNum dd commands with bs = bytes / uint64(processNum) ,count = 1.
// And one ddArgBlock stand by the rest bytes will also add to the end of slice,
// even if rest bytes = 0.
func SplitBytesByProcessNum(bytes uint64, processNum uint8) ([]DdArgBlock, error) {
if bytes == 0 {
return []DdArgBlock{{
BlockSize: "1M",
Count: "0",
}}, nil
}
if processNum == 0 {
return nil, fmt.Errorf("num must not be zero")
}
ddArgBlocks := make([]DdArgBlock, processNum)
if bytes > uint64(processNum)*(1<<20) {
count := (bytes >> 20) / uint64(processNum)
for i := range ddArgBlocks {
ddArgBlocks[i].Count = strconv.FormatUint(count, 10)
ddArgBlocks[i].BlockSize = "1M"
bytes -= count << 20
}
} else {
blockSize := bytes / uint64(processNum)
for i := range ddArgBlocks {
ddArgBlocks[i].Count = "1"
ddArgBlocks[i].BlockSize = strconv.FormatUint(blockSize, 10) + "c"
bytes -= blockSize
}
}
ddArgBlocks = append(ddArgBlocks, DdArgBlock{
BlockSize: "1",
Count: strconv.FormatUint(bytes, 10) + "c",
})
return ddArgBlocks, nil
}

181
pkg/utils/units_test.go Normal file
View File

@ -0,0 +1,181 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"reflect"
"testing"
)
func TestParseUnit(t *testing.T) {
tests := []struct {
name string
args string
want uint64
wantErr bool
}{
{
name: "0",
args: "0",
want: 0,
wantErr: false,
},
{
name: "1",
args: "1",
want: 1 << 20,
wantErr: false,
},
{
name: "1MS",
args: "1MS",
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseUnit(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("ParseUnit() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParseUnit() got = %v, want %v", got, tt.want)
}
})
}
}
func TestSplitByteSize(t *testing.T) {
type args struct {
b uint64
num uint8
}
tests := []struct {
name string
args args
want []DdArgBlock
wantErr bool
}{
{
name: "0",
args: args{
b: 0,
num: 0,
},
want: []DdArgBlock{
{
BlockSize: "1M",
Count: "0",
},
{
BlockSize: "",
Count: "",
},
},
wantErr: false,
},
{
name: "1",
args: args{
b: 1 << 20,
num: 0,
},
want: nil,
wantErr: true,
},
{
name: "2",
args: args{
b: 1 << 20,
num: 2,
},
want: []DdArgBlock{{
BlockSize: "524288c",
Count: "1",
}, {
BlockSize: "524288c",
Count: "1",
}, {
BlockSize: "1",
Count: "0c",
}},
wantErr: false,
}, {
name: "3",
args: args{
b: 1<<20 + 1,
num: 2,
},
want: []DdArgBlock{{
BlockSize: "524288c",
Count: "1",
}, {
BlockSize: "524288c",
Count: "1",
}, {
BlockSize: "1",
Count: "1c",
}},
wantErr: false,
}, {
name: "4",
args: args{
b: 5 << 20,
num: 2,
},
want: []DdArgBlock{{
BlockSize: "1M",
Count: "2",
}, {
BlockSize: "1M",
Count: "2",
}, {
BlockSize: "1",
Count: "1048576c",
}},
wantErr: false,
}, {
name: "5",
args: args{
b: 5<<20 + 1,
num: 2,
},
want: []DdArgBlock{{
BlockSize: "1M",
Count: "2",
}, {
BlockSize: "1M",
Count: "2",
}, {
BlockSize: "1",
Count: "1048577c",
}},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SplitBytesByProcessNum(tt.args.b, tt.args.num)
if (err != nil) != tt.wantErr {
t.Errorf("SplitBytesByProcessNum() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SplitBytesByProcessNum() got = %v, want %v", got, tt.want)
}
})
}
}