├── VERSION ├── examples ├── runners │ └── ansible │ │ ├── .gitignore │ │ ├── inventory │ │ ├── dependency-iperf3.yaml │ │ ├── dependency-pingparsing.yaml │ │ ├── README.md │ │ └── testdefinition.yaml └── outputs │ └── gochart │ └── README.md ├── docs ├── images │ └── favicon.png ├── config-examples.md ├── outputs │ └── sql-based.md ├── README.md └── demos.md ├── NOTICE ├── design ├── runners.md ├── basic-flow.mmd └── flow.mmd ├── CHANGELOG.md ├── .github └── workflows │ ├── test.yml │ ├── documentation.yml │ └── build_release.yml ├── .gitignore ├── pkg ├── util │ ├── time.go │ ├── string.go │ ├── slices.go │ ├── slices_test.go │ ├── file.go │ ├── pointers.go │ ├── names.go │ └── cast.go ├── ansible │ ├── const.go │ ├── inventory.go │ └── inventory_test.go ├── cmdtemplate │ ├── templater_test.go │ └── templater.go ├── k8sutil │ ├── labels.go │ ├── node.go │ ├── client.go │ └── k8sutil.go ├── config │ ├── file.go │ └── defaults.go ├── hostsfilter │ ├── hostfilter_test.go │ └── hostsfilter.go ├── models │ ├── pingparsing │ │ └── pingparsing.go │ └── iperf3 │ │ └── iperf3.go └── executor │ ├── test │ └── mock.go │ └── executor.go ├── runners ├── kubernetes │ ├── spec_test.go │ ├── kubernetes_test.go │ └── spec.go ├── mock │ ├── mock_test.go │ └── mock.go ├── runners.go └── ansible │ └── ansible_test.go ├── cmd ├── docgen │ ├── main.go │ └── api.go └── ancientt │ ├── ancientt_test.go │ └── imports.go ├── Dockerfile ├── .promu.yml ├── testers ├── iperf3 │ ├── iperf3_test.go │ └── iperf3.go ├── pingparsing │ ├── pingparsing_test.go │ └── pingparsing.go └── testers.go ├── mkdocs.yml ├── parsers ├── parsers.go ├── iperf3 │ └── iperf3.go └── pingparsing │ └── pingparsing.go ├── tests └── k8s │ └── client.go ├── outputs ├── excelize │ ├── excelize_test.go │ └── excelize.go ├── tests │ └── mockdata.go ├── gochart │ ├── gochart_test.go │ └── gochart.go ├── outputs.go ├── data_test.go ├── sqlite │ ├── sqlite_test.go │ └── sqlite.go ├── mysql │ ├── mysql_test.go │ └── mysql.go ├── dump │ └── dump.go ├── data.go └── csv │ └── csv.go ├── testdefinition.example.yaml ├── README.md ├── Makefile ├── CODE_OF_CONDUCT.md └── go.mod /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.9 2 | -------------------------------------------------------------------------------- /examples/runners/ansible/.gitignore: -------------------------------------------------------------------------------- 1 | ancientt-* -------------------------------------------------------------------------------- /docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudical-io/ancientt/HEAD/docs/images/favicon.png -------------------------------------------------------------------------------- /examples/runners/ansible/inventory: -------------------------------------------------------------------------------- 1 | [server] 2 | localhost ansible_connection=local 3 | [clients] 4 | localhost ansible_connection=local 5 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Ancientt - Automated Network testing tool 2 | Copyright 2019 Cloudical Deutschland GmbH 3 | Licensed under the Apache License, Version 2.0 4 | -------------------------------------------------------------------------------- /examples/outputs/gochart/README.md: -------------------------------------------------------------------------------- 1 | # Outputs: GoChart 2 | 3 | ![Example Go Chart Output for one host](example-results-gbps_per_second_retransmits.png) 4 | -------------------------------------------------------------------------------- /design/runners.md: -------------------------------------------------------------------------------- 1 | # Runners Design 2 | 3 | * Use Golang template for commands (/ command generation) 4 | * That the testers that generate the commands don't need to worry about IPs (right now) 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.9 / 2022-02-18 2 | 3 | * [BUGFIX] disable windows builds (command executor is not compatible) 4 | 5 | ## 0.2.8 / 2022-02-16 6 | 7 | * [BUGFIX] fix transformations not being correctly applied 8 | 9 | ## 0.2.7 / 2021-11-12 10 | 11 | * [BUGFIX] fix promu build config 12 | -------------------------------------------------------------------------------- /examples/runners/ansible/dependency-iperf3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | remote_user: root 4 | tasks: 5 | - name: Install dependencies for Ancientt 6 | package: 7 | name: "{{ item }}" 8 | state: present 9 | with_items: 10 | - iperf3 11 | - procps 12 | -------------------------------------------------------------------------------- /design/basic-flow.mmd: -------------------------------------------------------------------------------- 1 | graph TD; 2 | Start((Start))-->TestDefinitions[Parse TestDefinitions]; 3 | TestDefinitions-->ForeachTest[Foreach TestDefinition]; 4 | ForeachTest-->GetConfigForTestDef[Get Config for test]; 5 | GetConfigForTestDef-->|Execute test|Runner 6 | Runner-->ForeachTest 7 | ForeachTest-->NoMoreTests 8 | NoMoreTests(No more tests definitions?)-->End((End)); 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | submodules: true 16 | - uses: actions/setup-go@v2 17 | with: 18 | go-version: '^1.17' 19 | - name: Run tests 20 | run: | 21 | make test 22 | make promu 23 | make check_license 24 | -------------------------------------------------------------------------------- /examples/runners/ansible/dependency-pingparsing.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | remote_user: root 4 | tasks: 5 | - name: Host Python must be at least 3.5+ 6 | assert: 7 | that: "ansible_python_version is version_compare('3.5', '>=')" 8 | msg: | 9 | pingparsing requires at least python 3.5+ on th target machines 10 | - name: Install dependencies for Ancientt 11 | package: 12 | name: "{{ item }}" 13 | state: present 14 | with_items: 15 | - procps 16 | - name: Pip install pingparsing package 17 | pip: 18 | name: pingparsing 19 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | generate-and-publish: 8 | runs-on: ubuntu-latest 9 | container: 10 | image: "squidfunk/mkdocs-material:latest" 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | submodules: false 15 | - name: Build 16 | run: | 17 | mkdocs build --clean 18 | - name: Deploy to GitHub pages 19 | uses: peaceiris/actions-gh-pages@v3 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | publish_dir: ./site 23 | -------------------------------------------------------------------------------- /examples/runners/ansible/README.md: -------------------------------------------------------------------------------- 1 | # Runners: Ansible 2 | 3 | ```bash 4 | # Check the generated plan and confirm by typing 'yes' 5 | ancientt 6 | # To just print the plan 7 | ancientt --only-print-plan 8 | # Generate and execute the plan without user prompt 9 | ancientt --yes 10 | ``` 11 | 12 | ## Depdency Playbooks 13 | 14 | * [`depdendency-iperf3.yaml`](dependency-iperf3.yaml) - Installs `iperf3` and `procps` package for IPerf3 testing. 15 | * [`depdendency-pingparsing.yaml`](dependency-pingparsing.yaml) - Installs `pingparsing` (using `pip`) and `procps` package for IPerf3 testing. This also checks that Python 3.5+ is used for Ansible on the target hosts. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | 8 | # https://github.com/github/gitignore/blob/master/Global/Vim.gitignore 9 | ## Swap 10 | [._]*.s[a-v][a-z] 11 | !*.svg # comment out if you don't need vector files 12 | [._]*.sw[a-p] 13 | [._]s[a-rt-v][a-z] 14 | [._]ss[a-gi-z] 15 | [._]sw[a-p] 16 | 17 | ## Session 18 | Session.vim 19 | Sessionx.vim 20 | 21 | ## Temporary 22 | .netrwhist 23 | *~ 24 | ## Auto-generated tag files 25 | tags 26 | ## Persistent undo 27 | [._]*.un~ 28 | 29 | ## Ignore node_modules for mermaid cli 30 | /node_modules/ 31 | 32 | # Ancientt project related files 33 | /ancientt 34 | /testdefinition.yaml 35 | -------------------------------------------------------------------------------- /pkg/util/time.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | const ( 17 | // TimeDateFormat used for ancientt outputs 18 | TimeDateFormat = "2006-01-02T15:04:05-0700" 19 | ) 20 | -------------------------------------------------------------------------------- /runners/kubernetes/spec_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package kubernetes 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func TestGetPodSpec(t *testing.T) { 21 | // TODO 22 | } 23 | -------------------------------------------------------------------------------- /pkg/util/string.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | // IntToChar This function is converting integer to a rune character 17 | func IntToChar(i int) string { 18 | return string(rune('A' - 1 + i)) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/docgen/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The prometheus-operator 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 | package main 16 | 17 | import ( 18 | "os" 19 | ) 20 | 21 | func main() { 22 | switch os.Args[1] { 23 | case "api": 24 | printAPIDocs(os.Args[2]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/ansible/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ansible 15 | 16 | const ( 17 | // AnsibleCommand `ansible` command 18 | AnsibleCommand = "ansible" 19 | // AnsibleInventoryCommand `ansible-inventory` command 20 | AnsibleInventoryCommand = "ansible-inventory" 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/cmdtemplate/templater_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package cmdtemplate 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/cloudical-io/ancientt/testers" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestTemplate(t *testing.T) { 24 | task := &testers.Task{} 25 | variables := Variables{} 26 | err := Template(task, variables) 27 | assert.Nil(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /runners/mock/mock_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package mock 15 | 16 | import ( 17 | "fmt" 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestGenerateMockServers(t *testing.T) { 24 | mockServers := generateMockServers() 25 | assert.Equal(t, 10, len(mockServers)) 26 | assert.Equal(t, fmt.Sprintf(mockServerNamePattern, 5), mockServers[5].Name) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/util/slices.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | // UniqueStringSlice return deduplicated string slice from one or more ins. 17 | func UniqueStringSlice(ins ...[]string) []string { 18 | keys := make(map[string]bool) 19 | list := []string{} 20 | for _, in := range ins { 21 | for _, k := range in { 22 | if _, value := keys[k]; !value { 23 | keys[k] = true 24 | list = append(list, k) 25 | } 26 | } 27 | } 28 | return list 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: build_release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | build_release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | submodules: true 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: '^1.17' 16 | - name: Run tests 17 | run: | 18 | make test 19 | make promu 20 | make check_license 21 | - name: Build and release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: | 25 | promu crossbuild 26 | promu crossbuild tarballs 27 | promu checksum .tarballs 28 | promu release .tarballs 29 | - name: Build and push to GHCR.io 30 | uses: elgohr/Publish-Docker-Github-Action@3.04 31 | with: 32 | name: ${{ github.repository }} 33 | registry: ghcr.io 34 | username: cloudical-io 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | tag_names: true 37 | -------------------------------------------------------------------------------- /pkg/util/slices_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestUniqueStringSlice(t *testing.T) { 23 | in1 := []string{"ABC"} 24 | in2 := []string{"ABC", "XYZ"} 25 | out := []string{"ABC", "XYZ"} 26 | assert.Equal(t, out, UniqueStringSlice(in1, in2)) 27 | 28 | in1 = []string{"ABC", "XYZ"} 29 | in2 = []string{"ABC", "XYZ", "123"} 30 | out = []string{"ABC", "XYZ", "123"} 31 | assert.Equal(t, out, UniqueStringSlice(in1, in2)) 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/galexrt/container-toolbox:v20210915-101121-713 2 | 3 | ARG BUILD_DATE="N/A" 4 | ARG REVISION="N/A" 5 | 6 | ARG ANCIENTT_VERSION="N/A" 7 | 8 | LABEL org.opencontainers.image.authors="Alexander Trost " \ 9 | org.opencontainers.image.created="${BUILD_DATE}" \ 10 | org.opencontainers.image.title="cloudical-io/ancientt" \ 11 | org.opencontainers.image.description="A tool to automate network testing tools, like iperf3, in dynamic environments such as Kubernetes and more to come dynamic environments." \ 12 | org.opencontainers.image.documentation="https://github.com/cloudical-io/ancientt/blob/main/README.md" \ 13 | org.opencontainers.image.url="https://github.com/cloudical-io/ancientt" \ 14 | org.opencontainers.image.source="https://github.com/cloudical-io/ancientt" \ 15 | org.opencontainers.image.revision="${REVISION}" \ 16 | org.opencontainers.image.vendor="cloudical-io" \ 17 | org.opencontainers.image.version="${ANCIENTT_VERSION}" 18 | 19 | ADD .build/linux-amd64/ancientt /bin/ancientt 20 | 21 | RUN chmod 755 /bin/ancientt 22 | 23 | ENTRYPOINT ["/bin/ancientt"] 24 | 25 | CMD ["--help"] 26 | -------------------------------------------------------------------------------- /pkg/util/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | import ( 17 | "os" 18 | ) 19 | 20 | // WriteNewTruncFile write new or truncate existing file 21 | func WriteNewTruncFile(filename string, buffer []byte) error { 22 | f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // Manually close the file handler here, because it must be closed not deffered but 28 | if _, err := f.Write(buffer); err != nil { 29 | f.Close() 30 | return err 31 | } 32 | f.Close() 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/util/pointers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | // BoolTruePointer return bool true as a pointer 17 | func BoolTruePointer() *bool { 18 | out := true 19 | return &out 20 | } 21 | 22 | // BoolFalsePointer return bool true as a pointer 23 | func BoolFalsePointer() *bool { 24 | out := false 25 | return &out 26 | } 27 | 28 | // FloatPointer return a pointer to a given float 29 | func FloatPointer(in float64) *float64 { 30 | return &in 31 | } 32 | 33 | // Int64Pointer return a pointer to a given int64 34 | func Int64Pointer(in int64) *int64 { 35 | return &in 36 | } 37 | -------------------------------------------------------------------------------- /pkg/k8sutil/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package k8sutil 15 | 16 | const ( 17 | // TaskIDLabel label for the task-id 18 | TaskIDLabel = "ancientt/task-id" 19 | ) 20 | 21 | // GetLabels return a default set of labels for "any" object ancientt is going to create. 22 | func GetLabels() map[string]string { 23 | name := "ancientt" 24 | return map[string]string{ 25 | "app.kubernetes.io/part-of": name, 26 | "app.kubernetes.io/managed-by": name, 27 | "app.kubernetes.io/version": "0.0.1", 28 | } 29 | } 30 | 31 | // GetPodLabels default labels combined with additional labels for Pods. 32 | func GetPodLabels(podName string, taskName string) map[string]string { 33 | labels := GetLabels() 34 | labels["app.kubernetes.io/instance"] = podName 35 | labels[TaskIDLabel] = taskName 36 | return labels 37 | } 38 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | version: 1.17 3 | repository: 4 | path: github.com/cloudical-io/ancientt 5 | build: 6 | flags: -a -tags 'netgo static_build' 7 | ldflags: | 8 | -s 9 | -X github.com/prometheus/common/version.Version={{.Version}} 10 | -X github.com/prometheus/common/version.Revision={{.Revision}} 11 | -X github.com/prometheus/common/version.Branch={{.Branch}} 12 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 13 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 14 | binaries: 15 | - name: ancientt 16 | path: ./cmd/ancientt 17 | tarball: 18 | files: 19 | - LICENSE 20 | - NOTICE 21 | - systemd/ancientt.service 22 | - systemd/sysconfig.ancientt 23 | crossbuild: 24 | platforms: 25 | - linux/amd64 26 | - linux/386 27 | #- darwin/amd64 28 | #- darwin/386 29 | #- windows/amd64 30 | #- windows/386 31 | #- freebsd/amd64 32 | #- freebsd/386 33 | #- openbsd/amd64 34 | #- openbsd/386 35 | #- netbsd/amd64 36 | #- netbsd/386 37 | #- dragonfly/amd64 38 | #- linux/arm 39 | #- linux/arm64 40 | #- freebsd/arm 41 | #- openbsd/arm 42 | #- linux/mips64 43 | #- linux/mips64le 44 | #- netbsd/arm 45 | #- linux/ppc64 46 | #- linux/ppc64le 47 | -------------------------------------------------------------------------------- /pkg/util/names.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | import ( 17 | "fmt" 18 | "time" 19 | ) 20 | 21 | // PNameRole task role names type 22 | type PNameRole string 23 | 24 | const ( 25 | // PNameRoleClient Client role name 26 | PNameRoleClient PNameRole = "client" 27 | // PNameRoleServer Server role name 28 | PNameRoleServer PNameRole = "server" 29 | ) 30 | 31 | // GetPNameFromTask get a "persistent" name for a task 32 | // This is done by calculating the checksums of the used names. 33 | func GetPNameFromTask(round int, hostname string, command string, role PNameRole, testStartTime time.Time) string { 34 | return fmt.Sprintf("ancientt-%s-%s-%d", role, command, testStartTime.UnixNano()) 35 | } 36 | 37 | // GetTaskName get a task name 38 | func GetTaskName(tester string, testStartTime time.Time) string { 39 | return fmt.Sprintf("ancientt-%s-%d", tester, testStartTime.Unix()) 40 | } 41 | -------------------------------------------------------------------------------- /testers/iperf3/iperf3_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package iperf3 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/cloudical-io/ancientt/pkg/config" 20 | "github.com/cloudical-io/ancientt/testers" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestIPerf3Plan(t *testing.T) { 26 | tester, err := NewIPerf3Tester(nil, nil) 27 | assert.Nil(t, err) 28 | assert.NotNil(t, tester) 29 | 30 | env := &testers.Environment{ 31 | Hosts: &testers.Hosts{ 32 | Clients: map[string]*testers.Host{}, 33 | Servers: map[string]*testers.Host{}, 34 | }, 35 | } 36 | test := &config.Test{ 37 | Type: "iperf3", 38 | } 39 | 40 | plan, err := tester.Plan(env, test) 41 | assert.Nil(t, err) 42 | require.NotNil(t, plan) 43 | assert.Equal(t, "iperf3", plan.Tester) 44 | assert.Equal(t, 0, len(plan.AffectedServers)) 45 | assert.Equal(t, 0, len(plan.Commands)) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/k8sutil/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 The Rook Authors. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package k8sutil 15 | 16 | import ( 17 | corev1 "k8s.io/api/core/v1" 18 | ) 19 | 20 | // INFO This code has been taken from the Rook project due to Golang dependency and modified to suite ancientt's needs. 21 | // Original file https://github.com/rook/rook/blob/master/pkg/operator/k8sutil/node.go 22 | 23 | // NodeIsTolerable returns true if the node's taints are all tolerated by the given tolerations. 24 | // There is the option to ignore well known taints defined in WellKnownTaints. See WellKnownTaints 25 | // for more information. 26 | func NodeIsTolerable(node corev1.Node, tolerations []corev1.Toleration) bool { 27 | for _, taint := range node.Spec.Taints { 28 | isTolerated := false 29 | for _, toleration := range tolerations { 30 | if toleration.ToleratesTaint(&taint) { 31 | isTolerated = true 32 | break 33 | } 34 | } 35 | if !isTolerated { 36 | return false 37 | } 38 | } 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /testers/pingparsing/pingparsing_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package pingparsing 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/cloudical-io/ancientt/pkg/config" 20 | "github.com/cloudical-io/ancientt/testers" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestIPerf3Plan(t *testing.T) { 26 | tester, err := NewPingParsingTester(nil, nil) 27 | assert.Nil(t, err) 28 | assert.NotNil(t, tester) 29 | 30 | env := &testers.Environment{ 31 | Hosts: &testers.Hosts{ 32 | Clients: map[string]*testers.Host{}, 33 | Servers: map[string]*testers.Host{}, 34 | }, 35 | } 36 | test := &config.Test{ 37 | Type: "pingparsing", 38 | } 39 | 40 | plan, err := tester.Plan(env, test) 41 | assert.Nil(t, err) 42 | require.NotNil(t, plan) 43 | assert.Equal(t, "pingparsing", plan.Tester) 44 | assert.Equal(t, 0, len(plan.AffectedServers)) 45 | assert.Equal(t, 0, len(plan.Commands)) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/ancientt/ancientt_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | // TODO Add a simple but useful test to verify config loading and basic functionality 23 | 24 | func TestAncienttRunCommand(t *testing.T) { 25 | err := run(nil, []string{}) 26 | // Not nill because there is no config file named `testdefinition.yaml` where the current working directory is 27 | assert.NotNil(t, err) 28 | 29 | // TODO Generate temp dir and create basic `testdefinition.yaml` in it, switch cwd to tmp dir, no error should be returned then 30 | /* 31 | tmpFile, err := ioutil.TempFile(os.TempDir(), "ancienttgotests") 32 | require.Nil(t, err) 33 | // defer delete tmpfile 34 | 35 | err = run(nil, []string{}) 36 | // Not nill because there is no config file named `testdefinition.yaml` where the current working directory is 37 | assert.Nil(t, err) 38 | */ 39 | } 40 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: cloudical-io/ancientt Documentation 2 | repo_url: https://github.com/cloudical-io/ancientt 3 | site_author: Cloudical Deutschland GmbH 4 | site_description: "GitHub cloudical-io/ancientt project documentation" 5 | use_directory_urls: true 6 | copyright: 'Copyright (c) Cloudical Deutschland GmbH 2021' 7 | theme: 8 | name: material 9 | font: false 10 | favicon: images/favicon.png 11 | logo: images/favicon.png 12 | palette: 13 | scheme: 'slate' 14 | primary: 'deep purple' 15 | accent: 'lime' 16 | icon: 17 | repo: fontawesome/brands/github 18 | features: 19 | - instant 20 | - navigation.expand 21 | plugins: 22 | - search 23 | # Removed in material mkdocs 6.0 24 | # See https://squidfunk.github.io/mkdocs-material/deprecations/#bundled-plugins 25 | #- awesome-pages 26 | #- git-revision-date-localized 27 | - minify: 28 | minify_html: true 29 | minify_js: true 30 | htmlmin_opts: 31 | remove_comments: true 32 | #js_files: [] 33 | - redirects: 34 | redirect_maps: {} 35 | markdown_extensions: 36 | - attr_list 37 | - meta 38 | - toc: 39 | permalink: true 40 | - tables 41 | - nl2br 42 | - admonition 43 | - pymdownx.details 44 | - pymdownx.highlight: 45 | use_pygments: true 46 | linenums: true 47 | - pymdownx.inlinehilite 48 | - pymdownx.tasklist 49 | - pymdownx.keys 50 | - pymdownx.magiclink 51 | - pymdownx.mark 52 | # Style is broken 53 | #- pymdownx.progressbar 54 | - pymdownx.snippets 55 | - pymdownx.superfences 56 | #- pymdownx.tabbed 57 | -------------------------------------------------------------------------------- /pkg/config/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package config 15 | 16 | import ( 17 | "io/ioutil" 18 | "os" 19 | 20 | "github.com/creasty/defaults" 21 | "gopkg.in/go-playground/validator.v9" 22 | "gopkg.in/yaml.v2" 23 | ) 24 | 25 | var validate *validator.Validate 26 | 27 | // Load load the given config file 28 | func Load(cfgFile string) (*Config, error) { 29 | file, err := os.Open(cfgFile) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | content, err := ioutil.ReadAll(file) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | cfg := New() 40 | 41 | if err := yaml.Unmarshal(content, cfg); err != nil { 42 | return nil, err 43 | } 44 | 45 | // Set defaults in the config struct 46 | if err := defaults.Set(cfg); err != nil { 47 | return nil, err 48 | } 49 | 50 | // Validate config struct 51 | validate = validator.New() 52 | if err := validate.Struct(cfg); err != nil { 53 | //validationErrors := err.(validator.ValidationErrors) 54 | return nil, err 55 | } 56 | 57 | return cfg, nil 58 | } 59 | -------------------------------------------------------------------------------- /examples/runners/ansible/testdefinition.yaml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | runner: 3 | name: ansible 4 | ansible: 5 | inventoryFilePath: ./inventory 6 | groups: 7 | server: "server" 8 | clients: "clients" 9 | ansibleCommand: ansible 10 | ansibleInventoryCommand: ansible-inventory 11 | commandRetries: 10 12 | parallelHostFactCalls: 7 13 | tests: 14 | - name: iperf3-all-to-all 15 | type: iperf3 16 | transformations: 17 | - source: "bits_per_second" 18 | destination: "gigabits_per_second" 19 | action: "add" 20 | modifier: 100000000 21 | modifierAction: "division" 22 | outputs: 23 | - name: gochart 24 | goChart: 25 | filePath: . 26 | namePattern: "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}-{{ .Data.ServerHost }}_{{ .Data.ClientHost }}-{{ .Extra.Rows }}.png" 27 | graphs: 28 | - timeColumn: start 29 | dataRows: 30 | - bits_per_second,retransmits 31 | withLinearRegression: true 32 | - name: csv 33 | csv: 34 | filePath: . 35 | namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.csv' 36 | runOptions: 37 | continueOnError: true 38 | rounds: 1 39 | interval: 10s 40 | mode: "sequential" 41 | parallelcount: 1 42 | # This hosts section would cause iperf3 to be run from all hosts to the hosts selected in the `destinations` section 43 | # Each entry will be merged into one list 44 | hosts: 45 | clients: 46 | - name: all-hosts 47 | all: true 48 | servers: 49 | - name: all-hosts 50 | all: true 51 | iperf3: 52 | udp: false 53 | additionalFlags: 54 | clients: [] 55 | server: [] 56 | -------------------------------------------------------------------------------- /cmd/ancientt/imports.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | // This file contains the imports for each output, parser, runner and tester. 17 | // Importing them from the, e.g., `outputs` pkg would cause a import cycle. 18 | 19 | import ( 20 | // Outputs 21 | _ "github.com/cloudical-io/ancientt/outputs/csv" 22 | _ "github.com/cloudical-io/ancientt/outputs/dump" 23 | _ "github.com/cloudical-io/ancientt/outputs/excelize" 24 | _ "github.com/cloudical-io/ancientt/outputs/gochart" 25 | _ "github.com/cloudical-io/ancientt/outputs/mysql" 26 | _ "github.com/cloudical-io/ancientt/outputs/sqlite" 27 | 28 | // Parsers 29 | _ "github.com/cloudical-io/ancientt/parsers/iperf3" 30 | _ "github.com/cloudical-io/ancientt/parsers/pingparsing" 31 | 32 | // Runners 33 | _ "github.com/cloudical-io/ancientt/runners/ansible" 34 | _ "github.com/cloudical-io/ancientt/runners/kubernetes" 35 | _ "github.com/cloudical-io/ancientt/runners/mock" 36 | 37 | // Testers 38 | _ "github.com/cloudical-io/ancientt/testers/iperf3" 39 | _ "github.com/cloudical-io/ancientt/testers/pingparsing" 40 | ) 41 | -------------------------------------------------------------------------------- /runners/runners.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package runners 15 | 16 | import ( 17 | "github.com/cloudical-io/ancientt/parsers" 18 | "github.com/cloudical-io/ancientt/pkg/config" 19 | "github.com/cloudical-io/ancientt/testers" 20 | ) 21 | 22 | // Factories contains the list of all available runners. 23 | // The runners can each then be created using the function saved in the map. 24 | var Factories = make(map[string]func(cfg *config.Config) (Runner, error)) 25 | 26 | // Runner is the interface a runner has to implement. 27 | type Runner interface { 28 | // GetHostsForTest return a list of hots from the Runner 29 | GetHostsForTest(test *config.Test) (*testers.Hosts, error) 30 | // Prepare run steps to prepare the Runner and / or itself to things. 31 | Prepare(runOpts config.RunOptions, plan *testers.Plan) error 32 | // Execute run / execute certain commands and so that are in the testers.Plan 33 | Execute(plan *testers.Plan, parser chan<- parsers.Input) error 34 | // Cleanup cleanup resources and other things after the commands from the testers.Plan ran. 35 | Cleanup(plan *testers.Plan) error 36 | } 37 | -------------------------------------------------------------------------------- /parsers/parsers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package parsers 15 | 16 | import ( 17 | "io" 18 | "time" 19 | 20 | "github.com/cloudical-io/ancientt/outputs" 21 | "github.com/cloudical-io/ancientt/pkg/config" 22 | ) 23 | 24 | // Factories contains the list of all available testers. 25 | // The parser can each then be created using the function saved in the map. 26 | var Factories = make(map[string]func(cfg *config.Config, test *config.Test) (Parser, error)) 27 | 28 | // Parser is the interface a parser has to implement 29 | type Parser interface { 30 | // Parse parse data from runners.Execute() func 31 | Parse(doneCh chan struct{}, inCh <-chan Input, dataCh chan<- outputs.Data) error 32 | // Summary send summary of parsed data to outputs.Output 33 | Summary(doneCh chan struct{}, inCh <-chan Input, dataCh chan<- outputs.Data) error 34 | } 35 | 36 | // Input structured parse 37 | type Input struct { 38 | TestStartTime time.Time 39 | TestTime time.Time 40 | Round int 41 | DataStream *io.ReadCloser 42 | Data []byte 43 | Tester string 44 | ServerHost string 45 | ClientHost string 46 | AdditionalInfo string 47 | } 48 | -------------------------------------------------------------------------------- /tests/k8s/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package k8s 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/client-go/kubernetes/fake" 23 | ) 24 | 25 | // NewClient return a new fake client 26 | func NewClient(nodes int) (*fake.Clientset, error) { 27 | clientset := fake.NewSimpleClientset() 28 | for i := 0; i < nodes; i++ { 29 | n := &v1.Node{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: fmt.Sprintf("node-%d", i), 32 | }, 33 | Status: v1.NodeStatus{ 34 | Conditions: []v1.NodeCondition{ 35 | v1.NodeCondition{ 36 | Type: v1.NodeReady, 37 | Status: v1.ConditionTrue, 38 | }, 39 | }, 40 | Addresses: []v1.NodeAddress{ 41 | { 42 | Type: v1.NodeExternalIP, 43 | Address: fmt.Sprintf("%d.%d.%d.%d", i, i, i, i), 44 | }, 45 | }, 46 | }, 47 | } 48 | ctx := context.TODO() 49 | _, err := clientset.CoreV1().Nodes().Create(ctx, n, metav1.CreateOptions{}) 50 | if err != nil { 51 | // Something is definitely wrong in the fake client 52 | return nil, err 53 | } 54 | } 55 | return clientset, nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/hostsfilter/hostfilter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package hostsfilter 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | 21 | "github.com/cloudical-io/ancientt/testers" 22 | ) 23 | 24 | func TestAntiAffinity(t *testing.T) { 25 | hosts := []*testers.Host{ 26 | { 27 | Name: "host-a", 28 | Labels: map[string]string{ 29 | "foo": "bar", 30 | }, 31 | }, 32 | { 33 | Name: "host-b", 34 | Labels: map[string]string{ 35 | "foo": "bar", 36 | }, 37 | }, 38 | { 39 | Name: "host-c", 40 | Labels: map[string]string{ 41 | "foo": "notbar", 42 | }, 43 | }, 44 | { 45 | Name: "host-d", 46 | Labels: map[string]string{ 47 | "bar": "foo", 48 | }, 49 | }, 50 | } 51 | antiAffinityFilter := []string{ 52 | "foo", 53 | } 54 | 55 | filteredHosts := checkAntiAffinity(hosts, antiAffinityFilter) 56 | assert.Equal(t, 3, len(filteredHosts)) 57 | 58 | // host-b will not be in the list because `host-a` has the same anti affinity label as `host-a` 59 | assert.Equal(t, "host-a", filteredHosts[0].Name) 60 | assert.Equal(t, "host-c", filteredHosts[1].Name) 61 | assert.Equal(t, "host-d", filteredHosts[2].Name) 62 | } 63 | -------------------------------------------------------------------------------- /outputs/excelize/excelize_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package excelize 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path" 20 | "testing" 21 | 22 | "github.com/cloudical-io/ancientt/outputs/tests" 23 | "github.com/cloudical-io/ancientt/pkg/config" 24 | "github.com/creasty/defaults" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestDo(t *testing.T) { 30 | table := tests.GenerateMockTableData(2) 31 | 32 | tempDir := os.TempDir() 33 | outName := fmt.Sprintf("ancientt-test-%s.xlsx", t.Name()) 34 | tmpOutFile := path.Join(tempDir, outName) 35 | defer os.Remove(tmpOutFile) 36 | 37 | outCfg := &config.Output{ 38 | Excelize: &config.Excelize{ 39 | FilePath: config.FilePath{ 40 | FilePath: tempDir, 41 | NamePattern: outName, 42 | }, 43 | }, 44 | } 45 | require.Nil(t, defaults.Set(outCfg)) 46 | 47 | e, err := NewExcelizeOutput(nil, outCfg) 48 | assert.Nil(t, err) 49 | err = e.Do(table) 50 | assert.Nil(t, err) 51 | 52 | fInfo, err := os.Stat(tmpOutFile) 53 | require.Nil(t, err) 54 | require.NotNil(t, fInfo) 55 | 56 | // For now just check that the file size is bigger than 0 57 | assert.True(t, fInfo.Size() > 0) 58 | // TODO Add more sophisticated validation of file content 59 | } 60 | -------------------------------------------------------------------------------- /pkg/models/pingparsing/pingparsing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package pingparsing 15 | 16 | // ClientResults PingParsing Client Result map 17 | type ClientResults map[string]PingResult 18 | 19 | // PingResult Ping target client results structure 20 | type PingResult struct { 21 | Destination string `json:"destination"` 22 | PacketTransmit int64 `json:"packet_transmit"` 23 | PacketReceive int64 `json:"packet_receive"` 24 | PacketLossRate float64 `json:"packet_loss_rate"` 25 | PacketLossCount int64 `json:"packet_loss_count"` 26 | RTTMin float64 `json:"rtt_min"` 27 | RTTAvg float64 `json:"rtt_avg"` 28 | RTTMax float64 `json:"rtt_max"` 29 | RTTMDev float64 `json:"rtt_mdev"` 30 | PacketDuplicateRate float64 `json:"packet_duplicate_rate"` 31 | PacketDuplicateCount int64 `json:"packet_duplicate_count"` 32 | ICMPReplies []ICMPReply `json:"icmp_replies"` 33 | } 34 | 35 | // ICMPReply ICMP reply entry 36 | type ICMPReply struct { 37 | Timestamp string `json:"timestamp"` 38 | ICMPSeq int64 `json:"icmp_seq"` 39 | TTL int64 `json:"ttl"` 40 | Time float64 `json:"time"` 41 | Duplicate bool `json:"duplicate"` 42 | } 43 | -------------------------------------------------------------------------------- /outputs/tests/mockdata.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package tests 15 | 16 | import ( 17 | "math/rand" 18 | "time" 19 | 20 | "github.com/cloudical-io/ancientt/outputs" 21 | ) 22 | 23 | // GenerateMockTableData generate some mock DataTable data for testing purposes 24 | func GenerateMockTableData(length int) outputs.Data { 25 | table := &outputs.Table{ 26 | Headers: []*outputs.Row{ 27 | {Value: "isthisfloat64"}, 28 | {Value: "iamafloat64part2"}, 29 | {Value: "isthisinteger64"}, 30 | {Value: "isittrue"}, 31 | {Value: "data"}, 32 | {Value: "interval"}, 33 | }, 34 | Rows: [][]*outputs.Row{}, 35 | } 36 | 37 | s := rand.NewSource(time.Now().UnixNano()) 38 | r := rand.New(s) 39 | 40 | for i := 0; i < length; i++ { 41 | f := float64(i) 42 | r := []*outputs.Row{ 43 | {Value: (r.Float64() * f) + f}, 44 | {Value: (r.Float64() * f) + f}, 45 | {Value: int64(r.Intn(99999))}, 46 | {Value: true}, 47 | {Value: "data"}, 48 | {Value: i}, 49 | } 50 | table.Rows = append(table.Rows, r) 51 | } 52 | 53 | return outputs.Data{ 54 | AdditionalInfo: "mock data generated", 55 | ClientHost: "host1", 56 | ServerHost: "host2", 57 | TestStartTime: time.Now(), 58 | TestTime: time.Now(), 59 | Tester: "foobar", 60 | Data: table, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/cmdtemplate/templater.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package cmdtemplate 15 | 16 | import ( 17 | "bytes" 18 | "text/template" 19 | 20 | "github.com/cloudical-io/ancientt/testers" 21 | ) 22 | 23 | // Variables variables used for templating 24 | type Variables struct { 25 | ServerAddressV4 string 26 | ServerAddressV6 string 27 | ServerPort int32 28 | } 29 | 30 | // Template template a given cmd and args with the given host information struct 31 | func Template(task *testers.Task, variables Variables) error { 32 | templatedArgs := []string{} 33 | 34 | var err error 35 | task.Command, err = templateString(task.Command, variables) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | for _, arg := range task.Args { 41 | arg, err = templateString(arg, variables) 42 | if err != nil { 43 | return err 44 | } 45 | templatedArgs = append(templatedArgs, arg) 46 | } 47 | task.Args = templatedArgs 48 | return nil 49 | } 50 | 51 | // templateString execute a given template with the variables given 52 | func templateString(in string, variable interface{}) (string, error) { 53 | t, err := template.New("main").Parse(in) 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | var out bytes.Buffer 59 | if err = t.ExecuteTemplate(&out, "main", variable); err != nil { 60 | return "", err 61 | } 62 | return out.String(), err 63 | } 64 | -------------------------------------------------------------------------------- /outputs/gochart/gochart_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package gochart 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path" 20 | "testing" 21 | 22 | "github.com/cloudical-io/ancientt/outputs/tests" 23 | "github.com/cloudical-io/ancientt/pkg/config" 24 | "github.com/cloudical-io/ancientt/pkg/util" 25 | "github.com/creasty/defaults" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestDo(t *testing.T) { 31 | table := tests.GenerateMockTableData(3) 32 | 33 | tempDir := os.TempDir() 34 | outName := fmt.Sprintf("ancientt-test-%s.png", t.Name()) 35 | tmpOutFile := path.Join(tempDir, outName) 36 | //defer os.Remove(tmpOutFile) 37 | 38 | outCfg := &config.Output{ 39 | GoChart: &config.GoChart{ 40 | FilePath: config.FilePath{ 41 | FilePath: tempDir, 42 | NamePattern: outName, 43 | }, 44 | Graphs: []*config.GoChartGraph{ 45 | { 46 | TimeColumn: "interval", 47 | RightY: "isthisfloat64", 48 | LeftY: "iamafloat64part2", 49 | WithSimpleMovingAverage: util.BoolTruePointer(), 50 | }, 51 | }, 52 | }, 53 | } 54 | require.Nil(t, defaults.Set(outCfg)) 55 | 56 | e, err := NewGoChartOutput(nil, outCfg) 57 | assert.Nil(t, err) 58 | err = e.Do(table) 59 | assert.Nil(t, err) 60 | 61 | fInfo, err := os.Stat(tmpOutFile) 62 | require.Nil(t, err) 63 | require.NotNil(t, fInfo) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/util/cast.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package util 15 | 16 | import ( 17 | "fmt" 18 | ) 19 | 20 | // CastToString cast interface{} to string 21 | func CastToString(val interface{}) string { 22 | if cVal, ok := val.(float32); ok { 23 | return fmt.Sprintf("%f", cVal) 24 | } else if cVal, ok := val.(float64); ok { 25 | return fmt.Sprintf("%f", cVal) 26 | } else if cVal, ok := val.(int); ok { 27 | return fmt.Sprintf("%d", cVal) 28 | } else if cVal, ok := val.(int8); ok { 29 | return fmt.Sprintf("%d", cVal) 30 | } else if cVal, ok := val.(int16); ok { 31 | return fmt.Sprintf("%d", cVal) 32 | } else if cVal, ok := val.(int32); ok { 33 | return fmt.Sprintf("%d", cVal) 34 | } else if cVal, ok := val.(int64); ok { 35 | return fmt.Sprintf("%d", cVal) 36 | } 37 | return fmt.Sprintf("%v", val) 38 | } 39 | 40 | // CastNumberToFloat64 cast a number from interface{} to float64 41 | func CastNumberToFloat64(in interface{}) (float64, error) { 42 | switch in.(type) { 43 | case float64: 44 | return in.(float64), nil 45 | case float32: 46 | return in.(float64), nil 47 | case int: 48 | return float64(in.(int)), nil 49 | case int8: 50 | return float64(in.(int8)), nil 51 | case int16: 52 | return float64(in.(int16)), nil 53 | case int32: 54 | return float64(in.(int32)), nil 55 | case int64: 56 | return float64(in.(int64)), nil 57 | default: 58 | return 0.0, fmt.Errorf("non number character can't be casted to float64") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/config-examples.md: -------------------------------------------------------------------------------- 1 | # Config Examples 2 | 3 | This page contains some example configurations. 4 | 5 | Be sure to also checkout the [Demos](demos.md) page for examples with example `ancientt` output, config and a snippet or whole file of the output results. 6 | 7 | ## Kubernetes + IPerf3 = CSV Output: IPerf3 test between all to all Nodes 8 | 9 | ```yaml 10 | version: '0' 11 | runner: 12 | #name: mock 13 | name: kubernetes 14 | kubernetes: 15 | # Assuming you are in your home directory 16 | kubeconfig: .kube/config 17 | image: quay.io/galexrt/container-toolbox:v20210915-101121-713 18 | namespace: ancientt 19 | timeouts: 20 | deleteTimeout: 20 21 | runningTimeout: 60 22 | succeedTimeout: 60 23 | hosts: 24 | ignoreSchedulingDisabled: true 25 | tolerations: [] 26 | tests: 27 | - name: iperf3-one-rand-to-one-rand 28 | type: iperf3 29 | transformations: 30 | - source: "bits_per_second" 31 | destination: "gigabits_per_second" 32 | action: "add" 33 | modifier: 100000000 34 | modifierAction: "division" 35 | outputs: 36 | - name: csv 37 | csv: 38 | filePath: . 39 | namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.csv' 40 | # If you want one CSV per server and client host test run, you can use the following: 41 | #namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}-{{ .Data.ServerHost }}_{{ .Data.ClientHost }}.csv' 42 | runOptions: 43 | continueOnError: true 44 | # If you wanna do the test(s) more than once in one go, set to higher than 1 45 | rounds: 1 46 | # Wait 10 seconds between each round 47 | interval: 10s 48 | mode: "sequential" 49 | parallelcount: 1 50 | # This hosts section would cause iperf3 to be run from all hosts to the hosts selected in the `destinations` section 51 | # Each entry will be merged into one list 52 | hosts: 53 | clients: 54 | - name: all-hosts 55 | all: true 56 | servers: 57 | - name: all-hosts 58 | all: true 59 | iperf3: 60 | udp: false 61 | duration: 10 62 | interval: 1 63 | additionalFlags: 64 | clients: [] 65 | server: [] 66 | ``` 67 | -------------------------------------------------------------------------------- /outputs/outputs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package outputs 15 | 16 | import ( 17 | "bytes" 18 | "html/template" 19 | 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | ) 22 | 23 | // Factories contains the list of all available outputs. 24 | // The outputs can each then be created using the function saved in the map. 25 | var Factories = make(map[string]func(cfg *config.Config, outCfg *config.Output) (Output, error)) 26 | 27 | // Output is the interface a output has to implement. 28 | type Output interface { 29 | // Do do output related work on the given Data 30 | Do(data Data) error 31 | // OutputFiles return a list of output files 32 | OutputFiles() []string 33 | // Close run "cleanup" / close tasks, e.g., close file handles and others 34 | Close() error 35 | } 36 | 37 | // GetFilenameFromPattern get filename from given pattern, data and extra data for templating. 38 | func GetFilenameFromPattern(pattern string, role string, data Data, extra map[string]interface{}) (string, error) { 39 | t, err := template.New("main").Parse(pattern) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | variables := struct { 45 | Role string 46 | Data Data 47 | TestStartTime int64 48 | TestTime int64 49 | Extra map[string]interface{} 50 | }{ 51 | Role: role, 52 | Data: data, 53 | TestStartTime: data.TestStartTime.Unix(), 54 | TestTime: data.TestTime.Unix(), 55 | Extra: extra, 56 | } 57 | 58 | var out bytes.Buffer 59 | if err = t.ExecuteTemplate(&out, "main", variables); err != nil { 60 | return "", err 61 | } 62 | return out.String(), nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/ansible/inventory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ansible 15 | 16 | import ( 17 | "encoding/json" 18 | ) 19 | 20 | /* 21 | // The _meta is ignored 22 | { 23 | "_meta": { 24 | "hostvars": {} 25 | }, 26 | "all": { 27 | "children": [ 28 | "clients", 29 | "server", 30 | "ungrouped" 31 | ] 32 | }, 33 | "clients": { 34 | "hosts": [ 35 | "server1", 36 | "server2" 37 | ] 38 | }, 39 | "server": { 40 | "hosts": [ 41 | "server4" 42 | ] 43 | } 44 | } 45 | */ 46 | 47 | // InventoryList Basic `ansible-inventory` JSON output structure 48 | type InventoryList map[string]HostGroup 49 | 50 | // HostGroup Host and groups of a group `ansible-inventory` JSON sub-structure 51 | type HostGroup struct { 52 | Children []string `json:"children"` 53 | Hosts []string `json:"hosts"` 54 | } 55 | 56 | // Parse raw JSON into 57 | func Parse(in []byte) (*InventoryList, error) { 58 | inv := &InventoryList{} 59 | 60 | if err := json.Unmarshal(in, inv); err != nil { 61 | return nil, err 62 | } 63 | 64 | return inv, nil 65 | } 66 | 67 | // GetHostsForGroup return resolved list of hosts for a given group name 68 | func (inv *InventoryList) GetHostsForGroup(group string) []string { 69 | hosts := []string{} 70 | 71 | for k, hg := range *inv { 72 | if k == group { 73 | if len(hg.Children) > 0 { 74 | for _, child := range hg.Children { 75 | hosts = append(hosts, inv.GetHostsForGroup(child)...) 76 | } 77 | } 78 | if len(hg.Hosts) > 0 { 79 | hosts = append(hosts, hg.Hosts...) 80 | } 81 | } 82 | } 83 | 84 | return hosts 85 | } 86 | -------------------------------------------------------------------------------- /docs/outputs/sql-based.md: -------------------------------------------------------------------------------- 1 | # SQL-based 2 | 3 | For SQL based outputs (e.g., `mysql`, `sqlite`) these are example queries for (the queries assume that the table name with the results is named `iperf3results`). Be sure to specify `iperf3results` as the table name in the SQL based output's config. 4 | 5 | > **NOTE** 6 | > 7 | > Using `mysql` output will by default autogenerate the tables necessary, unless the `autoCreateTables` has been set to `false. 8 | > 9 | > `sqlite output will always create the tables when they don't exist. 10 | 11 | ## `CREATE TABLE` 12 | 13 | > **NOTE** This is only necessary if you, e.g., want to import existing CSV results. 14 | 15 | ```sql 16 | CREATE TABLE `iperf3results` ( 17 | `test_time` varchar(100) COLLATE utf8_bin DEFAULT NULL, 18 | `round` int(11) DEFAULT NULL, 19 | `tester` varchar(100) COLLATE utf8_bin DEFAULT NULL, 20 | `server_host` varchar(100) COLLATE utf8_bin DEFAULT NULL, 21 | `client_host` varchar(100) COLLATE utf8_bin DEFAULT NULL, 22 | `socket` int(11) DEFAULT NULL, 23 | `start` float DEFAULT NULL, 24 | `end` float DEFAULT NULL, 25 | `seconds` float DEFAULT NULL, 26 | `bytes` bigint(20) DEFAULT NULL, 27 | `bits_per_second` float DEFAULT NULL, 28 | `retransmits` bigint(20) DEFAULT NULL, 29 | `snd_cwnd` bigint(20) DEFAULT NULL, 30 | `rtt` bigint(20) DEFAULT NULL, 31 | `rttvar` bigint(20) DEFAULT NULL, 32 | `pmtu` int(11) DEFAULT NULL, 33 | `omitted` tinyint(1) DEFAULT NULL, 34 | `iperf3_version` varchar(100) COLLATE utf8_bin DEFAULT NULL, 35 | `system_info` varchar(300) COLLATE utf8_bin DEFAULT NULL, 36 | `additional_info` varchar(255) COLLATE utf8_bin DEFAULT NULL 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=' '; 38 | ``` 39 | 40 | ## `SELECT` Queries 41 | 42 | ### Averaged Gbps grouped by `round`, `server_host` and `client_host` 43 | 44 | ```sql 45 | SELECT 46 | test_time, 47 | server_host, 48 | client_host, 49 | AVG(bits_per_second) AS bits_per_second_avg, 50 | AVG(bits_per_second / 1000000000) AS gbps_avg, 51 | AVG(rtt) AS rtt_avg, 52 | SUM(retransmits) AS total_retransmits 53 | FROM 54 | `iperf3results` 55 | WHERE 56 | server_host != client_host 57 | GROUP BY 58 | round, 59 | server_host, 60 | client_host 61 | ORDER BY 62 | gbps_avg DESC; 63 | ``` 64 | -------------------------------------------------------------------------------- /runners/kubernetes/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package kubernetes 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/cloudical-io/ancientt/pkg/config" 20 | "github.com/cloudical-io/ancientt/pkg/util" 21 | "github.com/cloudical-io/ancientt/tests/k8s" 22 | "github.com/creasty/defaults" 23 | "github.com/sirupsen/logrus" 24 | log "github.com/sirupsen/logrus" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | // TODO add tests 30 | 31 | func TestGetHostsForTest(t *testing.T) { 32 | // TODO 33 | clientset, err := k8s.NewClient(3) 34 | require.Nil(t, err) 35 | require.NotNil(t, clientset) 36 | 37 | conf := &config.RunnerKubernetes{} 38 | // Set defaults in the config struct 39 | require.Nil(t, defaults.Set(conf)) 40 | 41 | runner := &Kubernetes{ 42 | logger: log.WithFields(logrus.Fields{"runner": Name, "namespace": ""}), 43 | config: conf, 44 | k8sclient: clientset, 45 | } 46 | 47 | test := &config.Test{} 48 | hosts, err := runner.GetHostsForTest(test) 49 | require.Nil(t, err) 50 | assert.Equal(t, 0, len(hosts.Servers)) 51 | assert.Equal(t, 0, len(hosts.Clients)) 52 | 53 | test.Hosts.Servers = append(test.Hosts.Servers, config.Hosts{All: util.BoolTruePointer()}) 54 | test.Hosts.Clients = append(test.Hosts.Clients, config.Hosts{All: util.BoolTruePointer()}) 55 | hosts, err = runner.GetHostsForTest(test) 56 | require.Nil(t, err) 57 | assert.Equal(t, 3, len(hosts.Servers)) 58 | assert.Equal(t, 3, len(hosts.Clients)) 59 | 60 | test.Hosts.Servers[0] = config.Hosts{Count: 1, Random: util.BoolTruePointer()} 61 | hosts, err = runner.GetHostsForTest(test) 62 | require.Nil(t, err) 63 | assert.Equal(t, 1, len(hosts.Servers)) 64 | assert.Equal(t, 3, len(hosts.Clients)) 65 | } 66 | -------------------------------------------------------------------------------- /design/flow.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant User 3 | participant Ancientt 4 | participant Runner 5 | participant Tester 6 | participant Parser 7 | participant Output 8 | participant Data 9 | 10 | Note over User,Ancientt: Config provided by User 11 | User->>Ancientt: Start 12 | Ancientt->>Runner: Get host information 13 | loop Gather host info 14 | Runner->>Runner: Get Target information 15 | end 16 | Runner->>Ancientt: Return host information 17 | Ancientt->>Tester: Get target plan 18 | Tester->>Tester: Generate run plan from host information 19 | Tester->>Ancientt: Create and return run plan 20 | Ancientt->>User: Prompt user for confirmation 21 | alt cancels 22 | User->>Ancientt: Responds with "No" 23 | Ancientt->>Ancientt: Terminate execution 24 | else accepts 25 | User->>Ancientt: Responds with "Yes" 26 | Ancientt->>Runner: Execute plan 27 | loop Execute plan 28 | Runner->>Runner: Execute CMD(s) on target(s) 29 | Runner->>Runner: Receive output / results from target(s) 30 | Runner->>Parser: Send results for parsing 31 | alt more tests / targets to go? 32 | Runner->>Runner: Continue with next test / target 33 | else done 34 | Runner->>Runner: Close parser channel 35 | Runner->>Ancientt: Return 36 | end 37 | loop Parse results 38 | Parser->>Parser: Parse incoming Runner output 39 | Parser-->Data: Put data into `Data` interface format 40 | Parser->>Output: Send `Data` to Outputs 41 | Output->>Data: Apply user transformations 42 | Data-->Data: Transform results 43 | end 44 | end 45 | loop process data 46 | Parser->>Output: Receive Data 47 | Output->>Output: Process results 48 | Output->>Output: Write data in output format 49 | Note right of Output: If applicable, keep
list of written files 50 | alt data channel is open 51 | Output->>Ancientt: Continue / Wait for incoming data 52 | else data chanel is closed 53 | Output->>Ancientt: Return 54 | end 55 | end 56 | Ancientt->>User: Output success / error info 57 | end 58 | -------------------------------------------------------------------------------- /pkg/k8sutil/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package k8sutil 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | 21 | // Load all k8s client auth plugins 22 | _ "k8s.io/client-go/plugin/pkg/client/auth" 23 | 24 | "github.com/mitchellh/go-homedir" 25 | "k8s.io/client-go/kubernetes" 26 | "k8s.io/client-go/rest" 27 | "k8s.io/client-go/tools/clientcmd" 28 | ) 29 | 30 | // NewClient create a new Kubernetes clientset 31 | func NewClient(inClusterConfig bool, kubeconfig string) (kubernetes.Interface, error) { 32 | var k8sconfig *rest.Config 33 | if inClusterConfig { 34 | var err error 35 | k8sconfig, err = rest.InClusterConfig() 36 | if err != nil { 37 | return nil, fmt.Errorf("kubeconfig in-cluster configuration error. %+v", err) 38 | } 39 | } else { 40 | var kubeconfig string 41 | // Try to fallback to the `KUBECONFIG` env var 42 | if kubeconfig == "" { 43 | kubeconfig = os.Getenv("KUBECONFIG") 44 | } 45 | // If the `KUBECONFIG` is empty, default to home dir default kube config path 46 | if kubeconfig == "" { 47 | home, err := homedir.Dir() 48 | if err != nil { 49 | return nil, fmt.Errorf("kubeconfig unable to get home dir. %+v", err) 50 | } 51 | kubeconfig = filepath.Join(home, ".kube", "config") 52 | } 53 | var err error 54 | // This will simply use the current context in the kubeconfig 55 | k8sconfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 56 | if err != nil { 57 | return nil, fmt.Errorf("kubeconfig out-of-cluster configuration (%s) error. %+v", kubeconfig, err) 58 | } 59 | } 60 | 61 | // Create the clientset 62 | clientset, err := kubernetes.NewForConfig(k8sconfig) 63 | if err != nil { 64 | return nil, fmt.Errorf("kubernetes new client error. %+v", err) 65 | } 66 | return clientset, nil 67 | } 68 | -------------------------------------------------------------------------------- /outputs/data_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package outputs 15 | 16 | import ( 17 | "fmt" 18 | "testing" 19 | 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | "github.com/cloudical-io/ancientt/pkg/util" 22 | "github.com/k0kubun/pp" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestDataTableTransform(t *testing.T) { 27 | dataTable := Table{ 28 | Headers: []*Row{ 29 | {Value: "bits_per_second"}, 30 | {Value: "willremain"}, 31 | {Value: "replacedwithwillremain"}, 32 | }, 33 | Rows: [][]*Row{ 34 | { 35 | {Value: float64(123.0)}, 36 | {Value: "nope"}, 37 | {Value: int64(50)}, 38 | }, 39 | { 40 | {Value: int64(15)}, 41 | {Value: "nope"}, 42 | {Value: int64(30)}, 43 | }, 44 | { 45 | {Value: int64(15)}, 46 | {Value: "nope"}, 47 | {Value: int64(75)}, 48 | }, 49 | }, 50 | } 51 | 52 | transformations := []*config.Transformation{ 53 | { 54 | Action: config.TransformationActionAdd, 55 | Source: "bits_per_second", 56 | Destination: "gigabits_per_second", 57 | Modifier: util.FloatPointer(float64(100)), 58 | ModifierAction: config.ModifierActionDivison, 59 | }, 60 | { 61 | Source: "bits_per_second", 62 | Action: config.TransformationActionDelete, 63 | }, 64 | { 65 | Action: config.TransformationActionReplace, 66 | Source: "replacedwithwillremain", 67 | Destination: "tb_per_second", 68 | Modifier: util.FloatPointer(float64(1000)), 69 | ModifierAction: config.ModifierActionMultiply, 70 | }, 71 | } 72 | 73 | fmt.Println("BEFORE TRANSFORMATION:") 74 | pp.Println(dataTable) 75 | 76 | err := dataTable.Transform(transformations) 77 | assert.Nil(t, err) 78 | 79 | fmt.Println("===\nAFTER TRANSFORMATION:") 80 | pp.Println(dataTable) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/ansible/inventory_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ansible 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestGetHostsForTest(t *testing.T) { 24 | inv, err := Parse([]byte(`{ 25 | "_meta": { 26 | "hostvars": {} 27 | }, 28 | "all": { 29 | "children": [ 30 | "clients", 31 | "server", 32 | "ungrouped" 33 | ] 34 | }, 35 | "clients": { 36 | "hosts": [ 37 | "server1", 38 | "server2" 39 | ] 40 | }, 41 | "server": { 42 | "hosts": [ 43 | "server4" 44 | ] 45 | }, 46 | "test123": { 47 | "children": [ 48 | "all" 49 | ], 50 | "hosts": [ 51 | "server8", 52 | "server9" 53 | ] 54 | } 55 | }`)) 56 | require.NotNil(t, inv) 57 | require.Nil(t, err) 58 | 59 | all := inv.GetHostsForGroup("all") 60 | assert.Equal(t, 3, len(all)) 61 | assert.Contains(t, all, "server1") 62 | assert.Contains(t, all, "server2") 63 | assert.Contains(t, all, "server4") 64 | 65 | clients := inv.GetHostsForGroup("clients") 66 | assert.Equal(t, 2, len(clients)) 67 | assert.Contains(t, clients, "server1") 68 | assert.Contains(t, clients, "server2") 69 | assert.NotContains(t, clients, "server4") 70 | 71 | server := inv.GetHostsForGroup("server") 72 | assert.Equal(t, 1, len(server)) 73 | assert.NotContains(t, server, "server1") 74 | assert.NotContains(t, server, "server2") 75 | assert.Contains(t, server, "server4") 76 | 77 | meta := inv.GetHostsForGroup("_meta") 78 | assert.Equal(t, 0, len(meta)) 79 | 80 | test123 := inv.GetHostsForGroup("test123") 81 | assert.Equal(t, 5, len(test123)) 82 | assert.Contains(t, test123, "server1") 83 | assert.Contains(t, test123, "server2") 84 | assert.Contains(t, test123, "server4") 85 | assert.Contains(t, test123, "server8") 86 | assert.Contains(t, test123, "server9") 87 | } 88 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ancientt 2 | 3 | A tool to automate network testing tools, like iperf3, in dynamic environments such as Kubernetes and more to come dynamic environments. 4 | 5 | ## Features 6 | 7 | **TL;DR** A network test tool, like `iperf3` can be run in, e.g., Kubernetes, cluster from all-to-all Nodes. 8 | 9 | * Run network tests with the following projects: 10 | * `iperf3` 11 | * Soon other tools will be available as well, like `smokeping`. 12 | * Tests can be run through the following "runners": 13 | * Ansible (an inventory file is needed) 14 | * Kubernetes (a kubeconfig connected to a cluster) 15 | * Results of the network tests can be output in different formats: 16 | * CSV 17 | * Dump (uses `pp.Sprint()` ([GitHub k0kubun/pp](https://github.com/k0kubun/pp), dump pretty print library)) 18 | * Excel files (Excelize) 19 | * go-chart Charts (WIP) 20 | * MySQL 21 | * SQLite 22 | 23 | ## Usage 24 | 25 | Either [build (`go get`)](#building) or download the Ancientt executable. 26 | 27 | A config file containing test definitions must be given by flag `--testdefinition` (or short flag `-c`) or named `testdefinition.yaml` in the current directory. 28 | 29 | Below command will try loading `your-testdefinitions.yaml` as the test definitions config: 30 | 31 | ```shell 32 | # You can also use the short flag `-c 33 | ancientt --testdefinition your-testdefinitions.yaml 34 | ``` 35 | 36 | ## Demos 37 | 38 | See [Demos](docs/demos.md). 39 | 40 | ## Goals of this Project 41 | 42 | * A bit like Prometheus blackbox exporter which contains "definitions" for probes. The "tests" would be pluggable through a Golang interface. 43 | * "Runner" interface, e.g., for Kubernetes, Ansible, etc. The "runner" abstracts the "how it is run", e.g., for Kubernetes creates a Job, Ansible (download and) trigger a playbook to run the test. 44 | * Store result data in different formats, e.g., CSV, excel, MySQL 45 | * Up for discussion: graph database ([Dgraph](https://dgraph.io/)) and / or TSDB support 46 | * "Visualization" for humans, e.g., possibility to automatically draw "shiny" graphs from the results. 47 | 48 | ## Development 49 | 50 | **Golang version**: `v1.15` or higher (tested with `v1.15.2` on `linux/amd64`) 51 | 52 | ### Dependencies 53 | 54 | `go mod` is used to manage the depeendencies. 55 | 56 | ### Building 57 | 58 | Quickest way to just get ancientt built is to run the following command: 59 | 60 | ```bash 61 | go get -u github.com/cloudical-io/ancientt/cmd/ancientt 62 | ``` 63 | 64 | ## Licensing 65 | 66 | Ancientt is licensed under the Apache 2.0 License. 67 | -------------------------------------------------------------------------------- /runners/kubernetes/spec.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package kubernetes 15 | 16 | import ( 17 | "github.com/cloudical-io/ancientt/pkg/k8sutil" 18 | "github.com/cloudical-io/ancientt/pkg/util" 19 | "github.com/cloudical-io/ancientt/testers" 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | const ( 25 | clientsRole = "clients" 26 | serverRole = "server" 27 | ) 28 | 29 | func (k Kubernetes) getPodSpec(pName string, taskName string, task *testers.Task) *corev1.Pod { 30 | hostNetwork := false 31 | if k.config.HostNetwork != nil && *k.config.HostNetwork { 32 | hostNetwork = true 33 | } 34 | 35 | pod := &corev1.Pod{ 36 | ObjectMeta: metav1.ObjectMeta{ 37 | Annotations: k.config.Annotations, 38 | Labels: k8sutil.GetPodLabels(pName, taskName), 39 | Name: pName, 40 | Namespace: k.config.Namespace, 41 | }, 42 | Spec: corev1.PodSpec{ 43 | Containers: []corev1.Container{ 44 | { 45 | Name: "ancientt", 46 | Image: k.config.Image, 47 | Command: []string{task.Command}, 48 | Args: task.Args, 49 | Ports: k8sutil.PortsListToPorts(task.Ports), 50 | }, 51 | }, 52 | NodeSelector: map[string]string{ 53 | corev1.LabelHostname: task.Host.Name, 54 | }, 55 | HostNetwork: hostNetwork, 56 | RestartPolicy: corev1.RestartPolicyOnFailure, 57 | Tolerations: k.config.Hosts.Tolerations, 58 | TerminationGracePeriodSeconds: util.Int64Pointer(5), 59 | }, 60 | } 61 | 62 | return pod 63 | } 64 | 65 | func (k Kubernetes) applyServiceAccountToPod(p *corev1.Pod, role string) { 66 | if k.config.ServiceAccounts != nil { 67 | switch role { 68 | case serverRole: 69 | if k.config.ServiceAccounts.Server != "" { 70 | p.Spec.ServiceAccountName = k.config.ServiceAccounts.Server 71 | } 72 | case clientsRole: 73 | if k.config.ServiceAccounts.Clients != "" { 74 | p.Spec.ServiceAccountName = k.config.ServiceAccounts.Clients 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /outputs/sqlite/sqlite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package sqlite 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/DATA-DOG/go-sqlmock" 23 | "github.com/cloudical-io/ancientt/outputs" 24 | "github.com/cloudical-io/ancientt/outputs/tests" 25 | "github.com/cloudical-io/ancientt/pkg/config" 26 | "github.com/creasty/defaults" 27 | "github.com/jmoiron/sqlx" 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | func TestSQLite(t *testing.T) { 33 | // Create mock database 34 | db, mock, err := sqlmock.New() 35 | require.Nil(t, err) 36 | 37 | tempDir := os.TempDir() 38 | outName := fmt.Sprintf("ancientt-test-%s.sqlite3", t.Name()) 39 | outCfg := &config.Output{ 40 | SQLite: &config.SQLite{ 41 | FilePath: config.FilePath{ 42 | FilePath: tempDir, 43 | NamePattern: outName, 44 | }, 45 | }, 46 | } 47 | require.Nil(t, defaults.Set(outCfg)) 48 | 49 | // Generate mock Data with Table data 50 | data := tests.GenerateMockTableData(5) 51 | 52 | filename, err := outputs.GetFilenameFromPattern(outCfg.SQLite.NamePattern, "", data, nil) 53 | require.Nil(t, err) 54 | 55 | tableName, err := outputs.GetFilenameFromPattern(outCfg.SQLite.TableNamePattern, "", data, nil) 56 | require.Nil(t, err) 57 | 58 | // Because the db driver already exists, the "CREATE TABLE" query is not triggered 59 | // Match the two inserts 60 | mock.ExpectExec(fmt.Sprintf("INSERT INTO %s", tableName)) 61 | mock.ExpectClose() 62 | 63 | dbx := sqlx.NewDb(db, "sqlmock") 64 | 65 | m, err := NewSQLiteOutput(nil, outCfg) 66 | assert.Nil(t, err) 67 | 68 | // Cast the outputs.Output to the SQLite so we can manipulate the object 69 | ms, ok := m.(SQLite) 70 | require.True(t, ok) 71 | 72 | outPath := filepath.Join(outCfg.SQLite.FilePath.FilePath, filename) 73 | ms.dbCons[outPath] = dbx 74 | 75 | // Do() and Close() to run the database flow 76 | err = m.Do(data) 77 | assert.NotNil(t, err) 78 | err = m.Close() 79 | assert.Nil(t, err) 80 | 81 | // Check if all expectations were met 82 | err = mock.ExpectationsWereMet() 83 | assert.Nil(t, err) 84 | 85 | // TODO Verify data written to database 86 | } 87 | -------------------------------------------------------------------------------- /outputs/mysql/mysql_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package mysql 15 | 16 | import ( 17 | "fmt" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/cloudical-io/ancientt/outputs" 22 | "github.com/cloudical-io/ancientt/outputs/tests" 23 | "github.com/cloudical-io/ancientt/pkg/config" 24 | "github.com/creasty/defaults" 25 | "github.com/jmoiron/sqlx" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestMySQL(t *testing.T) { 31 | // Create mock database 32 | db, mock, err := sqlmock.New() 33 | require.Nil(t, err) 34 | 35 | outCfg := &config.Output{ 36 | Name: "TEST", 37 | MySQL: &config.MySQL{ 38 | // Set a non-empty DSN, otherwise we get an error 39 | DSN: "username:password@127.0.0.1/mydb", 40 | }, 41 | } 42 | require.Nil(t, defaults.Set(outCfg)) 43 | 44 | // Generate mock Data with Table data 45 | data := tests.GenerateMockTableData(5) 46 | 47 | tableName, err := outputs.GetFilenameFromPattern(defaultTableNamePattern, "", data, nil) 48 | require.Nil(t, err) 49 | 50 | // Because the db driver already exists, the "CREATE TABLE" query is not triggered 51 | // Match the two inserts 52 | mock.ExpectExec(fmt.Sprintf("SELECT 1 FROM `%s` LIMIT 1", tableName)).WillReturnError(fmt.Errorf("table does not exist fake error")) 53 | mock.ExpectBegin() 54 | mock.ExpectExec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`", tableName)) 55 | mock.ExpectCommit() 56 | mock.ExpectExec(fmt.Sprintf("INSERT INTO %s .*", tableName)) 57 | mock.ExpectClose() 58 | 59 | dbx := sqlx.NewDb(db, "sqlmock") 60 | 61 | m, err := NewMySQLOutput(nil, outCfg) 62 | assert.Nil(t, err) 63 | 64 | // Cast the outputs.Output to the SQLite so we can manipulate the object 65 | ms, ok := m.(MySQL) 66 | require.True(t, ok) 67 | 68 | outPath := fmt.Sprintf("%s-%s", outCfg.MySQL.DSN, tableName) 69 | ms.dbCons[outPath] = dbx 70 | 71 | // Do() and Close() to run the database flow 72 | err = m.Do(data) 73 | assert.NotNil(t, err) 74 | err = m.Close() 75 | assert.Nil(t, err) 76 | 77 | // Check if all expectations were met 78 | err = mock.ExpectationsWereMet() 79 | assert.Nil(t, err) 80 | 81 | // TODO Verify data written to database 82 | } 83 | -------------------------------------------------------------------------------- /runners/ansible/ansible_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ansible 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "sync" 20 | "testing" 21 | 22 | "github.com/cloudical-io/ancientt/pkg/config" 23 | exectest "github.com/cloudical-io/ancientt/pkg/executor/test" 24 | "github.com/sirupsen/logrus" 25 | log "github.com/sirupsen/logrus" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestGetHostsForTest(t *testing.T) { 31 | var lock sync.Mutex 32 | run := 1 33 | mockexec := exectest.MockExecutor{ 34 | MockExecuteCommandWithOutputByte: func(ctx context.Context, actionName string, command string, arg ...string) ([]byte, error) { 35 | lock.Lock() 36 | defer func() { 37 | run++ 38 | lock.Unlock() 39 | }() 40 | 41 | switch run { 42 | case 1: 43 | return []byte(`{ 44 | "_meta": { 45 | "hostvars": {} 46 | }, 47 | "all": { 48 | "children": [ 49 | "clients", 50 | "server", 51 | "ungrouped" 52 | ] 53 | }, 54 | "clients": { 55 | "hosts": [ 56 | "server1", 57 | "server2" 58 | ] 59 | }, 60 | "server": { 61 | "hosts": [ 62 | "server4" 63 | ] 64 | } 65 | }`), nil 66 | case 2, 3, 4: 67 | return []byte(fmt.Sprintf(`192.0.2.5 | SUCCESS => { 68 | "ansible_facts": { 69 | "ansible_default_ipv4": { 70 | "address": "192.0.2.1%d" 71 | }, 72 | "ansible_default_ipv6": { 73 | "address": "2001:DB8::%d337" 74 | } 75 | } 76 | }`, run, run)), nil 77 | default: 78 | err := fmt.Errorf("no command for run %d (actionName: %s; cmd: %s; args: %s", run, actionName, command, arg) 79 | t.Fatal(err) 80 | return nil, err 81 | } 82 | }, 83 | } 84 | 85 | log.SetLevel(log.TraceLevel) 86 | 87 | conf := &config.RunnerAnsible{ 88 | InventoryFilePath: "/tmp/test-ancientt-ansible-inventory", 89 | } 90 | conf.SetDefaults() 91 | a := Ansible{ 92 | logger: log.WithFields(logrus.Fields{"runner": Name}), 93 | config: conf, 94 | executor: mockexec, 95 | } 96 | require.NotNil(t, a) 97 | 98 | hosts, err := a.GetHostsForTest(&config.Test{}) 99 | require.Nil(t, err) 100 | 101 | assert.Equal(t, 2, len(hosts.Clients)) 102 | assert.Equal(t, 1, len(hosts.Servers)) 103 | } 104 | -------------------------------------------------------------------------------- /testdefinition.example.yaml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | runner: 3 | name: kubernetes 4 | kubernetes: 5 | #kubeconfig: .kube/config 6 | image: 'quay.io/galexrt/container-toolbox:v20210915-101121-713' 7 | namespace: ancientt 8 | timeouts: 9 | deleteTimeout: 20 10 | runningTimeout: 60 11 | succeedTimeout: 60 12 | hosts: 13 | ignoreSchedulingDisabled: true 14 | tolerations: [] 15 | # If the Pods should be run with `hostNetwork: true` option 16 | hostNetwork: false 17 | tests: 18 | - name: iperf3-one-rand-to-one-rand 19 | type: iperf3 20 | transformations: 21 | - source: "bits_per_second" 22 | destination: "gigabits_per_second" 23 | action: "add" 24 | modifier: 100000000 25 | modifierAction: "division" 26 | outputs: 27 | - name: csv 28 | csv: 29 | filePath: /tmp 30 | namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.csv' 31 | # If you want one CSV per server and client host test run, you can use the following: 32 | #namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}-{{ .Data.ServerHost }}_{{ .Data.ClientHost }}.csv' 33 | #- name: sqlite 34 | # sqlite: 35 | # filePath: /tmp 36 | # namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.sqlite3' 37 | # tableNamePattern: 'ancientt{{ .TestStartTime }}{{ .Data.Tester }}' 38 | #- name: mysql 39 | # mysql: 40 | # dsn: "username:password@127.0.0.1/mydb" 41 | # tableNamePattern: 'ancientt{{ .TestStartTime }}{{ .Data.Tester }}' 42 | # autoCreateTables: true 43 | #- name: excelize 44 | # excelize: 45 | # filePath: /tmp 46 | # namePattern: "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.xlsx" 47 | # saveAfterRows: 200 48 | runOptions: 49 | continueOnError: true 50 | rounds: 1 51 | # Wait 10 seconds between each round 52 | interval: 10s 53 | mode: "sequential" 54 | parallelcount: 1 55 | # This hosts section would cause iperf3 to be run from all hosts to the hosts selected in the `destinations` section 56 | # Each entry will be merged into one list 57 | hosts: 58 | clients: 59 | - name: all-host 60 | all: true 61 | # hostSelector: 62 | # nope: foo 63 | #- name: all-hosts 64 | # all: true 65 | # hosts: 66 | # - servers-0 67 | # - servers-4 68 | servers: 69 | - name: one-randomly-selected-server 70 | random: true 71 | count: 2 72 | # hostSelector: 73 | # nope: foo 74 | #- name: different-hosts-1 75 | # random: true 76 | # count: 2 # When more than one hosts is wanted, 77 | # hostSelector: # For Labels in K8s and Variables of hosts in Ansible 78 | # network-tests-run-here: "true" 79 | # antiAffinity: 80 | # - openstack-region # This makes sure hosts with each different "openstack-region" are picked 81 | #- name: specific-host-list 82 | # hosts: 83 | # - serverabc123 84 | # - serverxyz789 85 | iperf3: 86 | udp: false 87 | duration: 10 88 | interval: 1 89 | additionalFlags: 90 | clients: [] 91 | server: [] 92 | -------------------------------------------------------------------------------- /outputs/dump/dump.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package dump 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | 21 | "github.com/cloudical-io/ancientt/outputs" 22 | "github.com/cloudical-io/ancientt/pkg/config" 23 | "github.com/k0kubun/pp" 24 | "github.com/sirupsen/logrus" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | // NameDump Dump output name 29 | const NameDump = "dump" 30 | 31 | func init() { 32 | outputs.Factories[NameDump] = NewDumpOutput 33 | } 34 | 35 | // Dump Dump tester structure 36 | type Dump struct { 37 | outputs.Output 38 | logger *log.Entry 39 | config *config.Dump 40 | files map[string]*os.File 41 | } 42 | 43 | // NewDumpOutput return a new Dump tester instance 44 | func NewDumpOutput(cfg *config.Config, outCfg *config.Output) (outputs.Output, error) { 45 | dump := Dump{ 46 | logger: log.WithFields(logrus.Fields{"output": NameDump}), 47 | config: outCfg.Dump, 48 | files: map[string]*os.File{}, 49 | } 50 | if dump.config.FilePath.NamePattern == "" { 51 | dump.config.FilePath.NamePattern = "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.txt" 52 | } 53 | return dump, nil 54 | } 55 | 56 | // Do make Dump outputs 57 | func (d Dump) Do(data outputs.Data) error { 58 | dataTable, ok := data.Data.(*outputs.Table) 59 | if !ok { 60 | return fmt.Errorf("data not in data table format for dump output") 61 | } 62 | 63 | filename, err := outputs.GetFilenameFromPattern(d.config.FilePath.NamePattern, "", data, nil) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | outPath := filepath.Join(d.config.FilePath.FilePath, filename) 69 | file, ok := d.files[outPath] 70 | if !ok { 71 | file, err = os.Create(outPath) 72 | if err != nil { 73 | return err 74 | } 75 | d.files[outPath] = file 76 | } 77 | 78 | // FIXME should the output be improved? 79 | 80 | if _, err := file.WriteString(pp.Sprint(dataTable)); err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // OutputFiles return a list of output files 88 | func (d Dump) OutputFiles() []string { 89 | list := []string{} 90 | for file := range d.files { 91 | list = append(list, file) 92 | } 93 | return list 94 | } 95 | 96 | // Close close open files 97 | func (d Dump) Close() error { 98 | for name, file := range d.files { 99 | d.logger.WithFields(logrus.Fields{"filepath": name}).Debug("closing file") 100 | if err := file.Close(); err != nil { 101 | d.logger.WithFields(logrus.Fields{"filepath": name}).Errorf("error closing file. %+v", err) 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ancientt 2 | 3 | A tool to automate network testing tools, like iperf3, in dynamic environments such as Kubernetes and more to come dynamic environments. 4 | 5 | Container Image available from: 6 | 7 | * [GHCR.io](https://github.com/users/cloudical-io/packages/container/package/ancientt) 8 | 9 | Container Image Tags: 10 | 11 | * `main` - Latest build of the `main` branch. 12 | * `vx.y.z` - Tagged build of the application. 13 | 14 | ## Features 15 | 16 | **TL;DR** A network test tool, like `iperf3` can be run in, e.g., Kubernetes, cluster from all-to-all Nodes. 17 | 18 | * Run network tests with the following projects: 19 | * [`iperf3`](https://iperf.fr/) 20 | * [PingParsing](https://github.com/thombashi/pingparsing) 21 | * Soon more tools will be available as well, see [GitHub Issues with "testers" Label](https://github.com/cloudical-io/ancientt/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Atesters+). 22 | * Tests can be run through the following "runners": 23 | * Ansible (an inventory file is needed) 24 | * Kubernetes (a kubeconfig connected to a cluster) 25 | * Results of the network tests can be output in different formats: 26 | * CSV 27 | * Dump (uses `pp.Sprint()` ([GitHub k0kubun/pp](https://github.com/k0kubun/pp), pretty print library)) 28 | * Excel files (Excelize) 29 | * go-chart Charts (WIP) 30 | * MySQL 31 | * SQLite 32 | 33 | ## Usage 34 | 35 | Either [build (`go get`)](#building), download the Ancientt executable from the GitHub release page or use the Container image. 36 | 37 | A config file containing test definitions must be given by flag `--testdefinition` (or short flag `-c`) or named `testdefinition.yaml` in the current directory. 38 | 39 | Below command will try loading `your-testdefinitions.yaml` as the test definitions config: 40 | 41 | ```shell 42 | $ ancientt --testdefinition your-testdefinitions.yaml 43 | # You can also use the short flag `-c` instead of `--testdefinition` 44 | # and also with `-y` run the tests immediately 45 | $ ancientt -c your-testdefinitions.yaml -y 46 | ``` 47 | 48 | ## Demos 49 | 50 | See [Demos](docs/demos.md). 51 | 52 | ## Goals of this Project 53 | 54 | * A bit like Prometheus blackbox exporter which contains "definitions" for probes. The "tests" would be pluggable through a Golang interface. 55 | * "Runner" interface, e.g., for Kubernetes, Ansible, etc. The "runner" abstracts the "how it is run", e.g., for Kubernetes creates a Job, Ansible (download and) trigger a playbook to run the test. 56 | * Store result data in different formats, e.g., CSV, excel, MySQL 57 | * Up for discussion: graph database ([Dgraph](https://dgraph.io/)) and / or TSDB support 58 | * "Visualization" for humans, e.g., possibility to automatically draw "shiny" graphs from the results. 59 | 60 | ## Development 61 | 62 | **Golang version**: `v1.17` or higher (tested with `v1.17.6` on `linux/amd64`) 63 | 64 | ### Creating Release 65 | 66 | 1. Add new entry for release to [`CHANGELOG.md`](CHANGELOG.md). 67 | 2. Update [`VERSION`](VERSION) with new version number. 68 | 3. `git commit` and `git push` both changes (e.g., `version: update to VERSION_HERE`). 69 | 4. Now create the git tag and push the tag `git tag VERSION_HERE` followed by `git push --tags`. 70 | 71 | ### Dependencies 72 | 73 | `go mod` is used to manage the dependencies. 74 | 75 | ### Building 76 | 77 | Quickest way to just get ancientt built is to run the following command: 78 | 79 | ```bash 80 | go get -u github.com/cloudical-io/ancientt/cmd/ancientt 81 | ``` 82 | 83 | ## Licensing 84 | 85 | Ancientt is licensed under the Apache 2.0 License. 86 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECTNAME ?= ancientt 2 | DESCRIPTION ?= ancientt - A tool to automate network testing tools, like iperf3, in dynamic environments such as Kubernetes and more to come dynamic environments. 3 | MAINTAINER ?= Alexander Trost 4 | HOMEPAGE ?= https://github.com/cloudical-io/ancientt 5 | 6 | GO111MODULE ?= on 7 | GO ?= go 8 | PREFIX ?= $(shell pwd) 9 | BIN_DIR ?= $(PREFIX)/.bin 10 | TARBALL_DIR ?= $(PREFIX)/.tarball 11 | PACKAGE_DIR ?= $(PREFIX)/.package 12 | ARCH ?= amd64 13 | PACKAGE_ARCH ?= linux-amd64 14 | 15 | # The GOHOSTARM and PROMU parts have been taken from the prometheus/promu repository 16 | # which is licensed under Apache License 2.0 Copyright 2018 The Prometheus Authors 17 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 18 | 19 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 20 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 21 | 22 | ifeq (arm, $(GOHOSTARCH)) 23 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 24 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 25 | else 26 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 27 | endif 28 | 29 | PROMU_VERSION ?= 0.7.0 30 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 31 | 32 | PROMU := $(FIRST_GOPATH)/bin/promu 33 | # END copied code 34 | 35 | pkgs = $(shell go list ./... | grep -v /vendor/ | grep -v /test/) 36 | 37 | DOCKER_IMAGE_NAME ?= ancientt 38 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 39 | 40 | go.check: 41 | ifneq ($(shell $(GO) version | grep -q -E '\bgo($(GO_SUPPORTED_VERSIONS))\b' && echo 0 || echo 1), 0) 42 | $(error unsupported go version. Please make install one of the following supported version: '$(GO_SUPPORTED_VERSIONS)') 43 | endif 44 | 45 | all: format style vet test build 46 | 47 | build: promu 48 | @echo ">> building binaries" 49 | GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX) 50 | 51 | check_license: 52 | @OUTPUT="$$($(PROMU) check licenses)"; \ 53 | if [[ $$OUTPUT ]]; then \ 54 | echo "Found go files without license header:"; \ 55 | echo "$$OUTPUT"; \ 56 | exit 1; \ 57 | else \ 58 | echo "All files with license header"; \ 59 | fi 60 | 61 | docker: 62 | @echo ">> building docker image" 63 | docker build \ 64 | --build-arg BUILD_DATE="$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" \ 65 | --build-arg VCS_REF="$(shell git rev-parse HEAD)" \ 66 | -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" \ 67 | . 68 | 69 | format: 70 | go fmt $(pkgs) 71 | 72 | promu: 73 | $(eval PROMU_TMP := $(shell mktemp -d)) 74 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 75 | mkdir -p $(FIRST_GOPATH)/bin 76 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 77 | rm -r $(PROMU_TMP) 78 | 79 | style: 80 | @echo ">> checking code style" 81 | @! gofmt -d $(shell find . -path ./vendor -prune -o -name '*.go' -print) | grep '^' 82 | 83 | tarball: 84 | @echo ">> building release tarball" 85 | @$(PROMU) tarball --prefix $(TARBALL_DIR) $(BIN_DIR) 86 | 87 | test: 88 | @$(GO) test $(pkgs) 89 | 90 | test-short: 91 | @echo ">> running short tests" 92 | @$(GO) test -short $(pkgs) 93 | 94 | vet: 95 | @echo ">> vetting code" 96 | @$(GO) vet $(pkgs) 97 | 98 | docs: pkg/config/config.go 99 | @echo ">> generating docs" 100 | $(GO) run ./cmd/docgen/ api pkg/config/*.go > docs/config-structure.md 101 | 102 | .PHONY: all build docker docs format go.check style test test-short vet 103 | -------------------------------------------------------------------------------- /pkg/executor/test/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | // Package test mock executor heavily inspired from the Executor from github.com/rook/rook/pkg/util/exec pkg# 15 | package test 16 | 17 | import ( 18 | "context" 19 | "os/exec" 20 | 21 | "github.com/cloudical-io/ancientt/pkg/executor" 22 | "github.com/sirupsen/logrus" 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | // MockExecutor Mock Executor implementation for tests 27 | type MockExecutor struct { 28 | executor.Executor 29 | MockExecuteCommand func(ctx context.Context, actionName string, command string, arg ...string) error 30 | MockExecuteCommandWithOutput func(ctx context.Context, actionName string, command string, arg ...string) (string, error) 31 | MockExecuteCommandWithOutputByte func(ctx context.Context, actionName string, command string, arg ...string) ([]byte, error) 32 | MockSetEnv func(e []string) 33 | 34 | env []string 35 | } 36 | 37 | // ExecuteCommand execute a given command with its arguments but don't return any output 38 | func (ce MockExecutor) ExecuteCommand(ctx context.Context, actionName string, command string, arg ...string) error { 39 | if ce.MockExecuteCommand != nil { 40 | return ce.MockExecuteCommand(ctx, actionName, command, arg...) 41 | } 42 | 43 | cmd := exec.CommandContext(ctx, command, arg...) 44 | 45 | log.WithFields(logrus.Fields{ 46 | "command": command, 47 | "args": arg, 48 | }).Info("executing") 49 | 50 | return cmd.Run() 51 | } 52 | 53 | // ExecuteCommandWithOutput execute a given command with its arguments and return the output as a string 54 | func (ce MockExecutor) ExecuteCommandWithOutput(ctx context.Context, actionName string, command string, arg ...string) (string, error) { 55 | if ce.MockExecuteCommandWithOutput != nil { 56 | return ce.MockExecuteCommandWithOutput(ctx, actionName, command, arg...) 57 | } 58 | 59 | out, err := ce.ExecuteCommandWithOutputByte(ctx, actionName, command, arg...) 60 | return string(out), err 61 | } 62 | 63 | // ExecuteCommandWithOutputByte execute a given command with its arguments and return the output as a byte array ([]byte) 64 | func (ce MockExecutor) ExecuteCommandWithOutputByte(ctx context.Context, actionName string, command string, arg ...string) ([]byte, error) { 65 | if ce.MockExecuteCommandWithOutputByte != nil { 66 | out, err := ce.MockExecuteCommandWithOutputByte(ctx, actionName, command, arg...) 67 | log.WithField("action", actionName).Debug(string(out)) 68 | return out, err 69 | } 70 | 71 | cmd := exec.CommandContext(ctx, command, arg...) 72 | 73 | out, err := cmd.CombinedOutput() 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | log.WithField("action", actionName).Debug(string(out)) 79 | 80 | return out, nil 81 | } 82 | 83 | // SetEnv set env for command execution 84 | func (ce MockExecutor) SetEnv(e []string) { 85 | log.WithField("action", "setEnv()").Debugf("%+v", ce.env) 86 | if ce.MockSetEnv != nil { 87 | ce.MockSetEnv(e) 88 | return 89 | } 90 | 91 | ce.env = e 92 | } 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Alexander Trost and Michal Janus . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq) 77 | -------------------------------------------------------------------------------- /pkg/hostsfilter/hostsfilter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package hostsfilter 15 | 16 | import ( 17 | "math/rand" 18 | "time" 19 | 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | "github.com/cloudical-io/ancientt/testers" 22 | ) 23 | 24 | // FilterHostsList filter a given host list 25 | func FilterHostsList(inHosts []*testers.Host, filter config.Hosts) ([]*testers.Host, error) { 26 | hosts := []*testers.Host{} 27 | 28 | if len(filter.Hosts) > 0 { 29 | for _, host := range filter.Hosts { 30 | hosts = append(hosts, &testers.Host{ 31 | Name: host, 32 | }) 33 | } 34 | return hosts, nil 35 | } 36 | 37 | filteredHosts := filterHostsByLabels(inHosts, filter.HostSelector) 38 | 39 | filteredHosts = checkAntiAffinity(filteredHosts, filter.AntiAffinity) 40 | 41 | if len(filteredHosts) == 0 || (filter.All != nil && *filter.All) { 42 | return filteredHosts, nil 43 | } 44 | 45 | // Create and seed randomness source for the `random` selection of hosts 46 | s := rand.NewSource(time.Now().Unix()) 47 | r := rand.New(s) 48 | r.Seed(time.Now().UnixNano()) 49 | 50 | // Get random server(s) 51 | if filter.Random != nil && *filter.Random { 52 | for i := 0; i < filter.Count; i++ { 53 | inHost := filteredHosts[r.Intn(len(filteredHosts))] 54 | hosts = append(hosts, inHost) 55 | } 56 | return hosts, nil 57 | } 58 | 59 | return filteredHosts, nil 60 | } 61 | 62 | // filterHostsByLabels all labels must match 63 | func filterHostsByLabels(hosts []*testers.Host, labels map[string]string) []*testers.Host { 64 | if len(labels) == 0 { 65 | return hosts 66 | } 67 | 68 | filtered := []*testers.Host{} 69 | for _, host := range hosts { 70 | // Compare host and filter labels list, all labels list must match 71 | match := true 72 | for k, v := range labels { 73 | if labelValue, ok := host.Labels[k]; ok { 74 | if labelValue != v { 75 | match = false 76 | break 77 | } 78 | } else { 79 | match = false 80 | } 81 | } 82 | if match { 83 | filtered = append(filtered, host) 84 | } 85 | } 86 | 87 | return filtered 88 | } 89 | 90 | func checkAntiAffinity(hosts []*testers.Host, labels []string) []*testers.Host { 91 | if len(labels) == 0 { 92 | return hosts 93 | } 94 | 95 | filtered := []*testers.Host{} 96 | usedLabels := map[string][]string{} 97 | 98 | for _, host := range hosts { 99 | // Compare host and filter labels list, all labels list must match 100 | match := true 101 | for _, label := range labels { 102 | hostLabelVal, ok := host.Labels[label] 103 | if !ok { 104 | continue 105 | } 106 | // Check if label and value is in usedLabels list and if it is the host should not be added 107 | if usedLabelValues, ok := usedLabels[label]; ok { 108 | for _, usedLabelVal := range usedLabelValues { 109 | if usedLabelVal == hostLabelVal { 110 | match = false 111 | break 112 | } 113 | } 114 | usedLabels[label] = append(usedLabels[label], hostLabelVal) 115 | } else { 116 | usedLabels[label] = []string{hostLabelVal} 117 | } 118 | } 119 | if match { 120 | filtered = append(filtered, host) 121 | } 122 | } 123 | return filtered 124 | } 125 | -------------------------------------------------------------------------------- /pkg/executor/executor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | // Package executor executor heavily inspired from the Executor from github.com/rook/rook/pkg/util/exec pkg 15 | package executor 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "os/exec" 21 | "syscall" 22 | 23 | "github.com/sirupsen/logrus" 24 | log "github.com/sirupsen/logrus" 25 | ) 26 | 27 | // Executor heavily inspired from the Executor from github.com/rook/rook/pkg/util/exec pkg 28 | type Executor interface { 29 | ExecuteCommand(ctx context.Context, actionName string, command string, arg ...string) error 30 | ExecuteCommandWithOutput(ctx context.Context, actionName string, command string, arg ...string) (string, error) 31 | ExecuteCommandWithOutputByte(ctx context.Context, actionName string, command string, arg ...string) ([]byte, error) 32 | SetEnv([]string) 33 | } 34 | 35 | // CommandExecutor Executor implementation 36 | type CommandExecutor struct { 37 | Executor 38 | logger *log.Entry 39 | env []string 40 | } 41 | 42 | // NewCommandExecutor create and return a new CommandExecutor 43 | func NewCommandExecutor(pkg string) Executor { 44 | return CommandExecutor{ 45 | logger: log.WithFields(logrus.Fields{ 46 | "executor": pkg, 47 | }), 48 | } 49 | } 50 | 51 | // ExecuteCommand execute a given command with its arguments but don't return any output 52 | func (ce CommandExecutor) ExecuteCommand(ctx context.Context, actionName string, command string, arg ...string) error { 53 | cmd := exec.CommandContext(ctx, command, arg...) 54 | cmd.Env = os.Environ() 55 | cmd.SysProcAttr = &syscall.SysProcAttr{ 56 | Pdeathsig: syscall.SIGTERM, 57 | Setpgid: true, 58 | } 59 | 60 | ce.logger.WithFields(logrus.Fields{ 61 | "command": command, 62 | "args": arg, 63 | }).Info("executing command") 64 | 65 | if err := cmd.Start(); err != nil { 66 | return err 67 | } 68 | 69 | return cmd.Wait() 70 | } 71 | 72 | // ExecuteCommandWithOutput execute a given command with its arguments and return the output as a string 73 | func (ce CommandExecutor) ExecuteCommandWithOutput(ctx context.Context, actionName string, command string, arg ...string) (string, error) { 74 | out, err := ce.ExecuteCommandWithOutputByte(ctx, actionName, command, arg...) 75 | return string(out), err 76 | } 77 | 78 | // ExecuteCommandWithOutputByte execute a given command with its arguments and return the output as a byte array ([]byte) 79 | func (ce CommandExecutor) ExecuteCommandWithOutputByte(ctx context.Context, actionName string, command string, arg ...string) ([]byte, error) { 80 | cmd := exec.CommandContext(ctx, command, arg...) 81 | cmd.Env = os.Environ() 82 | cmd.SysProcAttr = &syscall.SysProcAttr{ 83 | Pdeathsig: syscall.SIGTERM, 84 | Setpgid: true, 85 | } 86 | 87 | ce.logger.WithFields(logrus.Fields{ 88 | "command": command, 89 | "args": arg, 90 | }).Info("executing command") 91 | 92 | out, err := cmd.CombinedOutput() 93 | ce.logger.WithField("action", actionName).Debug(string(out)) 94 | 95 | if err != nil { 96 | return out, err 97 | } 98 | 99 | return out, nil 100 | } 101 | 102 | // SetEnv set env for command execution 103 | func (ce CommandExecutor) SetEnv(e []string) { 104 | ce.env = e 105 | } 106 | -------------------------------------------------------------------------------- /docs/demos.md: -------------------------------------------------------------------------------- 1 | # Demos 2 | 3 | This page contains demos of `ancientt` in action. 4 | 5 | Each demo has an [asciinema recording](https://asciinema.org/), the used `testdefinition.yaml` and depending on the used output format a short snippet or the whole output available. 6 | 7 | ## Kubernetes: `iperf3` One Random Node to All Kubernetes Nodes 8 | 9 | [![asciicast](https://asciinema.org/a/kCpLvkjVRAMcyYraBz2ZIp5h6.svg)](https://asciinema.org/a/kCpLvkjVRAMcyYraBz2ZIp5h6) 10 | 11 | `testdefinition.yaml`: 12 | ```yaml 13 | version: '0' 14 | runner: 15 | name: kubernetes 16 | kubernetes: 17 | kubeconfig: .kube/config 18 | image: quay.io/galexrt/container-toolbox:v20210915-101121-713 19 | hosts: 20 | ignoreSchedulingDisabled: true 21 | tests: 22 | - name: iperf3-one-to-all 23 | type: iperf3 24 | outputs: 25 | - name: csv 26 | csv: 27 | filePath: ./results 28 | namePattern: 'ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.csv' 29 | runOptions: 30 | continueOnError: true 31 | rounds: 1 32 | hosts: 33 | clients: 34 | - name: all-hosts 35 | all: true 36 | servers: 37 | - name: one-host 38 | count: 1 39 | random: true 40 | iperf3: 41 | additionalFlags: 42 | clients: ["--interval=1"] 43 | server: ["--interval=1"] 44 | ``` 45 | 46 | Output `csv (partial snippet): 47 | ```csv 48 | test_time,round,tester,server_host,client_host,socket,start,end,seconds,bytes,bits_per_second,retransmits,snd_cwnd,rtt,rttvar,pmtu,omitted,iperf3_version,system_info,additional_info 49 | 2019-09-24T20:43:13+0200,0,iperf3,mycoolk8scluster-worker-01,mycoolk8scluster-worker-01,5,0.000000,1.000295,1.000295,1982632968,15856387318.277708,0,3191392,308,384,1500,false,iperf 3.6,Linux ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64,ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 50 | 2019-09-24T20:43:13+0200,0,iperf3,mycoolk8scluster-worker-01,mycoolk8scluster-worker-01,5,1.000295,2.000191,0.999896,2487746560,19904041515.077232,0,3191392,246,310,1500,false,iperf 3.6,Linux ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64,ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 51 | 2019-09-24T20:43:13+0200,0,iperf3,mycoolk8scluster-worker-01,mycoolk8scluster-worker-01,5,2.000191,3.000123,0.999932,2199388160,17596300936.243999,0,3191392,761,622,1500,false,iperf 3.6,Linux ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64,ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 52 | 2019-09-24T20:43:13+0200,0,iperf3,mycoolk8scluster-worker-01,mycoolk8scluster-worker-01,5,3.000123,4.000487,1.000364,2596536320,20764735793.626110,0,3191392,262,322,1500,false,iperf 3.6,Linux ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64,ancientt-client-iperf3-b411d4627abf6b743515539a060f8a84292e2267 53 | [...] 54 | 2019-09-24T20:43:29+0200,0,iperf3,mycoolk8scluster-worker-01,mycoolk8scluster-worker-02,5,0.000000,1.000507,1.000507,176868460,1414230500.636069,819,1107216,5421,1645,1450,false,iperf 3.6,Linux ancientt-client-iperf3-6ca1e56296767da8d29adf6c8bba8ba6fc32fce4 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64,ancientt-client-iperf3-6ca1e56296767da8d29adf6c8bba8ba6fc32fce4 55 | [...] 56 | 2019-09-24T20:43:43+0200,0,iperf3,mycoolk8scluster-worker-01,mycoolk8scluster-worker-03,5,9.000335,10.000137,0.999802,192675840,1541711805.372557,289,1077858,3027,325,1450,false,iperf 3.6,Linux ancientt-client-iperf3-af06030e7b7611f917582390fb1db0e38e799c08 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64,ancientt-client-iperf3-af06030e7b7611f917582390fb1db0e38e799c08 57 | ``` 58 | 59 | ## More demos to come soon 60 | 61 | There will soon be more demos, about the different runners, testers and outputs available in `ancientt`. -------------------------------------------------------------------------------- /runners/mock/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package mock 15 | 16 | import ( 17 | "fmt" 18 | 19 | "github.com/cloudical-io/ancientt/parsers" 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | "github.com/cloudical-io/ancientt/pkg/hostsfilter" 22 | "github.com/cloudical-io/ancientt/runners" 23 | "github.com/cloudical-io/ancientt/testers" 24 | "github.com/sirupsen/logrus" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | const ( 29 | // Name Mock Runner Name 30 | Name = "mock" 31 | mockServerNamePattern = "servers-%d" 32 | ) 33 | 34 | func init() { 35 | runners.Factories[Name] = NewRunner 36 | } 37 | 38 | // Mock Mock Runner struct 39 | type Mock struct { 40 | runners.Runner 41 | logger *log.Entry 42 | } 43 | 44 | // NewRunner returns a new Mock Runner 45 | func NewRunner(cfg *config.Config) (runners.Runner, error) { 46 | return Mock{ 47 | logger: log.WithFields(logrus.Fields{"runner": Name}), 48 | }, nil 49 | } 50 | 51 | // GetHostsForTest return a mocked list of hots for the given test config 52 | func (m Mock) GetHostsForTest(test *config.Test) (*testers.Hosts, error) { 53 | // Pre create the structure to return 54 | hosts := &testers.Hosts{ 55 | Clients: map[string]*testers.Host{}, 56 | Servers: map[string]*testers.Host{}, 57 | } 58 | 59 | mockHosts := generateMockServers() 60 | 61 | // Go through Hosts Servers list to get the servers hosts 62 | for _, servers := range test.Hosts.Servers { 63 | filtered, err := hostsfilter.FilterHostsList(mockHosts, servers) 64 | if err != nil { 65 | return nil, err 66 | } 67 | for _, host := range filtered { 68 | if _, ok := hosts.Servers[host.Name]; !ok { 69 | hosts.Servers[host.Name] = host 70 | } 71 | } 72 | } 73 | 74 | // Go through Hosts Clients list to get the clients hosts 75 | for _, clients := range test.Hosts.Clients { 76 | filtered, err := hostsfilter.FilterHostsList(mockHosts, clients) 77 | if err != nil { 78 | return nil, err 79 | } 80 | for _, host := range filtered { 81 | if _, ok := hosts.Clients[host.Name]; !ok { 82 | hosts.Clients[host.Name] = host 83 | } 84 | } 85 | } 86 | 87 | return hosts, nil 88 | } 89 | 90 | // generateMockServers generate a list of mcoekd servers for testing purposes 91 | func generateMockServers() []*testers.Host { 92 | hosts := []*testers.Host{} 93 | for i := 0; i < 10; i++ { 94 | hosts = append(hosts, &testers.Host{ 95 | Name: fmt.Sprintf(mockServerNamePattern, i), 96 | Addresses: &testers.IPAddresses{}, 97 | Labels: map[string]string{ 98 | "i-am-server": fmt.Sprintf(mockServerNamePattern, i), 99 | }, 100 | }) 101 | } 102 | return hosts 103 | } 104 | 105 | // Prepare NOOP because there is nothing to prepare because this is Mock. 106 | func (m Mock) Prepare(runOpts config.RunOptions, plan *testers.Plan) error { 107 | m.logger.Info("Mock.Prepare() called") 108 | return nil 109 | } 110 | 111 | // Execute run the given testers.Plan and return the logs of each step and / or error 112 | func (m Mock) Execute(plan *testers.Plan, parser chan<- parsers.Input) error { 113 | m.logger.Info("Mock.Execute() called") 114 | // Return nothing because we don't do anything in the Mock 115 | return nil 116 | } 117 | 118 | // Cleanup NOOP because Mock doesn't create any resource nor connection or so to any hosts. 119 | func (m Mock) Cleanup(plan *testers.Plan) error { 120 | m.logger.Info("Mock.Cleanup() called") 121 | // Return nothing because we don't do anything in the Mock 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /outputs/data.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package outputs 15 | 16 | import ( 17 | "fmt" 18 | "time" 19 | 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | ) 22 | 23 | // Data structured parsed data 24 | type Data struct { 25 | TestStartTime time.Time 26 | TestTime time.Time 27 | Tester string 28 | ServerHost string 29 | ClientHost string 30 | AdditionalInfo string 31 | Data DataFormat 32 | } 33 | 34 | // DataFormat DataFormat interface that must be implemented by data formats, e.g., Table. 35 | type DataFormat interface { 36 | // Transform run transformations on the `Data`. 37 | Transform(ts []*config.Transformation) error 38 | } 39 | 40 | // Table Data format for data in Table form 41 | type Table struct { 42 | DataFormat 43 | Headers []*Row 44 | Rows [][]*Row 45 | } 46 | 47 | // Row Row of the Table data format 48 | type Row struct { 49 | Value interface{} 50 | } 51 | 52 | // Transform transformation of table data 53 | func (d *Table) Transform(ts []*config.Transformation) error { 54 | // Iterate over each transformation 55 | for _, t := range ts { 56 | index, err := d.GetHeaderIndexByName(t.Source) 57 | if err != nil { 58 | return err 59 | } 60 | if index == -1 { 61 | return nil 62 | } 63 | 64 | switch t.Action { 65 | case config.TransformationActionAdd: 66 | d.Headers = append(d.Headers, &Row{ 67 | Value: t.Destination, 68 | }) 69 | case config.TransformationActionDelete: 70 | d.Headers[index] = nil 71 | case config.TransformationActionReplace: 72 | toHeader := t.Destination 73 | if toHeader == "" { 74 | toHeader = t.Source 75 | } 76 | d.Headers[index].Value = toHeader 77 | } 78 | 79 | for row := range d.Rows { 80 | if len(d.Rows[row]) < index { 81 | continue 82 | } 83 | 84 | switch t.Action { 85 | case config.TransformationActionAdd: 86 | d.Rows[row] = append(d.Rows[row], &Row{ 87 | Value: d.modifyValue(d.Rows[row][index].Value, t), 88 | }) 89 | case config.TransformationActionDelete: 90 | d.Rows[row][index] = nil 91 | case config.TransformationActionReplace: 92 | d.Rows[row][index].Value = d.modifyValue(d.Rows[row][index].Value, t) 93 | } 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (d *Table) modifyValue(in interface{}, t *config.Transformation) interface{} { 101 | value, ok := in.(float64) 102 | if !ok { 103 | valInt, ok := in.(int64) 104 | if !ok { 105 | return in 106 | } 107 | value = float64(valInt) 108 | } 109 | 110 | switch t.ModifierAction { 111 | case config.ModifierActionAddition: 112 | return value + *t.Modifier 113 | case config.ModifierActionSubstract: 114 | return value - *t.Modifier 115 | case config.ModifierActionDivison: 116 | return value / *t.Modifier 117 | case config.ModifierActionMultiply: 118 | return value * *t.Modifier 119 | } 120 | 121 | return in 122 | } 123 | 124 | // CheckIfHeaderExists check if a header exists by name in the Table 125 | func (d *Table) CheckIfHeaderExists(name interface{}) (int, bool) { 126 | for k, c := range d.Headers { 127 | if c == nil { 128 | continue 129 | } 130 | if c.Value == name { 131 | return k, true 132 | } 133 | } 134 | 135 | return 0, false 136 | } 137 | 138 | // GetHeaderIndexByName return the header index for a given key (name) string 139 | func (d *Table) GetHeaderIndexByName(name string) (int, error) { 140 | for i := range d.Headers { 141 | if d.Headers[i] == nil { 142 | continue 143 | } 144 | val, ok := d.Headers[i].Value.(string) 145 | if !ok { 146 | return -1, fmt.Errorf("failed to cast result header into string, header: %+v", d.Headers[i].Value) 147 | } 148 | if val == name { 149 | return i, nil 150 | } 151 | } 152 | return -1, nil 153 | } 154 | -------------------------------------------------------------------------------- /outputs/csv/csv.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package csv 15 | 16 | import ( 17 | "encoding/csv" 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | 22 | "github.com/cloudical-io/ancientt/outputs" 23 | "github.com/cloudical-io/ancientt/pkg/config" 24 | "github.com/cloudical-io/ancientt/pkg/util" 25 | "github.com/sirupsen/logrus" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | // NameCSV CSV output name 30 | const NameCSV = "csv" 31 | 32 | func init() { 33 | outputs.Factories[NameCSV] = NewCSVOutput 34 | } 35 | 36 | // CSV CSV tester structure 37 | type CSV struct { 38 | outputs.Output 39 | logger *log.Entry 40 | config *config.CSV 41 | files map[string]*os.File 42 | writers map[string]*csv.Writer 43 | } 44 | 45 | // NewCSVOutput return a new CSV tester instance 46 | func NewCSVOutput(cfg *config.Config, outCfg *config.Output) (outputs.Output, error) { 47 | c := CSV{ 48 | logger: log.WithFields(logrus.Fields{"output": NameCSV}), 49 | config: outCfg.CSV, 50 | files: map[string]*os.File{}, 51 | writers: map[string]*csv.Writer{}, 52 | } 53 | if c.config.FilePath.NamePattern == "" { 54 | c.config.FilePath.NamePattern = "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.csv" 55 | } 56 | return c, nil 57 | } 58 | 59 | // Do make CSV outputs 60 | func (c CSV) Do(data outputs.Data) error { 61 | dataTable, ok := data.Data.(*outputs.Table) 62 | if !ok { 63 | return fmt.Errorf("data not in Table interface format for csv output") 64 | } 65 | 66 | filename, err := outputs.GetFilenameFromPattern(c.config.FilePath.NamePattern, "", data, nil) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | var writeHeaders bool 72 | 73 | outPath := filepath.Join(c.config.FilePath.FilePath, filename) 74 | writer, ok := c.writers[outPath] 75 | if !ok { 76 | file, ok := c.files[outPath] 77 | if !ok { 78 | file, err = os.Create(outPath) 79 | if err != nil { 80 | return err 81 | } 82 | c.files[outPath] = file 83 | } 84 | 85 | writer = csv.NewWriter(file) 86 | writer.Comma = *c.config.Separator 87 | c.writers[outPath] = writer 88 | writeHeaders = true 89 | } 90 | 91 | defer writer.Flush() 92 | 93 | if writeHeaders { 94 | // Iterate over header columns 95 | headers := []string{} 96 | for _, r := range dataTable.Headers { 97 | if r == nil { 98 | continue 99 | } 100 | headers = append(headers, util.CastToString(r.Value)) 101 | } 102 | if err := writer.Write(headers); err != nil { 103 | return err 104 | } 105 | } 106 | 107 | // Iterate over data columns 108 | for _, row := range dataTable.Rows { 109 | cells := []string{} 110 | for _, r := range row { 111 | if r == nil { 112 | continue 113 | } 114 | cells = append(cells, util.CastToString(r.Value)) 115 | } 116 | if len(cells) == 0 { 117 | continue 118 | } 119 | 120 | if err := writer.Write(cells); err != nil { 121 | return err 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // OutputFiles return a list of output files 129 | func (c CSV) OutputFiles() []string { 130 | list := []string{} 131 | for file := range c.files { 132 | list = append(list, file) 133 | } 134 | return list 135 | } 136 | 137 | // Close close all file descriptors here 138 | func (c CSV) Close() error { 139 | for name, writer := range c.writers { 140 | c.logger.WithFields(logrus.Fields{"filepath": name}).Debug("closing file") 141 | writer.Flush() 142 | if err := writer.Error(); err != nil { 143 | c.logger.WithFields(logrus.Fields{"filepath": name}).Errorf("error flushing file. %+v", err) 144 | } 145 | } 146 | 147 | for name, file := range c.files { 148 | c.logger.WithFields(logrus.Fields{"filepath": name}).Debug("closing file") 149 | if err := file.Close(); err != nil { 150 | c.logger.WithFields(logrus.Fields{"filepath": name}).Errorf("error closing file. %+v", err) 151 | } 152 | } 153 | 154 | return nil 155 | 156 | } 157 | -------------------------------------------------------------------------------- /testers/pingparsing/pingparsing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package pingparsing 15 | 16 | import ( 17 | "fmt" 18 | 19 | "github.com/cloudical-io/ancientt/pkg/config" 20 | "github.com/cloudical-io/ancientt/testers" 21 | "github.com/sirupsen/logrus" 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // NamePingParsing PingParsing tester name 26 | const NamePingParsing = "pingparsing" 27 | 28 | func init() { 29 | testers.Factories[NamePingParsing] = NewPingParsingTester 30 | } 31 | 32 | // PingParsing PingParsing tester structure 33 | type PingParsing struct { 34 | testers.Tester 35 | logger *log.Entry 36 | config *config.PingParsing 37 | } 38 | 39 | // NewPingParsingTester return a new PingParsing tester instance 40 | func NewPingParsingTester(cfg *config.Config, test *config.Test) (testers.Tester, error) { 41 | if test == nil { 42 | test = &config.Test{ 43 | PingParsing: &config.PingParsing{}, 44 | } 45 | } 46 | 47 | return PingParsing{ 48 | logger: log.WithFields(logrus.Fields{"tester": NamePingParsing}), 49 | config: test.PingParsing, 50 | }, nil 51 | } 52 | 53 | // Plan 54 | func (t PingParsing) Plan(env *testers.Environment, test *config.Test) (*testers.Plan, error) { 55 | plan := &testers.Plan{ 56 | Tester: test.Type, 57 | AffectedServers: map[string]*testers.Host{}, 58 | Commands: make([][]*testers.Task, test.RunOptions.Rounds), 59 | } 60 | 61 | for i := 0; i < test.RunOptions.Rounds; i++ { 62 | for _, server := range env.Hosts.Servers { 63 | round := &testers.Task{ 64 | Status: &testers.Status{ 65 | SuccessfulHosts: testers.StatusHosts{ 66 | Servers: map[string]int{}, 67 | Clients: map[string]int{}, 68 | }, 69 | FailedHosts: testers.StatusHosts{ 70 | Servers: map[string]int{}, 71 | Clients: map[string]int{}, 72 | }, 73 | Errors: map[string][]error{}, 74 | }, 75 | } 76 | // Add server host to AffectedServers list 77 | if _, ok := plan.AffectedServers[server.Name]; !ok { 78 | plan.AffectedServers[server.Name] = server 79 | } 80 | 81 | // Setting a server is not really needed, but we set it to `sleep 99999` 82 | // for compatibility with Runners such as Kubernetes where the IP is only 83 | // available when a Server Pod is running 84 | round.Host = server 85 | round.Command, round.Args = t.buildPingParsingServerCommand(server) 86 | 87 | // Now go over each client and generate their Task 88 | for _, client := range env.Hosts.Clients { 89 | // Add client host to AffectedServers list 90 | if _, ok := plan.AffectedServers[client.Name]; !ok { 91 | plan.AffectedServers[client.Name] = client 92 | } 93 | 94 | // Build the PingParsing command 95 | cmd, args := t.buildPingParsingClientCommand(server, client) 96 | round.SubTasks = append(round.SubTasks, &testers.Task{ 97 | Host: client, 98 | Command: cmd, 99 | Args: args, 100 | }) 101 | } 102 | plan.Commands[i] = append(plan.Commands[i], round) 103 | 104 | // Add the given interval after each round except the last one 105 | if test.RunOptions.Interval != 0 && i != test.RunOptions.Rounds-1 { 106 | plan.Commands[i] = append(plan.Commands[i], &testers.Task{ 107 | Sleep: test.RunOptions.Interval, 108 | }) 109 | } 110 | } 111 | } 112 | 113 | return plan, nil 114 | } 115 | 116 | // buildPingParsingServerCommand 117 | func (t PingParsing) buildPingParsingServerCommand(server *testers.Host) (string, []string) { 118 | return "sleep", []string{"9999999"} 119 | } 120 | 121 | // buildPingParsingClientCommand 122 | func (t PingParsing) buildPingParsingClientCommand(server *testers.Host, client *testers.Host) (string, []string) { 123 | // Base command and args 124 | cmd := "pingparsing" 125 | args := []string{ 126 | "--icmp-reply", 127 | "--timestamp=datetime", 128 | fmt.Sprintf("-c=%d", *t.config.Count), 129 | fmt.Sprintf("-w=%s", *t.config.Deadline), 130 | fmt.Sprintf("--timeout=%s", *t.config.Timeout), 131 | fmt.Sprintf("-I=%s", t.config.Interface), 132 | "{{ .ServerAddressV4 }}", 133 | } 134 | 135 | return cmd, args 136 | } 137 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudical-io/ancientt 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0 7 | github.com/DATA-DOG/go-sqlmock v1.4.1 8 | github.com/creasty/defaults v1.5.2 9 | github.com/go-sql-driver/mysql v1.6.0 10 | github.com/jmoiron/sqlx v1.3.4 11 | github.com/k0kubun/pp v3.0.1+incompatible 12 | github.com/logrusorgru/aurora v2.0.3+incompatible 13 | github.com/mattn/go-isatty v0.0.14 14 | github.com/mattn/go-sqlite3 v1.14.11 15 | github.com/mitchellh/go-homedir v1.1.0 16 | github.com/prometheus/common v0.32.1 17 | github.com/sirupsen/logrus v1.8.1 18 | github.com/spf13/cobra v1.3.0 19 | github.com/spf13/viper v1.10.1 20 | github.com/stretchr/testify v1.7.0 21 | github.com/wcharczuk/go-chart v2.0.2-0.20190910040548-3a7bc5543113+incompatible 22 | gopkg.in/go-playground/validator.v9 v9.31.0 23 | gopkg.in/yaml.v2 v2.4.0 24 | k8s.io/api v0.23.3 25 | k8s.io/apimachinery v0.23.3 26 | k8s.io/client-go v0.23.3 27 | ) 28 | 29 | require ( 30 | cloud.google.com/go/compute v1.3.0 // indirect 31 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 32 | github.com/Azure/go-autorest/autorest v0.11.24 // indirect 33 | github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect 34 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 35 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 36 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 37 | github.com/beorn7/perks v1.0.1 // indirect 38 | github.com/blend/go-sdk v2.0.0+incompatible // indirect 39 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 40 | github.com/davecgh/go-spew v1.1.1 // indirect 41 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 42 | github.com/fsnotify/fsnotify v1.5.1 // indirect 43 | github.com/go-logr/logr v1.2.2 // indirect 44 | github.com/go-playground/locales v0.14.0 // indirect 45 | github.com/go-playground/universal-translator v0.18.0 // indirect 46 | github.com/gogo/protobuf v1.3.2 // indirect 47 | github.com/golang-jwt/jwt/v4 v4.3.0 // indirect 48 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 49 | github.com/golang/protobuf v1.5.2 // indirect 50 | github.com/google/go-cmp v0.5.7 // indirect 51 | github.com/google/gofuzz v1.2.0 // indirect 52 | github.com/googleapis/gnostic v0.5.5 // indirect 53 | github.com/hashicorp/hcl v1.0.0 // indirect 54 | github.com/imdario/mergo v0.3.12 // indirect 55 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 56 | github.com/json-iterator/go v1.1.12 // indirect 57 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect 58 | github.com/leodido/go-urn v1.2.1 // indirect 59 | github.com/magiconair/properties v1.8.5 // indirect 60 | github.com/mattn/go-colorable v0.1.12 // indirect 61 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 62 | github.com/mitchellh/mapstructure v1.4.3 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 66 | github.com/pelletier/go-toml v1.9.4 // indirect 67 | github.com/pkg/errors v0.9.1 // indirect 68 | github.com/pmezard/go-difflib v1.0.0 // indirect 69 | github.com/prometheus/client_golang v1.12.1 // indirect 70 | github.com/prometheus/client_model v0.2.0 // indirect 71 | github.com/prometheus/procfs v0.7.3 // indirect 72 | github.com/richardlehane/mscfb v1.0.4 // indirect 73 | github.com/richardlehane/msoleps v1.0.1 // indirect 74 | github.com/spf13/afero v1.8.1 // indirect 75 | github.com/spf13/cast v1.4.1 // indirect 76 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 77 | github.com/spf13/pflag v1.0.5 // indirect 78 | github.com/subosito/gotenv v1.2.0 // indirect 79 | github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d // indirect 80 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 81 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect 82 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 83 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 84 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 85 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 86 | golang.org/x/text v0.3.7 // indirect 87 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 88 | google.golang.org/appengine v1.6.7 // indirect 89 | google.golang.org/protobuf v1.27.1 // indirect 90 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 91 | gopkg.in/inf.v0 v0.9.1 // indirect 92 | gopkg.in/ini.v1 v1.66.4 // indirect 93 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 94 | k8s.io/klog/v2 v2.40.1 // indirect 95 | k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect 96 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 97 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 98 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 99 | sigs.k8s.io/yaml v1.3.0 // indirect 100 | ) 101 | -------------------------------------------------------------------------------- /outputs/excelize/excelize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package excelize 15 | 16 | import ( 17 | "fmt" 18 | "path" 19 | 20 | //include excelize library for .xlsx output 21 | "github.com/360EntSecGroup-Skylar/excelize/v2" 22 | 23 | "github.com/cloudical-io/ancientt/outputs" 24 | "github.com/cloudical-io/ancientt/pkg/config" 25 | "github.com/cloudical-io/ancientt/pkg/util" 26 | "github.com/sirupsen/logrus" 27 | log "github.com/sirupsen/logrus" 28 | ) 29 | 30 | // NameExcelize Excelize output name 31 | const NameExcelize = "excelize" 32 | 33 | func init() { 34 | outputs.Factories[NameExcelize] = NewExcelizeOutput 35 | } 36 | 37 | // Excelize Excelize tester structure 38 | type Excelize struct { 39 | outputs.Output 40 | logger *log.Entry 41 | config *config.Excelize 42 | files map[string]*fileState 43 | } 44 | 45 | type fileState struct { 46 | file *excelize.File 47 | row int 48 | } 49 | 50 | // NewExcelizeOutput return a new Excelize tester instance 51 | func NewExcelizeOutput(cfg *config.Config, outCfg *config.Output) (outputs.Output, error) { 52 | excelize := Excelize{ 53 | logger: log.WithFields(logrus.Fields{"output": NameExcelize}), 54 | config: outCfg.Excelize, 55 | files: map[string]*fileState{}, 56 | } 57 | if excelize.config.FilePath.NamePattern == "" { 58 | excelize.config.FilePath.NamePattern = "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.xlsx" 59 | } 60 | if excelize.config.SaveAfterRows == 0 { 61 | excelize.config.SaveAfterRows = 200 62 | } 63 | return excelize, nil 64 | } 65 | 66 | // Do Inputs the data into the excel sheet, contains all logic necessary to perform this task 67 | func (e Excelize) Do(data outputs.Data) error { 68 | dataTable, ok := data.Data.(*outputs.Table) 69 | if !ok { 70 | return fmt.Errorf("data not in Table data type for excel output") 71 | } 72 | 73 | outputFilename, err := outputs.GetFilenameFromPattern(e.config.FilePath.NamePattern, "", data, nil) 74 | if err != nil { 75 | return err 76 | } 77 | filePath := path.Join(e.config.FilePath.FilePath, outputFilename) 78 | 79 | // Check if the file had already been opened, reuse it if so 80 | var fState *fileState 81 | if _, ok := e.files[filePath]; !ok { 82 | // Create new excelize file 83 | excelFile := excelize.NewFile() 84 | excelFile.Path = filePath 85 | 86 | // Initial state for a new file 87 | // The fileState of a file will keep the *excelize.Fileand the current row 88 | // Current row is needed if the file is reused as otherwise it would start 89 | // at the first row again 90 | state := &fileState{ 91 | file: excelFile, 92 | row: 1, 93 | } 94 | fState = state 95 | e.files[filePath] = state 96 | } else { 97 | fState = e.files[filePath] 98 | } 99 | 100 | // Initially save file on each (re-)use 101 | if err = fState.file.Save(); err != nil { 102 | return err 103 | } 104 | 105 | if fState.row == 1 { 106 | if err := e.inputData(fState.row, [][]*outputs.Row{dataTable.Headers}, fState); err != nil { 107 | return err 108 | } 109 | } 110 | if err := e.inputData(fState.row, dataTable.Rows, fState); err != nil { 111 | return err 112 | } 113 | 114 | // NOTE If this isn't enough use the `Close()` func 115 | if err = fState.file.Save(); err != nil { 116 | return err 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (e Excelize) inputData(startRow int, rows [][]*outputs.Row, fState *fileState) error { 123 | // Iterate over data columns to get the first row of data. 124 | for i, row := range rows { 125 | fState.row++ 126 | 127 | // Set each cell value 128 | for j, r := range row { 129 | if r == nil { 130 | continue 131 | } 132 | 133 | if err := fState.file.SetCellValue("Sheet1", fmt.Sprintf("%s%d", util.IntToChar(j+1), startRow+i), r.Value); err != nil { 134 | // TODO Return a final concated error after the whole data has been written 135 | e.logger.WithFields(logrus.Fields{"filepath": fState.file.Path}).Errorf("unable to set cell value in excelize file. %+v", err) 136 | } 137 | } 138 | 139 | if i != 0 && (e.config.SaveAfterRows%i) == 0 { 140 | if err := fState.file.Save(); err != nil { 141 | return err 142 | } 143 | } 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // OutputFiles return a list of output files 150 | func (e Excelize) OutputFiles() []string { 151 | list := []string{} 152 | for file := range e.files { 153 | list = append(list, file) 154 | } 155 | return list 156 | } 157 | 158 | // Close Nothing to do here, all files are written during "creation" and "usage" (writing data) to the files 159 | func (e Excelize) Close() error { 160 | return nil 161 | } 162 | 163 | func toChar(i int) rune { 164 | return rune('A' - 1 + i) 165 | } 166 | -------------------------------------------------------------------------------- /pkg/config/defaults.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package config 15 | 16 | import ( 17 | "time" 18 | 19 | corev1 "k8s.io/api/core/v1" 20 | 21 | "github.com/cloudical-io/ancientt/pkg/ansible" 22 | "github.com/cloudical-io/ancientt/pkg/util" 23 | ) 24 | 25 | // Defaults interface to implement for config parts which allow a "verification" / Setting Defaults 26 | type Defaults interface { 27 | Defaults() 28 | } 29 | 30 | // SetDefaults set defaults on config part 31 | func (c *RunnerKubernetes) SetDefaults() { 32 | if c.Annotations == nil { 33 | c.Annotations = map[string]string{} 34 | } 35 | 36 | if c.Image == "" { 37 | c.Image = "quay.io/galexrt/container-toolbox:v20210915-101121-713" 38 | } 39 | 40 | if c.Namespace == "" { 41 | c.Namespace = "ancientt" 42 | } 43 | } 44 | 45 | // SetDefaults set defaults on config part 46 | func (c *KubernetesHosts) SetDefaults() { 47 | if c.IgnoreSchedulingDisabled == nil { 48 | c.IgnoreSchedulingDisabled = util.BoolTruePointer() 49 | } 50 | if c.Tolerations == nil { 51 | c.Tolerations = []corev1.Toleration{} 52 | } 53 | } 54 | 55 | // SetDefaults set defaults on config part 56 | func (c *KubernetesTimeouts) SetDefaults() { 57 | if c.DeleteTimeout == 0 { 58 | c.DeleteTimeout = 20 59 | } 60 | if c.RunningTimeout == 0 { 61 | c.RunningTimeout = 60 62 | } 63 | if c.SucceedTimeout == 0 { 64 | c.SucceedTimeout = 60 65 | } 66 | } 67 | 68 | // SetDefaults set defaults on config part 69 | func (c *RunnerAnsible) SetDefaults() { 70 | if c.AnsibleCommand == "" { 71 | c.AnsibleCommand = ansible.AnsibleCommand 72 | } 73 | if c.AnsibleInventoryCommand == "" { 74 | c.AnsibleInventoryCommand = ansible.AnsibleInventoryCommand 75 | } 76 | 77 | if c.Timeouts == nil { 78 | c.Timeouts = &AnsibleTimeouts{} 79 | } 80 | if c.Timeouts.CommandTimeout == 0 { 81 | c.Timeouts.CommandTimeout = 20 * time.Second 82 | } 83 | if c.Timeouts.TaskCommandTimeout == 0 { 84 | c.Timeouts.TaskCommandTimeout = 45 * time.Second 85 | } 86 | 87 | if c.CommandRetries == nil || *c.CommandRetries == 0 { 88 | defVal := 10 89 | c.CommandRetries = &defVal 90 | } 91 | if c.ParallelHostFactCalls == nil || *c.ParallelHostFactCalls == 0 { 92 | defVal := 7 93 | c.ParallelHostFactCalls = &defVal 94 | } 95 | 96 | if c.Groups == nil { 97 | c.Groups = &AnsibleGroups{} 98 | } 99 | if c.Groups.Server == "" { 100 | c.Groups.Server = "server" 101 | } 102 | if c.Groups.Clients == "" { 103 | c.Groups.Clients = "clients" 104 | } 105 | } 106 | 107 | // SetDefaults set defaults on config part 108 | func (c *RunOptions) SetDefaults() { 109 | if c.ContinueOnError == nil { 110 | c.ContinueOnError = util.BoolTruePointer() 111 | } 112 | 113 | if c.Rounds == 0 { 114 | c.Rounds = 1 115 | } 116 | 117 | if c.Interval == 0 { 118 | c.Interval = 10 * time.Second 119 | } 120 | 121 | if c.Mode == "" { 122 | c.Mode = RunModeSequential 123 | } 124 | } 125 | 126 | // SetDefaults set defaults on config part 127 | func (c *IPerf3) SetDefaults() { 128 | if c.Duration == nil { 129 | defValue := 10 130 | c.Duration = &defValue 131 | } 132 | 133 | if c.Interval == nil { 134 | defValue := 1 135 | c.Interval = &defValue 136 | } 137 | 138 | if c.UDP == nil { 139 | c.UDP = util.BoolFalsePointer() 140 | } 141 | } 142 | 143 | // SetDefaults set defaults on config part 144 | func (c *PingParsing) SetDefaults() { 145 | if c.Count == nil { 146 | defValue := 10 147 | c.Count = &defValue 148 | } 149 | 150 | if c.Deadline == nil { 151 | defValue := 15 * time.Second 152 | c.Deadline = &defValue 153 | } 154 | 155 | if c.Timeout == nil { 156 | defValue := 10 * time.Second 157 | c.Timeout = &defValue 158 | } 159 | } 160 | 161 | // SetDefaults set defaults on config part 162 | func (c *AdditionalFlags) SetDefaults() { 163 | if c.Server == nil { 164 | c.Server = []string{} 165 | } 166 | if c.Clients == nil { 167 | c.Clients = []string{} 168 | } 169 | } 170 | 171 | // SetDefaults set defaults on config part 172 | func (c *Excelize) SetDefaults() { 173 | if c.SaveAfterRows == 0 { 174 | c.SaveAfterRows = 1 175 | } 176 | } 177 | 178 | // SetDefaults set defaults on config part 179 | func (c *MySQL) SetDefaults() { 180 | if c.AutoCreateTables == nil { 181 | c.AutoCreateTables = util.BoolTruePointer() 182 | } 183 | } 184 | 185 | // SetDefaults set defaults on config part 186 | func (c *CSV) SetDefaults() { 187 | if c.Separator == nil { 188 | semiColon := ';' 189 | c.Separator = &semiColon 190 | } 191 | } 192 | 193 | // SetDefaults set defaults on confg part 194 | func (c *GoChartGraph) SetDefaults() { 195 | if c.WithLinearRegression == nil { 196 | c.WithLinearRegression = util.BoolFalsePointer() 197 | } 198 | if c.WithSimpleMovingAverage == nil { 199 | c.WithSimpleMovingAverage = util.BoolTruePointer() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /testers/iperf3/iperf3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package iperf3 15 | 16 | import ( 17 | "fmt" 18 | 19 | "github.com/cloudical-io/ancientt/pkg/config" 20 | "github.com/cloudical-io/ancientt/testers" 21 | "github.com/sirupsen/logrus" 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // NameIPerf3 IPerf3 tester name 26 | const NameIPerf3 = "iperf3" 27 | 28 | func init() { 29 | testers.Factories[NameIPerf3] = NewIPerf3Tester 30 | } 31 | 32 | // IPerf3 IPerf3 tester structure 33 | type IPerf3 struct { 34 | testers.Tester 35 | logger *log.Entry 36 | config *config.IPerf3 37 | } 38 | 39 | // NewIPerf3Tester return a new IPerf3 tester instance 40 | func NewIPerf3Tester(cfg *config.Config, test *config.Test) (testers.Tester, error) { 41 | if test == nil { 42 | test = &config.Test{ 43 | IPerf3: &config.IPerf3{}, 44 | } 45 | } 46 | 47 | return IPerf3{ 48 | logger: log.WithFields(logrus.Fields{"tester": NameIPerf3}), 49 | config: test.IPerf3, 50 | }, nil 51 | } 52 | 53 | // Plan return a plan to run IPerf3 from the given config.Test and Environment information (hosts) 54 | func (t IPerf3) Plan(env *testers.Environment, test *config.Test) (*testers.Plan, error) { 55 | plan := &testers.Plan{ 56 | Tester: test.Type, 57 | AffectedServers: map[string]*testers.Host{}, 58 | Commands: make([][]*testers.Task, test.RunOptions.Rounds), 59 | } 60 | 61 | var ports testers.Ports 62 | if t.config.UDP != nil && *t.config.UDP { 63 | ports = testers.Ports{ 64 | UDP: []int32{5601}, 65 | } 66 | } else { 67 | ports = testers.Ports{ 68 | TCP: []int32{5601}, 69 | } 70 | } 71 | 72 | for i := 0; i < test.RunOptions.Rounds; i++ { 73 | for _, server := range env.Hosts.Servers { 74 | round := &testers.Task{ 75 | Status: &testers.Status{ 76 | SuccessfulHosts: testers.StatusHosts{ 77 | Servers: map[string]int{}, 78 | Clients: map[string]int{}, 79 | }, 80 | FailedHosts: testers.StatusHosts{ 81 | Servers: map[string]int{}, 82 | Clients: map[string]int{}, 83 | }, 84 | Errors: map[string][]error{}, 85 | }, 86 | } 87 | // Add server host to AffectedServers list 88 | if _, ok := plan.AffectedServers[server.Name]; !ok { 89 | plan.AffectedServers[server.Name] = server 90 | } 91 | 92 | // Set the server that will run the iperf3 server in the "main" command 93 | round.Host = server 94 | round.Command, round.Args = t.buildIPerf3ServerCommand(server) 95 | round.Ports = ports 96 | 97 | // Now go over each client and generate their Task 98 | for _, client := range env.Hosts.Clients { 99 | // Add client host to AffectedServers list 100 | if _, ok := plan.AffectedServers[client.Name]; !ok { 101 | plan.AffectedServers[client.Name] = client 102 | } 103 | 104 | // Build the IPerf3 command 105 | cmd, args := t.buildIPerf3ClientCommand(server, client) 106 | round.SubTasks = append(round.SubTasks, &testers.Task{ 107 | Host: client, 108 | Command: cmd, 109 | Args: args, 110 | Ports: ports, 111 | }) 112 | } 113 | plan.Commands[i] = append(plan.Commands[i], round) 114 | 115 | // Add the given interval after each round except the last one 116 | if test.RunOptions.Interval != 0 && i != test.RunOptions.Rounds-1 { 117 | plan.Commands[i] = append(plan.Commands[i], &testers.Task{ 118 | Sleep: test.RunOptions.Interval, 119 | }) 120 | } 121 | } 122 | } 123 | 124 | return plan, nil 125 | } 126 | 127 | // buildIPerf3ServerCommand generate IPer3 server command 128 | func (t IPerf3) buildIPerf3ServerCommand(server *testers.Host) (string, []string) { 129 | // Base command and args 130 | cmd := "iperf3" 131 | args := []string{ 132 | "--json", 133 | "--port={{ .ServerPort }}", 134 | "--server", 135 | } 136 | 137 | // Add --udp flag when UDP should be used 138 | if t.config.UDP != nil && *t.config.UDP { 139 | args = append(args, "--udp") 140 | } 141 | 142 | // Append additional server flags to args array 143 | args = append(args, t.config.AdditionalFlags.Server...) 144 | 145 | return cmd, args 146 | } 147 | 148 | // buildIPerf3ClientCommand generate IPer3 client command 149 | func (t IPerf3) buildIPerf3ClientCommand(server *testers.Host, client *testers.Host) (string, []string) { 150 | // Base command and args 151 | cmd := "iperf3" 152 | args := []string{ 153 | fmt.Sprintf("--time=%d", *t.config.Duration), 154 | fmt.Sprintf("--interval=%d", *t.config.Interval), 155 | "--json", 156 | "--port={{ .ServerPort }}", 157 | "--client={{ .ServerAddressV4 }}", 158 | } 159 | 160 | // Add --udp flag when UDP should be used 161 | if t.config.UDP != nil && *t.config.UDP { 162 | args = append(args, "--udp") 163 | } 164 | 165 | // Append additional client flags to args array 166 | args = append(args, t.config.AdditionalFlags.Clients...) 167 | 168 | return cmd, args 169 | } 170 | -------------------------------------------------------------------------------- /parsers/iperf3/iperf3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package iperf3 15 | 16 | import ( 17 | "bytes" 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | 22 | "github.com/cloudical-io/ancientt/outputs" 23 | "github.com/cloudical-io/ancientt/parsers" 24 | "github.com/cloudical-io/ancientt/pkg/config" 25 | models "github.com/cloudical-io/ancientt/pkg/models/iperf3" 26 | "github.com/cloudical-io/ancientt/pkg/util" 27 | "github.com/sirupsen/logrus" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | // NameIPerf3 IPerf3 tester name 32 | const NameIPerf3 = "iperf3" 33 | 34 | func init() { 35 | parsers.Factories[NameIPerf3] = NewIPerf3Tester 36 | } 37 | 38 | // IPerf3 IPerf3 tester structure 39 | type IPerf3 struct { 40 | parsers.Parser 41 | logger *log.Entry 42 | config *config.Test 43 | } 44 | 45 | // NewIPerf3Tester return a new IPerf3 tester instance 46 | func NewIPerf3Tester(cfg *config.Config, test *config.Test) (parsers.Parser, error) { 47 | return IPerf3{ 48 | logger: log.WithFields(logrus.Fields{"parers": NameIPerf3}), 49 | config: test, 50 | }, nil 51 | } 52 | 53 | // Parse parse IPerf3 JSON responses 54 | func (p IPerf3) Parse(doneCh chan struct{}, inCh <-chan parsers.Input, dataCh chan<- outputs.Data) error { 55 | for { 56 | select { 57 | case <-doneCh: 58 | return nil 59 | case input, ok := <-inCh: 60 | if !ok { 61 | return nil 62 | } 63 | if input.ClientHost == "" && input.ServerHost == "" && input.Tester == "" { 64 | log.Warn("received input.Data with empty input.Tester and others are empty, 'signal' channel closed") 65 | close(dataCh) 66 | return nil 67 | } 68 | if err := p.parse(input, dataCh); err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | } 74 | 75 | func (p IPerf3) parse(input parsers.Input, dataCh chan<- outputs.Data) error { 76 | var logs *bytes.Buffer 77 | if input.DataStream != nil { 78 | logs = new(bytes.Buffer) 79 | if _, err := io.Copy(logs, *input.DataStream); err != nil { 80 | return fmt.Errorf("error in copy information from logs to buffer") 81 | } 82 | if err := (*input.DataStream).Close(); err != nil { 83 | return fmt.Errorf("error during closing input.DataStream. %+v", err) 84 | } 85 | } else if len(input.Data) > 0 { 86 | // Directly pump the data in the logs var 87 | p.logger.Warn("received input.Data instead of input.DataStream, who wrote that runners without stream support") 88 | logs = bytes.NewBuffer(input.Data) 89 | } else { 90 | return fmt.Errorf("no data stream nor data from Input channel") 91 | } 92 | 93 | // Parse JSON response 94 | result := &models.ClientResult{} 95 | if err := json.Unmarshal(logs.Bytes(), result); err != nil { 96 | return err 97 | } 98 | 99 | intervalTable := &outputs.Table{ 100 | Headers: []*outputs.Row{ 101 | {Value: "test_time"}, 102 | {Value: "round"}, 103 | {Value: "tester"}, 104 | {Value: "server_host"}, 105 | {Value: "client_host"}, 106 | {Value: "socket"}, 107 | {Value: "start"}, 108 | {Value: "end"}, 109 | {Value: "seconds"}, 110 | {Value: "bytes"}, 111 | {Value: "bits_per_second"}, 112 | {Value: "retransmits"}, 113 | {Value: "snd_cwnd"}, 114 | {Value: "rtt"}, 115 | {Value: "rttvar"}, 116 | {Value: "pmtu"}, 117 | {Value: "omitted"}, 118 | {Value: "iperf3_version"}, 119 | {Value: "system_info"}, 120 | {Value: "additional_info"}, 121 | }, 122 | Rows: [][]*outputs.Row{}, 123 | } 124 | 125 | for _, interval := range result.Intervals { 126 | for _, stream := range interval.Streams { 127 | intervalTable.Rows = append(intervalTable.Rows, []*outputs.Row{ 128 | {Value: input.TestTime.Format(util.TimeDateFormat)}, 129 | {Value: input.Round}, 130 | {Value: input.Tester}, 131 | {Value: input.ServerHost}, 132 | {Value: input.ClientHost}, 133 | {Value: stream.Socket}, 134 | {Value: stream.Start}, 135 | {Value: stream.End}, 136 | {Value: stream.Seconds}, 137 | {Value: stream.Bytes}, 138 | {Value: stream.BitsPerSecond}, 139 | {Value: stream.Retransmits}, 140 | {Value: stream.SndCwnd}, 141 | {Value: stream.RTT}, 142 | {Value: stream.RTTVar}, 143 | {Value: stream.PMTU}, 144 | {Value: stream.Omitted}, 145 | {Value: result.Start.Version}, 146 | {Value: result.Start.SystemInfo}, 147 | {Value: input.AdditionalInfo}, 148 | }) 149 | } 150 | } 151 | 152 | p.logger.Debug("parsed data input") 153 | 154 | // Transform Input into outputs.Data struct 155 | data := outputs.Data{ 156 | TestStartTime: input.TestStartTime, 157 | TestTime: input.TestTime, 158 | AdditionalInfo: input.AdditionalInfo, 159 | ServerHost: input.ServerHost, 160 | ClientHost: input.ClientHost, 161 | Tester: input.Tester, 162 | Data: intervalTable, 163 | } 164 | 165 | p.logger.Debug("sending parsed data to dataCh") 166 | 167 | dataCh <- data 168 | 169 | // TODO generate sum and / or end table and send to output 170 | 171 | p.logger.Debug("sent parsed data to dataCh") 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /parsers/pingparsing/pingparsing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package pingparsing 15 | 16 | import ( 17 | "bytes" 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | 22 | "github.com/cloudical-io/ancientt/outputs" 23 | "github.com/cloudical-io/ancientt/parsers" 24 | "github.com/cloudical-io/ancientt/pkg/config" 25 | models "github.com/cloudical-io/ancientt/pkg/models/pingparsing" 26 | "github.com/cloudical-io/ancientt/pkg/util" 27 | "github.com/sirupsen/logrus" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | // NamePingParsing PingParsing tester name 32 | const NamePingParsing = "pingparsing" 33 | 34 | func init() { 35 | parsers.Factories[NamePingParsing] = NewPingParsingTester 36 | } 37 | 38 | // PingParsing PingParsing tester structure 39 | type PingParsing struct { 40 | parsers.Parser 41 | logger *log.Entry 42 | config *config.Test 43 | } 44 | 45 | // NewPingParsingTester return a new PingParsing tester instance 46 | func NewPingParsingTester(cfg *config.Config, test *config.Test) (parsers.Parser, error) { 47 | return PingParsing{ 48 | logger: log.WithFields(logrus.Fields{"parers": NamePingParsing}), 49 | config: test, 50 | }, nil 51 | } 52 | 53 | // Parse parse PingParsing JSON responses 54 | func (p PingParsing) Parse(doneCh chan struct{}, inCh <-chan parsers.Input, dataCh chan<- outputs.Data) error { 55 | for { 56 | select { 57 | case <-doneCh: 58 | return nil 59 | case input, ok := <-inCh: 60 | if !ok { 61 | return nil 62 | } 63 | if input.ClientHost == "" && input.ServerHost == "" && input.Tester == "" { 64 | log.Warn("received input.Data with empty input.Tester and others are empty, 'signal' channel closed") 65 | close(dataCh) 66 | return nil 67 | } 68 | if err := p.parse(input, dataCh); err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | } 74 | 75 | func (p PingParsing) parse(input parsers.Input, dataCh chan<- outputs.Data) error { 76 | var logs *bytes.Buffer 77 | if input.DataStream != nil { 78 | logs = new(bytes.Buffer) 79 | if _, err := io.Copy(logs, *input.DataStream); err != nil { 80 | return fmt.Errorf("error in copy information from logs to buffer") 81 | } 82 | if err := (*input.DataStream).Close(); err != nil { 83 | return fmt.Errorf("error during closing input.DataStream. %+v", err) 84 | } 85 | } else if len(input.Data) > 0 { 86 | // Directly pump the data in the logs var 87 | p.logger.Warn("received input.Data instead of input.DataStream, who wrote that runners without stream support") 88 | logs = bytes.NewBuffer(input.Data) 89 | } else { 90 | return fmt.Errorf("no data stream nor data from Input channel") 91 | } 92 | 93 | // Parse JSON response 94 | results := models.ClientResults{} 95 | if err := json.Unmarshal(logs.Bytes(), &results); err != nil { 96 | return err 97 | } 98 | 99 | table := &outputs.Table{ 100 | Headers: []*outputs.Row{ 101 | {Value: "test_time"}, 102 | {Value: "round"}, 103 | {Value: "tester"}, 104 | {Value: "server_host"}, 105 | {Value: "client_host"}, 106 | {Value: "target"}, 107 | {Value: "destination"}, 108 | {Value: "packet_transmit"}, 109 | {Value: "packet_receive"}, 110 | {Value: "packet_loss_rate"}, 111 | {Value: "packet_loss_count"}, 112 | {Value: "rtt_min"}, 113 | {Value: "rtt_avg"}, 114 | {Value: "rtt_max"}, 115 | {Value: "rtt_mdev"}, 116 | {Value: "packet_duplicate_rate"}, 117 | {Value: "packet_duplicate_count"}, 118 | {Value: "timestamp"}, 119 | {Value: "icmp_seq"}, 120 | {Value: "ttl"}, 121 | {Value: "time"}, 122 | {Value: "duplicate"}, 123 | {Value: "additional_info"}, 124 | }, 125 | Rows: [][]*outputs.Row{}, 126 | } 127 | 128 | for name, r := range results { 129 | base := []*outputs.Row{ 130 | {Value: input.TestTime.Format(util.TimeDateFormat)}, 131 | {Value: input.Round}, 132 | {Value: input.Tester}, 133 | {Value: input.ServerHost}, 134 | {Value: input.ClientHost}, 135 | {Value: name}, 136 | {Value: r.Destination}, 137 | {Value: r.PacketTransmit}, 138 | {Value: r.PacketReceive}, 139 | {Value: r.PacketLossRate}, 140 | {Value: r.PacketLossCount}, 141 | {Value: r.RTTMin}, 142 | {Value: r.RTTAvg}, 143 | {Value: r.RTTMax}, 144 | {Value: r.RTTMDev}, 145 | {Value: r.PacketDuplicateRate}, 146 | {Value: r.PacketDuplicateCount}, 147 | } 148 | for _, e := range r.ICMPReplies { 149 | table.Rows = append(table.Rows, append(base, []*outputs.Row{ 150 | {Value: e.Timestamp}, 151 | {Value: e.ICMPSeq}, 152 | {Value: e.TTL}, 153 | {Value: e.Time}, 154 | {Value: e.Duplicate}, 155 | {Value: input.AdditionalInfo}, 156 | }...)) 157 | } 158 | } 159 | 160 | p.logger.Debug("parsed data input") 161 | 162 | // Transform Input into outputs.Data struct 163 | data := outputs.Data{ 164 | TestStartTime: input.TestStartTime, 165 | TestTime: input.TestTime, 166 | AdditionalInfo: input.AdditionalInfo, 167 | ServerHost: input.ServerHost, 168 | ClientHost: input.ClientHost, 169 | Tester: input.Tester, 170 | Data: table, 171 | } 172 | 173 | p.logger.Debug("sending parsed data to dataCh") 174 | 175 | dataCh <- data 176 | 177 | // TODO generate sum and / or end table and send to output 178 | 179 | p.logger.Debug("sent parsed data to dataCh") 180 | 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /pkg/models/iperf3/iperf3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package iperf3 15 | 16 | // ClientResult IPerf3 client result output 17 | type ClientResult struct { 18 | Start Start `json:"start"` 19 | Intervals []Interval `json:"intervals"` 20 | End End `json:"end"` 21 | } 22 | 23 | // Start 24 | type Start struct { 25 | Connected []ConnectedEntry `json:"connected"` 26 | Version string `json:"version"` 27 | SystemInfo string `json:"system_info"` 28 | Timestamp Timestamp `json:"timestamp"` 29 | ConnectingTo ConnectingTo `json:"connecting_to"` 30 | Cookie string `json:"cookie"` 31 | TCPMSSDefault int64 `json:"tcp_mss_default"` 32 | SockBufsize int64 `json:"sock_bufsize"` 33 | SndbufActual int64 `json:"sndbuf_actual"` 34 | RcvbufActual int64 `json:"rcvbuf_actual"` 35 | TestStart TestStart `json:"test_start"` 36 | } 37 | 38 | // ConnectedEntry 39 | type ConnectedEntry struct { 40 | Socket int `json:"socket"` 41 | LocalHost string `json:"local_host"` 42 | LocalPort int `json:"local_port"` 43 | RemoteHost string `json:"remote_host"` 44 | RemotePort int `json:"remote_port"` 45 | } 46 | 47 | // Timestamp 48 | type Timestamp struct { 49 | Time string `json:"time"` 50 | Timesecs int64 `json:"timesecs"` 51 | } 52 | 53 | // ConnectingTo 54 | type ConnectingTo struct { 55 | Host string `json:"host"` 56 | Port int32 `json:"port"` 57 | } 58 | 59 | // TestStart 60 | type TestStart struct { 61 | Protocol string `json:"protocol"` 62 | NumStreams int64 `json:"num_streams"` 63 | BlkSize int64 `json:"blksize"` 64 | Omit int64 `json:"omit"` 65 | Duration int64 `json:"duration"` 66 | Bytes int64 `json:"bytes"` 67 | Blocks int64 `json:"blocks"` 68 | Reverse int64 `json:"reverse"` 69 | Tos int64 `json:"tos"` 70 | } 71 | 72 | // Interval 73 | type Interval struct { 74 | Streams []Stream `json:"streams"` 75 | Sum Sum `json:"sum"` 76 | } 77 | 78 | // Stream 79 | type Stream struct { 80 | Socket int64 `json:"socket"` 81 | Start float64 `json:"start"` 82 | End float64 `json:"end"` 83 | Seconds float64 `json:"seconds"` 84 | Bytes int64 `json:"bytes"` 85 | BitsPerSecond float64 `json:"bits_per_second"` 86 | Retransmits int64 `json:"retransmits"` 87 | SndCwnd int64 `json:"snd_cwnd"` 88 | RTT int64 `json:"rtt"` 89 | RTTVar int64 `json:"rttvar"` 90 | PMTU int64 `json:"pmtu"` 91 | Omitted bool `json:"omitted"` 92 | } 93 | 94 | // Sum 95 | type Sum struct { 96 | Start float64 `json:"start"` 97 | End float64 `json:"end"` 98 | Seconds float64 `json:"seconds"` 99 | Bytes int64 `json:"bytes"` 100 | BitsPerSecond float64 `json:"bits_per_second"` 101 | Retransmits int64 `json:"retransmits"` 102 | Omitted bool `json:"omitted"` 103 | } 104 | 105 | // End 106 | type End struct { 107 | Streams []EndStream `json:"streams"` 108 | SumSent SumSent `json:"sum_sent"` 109 | SumReceived SumReceived `json:"sum_received"` 110 | CPUUtilizationPercent CPUUtilizationPercent `json:"cpu_utilization_percent"` 111 | SenderTCPCongestion string `json:"sender_tcp_congestion"` 112 | ReceiverTCPCongestion string `json:"receiver_tcp_congestion"` 113 | } 114 | 115 | // EndStream 116 | type EndStream struct { 117 | Sender Sender `json:"sender"` 118 | Receiver Receiver `json:"receiver"` 119 | } 120 | 121 | // Sender 122 | type Sender struct { 123 | Socket int64 `json:"socket"` 124 | Start float64 `json:"start"` 125 | End float64 `json:"end"` 126 | Seconds float64 `json:"seconds"` 127 | Bytes int64 `json:"bytes"` 128 | BitsPerSecond float64 `json:"bits_per_second"` 129 | Retransmits int64 `json:"retransmits"` 130 | MaxSndCwnd int64 `json:"max_snd_cwnd"` 131 | MaxRTT int64 `json:"max_rtt"` 132 | MinRTT int64 `json:"min_rtt"` 133 | MeanRTT int64 `json:"mean_rtt"` 134 | } 135 | 136 | // Receiver 137 | type Receiver struct { 138 | Socket int64 `json:"socket"` 139 | Start float64 `json:"start"` 140 | End float64 `json:"end"` 141 | Seconds float64 `json:"seconds"` 142 | Bytes int64 `json:"bytes"` 143 | BitsPerSecond float64 `json:"bits_per_second"` 144 | } 145 | 146 | // SumSent 147 | type SumSent struct { 148 | Start float64 `json:"start"` 149 | End float64 `json:"end"` 150 | Seconds float64 `json:"seconds"` 151 | Bytes int64 `json:"bytes"` 152 | BitsPerSecond float64 `json:"bits_per_second"` 153 | Retransmits int64 `json:"retransmits"` 154 | } 155 | 156 | // SumReceived 157 | type SumReceived struct { 158 | Start float64 `json:"start"` 159 | End float64 `json:"end"` 160 | Seconds float64 `json:"seconds"` 161 | Bytes int64 `json:"bytes"` 162 | BitsPerSecond float64 `json:"bits_per_second"` 163 | } 164 | 165 | // CPUUtilizationPercent 166 | type CPUUtilizationPercent struct { 167 | HostTotal float64 `json:"host_total"` 168 | HostUser float64 `json:"host_user"` 169 | HostSystem float64 `json:"host_system"` 170 | RemoteTotal float64 `json:"remote_total"` 171 | RemoteUser float64 `json:"remote_user"` 172 | RemoteSystem float64 `json:"remote_system"` 173 | } 174 | -------------------------------------------------------------------------------- /pkg/k8sutil/k8sutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package k8sutil 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "time" 20 | 21 | "github.com/cloudical-io/ancientt/testers" 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/labels" 26 | "k8s.io/client-go/kubernetes" 27 | ) 28 | 29 | // PodRecreate delete Pod if it exists and create it again. If the Pod does not exist, create it. 30 | func PodRecreate(k8sclient kubernetes.Interface, pod *corev1.Pod, delTimeout int) error { 31 | // Delete Pod if it exists 32 | if err := PodDelete(k8sclient, pod, delTimeout); err != nil { 33 | return err 34 | } 35 | 36 | // Create Pod again 37 | ctx := context.TODO() 38 | if _, err := k8sclient.CoreV1().Pods(pod.ObjectMeta.Namespace).Create(ctx, pod, metav1.CreateOptions{}); err != nil { 39 | if !errors.IsAlreadyExists(err) { 40 | return err 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // PodDelete delete Pod if it exists, wait for it till it has been for custom amount deleted 48 | func PodDelete(k8sclient kubernetes.Interface, pod *corev1.Pod, timeout int) error { 49 | namespace := pod.ObjectMeta.Namespace 50 | podName := pod.ObjectMeta.Name 51 | 52 | // Delete Pod 53 | ctx := context.TODO() 54 | if err := k8sclient.CoreV1().Pods(namespace).Delete(ctx, podName, metav1.DeleteOptions{}); err != nil { 55 | if errors.IsNotFound(err) { 56 | return nil 57 | } 58 | return err 59 | } 60 | 61 | for i := 0; i < timeout; i++ { 62 | // Check if Pod still exists 63 | ctx := context.TODO() 64 | if _, err := k8sclient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}); err != nil { 65 | if errors.IsNotFound(err) { 66 | return nil 67 | } 68 | return err 69 | } 70 | 71 | time.Sleep(1 * time.Second) 72 | } 73 | 74 | return fmt.Errorf("pod %s/%s not deleted after 30s", namespace, podName) 75 | } 76 | 77 | // PodDeleteByName delete Pod by namespace and name if it exists 78 | func PodDeleteByName(k8sclient kubernetes.Interface, namespace string, podName string, timeout int) error { 79 | return PodDelete(k8sclient, &corev1.Pod{ 80 | ObjectMeta: metav1.ObjectMeta{ 81 | Namespace: namespace, 82 | Name: podName, 83 | }, 84 | }, timeout) 85 | } 86 | 87 | // PodDeleteByLabels delete Pods by labels 88 | func PodDeleteByLabels(k8sclient kubernetes.Interface, namespace string, selectorLabels map[string]string) error { 89 | set := labels.Set(selectorLabels) 90 | 91 | ctx := context.TODO() 92 | pods, err := k8sclient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ 93 | LabelSelector: set.AsSelector().String(), 94 | }) 95 | if err != nil { 96 | if errors.IsNotFound(err) { 97 | return nil 98 | } 99 | return err 100 | } 101 | 102 | if pods.Items == nil || len(pods.Items) == 0 { 103 | return nil 104 | } 105 | 106 | for _, pod := range pods.Items { 107 | // Delete Pods by labels 108 | ctx := context.TODO() 109 | if err := k8sclient.CoreV1().Pods(namespace).Delete(ctx, pod.ObjectMeta.Name, metav1.DeleteOptions{}); err != nil { 110 | if errors.IsNotFound(err) { 111 | return nil 112 | } 113 | return err 114 | } 115 | } 116 | return nil 117 | } 118 | 119 | // WaitForPodToRun wait for a Pod to be in phase Running. In case of phase Running, return true and no error 120 | func WaitForPodToRun(k8sclient kubernetes.Interface, namespace string, podName string, timeout int) (bool, error) { 121 | for i := 0; i < timeout; i++ { 122 | ctx := context.TODO() 123 | pod, err := k8sclient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) 124 | if err != nil { 125 | if !errors.IsAlreadyExists(err) { 126 | return false, err 127 | } 128 | } 129 | if pod.Status.Phase == corev1.PodRunning { 130 | return true, nil 131 | } 132 | 133 | time.Sleep(1 * time.Second) 134 | } 135 | 136 | return false, nil 137 | } 138 | 139 | // WaitForPodToSucceed wait for a Pod to be in phase Succeeded. In case of phase Succeeded, return true and no error 140 | func WaitForPodToSucceed(k8sclient kubernetes.Interface, namespace string, podName string, timeout int) (bool, error) { 141 | for i := 0; i < timeout; i++ { 142 | ctx := context.TODO() 143 | pod, err := k8sclient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) 144 | if err != nil { 145 | return false, err 146 | } 147 | if pod.Status.Phase == corev1.PodSucceeded { 148 | return true, nil 149 | } 150 | 151 | time.Sleep(1 * time.Second) 152 | } 153 | 154 | return false, nil 155 | } 156 | 157 | // WaitForPodToRunOrSucceed wait for a Pod to be in phase Running or Succeeded. In case of one of the phases, return true and no error 158 | func WaitForPodToRunOrSucceed(k8sclient kubernetes.Interface, namespace string, podName string, timeout int) (bool, error) { 159 | for i := 0; i < timeout; i++ { 160 | ctx := context.TODO() 161 | pod, err := k8sclient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) 162 | if err != nil { 163 | return false, err 164 | } 165 | if pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodSucceeded { 166 | return true, nil 167 | } 168 | 169 | time.Sleep(1 * time.Second) 170 | } 171 | 172 | return false, nil 173 | } 174 | 175 | // PortsListToPorts PortList testers.Port to Kubernetes []corev1.ContainerPort conversion (for TCP and UDP) 176 | func PortsListToPorts(list testers.Ports) []corev1.ContainerPort { 177 | ports := []corev1.ContainerPort{} 178 | for _, p := range list.TCP { 179 | ports = append(ports, corev1.ContainerPort{ 180 | ContainerPort: p, 181 | Protocol: corev1.ProtocolTCP, 182 | }) 183 | } 184 | for _, p := range list.UDP { 185 | ports = append(ports, corev1.ContainerPort{ 186 | ContainerPort: p, 187 | Protocol: corev1.ProtocolUDP, 188 | }) 189 | } 190 | return ports 191 | } 192 | -------------------------------------------------------------------------------- /testers/testers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package testers 15 | 16 | import ( 17 | "fmt" 18 | "time" 19 | 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | ) 22 | 23 | // Factories contains the list of all available testers. 24 | // The tester can each then be created using the function saved in the map. 25 | var Factories = make(map[string]func(cfg *config.Config, test *config.Test) (Tester, error)) 26 | 27 | // Tester is the interface a tester has to implement 28 | type Tester interface { 29 | // Plan return a map of commands and on which servers to run them thanks to the info of the runners.Runner 30 | Plan(env *Environment, test *config.Test) (*Plan, error) 31 | } 32 | 33 | // Environment environment information such as which hosts are doing what (clients, servers) 34 | type Environment struct { 35 | Hosts *Hosts 36 | } 37 | 38 | // Hosts contains a list of clients and servers hosts that will be used in the test environment. 39 | type Hosts struct { 40 | Clients map[string]*Host `yaml:"clients"` 41 | Servers map[string]*Host `yaml:"servers"` 42 | } 43 | 44 | // Host host information, like labels and addresses (will most of the time be filled by the runners.Runner) 45 | type Host struct { 46 | Name string `json:"name"` 47 | Labels map[string]string `json:"labels"` 48 | Addresses *IPAddresses `json:"addresses"` 49 | } 50 | 51 | // IPAddresses list of IPv4 and IPv6 addresses a host has 52 | type IPAddresses struct { 53 | IPv4 []string `json:"ipv4"` 54 | IPv6 []string `json:"ipv6"` 55 | } 56 | 57 | // Plan contains the information needed to execute the plan 58 | type Plan struct { 59 | TestStartTime time.Time `json:"plannedTime"` 60 | AffectedServers map[string]*Host `json:"affectedServers"` 61 | Commands [][]*Task `json:"commands"` 62 | Tester string `json:"tester"` 63 | RunOptions config.RunOptions `json:"runOptions"` 64 | } 65 | 66 | // PrettyPrint "pretty" prints a plan 67 | func (p Plan) PrettyPrint() { 68 | fmt.Println("-> BEGIN AffectedServers") 69 | for _, server := range p.AffectedServers { 70 | fmt.Println(server.Name) 71 | } 72 | fmt.Println("=> END AffectedServers") 73 | fmt.Println("-> BEGIN Commands") 74 | for k, commands := range p.Commands { 75 | round := k + 1 76 | fmt.Printf("--> BEGIN Round %d\n", round) 77 | for _, command := range commands { 78 | if command.Sleep != 0 { 79 | fmt.Printf("---> Wait for %+v\n", command.Sleep) 80 | continue 81 | } 82 | fmt.Printf("---> BEGIN Server %s\n", command.Host.Name) 83 | fmt.Printf("----> RUN %s %s (Additional info: %+v; %+v)\n", command.Command, command.Args, command.Ports, command.Sleep) 84 | for _, task := range command.SubTasks { 85 | fmt.Printf("-----> BEGIN Client %s\n", task.Host.Name) 86 | fmt.Printf("------> RUN %s %s (Additional info: %+v)\n", task.Command, task.Args, task.Ports) 87 | fmt.Printf("=====> END Client %s\n", task.Host.Name) 88 | } 89 | fmt.Printf("===> END Server %s\n", command.Host.Name) 90 | } 91 | fmt.Printf("==> END Round %d\n", round) 92 | } 93 | fmt.Println("=> END Commands") 94 | } 95 | 96 | // Task information for the task to execute 97 | type Task struct { 98 | Host *Host `json:"host"` 99 | Command string `json:"command"` 100 | Args []string `json:"args"` 101 | Sleep time.Duration `json:"sleep"` 102 | Ports Ports `json:"ports"` 103 | SubTasks []*Task `json:"subTasks"` 104 | Status *Status `yaml:"status"` 105 | } 106 | 107 | // Ports TCP and UDP ports list 108 | type Ports struct { 109 | TCP []int32 110 | UDP []int32 111 | } 112 | 113 | // Status status info for a task 114 | type Status struct { 115 | SuccessfulHosts StatusHosts `json:"successfulHosts"` 116 | FailedHosts StatusHosts `json:"failedHosts"` 117 | Errors map[string][]error `json:"errors"` 118 | } 119 | 120 | // StatusHosts status per servers and clients list with counter 121 | type StatusHosts struct { 122 | Servers map[string]int `json:"servers"` 123 | Clients map[string]int `json:"clients"` 124 | } 125 | 126 | // AddFailedServer add a server host that failed with error to the Status list 127 | func (st *Status) AddFailedServer(host *Host, err error) { 128 | if _, ok := st.Errors[host.Name]; !ok { 129 | st.Errors[host.Name] = []error{} 130 | } 131 | st.Errors[host.Name] = append(st.Errors[host.Name], err) 132 | 133 | // Increase failed host counter 134 | if _, ok := st.FailedHosts.Servers[host.Name]; !ok { 135 | st.FailedHosts.Servers[host.Name] = 1 136 | } else { 137 | st.FailedHosts.Servers[host.Name]++ 138 | } 139 | } 140 | 141 | // AddFailedClient add a client host that failed with error to the Status list 142 | func (st *Status) AddFailedClient(host *Host, err error) { 143 | if _, ok := st.Errors[host.Name]; !ok { 144 | st.Errors[host.Name] = []error{} 145 | } 146 | st.Errors[host.Name] = append(st.Errors[host.Name], err) 147 | 148 | // Increase failed host counter 149 | if _, ok := st.FailedHosts.Clients[host.Name]; !ok { 150 | st.FailedHosts.Clients[host.Name] = 1 151 | } else { 152 | st.FailedHosts.Clients[host.Name]++ 153 | } 154 | } 155 | 156 | // AddSuccessfulServer add a successful server host to the list 157 | func (st *Status) AddSuccessfulServer(host *Host) { 158 | // Increase successful host counter 159 | if _, ok := st.SuccessfulHosts.Servers[host.Name]; !ok { 160 | st.SuccessfulHosts.Servers[host.Name] = 1 161 | } else { 162 | st.SuccessfulHosts.Servers[host.Name]++ 163 | } 164 | } 165 | 166 | // AddSuccessfulClient add a successful client host to the list 167 | func (st *Status) AddSuccessfulClient(host *Host) { 168 | // Increase successful host counter 169 | if _, ok := st.SuccessfulHosts.Clients[host.Name]; !ok { 170 | st.SuccessfulHosts.Clients[host.Name] = 1 171 | } else { 172 | st.SuccessfulHosts.Clients[host.Name]++ 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /outputs/gochart/gochart.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package gochart 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "path/filepath" 20 | 21 | "github.com/cloudical-io/ancientt/outputs" 22 | "github.com/cloudical-io/ancientt/pkg/config" 23 | "github.com/cloudical-io/ancientt/pkg/util" 24 | "github.com/sirupsen/logrus" 25 | log "github.com/sirupsen/logrus" 26 | chart "github.com/wcharczuk/go-chart" 27 | ) 28 | 29 | // NameGoChart GoChart output name 30 | const NameGoChart = "gochart" 31 | 32 | func init() { 33 | outputs.Factories[NameGoChart] = NewGoChartOutput 34 | } 35 | 36 | // GoChart GoChart tester structure 37 | type GoChart struct { 38 | outputs.Output 39 | logger *log.Entry 40 | config *config.GoChart 41 | files map[string]struct{} 42 | } 43 | 44 | // NewGoChartOutput return a new GoChart tester instance 45 | func NewGoChartOutput(cfg *config.Config, outCfg *config.Output) (outputs.Output, error) { 46 | goChart := GoChart{ 47 | logger: log.WithFields(logrus.Fields{"output": NameGoChart}), 48 | config: outCfg.GoChart, 49 | files: map[string]struct{}{}, 50 | } 51 | if goChart.config.NamePattern == "" { 52 | goChart.config.NamePattern = "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}-{{ .Data.ServerHost }}_{{ .Data.ClientHost }}-{{ .Extra.Axises }}.png" 53 | } 54 | return goChart, nil 55 | } 56 | 57 | // Do make GoChart charts 58 | func (gc GoChart) Do(data outputs.Data) error { 59 | if _, ok := data.Data.(*outputs.Table); !ok { 60 | return fmt.Errorf("data not in data table format for gochart output") 61 | } 62 | 63 | // Iterate over wanted graph types 64 | for _, graph := range gc.config.Graphs { 65 | err := gc.drawAxisChart(graph, data) 66 | if err != nil { 67 | return err 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (gc *GoChart) drawAxisChart(chartOpts *config.GoChartGraph, data outputs.Data) error { 75 | dataTable, ok := data.Data.(*outputs.Table) 76 | if !ok { 77 | return fmt.Errorf("data not in table format for gochart output") 78 | } 79 | 80 | if len(dataTable.Headers) == 0 { 81 | gc.logger.Warning("no table headers found in data table result, returning") 82 | return nil 83 | } 84 | 85 | timeKey := chartOpts.TimeColumn 86 | 87 | var leftYKey string 88 | if chartOpts.LeftY != "" { 89 | leftYKey = chartOpts.LeftY 90 | } 91 | rightYKey := chartOpts.RightY 92 | 93 | graph := chart.Chart{ 94 | Series: []chart.Series{}, 95 | XAxis: chart.XAxis{ 96 | Name: timeKey, 97 | }, 98 | YAxis: chart.YAxis{ 99 | Name: rightYKey, 100 | }, 101 | } 102 | 103 | axises := rightYKey 104 | if chartOpts.LeftY != "" && leftYKey != "" { 105 | axises += "_" + leftYKey 106 | } 107 | filename, err := outputs.GetFilenameFromPattern(gc.config.FilePath.NamePattern, "", data, map[string]interface{}{ 108 | "Axises": axises, 109 | }) 110 | if err != nil { 111 | return err 112 | } 113 | outPath := filepath.Join(gc.config.FilePath.FilePath, filename) 114 | 115 | vals := map[string][]float64{} 116 | for _, search := range []string{chartOpts.TimeColumn, chartOpts.RightY, chartOpts.LeftY} { 117 | headIndex, err := dataTable.GetHeaderIndexByName(search) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | for _, r := range dataTable.Rows { 123 | // Skip empty rows 124 | if len(r) == 0 { 125 | continue 126 | } 127 | if len(r)-1 < headIndex || r[headIndex] == nil { 128 | return fmt.Errorf("unable to find header with index %d or nil (search: %q)", headIndex, search) 129 | } 130 | 131 | val, err := util.CastNumberToFloat64(r[headIndex].Value) 132 | if err != nil { 133 | return err 134 | } 135 | vals[search] = append(vals[search], val) 136 | } 137 | } 138 | 139 | series := chart.ContinuousSeries{ 140 | Name: rightYKey, 141 | Style: chart.Style{ 142 | StrokeColor: chart.GetDefaultColor(1).WithAlpha(64), 143 | FillColor: chart.GetDefaultColor(1).WithAlpha(64), 144 | }, 145 | XValues: vals[timeKey], 146 | YValues: vals[rightYKey], 147 | } 148 | graph.Series = append(graph.Series, series) 149 | gc.additionalSeries(chartOpts, &graph, &series) 150 | 151 | if chartOpts.LeftY != "" { 152 | if _, ok := vals[leftYKey]; ok { 153 | series = chart.ContinuousSeries{ 154 | Name: leftYKey, 155 | Style: chart.Style{ 156 | StrokeColor: chart.GetDefaultColor(4).WithAlpha(64), 157 | FillColor: chart.GetDefaultColor(4).WithAlpha(64), 158 | }, 159 | YAxis: chart.YAxisSecondary, 160 | XValues: vals[timeKey], 161 | YValues: vals[leftYKey], 162 | } 163 | graph.Series = append(graph.Series, series) 164 | gc.additionalSeries(chartOpts, &graph, &series) 165 | } 166 | } 167 | 168 | graph.Elements = []chart.Renderable{ 169 | chart.Legend(&graph), 170 | } 171 | 172 | gc.files[outPath] = struct{}{} 173 | 174 | buffer := bytes.NewBuffer([]byte{}) 175 | if err := graph.Render(chart.PNG, buffer); err != nil { 176 | return fmt.Errorf("failed to render graph to PNG file. %+v", err) 177 | } 178 | 179 | if err := util.WriteNewTruncFile(outPath, buffer.Bytes()); err != nil { 180 | return err 181 | } 182 | 183 | return nil 184 | } 185 | 186 | func (gc *GoChart) additionalSeries(chartOpts *config.GoChartGraph, graph *chart.Chart, series *chart.ContinuousSeries) { 187 | graph.Series = append(graph.Series, chart.LastValueAnnotationSeries(series), chart.LastValueAnnotationSeries(series)) 188 | 189 | if chartOpts.WithLinearRegression != nil && *chartOpts.WithLinearRegression { 190 | linearRegresSeries := &chart.LinearRegressionSeries{ 191 | Name: fmt.Sprintf("%q - LinearRegress", series.Name), 192 | InnerSeries: series, 193 | } 194 | graph.Series = append(graph.Series, linearRegresSeries) 195 | } 196 | if chartOpts.WithSimpleMovingAverage != nil && *chartOpts.WithSimpleMovingAverage { 197 | smaSeries := &chart.SMASeries{ 198 | Name: fmt.Sprintf("%q - SimpleMovingAvg", series.Name), 199 | InnerSeries: series, 200 | } 201 | graph.Series = append(graph.Series, smaSeries) 202 | } 203 | } 204 | 205 | // OutputFiles return a list of output files 206 | func (gc GoChart) OutputFiles() []string { 207 | list := []string{} 208 | for file := range gc.files { 209 | list = append(list, file) 210 | } 211 | return list 212 | } 213 | 214 | // Close NOOP, as graph pictures are written once and closed immediately, no need to do anything here 215 | func (gc GoChart) Close() error { 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /outputs/sqlite/sqlite.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package sqlite 15 | 16 | import ( 17 | "fmt" 18 | "path/filepath" 19 | 20 | "github.com/cloudical-io/ancientt/pkg/config" 21 | "github.com/cloudical-io/ancientt/pkg/util" 22 | "github.com/jmoiron/sqlx" 23 | 24 | // Include sqlite driver for sqlite output 25 | _ "github.com/mattn/go-sqlite3" 26 | 27 | "github.com/cloudical-io/ancientt/outputs" 28 | "github.com/sirupsen/logrus" 29 | log "github.com/sirupsen/logrus" 30 | ) 31 | 32 | // NameSQLite SQLite output name 33 | const ( 34 | NameSQLite = "sqlite" 35 | 36 | SQLiteIntType = "BIGINT" 37 | SQLiteFloatType = "FLOAT" 38 | SQLiteBoolType = "BOOLEAN" 39 | 40 | defaultNamePattern = "ancientt-{{ .TestStartTime }}-{{ .Data.Tester }}.sqlite3" 41 | defaultTableNamePattern = "ancientt{{ .TestStartTime }}{{ .Data.Tester }}{{ .Data.ServerHost }}{{ .Data.ClientHost }}" 42 | 43 | createTableBeginQuery = "CREATE TABLE IF NOT EXISTS `%s` (\n" 44 | createTableEndQuery = `);` 45 | insertDataBeginQuery = "INSERT INTO %s VALUES (" 46 | insertDataEndQuery = `);` 47 | ) 48 | 49 | func init() { 50 | outputs.Factories[NameSQLite] = NewSQLiteOutput 51 | } 52 | 53 | // SQLite SQLite tester structure 54 | type SQLite struct { 55 | outputs.Output 56 | logger *log.Entry 57 | config *config.SQLite 58 | dbCons map[string]*sqlx.DB 59 | } 60 | 61 | // NewSQLiteOutput return a new SQLite tester instance 62 | func NewSQLiteOutput(cfg *config.Config, outCfg *config.Output) (outputs.Output, error) { 63 | if outCfg == nil { 64 | outCfg = &config.Output{ 65 | SQLite: &config.SQLite{}, 66 | } 67 | } 68 | s := SQLite{ 69 | logger: log.WithFields(logrus.Fields{"output": NameSQLite}), 70 | config: outCfg.SQLite, 71 | dbCons: map[string]*sqlx.DB{}, 72 | } 73 | if s.config.NamePattern == "" { 74 | s.config.NamePattern = defaultNamePattern 75 | } 76 | if s.config.TableNamePattern == "" { 77 | s.config.TableNamePattern = defaultTableNamePattern 78 | } 79 | 80 | return s, nil 81 | } 82 | 83 | // Do make SQLite outputs 84 | func (s SQLite) Do(data outputs.Data) error { 85 | dataTable, ok := data.Data.(*outputs.Table) 86 | if !ok { 87 | return fmt.Errorf("data not in data table format for sqlite output") 88 | } 89 | 90 | filename, err := outputs.GetFilenameFromPattern(s.config.FilePath.NamePattern, "", data, nil) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | tableName, err := outputs.GetFilenameFromPattern(s.config.TableNamePattern, "", data, nil) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | var createTable bool 101 | 102 | outPath := filepath.Join(s.config.FilePath.FilePath, filename) 103 | db, ok := s.dbCons[outPath] 104 | if !ok { 105 | db, err = sqlx.Connect("sqlite3", outPath) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | s.dbCons[outPath] = db 111 | createTable = true 112 | } 113 | 114 | if createTable { 115 | // Iterate over headers 116 | headers := []string{} 117 | for _, r := range dataTable.Headers { 118 | if r == nil { 119 | continue 120 | } 121 | headers = append(headers, util.CastToString(r.Value)) 122 | } 123 | 124 | // Iterate over data columns to get the first row of data. 125 | // The first row of data is needed to set the types on the to be created SQLite table 126 | dataRows := []interface{}{} 127 | for _, row := range dataTable.Rows { 128 | for _, r := range row { 129 | if r == nil { 130 | continue 131 | } 132 | dataRows = append(dataRows, r.Value) 133 | } 134 | if len(dataRows) == 0 { 135 | continue 136 | } 137 | // Break after first round as we only need the first row! 138 | break 139 | } 140 | 141 | tx, err := db.Begin() 142 | if err != nil { 143 | return fmt.Errorf("couldn't begin transaction in sqlite database. %+v", err) 144 | } 145 | tx.Exec(s.buildCreateTableQuery(tableName, headers, dataRows)) 146 | if err := tx.Commit(); err != nil { 147 | return fmt.Errorf("couldn't create table in sqlite database. %+v", err) 148 | } 149 | } 150 | 151 | // Iterate over data columns 152 | for _, row := range dataTable.Rows { 153 | dataRows := []interface{}{} 154 | for _, r := range row { 155 | dataRows = append(dataRows, r.Value) 156 | } 157 | if len(dataRows) == 0 { 158 | continue 159 | } 160 | 161 | query := s.buildInsertQuery(tableName, len(dataRows)) 162 | if _, err := db.Exec(query, dataRows...); err != nil { 163 | return fmt.Errorf("couldn't insert data in sqlite database. %+v", err) 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func (s SQLite) buildCreateTableQuery(tableName string, columns []string, firstRow []interface{}) string { 171 | query := fmt.Sprintf(createTableBeginQuery, tableName) 172 | 173 | for i, c := range columns { 174 | cType := "TEXT" 175 | 176 | if len(firstRow) >= i+1 { 177 | switch firstRow[i].(type) { 178 | case bool: 179 | cType = SQLiteBoolType 180 | case float32: 181 | cType = SQLiteFloatType 182 | case float64: 183 | cType = SQLiteFloatType 184 | case int: 185 | cType = SQLiteIntType 186 | case int8: 187 | cType = SQLiteIntType 188 | case int16: 189 | cType = SQLiteIntType 190 | case int32: 191 | cType = SQLiteIntType 192 | case int64: 193 | cType = SQLiteIntType 194 | } 195 | } 196 | query += fmt.Sprintf(" `%s` %s", c, cType) 197 | if len(columns) != i+1 { 198 | query += "," 199 | } 200 | query += "\n" 201 | } 202 | 203 | query += createTableEndQuery 204 | 205 | return query 206 | } 207 | 208 | func (s SQLite) buildInsertQuery(tableName string, count int) string { 209 | query := fmt.Sprintf(insertDataBeginQuery, tableName) 210 | 211 | // Generate the placeholder `$1` and so on 212 | for i := 1; i <= count; i++ { 213 | query += fmt.Sprintf("$%d", i) 214 | if count >= i+1 { 215 | query += ", " 216 | } 217 | } 218 | 219 | query += insertDataEndQuery 220 | return query 221 | } 222 | 223 | // OutputFiles return a list of output files 224 | func (s SQLite) OutputFiles() []string { 225 | list := []string{} 226 | for file := range s.dbCons { 227 | list = append(list, file) 228 | } 229 | return list 230 | } 231 | 232 | // Close closes all sqlite3 connections 233 | func (s SQLite) Close() error { 234 | for name, db := range s.dbCons { 235 | s.logger.WithFields(logrus.Fields{"filepath": name}).Debug("closing db connection") 236 | if err := db.Close(); err != nil { 237 | s.logger.WithFields(logrus.Fields{"filepath": name}).Errorf("error closing db connection. %+v", err) 238 | } 239 | } 240 | 241 | return nil 242 | } 243 | -------------------------------------------------------------------------------- /outputs/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Cloudical Deutschland GmbH. All rights reserved. 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 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package mysql 15 | 16 | import ( 17 | "fmt" 18 | 19 | "github.com/cloudical-io/ancientt/pkg/config" 20 | "github.com/cloudical-io/ancientt/pkg/util" 21 | "github.com/jmoiron/sqlx" 22 | 23 | // Include MySQL driver for mysql output 24 | _ "github.com/go-sql-driver/mysql" 25 | 26 | "github.com/cloudical-io/ancientt/outputs" 27 | "github.com/sirupsen/logrus" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | // NameMySQL MySQL output name 32 | const ( 33 | NameMySQL = "mysql" 34 | MySQLIntType = "BIGINT" 35 | MySQLFloatType = "FLOAT" 36 | MySQLBoolType = "BOOLEAN" 37 | ) 38 | 39 | func init() { 40 | outputs.Factories[NameMySQL] = NewMySQLOutput 41 | } 42 | 43 | // MySQL MySQL tester structure 44 | type MySQL struct { 45 | outputs.Output 46 | logger *log.Entry 47 | config *config.MySQL 48 | dbCons map[string]*sqlx.DB 49 | } 50 | 51 | const ( 52 | defaultTableNamePattern = "ancientt{{ .TestStartTime }}{{ .Data.Tester }}{{ .Data.ServerHost }}{{ .Data.ClientHost }}" 53 | 54 | checkIfTableExistsQuery = "SELECT 1 FROM `%s` LIMIT 1;" 55 | createTableBeginQuery = "CREATE TABLE IF NOT EXISTS `%s` (\n" 56 | createTableEndQuery = `);` 57 | insertDataBeginQuery = "INSERT INTO %s VALUES (" 58 | insertDataEndQuery = `);` 59 | ) 60 | 61 | // NewMySQLOutput return a new MySQL tester instance 62 | func NewMySQLOutput(cfg *config.Config, outCfg *config.Output) (outputs.Output, error) { 63 | if outCfg == nil { 64 | outCfg = &config.Output{ 65 | MySQL: &config.MySQL{}, 66 | } 67 | } 68 | m := MySQL{ 69 | logger: log.WithFields(logrus.Fields{"output": NameMySQL}), 70 | config: outCfg.MySQL, 71 | dbCons: map[string]*sqlx.DB{}, 72 | } 73 | if m.config.DSN == "" { 74 | return nil, fmt.Errorf("no DSN for mysql connection given") 75 | } 76 | if m.config.TableNamePattern == "" { 77 | m.config.TableNamePattern = defaultTableNamePattern 78 | } 79 | 80 | return m, nil 81 | } 82 | 83 | // Do make MySQL outputs 84 | func (m MySQL) Do(data outputs.Data) error { 85 | dataTable, ok := data.Data.(*outputs.Table) 86 | if !ok { 87 | return fmt.Errorf("data not in data table format for mysql output") 88 | } 89 | 90 | tableName, err := outputs.GetFilenameFromPattern(m.config.TableNamePattern, "", data, nil) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | dbPath := fmt.Sprintf("%s-%s", m.config.DSN, tableName) 96 | 97 | db, ok := m.dbCons[dbPath] 98 | if !ok { 99 | db, err = sqlx.Connect("mysql", dbPath) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | m.dbCons[dbPath] = db 105 | } 106 | 107 | if err := m.createTable(db, dataTable, tableName); err != nil { 108 | return err 109 | } 110 | 111 | // Iterate over data columns 112 | for _, row := range dataTable.Rows { 113 | cells := []interface{}{} 114 | for _, r := range row { 115 | if r == nil { 116 | continue 117 | } 118 | cells = append(cells, r.Value) 119 | } 120 | if len(cells) == 0 { 121 | continue 122 | } 123 | 124 | query := m.buildInsertQuery(tableName, len(cells)) 125 | if _, err := db.Exec(query, cells...); err != nil { 126 | return fmt.Errorf("couldn't insert data in mysql database. %+v", err) 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func (m MySQL) createTable(db *sqlx.DB, dataTable *outputs.Table, tableName string) error { 134 | // Iterate over headers 135 | headers := []string{} 136 | for _, r := range dataTable.Headers { 137 | if r == nil { 138 | continue 139 | } 140 | headers = append(headers, util.CastToString(r.Value)) 141 | } 142 | 143 | // Iterate over data row to get the first row of data. 144 | // The first row of data is needed to set the types on the to be created MySQL table 145 | cells := []interface{}{} 146 | for _, row := range dataTable.Rows { 147 | for _, r := range row { 148 | if r == nil { 149 | continue 150 | } 151 | cells = append(cells, r.Value) 152 | } 153 | if len(cells) == 0 { 154 | continue 155 | } 156 | // Break after first round as we only need the first row! 157 | break 158 | } 159 | 160 | // The error should not return an error when the table exists, try to create the database 161 | if _, err := db.Exec(fmt.Sprintf(checkIfTableExistsQuery, tableName)); err != nil { 162 | // Only auto create tables when enabled 163 | if m.config.AutoCreateTables != nil && *m.config.AutoCreateTables { 164 | // Start transaction, exec the CREATE TABLE query and commit the result 165 | tx, err := db.Begin() 166 | if err != nil { 167 | return fmt.Errorf("couldn't begin transaction in mysql database. %+v", err) 168 | } 169 | tx.Exec(m.buildCreateTableQuery(tableName, headers, cells)) 170 | if err := tx.Commit(); err != nil { 171 | return fmt.Errorf("couldn't create table in mysql database. %+v", err) 172 | } 173 | } else { 174 | return fmt.Errorf("table %s doesn't exist in mysql database and AutoCreateTables is false", tableName) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (m MySQL) buildCreateTableQuery(tableName string, columns []string, firstRow []interface{}) string { 182 | query := fmt.Sprintf(createTableBeginQuery, tableName) 183 | 184 | for i, c := range columns { 185 | cType := "TEXT" 186 | 187 | if len(firstRow) >= i+1 { 188 | switch firstRow[i].(type) { 189 | case bool: 190 | cType = MySQLBoolType 191 | case float32: 192 | cType = MySQLFloatType 193 | case float64: 194 | cType = MySQLFloatType 195 | case int: 196 | cType = MySQLIntType 197 | case int8: 198 | cType = MySQLIntType 199 | case int16: 200 | cType = MySQLIntType 201 | case int32: 202 | cType = MySQLIntType 203 | case int64: 204 | cType = MySQLIntType 205 | } 206 | } 207 | query += fmt.Sprintf(" `%s` %s", c, cType) 208 | if len(columns) != i+1 { 209 | query += "," 210 | } 211 | query += "\n" 212 | } 213 | 214 | query += createTableEndQuery 215 | 216 | return query 217 | } 218 | 219 | func (m MySQL) buildInsertQuery(tableName string, count int) string { 220 | query := fmt.Sprintf(insertDataBeginQuery, tableName) 221 | 222 | // Generate the placeholder `$1` and so on 223 | for i := 1; i <= count; i++ { 224 | query += fmt.Sprintf("$%d", i) 225 | if count >= i+1 { 226 | query += ", " 227 | } 228 | } 229 | 230 | query += insertDataEndQuery 231 | return query 232 | } 233 | 234 | // OutputFiles return a list of output files 235 | func (m MySQL) OutputFiles() []string { 236 | return []string{} 237 | } 238 | 239 | // Close closes all MySQL connections 240 | func (m MySQL) Close() error { 241 | for name, db := range m.dbCons { 242 | m.logger.WithFields(logrus.Fields{"filepath": name}).Debug("closing db connection") 243 | if err := db.Close(); err != nil { 244 | m.logger.WithFields(logrus.Fields{"filepath": name}).Errorf("error closing db connection. %+v", err) 245 | } 246 | } 247 | 248 | return nil 249 | } 250 | -------------------------------------------------------------------------------- /cmd/docgen/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The prometheus-operator 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 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "go/ast" 21 | "go/doc" 22 | "go/parser" 23 | "go/token" 24 | "reflect" 25 | "strings" 26 | ) 27 | 28 | const ( 29 | firstParagraph = `# Config Structure 30 | 31 | This Document documents the types introduced by Ancientt for configuration to be used by users. 32 | 33 | > **NOTE**: This document is generated from code comments. When contributing a change to this document please do so by changing the code comments.` 34 | ) 35 | 36 | var ( 37 | links = map[string]string{ 38 | "corev1.Toleration": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#toleration-v1-core", 39 | } 40 | 41 | selfLinks = map[string]string{} 42 | ) 43 | 44 | func toSectionLink(name string) string { 45 | name = strings.ToLower(name) 46 | name = strings.Replace(name, " ", "-", -1) 47 | return name 48 | } 49 | 50 | func printTOC(types []KubeTypes) { 51 | fmt.Printf("\n## Table of Contents\n\n") 52 | for _, t := range types { 53 | strukt := t[0] 54 | fmt.Printf("* [%s](#%s)\n", strukt.Name, toSectionLink(strukt.Name)) 55 | } 56 | } 57 | 58 | func printAPIDocs(path string) { 59 | fmt.Println(firstParagraph) 60 | 61 | types := ParseDocumentationFrom(path) 62 | for _, t := range types { 63 | strukt := t[0] 64 | selfLinks[strukt.Name] = "#" + strings.ToLower(strukt.Name) 65 | } 66 | 67 | // we need to parse once more to now add the self links 68 | types = ParseDocumentationFrom(path) 69 | 70 | printTOC(types) 71 | 72 | for _, t := range types { 73 | strukt := t[0] 74 | fmt.Printf("\n## %s\n\n%s\n\n", strukt.Name, strukt.Doc) 75 | 76 | fmt.Println("| Field | Description | Scheme | Required | Validation |") 77 | fmt.Println("| ----- | ----------- | ------ | -------- | ---------- |") 78 | fields := t[1:(len(t))] 79 | for _, f := range fields { 80 | fmt.Println("|", f.Name, "|", f.Doc, "|", f.Type, "|", f.Mandatory, "|", f.Validation, "|") 81 | } 82 | fmt.Println("") 83 | fmt.Println("[Back to TOC](#table-of-contents)") 84 | } 85 | } 86 | 87 | // Pair of strings. We keed the name of fields and the doc 88 | type Pair struct { 89 | Name, Doc, Type, Validation string 90 | Mandatory bool 91 | } 92 | 93 | // KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself 94 | type KubeTypes []Pair 95 | 96 | // ParseDocumentationFrom gets all types' documentation and returns them as an 97 | // array. Each type is again represented as an array (we have to use arrays as we 98 | // need to be sure for the order of the fields). This function returns fields and 99 | // struct definitions that have no documentation as {name, ""}. 100 | func ParseDocumentationFrom(src string) []KubeTypes { 101 | var docForTypes []KubeTypes 102 | 103 | pkg := astFrom(src) 104 | 105 | for _, kubType := range pkg.Types { 106 | if strings.HasSuffix(strings.TrimSpace(kubType.Doc), "-") { 107 | continue 108 | } 109 | if structType, ok := kubType.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType); ok { 110 | var ks KubeTypes 111 | ks = append(ks, Pair{kubType.Name, fmtRawDoc(kubType.Doc), "", "", false}) 112 | 113 | for _, field := range structType.Fields.List { 114 | typeString := fieldType(field.Type) 115 | validation := fieldValidation(field) 116 | fieldMandatory := fieldRequired(field) 117 | if n := fieldName(field); n != "-" { 118 | fieldDoc := fmtRawDoc(field.Doc.Text()) 119 | ks = append(ks, Pair{n, fieldDoc, typeString, validation, fieldMandatory}) 120 | } 121 | } 122 | docForTypes = append(docForTypes, ks) 123 | } 124 | } 125 | 126 | return docForTypes 127 | } 128 | 129 | func astFrom(filePath string) *doc.Package { 130 | fset := token.NewFileSet() 131 | m := make(map[string]*ast.File) 132 | 133 | f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) 134 | if err != nil { 135 | fmt.Println(err) 136 | return nil 137 | } 138 | 139 | m[filePath] = f 140 | apkg, _ := ast.NewPackage(fset, m, nil, nil) 141 | 142 | return doc.New(apkg, "", 0) 143 | } 144 | 145 | func fmtRawDoc(rawDoc string) string { 146 | var buffer bytes.Buffer 147 | delPrevChar := func() { 148 | if buffer.Len() > 0 { 149 | buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n" 150 | } 151 | } 152 | 153 | // Ignore all lines after --- 154 | rawDoc = strings.Split(rawDoc, "---")[0] 155 | 156 | for _, line := range strings.Split(rawDoc, "\n") { 157 | line = strings.TrimRight(line, " ") 158 | leading := strings.TrimLeft(line, " ") 159 | switch { 160 | case len(line) == 0: // Keep paragraphs 161 | delPrevChar() 162 | buffer.WriteString("\n\n") 163 | case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs 164 | case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl 165 | default: 166 | if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { 167 | delPrevChar() 168 | line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..." 169 | } else { 170 | line += " " 171 | } 172 | buffer.WriteString(line) 173 | } 174 | } 175 | 176 | postDoc := strings.TrimRight(buffer.String(), "\n") 177 | postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to " 178 | postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape " 179 | postDoc = strings.Replace(postDoc, "\n", "\\n", -1) 180 | postDoc = strings.Replace(postDoc, "\t", "\\t", -1) 181 | postDoc = strings.Replace(postDoc, "|", "\\|", -1) 182 | 183 | return postDoc 184 | } 185 | 186 | func toLink(typeName string) string { 187 | selfLink, hasSelfLink := selfLinks[typeName] 188 | if hasSelfLink { 189 | return wrapInLink(typeName, selfLink) 190 | } 191 | 192 | link, hasLink := links[typeName] 193 | if hasLink { 194 | return wrapInLink(typeName, link) 195 | } 196 | 197 | return typeName 198 | } 199 | 200 | func wrapInLink(text, link string) string { 201 | return fmt.Sprintf("[%s](%s)", text, link) 202 | } 203 | 204 | // fieldName returns the name of the field as it should appear in JSON format 205 | // "-" indicates that this field is not part of the JSON representation 206 | func fieldName(field *ast.Field) string { 207 | tag := "" 208 | if field.Tag != nil { 209 | tag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation 210 | if tag == "" { 211 | tag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("yaml") 212 | } 213 | if strings.Contains(tag, "inline") { 214 | return "-" 215 | } 216 | } 217 | 218 | tag = strings.Split(tag, ",")[0] // This can return "-" 219 | if tag == "" { 220 | if field.Names != nil { 221 | return field.Names[0].Name 222 | } 223 | return field.Type.(*ast.Ident).Name 224 | } 225 | return tag 226 | } 227 | 228 | func fieldValidation(field *ast.Field) string { 229 | if field.Tag != nil { 230 | return reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("validate") // Delete first and last quotation 231 | } 232 | return "" 233 | } 234 | 235 | // fieldRequired returns whether a field is a required field. 236 | func fieldRequired(field *ast.Field) bool { 237 | tag := "" 238 | if field.Tag != nil { 239 | tag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation 240 | if tag == "" { 241 | tag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("yaml") 242 | } 243 | return !strings.Contains(tag, "omitempty") 244 | } 245 | 246 | return false 247 | } 248 | 249 | func fieldType(typ ast.Expr) string { 250 | switch typ.(type) { 251 | case *ast.Ident: 252 | return toLink(typ.(*ast.Ident).Name) 253 | case *ast.StarExpr: 254 | return "*" + toLink(fieldType(typ.(*ast.StarExpr).X)) 255 | case *ast.SelectorExpr: 256 | e := typ.(*ast.SelectorExpr) 257 | pkg := e.X.(*ast.Ident) 258 | t := e.Sel 259 | return toLink(pkg.Name + "." + t.Name) 260 | case *ast.ArrayType: 261 | return "[]" + toLink(fieldType(typ.(*ast.ArrayType).Elt)) 262 | case *ast.MapType: 263 | mapType := typ.(*ast.MapType) 264 | return "map[" + toLink(fieldType(mapType.Key)) + "]" + toLink(fieldType(mapType.Value)) 265 | default: 266 | return "" 267 | } 268 | } 269 | --------------------------------------------------------------------------------