factor disk attack&add schedule support for disk read&write (#77)

* fix bugs in SplitBytesByProcessNum & add overwrite control

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* delete overwrite control

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* add recover for disk&&delete fill destory

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* patch

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* factor disk attack

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* add schedule support for disk read&write

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* remove some test & add new unit test & fix some bug
Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* fix some mistakes in log

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* fix unsupportted unit `c` in fallocate.

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* roll back scheduler

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* add comment

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* save some indentation

Signed-off-by: Andrewmatilde <davis6813585853062@outlook.com>

* add a comment

Signed-off-by: andrewmatilde <davis6813585853062@outlook.com>
This commit is contained in:
Andrewmatilde 2021-09-08 18:26:53 +08:00 committed by GitHub
parent c2fddc2942
commit 352ba5a47a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 603 additions and 576 deletions

View File

@ -13,7 +13,11 @@
package attack
import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
"github.com/chaos-mesh/chaosd/pkg/core"
)
func NewAttackCommand() *cobra.Command {
cmd := &cobra.Command{
@ -35,3 +39,8 @@ func NewAttackCommand() *cobra.Command {
return cmd
}
func SetScheduleFlags(cmd *cobra.Command, conf *core.SchedulerConfig) {
cmd.Flags().StringVar(&conf.Duration, "duration", "",
`Work duration of attacks.A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`)
}

View File

@ -80,6 +80,7 @@ func NewDiskWritePayloadCommand(dep fx.Option, options *core.DiskOption) *cobra.
"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")
SetScheduleFlags(cmd, &options.SchedulerConfig)
return cmd
}
@ -103,6 +104,7 @@ func NewDiskReadPayloadCommand(dep fx.Option, options *core.DiskOption) *cobra.C
"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")
SetScheduleFlags(cmd, &options.SchedulerConfig)
return cmd
}
@ -126,15 +128,17 @@ func NewDiskFillCommand(dep fx.Option, options *core.DiskOption) *cobra.Command
"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.FillByFAllocate, "fallocate", "f", true, "fill disk by fallocate instead of dd")
return cmd
}
func processDiskAttack(options *core.DiskOption, chaos *chaosd.Server) {
if err := options.Validate(); err != nil {
attackConfig, err := options.PreProcess()
if err != nil {
utils.ExitWithError(utils.ExitBadArgs, err)
}
uid, err := chaos.ExecuteAttack(chaosd.DiskAttack, options, core.CommandMode)
uid, err := chaos.ExecuteAttack(chaosd.DiskAttack, attackConfig, core.CommandMode)
if err != nil {
utils.ExitWithError(utils.ExitError, err)
}

View File

@ -1,323 +0,0 @@
// 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 attack
import (
"os"
"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
option *core.DiskOption
wantErr bool
}
func TestServer_DiskFill(t *testing.T) {
fxtest.New(
t,
server.Module,
fx.Provide(func() []diskTest {
return []diskTest{
{
name: "0",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: "1024M",
Path: "./temp",
FillByFallocate: true,
PayloadProcessNum: 1,
},
wantErr: false,
}, {
name: "1",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: "24MB",
Path: "./temp",
FillByFallocate: false,
PayloadProcessNum: 1,
},
wantErr: false,
},
}
}),
fx.Invoke(func(s *chaosd.Server, tests []diskTest) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := s.ExecuteAttack(chaosd.DiskAttack, tt.option, core.CommandMode)
if (err != nil) != tt.wantErr {
t.Errorf("DiskFill() error = %v, wantErr %v", err, tt.wantErr)
return
}
stat, err := os.Stat(tt.option.Path)
if err != nil {
t.Errorf("unexpected err %v when stat temp file", err)
return
}
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.option.Path)
})
}
}),
)
}
func TestServer_DiskPayload(t *testing.T) {
fxtest.New(
t,
server.Module,
fx.Provide(func() []diskTest {
return []diskTest{
{
name: "0",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskWritePayloadAction,
Kind: core.DiskAttack,
},
Size: "24M",
Path: "./temp",
PayloadProcessNum: 1,
},
wantErr: false,
}, {
name: "1",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskReadPayloadAction,
Kind: core.DiskAttack,
},
Size: "24M",
Path: "./temp",
PayloadProcessNum: 1,
},
wantErr: false,
},
}
}),
fx.Invoke(func(s *chaosd.Server, tests []diskTest) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := s.ExecuteAttack(chaosd.DiskAttack, &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
PayloadProcessNum: 1,
Size: tt.option.Size,
Path: "./temp",
FillByFallocate: true,
}, core.CommandMode)
if err != nil {
t.Error(err)
return
}
_, err = s.ExecuteAttack(chaosd.DiskAttack, tt.option, core.CommandMode)
if (err != nil) != tt.wantErr {
t.Errorf("DiskPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
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,
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,
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,
}))
}

View File

@ -18,7 +18,12 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pingcap/log"
"go.uber.org/zap"
"github.com/chaos-mesh/chaosd/pkg/utils"
)
@ -29,6 +34,41 @@ const (
DiskReadPayloadAction = "read-payload"
)
var _ AttackConfig = &DiskAttackConfig{}
type DiskAttackConfig struct {
CommonAttackConfig
DdOptions *[]DdOption
FAllocateOption *FAllocateOption
Path string
}
func (d DiskAttackConfig) RecoverData() string {
data, _ := json.Marshal(d)
return string(data)
}
var DdCommand = utils.Command{Name: "dd"}
type DdOption struct {
ReadPath string `dd:"if"`
WritePath string `dd:"of"`
BlockSize string `dd:"bs"`
Count string `dd:"count"`
Iflag string `dd:"iflag"`
Oflag string `dd:"oflag"`
Conv string `dd:"conv"`
}
var FAllocateCommand = utils.Command{Name: "fallocate"}
type FAllocateOption struct {
LengthOpt string `fallocate:"-"`
Length string `fallocate:"-"`
FileName string `fallocate:"-"`
}
type DiskOption struct {
CommonAttackConfig
@ -37,71 +77,7 @@ type DiskOption struct {
Percent string `json:"percent"`
PayloadProcessNum uint8 `json:"payload_process_num"`
FillByFallocate bool `json:"fill_by_fallocate"`
}
var _ AttackConfig = &DiskOption{}
func (d *DiskOption) Validate() error {
if err := d.CommonAttackConfig.Validate(); err != nil {
return err
}
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.Action == DiskFillAction || d.Action == DiskWritePayloadAction {
if d.Action == DiskFillAction && 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 := os.Stat(d.Path)
if err != nil {
if os.IsNotExist(err) {
// check if Path of file is valid when Path is not empty
if d.Path != "" {
var b []byte
if err := ioutil.WriteFile(d.Path, b, 0644); err != nil {
return err
}
if err := os.Remove(d.Path); err != nil {
return err
}
}
} else {
return err
}
} else {
if d.Action == DiskFillAction {
return fmt.Errorf("fill into an existing file")
}
return fmt.Errorf("write into an existing file")
}
}
if d.PayloadProcessNum == 0 {
return fmt.Errorf("unsupport process num : %d, DiskOption : %v", d.PayloadProcessNum, d.Action)
}
return nil
}
func (d DiskOption) RecoverData() string {
data, _ := json.Marshal(d)
return string(data)
FillByFAllocate bool `json:"fill_by_fallocate"`
}
func NewDiskOption() *DiskOption {
@ -111,3 +87,186 @@ func NewDiskOption() *DiskOption {
},
}
}
func (opt *DiskOption) PreProcess() (*DiskAttackConfig, error) {
if err := opt.CommonAttackConfig.Validate(); err != nil {
return nil, err
}
path, err := initPath(opt)
if err != nil {
return nil, err
}
byteSize, err := initSize(opt)
if err != nil {
return nil, err
}
if opt.Action == DiskFillAction && opt.FillByFAllocate && byteSize != 0 {
return &DiskAttackConfig{
CommonAttackConfig: opt.CommonAttackConfig,
DdOptions: nil,
FAllocateOption: &FAllocateOption{
LengthOpt: "-l",
Length: strconv.FormatUint(byteSize, 10),
FileName: path,
},
Path: path,
}, nil
}
ddOptions, err := initDdOptions(opt, path, byteSize)
if err != nil {
return nil, err
}
return &DiskAttackConfig{
CommonAttackConfig: opt.CommonAttackConfig,
DdOptions: &ddOptions,
FAllocateOption: nil,
Path: path,
}, nil
}
func initDdOptions(opt *DiskOption, path string, byteSize uint64) ([]DdOption, error) {
ddBlocks, err := utils.SplitBytesByProcessNum(byteSize, opt.PayloadProcessNum)
if err != nil {
log.Error("fail to split disk size", zap.Error(err))
return nil, err
}
var ddOpts []DdOption
switch opt.Action {
case DiskFillAction:
for _, block := range ddBlocks {
ddOpts = append(ddOpts, DdOption{
ReadPath: "/dev/zero",
WritePath: path,
BlockSize: block.BlockSize,
Count: block.Count,
Iflag: "fullblock", // fullblock : accumulate full blocks of input.
Oflag: "append",
Conv: "notrunc", // notrunc : do not truncate the output file.
})
}
case DiskWritePayloadAction:
for _, block := range ddBlocks {
ddOpts = append(ddOpts, DdOption{
ReadPath: "/dev/zero",
WritePath: path,
BlockSize: block.BlockSize,
Count: block.Count,
Oflag: "dsync", // dsync : use synchronized I/O for data.
})
}
case DiskReadPayloadAction:
for _, block := range ddBlocks {
ddOpts = append(ddOpts, DdOption{
ReadPath: path,
WritePath: "/dev/null",
BlockSize: block.BlockSize,
Count: block.Count,
Iflag: "dsync,fullblock,nocache", // nocache : Request to drop cache.
})
}
}
return ddOpts, nil
}
func initPath(opt *DiskOption) (string, error) {
switch opt.Action {
case DiskFillAction, DiskWritePayloadAction:
if opt.Path == "" {
var err error
// Check if the path is valid.
opt.Path, err = utils.CreateTempFile()
if err != nil {
log.Error(fmt.Sprintf("unexpected err when CreateTempFile in action: %s", opt.Action))
return "", err
}
if err := os.Remove(opt.Path); err != nil {
return "", err
}
} else {
_, err := os.Stat(opt.Path)
if err != nil {
// check if Path of file is valid when Path is not empty
if os.IsNotExist(err) {
var b []byte
if err := ioutil.WriteFile(opt.Path, b, 0600); err != nil {
return "", err
}
if err := os.Remove(opt.Path); err != nil {
return "", err
}
} else {
return "", err
}
} else {
return "", fmt.Errorf("fill into an existing file")
}
}
return opt.Path, nil
case DiskReadPayloadAction:
if opt.Path == "" {
path, err := utils.GetRootDevice()
if err != nil {
log.Error("err when GetRootDevice in reading payload", zap.Error(err))
return "", err
}
if path == "" {
err = fmt.Errorf("can not get root device path")
log.Error(fmt.Sprintf("payload action: %s", opt.Action), zap.Error(err))
return "", err
}
return path, nil
}
var fi os.FileInfo
var err error
if fi, err = os.Stat(opt.Path); err != nil {
return "", err
}
if fi.IsDir() {
return "", fmt.Errorf("path is a dictory, path : %s", opt.Path)
}
f, err := os.Open(opt.Path)
if err != nil {
return "", err
}
err = f.Close()
if err != nil {
return "", nil
}
return opt.Path, nil
default:
return "", fmt.Errorf("unsupported action %s", opt.Action)
}
}
func initSize(opt *DiskOption) (uint64, error) {
if opt.Size != "" {
byteSize, err := utils.ParseUnit(opt.Size)
if err != nil {
log.Error(fmt.Sprintf("fail to get parse size per units , %s", opt.Size), zap.Error(err))
return 0, err
}
return byteSize, nil
} else if opt.Percent != "" {
opt.Percent = strings.Trim(opt.Percent, " %")
percent, err := strconv.ParseUint(opt.Percent, 10, 0)
if err != nil {
log.Error(fmt.Sprintf("unexcepted err when parsing disk percent '%s'", opt.Percent), zap.Error(err))
return 0, err
}
dir := filepath.Dir(opt.Path)
totalSize, err := utils.GetDiskTotalSize(dir)
if err != nil {
log.Error("fail to get disk total size", zap.Error(err))
return 0, err
}
return totalSize * percent / 100, nil
}
if opt.Action == DiskFillAction {
return 0, fmt.Errorf("one of percent and size must not be empty, DiskOption : %v", opt)
}
return 0, fmt.Errorf("size must not be empty, DiskOption : %v", opt)
}

63
pkg/core/disk_test.go Normal file
View File

@ -0,0 +1,63 @@
// 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 core
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_initSize(t *testing.T) {
opt := DiskOption{
CommonAttackConfig: CommonAttackConfig{
Action: DiskFillAction,
},
Size: "1024M",
}
byteSize, err := initSize(&opt)
assert.NoError(t, err)
assert.EqualValues(t, 1024<<20, byteSize)
opt.Percent = "99%"
opt.Size = ""
byteSize, err = initSize(&opt)
assert.NoError(t, err)
t.Logf("percent %s with bytesize %sGB\n", opt.Percent, strconv.Itoa(int(byteSize>>30)))
opt.Percent = ""
opt.Size = ""
_, err = initSize(&opt)
assert.Error(t, err)
}
func Test_initPath(t *testing.T) {
opt := DiskOption{
CommonAttackConfig: CommonAttackConfig{
Action: DiskFillAction,
},
Path: "/1/12/1/2/1/21",
}
_, err := initPath(&opt)
assert.Error(t, err)
opt = DiskOption{
CommonAttackConfig: CommonAttackConfig{
Action: DiskReadPayloadAction,
},
Path: "/",
}
_, err = initPath(&opt)
assert.NoError(t, err)
}

View File

@ -101,7 +101,7 @@ func GetAttackByKind(kind string) *AttackConfig {
case StressAttack:
attackConfig = &StressCommand{}
case DiskAttack:
attackConfig = &DiskOption{}
attackConfig = &DiskAttackConfig{}
case JVMAttack:
attackConfig = &JVMCommand{}
default:

View File

@ -46,11 +46,6 @@ func (s *Server) newEnvironment(uid string) Environment {
// If options.Schedule isn't provided, then the attack is executed immediately.
// Otherwise the attack is scheduled based on the provided schedule spec and duration.
func (s *Server) ExecuteAttack(attackType AttackType, options core.AttackConfig, launchMode string) (uid string, err error) {
if err = options.Validate(); err != nil {
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
return
}
uid = options.GetUID()
if len(uid) == 0 {
uid = uuid.New().String()

View File

@ -17,196 +17,43 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/hashicorp/go-multierror"
"github.com/pingcap/errors"
"github.com/pingcap/log"
"go.uber.org/zap"
"github.com/chaos-mesh/chaosd/pkg/server/utils"
"github.com/chaos-mesh/chaosd/pkg/core"
"github.com/chaos-mesh/chaosd/pkg/utils"
)
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,fullblock,nocache"
func (disk diskAttack) Attack(options core.AttackConfig, env Environment) (err error) {
attack := options.(*core.DiskOption)
if options.String() == core.DiskFillAction {
return disk.diskFill(attack)
func (disk diskAttack) Attack(options core.AttackConfig, env Environment) error {
var attackConf *core.DiskAttackConfig
var ok bool
if attackConf, ok = options.(*core.DiskAttackConfig); !ok {
return fmt.Errorf("AttackConfig -> *DiskAttackConfig meet error")
}
return disk.diskPayload(attack)
}
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 == "" {
err := initWritePayloadPath(payload)
if err != nil {
return err
}
}
case core.DiskReadPayloadAction:
cmdFormat = DDReadPayloadCommand
if payload.Path == "" {
err := initReadPayloadPath(payload)
if err != nil {
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()
if attackConf.Action == core.DiskFillAction {
if attackConf.FAllocateOption != nil {
cmd := core.FAllocateCommand.Unmarshal(*attackConf.FAllocateOption)
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.Error(string(output), zap.Error(err))
return err
}
log.Info(string(output))
}(cmd)
}
wg.Wait()
if errs != nil {
return errs
}
return nil
}
// dd command with 'oflag=append conv=notrunc' will append new data in the file.
const DDFillCommand = "dd if=/dev/zero of=%s bs=%s count=%s iflag=fullblock oflag=append conv=notrunc"
const FallocateCommand = "fallocate -l %s %s"
// diskFill will execute a dd command (DDFillCommand or FallocateCommand)
// to fill the disk.
func (diskAttack) diskFill(fill *core.DiskOption) error {
if fill.Path == "" {
var err error
fill.Path, err = utils.CreateTempFile()
if err != nil {
log.Error(fmt.Sprintf("unexpected err when CreateTempFile in action: %s", fill.Action))
return err
}
}
if fill.Size != "" {
fill.Size = strings.Trim(fill.Size, " ")
} else if fill.Percent != "" {
fill.Percent = strings.Trim(fill.Percent, " ")
percent, err := strconv.ParseUint(fill.Percent, 10, 0)
if err != nil {
log.Error(fmt.Sprintf(" unexcepted err when parsing disk percent '%s'", fill.Percent), zap.Error(err))
return err
}
dir := filepath.Dir(fill.Path)
totalSize, err := utils.GetDiskTotalSize(dir)
if err != nil {
log.Error("fail to get disk total size", zap.Error(err))
return err
}
fill.Size = strconv.FormatUint(totalSize*percent/100, 10) + "c"
}
var cmd *exec.Cmd
if fill.FillByFallocate {
cmd = exec.Command("bash", "-c", fmt.Sprintf(FallocateCommand, fill.Size, fill.Path))
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(string(output), zap.Error(err))
return err
}
log.Info(string(output))
} else {
byteSize, err := utils.ParseUnit(fill.Size)
if err != nil {
log.Error("fail to parse disk size", zap.Error(err))
return err
return nil
}
ddBlocks, err := utils.SplitBytesByProcessNum(byteSize, 1)
if err != nil {
log.Error("fail to split disk size", zap.Error(err))
return err
}
for _, block := range ddBlocks {
cmd = exec.Command("bash", "-c", fmt.Sprintf(DDFillCommand, fill.Path, block.BlockSize, block.Count))
for _, DdOption := range *attackConf.DdOptions {
cmd := core.DdCommand.Unmarshal(DdOption)
output, err := cmd.CombinedOutput()
if err != nil {
@ -215,22 +62,69 @@ func (diskAttack) diskFill(fill *core.DiskOption) error {
}
log.Info(string(output))
}
return nil
}
if attackConf.DdOptions != nil {
duration, _ := options.ScheduleDuration()
var deadline <-chan time.Time
if duration != nil {
deadline = time.After(*duration)
}
if len(*attackConf.DdOptions) == 0 {
return nil
}
rest := (*attackConf.DdOptions)[len(*attackConf.DdOptions)-1]
*attackConf.DdOptions = (*attackConf.DdOptions)[:len(*attackConf.DdOptions)-1]
cmd := core.DdCommand.Unmarshal(rest)
err := utils.ExecWithDeadline(deadline, cmd)
if err != nil {
return err
}
var wg sync.WaitGroup
var mu sync.Mutex
var errs error
wg.Add(len(*attackConf.DdOptions))
for _, ddOpt := range *attackConf.DdOptions {
cmd := core.DdCommand.Unmarshal(ddOpt)
go func(cmd *exec.Cmd) {
defer wg.Done()
err := utils.ExecWithDeadline(deadline, cmd)
if err != nil {
log.Error(cmd.String(), zap.Error(err))
mu.Lock()
defer mu.Unlock()
errs = multierror.Append(errs, err)
return
}
}(cmd)
}
wg.Wait()
if errs != nil {
return errs
}
}
return nil
}
func (diskAttack) Recover(exp core.Experiment, _ Environment) error {
config, err := exp.GetRequestCommand()
attackConfig, err := exp.GetRequestCommand()
if err != nil {
return err
}
option := *config.(*core.DiskOption)
switch option.Action {
config := *attackConfig.(*core.DiskAttackConfig)
switch config.Action {
case core.DiskFillAction, core.DiskWritePayloadAction:
err = os.Remove(option.Path)
err = os.Remove(config.Path)
if err != nil {
log.Warn(fmt.Sprintf("recover disk: remove %s failed", option.Path), zap.Error(err))
log.Warn(fmt.Sprintf("recover disk: remove %s failed", config.Path), zap.Error(err))
}
}
return nil

View File

@ -0,0 +1,66 @@
// 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 chaosd
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/chaos-mesh/chaosd/pkg/core"
)
func Test_diskAttack_Attack(t *testing.T) {
opt := core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
},
Size: "10M",
Path: "./a",
PayloadProcessNum: 1,
}
conf, err := opt.PreProcess()
assert.NoError(t, err)
err = DiskAttack.Attack(conf, Environment{})
assert.NoError(t, err)
f, err := os.Open("./a")
assert.NoError(t, err)
fi, err := f.Stat()
assert.NoError(t, err)
assert.Equal(t, int64(10), fi.Size()>>20)
err = os.Remove("./a")
assert.NoError(t, err)
opt.Action = core.DiskWritePayloadAction
opt.PayloadProcessNum = 4
wConf, err := opt.PreProcess()
assert.NoError(t, err)
err = DiskAttack.Attack(wConf, Environment{})
assert.NoError(t, err)
f, err = os.Open("./a")
assert.NoError(t, err)
fi, err = f.Stat()
assert.NoError(t, err)
assert.Equal(t, fi.Size()>>20, int64(2))
err = os.Remove("./a")
assert.NoError(t, err)
opt.Action = core.DiskReadPayloadAction
opt.PayloadProcessNum = 4
opt.Path = "./"
_, err = opt.PreProcess()
assert.Error(t, err)
}

View File

@ -116,6 +116,12 @@ func (s *httpServer) createProcessAttack(c *gin.Context) {
return
}
if err := attack.Validate(); err != nil {
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
handleError(c, err)
return
}
uid, err := s.chaos.ExecuteAttack(chaosd.ProcessAttack, attack, core.ServerMode)
if err != nil {
handleError(c, err)
@ -141,6 +147,12 @@ func (s *httpServer) createNetworkAttack(c *gin.Context) {
return
}
if err := attack.Validate(); err != nil {
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
handleError(c, err)
return
}
uid, err := s.chaos.ExecuteAttack(chaosd.NetworkAttack, attack, core.ServerMode)
if err != nil {
handleError(c, err)
@ -166,6 +178,12 @@ func (s *httpServer) createStressAttack(c *gin.Context) {
return
}
if err := attack.Validate(); err != nil {
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
handleError(c, err)
return
}
uid, err := s.chaos.ExecuteAttack(chaosd.StressAttack, attack, core.ServerMode)
if err != nil {
handleError(c, err)
@ -185,14 +203,20 @@ func (s *httpServer) createStressAttack(c *gin.Context) {
// @Failure 500 {object} utils.APIError
// @Router /api/attack/disk [post]
func (s *httpServer) createDiskAttack(c *gin.Context) {
attack := core.NewDiskOption()
if err := c.ShouldBindJSON(attack); err != nil {
options := core.NewDiskOption()
if err := c.ShouldBindJSON(options); err != nil {
c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
return
}
uid, err := s.chaos.ExecuteAttack(chaosd.DiskAttack, attack, core.ServerMode)
attackConfig, err := options.PreProcess()
if err != nil {
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
handleError(c, err)
return
}
uid, err := s.chaos.ExecuteAttack(chaosd.DiskAttack, attackConfig, core.ServerMode)
if err != nil {
handleError(c, err)
return

View File

@ -0,0 +1,47 @@
// 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 (
"os/exec"
"time"
"github.com/pingcap/log"
"go.uber.org/zap"
)
func ExecWithDeadline(t <-chan time.Time, cmd *exec.Cmd) error {
done := make(chan error, 1)
var output []byte
var err error
go func() {
output, err = cmd.CombinedOutput()
done <- err
}()
select {
case <-t:
if err := cmd.Process.Kill(); err != nil {
log.Error("failed to kill process: ", zap.Error(err))
return err
}
case err := <-done:
if err != nil {
log.Error(err.Error()+string(output), zap.Error(err))
return err
}
log.Info(string(output))
}
return nil
}

43
pkg/utils/command.go Normal file
View File

@ -0,0 +1,43 @@
// 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"
"os/exec"
"reflect"
)
type Command struct {
Name string
}
func (c Command) Unmarshal(val interface{}) *exec.Cmd {
v := reflect.ValueOf(val)
var options []string
for i := 0; i < v.NumField(); i++ {
tag := v.Type().Field(i).Tag.Get(c.Name)
if v.Field(i).String() == "" || tag == "" {
continue
}
if tag == "-" {
options = append(options, v.Field(i).String())
} else {
options = append(options, fmt.Sprintf("%s=%v", tag, v.Field(i).String()))
}
}
return exec.Command(c.Name, options...) //
}

46
pkg/utils/command_test.go Normal file
View File

@ -0,0 +1,46 @@
// 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"
"testing"
)
func TestCommand_Unmarshal(t *testing.T) {
type dd struct {
If string `dd:"if"`
Of string `dd:"oflag"`
Iflag string `dd:"iflag"`
}
dc := Command{Name: "dd"}
tests := []struct {
name string
d dd
}{
{
name: "0",
d: dd{
"/dev/zero",
"i,2,3",
"",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := dc.Unmarshal(tt.d)
fmt.Println(cmd.String())
})
}
}