Compare commits

...

81 Commits
v1.0.1 ... main

Author SHA1 Message Date
Yue Yang bc039aa483
fix: install gcc (#261) 2024-03-25 14:35:53 +08:00
Yue Yang ddcd31e68e
fix: install make (#260) 2024-03-25 14:25:45 +08:00
Yue Yang 62d1ceb50f
chore: deprecate centos (#259) 2024-03-25 14:09:56 +08:00
Yue Yang 00c8a256ef
chore: use setup-python (#258) 2024-03-25 11:14:13 +08:00
Yue Yang 9efc3a19d8
chore: upgrade to centos8 (#257) 2024-03-25 11:03:34 +08:00
Yue Yang 5cb6b0e45f
chore: upgrade go (#256) 2024-03-25 10:50:43 +08:00
Yue Yang 5c7014f45d
chore: upgrade actions (#255) 2024-03-25 10:13:27 +08:00
yuriscott c31ee824d7
fix: device name always eth0 in iptable network attack partition (#252)
Co-authored-by: yuri.yin <yincc8@chinatelecom.cn>
Co-authored-by: Yue Yang <g1enyy0ung@gmail.com>
2024-03-13 10:07:09 +08:00
Andrewmatilde 19a157239e
Fix issue about disk attack cannot work well in chaos-mesh:physical machine chaos (#236)
* add default value of PayloadProcessNum&FillByFAllocate

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

* recover unit-test

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

* recover unit-test

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

* Enable fill | write in dir.

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

* Enable fill | write in dir.

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

* Enable fill | write in dir.

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

* fix lint

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

* fix log

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

* ignore some unhandled errors & fix unexported returned value in exported function

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

* fix deprecated function

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

* complete async disk attack in server side

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

* better input args type in command pool

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

* complete test for command pool

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

* add license

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

* add license

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

* fix comment

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

* add output channel to pools

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

* manually test & fix disk attack

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

* fix ut in disk attack

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

* fix Boilerplate header

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

---------

Signed-off-by: andrewmatilde <davis6813585853062@outlook.com>
Co-authored-by: Cwen Yin <cwenyin0@gmail.com>
2023-02-08 17:01:30 +08:00
Cwen Yin e7715f72b9
Revert "fix hostname params and add full-disable params (#226) (#227)" (#235)
This reverts commit 6c378aaed6.
2023-02-08 16:10:04 +08:00
Nikita Savchenko d32144829e
add reboot command (#119) (#228)
Signed-off-by: Nikita Savchenko <nikisavchenko@ozon.ru>
Co-authored-by: Nikita Savchenko <nikisavchenko@ozon.ru>
2023-02-06 17:54:40 +08:00
Nikita Savchenko 6c378aaed6
fix hostname params and add full-disable params (#226) (#227)
Signed-off-by: Nikita Savchenko <nikisavchenko@ozon.ru>
Co-authored-by: Nikita Savchenko <nikisavchenko@ozon.ru>
2023-02-03 09:54:23 +08:00
Cwen Yin 0fa7a8eebe
Kafka attack: support more auth mechanisms (#233)
* kafka attack: support more auth mechanism

Signed-off-by: cwen0 <cwenyin0@gmail.com>

* fix typo

Signed-off-by: cwen0 <cwenyin0@gmail.com>

* format header

Signed-off-by: cwen0 <cwenyin0@gmail.com>

* format header

Signed-off-by: cwen0 <cwenyin0@gmail.com>

---------

Signed-off-by: cwen0 <cwenyin0@gmail.com>
2023-02-01 19:15:19 +08:00
FingerLeader 3a6efb7db2
fix ignored json tag (#232)
Signed-off-by: Ningxuan Wang <wanxfinger@gmail.com>
2023-01-31 20:36:24 +08:00
Cwen Yin 31631c3178
kafka io attack: add username and password flags (#231)
* add username and password flags

Signed-off-by: cwen0 <cwenyin0@gmail.com>

* fix ci

Signed-off-by: cwen0 <cwenyin0@gmail.com>

---------

Signed-off-by: cwen0 <cwenyin0@gmail.com>
2023-01-29 18:07:55 +08:00
Cwen Yin a9c05406d2
Download tproxy to tools directory (#225)
* download tproxy

Signed-off-by: cwen0 <cwenyin0@gmail.com>

* fix makeflie

Signed-off-by: cwen0 <cwenyin0@gmail.com>

Signed-off-by: cwen0 <cwenyin0@gmail.com>
2023-01-06 12:15:37 +08:00
Cwen Yin 55b454f281
upgrade byteman-helper to v4.0.20-0.12 (#224)
Signed-off-by: cwen0 <cwenyin0@gmail.com>

Signed-off-by: cwen0 <cwenyin0@gmail.com>
2022-12-14 10:39:11 +08:00
Cwen Yin f6d3a9fb1e
Rename "FillByFAllocate" to "FillByFallocate" (#217)
Signed-off-by: cwen0 <cwenyin0@gmail.com>

Signed-off-by: cwen0 <cwenyin0@gmail.com>
2022-09-20 17:58:15 +08:00
Ningxuan Wang 98f3ca40c1
Store iptables when creating network attack (#215)
* store iptables

Signed-off-by: root <root@localhost.localdomain>

* make check

Signed-off-by: root <root@localhost.localdomain>

* update network test

Signed-off-by: root <root@localhost.localdomain>

* move NeedApplyTC() at the begin of applying to the middle

Signed-off-by: root <root@localhost.localdomain>

* remove NeedApplyIptables

Signed-off-by: root <root@localhost.localdomain>

Signed-off-by: root <root@localhost.localdomain>
Co-authored-by: root <root@localhost.localdomain>
2022-09-15 00:39:31 +08:00
Cwen Yin 5ecc6c265b
fix partition not work (#214)
Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>
2022-09-10 16:24:01 +08:00
Cwen Yin 8f5b737967
Fix wrong gitVersion info and github action failed on arm (#211)
* Fix some minor bugs

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

* add some comments

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>
2022-08-17 14:46:51 +08:00
Cwen Yin 25e098e4a7
Fix some minor bugs (#209)
* upgrade byteman-helper to v4.0.18-0.11

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

* fix some minor bugs

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

* update git action

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>
2022-08-16 17:11:12 +08:00
Cwen Yin f5b6b8a4bc
upgrade byteman-helper to v4.0.18-0.11 (#206)
Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>
2022-08-16 14:46:15 +08:00
YangKeao 1f4e6bedbc
add arm64 master build (#201)
Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>
Co-authored-by: Andrewmatilde <davis6813585853062@outlook.com>
Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-08-16 13:30:51 +08:00
Cwen Yin 2c429506d2
Fix: gitVersion is invalid when executing `chaosd version` (#205)
Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>
2022-08-16 12:50:51 +08:00
Ningxuan Wang 4cbe994b2c
update chaos-mesh pkg to latest (#204)
* update chaos-mesh pkg

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* go mod tidy

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-08-16 11:59:38 +08:00
Cwen Yin e771b9d10c
Make byteman plugins runnable on jdk8 (#202)
Signed-off-by: Cwen Yin <cwenyin0@gmail.com>

Signed-off-by: Cwen Yin <cwenyin0@gmail.com>
Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-08-15 10:36:50 +08:00
YangKeao 9f6a5b83f3
download memStress and stress-ng according to the architecture (#198)
* download memStress and stress-ng according to the architecture

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* use the same tag for stress-ng and memStress

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>
2022-08-13 15:16:49 +08:00
Ningxuan Wang ad91098215
Add flag `accept-tcp-flag` to network delay (#195)
* add flag  to network delay

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* update test

Signed-off-by: root <root@localhost.localdomain>

* change the logic of adding chains

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

Co-authored-by: root <root@localhost.localdomain>
2022-08-05 16:24:06 +08:00
Ningxuan Wang 91bb4c99e5
add Type to IPSet (#194)
Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-08-03 16:10:02 +08:00
Ningxuan Wang 6118c19a89
Add device to ToTc() (#192)
Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-07-29 16:15:12 +08:00
Ningxuan Wang e2b71ee788
Use shirou/gopsutils to get process full name (#189)
* use shiruo/gopsutils to get process full name

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add error handling

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-07-28 13:59:10 +08:00
Ningxuan Wang 41bf629270
Add json tag for kafka attack (#188)
* add json tag for kafka attack

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-07-25 14:37:10 +08:00
Ningxuan Wang 74388bdc4c
Merge update into main (#184)
* Update chaos-mesh version to 20220511035234-10df92fcde77. (#161)

* update chaos-mesh version

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

* update go.mod&go.sum

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

* update go version of workflow

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

* fix lint

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

* install go imports in ci

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

* replace go get with go install in makefile

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

* make fmt

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

* add ci to branch update

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

* fix comment

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

* revert changes: clock.go

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

* update to golang 1.18

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

* fix bug :Provide server don't have (prometheus.Registerer, logr.Logger)

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

* make fmt

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

* make fmt

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

* support HTTP Attack on chaosd (#165)

* update chaos-mesh version

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

* update go.mod&go.sum

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

* update go version of workflow

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

* fix lint

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

* install go imports in ci

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

* replace go get with go install in makefile

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

* make fmt

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

* add ci to branch update

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

* fix comment

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

* revert changes: clock.go

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

* add part of http support

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

* update to golang 1.18

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

* fix bug :Provide server don't have (prometheus.Registerer, logr.Logger)

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

* complete part of http attack

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

* make fmt

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

* make fmt

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

* chaos

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

* fix some bug

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

* add recover

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

* try fix recover bug

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

* try fix recover bug

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

* sig kill -> sig term

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

* fix lint

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

* add server side

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

* make logger instead of fmt.Printf

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

* make logger instead of fmt.Printf

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

* fix typo Mathc -> Match

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

* fix type AttackHTTP -> HTTPAttack

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

* rules -> rule

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

* tion

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

* errors.Errorf(string) -> errors.New

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

* fix typo:space

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

* fix typo:proxy_ports

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

* add comment for pkg/server/chaosd/http.go:L97

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

* fix description

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

* abort HTTP Package -> HTTP connection

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

* New type

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

* minor fix BUG in http chaos config action (#182)

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

* HTTP attack: support send HTTP request (#177)

* add http request

Signed-off-by: xiang <xiang13225080@163.com>

* update description

Signed-off-by: xiang <xiang13225080@163.com>

* fix stress ci

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* use memStress to implement mem attack

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* update mem attack test

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* update ci

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* fix tcsRequest

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* fix ci

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* fix ci

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* bump chaos-mesh pkg

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

Co-authored-by: Andrewmatilde <davis6813585853062@outlook.com>
Co-authored-by: WangXiang <xiang13225080@163.com>
2022-07-20 11:23:08 +08:00
WangXiang 499c6652fe
network: generate network traffic by using iperf (#174)
* add network flood

Signed-off-by: xiang <xiang13225080@163.com>

* address comment

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-07-14 10:53:06 +08:00
xixi adea8d5043
kafka attack: fill action support change retention bytes (#181)
* kafka attack: fill action support change retention bytes

Signed-off-by: xixi <i@hexilee.me>

* modify retentionBytes successfully

Signed-off-by: xixi <i@hexilee.me>

* support recover retention bytes

Signed-off-by: xixi <i@hexilee.me>
2022-07-13 22:55:06 +08:00
WangXiang b003e0ad4a
support user defined attack (#170)
* support user-defined attack

Signed-off-by: xiang <xiang13225080@163.com>

* add integration test

Signed-off-by: xiang <xiang13225080@163.com>

* support http server

Signed-off-by: xiang <xiang13225080@163.com>

* format

Signed-off-by: xiang <xiang13225080@163.com>

* minor refine

Signed-off-by: xiang <xiang13225080@163.com>

* address comment

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-06-20 17:06:37 +08:00
Ningxuan Wang e4cabb4419
RadisChaos: Add Cache Expiration (#175)
* add redis cache expiration

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* modify default value of key

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-06-19 10:58:35 +08:00
Ningxuan Wang f1df7d3f15
redisChaos: add cache limit (#163)
* add cache limit

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add a comment

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* redis cache limit add a flag percent

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* fix ci

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-06-06 10:48:29 +08:00
Ningxuan Wang 3cb3a334f6
Add flag `redis-path` to sentinel restart (#172)
Signed-off-by: FingerLeader <wanxfinger@gmail.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-06-02 19:50:28 +08:00
WangXiang 46bbee557b
stress attack: ignore error when stress-ng process is not exists (#173)
Signed-off-by: xiang <xiang13225080@163.com>
2022-06-02 15:06:28 +08:00
Ningxuan Wang 62d573059c
Support Redis cache penetration (#159)
* use pipe to implement cache penetration

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* make check

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add validation for cache penetration

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* make check

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-05-26 12:50:47 +08:00
xixi c9dcbc36f7
implement kafka chaos (#154)
* add kafka chaos

Signed-off-by: xixi <i@hexilee.me>

* register all options

Signed-off-by: xixi <i@hexilee.me>

* move some global flags to local

Signed-off-by: xixi <i@hexilee.me>

* set default rps to 1 << 32

Signed-off-by: xixi <i@hexilee.me>

* complete flood

Signed-off-by: xixi <i@hexilee.me>

* add --no-silent option

Signed-off-by: xixi <i@hexilee.me>

* complete fill subcommand

Signed-off-by: xixi <i@hexilee.me>

* add partition option

Signed-off-by: xixi <i@hexilee.me>

* fix headers

Signed-off-by: xixi <i@hexilee.me>

* add config file

Signed-off-by: xixi <i@hexilee.me>

* complete io inject

Signed-off-by: xixi <i@hexilee.me>

* mod tidy

Signed-off-by: xixi <i@hexilee.me>

* handle errors

Signed-off-by: xixi <i@hexilee.me>

* fix executable mode

Signed-off-by: xixi <i@hexilee.me>

* fix io fault

Signed-off-by: xixi <i@hexilee.me>

* modify perm of dir

Signed-off-by: xixi <i@hexilee.me>

* change default threads and tps

Signed-off-by: xixi <i@hexilee.me>

* support SASL

Signed-off-by: xixi <i@hexilee.me>

* add MaxBytes for fill

Signed-off-by: xixi <i@hexilee.me>

* fix headers

Signed-off-by: xixi <i@hexilee.me>

* fix headers

Signed-off-by: xixi <i@hexilee.me>

* remove NoSilent flag

Signed-off-by: xixi <i@hexilee.me>

* return error in attack

Signed-off-by: xixi <i@hexilee.me>

* split Validate of KafkaCommand

Signed-off-by: xixi <i@hexilee.me>

* remove empty line

Signed-off-by: xixi <i@hexilee.me>

* use writer instead of connection

Signed-off-by: xixi <i@hexilee.me>

* use conn instead of writer

Signed-off-by: xixi <i@hexilee.me>

* fix bugs

Signed-off-by: xixi <i@hexilee.me>

* fix description

Signed-off-by: xixi <i@hexilee.me>

* fix some bugs

Signed-off-by: xixi <i@hexilee.me>

* return err when reading dir fails

Signed-off-by: xixi <i@hexilee.me>

* save origin file mode

Signed-off-by: xixi <i@hexilee.me>

* add unit-test for attackKafkaIO

Signed-off-by: xixi <i@hexilee.me>

* rename newFs to originFs in kafka_test

Signed-off-by: xixi <i@hexilee.me>

* remove rps limit

Signed-off-by: xixi <i@hexilee.me>

Co-authored-by: WangXiang <xiang13225080@163.com>
2022-05-24 17:14:47 +08:00
WangXiang bc4ca8cb07
support vm attack (#158)
Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-05-24 14:10:46 +08:00
YangKeao 03541d3687
add ci test on arm (#168)
* add test on arm

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* download arm tidb

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>
2022-05-20 13:50:43 +08:00
Ningxuan Wang bf14130d80
redisChaos: support sentinel stop/restart (#152)
* sentinel stop attack draft

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* sentinel stop and restart attack draft

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* remove flag DB

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add flag flush-config

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* make boilerplate

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* make check

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* make check

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add flag redis-path

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-05-05 18:20:55 +08:00
WangXiang 38e871fbcc
JVM attack: add json label for MySQL config (#157)
Signed-off-by: xiang <xiang13225080@163.com>
2022-04-28 11:30:52 +08:00
WangXiang 84daf15fff
support file attack (#123)
* add dile

Signed-off-by: xiang <xiang13225080@163.com>

* add command

Signed-off-by: xiang <xiang13225080@163.com>

* upadte comment

Signed-off-by: xiang <xiang13225080@163.com>

* fix recover

Signed-off-by: xiang <xiang13225080@163.com>

* add file tools

Signed-off-by: xiang <xiang13225080@163.com>

* update file attack 1

Signed-off-by: xiang <xiang13225080@163.com>

* update file attack 2

Signed-off-by: xiang <xiang13225080@163.com>

* add integration test

Signed-off-by: xiang <xiang13225080@163.com>

* add missed file

Signed-off-by: xiang <xiang13225080@163.com>

* minor update on env

Signed-off-by: xiang <xiang13225080@163.com>

* add missed file

Signed-off-by: xiang <xiang13225080@163.com>

* update year in license

Signed-off-by: xiang <xiang13225080@163.com>

* add json label

Signed-off-by: xiang <xiang13225080@163.com>

* minor update

Signed-off-by: xiang <xiang13225080@163.com>

* replace data in file by sed

Signed-off-by: xiang <xiang13225080@163.com>

* add recover for replace

Signed-off-by: xiang <xiang13225080@163.com>

* add missed file

Signed-off-by: xiang <xiang13225080@163.com>

* format

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-04-20 10:28:03 +08:00
Ningxuan Wang 2d07a779fe
bump gopsutil to 3.21.11 (#155)
Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-04-20 10:14:03 +08:00
WangXiang 10652a81a7
JVM attack: minor fix on memory (#153)
* minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* remove log

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-04-15 11:52:35 +08:00
WangXiang e5e2c4c0ef
support "both" direction for network partition (#136)
* support both direction for partition

Signed-off-by: xiang <xiang13225080@163.com>

* add unit test

Signed-off-by: xiang <xiang13225080@163.com>

* update error

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-04-13 11:50:36 +08:00
YangKeao 83893b4fea
ignore process not found while recovering the stress (#145)
* fix stress recover

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix error assert

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>
2022-03-28 11:06:28 +08:00
xixi d93fc1f06c
support auto completion (#139)
* add subcommand completion

Signed-off-by: xixi <i@hexilee.me>

* add uid completion for recover

Signed-off-by: xixi <i@hexilee.me>

* fix uid completion for recover

Signed-off-by: xixi <i@hexilee.me>

* handle errors

Signed-off-by: xixi <i@hexilee.me>

* bump cobra to 1.4.0

Signed-off-by: xixi <i@hexilee.me>

* go mod tidy

Signed-off-by: xixi <i@hexilee.me>

* resolve comments

Signed-off-by: xixi <i@hexilee.me>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-03-23 18:28:34 +08:00
Ningxuan Wang 81a670e824
chore: update help information for recover-cmd (#140)
Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-03-21 11:46:32 +08:00
WangXiang bc1b29af2d
Makefile: support build and download releated tools (#135)
* support build && download tools in Makefile

Signed-off-by: xiang <xiang13225080@163.com>

* update readme

Signed-off-by: xiang <xiang13225080@163.com>

* update release ci(test)

Signed-off-by: xiang <xiang13225080@163.com>

* update yum package repository

Signed-off-by: xiang <xiang13225080@163.com>

* update ci

Signed-off-by: xiang <xiang13225080@163.com>

* update readme

Signed-off-by: xiang <xiang13225080@163.com>

* put document at the header

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2022-03-07 10:59:48 +08:00
FingerLeader 81dc95afc2
Network: support down NIC (#130)
* add new feature nic down

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add nic down

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add nic down validate

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* use SchedulerConfig

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add support fot sub-interface

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add test for nic down

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add test for nic down

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* use bash to implement nic down

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* edit some details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* replace cmd.Start with cmd.CombinedOutput

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* remove nohup

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* remove useless log.Error

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add some comments

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add some comments

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add some comments

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-03-02 18:41:46 +08:00
WangXiang 4e99e3cc8e
update roadmap (#134)
* update roadmap

Signed-off-by: xiang <xiang13225080@163.com>

* update status

Signed-off-by: xiang <xiang13225080@163.com>
2022-02-21 14:00:05 +08:00
WangXiang 90f7f9b29e
JVM: support inject fault to MySQL Java client (#106)
* support inject fault to MySQL Java client

Signed-off-by: xiang <xiang13225080@163.com>

* remove useless code

Signed-off-by: xiang <xiang13225080@163.com>

* fix unit test

Signed-off-by: xiang <xiang13225080@163.com>

* minor update

Signed-off-by: xiang <xiang13225080@163.com>

* add unit test && minor update

Signed-off-by: xiang <xiang13225080@163.com>

* update stress and gc

Signed-off-by: xiang <xiang13225080@163.com>

* submit helper

Signed-off-by: xiang <xiang13225080@163.com>

* update release ci && minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* add integration test && minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* add retry

Signed-off-by: xiang <xiang13225080@163.com>

* update comment && add config validate

Signed-off-by: xiang <xiang13225080@163.com>

* update comment

Signed-off-by: xiang <xiang13225080@163.com>
2022-02-21 10:21:41 +08:00
WangXiang 2368d63509
JVM: refine with byteman-helper (#128)
* support inject fault to MySQL Java client

Signed-off-by: xiang <xiang13225080@163.com>

* remove useless code

Signed-off-by: xiang <xiang13225080@163.com>

* fix unit test

Signed-off-by: xiang <xiang13225080@163.com>

* minor update

Signed-off-by: xiang <xiang13225080@163.com>

* add unit test && minor update

Signed-off-by: xiang <xiang13225080@163.com>

* update stress and gc

Signed-off-by: xiang <xiang13225080@163.com>

* submit helper

Signed-off-by: xiang <xiang13225080@163.com>

* update release ci && minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* add integration test && minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* add retry

Signed-off-by: xiang <xiang13225080@163.com>

* remove mysql

Signed-off-by: xiang <xiang13225080@163.com>

* minor update

Signed-off-by: xiang <xiang13225080@163.com>
2022-02-15 12:17:39 +08:00
Andrewmatilde ed3ab388be
Enable fill | write in dir. (#126)
* add default value of PayloadProcessNum&FillByFAllocate

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

* recover unit-test

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

* recover unit-test

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

* Enable fill | write in dir.

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

* Enable fill | write in dir.

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

* Enable fill | write in dir.

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

* fix lint

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

* fix log

Signed-off-by: andrewmatilde <davis6813585853062@outlook.com>
2022-01-17 13:05:44 +08:00
FingerLeader fafc3eb912
add parameter recover-cmd for process kill (#122)
* add parameter recover-cmd for process kill

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add parameter recover-cmd for process kill

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* resolve confilct

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* add test for recover-cmd

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* fix some detail

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* modify details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* modify details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>

* modify details

Signed-off-by: FingerLeader <wanxfinger@gmail.com>
2022-01-12 17:27:43 +08:00
Andrewmatilde 22a8a7cfc9
add default value of PayloadProcessNum&FillByFAllocate (#121)
* add default value of PayloadProcessNum&FillByFAllocate

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

* recover unit-test

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

* recover unit-test

Signed-off-by: andrewmatilde <davis6813585853062@outlook.com>
2022-01-11 19:29:44 +08:00
WangXiang 7103539ffe
Network: set default value for duplicate and corrupt action && fix partition (#115)
* add default value

Signed-off-by: xiang <xiang13225080@163.com>

* set default value for corrupt

Signed-off-by: xiang <xiang13225080@163.com>

* fix partition

Signed-off-by: xiang <xiang13225080@163.com>
2021-12-22 13:35:47 +08:00
Siyu Chen 2fe4276f24
chore: divide http and https server (#113)
* chore: devide http and https server

Signed-off-by: SiyuChen <ryougi201@gmail.com>

* fix: integration test

Signed-off-by: SiyuChen <ryougi201@gmail.com>

* fix: ci integration test

Signed-off-by: SiyuChen <ryougi201@gmail.com>

* fix: ci integration test

Signed-off-by: SiyuChen <ryougi201@gmail.com>

* fix: generate cert

Signed-off-by: SiyuChen <ryougi201@gmail.com>

* fix: verify certs

Signed-off-by: SiyuChen <ryougi201@gmail.com>

* fix: register mtls middleware

Signed-off-by: SiyuChen <ryougi201@gmail.com>
2021-12-16 13:32:35 +08:00
WangXiang 54a0b0b898
some minor update (#100)
* add json label && set deault value for server mode && support jvm in server mode

Signed-off-by: xiang <xiang13225080@163.com>

* add default value for clock

Signed-off-by: xiang <xiang13225080@163.com>

* update config name && update description

Signed-off-by: xiang <xiang13225080@163.com>

* minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* update json field of disk

Signed-off-by: xiang <xiang13225080@163.com>

Co-authored-by: Ti Chi Robot <ti-community-prow-bot@tidb.io>
2021-11-29 14:25:52 +08:00
WangXiang 9d861673eb
support build in arm64 platform (#105)
Signed-off-by: xiang <xiang13225080@163.com>
2021-11-29 11:29:52 +08:00
WangXiang 50bfa4ca39
network attack: support bandwidth limit (#91)
* support limit network bandwidth

Signed-off-by: xiang <xiang13225080@163.com>
2021-11-17 15:16:05 +08:00
WangXiang 9e6d9eedce
JVM attack: combine install and submit into one (#85)
Signed-off-by: xiang <xiang13225080@163.com>
2021-11-09 20:23:10 +08:00
WangXiang 8a90c5a85b
support oom on heap and stack (#79)
Signed-off-by: xiang <xiang13225080@163.com>
2021-10-11 14:24:48 +08:00
WangXiang 693b6f7cc8
support partition in network attack (#65)
* add partition

Signed-off-by: xiang <xiang13225080@163.com>

* minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* minor update

Signed-off-by: xiang <xiang13225080@163.com>

* format code

Signed-off-by: xiang <xiang13225080@163.com>

* format code

Signed-off-by: xiang <xiang13225080@163.com>

* add direction config

Signed-off-by: xiang <xiang13225080@163.com>

* update pkg

Signed-off-by: xiang <xiang13225080@163.com>

* update go version

Signed-off-by: xiang <xiang13225080@163.com>

* test

Signed-off-by: xiang <xiang13225080@163.com>

* minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* test

Signed-off-by: xiang <xiang13225080@163.com>

* upgrade goimports

Signed-off-by: xiang <xiang13225080@163.com>

* minor update

Signed-off-by: xiang <xiang13225080@163.com>

* update go.mod

Signed-off-by: xiang <xiang13225080@163.com>

* address comment

Signed-off-by: xiang <xiang13225080@163.com>

* minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* minor fix

Signed-off-by: xiang <xiang13225080@163.com>

* update go.mod

Signed-off-by: xiang <xiang13225080@163.com>

* update function name

Signed-off-by: xiang <xiang13225080@163.com>
2021-10-09 14:51:25 +08:00
Pablo Caderno 684f94e20d
return 404 code when delete not exist experiment (#96)
Signed-off-by: Pablo Caderno <kaderno@gmail.com>
2021-10-08 13:55:08 +08:00
Shivansh Saini d795cf65e5
feat: support ssl + client ssl authentication (#69)
Signed-off-by: Shivansh Saini <shivanshs9@gmail.com>
2021-09-30 17:08:45 +08:00
Andrewmatilde c1b722e87e
Add clock attack support. (#90)
Signed-off-by: andrewmatilde <davis6813585853062@outlook.com>
2021-09-30 16:22:20 +08:00
yujinqiu c54f9a5d4d
Fix signal error (#92)
Co-authored-by: yujinqiu <yujinqiu@gmail.com>
2021-09-16 15:18:52 +08:00
WangXiang 69a5aa40f3
supoort all signal for process (#87)
Signed-off-by: xiang <xiang13225080@163.com>
2021-09-15 14:38:14 +08:00
WangXiang 2dcda95c83
add roadmap (#80)
* add roadmap

Signed-off-by: xiang <xiang13225080@163.com>

* add time skew

Signed-off-by: xiang <xiang13225080@163.com>

* add jvm attack

Signed-off-by: xiang <xiang13225080@163.com>
2021-09-15 14:23:33 +08:00
Andrewmatilde 352ba5a47a
factor disk attack&add schedule support for disk read&write (#77)
* fix bugs in SplitBytesByProcessNum & add overwrite control

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

* delete overwrite control

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

* add recover for disk&&delete fill destory

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

* patch

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

* factor disk attack

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

* add schedule support for disk read&write

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

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

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

* fix some mistakes in log

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

* fix unsupportted unit `c` in fallocate.

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

* roll back scheduler

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

* add comment

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

* save some indentation

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

* add a comment

Signed-off-by: andrewmatilde <davis6813585853062@outlook.com>
2021-09-08 18:26:53 +08:00
CWen c2fddc2942
chore: delete useless binary (#88)
Signed-off-by: cwen0 <cwenyin0@gmail.com>
2021-08-26 14:25:36 +08:00
WangXiang d077fdaf12
support set uid when injectting fault (#84)
Signed-off-by: xiang <xiang13225080@163.com>
2021-08-17 14:54:01 +08:00
WangXiang cbc5801ef2
some minor update (#76)
* remove workers in mem stress

Signed-off-by: xiang <xiang13225080@163.com>

* fix search

Signed-off-by: xiang <xiang13225080@163.com>

* fix test

Signed-off-by: xiang <xiang13225080@163.com>

* address comment

Signed-off-by: xiang <xiang13225080@163.com>
2021-08-02 14:38:30 +08:00
Potato f8e6aa3600
fix wrong comments (#83)
Signed-off-by: LebronAl <TXYPotato@gmail.com>
2021-07-28 10:53:08 +08:00
116 changed files with 10039 additions and 2332 deletions

View File

@ -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

26
.github/workflows/ci_skip.yml vendored Normal file
View File

@ -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."

View File

@ -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

View File

@ -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

11
.gitignore vendored
View File

@ -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

View File

@ -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
View File

@ -2,23 +2,31 @@
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](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:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/chaos-mesh/chaosd)

16
ROADMAP.md Normal file
View File

@ -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.

View File

@ -13,7 +13,11 @@
package attack
import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
"github.com/chaos-mesh/chaosd/pkg/core"
)
func NewAttackCommand() *cobra.Command {
cmd := &cobra.Command{
@ -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".`)
}

68
cmd/attack/clock.go Normal file
View File

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

View File

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

View File

@ -1,323 +0,0 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package attack
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"github.com/chaos-mesh/chaosd/cmd/server"
"github.com/chaos-mesh/chaosd/pkg/core"
"github.com/chaos-mesh/chaosd/pkg/server/chaosd"
"github.com/chaos-mesh/chaosd/pkg/utils"
)
type diskTest struct {
name string
option *core.DiskOption
wantErr bool
}
func TestServer_DiskFill(t *testing.T) {
fxtest.New(
t,
server.Module,
fx.Provide(func() []diskTest {
return []diskTest{
{
name: "0",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: "1024M",
Path: "./temp",
FillByFallocate: true,
PayloadProcessNum: 1,
},
wantErr: false,
}, {
name: "1",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
Size: "24MB",
Path: "./temp",
FillByFallocate: false,
PayloadProcessNum: 1,
},
wantErr: false,
},
}
}),
fx.Invoke(func(s *chaosd.Server, tests []diskTest) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := s.ExecuteAttack(chaosd.DiskAttack, tt.option, core.CommandMode)
if (err != nil) != tt.wantErr {
t.Errorf("DiskFill() error = %v, wantErr %v", err, tt.wantErr)
return
}
stat, err := os.Stat(tt.option.Path)
if err != nil {
t.Errorf("unexpected err %v when stat temp file", err)
return
}
size, _ := utils.ParseUnit(tt.option.Size)
if stat.Size() != int64(size) {
t.Errorf("DiskFill() size %v, expect %d", stat.Size(), size)
return
}
os.Remove(tt.option.Path)
})
}
}),
)
}
func TestServer_DiskPayload(t *testing.T) {
fxtest.New(
t,
server.Module,
fx.Provide(func() []diskTest {
return []diskTest{
{
name: "0",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskWritePayloadAction,
Kind: core.DiskAttack,
},
Size: "24M",
Path: "./temp",
PayloadProcessNum: 1,
},
wantErr: false,
}, {
name: "1",
option: &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskReadPayloadAction,
Kind: core.DiskAttack,
},
Size: "24M",
Path: "./temp",
PayloadProcessNum: 1,
},
wantErr: false,
},
}
}),
fx.Invoke(func(s *chaosd.Server, tests []diskTest) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := s.ExecuteAttack(chaosd.DiskAttack, &core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
Action: core.DiskFillAction,
Kind: core.DiskAttack,
},
PayloadProcessNum: 1,
Size: tt.option.Size,
Path: "./temp",
FillByFallocate: true,
}, core.CommandMode)
if err != nil {
t.Error(err)
return
}
_, err = s.ExecuteAttack(chaosd.DiskAttack, tt.option, core.CommandMode)
if (err != nil) != tt.wantErr {
t.Errorf("DiskPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
os.Remove(tt.option.Path)
})
}
}),
)
}
type writeArgs struct {
Size string
Path string
PayloadProcessNum uint8
}
func writeArgsToDiskOption(args writeArgs) core.DiskOption {
return core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
SchedulerConfig: core.SchedulerConfig{},
Action: core.DiskWritePayloadAction,
Kind: "",
},
Size: args.Size,
Path: args.Path,
Percent: "",
FillByFallocate: false,
PayloadProcessNum: args.PayloadProcessNum,
}
}
func writeArgsAttack(args writeArgs) error {
opt := writeArgsToDiskOption(args)
return chaosd.DiskAttack.Attack(&opt, chaosd.Environment{})
}
func TestNewDiskWritePayloadCommand(t *testing.T) {
var opt core.DiskOption
var err error
opt = writeArgsToDiskOption(writeArgs{
Size: "",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "one of percent and size must not be empty, DiskOption : write-payload")
opt = writeArgsToDiskOption(writeArgs{
Size: "1Ms",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unknown units of size : 1Ms, DiskOption : write-payload")
opt = writeArgsToDiskOption(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unsupport process num : 0, DiskOption : write-payload")
opt = writeArgsToDiskOption(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 1,
})
err = opt.Validate()
assert.NoError(t, err)
assert.NoError(t, writeArgsAttack(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 1,
}))
assert.NoError(t, writeArgsAttack(writeArgs{
Size: "0",
Path: "",
PayloadProcessNum: 255,
}))
assert.NoError(t, writeArgsAttack(writeArgs{
Size: "1",
Path: "",
PayloadProcessNum: 2,
}))
assert.Error(t, writeArgsAttack(writeArgs{
Size: "1",
Path: "&^%$#@#$%^&*(",
PayloadProcessNum: 5,
}))
}
type readArgs struct {
Size string
Path string
PayloadProcessNum uint8
}
func readArgsToDiskOption(args readArgs) core.DiskOption {
return core.DiskOption{
CommonAttackConfig: core.CommonAttackConfig{
SchedulerConfig: core.SchedulerConfig{},
Action: core.DiskReadPayloadAction,
Kind: "",
},
Size: args.Size,
Path: args.Path,
Percent: "",
FillByFallocate: false,
PayloadProcessNum: args.PayloadProcessNum,
}
}
func readArgsAttack(args readArgs) error {
opt := readArgsToDiskOption(args)
return chaosd.DiskAttack.Attack(&opt, chaosd.Environment{})
}
func TestNewDiskReadPayloadCommand(t *testing.T) {
var opt core.DiskOption
var err error
opt = readArgsToDiskOption(readArgs{
Size: "",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "one of percent and size must not be empty, DiskOption : read-payload")
opt = readArgsToDiskOption(readArgs{
Size: "1Ms",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unknown units of size : 1Ms, DiskOption : read-payload")
opt = readArgsToDiskOption(readArgs{
Size: "0",
Path: "",
PayloadProcessNum: 0,
})
err = opt.Validate()
assert.EqualError(t, err, "unsupport process num : 0, DiskOption : read-payload")
opt = readArgsToDiskOption(readArgs{
Size: "0",
Path: "",
PayloadProcessNum: 1,
})
err = opt.Validate()
assert.NoError(t, err)
assert.NoError(t, readArgsAttack(readArgs{
Size: "0",
Path: "/dev/zero",
PayloadProcessNum: 1,
}))
assert.NoError(t, readArgsAttack(readArgs{
Size: "1",
Path: "/dev/zero",
PayloadProcessNum: 2,
}))
assert.NoError(t, readArgsAttack(readArgs{
Size: "100GB",
Path: "/dev/zero",
PayloadProcessNum: 1,
}))
}

172
cmd/attack/file.go Normal file
View File

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

View File

@ -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()
},
}

143
cmd/attack/http.go Normal file
View File

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

View File

@ -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()

132
cmd/attack/kafka.go Normal file
View File

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

View File

@ -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
}

View File

@ -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
}

157
cmd/attack/redis.go Normal file
View File

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

View File

@ -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.")

View File

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

58
cmd/attack/vm.go Normal file
View File

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

View File

@ -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)
}
}
},
}
}

BIN
cmd/main

Binary file not shown.

View File

@ -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

46
cmd/recover/completion.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}

View File

@ -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
}
}
}

View File

@ -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, "+

View File

@ -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
View File

@ -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

1820
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
}

122
pkg/core/clock.go Normal file
View File

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

View File

@ -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
}

View File

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

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

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

View File

@ -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
}

View File

@ -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,
}
}

167
pkg/core/file.go Normal file
View File

@ -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,
},
}
}

View File

@ -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,
},
}
}

176
pkg/core/http.go Normal file
View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,
},
},
"",
},

176
pkg/core/kafka.go Normal file
View File

@ -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),
}
}

View File

@ -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),
},
}
}

View File

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

156
pkg/core/network_test.go Normal file
View File

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

View File

@ -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

90
pkg/core/redis.go Normal file
View File

@ -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,
},
}
}

View File

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

View File

@ -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)

58
pkg/core/user_defined.go Normal file
View File

@ -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,
},
}
}

46
pkg/core/vm.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}

View File

@ -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
}

View File

@ -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))
}
}()

View File

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

View File

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

View File

@ -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
}

View File

@ -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
}

View File

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

278
pkg/server/chaosd/file.go Normal file
View File

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

View File

@ -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
}

View File

@ -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
}

195
pkg/server/chaosd/http.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

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

421
pkg/server/chaosd/kafka.go Normal file
View File

@ -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
}

View File

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

View File

@ -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
}

View File

@ -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

View File

@ -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

230
pkg/server/chaosd/redis.go Normal file
View File

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

View File

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

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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
}

66
pkg/server/chaosd/vm.go Normal file
View File

@ -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
}

View File

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

View File

@ -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)

View File

@ -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 {

View File

@ -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())
}

View File

@ -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
}

View File

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

View File

@ -0,0 +1,47 @@
// Copyright 2021 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"os/exec"
"time"
"github.com/pingcap/log"
"go.uber.org/zap"
)
func ExecWithDeadline(t <-chan time.Time, cmd *exec.Cmd) error {
done := make(chan error, 1)
var output []byte
var err error
go func() {
output, err = cmd.CombinedOutput()
done <- err
}()
select {
case <-t:
if err := cmd.Process.Kill(); err != nil {
log.Error("failed to kill process: ", zap.Error(err))
return err
}
case err := <-done:
if err != nil {
log.Error(err.Error()+string(output), zap.Error(err))
return err
}
log.Info(string(output))
}
return nil
}

View File

@ -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"

View File

@ -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

View File

@ -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

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

@ -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
}

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

@ -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())
})
}
}

View File

@ -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

117
pkg/utils/pool.go Normal file
View File

@ -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()
}

81
pkg/utils/pool_test.go Normal file
View File

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

View File

@ -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))

View File

@ -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,
},

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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