├── .gitattributes ├── README.md ├── .github ├── issue_template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── log ├── log_test.go └── log.go ├── hack ├── update-gofmt.sh ├── update-imports.sh ├── verify-imports.sh ├── init.sh └── verify-gofmt.sh ├── spec ├── constants.go ├── executor.go ├── channel.go ├── response.go └── model.go ├── util ├── util_test.go ├── signal.go ├── archive_tar.go ├── slice.go ├── slice_test.go ├── log.go ├── spec.go └── util.go ├── go.mod ├── Makefile ├── go.sum ├── licenserc.toml ├── channel ├── local_mock.go ├── channel.go ├── nsexec.go ├── local_windows.go └── local_unixs.go └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chaosblade-spec-go 2 | chaosblade specification of chaos experiments 3 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Issue Description 8 | 9 | Type: *bug report* or *feature request* 10 | 11 | ### Describe what happened (or what feature you want) 12 | 13 | 14 | ### Describe what you expected to happen 15 | 16 | 17 | ### How to reproduce it (as minimally and precisely as possible) 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 23 | ### Tell us your environment 24 | 25 | 26 | ### Anything else we need to know? 27 | 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ### Describe what this PR does / why we need it 7 | 8 | 9 | ### Does this pull request fix one issue? 10 | 11 | 12 | 13 | ### Describe how you did it 14 | 15 | 16 | ### Describe how to verify it 17 | 18 | 19 | ### Special notes for reviews 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | .vagrant 10 | releases 11 | tmp 12 | .idea/ 13 | 14 | # Architecture specific extensions/prefixes 15 | trace.out 16 | *.out 17 | .DS_Store 18 | 19 | *.cgo1.go 20 | *.cgo2.c 21 | _cgo_defun.c 22 | _cgo_gotypes.go 23 | _cgo_export.* 24 | 25 | _testmain.go 26 | 27 | *.exe 28 | *.test 29 | *.prof 30 | profile.cov 31 | coverage.html 32 | 33 | # Emacs backup files 34 | *~ 35 | 36 | # ctags files 37 | tags 38 | 39 | # Project specific 40 | /chaosblade 41 | /blade 42 | /bin/java-agent*.jar 43 | /hack/chaosblade* 44 | /hack/lib/** 45 | /chaosblade.dat 46 | target 47 | coverage.txt 48 | vendor 49 | 50 | # Website 51 | site-build 52 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | ) 23 | 24 | func Test_info(t *testing.T) { 25 | Infof(context.WithValue(context.Background(), "uid", "123"), "cpu used: %d", 10) 26 | } 27 | -------------------------------------------------------------------------------- /hack/update-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2025 The ChaosBlade Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | source "$(dirname "$0")/init.sh" 22 | go install mvdan.cc/gofumpt@latest 23 | 24 | # Serially process each file to avoid concurrent write issues 25 | for f in $(git_find); do 26 | gofumpt -w "$f" 27 | done 28 | 29 | -------------------------------------------------------------------------------- /hack/update-imports.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2025 The ChaosBlade Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | source "$(dirname "$0")/init.sh" 22 | go install golang.org/x/tools/cmd/goimports@latest 23 | 24 | # Serially process each file to avoid concurrent write issues 25 | for f in $(git_find); do 26 | goimports -w -local github.com/chaosblade-io/chaosblade-spec-go -srcdir "$(dirname "$f")" "$f" 27 | done 28 | 29 | -------------------------------------------------------------------------------- /hack/verify-imports.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2025 The ChaosBlade Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | source "$(dirname "$0")/init.sh" 22 | go install golang.org/x/tools/cmd/goimports@latest 23 | 24 | diff=$(git_find | xargs goimports -l -local github.com/chaosblade-io/chaosblade-spec-go 2>&1) || true 25 | if [[ -n "${diff}" ]]; then 26 | echo "The following files have incorrect import order. Please run ./hack/update-imports.sh to fix them:" >&2 27 | echo "${diff}" >&2 28 | exit 1 29 | fi 30 | -------------------------------------------------------------------------------- /spec/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package spec 18 | 19 | const ( 20 | LocalChannel = "local" 21 | NSExecBin = "nsexec" 22 | ChaosOsBin = "chaos_os" 23 | ChaosMiddlewareBin = "chaos_middleware" 24 | ChaosCloudBin = "chaos_cloud" 25 | Destroy = "destroy" 26 | Create = "create" 27 | True = "true" 28 | False = "false" 29 | BinPath = "bin" 30 | ExperimentId = "experiment" 31 | DefaultCGroupPath = "/sys/fs/cgroup/" 32 | Uid = "uid" 33 | YamlPathEnv = "YAML_PATH" 34 | ) 35 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "runtime" 21 | "testing" 22 | ) 23 | 24 | func TestIsExist_ForMountPoint(t *testing.T) { 25 | // Skip this test on Windows as it tests Unix-specific mount points 26 | if runtime.GOOS == "windows" { 27 | t.Skip("Skipping mount point test on Windows") 28 | } 29 | 30 | tests := []struct { 31 | device string 32 | want bool 33 | }{ 34 | {"/", true}, 35 | {"/dev", true}, 36 | {"devfs", false}, 37 | } 38 | for _, tt := range tests { 39 | if got := IsExist(tt.device); got != tt.want { 40 | t.Errorf("unexpected result: %t, expected: %t", got, tt.want) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The ChaosBlade Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module github.com/chaosblade-io/chaosblade-spec-go 16 | 17 | go 1.25 18 | 19 | require ( 20 | github.com/shirou/gopsutil v3.21.11+incompatible 21 | github.com/sirupsen/logrus v1.4.2 22 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 23 | gopkg.in/yaml.v2 v2.2.8 24 | ) 25 | 26 | require ( 27 | github.com/BurntSushi/toml v0.3.1 // indirect 28 | github.com/go-ole/go-ole v1.2.6 // indirect 29 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect 30 | github.com/tklauser/go-sysconf v0.3.9 // indirect 31 | github.com/tklauser/numcpus v0.3.0 // indirect 32 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 33 | golang.org/x/sys v0.1.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /hack/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2025 The ChaosBlade Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | function git_find() { 18 | # Similar to find but faster and easier to understand. We want to include 19 | # modified and untracked files because this might be running against code 20 | # which is not tracked by git yet. 21 | git ls-files -cmo --exclude-standard \ 22 | ':!:vendor/*' `# catches vendor/...` \ 23 | ':!:*/vendor/*' `# catches any subdir/vendor/...` \ 24 | ':!:third_party/*' `# catches third_party/...` \ 25 | ':!:*/third_party/*' `# catches third_party/...` \ 26 | ':!:*/testdata/*' `# catches any subdir/testdata/...` \ 27 | ':(glob)**/*.go' \ 28 | "$@" 29 | } -------------------------------------------------------------------------------- /hack/verify-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2025 The ChaosBlade Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | source "$(dirname "$0")/init.sh" 22 | go install mvdan.cc/gofumpt@latest 23 | 24 | # gofmt exits with non-zero exit code if it finds a problem unrelated to 25 | # formatting (e.g., a file does not parse correctly). Without "|| true" this 26 | # would have led to no useful error message from gofmt, because the script would 27 | # have failed before getting to the "echo" in the block below. 28 | diff=$(git_find | xargs gofumpt -d 2>&1) || true 29 | if [[ -n "${diff}" ]]; then 30 | echo "${diff}" >&2 31 | echo >&2 32 | echo "Run ./hack/update-gofmt.sh" >&2 33 | exit 1 34 | fi -------------------------------------------------------------------------------- /util/signal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "os" 21 | "os/signal" 22 | "runtime" 23 | "syscall" 24 | 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | type ShutdownHook interface { 29 | Shutdown() error 30 | } 31 | 32 | func Hold(hooks ...ShutdownHook) { 33 | sig := make(chan os.Signal, 1) 34 | signal.Notify(sig, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 35 | buf := make([]byte, 1<<10) 36 | for { 37 | switch <-sig { 38 | case syscall.SIGINT, syscall.SIGTERM: 39 | logrus.Warningln("received SIGINT/SIGTERM, exit") 40 | for _, hook := range hooks { 41 | hook.Shutdown() 42 | } 43 | return 44 | case syscall.SIGQUIT: 45 | for _, hook := range hooks { 46 | hook.Shutdown() 47 | } 48 | len := runtime.Stack(buf, true) 49 | logrus.Warningf("received SIGQUIT\n%s\n", buf[:len]) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 The ChaosBlade Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | .PHONY: help format verify 16 | 17 | help: 18 | @echo "Available make targets:" 19 | @echo " format Run update-gofmt.sh and update-imports.sh to automatically fix Go code formatting and import order." 20 | @echo " verify Run verify-gofmt.sh and verify-imports.sh to check Go code formatting and import order." 21 | @echo " license-check Check for proper license headers in source files." 22 | @echo " help Show this help message." 23 | 24 | format: license-format 25 | @echo "Running goimports and gofumpt to format Go code..." 26 | @./hack/update-imports.sh 27 | @./hack/update-gofmt.sh 28 | 29 | verify: 30 | @echo "Verifying Go code formatting and import order..." 31 | @./hack/verify-gofmt.sh 32 | @./hack/verify-imports.sh 33 | 34 | .PHONY: license-check 35 | license-check: 36 | @echo "Checking license headers..." 37 | docker run -it --rm -v $(shell pwd):/github/workspace ghcr.io/korandoru/hawkeye check 38 | 39 | .PHONY: license-format 40 | license-format: 41 | @echo "Formatting license headers..." 42 | docker run -it --rm -v $(shell pwd):/github/workspace ghcr.io/korandoru/hawkeye format -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 The ChaosBlade Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: CI 16 | 17 | on: 18 | push: 19 | branches: [ main, master ] 20 | pull_request: 21 | branches: [ main, master ] 22 | 23 | jobs: 24 | test: 25 | name: Test and Build 26 | runs-on: ${{ matrix.os }} 27 | 28 | strategy: 29 | matrix: 30 | os: [ ubuntu-latest, windows-latest, macos-latest ] 31 | go-version: [ '1.25' ] 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | 37 | - name: Set up Go 38 | uses: actions/setup-go@v4 39 | with: 40 | go-version: ${{ matrix.go-version }} 41 | cache: true 42 | 43 | - name: Verify dependencies 44 | run: go mod verify 45 | 46 | - name: Download dependencies 47 | run: go mod download 48 | 49 | - name: Run tests 50 | run: go test -v ./... 51 | 52 | - name: Build project 53 | run: go build ./... 54 | 55 | - name: Check for race conditions 56 | run: go test -race ./... 57 | 58 | - name: Run vet 59 | run: go vet ./... 60 | 61 | - name: Run code style and import order verification 62 | run: make verify 63 | 64 | - name: Check License Header 65 | uses: korandoru/hawkeye@v6 66 | -------------------------------------------------------------------------------- /util/archive_tar.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "archive/tar" 21 | "io" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | ) 26 | 27 | func ArchiveTar(file string, writer *tar.Writer) error { 28 | return filepath.Walk(file, func(path string, fileInfo os.FileInfo, err error) error { 29 | if fileInfo == nil { 30 | return err 31 | } 32 | if fileInfo.IsDir() { 33 | if path == path { 34 | return nil 35 | } 36 | header, err := tar.FileInfoHeader(fileInfo, "") 37 | if err != nil { 38 | return err 39 | } 40 | header.Name = filepath.Join(path, strings.TrimPrefix(path, path)) 41 | if err = writer.WriteHeader(header); err != nil { 42 | return err 43 | } 44 | os.Mkdir(strings.TrimPrefix(path, fileInfo.Name()), os.ModeDir) 45 | return ArchiveTar(path, writer) 46 | } 47 | return func(originFile, path string, fileInfo os.FileInfo, writer *tar.Writer) error { 48 | if file, err := os.Open(path); err != nil { 49 | return err 50 | } else { 51 | if header, err := tar.FileInfoHeader(fileInfo, ""); err != nil { 52 | return err 53 | } else { 54 | 55 | index := strings.LastIndex(originFile, "/") 56 | header.Name = strings.ReplaceAll(path, originFile[0:index+1], "") 57 | 58 | if err := writer.WriteHeader(header); err != nil { 59 | return err 60 | } 61 | 62 | if _, err = io.Copy(writer, file); err != nil { 63 | return err 64 | } 65 | } 66 | } 67 | return nil 68 | }(file, path, fileInfo, writer) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /spec/executor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package spec 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strings" 23 | ) 24 | 25 | const ( 26 | DestroyKey = "suid" 27 | ) 28 | 29 | // ExpModel is the experiment data object 30 | type ExpModel struct { 31 | // Target is experiment target 32 | Target string `json:"target,omitempty"` 33 | 34 | // Scope is the experiment scope 35 | Scope string `json:"scope,omitempty"` 36 | 37 | // ActionName is the experiment action FlagName, for example delay 38 | ActionName string `json:"action,omitempty"` 39 | 40 | // ActionFlags is the experiment action flags, for example time and offset 41 | ActionFlags map[string]string `json:"flags,omitempty"` 42 | 43 | // Programs 44 | ActionPrograms []string `json:"programs,omitempty"` 45 | 46 | // Categories 47 | ActionCategories []string `json:"categories,omitempty"` 48 | 49 | ActionProcessHang bool `yaml:"actionProcessHang"` 50 | } 51 | 52 | // ExpExecutor defines the ExpExecutor interface 53 | type Executor interface { 54 | // Name is used to identify the ExpExecutor 55 | Name() string 56 | 57 | // Exec is used to execute the experiment 58 | Exec(uid string, ctx context.Context, model *ExpModel) *Response 59 | 60 | // SetChannel 61 | SetChannel(channel Channel) 62 | } 63 | 64 | func (exp *ExpModel) GetFlags() string { 65 | flags := make([]string, 0) 66 | for k, v := range exp.ActionFlags { 67 | if v == "" { 68 | continue 69 | } 70 | flags = append(flags, fmt.Sprintf("--%s %s", k, v)) 71 | } 72 | return strings.Join(flags, " ") 73 | } 74 | 75 | const UnknownUid = "unknown" 76 | 77 | func SetDestroyFlag(ctx context.Context, suid string) context.Context { 78 | return context.WithValue(ctx, DestroyKey, suid) 79 | } 80 | 81 | // IsDestroy command 82 | func IsDestroy(ctx context.Context) (string, bool) { 83 | suid := ctx.Value(DestroyKey) 84 | if suid == nil { 85 | return "", false 86 | } 87 | return suid.(string), true 88 | } 89 | -------------------------------------------------------------------------------- /spec/channel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package spec 18 | 19 | import ( 20 | "context" 21 | ) 22 | 23 | // Channel is an interface for command invocation 24 | type Channel interface { 25 | // channel name unique 26 | Name() string 27 | 28 | // Run script with args and returns response that wraps the result 29 | Run(ctx context.Context, script, args string) *Response 30 | 31 | // GetScriptPath return the script path 32 | GetScriptPath() string 33 | 34 | // GetPidsByProcessCmdName returns the matched process other than the current process by the program command 35 | GetPidsByProcessCmdName(processName string, ctx context.Context) ([]string, error) 36 | 37 | // GetPidsByProcessName returns the matched process other than the current process by the process keyword 38 | GetPidsByProcessName(processName string, ctx context.Context) ([]string, error) 39 | 40 | // GetPsArgs returns the ps command output format 41 | GetPsArgs(ctx context.Context) string 42 | 43 | // isAlpinePlatform returns true if the os version is alpine. 44 | // If the /etc/os-release file doesn't exist, the function returns false. 45 | IsAlpinePlatform(ctx context.Context) bool 46 | 47 | // IsAllCommandsAvailable returns nil,true if all commands exist 48 | IsAllCommandsAvailable(ctx context.Context, commandNames []string) (*Response, bool) 49 | 50 | // IsCommandAvailable returns true if the command exists 51 | IsCommandAvailable(ctx context.Context, commandName string) bool 52 | 53 | // ProcessExists returns true if the pid exists, otherwise return false. 54 | ProcessExists(pid string) (bool, error) 55 | 56 | // GetPidUser returns the process user by pid 57 | GetPidUser(pid string) (string, error) 58 | 59 | // GetPidsByLocalPorts returns the process ids using the ports 60 | GetPidsByLocalPorts(ctx context.Context, localPorts []string) ([]string, error) 61 | 62 | // GetPidsByLocalPort returns the process pid corresponding to the port 63 | GetPidsByLocalPort(ctx context.Context, localPort string) ([]string, error) 64 | } 65 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "runtime" 23 | 24 | "github.com/sirupsen/logrus" 25 | 26 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 27 | ) 28 | 29 | func Panicf(ctx context.Context, format string, a ...interface{}) { 30 | uid := ctx.Value(spec.Uid) 31 | logrus.WithFields(logrus.Fields{ 32 | "uid": uid, 33 | "location": GetRunFuncLocation(), 34 | }).Panicf("%s", fmt.Sprintf(format, a...)) 35 | } 36 | 37 | func Fatalf(ctx context.Context, format string, a ...interface{}) { 38 | uid := ctx.Value(spec.Uid) 39 | logrus.WithFields(logrus.Fields{ 40 | "uid": uid, 41 | "location": GetRunFuncLocation(), 42 | }).Fatalf("%s", fmt.Sprintf(format, a...)) 43 | } 44 | 45 | func Errorf(ctx context.Context, format string, a ...interface{}) { 46 | uid := ctx.Value(spec.Uid) 47 | logrus.WithFields(logrus.Fields{ 48 | "uid": uid, 49 | "location": GetRunFuncLocation(), 50 | }).Errorf("%s", fmt.Sprintf(format, a...)) 51 | } 52 | 53 | func Warnf(ctx context.Context, format string, a ...interface{}) { 54 | uid := ctx.Value(spec.Uid) 55 | logrus.WithFields(logrus.Fields{ 56 | "uid": uid, 57 | "location": GetRunFuncLocation(), 58 | }).Warnf("%s", fmt.Sprintf(format, a...)) 59 | } 60 | 61 | func Infof(ctx context.Context, format string, a ...interface{}) { 62 | uid := ctx.Value(spec.Uid) 63 | logrus.WithFields(logrus.Fields{ 64 | "uid": uid, 65 | "location": GetRunFuncLocation(), 66 | }).Infof("%s", fmt.Sprintf(format, a...)) 67 | } 68 | 69 | func Debugf(ctx context.Context, format string, a ...interface{}) { 70 | uid := ctx.Value(spec.Uid) 71 | logrus.WithFields(logrus.Fields{ 72 | "uid": uid, 73 | "location": GetRunFuncLocation(), 74 | }).Debugf("%s", fmt.Sprintf(format, a...)) 75 | } 76 | 77 | func Tracef(ctx context.Context, format string, a ...interface{}) { 78 | uid := ctx.Value(spec.Uid) 79 | logrus.WithFields(logrus.Fields{ 80 | "uid": uid, 81 | "location": GetRunFuncLocation(), 82 | }).Tracef("%s", fmt.Sprintf(format, a...)) 83 | } 84 | 85 | func GetRunFuncLocation() string { 86 | _, file, line, _ := runtime.Caller(2) 87 | return fmt.Sprintf("%s:%d", file, line) 88 | } 89 | -------------------------------------------------------------------------------- /util/slice.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "errors" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 25 | ) 26 | 27 | // Remove the item by the index 28 | func Remove(items []string, idx int) []string { 29 | length := len(items) 30 | items[length-1], items[idx] = items[idx], items[length-1] 31 | return items[:length-1] 32 | } 33 | 34 | func RemoveDuplicates(items []string) []string { 35 | result := make([]string, 0) 36 | cache := map[string]struct{}{} 37 | for _, i := range items { 38 | if _, ok := cache[i]; !ok { 39 | cache[i] = struct{}{} 40 | result = append(result, i) 41 | } 42 | } 43 | return result 44 | } 45 | 46 | // ParseIntegerListToStringSlice func parses the multiple integer values to string slice. 47 | // Support the below formats: 0 | 0,1 | 0,2,3 | 0-3 | 0,2-4 | 0,1,3-5 48 | // For example, the flag value is 0,2-3, the func returns []string{"0", "2", "3"} 49 | func ParseIntegerListToStringSlice(flagName, flagValue string) ([]string, error) { 50 | values := make([]string, 0) 51 | commaParts := strings.Split(flagValue, ",") 52 | for _, part := range commaParts { 53 | value := strings.TrimSpace(part) 54 | if value == "" { 55 | continue 56 | } 57 | if !strings.Contains(value, "-") { 58 | _, err := strconv.Atoi(value) 59 | if err != nil { 60 | return values, errors.New(spec.ParameterIllegal.Sprintf(flagName, flagValue, err)) 61 | } 62 | values = append(values, value) 63 | continue 64 | } 65 | ranges := strings.Split(value, "-") 66 | if len(ranges) != 2 { 67 | return values, errors.New(spec.ParameterIllegal.Sprintf(flagName, flagValue, 68 | "Does not conform to the data format, a connector is required")) 69 | } 70 | startIndex, err := strconv.Atoi(strings.TrimSpace(ranges[0])) 71 | if err != nil { 72 | return values, errors.New(spec.ParameterIllegal.Sprintf(flagName, flagValue, err)) 73 | } 74 | endIndex, err := strconv.Atoi(strings.TrimSpace(ranges[1])) 75 | if err != nil { 76 | return values, errors.New(spec.ParameterIllegal.Sprintf(flagName, flagValue, err)) 77 | } 78 | for i := startIndex; i <= endIndex; i++ { 79 | values = append(values, strconv.Itoa(i)) 80 | } 81 | } 82 | return values, nil 83 | } 84 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 6 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 8 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= 12 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 13 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 14 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 15 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 17 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 18 | github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= 19 | github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= 20 | github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= 21 | github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= 22 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 23 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 24 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 28 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 32 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 33 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 34 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 The ChaosBlade Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Base directory for the whole execution. 16 | # All relative paths is based on this path. 17 | # default: current working directory 18 | baseDir = "." 19 | 20 | headerPath = "Apache-2.0.txt" 21 | 22 | # On enabled, check the license header matches exactly with whitespace. 23 | # Otherwise, strip the header in one line and check. 24 | # default: true 25 | strictCheck = true 26 | 27 | # Whether you use the default excludes. Check Default.EXCLUDES for the completed list. 28 | # To suppress part of excludes in the list, declare exact the same pattern in `includes` list. 29 | # default: true 30 | useDefaultExcludes = true 31 | 32 | excludes = [ 33 | "*.txt", 34 | ] 35 | 36 | # The supported patterns of includes and excludes follow gitignore pattern format, plus that: 37 | # 1. `includes` does not support `!` 38 | # 2. backslash does not escape letter 39 | # 3. whitespaces and `#` are normal since we configure line by line 40 | # See also https://git-scm.com/docs/gitignore#_pattern_format 41 | 42 | # Keywords that should occur in the header, case-insensitive. 43 | # default: ["copyright"] 44 | keywords = ["copyright", ] 45 | 46 | # Whether you use the default mapping. Check DocumentType.defaultMapping() for the completed list. 47 | # default: true 48 | useDefaultMapping = true 49 | 50 | # Properties to fulfill the template. 51 | # For a defined key-value pair, you can use {{props["key"]}} in the header template, which will be 52 | # substituted with the corresponding value. 53 | [properties] 54 | inceptionYear = 2025 55 | copyrightOwner = "The ChaosBlade Authors" 56 | 57 | # There are also preset attributes that can be used in the header template (no need to surround them with `props[]`).: 58 | # * 'attrs.filename' is the current file name, like: pom.xml. 59 | 60 | # Options to configure Git features. 61 | [git] 62 | # If enabled, do not process files that are ignored by Git; possible value: ['auto', 'enable', 'disable'] 63 | # 'auto' means this feature tries to be enabled with: 64 | # * gix - if `basedir` is in a Git repository. 65 | # * ignore crate's gitignore rules - if `basedir` is not in a Git repository. 66 | # 'enable' means always enabled with gix; failed if it is impossible. 67 | # default: 'auto' 68 | ignore = 'auto' 69 | # If enabled, populate file attrs determinated by Git; possible value: ['auto', 'enable', 'disable'] 70 | # Attributes contains: 71 | # * 'attrs.git_file_created_year' 72 | # * 'attrs.git_file_modified_year' 73 | # 'auto' means this feature tries to be enabled with: 74 | # * gix - if `basedir` is in a Git repository. 75 | # 'enable' means always enabled with gix; failed if it is impossible. 76 | # default: 'disable' 77 | attrs = 'disable' 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 The ChaosBlade Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release 16 | 17 | on: 18 | push: 19 | tags: 20 | - 'v*' 21 | 22 | jobs: 23 | release: 24 | name: Create Release 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v4 35 | with: 36 | go-version: '1.25' 37 | cache: true 38 | 39 | - name: Verify dependencies 40 | run: go mod verify 41 | 42 | - name: Download dependencies 43 | run: go mod download 44 | 45 | - name: Run tests 46 | run: go test -v ./... 47 | 48 | - name: Run vet 49 | run: go vet ./... 50 | 51 | - name: Check for race conditions 52 | run: go test -race ./... 53 | 54 | - name: Generate changelog 55 | id: changelog 56 | run: | 57 | # Get previous tag 58 | PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "") 59 | 60 | if [ -z "$PREVIOUS_TAG" ]; then 61 | echo "This is the first release" 62 | cat > changelog.md << 'EOF' 63 | ## Changelog 64 | 65 | This is the first release of chaosblade-spec-go. 66 | 67 | ### Features 68 | - Chaos experiment specification definition 69 | - Support for multiple operating systems (Linux, Windows, macOS) 70 | - Unified experiment execution interface 71 | - Basic components including logging and utility functions 72 | EOF 73 | else 74 | echo "Generating changelog from $PREVIOUS_TAG to $GITHUB_REF_NAME" 75 | cat > changelog.md << EOF 76 | ## Changelog 77 | 78 | ### From $PREVIOUS_TAG to $GITHUB_REF_NAME 79 | 80 | \`\`\` 81 | $(git log --pretty=format:'- %s (%h)' $PREVIOUS_TAG..HEAD) 82 | \`\`\` 83 | EOF 84 | fi 85 | 86 | echo "changelog<> $GITHUB_OUTPUT 87 | cat changelog.md >> $GITHUB_OUTPUT 88 | echo "EOF" >> $GITHUB_OUTPUT 89 | 90 | - name: Create Release 91 | uses: actions/create-release@v1 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | with: 95 | tag_name: ${{ github.ref_name }} 96 | release_name: Release ${{ github.ref_name }} 97 | body: ${{ steps.changelog.outputs.changelog }} 98 | draft: false 99 | prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} -------------------------------------------------------------------------------- /util/slice_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | func TestRemove(t *testing.T) { 25 | type args struct { 26 | items []string 27 | idx int 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want []string 33 | }{ 34 | { 35 | args: struct { 36 | items []string 37 | idx int 38 | }{items: []string{"1", "2", "3"}, idx: 2}, 39 | want: []string{"1", "2"}, 40 | }, 41 | { 42 | args: struct { 43 | items []string 44 | idx int 45 | }{items: []string{"1", "2", "3"}, idx: 0}, 46 | want: []string{"3", "2"}, 47 | }, 48 | { 49 | args: struct { 50 | items []string 51 | idx int 52 | }{items: []string{"1", "2", "3"}, idx: 1}, 53 | want: []string{"1", "3"}, 54 | }, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | if got := Remove(tt.args.items, tt.args.idx); !reflect.DeepEqual(got, tt.want) { 59 | t.Errorf("Remove() = %v, want %v", got, tt.want) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestRemoveDuplicates(t *testing.T) { 66 | type args struct { 67 | items []string 68 | } 69 | tests := []struct { 70 | name string 71 | args args 72 | want []string 73 | }{ 74 | {name: "testEmptySlice", args: args{items: []string{}}, want: []string{}}, 75 | {name: "testDuplicatesSlice", args: args{items: []string{"1", "2", "3", "1", "3"}}, want: []string{"1", "2", "3"}}, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | if got := RemoveDuplicates(tt.args.items); !reflect.DeepEqual(got, tt.want) { 80 | t.Errorf("RemoveDuplicates() = %v, want %v", got, tt.want) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func TestParseIntegerListToStringSlice(t *testing.T) { 87 | type args struct { 88 | flagName string 89 | flagValue string 90 | } 91 | tests := []struct { 92 | name string 93 | args args 94 | want []string 95 | wantErr bool 96 | }{ 97 | { 98 | name: "split by comma", args: args{flagName: "local-port", flagValue: "8080,8081,8082"}, 99 | want: []string{"8080", "8081", "8082"}, 100 | }, 101 | { 102 | name: "split by connector", args: args{flagName: "local-port", flagValue: "8080-8083"}, 103 | want: []string{"8080", "8081", "8082", "8083"}, 104 | }, 105 | { 106 | name: "split by comma and connector", args: args{flagName: "local-port", flagValue: "7001,8080-8083"}, 107 | want: []string{"7001", "8080", "8081", "8082", "8083"}, 108 | }, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | got, err := ParseIntegerListToStringSlice(tt.args.flagName, tt.args.flagValue) 113 | if (err != nil) != tt.wantErr { 114 | t.Errorf("ParseIntegerListToStringSlice() error = %v, wantErr %v", err, tt.wantErr) 115 | return 116 | } 117 | if !reflect.DeepEqual(got, tt.want) { 118 | t.Errorf("ParseIntegerListToStringSlice() got = %v, want %v", got, tt.want) 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /util/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "flag" 21 | "io" 22 | "os" 23 | "path" 24 | 25 | "github.com/sirupsen/logrus" 26 | "gopkg.in/natefinch/lumberjack.v2" 27 | ) 28 | 29 | const ( 30 | Blade = 1 31 | Bin = 2 32 | Custom = 3 33 | ) 34 | 35 | const BladeLog = "chaosblade.log" 36 | 37 | var ( 38 | Debug bool 39 | LogPath string 40 | LogLevel string 41 | ) 42 | 43 | func AddDebugFlag() { 44 | flag.BoolVar(&Debug, "debug", false, "set debug mode") 45 | } 46 | 47 | func AddLogPathFlag() { 48 | flag.StringVar(&LogPath, "log-path", GetProgramPath(), "the directory path to save chaosblade.logrus.") 49 | } 50 | 51 | func AddLogLevelFlag() { 52 | flag.StringVar(&LogLevel, "log-level", "info", "level of logging wanted.") 53 | } 54 | 55 | // InitLog invoked after flag parsed 56 | func InitLog(programType int) { 57 | logFile, err := GetLogFile(programType) 58 | if err != nil { 59 | return 60 | } 61 | output := &lumberjack.Logger{ 62 | Filename: logFile, 63 | MaxSize: 30, // m 64 | MaxBackups: 1, 65 | MaxAge: 2, // days 66 | Compress: false, 67 | } 68 | logrus.SetOutput(&fileWriterWithoutErr{output}) 69 | 70 | formatter := &logrus.TextFormatter{ 71 | FullTimestamp: true, 72 | TimestampFormat: "2006-01-02 15:04:05.999999999 MST", 73 | DisableColors: true, 74 | } 75 | logrus.SetFormatter(formatter) 76 | 77 | if Debug { 78 | logrus.SetLevel(logrus.DebugLevel) 79 | } 80 | } 81 | 82 | func Panicf(uid, funcName, msg string) { 83 | logger(uid, funcName, msg, logrus.PanicLevel) 84 | } 85 | 86 | func Fatalf(uid, funcName, msg string) { 87 | logger(uid, funcName, msg, logrus.FatalLevel) 88 | } 89 | 90 | func Errorf(uid, funcName, msg string) { 91 | logger(uid, funcName, msg, logrus.ErrorLevel) 92 | } 93 | 94 | func Warnf(uid, funcName, msg string) { 95 | logger(uid, funcName, msg, logrus.WarnLevel) 96 | } 97 | 98 | func Infof(uid, funcName, msg string) { 99 | logger(uid, funcName, msg, logrus.InfoLevel) 100 | } 101 | 102 | func Debugf(uid, funcName, msg string) { 103 | logger(uid, funcName, msg, logrus.DebugLevel) 104 | } 105 | 106 | func Tracef(uid, funcName, msg string) { 107 | logger(uid, funcName, msg, logrus.TraceLevel) 108 | } 109 | 110 | func logger(uid, funcName, msg string, level logrus.Level) { 111 | entry := logrus.WithFields(logrus.Fields{ 112 | "uid": uid, 113 | "location": funcName, 114 | }) 115 | switch level { 116 | case logrus.PanicLevel: 117 | entry.Panic(msg) 118 | case logrus.FatalLevel: 119 | entry.Fatal(msg) 120 | case logrus.ErrorLevel: 121 | entry.Error(msg) 122 | case logrus.WarnLevel: 123 | entry.Warn(msg) 124 | case logrus.InfoLevel: 125 | entry.Info(msg) 126 | case logrus.DebugLevel: 127 | entry.Debug(msg) 128 | case logrus.TraceLevel: 129 | entry.Trace(msg) 130 | default: 131 | Errorf(uid, funcName, msg) 132 | } 133 | } 134 | 135 | func GetLogPath(programType int) (string, error) { 136 | var binDir string 137 | switch programType { 138 | case Blade: 139 | binDir = GetProgramPath() 140 | case Bin: 141 | binDir = GetProgramParentPath() 142 | case Custom: 143 | binDir = LogPath 144 | default: 145 | binDir = GetProgramPath() 146 | } 147 | logsPath := path.Join(binDir, "logs") 148 | if IsExist(logsPath) { 149 | return logsPath, nil 150 | } 151 | // mk dir 152 | err := os.MkdirAll(logsPath, os.ModePerm) 153 | if err != nil { 154 | return "", err 155 | } 156 | return logsPath, nil 157 | } 158 | 159 | // GetLogFile 160 | func GetLogFile(programType int) (string, error) { 161 | logPath, err := GetLogPath(programType) 162 | if err != nil { 163 | return "", err 164 | } 165 | logFile := path.Join(logPath, BladeLog) 166 | return logFile, nil 167 | } 168 | 169 | // GetNohupOutput 170 | func GetNohupOutput(programType int, logFileName string) string { 171 | logPath, err := GetLogPath(programType) 172 | if err != nil { 173 | return "/dev/null" 174 | } 175 | return path.Join(logPath, logFileName) 176 | } 177 | 178 | // fileWriterWithoutErr write func does not return err under any conditions 179 | // To solve "Failed to write to log, write logs/chaosblade.log: no space left on device" err 180 | type fileWriterWithoutErr struct { 181 | io.Writer 182 | } 183 | 184 | func (f *fileWriterWithoutErr) Write(b []byte) (n int, err error) { 185 | i, _ := f.Writer.Write(b) 186 | return i, nil 187 | } 188 | -------------------------------------------------------------------------------- /channel/local_mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package channel 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 23 | "github.com/chaosblade-io/chaosblade-spec-go/util" 24 | ) 25 | 26 | // MockLocalChannel for testing 27 | type MockLocalChannel struct { 28 | ScriptPath string 29 | // mock function 30 | RunFunc func(ctx context.Context, script, args string) *spec.Response 31 | GetPidsByProcessCmdNameFunc func(processName string, ctx context.Context) ([]string, error) 32 | GetPidsByProcessNameFunc func(processName string, ctx context.Context) ([]string, error) 33 | GetPsArgsFunc func(ctx context.Context) string 34 | IsCommandAvailableFunc func(ctx context.Context, commandName string) bool 35 | ProcessExistsFunc func(pid string) (bool, error) 36 | GetPidUserFunc func(pid string) (string, error) 37 | GetPidsByLocalPortsFunc func(ctx context.Context, localPorts []string) ([]string, error) 38 | GetPidsByLocalPortFunc func(ctx context.Context, localPort string) ([]string, error) 39 | } 40 | 41 | func NewMockLocalChannel() spec.Channel { 42 | return &MockLocalChannel{ 43 | ScriptPath: util.GetBinPath(), 44 | RunFunc: defaultRunFunc, 45 | GetPidsByProcessCmdNameFunc: defaultGetPidsByProcessCmdNameFunc, 46 | GetPidsByProcessNameFunc: defaultGetPidsByProcessNameFunc, 47 | GetPsArgsFunc: defaultGetPsArgsFunc, 48 | IsCommandAvailableFunc: defaultIsCommandAvailableFunc, 49 | ProcessExistsFunc: defaultProcessExistsFunc, 50 | GetPidUserFunc: defaultGetPidUserFunc, 51 | GetPidsByLocalPortsFunc: defaultGetPidsByLocalPortsFunc, 52 | GetPidsByLocalPortFunc: defaultGetPidsByLocalPortFunc, 53 | } 54 | } 55 | 56 | func (l *MockLocalChannel) Name() string { 57 | return "mock" 58 | } 59 | 60 | func (mlc *MockLocalChannel) GetPidsByProcessCmdName(processName string, ctx context.Context) ([]string, error) { 61 | return mlc.GetPidsByProcessCmdNameFunc(processName, ctx) 62 | } 63 | 64 | func (mlc *MockLocalChannel) GetPidsByProcessName(processName string, ctx context.Context) ([]string, error) { 65 | return mlc.GetPidsByProcessNameFunc(processName, ctx) 66 | } 67 | 68 | func (mlc *MockLocalChannel) GetPsArgs(ctx context.Context) string { 69 | return mlc.GetPsArgsFunc(ctx) 70 | } 71 | 72 | func (mlc *MockLocalChannel) IsAlpinePlatform(ctx context.Context) bool { 73 | return false 74 | } 75 | 76 | func (mlc *MockLocalChannel) IsAllCommandsAvailable(ctx context.Context, commandNames []string) (*spec.Response, bool) { 77 | return nil, false 78 | } 79 | 80 | func (mlc *MockLocalChannel) IsCommandAvailable(ctx context.Context, commandName string) bool { 81 | return mlc.IsCommandAvailableFunc(ctx, commandName) 82 | } 83 | 84 | func (mlc *MockLocalChannel) ProcessExists(pid string) (bool, error) { 85 | return mlc.ProcessExistsFunc(pid) 86 | } 87 | 88 | func (mlc *MockLocalChannel) GetPidUser(pid string) (string, error) { 89 | return mlc.GetPidUserFunc(pid) 90 | } 91 | 92 | func (mlc *MockLocalChannel) GetPidsByLocalPorts(ctx context.Context, localPorts []string) ([]string, error) { 93 | return mlc.GetPidsByLocalPortsFunc(ctx, localPorts) 94 | } 95 | 96 | func (mlc *MockLocalChannel) GetPidsByLocalPort(ctx context.Context, localPort string) ([]string, error) { 97 | return mlc.GetPidsByLocalPortFunc(ctx, localPort) 98 | } 99 | 100 | func (mlc *MockLocalChannel) Run(ctx context.Context, script, args string) *spec.Response { 101 | return mlc.RunFunc(ctx, script, args) 102 | } 103 | 104 | func (mlc *MockLocalChannel) GetScriptPath() string { 105 | return mlc.ScriptPath 106 | } 107 | 108 | var defaultGetPidsByProcessCmdNameFunc = func(processName string, ctx context.Context) ([]string, error) { 109 | return []string{}, nil 110 | } 111 | 112 | var defaultGetPidsByProcessNameFunc = func(processName string, ctx context.Context) ([]string, error) { 113 | return []string{}, nil 114 | } 115 | 116 | var defaultGetPsArgsFunc = func(ctx context.Context) string { 117 | return "-eo user,pid,ppid,args" 118 | } 119 | 120 | var defaultIsCommandAvailableFunc = func(ctx context.Context, commandName string) bool { 121 | return false 122 | } 123 | 124 | var defaultProcessExistsFunc = func(pid string) (bool, error) { 125 | return false, nil 126 | } 127 | 128 | var defaultGetPidUserFunc = func(pid string) (string, error) { 129 | return "admin", nil 130 | } 131 | 132 | var defaultGetPidsByLocalPortsFunc = func(ctx context.Context, localPorts []string) ([]string, error) { 133 | return []string{}, nil 134 | } 135 | 136 | var defaultGetPidsByLocalPortFunc = func(ctx context.Context, localPort string) ([]string, error) { 137 | return []string{}, nil 138 | } 139 | 140 | var defaultRunFunc = func(ctx context.Context, script, args string) *spec.Response { 141 | return spec.ReturnSuccess("success") 142 | } 143 | -------------------------------------------------------------------------------- /channel/channel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package channel 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "regexp" 24 | "strings" 25 | 26 | "github.com/chaosblade-io/chaosblade-spec-go/log" 27 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 28 | "github.com/chaosblade-io/chaosblade-spec-go/util" 29 | ) 30 | 31 | // grep ${key} 32 | const ( 33 | ProcessKey = "process" 34 | ExcludeProcessKey = "excludeProcess" 35 | ProcessCommandKey = "processCommand" 36 | ) 37 | 38 | func GetPidsByLocalPort(ctx context.Context, channel spec.Channel, localPort string) ([]string, error) { 39 | available := channel.IsCommandAvailable(ctx, "ss") 40 | if !available { 41 | return nil, fmt.Errorf("ss command not found, can't get pid by port") 42 | } 43 | 44 | pids := []string{} 45 | 46 | //on centos7, ss outupt pid with 'pid=' 47 | //$ss -lpn 'sport = :80' 48 | //Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port 49 | //tcp LISTEN 0 128 *:80 *:* users:(("tengine",pid=237768,fd=6),("tengine",pid=237767,fd=6)) 50 | 51 | //on centos6, ss output pid without 'pid=' 52 | //$ss -lpn 'sport = :80' 53 | //Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port 54 | //tcp LISTEN 0 128 *:80 *:* users:(("tengine",237768,fd=6),("tengine",237767,fd=6)) 55 | response := channel.Run(ctx, "ss", fmt.Sprintf("-pln sport = :%s", localPort)) 56 | if !response.Success { 57 | return pids, errors.New(response.Err) 58 | } 59 | if util.IsNil(response.Result) { 60 | return pids, nil 61 | } 62 | result := response.Result.(string) 63 | ssMsg := strings.TrimSpace(result) 64 | if ssMsg == "" { 65 | return pids, nil 66 | } 67 | sockets := strings.Split(ssMsg, "\n") 68 | log.Infof(ctx, "sockets for %s, %v", localPort, sockets) 69 | for idx, s := range sockets { 70 | if idx == 0 { 71 | continue 72 | } 73 | fields := strings.Fields(s) 74 | // centos7: users:(("tengine",pid=237768,fd=6),("tengine",pid=237767,fd=6)) 75 | // centos6: users:(("tengine",237768,fd=6),("tengine",237767,fd=6)) 76 | lastField := fields[len(fields)-1] 77 | log.Infof(ctx, "GetPidsByLocalPort: lastField: %v", lastField) 78 | pidExp := regexp.MustCompile(`pid=(\d+)|,(\d+),`) 79 | // extract all the pids that conforms to pidExp 80 | matchedPidArrays := pidExp.FindAllStringSubmatch(lastField, -1) 81 | if matchedPidArrays == nil || len(matchedPidArrays) == 0 { 82 | return pids, nil 83 | } 84 | 85 | for _, matchedPidArray := range matchedPidArrays { 86 | 87 | var pid string 88 | 89 | // centos7: matchedPidArray is [pid=29863 29863 ], matchedPidArray[len(matchedPidArray)-1] is whitespace 90 | 91 | pid = strings.TrimSpace(matchedPidArray[len(matchedPidArray)-1]) 92 | 93 | if pid != "" { 94 | pids = append(pids, pid) 95 | continue 96 | } 97 | 98 | // centos6: matchedPidArray is [,237768, 237768] matchedPidArray[len(matchedPidArray)-1] is pid 99 | pid = strings.TrimSpace(matchedPidArray[len(matchedPidArray)-2]) 100 | if pid != "" { 101 | pids = append(pids, pid) 102 | continue 103 | } 104 | 105 | } 106 | } 107 | log.Infof(ctx, "GetPidsByLocalPort: pids: %v", pids) 108 | return pids, nil 109 | } 110 | 111 | func IsAllCommandsAvailable(ctx context.Context, channel spec.Channel, commandNames []string) (*spec.Response, bool) { 112 | if len(commandNames) == 0 { 113 | return nil, true 114 | } 115 | 116 | for _, commandName := range commandNames { 117 | if channel.IsCommandAvailable(ctx, commandName) { 118 | continue 119 | } 120 | switch commandName { 121 | case "rm": 122 | return spec.ResponseFailWithFlags(spec.CommandRmNotFound), false 123 | case "dd": 124 | return spec.ResponseFailWithFlags(spec.CommandDdNotFound), false 125 | case "touch": 126 | return spec.ResponseFailWithFlags(spec.CommandTouchNotFound), false 127 | case "mkdir": 128 | return spec.ResponseFailWithFlags(spec.CommandMkdirNotFound), false 129 | case "echo": 130 | return spec.ResponseFailWithFlags(spec.CommandEchoNotFound), false 131 | case "kill": 132 | return spec.ResponseFailWithFlags(spec.CommandKillNotFound), false 133 | case "mv": 134 | return spec.ResponseFailWithFlags(spec.CommandMvNotFound), false 135 | case "mount": 136 | return spec.ResponseFailWithFlags(spec.CommandMountNotFound), false 137 | case "umount": 138 | return spec.ResponseFailWithFlags(spec.CommandUmountNotFound), false 139 | case "tc": 140 | return spec.ResponseFailWithFlags(spec.CommandTcNotFound), false 141 | case "head": 142 | return spec.ResponseFailWithFlags(spec.CommandHeadNotFound), false 143 | case "grep": 144 | return spec.ResponseFailWithFlags(spec.CommandGrepNotFound), false 145 | case "cat": 146 | return spec.ResponseFailWithFlags(spec.CommandCatNotFound), false 147 | case "iptables": 148 | return spec.ResponseFailWithFlags(spec.CommandIptablesNotFound), false 149 | case "sed": 150 | return spec.ResponseFailWithFlags(spec.CommandSedNotFound), false 151 | case "awk": 152 | return spec.ResponseFailWithFlags(spec.CommandAwkNotFound), false 153 | case "tar": 154 | return spec.ResponseFailWithFlags(spec.CommandTarNotFound), false 155 | } 156 | } 157 | return nil, true 158 | } 159 | -------------------------------------------------------------------------------- /util/spec.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "io" 21 | "io/ioutil" 22 | "os" 23 | 24 | "gopkg.in/yaml.v2" 25 | 26 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 27 | ) 28 | 29 | // CreateYamlFile converts the spec.Models to spec file 30 | func CreateYamlFile(models *spec.Models, specFile string) error { 31 | file, err := os.OpenFile(specFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o755) 32 | if err != nil { 33 | return err 34 | } 35 | defer file.Close() 36 | return MarshalModelSpec(models, file) 37 | } 38 | 39 | // MarshalModelSpec marshals the spec.Models to bytes and output to writer 40 | func MarshalModelSpec(models *spec.Models, writer io.Writer) error { 41 | bytes, err := yaml.Marshal(models) 42 | if err != nil { 43 | return err 44 | } 45 | writer.Write(bytes) 46 | return nil 47 | } 48 | 49 | // ParseSpecsToModel parses the yaml file to spec.Models and set the executor to the spec.Models 50 | func ParseSpecsToModel(file string, executor spec.Executor) (*spec.Models, error) { 51 | bytes, err := ioutil.ReadFile(file) 52 | if err != nil { 53 | return nil, err 54 | } 55 | models := &spec.Models{} 56 | err = yaml.Unmarshal(bytes, models) 57 | if err != nil { 58 | return nil, err 59 | } 60 | for idx := range models.Models { 61 | models.Models[idx].ExpExecutor = executor 62 | } 63 | return models, nil 64 | } 65 | 66 | // ConvertSpecToModels converts the spec.ExpModelCommandSpec to spec.Models 67 | func ConvertSpecToModels(commandSpec spec.ExpModelCommandSpec, prepare spec.ExpPrepareModel, scope string) *spec.Models { 68 | models := &spec.Models{ 69 | Version: "v1", 70 | Kind: "plugin", 71 | Models: make([]spec.ExpCommandModel, 0), 72 | } 73 | 74 | model := spec.ExpCommandModel{ 75 | ExpName: commandSpec.Name(), 76 | ExpShortDesc: commandSpec.ShortDesc(), 77 | ExpLongDesc: commandSpec.LongDesc(), 78 | ExpActions: make([]spec.ActionModel, 0), 79 | ExpSubTargets: make([]string, 0), 80 | ExpPrepareModel: prepare, 81 | ExpScope: scope, 82 | } 83 | for _, action := range commandSpec.Actions() { 84 | actionModel := spec.ActionModel{ 85 | ActionName: action.Name(), 86 | ActionAliases: action.Aliases(), 87 | ActionShortDesc: action.ShortDesc(), 88 | ActionLongDesc: action.LongDesc(), 89 | ActionExample: action.Example(), 90 | ActionMatchers: func() []spec.ExpFlag { 91 | matchers := make([]spec.ExpFlag, 0) 92 | for _, m := range action.Matchers() { 93 | matchers = append(matchers, spec.ExpFlag{ 94 | Name: m.FlagName(), 95 | Desc: m.FlagDesc(), 96 | NoArgs: m.FlagNoArgs(), 97 | Required: m.FlagRequired(), 98 | RequiredWhenDestroyed: m.FlagRequiredWhenDestroyed(), 99 | }) 100 | } 101 | return matchers 102 | }(), 103 | ActionFlags: func() []spec.ExpFlag { 104 | flagsMap := make(map[string]struct{}, 0) 105 | flags := make([]spec.ExpFlag, 0) 106 | for _, m := range action.Flags() { 107 | if _, ok := flagsMap[m.FlagName()]; ok { 108 | continue 109 | } 110 | flags = append(flags, spec.ExpFlag{ 111 | Name: m.FlagName(), 112 | Desc: m.FlagDesc(), 113 | NoArgs: m.FlagNoArgs(), 114 | Required: m.FlagRequired(), 115 | RequiredWhenDestroyed: m.FlagRequiredWhenDestroyed(), 116 | }) 117 | flagsMap[m.FlagName()] = struct{}{} 118 | } 119 | for _, m := range commandSpec.Flags() { 120 | if _, ok := flagsMap[m.FlagName()]; ok { 121 | continue 122 | } 123 | flags = append(flags, spec.ExpFlag{ 124 | Name: m.FlagName(), 125 | Desc: m.FlagDesc(), 126 | NoArgs: m.FlagNoArgs(), 127 | Required: m.FlagRequired(), 128 | RequiredWhenDestroyed: m.FlagRequiredWhenDestroyed(), 129 | }) 130 | flagsMap[m.FlagName()] = struct{}{} 131 | } 132 | if _, ok := flagsMap["timeout"]; !ok { 133 | flags = append(flags, spec.ExpFlag{ 134 | Name: "timeout", 135 | Desc: "set timeout for experiment", 136 | Required: false, 137 | RequiredWhenDestroyed: false, 138 | }) 139 | flagsMap["timeout"] = struct{}{} 140 | } 141 | if _, ok := flagsMap["async"]; !ok { 142 | flags = append(flags, spec.ExpFlag{ 143 | Name: "async", 144 | Desc: "whether to create asynchronously, default is false", 145 | Required: false, 146 | NoArgs: true, 147 | }) 148 | flagsMap["async"] = struct{}{} 149 | } 150 | if _, ok := flagsMap["endpoint"]; !ok { 151 | flags = append(flags, spec.ExpFlag{ 152 | Name: "endpoint", 153 | Desc: "the create result reporting address. It takes effect only when the async value is true and the value is not empty", 154 | Required: false, 155 | }) 156 | flagsMap["endpoint"] = struct{}{} 157 | } 158 | return flags 159 | }(), 160 | ActionPrograms: action.Programs(), 161 | ActionCategories: action.Categories(), 162 | ActionProcessHang: action.ProcessHang(), 163 | } 164 | model.ExpActions = append(model.ExpActions, actionModel) 165 | } 166 | models.Models = append(models.Models, model) 167 | return models 168 | } 169 | 170 | // AddModels adds the child model to parent 171 | func AddModels(parent *spec.Models, child *spec.Models) { 172 | for idx, model := range parent.Models { 173 | for _, sub := range child.Models { 174 | model.ExpSubTargets = append(model.ExpSubTargets, sub.ExpName) 175 | } 176 | parent.Models[idx] = model 177 | } 178 | } 179 | 180 | // MergeModels 181 | func MergeModels(models ...*spec.Models) *spec.Models { 182 | result := &spec.Models{ 183 | Models: make([]spec.ExpCommandModel, 0), 184 | } 185 | for _, model := range models { 186 | result.Version = model.Version 187 | result.Kind = model.Kind 188 | result.Models = append(result.Models, model.Models...) 189 | } 190 | return result 191 | } 192 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/hex" 23 | "errors" 24 | "fmt" 25 | "io/ioutil" 26 | "math/rand" 27 | "net" 28 | "net/http" 29 | "os" 30 | "os/exec" 31 | "os/user" 32 | "path" 33 | "path/filepath" 34 | "reflect" 35 | "runtime" 36 | "time" 37 | 38 | "github.com/sirupsen/logrus" 39 | 40 | "github.com/chaosblade-io/chaosblade-spec-go/log" 41 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 42 | ) 43 | 44 | var ( 45 | proPath string 46 | binPath string 47 | libPath string 48 | yamlPath string 49 | ) 50 | 51 | func init() { 52 | rand.Seed(time.Now().UnixNano()) 53 | yamlPath = os.Getenv(spec.YamlPathEnv) 54 | } 55 | 56 | // GetProgramPath 57 | func GetProgramPath() string { 58 | if proPath != "" { 59 | return proPath 60 | } 61 | dir, err := exec.LookPath(os.Args[0]) 62 | if err != nil { 63 | logrus.Fatal("cannot get the process path") 64 | } 65 | if p, err := os.Readlink(dir); err == nil { 66 | dir = p 67 | } 68 | proPath, err = filepath.Abs(filepath.Dir(dir)) 69 | if err != nil { 70 | logrus.Fatal("cannot get the full process path") 71 | } 72 | return proPath 73 | } 74 | 75 | // GetBinPath 76 | func GetBinPath() string { 77 | if binPath != "" { 78 | return binPath 79 | } 80 | binPath = path.Join(GetProgramPath(), "bin") 81 | return binPath 82 | } 83 | 84 | // GetLibHome 85 | func GetLibHome() string { 86 | if libPath != "" { 87 | return libPath 88 | } 89 | libPath = path.Join(GetProgramPath(), "lib") 90 | return libPath 91 | } 92 | 93 | func GetYamlHome() string { 94 | if yamlPath != "" { 95 | return yamlPath 96 | } 97 | yamlPath = path.Join(GetProgramPath(), "yaml") 98 | return yamlPath 99 | } 100 | 101 | // GenerateUid for exp 102 | func GenerateUid() (string, error) { 103 | b := make([]byte, 8) 104 | _, err := rand.Read(b) 105 | if err != nil { 106 | return "", err 107 | } 108 | return hex.EncodeToString(b), nil 109 | } 110 | 111 | // GenerateContainerId for container 112 | func GenerateContainerId() string { 113 | b := make([]byte, 32) 114 | rand.Read(b) 115 | return hex.EncodeToString(b) 116 | } 117 | 118 | func GenerateExecID() string { 119 | bytesLength := 8 120 | b := make([]byte, bytesLength) 121 | n, err := rand.Read(b) 122 | if err != nil { 123 | panic(err) 124 | } 125 | if n != bytesLength { 126 | panic(errors.New(fmt.Sprintf("expected %d bytes, got %d bytes", bytesLength, n))) 127 | } 128 | return hex.EncodeToString(b) 129 | } 130 | 131 | func IsNil(i interface{}) bool { 132 | v := reflect.ValueOf(i) 133 | if v.Kind() == reflect.Ptr { 134 | return v.IsNil() 135 | } 136 | return false 137 | } 138 | 139 | // IsExist returns true if file exists 140 | func IsExist(fileName string) bool { 141 | _, err := os.Stat(fileName) 142 | return err == nil || os.IsExist(err) 143 | } 144 | 145 | // IsDir returns true if the path is directory 146 | func IsDir(path string) bool { 147 | fileInfo, err := os.Stat(path) 148 | if err != nil || fileInfo == nil { 149 | return false 150 | } 151 | return fileInfo.IsDir() 152 | } 153 | 154 | // GetUserHome return user home. 155 | func GetUserHome() string { 156 | user, err := user.Current() 157 | if err == nil { 158 | return user.HomeDir 159 | } 160 | return "/root" 161 | } 162 | 163 | // GetSpecifyingUserHome 164 | func GetSpecifyingUserHome(username string) string { 165 | usr, err := user.Lookup(username) 166 | if err == nil { 167 | return usr.HomeDir 168 | } 169 | return fmt.Sprintf("/home/%s", username) 170 | } 171 | 172 | // Curl url 173 | func Curl(ctx context.Context, url string) (string, error, int) { 174 | log.Infof(ctx, "%s", url) 175 | trans := http.Transport{ 176 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 177 | return net.DialTimeout(network, addr, 10*time.Second) 178 | }, 179 | } 180 | client := http.Client{ 181 | Transport: &trans, 182 | } 183 | resp, err := client.Get(url) 184 | if err != nil { 185 | return "", err, 0 186 | } 187 | defer resp.Body.Close() 188 | bytes, err := ioutil.ReadAll(resp.Body) 189 | if err != nil { 190 | return "", err, resp.StatusCode 191 | } 192 | return string(bytes), nil, resp.StatusCode 193 | } 194 | 195 | // PostCurl 196 | func PostCurl(url string, body []byte, contentType string) (string, error, int) { 197 | trans := http.Transport{ 198 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 199 | return net.DialTimeout(network, addr, 10*time.Second) 200 | }, 201 | } 202 | client := http.Client{ 203 | Transport: &trans, 204 | } 205 | req, err := http.NewRequest("POST", url, bytes.NewReader(body)) 206 | if err != nil { 207 | return "", err, 0 208 | } 209 | if contentType != "" { 210 | req.Header.Set("Content-Type", contentType) 211 | } 212 | response, err := client.Do(req) 213 | if err != nil { 214 | return "", err, 0 215 | } 216 | defer response.Body.Close() 217 | bytes, err := ioutil.ReadAll(response.Body) 218 | if err != nil { 219 | return "", err, response.StatusCode 220 | } 221 | return string(bytes), nil, response.StatusCode 222 | } 223 | 224 | // CheckPortInUse returns true if the port is in use, otherwise returns false. 225 | func CheckPortInUse(port string) bool { 226 | conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", port), time.Second) 227 | if err != nil { 228 | return false 229 | } 230 | defer conn.Close() 231 | if conn != nil { 232 | return true 233 | } 234 | return false 235 | } 236 | 237 | func GetUnusedPort() (int, error) { 238 | addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") 239 | if err != nil { 240 | return 0, err 241 | } 242 | listener, err := net.ListenTCP("tcp", addr) 243 | if err != nil { 244 | return 0, err 245 | } 246 | defer listener.Close() 247 | return listener.Addr().(*net.TCPAddr).Port, nil 248 | } 249 | 250 | // GetProgramParentPath returns the parent directory end with / 251 | func GetProgramParentPath() string { 252 | dir, _ := path.Split(GetProgramPath()) 253 | return dir 254 | } 255 | 256 | func GetRunFuncName() string { 257 | pc := make([]uintptr, 1) 258 | runtime.Callers(2, pc) 259 | f := runtime.FuncForPC(pc[0]) 260 | return f.Name() 261 | } 262 | -------------------------------------------------------------------------------- /channel/nsexec.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package channel 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "os" 24 | "os/exec" 25 | "path" 26 | "strconv" 27 | "strings" 28 | "time" 29 | 30 | "github.com/chaosblade-io/chaosblade-spec-go/log" 31 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 32 | "github.com/chaosblade-io/chaosblade-spec-go/util" 33 | ) 34 | 35 | const ( 36 | NSTargetFlagName = "ns_target" 37 | NSPidFlagName = "ns_pid" 38 | NSMntFlagName = "ns_mnt" 39 | NSNetFlagName = "ns_net" 40 | ) 41 | 42 | type NSExecChannel struct { 43 | LocalChannel 44 | } 45 | 46 | func NewNSExecChannel() spec.Channel { 47 | return &NSExecChannel{} 48 | } 49 | 50 | func (l *NSExecChannel) Name() string { 51 | return "nsexec" 52 | } 53 | 54 | func (l *NSExecChannel) Run(ctx context.Context, script, args string) *spec.Response { 55 | pid := ctx.Value(NSTargetFlagName) 56 | if pid == nil { 57 | return spec.ResponseFailWithFlags(spec.CommandIllegal, script) 58 | } 59 | 60 | ns_script := fmt.Sprintf("-t %s", pid) 61 | 62 | if ctx.Value(NSPidFlagName) == spec.True { 63 | ns_script = fmt.Sprintf("%s -p", ns_script) 64 | } 65 | 66 | if ctx.Value(NSMntFlagName) == spec.True { 67 | ns_script = fmt.Sprintf("%s -m", ns_script) 68 | } 69 | 70 | if ctx.Value(NSNetFlagName) == spec.True { 71 | ns_script = fmt.Sprintf("%s -n", ns_script) 72 | } 73 | 74 | isBladeCommand := isBladeCommand(script) 75 | if isBladeCommand && !util.IsExist(script) { 76 | // TODO nohup invoking 77 | return spec.ResponseFailWithFlags(spec.ChaosbladeFileNotFound, script) 78 | } 79 | timeoutCtx, cancel := context.WithTimeout(ctx, 60*time.Second) 80 | defer cancel() 81 | 82 | if args != "" { 83 | args = script + " " + args 84 | } else { 85 | args = script 86 | } 87 | 88 | ns_script = fmt.Sprintf("%s -- /bin/sh -c", ns_script) 89 | 90 | programPath := util.GetProgramPath() 91 | if path.Base(programPath) != spec.BinPath { 92 | programPath = path.Join(programPath, spec.BinPath) 93 | } 94 | bin := path.Join(programPath, spec.NSExecBin) 95 | log.Debugf(ctx, `Command: %s %s "%s"`, bin, ns_script, args) 96 | 97 | split := strings.Split(ns_script, " ") 98 | 99 | cmd := exec.CommandContext(timeoutCtx, bin, append(split, args)...) 100 | output, err := cmd.CombinedOutput() 101 | outMsg := string(output) 102 | log.Debugf(ctx, "Command Result, output: %v, err: %v", outMsg, err) 103 | // TODO shell-init错误 104 | if strings.TrimSpace(outMsg) != "" && (strings.HasPrefix(strings.TrimSpace(outMsg), "{") || strings.HasPrefix(strings.TrimSpace(outMsg), "[")) { 105 | resp := spec.Decode(outMsg, nil) 106 | if resp.Code != spec.ResultUnmarshalFailed.Code { 107 | return resp 108 | } 109 | } 110 | if err == nil { 111 | return spec.ReturnSuccess(outMsg) 112 | } 113 | outMsg += " " + err.Error() 114 | return spec.ResponseFailWithFlags(spec.OsCmdExecFailed, cmd, outMsg) 115 | } 116 | 117 | func (l *NSExecChannel) GetPidsByProcessCmdName(processName string, ctx context.Context) ([]string, error) { 118 | excludeProcesses := ctx.Value(ExcludeProcessKey) 119 | excludeGrepInfo := "" 120 | if excludeProcesses != nil { 121 | excludeProcessesString := excludeProcesses.(string) 122 | excludeProcessArrays := strings.Split(excludeProcessesString, ",") 123 | for _, excludeProcess := range excludeProcessArrays { 124 | if excludeProcess != "" { 125 | excludeGrepInfo += fmt.Sprintf(`| grep -v -w %s`, excludeProcess) 126 | } 127 | } 128 | } 129 | response := l.Run(ctx, "pgrep", 130 | fmt.Sprintf(`-l %s %s | grep -v -w chaos_killprocess | grep -v -w chaos_stopprocess | awk '{print $1}' | tr '\n' ' '`, 131 | processName, excludeGrepInfo)) 132 | if !response.Success { 133 | return nil, errors.New(response.Err) 134 | } 135 | pidString := response.Result.(string) 136 | pids := strings.Fields(strings.TrimSpace(pidString)) 137 | currPid := strconv.Itoa(os.Getpid()) 138 | for idx, pid := range pids { 139 | if pid == currPid { 140 | return util.Remove(pids, idx), nil 141 | } 142 | } 143 | return pids, nil 144 | } 145 | 146 | func (l *NSExecChannel) GetPidsByProcessName(processName string, ctx context.Context) ([]string, error) { 147 | psArgs := l.GetPsArgs(ctx) 148 | otherProcess := ctx.Value(ProcessKey) 149 | otherGrepInfo := "" 150 | if otherProcess != nil { 151 | processString := otherProcess.(string) 152 | if processString != "" { 153 | otherGrepInfo = fmt.Sprintf(`| grep "%s"`, processString) 154 | } 155 | } 156 | excludeProcesses := ctx.Value(ExcludeProcessKey) 157 | excludeGrepInfo := "" 158 | if excludeProcesses != nil { 159 | excludeProcessesString := excludeProcesses.(string) 160 | excludeProcessArrays := strings.Split(excludeProcessesString, ",") 161 | for _, excludeProcess := range excludeProcessArrays { 162 | if excludeProcess != "" { 163 | excludeGrepInfo += fmt.Sprintf(`| grep -v -w %s`, excludeProcess) 164 | } 165 | } 166 | } 167 | if strings.HasPrefix(processName, "-") { 168 | processName = fmt.Sprintf(`\%s`, processName) 169 | } 170 | response := l.Run(ctx, "ps", 171 | fmt.Sprintf(`%s | grep "%s" %s %s | grep -v -w grep | grep -v -w chaos_killprocess | grep -v -w chaos_stopprocess | awk '{print $2}' | tr '\n' ' '`, 172 | psArgs, processName, otherGrepInfo, excludeGrepInfo)) 173 | if !response.Success { 174 | return nil, errors.New(response.Err) 175 | } 176 | pidString := strings.TrimSpace(response.Result.(string)) 177 | if pidString == "" { 178 | return make([]string, 0), nil 179 | } 180 | pids := strings.Fields(pidString) 181 | currPid := strconv.Itoa(os.Getpid()) 182 | for idx, pid := range pids { 183 | if pid == currPid { 184 | return util.Remove(pids, idx), nil 185 | } 186 | } 187 | return pids, nil 188 | } 189 | 190 | func (l *NSExecChannel) IsAllCommandsAvailable(ctx context.Context, commandNames []string) (*spec.Response, bool) { 191 | return IsAllCommandsAvailable(ctx, l, commandNames) 192 | } 193 | 194 | func (l *NSExecChannel) IsCommandAvailable(ctx context.Context, commandName string) bool { 195 | response := l.Run(ctx, "command", fmt.Sprintf("-v %s", commandName)) 196 | if response.Success { 197 | if response.Result != nil && strings.Contains(response.Result.(string), commandName) { 198 | return true 199 | } 200 | } 201 | return false 202 | } 203 | 204 | func (l *NSExecChannel) GetPsArgs(ctx context.Context) string { 205 | psArgs := "-eo user,pid,ppid,args" 206 | if l.IsAlpinePlatform(ctx) { 207 | psArgs = "-o user,pid,ppid,args" 208 | } 209 | return psArgs 210 | } 211 | 212 | func (l *NSExecChannel) IsAlpinePlatform(ctx context.Context) bool { 213 | osVer := "" 214 | if util.IsExist("/etc/os-release") { 215 | response := l.Run(ctx, "awk", "-F '=' '{if ($1 == \"ID\") {print $2;exit 0}}' /etc/os-release") 216 | if response.Success { 217 | osVer = response.Result.(string) 218 | } 219 | } 220 | return strings.TrimSpace(osVer) == "alpine" 221 | } 222 | 223 | func (l *NSExecChannel) GetPidsByLocalPort(ctx context.Context, localPort string) ([]string, error) { 224 | return GetPidsByLocalPort(ctx, l, localPort) 225 | } 226 | -------------------------------------------------------------------------------- /channel/local_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | /* 5 | * Copyright 2025 The ChaosBlade Authors 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | /* 21 | * Copyright 1999-2019 Alibaba Group Holding Ltd. 22 | * 23 | * Licensed under the Apache License, Version 2.0 (the "License"); 24 | * you may not use this file except in compliance with the License. 25 | * You may obtain a copy of the License at 26 | * 27 | * http://www.apache.org/licenses/LICENSE-2.0 28 | * 29 | * Unless required by applicable law or agreed to in writing, software 30 | * distributed under the License is distributed on an "AS IS" BASIS, 31 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | * See the License for the specific language governing permissions and 33 | * limitations under the License. 34 | */ 35 | 36 | package channel 37 | 38 | import ( 39 | "context" 40 | "fmt" 41 | "os" 42 | "os/exec" 43 | "strconv" 44 | "strings" 45 | "time" 46 | 47 | "github.com/shirou/gopsutil/process" 48 | 49 | "github.com/chaosblade-io/chaosblade-spec-go/log" 50 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 51 | "github.com/chaosblade-io/chaosblade-spec-go/util" 52 | ) 53 | 54 | type LocalChannel struct{} 55 | 56 | // NewLocalChannel returns a local channel for invoking the host command 57 | func NewLocalChannel() spec.Channel { 58 | return &LocalChannel{} 59 | } 60 | 61 | func (l *LocalChannel) Name() string { 62 | return "local" 63 | } 64 | 65 | func (l *LocalChannel) Run(ctx context.Context, script, args string) *spec.Response { 66 | return execScript(ctx, script, args) 67 | } 68 | 69 | func (l *LocalChannel) GetScriptPath() string { 70 | return util.GetProgramPath() 71 | } 72 | 73 | func (l *LocalChannel) GetPidsByProcessCmdName(processName string, ctx context.Context) ([]string, error) { 74 | processName = strings.TrimSpace(processName) 75 | if processName == "" { 76 | return []string{}, fmt.Errorf("processName is blank") 77 | } 78 | processes, err := process.Processes() 79 | if err != nil { 80 | return []string{}, err 81 | } 82 | currPid := os.Getpid() 83 | excludeProcesses := getExcludeProcesses(ctx) 84 | pids := make([]string, 0) 85 | for _, p := range processes { 86 | name, err := p.Name() 87 | if err != nil { 88 | log.Debugf(ctx, "get process name error, pid: %d, err: %v", p.Pid, err) 89 | continue 90 | } 91 | if processName != name { 92 | continue 93 | } 94 | if int32(os.Getpid()) == p.Pid { 95 | continue 96 | } 97 | cmdline, _ := p.Cmdline() 98 | containsExcludeProcess := false 99 | log.Debugf(ctx, "process info, name: %s, cmdline: %s, processName: %s", name, cmdline, processName) 100 | for _, ep := range excludeProcesses { 101 | if strings.Contains(cmdline, strings.TrimSpace(ep)) { 102 | containsExcludeProcess = true 103 | break 104 | } 105 | } 106 | if containsExcludeProcess { 107 | continue 108 | } 109 | if p.Pid == int32(currPid) { 110 | continue 111 | } 112 | pids = append(pids, fmt.Sprintf("%d", p.Pid)) 113 | } 114 | return pids, nil 115 | } 116 | 117 | func (l *LocalChannel) GetPidsByProcessName(processName string, ctx context.Context) ([]string, error) { 118 | processName = strings.TrimSpace(processName) 119 | if processName == "" { 120 | return []string{}, fmt.Errorf("process keyword is blank") 121 | } 122 | processes, err := process.Processes() 123 | if err != nil { 124 | return []string{}, err 125 | } 126 | otherConditionProcessValue := ctx.Value(ProcessKey) 127 | otherConditionProcessName := "" 128 | if otherConditionProcessValue != nil { 129 | otherConditionProcessName = otherConditionProcessValue.(string) 130 | } 131 | processCommandValue := ctx.Value(ProcessCommandKey) 132 | processCommandName := "" 133 | if processCommandValue != nil { 134 | processCommandName = processCommandValue.(string) 135 | } 136 | currPid := os.Getpid() 137 | excludeProcesses := getExcludeProcesses(ctx) 138 | pids := make([]string, 0) 139 | for _, p := range processes { 140 | if processCommandName != "" { 141 | name, err := p.Name() 142 | if err != nil { 143 | log.Debugf(ctx, "get process command error, processCommand: %s, err: %v, ", processCommandName, err) 144 | continue 145 | } 146 | if !strings.Contains(name, processCommandName) { 147 | continue 148 | } 149 | } 150 | cmdline, err := p.Cmdline() 151 | if err != nil { 152 | log.Debugf(ctx, "get command line error, pid: %d, err: %v", p.Pid, err) 153 | continue 154 | } 155 | if !strings.Contains(cmdline, processName) { 156 | continue 157 | } 158 | log.Debugf(ctx, "process info, cmdline: %s, processName: %s, processCommand: %s, otherConditionProcessName: %s, excludeProcesses: %s", 159 | cmdline, processName, processCommandName, otherConditionProcessName, excludeProcesses) 160 | 161 | if otherConditionProcessName != "" && !strings.Contains(cmdline, otherConditionProcessName) { 162 | continue 163 | } 164 | containsExcludeProcess := false 165 | for _, ep := range excludeProcesses { 166 | if strings.Contains(cmdline, ep) { 167 | containsExcludeProcess = true 168 | break 169 | } 170 | } 171 | if containsExcludeProcess { 172 | continue 173 | } 174 | if p.Pid == int32(currPid) { 175 | continue 176 | } 177 | pids = append(pids, fmt.Sprintf("%d", p.Pid)) 178 | } 179 | return pids, nil 180 | } 181 | 182 | func getExcludeProcesses(ctx context.Context) []string { 183 | excludeProcessValue := ctx.Value(ExcludeProcessKey) 184 | excludeProcesses := make([]string, 0) 185 | if excludeProcessValue != nil { 186 | excludeProcessesString := excludeProcessValue.(string) 187 | processNames := strings.Split(excludeProcessesString, ",") 188 | for _, name := range processNames { 189 | name = strings.TrimSpace(name) 190 | if name == "" { 191 | continue 192 | } 193 | excludeProcesses = append(excludeProcesses, name) 194 | } 195 | } 196 | excludeProcesses = append(excludeProcesses, "chaos_killprocess", "chaos_stopprocess") 197 | return excludeProcesses 198 | } 199 | 200 | func (l *LocalChannel) GetPsArgs(ctx context.Context) string { 201 | psArgs := "-eo user,pid,ppid,args" 202 | if l.IsAlpinePlatform(ctx) { 203 | psArgs = "-o user,pid,ppid,args" 204 | } 205 | return psArgs 206 | } 207 | 208 | func (l *LocalChannel) IsAlpinePlatform(ctx context.Context) bool { 209 | osVer := "" 210 | if util.IsExist("/etc/os-release") { 211 | response := l.Run(ctx, "awk", "-F '=' '{if ($1 == \"ID\") {print $2;exit 0}}' /etc/os-release") 212 | if response.Success { 213 | osVer = response.Result.(string) 214 | } 215 | } 216 | return strings.TrimSpace(osVer) == "alpine" 217 | } 218 | 219 | // check command is available or not 220 | // now, all commands are: ["rm", "dd" ,"touch", "mkdir", "echo", "kill", ,"mv","mount", "umount","tc", "head" 221 | // "grep", "cat", "iptables", "sed", "awk", "tar"] 222 | func (l *LocalChannel) IsAllCommandsAvailable(ctx context.Context, commandNames []string) (*spec.Response, bool) { 223 | return IsAllCommandsAvailable(ctx, l, commandNames) 224 | } 225 | 226 | func (l *LocalChannel) IsCommandAvailable(ctx context.Context, commandName string) bool { 227 | response := l.Run(ctx, "command", fmt.Sprintf("-v %s", commandName)) 228 | return response.Success 229 | } 230 | 231 | func (l *LocalChannel) ProcessExists(pid string) (bool, error) { 232 | p, err := strconv.Atoi(pid) 233 | if err != nil { 234 | return false, err 235 | } 236 | return process.PidExists(int32(p)) 237 | } 238 | 239 | func (l *LocalChannel) GetPidUser(pid string) (string, error) { 240 | p, err := strconv.Atoi(pid) 241 | if err != nil { 242 | return "", err 243 | } 244 | process, err := process.NewProcess(int32(p)) 245 | if err != nil { 246 | return "", err 247 | } 248 | return process.Username() 249 | } 250 | 251 | func (l *LocalChannel) GetPidsByLocalPorts(ctx context.Context, localPorts []string) ([]string, error) { 252 | if localPorts == nil || len(localPorts) == 0 { 253 | return nil, fmt.Errorf("the local port parameter is empty") 254 | } 255 | result := make([]string, 0) 256 | for _, port := range localPorts { 257 | pids, err := l.GetPidsByLocalPort(ctx, port) 258 | if err != nil { 259 | return nil, fmt.Errorf("failed to get pid by %s, %v", port, err) 260 | } 261 | log.Infof(ctx, "get pids by %s port returns %v", port, pids) 262 | if pids != nil && len(pids) > 0 { 263 | result = append(result, pids...) 264 | } 265 | } 266 | return result, nil 267 | } 268 | 269 | func (l *LocalChannel) GetPidsByLocalPort(ctx context.Context, localPort string) ([]string, error) { 270 | return GetPidsByLocalPort(ctx, l, localPort) 271 | } 272 | 273 | // execScript invokes exec.CommandContext 274 | func execScript(ctx context.Context, script, args string) *spec.Response { 275 | isBladeCommand := isBladeCommand(script) 276 | if isBladeCommand && !util.IsExist(script) { 277 | // TODO nohup invoking 278 | return spec.ResponseFailWithFlags(spec.ChaosbladeFileNotFound, script) 279 | } 280 | newCtx, cancel := context.WithTimeout(ctx, 60*time.Second) 281 | defer cancel() 282 | if ctx == context.Background() { 283 | ctx = newCtx 284 | } 285 | log.Debugf(ctx, "Command: %s %s", script, args) 286 | cmd := exec.CommandContext(ctx, "cmd", "/C", script+` `+args) 287 | output, err := cmd.CombinedOutput() 288 | outMsg := string(output) 289 | log.Debugf(ctx, "Command Result, output: %v, err: %v", outMsg, err) 290 | if strings.TrimSpace(outMsg) != "" && (strings.HasPrefix(strings.TrimSpace(outMsg), "{") || strings.HasPrefix(strings.TrimSpace(outMsg), "[")) { 291 | resp := spec.Decode(outMsg, nil) 292 | if resp.Code != spec.ResultUnmarshalFailed.Code { 293 | return resp 294 | } 295 | } 296 | if err == nil { 297 | return spec.ReturnSuccess(outMsg) 298 | } 299 | outMsg += " " + err.Error() 300 | return spec.ResponseFailWithFlags(spec.OsCmdExecFailed, cmd, outMsg) 301 | } 302 | 303 | func isBladeCommand(script string) bool { 304 | return strings.HasSuffix(script, util.GetProgramPath()) 305 | } 306 | -------------------------------------------------------------------------------- /channel/local_unixs.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | * Copyright 2025 The ChaosBlade Authors 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | /* 21 | * Copyright 1999-2019 Alibaba Group Holding Ltd. 22 | * 23 | * Licensed under the Apache License, Version 2.0 (the "License"); 24 | * you may not use this file except in compliance with the License. 25 | * You may obtain a copy of the License at 26 | * 27 | * http://www.apache.org/licenses/LICENSE-2.0 28 | * 29 | * Unless required by applicable law or agreed to in writing, software 30 | * distributed under the License is distributed on an "AS IS" BASIS, 31 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | * See the License for the specific language governing permissions and 33 | * limitations under the License. 34 | */ 35 | 36 | package channel 37 | 38 | import ( 39 | "context" 40 | "fmt" 41 | "os" 42 | "os/exec" 43 | "strconv" 44 | "strings" 45 | "time" 46 | 47 | "github.com/shirou/gopsutil/process" 48 | 49 | "github.com/chaosblade-io/chaosblade-spec-go/log" 50 | "github.com/chaosblade-io/chaosblade-spec-go/spec" 51 | "github.com/chaosblade-io/chaosblade-spec-go/util" 52 | ) 53 | 54 | type LocalChannel struct{} 55 | 56 | // NewLocalChannel returns a local channel for invoking the host command 57 | func NewLocalChannel() spec.Channel { 58 | return &LocalChannel{} 59 | } 60 | 61 | func (l *LocalChannel) Name() string { 62 | return "local" 63 | } 64 | 65 | func (l *LocalChannel) Run(ctx context.Context, script, args string) *spec.Response { 66 | return execScript(ctx, script, args) 67 | } 68 | 69 | func (l *LocalChannel) GetScriptPath() string { 70 | return util.GetProgramPath() 71 | } 72 | 73 | func (l *LocalChannel) GetPidsByProcessCmdName(processName string, ctx context.Context) ([]string, error) { 74 | processName = strings.TrimSpace(processName) 75 | if processName == "" { 76 | return []string{}, fmt.Errorf("processName is blank") 77 | } 78 | processes, err := process.Processes() 79 | if err != nil { 80 | return []string{}, err 81 | } 82 | currPid := os.Getpid() 83 | excludeProcesses := getExcludeProcesses(ctx) 84 | pids := make([]string, 0) 85 | for _, p := range processes { 86 | name, err := p.Name() 87 | if err != nil { 88 | log.Debugf(ctx, "get process name error, pid: %v, err: %v", p.Pid, err) 89 | continue 90 | } 91 | if processName != name { 92 | continue 93 | } 94 | if int32(os.Getpid()) == p.Pid { 95 | continue 96 | } 97 | cmdline, _ := p.Cmdline() 98 | containsExcludeProcess := false 99 | log.Debugf(ctx, "process info, name: %s, cmdline: %s, processName: %s", name, cmdline, processName) 100 | for _, ep := range excludeProcesses { 101 | if strings.Contains(cmdline, strings.TrimSpace(ep)) { 102 | containsExcludeProcess = true 103 | break 104 | } 105 | } 106 | if containsExcludeProcess { 107 | continue 108 | } 109 | if p.Pid == int32(currPid) { 110 | continue 111 | } 112 | pids = append(pids, fmt.Sprintf("%d", p.Pid)) 113 | } 114 | return pids, nil 115 | } 116 | 117 | func (l *LocalChannel) GetPidsByProcessName(processName string, ctx context.Context) ([]string, error) { 118 | processName = strings.TrimSpace(processName) 119 | if processName == "" { 120 | return []string{}, fmt.Errorf("process keyword is blank") 121 | } 122 | processes, err := process.Processes() 123 | if err != nil { 124 | return []string{}, err 125 | } 126 | otherConditionProcessValue := ctx.Value(ProcessKey) 127 | otherConditionProcessName := "" 128 | if otherConditionProcessValue != nil { 129 | otherConditionProcessName = otherConditionProcessValue.(string) 130 | } 131 | processCommandValue := ctx.Value(ProcessCommandKey) 132 | processCommandName := "" 133 | if processCommandValue != nil { 134 | processCommandName = processCommandValue.(string) 135 | } 136 | currPid := os.Getpid() 137 | excludeProcesses := getExcludeProcesses(ctx) 138 | pids := make([]string, 0) 139 | for _, p := range processes { 140 | if processCommandName != "" { 141 | name, err := p.Name() 142 | if err != nil { 143 | log.Debugf(ctx, "get process command error, processCommand: %s, err: %v, ", processCommandName, err) 144 | continue 145 | } 146 | if !strings.Contains(name, processCommandName) { 147 | continue 148 | } 149 | } 150 | cmdline, err := p.Cmdline() 151 | if err != nil { 152 | log.Debugf(ctx, "get command line error, pid: %v, err: %v", p.Pid, err) 153 | continue 154 | } 155 | if !strings.Contains(cmdline, processName) { 156 | continue 157 | } 158 | log.Debugf(ctx, "process info, cmdline: %s, processName: %s, processCommand: %s, otherConditionProcessName: %s, excludeProcesses: %s", 159 | cmdline, processName, processCommandName, otherConditionProcessName, excludeProcesses) 160 | 161 | if otherConditionProcessName != "" && !strings.Contains(cmdline, otherConditionProcessName) { 162 | continue 163 | } 164 | containsExcludeProcess := false 165 | for _, ep := range excludeProcesses { 166 | if strings.Contains(cmdline, ep) { 167 | containsExcludeProcess = true 168 | break 169 | } 170 | } 171 | if containsExcludeProcess { 172 | continue 173 | } 174 | if p.Pid == int32(currPid) { 175 | continue 176 | } 177 | pids = append(pids, fmt.Sprintf("%d", p.Pid)) 178 | } 179 | return pids, nil 180 | } 181 | 182 | func getExcludeProcesses(ctx context.Context) []string { 183 | excludeProcessValue := ctx.Value(ExcludeProcessKey) 184 | excludeProcesses := make([]string, 0) 185 | if excludeProcessValue != nil { 186 | excludeProcessesString := excludeProcessValue.(string) 187 | processNames := strings.Split(excludeProcessesString, ",") 188 | for _, name := range processNames { 189 | name = strings.TrimSpace(name) 190 | if name == "" { 191 | continue 192 | } 193 | excludeProcesses = append(excludeProcesses, name) 194 | } 195 | } 196 | excludeProcesses = append(excludeProcesses, "chaos_killprocess", "chaos_stopprocess") 197 | return excludeProcesses 198 | } 199 | 200 | func (l *LocalChannel) GetPsArgs(ctx context.Context) string { 201 | psArgs := "-eo user,pid,ppid,args" 202 | if l.IsAlpinePlatform(ctx) { 203 | psArgs = "-o user,pid,ppid,args" 204 | } 205 | return psArgs 206 | } 207 | 208 | func (l *LocalChannel) IsAlpinePlatform(ctx context.Context) bool { 209 | osVer := "" 210 | if util.IsExist("/etc/os-release") { 211 | response := l.Run(ctx, "awk", "-F '=' '{if ($1 == \"ID\") {print $2;exit 0}}' /etc/os-release") 212 | if response.Success { 213 | osVer = response.Result.(string) 214 | } 215 | } 216 | return strings.TrimSpace(osVer) == "alpine" 217 | } 218 | 219 | // check command is available or not 220 | // now, all commands are: ["rm", "dd" ,"touch", "mkdir", "echo", "kill", ,"mv","mount", "umount","tc", "head" 221 | // "grep", "cat", "iptables", "sed", "awk", "tar"] 222 | func (l *LocalChannel) IsAllCommandsAvailable(ctx context.Context, commandNames []string) (*spec.Response, bool) { 223 | return IsAllCommandsAvailable(ctx, l, commandNames) 224 | } 225 | 226 | func (l *LocalChannel) IsCommandAvailable(ctx context.Context, commandName string) bool { 227 | response := l.Run(ctx, "command", fmt.Sprintf("-v %s", commandName)) 228 | return response.Success 229 | } 230 | 231 | func (l *LocalChannel) ProcessExists(pid string) (bool, error) { 232 | p, err := strconv.Atoi(pid) 233 | if err != nil { 234 | return false, err 235 | } 236 | return process.PidExists(int32(p)) 237 | } 238 | 239 | func (l *LocalChannel) GetPidUser(pid string) (string, error) { 240 | p, err := strconv.Atoi(pid) 241 | if err != nil { 242 | return "", err 243 | } 244 | process, err := process.NewProcess(int32(p)) 245 | if err != nil { 246 | return "", err 247 | } 248 | return process.Username() 249 | } 250 | 251 | func (l *LocalChannel) GetPidsByLocalPorts(ctx context.Context, localPorts []string) ([]string, error) { 252 | if len(localPorts) == 0 { 253 | return nil, fmt.Errorf("the local port parameter is empty") 254 | } 255 | result := make([]string, 0) 256 | for _, port := range localPorts { 257 | pids, err := l.GetPidsByLocalPort(ctx, port) 258 | if err != nil { 259 | return nil, fmt.Errorf("failed to get pid by %s, %v", port, err) 260 | } 261 | log.Infof(ctx, "get pids by %s port returns %v", port, pids) 262 | if len(pids) > 0 { 263 | result = append(result, pids...) 264 | } 265 | } 266 | return result, nil 267 | } 268 | 269 | func (l *LocalChannel) GetPidsByLocalPort(ctx context.Context, localPort string) ([]string, error) { 270 | return GetPidsByLocalPort(ctx, l, localPort) 271 | } 272 | 273 | // execScript invokes exec.CommandContext 274 | func execScript(ctx context.Context, script, args string) *spec.Response { 275 | isBladeCommand := isBladeCommand(script) 276 | if isBladeCommand && !util.IsExist(script) { 277 | // TODO nohup invoking 278 | return spec.ResponseFailWithFlags(spec.ChaosbladeFileNotFound, script) 279 | } 280 | newCtx, cancel := context.WithTimeout(ctx, 60*time.Second) 281 | defer cancel() 282 | if ctx == context.Background() { 283 | ctx = newCtx 284 | } 285 | log.Debugf(ctx, "Command: %s %s", script, args) 286 | // TODO /bin/sh 的问题 287 | cmd := exec.CommandContext(ctx, "/bin/sh", "-c", script+" "+args) 288 | output, err := cmd.CombinedOutput() 289 | outMsg := string(output) 290 | log.Debugf(ctx, "Command Result, output: %v, err: %v", outMsg, err) 291 | // TODO shell-init错误 292 | if strings.TrimSpace(outMsg) != "" && (strings.HasPrefix(strings.TrimSpace(outMsg), "{") || strings.HasPrefix(strings.TrimSpace(outMsg), "[")) { 293 | resp := spec.Decode(outMsg, nil) 294 | if resp.Code != spec.ResultUnmarshalFailed.Code { 295 | return resp 296 | } 297 | } 298 | if err == nil { 299 | return spec.ReturnSuccess(outMsg) 300 | } 301 | outMsg += " " + err.Error() 302 | return spec.ResponseFailWithFlags(spec.OsCmdExecFailed, cmd, outMsg) 303 | } 304 | 305 | func isBladeCommand(script string) bool { 306 | return strings.HasSuffix(script, util.GetProgramPath()) 307 | } 308 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 1999-2019 Alibaba Group Holding Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /spec/response.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package spec 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | type CodeType struct { 28 | Code int32 29 | Msg string 30 | } 31 | 32 | var ( 33 | IgnoreCode = CodeType{100, "ignore code"} 34 | OK = CodeType{200, "success"} 35 | ReturnOKDirectly = CodeType{201, "return ok directly"} 36 | Forbidden = CodeType{43000, "Forbidden: must be root"} 37 | ActionNotSupport = CodeType{44000, "`%s`: action not supported"} 38 | ParameterLess = CodeType{45000, "less parameter: `%s`"} 39 | ParameterIllegal = CodeType{46000, "illegal `%s` parameter value: `%s`. %v"} 40 | ParameterInvalid = CodeType{47000, "invalid `%s` parameter value: `%s`. %v"} 41 | ParameterInvalidProName = CodeType{47001, "invalid parameter `%s`, `%s` process not found"} 42 | ParameterInvalidProIdNotByName = CodeType{47002, "invalid parameter `process|pid`, the process ids got by %s does not contain the pid %s value"} 43 | ParameterInvalidCplusPort = CodeType{47003, "invalid parameter port, `%s` port not found, please execute prepare command firstly"} 44 | ParameterInvalidDbQuery = CodeType{47004, "invalid parameter `%s`, db record not found"} 45 | ParameterInvalidCplusTarget = CodeType{47005, "invalid parameter target, `%s` target not support"} 46 | ParameterInvalidBladePathError = CodeType{47006, "invalid parameter `%s`, deploy chaosblade to `%s` failed, err: %v"} 47 | ParameterInvalidNSNotOne = CodeType{47007, "invalid parameter `%s`, only one value can be specified"} 48 | ParameterInvalidK8sPodQuery = CodeType{47008, "invalid parameter `%s`, can not find pods"} 49 | ParameterInvalidK8sNodeQuery = CodeType{47009, "invalid parameter `%s`, can not find node"} 50 | ParameterInvalidDockContainerId = CodeType{47010, "invalid parameter `%s`, can not find container by id"} 51 | ParameterInvalidDockContainerName = CodeType{47011, "invalid parameter `%s`, can not find container by name"} 52 | ParameterInvalidTooManyProcess = CodeType{47012, "invalid parameter process, too many `%s` processes found"} 53 | DeployChaosBladeFailed = CodeType{47013, "deploy chaosblade to `%s` failed, err: %v"} 54 | ParameterRequestFailed = CodeType{48000, "get request parameter failed"} 55 | CommandIllegal = CodeType{49000, "illegal command, err: %v"} 56 | CommandNetworkExist = CodeType{49001, "network tc exec failed! RTNETLINK answers: File exists"} 57 | ChaosbladeFileNotFound = CodeType{51000, "`%s`: chaosblade file not found"} 58 | CommandTasksetNotFound = CodeType{52000, "`taskset`: command not found"} 59 | CommandMountNotFound = CodeType{52001, "`mount`: command not found"} 60 | CommandUmountNotFound = CodeType{52002, "`umount`: command not found"} 61 | CommandTcNotFound = CodeType{52003, "`tc`: command not found"} 62 | CommandIptablesNotFound = CodeType{52004, "`iptables`: command not found"} 63 | CommandSedNotFound = CodeType{52005, "`sed`: command not found"} 64 | CommandCatNotFound = CodeType{52006, "`cat`: command not found"} 65 | CommandSsNotFound = CodeType{52007, "`ss`: command not found"} 66 | CommandDdNotFound = CodeType{52008, "`dd`: command not found"} 67 | CommandRmNotFound = CodeType{52009, "`rm`: command not found"} 68 | CommandTouchNotFound = CodeType{52010, "`touch`: command not found"} 69 | CommandMkdirNotFound = CodeType{52011, "`mkdir`: command not found"} 70 | CommandEchoNotFound = CodeType{52012, "`echo`: command not found"} 71 | CommandKillNotFound = CodeType{52013, "`kill`: command not found"} 72 | CommandMvNotFound = CodeType{52014, "`mv`: command not found"} 73 | CommandHeadNotFound = CodeType{52015, "`head`: command not found"} 74 | CommandGrepNotFound = CodeType{52016, "`grep`: command not found"} 75 | CommandAwkNotFound = CodeType{52017, "`awk`: command not found"} 76 | CommandTarNotFound = CodeType{52018, "`tar`: command not found"} 77 | CommandSystemctlNotFound = CodeType{52019, "`systemctl`: command not found"} 78 | CommandNohupNotFound = CodeType{52020, "`nohup`: command not found"} 79 | ChaosbladeServerStarted = CodeType{53000, "the chaosblade has been started. If you want to stop it, you can execute blade server stop command"} 80 | UnexpectedStatus = CodeType{54000, "unexpected status, expected status: `%s`, but the real status: `%s`, please wait!"} 81 | DockerExecNotFound = CodeType{55000, "`%s`: the docker exec not found"} 82 | DockerImagePullFailed = CodeType{55001, "pull image failed, err: %v"} 83 | CriExecNotFound = CodeType{55002, "`%s`, the cri exc not found"} 84 | ImagePullFailed = CodeType{55003, "`%s`, pull image failed, err: %v"} 85 | HandlerExecNotFound = CodeType{56000, "`%s`: the handler exec not found"} 86 | CplusActionNotSupport = CodeType{56001, "`%s`: cplus action not support"} 87 | ContainerInContextNotFound = CodeType{56002, "cannot find container, please confirm if the container exists"} 88 | PodNotReady = CodeType{56003, "`%s` pod is not ready"} 89 | ResultUnmarshalFailed = CodeType{60000, "`%s`: exec result unmarshal failed, err: %v"} 90 | ResultMarshalFailed = CodeType{60001, "`%v`: exec result marshal failed, err: %v"} 91 | GenerateUidFailed = CodeType{60002, "generate experiment uid failed, err: %v"} 92 | ChaosbladeServiceStoped = CodeType{61000, "chaosblade service has been stopped"} 93 | ProcessIdByNameFailed = CodeType{63010, "`%s`: get process id by name failed, err: %v"} 94 | ProcessJudgeExistFailed = CodeType{63011, "`%s`: judge the process exist or not, failed, err: %v"} 95 | ProcessNotExist = CodeType{63012, "`%s`: the process not exist"} 96 | ProcessGetUsernameFailed = CodeType{63014, "`%s`: get username failed by the process id, err: %v"} 97 | ChannelNil = CodeType{63020, "chanel is nil"} 98 | SandboxGetPortFailed = CodeType{63030, "get sandbox port failed, err: %v"} 99 | SandboxCreateTokenFailed = CodeType{63031, "create sandbox token failed, err: %v"} 100 | FileCantGetLogFile = CodeType{63040, "can not get log file"} 101 | FileNotExist = CodeType{63041, "`%s`: not exist"} 102 | FileCantReadOrOpen = CodeType{63042, "`%s`: can not read or open"} 103 | BackfileExists = CodeType{63050, "`%s`: backup file exists, may be annother experiment is running"} 104 | DbQueryFailed = CodeType{63060, "`%s`: db query failed, err: %v"} 105 | K8sExecFailed = CodeType{63061, "`%s`: k8s exec failed, err: %v"} 106 | DockerExecFailed = CodeType{63062, "`%s`: docker exec failed, err: %v"} 107 | OsCmdExecFailed = CodeType{63063, "`%s`: cmd exec failed, err: %v"} 108 | HttpExecFailed = CodeType{63064, "`%s`: http cmd failed, err: %v"} 109 | GetIdentifierFailed = CodeType{63065, "get experiment identifier failed, err: %v"} 110 | CreateContainerFailed = CodeType{63066, "create container failed, err: %v"} 111 | ContainerExecFailed = CodeType{63067, "`%s`: container exec failed, err: %v"} 112 | OsExecutorNotFound = CodeType{63070, "`%s`: os executor not found"} 113 | ChaosfsClientFailed = CodeType{64000, "init chaosfs client failed in pod %v, err: %v"} 114 | ChaosfsInjectFailed = CodeType{64001, "inject io exception in pod %s failed, request %v, err: %v"} 115 | ChaosfsRecoverFailed = CodeType{64002, "recover io exception failed in pod %v, err: %v"} 116 | SshExecFailed = CodeType{65000, "ssh exec failed, result: %v, err %v"} 117 | SshExecNothing = CodeType{65001, "cannot get result from remote host, please execute recovery and try again"} 118 | SystemdNotFound = CodeType{66001, "`%s`: systemd not found, err: %v"} 119 | DatabaseError = CodeType{67001, "`%s`: failed to execute, err: %v"} 120 | DataNotFound = CodeType{67002, "`%s` record not found, if it's k8s experiment, please add --target k8s flag to retry"} 121 | ) 122 | 123 | func (c CodeType) Sprintf(values ...interface{}) string { 124 | return fmt.Sprintf(c.Msg, values...) 125 | } 126 | 127 | type Response struct { 128 | Code int32 `json:"code"` 129 | Success bool `json:"success"` 130 | Err string `json:"error,omitempty"` 131 | Result interface{} `json:"result,omitempty"` 132 | } 133 | 134 | func (response *Response) Error() string { 135 | return response.Print() 136 | } 137 | 138 | func (response *Response) Print() string { 139 | bytes, err := json.Marshal(response) 140 | if err != nil { 141 | return fmt.Sprintf("marshall response err, %s; code: %d", err.Error(), response.Code) 142 | } 143 | return string(bytes) 144 | } 145 | 146 | func Return(codeType CodeType, success bool) *Response { 147 | return &Response{Code: codeType.Code, Success: success, Err: codeType.Msg} 148 | } 149 | 150 | func ReturnFail(codeType CodeType, err string) *Response { 151 | return &Response{Code: codeType.Code, Success: false, Err: err} 152 | } 153 | 154 | func ReturnSuccess(result interface{}) *Response { 155 | return &Response{Code: OK.Code, Success: true, Result: result} 156 | } 157 | 158 | func ReturnResultIgnoreCode(result interface{}) *Response { 159 | return &Response{Code: IgnoreCode.Code, Result: result} 160 | } 161 | 162 | func ResponseFail(status int32, err string, result interface{}) *Response { 163 | return &Response{Code: status, Success: false, Err: err, Result: result} 164 | } 165 | 166 | func ResponseFailWithFlags(codeType CodeType, flags ...interface{}) *Response { 167 | if flags == nil { 168 | return &Response{Code: codeType.Code, Success: false, Err: codeType.Msg} 169 | } 170 | return &Response{Code: codeType.Code, Success: false, Err: fmt.Sprintf(codeType.Msg, flags...)} 171 | } 172 | 173 | func ResponseFailWithResult(codeType CodeType, result interface{}, flags ...interface{}) *Response { 174 | return &Response{Code: codeType.Code, Success: false, Result: result, Err: fmt.Sprintf(codeType.Msg, flags...)} 175 | } 176 | 177 | func Success() *Response { 178 | return ReturnSuccess(nil) 179 | } 180 | 181 | // ToString 182 | func (response *Response) ToString() string { 183 | bytes, err := json.MarshalIndent(response, "", "\t") 184 | if err != nil { 185 | return err.Error() 186 | } 187 | return fmt.Sprintln(string(bytes)) 188 | } 189 | 190 | // Decode return the response that wraps the content 191 | func Decode(content string, defaultValue *Response) *Response { 192 | var resp Response 193 | content = strings.TrimSpace(content) 194 | err := json.Unmarshal([]byte(content), &resp) 195 | if err != nil { 196 | if defaultValue == nil { 197 | defaultValue = ResponseFailWithFlags(ResultUnmarshalFailed, content, err.Error()) 198 | } 199 | logrus.Debugf("decode %s err, return default value, %s", content, defaultValue.Print()) 200 | return defaultValue 201 | } 202 | return &resp 203 | } 204 | -------------------------------------------------------------------------------- /spec/model.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 The ChaosBlade Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package spec 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | // ExpModelCommandSpec defines the command interface for the experimental plugin 25 | type ExpModelCommandSpec interface { 26 | // Name returns the target name 27 | Name() string 28 | 29 | // Scope returns the experiment scope 30 | Scope() string 31 | 32 | // ShortDesc returns short description for the command 33 | ShortDesc() string 34 | 35 | // LongDesc returns full description for the command 36 | LongDesc() string 37 | 38 | // Actions returns the list of actions supported by the command 39 | Actions() []ExpActionCommandSpec 40 | 41 | // Flags returns the command flags 42 | Flags() []ExpFlagSpec 43 | 44 | // SetFlags 45 | SetFlags(flags []ExpFlagSpec) 46 | } 47 | 48 | // ExpActionCommandSpec defines the action command interface for the experimental plugin 49 | type ExpActionCommandSpec interface { 50 | // Name returns the action name 51 | Name() string 52 | 53 | // Aliases returns command alias names 54 | Aliases() []string 55 | 56 | // ShortDesc returns short description for the action 57 | ShortDesc() string 58 | 59 | // LongDesc returns full description for the action 60 | LongDesc() string 61 | 62 | // SetLongDesc 63 | SetLongDesc(longDesc string) 64 | 65 | // Matchers returns the list of matchers supported by the action 66 | Matchers() []ExpFlagSpec 67 | 68 | // Flags returns the list of flags supported by the action 69 | Flags() []ExpFlagSpec 70 | 71 | // Example returns command example 72 | Example() string 73 | 74 | // Example returns command example 75 | SetExample(example string) 76 | 77 | // ExpExecutor returns the action command ExpExecutor 78 | Executor() Executor 79 | 80 | // SetExecutor 81 | SetExecutor(executor Executor) 82 | 83 | // Programs executed 84 | Programs() []string 85 | 86 | // Scenario categories 87 | Categories() []string 88 | 89 | // SetCategories 90 | SetCategories(categories []string) 91 | 92 | // process is hang up 93 | ProcessHang() bool 94 | } 95 | 96 | type ExpFlagSpec interface { 97 | // FlagName returns the flag FlagName 98 | FlagName() string 99 | // FlagDesc returns the flag description 100 | FlagDesc() string 101 | // FlagNoArgs returns true if the flag is bool type 102 | FlagNoArgs() bool 103 | // FlagRequired returns true if the flag is necessary when creating experiment 104 | FlagRequired() bool 105 | // FlagRequiredWhenDestroyed returns true if the flag is necessary when destroying experiment 106 | FlagRequiredWhenDestroyed() bool 107 | // FlagDefault return the flag Defaule 108 | FlagDefault() string 109 | } 110 | 111 | // ExpFlag defines the action flag 112 | type ExpFlag struct { 113 | // Name returns the flag FlagName 114 | Name string `yaml:"name"` 115 | 116 | // Desc returns the flag description 117 | Desc string `yaml:"desc"` 118 | 119 | // NoArgs means no arguments 120 | NoArgs bool `yaml:"noArgs"` 121 | 122 | // Required means necessary or not 123 | Required bool `yaml:"required"` 124 | // RequiredWhenDestroyed is true if the flag is necessary when destroying experiment 125 | RequiredWhenDestroyed bool `yaml:"requiredWhenDestroyed"` 126 | 127 | // default value 128 | Default string `yaml:"default,omitempty"` 129 | } 130 | 131 | func (f *ExpFlag) FlagName() string { 132 | return f.Name 133 | } 134 | 135 | func (f *ExpFlag) FlagDesc() string { 136 | return f.Desc 137 | } 138 | 139 | func (f *ExpFlag) FlagNoArgs() bool { 140 | return f.NoArgs 141 | } 142 | 143 | func (f *ExpFlag) FlagRequired() bool { 144 | return f.Required 145 | } 146 | 147 | func (f *ExpFlag) FlagRequiredWhenDestroyed() bool { 148 | return f.RequiredWhenDestroyed 149 | } 150 | 151 | func (f *ExpFlag) FlagDefault() string { 152 | return f.Default 153 | } 154 | 155 | // BaseExpModelCommandSpec defines the common struct of the implementation of ExpModelCommandSpec 156 | type BaseExpModelCommandSpec struct { 157 | ExpScope string 158 | ExpActions []ExpActionCommandSpec 159 | ExpFlags []ExpFlagSpec 160 | } 161 | 162 | // Scope default value is "" means localhost 163 | func (b *BaseExpModelCommandSpec) Scope() string { 164 | return "" 165 | } 166 | 167 | func (b *BaseExpModelCommandSpec) Actions() []ExpActionCommandSpec { 168 | return b.ExpActions 169 | } 170 | 171 | func (b *BaseExpModelCommandSpec) Flags() []ExpFlagSpec { 172 | return b.ExpFlags 173 | } 174 | 175 | func (b *BaseExpModelCommandSpec) SetFlags(flags []ExpFlagSpec) { 176 | b.ExpFlags = flags 177 | } 178 | 179 | // BaseExpActionCommandSpec defines the common struct of the implementation of ExpActionCommandSpec 180 | type BaseExpActionCommandSpec struct { 181 | ActionMatchers []ExpFlagSpec 182 | ActionFlags []ExpFlagSpec 183 | ActionExecutor Executor 184 | ActionLongDesc string 185 | ActionExample string 186 | ActionPrograms []string 187 | ActionCategories []string 188 | ActionProcessHang bool 189 | } 190 | 191 | func (b *BaseExpActionCommandSpec) Matchers() []ExpFlagSpec { 192 | return b.ActionMatchers 193 | } 194 | 195 | func (b *BaseExpActionCommandSpec) Flags() []ExpFlagSpec { 196 | return b.ActionFlags 197 | } 198 | 199 | func (b *BaseExpActionCommandSpec) Executor() Executor { 200 | return b.ActionExecutor 201 | } 202 | 203 | func (b *BaseExpActionCommandSpec) SetExecutor(executor Executor) { 204 | b.ActionExecutor = executor 205 | } 206 | 207 | func (b *BaseExpActionCommandSpec) SetLongDesc(longDesc string) { 208 | b.ActionLongDesc = longDesc 209 | } 210 | 211 | func (b *BaseExpActionCommandSpec) SetExample(example string) { 212 | b.ActionExample = example 213 | } 214 | 215 | func (b *BaseExpActionCommandSpec) Example() string { 216 | return b.ActionExample 217 | } 218 | 219 | func (b *BaseExpActionCommandSpec) Programs() []string { 220 | return b.ActionPrograms 221 | } 222 | 223 | func (b *BaseExpActionCommandSpec) Categories() []string { 224 | return b.ActionCategories 225 | } 226 | 227 | func (b *BaseExpActionCommandSpec) SetCategories(categories []string) { 228 | b.ActionCategories = categories 229 | } 230 | 231 | func (b *BaseExpActionCommandSpec) ProcessHang() bool { 232 | return b.ActionProcessHang 233 | } 234 | 235 | // ActionModel for yaml file 236 | type ActionModel struct { 237 | ActionName string `yaml:"action"` 238 | ActionAliases []string `yaml:"aliases,flow,omitempty"` 239 | ActionShortDesc string `yaml:"shortDesc"` 240 | ActionLongDesc string `yaml:"longDesc"` 241 | ActionMatchers []ExpFlag `yaml:"matchers,omitempty"` 242 | ActionFlags []ExpFlag `yaml:"flags,omitempty"` 243 | ActionExample string `yaml:"example"` 244 | executor Executor 245 | ActionPrograms []string `yaml:"programs,omitempty"` 246 | ActionCategories []string `yaml:"categories,omitempty"` 247 | ActionProcessHang bool `yaml:"actionProcessHang"` 248 | } 249 | 250 | func (am *ActionModel) Programs() []string { 251 | return am.ActionPrograms 252 | } 253 | 254 | func (am *ActionModel) SetExample(example string) { 255 | am.ActionExample = example 256 | } 257 | 258 | func (am *ActionModel) Example() string { 259 | return am.ActionExample 260 | } 261 | 262 | func (am *ActionModel) SetExecutor(executor Executor) { 263 | am.executor = executor 264 | } 265 | 266 | func (am *ActionModel) Executor() Executor { 267 | return am.executor 268 | } 269 | 270 | func (am *ActionModel) Name() string { 271 | return am.ActionName 272 | } 273 | 274 | func (am *ActionModel) Aliases() []string { 275 | return am.ActionAliases 276 | } 277 | 278 | func (am *ActionModel) ShortDesc() string { 279 | return am.ActionShortDesc 280 | } 281 | 282 | func (am *ActionModel) SetLongDesc(longDesc string) { 283 | am.ActionLongDesc = longDesc 284 | } 285 | 286 | func (am *ActionModel) LongDesc() string { 287 | return am.ActionLongDesc 288 | } 289 | 290 | func (am *ActionModel) Matchers() []ExpFlagSpec { 291 | flags := make([]ExpFlagSpec, 0) 292 | for idx := range am.ActionMatchers { 293 | flags = append(flags, &am.ActionMatchers[idx]) 294 | } 295 | return flags 296 | } 297 | 298 | func (am *ActionModel) Flags() []ExpFlagSpec { 299 | flags := make([]ExpFlagSpec, 0) 300 | for idx := range am.ActionFlags { 301 | flags = append(flags, &am.ActionFlags[idx]) 302 | } 303 | return flags 304 | } 305 | 306 | func (am *ActionModel) Categories() []string { 307 | return am.ActionCategories 308 | } 309 | 310 | func (am *ActionModel) SetCategories(categories []string) { 311 | am.ActionCategories = categories 312 | } 313 | 314 | func (am *ActionModel) ProcessHang() bool { 315 | return am.ActionProcessHang 316 | } 317 | 318 | type ExpPrepareModel struct { 319 | PrepareType string `yaml:"type"` 320 | PrepareFlags []ExpFlag `yaml:"flags"` 321 | PrepareRequired bool `yaml:"required"` 322 | } 323 | 324 | type ExpCommandModel struct { 325 | ExpName string `yaml:"target"` 326 | ExpShortDesc string `yaml:"shortDesc"` 327 | ExpLongDesc string `yaml:"longDesc"` 328 | ExpActions []ActionModel `yaml:"actions"` 329 | ExpExecutor Executor `yaml:"-"` 330 | ExpFlags []ExpFlag `yaml:"flags,omitempty"` 331 | ExpScope string `yaml:"scope"` 332 | ExpPrepareModel ExpPrepareModel `yaml:"prepare,omitempty"` 333 | ExpSubTargets []string `yaml:"subTargets,flow,omitempty"` 334 | } 335 | 336 | func (ecm *ExpCommandModel) Scope() string { 337 | return ecm.ExpScope 338 | } 339 | 340 | func (ecm *ExpCommandModel) Name() string { 341 | return ecm.ExpName 342 | } 343 | 344 | func (ecm *ExpCommandModel) ShortDesc() string { 345 | return ecm.ExpShortDesc 346 | } 347 | 348 | func (ecm *ExpCommandModel) LongDesc() string { 349 | return ecm.ExpLongDesc 350 | } 351 | 352 | func (ecm *ExpCommandModel) Actions() []ExpActionCommandSpec { 353 | specs := make([]ExpActionCommandSpec, 0) 354 | for idx := range ecm.ExpActions { 355 | if ecm.ExpExecutor != nil { 356 | ecm.ExpActions[idx].executor = ecm.ExpExecutor 357 | } 358 | specs = append(specs, &ecm.ExpActions[idx]) 359 | } 360 | return specs 361 | } 362 | 363 | func (ecm *ExpCommandModel) Flags() []ExpFlagSpec { 364 | flags := make([]ExpFlagSpec, 0) 365 | for idx := range ecm.ExpFlags { 366 | flags = append(flags, &ecm.ExpFlags[idx]) 367 | } 368 | return flags 369 | } 370 | 371 | func (ecm *ExpCommandModel) SetFlags(flags []ExpFlagSpec) { 372 | expFlags := make([]ExpFlag, 0) 373 | for idx := range flags { 374 | expFlags = append(expFlags, *flags[idx].(*ExpFlag)) 375 | } 376 | ecm.ExpFlags = expFlags 377 | } 378 | 379 | type Models struct { 380 | Version string `yaml:"version"` 381 | Kind string `yaml:"kind"` 382 | Models []ExpCommandModel `yaml:"items"` 383 | } 384 | 385 | type Empty struct{} 386 | 387 | // ConvertExpMatchersToString returns the flag arguments for cli 388 | func ConvertExpMatchersToString(expModel *ExpModel, createExcludeKeyFunc func() map[string]Empty) string { 389 | matchers := "" 390 | excludeKeys := createExcludeKeyFunc() 391 | flags := expModel.ActionFlags 392 | if flags != nil && len(flags) > 0 { 393 | for name, value := range flags { 394 | // exclude unsupported key in blade 395 | if _, ok := excludeKeys[name]; ok { 396 | continue 397 | } 398 | if value == "" { 399 | continue 400 | } 401 | if strings.Contains(value, " ") { 402 | value = strings.ReplaceAll(value, " ", "@@##") 403 | } 404 | matchers = fmt.Sprintf(`%s --%s=%s`, matchers, name, value) 405 | } 406 | } 407 | return matchers 408 | } 409 | 410 | // ConvertCommandsToExpModel returns the ExpModel by action, target and flags 411 | func ConvertCommandsToExpModel(action, target, rules string) *ExpModel { 412 | model := &ExpModel{ 413 | Target: target, 414 | ActionName: action, 415 | ActionFlags: make(map[string]string, 0), 416 | } 417 | flags := strings.Split(rules, " ") 418 | for _, flag := range flags { 419 | keyAndValue := strings.SplitN(flag, "=", 2) 420 | if len(keyAndValue) != 2 { 421 | continue 422 | } 423 | key := keyAndValue[0][2:] 424 | model.ActionFlags[key] = strings.ReplaceAll(keyAndValue[1], "@@##", " ") 425 | } 426 | return model 427 | } 428 | 429 | // AddFlagsToModelSpec 430 | func AddFlagsToModelSpec(flagsFunc func() []ExpFlagSpec, expSpecs ...ExpModelCommandSpec) { 431 | flagSpecs := flagsFunc() 432 | for _, expSpec := range expSpecs { 433 | flags := expSpec.Flags() 434 | if flags == nil { 435 | flags = make([]ExpFlagSpec, 0) 436 | } 437 | flags = append(flags, flagSpecs...) 438 | expSpec.SetFlags(flags) 439 | } 440 | } 441 | 442 | // AddExecutorToModelSpec 443 | func AddExecutorToModelSpec(executor Executor, expSpecs ...ExpModelCommandSpec) { 444 | for _, expSpec := range expSpecs { 445 | actions := expSpec.Actions() 446 | if actions == nil { 447 | continue 448 | } 449 | for _, action := range actions { 450 | action.SetExecutor(executor) 451 | } 452 | } 453 | } 454 | --------------------------------------------------------------------------------