mirror of https://github.com/chaos-mesh/chaosd.git
Compare commits
81 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
bc039aa483 | |
|
|
ddcd31e68e | |
|
|
62d1ceb50f | |
|
|
00c8a256ef | |
|
|
9efc3a19d8 | |
|
|
5cb6b0e45f | |
|
|
5c7014f45d | |
|
|
c31ee824d7 | |
|
|
19a157239e | |
|
|
e7715f72b9 | |
|
|
d32144829e | |
|
|
6c378aaed6 | |
|
|
0fa7a8eebe | |
|
|
3a6efb7db2 | |
|
|
31631c3178 | |
|
|
a9c05406d2 | |
|
|
55b454f281 | |
|
|
f6d3a9fb1e | |
|
|
98f3ca40c1 | |
|
|
5ecc6c265b | |
|
|
8f5b737967 | |
|
|
25e098e4a7 | |
|
|
f5b6b8a4bc | |
|
|
1f4e6bedbc | |
|
|
2c429506d2 | |
|
|
4cbe994b2c | |
|
|
e771b9d10c | |
|
|
9f6a5b83f3 | |
|
|
ad91098215 | |
|
|
91bb4c99e5 | |
|
|
6118c19a89 | |
|
|
e2b71ee788 | |
|
|
41bf629270 | |
|
|
74388bdc4c | |
|
|
499c6652fe | |
|
|
adea8d5043 | |
|
|
b003e0ad4a | |
|
|
e4cabb4419 | |
|
|
f1df7d3f15 | |
|
|
3cb3a334f6 | |
|
|
46bbee557b | |
|
|
62d573059c | |
|
|
c9dcbc36f7 | |
|
|
bc4ca8cb07 | |
|
|
03541d3687 | |
|
|
bf14130d80 | |
|
|
38e871fbcc | |
|
|
84daf15fff | |
|
|
2d07a779fe | |
|
|
10652a81a7 | |
|
|
e5e2c4c0ef | |
|
|
83893b4fea | |
|
|
d93fc1f06c | |
|
|
81a670e824 | |
|
|
bc1b29af2d | |
|
|
81dc95afc2 | |
|
|
4e99e3cc8e | |
|
|
90f7f9b29e | |
|
|
2368d63509 | |
|
|
ed3ab388be | |
|
|
fafc3eb912 | |
|
|
22a8a7cfc9 | |
|
|
7103539ffe | |
|
|
2fe4276f24 | |
|
|
54a0b0b898 | |
|
|
9d861673eb | |
|
|
50bfa4ca39 | |
|
|
9e6d9eedce | |
|
|
8a90c5a85b | |
|
|
693b6f7cc8 | |
|
|
684f94e20d | |
|
|
d795cf65e5 | |
|
|
c1b722e87e | |
|
|
c54f9a5d4d | |
|
|
69a5aa40f3 | |
|
|
2dcda95c83 | |
|
|
352ba5a47a | |
|
|
c2fddc2942 | |
|
|
d077fdaf12 | |
|
|
cbc5801ef2 | |
|
|
f8e6aa3600 |
|
|
@ -4,48 +4,51 @@ on:
|
|||
branches:
|
||||
- main
|
||||
- release-*
|
||||
paths:
|
||||
- .github/workflows/ci.yml
|
||||
- Makefile
|
||||
- go.*
|
||||
- '**.go'
|
||||
|
||||
jobs:
|
||||
pull:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
job:
|
||||
- verify
|
||||
- build
|
||||
- unit-test
|
||||
- integration-test
|
||||
runs-on: ${{ fromJson('{"amd64":"ubuntu-latest", "arm64":["self-hosted", "Linux", "ARM64"]}')[matrix.arch] }}
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.2
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: go/src/github.com/${{ github.repository }}
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
|
||||
- name: ${{ matrix.job }}
|
||||
run: |
|
||||
# workaround for https://github.com/actions/setup-go/issues/14
|
||||
export GOPATH=${GITHUB_WORKSPACE}/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
make groupimports || echo 0
|
||||
#use sh function
|
||||
# use sh function
|
||||
if [[ "$job" == "verify" ]]; then
|
||||
# preload go modules before goimports
|
||||
go mod download -x
|
||||
make check
|
||||
make groupimports || echo 0
|
||||
echo "Please make check before creating a PR"
|
||||
git diff --quiet -- . || (git diff | cat && false)
|
||||
elif [[ "$job" == "build" ]]; then
|
||||
make build
|
||||
elif [[ "$job" == "unit-test" ]]; then
|
||||
make test
|
||||
make unit-test
|
||||
elif [[ "$job" == "integration-test" ]]; then
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y stress-ng
|
||||
make integration-test
|
||||
else
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
name: ci
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
paths-ignore:
|
||||
- .github/workflows/ci.yml
|
||||
- Makefile
|
||||
- go.*
|
||||
- "**.go"
|
||||
|
||||
jobs:
|
||||
pull:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
job:
|
||||
- verify
|
||||
- build
|
||||
- unit-test
|
||||
- integration-test
|
||||
runs-on: ${{ fromJson('{"amd64":"ubuntu-latest", "arm64":["self-hosted", "Linux", "ARM64"]}')[matrix.arch] }}
|
||||
|
||||
steps:
|
||||
- run: echo "Not required to run pull jobs."
|
||||
|
|
@ -7,60 +7,35 @@ on:
|
|||
jobs:
|
||||
run:
|
||||
name: Upload
|
||||
runs-on: ubuntu-latest
|
||||
# glibc version 2.17
|
||||
container: docker.io/centos:7.2.1511
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
runs-on: ${{ fromJson('{"amd64":"ubuntu-latest", "arm64":["self-hosted", "Linux", "ARM64"]}')[matrix.arch] }}
|
||||
container: ${{ fromJson('{"amd64":"docker.io/rockylinux:8", "arm64":"docker.io/rockylinux:8"}')[matrix.arch] }}
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
uses: actions/setup-go@v1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
go-version: 1.16.2
|
||||
id: go
|
||||
|
||||
- name: Prepare build environment
|
||||
run: |
|
||||
# actions/checkout require git v2.X
|
||||
yum -y install https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.7-1.x86_64.rpm
|
||||
yum install -y gcc
|
||||
yum install -y make
|
||||
yum install -y binutils
|
||||
yum install -y git
|
||||
|
||||
- uses: actions/checkout@master
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
# Must use at least depth 2!
|
||||
fetch-depth: 2
|
||||
go-version: 1.20.x
|
||||
|
||||
- name: Setup python3
|
||||
- name: Prepare tools
|
||||
run: |
|
||||
yum install -y python3
|
||||
alias python=python3
|
||||
dnf install -y make gcc python3
|
||||
|
||||
- name: Build binary and related tools
|
||||
run: make build
|
||||
|
||||
- name: Configure awscli
|
||||
run: |
|
||||
pip3 install awscli
|
||||
printf "%s\n" ${{ secrets.AWS_ACCESS_KEY }} ${{ secrets.AWS_SECRET_KEY }} ${{ secrets.AWS_REGION }} "json" | aws configure
|
||||
|
||||
- name: Build binary
|
||||
run: make build
|
||||
|
||||
# TODO: release on github package / release
|
||||
- name: Upload files
|
||||
run: |
|
||||
|
||||
# download tools
|
||||
curl -fsSL -o byteman.tar.gz https://mirrors.chaos-mesh.org/latest/byteman.tar.gz
|
||||
curl -fsSL -o stress-ng https://mirrors.chaos-mesh.org/latest/stress-ng
|
||||
tar zxvf byteman.tar.gz
|
||||
chmod +x ./stress-ng
|
||||
|
||||
# prepare package
|
||||
mkdir chaosd-latest-linux-amd64
|
||||
mkdir chaosd-latest-linux-amd64/tools
|
||||
mv bin/chaosd chaosd-latest-linux-amd64/
|
||||
mv bin/PortOccupyTool chaosd-latest-linux-amd64/tools/
|
||||
mv byteman chaosd-latest-linux-amd64/tools/
|
||||
mv stress-ng chaosd-latest-linux-amd64/tools/
|
||||
|
||||
# upload package
|
||||
tar czvf chaosd-latest-linux-amd64.tar.gz chaosd-latest-linux-amd64
|
||||
aws s3 cp chaosd-latest-linux-amd64.tar.gz ${{ secrets.AWS_BUCKET_NAME }}/chaosd-latest-linux-amd64.tar.gz
|
||||
mv bin chaosd-latest-linux-${{ matrix.arch }}
|
||||
tar czvf chaosd-latest-linux-${{ matrix.arch }}.tar.gz chaosd-latest-linux-${{ matrix.arch }}
|
||||
aws s3 cp chaosd-latest-linux-${{ matrix.arch }}.tar.gz ${{ secrets.AWS_BUCKET_NAME }}/chaosd-latest-linux-${{ matrix.arch }}.tar.gz
|
||||
|
|
|
|||
|
|
@ -6,62 +6,35 @@ on:
|
|||
jobs:
|
||||
run:
|
||||
name: Upload
|
||||
runs-on: ubuntu-latest
|
||||
# glibc version 2.17
|
||||
container: docker.io/centos:7.2.1511
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
runs-on: ${{ fromJson('{"amd64":"ubuntu-latest", "arm64":["self-hosted", "Linux", "ARM64"]}')[matrix.arch] }}
|
||||
container: ${{ fromJson('{"amd64":"docker.io/rockylinux:8", "arm64":"docker.io/rockylinux:8"}')[matrix.arch] }}
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
uses: actions/setup-go@v1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
go-version: 1.16.2
|
||||
id: go
|
||||
|
||||
- name: Prepare build environment
|
||||
run: |
|
||||
# actions/checkout require git v2.X
|
||||
yum -y install https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.7-1.x86_64.rpm
|
||||
yum install -y gcc
|
||||
yum install -y make
|
||||
yum install -y binutils
|
||||
yum install -y git
|
||||
|
||||
- uses: actions/checkout@master
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
# Must use at least depth 2!
|
||||
fetch-depth: 2
|
||||
go-version: 1.20.x
|
||||
|
||||
- name: Setup python3
|
||||
- name: Prepare tools
|
||||
run: |
|
||||
yum install -y python3
|
||||
alias python=python3
|
||||
dnf install -y make gcc python3
|
||||
|
||||
- name: Build binary and related tools
|
||||
run: make build
|
||||
|
||||
- name: Configure awscli
|
||||
run: |
|
||||
pip3 install awscli
|
||||
printf "%s\n" ${{ secrets.AWS_ACCESS_KEY }} ${{ secrets.AWS_SECRET_KEY }} ${{ secrets.AWS_REGION }} "json" | aws configure
|
||||
|
||||
- name: Build binary
|
||||
run: make build
|
||||
|
||||
- name: Upload files
|
||||
run: |
|
||||
|
||||
GIT_TAG=${GITHUB_REF##*/}
|
||||
|
||||
# download tools
|
||||
curl -fsSL -o byteman.tar.gz https://mirrors.chaos-mesh.org/latest/byteman.tar.gz
|
||||
curl -fsSL -o stress-ng https://mirrors.chaos-mesh.org/latest/stress-ng
|
||||
tar zxvf byteman.tar.gz
|
||||
chmod +x ./stress-ng
|
||||
|
||||
# prepare package
|
||||
mkdir chaosd-${GIT_TAG}-linux-amd64
|
||||
mkdir chaosd-${GIT_TAG}-linux-amd64/tools
|
||||
mv bin/chaosd chaosd-${GIT_TAG}-linux-amd64/
|
||||
mv bin/PortOccupyTool chaosd-${GIT_TAG}-linux-amd64/tools/
|
||||
mv byteman chaosd-${GIT_TAG}-linux-amd64/tools/
|
||||
mv stress-ng chaosd-${GIT_TAG}-linux-amd64/tools/
|
||||
|
||||
# upload package
|
||||
tar czvf chaosd-${GIT_TAG}-linux-amd64.tar.gz chaosd-${GIT_TAG}-linux-amd64
|
||||
aws s3 cp chaosd-${GIT_TAG}-linux-amd64.tar.gz ${{ secrets.AWS_BUCKET_NAME }}/chaosd-${GIT_TAG}-linux-amd64.tar.gz
|
||||
mv bin chaosd-${GIT_TAG}-linux-${{ matrix.arch }}
|
||||
tar czvf chaosd-${GIT_TAG}-linux-${{ matrix.arch }}.tar.gz chaosd-${GIT_TAG}-linux-${{ matrix.arch }}
|
||||
aws s3 cp chaosd-${GIT_TAG}-linux-${{ matrix.arch }}.tar.gz ${{ secrets.AWS_BUCKET_NAME }}/chaosd-${GIT_TAG}-linux-${{ matrix.arch }}.tar.gz
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@
|
|||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
.idea
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
*.swp
|
||||
*.log
|
||||
*.fail.go
|
||||
.DS_Store
|
||||
|
||||
bin/
|
||||
|
||||
test/integration_test/**/*.*
|
||||
!test/integration_test/**/*.sh
|
||||
|
|
|
|||
44
Makefile
44
Makefile
|
|
@ -10,6 +10,7 @@ GO := $(GOENV) go
|
|||
CGO := $(CGOENV) go
|
||||
GOTEST := TEST_USE_EXISTING_CLUSTER=false NO_PROXY="${NO_PROXY},testhost" go test
|
||||
SHELL := /usr/bin/env bash
|
||||
BYTEMAN_DIR := byteman-chaos-mesh-download-v4.0.20-0.12
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
|
|
@ -29,11 +30,25 @@ endif
|
|||
PACKAGE_LIST := go list ./... | grep -vE "chaos-daemon/test|pkg/ptrace|zz_generated|vendor"
|
||||
PACKAGE_DIRECTORIES := $(PACKAGE_LIST) | sed 's|github.com/chaos-mesh/chaosd/||'
|
||||
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
ARCH = x86_64
|
||||
endif
|
||||
ifeq ($(UNAME_M),amd64)
|
||||
ARCH = x86_64
|
||||
endif
|
||||
ifeq ($(UNAME_M),aarch64)
|
||||
ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(UNAME_M),arm64)
|
||||
ARCH = aarch64
|
||||
endif
|
||||
|
||||
$(GOBIN)/revive:
|
||||
$(GO) get github.com/mgechev/revive@v1.0.2-0.20200225072153-6219ca02fffb
|
||||
$(GO) install github.com/mgechev/revive@v1.0.2-0.20200225072153-6219ca02fffb
|
||||
|
||||
$(GOBIN)/goimports:
|
||||
$(GO) get golang.org/x/tools/cmd/goimports@v0.0.0-20200309202150-20ab64c0d93f
|
||||
$(GO) install golang.org/x/tools/cmd/goimports@v0.1.1
|
||||
|
||||
build: binary
|
||||
|
||||
|
|
@ -61,8 +76,29 @@ endif
|
|||
chaosd:
|
||||
$(CGOENV) go build -ldflags '$(LDFLAGS)' -tags "${BUILD_TAGS}" -o bin/chaosd ./cmd/main.go
|
||||
|
||||
|
||||
chaos-tools:
|
||||
$(CGOENV) go build -o bin/PortOccupyTool tools/PortOccupyTool.go
|
||||
$(CGOENV) go build -o bin/tools/PortOccupyTool tools/PortOccupyTool.go
|
||||
$(CGOENV) go build -o bin/tools/FileTool tools/file/*.go
|
||||
ifeq (,$(wildcard bin/tools/stress-ng))
|
||||
curl -fsSL -o ./bin/tools/stress-ng https://github.com/chaos-mesh/stress-ng/releases/download/v0.14.02/stress-ng-${ARCH}
|
||||
chmod +x ./bin/tools/stress-ng
|
||||
endif
|
||||
ifeq (,$(wildcard bin/tools/byteman))
|
||||
curl -fsSL -o ${BYTEMAN_DIR}.tar.gz https://mirrors.chaos-mesh.org/${BYTEMAN_DIR}.tar.gz
|
||||
tar zxvf ${BYTEMAN_DIR}.tar.gz
|
||||
mv ${BYTEMAN_DIR} ./bin/tools/byteman
|
||||
endif
|
||||
ifeq (,$(wildcard bin/tools/memStress))
|
||||
curl -fsSL -o memStress_v0.3-${ARCH}-linux-gnu.tar.gz https://github.com/chaos-mesh/memStress/releases/download/v0.3/memStress_v0.3-${ARCH}-linux-gnu.tar.gz
|
||||
tar zxvf memStress_v0.3-${ARCH}-linux-gnu.tar.gz
|
||||
mv memStress ./bin/tools/memStress
|
||||
endif
|
||||
ifeq (,$(wildcard bin/tools/tproxy))
|
||||
curl -fsSL -o tproxy-${ARCH}.tar.gz https://github.com/chaos-mesh/chaos-tproxy/releases/download/v0.5.4/tproxy-${ARCH}.tar.gz
|
||||
tar zxvf tproxy-${ARCH}.tar.gz
|
||||
mv tproxy ./bin/tools/tproxy
|
||||
endif
|
||||
|
||||
swagger_spec:
|
||||
ifeq ($(SWAGGER),1)
|
||||
|
|
@ -100,7 +136,7 @@ tidy:
|
|||
GO111MODULE=on go mod tidy
|
||||
git diff -U --exit-code go.mod go.sum
|
||||
|
||||
test:
|
||||
unit-test:
|
||||
rm -rf cover.* cover
|
||||
$(GOTEST) $$($(PACKAGE_LIST)) -coverprofile cover.out.tmp
|
||||
cat cover.out.tmp | grep -v "_generated.deepcopy.go" > cover.out
|
||||
|
|
|
|||
374
README.md
374
README.md
|
|
@ -2,23 +2,31 @@
|
|||
|
||||
[](https://gitpod.io/#https://github.com/chaos-mesh/chaosd)
|
||||
|
||||
chaosd is an easy-to-use Chaos Engineering tool used to inject failures to a physical node. Currently, two modes are supported:
|
||||
chaosd is an easy-to-use Chaos Engineering tool used to inject failures to a physical node.
|
||||
|
||||
- **Command mode** - Using chaosd as a command-line tool. Supported failure types are:
|
||||
|
||||
- [Process attack](#process-attack)
|
||||
- [Network attack](#network-attack)
|
||||
- [Stress attack](#stress-attack)
|
||||
- [Disk attack](#disk-attack)
|
||||
- [Host attack](#host-attack)
|
||||
- [Recover attack](#recover-attack)
|
||||
## Document
|
||||
|
||||
- **Server mode** - Running chaosd as a daemon server. Supported failure types are:
|
||||
- [Process attack](#process-attack-1)
|
||||
- [Network attack](#network-attack-1)
|
||||
- [Stress attack](#stress-attack-1)
|
||||
- [Disk attack](#disk-attack-1)
|
||||
- [Recover attack](#recover-attack-1)
|
||||
For details about the introduction and usage of chaosd, refer to the [documentation](https://chaos-mesh.org/docs/chaosd-overview/).
|
||||
|
||||
## Types of fault
|
||||
|
||||
You can use Chaosd to simulate the following fault types:
|
||||
|
||||
- Process: Injects faults into the processes. Operations such as killing the process or stopping the process are supported.
|
||||
- Network: Injects faults into the network of physical machines. Operations such as increasing network latency, losing packets, and corrupting packets are supported.
|
||||
- Stress: Injects stress on the CPU or memory of the physical machines.
|
||||
- Disk: Injects faults into disks of the physical machines. Operations such as increasing disk load of reads and writes, and filling disks are supported.
|
||||
- Host: Injects faults into the physical machine. Operations such as shutdown the physical machine are supported.
|
||||
|
||||
For details about the introduction and usage of each fault type, refer to the related documentation.
|
||||
|
||||
## Work modes
|
||||
|
||||
You can use Chaosd in the following modes:
|
||||
|
||||
- Command-line mode: Run Chaosd directly as a command-line tool to inject and recover faults.
|
||||
|
||||
- Service mode: Run Chaosd as a service in the background, to inject and recover faults by sending HTTP requests.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -36,9 +44,24 @@ You can either build directly from the source or download the binary to finish t
|
|||
|
||||
- Build from source code
|
||||
|
||||
|
||||
Build chaosd:
|
||||
|
||||
```bash
|
||||
make chaosd
|
||||
mv chaosd /usr/local/bin/chaosd
|
||||
```
|
||||
|
||||
Build or download tools related to Chaosd:
|
||||
|
||||
```bash
|
||||
make chaos-tools
|
||||
```
|
||||
|
||||
Put Chaosd into `PATH`:
|
||||
|
||||
```
|
||||
mv ./bin /usr/local/chaosd
|
||||
export PATH=$PATH:/usr/local/chaosd
|
||||
```
|
||||
|
||||
- Download binary
|
||||
|
|
@ -49,326 +72,21 @@ You can either build directly from the source or download the binary to finish t
|
|||
curl -fsSL -o chaosd-latest-linux-amd64.tar.gz https://mirrors.chaos-mesh.org/chaosd-latest-linux-amd64.tar.gz
|
||||
```
|
||||
|
||||
If you want to download the release version, you can replace the `latest` in the above command with the version number. For example, download `v1.0.0` by executing the command below:
|
||||
If you want to download the release version, you can replace the `latest` in the above command with the version number. For example, download `v1.1.1` by executing the command below:
|
||||
|
||||
```bash
|
||||
curl -fsSL -o chaosd-v1.0.0-linux-amd64.tar.gz https://mirrors.chaos-mesh.org/chaosd-v1.0.0-linux-amd64.tar.gz
|
||||
curl -fsSL -o chaosd-v1.1.1-linux-amd64.tar.gz https://mirrors.chaos-mesh.org/chaosd-v1.1.1-linux-amd64.tar.gz
|
||||
```
|
||||
|
||||
Then uncompress the archived file, and you can go into the folder and execute chaosd:
|
||||
Then uncompress the archived file:
|
||||
|
||||
```bash
|
||||
tar zxvf chaosd-latest-linux-amd64.tar.gz && cd chaosd-latest-linux-amd64
|
||||
tar zxvf chaosd-latest-linux-amd64.tar.gz
|
||||
```
|
||||
|
||||
## Usages
|
||||
|
||||
### Command mode
|
||||
|
||||
#### Process attack
|
||||
|
||||
Attacks a process according to the PID or process name. Supported tasks are:
|
||||
|
||||
- **kill process**
|
||||
|
||||
Description: Kills a process by sending the `SIGKILL` signal
|
||||
|
||||
Sample usage:
|
||||
Put Chaosd into `PATH`:
|
||||
|
||||
```bash
|
||||
$ chaosd attack process kill -p [pid] # set pid or pod name
|
||||
# the generated uid is used to recover chaos attack
|
||||
Attack network successfully, uid: 2c865e6f-299f-4adf-ab37-94dc4fb8fea6
|
||||
mv ./chaosd-latest-linux-amd64 /usr/local/chaosd
|
||||
export PATH=$PATH:/usr/local/chaosd
|
||||
```
|
||||
|
||||
- **stop process**
|
||||
|
||||
Description: Kills a process by sending the `SIGKILL` signal
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack process stop -p [pid] # set pid or pod name
|
||||
```
|
||||
|
||||
#### Network attack
|
||||
|
||||
Attacks the network using `iptables`, `ipset`, and `tc`. Supported tasks are:
|
||||
|
||||
- **delay network packet**
|
||||
|
||||
Description: Sends messages with the specified latency
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack network delay -d eth0 -i 172.16.4.4 -l 10ms
|
||||
```
|
||||
|
||||
- **lose network packet**
|
||||
|
||||
Description: Drops network packets randomly
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack network loss -d eth0 -i 172.16.4.4 --percent 50
|
||||
```
|
||||
|
||||
- **corrupt network packet**
|
||||
|
||||
Description: Causes packet corruption
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack network corrupt -d eth0 -i 172.16.4.4 --percent 50
|
||||
```
|
||||
|
||||
- **duplicate network packet**
|
||||
|
||||
Description: Sends duplicated packets
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack network duplicate -d eth0 -i 172.16.4.4 --percent 50
|
||||
```
|
||||
|
||||
#### Stress attack
|
||||
|
||||
Generates stress on the host. Supported tasks are:
|
||||
|
||||
- **CPU stress**
|
||||
|
||||
Description: Generates stress on the host CPU
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack stress cpu -l 100 -w 2
|
||||
```
|
||||
|
||||
- **Memory stress**
|
||||
|
||||
Description: Generates stress on the host memory
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd attack stress mem -w 2 # stress 2 CPU and each cpu loads 100%
|
||||
```
|
||||
|
||||
#### Disk attack
|
||||
|
||||
Attacks the disk by increasing write/read payload, or filling up the disk. Supported tasks are:
|
||||
|
||||
- **add payload**
|
||||
|
||||
Description: Add read/write payload
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
./bin/chaosd attack disk add-payload read --path /tmp/temp --size 100
|
||||
```
|
||||
|
||||
```bash
|
||||
./bin/chaosd attack disk add-payload write --path /tmp/temp --size 100
|
||||
```
|
||||
|
||||
- **fill disk**
|
||||
|
||||
Description: Fills up the disk
|
||||
|
||||
Sample usage:
|
||||
|
||||
|
||||
```bash
|
||||
./bin/chaosd attack disk fill --fallocate true --path /tmp/temp --size 100 //filling using fallocate
|
||||
```
|
||||
|
||||
```bash
|
||||
./bin/chaosd attack disk fill --fallocate false --path /tmp/temp --size 100 //filling by writing data to files
|
||||
```
|
||||
|
||||
#### Host attack
|
||||
|
||||
Shuts down the host
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
./bin/chaosd attack host shutdown
|
||||
```
|
||||
|
||||
> **Note:**
|
||||
>
|
||||
> This command will shut down the host. Be cautious when you execute it.
|
||||
|
||||
#### Recover attack
|
||||
|
||||
Recovers an attack
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ chaosd recover 2c865e6f-299f-4adf-ab37-94dc4fb8fea6
|
||||
```
|
||||
|
||||
### Server Mode
|
||||
|
||||
To enter server mode, execute the following:
|
||||
|
||||
```bash
|
||||
nohup ./bin/chaosd server > chaosd.log 2>&1 &
|
||||
```
|
||||
|
||||
And then you can inject failures by sending HTTP requests.
|
||||
|
||||
> **Note**:
|
||||
>
|
||||
> Make sure you are operating with the privileges to run iptables, ipset, etc. Or you can run chaosd with `sudo`.
|
||||
|
||||
#### Process attack
|
||||
|
||||
Attacks a process according to the PID or process name. Supported tasks are:
|
||||
|
||||
- **kill process**
|
||||
|
||||
Description: Kills a process by sending the `SIGKILL` signal
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
curl -X POST "127.0.0.1:31767/api/attack/process" -H "Content-Type: application/json" -d '{"process": "{pid}", "signal": 9}' # set pid or pod name
|
||||
{"status":200,"message":"attack successfully","uid":"e6d01a30-4528-4c70-b4fb-4dc47c4d39be"}
|
||||
```
|
||||
|
||||
- **stop process**
|
||||
|
||||
Description: Kills a process by sending the `SIGKILL` signal
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
curl -X POST "127.0.0.1:31767/api/attack/process" -H "Content-Type: application/json" -d '{"process": "{pid}", "signal": 15}' # set pid or pod name
|
||||
{"status":200,"message":"attack successfully","uid":"ecf3f564-c4c0-4aaf-83c6-4b511a6e3a85"}
|
||||
```
|
||||
|
||||
#### Network attack
|
||||
|
||||
Attacks the network using `iptables`, `ipset`, and `tc`. Supported tasks are:
|
||||
|
||||
- **delay network packet**
|
||||
|
||||
Description: Sends messages with the specified latency
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X POST "127.0.0.1:31767/api/attack/network" -H "Content-Type: application/json" -d '{"device": "eth0", "ipaddress": "172.16.4.4", "action": "delay", "latency": "10ms", "jitter": "10ms", "correlation": "0"}'
|
||||
```
|
||||
|
||||
- **lose network packet**
|
||||
|
||||
Description: Drops network packets randomly
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X POST "127.0.0.1:31767/api/attack/network" -H "Content-Type: application/json" -d '{"device": "eth0", "ipaddress": "172.16.4.4", "action": "loss", "percent": "50", "correlation": "0"}'
|
||||
```
|
||||
|
||||
- **corrupt network packet**
|
||||
|
||||
Description: Causes packet corruption
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X POST "127.0.0.1:31767/api/attack/network" -H "Content-Type: application/json" -d '{"device": "eth0", "ipaddress": "172.16.4.4", "action": "corrupt", "percent": "50", "correlation": "0"}'
|
||||
```
|
||||
|
||||
- **duplicate network packet**
|
||||
|
||||
Description: Sends duplicated packets
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X POST "127.0.0.1:31767/api/attack/network" -H "Content-Type: application/json" -d '{"device": "eth0", "ipaddress": "172.16.4.4", "action": "duplicate", "percent": "50", "correlation": "0"}'
|
||||
```
|
||||
|
||||
#### Stress attack
|
||||
|
||||
Generates stress on the host. Supported tasks are:
|
||||
|
||||
- **CPU stress**
|
||||
|
||||
Description: Generates stress on the host CPU
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X POST 127.0.0.1:31767/api/attack/stress -H "Content-Type:application/json" -d '{"action":"cpu", "load": 100, "workers": 2}'
|
||||
```
|
||||
|
||||
- **Memory stress**
|
||||
|
||||
Description: Generates stress on the host memory
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X POST 127.0.0.1:31767/api/attack/stress -H "Content-Type:application/json" -d '{"action":"mem", "workers": 2}'
|
||||
```
|
||||
|
||||
#### Disk attack
|
||||
|
||||
Attacks the disk by increasing write/read payload, or filling up the disk. Supported tasks are:
|
||||
|
||||
- Add payload
|
||||
|
||||
Description: Add read/write payload
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
curl -X POST "127.0.0.1:31767/api/attack/disk" -H "Content-Type: application/json" -d '{"action":"read-payload","size":1024,"path":"temp"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST "127.0.0.1:31767/api/attack/disk" -H "Content-Type: application/json" -d '{"action":"write-payload","size":1024,"path":"temp"}'
|
||||
```
|
||||
|
||||
- Fill disk
|
||||
|
||||
Description: Fills up the disk
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
curl -X POST "127.0.0.1:31767/api/attack/disk" -H "Content-Type: application/json" -d '{"action":"fill", "size":1024, "path":"temp", "fill_by_fallocate": true}' //filling using fallocate
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST "127.0.0.1:31767/api/attack/disk" -H "Content-Type: application/json" -d '{"action":"fill", "size":1024, "path":"temp", "fill_by_fallocate": false}' //filling by writing data to files
|
||||
```
|
||||
|
||||
#### Recover attack
|
||||
|
||||
Recovers an attack
|
||||
|
||||
Sample usage:
|
||||
|
||||
```bash
|
||||
$ curl -X DELETE "127.0.0.1:31767/api/attack/20df86e9-96e7-47db-88ce-dd31bc70c4f0"
|
||||
```
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
You can develop chaosd directly from your browser in a pre-configured development environment in the cloud:
|
||||
|
||||
[](https://gitpod.io/#https://github.com/chaos-mesh/chaosd)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# Chaosd Roadmap
|
||||
|
||||
This document defines the roadmap for Chaosd development.
|
||||
|
||||
## v2.0
|
||||
|
||||
- [ ] Support HTTP attack to simulate HTTP faults, such as abort connection, delay, etc.
|
||||
- [ ] Support IO attack to simulate file system faults, such as IO delay and read/write errors.
|
||||
- [x] Support workflow to manage a group of chaos experiments.
|
||||
- [x] Support use Dashboard to manage chaos experiments.
|
||||
- [x] Support time skew.
|
||||
- [ ] JVM Attack supports fault injection for applications including MySQL, PostgreSQL, RoketMQ, etc.
|
||||
- [ ] Support file attack, including deleting files, appending data to files, renaming files, modifying files' privilege.
|
||||
- [ ] Support inject faults into Kafka, including high payload, the message queue is full, unable to write into, etc.
|
||||
- [ ] Support inject faults into Zookeeper, including fail to receive publisher's message, fail to update config, fail to notify the update, etc.
|
||||
- [ ] Support inject faults into Redis, including switch primary and secondary, cache hotspot, the sentinel is unavailable, sentinel restart, etc.
|
||||
|
|
@ -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{
|
||||
|
|
@ -21,14 +25,29 @@ func NewAttackCommand() *cobra.Command {
|
|||
Short: "Attack related commands",
|
||||
}
|
||||
|
||||
var uid string
|
||||
cmd.PersistentFlags().StringVarP(&uid, "uid", "", "", "the experiment ID")
|
||||
|
||||
cmd.AddCommand(
|
||||
NewProcessAttackCommand(),
|
||||
NewNetworkAttackCommand(),
|
||||
NewStressAttackCommand(),
|
||||
NewDiskAttackCommand(),
|
||||
NewHostAttackCommand(),
|
||||
NewJVMAttackCommand(),
|
||||
NewProcessAttackCommand(&uid),
|
||||
NewNetworkAttackCommand(&uid),
|
||||
NewStressAttackCommand(&uid),
|
||||
NewDiskAttackCommand(&uid),
|
||||
NewHostAttackCommand(&uid),
|
||||
NewJVMAttackCommand(&uid),
|
||||
NewClockAttackCommand(&uid),
|
||||
NewKafkaAttackCommand(&uid),
|
||||
NewRedisAttackCommand(&uid),
|
||||
NewFileAttackCommand(&uid),
|
||||
NewHTTPAttackCommand(&uid),
|
||||
NewVMAttackCommand(&uid),
|
||||
NewUserDefinedCommand(&uid),
|
||||
)
|
||||
|
||||
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".`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewClockAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewClockOption()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.ClockOption {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "clock attack",
|
||||
Short: "clock skew",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.ClockAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processClockAttack)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&options.Pid, "pid", "p", 0, "Pid of target program.")
|
||||
cmd.Flags().StringVarP(&options.TimeOffset, "time-offset", "t", "", "Specifies the length of time offset.")
|
||||
cmd.Flags().StringVarP(&options.ClockIdsSlice, "clock-ids-slice", "c", "CLOCK_REALTIME",
|
||||
"The identifier of the particular clock on which to act."+
|
||||
"More clock description in linux kernel can be found in man page of clock_getres, clock_gettime, clock_settime."+
|
||||
"Muti clock ids should be split with \",\"")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func processClockAttack(options *core.ClockOption, chaos *chaosd.Server) {
|
||||
err := options.PreProcess()
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
}
|
||||
|
||||
uid, err := chaos.ExecuteAttack(chaosd.ClockAttack, options, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("Clock attack %v successfully, uid: %s", options, uid))
|
||||
}
|
||||
|
|
@ -25,11 +25,12 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func NewDiskAttackCommand() *cobra.Command {
|
||||
func NewDiskAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewDiskOption()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.DiskOption {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
|
@ -79,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
|
||||
}
|
||||
|
||||
|
|
@ -102,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
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +114,7 @@ func NewDiskFillCommand(dep fx.Option, options *core.DiskOption) *cobra.Command
|
|||
Short: "fill disk",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.DiskFillAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processDiskAttack), fx.NopLogger).Run()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processDiskAttack)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -130,10 +133,12 @@ func NewDiskFillCommand(dep fx.Option, options *core.DiskOption) *cobra.Command
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewFileAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewFileCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.FileCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "file <subcommand>",
|
||||
Short: "File attack related commands",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewFileCreateCommand(dep, options),
|
||||
NewFileModifyPrivilegeCommand(dep, options),
|
||||
NewFileDeleteCommand(dep, options),
|
||||
NewFileRenameCommand(dep, options),
|
||||
NewFileAppendCommand(dep, options),
|
||||
NewFileReplaceCommand(dep, options),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewFileCreateCommand(dep fx.Option, options *core.FileCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create file",
|
||||
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.FileCreateAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonFileAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.FileName, "file-name", "f", "", "the name of file to be created")
|
||||
cmd.Flags().StringVarP(&options.DirName, "dir-name", "d", "", "the name of directory to be created")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewFileModifyPrivilegeCommand(dep fx.Option, options *core.FileCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "modify",
|
||||
Short: "modify file privilege",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.FileModifyPrivilegeAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonFileAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.FileName, "file-name", "f", "", "file to be change privilege")
|
||||
cmd.Flags().Uint32VarP(&options.Privilege, "privilege", "p", 0, "privilege to be update")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewFileDeleteCommand(dep fx.Option, options *core.FileCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete file",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.FileDeleteAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonFileAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.FileName, "file-name", "f", "", "the file to be deleted")
|
||||
cmd.Flags().StringVarP(&options.DirName, "dir-name", "d", "", "the directory to be deleted")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewFileRenameCommand(dep fx.Option, options *core.FileCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "rename",
|
||||
Short: "rename file",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.FileRenameAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonFileAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.SourceFile, "source-file", "s", "", "the source file/dir of rename")
|
||||
cmd.Flags().StringVarP(&options.DestFile, "dest-file", "d", "", "the destination file/dir of rename")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewFileAppendCommand(dep fx.Option, options *core.FileCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "append",
|
||||
Short: "append file",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.FileAppendAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonFileAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.FileName, "file-name", "f", "", "append data to the file")
|
||||
cmd.Flags().StringVarP(&options.Data, "data", "d", "", "append data")
|
||||
cmd.Flags().IntVarP(&options.Count, "count", "c", 1, "append count with default value is 1")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewFileReplaceCommand(dep fx.Option, options *core.FileCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "replace",
|
||||
Short: "replace data in file",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.FileReplaceAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonFileAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.FileName, "file-name", "f", "", "replace data in the file")
|
||||
cmd.Flags().StringVarP(&options.OriginStr, "origin-string", "o", "", "the origin string to be replaced")
|
||||
cmd.Flags().StringVarP(&options.DestStr, "dest-string", "d", "", "the destination string to replace the origin string")
|
||||
cmd.Flags().IntVarP(&options.Line, "line", "l", 0, "the line number to replace, default is 0, means replace all lines")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func commonFileAttackFunc(options *core.FileCommand, chaos *chaosd.Server) {
|
||||
if err := options.Validate(); err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
}
|
||||
|
||||
uid, err := chaos.ExecuteAttack(chaosd.FileAttack, options, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("Attack file successfully, uid: %s", uid))
|
||||
}
|
||||
|
|
@ -25,11 +25,12 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func NewHostAttackCommand() *cobra.Command {
|
||||
func NewHostAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewHostCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.HostCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
|
@ -40,6 +41,7 @@ func NewHostAttackCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
cmd.AddCommand(NewHostShutdownCommand(dep, options))
|
||||
cmd.AddCommand(NewHostRebootCommand(dep, options))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -50,6 +52,21 @@ func NewHostShutdownCommand(dep fx.Option, options *core.HostCommand) *cobra.Com
|
|||
Short: "shutdowns system, this action will trigger shutdown of the host machine",
|
||||
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.HostShutdownAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(hostAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewHostRebootCommand(dep fx.Option, options *core.HostCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "reboot",
|
||||
Short: "reboot system, this action will trigger reboot of the host machine",
|
||||
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.HostRebootAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(hostAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewHTTPAttackCommand(uid *string) *cobra.Command {
|
||||
option := core.NewHTTPAttackOption()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.HTTPAttackOption {
|
||||
option.UID = *uid
|
||||
return option
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "http <subcommand>",
|
||||
Short: "HTTP attack related commands",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewHTTPAbortCommand(dep, option),
|
||||
NewHTTPDelayCommand(dep, option),
|
||||
NewHTTPConfigCommand(dep, option),
|
||||
NewHTTPRequestCommand(dep, option),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewHTTPAbortCommand(dep fx.Option, o *core.HTTPAttackOption) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "abort",
|
||||
Short: "abort selected HTTP connection",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
o.Action = core.HTTPAbortAction
|
||||
o.Abort = true
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processHTTPAttack)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
setTarget(cmd, o)
|
||||
setSelector(cmd, o)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewHTTPDelayCommand(dep fx.Option, o *core.HTTPAttackOption) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delay",
|
||||
Short: "delay selected HTTP Package",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
o.Action = core.HTTPDelayAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processHTTPAttack)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
setTarget(cmd, o)
|
||||
setSelector(cmd, o)
|
||||
|
||||
cmd.Flags().StringVarP(&o.Delay, "delay time", "d", "", "Delay represents the delay of the target request/response.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func setTarget(cmd *cobra.Command, o *core.HTTPAttackOption) {
|
||||
cmd.Flags().UintSliceVarP(&o.ProxyPorts, "proxy-ports", "p", nil,
|
||||
"composed with one of the port of HTTP connection, "+
|
||||
"we will only attack HTTP connection with port inside proxy_ports")
|
||||
cmd.Flags().StringVarP(&o.Target, "target", "t", "",
|
||||
"HTTP target: Request or Response")
|
||||
}
|
||||
|
||||
func setSelector(cmd *cobra.Command, c *core.HTTPAttackOption) {
|
||||
cmd.Flags().Int32Var(&c.Port, "port", 0, "The TCP port that the target service listens on.")
|
||||
cmd.Flags().StringVar(&c.Path, "path", "",
|
||||
"Match path of Uri with wildcard matches.")
|
||||
cmd.Flags().StringVarP(&c.Method, "method", "m", "", "HTTP method")
|
||||
cmd.Flags().StringVarP(&c.Code, "code", "c", "", "Code is a rule to select target by http status code in response.")
|
||||
}
|
||||
|
||||
func NewHTTPConfigCommand(dep fx.Option, o *core.HTTPAttackOption) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "attack with config file",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
o.Action = core.HTTPConfigAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processHTTPAttack)).Run()
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&o.FilePath, "file path", "p", "", "Config file path.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewHTTPRequestCommand(dep fx.Option, o *core.HTTPAttackOption) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "request",
|
||||
Short: "request specific URL, only support GET",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
o.Action = core.HTTPRequestAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(processHTTPAttack)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&o.HTTPRequestConfig.URL, "url", "", "", "Request to send")
|
||||
cmd.Flags().IntVarP(&o.HTTPRequestConfig.Count, "count", "c", 1, "Number of requests to send")
|
||||
cmd.Flags().BoolVarP(&o.HTTPRequestConfig.EnableConnPool, "enable-conn-pool", "p", false, "Enable connection pool")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func processHTTPAttack(o *core.HTTPAttackOption, chaos *chaosd.Server) {
|
||||
attackConfig, err := o.PreProcess()
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
}
|
||||
|
||||
uid, err := chaos.ExecuteAttack(chaosd.HTTPAttack, attackConfig, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("HTTP attack successfully, uid: %s", uid))
|
||||
}
|
||||
|
|
@ -25,11 +25,12 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func NewJVMAttackCommand() *cobra.Command {
|
||||
func NewJVMAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewJVMCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.JVMCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
|
@ -39,38 +40,8 @@ func NewJVMAttackCommand() *cobra.Command {
|
|||
Short: "JVM attack related commands",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewJVMInstallCommand(dep, options),
|
||||
NewJVMSubmitCommand(dep, options),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewJVMInstallCommand(dep fx.Option, options *core.JVMCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "install [options]",
|
||||
Short: "install agent to Java process",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Type = core.JVMInstallType
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(jvmCommandFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&options.Port, "port", "", 9288, "the port of agent server")
|
||||
cmd.Flags().IntVarP(&options.Pid, "pid", "", 0, "the pid of Java process which need to attach")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewJVMSubmitCommand(dep fx.Option, options *core.JVMCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "submit [options]",
|
||||
Short: "submit rules for byteman agent",
|
||||
}
|
||||
options.Type = core.JVMSubmitType
|
||||
|
||||
cmd.PersistentFlags().IntVarP(&options.Port, "port", "", 9288, "the port of agent server")
|
||||
cmd.PersistentFlags().IntVarP(&options.Pid, "pid", "", 0, "the pid of Java process which need to attach")
|
||||
|
||||
cmd.AddCommand(
|
||||
NewJVMLatencyCommand(dep, options),
|
||||
|
|
@ -79,6 +50,7 @@ func NewJVMSubmitCommand(dep fx.Option, options *core.JVMCommand) *cobra.Command
|
|||
NewJVMStressCommand(dep, options),
|
||||
NewJVMGCCommand(dep, options),
|
||||
NewJVMRuleFileCommand(dep, options),
|
||||
NewJVMMySQLCommand(dep, options),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
@ -96,7 +68,7 @@ func NewJVMLatencyCommand(dep fx.Option, options *core.JVMCommand) *cobra.Comman
|
|||
|
||||
cmd.Flags().StringVarP(&options.Class, "class", "c", "", "Java class name")
|
||||
cmd.Flags().StringVarP(&options.Method, "method", "m", "", "the method name in Java class")
|
||||
cmd.Flags().StringVarP(&options.LatencyDuration, "latency", "", "", "the latency duration")
|
||||
cmd.Flags().IntVarP(&options.LatencyDuration, "latency", "", 0, "the latency duration, unit ms")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -113,7 +85,7 @@ func NewJVMReturnCommand(dep fx.Option, options *core.JVMCommand) *cobra.Command
|
|||
|
||||
cmd.Flags().StringVarP(&options.Class, "class", "c", "", "Java class name")
|
||||
cmd.Flags().StringVarP(&options.Method, "method", "m", "", "the method name in Java class")
|
||||
cmd.Flags().StringVarP(&options.ReturnValue, "value", "", "", "the return value for action 'return', only support number and string type now")
|
||||
cmd.Flags().StringVarP(&options.ReturnValue, "value", "", "", "the return value for action 'return'. Only supports number and string types.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -145,8 +117,8 @@ func NewJVMStressCommand(dep fx.Option, options *core.JVMCommand) *cobra.Command
|
|||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&options.CPUCount, "cpu-count", "", 0, "the CPU core number need to use")
|
||||
cmd.Flags().IntVarP(&options.MemorySize, "mem-size", "", 0, "the memory size need to locate, the unit is MB")
|
||||
cmd.Flags().IntVarP(&options.CPUCount, "cpu-count", "", 0, "the CPU core number")
|
||||
cmd.Flags().StringVarP(&options.MemoryType, "mem-type", "", "", "the memory type to be allocated. The value can be 'stack' or 'heap'")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -179,6 +151,26 @@ func NewJVMRuleFileCommand(dep fx.Option, options *core.JVMCommand) *cobra.Comma
|
|||
return cmd
|
||||
}
|
||||
|
||||
func NewJVMMySQLCommand(dep fx.Option, options *core.JVMCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "mysql [options]",
|
||||
Short: "inject fault into MySQL client",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.JVMMySQLAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(jvmCommandFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.SQLType, "sql-type", "", "", "the match sql type")
|
||||
cmd.Flags().StringVarP(&options.Database, "database", "d", "", "the match database")
|
||||
cmd.Flags().StringVarP(&options.Table, "table", "t", "", "the match table")
|
||||
cmd.Flags().StringVarP(&options.MySQLConnectorVersion, "mysql-connector-version", "v", "8", "the version of mysql-connector-java, only support 5.X.X(set to 5) and 8.X.X(set to 8)")
|
||||
cmd.Flags().StringVarP(&options.ThrowException, "exception", "", "", "the exception message needs to throw")
|
||||
cmd.Flags().IntVarP(&options.LatencyDuration, "latency", "", 0, "the latency duration, unit ms")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func jvmCommandFunc(options *core.JVMCommand, chaos *chaosd.Server) {
|
||||
options.CompleteDefaults()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewKafkaAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewKafkaCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.KafkaCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "kafka <subcommand>",
|
||||
Short: "Kafka attack related commands",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewKafkaFillCommand(dep, options),
|
||||
NewKafkaFloodCommand(dep, options),
|
||||
NewKafkaIOCommand(dep, options),
|
||||
)
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&options.Topic, "topic", "T", "", "the topic to attack")
|
||||
_ = cmd.MarkPersistentFlagRequired("topic")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewKafkaFillCommand(dep fx.Option, options *core.KafkaCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "fill [options]",
|
||||
Short: "Fill kafka cluster with messages",
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.KafkaFillAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(kafkaCommandFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Host, "host", "H", "localhost", "the host of kafka server")
|
||||
cmd.Flags().Uint16VarP(&options.Port, "port", "P", 9092, "the port of kafka server")
|
||||
cmd.Flags().StringVarP(&options.Username, "username", "u", "", "the username of kafka client")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "the password of kafka client")
|
||||
cmd.Flags().StringVarP(&options.AuthMechanism, "auth-mechanism", "a", "sasl/plain", "the authentication mechanism of kafka, supported value: sasl/plain, sasl/scram-sha-256, sasl/scram-sha-512")
|
||||
cmd.Flags().UintVarP(&options.MessageSize, "size", "s", 4*1024, "the size of each message")
|
||||
cmd.Flags().Uint64VarP(&options.MaxBytes, "max-bytes", "m", 1<<34, "the max bytes to fill")
|
||||
cmd.Flags().StringVarP(&options.ReloadCommand, "reload-cmd", "r", "", "the command to reload kafka config")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewKafkaFloodCommand(dep fx.Option, options *core.KafkaCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "flood [options]",
|
||||
Short: "Flood kafka cluster with messages",
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.KafkaFloodAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(kafkaCommandFunc)).Run()
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&options.Host, "host", "H", "localhost", "the host of kafka server")
|
||||
cmd.Flags().Uint16VarP(&options.Port, "port", "P", 9092, "the port of kafka server")
|
||||
cmd.Flags().StringVarP(&options.Username, "username", "u", "", "the username of kafka client")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "the password of kafka client")
|
||||
cmd.Flags().StringVarP(&options.AuthMechanism, "auth-mechanism", "a", "sasl/plain", "the authentication mechanism of kafka, supported value: sasl/plain, sasl/scram-sha-256, sasl/scram-sha-512")
|
||||
cmd.Flags().UintVarP(&options.MessageSize, "size", "s", 1024, "the size of each message")
|
||||
cmd.Flags().UintVarP(&options.Threads, "threads", "t", 100, "the numbers of worker threads")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewKafkaIOCommand(dep fx.Option, options *core.KafkaCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "io [options]",
|
||||
Short: "Make kafka cluster non-writable/non-readable",
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.KafkaIOAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(kafkaCommandFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Host, "host", "H", "localhost", "the host of kafka server")
|
||||
cmd.Flags().Uint16VarP(&options.Port, "port", "P", 9092, "the port of kafka server")
|
||||
cmd.Flags().StringVarP(&options.Username, "username", "u", "", "the username of kafka client")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "the password of kafka client")
|
||||
cmd.Flags().StringVarP(&options.AuthMechanism, "auth-mechanism", "a", "sasl/plain", "the authentication mechanism of kafka, supported value: sasl/plain, sasl/scram-sha-256, sasl/scram-sha-512")
|
||||
cmd.Flags().StringVarP(&options.ConfigFile, "config", "c", "/etc/kafka/server.properties", "the path of server config")
|
||||
cmd.Flags().BoolVarP(&options.NonReadable, "non-readable", "r", false, "make kafka cluster non-readable")
|
||||
cmd.Flags().BoolVarP(&options.NonWritable, "non-writable", "w", false, "make kafka cluster non-writable")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func kafkaCommandFunc(options *core.KafkaCommand, chaos *chaosd.Server) {
|
||||
options.CompleteDefaults()
|
||||
|
||||
if err := options.Validate(); err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
}
|
||||
|
||||
uid, err := chaos.ExecuteAttack(chaosd.KafkaAttack, options, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("Attack kafka successfully, uid: %s", uid))
|
||||
}
|
||||
|
|
@ -25,11 +25,12 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func NewNetworkAttackCommand() *cobra.Command {
|
||||
func NewNetworkAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewNetworkCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.NetworkCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
|
@ -44,8 +45,12 @@ func NewNetworkAttackCommand() *cobra.Command {
|
|||
NewNetworkLossCommand(dep, options),
|
||||
NewNetworkCorruptCommand(dep, options),
|
||||
NetworkDuplicateCommand(dep, options),
|
||||
NetworkPartitionCommand(dep, options),
|
||||
NetworkDNSCommand(dep, options),
|
||||
NewNetworkPortOccupiedCommand(dep, options),
|
||||
NewNetworkBandwidthCommand(dep, options),
|
||||
NewNICDownCommand(dep, options),
|
||||
NewNetworkFloodCommand(dep, options),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
@ -79,6 +84,7 @@ func NewNetworkDelayCommand(dep fx.Option, options *core.NetworkCommand) *cobra.
|
|||
cmd.Flags().StringVarP(&options.Hostname, "hostname", "H", "", "only impact traffic to these hostnames")
|
||||
cmd.Flags().StringVarP(&options.IPProtocol, "protocol", "p", "",
|
||||
"only impact traffic using this IP protocol, supported: tcp, udp, icmp, all")
|
||||
cmd.Flags().StringVarP(&options.AcceptTCPFlags, "accept-tcp-flags", "", "", "only the packet which match the tcp flag can be accepted, others will be dropped. only set when the protocol is tcp.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -153,7 +159,7 @@ func NetworkDuplicateCommand(dep fx.Option, options *core.NetworkCommand) *cobra
|
|||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&options.Percent, "percent", "1", "percentage of packets to corrupt (10 is 10%)")
|
||||
cmd.Flags().StringVar(&options.Percent, "percent", "1", "percentage of packets to duplicate (10 is 10%)")
|
||||
cmd.Flags().StringVarP(&options.Correlation, "correlation", "c", "0", "correlation is percentage (10 is 10%)")
|
||||
cmd.Flags().StringVarP(&options.Device, "device", "d", "", "the network interface to impact")
|
||||
cmd.Flags().StringVarP(&options.EgressPort, "egress-port", "e", "",
|
||||
|
|
@ -170,6 +176,29 @@ func NetworkDuplicateCommand(dep fx.Option, options *core.NetworkCommand) *cobra
|
|||
return cmd
|
||||
}
|
||||
|
||||
func NetworkPartitionCommand(dep fx.Option, options *core.NetworkCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "partition",
|
||||
Short: "partition",
|
||||
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.NetworkPartitionAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonNetworkAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.IPAddress, "ip", "i", "", "only impact egress traffic to these IP addresses")
|
||||
cmd.Flags().StringVarP(&options.Hostname, "hostname", "H", "", "only impact traffic to these hostnames")
|
||||
cmd.Flags().StringVarP(&options.Direction, "direction", "", "both", "specifies the partition direction, values can be 'to', 'from' or 'both'. 'from' means packets coming from the 'IPAddress' or 'Hostname' and going to your server, 'to' means packets originating from your server and going to the 'IPAddress' or 'Hostname'.")
|
||||
cmd.Flags().StringVarP(&options.Device, "device", "d", "", "the network interface to impact")
|
||||
cmd.Flags().StringVarP(&options.IPProtocol, "protocol", "p", "",
|
||||
"only impact traffic using this IP protocol, supported: tcp, udp, icmp, all")
|
||||
cmd.Flags().StringVarP(&options.AcceptTCPFlags, "accept-tcp-flags", "", "", "only the packet which match the tcp flag can be accepted, others will be dropped. only set when the protocol is tcp.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NetworkDNSCommand(dep fx.Option, options *core.NetworkCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dns",
|
||||
|
|
@ -184,12 +213,36 @@ func NetworkDNSCommand(dep fx.Option, options *core.NetworkCommand) *cobra.Comma
|
|||
|
||||
cmd.Flags().StringVarP(&options.DNSServer, "dns-server", "", "123.123.123.123",
|
||||
"update the DNS server in /etc/resolv.conf with this value")
|
||||
cmd.Flags().StringVarP(&options.DNSHost, "dns-hostname", "H", "", "map this host to specified IP")
|
||||
cmd.Flags().StringVarP(&options.DNSDomainName, "dns-domain-name", "d", "", "map this host to specified IP")
|
||||
cmd.Flags().StringVarP(&options.DNSIp, "dns-ip", "i", "", "map specified host to this IP address")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewNetworkBandwidthCommand(dep fx.Option, options *core.NetworkCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "bandwidth",
|
||||
Short: "limit network bandwidth",
|
||||
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.NetworkBandwidthAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonNetworkAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Rate, "rate", "r", "", "the speed knob, allows bps, kbps, mbps, gbps, tbps unit. bps means bytes per second")
|
||||
cmd.Flags().Uint32VarP(&options.Limit, "limit", "l", 0, "the number of bytes that can be queued waiting for tokens to become available")
|
||||
cmd.Flags().Uint32VarP(&options.Buffer, "buffer", "b", 0, "the maximum amount of bytes that tokens can be available for instantaneously")
|
||||
cmd.Flags().Uint64VarP(options.Peakrate, "peakrate", "", 0, "the maximum depletion rate of the bucket")
|
||||
cmd.Flags().Uint32VarP(options.Minburst, "minburst", "m", 0, "specifies the size of the peakrate bucket")
|
||||
cmd.Flags().StringVarP(&options.Device, "device", "d", "", "the network interface to impact")
|
||||
cmd.Flags().StringVarP(&options.IPAddress, "ip", "i", "", "only impact egress traffic to these IP addresses")
|
||||
cmd.Flags().StringVarP(&options.Hostname, "hostname", "H", "", "only impact traffic to these hostnames")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func commonNetworkAttackFunc(options *core.NetworkCommand, chaos *chaosd.Server) {
|
||||
if err := options.Validate(); err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
|
|
@ -209,7 +262,7 @@ func NewNetworkPortOccupiedCommand(dep fx.Option, options *core.NetworkCommand)
|
|||
Short: "attack network port",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.NetworkPortOccupied
|
||||
options.Action = core.NetworkPortOccupiedAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonNetworkAttackFunc)).Run()
|
||||
},
|
||||
|
|
@ -218,3 +271,41 @@ func NewNetworkPortOccupiedCommand(dep fx.Option, options *core.NetworkCommand)
|
|||
cmd.Flags().StringVarP(&options.Port, "port", "p", "", "this specified port is to occupied")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewNetworkFloodCommand(dep fx.Option, options *core.NetworkCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "flood",
|
||||
Short: "generate a mount of network traffic by using iperf client",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.NetworkFloodAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonNetworkAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Rate, "rate", "r", "", "the speed of network traffic, allows bps, kbps, mbps, gbps, tbps unit. bps means bytes per second")
|
||||
cmd.Flags().StringVarP(&options.IPAddress, "ip", "i", "", "generate traffic to this IP address")
|
||||
cmd.Flags().StringVarP(&options.Port, "port", "p", "", "generate traffic to this port on the IP address")
|
||||
cmd.Flags().Int32VarP(&options.Parallel, "parallel", "", 1, "number of iperf parallel client threads to run")
|
||||
cmd.Flags().StringVarP(&options.Duration, "duration", "", "99999999", "number of seconds to run the iperf test")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewNICDownCommand(dep fx.Option, options *core.NetworkCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "down",
|
||||
Short: "down network interface card",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
options.Action = core.NetworkNICDownAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(commonNetworkAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Device, "device", "d", "", "the network interface to impact")
|
||||
SetScheduleFlags(cmd, &options.SchedulerConfig)
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func NewProcessAttackCommand() *cobra.Command {
|
||||
func NewProcessAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewProcessCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.ProcessCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
|
@ -61,6 +62,7 @@ func NewProcessKillCommand(dep fx.Option, options *core.ProcessCommand) *cobra.C
|
|||
|
||||
cmd.Flags().StringVarP(&options.Process, "process", "p", "", "The process name or the process ID")
|
||||
cmd.Flags().IntVarP(&options.Signal, "signal", "s", 9, "The signal number to send")
|
||||
cmd.Flags().StringVarP(&options.RecoverCmd, "recover-cmd", "r", "", "The command to be run when recovering experiment")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewRedisAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewRedisCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.RedisCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "redis <subcommand>",
|
||||
Short: "Redis attack related commands",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewRedisSentinelRestartCommand(dep, options),
|
||||
NewRedisSentinelStopCommand(dep, options),
|
||||
NewRedisCachePenetrationCommand(dep, options),
|
||||
NewRedisCacheLimitCommand(dep, options),
|
||||
NewRedisCacheExpirationCommand(dep, options),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewRedisSentinelRestartCommand(dep fx.Option, options *core.RedisCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sentinel-restart",
|
||||
Short: "restart sentinel",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.RedisSentinelRestartAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(redisAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Addr, "addr", "a", "", "The address of redis server")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "The password of server")
|
||||
cmd.Flags().StringVarP(&options.Conf, "conf", "c", "", "The config of Redis server")
|
||||
cmd.Flags().BoolVarP(&options.FlushConfig, "flush-config", "", true, " Force Sentinel to rewrite its configuration on disk")
|
||||
cmd.Flags().StringVarP(&options.RedisPath, "redis-path", "", "", "The path of the redis-server command")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewRedisSentinelStopCommand(dep fx.Option, options *core.RedisCommand) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sentinel-stop",
|
||||
Short: "stop sentinel",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.RedisSentinelStopAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(redisAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Addr, "addr", "a", "", "The address of redis server")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "The password of server")
|
||||
cmd.Flags().StringVarP(&options.Conf, "conf", "c", "", "The config path of Redis server")
|
||||
cmd.Flags().BoolVarP(&options.FlushConfig, "flush-config", "", true, "Force Sentinel to rewrite its configuration on disk")
|
||||
cmd.Flags().StringVarP(&options.RedisPath, "redis-path", "", "", "The path of the redis-server command")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewRedisCachePenetrationCommand(dep fx.Option, options *core.RedisCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cache-penetration",
|
||||
Short: "penetrate cache",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.RedisCachePenetrationAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(redisAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Addr, "addr", "a", "", "The address of redis server")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "The password of server")
|
||||
cmd.Flags().IntVarP(&options.RequestNum, "request-num", "", 0, "The number of requests")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewRedisCacheLimitCommand(dep fx.Option, options *core.RedisCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cache-limit",
|
||||
Short: "set maxmemory of Redis",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.RedisCacheLimitAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(redisAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Addr, "addr", "a", "", "The address of redis server")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "The password of server")
|
||||
cmd.Flags().StringVarP(&options.CacheSize, "size", "s", "0", "The size of cache")
|
||||
cmd.Flags().StringVarP(&options.Percent, "percent", "", "", "The percentage of maxmemory")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewRedisCacheExpirationCommand(dep fx.Option, options *core.RedisCommand) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cache-expiration",
|
||||
Short: "expire keys in Redis",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.RedisCacheExpirationAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(redisAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.Addr, "addr", "a", "", "The address of redis server")
|
||||
cmd.Flags().StringVarP(&options.Password, "password", "p", "", "The password of server")
|
||||
cmd.Flags().StringVarP(&options.Key, "key", "k", "", "The key to be set a expiration, default expire all keys")
|
||||
cmd.Flags().StringVarP(&options.Expiration, "expiration", "", "0", `The expiration of the key. A expiration string should be able to be converted to a time duration, such as "5s" or "30m"`)
|
||||
cmd.Flags().StringVarP(&options.Option, "option", "", "", "The additional options of expiration, only NX, XX, GT, LT supported")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func redisAttackF(chaos *chaosd.Server, options *core.RedisCommand) {
|
||||
if err := options.Validate(); err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
}
|
||||
uid, err := chaos.ExecuteAttack(chaosd.RedisAttack, options, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("Attack redis %s successfully, uid: %s", options.Action, uid))
|
||||
}
|
||||
|
|
@ -25,11 +25,12 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func NewStressAttackCommand() *cobra.Command {
|
||||
func NewStressAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewStressCommand()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.StressCommand {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
|
@ -53,6 +54,7 @@ func NewStressCPUCommand(dep fx.Option, options *core.StressCommand) *cobra.Comm
|
|||
Short: "continuously stress CPU out",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.StressCPUAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(stressAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
|
@ -70,11 +72,11 @@ func NewStressMemCommand(dep fx.Option, options *core.StressCommand) *cobra.Comm
|
|||
Short: "continuously stress virtual memory out",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.StressMemAction
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(stressAttackF)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&options.Workers, "workers", "w", 1, "Workers specifies N workers to apply the stressor.")
|
||||
cmd.Flags().StringVarP(&options.Size, "size", "s", "", "Size specifies N bytes consumed per vm worker, default is the total available memory. One can specify the size as % of total available memory or in units of B, KB/KiB, MB/MiB, GB/GiB, TB/TiB..")
|
||||
cmd.Flags().StringSliceVarP(&options.Options, "options", "o", []string{}, "extend stress-ng options.")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewUserDefinedCommand(uid *string) *cobra.Command {
|
||||
options := core.NewUserDefinedOption()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.UserDefinedOption {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "user-defined <subcommand>",
|
||||
Short: "user defined attack related commands",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.CompleteDefaults()
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(userDefinedAttackFunc)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.AttackCmd, "attack-cmd", "a", "", "the command to be executed when attack")
|
||||
cmd.Flags().StringVarP(&options.RecoverCmd, "recover-cmd", "r", "", "the command to be executed when recover")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func userDefinedAttackFunc(options *core.UserDefinedOption, chaos *chaosd.Server) {
|
||||
if err := options.Validate(); err != nil {
|
||||
utils.ExitWithError(utils.ExitBadArgs, err)
|
||||
}
|
||||
|
||||
uid, err := chaos.ExecuteAttack(chaosd.UserDefinedAttack, options, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("Attack user defined comamnd successfully, uid: %s", uid))
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewVMAttackCommand(uid *string) *cobra.Command {
|
||||
options := core.NewVMOption()
|
||||
dep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *core.VMOption {
|
||||
options.UID = *uid
|
||||
return options
|
||||
}),
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "vm attack",
|
||||
Short: "vm attack by using virsh",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
options.Action = core.VMAction
|
||||
utils.FxNewAppWithoutLog(dep, fx.Invoke(vmAttack)).Run()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.VMName, "vm-name", "v", "", "The name of the vm to be destoryed")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func vmAttack(options *core.VMOption, chaos *chaosd.Server) {
|
||||
uid, err := chaos.ExecuteAttack(chaosd.VMAttack, options, core.CommandMode)
|
||||
if err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
|
||||
utils.NormalExit(fmt.Sprintf("VM attack %v successfully, uid: %s", options, uid))
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2022 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 completion
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
// NewCompletionCommand returns the completion command
|
||||
func NewCompletionCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
Long: `To load completions:
|
||||
Bash:
|
||||
|
||||
$ source <(./bin/chaosd completion bash)
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
Linux:
|
||||
$ ./bin/chaosd completion bash > /etc/bash_completion.d/chaosd
|
||||
MacOS:
|
||||
$ ./bin/chaosd completion bash > /usr/local/etc/bash_completion.d/chaosd
|
||||
|
||||
Zsh:
|
||||
|
||||
$ compdef _chaosd ./bin/chaosd
|
||||
|
||||
# If shell completion is not already enabled in your environment you will need
|
||||
# to enable it. You can execute the following once:
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ ./bin/chaosd completion zsh > "${fpath[1]}/_chaosd"
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
Fish:
|
||||
|
||||
$ ./bin/chaosd completion fish | source
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ ./bin/chaosd completion fish > ~/.config/fish/completions/chaosd.fish
|
||||
|
||||
Powershell:
|
||||
|
||||
PS> ./bin/chaosd completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
# To load completions for every new session, run:
|
||||
PS> ./bin/chaosd completion powershell > chaosd.ps1
|
||||
# and source this file from your powershell profile.
|
||||
`,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
case "zsh":
|
||||
if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
case "fish":
|
||||
if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil {
|
||||
utils.ExitWithError(utils.ExitError, err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
13
cmd/main.go
13
cmd/main.go
|
|
@ -21,8 +21,11 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
_ "github.com/swaggo/swag"
|
||||
"go.uber.org/zap"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
ctrlzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/cmd/attack"
|
||||
"github.com/chaos-mesh/chaosd/cmd/completion"
|
||||
"github.com/chaos-mesh/chaosd/cmd/recover"
|
||||
"github.com/chaos-mesh/chaosd/cmd/search"
|
||||
"github.com/chaos-mesh/chaosd/cmd/server"
|
||||
|
|
@ -39,8 +42,8 @@ var rootCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(setLogLevel)
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "", "", "the log level of chaosd, the value can be 'debug', 'info', 'warn' and 'error'")
|
||||
cobra.OnInitialize(setLog)
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "", "", "the log level of chaosd. The value can be 'debug', 'info', 'warn' and 'error'")
|
||||
|
||||
rootCmd.AddCommand(
|
||||
server.NewServerCommand(),
|
||||
|
|
@ -48,12 +51,13 @@ func init() {
|
|||
recover.NewRecoverCommand(),
|
||||
search.NewSearchCommand(),
|
||||
version.NewVersionCommand(),
|
||||
completion.NewCompletionCommand(),
|
||||
)
|
||||
|
||||
_ = utils.SetRuntimeEnv()
|
||||
}
|
||||
|
||||
func setLogLevel() {
|
||||
func setLog() {
|
||||
conf := &log.Config{Level: logLevel}
|
||||
lg, r, err := log.InitLogger(conf)
|
||||
if err != nil {
|
||||
|
|
@ -62,6 +66,9 @@ func setLogLevel() {
|
|||
}
|
||||
log.ReplaceGlobals(lg, r)
|
||||
|
||||
// set log of controller-runtime, so that can print logs in chaos mesh
|
||||
ctrl.SetLogger(ctrlzap.New(ctrlzap.UseDevMode(true)))
|
||||
|
||||
// only in debug mode print log of go.uber.org/fx
|
||||
if strings.ToLower(logLevel) == "debug" {
|
||||
utils.PrintFxLog = true
|
||||
|
|
|
|||
|
|
@ -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 recover
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
"github.com/chaos-mesh/chaosd/pkg/server/chaosd"
|
||||
)
|
||||
|
||||
type completionContext struct {
|
||||
uids chan string
|
||||
err chan error
|
||||
}
|
||||
|
||||
func newCompletionCtx() *completionContext {
|
||||
return &completionContext{
|
||||
uids: make(chan string),
|
||||
err: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
func listUid(ctx *completionContext, chaos *chaosd.Server) {
|
||||
exps, err := chaos.Search(&core.SearchCommand{Status: core.Success})
|
||||
if err != nil {
|
||||
ctx.err <- errors.Wrap(err, "list exp")
|
||||
return
|
||||
}
|
||||
|
||||
for _, exp := range exps {
|
||||
ctx.uids <- exp.Uid
|
||||
}
|
||||
close(ctx.uids)
|
||||
}
|
||||
|
|
@ -14,11 +14,15 @@
|
|||
package recover
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
"github.com/pingcap/log"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/cmd/server"
|
||||
"github.com/chaos-mesh/chaosd/pkg/server/chaosd"
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
|
|
@ -38,9 +42,10 @@ func NewRecoverCommand() *cobra.Command {
|
|||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "recover UID",
|
||||
Short: "Recover a chaos experiment",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Use: "recover UID",
|
||||
Short: "Recover a chaos experiment",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
ValidArgsFunction: completeUid,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
utils.ExitWithMsg(utils.ExitBadArgs, "UID is required")
|
||||
|
|
@ -60,3 +65,33 @@ func recoverCommandF(chaos *chaosd.Server, options *recoverCommand) {
|
|||
|
||||
utils.NormalExit(fmt.Sprintf("Recover %s successfully", options.uid))
|
||||
}
|
||||
|
||||
func completeUid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
completionCtx := newCompletionCtx()
|
||||
completionDep := fx.Options(
|
||||
server.Module,
|
||||
fx.Provide(func() *completionContext {
|
||||
return completionCtx
|
||||
}),
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go func() {
|
||||
if err := utils.FxNewAppWithoutLog(completionDep, fx.Invoke(listUid)).Start(ctx); err != nil {
|
||||
log.Error(errors.Wrap(err, "start application").Error())
|
||||
}
|
||||
}()
|
||||
var uids []string
|
||||
for {
|
||||
select {
|
||||
case uid := <-completionCtx.uids:
|
||||
if len(uid) == 0 {
|
||||
return uids, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
uids = append(uids, uid)
|
||||
case err := <-completionCtx.err:
|
||||
log.Error(err.Error())
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func NewSearchCommand() *cobra.Command {
|
|||
cmd.Flags().StringVarP(&options.Status, "status", "s", "", "attack status, "+
|
||||
"supported value: created, success, error, destroyed, revoked")
|
||||
cmd.Flags().StringVarP(&options.Kind, "kind", "k", "", "attack kind, "+
|
||||
"supported value: network, process")
|
||||
"supported value: network, process, stress, disk, host, jvm")
|
||||
cmd.Flags().Uint32VarP(&options.Offset, "offset", "o", 0, "starting to search attacks from offset")
|
||||
cmd.Flags().Uint32VarP(&options.Limit, "limit", "l", 0, "limit the count of attacks")
|
||||
cmd.Flags().BoolVar(&options.Asc, "asc", false, "order by CreateTime, "+
|
||||
|
|
|
|||
|
|
@ -30,8 +30,13 @@ func NewServerCommand() *cobra.Command {
|
|||
Run: serverCommandFunc,
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&conf.ListenPort, "port", "p", 31767, "listen port of the Chaosd Server")
|
||||
cmd.Flags().IntVarP(&conf.ListenPort, "port", "p", 31767, "listen port of the Chaosd http Server")
|
||||
cmd.Flags().IntVar(&conf.ListenHttpsPort, "https-port", 31768, "listen port of the Chaosd https Server")
|
||||
cmd.Flags().StringVarP(&conf.ListenHost, "host", "a", "0.0.0.0", "listen host of the Chaosd Server")
|
||||
cmd.Flags().StringVar(&conf.SSLCertFile, "cert", "", "path to a PEM encoded certificate file")
|
||||
cmd.Flags().StringVar(&conf.SSLKeyFile, "key", "", "path to a PEM encoded private key file")
|
||||
cmd.Flags().StringVar(&conf.SSLClientCAFile, "CA", "", "path to a PEM encoded CA's certificate file")
|
||||
cmd.Flags().StringVar(&conf.ServerName, "server-name", "chaosd.chaos-mesh.org", "server name is used to verify the hostname on the returned certificates")
|
||||
cmd.Flags().StringVarP(&conf.Runtime, "runtime", "r", "docker", "current container runtime")
|
||||
cmd.Flags().BoolVar(&conf.EnablePprof, "enable-pprof", true, "enable pprof")
|
||||
cmd.Flags().IntVar(&conf.PprofPort, "pprof-port", 31766, "listen port of the pprof server")
|
||||
|
|
|
|||
230
go.mod
230
go.mod
|
|
@ -1,73 +1,199 @@
|
|||
module github.com/chaos-mesh/chaosd
|
||||
|
||||
require (
|
||||
github.com/Jeffail/tunny v0.1.4
|
||||
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/go-logr/zapr v0.1.0
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
|
||||
github.com/chaos-mesh/chaos-mesh v0.9.1-0.20220812140450-4bc7ef589c13
|
||||
github.com/chaos-mesh/chaos-mesh/api v0.0.0
|
||||
github.com/containerd/containerd v1.5.10
|
||||
github.com/docker/docker v20.10.7+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/go-logr/logr v1.2.0
|
||||
github.com/go-logr/zapr v1.2.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/google/uuid v1.2.0
|
||||
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
|
||||
github.com/onsi/gomega v1.9.0
|
||||
github.com/magiconair/properties v1.8.5
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/onsi/gomega v1.18.1
|
||||
github.com/pingcap/errors v0.11.5-0.20190809092503-95897b64e011
|
||||
github.com/pingcap/failpoint v0.0.0-20200210140405-f8f9fb234798
|
||||
github.com/pingcap/log v0.0.0-20200117041106-d28c14d3b1cd
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7
|
||||
github.com/spf13/cobra v1.1.1
|
||||
github.com/samber/lo v1.37.0
|
||||
github.com/samber/mo v1.8.0
|
||||
github.com/segmentio/kafka-go v0.4.31
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
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
|
||||
go.uber.org/fx v1.13.1
|
||||
go.uber.org/zap v1.15.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
google.golang.org/grpc v1.27.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe
|
||||
github.com/swaggo/gin-swagger v1.5.0
|
||||
github.com/swaggo/swag v1.8.3
|
||||
go.uber.org/fx v1.17.1
|
||||
go.uber.org/zap v1.21.0
|
||||
google.golang.org/grpc v1.40.0
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.20.7
|
||||
k8s.io/api v0.17.0
|
||||
k8s.io/apimachinery v0.17.0
|
||||
sigs.k8s.io/controller-runtime v0.4.0
|
||||
k8s.io/api v0.23.1
|
||||
k8s.io/apimachinery v0.23.1
|
||||
sigs.k8s.io/controller-runtime v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.8.23 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chaos-mesh/chaos-driver v0.2.1 // indirect
|
||||
github.com/cilium/ebpf v0.6.2 // indirect
|
||||
github.com/containerd/cgroups v1.0.2-0.20210605143700-23b51209bf7b // indirect
|
||||
github.com/containerd/continuity v0.1.0 // indirect
|
||||
github.com/containerd/fifo v1.0.0 // indirect
|
||||
github.com/containerd/ttrpc v1.1.0 // indirect
|
||||
github.com/containerd/typeurl v1.0.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/ethereum/go-ethereum v1.10.2 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.6 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/godbus/dbus/v5 v5.0.4 // indirect
|
||||
github.com/gogo/googleapis v1.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.14.2 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/sys/mountinfo v0.4.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect
|
||||
github.com/opencontainers/selinux v1.8.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.28.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/retailnext/iptables_exporter v0.1.0 // indirect
|
||||
github.com/romana/ipset v1.0.0 // indirect
|
||||
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
|
||||
github.com/xdg/stringprep v1.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/dig v1.14.1 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.23.0 // indirect
|
||||
k8s.io/client-go v0.23.1 // indirect
|
||||
k8s.io/component-base v0.23.1 // indirect
|
||||
k8s.io/cri-api v0.20.6 // indirect
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
|
||||
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
// github.com/chaos-mesh/chaos-mesh require /api/v1alpha1 v0.0.0, but v0.0.0 can not be found, so use replace here
|
||||
github.com/chaos-mesh/chaos-mesh/api/v1alpha1 => github.com/chaos-mesh/chaos-mesh/api/v1alpha1 v0.0.0-20210329070828-9be168b2b489
|
||||
google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
k8s.io/api => k8s.io/api v0.17.0
|
||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.17.0
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.17.1-beta.0
|
||||
k8s.io/apiserver => k8s.io/apiserver v0.17.0
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.17.0
|
||||
k8s.io/client-go => k8s.io/client-go v0.17.0
|
||||
k8s.io/cloud-provider => k8s.io/cloud-provider v0.17.0
|
||||
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.17.0
|
||||
k8s.io/code-generator => k8s.io/code-generator v0.17.1-beta.0
|
||||
k8s.io/component-base => k8s.io/component-base v0.17.0
|
||||
k8s.io/cri-api => k8s.io/cri-api v0.17.1-beta.0
|
||||
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.17.0
|
||||
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.17.0
|
||||
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.17.0
|
||||
k8s.io/kube-proxy => k8s.io/kube-proxy v0.17.0
|
||||
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.17.0
|
||||
k8s.io/kubectl => k8s.io/kubectl v0.17.0
|
||||
k8s.io/kubelet => k8s.io/kubelet v0.17.0
|
||||
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.17.0
|
||||
k8s.io/metrics => k8s.io/metrics v0.17.0
|
||||
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.17.0
|
||||
github.com/chaos-mesh/chaos-mesh/api => github.com/chaos-mesh/chaos-mesh/api v0.0.0-20220717162241-8644a0680800
|
||||
google.golang.org/grpc => google.golang.org/grpc v1.33.2
|
||||
k8s.io/api => k8s.io/api v0.23.1
|
||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.1
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.23.1
|
||||
k8s.io/apiserver => k8s.io/apiserver v0.23.1
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.1
|
||||
k8s.io/client-go => k8s.io/client-go v0.23.1
|
||||
k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.1
|
||||
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.1
|
||||
k8s.io/code-generator => k8s.io/code-generator v0.23.1
|
||||
k8s.io/component-base => k8s.io/component-base v0.23.1
|
||||
k8s.io/cri-api => k8s.io/cri-api v0.23.1
|
||||
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.1
|
||||
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.1
|
||||
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.1
|
||||
k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.1
|
||||
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.1
|
||||
k8s.io/kubectl => k8s.io/kubectl v0.23.1
|
||||
k8s.io/kubelet => k8s.io/kubelet v0.23.1
|
||||
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.1
|
||||
k8s.io/metrics => k8s.io/metrics v0.23.1
|
||||
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.1
|
||||
vbom.ml/util => github.com/fvbommel/util v0.0.2
|
||||
)
|
||||
|
||||
go 1.14
|
||||
go 1.18
|
||||
|
|
|
|||
|
|
@ -17,16 +17,14 @@ set -euo pipefail
|
|||
|
||||
function chaosd::version::get_version_vars() {
|
||||
if [[ -n ${GIT_COMMIT-} ]] || GIT_COMMIT=$(git rev-parse "HEAD^{commit}" 2>/dev/null); then
|
||||
|
||||
# Use git describe to find the version based on tags.
|
||||
if [[ -n ${GIT_VERSION-} ]] || GIT_VERSION=$(git describe --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null); then
|
||||
DASHES_IN_VERSION=$(echo "${GIT_VERSION}" | sed "s/[^-]//g")
|
||||
if [[ "${DASHES_IN_VERSION}" == "---" ]] ; then
|
||||
# We have distance to subversion (v1.1.0-subversion-1-gCommitHash)
|
||||
GIT_VERSION=$(echo "${GIT_VERSION}" | sed "s/-\([0-9]\{1,\}\)-g\([0-9a-f]\{14\}\)$/.\1\+\2/")
|
||||
elif [[ "${DASHES_IN_VERSION}" == "--" ]] ; then
|
||||
# We have distance to base tag (v1.1.0-1-gCommitHash)
|
||||
GIT_VERSION=$(echo "${GIT_VERSION}" | sed "s/-g\([0-9a-f]\{14\}\)$/+\1/")
|
||||
# --always would show the unique abbr commit as fallback.
|
||||
if [[ -n ${GIT_VERSION-} ]] || GIT_VERSION=$(git describe --always --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null); then
|
||||
# if current commit is not on a certain tag
|
||||
if ! git describe --tags --exact-match >/dev/null 2>&1 ; then
|
||||
# GIT_VERSION=gitBranch-gitCommitHash
|
||||
IFS='-' read -ra GIT_ARRAY <<< "$GIT_VERSION"
|
||||
GIT_VERSION=$(git rev-parse --abbrev-ref HEAD)-${GIT_ARRAY[${#GIT_ARRAY[@]}-1]}
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -26,12 +26,17 @@ type Config struct {
|
|||
|
||||
Version bool
|
||||
|
||||
ListenPort int
|
||||
ListenHost string
|
||||
Runtime string
|
||||
EnablePprof bool
|
||||
PprofPort int
|
||||
Platform string
|
||||
ListenPort int
|
||||
ListenHttpsPort int
|
||||
ListenHost string
|
||||
SSLCertFile string
|
||||
SSLKeyFile string
|
||||
SSLClientCAFile string
|
||||
Runtime string
|
||||
EnablePprof bool
|
||||
PprofPort int
|
||||
Platform string
|
||||
ServerName string
|
||||
}
|
||||
|
||||
// Parse parses flag definitions from the argument list.
|
||||
|
|
@ -48,6 +53,11 @@ func (c *Config) Address() string {
|
|||
return fmt.Sprintf("%s:%d", c.ListenHost, c.ListenPort)
|
||||
}
|
||||
|
||||
// Get the https server address
|
||||
func (c *Config) HttpsServerAddress() string {
|
||||
return fmt.Sprintf("%s:%d", c.ListenHost, c.ListenHttpsPort)
|
||||
}
|
||||
|
||||
// Validate is used to validate if some configurations are right.
|
||||
func (c *Config) Validate() error {
|
||||
if !checkPlatform(c.Platform) {
|
||||
|
|
@ -58,6 +68,14 @@ func (c *Config) Validate() error {
|
|||
return errors.Errorf("container runtime %s is not supported", c.Runtime)
|
||||
}
|
||||
|
||||
if (len(c.SSLCertFile) > 0 || len(c.SSLKeyFile) > 0) && (len(c.SSLCertFile) == 0 || len(c.SSLKeyFile) == 0) {
|
||||
return errors.New("provide both certificate and private key")
|
||||
}
|
||||
|
||||
if len(c.SSLClientCAFile) > 0 && len(c.SSLCertFile) == 0 {
|
||||
return errors.New("attempting to use client CA without providing server certificate")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/time/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
ClockAction = "clock"
|
||||
)
|
||||
|
||||
type ClockOption struct {
|
||||
CommonAttackConfig
|
||||
|
||||
Pid int `json:"pid,omitempty"`
|
||||
|
||||
TimeOffset string `json:"time-offset,omitempty"`
|
||||
SecDelta int64
|
||||
NsecDelta int64
|
||||
|
||||
ClockIdsSlice string `json:"clock-ids-slice,omitempty"`
|
||||
|
||||
Store ClockFuncStore
|
||||
|
||||
ClockIdsMask uint64
|
||||
}
|
||||
|
||||
type ClockFuncStore struct {
|
||||
CodeOfGetClockFunc []byte
|
||||
OriginAddress uint64
|
||||
}
|
||||
|
||||
func NewClockOption() *ClockOption {
|
||||
return &ClockOption{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: ClockAttack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (opt *ClockOption) PreProcess() error {
|
||||
clkIds := strings.Split(opt.ClockIdsSlice, ",")
|
||||
|
||||
offset, err := time.ParseDuration(opt.TimeOffset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.SecDelta = int64(offset / time.Second)
|
||||
opt.NsecDelta = int64(offset % time.Second)
|
||||
|
||||
clockIdsMask, err := utils.EncodeClkIds(clkIds)
|
||||
if err != nil {
|
||||
log.Error("error while converting clock ids to mask", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if clockIdsMask == 0 {
|
||||
log.Error("clock ids must not be empty")
|
||||
return fmt.Errorf("clock ids must not be empty")
|
||||
}
|
||||
opt.ClockIdsMask = clockIdsMask
|
||||
|
||||
if uint64(opt.SecDelta) > 1<<31 {
|
||||
log.Warn("Monotonic clock will be broken when sec delta is too large or too small.")
|
||||
if uint64(opt.SecDelta) > 1<<56 {
|
||||
log.Warn("Time zone info will be broken when sec delta is too large or too small.")
|
||||
}
|
||||
}
|
||||
|
||||
if uint64(opt.NsecDelta) > 1<<56 {
|
||||
log.Warn("Time will be broken when nanosecond delta is too large or too small")
|
||||
}
|
||||
|
||||
// Since os.FindProcess in unix systems will always succeed
|
||||
// regardless of whether the process exists (https://pkg.go.dev/os#FindProcess),
|
||||
// we need to use process.Signal to check if pid is accessible.
|
||||
process, err := os.FindProcess(opt.Pid)
|
||||
if err != nil {
|
||||
log.Error("failed to find process", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
if err != nil {
|
||||
log.Error("pid may not be accessible", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opt *ClockOption) CompleteDefaults() {
|
||||
if len(opt.ClockIdsSlice) == 0 {
|
||||
opt.ClockIdsSlice = "CLOCK_REALTIME"
|
||||
}
|
||||
}
|
||||
|
||||
func (opt ClockOption) RecoverData() string {
|
||||
data, _ := json.Marshal(opt)
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
|
@ -33,6 +33,9 @@ type AttackConfig interface {
|
|||
|
||||
// CompleteDefaults is used to fill flags with default values
|
||||
CompleteDefaults()
|
||||
|
||||
// GetUID returns the experiment's ID
|
||||
GetUID() string
|
||||
}
|
||||
|
||||
type SchedulerConfig struct {
|
||||
|
|
@ -57,6 +60,7 @@ type CommonAttackConfig struct {
|
|||
|
||||
Action string `json:"action"`
|
||||
Kind string `json:"kind"`
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
func (config CommonAttackConfig) String() string {
|
||||
|
|
@ -79,3 +83,7 @@ func (config *CommonAttackConfig) Validate() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *CommonAttackConfig) GetUID() string {
|
||||
return config.UID
|
||||
}
|
||||
|
|
|
|||
311
pkg/core/disk.go
311
pkg/core/disk.go
|
|
@ -16,9 +16,13 @@ package core
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
|
@ -29,85 +33,256 @@ const (
|
|||
DiskReadPayloadAction = "read-payload"
|
||||
)
|
||||
|
||||
type DiskOption struct {
|
||||
var _ AttackConfig = &DiskAttackConfig{}
|
||||
|
||||
type DiskAttackConfig struct {
|
||||
CommonAttackConfig
|
||||
|
||||
Size string `json:"size"`
|
||||
Path string `json:"path"`
|
||||
Percent string `json:"percent"`
|
||||
PayloadProcessNum uint8 `json:"payload_process_num"`
|
||||
|
||||
FillByFallocate bool `json:"fill_by_fallocate"`
|
||||
DdOptions *[]DdOption
|
||||
FAllocateOption *FAllocateOption
|
||||
Path string
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
Size string `json:"size,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Percent string `json:"percent,omitempty"`
|
||||
PayloadProcessNum uint8 `json:"payload-process-num,omitempty"`
|
||||
|
||||
FillByFallocate bool `json:"fallocate,omitempty"`
|
||||
}
|
||||
|
||||
func NewDiskOption() *DiskOption {
|
||||
return &DiskOption{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: DiskAttack,
|
||||
},
|
||||
PayloadProcessNum: 1,
|
||||
FillByFallocate: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDiskOptionForServer() *DiskOption {
|
||||
return &DiskOption{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: DiskServerAttack,
|
||||
},
|
||||
PayloadProcessNum: 1,
|
||||
FillByFallocate: true,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
opt.Path, err = os.Getwd()
|
||||
if err != nil {
|
||||
log.Error("unexpected err when execute os.Getwd()", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
fi, 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 := os.WriteFile(opt.Path, b, 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Remove(opt.Path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return opt.Path, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
opt.Path, err = utils.CreateTempFile(opt.Path)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("unexpected err : %v , when CreateTempFile in action %s with path %s.", err, opt.Action, opt.Path))
|
||||
return "", err
|
||||
}
|
||||
if err := os.Remove(opt.Path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return opt.Path, err
|
||||
}
|
||||
return "", fmt.Errorf("fill into an existing file")
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2023 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)
|
||||
}
|
||||
|
|
@ -31,12 +31,20 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
ProcessAttack = "process"
|
||||
NetworkAttack = "network"
|
||||
StressAttack = "stress"
|
||||
DiskAttack = "disk"
|
||||
HostAttack = "host"
|
||||
JVMAttack = "jvm"
|
||||
ProcessAttack = "process"
|
||||
NetworkAttack = "network"
|
||||
StressAttack = "stress"
|
||||
DiskAttack = "disk"
|
||||
DiskServerAttack = "disk-server"
|
||||
ClockAttack = "clock"
|
||||
HostAttack = "host"
|
||||
JVMAttack = "jvm"
|
||||
KafkaAttack = "kafka"
|
||||
RedisAttack = "redis"
|
||||
FileAttack = "file"
|
||||
HTTPAttack = "http"
|
||||
VMAttack = "vm"
|
||||
UserDefinedAttack = "userDefined"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -77,8 +85,21 @@ func (exp *Experiment) GetRequestCommand() (AttackConfig, error) {
|
|||
return exp.cachedRequestCommand, nil
|
||||
}
|
||||
|
||||
attackConfig := GetAttackByKind(exp.Kind)
|
||||
if attackConfig == nil {
|
||||
return nil, perr.Errorf("chaos experiment kind %s not found", exp.Kind)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(exp.RecoverCommand), attackConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exp.cachedRequestCommand = *attackConfig
|
||||
return *attackConfig, nil
|
||||
}
|
||||
|
||||
func GetAttackByKind(kind string) *AttackConfig {
|
||||
var attackConfig AttackConfig
|
||||
switch exp.Kind {
|
||||
switch kind {
|
||||
case ProcessAttack:
|
||||
attackConfig = &ProcessCommand{}
|
||||
case NetworkAttack:
|
||||
|
|
@ -88,14 +109,28 @@ func (exp *Experiment) GetRequestCommand() (AttackConfig, error) {
|
|||
case StressAttack:
|
||||
attackConfig = &StressCommand{}
|
||||
case DiskAttack:
|
||||
attackConfig = &DiskOption{}
|
||||
attackConfig = &DiskAttackConfig{}
|
||||
case DiskServerAttack:
|
||||
attackConfig = &DiskAttackConfig{}
|
||||
case JVMAttack:
|
||||
attackConfig = &JVMCommand{}
|
||||
case ClockAttack:
|
||||
attackConfig = &ClockOption{}
|
||||
case KafkaAttack:
|
||||
attackConfig = &KafkaCommand{}
|
||||
case RedisAttack:
|
||||
attackConfig = &RedisCommand{}
|
||||
case FileAttack:
|
||||
attackConfig = &FileCommand{}
|
||||
case HTTPAttack:
|
||||
attackConfig = &HTTPAttackConfig{}
|
||||
case VMAttack:
|
||||
attackConfig = &VMOption{}
|
||||
case UserDefinedAttack:
|
||||
attackConfig = &UserDefinedOption{}
|
||||
default:
|
||||
return nil, perr.Errorf("chaos experiment kind %s not found", exp.Kind)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(exp.RecoverCommand), attackConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exp.cachedRequestCommand = attackConfig
|
||||
return attackConfig, nil
|
||||
return &attackConfig
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ type ExperimentRun struct {
|
|||
func (exp Experiment) NewRun() *ExperimentRun {
|
||||
return &ExperimentRun{
|
||||
ExperimentID: exp.ID,
|
||||
UID: uuid.New().String(),
|
||||
Status: RunStarted,
|
||||
// TODO: maybe need to use specified uid
|
||||
UID: uuid.New().String(),
|
||||
Status: RunStarted,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2022 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
)
|
||||
|
||||
type FileCommand struct {
|
||||
CommonAttackConfig
|
||||
|
||||
// FileName is the name of the file to be created, modified, deleted, renamed, or appended.
|
||||
FileName string `json:"file-name,omitempty"`
|
||||
// DirName is the directory name to create or delete.
|
||||
DirName string `json:"dir-name,omitempty"`
|
||||
// Privilege is the file privilege to be set.
|
||||
Privilege uint32 `json:"privilege,omitempty"`
|
||||
// SourceFile is the name need to be renamed.
|
||||
SourceFile string `json:"source-file,omitempty"`
|
||||
// DestFile is the name to be renamed.
|
||||
DestFile string `json:"dest-file,omitempty"`
|
||||
// Data is the data for append.
|
||||
Data string `json:"data,omitempty"`
|
||||
// Count is the number of times to append the data.
|
||||
Count int `json:"count,omitempty"`
|
||||
// OriginPrivilege used to save the file's origin privilege.
|
||||
OriginPrivilege int `json:"origin-privilege,omitempty"`
|
||||
|
||||
// OriginStr is the origin string of the file.
|
||||
OriginStr string `json:"origin-string,omitempty"`
|
||||
// DestStr is the destination string of the file.
|
||||
DestStr string `json:"dest-string,omitempty"`
|
||||
// Line is the line number of the file to be replaced.
|
||||
Line int `json:"line,omitempty"`
|
||||
}
|
||||
|
||||
var _ AttackConfig = &FileCommand{}
|
||||
|
||||
const (
|
||||
FileCreateAction = "create"
|
||||
FileModifyPrivilegeAction = "modify"
|
||||
FileDeleteAction = "delete"
|
||||
FileRenameAction = "rename"
|
||||
FileAppendAction = "append"
|
||||
FileReplaceAction = "replace"
|
||||
)
|
||||
|
||||
func (n *FileCommand) Validate() error {
|
||||
if err := n.CommonAttackConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch n.Action {
|
||||
case FileCreateAction:
|
||||
return n.validFileCreate()
|
||||
case FileModifyPrivilegeAction:
|
||||
return n.validFileModify()
|
||||
case FileDeleteAction:
|
||||
return n.validFileDelete()
|
||||
case FileRenameAction:
|
||||
return n.validFileRename()
|
||||
case FileAppendAction:
|
||||
return n.validFileAppend()
|
||||
case FileReplaceAction:
|
||||
return n.validFileReplace()
|
||||
default:
|
||||
return errors.Errorf("file action %s not supported", n.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *FileCommand) validFileCreate() error {
|
||||
if len(n.FileName) == 0 && len(n.DirName) == 0 {
|
||||
return errors.New("one of file-name and dir-name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FileCommand) validFileModify() error {
|
||||
if len(n.FileName) == 0 {
|
||||
return errors.New("file name is required")
|
||||
}
|
||||
|
||||
if n.Privilege == 0 {
|
||||
return errors.New("file privilege is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FileCommand) validFileDelete() error {
|
||||
if len(n.FileName) == 0 && len(n.DirName) == 0 {
|
||||
return errors.New("one of file-name and dir-name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FileCommand) validFileRename() error {
|
||||
if len(n.SourceFile) == 0 || len(n.DestFile) == 0 {
|
||||
return errors.New("both source file and destination file are required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FileCommand) validFileAppend() error {
|
||||
if len(n.FileName) == 0 {
|
||||
return errors.New("file-name is required")
|
||||
}
|
||||
|
||||
if len(n.Data) == 0 {
|
||||
return errors.New("append data is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FileCommand) validFileReplace() error {
|
||||
if len(n.FileName) == 0 {
|
||||
return errors.New("file-name is required")
|
||||
}
|
||||
|
||||
if len(n.OriginStr) == 0 || len(n.DestStr) == 0 {
|
||||
return errors.New("both origin and destination string are required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FileCommand) CompleteDefaults() {
|
||||
switch n.Action {
|
||||
case FileAppendAction:
|
||||
n.setDefaultForFileAppend()
|
||||
}
|
||||
}
|
||||
|
||||
func (n *FileCommand) setDefaultForFileAppend() {
|
||||
if n.Count == 0 {
|
||||
n.Count = 1
|
||||
}
|
||||
}
|
||||
|
||||
func (n FileCommand) RecoverData() string {
|
||||
data, _ := json.Marshal(n)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func NewFileCommand() *FileCommand {
|
||||
return &FileCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: FileAttack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
const (
|
||||
HostShutdownAction = "shutdown"
|
||||
HostRebootAction = "reboot"
|
||||
)
|
||||
|
||||
type HostCommand struct {
|
||||
|
|
@ -40,8 +41,7 @@ func (h HostCommand) RecoverData() string {
|
|||
func NewHostCommand() *HostCommand {
|
||||
return &HostCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: HostAttack,
|
||||
Action: HostShutdownAction,
|
||||
Kind: HostAttack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2022 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 (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/zapr"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/chaosdaemon/tproxyconfig"
|
||||
"github.com/pingcap/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
TargetRequest tproxyconfig.PodHttpChaosTarget = "Request"
|
||||
TargetResponse tproxyconfig.PodHttpChaosTarget = "Response"
|
||||
)
|
||||
|
||||
const (
|
||||
HTTPAbortAction = "abort"
|
||||
HTTPDelayAction = "delay"
|
||||
HTTPConfigAction = "config"
|
||||
HTTPRequestAction = "request"
|
||||
)
|
||||
|
||||
var _ AttackConfig = &HTTPAttackConfig{}
|
||||
|
||||
type HTTPAttackConfig struct {
|
||||
CommonAttackConfig
|
||||
Config tproxyconfig.Config
|
||||
ProxyPID int
|
||||
|
||||
Logger logr.Logger
|
||||
|
||||
HTTPRequestConfig
|
||||
}
|
||||
|
||||
func (c HTTPAttackConfig) RecoverData() string {
|
||||
data, _ := json.Marshal(c)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type HTTPAttackOption struct {
|
||||
CommonAttackConfig
|
||||
|
||||
ProxyPorts []uint `json:"proxy_ports"`
|
||||
Target string `json:"target"`
|
||||
Port int32 `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Abort bool `json:"abort"`
|
||||
Delay string `json:"delay"`
|
||||
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
|
||||
HTTPRequestConfig `json:",inline"`
|
||||
}
|
||||
|
||||
type HTTPRequestConfig struct {
|
||||
// used for HTTP request, now only support GET
|
||||
URL string `json:"url,omitempty"`
|
||||
EnableConnPool bool `json:"enable-conn-pool,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
func NewHTTPAttackOption() *HTTPAttackOption {
|
||||
return &HTTPAttackOption{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: HTTPAttack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPAttackOption) PreProcess() (*HTTPAttackConfig, error) {
|
||||
var c tproxyconfig.Config
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger).WithName("HTTP Attack")
|
||||
switch o.CommonAttackConfig.Action {
|
||||
case HTTPAbortAction, HTTPDelayAction:
|
||||
switch o.Target {
|
||||
case string(TargetRequest), string(TargetResponse):
|
||||
default:
|
||||
return nil, errors.New("HTTP Attack Target must be Request or Response")
|
||||
}
|
||||
rule := tproxyconfig.PodHttpChaosBaseRule{
|
||||
Target: tproxyconfig.PodHttpChaosTarget(o.Target),
|
||||
Selector: tproxyconfig.PodHttpChaosSelector{},
|
||||
Actions: tproxyconfig.PodHttpChaosActions{},
|
||||
}
|
||||
if o.Path != "" {
|
||||
rule.Selector.Path = &o.Path
|
||||
}
|
||||
if o.Method != "" {
|
||||
rule.Selector.Method = &o.Method
|
||||
}
|
||||
if o.Code != "" {
|
||||
code, err := strconv.Atoi(o.Code)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "parsing %v", o)
|
||||
}
|
||||
codeI32 := int32(code)
|
||||
rule.Selector.Code = &codeI32
|
||||
}
|
||||
if o.Port != 0 {
|
||||
rule.Selector.Port = &o.Port
|
||||
}
|
||||
rule.Actions.Abort = &o.Abort
|
||||
if o.CommonAttackConfig.Action == HTTPDelayAction {
|
||||
rule.Actions.Abort = nil
|
||||
_, err := time.ParseDuration(o.Delay)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "HTTP Delay")
|
||||
}
|
||||
rule.Actions.Delay = &o.Delay
|
||||
}
|
||||
ports := make([]uint32, len(o.ProxyPorts))
|
||||
for i, port := range o.ProxyPorts {
|
||||
ports[i] = uint32(port)
|
||||
}
|
||||
c.ProxyPorts = ports
|
||||
c.Rules = []tproxyconfig.PodHttpChaosBaseRule{rule}
|
||||
case HTTPConfigAction:
|
||||
b, err := os.ReadFile(o.FilePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read HTTP attack config file")
|
||||
}
|
||||
|
||||
ext := filepath.Ext(o.FilePath)
|
||||
switch ext {
|
||||
case ".json":
|
||||
err := json.Unmarshal(b, &c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "json unmarshal HTTP attack config file")
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("ext: %s, is not support", ext)
|
||||
}
|
||||
case HTTPRequestAction:
|
||||
if o.URL == "" {
|
||||
return nil, errors.New("URL is required")
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported action: %s", o.CommonAttackConfig.Action)
|
||||
}
|
||||
if len(c.ProxyPorts) == 0 && o.CommonAttackConfig.Action != HTTPRequestAction {
|
||||
return nil, errors.New("proxy_ports is not an option, you must offer it")
|
||||
}
|
||||
|
||||
return &HTTPAttackConfig{
|
||||
CommonAttackConfig: o.CommonAttackConfig,
|
||||
Config: c,
|
||||
Logger: logger,
|
||||
HTTPRequestConfig: o.HTTPRequestConfig,
|
||||
}, nil
|
||||
}
|
||||
241
pkg/core/jvm.go
241
pkg/core/jvm.go
|
|
@ -23,112 +23,201 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
JVMInstallType = "install"
|
||||
JVMSubmitType = "submit"
|
||||
|
||||
// jvm action
|
||||
JVMLatencyAction = "latency"
|
||||
JVMExceptionAction = "exception"
|
||||
JVMReturnAction = "return"
|
||||
JVMStressAction = "stress"
|
||||
JVMGCAction = "gc"
|
||||
JVMRuleFileAction = "rule_file"
|
||||
JVMRuleFileAction = "rule-file"
|
||||
JVMRuleDataAction = "rule-data"
|
||||
JVMMySQLAction = "mysql"
|
||||
|
||||
// for action 'mysql', 'gc' and 'stress'
|
||||
SQLHelper = "org.chaos_mesh.byteman.helper.SQLHelper"
|
||||
GCHelper = "org.chaos_mesh.byteman.helper.GCHelper"
|
||||
StressHelper = "org.chaos_mesh.byteman.helper.StressHelper"
|
||||
|
||||
// the trigger point for 'gc' and 'stress'
|
||||
TriggerClass = "org.chaos_mesh.chaos_agent.TriggerThread"
|
||||
TriggerMethod = "triggerFunc"
|
||||
|
||||
MySQL5InjectClass = "com.mysql.jdbc.MysqlIO"
|
||||
MySQL5InjectMethod = "sqlQueryDirect"
|
||||
MySQL5Exception = "java.sql.SQLException(\"%s\")"
|
||||
|
||||
MySQL8InjectClass = "com.mysql.cj.NativeSession"
|
||||
MySQL8InjectMethod = "execSQL"
|
||||
MySQL8Exception = "com.mysql.cj.exceptions.CJException(\"%s\")"
|
||||
)
|
||||
|
||||
// byteman rule template
|
||||
const (
|
||||
SimpleRuleTemplate = `
|
||||
RULE {{.Name}}
|
||||
CLASS {{.Class}}
|
||||
METHOD {{.Method}}
|
||||
AT ENTRY
|
||||
IF true
|
||||
DO
|
||||
{{.Do}};
|
||||
ENDRULE
|
||||
`
|
||||
|
||||
CompleteRuleTemplate = `
|
||||
RULE {{.Name}}
|
||||
CLASS {{.Class}}
|
||||
METHOD {{.Method}}
|
||||
HELPER {{.Helper}}
|
||||
AT ENTRY
|
||||
BIND {{.Bind}};
|
||||
IF {{.Condition}}
|
||||
DO
|
||||
{{.Do}};
|
||||
ENDRULE
|
||||
`
|
||||
)
|
||||
|
||||
type JVMCommand struct {
|
||||
CommonAttackConfig
|
||||
|
||||
JVMCommonSpec
|
||||
|
||||
JVMClassMethodSpec
|
||||
|
||||
JVMStressSpec
|
||||
|
||||
JVMMySQLSpec
|
||||
|
||||
// rule name, should be unique, and will generate by chaosd automatically
|
||||
Name string
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Java class
|
||||
Class string
|
||||
|
||||
// the method in Java class
|
||||
Method string
|
||||
|
||||
// fault action, values can be latency, exception, return, stress
|
||||
Action string
|
||||
// fault action, values can be latency, exception, return, stress, gc, rule-file, rule-data, mysql
|
||||
Action string `json:"action,omitempty"`
|
||||
|
||||
// the return value for action 'return'
|
||||
ReturnValue string
|
||||
ReturnValue string `json:"value,omitempty"`
|
||||
|
||||
// the exception which needs to throw dor action `exception`
|
||||
ThrowException string
|
||||
// the exception which needs to throw for action `exception`
|
||||
// or the exception message needs to throw in action `mysql`
|
||||
ThrowException string `json:"exception,omitempty"`
|
||||
|
||||
// the latency duration for action 'latency'
|
||||
LatencyDuration string
|
||||
// or the latency duration in action `mysql`
|
||||
LatencyDuration int `json:"latency,omitempty"`
|
||||
|
||||
// the CPU core number need to use, only set it when action is stress
|
||||
CPUCount int
|
||||
// btm rule file path for action 'rule-file'
|
||||
RuleFile string `json:"rule-file,omitempty"`
|
||||
|
||||
// the memory size need to locate, only set it when action is stress
|
||||
MemorySize int
|
||||
|
||||
// attach or agent
|
||||
Type string
|
||||
// RuleData used to save the rule file's data, will use it when recover, for action 'rule-data'
|
||||
RuleData string `json:"rule-data,omitempty"`
|
||||
}
|
||||
|
||||
type JVMCommonSpec struct {
|
||||
// the port of agent server
|
||||
Port int
|
||||
Port int `json:"port,omitempty"`
|
||||
|
||||
// the pid of Java process which need to attach
|
||||
Pid int
|
||||
Pid int `json:"pid,omitempty"`
|
||||
}
|
||||
|
||||
// below is only used for template
|
||||
Do string
|
||||
type JVMClassMethodSpec struct {
|
||||
// Java class
|
||||
Class string `json:"class,omitempty"`
|
||||
|
||||
StressType string
|
||||
// the method in Java class
|
||||
Method string `json:"method,omitempty"`
|
||||
}
|
||||
|
||||
type JVMStressSpec struct {
|
||||
// the CPU core number need to use, only set it when action is stress
|
||||
CPUCount int `json:"cpu-count,omitempty"`
|
||||
|
||||
// the memory type need to locate, only set it when action is stress, the value can be 'stack' or 'heap'
|
||||
MemoryType string `json:"mem-type,omitempty"`
|
||||
}
|
||||
|
||||
// JVMMySQLSpec is the specification of MySQL fault injection in JVM
|
||||
// only when SQL match the Database, Table and SQLType, chaosd will inject fault
|
||||
// for example:
|
||||
//
|
||||
// SQL is "select * from test.t1",
|
||||
// only when ((Database == "test" || Database == "") && (Table == "t1" || Table == "") && (SQLType == "select" || SQLType == "")) is true, chaosd will inject fault
|
||||
type JVMMySQLSpec struct {
|
||||
// the version of mysql-connector-java, only support 5.X.X(set to 5) and 8.X.X(set to 8) now
|
||||
MySQLConnectorVersion string `json:"mysql-connector-version,omitempty"`
|
||||
|
||||
// the match database
|
||||
// default value is "", means match all database
|
||||
Database string `json:"database,omitempty"`
|
||||
|
||||
// the match table
|
||||
// default value is "", means match all table
|
||||
Table string `json:"table,omitempty"`
|
||||
|
||||
// the match sql type
|
||||
// default value is "", means match all SQL type
|
||||
SQLType string `json:"sql-type,omitempty"`
|
||||
}
|
||||
|
||||
type BytemanTemplateSpec struct {
|
||||
Name string
|
||||
Class string
|
||||
Method string
|
||||
Helper string
|
||||
Bind string
|
||||
Condition string
|
||||
Do string
|
||||
|
||||
// below is only used for stress template
|
||||
StressType string
|
||||
StressValueName string
|
||||
|
||||
StressValue int
|
||||
|
||||
// btm rule file path
|
||||
RuleFile string
|
||||
|
||||
// RuleData used to save the rule file's data, will use it when recover
|
||||
RuleData []byte
|
||||
StressValue string
|
||||
}
|
||||
|
||||
func (j *JVMCommand) Validate() error {
|
||||
switch j.Type {
|
||||
case JVMInstallType:
|
||||
if j.Pid == 0 {
|
||||
return errors.New("pid can't be 0")
|
||||
}
|
||||
case JVMSubmitType:
|
||||
switch j.Action {
|
||||
case JVMStressAction:
|
||||
if j.CPUCount == 0 && j.MemorySize == 0 {
|
||||
return errors.New("must set one of cpu-count and mem-size when action is 'stress'")
|
||||
}
|
||||
if j.Pid == 0 {
|
||||
return errors.New("pid can't be 0")
|
||||
}
|
||||
|
||||
if j.CPUCount > 0 && j.MemorySize > 0 {
|
||||
return errors.New("inject stress on both CPU and memory is not support now")
|
||||
}
|
||||
case JVMGCAction:
|
||||
// do nothing
|
||||
case JVMExceptionAction, JVMReturnAction, JVMLatencyAction:
|
||||
if len(j.Class) == 0 {
|
||||
return errors.New("class not provided")
|
||||
}
|
||||
|
||||
if len(j.Method) == 0 {
|
||||
return errors.New("method not provided")
|
||||
}
|
||||
case JVMRuleFileAction:
|
||||
if len(j.RuleFile) == 0 {
|
||||
return errors.New("rule file not provided")
|
||||
}
|
||||
case "":
|
||||
return errors.New("action not provided, action can be 'latency', 'exception', 'return', 'stress' or 'gc'")
|
||||
default:
|
||||
return errors.New(fmt.Sprintf("action %s not supported, action can be 'latency', 'exception', 'return', 'stress' or 'gc'", j.Action))
|
||||
switch j.Action {
|
||||
case JVMStressAction:
|
||||
if j.CPUCount == 0 && len(j.MemoryType) == 0 {
|
||||
return errors.New("must set one of cpu-count and mem-type when action is 'stress'")
|
||||
}
|
||||
|
||||
if j.CPUCount > 0 && len(j.MemoryType) > 0 {
|
||||
return errors.New("inject stress on both CPU and memory is not support now")
|
||||
}
|
||||
case JVMGCAction:
|
||||
// do nothing
|
||||
case JVMExceptionAction, JVMReturnAction, JVMLatencyAction:
|
||||
if len(j.Class) == 0 {
|
||||
return errors.New("class not provided")
|
||||
}
|
||||
|
||||
if len(j.JVMClassMethodSpec.Method) == 0 {
|
||||
return errors.New("method not provided")
|
||||
}
|
||||
case JVMRuleFileAction:
|
||||
if len(j.RuleFile) == 0 {
|
||||
return errors.New("rule file not provided")
|
||||
}
|
||||
case JVMRuleDataAction:
|
||||
if len(j.RuleData) == 0 {
|
||||
return errors.New("rule data not provide")
|
||||
}
|
||||
case JVMMySQLAction:
|
||||
if len(j.MySQLConnectorVersion) == 0 {
|
||||
return errors.New("MySQL connector version not provided")
|
||||
}
|
||||
if len(j.ThrowException) == 0 && j.LatencyDuration == 0 {
|
||||
return errors.New("must set one of exception or latency")
|
||||
}
|
||||
case "":
|
||||
return errors.New("type not provided, type can be 'install' or 'submit'")
|
||||
return errors.New("action not provided")
|
||||
default:
|
||||
return errors.New(fmt.Sprintf("type %s not supported, type can be 'install' or 'submit'", j.Type))
|
||||
return errors.New(fmt.Sprintf("action %s not supported, action can be 'latency', 'exception', 'return', 'stress', 'gc', 'rule-file' of 'rule-data'", j.Action))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -141,10 +230,12 @@ func (j *JVMCommand) RecoverData() string {
|
|||
}
|
||||
|
||||
func (j *JVMCommand) CompleteDefaults() {
|
||||
if j.Type == JVMSubmitType {
|
||||
if len(j.Name) == 0 {
|
||||
j.Name = fmt.Sprintf("%s-%s-%s-%s-%s", j.Class, j.Method, j.Action, j.Type, utils.RandomStringWithCharset(5))
|
||||
}
|
||||
if len(j.Name) == 0 {
|
||||
j.Name = fmt.Sprintf("%s-%s-%s-%s", j.Class, j.Method, j.Action, utils.RandomStringWithCharset(5))
|
||||
}
|
||||
|
||||
if j.Port == 0 {
|
||||
j.Port = 9288
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,79 +28,90 @@ func TestJVMCommand(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
&JVMCommand{},
|
||||
"type not provided",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMInstallType,
|
||||
},
|
||||
"pid can't be 0",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMInstallType,
|
||||
Pid: 123,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
},
|
||||
"action not provided",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: "test",
|
||||
},
|
||||
"action test not supported",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: JVMLatencyAction,
|
||||
},
|
||||
"class not provided",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: JVMExceptionAction,
|
||||
Class: "test",
|
||||
JVMClassMethodSpec: JVMClassMethodSpec{
|
||||
Class: "test",
|
||||
},
|
||||
},
|
||||
"method not provided",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: JVMExceptionAction,
|
||||
Class: "test",
|
||||
Method: "test",
|
||||
JVMClassMethodSpec: JVMClassMethodSpec{
|
||||
Class: "test",
|
||||
Method: "test",
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: JVMStressAction,
|
||||
},
|
||||
"must set one of cpu-count and mem-size",
|
||||
"must set one of cpu-count and mem-type",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
Action: JVMStressAction,
|
||||
CPUCount: 1,
|
||||
MemorySize: 1,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: JVMStressAction,
|
||||
JVMStressSpec: JVMStressSpec{
|
||||
CPUCount: 1,
|
||||
MemoryType: "heap",
|
||||
},
|
||||
},
|
||||
"inject stress on both CPU and memory is not support now",
|
||||
},
|
||||
{
|
||||
&JVMCommand{
|
||||
Type: JVMSubmitType,
|
||||
Action: JVMStressAction,
|
||||
CPUCount: 1,
|
||||
JVMCommonSpec: JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: JVMStressAction,
|
||||
JVMStressSpec: JVMStressSpec{
|
||||
CPUCount: 1,
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2022 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 (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type KafkaAttackAction string
|
||||
|
||||
const (
|
||||
// Kafka actions
|
||||
KafkaFillAction KafkaAttackAction = "fill"
|
||||
KafkaFloodAction = "flood"
|
||||
KafkaIOAction = "io"
|
||||
)
|
||||
|
||||
type KafkaAuthMechanism string
|
||||
|
||||
const (
|
||||
SaslPlain KafkaAuthMechanism = "sasl/plain"
|
||||
SaslScream256 = "sasl/scram-sha-256"
|
||||
SaslScram512 = "sasl/scram-sha-512"
|
||||
AuthMechanismEmpty = ""
|
||||
)
|
||||
|
||||
var _ AttackConfig = &KafkaCommand{}
|
||||
|
||||
type KafkaCommand struct {
|
||||
CommonAttackConfig
|
||||
|
||||
// global options
|
||||
Action KafkaAttackAction
|
||||
Topic string `json:"topic,omitempty"`
|
||||
|
||||
// options for fill and flood attack
|
||||
Host string `json:"host,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
AuthMechanism string `json:"authMechanism,omitempty"`
|
||||
MessageSize uint `json:"messageSize,omitempty"`
|
||||
MaxBytes uint64 `json:"maxBytes,omitempty"`
|
||||
|
||||
// options for fill attack
|
||||
ReloadCommand string `json:"reloadCommand,omitempty"`
|
||||
|
||||
// options for flood attack
|
||||
Threads uint `json:"threads,omitempty"`
|
||||
|
||||
// options for fill and io attack
|
||||
ConfigFile string `json:"configFile,omitempty"`
|
||||
|
||||
// options for io attack
|
||||
NonReadable bool `json:"nonReadable,omitempty"`
|
||||
NonWritable bool `json:"nonWritable,omitempty"`
|
||||
|
||||
// recover data for io attack
|
||||
OriginModeOfFiles map[string]uint32 `json:"originModeOfFiles,omitempty"`
|
||||
OriginConfig string `json:"originConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) Validate() error {
|
||||
if c.Topic == "" {
|
||||
return errors.New("topic is required")
|
||||
}
|
||||
|
||||
if err := c.validateAuthMechanism(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch c.Action {
|
||||
case KafkaFillAction:
|
||||
return c.validateFillAction()
|
||||
case KafkaFloodAction:
|
||||
return c.validateFloodAction()
|
||||
case KafkaIOAction:
|
||||
return c.validateIOAction()
|
||||
default:
|
||||
return errors.Errorf("invalid action: %s", c.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) validateAuthMechanism() error {
|
||||
if c.Username != "" && c.AuthMechanism == "" {
|
||||
return errors.New("auth mechanism is required")
|
||||
}
|
||||
|
||||
switch KafkaAuthMechanism(c.AuthMechanism) {
|
||||
case SaslPlain:
|
||||
fallthrough
|
||||
case SaslScram512:
|
||||
fallthrough
|
||||
case SaslScream256:
|
||||
fallthrough
|
||||
case AuthMechanismEmpty:
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("invalid auth mechanism: %s", c.AuthMechanism)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) validateDSNAndMessageSize() error {
|
||||
if c.Host == "" {
|
||||
return errors.New("host is required")
|
||||
}
|
||||
if c.Port == 0 {
|
||||
return errors.New("port is required")
|
||||
}
|
||||
if c.MessageSize == 0 {
|
||||
return errors.New("message size is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) validateFillAction() error {
|
||||
if c.MaxBytes == 0 {
|
||||
return errors.New("max bytes is required")
|
||||
}
|
||||
if c.ReloadCommand == "" {
|
||||
return errors.New("reload command is required")
|
||||
}
|
||||
if _, err := os.Stat(c.ConfigFile); errors.Is(err, os.ErrNotExist) {
|
||||
return errors.Errorf("config file %s not exists", c.ConfigFile)
|
||||
}
|
||||
return c.validateDSNAndMessageSize()
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) validateFloodAction() error {
|
||||
if c.Threads == 0 {
|
||||
return errors.New("threads is required")
|
||||
}
|
||||
return c.validateDSNAndMessageSize()
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) validateIOAction() error {
|
||||
if _, err := os.Stat(c.ConfigFile); errors.Is(err, os.ErrNotExist) {
|
||||
return errors.Errorf("config file %s not exists", c.ConfigFile)
|
||||
}
|
||||
if !c.NonReadable && !c.NonWritable {
|
||||
return errors.New("at least one of non-readable or non-writable is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) RecoverData() string {
|
||||
data, _ := json.Marshal(c)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (c *KafkaCommand) CompleteDefaults() {
|
||||
c.CommonAttackConfig.CompleteDefaults()
|
||||
}
|
||||
|
||||
func NewKafkaCommand() *KafkaCommand {
|
||||
return &KafkaCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: KafkaAttack,
|
||||
},
|
||||
OriginModeOfFiles: make(map[string]uint32),
|
||||
}
|
||||
}
|
||||
|
|
@ -20,9 +20,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
|
||||
"github.com/chaos-mesh/chaos-mesh/controllers/podnetworkchaos/netutils"
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/chaosdaemon/pb"
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/netem"
|
||||
"github.com/pingcap/errors"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
|
@ -30,34 +32,57 @@ import (
|
|||
type NetworkCommand struct {
|
||||
CommonAttackConfig
|
||||
|
||||
Latency string
|
||||
Jitter string
|
||||
Correlation string
|
||||
Percent string
|
||||
Device string
|
||||
SourcePort string
|
||||
EgressPort string
|
||||
IPAddress string
|
||||
IPProtocol string
|
||||
Hostname string
|
||||
Latency string `json:"latency,omitempty"`
|
||||
Jitter string `json:"jitter,omitempty"`
|
||||
Correlation string `json:"correlation,omitempty"`
|
||||
Percent string `json:"percent,omitempty"`
|
||||
Device string `json:"device,omitempty"`
|
||||
SourcePort string `json:"source-port,omitempty"`
|
||||
EgressPort string `json:"egress-port,omitempty"`
|
||||
IPAddress string `json:"ip-address,omitempty"`
|
||||
IPProtocol string `json:"ip-protocol,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
|
||||
Direction string `json:"direction,omitempty"`
|
||||
|
||||
// used for DNS attack
|
||||
DNSServer string
|
||||
Port string
|
||||
PortPid int32
|
||||
DNSIp string
|
||||
DNSHost string
|
||||
DNSServer string `json:"dns-server,omitempty"`
|
||||
DNSIp string `json:"dns-ip,omitempty"`
|
||||
DNSDomainName string `json:"dns-domain-name,omitempty"`
|
||||
|
||||
// used for port occupied or flood
|
||||
Port string `json:"port,omitempty"`
|
||||
PortPid int32 `json:"port-pid,omitempty"`
|
||||
|
||||
*BandwidthSpec `json:",inline"`
|
||||
// only the packet which match the tcp flag can be accepted, others will be dropped.
|
||||
// only set when the IPProtocol is tcp, used for partition.
|
||||
AcceptTCPFlags string `json:"accept-tcp-flags,omitempty"`
|
||||
|
||||
// used for flood
|
||||
// number of iperf parallel client threads to run
|
||||
Parallel int32 `json:"parallel,omitempty"`
|
||||
|
||||
// used for flood
|
||||
// the pid of iperf
|
||||
IperfPid int32 `json:"iperf-pid,omitempty"`
|
||||
}
|
||||
|
||||
var _ AttackConfig = &NetworkCommand{}
|
||||
|
||||
const (
|
||||
NetworkDelayAction = "delay"
|
||||
NetworkLossAction = "loss"
|
||||
NetworkCorruptAction = "corrupt"
|
||||
NetworkDuplicateAction = "duplicate"
|
||||
NetworkDNSAction = "dns"
|
||||
NetworkPortOccupied = "occupied"
|
||||
NetworkDelayAction = "delay"
|
||||
NetworkLossAction = "loss"
|
||||
NetworkCorruptAction = "corrupt"
|
||||
NetworkDuplicateAction = "duplicate"
|
||||
NetworkDNSAction = "dns"
|
||||
NetworkPartitionAction = "partition"
|
||||
NetworkBandwidthAction = "bandwidth"
|
||||
NetworkPortOccupiedAction = "occupied"
|
||||
NetworkNICDownAction = "down"
|
||||
NetworkFloodAction = "flood"
|
||||
|
||||
NetIPSet = "hash:net"
|
||||
)
|
||||
|
||||
func (n *NetworkCommand) Validate() error {
|
||||
|
|
@ -71,8 +96,16 @@ func (n *NetworkCommand) Validate() error {
|
|||
return n.validNetworkCommon()
|
||||
case NetworkDNSAction:
|
||||
return n.validNetworkDNS()
|
||||
case NetworkPortOccupied:
|
||||
case NetworkPartitionAction:
|
||||
return n.validNetworkPartition()
|
||||
case NetworkPortOccupiedAction:
|
||||
return n.validNetworkOccupied()
|
||||
case NetworkBandwidthAction:
|
||||
return n.validNetworkBandwidth()
|
||||
case NetworkNICDownAction:
|
||||
return n.validNetworkNICDown()
|
||||
case NetworkFloodAction:
|
||||
return n.validNetworkFlood()
|
||||
default:
|
||||
return errors.Errorf("network action %s not supported", n.Action)
|
||||
}
|
||||
|
|
@ -105,9 +138,21 @@ func (n *NetworkCommand) validNetworkDelay() error {
|
|||
return errors.Errorf("ip addressed %s not valid", n.IPAddress)
|
||||
}
|
||||
|
||||
if len(n.AcceptTCPFlags) > 0 && n.IPProtocol != "tcp" {
|
||||
return errors.Errorf("protocol should be 'tcp' when set accept-tcp-flags")
|
||||
}
|
||||
|
||||
return checkProtocolAndPorts(n.IPProtocol, n.SourcePort, n.EgressPort)
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) validNetworkBandwidth() error {
|
||||
if len(n.Rate) == 0 || n.Limit == 0 || n.Buffer == 0 {
|
||||
return errors.Errorf("rate, limit and buffer both are required when action is bandwidth")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) validNetworkCommon() error {
|
||||
if len(n.Percent) == 0 {
|
||||
return errors.New("percent is required")
|
||||
|
|
@ -132,6 +177,30 @@ func (n *NetworkCommand) validNetworkCommon() error {
|
|||
return checkProtocolAndPorts(n.IPProtocol, n.SourcePort, n.EgressPort)
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) validNetworkPartition() error {
|
||||
if len(n.Device) == 0 {
|
||||
return errors.New("device is required")
|
||||
}
|
||||
|
||||
if !utils.CheckIPs(n.IPAddress) {
|
||||
return errors.Errorf("ip addressed %s not valid", n.IPAddress)
|
||||
}
|
||||
|
||||
if n.Direction != "to" && n.Direction != "from" && n.Direction != "both" {
|
||||
return errors.Errorf("direction should be one of to, from or both, but got %s", n.Direction)
|
||||
}
|
||||
|
||||
if len(n.AcceptTCPFlags) > 0 && n.IPProtocol != "tcp" {
|
||||
return errors.Errorf("protocol should be 'tcp' when set accept-tcp-flags")
|
||||
}
|
||||
|
||||
if !utils.CheckIPProtocols(n.IPProtocol) {
|
||||
return errors.Errorf("ip protocols %s not valid", n.IPProtocol)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) validNetworkDNS() error {
|
||||
if !utils.CheckIPs(n.DNSServer) {
|
||||
return errors.Errorf("server addresse %s not valid", n.DNSServer)
|
||||
|
|
@ -141,8 +210,8 @@ func (n *NetworkCommand) validNetworkDNS() error {
|
|||
return errors.Errorf("ip addresse %s not valid", n.DNSIp)
|
||||
}
|
||||
|
||||
if (len(n.DNSHost) != 0 && len(n.DNSIp) == 0) || (len(n.DNSHost) == 0 && len(n.DNSIp) != 0) {
|
||||
return errors.Errorf("DNS host %s must match a DNS ip %s", n.DNSHost, n.DNSIp)
|
||||
if (len(n.DNSDomainName) != 0 && len(n.DNSIp) == 0) || (len(n.DNSDomainName) == 0 && len(n.DNSIp) != 0) {
|
||||
return errors.Errorf("DNS host %s must match a DNS ip %s", n.DNSDomainName, n.DNSIp)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -155,6 +224,42 @@ func (n *NetworkCommand) validNetworkOccupied() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) validNetworkNICDown() error {
|
||||
if len(n.Duration) == 0 {
|
||||
return errors.New("duration is required")
|
||||
}
|
||||
|
||||
if len(n.Device) == 0 {
|
||||
return errors.New("device is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) validNetworkFlood() error {
|
||||
if len(n.IPAddress) == 0 {
|
||||
return errors.New("IP is required")
|
||||
}
|
||||
|
||||
if !utils.CheckIPs(n.IPAddress) {
|
||||
return errors.Errorf("ip addressed %s not valid", n.IPAddress)
|
||||
}
|
||||
|
||||
if len(n.Port) == 0 {
|
||||
return errors.New("port is required")
|
||||
}
|
||||
|
||||
if len(n.Rate) == 0 {
|
||||
return errors.New("rate is required")
|
||||
}
|
||||
|
||||
if len(n.Duration) == 0 {
|
||||
return errors.New("duration is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) CompleteDefaults() {
|
||||
switch n.Action {
|
||||
case NetworkDelayAction:
|
||||
|
|
@ -163,6 +268,10 @@ func (n *NetworkCommand) CompleteDefaults() {
|
|||
n.setDefaultForNetworkLoss()
|
||||
case NetworkDNSAction:
|
||||
n.setDefaultForNetworkDNS()
|
||||
case NetworkDuplicateAction:
|
||||
n.setDefaultForNetworkDuplicate()
|
||||
case NetworkCorruptAction:
|
||||
n.setDefaultForNetworkCorrupt()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +291,18 @@ func (n *NetworkCommand) setDefaultForNetworkLoss() {
|
|||
}
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) setDefaultForNetworkDuplicate() {
|
||||
if len(n.Correlation) == 0 {
|
||||
n.Correlation = "0"
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) setDefaultForNetworkCorrupt() {
|
||||
if len(n.Correlation) == 0 {
|
||||
n.Correlation = "0"
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) setDefaultForNetworkDNS() {
|
||||
if len(n.DNSServer) == 0 {
|
||||
n.DNSServer = "123.123.123.123"
|
||||
|
|
@ -293,12 +414,34 @@ func (n *NetworkCommand) ToDuplicateNetem() (*pb.Netem, error) {
|
|||
}
|
||||
|
||||
func (n *NetworkCommand) ToTC(ipset string) (*pb.Tc, error) {
|
||||
if n.Action == NetworkBandwidthAction {
|
||||
tbf, err := netem.FromBandwidth(&v1alpha1.BandwidthSpec{
|
||||
Rate: n.Rate,
|
||||
Limit: n.Limit,
|
||||
Buffer: n.Buffer,
|
||||
Peakrate: n.Peakrate,
|
||||
Minburst: n.Minburst,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.Tc{
|
||||
Type: pb.Tc_BANDWIDTH,
|
||||
Tbf: tbf,
|
||||
Ipset: ipset,
|
||||
Device: n.Device,
|
||||
}, nil
|
||||
}
|
||||
|
||||
tc := &pb.Tc{
|
||||
Type: pb.Tc_NETEM,
|
||||
Ipset: ipset,
|
||||
Protocol: n.IPProtocol,
|
||||
SourcePort: n.SourcePort,
|
||||
EgressPort: n.EgressPort,
|
||||
Device: n.Device,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -322,6 +465,8 @@ func (n *NetworkCommand) ToTC(ipset string) (*pb.Tc, error) {
|
|||
if netem, err = n.ToDuplicateNetem(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
case NetworkPartitionAction:
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("action %s not supported", n.Action)
|
||||
}
|
||||
|
|
@ -354,6 +499,7 @@ func (n *NetworkCommand) ToIPSet(name string) (*pb.IPSet, error) {
|
|||
return &pb.IPSet{
|
||||
Name: name,
|
||||
Cidrs: cidrs,
|
||||
Type: NetIPSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -365,21 +511,83 @@ func (n *NetworkCommand) NeedApplyIPSet() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) NeedApplyIptables() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) NeedApplyTC() bool {
|
||||
switch n.Action {
|
||||
case NetworkDelayAction, NetworkLossAction, NetworkCorruptAction, NetworkDuplicateAction:
|
||||
case NetworkDelayAction, NetworkLossAction, NetworkCorruptAction, NetworkDuplicateAction, NetworkBandwidthAction:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) AdditionalChain(ipset, device, uid string) ([]*pb.Chain, error) {
|
||||
chains := make([]*pb.Chain, 0, 2)
|
||||
var toChains, fromChains []*pb.Chain
|
||||
var err error
|
||||
|
||||
if n.Direction == "to" || n.Direction == "both" {
|
||||
toChains, err = n.getAdditionalChain(ipset, device, "to", uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if n.Direction == "from" || n.Direction == "both" {
|
||||
fromChains, err = n.getAdditionalChain(ipset, device, "from", uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
chains = append(chains, toChains...)
|
||||
chains = append(chains, fromChains...)
|
||||
|
||||
return chains, nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) getAdditionalChain(ipset, device, direction, uid string) ([]*pb.Chain, error) {
|
||||
var directionStr string
|
||||
var directionChain pb.Chain_Direction
|
||||
if direction == "to" {
|
||||
directionStr = "OUTPUT"
|
||||
directionChain = pb.Chain_OUTPUT
|
||||
} else if direction == "from" {
|
||||
directionStr = "INPUT"
|
||||
directionChain = pb.Chain_INPUT
|
||||
} else {
|
||||
return nil, errors.New(fmt.Sprintf("direction %s not supported", n.Direction))
|
||||
}
|
||||
|
||||
chains := make([]*pb.Chain, 0, 2)
|
||||
// The `targetLength`s in `netutils.CompressName()` are different because of
|
||||
// the need to distinguish between the different chains.
|
||||
if len(n.AcceptTCPFlags) > 0 {
|
||||
chains = append(chains, &pb.Chain{
|
||||
Name: fmt.Sprintf("%s/%s", directionStr, netutils.CompressName(uid, 19, "")),
|
||||
Ipsets: []string{ipset},
|
||||
Direction: directionChain,
|
||||
Protocol: n.IPProtocol,
|
||||
TcpFlags: n.AcceptTCPFlags,
|
||||
Target: "ACCEPT",
|
||||
Device: device,
|
||||
})
|
||||
}
|
||||
|
||||
if n.Action == NetworkPartitionAction {
|
||||
chains = append(chains, &pb.Chain{
|
||||
Name: fmt.Sprintf("%s/%s", directionStr, netutils.CompressName(uid, 20, "")),
|
||||
Ipsets: []string{ipset},
|
||||
Direction: directionChain,
|
||||
Protocol: n.IPProtocol,
|
||||
Target: "DROP",
|
||||
Device: device,
|
||||
})
|
||||
}
|
||||
return chains, nil
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) NeedApplyEtcHosts() bool {
|
||||
if len(n.DNSHost) > 0 || len(n.DNSIp) > 0 {
|
||||
if len(n.DNSDomainName) > 0 || len(n.DNSIp) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -390,8 +598,11 @@ func (n *NetworkCommand) NeedApplyDNSServer() bool {
|
|||
return len(n.DNSServer) > 0
|
||||
}
|
||||
|
||||
func (n *NetworkCommand) ToChain() (*pb.Chain, error) {
|
||||
return nil, nil
|
||||
func (n *NetworkCommand) NeedAdditionalChains() bool {
|
||||
if n.Action == NetworkPartitionAction || (n.Action == NetworkDelayAction && len(n.AcceptTCPFlags) != 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewNetworkCommand() *NetworkCommand {
|
||||
|
|
@ -399,5 +610,9 @@ func NewNetworkCommand() *NetworkCommand {
|
|||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: NetworkAttack,
|
||||
},
|
||||
BandwidthSpec: &BandwidthSpec{
|
||||
Peakrate: new(uint64),
|
||||
Minburst: new(uint32),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ type IptablesRule struct {
|
|||
Direction string `json:"direction"`
|
||||
// Experiment represents the experiment which the rule belong to.
|
||||
Experiment string `gorm:"index:experiment" json:"experiment"`
|
||||
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
func (i *IptablesRule) ToChain() *pb.Chain {
|
||||
|
|
@ -422,7 +424,7 @@ func toNetem(spec *TcParameter) (*pb.Netem, error) {
|
|||
for _, spec := range emSpecs {
|
||||
em, err := spec.ToNetem()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
merged = mergeNetem(merged, em)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright 2022 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/chaosdaemon/pb"
|
||||
)
|
||||
|
||||
func TestPatitionChain(t *testing.T) {
|
||||
t.Run("PattitionOnDirection", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
cmd *NetworkCommand
|
||||
chains []*pb.Chain
|
||||
}{
|
||||
{
|
||||
cmd: &NetworkCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Action: NetworkPartitionAction,
|
||||
},
|
||||
Direction: "to",
|
||||
IPProtocol: "tcp",
|
||||
},
|
||||
chains: []*pb.Chain{
|
||||
{
|
||||
Name: "OUTPUT/3c552_e0172bc4fd046_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_OUTPUT,
|
||||
Protocol: "tcp",
|
||||
Target: "DROP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: &NetworkCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Action: NetworkPartitionAction,
|
||||
},
|
||||
Direction: "from",
|
||||
IPProtocol: "tcp",
|
||||
},
|
||||
chains: []*pb.Chain{
|
||||
{
|
||||
Name: "INPUT/3c552_e0172bc4fd046_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_INPUT,
|
||||
Protocol: "tcp",
|
||||
Target: "DROP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: &NetworkCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Action: NetworkPartitionAction,
|
||||
},
|
||||
Direction: "both",
|
||||
IPProtocol: "tcp",
|
||||
},
|
||||
chains: []*pb.Chain{
|
||||
{
|
||||
Name: "OUTPUT/3c552_e0172bc4fd046_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_OUTPUT,
|
||||
Protocol: "tcp",
|
||||
Target: "DROP",
|
||||
},
|
||||
{
|
||||
Name: "INPUT/3c552_e0172bc4fd046_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_INPUT,
|
||||
Protocol: "tcp",
|
||||
Target: "DROP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: &NetworkCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Action: NetworkPartitionAction,
|
||||
},
|
||||
Direction: "both",
|
||||
IPProtocol: "tcp",
|
||||
AcceptTCPFlags: "SYN,ACK SYN,ACK",
|
||||
},
|
||||
chains: []*pb.Chain{
|
||||
{
|
||||
Name: "OUTPUT/3c552_e0172bc4fd04_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_OUTPUT,
|
||||
Protocol: "tcp",
|
||||
TcpFlags: "SYN,ACK SYN,ACK",
|
||||
Target: "ACCEPT",
|
||||
},
|
||||
{
|
||||
Name: "OUTPUT/3c552_e0172bc4fd046_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_OUTPUT,
|
||||
Protocol: "tcp",
|
||||
Target: "DROP",
|
||||
},
|
||||
{
|
||||
Name: "INPUT/3c552_e0172bc4fd04_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_INPUT,
|
||||
Protocol: "tcp",
|
||||
TcpFlags: "SYN,ACK SYN,ACK",
|
||||
Target: "ACCEPT",
|
||||
},
|
||||
{
|
||||
Name: "INPUT/3c552_e0172bc4fd046_",
|
||||
Ipsets: []string{"test"},
|
||||
Direction: pb.Chain_INPUT,
|
||||
Protocol: "tcp",
|
||||
Target: "DROP",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
chains, err := tc.cmd.AdditionalChain("test", "eth0", "3c5528e1-4c32-4f80-983c-913ad7e860e2")
|
||||
if err != nil {
|
||||
t.Errorf("failed to partition chain: %v", err)
|
||||
}
|
||||
if len(chains) != len(tc.chains) {
|
||||
t.Errorf("invalid chains. expected: %v, actual: %v", tc.chains, chains)
|
||||
}
|
||||
for i, chain := range chains {
|
||||
if chain.Name != tc.chains[i].Name {
|
||||
t.Errorf("invalid chain name. expected: %v, actual: %v", tc.chains[i].Name, chain.Name)
|
||||
}
|
||||
if chain.Ipsets[0] != "test" {
|
||||
t.Errorf("invalid ipsets. expected: %v, actual: %v", tc.chains[i].Ipsets, chain.Ipsets)
|
||||
}
|
||||
if chain.Direction != tc.chains[i].Direction {
|
||||
t.Errorf("invalid direction. expected: %v, actual: %v", tc.chains[i].Direction, chain.Direction)
|
||||
}
|
||||
if chain.Target != tc.chains[i].Target {
|
||||
t.Errorf("invalid target. expected: %v, actual: %v", tc.chains[i].Target, chain.Target)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -30,9 +30,10 @@ type ProcessCommand struct {
|
|||
CommonAttackConfig
|
||||
|
||||
// Process defines the process name or the process ID.
|
||||
Process string
|
||||
Signal int
|
||||
PIDs []int
|
||||
Process string `json:"process,omitempty"`
|
||||
Signal int `json:"signal,omitempty"`
|
||||
PIDs []int
|
||||
RecoverCmd string `json:"recoverCmd,omitempty"`
|
||||
// TODO: support these feature
|
||||
// Newest bool
|
||||
// Oldest bool
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2020 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisSentinelRestartAction = "restart"
|
||||
RedisSentinelStopAction = "stop"
|
||||
RedisCachePenetrationAction = "penetration"
|
||||
RedisCacheLimitAction = "cacheLimit"
|
||||
RedisCacheExpirationAction = "expiration"
|
||||
)
|
||||
|
||||
var (
|
||||
_ AttackConfig = &RedisCommand{}
|
||||
ValidOptions = map[string]bool{"XX": true, "NX": true, "GT": true, "LT": true}
|
||||
)
|
||||
|
||||
type RedisCommand struct {
|
||||
CommonAttackConfig
|
||||
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Conf string `json:"conf,omitempty"`
|
||||
FlushConfig bool `json:"flushConfig,omitempty"`
|
||||
RedisPath string `json:"redisPath,omitempty"`
|
||||
RequestNum int `json:"requestNum,omitempty"`
|
||||
CacheSize string `json:"cacheSize,omitempty"`
|
||||
Percent string `json:"percent,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Expiration string `json:"expiration,omitempty"`
|
||||
Option string `json:"option,omitempty"`
|
||||
|
||||
OriginCacheSize string `json:"originCacheSize,omitempty"`
|
||||
}
|
||||
|
||||
func (r *RedisCommand) Validate() error {
|
||||
if err := r.CommonAttackConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(r.Addr) == 0 {
|
||||
return errors.New("addr of redis server is required")
|
||||
}
|
||||
switch r.Action {
|
||||
case RedisCachePenetrationAction:
|
||||
if r.RequestNum == 0 {
|
||||
return errors.New("request-num is required")
|
||||
}
|
||||
|
||||
case RedisCacheLimitAction:
|
||||
if r.CacheSize != "0" && r.Percent != "" {
|
||||
return errors.New("only one of cachesize and percent can be set")
|
||||
}
|
||||
case RedisCacheExpirationAction:
|
||||
if _, ok := ValidOptions[r.Option]; ok {
|
||||
return errors.New("option invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RedisCommand) RecoverData() string {
|
||||
data, _ := json.Marshal(r)
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func NewRedisCommand() *RedisCommand {
|
||||
return &RedisCommand{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: RedisAttack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -33,10 +33,8 @@ func (s SearchCommand) Validate() error {
|
|||
}
|
||||
|
||||
if len(s.Kind) > 0 {
|
||||
switch s.Kind {
|
||||
case NetworkAttack, ProcessAttack:
|
||||
break
|
||||
default:
|
||||
attack := GetAttackByKind(s.Kind)
|
||||
if attack == nil {
|
||||
return errors.Errorf("type %s not supported", s.Kind)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ const (
|
|||
type StressCommand struct {
|
||||
CommonAttackConfig
|
||||
|
||||
Load int
|
||||
Workers int
|
||||
Size string
|
||||
Options []string
|
||||
StressngPid int32
|
||||
Load int `json:"load,omitempty"`
|
||||
Workers int `json:"workers,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
StressngPid int32 `json:"stress-ng-pid,omitempty"`
|
||||
}
|
||||
|
||||
var _ AttackConfig = &StressCommand{}
|
||||
|
|
@ -47,6 +47,12 @@ func (s *StressCommand) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *StressCommand) CompleteDefaults() {
|
||||
if s.Workers == 0 {
|
||||
s.Workers = 1
|
||||
}
|
||||
}
|
||||
|
||||
func (s StressCommand) RecoverData() string {
|
||||
data, _ := json.Marshal(s)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2022 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
)
|
||||
|
||||
var _ AttackConfig = &UserDefinedOption{}
|
||||
|
||||
type UserDefinedOption struct {
|
||||
CommonAttackConfig
|
||||
|
||||
AttackCmd string `json:"attackCmd,omitempty"`
|
||||
RecoverCmd string `json:"recoverCmd,omitempty"`
|
||||
}
|
||||
|
||||
func (u *UserDefinedOption) Validate() error {
|
||||
if err := u.CommonAttackConfig.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(u.AttackCmd) == 0 {
|
||||
return errors.New("attack command not provided")
|
||||
}
|
||||
|
||||
if len(u.RecoverCmd) == 0 {
|
||||
return errors.New("recover command not provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserDefinedOption) RecoverData() string {
|
||||
data, _ := json.Marshal(u)
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func NewUserDefinedOption() *UserDefinedOption {
|
||||
return &UserDefinedOption{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: UserDefinedAttack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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 core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
VMAction = "vm"
|
||||
)
|
||||
|
||||
type VMOption struct {
|
||||
CommonAttackConfig
|
||||
|
||||
VMName string `json:"vm-name,omitempty"`
|
||||
}
|
||||
|
||||
func NewVMOption() *VMOption {
|
||||
return &VMOption{
|
||||
CommonAttackConfig: CommonAttackConfig{
|
||||
Kind: VMAction,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (opt *VMOption) CompleteDefaults() {
|
||||
return
|
||||
}
|
||||
|
||||
func (opt VMOption) RecoverData() string {
|
||||
data, _ := json.Marshal(opt)
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
|
@ -29,6 +29,14 @@ type NodeCRClient struct {
|
|||
Pid uint32
|
||||
}
|
||||
|
||||
func (n *NodeCRClient) ListContainerIDs(_ context.Context) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *NodeCRClient) GetLabelsFromContainerID(_ context.Context, _ string) (map[string]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *NodeCRClient) GetPidFromContainerID(_ context.Context, _ string) (uint32, error) {
|
||||
return n.Pid, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,14 @@ type Environment struct {
|
|||
}
|
||||
|
||||
type AttackType interface {
|
||||
// Attack execute attack with options and env.
|
||||
// ExecuteAttack will store the options ahead of Attack be executed
|
||||
// and will store options again after Attack be executed.
|
||||
// We can also use env.Chaos.expStore to touch the storage of chaosd.
|
||||
// But do not update it with your own uid ,
|
||||
// because it will be covered after Attack executed with options.
|
||||
Attack(options core.AttackConfig, env Environment) error
|
||||
// Recover can get marshaled options data from experiment and recover it.
|
||||
Recover(experiment core.Experiment, env Environment) error
|
||||
}
|
||||
|
||||
|
|
@ -46,13 +53,11 @@ 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()
|
||||
}
|
||||
|
||||
uid = uuid.New().String()
|
||||
|
||||
exp := &core.Experiment{
|
||||
Uid: uid,
|
||||
Status: core.Created,
|
||||
|
|
@ -61,25 +66,26 @@ func (s *Server) ExecuteAttack(attackType AttackType, options core.AttackConfig,
|
|||
RecoverCommand: options.RecoverData(),
|
||||
LaunchMode: launchMode,
|
||||
}
|
||||
if err = s.exp.Set(context.Background(), exp); err != nil {
|
||||
if err = s.expStore.Set(context.Background(), exp); err != nil {
|
||||
err = perr.WithStack(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if err := s.exp.Update(context.Background(), uid, core.Error, err.Error(), options.RecoverData()); err != nil {
|
||||
if err := s.expStore.Update(context.Background(), uid, core.Error, err.Error(), options.RecoverData()); err != nil {
|
||||
log.Error("failed to update experiment", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var newStatus string
|
||||
if len(options.Cron()) > 0 {
|
||||
newStatus = core.Scheduled
|
||||
} else {
|
||||
newStatus = core.Success
|
||||
}
|
||||
if err := s.exp.Update(context.Background(), uid, newStatus, "", options.RecoverData()); err != nil {
|
||||
if err := s.expStore.Update(context.Background(), uid, newStatus, "", options.RecoverData()); err != nil {
|
||||
log.Error("failed to update experiment", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
// 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 (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-logr/zapr"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/mapreader"
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/ptrace"
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type clockAttack struct{}
|
||||
|
||||
var ClockAttack AttackType = clockAttack{}
|
||||
|
||||
// Copied from chaos-mesh/pkg/time/time_linux_amd64,
|
||||
// I will move the recover part into it just future.
|
||||
var fakeImage = []byte{
|
||||
0xb8, 0xe4, 0x00, 0x00, 0x00, //mov $0xe4,%eax
|
||||
0x0f, 0x05, //syscall
|
||||
0xba, 0x01, 0x00, 0x00, 0x00, //mov $0x1,%edx
|
||||
0x89, 0xf9, //mov %edi,%ecx
|
||||
0xd3, 0xe2, //shl %cl,%edx
|
||||
0x48, 0x8d, 0x0d, 0x74, 0x00, 0x00, 0x00, //lea 0x74(%rip),%rcx # <CLOCK_IDS_MASK>
|
||||
0x48, 0x63, 0xd2, //movslq %edx,%rdx
|
||||
0x48, 0x85, 0x11, //test %rdx,(%rcx)
|
||||
0x74, 0x6b, //je 108a <clock_gettime+0x8a>
|
||||
0x48, 0x8d, 0x15, 0x6d, 0x00, 0x00, 0x00, //lea 0x6d(%rip),%rdx # <TV_SEC_DELTA>
|
||||
0x4c, 0x8b, 0x46, 0x08, //mov 0x8(%rsi),%r8
|
||||
0x48, 0x8b, 0x0a, //mov (%rdx),%rcx
|
||||
0x48, 0x8d, 0x15, 0x67, 0x00, 0x00, 0x00, //lea 0x67(%rip),%rdx # <TV_NSEC_DELTA>
|
||||
0x48, 0x8b, 0x3a, //mov (%rdx),%rdi
|
||||
0x4a, 0x8d, 0x14, 0x07, //lea (%rdi,%r8,1),%rdx
|
||||
0x48, 0x81, 0xfa, 0x00, 0xca, 0x9a, 0x3b, //cmp $0x3b9aca00,%rdx
|
||||
0x7e, 0x1c, //jle <clock_gettime+0x60>
|
||||
0x0f, 0x1f, 0x40, 0x00, //nopl 0x0(%rax)
|
||||
0x48, 0x81, 0xef, 0x00, 0xca, 0x9a, 0x3b, //sub $0x3b9aca00,%rdi
|
||||
0x48, 0x83, 0xc1, 0x01, //add $0x1,%rcx
|
||||
0x49, 0x8d, 0x14, 0x38, //lea (%r8,%rdi,1),%rdx
|
||||
0x48, 0x81, 0xfa, 0x00, 0xca, 0x9a, 0x3b, //cmp $0x3b9aca00,%rdx
|
||||
0x7f, 0xe8, //jg <clock_gettime+0x48>
|
||||
0x48, 0x85, 0xd2, //test %rdx,%rdx
|
||||
0x79, 0x1e, //jns <clock_gettime+0x83>
|
||||
0x4a, 0x8d, 0xbc, 0x07, 0x00, 0xca, 0x9a, //lea 0x3b9aca00(%rdi,%r8,1),%rdi
|
||||
0x3b, //
|
||||
0x0f, 0x1f, 0x00, //nopl (%rax)
|
||||
0x48, 0x89, 0xfa, //mov %rdi,%rdx
|
||||
0x48, 0x83, 0xe9, 0x01, //sub $0x1,%rcx
|
||||
0x48, 0x81, 0xc7, 0x00, 0xca, 0x9a, 0x3b, //add $0x3b9aca00,%rdi
|
||||
0x48, 0x85, 0xd2, //test %rdx,%rdx
|
||||
0x78, 0xed, //js <clock_gettime+0x70>
|
||||
0x48, 0x01, 0x0e, //add %rcx,(%rsi)
|
||||
0x48, 0x89, 0x56, 0x08, //mov %rdx,0x8(%rsi)
|
||||
0xc3, //retq
|
||||
// constant
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //CLOCK_IDS_MASK
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //TV_SEC_DELTA
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //TV_NSEC_DELTA
|
||||
}
|
||||
|
||||
func (c clockAttack) Attack(options core.AttackConfig, env Environment) error {
|
||||
var opt *core.ClockOption
|
||||
var ok bool
|
||||
if opt, ok = options.(*core.ClockOption); !ok {
|
||||
return fmt.Errorf("AttackConfig -> *ClockOption meet error")
|
||||
}
|
||||
runtime.LockOSThread()
|
||||
defer func() {
|
||||
runtime.UnlockOSThread()
|
||||
}()
|
||||
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger)
|
||||
program, err := ptrace.Trace(opt.Pid, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = program.Detach()
|
||||
if err != nil {
|
||||
log.Error("fail to detach program", zap.Error(err), zap.Int("pid", opt.Pid))
|
||||
}
|
||||
}()
|
||||
|
||||
var vdsoEntry *mapreader.Entry
|
||||
for index := range program.Entries {
|
||||
// reverse loop is faster
|
||||
e := program.Entries[len(program.Entries)-index-1]
|
||||
if e.Path == "[vdso]" {
|
||||
vdsoEntry = &e
|
||||
break
|
||||
}
|
||||
}
|
||||
if vdsoEntry == nil {
|
||||
return fmt.Errorf("cannot find [vdso] entry")
|
||||
}
|
||||
|
||||
// minus tailing variable part
|
||||
// 24 = 3 * 8 because we have three variables
|
||||
constImageLen := len(fakeImage) - 24
|
||||
var fakeEntry *mapreader.Entry
|
||||
|
||||
// find injected image to avoid redundant inject (which will lead to memory leak)
|
||||
for _, e := range program.Entries {
|
||||
e := e
|
||||
|
||||
image, err := program.ReadSlice(e.StartAddress, uint64(constImageLen))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(*image, fakeImage[0:constImageLen]) {
|
||||
fakeEntry = &e
|
||||
log.Warn("found injected image", zap.Uint64("addr", fakeEntry.StartAddress))
|
||||
}
|
||||
}
|
||||
|
||||
if fakeEntry == nil {
|
||||
fakeEntry, err = program.MmapSlice(fakeImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fakeAddr := fakeEntry.StartAddress
|
||||
|
||||
// 139 is the index of CLOCK_IDS_MASK in fakeImage
|
||||
err = program.WriteUint64ToAddr(fakeAddr+139, opt.ClockIdsMask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 147 is the index of TV_SEC_DELTA in fakeImage
|
||||
err = program.WriteUint64ToAddr(fakeAddr+147, uint64(opt.SecDelta))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 155 is the index of TV_NSEC_DELTA in fakeImage
|
||||
err = program.WriteUint64ToAddr(fakeAddr+155, uint64(opt.NsecDelta))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originAddr, size, err := FindSymbolInEntry(*program, "clock_gettime", vdsoEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
funcBytes, err := program.ReadSlice(originAddr, size)
|
||||
|
||||
exps, err := env.Chaos.Search(&core.SearchCommand{
|
||||
Status: core.Success,
|
||||
Kind: core.ClockAttack,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, exp := range exps {
|
||||
if exp.Kind == core.ClockAttack {
|
||||
lastOptions, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lastOpt *core.ClockOption
|
||||
var ok bool
|
||||
if lastOpt, ok = lastOptions.(*core.ClockOption); !ok {
|
||||
log.Warn("AttackConfig -> *ClockOption meet error")
|
||||
continue
|
||||
}
|
||||
if lastOpt.Pid == opt.Pid {
|
||||
return fmt.Errorf("plz recover the last clock attack on pid : %d first \n"+
|
||||
"chaosd recover %s", opt.Pid, exp.Uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opt.Store = core.ClockFuncStore{
|
||||
CodeOfGetClockFunc: *funcBytes,
|
||||
OriginAddress: originAddr,
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = program.JumpToFakeFunc(originAddr, fakeAddr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c clockAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
options, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opt *core.ClockOption
|
||||
var ok bool
|
||||
if opt, ok = options.(*core.ClockOption); !ok {
|
||||
return fmt.Errorf("AttackConfig -> *ClockOption meet error")
|
||||
}
|
||||
|
||||
runtime.LockOSThread()
|
||||
defer func() {
|
||||
runtime.UnlockOSThread()
|
||||
}()
|
||||
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger)
|
||||
|
||||
program, err := ptrace.Trace(opt.Pid, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = program.Detach()
|
||||
if err != nil {
|
||||
log.Error("fail to detach program", zap.Error(err), zap.Int("pid", opt.Pid))
|
||||
}
|
||||
}()
|
||||
|
||||
err = program.PtraceWriteSlice(opt.Store.OriginAddress, opt.Store.CodeOfGetClockFunc)
|
||||
return err
|
||||
}
|
||||
|
||||
// FindSymbolInEntry finds symbol in entry through parsing elf
|
||||
func FindSymbolInEntry(p ptrace.TracedProgram, symbolName string, entry *mapreader.Entry) (addr uint64, size uint64, err error) {
|
||||
libBuffer, err := p.GetLibBuffer(entry)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(*libBuffer)
|
||||
vdsoElf, err := elf.NewFile(reader)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
loadOffset := uint64(0)
|
||||
|
||||
for _, prog := range vdsoElf.Progs {
|
||||
if prog.Type == elf.PT_LOAD {
|
||||
loadOffset = prog.Vaddr - prog.Off
|
||||
|
||||
// break here is enough for vdso
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
symbols, err := vdsoElf.DynamicSymbols()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
for _, symbol := range symbols {
|
||||
if symbol.Name == symbolName {
|
||||
offset := symbol.Value
|
||||
return entry.StartAddress + (offset - loadOffset), symbol.Size, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("cannot find symbol")
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type clockAttack struct{}
|
||||
|
||||
var ClockAttack AttackType = clockAttack{}
|
||||
|
||||
func (c clockAttack) Attack(options core.AttackConfig, env Environment) error {
|
||||
return fmt.Errorf("clock attack not supported")
|
||||
|
||||
}
|
||||
|
||||
func (c clockAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
return fmt.Errorf("clock recover not supported")
|
||||
}
|
||||
|
|
@ -14,224 +14,104 @@
|
|||
package chaosd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
|
||||
pkgUtils "github.com/chaos-mesh/chaosd/pkg/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)
|
||||
}
|
||||
return disk.diskPayload(attack)
|
||||
}
|
||||
|
||||
func initWritePayloadPath(payload *core.DiskOption) error {
|
||||
var err error
|
||||
payload.Path, err = utils.CreateTempFile()
|
||||
func handleDiskAttackOutput(output []byte, err error, c chan interface{}) {
|
||||
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.Error(string(output), zap.Error(err))
|
||||
c <- 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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))
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
log.Info(string(output))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (diskAttack) Recover(exp core.Experiment, _ Environment) error {
|
||||
config, err := exp.GetRequestCommand()
|
||||
func (diskAttack) Attack(options core.AttackConfig, env Environment) error {
|
||||
err := ApplyDiskAttack(options, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
option := *config.(*core.DiskOption)
|
||||
switch option.Action {
|
||||
case core.DiskFillAction, core.DiskWritePayloadAction:
|
||||
err = os.Remove(option.Path)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("recover disk: remove %s failed", option.Path), zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleOutputChannelError(c chan interface{}) error {
|
||||
close(c)
|
||||
var multiErrs error
|
||||
for i := range c {
|
||||
if err, ok := i.(error); ok {
|
||||
multiErrs = multierror.Append(multiErrs, err)
|
||||
}
|
||||
}
|
||||
if multiErrs != nil {
|
||||
return multiErrs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyDiskAttack(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")
|
||||
}
|
||||
poolSize := getPoolSize(attackConf)
|
||||
outputChan := make(chan interface{}, poolSize+1)
|
||||
if attackConf.Action == core.DiskFillAction {
|
||||
cmdPool := pkgUtils.NewCommandPools(context.Background(), nil, poolSize)
|
||||
env.Chaos.CmdPools[env.AttackUid] = cmdPool
|
||||
fillDisk(attackConf, cmdPool, NewOutputHandler(handleDiskAttackOutput, outputChan))
|
||||
cmdPool.Wait()
|
||||
cmdPool.Close()
|
||||
return handleOutputChannelError(outputChan)
|
||||
}
|
||||
|
||||
if attackConf.DdOptions != nil {
|
||||
var cmdPool *pkgUtils.CommandPools
|
||||
deadline := getDeadline(options)
|
||||
if deadline != nil {
|
||||
cmdPool = pkgUtils.NewCommandPools(context.Background(), deadline, poolSize)
|
||||
}
|
||||
cmdPool = pkgUtils.NewCommandPools(context.Background(), nil, poolSize)
|
||||
env.Chaos.CmdPools[env.AttackUid] = cmdPool
|
||||
|
||||
applyPayload(attackConf, cmdPool, NewOutputHandler(handleDiskAttackOutput, outputChan))
|
||||
cmdPool.Wait()
|
||||
cmdPool.Close()
|
||||
return handleOutputChannelError(outputChan)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (diskAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
attackConfig, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := *attackConfig.(*core.DiskAttackConfig)
|
||||
switch config.Action {
|
||||
case core.DiskFillAction, core.DiskWritePayloadAction:
|
||||
err = os.Remove(config.Path)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("recover disk: remove %s failed", config.Path), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if cmdPool, ok := env.Chaos.CmdPools[exp.Uid]; ok {
|
||||
cmdPool.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
pkgUtils "github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type diskServerAttack struct{}
|
||||
|
||||
var DiskServerAttack AttackType = diskServerAttack{}
|
||||
|
||||
func handleDiskServerOutput(output []byte, err error, _ chan interface{}) {
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
}
|
||||
log.Info(string(output))
|
||||
}
|
||||
|
||||
func (diskServerAttack) Attack(options core.AttackConfig, env Environment) error {
|
||||
err := ApplyDiskServerAttack(options, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type OutputHandler struct {
|
||||
StdoutHandler func([]byte, error, chan interface{})
|
||||
OutputChan chan interface{}
|
||||
}
|
||||
|
||||
func NewOutputHandler(
|
||||
handler func([]byte, error, chan interface{}),
|
||||
outputChan chan interface{}) *OutputHandler {
|
||||
return &OutputHandler{
|
||||
StdoutHandler: handler,
|
||||
OutputChan: outputChan,
|
||||
}
|
||||
}
|
||||
|
||||
func getPoolSize(attackConf *core.DiskAttackConfig) int {
|
||||
poolSize := 1
|
||||
if attackConf.DdOptions != nil && len(*attackConf.DdOptions) > 0 {
|
||||
poolSize = len(*attackConf.DdOptions)
|
||||
}
|
||||
return poolSize
|
||||
}
|
||||
|
||||
func fillDisk(
|
||||
attackConf *core.DiskAttackConfig,
|
||||
cmdPool *pkgUtils.CommandPools,
|
||||
outputHandler *OutputHandler) {
|
||||
if attackConf.FAllocateOption != nil {
|
||||
name, args := core.FAllocateCommand.GetCmdArgs(*attackConf.FAllocateOption)
|
||||
runner := pkgUtils.NewCommandRunner(name, args).
|
||||
WithOutputHandler(outputHandler.StdoutHandler, outputHandler.OutputChan)
|
||||
cmdPool.Start(runner)
|
||||
return
|
||||
}
|
||||
|
||||
for _, DdOption := range *attackConf.DdOptions {
|
||||
name, args := core.DdCommand.GetCmdArgs(DdOption)
|
||||
runner := pkgUtils.NewCommandRunner(name, args).
|
||||
WithOutputHandler(outputHandler.StdoutHandler, outputHandler.OutputChan)
|
||||
cmdPool.Start(runner)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getDeadline(options core.AttackConfig) *time.Time {
|
||||
duration, _ := options.ScheduleDuration()
|
||||
if duration != nil {
|
||||
deadline := time.Now().Add(*duration)
|
||||
return &deadline
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyPayload(
|
||||
attackConf *core.DiskAttackConfig,
|
||||
cmdPool *pkgUtils.CommandPools,
|
||||
outputHandler *OutputHandler) {
|
||||
if len(*attackConf.DdOptions) == 0 {
|
||||
return
|
||||
}
|
||||
rest := (*attackConf.DdOptions)[len(*attackConf.DdOptions)-1]
|
||||
*attackConf.DdOptions = (*attackConf.DdOptions)[:len(*attackConf.DdOptions)-1]
|
||||
name, args := core.DdCommand.GetCmdArgs(rest)
|
||||
runner := pkgUtils.NewCommandRunner(name, args).
|
||||
WithOutputHandler(outputHandler.StdoutHandler, outputHandler.OutputChan)
|
||||
cmdPool.Start(runner)
|
||||
|
||||
for _, ddOpt := range *attackConf.DdOptions {
|
||||
name, args := core.DdCommand.GetCmdArgs(ddOpt)
|
||||
runner := pkgUtils.NewCommandRunner(name, args).
|
||||
WithOutputHandler(outputHandler.StdoutHandler, outputHandler.OutputChan)
|
||||
cmdPool.Start(runner)
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyDiskServerAttack(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")
|
||||
}
|
||||
poolSize := getPoolSize(attackConf)
|
||||
if attackConf.Action == core.DiskFillAction {
|
||||
cmdPool := pkgUtils.NewCommandPools(context.Background(), nil, poolSize)
|
||||
env.Chaos.CmdPools[env.AttackUid] = cmdPool
|
||||
fillDisk(attackConf, cmdPool, NewOutputHandler(handleDiskServerOutput, nil))
|
||||
return nil
|
||||
}
|
||||
|
||||
if attackConf.DdOptions != nil {
|
||||
var cmdPool *pkgUtils.CommandPools
|
||||
deadline := getDeadline(options)
|
||||
if deadline != nil {
|
||||
cmdPool = pkgUtils.NewCommandPools(context.Background(), deadline, poolSize)
|
||||
}
|
||||
cmdPool = pkgUtils.NewCommandPools(context.Background(), nil, poolSize)
|
||||
env.Chaos.CmdPools[env.AttackUid] = cmdPool
|
||||
|
||||
applyPayload(attackConf, cmdPool, NewOutputHandler(handleDiskServerOutput, nil))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (diskServerAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
attackConfig, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := *attackConfig.(*core.DiskAttackConfig)
|
||||
|
||||
switch config.Action {
|
||||
case core.DiskFillAction, core.DiskWritePayloadAction:
|
||||
err = os.Remove(config.Path)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("recover disk: remove %s failed", config.Path), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if cmdPool, ok := env.Chaos.CmdPools[exp.Uid]; ok {
|
||||
log.Info(fmt.Sprintf("stop disk attack,read: %s", config.Path))
|
||||
cmdPool.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2023 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"
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
func Test_diskAttack_Attack(t *testing.T) {
|
||||
opt := core.DiskOption{
|
||||
CommonAttackConfig: core.CommonAttackConfig{
|
||||
Action: core.DiskFillAction,
|
||||
},
|
||||
Size: "10M",
|
||||
Path: "./a",
|
||||
PayloadProcessNum: 1,
|
||||
}
|
||||
env := Environment{
|
||||
AttackUid: "a",
|
||||
Chaos: &Server{
|
||||
CmdPools: make(map[string]*utils.CommandPools),
|
||||
},
|
||||
}
|
||||
conf, err := opt.PreProcess()
|
||||
assert.NoError(t, err)
|
||||
err = DiskAttack.Attack(conf, env)
|
||||
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, env)
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
type fileAttack struct{}
|
||||
|
||||
var FileAttack AttackType = fileAttack{}
|
||||
|
||||
func (fileAttack) Attack(options core.AttackConfig, env Environment) (err error) {
|
||||
attack := options.(*core.FileCommand)
|
||||
|
||||
switch attack.Action {
|
||||
case core.FileCreateAction:
|
||||
if err = env.Chaos.createFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileModifyPrivilegeAction:
|
||||
if err = env.Chaos.modifyFilePrivilege(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileDeleteAction:
|
||||
if err = env.Chaos.deleteFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileRenameAction:
|
||||
if err = env.Chaos.renameFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileAppendAction:
|
||||
if err = env.Chaos.appendFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileReplaceAction:
|
||||
if err = env.Chaos.replaceFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) createFile(attack *core.FileCommand, uid string) error {
|
||||
var cmdStr string
|
||||
if len(attack.DirName) > 0 {
|
||||
cmdStr = fmt.Sprintf("FileTool create --dir-name %s", attack.DirName)
|
||||
} else {
|
||||
cmdStr = fmt.Sprintf("FileTool create --file-name %s", attack.FileName)
|
||||
}
|
||||
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) modifyFilePrivilege(attack *core.FileCommand, uid string) error {
|
||||
// get the privilege of file and save it, used for recover
|
||||
cmdStr := "stat -c %a" + " " + attack.FileName
|
||||
output, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fileModeStr := strings.Replace(output, "\n", "", -1)
|
||||
attack.OriginPrivilege, err = strconv.Atoi(string(fileModeStr))
|
||||
if err != nil {
|
||||
log.Error("transform string to int failed", zap.String("string", fileModeStr), zap.Error(err))
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// modify the file privilege
|
||||
cmdStr = fmt.Sprintf("FileTool modify --file-name %s --privilege %d", attack.FileName, attack.Privilege)
|
||||
_, err = utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteFile will not really delete the file, just rename it
|
||||
func (s *Server) deleteFile(attack *core.FileCommand, uid string) error {
|
||||
var source, dest string
|
||||
if len(attack.FileName) > 0 {
|
||||
dest = fmt.Sprintf("%s.%s", attack.FileName, uid)
|
||||
source = attack.FileName
|
||||
} else if len(attack.DirName) > 0 {
|
||||
dest = fmt.Sprintf("%s.%s", attack.DirName, uid)
|
||||
source = attack.DirName
|
||||
}
|
||||
|
||||
return renameFile(source, dest)
|
||||
}
|
||||
|
||||
func (s *Server) renameFile(attack *core.FileCommand, uid string) error {
|
||||
return renameFile(attack.SourceFile, attack.DestFile)
|
||||
}
|
||||
|
||||
func renameFile(source, dest string) error {
|
||||
cmdStr := fmt.Sprintf("FileTool rename --old-name %s --new-name %s", source, dest)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) appendFile(attack *core.FileCommand, uid string) error {
|
||||
// first backup the file
|
||||
backupName := getBackupName(attack.FileName, uid)
|
||||
cmdStr := fmt.Sprintf("FileTool copy --file-name %s --copy-file-name %s", attack.FileName, backupName)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
cmdStr = fmt.Sprintf("FileTool append --count %d --data %s --file-name %s", attack.Count, attack.Data, attack.FileName)
|
||||
_, err = utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) replaceFile(attack *core.FileCommand, uid string) error {
|
||||
cmdStr := fmt.Sprintf("FileTool replace --file-name %s --origin-string %s --dest-string %s --line %d", attack.FileName, attack.OriginStr, attack.DestStr, attack.Line)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fileAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
config, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attack := config.(*core.FileCommand)
|
||||
|
||||
switch attack.Action {
|
||||
case core.FileCreateAction:
|
||||
if err = env.Chaos.recoverCreateFile(attack); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileModifyPrivilegeAction:
|
||||
if err = env.Chaos.recoverModifyPrivilege(attack); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileDeleteAction:
|
||||
if err = env.Chaos.recoverDeleteFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileRenameAction:
|
||||
if err = env.Chaos.recoverRenameFile(attack); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileAppendAction:
|
||||
if err = env.Chaos.recoverAppendFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.FileReplaceAction:
|
||||
if err = env.Chaos.recoverReplaceFile(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverCreateFile(attack *core.FileCommand) error {
|
||||
var fileName string
|
||||
if len(attack.FileName) > 0 {
|
||||
fileName = attack.FileName
|
||||
} else if len(attack.DirName) > 0 {
|
||||
fileName = attack.DirName
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("FileTool delete --file-name %s", fileName)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverModifyPrivilege(attack *core.FileCommand) error {
|
||||
cmdStr := fmt.Sprintf("FileTool modify --file-name %s --privilege %d", attack.FileName, attack.OriginPrivilege)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoverDeleteFile just rename the backup file/dir
|
||||
func (s *Server) recoverDeleteFile(attack *core.FileCommand, uid string) error {
|
||||
var backupName, sourceName string
|
||||
if len(attack.FileName) > 0 {
|
||||
backupName = getBackupName(attack.FileName, uid)
|
||||
sourceName = attack.FileName
|
||||
} else if len(attack.DirName) > 0 {
|
||||
backupName = getBackupName(attack.DirName, uid)
|
||||
sourceName = attack.DirName
|
||||
}
|
||||
|
||||
err := renameFile(backupName, sourceName)
|
||||
if err != nil {
|
||||
log.Error("recover delete file/dir failed", zap.Error(err))
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverRenameFile(attack *core.FileCommand) error {
|
||||
return renameFile(attack.DestFile, attack.SourceFile)
|
||||
}
|
||||
|
||||
func (s *Server) recoverAppendFile(attack *core.FileCommand, uid string) error {
|
||||
backupName := getBackupName(attack.FileName, uid)
|
||||
cmdStr := fmt.Sprintf("FileTool rename --old-name %s --new-name %s", backupName, attack.FileName)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverReplaceFile(attack *core.FileCommand, uid string) error {
|
||||
// TODO: this is not a good way to recover
|
||||
// For example:
|
||||
// The origin content is "test text", and replace "test" with "text", the result is "text text".
|
||||
// After recover, the file content is "test test", not equal to the origin content.
|
||||
cmdStr := fmt.Sprintf("FileTool replace --file-name %s --origin-string %s --dest-string %s --line %d", attack.FileName, attack.DestStr, attack.OriginStr, attack.Line)
|
||||
_, err := utils.ExecuteCmd(cmdStr)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBackupName gets the backup file or directory name
|
||||
func getBackupName(source string, uid string) string {
|
||||
return fmt.Sprintf("%s.%s", source, uid)
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
package chaosd
|
||||
|
||||
import (
|
||||
"github.com/pingcap/errors"
|
||||
perr "github.com/pkg/errors"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
|
|
@ -22,6 +23,7 @@ import (
|
|||
type HostManager interface {
|
||||
Name() string
|
||||
Shutdown() error
|
||||
Reboot() error
|
||||
}
|
||||
|
||||
type hostAttack struct{}
|
||||
|
|
@ -29,9 +31,23 @@ type hostAttack struct{}
|
|||
var HostAttack AttackType = hostAttack{}
|
||||
|
||||
func (hostAttack) Attack(options core.AttackConfig, _ Environment) error {
|
||||
if err := Host.Shutdown(); err != nil {
|
||||
return perr.WithStack(err)
|
||||
hostOption, ok := options.(*core.HostCommand)
|
||||
if !ok {
|
||||
return errors.New("the type is not HostOption")
|
||||
}
|
||||
|
||||
if hostOption.Action == core.HostShutdownAction {
|
||||
if err := Host.Shutdown(); err != nil {
|
||||
return perr.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if hostOption.Action == core.HostRebootAction {
|
||||
if err := Host.Reboot(); err != nil {
|
||||
return perr.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
// +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris
|
||||
|
||||
// Copyright 2021 Chaos Mesh Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
@ -13,6 +11,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || nacl || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris
|
||||
|
||||
package chaosd
|
||||
|
||||
import (
|
||||
|
|
@ -28,6 +29,8 @@ var Host HostManager = UnixHost{}
|
|||
|
||||
const CmdShutdown = "shutdown"
|
||||
|
||||
const CmdReboot = "reboot"
|
||||
|
||||
func (h UnixHost) Name() string {
|
||||
return "unix"
|
||||
}
|
||||
|
|
@ -40,3 +43,12 @@ func (h UnixHost) Shutdown() error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h UnixHost) Reboot() error {
|
||||
cmd := exec.Command(CmdReboot)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
// Copyright 2022 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type attackHTTP struct{}
|
||||
|
||||
var HTTPAttack AttackType = attackHTTP{}
|
||||
|
||||
func (attackHTTP) Attack(options core.AttackConfig, _ Environment) error {
|
||||
var attackConf *core.HTTPAttackConfig
|
||||
var ok bool
|
||||
if attackConf, ok = options.(*core.HTTPAttackConfig); !ok {
|
||||
return errors.New("AttackConfig -> *HTTPAttackConfig meet error")
|
||||
}
|
||||
|
||||
if attackConf.Action == core.HTTPRequestAction {
|
||||
return attackHTTPRequest(attackConf)
|
||||
}
|
||||
|
||||
cmd := exec.Command("tproxy", "-i", "-vv")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create stdin pipe")
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create stdout pipe")
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "start command `%s`", cmd.String())
|
||||
}
|
||||
|
||||
config, err := json.Marshal(&attackConf.Config)
|
||||
attackConf.Logger.Info(string(config))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "applying HTTP attack")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, "/", bytes.NewReader(config))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create http request")
|
||||
}
|
||||
|
||||
err = req.Write(stdin)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot request tproxy")
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(stdout), req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot read response")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
by, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot read err resp body, %s", resp.Status)
|
||||
}
|
||||
return errors.Errorf("%s: %s", resp.Status, string(by))
|
||||
}
|
||||
|
||||
by, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot read resp body")
|
||||
}
|
||||
attackConf.Logger.Info(string(by))
|
||||
|
||||
attackConf.ProxyPID = cmd.Process.Pid
|
||||
// In linux, a child process will become orphan process when a parent process dies.
|
||||
// But Golang runtime maintains a finalizer for a child process.
|
||||
// Release() will clear the finalizer for chaos-tproxy here.
|
||||
err = cmd.Process.Release()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Fatal error : release process fail, please clear PID: %d", attackConf.ProxyPID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (attackHTTP) Recover(exp core.Experiment, _ Environment) error {
|
||||
config, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attack, ok := config.(*core.HTTPAttackConfig)
|
||||
if !ok {
|
||||
return errors.Errorf("AttackConfig -> *HTTPAttackConfig meet error")
|
||||
}
|
||||
|
||||
proc, err := process.NewProcess(int32(attack.ProxyPID))
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
procName, err := proc.Name()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unexpected error when proc.Name. process pid: %d", proc.Pid)
|
||||
}
|
||||
|
||||
if !strings.Contains(procName, "tproxy") {
|
||||
attack.Logger.Info("the process %s:%d is not chaos-tproxy, please check and clear it manually\n", procName, attack.ProxyPID)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := proc.Terminate(); err != nil {
|
||||
attack.Logger.Info("the chaos-tproxy process kill failed with error: %s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func attackHTTPRequest(attackConf *core.HTTPAttackConfig) error {
|
||||
if attackConf.EnableConnPool {
|
||||
var HTTPTransport = &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
ExpectContinueTimeout: 30 * time.Second,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: HTTPTransport,
|
||||
}
|
||||
for i := 0; i < attackConf.Count; i++ {
|
||||
req, err := http.NewRequest(http.MethodGet, attackConf.URL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create HTTP request")
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "HTTP request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read response body")
|
||||
}
|
||||
attackConf.Logger.Info("response body: " + string(data))
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < attackConf.Count; i++ {
|
||||
resp, err := http.Get(attackConf.URL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "HTTP request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read response body")
|
||||
}
|
||||
attackConf.Logger.Info("response body: " + string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -18,7 +18,9 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
|
|
@ -28,159 +30,104 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
const ruleTemplate = `
|
||||
RULE {{.Name}}
|
||||
CLASS {{.Class}}
|
||||
METHOD {{.Method}}
|
||||
AT ENTRY
|
||||
IF true
|
||||
DO
|
||||
{{.Do}};
|
||||
ENDRULE
|
||||
`
|
||||
|
||||
const stressRuleTemplate = `
|
||||
RULE {{.Name}}
|
||||
STRESS {{.StressType}}
|
||||
{{.StressValueName}} {{.StressValue}}
|
||||
ENDRULE
|
||||
`
|
||||
|
||||
const gcRuleTemplate = `
|
||||
RULE {{.Name}}
|
||||
GC
|
||||
ENDRULE
|
||||
`
|
||||
|
||||
type jvmAttack struct{}
|
||||
|
||||
var JVMAttack AttackType = jvmAttack{}
|
||||
|
||||
const bmInstallCommand = "bminstall.sh -b -Dorg.jboss.byteman.transform.all -Dorg.jboss.byteman.verbose -p %d %d"
|
||||
const bmInstallCommand = "bminstall.sh -b -Dorg.jboss.byteman.transform.all -Dorg.jboss.byteman.verbose -Dorg.jboss.byteman.compileToBytecode -p %d %d"
|
||||
const bmSubmitCommand = "bmsubmit.sh -p %d -%s %s"
|
||||
|
||||
func (j jvmAttack) Attack(options core.AttackConfig, env Environment) (err error) {
|
||||
// install agent
|
||||
attack := options.(*core.JVMCommand)
|
||||
|
||||
if attack.Type == core.JVMInstallType {
|
||||
return j.install(attack)
|
||||
} else if attack.Type == core.JVMSubmitType {
|
||||
return j.submit(attack)
|
||||
}
|
||||
|
||||
return errors.Errorf("attack type %s not supported", attack.Type)
|
||||
}
|
||||
|
||||
func (j jvmAttack) install(attack *core.JVMCommand) error {
|
||||
var err error
|
||||
|
||||
bmInstallCmd := fmt.Sprintf(bmInstallCommand, attack.Port, attack.Pid)
|
||||
cmd := exec.Command("bash", "-c", bmInstallCmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// this error will occured when install agent more than once, and will ignore this error and continue to submit rule
|
||||
errMsg1 := "Agent JAR loaded but agent failed to initialize"
|
||||
|
||||
// these two errors will occured when java version less or euqal to 1.8, and don't know why
|
||||
// but it can install agent success even with this error, so just ignore it now.
|
||||
// TODO: Investigate the cause of these two error
|
||||
errMsg2 := "Provider sun.tools.attach.LinuxAttachProvider not found"
|
||||
errMsg3 := "install java.io.IOException: Non-numeric value found"
|
||||
if !strings.Contains(string(output), errMsg1) && !strings.Contains(string(output), errMsg2) &&
|
||||
!strings.Contains(string(output), errMsg3) {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
log.Debug(string(output), zap.Error(err))
|
||||
}
|
||||
|
||||
// submit helper jar
|
||||
bmSubmitCmd := fmt.Sprintf(bmSubmitCommand, attack.Port, "b", fmt.Sprintf("%s/lib/byteman-helper.jar", os.Getenv("BYTEMAN_HOME")))
|
||||
cmd = exec.Command("bash", "-c", bmSubmitCmd)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if len(output) > 0 {
|
||||
log.Info("submit helper", zap.String("output", string(output)))
|
||||
}
|
||||
|
||||
log.Info(string(output))
|
||||
return err
|
||||
}
|
||||
|
||||
func (j jvmAttack) submit(attack *core.JVMCommand) error {
|
||||
// submit rules
|
||||
ruleFile, err := j.generateRuleFile(attack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bmSubmitCmd := fmt.Sprintf(bmSubmitCommand, attack.Port, "l", ruleFile)
|
||||
cmd := exec.Command("bash", "-c", bmSubmitCmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
bmSubmitCmd = fmt.Sprintf(bmSubmitCommand, attack.Port, "l", ruleFile)
|
||||
cmd = exec.Command("bash", "-c", bmSubmitCmd)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(string(output))
|
||||
if len(output) > 0 {
|
||||
log.Info("submit rules", zap.String("output", string(output)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j jvmAttack) generateRuleFile(attack *core.JVMCommand) (string, error) {
|
||||
var err error
|
||||
if len(attack.RuleFile) > 0 {
|
||||
attack.RuleData, err = ioutil.ReadFile(attack.RuleFile)
|
||||
if len(attack.RuleData) > 0 {
|
||||
filename, err := writeDataIntoFile(attack.RuleData, "rule.btm")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Info("rule file data:" + string(attack.RuleData))
|
||||
log.Info("byteman rule", zap.String("rule", string(attack.RuleData)), zap.String("file", filename))
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
if len(attack.RuleFile) > 0 {
|
||||
data, err := ioutil.ReadFile(attack.RuleFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
attack.RuleData = string(data)
|
||||
log.Info("rule file data:" + attack.RuleData)
|
||||
|
||||
return attack.RuleFile, nil
|
||||
}
|
||||
|
||||
if len(attack.Do) == 0 {
|
||||
switch attack.Action {
|
||||
case core.JVMLatencyAction:
|
||||
attack.Do = fmt.Sprintf("Thread.sleep(%s)", attack.LatencyDuration)
|
||||
case core.JVMExceptionAction:
|
||||
attack.Do = fmt.Sprintf("throw new %s", attack.ThrowException)
|
||||
case core.JVMReturnAction:
|
||||
attack.Do = fmt.Sprintf("return %s", attack.ReturnValue)
|
||||
case core.JVMStressAction:
|
||||
if attack.CPUCount > 0 {
|
||||
attack.StressType = "CPU"
|
||||
attack.StressValueName = "CPUCOUNT"
|
||||
attack.StressValue = attack.CPUCount
|
||||
} else {
|
||||
attack.StressType = "MEMORY"
|
||||
attack.StressValueName = "MEMORYSIZE"
|
||||
attack.StressValue = attack.MemorySize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
var t *template.Template
|
||||
switch attack.Action {
|
||||
case core.JVMStressAction:
|
||||
t = template.Must(template.New("byteman rule").Parse(stressRuleTemplate))
|
||||
case core.JVMExceptionAction, core.JVMLatencyAction, core.JVMReturnAction:
|
||||
t = template.Must(template.New("byteman rule").Parse(ruleTemplate))
|
||||
case core.JVMGCAction:
|
||||
t = template.Must(template.New("byteman rule").Parse(gcRuleTemplate))
|
||||
default:
|
||||
return "", errors.Errorf("jvm action %s not supported", attack.Action)
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
return "", errors.Errorf("parse byeman rule template failed")
|
||||
}
|
||||
|
||||
err = t.Execute(buf, attack)
|
||||
if err != nil {
|
||||
log.Error("executing template", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("byteman rule", zap.String("rule", buf.String()))
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "rule.btm")
|
||||
attack.RuleData, err = generateRuleData(attack)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("create btm file", zap.String("file", tmpfile.Name()))
|
||||
|
||||
if _, err := tmpfile.Write(buf.Bytes()); err != nil {
|
||||
filename, err := writeDataIntoFile(attack.RuleData, "rule.btm")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Info("byteman rule", zap.String("rule", attack.RuleData), zap.String("file", filename))
|
||||
|
||||
attack.RuleData = buf.Bytes()
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpfile.Name(), nil
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func (j jvmAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
|
|
@ -189,22 +136,13 @@ func (j jvmAttack) Recover(exp core.Experiment, env Environment) error {
|
|||
return err
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "rule.btm")
|
||||
filename, err := writeDataIntoFile(attack.RuleData, "rule.btm")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("create btm file", zap.String("file", filename))
|
||||
|
||||
if _, err := tmpfile.Write(attack.RuleData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("create btm file", zap.String("file", tmpfile.Name()))
|
||||
|
||||
bmSubmitCmd := fmt.Sprintf(bmSubmitCommand, attack.Port, "u", tmpfile.Name())
|
||||
bmSubmitCmd := fmt.Sprintf(bmSubmitCommand, attack.Port, "u", filename)
|
||||
cmd := exec.Command("bash", "-c", bmSubmitCmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
|
|
@ -216,3 +154,104 @@ func (j jvmAttack) Recover(exp core.Experiment, env Environment) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateRuleData(attack *core.JVMCommand) (string, error) {
|
||||
bytemanTemplateSpec := core.BytemanTemplateSpec{
|
||||
Name: attack.Name,
|
||||
Class: attack.Class,
|
||||
Method: attack.Method,
|
||||
}
|
||||
|
||||
var mysqlException string
|
||||
switch attack.Action {
|
||||
case core.JVMLatencyAction:
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("Thread.sleep(%d)", attack.LatencyDuration)
|
||||
case core.JVMExceptionAction:
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("throw new %s", attack.ThrowException)
|
||||
case core.JVMReturnAction:
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("return %s", attack.ReturnValue)
|
||||
case core.JVMStressAction:
|
||||
bytemanTemplateSpec.Helper = core.StressHelper
|
||||
bytemanTemplateSpec.Class = core.TriggerClass
|
||||
bytemanTemplateSpec.Method = core.TriggerMethod
|
||||
// the bind and condition is useless, only used for fill the template
|
||||
bytemanTemplateSpec.Bind = "flag:boolean=true"
|
||||
bytemanTemplateSpec.Condition = "true"
|
||||
if attack.CPUCount > 0 {
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("injectCPUStress(\"%s\", %d)", attack.Name, attack.CPUCount)
|
||||
} else {
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("injectMemStress(\"%s\", \"%s\")", attack.Name, attack.MemoryType)
|
||||
}
|
||||
case core.JVMGCAction:
|
||||
bytemanTemplateSpec.Helper = core.GCHelper
|
||||
bytemanTemplateSpec.Class = core.TriggerClass
|
||||
bytemanTemplateSpec.Method = core.TriggerMethod
|
||||
// the bind and condition is useless, only used for fill the template
|
||||
bytemanTemplateSpec.Bind = "flag:boolean=true"
|
||||
bytemanTemplateSpec.Condition = "true"
|
||||
bytemanTemplateSpec.Do = "gc()"
|
||||
case core.JVMMySQLAction:
|
||||
bytemanTemplateSpec.Helper = core.SQLHelper
|
||||
// the first parameter of matchDBTable is the database which the SQL execute in, because the SQL may not contain database, for example: select * from t1;
|
||||
// can't get the database information now, so use a "" instead
|
||||
// TODO: get the database information and fill it in matchDBTable function
|
||||
bytemanTemplateSpec.Bind = fmt.Sprintf("flag:boolean=matchDBTable(\"\", $2, \"%s\", \"%s\", \"%s\")", attack.Database, attack.Table, attack.SQLType)
|
||||
bytemanTemplateSpec.Condition = "flag"
|
||||
if attack.MySQLConnectorVersion == "5" {
|
||||
bytemanTemplateSpec.Class = core.MySQL5InjectClass
|
||||
bytemanTemplateSpec.Method = core.MySQL5InjectMethod
|
||||
mysqlException = core.MySQL5Exception
|
||||
} else if attack.MySQLConnectorVersion == "8" {
|
||||
bytemanTemplateSpec.Class = core.MySQL8InjectClass
|
||||
bytemanTemplateSpec.Method = core.MySQL8InjectMethod
|
||||
mysqlException = core.MySQL8Exception
|
||||
} else {
|
||||
return "", errors.Errorf("mysql connector version %s is not supported", attack.MySQLConnectorVersion)
|
||||
}
|
||||
|
||||
if len(attack.ThrowException) > 0 {
|
||||
exception := fmt.Sprintf(mysqlException, attack.ThrowException)
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("throw new %s", exception)
|
||||
} else if attack.LatencyDuration > 0 {
|
||||
bytemanTemplateSpec.Do = fmt.Sprintf("Thread.sleep(%d)", attack.LatencyDuration)
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
var t *template.Template
|
||||
switch attack.Action {
|
||||
case core.JVMStressAction, core.JVMGCAction, core.JVMMySQLAction:
|
||||
t = template.Must(template.New("byteman rule").Parse(core.CompleteRuleTemplate))
|
||||
case core.JVMExceptionAction, core.JVMLatencyAction, core.JVMReturnAction:
|
||||
t = template.Must(template.New("byteman rule").Parse(core.SimpleRuleTemplate))
|
||||
default:
|
||||
return "", errors.Errorf("jvm action %s not supported", attack.Action)
|
||||
}
|
||||
if t == nil {
|
||||
return "", errors.Errorf("parse byeman rule template failed")
|
||||
}
|
||||
err := t.Execute(buf, bytemanTemplateSpec)
|
||||
if err != nil {
|
||||
log.Error("executing template", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func writeDataIntoFile(data string, filename string) (string, error) {
|
||||
tmpfile, err := ioutil.TempFile("", filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := tmpfile.WriteString(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpfile.Name(), err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
func TestGenerateRuleData(t *testing.T) {
|
||||
g := NewGomegaWithT(t)
|
||||
|
||||
testCases := []struct {
|
||||
cmd *core.JVMCommand
|
||||
ruleData string
|
||||
}{
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMExceptionAction,
|
||||
JVMClassMethodSpec: core.JVMClassMethodSpec{
|
||||
Class: "testClass",
|
||||
Method: "testMethod",
|
||||
},
|
||||
ThrowException: "java.io.IOException(\"BOOM\")",
|
||||
},
|
||||
"\nRULE test\nCLASS testClass\nMETHOD testMethod\nAT ENTRY\nIF true\nDO\n\tthrow new java.io.IOException(\"BOOM\");\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMReturnAction,
|
||||
JVMClassMethodSpec: core.JVMClassMethodSpec{
|
||||
Class: "testClass",
|
||||
Method: "testMethod",
|
||||
},
|
||||
ReturnValue: "\"test\"",
|
||||
},
|
||||
"\nRULE test\nCLASS testClass\nMETHOD testMethod\nAT ENTRY\nIF true\nDO\n\treturn \"test\";\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMLatencyAction,
|
||||
JVMClassMethodSpec: core.JVMClassMethodSpec{
|
||||
Class: "testClass",
|
||||
Method: "testMethod",
|
||||
},
|
||||
LatencyDuration: 5000,
|
||||
},
|
||||
"\nRULE test\nCLASS testClass\nMETHOD testMethod\nAT ENTRY\nIF true\nDO\n\tThread.sleep(5000);\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMStressAction,
|
||||
JVMStressSpec: core.JVMStressSpec{
|
||||
CPUCount: 1,
|
||||
},
|
||||
},
|
||||
"\nRULE test\nCLASS org.chaos_mesh.chaos_agent.TriggerThread\nMETHOD triggerFunc\nHELPER org.chaos_mesh.byteman.helper.StressHelper\nAT ENTRY\nBIND flag:boolean=true;\nIF true\nDO\n\tinjectCPUStress(\"test\", 1);\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMStressAction,
|
||||
JVMStressSpec: core.JVMStressSpec{
|
||||
MemoryType: "heap",
|
||||
},
|
||||
},
|
||||
"\nRULE test\nCLASS org.chaos_mesh.chaos_agent.TriggerThread\nMETHOD triggerFunc\nHELPER org.chaos_mesh.byteman.helper.StressHelper\nAT ENTRY\nBIND flag:boolean=true;\nIF true\nDO\n\tinjectMemStress(\"test\", \"heap\");\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMGCAction,
|
||||
},
|
||||
"\nRULE test\nCLASS org.chaos_mesh.chaos_agent.TriggerThread\nMETHOD triggerFunc\nHELPER org.chaos_mesh.byteman.helper.GCHelper\nAT ENTRY\nBIND flag:boolean=true;\nIF true\nDO\n\tgc();\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMMySQLAction,
|
||||
JVMMySQLSpec: core.JVMMySQLSpec{
|
||||
MySQLConnectorVersion: "8",
|
||||
Database: "test",
|
||||
Table: "t1",
|
||||
SQLType: "select",
|
||||
},
|
||||
ThrowException: "BOOM",
|
||||
},
|
||||
"\nRULE test\nCLASS com.mysql.cj.NativeSession\nMETHOD execSQL\nHELPER org.chaos_mesh.byteman.helper.SQLHelper\nAT ENTRY\nBIND flag:boolean=matchDBTable(\"\", $2, \"test\", \"t1\", \"select\");\nIF flag\nDO\n\tthrow new com.mysql.cj.exceptions.CJException(\"BOOM\");\nENDRULE\n",
|
||||
},
|
||||
{
|
||||
&core.JVMCommand{
|
||||
Name: "test",
|
||||
JVMCommonSpec: core.JVMCommonSpec{
|
||||
Pid: 1234,
|
||||
},
|
||||
Action: core.JVMMySQLAction,
|
||||
JVMMySQLSpec: core.JVMMySQLSpec{
|
||||
MySQLConnectorVersion: "8",
|
||||
Database: "test",
|
||||
Table: "t1",
|
||||
SQLType: "select",
|
||||
},
|
||||
LatencyDuration: 5000,
|
||||
},
|
||||
"\nRULE test\nCLASS com.mysql.cj.NativeSession\nMETHOD execSQL\nHELPER org.chaos_mesh.byteman.helper.SQLHelper\nAT ENTRY\nBIND flag:boolean=matchDBTable(\"\", $2, \"test\", \"t1\", \"select\");\nIF flag\nDO\n\tThread.sleep(5000);\nENDRULE\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
ruleData, err := generateRuleData(testCase.cmd)
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(ruleData).Should(Equal(testCase.ruleData))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
// Copyright 2022 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/magiconair/properties"
|
||||
"github.com/pingcap/errors"
|
||||
"github.com/pingcap/log"
|
||||
perr "github.com/pkg/errors"
|
||||
client "github.com/segmentio/kafka-go"
|
||||
"github.com/segmentio/kafka-go/sasl/plain"
|
||||
"github.com/segmentio/kafka-go/sasl/scram"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type kafkaAttack struct{}
|
||||
|
||||
var KafkaAttack AttackType = kafkaAttack{}
|
||||
|
||||
func (j kafkaAttack) Attack(options core.AttackConfig, env Environment) (err error) {
|
||||
attack := options.(*core.KafkaCommand)
|
||||
switch attack.Action {
|
||||
case core.KafkaFillAction:
|
||||
return attackKafkaFill(context.TODO(), attack)
|
||||
case core.KafkaFloodAction:
|
||||
return attackKafkaFlood(context.TODO(), attack)
|
||||
case core.KafkaIOAction:
|
||||
return attackKafkaIO(attack)
|
||||
default:
|
||||
return errors.Errorf("invalid action: %s", attack.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func (j kafkaAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
attack := new(core.KafkaCommand)
|
||||
if err := json.Unmarshal([]byte(exp.RecoverCommand), attack); err != nil {
|
||||
return perr.Wrap(err, "unmarshal kafka command")
|
||||
}
|
||||
switch attack.Action {
|
||||
case core.KafkaFillAction:
|
||||
return recoverKafkaFill(attack)
|
||||
case core.KafkaIOAction:
|
||||
return recoverKafkaIO(attack, env)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newDialer(attack *core.KafkaCommand) (dialer *client.Dialer, err error) {
|
||||
dialer = &client.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
if attack.Username != "" {
|
||||
switch core.KafkaAuthMechanism(attack.AuthMechanism) {
|
||||
case core.SaslPlain:
|
||||
dialer.SASLMechanism = plain.Mechanism{
|
||||
Username: attack.Username,
|
||||
Password: attack.Password,
|
||||
}
|
||||
case core.SaslScream256:
|
||||
dialer.SASLMechanism, err = scram.Mechanism(scram.SHA256, attack.Username, attack.Password)
|
||||
case core.SaslScram512:
|
||||
dialer.SASLMechanism, err = scram.Mechanism(scram.SHA512, attack.Username, attack.Password)
|
||||
default:
|
||||
return nil, errors.Errorf("invalid auth mechanism: %s", attack.AuthMechanism)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, perr.Wrap(err, "create scram mechanism")
|
||||
}
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func dial(attack *core.KafkaCommand) (conn *client.Conn, err error) {
|
||||
endpoint := fmt.Sprintf("%s:%d", attack.Host, attack.Port)
|
||||
dialer, err := newDialer(attack)
|
||||
if err != nil {
|
||||
return nil, perr.Wrap(err, "new dialer")
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", endpoint)
|
||||
if err != nil {
|
||||
return nil, perr.Wrapf(err, "dial endpoint: %s", endpoint)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func dialPartition(ctx context.Context, attack *core.KafkaCommand, partition int) (conn *client.Conn, err error) {
|
||||
endpoint := fmt.Sprintf("%s:%d", attack.Host, attack.Port)
|
||||
dialer, err := newDialer(attack)
|
||||
if err != nil {
|
||||
return nil, perr.Wrap(err, "new dialer")
|
||||
}
|
||||
conn, err = dialer.DialLeader(ctx, "tcp", endpoint, attack.Topic, partition)
|
||||
if err != nil {
|
||||
return nil, perr.Wrapf(err, "dial endpoint: %s", endpoint)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func getPartitions(attack *core.KafkaCommand) (partitions []int, err error) {
|
||||
dialer, err := newDialer(attack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s:%d", attack.Host, attack.Port)
|
||||
conn, err := dialer.Dial("tcp", endpoint)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "dial endpoint: %s", endpoint)
|
||||
}
|
||||
|
||||
if attack.Topic == "" {
|
||||
return nil, errors.New("topic is required")
|
||||
}
|
||||
pars, err := conn.ReadPartitions(attack.Topic)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "read partitions of topic %s", attack.Topic)
|
||||
}
|
||||
for _, par := range pars {
|
||||
if par.Error != nil {
|
||||
return nil, errors.Wrap(err, "read partition")
|
||||
}
|
||||
partitions = append(partitions, par.ID)
|
||||
}
|
||||
return partitions, nil
|
||||
}
|
||||
|
||||
func attackKafkaFill(ctx context.Context, attack *core.KafkaCommand) (err error) {
|
||||
// TODO: make it configurable
|
||||
const messagePerRequest = 128
|
||||
msg := make([]byte, attack.MessageSize)
|
||||
msgList := make([]client.Message, 0, messagePerRequest)
|
||||
for i := 0; i < messagePerRequest; i++ {
|
||||
msgList = append(msgList, client.Message{Topic: attack.Topic, Value: msg})
|
||||
}
|
||||
|
||||
partitions, err := getPartitions(attack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writeChan := make(chan uint64)
|
||||
errChan := make(chan error)
|
||||
c, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
for _, partition := range partitions {
|
||||
p := partition
|
||||
go func() {
|
||||
conn, err := dialPartition(c, attack, p)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
for c.Err() == nil {
|
||||
n, err := conn.WriteMessages(msgList...)
|
||||
if err != nil {
|
||||
errChan <- perr.Wrap(err, "write messages")
|
||||
return
|
||||
}
|
||||
writeChan <- uint64(n)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
written := "0 B"
|
||||
bytes := uint64(0)
|
||||
|
||||
err = func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case n := <-writeChan:
|
||||
bytes += uint64(n)
|
||||
newWritten := humanize.Bytes(bytes)
|
||||
if newWritten != written {
|
||||
written = newWritten
|
||||
log.Info(fmt.Sprintf("write %s in %s", written, time.Now().Sub(start)))
|
||||
}
|
||||
if bytes >= attack.MaxBytes {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attack.OriginConfig, err = setRetentionBytes(attack, attack.MaxBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func recoverKafkaFill(attack *core.KafkaCommand) error {
|
||||
newConfigFile := attack.ConfigFile + ".new"
|
||||
if err := ioutil.WriteFile(newConfigFile, []byte(attack.OriginConfig), 0644); err != nil {
|
||||
return perr.Wrapf(err, "write config file %s", newConfigFile)
|
||||
}
|
||||
err := os.Rename(newConfigFile, attack.ConfigFile)
|
||||
if err != nil {
|
||||
return perr.Wrapf(err, "rename config file %s to %s", newConfigFile, attack.ConfigFile)
|
||||
}
|
||||
rcmd := exec.Command("bash", "-c", attack.ReloadCommand)
|
||||
if err := rcmd.Start(); err != nil {
|
||||
return perr.Wrapf(err, "reload command %s", attack.ReloadCommand)
|
||||
}
|
||||
if err := rcmd.Wait(); err != nil {
|
||||
return perr.Wrapf(err, "wait reload command %s", attack.ReloadCommand)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setRetentionBytes(attack *core.KafkaCommand, bytes uint64) (string, error) {
|
||||
config, err := os.ReadFile(attack.ConfigFile)
|
||||
if err != nil {
|
||||
return "", perr.Wrap(err, "read config")
|
||||
}
|
||||
p, err := properties.Load(config, properties.UTF8)
|
||||
if err != nil {
|
||||
return "", perr.Wrapf(err, "load config file %s", attack.ConfigFile)
|
||||
}
|
||||
p.Set("log.retention.bytes", fmt.Sprintf("%d", bytes))
|
||||
newConfigFile := attack.ConfigFile + ".new"
|
||||
if err = ioutil.WriteFile(newConfigFile, []byte(p.String()), 0644); err != nil {
|
||||
return "", perr.Wrapf(err, "write config file %s", newConfigFile)
|
||||
}
|
||||
err = os.Rename(newConfigFile, attack.ConfigFile)
|
||||
if err != nil {
|
||||
return "", perr.Wrapf(err, "rename config file %s to %s", newConfigFile, attack.ConfigFile)
|
||||
}
|
||||
|
||||
rcmd := exec.Command("bash", "-c", attack.ReloadCommand)
|
||||
if err := rcmd.Start(); err != nil {
|
||||
return "", perr.Wrapf(err, "reload command %s", attack.ReloadCommand)
|
||||
}
|
||||
if err := rcmd.Wait(); err != nil {
|
||||
return "", perr.Wrapf(err, "wait reload command %s", attack.ReloadCommand)
|
||||
}
|
||||
return string(config), nil
|
||||
}
|
||||
|
||||
func attackKafkaFlood(ctx context.Context, attack *core.KafkaCommand) (err error) {
|
||||
msg := make([]byte, attack.MessageSize)
|
||||
wg := new(sync.WaitGroup)
|
||||
for i := 0; i < int(attack.Threads); i++ {
|
||||
logger := log.With(zap.String("thread", fmt.Sprintf("thread-%d", i)))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
conn, err := dialPartition(ctx, attack, 0)
|
||||
if err != nil {
|
||||
logger.Error(perr.Wrapf(err, "dial kafka broker: %s", fmt.Sprintf("%s:%d", attack.Host, attack.Port)).Error())
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
start := time.Now()
|
||||
succeeded := 0
|
||||
failed := 0
|
||||
for time.Now().Sub(start) < time.Second {
|
||||
_, err = conn.WriteMessages(client.Message{Value: msg})
|
||||
if err != nil {
|
||||
failed++
|
||||
logger.Debug("write message", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
succeeded++
|
||||
}
|
||||
logger.Info(fmt.Sprintf("succeeded: %d, failed: %d", succeeded, failed))
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func attackIOPath(
|
||||
attack *core.KafkaCommand,
|
||||
path string,
|
||||
stat func(string) (fs.FileInfo, error),
|
||||
chmod func(string, os.FileMode) error,
|
||||
) error {
|
||||
meta, err := stat(path)
|
||||
if err != nil {
|
||||
return perr.Wrapf(err, "stat %s", path)
|
||||
}
|
||||
mode := meta.Mode()
|
||||
attack.OriginModeOfFiles[path] = uint32(meta.Mode())
|
||||
if attack.NonReadable {
|
||||
mode &= ^os.FileMode(0444)
|
||||
}
|
||||
if attack.NonWritable {
|
||||
mode &= ^os.FileMode(0222)
|
||||
}
|
||||
log.Debug(fmt.Sprintf("change permission of %s to %s", path, mode))
|
||||
err = chmod(path, mode)
|
||||
if err != nil {
|
||||
return perr.Wrapf(err, "change permission of %s", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverIOPath(
|
||||
path string,
|
||||
originMode uint32,
|
||||
chmod func(string, os.FileMode) error,
|
||||
) error {
|
||||
err := chmod(path, os.FileMode(originMode))
|
||||
if err != nil {
|
||||
return perr.Wrapf(err, "change permission of %s", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func attackKafkaIO(attack *core.KafkaCommand) error {
|
||||
p, err := properties.LoadFile(attack.ConfigFile, properties.UTF8)
|
||||
if err != nil {
|
||||
return perr.Wrapf(err, "load config file %s", attack.ConfigFile)
|
||||
}
|
||||
partitionDirs, err := findPartitionDirs(attack, strings.Split(p.GetString("log.dirs", "/var/lib/kafka"), ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dirPath := range partitionDirs {
|
||||
err = attackIOPath(attack, dirPath, os.Stat, os.Chmod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return perr.Wrapf(err, "read partition dir %s", dirPath)
|
||||
}
|
||||
for _, file := range files {
|
||||
if !strings.HasSuffix(file.Name(), ".log") {
|
||||
continue
|
||||
}
|
||||
filePath := path.Join(dirPath, file.Name())
|
||||
err = attackIOPath(attack, filePath, os.Stat, os.Chmod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverKafkaIO(attack *core.KafkaCommand, env Environment) error {
|
||||
for path, originMode := range attack.OriginModeOfFiles {
|
||||
err := recoverIOPath(path, originMode, os.Chmod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPartitionDirs(attack *core.KafkaCommand, logDirs []string) ([]string, error) {
|
||||
partitions, err := getPartitions(attack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirs := make([]string, 0, len(partitions))
|
||||
|
||||
for _, partition := range partitions {
|
||||
dirName := fmt.Sprintf("%s-%d", attack.Topic, partition)
|
||||
for _, dir := range logDirs {
|
||||
entries, err := os.ReadDir(strings.TrimSpace(dir))
|
||||
if err != nil {
|
||||
return nil, perr.Wrapf(err, "read dir: %s", dir)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && entry.Name() == dirName {
|
||||
dirs = append(dirs, path.Join(strings.TrimSpace(dir), entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return dirs, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright 2022 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 (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
perr "github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type fakeFileInfo struct {
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
func (i *fakeFileInfo) Name() string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (i *fakeFileInfo) Size() int64 {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (i *fakeFileInfo) Mode() os.FileMode {
|
||||
return i.mode
|
||||
}
|
||||
|
||||
func (i *fakeFileInfo) ModTime() time.Time {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (i *fakeFileInfo) IsDir() bool {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (i *fakeFileInfo) Sys() interface{} {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
type fakeFs map[string]*fakeFileInfo
|
||||
|
||||
func newFakeFs() fakeFs {
|
||||
return make(fakeFs)
|
||||
}
|
||||
|
||||
func (f fakeFs) stat(path string) (fs.FileInfo, error) {
|
||||
info, ok := f[path]
|
||||
if !ok {
|
||||
return nil, perr.Errorf("fail to stat: %s", path)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (f fakeFs) chmod(path string, mode os.FileMode) error {
|
||||
info, ok := f[path]
|
||||
if !ok {
|
||||
return perr.Errorf("fail to stat: %s", path)
|
||||
}
|
||||
info.mode = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f fakeFs) clone() fakeFs {
|
||||
bak := newFakeFs()
|
||||
for p, i := range f {
|
||||
bak[p] = &fakeFileInfo{
|
||||
mode: i.mode,
|
||||
}
|
||||
}
|
||||
return bak
|
||||
}
|
||||
|
||||
func (f fakeFs) equal(other fakeFs) bool {
|
||||
if len(f) != len(other) {
|
||||
return false
|
||||
}
|
||||
for p, info := range f {
|
||||
otherInfo, ok := other[p]
|
||||
if !ok || info.mode != otherInfo.mode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f fakeFs) nonreadable() fakeFs {
|
||||
bak := f.clone()
|
||||
for _, i := range bak {
|
||||
i.mode &= ^os.FileMode(0444)
|
||||
}
|
||||
return bak
|
||||
}
|
||||
|
||||
func (f fakeFs) nonwritable() fakeFs {
|
||||
bak := f.clone()
|
||||
for _, i := range bak {
|
||||
i.mode &= ^os.FileMode(0222)
|
||||
}
|
||||
return bak
|
||||
}
|
||||
|
||||
func TestAttackIO(t *testing.T) {
|
||||
originFs := fakeFs{
|
||||
"/a": &fakeFileInfo{
|
||||
mode: os.FileMode(0777),
|
||||
},
|
||||
"/b": &fakeFileInfo{
|
||||
mode: os.FileMode(0700),
|
||||
},
|
||||
"/c": &fakeFileInfo{
|
||||
mode: os.FileMode(0070),
|
||||
},
|
||||
"/d": &fakeFileInfo{
|
||||
mode: os.FileMode(0007),
|
||||
},
|
||||
"/e": &fakeFileInfo{
|
||||
mode: os.FileMode(0123),
|
||||
},
|
||||
"/f": &fakeFileInfo{
|
||||
mode: os.FileMode(0124),
|
||||
},
|
||||
"/g": &fakeFileInfo{
|
||||
mode: os.FileMode(0247),
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, originFs.equal(originFs))
|
||||
assert.False(t, originFs.equal(originFs.nonreadable()))
|
||||
bakFs := originFs.clone()
|
||||
assert.True(t, originFs.equal(bakFs))
|
||||
|
||||
attack := core.NewKafkaCommand()
|
||||
attack.NonReadable = true
|
||||
// make only "/a" non-readable
|
||||
err := attackIOPath(attack, "/a", bakFs.stat, bakFs.chmod)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, bakFs["/a"].Mode(), originFs.nonreadable()["/a"].Mode())
|
||||
|
||||
// recover
|
||||
err = recoverIOPath("/a", uint32(originFs["/a"].Mode()), bakFs.chmod)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, bakFs.equal(originFs))
|
||||
|
||||
// make all path non-readable
|
||||
for p := range bakFs {
|
||||
err := attackIOPath(attack, p, bakFs.stat, bakFs.chmod)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.True(t, bakFs.equal(originFs.nonreadable()))
|
||||
|
||||
// recover
|
||||
for p, mode := range attack.OriginModeOfFiles {
|
||||
err := recoverIOPath(p, mode, bakFs.chmod)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.True(t, bakFs.equal(originFs))
|
||||
|
||||
// make all path non-readable and non-writable
|
||||
attack.NonWritable = true
|
||||
for p := range bakFs {
|
||||
err := attackIOPath(attack, p, bakFs.stat, bakFs.chmod)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.True(t, bakFs.equal(originFs.nonreadable().nonwritable()))
|
||||
|
||||
// recover
|
||||
for p, mode := range attack.OriginModeOfFiles {
|
||||
err := recoverIOPath(p, mode, bakFs.chmod)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.True(t, bakFs.equal(originFs))
|
||||
}
|
||||
|
|
@ -17,7 +17,9 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -25,16 +27,14 @@ import (
|
|||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-logr/zapr"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/bpm"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/chaosdaemon/pb"
|
||||
perrors "github.com/pingcap/errors"
|
||||
"github.com/pingcap/log"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
|
@ -53,54 +53,71 @@ func (networkAttack) Attack(options core.AttackConfig, env Environment) (err err
|
|||
case core.NetworkDNSAction:
|
||||
if attack.NeedApplyEtcHosts() {
|
||||
if err = env.Chaos.applyEtcHosts(attack, env.AttackUid, env); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if attack.NeedApplyDNSServer() {
|
||||
if err = env.Chaos.updateDNSServer(attack); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
case core.NetworkPortOccupied:
|
||||
case core.NetworkPortOccupiedAction:
|
||||
return env.Chaos.applyPortOccupied(attack)
|
||||
|
||||
case core.NetworkDelayAction, core.NetworkLossAction, core.NetworkCorruptAction, core.NetworkDuplicateAction:
|
||||
case core.NetworkDelayAction, core.NetworkLossAction, core.NetworkCorruptAction, core.NetworkDuplicateAction, core.NetworkBandwidthAction, core.NetworkPartitionAction:
|
||||
if attack.NeedApplyIPSet() {
|
||||
ipsetName, err = env.Chaos.applyIPSet(attack, env.AttackUid)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if attack.NeedApplyIptables() {
|
||||
if err = env.Chaos.applyIptables(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err = env.Chaos.applyIptables(attack, ipsetName, env.AttackUid); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if attack.NeedApplyTC() {
|
||||
if err = env.Chaos.applyTC(attack, ipsetName, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
// Because some tcs add filter iptables which will not be stored in the DB, we must re-apply these tcs to add the iptables.
|
||||
if err = env.Chaos.applyTC(attack, ipsetName, env.AttackUid); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
case core.NetworkNICDownAction:
|
||||
if err := env.Chaos.getNICIP(attack); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
NICDownCommand := fmt.Sprintf("ifconfig %s down", attack.Device)
|
||||
|
||||
cmd := exec.Command("bash", "-c", NICDownCommand)
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if attack.Duration != "-1" {
|
||||
err := env.Chaos.recoverNICDownScheduled(attack)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
case core.NetworkFloodAction:
|
||||
return env.Chaos.applyFlood(attack)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) applyIPSet(attack *core.NetworkCommand, uid string) (string, error) {
|
||||
ipset, err := attack.ToIPSet(fmt.Sprintf("chaos-%s", uid[:16]))
|
||||
ipset, err := attack.ToIPSet(fmt.Sprintf("chaos-%.16s", uid))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := s.svr.FlushIPSets(context.Background(), &pb.IPSetsRequest{
|
||||
Ipsets: []*pb.IPSet{ipset},
|
||||
EnterNS: false,
|
||||
}); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := s.ipsetRule.Set(context.Background(), &core.IPSetRule{
|
||||
|
|
@ -108,43 +125,47 @@ func (s *Server) applyIPSet(attack *core.NetworkCommand, uid string) (string, er
|
|||
Cidrs: strings.Join(ipset.Cidrs, ","),
|
||||
Experiment: uid,
|
||||
}); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return ipset.Name, nil
|
||||
}
|
||||
|
||||
func (s *Server) applyIptables(attack *core.NetworkCommand, uid string) error {
|
||||
func (s *Server) applyIptables(attack *core.NetworkCommand, ipset, uid string) error {
|
||||
iptables, err := s.iptablesRule.List(context.Background())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
chains := core.IptablesRuleList(iptables).ToChains()
|
||||
newChain, err := attack.ToChain()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if newChain != nil {
|
||||
chains = append(chains, newChain)
|
||||
var newChains []*pb.Chain
|
||||
// Presently, only partition and delay with `accept-tcp-flags` need to add additional chains
|
||||
if attack.NeedAdditionalChains() {
|
||||
newChains, err = attack.AdditionalChain(ipset, attack.Device, uid)
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
chains = append(chains, newChains...)
|
||||
}
|
||||
|
||||
if _, err := s.svr.SetIptablesChains(context.Background(), &pb.IptablesChainsRequest{
|
||||
Chains: chains,
|
||||
EnterNS: false,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
// TODO: cwen0
|
||||
//if err := s.iptablesRule.Set(context.Background(), &core.IptablesRule{
|
||||
// Name: newChain.Name,
|
||||
// IPSets: strings.Join(newChain.Ipsets, ","),
|
||||
// Direction: pb.Chain_Direction_name[int32(newChain.Direction)],
|
||||
// Experiment: uid,
|
||||
//}); err != nil {
|
||||
// return errors.WithStack(err)
|
||||
//}
|
||||
for _, newChain := range newChains {
|
||||
if err := s.iptablesRule.Set(context.Background(), &core.IptablesRule{
|
||||
Name: newChain.Name,
|
||||
IPSets: strings.Join(newChain.Ipsets, ","),
|
||||
Direction: pb.Chain_Direction_name[int32(newChain.Direction)],
|
||||
Protocol: newChain.Protocol,
|
||||
Experiment: uid,
|
||||
}); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -152,22 +173,30 @@ func (s *Server) applyIptables(attack *core.NetworkCommand, uid string) error {
|
|||
func (s *Server) applyTC(attack *core.NetworkCommand, ipset string, uid string) error {
|
||||
tcRules, err := s.tcRule.FindByDevice(context.Background(), attack.Device)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
tcs, err := core.TCRuleList(tcRules).ToTCs()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
newTC, err := attack.ToTC(ipset)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
var newTC *pb.Tc
|
||||
if attack.NeedApplyTC() {
|
||||
newTC, err = attack.ToTC(ipset)
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
tcs = append(tcs, newTC)
|
||||
}
|
||||
|
||||
tcs = append(tcs, newTC)
|
||||
if _, err := s.svr.SetTcs(context.Background(), &pb.TcsRequest{Tcs: tcs, Device: attack.Device, EnterNS: false}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
if _, err := s.svr.SetTcs(context.Background(), &pb.TcsRequest{Tcs: tcs, EnterNS: false}); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if !attack.NeedApplyTC() {
|
||||
return nil
|
||||
}
|
||||
|
||||
tc := &core.TcParameter{
|
||||
|
|
@ -195,13 +224,21 @@ func (s *Server) applyTC(attack *core.NetworkCommand, ipset string, uid string)
|
|||
Duplicate: attack.Percent,
|
||||
Correlation: attack.Correlation,
|
||||
}
|
||||
case core.NetworkBandwidthAction:
|
||||
tc.Bandwidth = &core.BandwidthSpec{
|
||||
Rate: attack.Rate,
|
||||
Limit: attack.Limit,
|
||||
Buffer: attack.Buffer,
|
||||
Peakrate: attack.Peakrate,
|
||||
Minburst: attack.Minburst,
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("network %s attack not supported", attack.Action)
|
||||
return perrors.Errorf("network %s attack not supported", attack.Action)
|
||||
}
|
||||
|
||||
tcString, err := json.Marshal(tc)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := s.tcRule.Set(context.Background(), &core.TCRule{
|
||||
|
|
@ -214,7 +251,7 @@ func (s *Server) applyTC(attack *core.NetworkCommand, ipset string, uid string)
|
|||
EgressPort: newTC.EgressPort,
|
||||
Experiment: uid,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -236,12 +273,12 @@ func (s *Server) applyEtcHosts(attack *core.NetworkCommand, uid string, env Envi
|
|||
stdout, err := backupCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(backupCmd.String()+string(stdout), zap.Error(err))
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadFile("/etc/hosts.chaosd." + uid) // #nosec
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(fileBytes), "\n")
|
||||
|
|
@ -250,21 +287,21 @@ func (s *Server) applyEtcHosts(attack *core.NetworkCommand, uid string, env Envi
|
|||
// example:
|
||||
// 10.86.33.102 qunarzz.com q.qunarzz.com common.qunarzz.com
|
||||
// 127.0.0.1 localhost
|
||||
needle := "^(\\d{1,3})(\\.\\d{1,3}){3}.*\\b" + attack.DNSHost + "\\b.*"
|
||||
needle := "^(\\d{1,3})(\\.\\d{1,3}){3}.*\\b" + attack.DNSDomainName + "\\b.*"
|
||||
re, err := regexp.Compile(needle)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
// match IP address, eg: 127.0.0.1
|
||||
reIp, err := regexp.Compile(`^(\d{1,3})(\.\d{1,3}){3}`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
fd, err := os.OpenFile("/etc/hosts", os.O_RDWR|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := fd.Close(); err != nil {
|
||||
|
|
@ -285,29 +322,54 @@ func (s *Server) applyEtcHosts(attack *core.NetworkCommand, uid string, env Envi
|
|||
line = line + "\n"
|
||||
_, err := w.WriteString(line)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
// if not match any, then add a new line.
|
||||
if newFlag {
|
||||
_, err := w.WriteString(attack.DNSIp + "\t" + attack.DNSHost + "\n")
|
||||
_, err := w.WriteString(attack.DNSIp + "\t" + attack.DNSDomainName + "\n")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = w.Flush()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
err = fd.Sync()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
recoverFlag = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) applyFlood(attack *core.NetworkCommand) error {
|
||||
cmd := bpm.DefaultProcessBuilder("bash", "-c", fmt.Sprintf("iperf -u -c %s -t %s -p %s -P %d -b %s", attack.IPAddress, attack.Duration, attack.Port, attack.Parallel, attack.Rate)).
|
||||
Build(context.Background())
|
||||
|
||||
// Build will set SysProcAttr.Pdeathsig = syscall.SIGTERM, and so iperf will exit while chaosd exit
|
||||
// so reset it here
|
||||
cmd.Cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger)
|
||||
backgroundProcessManager := bpm.StartBackgroundProcessManager(nil, logger)
|
||||
_, err = backgroundProcessManager.StartProcess(context.Background(), cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attack.IperfPid = int32(cmd.Process.Pid)
|
||||
log.Info("Start iperf process successfully", zap.String("command", cmd.String()), zap.Int32("Pid", attack.IperfPid))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (networkAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
config, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
|
|
@ -319,37 +381,35 @@ func (networkAttack) Recover(exp core.Experiment, env Environment) error {
|
|||
case core.NetworkDNSAction:
|
||||
if attack.NeedApplyEtcHosts() {
|
||||
if err := env.Chaos.recoverEtcHosts(attack, env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return env.Chaos.recoverDNSServer(attack)
|
||||
case core.NetworkPortOccupied:
|
||||
case core.NetworkPortOccupiedAction:
|
||||
return env.Chaos.recoverPortOccupied(attack, env.AttackUid)
|
||||
case core.NetworkDelayAction, core.NetworkLossAction, core.NetworkCorruptAction, core.NetworkDuplicateAction:
|
||||
if attack.NeedApplyIPSet() {
|
||||
if err := env.Chaos.recoverIPSet(env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case core.NetworkDelayAction, core.NetworkLossAction, core.NetworkCorruptAction, core.NetworkDuplicateAction, core.NetworkPartitionAction, core.NetworkBandwidthAction:
|
||||
if err := env.Chaos.recoverIPSet(env.AttackUid); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if attack.NeedApplyIptables() {
|
||||
if err := env.Chaos.recoverIptables(env.AttackUid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := env.Chaos.recoverIptables(env.AttackUid); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if attack.NeedApplyTC() {
|
||||
if err := env.Chaos.recoverTC(env.AttackUid, attack.Device); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := env.Chaos.recoverTC(env.AttackUid, attack.Device); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
case core.NetworkNICDownAction:
|
||||
return env.Chaos.recoverNICDown(attack)
|
||||
case core.NetworkFloodAction:
|
||||
return env.Chaos.recoverFlood(attack)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverIPSet(uid string) error {
|
||||
if err := s.ipsetRule.DeleteByExperiment(context.Background(), uid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -357,12 +417,12 @@ func (s *Server) recoverIPSet(uid string) error {
|
|||
|
||||
func (s *Server) recoverIptables(uid string) error {
|
||||
if err := s.iptablesRule.DeleteByExperiment(context.Background(), uid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
iptables, err := s.iptablesRule.List(context.Background())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
chains := core.IptablesRuleList(iptables).ToChains()
|
||||
|
|
@ -371,7 +431,7 @@ func (s *Server) recoverIptables(uid string) error {
|
|||
Chains: chains,
|
||||
EnterNS: false,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -379,18 +439,18 @@ func (s *Server) recoverIptables(uid string) error {
|
|||
|
||||
func (s *Server) recoverTC(uid string, device string) error {
|
||||
if err := s.tcRule.DeleteByExperiment(context.Background(), uid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
tcRules, err := s.tcRule.FindByDevice(context.Background(), device)
|
||||
|
||||
tcs, err := core.TCRuleList(tcRules).ToTCs()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := s.svr.SetTcs(context.Background(), &pb.TcsRequest{Tcs: tcs, Device: device, EnterNS: false}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
if _, err := s.svr.SetTcs(context.Background(), &pb.TcsRequest{Tcs: tcs, EnterNS: false}); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -402,7 +462,7 @@ func (s *Server) updateDNSServer(attack *core.NetworkCommand) error {
|
|||
Enable: true,
|
||||
EnterNS: false,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -413,7 +473,7 @@ func (s *Server) recoverDNSServer(attack *core.NetworkCommand) error {
|
|||
Enable: false,
|
||||
EnterNS: false,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -428,24 +488,28 @@ func (s *Server) applyPortOccupied(attack *core.NetworkCommand) error {
|
|||
flag, err := checkPortIsListened(attack.Port)
|
||||
if err != nil {
|
||||
if flag {
|
||||
return errors.Errorf("port %s has been occupied", attack.Port)
|
||||
return perrors.Errorf("port %s has been occupied", attack.Port)
|
||||
}
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if flag {
|
||||
return errors.Errorf("port %s has been occupied", attack.Port)
|
||||
return perrors.Errorf("port %s has been occupied", attack.Port)
|
||||
}
|
||||
|
||||
args := fmt.Sprintf("-p=%s", attack.Port)
|
||||
cmd := bpm.DefaultProcessBuilder("PortOccupyTool", args).Build()
|
||||
cmd := bpm.DefaultProcessBuilder("PortOccupyTool", args).Build(context.Background())
|
||||
|
||||
cmd.Cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
|
||||
backgroundProcessManager := bpm.NewBackgroundProcessManager()
|
||||
err = backgroundProcessManager.StartProcess(cmd)
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger)
|
||||
backgroundProcessManager := bpm.StartBackgroundProcessManager(nil, logger)
|
||||
_, err = backgroundProcessManager.StartProcess(context.Background(), cmd)
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
attack.PortPid = int32(cmd.Process.Pid)
|
||||
|
|
@ -463,7 +527,7 @@ func checkPortIsListened(port string) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
log.Error(cmd.String()+string(stdout), zap.Error(err))
|
||||
return true, errors.WithStack(err)
|
||||
return true, perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if string(stdout) == "" {
|
||||
|
|
@ -502,7 +566,87 @@ func (s *Server) recoverEtcHosts(attack *core.NetworkCommand, uid string) error
|
|||
stdout, err := recoverCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(recoverCmd.String()+string(stdout), zap.Error(err))
|
||||
return errors.WithStack(err)
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverNICDown(attack *core.NetworkCommand) error {
|
||||
NICUpCommand := fmt.Sprintf("ifconfig %s %s up", attack.Device, attack.IPAddress)
|
||||
|
||||
recoverCmd := exec.Command("bash", "-c", NICUpCommand)
|
||||
_, err := recoverCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverNICDownScheduled(attack *core.NetworkCommand) error {
|
||||
NICUpCommand := fmt.Sprintf("sleep %s && ifconfig %s %s up", attack.Duration, attack.Device, attack.IPAddress)
|
||||
|
||||
recoverCmd := exec.Command("bash", "-c", NICUpCommand)
|
||||
_, err := recoverCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverFlood(attack *core.NetworkCommand) error {
|
||||
proc, err := process.NewProcess(attack.IperfPid)
|
||||
if err != nil {
|
||||
if errors.Is(err, process.ErrorProcessNotRunning) || errors.Is(err, fs.ErrNotExist) {
|
||||
log.Warn("Failed to get iperf process", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
procName, err := proc.Name()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(procName, "iperf") {
|
||||
log.Warn("the process is not iperf, maybe it is killed by manual")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := proc.Kill(); err != nil {
|
||||
log.Error("the iperf process kill failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getNICIP() uses `ifconfig` to get interfaces' IP. The reason for
|
||||
// not using net.Interfaces() is that net.Interfaces() can't get
|
||||
// sub interfaces.
|
||||
func (s *Server) getNICIP(attack *core.NetworkCommand) error {
|
||||
getIPCommand := fmt.Sprintf("ifconfig %s | awk '/inet\\>/ {print $2}'", attack.Device)
|
||||
|
||||
cmd := exec.Command("bash", "-c", getIPCommand)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
|
||||
stdoutBytes := make([]byte, 1024)
|
||||
_, err = stdout.Read(stdoutBytes)
|
||||
if err != nil {
|
||||
return perrors.WithStack(err)
|
||||
}
|
||||
// When stdoutBytes is converted to string, the string will be IPAddress with a few unnecessary
|
||||
// zeros, which makes IPAddress' format wrong, so the trailing zeros needs to be trimmed.
|
||||
attack.IPAddress = strings.TrimRight(string(stdoutBytes), "\n\x00")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@
|
|||
package chaosd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/mitchellh/go-ps"
|
||||
"github.com/pingcap/errors"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
|
@ -30,30 +35,28 @@ var ProcessAttack AttackType = processAttack{}
|
|||
func (processAttack) Attack(options core.AttackConfig, _ Environment) error {
|
||||
attack := options.(*core.ProcessCommand)
|
||||
|
||||
processes, err := ps.Processes()
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
notFound := true
|
||||
for _, p := range processes {
|
||||
if attack.Process == strconv.Itoa(p.Pid()) || attack.Process == p.Executable() {
|
||||
notFound = false
|
||||
switch attack.Signal {
|
||||
case int(syscall.SIGKILL):
|
||||
err = syscall.Kill(p.Pid(), syscall.SIGKILL)
|
||||
case int(syscall.SIGTERM):
|
||||
err = syscall.Kill(p.Pid(), syscall.SIGTERM)
|
||||
case int(syscall.SIGSTOP):
|
||||
err = syscall.Kill(p.Pid(), syscall.SIGSTOP)
|
||||
default:
|
||||
return errors.Errorf("signal %d is not supported", attack.Signal)
|
||||
}
|
||||
pid := int(p.Pid)
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if attack.Process == strconv.Itoa(pid) || attack.Process == name {
|
||||
notFound = false
|
||||
|
||||
err = syscall.Kill(pid, syscall.Signal(attack.Signal))
|
||||
if err != nil {
|
||||
err = errors.Annotate(err, fmt.Sprintf("kill process with signal %d", attack.Signal))
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
attack.PIDs = append(attack.PIDs, p.Pid())
|
||||
attack.PIDs = append(attack.PIDs, pid)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,13 +75,23 @@ func (processAttack) Recover(exp core.Experiment, _ Environment) error {
|
|||
}
|
||||
pcmd := config.(*core.ProcessCommand)
|
||||
if pcmd.Signal != int(syscall.SIGSTOP) {
|
||||
return core.ErrNonRecoverableAttack.New("only SIGSTOP process attack is supported to recover")
|
||||
}
|
||||
if pcmd.RecoverCmd == "" {
|
||||
return core.ErrNonRecoverableAttack.New("only SIGSTOP process attack and process attack with the recover-cmd are supported to recover")
|
||||
}
|
||||
|
||||
for _, pid := range pcmd.PIDs {
|
||||
if err := syscall.Kill(pid, syscall.SIGCONT); err != nil {
|
||||
rcmd := exec.Command("bash", "-c", pcmd.RecoverCmd)
|
||||
if err := rcmd.Start(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
log.Info("Execute recover-cmd successfully", zap.String("recover-cmd", pcmd.RecoverCmd))
|
||||
|
||||
} else {
|
||||
for _, pid := range pcmd.PIDs {
|
||||
if err := syscall.Kill(pid, syscall.SIGCONT); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
)
|
||||
|
||||
func (s *Server) RecoverAttack(uid string) error {
|
||||
exp, err := s.exp.FindByUid(context.Background(), uid)
|
||||
exp, err := s.expStore.FindByUid(context.Background(), uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -61,8 +61,24 @@ func (s *Server) RecoverAttack(uid string) error {
|
|||
attackType = StressAttack
|
||||
case core.DiskAttack:
|
||||
attackType = DiskAttack
|
||||
case core.DiskServerAttack:
|
||||
attackType = DiskServerAttack
|
||||
case core.JVMAttack:
|
||||
attackType = JVMAttack
|
||||
case core.ClockAttack:
|
||||
attackType = ClockAttack
|
||||
case core.KafkaAttack:
|
||||
attackType = KafkaAttack
|
||||
case core.RedisAttack:
|
||||
attackType = RedisAttack
|
||||
case core.FileAttack:
|
||||
attackType = FileAttack
|
||||
case core.HTTPAttack:
|
||||
attackType = HTTPAttack
|
||||
case core.VMAttack:
|
||||
attackType = VMAttack
|
||||
case core.UserDefinedAttack:
|
||||
attackType = UserDefinedAttack
|
||||
default:
|
||||
return perr.Errorf("chaos experiment kind %s not found", exp.Kind)
|
||||
}
|
||||
|
|
@ -77,7 +93,7 @@ func (s *Server) RecoverAttack(uid string) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := s.exp.Update(context.Background(), uid, core.Destroyed, "", exp.RecoverCommand); err != nil {
|
||||
if err := s.expStore.Update(context.Background(), uid, core.Destroyed, "", exp.RecoverCommand); err != nil {
|
||||
return perr.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright 2022 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 (
|
||||
"fmt"
|
||||
"math"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/pingcap/errors"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type redisAttack struct{}
|
||||
|
||||
var RedisAttack AttackType = redisAttack{}
|
||||
|
||||
const (
|
||||
STATUSOK = "OK"
|
||||
OPTIONNX = "NX"
|
||||
OPTIONXX = "XX"
|
||||
OPTIONGT = "GT"
|
||||
OPTIONLT = "LT"
|
||||
)
|
||||
|
||||
func (redisAttack) Attack(options core.AttackConfig, env Environment) error {
|
||||
attack := options.(*core.RedisCommand)
|
||||
|
||||
cli := redis.NewClient(&redis.Options{
|
||||
Addr: attack.Addr,
|
||||
Password: attack.Password,
|
||||
})
|
||||
_, err := cli.Ping(cli.Context()).Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
switch attack.Action {
|
||||
case core.RedisSentinelRestartAction:
|
||||
err := env.Chaos.shutdownSentinelServer(attack, cli)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return env.Chaos.recoverSentinelStop(attack)
|
||||
|
||||
case core.RedisSentinelStopAction:
|
||||
return env.Chaos.shutdownSentinelServer(attack, cli)
|
||||
|
||||
case core.RedisCachePenetrationAction:
|
||||
pipe := cli.Pipeline()
|
||||
for i := 0; i < attack.RequestNum; i++ {
|
||||
pipe.Get(cli.Context(), "CHAOS_MESH_nqE3BWm7khHv")
|
||||
}
|
||||
_, err := pipe.Exec(cli.Context())
|
||||
if err != redis.Nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
case core.RedisCacheLimitAction:
|
||||
// `maxmemory` is an interface listwith content similar to `[maxmemory 1024]`
|
||||
maxmemory, err := cli.ConfigGet(cli.Context(), "maxmemory").Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
// Get the value of maxmemory
|
||||
attack.OriginCacheSize = fmt.Sprint(maxmemory[1])
|
||||
|
||||
var cacheSize string
|
||||
if attack.Percent != "" {
|
||||
percentage, err := strconv.ParseFloat(attack.Percent[0:len(attack.Percent)-1], 64)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
originCacheSize, err := strconv.ParseFloat(attack.OriginCacheSize, 64)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
cacheSize = fmt.Sprint(int(math.Floor(originCacheSize / 100.0 * percentage)))
|
||||
} else {
|
||||
cacheSize = attack.CacheSize
|
||||
}
|
||||
|
||||
result, err := cli.ConfigSet(cli.Context(), "maxmemory", cacheSize).Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if result != STATUSOK {
|
||||
return errors.WithStack(errors.Errorf("redis command status is %s", result))
|
||||
}
|
||||
|
||||
case core.RedisCacheExpirationAction:
|
||||
return env.Chaos.expireKeys(attack, cli)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisAttack) Recover(exp core.Experiment, env Environment) error {
|
||||
config, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attack := config.(*core.RedisCommand)
|
||||
|
||||
switch attack.Action {
|
||||
case core.RedisSentinelStopAction:
|
||||
return env.Chaos.recoverSentinelStop(attack)
|
||||
|
||||
case core.RedisCacheLimitAction:
|
||||
cli := redis.NewClient(&redis.Options{
|
||||
Addr: attack.Addr,
|
||||
Password: attack.Password,
|
||||
})
|
||||
|
||||
result, err := cli.ConfigSet(cli.Context(), "maxmemory", attack.OriginCacheSize).Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if result != STATUSOK {
|
||||
return errors.WithStack(errors.Errorf("redis command status is %s", result))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) shutdownSentinelServer(attack *core.RedisCommand, cli *redis.Client) error {
|
||||
if attack.FlushConfig {
|
||||
// Because redis.Client doesn't have the func `FlushConfig()`, a redis.SentinelClient has to be created
|
||||
sentinelCli := redis.NewSentinelClient(&redis.Options{
|
||||
Addr: attack.Addr,
|
||||
})
|
||||
result, err := sentinelCli.FlushConfig(sentinelCli.Context()).Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if result != STATUSOK {
|
||||
return errors.WithStack(errors.Errorf("redis command status is %s", result))
|
||||
}
|
||||
}
|
||||
|
||||
// If cli.Shutdown() runs successfully, the result will be nil and the err will be "connection refused"
|
||||
result, err := cli.Shutdown(cli.Context()).Result()
|
||||
if result != "" {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) recoverSentinelStop(attack *core.RedisCommand) error {
|
||||
if attack.Conf == "" {
|
||||
return errors.WithStack(errors.Errorf("redis config does not exist"))
|
||||
}
|
||||
var redisPath string
|
||||
if attack.RedisPath != "" {
|
||||
redisPath = attack.RedisPath + "/redis-server"
|
||||
} else {
|
||||
redisPath = "redis-server"
|
||||
}
|
||||
recoverCmd := exec.Command(redisPath, attack.Conf, "--sentinel")
|
||||
_, err := recoverCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) expireKeys(attack *core.RedisCommand, cli *redis.Client) error {
|
||||
|
||||
expiration, err := time.ParseDuration(attack.Expiration)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if attack.Key == "" {
|
||||
// Get all keys from the server
|
||||
allKeys, err := cli.Keys(cli.Context(), "*").Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, key := range allKeys {
|
||||
result, err := ExpireFunc(cli, key, expiration, attack.Option).Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if !result {
|
||||
return errors.WithStack(errors.Errorf("expire failed"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result, err := ExpireFunc(cli, attack.Key, expiration, attack.Option).Result()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if !result {
|
||||
return errors.WithStack(errors.Errorf("expire failed"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExpireFunc(cli *redis.Client, key string, expiration time.Duration, option string) *redis.BoolCmd {
|
||||
switch option {
|
||||
case OPTIONNX:
|
||||
return cli.ExpireNX(cli.Context(), key, expiration)
|
||||
case OPTIONXX:
|
||||
return cli.ExpireXX(cli.Context(), key, expiration)
|
||||
case OPTIONGT:
|
||||
return cli.ExpireGT(cli.Context(), key, expiration)
|
||||
case OPTIONLT:
|
||||
return cli.ExpireLT(cli.Context(), key, expiration)
|
||||
default:
|
||||
return cli.Expire(cli.Context(), key, expiration)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ import (
|
|||
|
||||
func (s *Server) Search(conds *core.SearchCommand) ([]*core.Experiment, error) {
|
||||
if len(conds.UID) > 0 {
|
||||
exp, err := s.exp.FindByUid(context.Background(), conds.UID)
|
||||
exp, err := s.expStore.FindByUid(context.Background(), conds.UID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ func (s *Server) Search(conds *core.SearchCommand) ([]*core.Experiment, error) {
|
|||
return []*core.Experiment{exp}, nil
|
||||
}
|
||||
|
||||
exps, err := s.exp.ListByConditions(context.Background(), conds)
|
||||
exps, err := s.expStore.ListByConditions(context.Background(), conds)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/config"
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
"github.com/chaos-mesh/chaosd/pkg/scheduler"
|
||||
"github.com/chaos-mesh/chaosd/pkg/utils"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
exp core.ExperimentStore
|
||||
expStore core.ExperimentStore
|
||||
ExpRun core.ExperimentRunStore
|
||||
Cron scheduler.Scheduler
|
||||
ipsetRule core.IPSetRuleStore
|
||||
|
|
@ -30,6 +31,8 @@ type Server struct {
|
|||
tcRule core.TCRuleStore
|
||||
conf *config.Config
|
||||
svr *chaosdaemon.DaemonServer
|
||||
|
||||
CmdPools map[string]*utils.CommandPools
|
||||
}
|
||||
|
||||
func NewServer(
|
||||
|
|
@ -44,12 +47,13 @@ func NewServer(
|
|||
) *Server {
|
||||
return &Server{
|
||||
conf: conf,
|
||||
exp: exp,
|
||||
expStore: exp,
|
||||
Cron: cron,
|
||||
ExpRun: expRun,
|
||||
ipsetRule: ipset,
|
||||
iptablesRule: iptables,
|
||||
tcRule: tc,
|
||||
svr: svr,
|
||||
CmdPools: make(map[string]*utils.CommandPools),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,15 @@
|
|||
package chaosd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-logr/zapr"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/bpm"
|
||||
"github.com/pingcap/log"
|
||||
|
|
@ -32,10 +37,18 @@ type stressAttack struct{}
|
|||
|
||||
var StressAttack AttackType = stressAttack{}
|
||||
|
||||
const (
|
||||
CPUSTRESSORTOOL = "stress-ng"
|
||||
MEMORYSTRESSORTOOL = "memStress"
|
||||
)
|
||||
|
||||
func (stressAttack) Attack(options core.AttackConfig, _ Environment) (err error) {
|
||||
attack := options.(*core.StressCommand)
|
||||
stressors := &v1alpha1.Stressors{}
|
||||
var stressorTool string
|
||||
|
||||
if attack.Action == core.StressCPUAction {
|
||||
stressorTool = CPUSTRESSORTOOL
|
||||
stressors.CPUStressor = &v1alpha1.CPUStressor{
|
||||
Stressor: v1alpha1.Stressor{
|
||||
Workers: attack.Workers,
|
||||
|
|
@ -44,6 +57,7 @@ func (stressAttack) Attack(options core.AttackConfig, _ Environment) (err error)
|
|||
Options: attack.Options,
|
||||
}
|
||||
} else if attack.Action == core.StressMemAction {
|
||||
stressorTool = MEMORYSTRESSORTOOL
|
||||
stressors.MemoryStressor = &v1alpha1.MemoryStressor{
|
||||
Stressor: v1alpha1.Stressor{
|
||||
Workers: attack.Workers,
|
||||
|
|
@ -53,32 +67,46 @@ func (stressAttack) Attack(options core.AttackConfig, _ Environment) (err error)
|
|||
}
|
||||
}
|
||||
|
||||
errs := stressors.Validate(field.NewPath("stressors"))
|
||||
var stressorsStr string
|
||||
if attack.Action == core.StressCPUAction {
|
||||
stressorsStr, _, err = stressors.Normalize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if attack.Action == core.StressMemAction {
|
||||
_, stressorsStr, err = stressors.Normalize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
errs := stressors.Validate(nil, field.NewPath("stressors"))
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
|
||||
stressorsStr, err := stressors.Normalize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Info("stressors normalize", zap.String("arguments", stressorsStr))
|
||||
|
||||
cmd := bpm.DefaultProcessBuilder("stress-ng", strings.Fields(stressorsStr)...).
|
||||
Build()
|
||||
cmd := bpm.DefaultProcessBuilder(stressorTool, strings.Fields(stressorsStr)...).
|
||||
Build(context.Background())
|
||||
|
||||
// Build will set SysProcAttr.Pdeathsig = syscall.SIGTERM, and so stress-ng will exit while chaosd exit
|
||||
// so reset it here
|
||||
cmd.Cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
|
||||
backgroundProcessManager := bpm.NewBackgroundProcessManager()
|
||||
err = backgroundProcessManager.StartProcess(cmd)
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger)
|
||||
backgroundProcessManager := bpm.StartBackgroundProcessManager(nil, logger)
|
||||
_, err = backgroundProcessManager.StartProcess(context.Background(), cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
attack.StressngPid = int32(cmd.Process.Pid)
|
||||
log.Info("Start stress-ng process successfully", zap.String("command", cmd.String()), zap.Int32("Pid", attack.StressngPid))
|
||||
log.Info(fmt.Sprintf("Start %s process successfully", stressorTool), zap.String("command", cmd.String()), zap.Int32("Pid", attack.StressngPid))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -91,6 +119,11 @@ func (stressAttack) Recover(exp core.Experiment, _ Environment) error {
|
|||
attack := config.(*core.StressCommand)
|
||||
proc, err := process.NewProcess(attack.StressngPid)
|
||||
if err != nil {
|
||||
log.Warn("Failed to get process", zap.Error(err))
|
||||
if errors.Is(err, process.ErrorProcessNotRunning) || errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -99,13 +132,13 @@ func (stressAttack) Recover(exp core.Experiment, _ Environment) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(procName, "stress-ng") {
|
||||
log.Warn("the process is not stress-ng, maybe it is killed by manual")
|
||||
if !strings.Contains(procName, CPUSTRESSORTOOL) && !strings.Contains(procName, MEMORYSTRESSORTOOL) {
|
||||
log.Warn("the process is not stress-ng or memStress, maybe it is killed by manual")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := proc.Kill(); err != nil {
|
||||
log.Error("the stress-ng process kill failed", zap.Error(err))
|
||||
log.Error("the process kill failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2022 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/exec"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type userDefinedAttack struct{}
|
||||
|
||||
var UserDefinedAttack AttackType = userDefinedAttack{}
|
||||
|
||||
func (userDefinedAttack) Attack(options core.AttackConfig, _ Environment) error {
|
||||
option := options.(*core.UserDefinedOption)
|
||||
log.Info("attack command", zap.String("command", option.AttackCmd))
|
||||
|
||||
cmd := exec.Command("bash", "-c", option.AttackCmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, string(output))
|
||||
}
|
||||
if len(output) > 0 {
|
||||
log.Info("attack command", zap.String("output", string(output)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (userDefinedAttack) Recover(exp core.Experiment, _ Environment) error {
|
||||
config, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
option := config.(*core.UserDefinedOption)
|
||||
log.Info("recover command", zap.String("command", option.RecoverCmd))
|
||||
|
||||
cmd := exec.Command("bash", "-c", option.RecoverCmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, string(output))
|
||||
}
|
||||
if len(output) > 0 {
|
||||
log.Info("recover command", zap.String("command", option.RecoverCmd), zap.String("output", string(output)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2022 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
type vmAttack struct{}
|
||||
|
||||
var VMAttack AttackType = vmAttack{}
|
||||
|
||||
func (vm vmAttack) Attack(options core.AttackConfig, env Environment) error {
|
||||
vmOption, ok := options.(*core.VMOption)
|
||||
if !ok {
|
||||
return errors.New("the type is not VMOption")
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", fmt.Sprintf("virsh destroy %s", vmOption.VMName))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vmAttack) Recover(exp core.Experiment, _ Environment) error {
|
||||
attackConfig, err := exp.GetRequestCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vmOption, ok := attackConfig.(*core.VMOption)
|
||||
if !ok {
|
||||
return errors.New("the type is not VMOption")
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", fmt.Sprintf("virsh start %s", vmOption.VMName))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// 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 httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
MTLSServer = "mTLS"
|
||||
TLSServer = "tls"
|
||||
HTTPServer = "http"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingClientCert = utils.ErrAuth.New("Sorry, but you need to provide a client certificate to continue")
|
||||
)
|
||||
|
||||
func (s *HttpServer) serverMode() string {
|
||||
if len(s.conf.SSLCertFile) > 0 {
|
||||
if len(s.conf.SSLClientCAFile) > 0 {
|
||||
return MTLSServer
|
||||
}
|
||||
return TLSServer
|
||||
}
|
||||
return HTTPServer
|
||||
}
|
||||
|
||||
func (s *HttpServer) startHttpsServer() (err error) {
|
||||
mode := s.serverMode()
|
||||
if mode == HTTPServer {
|
||||
return nil
|
||||
}
|
||||
|
||||
httpsServerAddr := s.conf.HttpsServerAddress()
|
||||
e := gin.Default()
|
||||
e.Use(utils.MWHandleErrors())
|
||||
|
||||
if mode == MTLSServer {
|
||||
log.Info("starting HTTPS server with Client Auth", zap.String("address", httpsServerAddr))
|
||||
|
||||
caCert, ioErr := os.ReadFile(s.conf.SSLClientCAFile)
|
||||
if ioErr != nil {
|
||||
err = ioErr
|
||||
return
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
ServerName: s.conf.ServerName,
|
||||
}
|
||||
e.Use(authenticateClientCert(tlsConfig))
|
||||
|
||||
// According to https://github.com/gin-gonic/gin/issues/531, we need to register middlewares before RouterGroup.
|
||||
s.handler(e)
|
||||
server := &http.Server{
|
||||
Addr: httpsServerAddr,
|
||||
TLSConfig: tlsConfig,
|
||||
Handler: e,
|
||||
}
|
||||
|
||||
err = server.ListenAndServeTLS(s.conf.SSLCertFile, s.conf.SSLKeyFile)
|
||||
} else if mode == TLSServer {
|
||||
log.Info("starting HTTPS server", zap.String("address", httpsServerAddr))
|
||||
s.handler(e)
|
||||
err = e.RunTLS(httpsServerAddr, s.conf.SSLCertFile, s.conf.SSLKeyFile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func verifyCertificates(config *tls.Config, certs []*x509.Certificate) error {
|
||||
t := config.Time
|
||||
if t == nil {
|
||||
t = time.Now
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: config.ClientCAs,
|
||||
CurrentTime: t(),
|
||||
Intermediates: x509.NewCertPool(),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
|
||||
for _, cert := range certs[1:] {
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
|
||||
_, err := certs[0].Verify(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authenticateClientCert(config *tls.Config) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
clientTLS := ctx.Request.TLS
|
||||
if len(clientTLS.PeerCertificates) > 0 {
|
||||
if err := verifyCertificates(config, clientTLS.PeerCertificates); err != nil {
|
||||
_ = ctx.AbortWithError(http.StatusForbidden, utils.ErrAuth.Wrap(err, "Unauthorized certificate credentials"))
|
||||
} else {
|
||||
ctx.Next()
|
||||
}
|
||||
} else {
|
||||
_ = ctx.AbortWithError(http.StatusUnauthorized, errMissingClientCert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
)
|
||||
|
||||
func (s *httpServer) listExperiments(c *gin.Context) {
|
||||
func (s *HttpServer) listExperiments(c *gin.Context) {
|
||||
mode, ok := c.GetQuery("launch_mode")
|
||||
var chaosList []*core.Experiment
|
||||
var err error
|
||||
|
|
@ -32,17 +32,17 @@ func (s *httpServer) listExperiments(c *gin.Context) {
|
|||
chaosList, err = s.exp.List(context.Background())
|
||||
}
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, chaosList)
|
||||
}
|
||||
|
||||
func (s *httpServer) listExperimentRuns(c *gin.Context) {
|
||||
func (s *HttpServer) listExperimentRuns(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
runsList, err := s.chaos.ExpRun.ListByExperimentUID(context.Background(), uid)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, runsList)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/joomcode/errorx"
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/config"
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
|
|
@ -29,50 +30,56 @@ import (
|
|||
"github.com/chaos-mesh/chaosd/pkg/swaggerserver"
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
conf *config.Config
|
||||
chaos *chaosd.Server
|
||||
exp core.ExperimentStore
|
||||
engine *gin.Engine
|
||||
type HttpServer struct {
|
||||
conf *config.Config
|
||||
chaos *chaosd.Server
|
||||
exp core.ExperimentStore
|
||||
}
|
||||
|
||||
func NewServer(
|
||||
conf *config.Config,
|
||||
chaos *chaosd.Server,
|
||||
exp core.ExperimentStore,
|
||||
) *httpServer {
|
||||
e := gin.Default()
|
||||
e.Use(utils.MWHandleErrors())
|
||||
|
||||
return &httpServer{
|
||||
conf: conf,
|
||||
chaos: chaos,
|
||||
exp: exp,
|
||||
engine: e,
|
||||
) *HttpServer {
|
||||
return &HttpServer{
|
||||
conf: conf,
|
||||
chaos: chaos,
|
||||
exp: exp,
|
||||
}
|
||||
}
|
||||
|
||||
func Register(s *httpServer, scheduler scheduler.Scheduler) {
|
||||
func Register(s *HttpServer, scheduler scheduler.Scheduler) {
|
||||
if s.conf.Platform != config.LocalPlatform {
|
||||
return
|
||||
}
|
||||
|
||||
handler(s)
|
||||
|
||||
go func() {
|
||||
addr := s.conf.Address()
|
||||
log.Debug("starting HTTP server", zap.String("address", addr))
|
||||
|
||||
if err := s.engine.Run(addr); err != nil {
|
||||
if err := s.startHttpServer(); err != nil {
|
||||
log.Fatal("failed to start HTTP server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := s.startHttpsServer(); err != nil {
|
||||
log.Fatal("failed to start HTTPS server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
scheduler.Start()
|
||||
}
|
||||
|
||||
func handler(s *httpServer) {
|
||||
api := s.engine.Group("/api")
|
||||
func (s *HttpServer) startHttpServer() error {
|
||||
httpServerAddr := s.conf.Address()
|
||||
log.Info("starting HTTP server", zap.String("address", httpServerAddr))
|
||||
e := gin.Default()
|
||||
e.Use(utils.MWHandleErrors())
|
||||
s.systemHandler(e)
|
||||
if s.serverMode() == HTTPServer {
|
||||
s.handler(e)
|
||||
}
|
||||
return e.Run(httpServerAddr)
|
||||
}
|
||||
|
||||
func (s *HttpServer) handler(engine *gin.Engine) {
|
||||
api := engine.Group("/api")
|
||||
{
|
||||
api.GET("/swagger/*any", swaggerserver.Handler())
|
||||
}
|
||||
|
|
@ -83,6 +90,12 @@ func handler(s *httpServer) {
|
|||
attack.POST("/stress", s.createStressAttack)
|
||||
attack.POST("/network", s.createNetworkAttack)
|
||||
attack.POST("/disk", s.createDiskAttack)
|
||||
attack.POST("/clock", s.createClockAttack)
|
||||
attack.POST("/jvm", s.createJVMAttack)
|
||||
attack.POST("/kafka", s.createKafkaAttack)
|
||||
attack.POST("/vm", s.createVMAttack)
|
||||
attack.POST("/redis", s.createRedisAttack)
|
||||
attack.POST("/user_defined", s.createUserDefinedAttack)
|
||||
|
||||
attack.DELETE("/:uid", s.recoverAttack)
|
||||
}
|
||||
|
|
@ -92,7 +105,10 @@ func handler(s *httpServer) {
|
|||
experiments.GET("/", s.listExperiments)
|
||||
experiments.GET("/:uid/runs", s.listExperimentRuns)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HttpServer) systemHandler(engine *gin.Engine) {
|
||||
api := engine.Group("/api")
|
||||
system := api.Group("/system")
|
||||
{
|
||||
system.GET("/health", s.healthcheck)
|
||||
|
|
@ -109,10 +125,17 @@ func handler(s *httpServer) {
|
|||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/process [post]
|
||||
func (s *httpServer) createProcessAttack(c *gin.Context) {
|
||||
func (s *HttpServer) createProcessAttack(c *gin.Context) {
|
||||
attack := core.NewProcessCommand()
|
||||
if err := c.ShouldBindJSON(attack); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
attack.CompleteDefaults()
|
||||
if err := attack.Validate(); err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -134,10 +157,17 @@ func (s *httpServer) createProcessAttack(c *gin.Context) {
|
|||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/network [post]
|
||||
func (s *httpServer) createNetworkAttack(c *gin.Context) {
|
||||
func (s *HttpServer) createNetworkAttack(c *gin.Context) {
|
||||
attack := core.NewNetworkCommand()
|
||||
if err := c.ShouldBindJSON(attack); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
attack.CompleteDefaults()
|
||||
if err := attack.Validate(); err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -159,10 +189,17 @@ func (s *httpServer) createNetworkAttack(c *gin.Context) {
|
|||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/stress [post]
|
||||
func (s *httpServer) createStressAttack(c *gin.Context) {
|
||||
func (s *HttpServer) createStressAttack(c *gin.Context) {
|
||||
attack := core.NewStressCommand()
|
||||
if err := c.ShouldBindJSON(attack); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
attack.CompleteDefaults()
|
||||
if err := attack.Validate(); err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -184,15 +221,246 @@ func (s *httpServer) createStressAttack(c *gin.Context) {
|
|||
// @Failure 400 {object} utils.APIError
|
||||
// @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 {
|
||||
c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
func (s *HttpServer) createDiskAttack(c *gin.Context) {
|
||||
options := core.NewDiskOptionForServer()
|
||||
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)
|
||||
options.CompleteDefaults()
|
||||
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.DiskServerAttack, attackConfig, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create clock attack.
|
||||
// @Description Create clock attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.ClockOption true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/clock [post]
|
||||
func (s *HttpServer) createClockAttack(c *gin.Context) {
|
||||
options := core.NewClockOption()
|
||||
if err := c.ShouldBindJSON(options); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
options.CompleteDefaults()
|
||||
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.ClockAttack, options, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create http attack.
|
||||
// @Description Create http attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.HTTPAttackOption true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/http [post]
|
||||
func (s *HttpServer) createHTTPAttack(c *gin.Context) {
|
||||
attack := core.NewHTTPAttackOption()
|
||||
if err := c.ShouldBindJSON(attack); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
attackConfig, err := attack.PreProcess()
|
||||
if err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := s.chaos.ExecuteAttack(chaosd.HostAttack, attackConfig, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create JVM attack.
|
||||
// @Description Create JVM attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.JVMCommand true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/jvm [post]
|
||||
func (s *HttpServer) createJVMAttack(c *gin.Context) {
|
||||
options := core.NewJVMCommand()
|
||||
if err := c.ShouldBindJSON(options); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
options.CompleteDefaults()
|
||||
if err := options.Validate(); err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := s.chaos.ExecuteAttack(chaosd.JVMAttack, options, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create Kafka attack.
|
||||
// @Description Create Kafka attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.KafkaCommand true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/kafka [post]
|
||||
func (s *HttpServer) createKafkaAttack(c *gin.Context) {
|
||||
options := core.NewKafkaCommand()
|
||||
if err := c.ShouldBindJSON(options); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
options.CompleteDefaults()
|
||||
if err := options.Validate(); err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := s.chaos.ExecuteAttack(chaosd.KafkaAttack, options, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create VM attack.
|
||||
// @Description Create VM attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.VMOption true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/vm [post]
|
||||
func (s *HttpServer) createVMAttack(c *gin.Context) {
|
||||
options := core.NewVMOption()
|
||||
if err := c.ShouldBindJSON(options); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
options.CompleteDefaults()
|
||||
if err := options.Validate(); err != nil {
|
||||
err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed")
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := s.chaos.ExecuteAttack(chaosd.VMAttack, options, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create redis attack.
|
||||
// @Description Create redis attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.RedisCommand true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/redis [post]
|
||||
func (s *HttpServer) createRedisAttack(c *gin.Context) {
|
||||
attack := core.NewRedisCommand()
|
||||
if err := c.ShouldBindJSON(attack); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
attack.CompleteDefaults()
|
||||
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.RedisAttack, attack, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid))
|
||||
}
|
||||
|
||||
// @Summary Create user defined attack.
|
||||
// @Description Create user defined attack.
|
||||
// @Tags attack
|
||||
// @Produce json
|
||||
// @Param request body core.RedisCommand true "Request body"
|
||||
// @Success 200 {object} utils.Response
|
||||
// @Failure 400 {object} utils.APIError
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/user_defined [post]
|
||||
func (s *HttpServer) createUserDefinedAttack(c *gin.Context) {
|
||||
attack := core.NewUserDefinedOption()
|
||||
if err := c.ShouldBindJSON(attack); err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
attack.CompleteDefaults()
|
||||
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.UserDefinedAttack, attack, core.ServerMode)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
|
|
@ -209,7 +477,7 @@ func (s *httpServer) createDiskAttack(c *gin.Context) {
|
|||
// @Success 200 {object} utils.Response
|
||||
// @Failure 500 {object} utils.APIError
|
||||
// @Router /api/attack/{uid} [delete]
|
||||
func (s *httpServer) recoverAttack(c *gin.Context) {
|
||||
func (s *HttpServer) recoverAttack(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
err := s.chaos.RecoverAttack(uid)
|
||||
if err != nil {
|
||||
|
|
@ -221,6 +489,10 @@ func (s *httpServer) recoverAttack(c *gin.Context) {
|
|||
}
|
||||
|
||||
func handleError(c *gin.Context, err error) {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
_ = c.AbortWithError(http.StatusNotFound, utils.ErrNotFound.WrapWithNoMessage(err))
|
||||
return
|
||||
}
|
||||
if errorx.IsOfType(err, core.ErrAttackConfigValidation) {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, utils.ErrInvalidRequest.WrapWithNoMessage(err))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ type healthInfo struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s *httpServer) healthcheck(c *gin.Context) {
|
||||
func (s *HttpServer) healthcheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, healthInfo{Status: 0})
|
||||
}
|
||||
|
||||
func (s *httpServer) version(c *gin.Context) {
|
||||
func (s *HttpServer) version(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, version.Get())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ package server
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/zapr"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/chaos-mesh/chaos-mesh/pkg/chaosdaemon"
|
||||
|
|
@ -28,6 +33,7 @@ import (
|
|||
|
||||
var Module = fx.Options(
|
||||
fx.Provide(
|
||||
provideNIl,
|
||||
chaosd.NewServer,
|
||||
httpserver.NewServer,
|
||||
crclient.NewNodeCRClient,
|
||||
|
|
@ -36,3 +42,12 @@ var Module = fx.Options(
|
|||
scheduler.NewScheduler,
|
||||
),
|
||||
)
|
||||
|
||||
func provideNIl() (prometheus.Registerer, logr.Logger) {
|
||||
zapLogger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger := zapr.NewLogger(zapLogger)
|
||||
return nil, logger
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
var (
|
||||
ErrNS = errorx.NewNamespace("error.api")
|
||||
ErrAuth = ErrNS.NewType("auth")
|
||||
ErrOther = ErrNS.NewType("other")
|
||||
ErrInvalidRequest = ErrNS.NewType("invalid_request")
|
||||
ErrInternalServer = ErrNS.NewType("internal_server_error")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -17,9 +17,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
perr "github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/chaos-mesh/chaosd/pkg/core"
|
||||
"github.com/chaos-mesh/chaosd/pkg/store/dbstore"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !swagger_server
|
||||
// +build !swagger_server
|
||||
|
||||
package swaggerserver
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build swagger_server
|
||||
// +build swagger_server
|
||||
|
||||
package swaggerserver
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c Command) Unmarshal(val interface{}) *exec.Cmd {
|
||||
name, args := c.GetCmdArgs(val)
|
||||
return exec.Command(name, args...)
|
||||
}
|
||||
|
||||
func (c Command) UnmarshalWithCtx(ctx context.Context, val interface{}) *exec.Cmd {
|
||||
name, args := c.GetCmdArgs(val)
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
|
||||
func (c Command) GetCmdArgs(val interface{}) (string, []string) {
|
||||
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 c.Name, options
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2023 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -31,12 +31,15 @@ func SetRuntimeEnv() error {
|
|||
return err
|
||||
}
|
||||
|
||||
path := os.Getenv("PATH")
|
||||
bytemanHome := fmt.Sprintf("%s/tools/byteman", wd)
|
||||
err = os.Setenv("BYTEMAN_HOME", bytemanHome)
|
||||
if err != nil {
|
||||
return err
|
||||
bytemanHome := os.Getenv("BYTEMAN_HOME")
|
||||
if len(bytemanHome) == 0 {
|
||||
err = os.Setenv("BYTEMAN_HOME", fmt.Sprintf("%s/tools/byteman", wd))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
path := os.Getenv("PATH")
|
||||
err = os.Setenv("PATH", fmt.Sprintf("%s/tools:%s/bin:%s", wd, bytemanHome, path))
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2023 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Jeffail/tunny"
|
||||
"github.com/samber/lo"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
// CommandPools is a group of commands runner
|
||||
type CommandPools struct {
|
||||
cancel context.CancelFunc
|
||||
pools *tunny.Pool
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewCommandPools returns a new CommandPools
|
||||
func NewCommandPools(ctx context.Context, deadline *time.Time, size int) *CommandPools {
|
||||
var ctx2 context.Context
|
||||
var cancel context.CancelFunc
|
||||
if deadline != nil {
|
||||
ctx2, cancel = context.WithDeadline(ctx, *deadline)
|
||||
} else {
|
||||
ctx2, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
return &CommandPools{
|
||||
cancel: cancel,
|
||||
pools: tunny.NewFunc(size, func(payload interface{}) interface{} {
|
||||
cmdPayload, ok := payload.(lo.Tuple2[string, []string])
|
||||
if !ok {
|
||||
return mo.Err[[]byte](fmt.Errorf("payload is not CommandPayload"))
|
||||
}
|
||||
name, args := cmdPayload.Unpack()
|
||||
cmd := exec.CommandContext(ctx2, name, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return mo.Err[[]byte](fmt.Errorf("%s: %s", err, string(output)))
|
||||
}
|
||||
return mo.Ok[[]byte](output)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type CommandRunner struct {
|
||||
Name string
|
||||
Args []string
|
||||
|
||||
outputHandler func([]byte, error, chan interface{})
|
||||
outputChanel chan interface{}
|
||||
}
|
||||
|
||||
func NewCommandRunner(name string, args []string) *CommandRunner {
|
||||
return &CommandRunner{
|
||||
Name: name,
|
||||
Args: args,
|
||||
outputHandler: func(bytes []byte, err error, c chan interface{}) {},
|
||||
outputChanel: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CommandRunner) WithOutputHandler(
|
||||
handler func([]byte, error, chan interface{}),
|
||||
outputChanel chan interface{},
|
||||
) *CommandRunner {
|
||||
r.outputHandler = handler
|
||||
r.outputChanel = outputChanel
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *CommandPools) Process(name string, args []string) ([]byte, error) {
|
||||
result, ok := p.pools.Process(lo.Tuple2[string, []string]{
|
||||
A: name,
|
||||
B: args,
|
||||
}).(mo.Result[[]byte])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("payload is not Result[[]byte]")
|
||||
}
|
||||
return result.Get()
|
||||
}
|
||||
|
||||
// Start command async.
|
||||
func (p *CommandPools) Start(runner *CommandRunner) {
|
||||
p.wg.Add(1)
|
||||
go func() {
|
||||
output, err := p.Process(runner.Name, runner.Args)
|
||||
runner.outputHandler(output, err, runner.outputChanel)
|
||||
p.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *CommandPools) Wait() {
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
func (p *CommandPools) Close() {
|
||||
p.cancel()
|
||||
p.Wait()
|
||||
p.pools.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2023 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 (
|
||||
"context"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestCommandPools_Cancel(t *testing.T) {
|
||||
now := time.Now()
|
||||
cmdPools := NewCommandPools(context.Background(), nil, 1)
|
||||
var gErr []error
|
||||
runner := NewCommandRunner("sleep", []string{"10s"}).
|
||||
WithOutputHandler(func(output []byte, err error, _ chan interface{}) {
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
gErr = append(gErr, err)
|
||||
}
|
||||
log.Info(string(output))
|
||||
}, nil)
|
||||
cmdPools.Start(runner)
|
||||
cmdPools.Close()
|
||||
assert.Less(t, time.Since(now).Seconds(), 10.0)
|
||||
assert.Equal(t, 1, len(gErr))
|
||||
}
|
||||
|
||||
func TestCommandPools_Deadline(t *testing.T) {
|
||||
now := time.Now()
|
||||
deadline := time.Now().Add(time.Millisecond * 50)
|
||||
cmdPools := NewCommandPools(context.Background(), &deadline, 1)
|
||||
var gErr []error
|
||||
runner := NewCommandRunner("sleep", []string{"10s"}).
|
||||
WithOutputHandler(func(output []byte, err error, _ chan interface{}) {
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
gErr = append(gErr, err)
|
||||
}
|
||||
log.Info(string(output))
|
||||
}, nil)
|
||||
cmdPools.Start(runner)
|
||||
cmdPools.Wait()
|
||||
assert.Less(t, math.Abs(float64(time.Since(now).Milliseconds()-50)), 10.0)
|
||||
assert.Equal(t, 1, len(gErr))
|
||||
|
||||
}
|
||||
|
||||
func TestCommandPools_Normal(t *testing.T) {
|
||||
now := time.Now()
|
||||
cmdPools := NewCommandPools(context.Background(), nil, 1)
|
||||
var gErr []error
|
||||
runner := NewCommandRunner("sleep", []string{"1s"}).
|
||||
WithOutputHandler(func(output []byte, err error, _ chan interface{}) {
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
gErr = append(gErr, err)
|
||||
}
|
||||
log.Info(string(output))
|
||||
}, nil)
|
||||
cmdPools.Start(runner)
|
||||
cmdPools.Wait()
|
||||
assert.Less(t, time.Since(now).Seconds(), 2.0)
|
||||
assert.Equal(t, 0, len(gErr))
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ package utils
|
|||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
"github.com/pingcap/log"
|
||||
|
|
@ -23,12 +22,7 @@ import (
|
|||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
func CreateTempFile(path string) (string, error) {
|
||||
tempFile, err := ioutil.TempFile(path, "example")
|
||||
if err != nil {
|
||||
log.Error("unexpected err when open temp file", zap.Error(err))
|
||||
|
|
|
|||
|
|
@ -80,10 +80,6 @@ func TestSplitByteSize(t *testing.T) {
|
|||
BlockSize: "1M",
|
||||
Count: "0",
|
||||
},
|
||||
{
|
||||
BlockSize: "",
|
||||
Count: "",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -109,8 +105,8 @@ func TestSplitByteSize(t *testing.T) {
|
|||
BlockSize: "524288c",
|
||||
Count: "1",
|
||||
}, {
|
||||
BlockSize: "1",
|
||||
Count: "0c",
|
||||
BlockSize: "1M",
|
||||
Count: "0",
|
||||
}},
|
||||
wantErr: false,
|
||||
}, {
|
||||
|
|
@ -126,8 +122,8 @@ func TestSplitByteSize(t *testing.T) {
|
|||
BlockSize: "524288c",
|
||||
Count: "1",
|
||||
}, {
|
||||
BlockSize: "1",
|
||||
Count: "1c",
|
||||
BlockSize: "1c",
|
||||
Count: "1",
|
||||
}},
|
||||
wantErr: false,
|
||||
}, {
|
||||
|
|
@ -143,8 +139,8 @@ func TestSplitByteSize(t *testing.T) {
|
|||
BlockSize: "1M",
|
||||
Count: "2",
|
||||
}, {
|
||||
BlockSize: "1",
|
||||
Count: "1048576c",
|
||||
BlockSize: "1048576c",
|
||||
Count: "1",
|
||||
}},
|
||||
wantErr: false,
|
||||
}, {
|
||||
|
|
@ -160,8 +156,8 @@ func TestSplitByteSize(t *testing.T) {
|
|||
BlockSize: "1M",
|
||||
Count: "2",
|
||||
}, {
|
||||
BlockSize: "1",
|
||||
Count: "1048577c",
|
||||
BlockSize: "1048577c",
|
||||
Count: "1",
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ package utils
|
|||
|
||||
import (
|
||||
"math/rand"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/pingcap/log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
|
@ -28,3 +32,18 @@ func RandomStringWithCharset(length int) string {
|
|||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func ExecuteCmd(cmdStr string) (string, error) {
|
||||
log.Info("execute cmd", zap.String("cmd", cmdStr))
|
||||
cmd := exec.Command("bash", "-c", cmdStr)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(string(output), zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
if len(output) > 0 {
|
||||
log.Info("command output: "+string(output), zap.String("command", cmdStr))
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2022 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.
|
||||
|
||||
set -eu
|
||||
|
||||
cur=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
cd $cur
|
||||
|
||||
bin_path=../../../bin
|
||||
|
||||
chaos_test_file="/tmp/chaos-test"
|
||||
|
||||
echo "create file"
|
||||
${bin_path}/chaosd attack file create --file-name ${chaos_test_file} --uid 12345
|
||||
|
||||
if [[ ! (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} not exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${bin_path}/chaosd recover 12345
|
||||
|
||||
if [[ (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} exists after recover"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "create directory"
|
||||
${bin_path}/chaosd attack file create --dir-name ${chaos_test_file} --uid 12345
|
||||
|
||||
if [[ ! (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} not exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${bin_path}/chaosd recover 12345
|
||||
|
||||
if [[ (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} exists after recover"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "delete file"
|
||||
touch ${chaos_test_file}
|
||||
${bin_path}/chaosd attack file delete --dir-name ${chaos_test_file} --uid 12345
|
||||
|
||||
if [[ (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} exists after delete"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${bin_path}/chaosd recover 12345
|
||||
|
||||
if [[ ! (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} not exists after recover"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
echo "append file"
|
||||
touch ${chaos_test_file}
|
||||
|
||||
${bin_path}/chaosd attack file append --file-name ${chaos_test_file} --data "chaos-mesh" --count 5 --uid 12345
|
||||
|
||||
num=`cat ${chaos_test_file} | grep "chaos-mesh" | wc -l`
|
||||
if [[ ${num} -ne 5 ]]; then
|
||||
echo "append file failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${bin_path}/chaosd recover 12345
|
||||
num=`cat ${chaos_test_file} | grep "chaos-mesh" | wc -l`
|
||||
if [[ ${num} -ne 0 ]]; then
|
||||
echo "recover append file failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "modify file"
|
||||
${bin_path}/chaosd attack file modify --file-name ${chaos_test_file} --privilege 777 --uid 12345
|
||||
|
||||
privilege=`stat -c %a ${chaos_test_file}`
|
||||
if [[ ${privilege} -ne 777 ]]; then
|
||||
echo "modify file failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${bin_path}/chaosd recover 12345
|
||||
privilege=`stat -c %a ${chaos_test_file}`
|
||||
if [[ ${privilege} == 777 ]]; then
|
||||
echo "recover modify file failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "rename file"
|
||||
|
||||
${bin_path}/chaosd attack file rename --source-file ${chaos_test_file} --dest-file ${chaos_test_file}-bak --uid 12345
|
||||
if [[ (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} exists after rename"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! (-e ${chaos_test_file}-bak) ]]; then
|
||||
echo "${chaos_test_file}-bak not exists after rename"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${bin_path}/chaosd recover 12345
|
||||
|
||||
if [[ ! (-e ${chaos_test_file}) ]]; then
|
||||
echo "${chaos_test_file} not exists after recover"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ (-e ${chaos_test_file}-bak) ]]; then
|
||||
echo "${chaos_test_file}-bak exists after recover"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm ${chaos_test_file}
|
||||
exit 0
|
||||
|
|
@ -13,15 +13,28 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -u
|
||||
set -eu
|
||||
|
||||
cur=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
cd $cur
|
||||
|
||||
bin_path=../../../bin
|
||||
|
||||
echo "download && build && run Java example program"
|
||||
git clone https://github.com/WangXiangUSTC/byteman-example.git
|
||||
echo "download byteman example"
|
||||
if [[ ! (-e byteman-example) ]]; then
|
||||
git clone https://github.com/chaos-mesh/byteman-example.git
|
||||
fi
|
||||
|
||||
echo "download byteman && set environment variable"
|
||||
byteman_dir="byteman-chaos-mesh-download-v4.0.20-0.12"
|
||||
if [[ ! (-e ${byteman_dir}.tar.gz) ]]; then
|
||||
curl -fsSL -o ${byteman_dir}.tar.gz https://mirrors.chaos-mesh.org/${byteman_dir}.tar.gz
|
||||
tar zxvf ${byteman_dir}.tar.gz
|
||||
fi
|
||||
export BYTEMAN_HOME=$cur/${byteman_dir}
|
||||
export PATH=$PATH:${BYTEMAN_HOME}/bin
|
||||
|
||||
echo "build && run Java example program helloworld"
|
||||
cd byteman-example/example.helloworld
|
||||
javac HelloWorld/Main.java
|
||||
jar cfme HelloWorld.jar Manifest.txt HelloWorld.Main HelloWorld/Main.class
|
||||
|
|
@ -33,22 +46,68 @@ cat helloworld.log
|
|||
# TODO: get the PID more accurately
|
||||
pid=`pgrep -n java`
|
||||
|
||||
echo "download byteman && set environment variable"
|
||||
curl -fsSL -o chaosd-byteman-download.tar.gz https://mirrors.chaos-mesh.org/jvm/chaosd-byteman-download.tar.gz
|
||||
tar zxvf chaosd-byteman-download.tar.gz
|
||||
export BYTEMAN_HOME=$cur/chaosd-byteman-download
|
||||
export PATH=$PATH:${BYTEMAN_HOME}/bin
|
||||
|
||||
echo "run chaosd to inject failure into JVM, and check"
|
||||
$bin_path/chaosd attack jvm install --port 9288 --pid $pid
|
||||
|
||||
$bin_path/chaosd attack jvm submit return --class Main --method getnum --port 9288 --value 99999
|
||||
$bin_path/chaosd attack jvm return --class Main --method getnum --port 9288 --value 99999 --pid $pid
|
||||
sleep 1
|
||||
check_contains "99999" helloworld.log
|
||||
|
||||
$bin_path/chaosd attack jvm submit exception --class Main --method sayhello --port 9288 --exception 'java.io.IOException("BOOM")'
|
||||
$bin_path/chaosd attack jvm exception --class Main --method sayhello --port 9288 --exception 'java.io.IOException("BOOM")' --pid $pid
|
||||
sleep 1
|
||||
check_contains "BOOM" helloworld.log
|
||||
|
||||
kill $pid
|
||||
|
||||
# TODO: add test for latency, stress and gc
|
||||
|
||||
echo "download && run tidb"
|
||||
case $( uname -m ) in
|
||||
aarch64) ARCH=arm64;;
|
||||
arm64) ARCH=arm64;;
|
||||
*) ARCH=amd64;;
|
||||
esac
|
||||
tidb_dir="tidb-v5.3.0-linux-$ARCH"
|
||||
if [[ ! (-e ${tidb_dir}.tar.gz) ]]; then
|
||||
curl -fsSL -o ${tidb_dir}.tar.gz https://download.pingcap.org/${tidb_dir}.tar.gz
|
||||
tar zxvf ${tidb_dir}.tar.gz
|
||||
fi
|
||||
${tidb_dir}/bin/tidb-server -store mocktikv -P 4111 > tidb.log 2>&1 &
|
||||
sleep 5
|
||||
tidb_pid=`pgrep -n tidb-server`
|
||||
|
||||
echo "build && run Java example program mysqldemo"
|
||||
cd byteman-example/mysqldemo
|
||||
mvn -X package -Dmaven.test.skip=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true
|
||||
|
||||
export MYSQL_DSN=jdbc:"mysql://127.0.0.1:4111/test"
|
||||
export MYSQL_USER=root
|
||||
export MYSQL_CONNECTOR_VERSION=8
|
||||
mvn exec:java -Dexec.mainClass="com.mysqldemo.App" > mysqldemo.log 2>&1 &
|
||||
# make sure it works
|
||||
for (( i=0; i<=20; i++ ))
|
||||
do
|
||||
tail_log=`tail -1 mysqldemo.log`
|
||||
if [ "$tail_log" == "Server start!" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
cd -
|
||||
|
||||
# TODO: get the PID more accurately
|
||||
pid=`pgrep -n java`
|
||||
|
||||
echo "send request to mysqldemo, and can get result success"
|
||||
curl -X GET "http://127.0.0.1:8001/query?sql=SELECT%20*%20FROM%20mysql.user" > user_info.log
|
||||
check_contains "root" user_info.log
|
||||
|
||||
$bin_path/chaosd attack jvm mysql --database mysql --table user --port 9299 --exception "BOOM" --pid $pid
|
||||
sleep 1
|
||||
|
||||
echo "send request to mysqldemo, and will get a BOOM exception"
|
||||
curl -X GET "http://127.0.0.1:8001/query?sql=SELECT%20*%20FROM%20mysql.user" > user_info.log
|
||||
check_contains "BOOM" user_info.log
|
||||
|
||||
echo "clean"
|
||||
kill $pid
|
||||
kill $tidb_pid
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# 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.
|
||||
|
||||
# Generate script because certs expire in 1 year (365 days)
|
||||
|
||||
mkdir -p client server
|
||||
|
||||
# generate server certificate
|
||||
openssl req \
|
||||
-x509 \
|
||||
-newkey rsa:4096 \
|
||||
-keyout server/server_key.pem \
|
||||
-out server/server_cert.pem \
|
||||
-nodes \
|
||||
-days 365 \
|
||||
-subj "/CN=localhost/O=Client\ Certificate\ Demo"
|
||||
|
||||
# generate server-signed (valid) certificate
|
||||
openssl req \
|
||||
-newkey rsa:4096 \
|
||||
-keyout client/valid_key.pem \
|
||||
-out client/valid_csr.pem \
|
||||
-nodes \
|
||||
-days 365 \
|
||||
-subj "/CN=Valid"
|
||||
|
||||
# sign with server_cert.pem
|
||||
openssl x509 \
|
||||
-req \
|
||||
-in client/valid_csr.pem \
|
||||
-CA server/server_cert.pem \
|
||||
-CAkey server/server_key.pem \
|
||||
-out client/valid_cert.pem \
|
||||
-set_serial 01 \
|
||||
-days 365
|
||||
|
||||
# generate self-signed (invalid) certificate
|
||||
openssl req \
|
||||
-newkey rsa:4096 \
|
||||
-keyout client/invalid_key.pem \
|
||||
-out client/invalid_csr.pem \
|
||||
-nodes \
|
||||
-days 365 \
|
||||
-subj "/CN=Invalid"
|
||||
|
||||
# sign with invalid_csr.pem
|
||||
openssl x509 \
|
||||
-req \
|
||||
-in client/invalid_csr.pem \
|
||||
-signkey client/invalid_key.pem \
|
||||
-out client/invalid_cert.pem \
|
||||
-days 365
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue